@hecer/yoke 1.21.1 → 1.22.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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +6 -1
  5. package/bench/analyze-codex-comparison.mjs +90 -17
  6. package/bench/compare-codex.mjs +159 -36
  7. package/bench/result-schema.mjs +132 -0
  8. package/canon/manifest.yaml +1 -1
  9. package/dist/agents/pi-telemetry.js +2 -1
  10. package/dist/agents/process-streams.js +12 -64
  11. package/dist/agents/provider-selection.js +12 -0
  12. package/dist/agents/telemetry.js +52 -52
  13. package/dist/change/inbox.js +8 -3
  14. package/dist/check/command.js +69 -17
  15. package/dist/check/delivery.js +121 -0
  16. package/dist/cli.js +12 -3
  17. package/dist/code-intelligence/budgets.js +138 -0
  18. package/dist/code-intelligence/contracts.js +2 -0
  19. package/dist/code-intelligence/coordinator.js +156 -84
  20. package/dist/code-intelligence/evidence.js +87 -34
  21. package/dist/code-intelligence/mcp-client.js +10 -2
  22. package/dist/code-intelligence/mcp-server.js +10 -10
  23. package/dist/dashboard/analytics.js +5 -3
  24. package/dist/goals/command.js +183 -53
  25. package/dist/goals/usage.js +87 -0
  26. package/dist/loop/candidate-cleanup.js +47 -17
  27. package/dist/loop/candidates.js +17 -11
  28. package/dist/loop/dispatcher.js +89 -26
  29. package/dist/loop/failure.js +104 -0
  30. package/dist/loop/gate-snapshot.js +19 -0
  31. package/dist/loop/git.js +1 -1
  32. package/dist/loop/loop.js +100 -66
  33. package/dist/loop/parallel-adapters.js +22 -4
  34. package/dist/loop/parallel-command.js +32 -5
  35. package/dist/loop/recovery.js +23 -5
  36. package/dist/loop/reporter.js +21 -4
  37. package/dist/loop/run-command.js +95 -46
  38. package/dist/loop/runner.js +5 -4
  39. package/dist/loop/worker.js +145 -91
  40. package/dist/observability/history.js +1 -1
  41. package/dist/observability/invocation.js +42 -0
  42. package/dist/observability/usage.js +17 -0
  43. package/dist/prd/command.js +20 -7
  44. package/dist/prd/decompose.js +5 -2
  45. package/dist/retrofit/config.js +26 -1
  46. package/dist/retrofit/gitignore.js +2 -0
  47. package/dist/routing/attempts.js +241 -0
  48. package/dist/routing/capability.js +13 -9
  49. package/dist/routing/optimization.js +73 -0
  50. package/dist/routing/registry.js +7 -1
  51. package/dist/routing/router.js +280 -127
  52. package/dist/setup/command.js +8 -2
  53. package/dist/smoke/command.js +302 -74
  54. package/docs/BENCHMARK-MANIFEST.md +131 -0
  55. package/docs/CODE-INTELLIGENCE.md +43 -1
  56. package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
  57. package/docs/DELIVERY-JOURNEYS.md +199 -0
  58. package/docs/ECONOMIC-ROUTING.md +180 -0
  59. package/docs/GOALS.md +61 -4
  60. package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
  61. package/docs/parallel-execution.md +37 -9
  62. package/gemini-extension.json +1 -1
  63. package/package.json +1 -1
@@ -1,5 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { assertSafePath } from './snapshots.js';
3
+ /** Truncate only between Unicode code points, including at normalizer limits. */
4
+ export function shorten(value, maxChars) {
5
+ const clipped = value.slice(0, Math.max(0, maxChars));
6
+ return /[\uD800-\uDBFF]$/u.test(clipped) ? clipped.slice(0, -1) : clipped;
7
+ }
3
8
  function hash(value) { return createHash('sha256').update(value).digest('hex'); }
4
9
  function textOf(raw) {
5
10
  if (typeof raw === 'string')
@@ -22,32 +27,46 @@ function locate(text, root) {
22
27
  return { path: null, line: null };
23
28
  }
24
29
  }
25
- export function makeProvenance(options, text, resolution = 'resolved') {
30
+ export function makeProvenance(options, text, resolution = 'unresolved') {
26
31
  const location = locate(text, options.root);
27
- const file = location.path ? options.snapshot.files.find(item => item.path === location.path) : undefined;
28
32
  return {
29
- backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: file?.hash ?? null,
30
- byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'current',
33
+ backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: null,
34
+ // A current workspace snapshot does not prove that a backend index read it.
35
+ // None of the supported backend wire contracts supplies a validated index
36
+ // snapshot binding. Do not trust raw `freshness` or attach a newer file hash.
37
+ byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'unknown',
31
38
  };
32
39
  }
33
40
  export function evidenceId(provenance, excerpt) {
34
- return `evidence-${hash(JSON.stringify({ provenance, excerpt })).slice(0, 24)}`;
41
+ const { evidence_id: _id, ...fields } = provenance;
42
+ return `evidence-${hash(JSON.stringify({ provenance: fields, excerpt })).slice(0, 24)}`;
43
+ }
44
+ function candidates(raw) {
45
+ if (Array.isArray(raw))
46
+ return raw;
47
+ if (raw && typeof raw === 'object' && Array.isArray(raw.results))
48
+ return raw.results;
49
+ return null;
35
50
  }
36
51
  export function normalizeItems(raw, options, kind = 'code', limit = 100) {
37
- const full = textOf(raw);
38
- const chunks = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : [raw]);
52
+ const chunks = candidates(raw) ?? (raw === null || raw === undefined || raw === '' ? [] : [raw]);
39
53
  const provenance = [];
40
54
  const items = [];
41
- const maxChars = options.maxChars ?? 12000;
55
+ const warnings = [];
56
+ const maxChars = Math.min(12000, options.maxChars ?? 12000);
57
+ if (chunks.length > limit)
58
+ warnings.push('backend result list truncated');
42
59
  for (const candidate of chunks.slice(0, limit)) {
43
- const excerpt = textOf(candidate).slice(0, maxChars);
60
+ const text = textOf(candidate);
61
+ const excerpt = shorten(text, maxChars);
62
+ if (excerpt !== text)
63
+ warnings.push('backend result excerpt truncated');
44
64
  if (!excerpt)
45
65
  continue;
46
- const p = makeProvenance(options, excerpt, excerpt.toLowerCase().includes('ambiguous') ? 'ambiguous' : 'resolved');
47
- if (candidate && typeof candidate === 'object' && typeof candidate.relative_path === 'string') {
66
+ const p = makeProvenance(options, excerpt);
67
+ if (candidate && typeof candidate === 'object' && 'relative_path' in candidate && typeof candidate.relative_path === 'string') {
48
68
  try {
49
69
  p.source_path = assertSafePath(options.root, candidate.relative_path);
50
- p.content_hash = options.snapshot.files.find(file => file.path === p.source_path)?.hash ?? null;
51
70
  }
52
71
  catch {
53
72
  p.source_path = null;
@@ -56,33 +75,67 @@ export function normalizeItems(raw, options, kind = 'code', limit = 100) {
56
75
  }
57
76
  }
58
77
  const id = evidenceId(p, excerpt);
78
+ p.evidence_id = id;
59
79
  provenance.push(p);
60
80
  items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: items.length });
61
81
  }
62
- if (items.length === 0 && full) {
63
- const excerpt = full.slice(0, maxChars);
64
- const p = makeProvenance(options, excerpt, 'unresolved');
65
- const id = evidenceId(p, excerpt);
66
- provenance.push(p);
67
- items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: 0 });
68
- }
69
- return { items, provenance };
82
+ return { items, provenance, warnings: [...new Set(warnings)] };
83
+ }
84
+ export function serenaSymbolId(namePath, relativePath) {
85
+ return `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: relativePath }))}`;
70
86
  }
71
87
  export function normalizeSymbols(raw, options) {
72
- const normalized = normalizeItems(raw, options, 'symbol');
73
- const candidates = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : []);
74
- const symbols = normalized.items.map((item, index) => {
75
- const candidate = candidates[index] && typeof candidates[index] === 'object' ? candidates[index] : {};
76
- const namePath = String(candidate.name_path ?? candidate.namePath ?? candidate.name ?? item.excerpt.split(/\s|\(|\{/u)[0] ?? 'unknown');
77
- const path = typeof candidate.relative_path === 'string' ? candidate.relative_path : item.path;
78
- const stableId = `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: path ?? '' }))}`;
79
- return { symbol_id: stableId, name: String(candidate.name ?? namePath.split('/').at(-1) ?? namePath), path, kind: String(candidate.kind ?? 'unknown'), signature: item.excerpt.slice(0, 500), evidence_ids: item.evidence_ids };
80
- });
81
- return { symbols, references: [], diagnostics: [], provenance: normalized.provenance };
88
+ const list = candidates(raw);
89
+ const symbols = [];
90
+ const provenance = [];
91
+ const warnings = [];
92
+ if (!list) {
93
+ const normalized = normalizeItems(raw, options, 'symbol');
94
+ return { symbols, references: [], diagnostics: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'unrecognized semantic symbol response'] };
95
+ }
96
+ if (list.length > 100)
97
+ warnings.push('semantic symbol result list truncated');
98
+ for (const value of list.slice(0, 100)) {
99
+ const normalized = normalizeItems([value], options, 'symbol', 1);
100
+ warnings.push(...normalized.warnings);
101
+ const item = normalized.items[0];
102
+ const p = normalized.provenance[0];
103
+ if (!item || !p)
104
+ continue;
105
+ const candidate = value && typeof value === 'object' ? value : {};
106
+ const namePath = typeof candidate.name_path === 'string' ? candidate.name_path.trim() : '';
107
+ if (!options.adapter.semantic || !namePath || !item.path || !options.snapshot.files.some(file => file.path === item.path)) {
108
+ provenance.push(p);
109
+ warnings.push('unresolved semantic symbol record');
110
+ continue;
111
+ }
112
+ p.resolution = 'resolved';
113
+ p.evidence_id = evidenceId(p, item.excerpt);
114
+ provenance.push(p);
115
+ symbols.push({ symbol_id: serenaSymbolId(namePath, item.path), name: typeof candidate.name === 'string' && candidate.name ? candidate.name : namePath.split('/').at(-1) || namePath,
116
+ path: item.path, kind: typeof candidate.kind === 'string' ? candidate.kind : 'unknown', signature: shorten(item.excerpt, 500), evidence_ids: [p.evidence_id] });
117
+ }
118
+ return { symbols, references: [], diagnostics: [], provenance, warnings: [...new Set(warnings)] };
82
119
  }
83
- export function normalizeEdges(raw, options, relation) {
120
+ /** Graft prose/search hits are candidates, not an ordered graph path. */
121
+ export function normalizeEdges(raw, options, _relation) {
84
122
  const normalized = normalizeItems(raw, options, 'code');
85
- const nodes = normalized.items.map(item => ({ id: item.item_id, label: item.excerpt.slice(0, 160), path: item.path, evidence_ids: item.evidence_ids }));
86
- const edges = nodes.slice(1).map((node, index) => ({ from: nodes[index].id, to: node.id, relation, evidence_ids: node.evidence_ids }));
87
- return { nodes, edges, provenance: normalized.provenance };
123
+ const nodes = normalized.items.map(item => ({ id: item.item_id, label: shorten(item.excerpt, 160), path: item.path, evidence_ids: item.evidence_ids }));
124
+ return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'structural trace response has no validated edge contract'] };
125
+ }
126
+ export function normalizeReferenceEdges(raw, options, target, relation = 'references') {
127
+ const normalized = normalizeSymbols(raw, options);
128
+ const nodes = normalized.symbols.map(symbol => ({ id: symbol.symbol_id, label: symbol.name, path: symbol.path, evidence_ids: symbol.evidence_ids }));
129
+ let targetPath;
130
+ try {
131
+ targetPath = assertSafePath(options.root, target.relative_path);
132
+ if (!options.snapshot.files.some(file => file.path === targetPath))
133
+ throw new Error('target is absent from snapshot');
134
+ }
135
+ catch {
136
+ return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'reference target is unresolved'] };
137
+ }
138
+ const targetNode = { id: target.symbol_id, label: target.name_path, path: targetPath, evidence_ids: [] };
139
+ const edges = nodes.map(node => ({ from: node.id, to: targetNode.id, relation, evidence_ids: node.evidence_ids }));
140
+ return { nodes: [targetNode, ...nodes], edges, provenance: normalized.provenance, warnings: normalized.warnings };
88
141
  }
@@ -20,8 +20,14 @@ export class McpStdioClient {
20
20
  this.maxBytes = maxBytes;
21
21
  }
22
22
  async call(tool, arguments_, timeoutMs) {
23
+ const deadline = Date.now() + timeoutMs;
23
24
  await this.start(timeoutMs);
24
- return this.request('tools/call', { name: tool, arguments: arguments_ }, timeoutMs);
25
+ const remaining = deadline - Date.now();
26
+ if (remaining <= 0) {
27
+ void this.close();
28
+ throw new Error('MCP call deadline exceeded during initialization');
29
+ }
30
+ return this.request('tools/call', { name: tool, arguments: arguments_ }, remaining);
25
31
  }
26
32
  async close() {
27
33
  if (!this.child)
@@ -35,7 +41,7 @@ export class McpStdioClient {
35
41
  this.pending.clear();
36
42
  child.kill('SIGTERM');
37
43
  await new Promise(resolve => {
38
- const timer = setTimeout(() => { if (!child.killed)
44
+ const timer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
39
45
  child.kill('SIGKILL'); resolve(); }, 500);
40
46
  child.once('close', () => { clearTimeout(timer); resolve(); });
41
47
  });
@@ -60,6 +66,8 @@ export class McpStdioClient {
60
66
  return Promise.resolve();
61
67
  }
62
68
  request(method, params, timeoutMs) {
69
+ if (timeoutMs <= 0)
70
+ return Promise.reject(new Error(`MCP request deadline exceeded: ${method}`));
63
71
  if (!this.child)
64
72
  return Promise.reject(new Error('MCP process is not running'));
65
73
  const id = this.nextId++;
@@ -4,22 +4,22 @@ import { TOOL_DEFINITIONS } from './contracts.js';
4
4
  import { loadConfig } from '../retrofit/config.js';
5
5
  const INPUT_SCHEMAS = {
6
6
  code_context: { type: 'object', required: ['workspace_id', 'query'], properties: { workspace_id: { type: 'string' }, query: { type: 'string' }, snapshot_id: { type: 'string' }, mode: { type: 'string', enum: ['orient', 'locate', 'explain'] }, paths: { type: 'array', items: { type: 'string' } }, include_docs: { type: 'boolean' }, token_budget: { type: 'integer' }, timeout_ms: { type: 'integer' } } },
7
- code_symbol: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, query: { type: 'string' }, path: { type: 'string' }, include: { type: 'array' } } },
8
- code_trace: { type: 'object', required: ['workspace_id', 'snapshot_id', 'symbol_id', 'direction', 'relations'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, direction: { type: 'string', enum: ['in', 'out'] }, relations: { type: 'array' }, max_depth: { type: 'integer' }, max_nodes: { type: 'integer' } } },
9
- code_impact: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, targets: { type: 'array' }, plan_id: { type: 'string' }, include_docs: { type: 'boolean' }, require_semantic: { type: 'boolean' } } },
10
- code_edit_preview: { type: 'object', required: ['workspace_id', 'snapshot_id', 'operations', 'validation_profile'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, operations: { type: 'array' }, validation_profile: { type: 'string', enum: ['default', 'extended'] } } },
7
+ code_symbol: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, query: { type: 'string' }, path: { type: 'string' }, include: { type: 'array' }, token_budget: { type: 'integer', minimum: 128, maximum: 16000 }, timeout_ms: { type: 'integer', minimum: 100, maximum: 60000 } } },
8
+ code_trace: { type: 'object', required: ['workspace_id', 'snapshot_id', 'symbol_id', 'direction', 'relations'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, direction: { type: 'string', enum: ['in', 'out'] }, relations: { type: 'array' }, max_depth: { type: 'integer' }, max_nodes: { type: 'integer' }, require_resolved: { type: 'boolean' }, timeout_ms: { type: 'integer', minimum: 100, maximum: 60000 } } },
9
+ code_impact: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, targets: { type: 'array' }, plan_id: { type: 'string' }, include_docs: { type: 'boolean' }, require_semantic: { type: 'boolean' }, max_depth: { type: 'integer' }, timeout_ms: { type: 'integer', minimum: 100, maximum: 60000 } } },
10
+ code_edit_preview: { type: 'object', required: ['workspace_id', 'snapshot_id', 'operations', 'validation_profile'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, operations: { type: 'array' }, validation_profile: { type: 'string', enum: ['default', 'extended'] }, timeout_ms: { type: 'integer', minimum: 100, maximum: 600000 } } },
11
11
  code_edit_apply: { type: 'object', required: ['workspace_id', 'plan_id', 'expected_snapshot_id', 'idempotency_key'], properties: { workspace_id: { type: 'string' }, plan_id: { type: 'string' }, expected_snapshot_id: { type: 'string' }, idempotency_key: { type: 'string' } } },
12
12
  };
13
13
  function arg(name, args) { return args.find(value => value.startsWith(`--${name}=`))?.slice(name.length + 3); }
14
14
  function summary(response) { return `status=${response.status} snapshot=${response.snapshot_id} warnings=${response.warnings.length} (full result in structuredContent)`; }
15
- export async function runCodeIntelligenceServer(argv = process.argv.slice(2)) {
15
+ export async function runCodeIntelligenceServer(argv = process.argv.slice(2), io = process) {
16
16
  const workspace = arg('workspace', argv) ?? process.cwd();
17
17
  const configured = loadConfig(workspace)?.codeIntelligence;
18
18
  const mode = arg('mode', argv) ?? configured?.mode ?? 'active';
19
19
  const coordinator = new CodeIntelligenceCoordinator(workspace, {
20
20
  mode,
21
21
  policy: { excludePatterns: configured?.policy?.excludePatterns, maxFileBytes: configured?.policy?.maxFileBytes },
22
- limits: { maxBytes: configured?.limits?.maxBytes, maxBackends: configured?.limits?.maxBackends },
22
+ limits: configured?.limits,
23
23
  graft: configured?.graft, graphify: configured?.graphify, serena: configured?.serena,
24
24
  });
25
25
  let framing = 'line';
@@ -27,9 +27,9 @@ export async function runCodeIntelligenceServer(argv = process.argv.slice(2)) {
27
27
  const send = (message) => {
28
28
  const payload = JSON.stringify(message);
29
29
  if (framing === 'line')
30
- process.stdout.write(`${payload}\n`);
30
+ io.stdout.write(`${payload}\n`);
31
31
  else
32
- process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
32
+ io.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
33
33
  };
34
34
  const handle = async (message) => {
35
35
  if (message.method === 'notifications/initialized' || message.method?.startsWith('notifications/'))
@@ -51,7 +51,7 @@ export async function runCodeIntelligenceServer(argv = process.argv.slice(2)) {
51
51
  if (message.id !== undefined)
52
52
  send({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `method not found: ${message.method}` } });
53
53
  };
54
- process.stdin.on('data', chunk => {
54
+ io.stdin.on('data', chunk => {
55
55
  buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
56
56
  while (buffer.length) {
57
57
  let raw;
@@ -87,7 +87,7 @@ export async function runCodeIntelligenceServer(argv = process.argv.slice(2)) {
87
87
  catch { /* malformed protocol input is ignored */ }
88
88
  }
89
89
  });
90
- await new Promise(resolve => { const stop = () => { process.off('SIGTERM', stop); process.off('SIGINT', stop); void coordinator.close().finally(resolve); }; process.once('SIGTERM', stop); process.once('SIGINT', stop); process.stdin.once('end', stop); });
90
+ await new Promise(resolve => { const stop = () => { process.off('SIGTERM', stop); process.off('SIGINT', stop); io.stdin.off('end', stop); void coordinator.close().finally(resolve); }; process.once('SIGTERM', stop); process.once('SIGINT', stop); io.stdin.once('end', stop); });
91
91
  }
92
92
  if (process.argv[1]?.endsWith('code-intelligence-server.js'))
93
93
  void runCodeIntelligenceServer();
@@ -50,7 +50,8 @@ export function aggregateMeasurements(events, period) {
50
50
  for (const target of targets)
51
51
  target.activity++;
52
52
  if (event.type === 'tokens') {
53
- const calls = Array.isArray(data.calls) && data.calls.length ? data.calls : [{ ...data, actualModel: data.model, durationMs: event.durationMs }];
53
+ const hasCallBreakdown = Array.isArray(data.calls) && data.calls.length > 0;
54
+ const calls = hasCallBreakdown ? data.calls : [{ ...data, actualModel: data.model, durationMs: event.durationMs }];
54
55
  for (const item of calls) {
55
56
  if (!item || typeof item !== 'object')
56
57
  continue;
@@ -70,7 +71,8 @@ export function aggregateMeasurements(events, period) {
70
71
  dimensionTargets.push(dimension);
71
72
  }
72
73
  for (const target of [...targets, get(models, key), get(modelBuckets, JSON.stringify([bucketOf(event.timestamp, period.bucket), provider, model, role])), ...dimensionTargets]) {
73
- const usageAvailable = call.usageAvailable !== false && data.usageAvailable !== false && call.measurementComplete !== false && data.measurementComplete !== false;
74
+ const usageAvailable = call.usageAvailable !== false && call.measurementComplete !== false
75
+ && (hasCallBreakdown || (data.usageAvailable !== false && data.measurementComplete !== false));
74
76
  if (usageAvailable) {
75
77
  addMetric(target, 'inputTokens', call.inputTokens);
76
78
  addMetric(target, 'outputTokens', call.outputTokens);
@@ -87,7 +89,7 @@ export function aggregateMeasurements(events, period) {
87
89
  target.unknownCalls++;
88
90
  if (finite(call.totalCostUsd))
89
91
  target.costReportedCalls++;
90
- if (call.costMeasurementComplete === false || data.costMeasurementComplete === false)
92
+ if (call.costMeasurementComplete === false || (!hasCallBreakdown && data.costMeasurementComplete === false))
91
93
  target.incompleteCosts++;
92
94
  }
93
95
  }