@dzhechkov/harness-core 0.8.47 → 0.8.48

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.
@@ -54,6 +54,199 @@ function price(row: RecordRow, dimensions: Record<typeof tokenFields[number], nu
54
54
  capturedAt: text(prices?.['snapshotAt']), fingerprint: fnv1a64(JSON.stringify(prices ?? MODEL_PRICES)), current: false, billed: false } };
55
55
  }
56
56
 
57
+ type RoutingInput = Parameters<typeof buildStageUsageReport>[0];
58
+ type RoutingAttempt = { ordinal: number; model: string | null; family: 'openai' | 'claude'; wrapperInvoked: boolean;
59
+ outcome: 'answered' | 'failed' | 'rejected'; reason: string; selected: boolean };
60
+ type RoutingStage = { evidenceKey: string; dispatchSeq: number; plannedModel: string | null;
61
+ plannedModelSource: 'plan-declared' | 'plan-omitted' | 'unavailable' | 'not-recorded'; requestedModel: string | null;
62
+ probeId: string | null; linkStatus: string };
63
+ type RoutingProbe = { probeId: string; runId: string; family: 'openai' | 'claude'; source: string;
64
+ selectedModel: string | null; complete: boolean; totalConsidered: number; attempts: RoutingAttempt[] };
65
+ const safeRoutingModel = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value);
66
+ const routingId = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{32}$/.test(value);
67
+ const routingRun = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,128}$/.test(value) && value !== '.' && value !== '..';
68
+ const routingFamily = (value: unknown): value is 'openai' | 'claude' => value === 'openai' || value === 'claude';
69
+ const exactRoutingKeys = (value: unknown, keys: readonly string[]): value is RecordRow => record(value)
70
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
71
+ && Reflect.ownKeys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
72
+ const routingAbsenceReasons = ['producer-not-recorded', 'id-factory-missing'];
73
+ const routingErrorReasons = ['id-factory-invalid', 'provenance-invalid', 'candidate-model-invalid', 'wrapper-result-invalid', 'selected-model-invalid'];
74
+
75
+ function validateRoutingAttempts(value: unknown, family: unknown, selectedModel: unknown): { complete: boolean; totalConsidered: number; attempts: RoutingAttempt[] } | null {
76
+ if (!exactRoutingKeys(value, ['schema', 'complete', 'totalConsidered', 'attempts']) || value['schema'] !== 'wf-probe-attempts-1'
77
+ || typeof value['complete'] !== 'boolean' || !routingFamily(family) || (selectedModel !== null && !safeRoutingModel(selectedModel))) return null;
78
+ const total = count(value['totalConsidered']);
79
+ const attempts = value['attempts'];
80
+ if (total === null || total === 0 || !Array.isArray(attempts) || attempts.length !== Math.min(total, 32) || value['complete'] !== (total <= 32)) return null;
81
+ const safe: RoutingAttempt[] = [];
82
+ for (const [index, a] of attempts.entries()) {
83
+ if (!exactRoutingKeys(a, ['ordinal', 'model', 'family', 'wrapperInvoked', 'outcome', 'reason', 'selected']) || a['ordinal'] !== index + 1
84
+ || a['family'] !== family || typeof a['wrapperInvoked'] !== 'boolean' || typeof a['selected'] !== 'boolean') return null;
85
+ const rejected = family === 'claude' && !a['wrapperInvoked'] && !a['selected'] && a['model'] === null && a['outcome'] === 'rejected' && a['reason'] === 'invalid-candidate';
86
+ const answered = a['wrapperInvoked'] && a['selected'] && safeRoutingModel(a['model']) && a['outcome'] === 'answered' && a['reason'] === 'answered';
87
+ const failed = a['wrapperInvoked'] && !a['selected'] && safeRoutingModel(a['model']) && a['outcome'] === 'failed'
88
+ && ['timeout', 'spawn-error', 'no-exit-code', 'exit-nonzero', 'unexpected-response'].includes(a['reason'] as string);
89
+ if (!(rejected || answered || failed)) return null;
90
+ safe.push({ ordinal: index + 1, model: a['model'] as string | null, family, wrapperInvoked: a['wrapperInvoked'],
91
+ outcome: a['outcome'] as RoutingAttempt['outcome'], reason: a['reason'] as string, selected: a['selected'] });
92
+ }
93
+ const selected = safe.filter(a => a.selected);
94
+ if (!value['complete'] ? selected.length !== 0 : selectedModel === null ? selected.length !== 0
95
+ : selected.length !== 1 || selected[0]?.ordinal !== total || selected[0]?.model !== selectedModel) return null;
96
+ return { complete: value['complete'], totalConsidered: total, attempts: safe };
97
+ }
98
+
99
+ function routingStageIdentity(row: RecordRow): { evidenceKey: string; dispatchSeq: number } | null {
100
+ const seq = count(row['dispatchSeq']);
101
+ if (seq === null || seq === 0 || !routingRun(row['runId'])) return null;
102
+ const evidenceKey = JSON.stringify(['wf-dispatch', row['runId'], seq]);
103
+ if (Object.hasOwn(row, 'evidenceKey') && row['evidenceKey'] !== evidenceKey) return null;
104
+ return { evidenceKey, dispatchSeq: seq };
105
+ }
106
+
107
+ /** Routing uses raw bounded evidence, independently of numerical first-wins normalization. */
108
+ function projectRoutingProvenance(input: RoutingInput) {
109
+ const diagnostics = new Set<string>();
110
+ let defect = false;
111
+ let partial = false;
112
+ let scopeInvalid = false;
113
+ let scopeIncomplete = false;
114
+ const mark = (reason: string, severity: 'defect' | 'partial' = 'partial') => {
115
+ diagnostics.add(reason);
116
+ if (severity === 'defect') defect = true; else partial = true;
117
+ };
118
+ const metadataInvalid = (reason = 'metadata-invalid') => mark(reason, 'defect');
119
+ const sourceMap: Record<string, [string, 'defect' | 'partial']> = {
120
+ 'inventory-truncated': ['source-truncated', 'partial'], 'source-input-too-large': ['source-too-large', 'partial'],
121
+ 'missing-or-unreadable-source': ['source-unreadable', 'partial'], 'malformed-record': ['source-malformed', 'defect'],
122
+ 'malformed-budget-schema': ['source-malformed', 'defect'],
123
+ 'inventory-run-state-unavailable-or-foreign': ['source-scope-incomplete', 'partial'],
124
+ 'inventory-trace-binding-unavailable': ['source-scope-incomplete', 'partial'],
125
+ };
126
+ const invalidSources = ['source-outside-root-or-nonregular', 'source-symlink', 'source-changed-during-read',
127
+ 'inventory-trace-binding-mismatch', 'inventory-trace-invalid', 'inventory-workflow-projection-identity-invalid',
128
+ 'inventory-workflow-projection-identity-mismatch', 'foreign-project-root', 'foreign-trace-run', 'unknown-source',
129
+ 'source-selection-ambiguous', 'source-selection-conflicting-run-dir', 'workflow-source-missing-or-ambiguous', 'fa-run-selection-ambiguous-or-unidentified'];
130
+ for (const diagnostic of input.diagnostics ?? []) {
131
+ if (typeof diagnostic !== 'string' || diagnostic.length === 0) continue;
132
+ const name = diagnostic.split(':', 1)[0]!;
133
+ const mapping = invalidSources.includes(name) ? ['source-scope-invalid', 'defect'] as const : sourceMap[name];
134
+ if (mapping) { mark(mapping[0], mapping[1]); scopeInvalid ||= mapping[0] === 'source-scope-invalid'; scopeIncomplete ||= mapping[1] === 'partial'; }
135
+ else { mark('source-diagnostic-unmapped'); scopeIncomplete = true; }
136
+ }
137
+ const maximum = input.maxRecords ?? 100000;
138
+ if (input.rows.length > maximum || (input.expected?.length ?? 0) > maximum) { mark('source-truncated'); scopeIncomplete = true; }
139
+ const rawRows = input.rows.slice(0, maximum);
140
+ const invalidNormalizedKeys = new Set<string>();
141
+ const stageMaps = new Map<string, { raw: RecordRow; tuple: string; conflict: boolean; legacy: boolean; valid: boolean; modelInvalid: boolean }>();
142
+ const probeMaps = new Map<string, { raw: RecordRow; tuple: string; conflict: boolean; valid: boolean; reason: string | null; summary: RoutingProbe | null }>();
143
+ let hasNew = false;
144
+ let hasLegacy = false;
145
+ if (input.sourceKind === 'workflow-budget') {
146
+ if (!routingRun(input.runId)) { mark('source-scope-invalid', 'defect'); scopeInvalid = true; }
147
+ if (input.expected === undefined) { mark('source-scope-incomplete'); scopeIncomplete = true; }
148
+ for (const raw of rawRows) {
149
+ if (raw['kind'] === 'probe') {
150
+ const keys = ['probeId', 'probeProvenance', 'probeSource', 'probeObservationReason'];
151
+ const present = keys.map(key => Object.hasOwn(raw, key));
152
+ if (present.every(value => !value)) { hasLegacy ||= raw['runId'] === input.runId; continue; }
153
+ hasNew ||= raw['runId'] === input.runId;
154
+ const reason = raw['probeObservationReason'];
155
+ const modelValid = raw['model'] === null || safeRoutingModel(raw['model']);
156
+ const reasonValid = reason === null || (typeof reason === 'string' && [...routingAbsenceReasons, ...routingErrorReasons].includes(reason));
157
+ const idValid = raw['probeId'] === null || routingId(raw['probeId']);
158
+ const sourceValid = raw['probeSource'] === 'dispatcher-child-seam' || raw['probeSource'] === 'scripted-dispatcher';
159
+ const provenance = reason === null ? validateRoutingAttempts(raw['probeProvenance'], raw['family'], raw['model']) : null;
160
+ const valid = present.every(Boolean) && routingRun(raw['runId']) && routingFamily(raw['family']) && idValid && sourceValid && modelValid && reasonValid
161
+ && (reason === null ? routingId(raw['probeId']) && provenance !== null : raw['probeProvenance'] === null);
162
+ const supplied = raw['probeProvenance'];
163
+ const unsafeAttemptModel = record(supplied) && Array.isArray(supplied['attempts']) && supplied['attempts'].slice(0, 32).some(a => record(a)
164
+ && Object.hasOwn(a, 'model') && a['model'] !== null && !safeRoutingModel(a['model']));
165
+ if (!valid) metadataInvalid(modelValid && !unsafeAttemptModel ? 'metadata-invalid' : 'model-invalid');
166
+ else if (typeof reason === 'string') mark(reason, routingErrorReasons.includes(reason) ? 'defect' : 'partial');
167
+ else if (provenance && !provenance.complete) mark('attempts-truncated');
168
+ if (!routingId(raw['probeId'])) continue;
169
+ const summary: RoutingProbe | null = valid && reason === null && provenance !== null ? {
170
+ probeId: raw['probeId'], runId: raw['runId'] as string, family: raw['family'] as RoutingProbe['family'], source: raw['probeSource'] as string,
171
+ selectedModel: raw['model'] as string | null, complete: provenance.complete, totalConsidered: provenance.totalConsidered, attempts: provenance.attempts,
172
+ } : null;
173
+ // Rejected metadata is one closed marker: never traverse, copy or stringify its raw payload.
174
+ const tuple = JSON.stringify(valid
175
+ ? ['valid', raw['runId'], raw['family'], raw['model'], raw['probeSource'], reason, provenance]
176
+ : ['invalid']);
177
+ const old = probeMaps.get(raw['probeId']);
178
+ if (old && old.tuple !== tuple) { old.conflict = true; metadataInvalid('identity-conflict'); }
179
+ else if (!old) probeMaps.set(raw['probeId'], { raw, tuple, conflict: false, valid, reason: typeof reason === 'string' ? reason : null, summary });
180
+ } else if (raw['kind'] === 'stage') {
181
+ const identity = routingStageIdentity(raw);
182
+ if (!identity || raw['runId'] !== input.runId) {
183
+ metadataInvalid();
184
+ const key = text(raw['evidenceKey']) ?? (count(raw['dispatchSeq']) !== null ? JSON.stringify(['wf-dispatch', raw['runId'], raw['dispatchSeq']]) : null);
185
+ if (key !== null) invalidNormalizedKeys.add(key);
186
+ continue;
187
+ }
188
+ const keys = ['plannedModel', 'plannedModelSource', 'probeId'];
189
+ const present = keys.map(key => Object.hasOwn(raw, key));
190
+ const legacy = present.every(value => !value);
191
+ hasLegacy ||= legacy; hasNew ||= !legacy;
192
+ const plan = raw['plannedModel'];
193
+ const source = raw['plannedModelSource'];
194
+ const requestValid = raw['requestedModel'] === null || safeRoutingModel(raw['requestedModel']);
195
+ const planValid = source === 'plan-declared' ? safeRoutingModel(plan) : (source === 'plan-omitted' || source === 'unavailable') && plan === null;
196
+ const valid = legacy || present.every(Boolean) && requestValid && planValid && routingFamily(raw['family']) && (raw['probeId'] === null || routingId(raw['probeId']));
197
+ const modelInvalid = !legacy && (!requestValid || (source === 'plan-declared' && !safeRoutingModel(plan)) || source === 'unavailable');
198
+ if (!valid || modelInvalid) metadataInvalid(modelInvalid ? 'model-invalid' : 'metadata-invalid');
199
+ const tuple = JSON.stringify(legacy
200
+ ? ['legacy', routingFamily(raw['family']) ? raw['family'] : null, safeRoutingModel(raw['requestedModel']) ? raw['requestedModel'] : null]
201
+ : valid ? ['valid', raw['family'], raw['requestedModel'], plan, source, raw['probeId']] : ['invalid']);
202
+ const old = stageMaps.get(identity.evidenceKey);
203
+ if (old && old.tuple !== tuple) { old.conflict = true; metadataInvalid('identity-conflict'); }
204
+ else if (!old) stageMaps.set(identity.evidenceKey, { raw, tuple, conflict: false, legacy, valid, modelInvalid });
205
+ } else metadataInvalid();
206
+ }
207
+ for (const expected of (input.expected ?? []).slice(0, maximum)) {
208
+ const identity = routingStageIdentity(expected);
209
+ if (!identity || expected['runId'] !== input.runId) { metadataInvalid(); continue; }
210
+ if (!stageMaps.has(identity.evidenceKey)) { mark('expected-dispatch-missing'); scopeIncomplete = true; }
211
+ }
212
+ }
213
+ if (hasNew && hasLegacy) mark('not-recorded');
214
+ const stages: RoutingStage[] = [];
215
+ for (const [evidenceKey, stage] of [...stageMaps].sort(([a], [b]) => a.localeCompare(b))) {
216
+ const raw = stage.raw;
217
+ const referenced = routingId(raw['probeId']) ? probeMaps.get(raw['probeId']) : undefined;
218
+ let linkStatus = 'linked';
219
+ if (stage.conflict || referenced?.conflict) linkStatus = 'identity-conflict';
220
+ else if (scopeInvalid || !stage.valid || stage.modelInvalid || referenced && (!referenced.valid || referenced.reason !== null && routingErrorReasons.includes(referenced.reason))) linkStatus = 'invalid';
221
+ else if (stage.legacy) linkStatus = 'not-recorded';
222
+ else if (referenced && referenced.raw['runId'] !== input.runId) linkStatus = 'foreign-probe';
223
+ else if (referenced && referenced.raw['family'] !== raw['family']) linkStatus = 'family-mismatch';
224
+ else if (referenced?.summary && (referenced.summary.selectedModel === null || referenced.summary.selectedModel !== raw['requestedModel'])) linkStatus = 'selection-mismatch';
225
+ else if (raw['probeId'] !== null && !referenced) linkStatus = 'missing-probe';
226
+ else if (raw['probeId'] === null || referenced?.reason !== null && referenced?.reason !== undefined) linkStatus = 'probe-unavailable';
227
+ else if (scopeIncomplete) linkStatus = 'scope-incomplete';
228
+ const suppress = ['identity-conflict', 'invalid', 'foreign-probe', 'family-mismatch', 'selection-mismatch'].includes(linkStatus);
229
+ if (suppress) metadataInvalid(linkStatus === 'invalid' ? 'metadata-invalid' : linkStatus);
230
+ else if (linkStatus !== 'linked' && linkStatus !== 'not-recorded') mark(linkStatus === 'scope-incomplete' ? 'source-scope-incomplete' : linkStatus);
231
+ const identity = routingStageIdentity(raw)!;
232
+ stages.push({ evidenceKey, dispatchSeq: identity.dispatchSeq, plannedModel: suppress || stage.legacy ? null : raw['plannedModel'] as string | null,
233
+ plannedModelSource: suppress ? 'unavailable' : stage.legacy ? 'not-recorded' : raw['plannedModelSource'] as RoutingStage['plannedModelSource'],
234
+ requestedModel: suppress || stage.legacy ? null : raw['requestedModel'] as string | null,
235
+ probeId: !suppress && (linkStatus === 'linked' || linkStatus === 'scope-incomplete') ? raw['probeId'] as string : null, linkStatus });
236
+ }
237
+ const probes = [...probeMaps.values()].filter(probe => !scopeInvalid && !probe.conflict && probe.summary && probe.raw['runId'] === input.runId)
238
+ .map(probe => probe.summary!).sort((a, b) => a.probeId.localeCompare(b.probeId));
239
+ if (!hasNew) diagnostics.add('not-recorded');
240
+ const status = defect ? 'defect' : partial ? 'partial' : hasNew ? 'observed' : 'not-recorded';
241
+ const exposedStages = hasNew || defect ? stages : [];
242
+ const stageFields = new Map(stages.map(stage => [stage.evidenceKey, {
243
+ plannedModel: stage.plannedModel, plannedModelSource: stage.plannedModelSource, probeId: stage.probeId,
244
+ }]));
245
+ for (const key of invalidNormalizedKeys) stageFields.set(key, { plannedModel: null, plannedModelSource: 'unavailable', probeId: null });
246
+ return { report: { schema: 'routing-provenance-1', status, probes: probes.length ? probes : null,
247
+ stages: exposedStages.length ? exposedStages : null, diagnostics: [...diagnostics].sort() }, stageFields };
248
+ }
249
+
57
250
  /** All amounts remain source-specific; inventory never substitutes for a numeric witness. */
58
251
  export function buildStageUsageReport(input: {
59
252
  sourceKind: string; sourcePath: string; runId: string | null; rows: readonly RecordRow[];
@@ -173,8 +366,9 @@ export function buildStageUsageReport(input: {
173
366
  const pricingComplete = rows.length > 0 && rows.every((r) => r.pricingKnown) && totalComplete;
174
367
  const conservation = { status: quantitativeDefect ? 'DEFECT' : totalComplete ? 'BALANCED' : 'INSUFFICIENT_DATA', scope: 'reported receipt attribution; not independent source verification', metric };
175
368
  const dimensionCoverage = Object.fromEntries(tokenFields.map((key) => [key, { known: rows.filter((r) => r[key] !== null).length, unknown: rows.filter((r) => r[key] === null).length + expectedMissing.length }]));
176
- return { schema: 'stage-usage-1', sourceKind: input.sourceKind, sourcePath: input.sourcePath, runId: input.runId, metric: compatible ? metric : 'incompatible-metrics',
177
- rows, verdict: conservation.status, complete: totalComplete && witnessComplete && pricingComplete,
369
+ const routing = projectRoutingProvenance(input);
370
+ return { routingProvenance: routing.report, schema: 'stage-usage-1', sourceKind: input.sourceKind, sourcePath: input.sourcePath, runId: input.runId, metric: compatible ? metric : 'incompatible-metrics',
371
+ rows: rows.map(row => ({ ...row, ...(routing.stageFields.get(row.evidenceKey ?? '') ?? { plannedModel: null, plannedModelSource: 'not-recorded', probeId: null }) })), verdict: conservation.status, complete: totalComplete && witnessComplete && pricingComplete,
178
372
  knownRunTotalTokens: known, knownAccountedTokens: accounted, knownUnaccountedTokens: unaccounted,
179
373
  stageTokensSum: accounted !== null && doubled !== null ? accounted + doubled : null, doubleAttributedTokens: doubled,
180
374
  runTotalTokens: totalComplete ? known : null, sourceVerifiedTotalTokens: witnessComplete && totalComplete ? known : null,
@@ -80,6 +80,13 @@ export interface ProbeOutcome {
80
80
  id: string | null;
81
81
  wallMs: number;
82
82
  detail: string;
83
+ provenance?: {
84
+ schema: 'wf-probe-attempts-1'; complete: boolean; totalConsidered: number;
85
+ attempts: Array<{ ordinal: number; model: string | null; family: BridgeFamily; wrapperInvoked: boolean;
86
+ outcome: 'answered' | 'failed' | 'rejected'; selected: boolean;
87
+ reason: 'answered' | 'timeout' | 'spawn-error' | 'no-exit-code' | 'exit-nonzero' | 'unexpected-response' | 'invalid-candidate' }>;
88
+ } | null;
89
+ provenanceReason?: 'candidate-model-invalid' | 'wrapper-result-invalid' | null;
83
90
  }
84
91
 
85
92
  export interface Dispatcher {
@@ -342,6 +349,36 @@ function codexStructuredOutput(stdout: string): { structured: boolean; valid: bo
342
349
  return { structured: true, valid: !malformed && !failedTerminal, text, usage: result, reportedModel: reportedModels.size === 1 ? [...reportedModels][0]! : null };
343
350
  }
344
351
 
352
+ /** Observe the existing wrapper seam; this does not attest OS child start. */
353
+ function collectProbeMetadata(family: BridgeFamily) {
354
+ const attempts: NonNullable<ProbeOutcome['provenance']>['attempts'] = [];
355
+ let totalConsidered = 0;
356
+ let invalid: Exclude<ProbeOutcome['provenanceReason'], undefined> = null;
357
+ return {
358
+ record(id: string, wrapperInvoked: boolean, r: ChildRun | null, selected: boolean) {
359
+ const ordinal = ++totalConsidered;
360
+ if (wrapperInvoked && (typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(id))) invalid = 'candidate-model-invalid';
361
+ let reason: NonNullable<ProbeOutcome['provenance']>['attempts'][number]['reason'] = 'invalid-candidate';
362
+ if (r !== null) {
363
+ const typed = typeof r.stdout === 'string' && typeof r.stderr === 'string' && typeof r.timedOut === 'boolean'
364
+ && (r.exitCode === null || (Number.isSafeInteger(r.exitCode) && r.exitCode >= 0))
365
+ && (r.spawnError === null || (typeof r.spawnError === 'string' && r.spawnError.length > 0));
366
+ const consistent = typed && !(r.timedOut && (r.spawnError !== null || r.exitCode === 0))
367
+ && !(r.spawnError !== null && r.exitCode !== null) && !(selected && (r.timedOut || r.spawnError !== null || r.exitCode !== 0));
368
+ if (!consistent && invalid !== 'candidate-model-invalid') invalid = 'wrapper-result-invalid';
369
+ reason = r.timedOut ? 'timeout' : r.spawnError !== null ? 'spawn-error' : r.exitCode === null ? 'no-exit-code'
370
+ : r.exitCode !== 0 ? 'exit-nonzero' : selected ? 'answered' : 'unexpected-response';
371
+ }
372
+ if (ordinal <= 32) attempts.push({ ordinal, model: wrapperInvoked ? id : null, family, wrapperInvoked,
373
+ outcome: !wrapperInvoked ? 'rejected' : selected ? 'answered' : 'failed', reason, selected });
374
+ },
375
+ finish(): Pick<ProbeOutcome, 'provenance' | 'provenanceReason'> {
376
+ return { provenance: invalid === null ? { schema: 'wf-probe-attempts-1', complete: totalConsidered <= 32, totalConsidered, attempts } : null,
377
+ provenanceReason: invalid };
378
+ },
379
+ };
380
+ }
381
+
345
382
  // ── the adapter factories (pure over the injected ChildRunner) ───────────────────────────────────
346
383
 
347
384
  function failed(family: BridgeFamily, model: string | null, wallMs: number, reason: DispatchReason, detail: string, outcome: 'null' | 'error' = 'null'): DispatchResult {
@@ -381,12 +418,16 @@ export function makeCodexExecDispatcher(run: ChildRunner, opts?: { bin?: string;
381
418
  const t0 = clock();
382
419
  const list = candidates.length > 0 ? candidates : ['gpt-5.5'];
383
420
  const started: string[] = [];
421
+ const observation = collectProbeMetadata('openai');
384
422
  for (const id of list) {
423
+ const wrapperInvoked = true;
385
424
  const r = await run(bin, codexProbeArgv(id), { stdinText: null, timeoutMs: 120_000, cwd: isolatedCwd(), detached: true });
386
425
  started.push(id);
387
- if (interpretCodexProbe(r)) return { id, wallMs: clock() - t0, detail: `codex answered on ${id}` };
426
+ const selected = interpretCodexProbe(r);
427
+ observation.record(id, wrapperInvoked, r, selected);
428
+ if (selected) return { id, wallMs: clock() - t0, detail: `codex answered on ${id}`, ...observation.finish() };
388
429
  }
389
- return { id: null, wallMs: clock() - t0, detail: `no codex candidate answered a probe (tried: ${started.join(', ')}) — an allowlist says an id is spellable, only a probe says it answers` };
430
+ return { id: null, wallMs: clock() - t0, detail: `no codex candidate answered a probe (tried: ${started.join(', ')}) — an allowlist says an id is spellable, only a probe says it answers`, ...observation.finish() };
390
431
  },
391
432
  dispatch: async (req) => {
392
433
  const t0 = clock();
@@ -435,14 +476,18 @@ export function makeClaudePDispatcher(run: ChildRunner, opts?: { bin?: string; i
435
476
  const t0 = clock();
436
477
  const list = candidates.length > 0 ? candidates : ['sonnet'];
437
478
  const tried: string[] = [];
479
+ const observation = collectProbeMetadata('claude');
438
480
  for (const id of list) {
439
481
  const argv = claudeProbeArgs(id);
440
482
  tried.push(id);
441
- if (argv === null) continue; // an unsafe id is not spellable, let alone answerable
483
+ if (argv === null) { observation.record(id, false, null, false); continue; } // existing unsafe rejection
484
+ const wrapperInvoked = true;
442
485
  const r = await run(bin, argv, { stdinText: null, timeoutMs: 120_000, cwd: isolatedCwd(), detached: true });
443
- if (interpretClaudeProbe({ stdout: r.stdout, exitCode: r.exitCode ?? 1 })) return { id, wallMs: clock() - t0, detail: `claude answered on ${id}` };
486
+ const selected = interpretClaudeProbe({ stdout: r.stdout, exitCode: r.exitCode ?? 1 });
487
+ observation.record(id, wrapperInvoked, r, selected);
488
+ if (selected) return { id, wallMs: clock() - t0, detail: `claude answered on ${id}`, ...observation.finish() };
444
489
  }
445
- return { id: null, wallMs: clock() - t0, detail: `no claude candidate answered a probe (tried: ${tried.join(', ')})` };
490
+ return { id: null, wallMs: clock() - t0, detail: `no claude candidate answered a probe (tried: ${tried.join(', ')})`, ...observation.finish() };
446
491
  },
447
492
  dispatch: async (req) => {
448
493
  const t0 = clock();
@@ -43,7 +43,7 @@ import { classifyFailure, errSnap, gateVerdict, joinRegion, stepContractLines, t
43
43
  import { checkpointInputHash, decideCheckpointResume, parseCheckpointRead, serializeCheckpoint } from './feature-adr-checkpoints.js';
44
44
  import { modelFamily, type BridgeFamily } from './qe-bridge.js';
45
45
  import { buildReqeDebt } from './reqe.js';
46
- import { CODEX_EXEC_XHIGH_TIMEOUT_MS, defangGateEchoes, type DispatchResult, type Dispatcher } from './workflow-run-dispatch.js';
46
+ import { CODEX_EXEC_XHIGH_TIMEOUT_MS, defangGateEchoes, type DispatchResult, type Dispatcher, type ProbeOutcome } from './workflow-run-dispatch.js';
47
47
 
48
48
  // ─────────────────────────────────────────────────────────────────────────────
49
49
  // Schemas / constants
@@ -178,6 +178,12 @@ export interface WfBudgetRow extends Pick<DispatchResult, 'tokensTotal' | 'token
178
178
  model: string | null;
179
179
  modelProvenance?: string;
180
180
  requestedModel?: string | null;
181
+ plannedModel?: string | null;
182
+ plannedModelSource?: 'plan-declared' | 'plan-omitted' | 'unavailable';
183
+ probeId?: string | null;
184
+ probeProvenance?: Exclude<ProbeOutcome['provenance'], undefined>;
185
+ probeSource?: 'dispatcher-child-seam' | 'scripted-dispatcher';
186
+ probeObservationReason?: 'producer-not-recorded' | 'id-factory-missing' | 'id-factory-invalid' | 'provenance-invalid' | 'candidate-model-invalid' | 'wrapper-result-invalid' | 'selected-model-invalid' | null;
181
187
  wallMs: number;
182
188
  tokensIn: number | null;
183
189
  tokensOut: number | null;
@@ -936,6 +942,35 @@ export interface RunStore {
936
942
  writeReqeDebt(record: object): void;
937
943
  }
938
944
 
945
+ const safeRoutingModel = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value);
946
+ function probeRecordReason(probe: ProbeOutcome, family: BridgeFamily): Exclude<WfBudgetRow['probeObservationReason'], undefined> {
947
+ const exact = (value: unknown, keys: string[]): value is Record<string, unknown> => value !== null && typeof value === 'object'
948
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
949
+ && Reflect.ownKeys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
950
+ if (probe.id !== null && !safeRoutingModel(probe.id)) return 'selected-model-invalid';
951
+ const present = [Object.hasOwn(probe, 'provenance'), Object.hasOwn(probe, 'provenanceReason')];
952
+ if (present.every(value => !value)) return 'producer-not-recorded';
953
+ if (!present.every(Boolean)) return 'provenance-invalid';
954
+ if (probe.provenance === null && (probe.provenanceReason === 'candidate-model-invalid' || probe.provenanceReason === 'wrapper-result-invalid')) return probe.provenanceReason;
955
+ const p = probe.provenance;
956
+ if (probe.provenanceReason !== null || !exact(p, ['schema', 'complete', 'totalConsidered', 'attempts']) || p.schema !== 'wf-probe-attempts-1'
957
+ || typeof p.complete !== 'boolean' || !Number.isSafeInteger(p.totalConsidered) || p.totalConsidered <= 0 || !Array.isArray(p.attempts)
958
+ || p.attempts.length !== Math.min(p.totalConsidered, 32) || p.complete !== (p.totalConsidered <= 32)) return 'provenance-invalid';
959
+ for (const [index, a] of p.attempts.entries()) {
960
+ if (!exact(a, ['ordinal', 'model', 'family', 'wrapperInvoked', 'outcome', 'reason', 'selected']) || a.ordinal !== index + 1
961
+ || a.family !== family || typeof a.wrapperInvoked !== 'boolean' || typeof a.selected !== 'boolean') return 'provenance-invalid';
962
+ const rejected = family === 'claude' && !a.wrapperInvoked && !a.selected && a.model === null && a.outcome === 'rejected' && a.reason === 'invalid-candidate';
963
+ const answered = a.wrapperInvoked && a.selected && safeRoutingModel(a.model) && a.outcome === 'answered' && a.reason === 'answered';
964
+ const failed = a.wrapperInvoked && !a.selected && safeRoutingModel(a.model) && a.outcome === 'failed'
965
+ && ['timeout', 'spawn-error', 'no-exit-code', 'exit-nonzero', 'unexpected-response'].includes(a.reason);
966
+ if (!(rejected || answered || failed)) return 'provenance-invalid';
967
+ }
968
+ const selected = p.attempts.filter(a => a.selected);
969
+ if (!p.complete ? selected.length !== 0 : probe.id === null ? selected.length !== 0
970
+ : selected.length !== 1 || selected[0]?.ordinal !== p.totalConsidered || selected[0]?.model !== probe.id) return 'provenance-invalid';
971
+ return null;
972
+ }
973
+
939
974
  export interface SchedulerDeps {
940
975
  store: RunStore;
941
976
  dispatchers: Record<BridgeFamily, Dispatcher>;
@@ -944,6 +979,7 @@ export interface SchedulerDeps {
944
979
  /** Injected ISO clock (determinism, NFR-2). */
945
980
  now(): string;
946
981
  monotonicMs(): number;
982
+ newProbeId?: () => string;
947
983
  /**
948
984
  * TEST SEAM (named, never a casual flag): disables the landed barrier so the F5 mutant can show
949
985
  * the lying file-step passing. Default false; a run that sets it records `dispatcherOverride`.
@@ -987,6 +1023,7 @@ function failureClassOf(res: DispatchResult): FailureClass | null {
987
1023
  }
988
1024
 
989
1025
  interface RunCtx {
1026
+ probeIds: Partial<Record<BridgeFamily, string | null>>;
990
1027
  inputs: RunnerInputs;
991
1028
  pre: PreflightOk;
992
1029
  deps: SchedulerDeps;
@@ -1062,15 +1099,34 @@ export async function runWorkflow(inputs: RunnerInputs, pre: PreflightOk, deps:
1062
1099
 
1063
1100
  const usedFamilies = [...new Set(Object.values(pre.families))];
1064
1101
  const probedIds: Partial<Record<BridgeFamily, string | null>> = {};
1102
+ const probeIds: Partial<Record<BridgeFamily, string | null>> = {};
1065
1103
  let probeAgentCalls = 0;
1066
1104
  const probeDetail: Partial<Record<BridgeFamily, string>> = {};
1067
1105
  for (const family of usedFamilies) {
1068
1106
  const candidates = [...new Set(allSpecs(pre.projection).filter((s) => pre.families[s.stepId] === family).map((s) => s.model).filter((m): m is string => typeof m === 'string'))];
1069
1107
  const probe = await deps.dispatchers[family].probe(candidates);
1070
1108
  probeAgentCalls++;
1109
+ let probeId: string | null = null;
1110
+ let idReason: Exclude<WfBudgetRow['probeObservationReason'], undefined> = 'id-factory-missing';
1111
+ if (deps.newProbeId) {
1112
+ try {
1113
+ const id = deps.newProbeId();
1114
+ if (typeof id === 'string' && /^[0-9a-f]{32}$/.test(id)) {
1115
+ probeId = id;
1116
+ idReason = null;
1117
+ } else idReason = 'id-factory-invalid';
1118
+ } catch { idReason = 'id-factory-invalid'; }
1119
+ }
1120
+ const producerReason = probeRecordReason(probe, family);
1121
+ const probeObservationReason = producerReason && producerReason !== 'producer-not-recorded' ? producerReason : idReason ?? producerReason;
1122
+ probeIds[family] = probeId;
1071
1123
  store.appendBudgetRow({
1072
1124
  schema: WF_BUDGET_ROW_SCHEMA,
1073
1125
  kind: 'probe',
1126
+ probeId,
1127
+ probeProvenance: probeObservationReason === null ? probe.provenance! : null,
1128
+ probeSource: deps.dispatcherOverride ? 'scripted-dispatcher' : 'dispatcher-child-seam',
1129
+ probeObservationReason,
1074
1130
  runId: inputs.runId,
1075
1131
  projectRoot: inputs.projectRoot ?? inputs.cwdRoot,
1076
1132
  dispatchSeq: null,
@@ -1207,6 +1263,7 @@ export async function runWorkflow(inputs: RunnerInputs, pre: PreflightOk, deps:
1207
1263
 
1208
1264
  const opened = openTrace(inputs, pre, store, inputs.resume !== null);
1209
1265
  const ctx: RunCtx = {
1266
+ probeIds,
1210
1267
  inputs,
1211
1268
  pre,
1212
1269
  deps,
@@ -1526,6 +1583,9 @@ async function dispatchOnce(
1526
1583
  family,
1527
1584
  model: res.modelUsed,
1528
1585
  requestedModel: model,
1586
+ plannedModel: safeRoutingModel(spec.model) ? spec.model : null,
1587
+ plannedModelSource: safeRoutingModel(spec.model) ? 'plan-declared' : spec.model == null ? 'plan-omitted' : 'unavailable',
1588
+ probeId: ctx.probeIds[family] ?? null,
1529
1589
  modelProvenance: res.modelProvenance ?? (res.modelUsed === null ? 'not-recorded' : 'dispatcher-reported'),
1530
1590
  wallMs,
1531
1591
  tokensIn: res.tokensIn,