@voybio/ace-swarm 2.4.4 → 2.4.6

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 (120) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +72 -532
  3. package/assets/eval-battery/battery.json +85 -0
  4. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  5. package/assets/scripts/render-mcp-configs.sh +12 -0
  6. package/dist/ace-context.d.ts +18 -0
  7. package/dist/ace-context.js +161 -16
  8. package/dist/ace-server-instructions.js +36 -9
  9. package/dist/bridge-trace.d.ts +17 -0
  10. package/dist/bridge-trace.js +23 -0
  11. package/dist/cli.d.ts +9 -1
  12. package/dist/cli.js +619 -10
  13. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  14. package/dist/discovery-runtime-wrappers.js +65 -9
  15. package/dist/eval-harness.d.ts +110 -0
  16. package/dist/eval-harness.js +254 -0
  17. package/dist/experience-reflector.d.ts +13 -0
  18. package/dist/experience-reflector.js +62 -0
  19. package/dist/goal-engine.d.ts +155 -0
  20. package/dist/goal-engine.js +795 -0
  21. package/dist/helpers/store-resolution.js +40 -39
  22. package/dist/hermes/session-manager.d.ts +10 -0
  23. package/dist/hermes/session-manager.js +130 -27
  24. package/dist/job-scheduler.d.ts +58 -0
  25. package/dist/job-scheduler.js +202 -2
  26. package/dist/job-worker.d.ts +87 -0
  27. package/dist/job-worker.js +182 -0
  28. package/dist/local-model-policy.d.ts +8 -2
  29. package/dist/local-model-policy.js +31 -17
  30. package/dist/local-model-runtime.d.ts +6 -0
  31. package/dist/local-model-runtime.js +130 -2
  32. package/dist/measured-classing.d.ts +66 -0
  33. package/dist/measured-classing.js +165 -0
  34. package/dist/model-bridge.d.ts +45 -0
  35. package/dist/model-bridge.js +502 -44
  36. package/dist/orchestrator-supervisor.js +1 -1
  37. package/dist/plan-proposal.d.ts +7 -0
  38. package/dist/plan-proposal.js +74 -4
  39. package/dist/playbook-adaptation.d.ts +37 -0
  40. package/dist/playbook-adaptation.js +159 -0
  41. package/dist/playbook-curator.d.ts +16 -0
  42. package/dist/playbook-curator.js +106 -0
  43. package/dist/playbook-llm-reflector.d.ts +26 -0
  44. package/dist/playbook-llm-reflector.js +157 -0
  45. package/dist/public-surface.d.ts +2 -0
  46. package/dist/public-surface.js +26 -14
  47. package/dist/safe-edit.d.ts +20 -0
  48. package/dist/safe-edit.js +355 -78
  49. package/dist/server.d.ts +11 -2
  50. package/dist/server.js +55 -5
  51. package/dist/shared.d.ts +12 -0
  52. package/dist/shared.js +13 -1
  53. package/dist/source-mutation-coordinator.d.ts +54 -0
  54. package/dist/source-mutation-coordinator.js +259 -0
  55. package/dist/status-events.d.ts +4 -1
  56. package/dist/status-events.js +41 -6
  57. package/dist/store/ace-packed-store.d.ts +28 -9
  58. package/dist/store/ace-packed-store.js +324 -57
  59. package/dist/store/bgzf.d.ts +4 -0
  60. package/dist/store/bgzf.js +94 -0
  61. package/dist/store/bootstrap-store.d.ts +4 -1
  62. package/dist/store/bootstrap-store.js +57 -3
  63. package/dist/store/cache-workspace.d.ts +8 -0
  64. package/dist/store/cache-workspace.js +67 -1
  65. package/dist/store/crc32.d.ts +2 -0
  66. package/dist/store/crc32.js +16 -0
  67. package/dist/store/fsck.d.ts +16 -0
  68. package/dist/store/fsck.js +240 -0
  69. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  70. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  71. package/dist/store/materializers/projection-manager.d.ts +7 -0
  72. package/dist/store/materializers/projection-manager.js +37 -0
  73. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  74. package/dist/store/repositories/goal-repository.d.ts +112 -0
  75. package/dist/store/repositories/goal-repository.js +157 -0
  76. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  77. package/dist/store/repositories/playbook-repository.js +314 -0
  78. package/dist/store/store-artifacts.js +44 -2
  79. package/dist/store/store-authority-audit.js +1 -0
  80. package/dist/store/store-cli.d.ts +30 -0
  81. package/dist/store/store-cli.js +242 -0
  82. package/dist/store/store-snapshot.d.ts +12 -0
  83. package/dist/store/store-snapshot.js +74 -11
  84. package/dist/store/types.d.ts +1 -0
  85. package/dist/store/write-coordinator.d.ts +7 -0
  86. package/dist/store/write-coordinator.js +11 -0
  87. package/dist/todo-state.d.ts +21 -0
  88. package/dist/todo-state.js +150 -53
  89. package/dist/tools-agent.js +86 -22
  90. package/dist/tools-discovery.js +5 -3
  91. package/dist/tools-files.js +39 -18
  92. package/dist/tools-framework.js +31 -3
  93. package/dist/tools-goal.d.ts +11 -0
  94. package/dist/tools-goal.js +108 -0
  95. package/dist/tools-handoff.js +7 -11
  96. package/dist/tools-lifecycle.js +2 -0
  97. package/dist/tools-scheduler.js +44 -1
  98. package/dist/tools-skills.js +3 -2
  99. package/dist/tools-todo.js +64 -3
  100. package/dist/tools.js +2 -0
  101. package/dist/tui/chat.js +4 -0
  102. package/dist/tui/index.js +4 -1
  103. package/dist/tui/ollama.d.ts +2 -0
  104. package/dist/tui/openai-compatible.d.ts +14 -0
  105. package/dist/tui/openai-compatible.js +15 -2
  106. package/dist/tui/provider-discovery.d.ts +1 -0
  107. package/dist/tui/provider-discovery.js +2 -2
  108. package/docs/cli-reference.md +125 -0
  109. package/docs/development.md +46 -0
  110. package/docs/evaluation-safety.md +27 -0
  111. package/docs/experience-playbook.md +35 -0
  112. package/docs/hermes.md +129 -0
  113. package/docs/library-api.md +31 -0
  114. package/docs/mcp-tool-reference.md +290 -0
  115. package/docs/provider-security.md +85 -0
  116. package/docs/state-model.md +113 -0
  117. package/docs/troubleshooting.md +95 -0
  118. package/docs/tui-operator-guide.md +75 -0
  119. package/package.json +3 -1
  120. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
@@ -1,5 +1,5 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
3
3
  import { dirname, isAbsolute, resolve, sep } from "node:path";
4
4
  import { executeAceInternalTool, listAceInternalToolCatalog, } from "./ace-internal-tools.js";
5
5
  import { appendVericifyProcessPostSafe, deriveWorkspaceVericifyRunRef, } from "./vericify-bridge.js";
@@ -8,6 +8,11 @@ import { appendRunLedgerEntrySafe } from "./run-ledger.js";
8
8
  import { appendStatusEventSafe } from "./status-events.js";
9
9
  import { normalizeRelPath } from "./helpers.js";
10
10
  import { sanitizeJsonLikeText } from "./json-sanitizer.js";
11
+ const MUTATION_INTENT = /\b(write|create|mutate|edit|persist|save|generate|implement|fix|update|modify|patch|delete|remove)\b/i;
12
+ /** Keep completion-evidence policy identical across direct and Hermes execution. */
13
+ export function isMutationIntent(task) {
14
+ return MUTATION_INTENT.test(task);
15
+ }
11
16
  /**
12
17
  * Roles that MUST produce a valid JSON envelope in every response.
13
18
  * A parse_error from these roles is a contract failure, not a plain-text fallback.
@@ -15,6 +20,33 @@ import { sanitizeJsonLikeText } from "./json-sanitizer.js";
15
20
  * marking the run as failed with status "role_contract_violation".
16
21
  */
17
22
  const ROLES_REQUIRING_JSON_ENVELOPE = new Set(["coders", "builder", "qa"]);
23
+ /**
24
+ * JSON Schema for the ACE response envelope. Passed as `format` (Ollama ≥ 0.5) or
25
+ * `response_format.json_schema` (OpenAI-compatible) to enable schema-constrained
26
+ * generation. Falls back to plain json-mode if the provider rejects it with a 400.
27
+ */
28
+ export const ACE_ENVELOPE_JSON_SCHEMA = {
29
+ type: "object",
30
+ properties: {
31
+ status: { type: "string", enum: ["tool", "message", "complete", "need_input"] },
32
+ thinking: { type: "string" },
33
+ message: { type: "string" },
34
+ summary: { type: "string" },
35
+ tool_calls: {
36
+ type: "array",
37
+ items: {
38
+ type: "object",
39
+ properties: {
40
+ tool: { type: "string" },
41
+ input: { type: "object" },
42
+ },
43
+ required: ["tool"],
44
+ },
45
+ },
46
+ evidence_refs: { type: "array", items: { type: "string" } },
47
+ },
48
+ required: ["status"],
49
+ };
18
50
  const VALID_ENVELOPE_STATUSES = new Set(["tool", "message", "complete", "need_input"]);
19
51
  const MAX_PARSE_REPAIR_ATTEMPTS = 2;
20
52
  const MAX_OUTPUT_DRIFT_REPAIRS = 1;
@@ -93,6 +125,15 @@ function parseEnvelope(raw) {
93
125
  .map((ref) => ref.trim())
94
126
  .filter(Boolean);
95
127
  }
128
+ if (parsed.used_bullets !== undefined) {
129
+ if (!Array.isArray(parsed.used_bullets)) {
130
+ throw new Error("used_bullets must be an array");
131
+ }
132
+ parsed.used_bullets = parsed.used_bullets
133
+ .filter((id) => typeof id === "string")
134
+ .map((id) => id.trim())
135
+ .filter((id) => /^pb-[a-fA-F0-9]{8}$/.test(id));
136
+ }
96
137
  return parsed;
97
138
  }
98
139
  catch {
@@ -310,13 +351,14 @@ function mergeEvidenceRefs(...groups) {
310
351
  return merged.length > 0 ? [...new Set(merged)] : undefined;
311
352
  }
312
353
  async function appendFalseCompletionEvidence(input) {
354
+ const reasonCode = input.reason_code ?? "false_completion_no_evidence";
313
355
  await appendRunLedgerEntrySafe({
314
356
  tool: "model-bridge",
315
357
  category: "regression",
316
358
  message: input.summary,
317
359
  artifacts: input.evidence_refs ?? [],
318
360
  metadata: {
319
- reason_code: "false_completion_no_evidence",
361
+ reason_code: reasonCode,
320
362
  role: input.role,
321
363
  workspace: input.workspace,
322
364
  evidence_refs: input.evidence_refs ?? [],
@@ -329,20 +371,23 @@ async function appendFalseCompletionEvidence(input) {
329
371
  summary: input.summary,
330
372
  objective_id: "model-bridge-completion-verification",
331
373
  payload: {
332
- reason_code: "false_completion_no_evidence",
374
+ reason_code: reasonCode,
333
375
  role: input.role,
334
376
  workspace: input.workspace,
335
377
  evidence_refs: input.evidence_refs ?? [],
336
378
  },
337
379
  }).catch(() => undefined);
338
380
  }
339
- async function verifyCompletionArtifacts(result, context) {
381
+ export async function verifyCompletionArtifacts(result, context) {
340
382
  if (result.status !== "completed")
341
383
  return result;
342
- const expectedArtifacts = context.expectedArtifacts ?? [];
343
- const mutationIntent = /\b(write|create|mutate|edit|persist|save|generate)\b/i.test(context.task);
344
- const shouldVerify = ((context.role === "coders" || context.role === "builder") && mutationIntent) ||
384
+ const expectedArtifacts = (context.expectedArtifacts ?? []).filter((artifact) => artifact.required !== false);
385
+ const requiredEvidenceRefs = (context.requiredEvidenceRefs ?? []).filter((ref) => ref.trim().length > 0);
386
+ const mutationIntent = isMutationIntent(context.task);
387
+ const shouldVerify = context.forceCompletionVerification === true ||
388
+ ((context.role === "coders" || context.role === "builder") && mutationIntent) ||
345
389
  expectedArtifacts.length > 0 ||
390
+ requiredEvidenceRefs.length > 0 ||
346
391
  result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file");
347
392
  if (!shouldVerify)
348
393
  return result;
@@ -351,10 +396,42 @@ async function verifyCompletionArtifacts(result, context) {
351
396
  const evidenceRefsOk = (result.evidence_refs ?? []).length > 0 &&
352
397
  (result.evidence_refs ?? []).some((ref) => workspaceEvidenceExists(ref, context.workspace));
353
398
  const expectedArtifactsOk = expectedArtifacts.length > 0 &&
354
- expectedArtifacts
355
- .filter((artifact) => artifact.required !== false)
356
- .every((artifact) => workspaceEvidenceExists(artifact.path, context.workspace));
357
- if (writeEvidenceOk || evidenceRefsOk || expectedArtifactsOk)
399
+ expectedArtifacts.every((artifact) => workspaceEvidenceExists(artifact.path, context.workspace));
400
+ const requiredEvidenceRefsOk = requiredEvidenceRefs.length > 0 &&
401
+ requiredEvidenceRefs.every((required) => (result.evidence_refs ?? []).some((ref) => ref.includes(required)));
402
+ if (expectedArtifacts.length > 0 && !expectedArtifactsOk) {
403
+ const summary = "Model claimed completion but declared expected artifacts are missing.";
404
+ await appendFalseCompletionEvidence({
405
+ role: context.role,
406
+ workspace: context.workspace,
407
+ summary,
408
+ evidence_refs: result.evidence_refs,
409
+ reason_code: "forward_pacing_incomplete",
410
+ });
411
+ return {
412
+ ...result,
413
+ status: "blocked",
414
+ reason_code: "forward_pacing_incomplete",
415
+ summary,
416
+ };
417
+ }
418
+ if (requiredEvidenceRefs.length > 0 && !requiredEvidenceRefsOk) {
419
+ const summary = "Model claimed completion but required evidence refs are missing.";
420
+ await appendFalseCompletionEvidence({
421
+ role: context.role,
422
+ workspace: context.workspace,
423
+ summary,
424
+ evidence_refs: result.evidence_refs,
425
+ reason_code: "forward_pacing_incomplete",
426
+ });
427
+ return {
428
+ ...result,
429
+ status: "blocked",
430
+ reason_code: "forward_pacing_incomplete",
431
+ summary,
432
+ };
433
+ }
434
+ if (expectedArtifactsOk || requiredEvidenceRefsOk || writeEvidenceOk || evidenceRefsOk)
358
435
  return result;
359
436
  const summary = "Model claimed completion but no persisted evidence or tool-calls found.";
360
437
  await appendFalseCompletionEvidence({
@@ -370,9 +447,34 @@ async function verifyCompletionArtifacts(result, context) {
370
447
  summary,
371
448
  };
372
449
  }
450
+ /**
451
+ * Returns true when a provider rejected our schema-constrained format (400) and
452
+ * the bridge should retry with plain json-mode. Applies only when `jsonSchema` was
453
+ * set — any 400 from a schema request is treated as a format-rejection to degrade
454
+ * gracefully on older Ollama / backends that don't support json_schema yet.
455
+ */
456
+ function isSchemaFormatRejectedError(error) {
457
+ const statusCode = typeof error?.statusCode === "number"
458
+ ? error.statusCode
459
+ : undefined;
460
+ return statusCode === 400;
461
+ }
373
462
  function isRetryableProviderError(error) {
374
463
  const message = formatErrorMessage(error).toLowerCase();
375
- return !/(abort|aborted|cancelled|canceled|interrupted)/.test(message);
464
+ if (/(abort|aborted|cancelled|canceled|interrupted)/.test(message))
465
+ return false;
466
+ const statusCode = typeof error?.statusCode === "number"
467
+ ? error.statusCode
468
+ : undefined;
469
+ if (statusCode !== undefined) {
470
+ if (statusCode >= 500)
471
+ return true;
472
+ if (statusCode === 408 || statusCode === 429)
473
+ return true;
474
+ if (statusCode >= 400 && statusCode < 500)
475
+ return false;
476
+ }
477
+ return /(econnreset|econnrefused|etimedout|timeout|timed out|socket|network|fetch failed)/.test(message);
376
478
  }
377
479
  function formatToolFeedback(results) {
378
480
  return [
@@ -381,20 +483,124 @@ function formatToolFeedback(results) {
381
483
  "Continue with another JSON response.",
382
484
  ].join("\n");
383
485
  }
486
+ function stableJson(value) {
487
+ if (value === null || typeof value !== "object")
488
+ return JSON.stringify(value);
489
+ if (Array.isArray(value))
490
+ return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
491
+ const record = value;
492
+ const volatileKeys = new Set(["session_id", "sessionId", "timestamp", "updated_at", "created_at", "at"]);
493
+ return `{${Object.keys(record)
494
+ .filter((key) => !volatileKeys.has(key))
495
+ .sort()
496
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
497
+ .join(",")}}`;
498
+ }
499
+ function sha256Text(value) {
500
+ return createHash("sha256").update(value).digest("hex");
501
+ }
502
+ function toolDoomLoopKey(toolCall, result) {
503
+ return sha256Text([
504
+ toolCall.tool,
505
+ stableJson(toolCall.input ?? {}),
506
+ result.ok ? "ok" : "error",
507
+ sha256Text(result.summary),
508
+ ].join("\n"));
509
+ }
510
+ function defaultOutputTokenCap(numCtx) {
511
+ return Math.max(1, Math.min(2048, Math.floor(numCtx / 4)));
512
+ }
513
+ function addUsageTotals(total, usage) {
514
+ if (!usage)
515
+ return;
516
+ total.prompt_tokens += usage.prompt_tokens;
517
+ total.completion_tokens += usage.completion_tokens;
518
+ }
519
+ function withUsageTotals(result, usageTotals) {
520
+ if (usageTotals.prompt_tokens === 0 && usageTotals.completion_tokens === 0)
521
+ return result;
522
+ return {
523
+ ...result,
524
+ usage_totals: { ...usageTotals },
525
+ };
526
+ }
527
+ function contextOverflowSuspected(usage, numCtx) {
528
+ return Boolean(usage && usage.prompt_tokens >= Math.floor(numCtx * 0.98));
529
+ }
530
+ const DEFAULT_TOOL_DEADLINE_MS = 120_000;
531
+ function toolDeadlineMs() {
532
+ const raw = Number(process.env.ACE_TOOL_DEADLINE_MS);
533
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_TOOL_DEADLINE_MS;
534
+ }
535
+ /**
536
+ * Per-tool-call deadline (E12). A hung validation/test subprocess must never
537
+ * wedge a turn indefinitely — and under Promise.all one stuck call also holds
538
+ * every parallel sibling's feedback hostage. On timeout we resolve a classified
539
+ * `tool_timeout` failure that then feeds the WS-C doom-loop brake.
540
+ */
541
+ export async function executeToolWithDeadline(tool, args, sessionId, opts, deadlineMs, exec = executeAceInternalTool) {
542
+ let timer;
543
+ const timeout = new Promise((resolve) => {
544
+ timer = setTimeout(() => resolve({ rawToolResult: undefined, timedOut: true }), deadlineMs);
545
+ timer.unref?.();
546
+ });
547
+ try {
548
+ return await Promise.race([
549
+ exec(tool, args, sessionId, opts).then((rawToolResult) => ({
550
+ rawToolResult,
551
+ timedOut: false,
552
+ })),
553
+ timeout,
554
+ ]);
555
+ }
556
+ finally {
557
+ if (timer)
558
+ clearTimeout(timer);
559
+ }
560
+ }
561
+ function appendBridgeTrace(input) {
562
+ const safeRunId = input.runId.replace(/[^a-zA-Z0-9_.-]/g, "_");
563
+ const traceDir = resolve(input.workspace, "agent-state", "bridge-traces");
564
+ mkdirSync(traceDir, { recursive: true });
565
+ appendFileSync(resolve(traceDir, `${safeRunId}.ndjson`), `${JSON.stringify({
566
+ turn: input.turn,
567
+ envelope_status: input.envelopeStatus,
568
+ parse_repairs: input.parseRepairAttempts,
569
+ drift_repairs: input.outputDriftRepairs,
570
+ tool_calls: (input.toolCalls ?? []).map((toolCall) => ({
571
+ tool: toolCall.tool,
572
+ ok: toolCall.ok,
573
+ reason_code: toolCall.reason_code ?? (toolCall.ok ? undefined : "tool_error"),
574
+ })),
575
+ usage: input.usage,
576
+ compressed: input.compressed,
577
+ })}\n`, "utf-8");
578
+ }
384
579
  async function collectOllamaResponse(client, model, messages, options) {
385
580
  let combined = "";
581
+ let usage;
582
+ // Prefer schema-constrained (Ollama ≥ 0.5) over plain json-mode; caller degrades on 400.
583
+ const ollamaFormat = options?.jsonSchema ? options.jsonSchema : options?.jsonMode ? "json" : undefined;
386
584
  for await (const chunk of client.chat({
387
585
  model,
388
586
  messages,
389
587
  stream: true,
588
+ ...(ollamaFormat !== undefined ? { format: ollamaFormat } : {}),
390
589
  options: {
391
590
  temperature: 0.2,
392
591
  top_p: 0.9,
393
592
  num_ctx: options?.num_ctx ?? 8192,
593
+ num_predict: options?.num_predict ?? defaultOutputTokenCap(options?.num_ctx ?? 8192),
394
594
  },
395
595
  })) {
396
596
  const text = chunk.message?.content ?? "";
397
597
  combined += text;
598
+ if (typeof chunk.prompt_eval_count === "number" || typeof chunk.eval_count === "number") {
599
+ usage = {
600
+ prompt_tokens: chunk.prompt_eval_count ?? usage?.prompt_tokens ?? 0,
601
+ completion_tokens: chunk.eval_count ?? usage?.completion_tokens ?? 0,
602
+ };
603
+ }
398
604
  if (text || chunk.done) {
399
605
  options?.onProgress?.({
400
606
  kind: "model_chunk",
@@ -405,16 +611,25 @@ async function collectOllamaResponse(client, model, messages, options) {
405
611
  if (chunk.done)
406
612
  break;
407
613
  }
408
- return combined;
614
+ return { text: combined, usage };
409
615
  }
410
- async function collectOpenAiCompatibleResponse(client, provider, model, messages, onProgress) {
616
+ async function collectOpenAiCompatibleResponse(client, provider, model, messages, maxTokens, jsonMode, jsonSchema, onProgress) {
411
617
  let combined = "";
618
+ let usage;
619
+ // Prefer schema-constrained (OpenAI json_schema) over json_object; caller degrades on 400.
620
+ const openAiResponseFormat = jsonSchema
621
+ ? { type: "json_schema", json_schema: { name: "ace_envelope", schema: jsonSchema, strict: false } }
622
+ : jsonMode
623
+ ? { type: "json_object" }
624
+ : undefined;
412
625
  for await (const chunk of client.chat({
413
626
  provider,
414
627
  model,
415
628
  messages,
416
629
  temperature: 0.2,
417
630
  topP: 0.9,
631
+ maxTokens,
632
+ ...(openAiResponseFormat ? { responseFormat: openAiResponseFormat } : {}),
418
633
  onProviderEvent: (event) => {
419
634
  onProgress?.({
420
635
  kind: "thinking",
@@ -427,6 +642,12 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
427
642
  },
428
643
  })) {
429
644
  combined += chunk.text;
645
+ if (typeof chunk.promptTokens === "number" || typeof chunk.completionTokens === "number") {
646
+ usage = {
647
+ prompt_tokens: chunk.promptTokens ?? usage?.prompt_tokens ?? 0,
648
+ completion_tokens: chunk.completionTokens ?? usage?.completion_tokens ?? 0,
649
+ };
650
+ }
430
651
  if (chunk.text || chunk.done) {
431
652
  onProgress?.({
432
653
  kind: "model_chunk",
@@ -441,21 +662,37 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
441
662
  if (chunk.done)
442
663
  break;
443
664
  }
444
- return combined;
665
+ return { text: combined, usage };
445
666
  }
446
667
  async function collectProviderResponse(clients, provider, options) {
447
668
  return provider.client === "ollama"
448
669
  ? collectOllamaResponse(clients.ollama, options.model, options.messages, {
449
670
  num_ctx: options.numCtx,
671
+ num_predict: defaultOutputTokenCap(options.numCtx),
672
+ jsonMode: options.jsonMode,
673
+ jsonSchema: options.jsonSchema,
450
674
  onProgress: options.onProgress,
451
675
  })
452
- : collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, options.onProgress);
676
+ : collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, defaultOutputTokenCap(options.numCtx), options.jsonMode, options.jsonSchema, options.onProgress);
453
677
  }
454
678
  async function collectProviderResponseWithRetry(clients, provider, options, onThinking) {
455
679
  try {
456
680
  return await collectProviderResponse(clients, provider, options);
457
681
  }
458
682
  catch (error) {
683
+ // Schema-constrained format rejected by provider → degrade to plain json-mode.
684
+ // Any 400 from a json_schema request is treated as a format-rejection; if the
685
+ // underlying issue is something else (bad model name, etc.) json-mode will also
686
+ // fail and propagate the error normally.
687
+ if (options.jsonSchema && isSchemaFormatRejectedError(error)) {
688
+ onThinking?.(`[schema-degrade] provider rejected schema-constrained format — retrying with json-mode`);
689
+ options.onProgress?.({
690
+ kind: "thinking",
691
+ at: Date.now(),
692
+ detail: { reason: "schema_format_degrade" },
693
+ });
694
+ return collectProviderResponse(clients, provider, { ...options, jsonSchema: undefined });
695
+ }
459
696
  if (!isRetryableProviderError(error)) {
460
697
  throw error;
461
698
  }
@@ -529,7 +766,7 @@ export class ModelBridge {
529
766
  messages: selectionMessages,
530
767
  numCtx: Math.min(input.numCtx, 8192),
531
768
  });
532
- const parsed = parseToolPlan(rawSelection, plan.catalog);
769
+ const parsed = parseToolPlan(rawSelection.text, plan.catalog);
533
770
  const scoped = parsed.selected_tools.slice(0, plan.selection_limit);
534
771
  if (scoped.length === 0)
535
772
  return undefined;
@@ -571,6 +808,8 @@ export class ModelBridge {
571
808
  workspace: options.workspace,
572
809
  tier: requestedTier,
573
810
  tools: selectedToolScope,
811
+ modelClass: options.modelClass,
812
+ model: options.model,
574
813
  });
575
814
  const promptBudget = calculateContextBudget(numCtx);
576
815
  for (const candidateTier of buildTierFallbackOrder(requestedTier)) {
@@ -580,6 +819,8 @@ export class ModelBridge {
580
819
  workspace: options.workspace,
581
820
  tier: candidateTier,
582
821
  tools: selectedToolScope,
822
+ modelClass: options.modelClass,
823
+ model: options.model,
583
824
  });
584
825
  aceContext = candidateContext;
585
826
  if (candidateContext.tokenEstimate <= promptBudget || candidateTier === "brief") {
@@ -597,7 +838,18 @@ export class ModelBridge {
597
838
  const childResults = [];
598
839
  const touchedPaths = [];
599
840
  const declaredEvidenceRefs = [];
841
+ const usageTotals = { prompt_tokens: 0, completion_tokens: 0 };
600
842
  const evidenceRefs = () => mergeEvidenceRefs(touchedPaths, declaredEvidenceRefs);
843
+ const attachPlaybookMetadata = (result, usedBullets) => {
844
+ const cleanUsed = (usedBullets ?? []).filter((id) => aceContext.injected_ids.includes(id));
845
+ if (aceContext.injected_ids.length === 0 && cleanUsed.length === 0)
846
+ return result;
847
+ return {
848
+ ...result,
849
+ ...(aceContext.injected_ids.length > 0 ? { injected_bullets: [...aceContext.injected_ids] } : {}),
850
+ ...(cleanUsed.length > 0 ? { used_bullets: [...new Set(cleanUsed)] } : {}),
851
+ };
852
+ };
601
853
  const sessionId = this.bridgeId;
602
854
  const refs = deriveWorkspaceVericifyRunRef({
603
855
  session_id: this.bridgeId,
@@ -613,6 +865,11 @@ export class ModelBridge {
613
865
  let parseRepairAttempts = 0;
614
866
  // Tracks correction attempts for roles that produce semantically drifted output.
615
867
  let outputDriftRepairs = 0;
868
+ const toolDoomLoopCounts = new Map();
869
+ // Inter-turn text-loop brake (E2): catches a model that keeps emitting the same
870
+ // non-terminating output (repeated identical tool envelopes / parse errors). The
871
+ // intra-turn runaway is bounded separately by the num_predict output cap.
872
+ const textLoopCounts = new Map();
616
873
  try {
617
874
  await appendVericifyProcessPostSafe({
618
875
  run_id: refs.run_id,
@@ -632,12 +889,17 @@ export class ModelBridge {
632
889
  noteProgress("thinking", { reason: "context_compressed", turn });
633
890
  }
634
891
  this.activeProviderClient = provider.client;
635
- const rawResponse = await collectProviderResponseWithRetry(this.clients, provider, {
892
+ const useJsonEnvelope = ROLES_REQUIRING_JSON_ENVELOPE.has(role);
893
+ const providerResponse = await collectProviderResponseWithRetry(this.clients, provider, {
636
894
  model: options.model,
637
895
  messages,
638
896
  numCtx,
897
+ jsonMode: useJsonEnvelope,
898
+ jsonSchema: useJsonEnvelope ? ACE_ENVELOPE_JSON_SCHEMA : undefined,
639
899
  onProgress: options.onProgress,
640
900
  }, options.onThinking);
901
+ addUsageTotals(usageTotals, providerResponse.usage);
902
+ const rawResponse = providerResponse.text;
641
903
  this.activeProviderClient = null;
642
904
  const envelope = parseEnvelope(rawResponse);
643
905
  if (envelope.status !== "parse_error") {
@@ -650,7 +912,108 @@ export class ModelBridge {
650
912
  noteProgress("thinking", { turn });
651
913
  }
652
914
  messages.push({ role: "assistant", content: rawResponse });
915
+ // Text-loop brake (WS-C+, E2): a model stuck repeating the same non-terminating
916
+ // output. This fires only on turns that execute NO tool — turns that run tools are
917
+ // governed by the tool-doom-loop brake below (which exempts legitimate polling:
918
+ // same call, changing results). The gap this closes is an envelope loop that
919
+ // performs no work, e.g. repeated `{"status":"tool","tool_calls":[]}`, which the
920
+ // tool brake never sees. Terminal statuses (message/complete/need_input) return
921
+ // before a repeat can accumulate.
922
+ const turnExecutesTools = envelope.status === "tool" &&
923
+ Array.isArray(envelope.tool_calls) &&
924
+ envelope.tool_calls.length > 0;
925
+ if (!turnExecutesTools) {
926
+ const textLoopKey = sha256Text(rawResponse.trim());
927
+ const textLoopCount = (textLoopCounts.get(textLoopKey) ?? 0) + 1;
928
+ textLoopCounts.set(textLoopKey, textLoopCount);
929
+ if (textLoopCount === 3) {
930
+ messages.push({
931
+ role: "user",
932
+ content: "You have produced the same response three times without making progress. " +
933
+ "Change approach, call a tool, or report status:need_input — do not repeat yourself.",
934
+ });
935
+ noteProgress("thinking", { reason: "bridge_text_doom_loop_warning", count: textLoopCount });
936
+ }
937
+ if (textLoopCount >= 4) {
938
+ const summary = "Bridge blocked after repeating an identical response four times without progress.";
939
+ await appendVericifyProcessPostSafe({
940
+ run_id: refs.run_id,
941
+ branch_id: refs.branch_id,
942
+ lane_id: refs.lane_id,
943
+ agent_id: `agent-${role}`,
944
+ kind: "blocker",
945
+ summary,
946
+ tool_refs: toolResults.map((entry) => entry.tool),
947
+ blocker_category: "bridge_text_doom_loop",
948
+ });
949
+ noteProgress("process_post", { kind: "blocker", reason: "bridge_text_doom_loop" });
950
+ return attachPlaybookMetadata(withUsageTotals({
951
+ bridge_id: this.bridgeId,
952
+ role,
953
+ status: "blocked",
954
+ reason_code: "bridge_text_doom_loop",
955
+ summary,
956
+ turns: turn,
957
+ tool_calls: toolResults,
958
+ child_results: childResults,
959
+ evidence_refs: evidenceRefs(),
960
+ }, usageTotals));
961
+ }
962
+ }
963
+ if (contextOverflowSuspected(providerResponse.usage, numCtx)) {
964
+ appendBridgeTrace({
965
+ workspace: options.workspace,
966
+ runId: refs.run_id,
967
+ turn,
968
+ envelopeStatus: envelope.status,
969
+ parseRepairAttempts,
970
+ outputDriftRepairs,
971
+ compressed: compressed.compressed,
972
+ usage: providerResponse.usage,
973
+ });
974
+ const summary = `Context overflow suspected: provider reported ${providerResponse.usage?.prompt_tokens ?? 0} ` +
975
+ `prompt tokens for num_ctx ${numCtx}.`;
976
+ await appendStatusEventSafe({
977
+ source_module: "capability-qa",
978
+ event_type: "MODEL_CONTEXT_OVERFLOW_SUSPECTED",
979
+ status: "blocked",
980
+ summary,
981
+ objective_id: "model-bridge-context-overflow",
982
+ payload: {
983
+ reason_code: "context_overflow",
984
+ role,
985
+ provider: provider.display,
986
+ model: options.model,
987
+ prompt_tokens: providerResponse.usage?.prompt_tokens,
988
+ completion_tokens: providerResponse.usage?.completion_tokens,
989
+ num_ctx: numCtx,
990
+ threshold_ratio: 0.98,
991
+ },
992
+ });
993
+ noteProgress("output", { status: "blocked", reason: "context_overflow" });
994
+ return attachPlaybookMetadata(withUsageTotals({
995
+ bridge_id: this.bridgeId,
996
+ role,
997
+ status: "blocked",
998
+ reason_code: "context_overflow",
999
+ summary,
1000
+ turns: turn,
1001
+ tool_calls: toolResults,
1002
+ child_results: childResults,
1003
+ evidence_refs: evidenceRefs(),
1004
+ }, usageTotals));
1005
+ }
653
1006
  if (envelope.status === "parse_error") {
1007
+ appendBridgeTrace({
1008
+ workspace: options.workspace,
1009
+ runId: refs.run_id,
1010
+ turn,
1011
+ envelopeStatus: envelope.status,
1012
+ parseRepairAttempts,
1013
+ outputDriftRepairs,
1014
+ compressed: compressed.compressed,
1015
+ usage: providerResponse.usage,
1016
+ });
654
1017
  // Plain-text fallback: accepted only for roles that do NOT require a JSON envelope
655
1018
  // and only when they have no tool scope (i.e. they legitimately produce prose output).
656
1019
  if ((selectedToolScope ?? []).length === 0 && !ROLES_REQUIRING_JSON_ENVELOPE.has(role)) {
@@ -667,7 +1030,7 @@ export class ModelBridge {
667
1030
  tool_refs: [],
668
1031
  });
669
1032
  noteProgress("process_post", { kind: "completion", fallback: "plain_text" });
670
- return verifyCompletionArtifacts({
1033
+ return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
671
1034
  bridge_id: this.bridgeId,
672
1035
  role,
673
1036
  status: "completed",
@@ -676,12 +1039,13 @@ export class ModelBridge {
676
1039
  tool_calls: toolResults,
677
1040
  child_results: childResults,
678
1041
  evidence_refs: evidenceRefs(),
679
- }, {
1042
+ }, usageTotals)), {
680
1043
  role,
681
1044
  task: options.task,
682
1045
  workspace: options.workspace,
683
1046
  touchedPaths,
684
1047
  expectedArtifacts: options.expectedArtifacts,
1048
+ requiredEvidenceRefs: options.requiredEvidenceRefs,
685
1049
  });
686
1050
  }
687
1051
  // Repair path: roles that require a JSON envelope get up to MAX_PARSE_REPAIR_ATTEMPTS
@@ -723,7 +1087,7 @@ export class ModelBridge {
723
1087
  tool_refs: toolResults.map((entry) => entry.tool),
724
1088
  });
725
1089
  noteProgress("process_post", { kind: "blocker" });
726
- return {
1090
+ return attachPlaybookMetadata(withUsageTotals({
727
1091
  bridge_id: this.bridgeId,
728
1092
  role,
729
1093
  status: "failed",
@@ -732,7 +1096,7 @@ export class ModelBridge {
732
1096
  tool_calls: toolResults,
733
1097
  child_results: childResults,
734
1098
  evidence_refs: evidenceRefs(),
735
- };
1099
+ }, usageTotals));
736
1100
  }
737
1101
  if (envelope.status === "tool" &&
738
1102
  Array.isArray(envelope.tool_calls) &&
@@ -776,19 +1140,26 @@ export class ModelBridge {
776
1140
  });
777
1141
  continue;
778
1142
  }
779
- const executed = await Promise.all(envelope.tool_calls.map(async (toolCall) => {
1143
+ const executedPairs = await Promise.all(envelope.tool_calls.map(async (toolCall) => {
780
1144
  const args = toolCall.input ?? {};
781
1145
  options.onToolCall?.(toolCall.tool, args);
782
1146
  noteProgress("tool_start", { tool: toolCall.tool });
783
- const rawToolResult = await executeAceInternalTool(toolCall.tool, args, sessionId, {
784
- workspace_path: options.workspace,
785
- });
786
- const result = truncateToolResult({
787
- tool: toolCall.tool,
788
- ok: !Boolean(rawToolResult?.isError),
789
- isError: Boolean(rawToolResult?.isError),
790
- summary: summarizeToolText(rawToolResult),
791
- }, options.workspace);
1147
+ const deadlineMs = toolDeadlineMs();
1148
+ const outcome = await executeToolWithDeadline(toolCall.tool, args, sessionId, { workspace_path: options.workspace }, deadlineMs);
1149
+ const result = outcome.timedOut
1150
+ ? {
1151
+ tool: toolCall.tool,
1152
+ ok: false,
1153
+ isError: true,
1154
+ summary: `tool_timeout: ${toolCall.tool} exceeded ${deadlineMs}ms deadline`,
1155
+ reason_code: "tool_timeout",
1156
+ }
1157
+ : truncateToolResult({
1158
+ tool: toolCall.tool,
1159
+ ok: !Boolean(outcome.rawToolResult?.isError),
1160
+ isError: Boolean(outcome.rawToolResult?.isError),
1161
+ summary: summarizeToolText(outcome.rawToolResult),
1162
+ }, options.workspace);
792
1163
  options.onToolResult?.(toolCall.tool, result);
793
1164
  noteProgress("tool_finish", { tool: toolCall.tool, ok: result.ok });
794
1165
  toolResults.push(result);
@@ -801,8 +1172,63 @@ export class ModelBridge {
801
1172
  if (pathArg)
802
1173
  touchedPaths.push(pathArg);
803
1174
  }
804
- return result;
1175
+ return { toolCall, result };
805
1176
  }));
1177
+ const executed = executedPairs.map((entry) => entry.result);
1178
+ appendBridgeTrace({
1179
+ workspace: options.workspace,
1180
+ runId: refs.run_id,
1181
+ turn,
1182
+ envelopeStatus: envelope.status,
1183
+ parseRepairAttempts,
1184
+ outputDriftRepairs,
1185
+ compressed: compressed.compressed,
1186
+ usage: providerResponse.usage,
1187
+ toolCalls: executed,
1188
+ });
1189
+ for (const { toolCall, result } of executedPairs) {
1190
+ const loopKey = toolDoomLoopKey(toolCall, result);
1191
+ const count = (toolDoomLoopCounts.get(loopKey) ?? 0) + 1;
1192
+ toolDoomLoopCounts.set(loopKey, count);
1193
+ if (count === 3) {
1194
+ messages.push({
1195
+ role: "user",
1196
+ content: "You have repeated the same tool call with the same result three times. " +
1197
+ "Change approach or report blocked; do not repeat the same failing action.",
1198
+ });
1199
+ noteProgress("thinking", {
1200
+ reason: "bridge_tool_doom_loop_warning",
1201
+ tool: toolCall.tool,
1202
+ count,
1203
+ });
1204
+ options.onThinking?.(`[doom-loop] repeated ${toolCall.tool} with identical result three times; change approach or report blocked.`);
1205
+ }
1206
+ if (count >= 4) {
1207
+ const summary = `Bridge blocked after repeated identical tool result for ${toolCall.tool}.`;
1208
+ await appendVericifyProcessPostSafe({
1209
+ run_id: refs.run_id,
1210
+ branch_id: refs.branch_id,
1211
+ lane_id: refs.lane_id,
1212
+ agent_id: `agent-${role}`,
1213
+ kind: "blocker",
1214
+ summary,
1215
+ tool_refs: toolResults.map((entry) => entry.tool),
1216
+ blocker_category: "bridge_tool_doom_loop",
1217
+ });
1218
+ noteProgress("process_post", { kind: "blocker", reason: "bridge_tool_doom_loop" });
1219
+ return attachPlaybookMetadata(withUsageTotals({
1220
+ bridge_id: this.bridgeId,
1221
+ role,
1222
+ status: "blocked",
1223
+ reason_code: "bridge_tool_doom_loop",
1224
+ summary,
1225
+ turns: turn,
1226
+ tool_calls: toolResults,
1227
+ child_results: childResults,
1228
+ evidence_refs: evidenceRefs(),
1229
+ }, usageTotals));
1230
+ }
1231
+ }
806
1232
  messages.push({
807
1233
  role: "user",
808
1234
  content: amendedTools.length > 0
@@ -812,6 +1238,16 @@ export class ModelBridge {
812
1238
  continue;
813
1239
  }
814
1240
  if (envelope.status === "message") {
1241
+ appendBridgeTrace({
1242
+ workspace: options.workspace,
1243
+ runId: refs.run_id,
1244
+ turn,
1245
+ envelopeStatus: envelope.status,
1246
+ parseRepairAttempts,
1247
+ outputDriftRepairs,
1248
+ compressed: compressed.compressed,
1249
+ usage: providerResponse.usage,
1250
+ });
815
1251
  const message = readEnvelopeText(envelope.message) || rawResponse.trim();
816
1252
  options.onOutput?.(message);
817
1253
  noteProgress("output", { status: "message" });
@@ -825,7 +1261,7 @@ export class ModelBridge {
825
1261
  tool_refs: [],
826
1262
  });
827
1263
  noteProgress("process_post", { kind: "progress" });
828
- return verifyCompletionArtifacts({
1264
+ return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
829
1265
  bridge_id: this.bridgeId,
830
1266
  role,
831
1267
  status: "completed",
@@ -834,15 +1270,26 @@ export class ModelBridge {
834
1270
  tool_calls: toolResults,
835
1271
  child_results: childResults,
836
1272
  evidence_refs: evidenceRefs(),
837
- }, {
1273
+ }, usageTotals), envelope.used_bullets), {
838
1274
  role,
839
1275
  task: options.task,
840
1276
  workspace: options.workspace,
841
1277
  touchedPaths,
842
1278
  expectedArtifacts: options.expectedArtifacts,
1279
+ requiredEvidenceRefs: options.requiredEvidenceRefs,
843
1280
  });
844
1281
  }
845
1282
  if (envelope.status === "need_input") {
1283
+ appendBridgeTrace({
1284
+ workspace: options.workspace,
1285
+ runId: refs.run_id,
1286
+ turn,
1287
+ envelopeStatus: envelope.status,
1288
+ parseRepairAttempts,
1289
+ outputDriftRepairs,
1290
+ compressed: compressed.compressed,
1291
+ usage: providerResponse.usage,
1292
+ });
846
1293
  const message = readEnvelopeText(envelope.message) || "Additional operator input required.";
847
1294
  options.onOutput?.(message);
848
1295
  noteProgress("output", { status: "need_input" });
@@ -856,7 +1303,7 @@ export class ModelBridge {
856
1303
  tool_refs: [],
857
1304
  });
858
1305
  noteProgress("process_post", { kind: "blocker" });
859
- return {
1306
+ return attachPlaybookMetadata(withUsageTotals({
860
1307
  bridge_id: this.bridgeId,
861
1308
  role,
862
1309
  status: "needs_input",
@@ -865,10 +1312,20 @@ export class ModelBridge {
865
1312
  tool_calls: toolResults,
866
1313
  child_results: childResults,
867
1314
  evidence_refs: evidenceRefs(),
868
- };
1315
+ }, usageTotals));
869
1316
  }
870
1317
  if (envelope.status === "complete") {
871
1318
  const summary = readEnvelopeText(envelope.summary) || "Bridge completed.";
1319
+ appendBridgeTrace({
1320
+ workspace: options.workspace,
1321
+ runId: refs.run_id,
1322
+ turn,
1323
+ envelopeStatus: envelope.status,
1324
+ parseRepairAttempts,
1325
+ outputDriftRepairs,
1326
+ compressed: compressed.compressed,
1327
+ usage: providerResponse.usage,
1328
+ });
872
1329
  // Output shape drift check: detect semantic violations before accepting the result.
873
1330
  const driftViolation = checkOutputShapeDrift(role, summary, toolResults);
874
1331
  if (driftViolation) {
@@ -901,7 +1358,7 @@ export class ModelBridge {
901
1358
  tool_refs: toolResults.map((entry) => entry.tool),
902
1359
  });
903
1360
  noteProgress("process_post", { kind: "blocker" });
904
- return {
1361
+ return attachPlaybookMetadata(withUsageTotals({
905
1362
  bridge_id: this.bridgeId,
906
1363
  role,
907
1364
  status: "failed",
@@ -910,7 +1367,7 @@ export class ModelBridge {
910
1367
  tool_calls: toolResults,
911
1368
  child_results: childResults,
912
1369
  evidence_refs: evidenceRefs(),
913
- };
1370
+ }, usageTotals));
914
1371
  }
915
1372
  // Clean output — accept the completion.
916
1373
  options.onOutput?.(summary);
@@ -925,7 +1382,7 @@ export class ModelBridge {
925
1382
  tool_refs: toolResults.map((entry) => entry.tool),
926
1383
  });
927
1384
  noteProgress("process_post", { kind: "completion" });
928
- return verifyCompletionArtifacts({
1385
+ return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
929
1386
  bridge_id: this.bridgeId,
930
1387
  role,
931
1388
  status: "completed",
@@ -934,12 +1391,13 @@ export class ModelBridge {
934
1391
  tool_calls: toolResults,
935
1392
  child_results: childResults,
936
1393
  evidence_refs: evidenceRefs(),
937
- }, {
1394
+ }, usageTotals), envelope.used_bullets), {
938
1395
  role,
939
1396
  task: options.task,
940
1397
  workspace: options.workspace,
941
1398
  touchedPaths,
942
1399
  expectedArtifacts: options.expectedArtifacts,
1400
+ requiredEvidenceRefs: options.requiredEvidenceRefs,
943
1401
  });
944
1402
  }
945
1403
  }
@@ -954,7 +1412,7 @@ export class ModelBridge {
954
1412
  tool_refs: toolResults.map((entry) => entry.tool),
955
1413
  });
956
1414
  noteProgress("process_post", { kind: "blocker" });
957
- return {
1415
+ return attachPlaybookMetadata(withUsageTotals({
958
1416
  bridge_id: this.bridgeId,
959
1417
  role,
960
1418
  status: "max_turns",
@@ -963,7 +1421,7 @@ export class ModelBridge {
963
1421
  tool_calls: toolResults,
964
1422
  child_results: childResults,
965
1423
  evidence_refs: evidenceRefs(),
966
- };
1424
+ }, usageTotals));
967
1425
  }
968
1426
  finally {
969
1427
  this.activeProviderClient = null;