@dzhechkov/harness-core 0.8.45 → 0.8.47
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.
- package/.dz-manifest.json +206 -174
- package/README.md +136 -8
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +32 -2
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-snapshot.d.ts +14 -1
- package/dist/agentdb-snapshot.d.ts.map +1 -1
- package/dist/agentdb-snapshot.js +100 -4
- package/dist/agentdb-snapshot.js.map +1 -1
- package/dist/apply-leg.d.ts +9 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +47 -10
- package/dist/apply-leg.js.map +1 -1
- package/dist/architecture.d.ts +0 -5
- package/dist/architecture.d.ts.map +1 -1
- package/dist/architecture.js +25 -2
- package/dist/architecture.js.map +1 -1
- package/dist/codex-rollouts.d.ts +35 -7
- package/dist/codex-rollouts.d.ts.map +1 -1
- package/dist/codex-rollouts.js +201 -113
- package/dist/codex-rollouts.js.map +1 -1
- package/dist/cost-ledger.d.ts +42 -3
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +478 -15
- package/dist/cost-ledger.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +16 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +21 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +7 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +48 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-plan.d.ts +4 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +4 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/mutation-gate.d.ts +36 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +109 -19
- package/dist/mutation-gate.js.map +1 -1
- package/dist/npm-homepage.d.ts +64 -0
- package/dist/npm-homepage.d.ts.map +1 -0
- package/dist/npm-homepage.js +109 -0
- package/dist/npm-homepage.js.map +1 -0
- package/dist/operations.d.ts +6 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +150 -24
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts +13 -0
- package/dist/pack-inventory.d.ts.map +1 -1
- package/dist/pack-inventory.js +28 -2
- package/dist/pack-inventory.js.map +1 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +4 -1
- package/dist/parity.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +43 -1
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +269 -13
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +5 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +18 -1
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +63 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +82 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release-package-audit.d.ts +86 -0
- package/dist/release-package-audit.d.ts.map +1 -0
- package/dist/release-package-audit.js +272 -0
- package/dist/release-package-audit.js.map +1 -0
- package/dist/release.d.ts +8 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +58 -32
- package/dist/release.js.map +1 -1
- package/dist/round-exec-claim.d.ts +36 -0
- package/dist/round-exec-claim.d.ts.map +1 -0
- package/dist/round-exec-claim.js +17 -0
- package/dist/round-exec-claim.js.map +1 -0
- package/dist/round.d.ts +10 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +5 -1
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +11 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +124 -20
- package/dist/run-records.js.map +1 -1
- package/dist/session-retro.d.ts +33 -0
- package/dist/session-retro.d.ts.map +1 -1
- package/dist/session-retro.js +170 -34
- package/dist/session-retro.js.map +1 -1
- package/dist/sign.d.ts +44 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +126 -1
- package/dist/sign.js.map +1 -1
- package/dist/stage-usage.d.ts +148 -0
- package/dist/stage-usage.d.ts.map +1 -0
- package/dist/stage-usage.js +261 -0
- package/dist/stage-usage.js.map +1 -0
- package/dist/statusline.d.ts +23 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +167 -1
- package/dist/statusline.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +21 -19
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +206 -95
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +12 -3
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +29 -2
- package/dist/workflow-run.js.map +1 -1
- package/package.json +4 -4
- package/sbom.json +309 -229
- package/src/agentdb-index.ts +31 -2
- package/src/agentdb-snapshot.ts +105 -4
- package/src/apply-leg.ts +47 -10
- package/src/architecture.ts +15 -2
- package/src/codex-rollouts.ts +155 -147
- package/src/cost-ledger.ts +308 -19
- package/src/feature-adr-routing.ts +22 -0
- package/src/guard.ts +52 -0
- package/src/index.ts +19 -3
- package/src/loop-plan.ts +8 -0
- package/src/mutation-gate.ts +153 -19
- package/src/npm-homepage.ts +142 -0
- package/src/operations.ts +177 -22
- package/src/pack-inventory.ts +27 -2
- package/src/parity.ts +4 -1
- package/src/publish-sibling-drift.ts +214 -11
- package/src/publish.ts +20 -1
- package/src/qe-bridge.ts +86 -1
- package/src/release-package-audit.ts +264 -0
- package/src/release.ts +55 -22
- package/src/round-exec-claim.ts +43 -0
- package/src/round.ts +8 -0
- package/src/run-records.ts +101 -22
- package/src/session-retro.ts +153 -31
- package/src/sign.ts +136 -1
- package/src/stage-usage.ts +191 -0
- package/src/statusline.ts +149 -1
- package/src/workflow-run-dispatch.ts +148 -89
- package/src/workflow-run.ts +40 -4
- package/dist/ledger-cost-fill.d.ts +0 -58
- package/dist/ledger-cost-fill.d.ts.map +0 -1
- package/dist/ledger-cost-fill.js +0 -78
- package/dist/ledger-cost-fill.js.map +0 -1
- package/dist/retro.d.ts +0 -131
- package/dist/retro.d.ts.map +0 -1
- package/dist/retro.js +0 -207
- package/dist/retro.js.map +0 -1
- package/dist/sbom.d.ts +0 -42
- package/dist/sbom.d.ts.map +0 -1
- package/dist/sbom.js +0 -120
- package/dist/sbom.js.map +0 -1
|
@@ -60,7 +60,18 @@ export interface DispatchResult {
|
|
|
60
60
|
/** null when the runtime did not report a count — NEVER 0, never estimated (ADR-004 C4). */
|
|
61
61
|
tokensIn: number | null;
|
|
62
62
|
tokensOut: number | null;
|
|
63
|
-
tokensSource: 'claude-envelope' | 'codex-stderr' | null;
|
|
63
|
+
tokensSource: 'claude-envelope' | 'codex-stderr' | 'codex-json' | null;
|
|
64
|
+
tokensTotal?: number | null;
|
|
65
|
+
tokensCacheRead?: number | null;
|
|
66
|
+
tokensCacheWrite?: number | null;
|
|
67
|
+
tokensReasoning?: number | null;
|
|
68
|
+
reportedTotalBasis?: 'raw-inclusive' | 'uncached-display' | 'reported-unknown' | 'output-only' | 'unknown';
|
|
69
|
+
inputCacheSemantics?: 'includes-cache-read-write' | 'excludes-cache-read-write' | 'uncached-display' | 'unknown';
|
|
70
|
+
reportedCostUsd?: number | null;
|
|
71
|
+
usageDiagnostics?: string[];
|
|
72
|
+
totalDerivation?: string;
|
|
73
|
+
modelProvenance?: string;
|
|
74
|
+
usageSource?: { schema: string; scope: string; threadId: string | null; turnId: string | null; receiptId: string | null };
|
|
64
75
|
failure?: DispatchFailure;
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -181,7 +192,7 @@ export { CODEX_EXEC_PROMPT_CEILING_CHARS };
|
|
|
181
192
|
export function codexExecArgv(modelId: string, prompt: string, deliverable: Deliverable): string[] {
|
|
182
193
|
const returnValue = (deliverable ?? 'return-value') !== 'file';
|
|
183
194
|
const text = returnValue ? CODEX_SCOPING_PREFIX + '\n\n' + prompt : prompt;
|
|
184
|
-
return ['exec', '-m', modelId, ...(returnValue ? ['--sandbox', 'read-only'] : []), text];
|
|
195
|
+
return ['exec', '-m', modelId, ...(returnValue ? ['--sandbox', 'read-only'] : []), '--json', text];
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
/** The liveness probe: an allowlist says an id is SPELLABLE, only a probe says it ANSWERS. */
|
|
@@ -205,55 +216,130 @@ export function interpretCodexProbe(out: { stdout: string; exitCode: number | nu
|
|
|
205
216
|
* builds/configs do print one; it is tested, not assumed. (Named consequence: codex runs report no
|
|
206
217
|
* token counts today. That is the honest state, not a bug to paper over — see the manifest.)
|
|
207
218
|
*/
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
type ObservedUsage = Pick<DispatchResult, 'tokensIn' | 'tokensOut' | 'tokensSource' | 'tokensTotal' | 'tokensCacheRead' | 'tokensCacheWrite' | 'tokensReasoning' | 'reportedTotalBasis' | 'inputCacheSemantics' | 'reportedCostUsd' | 'usageDiagnostics' | 'usageSource' | 'totalDerivation'>;
|
|
220
|
+
const count = (value: unknown): number | null => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
221
|
+
const emptyUsage = (): ObservedUsage => ({ tokensIn: null, tokensOut: null, tokensTotal: null, tokensCacheRead: null,
|
|
222
|
+
tokensCacheWrite: null, tokensReasoning: null, tokensSource: null, reportedTotalBasis: 'unknown', inputCacheSemantics: 'unknown',
|
|
223
|
+
reportedCostUsd: null, usageDiagnostics: [] });
|
|
224
|
+
|
|
225
|
+
export function extractCodexTokens(stderr: string): ObservedUsage {
|
|
226
|
+
const text = String(stderr ?? ''); const usage = emptyUsage();
|
|
227
|
+
const numeric = (raw: string | undefined): number | null => raw === undefined ? null : count(Number(raw.replace(/[,_\s]/g, '')));
|
|
228
|
+
const input = /\binput\b[^\n\d+.-]{0,20}([+-]?\d[\d,_]*(?:\.\d+)?)|\btokens?\s+in\b[^\n\d+.-]{0,10}([+-]?\d[\d,_]*(?:\.\d+)?)/i.exec(text);
|
|
229
|
+
const output = /\boutput\b[^\n\d+.-]{0,20}([+-]?\d[\d,_]*(?:\.\d+)?)|\btokens?\s+out\b[^\n\d+.-]{0,10}([+-]?\d[\d,_]*(?:\.\d+)?)/i.exec(text);
|
|
230
|
+
const total = /tokens used\s*\n\s*([+-]?\d[\d,_]*(?:\.\d+)?)|Token usage:\s*total=([+-]?\d[\d,_]*(?:\.\d+)?)/i.exec(text);
|
|
231
|
+
usage.tokensIn = numeric(input?.[1] ?? input?.[2]); usage.tokensOut = numeric(output?.[1] ?? output?.[2]);
|
|
232
|
+
usage.tokensTotal = numeric(total?.[1] ?? total?.[2]);
|
|
233
|
+
if (input && usage.tokensIn === null) usage.usageDiagnostics!.push('invalid-counter:input');
|
|
234
|
+
if (output && usage.tokensOut === null) usage.usageDiagnostics!.push('invalid-counter:output');
|
|
235
|
+
if (total && usage.tokensTotal === null) usage.usageDiagnostics!.push('invalid-counter:total');
|
|
236
|
+
const cached = /\(\+([\d,_]+) cached\)/i.exec(text);
|
|
237
|
+
if (total?.[2] && cached) usage.tokensCacheRead = numeric(cached[1]);
|
|
238
|
+
usage.reportedTotalBasis = total?.[2] ? 'uncached-display' : 'reported-unknown';
|
|
239
|
+
usage.inputCacheSemantics = total?.[2] ? 'uncached-display' : 'unknown';
|
|
240
|
+
usage.tokensSource = usage.tokensTotal != null || usage.tokensIn != null || usage.tokensOut != null ? 'codex-stderr' : null;
|
|
241
|
+
usage.totalDerivation = usage.tokensTotal === null ? 'not-recorded' : 'reported';
|
|
242
|
+
return usage;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function extractClaudeUsage(stdout: string): ObservedUsage {
|
|
246
|
+
let chosen: Record<string, unknown> | null = null;
|
|
247
|
+
for (const line of [String(stdout).trim(), ...String(stdout).split(/\r?\n/)]) {
|
|
248
|
+
try { const obj = JSON.parse(line) as Record<string, unknown>; if (obj && obj['type'] === 'result') chosen = obj; } catch { /* non-envelope line */ }
|
|
249
|
+
}
|
|
250
|
+
const result = emptyUsage(); if (!chosen) return result;
|
|
251
|
+
const usage = chosen['usage']; if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return result;
|
|
252
|
+
const raw = usage as Record<string, unknown>;
|
|
253
|
+
const invalid = (obj: Record<string, unknown>, key: string) => obj[key] != null && count(obj[key]) === null;
|
|
254
|
+
const field = (obj: Record<string, unknown>, key: string): number | null => {
|
|
255
|
+
if (invalid(obj, key)) result.usageDiagnostics!.push('invalid-count:' + key);
|
|
256
|
+
return count(obj[key]);
|
|
214
257
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
258
|
+
result.tokensIn = field(raw, 'input_tokens'); result.tokensOut = field(raw, 'output_tokens');
|
|
259
|
+
result.tokensCacheRead = field(raw, 'cache_read_input_tokens'); result.tokensCacheWrite = field(raw, 'cache_creation_input_tokens');
|
|
260
|
+
const creation = raw['cache_creation'];
|
|
261
|
+
if (creation != null) {
|
|
262
|
+
if (typeof creation !== 'object' || Array.isArray(creation)) {
|
|
263
|
+
result.usageDiagnostics!.push('invalid-cache-creation-shape'); result.tokensCacheWrite = null;
|
|
264
|
+
} else {
|
|
265
|
+
const nested = creation as Record<string, unknown>;
|
|
266
|
+
const a = field(nested, 'ephemeral_5m_input_tokens'); const b = field(nested, 'ephemeral_1h_input_tokens');
|
|
267
|
+
const nestedInvalid = invalid(nested, 'ephemeral_5m_input_tokens') || invalid(nested, 'ephemeral_1h_input_tokens');
|
|
268
|
+
const total = a !== null && b !== null ? count(a + b) : null;
|
|
269
|
+
if (a !== null && b !== null && total === null) result.usageDiagnostics!.push('cache-creation-overflow');
|
|
270
|
+
if (nestedInvalid || (a !== null && b !== null && total === null)) result.tokensCacheWrite = null;
|
|
271
|
+
else if (raw['cache_creation_input_tokens'] == null) result.tokensCacheWrite = total;
|
|
272
|
+
else if (result.tokensCacheWrite !== null && total !== null && total !== result.tokensCacheWrite) {
|
|
273
|
+
result.usageDiagnostics!.push('cache-creation-mismatch'); result.tokensCacheWrite = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
result.tokensReasoning = field(raw, 'reasoning_output_tokens');
|
|
278
|
+
if (result.tokensReasoning !== null && result.tokensOut !== null && result.tokensReasoning > result.tokensOut) {
|
|
279
|
+
result.usageDiagnostics!.push('reasoning-exceeds-output'); result.tokensReasoning = null;
|
|
280
|
+
}
|
|
281
|
+
result.tokensTotal = field(raw, 'total_tokens');
|
|
282
|
+
const components = [result.tokensIn, result.tokensCacheRead, result.tokensCacheWrite, result.tokensOut];
|
|
283
|
+
const partitionKnown = components.every((n) => n !== null);
|
|
284
|
+
const derived = partitionKnown ? count(components.reduce<number>((n, v) => n + (v ?? 0), 0)) : null;
|
|
285
|
+
if (partitionKnown && derived === null) result.usageDiagnostics!.push('total-overflow');
|
|
286
|
+
if (invalid(raw, 'total_tokens')) result.totalDerivation = 'invalid-reported-total';
|
|
287
|
+
else if (raw['total_tokens'] == null) {
|
|
288
|
+
result.tokensTotal = derived; result.totalDerivation = derived === null ? 'not-recorded' : 'disjoint-dimensions';
|
|
289
|
+
} else result.totalDerivation = 'reported';
|
|
290
|
+
if (derived !== null && result.tokensTotal !== null && result.tokensTotal !== derived) result.usageDiagnostics!.push('total-split-mismatch');
|
|
291
|
+
result.reportedCostUsd = typeof chosen['total_cost_usd'] === 'number' && Number.isFinite(chosen['total_cost_usd']) && chosen['total_cost_usd'] >= 0 ? chosen['total_cost_usd'] : null;
|
|
292
|
+
result.reportedTotalBasis = 'raw-inclusive'; result.inputCacheSemantics = 'excludes-cache-read-write'; result.tokensSource = 'claude-envelope';
|
|
293
|
+
return result;
|
|
221
294
|
}
|
|
222
295
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
obj = JSON.parse(c);
|
|
244
|
-
} catch {
|
|
245
|
-
continue;
|
|
296
|
+
function codexStructuredOutput(stdout: string): { structured: boolean; valid: boolean; text: string; usage: ObservedUsage; reportedModel?: string | null } {
|
|
297
|
+
const lines = stdout.split(/\r?\n/).filter((line) => line.trim());
|
|
298
|
+
const events: Record<string, unknown>[] = []; let malformed = false; let recognized = false;
|
|
299
|
+
const types = new Set(['thread.started', 'turn.started', 'turn.completed', 'turn.failed', 'item.started', 'item.updated', 'item.completed', 'error']);
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
if (/"type"\s*:\s*"(?:thread\.|turn\.|item\.|error")/.test(line)) recognized = true;
|
|
302
|
+
try { const event = JSON.parse(line) as Record<string, unknown>;
|
|
303
|
+
if (event && typeof event === 'object' && typeof event['type'] === 'string' && (types.has(event['type']) || /^(thread|turn|item)\./.test(event['type']))) recognized = true;
|
|
304
|
+
events.push(event);
|
|
305
|
+
} catch { malformed = true; }
|
|
306
|
+
}
|
|
307
|
+
if (!recognized) return { structured: false, valid: true, text: stdout.trim(), usage: emptyUsage() };
|
|
308
|
+
const result = emptyUsage(); let failedTerminal = false; let text = ''; let threadId: string | null = null; const terminals: Record<string, unknown>[] = []; const reportedModels = new Set<string>();
|
|
309
|
+
for (const event of events) {
|
|
310
|
+
if (!event || typeof event !== 'object' || !types.has(String(event['type']))) { malformed = true; continue; }
|
|
311
|
+
if (typeof event['model'] === 'string' && event['model'].trim()) reportedModels.add(event['model']);
|
|
312
|
+
if (event['type'] === 'thread.started' && typeof event['thread_id'] === 'string') { if (threadId !== null && threadId !== event['thread_id']) malformed = true; threadId = event['thread_id']; }
|
|
313
|
+
if (event['type'] === 'item.completed' && event['item'] && typeof event['item'] === 'object') {
|
|
314
|
+
const item = event['item'] as Record<string, unknown>; if (item['type'] === 'agent_message' && typeof item['text'] === 'string') text = item['text'];
|
|
246
315
|
}
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
if (u !== null && typeof u === 'object' && !Array.isArray(u)) usage = u as Record<string, unknown>;
|
|
316
|
+
if (event['type'] === 'turn.completed' || (event['type'] === 'turn.failed' && event['usage'] != null)) terminals.push(event);
|
|
317
|
+
if (event['type'] === 'turn.failed' || event['type'] === 'error') failedTerminal = true;
|
|
250
318
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
319
|
+
const distinct = new Set(terminals.map((v) => { const u = v['usage'] && typeof v['usage'] === 'object' ? v['usage'] as Record<string,unknown> : {}; return JSON.stringify([v['turn_id'] ?? null, ...['input_tokens','cached_input_tokens','cache_write_input_tokens','output_tokens','reasoning_output_tokens','total_tokens'].map((key) => u[key] ?? null)]); }));
|
|
320
|
+
if (distinct.size !== 1) { malformed = true; result.usageDiagnostics!.push(distinct.size === 0 ? 'terminal-usage-unavailable' : 'conflicting-terminal-usage'); }
|
|
321
|
+
if (distinct.size === 1) {
|
|
322
|
+
const terminal = terminals[0]!; const raw = terminal['usage'];
|
|
323
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
324
|
+
const usage = raw as Record<string, unknown>;
|
|
325
|
+
result.tokensIn = count(usage['input_tokens']); result.tokensOut = count(usage['output_tokens']);
|
|
326
|
+
result.tokensCacheRead = count(usage['cached_input_tokens']); result.tokensCacheWrite = count(usage['cache_write_input_tokens']);
|
|
327
|
+
result.tokensReasoning = count(usage['reasoning_output_tokens']);
|
|
328
|
+
const derived = result.tokensIn !== null && result.tokensOut !== null ? count(result.tokensIn + result.tokensOut) : null;
|
|
329
|
+
result.tokensTotal = count(usage['total_tokens']) ?? derived;
|
|
330
|
+
result.totalDerivation = count(usage['total_tokens']) !== null ? 'reported' : derived === null ? 'not-recorded' : 'input-plus-output';
|
|
331
|
+
if (derived !== null && result.tokensTotal !== derived) result.usageDiagnostics!.push('total-split-mismatch');
|
|
332
|
+
if (result.tokensCacheRead != null && result.tokensIn != null && result.tokensCacheRead > result.tokensIn) result.usageDiagnostics!.push('cache-exceeds-input');
|
|
333
|
+
if (result.tokensReasoning != null && result.tokensOut != null && result.tokensReasoning > result.tokensOut) result.usageDiagnostics!.push('reasoning-exceeds-output');
|
|
334
|
+
for (const [key, value] of Object.entries(usage)) if (key.endsWith('_tokens') && count(value) === null) result.usageDiagnostics!.push('invalid-counter:' + key);
|
|
335
|
+
result.tokensSource = 'codex-json'; result.reportedTotalBasis = 'raw-inclusive'; result.inputCacheSemantics = 'includes-cache-read-write';
|
|
336
|
+
result.usageSource = { schema: 'codex-exec-json', scope: 'terminal-turn', threadId, turnId: typeof terminal['turn_id'] === 'string' ? terminal['turn_id'] : null, receiptId: null };
|
|
337
|
+
} else malformed = true;
|
|
338
|
+
}
|
|
339
|
+
if (malformed) result.usageDiagnostics!.push('malformed-event-stream');
|
|
340
|
+
if (failedTerminal) result.usageDiagnostics!.push('runtime-turn-failed');
|
|
341
|
+
if (reportedModels.size > 1) result.usageDiagnostics!.push('provider-model-mismatch');
|
|
342
|
+
return { structured: true, valid: !malformed && !failedTerminal, text, usage: result, reportedModel: reportedModels.size === 1 ? [...reportedModels][0]! : null };
|
|
257
343
|
}
|
|
258
344
|
|
|
259
345
|
// ── the adapter factories (pure over the injected ChildRunner) ───────────────────────────────────
|
|
@@ -264,6 +350,7 @@ function failed(family: BridgeFamily, model: string | null, wallMs: number, reas
|
|
|
264
350
|
text: null,
|
|
265
351
|
family,
|
|
266
352
|
modelUsed: model,
|
|
353
|
+
modelProvenance: 'probed-request',
|
|
267
354
|
wallMs,
|
|
268
355
|
tokensIn: null,
|
|
269
356
|
tokensOut: null,
|
|
@@ -314,25 +401,15 @@ export function makeCodexExecDispatcher(run: ChildRunner, opts?: { bin?: string;
|
|
|
314
401
|
cwd: req.cwd,
|
|
315
402
|
detached: true,
|
|
316
403
|
});
|
|
404
|
+
const parsed = codexStructuredOutput(String(r.stdout ?? ''));
|
|
405
|
+
const usage = parsed.structured ? parsed.usage : extractCodexTokens(r.stderr);
|
|
317
406
|
const cls = classifyChildRun(r);
|
|
318
|
-
if (cls.kind === 'timeout') return failed('openai', req.resolvedModelId, req.timeoutMs, 'dispatch-timeout', `the ${req.timeoutMs}ms deadline fired on step ${req.stepId}`);
|
|
319
|
-
if (cls.kind === 'dead') return failed('openai', req.resolvedModelId, clock() - t0, 'dispatch-dead', cls.detail);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
const tokens = extractCodexTokens(r.stderr);
|
|
326
|
-
return {
|
|
327
|
-
outcome: 'ok',
|
|
328
|
-
text,
|
|
329
|
-
family: 'openai',
|
|
330
|
-
modelUsed: req.resolvedModelId,
|
|
331
|
-
wallMs: clock() - t0,
|
|
332
|
-
tokensIn: tokens.tokensIn,
|
|
333
|
-
tokensOut: tokens.tokensOut,
|
|
334
|
-
tokensSource: tokens.tokensIn === null && tokens.tokensOut === null ? null : 'codex-stderr',
|
|
335
|
-
};
|
|
407
|
+
if (cls.kind === 'timeout') return { ...failed('openai', req.resolvedModelId, req.timeoutMs, 'dispatch-timeout', `the ${req.timeoutMs}ms deadline fired on step ${req.stepId}`), ...usage };
|
|
408
|
+
if (cls.kind === 'dead') return { ...failed('openai', req.resolvedModelId, clock() - t0, 'dispatch-dead', cls.detail), ...usage };
|
|
409
|
+
if (cls.exitCode !== 0 || !parsed.valid || parsed.text === '') return { ...failed('openai', req.resolvedModelId, clock() - t0, 'dispatch-dead', 'Codex exited nonzero, had an invalid structured event stream, or was spawned-but-mute with no final text'), ...usage };
|
|
410
|
+
return { outcome: 'ok', text: parsed.text, family: 'openai', modelUsed: parsed.reportedModel ?? req.resolvedModelId,
|
|
411
|
+
modelProvenance: parsed.reportedModel ? 'provider-reported' : 'probed-request', wallMs: clock() - t0, ...usage };
|
|
412
|
+
|
|
336
413
|
},
|
|
337
414
|
};
|
|
338
415
|
}
|
|
@@ -380,33 +457,15 @@ export function makeClaudePDispatcher(run: ChildRunner, opts?: { bin?: string; i
|
|
|
380
457
|
cwd: fileMode ? req.cwd : isolatedCwd(),
|
|
381
458
|
detached: true,
|
|
382
459
|
});
|
|
460
|
+
const usage = extractClaudeUsage(r.stdout);
|
|
383
461
|
const cls = classifyChildRun(r);
|
|
384
|
-
if (cls.kind === 'timeout') return failed('claude', req.resolvedModelId, req.timeoutMs, 'dispatch-timeout', `the ${req.timeoutMs}ms deadline fired on step ${req.stepId}`);
|
|
385
|
-
if (cls.kind === 'dead') return failed('claude', req.resolvedModelId, clock() - t0, 'dispatch-dead', cls.detail);
|
|
386
|
-
|
|
387
|
-
// from a process that exited nonzero is a CONTRADICTION, not a success: the runtime told us
|
|
388
|
-
// twice and the two answers differ, so believing the friendlier one is how a failed dispatch
|
|
389
|
-
// becomes a green step. The codex adapter already required exit 0; this one did not.
|
|
390
|
-
if (cls.exitCode !== 0) {
|
|
391
|
-
return failed('claude', req.resolvedModelId, clock() - t0, 'dispatch-dead',
|
|
392
|
-
`claude exited ${cls.exitCode} — a nonzero exit is a failed dispatch even when stdout carries a parseable success envelope; the two disagree and the exit code is the runtime's own verdict`);
|
|
393
|
-
}
|
|
462
|
+
if (cls.kind === 'timeout') return { ...failed('claude', req.resolvedModelId, req.timeoutMs, 'dispatch-timeout', `the ${req.timeoutMs}ms deadline fired on step ${req.stepId}`), ...usage };
|
|
463
|
+
if (cls.kind === 'dead') return { ...failed('claude', req.resolvedModelId, clock() - t0, 'dispatch-dead', cls.detail), ...usage };
|
|
464
|
+
if (cls.exitCode !== 0) return { ...failed('claude', req.resolvedModelId, clock() - t0, 'dispatch-dead', `claude exited ${cls.exitCode} — a nonzero exit is a failed dispatch even with a success envelope`), ...usage };
|
|
394
465
|
const env = extractClaudeResult(r.stdout);
|
|
395
|
-
if (!env.ok) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
const usage = extractClaudeUsage(r.stdout);
|
|
400
|
-
return {
|
|
401
|
-
outcome: 'ok',
|
|
402
|
-
text: env.text,
|
|
403
|
-
family: 'claude',
|
|
404
|
-
modelUsed: req.resolvedModelId,
|
|
405
|
-
wallMs: clock() - t0,
|
|
406
|
-
tokensIn: usage.tokensIn,
|
|
407
|
-
tokensOut: usage.tokensOut,
|
|
408
|
-
tokensSource: usage.tokensIn === null && usage.tokensOut === null ? null : 'claude-envelope',
|
|
409
|
-
};
|
|
466
|
+
if (!env.ok) return { ...failed('claude', req.resolvedModelId, clock() - t0, 'dispatch-dead', `reply is not readable as a result envelope: ${env.detail}`), ...usage };
|
|
467
|
+
return { outcome: 'ok', text: env.text, family: 'claude', modelUsed: req.resolvedModelId, modelProvenance: 'probed-request', wallMs: clock() - t0, ...usage };
|
|
468
|
+
|
|
410
469
|
},
|
|
411
470
|
};
|
|
412
471
|
}
|
package/src/workflow-run.ts
CHANGED
|
@@ -164,7 +164,8 @@ export interface WfRunState {
|
|
|
164
164
|
dispatcherOverride?: boolean;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
export interface WfBudgetRow {
|
|
167
|
+
export interface WfBudgetRow extends Pick<DispatchResult, 'tokensTotal' | 'tokensCacheRead' | 'tokensCacheWrite' | 'tokensReasoning' | 'reportedTotalBasis' | 'inputCacheSemantics' | 'reportedCostUsd' | 'usageDiagnostics' | 'usageSource' | 'totalDerivation'> {
|
|
168
|
+
projectRoot?: string;
|
|
168
169
|
schema: typeof WF_BUDGET_ROW_SCHEMA;
|
|
169
170
|
kind: 'stage' | 'probe';
|
|
170
171
|
runId: string;
|
|
@@ -175,10 +176,17 @@ export interface WfBudgetRow {
|
|
|
175
176
|
attempt: number | null;
|
|
176
177
|
family: BridgeFamily;
|
|
177
178
|
model: string | null;
|
|
179
|
+
modelProvenance?: string;
|
|
180
|
+
requestedModel?: string | null;
|
|
178
181
|
wallMs: number;
|
|
179
182
|
tokensIn: number | null;
|
|
180
183
|
tokensOut: number | null;
|
|
181
|
-
tokensSource: '
|
|
184
|
+
tokensSource: DispatchResult['tokensSource'];
|
|
185
|
+
phase?: string | null;
|
|
186
|
+
role?: string | null;
|
|
187
|
+
tier?: string | null;
|
|
188
|
+
mode?: string | null;
|
|
189
|
+
estimate?: unknown;
|
|
182
190
|
outcome: 'ok' | 'null' | 'error' | null;
|
|
183
191
|
timeoutMs: number | null;
|
|
184
192
|
}
|
|
@@ -239,6 +247,7 @@ export interface RunnerInputs {
|
|
|
239
247
|
wallClockExtraMs: number | null;
|
|
240
248
|
runnerVersion: string;
|
|
241
249
|
cwdRoot: string;
|
|
250
|
+
projectRoot?: string;
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
/** Small, dependency-free 64-bit FNV — the same shape the checkpoint plane uses, kept local so this
|
|
@@ -1063,6 +1072,7 @@ export async function runWorkflow(inputs: RunnerInputs, pre: PreflightOk, deps:
|
|
|
1063
1072
|
schema: WF_BUDGET_ROW_SCHEMA,
|
|
1064
1073
|
kind: 'probe',
|
|
1065
1074
|
runId: inputs.runId,
|
|
1075
|
+
projectRoot: inputs.projectRoot ?? inputs.cwdRoot,
|
|
1066
1076
|
dispatchSeq: null,
|
|
1067
1077
|
stepId: null,
|
|
1068
1078
|
itemKey: null,
|
|
@@ -1073,6 +1083,14 @@ export async function runWorkflow(inputs: RunnerInputs, pre: PreflightOk, deps:
|
|
|
1073
1083
|
tokensIn: null,
|
|
1074
1084
|
tokensOut: null,
|
|
1075
1085
|
tokensSource: null,
|
|
1086
|
+
tokensTotal: null,
|
|
1087
|
+
tokensCacheRead: null,
|
|
1088
|
+
tokensCacheWrite: null,
|
|
1089
|
+
tokensReasoning: null,
|
|
1090
|
+
reportedTotalBasis: 'unknown',
|
|
1091
|
+
inputCacheSemantics: 'unknown',
|
|
1092
|
+
reportedCostUsd: null,
|
|
1093
|
+
usageDiagnostics: ['probe-usage-not-recorded'],
|
|
1076
1094
|
outcome: null,
|
|
1077
1095
|
timeoutMs: null,
|
|
1078
1096
|
});
|
|
@@ -1500,16 +1518,34 @@ async function dispatchOnce(
|
|
|
1500
1518
|
schema: WF_BUDGET_ROW_SCHEMA,
|
|
1501
1519
|
kind: 'stage',
|
|
1502
1520
|
runId: ctx.inputs.runId,
|
|
1521
|
+
projectRoot: ctx.inputs.projectRoot ?? ctx.inputs.cwdRoot,
|
|
1503
1522
|
dispatchSeq,
|
|
1504
1523
|
stepId: spec.stepId,
|
|
1505
1524
|
itemKey,
|
|
1506
1525
|
attempt,
|
|
1507
1526
|
family,
|
|
1508
|
-
model: res.modelUsed
|
|
1527
|
+
model: res.modelUsed,
|
|
1528
|
+
requestedModel: model,
|
|
1529
|
+
modelProvenance: res.modelProvenance ?? (res.modelUsed === null ? 'not-recorded' : 'dispatcher-reported'),
|
|
1509
1530
|
wallMs,
|
|
1510
1531
|
tokensIn: res.tokensIn,
|
|
1511
1532
|
tokensOut: res.tokensOut,
|
|
1512
1533
|
tokensSource: res.tokensSource,
|
|
1534
|
+
tokensTotal: res.tokensTotal ?? null,
|
|
1535
|
+
totalDerivation: res.totalDerivation ?? 'not-recorded',
|
|
1536
|
+
tokensCacheRead: res.tokensCacheRead ?? null,
|
|
1537
|
+
tokensCacheWrite: res.tokensCacheWrite ?? null,
|
|
1538
|
+
tokensReasoning: res.tokensReasoning ?? null,
|
|
1539
|
+
reportedTotalBasis: res.reportedTotalBasis ?? 'unknown',
|
|
1540
|
+
inputCacheSemantics: res.inputCacheSemantics ?? 'unknown',
|
|
1541
|
+
reportedCostUsd: res.reportedCostUsd ?? null,
|
|
1542
|
+
usageDiagnostics: res.usageDiagnostics ?? [],
|
|
1543
|
+
...(res.usageSource !== undefined ? { usageSource: res.usageSource } : {}),
|
|
1544
|
+
phase: spec.phase,
|
|
1545
|
+
role: spec.role ?? null,
|
|
1546
|
+
tier: spec.tier ?? null,
|
|
1547
|
+
mode: spec.mode ?? null,
|
|
1548
|
+
estimate: spec.estimate ?? null,
|
|
1513
1549
|
outcome,
|
|
1514
1550
|
timeoutMs,
|
|
1515
1551
|
});
|
|
@@ -1699,7 +1735,7 @@ function appendLedger(ctx: RunCtx, outcome: string): void {
|
|
|
1699
1735
|
outcome,
|
|
1700
1736
|
date: null,
|
|
1701
1737
|
});
|
|
1702
|
-
if (line !== null) ctx.deps.store.appendLedgerLine(line);
|
|
1738
|
+
if (line !== null) ctx.deps.store.appendLedgerLine(JSON.stringify({ ...JSON.parse(line), summary: true, sourceProjection: 'workflow-budget', workflowRunId: ctx.inputs.runId }));
|
|
1703
1739
|
}
|
|
1704
1740
|
|
|
1705
1741
|
/** PAUSE — flush WITHOUT `run.closed` (parity with the render's top-level terminal return, which
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Заполнение пустых полей стоимости в леджере прогонов ИЗ ЗАПИСИ ХОСТА.
|
|
3
|
-
*
|
|
4
|
-
* ЗАЧЕМ. Запись 3c026d5d: стоимость фичи на три четверти набита руками. ИЗМЕРЕНО 2026-09-19 по
|
|
5
|
-
* живому `.dz/feature-adr/run-cost-ledger.jsonl`: 393 строки, из них 293 без числа токенов.
|
|
6
|
-
* Числа при этом НЕ пропали — для прогонов Workflow они лежат в записи хоста
|
|
7
|
-
* `~/.claude/projects/<проект>/<сессия>/workflows/wf_<runId>.json`, где есть `totalTokens`,
|
|
8
|
-
* `durationMs` и `agentCount`. Песочница воркфлоу их видеть не может (у неё нет файловой системы
|
|
9
|
-
* и нет уведомления о завершении), поэтому соединение делается СНАРУЖИ и ПОСЛЕ прогона.
|
|
10
|
-
*
|
|
11
|
-
* ЧЕСТНАЯ ДОСЯГАЕМОСТЬ, и она меньше, чем подразумевает запись. Из 293 пустых строк:
|
|
12
|
-
* • 75 — прогоны Workflow (`auto:true`); только их и может достать это соединение,
|
|
13
|
-
* и лишь 25 из них несут `runId`, по которому запись хоста находится СЕГОДНЯ;
|
|
14
|
-
* • 218 — ручные круги ведущего, для которых записи хоста НЕ СУЩЕСТВУЕТ ПО ПОСТРОЕНИЮ:
|
|
15
|
-
* Workflow не запускался, и стоимость этим путём не восстановима в принципе.
|
|
16
|
-
* То есть механизм закрывает не «три четверти», а максимум 26% разрыва, а сегодня 9%.
|
|
17
|
-
* Остальное — задача ПИСАТЕЛЯ (проставлять пригодный `runId`) и отдельный вопрос про то,
|
|
18
|
-
* чем мерить ручные круги.
|
|
19
|
-
*
|
|
20
|
-
* ПРОВЕНАНС ОБЯЗАТЕЛЕН. Заполненное поле помечается `costSource: 'derived'`, потому что
|
|
21
|
-
* выведенное число и набранное руками — разные по доверию величины, и строка, где их нельзя
|
|
22
|
-
* различить, обесценивает обе.
|
|
23
|
-
*/
|
|
24
|
-
/** Стоимость прогона, вычитанная из записи хоста. */
|
|
25
|
-
export interface DerivedRunCost {
|
|
26
|
-
readonly tokens: number;
|
|
27
|
-
readonly minutes: number;
|
|
28
|
-
readonly agents: number | null;
|
|
29
|
-
}
|
|
30
|
-
/** Строка леджера в той части, которая нас касается. */
|
|
31
|
-
export interface LedgerCostRow {
|
|
32
|
-
readonly runId?: unknown;
|
|
33
|
-
readonly tokens?: unknown;
|
|
34
|
-
readonly minutes?: unknown;
|
|
35
|
-
readonly agents?: unknown;
|
|
36
|
-
readonly costSource?: unknown;
|
|
37
|
-
readonly [k: string]: unknown;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Прочитать стоимость из записи хоста. `null`, если запись не несёт обязательных чисел —
|
|
41
|
-
* частичная запись НЕ даёт частичного заполнения: половина выведенной строки хуже пустой,
|
|
42
|
-
* потому что выглядит как измерение.
|
|
43
|
-
*/
|
|
44
|
-
export declare function readHostRunCost(record: unknown): DerivedRunCost | null;
|
|
45
|
-
/**
|
|
46
|
-
* Заполнить ТОЛЬКО пустые поля. Уже набранное руками число НИКОГДА не перетирается: оно могло
|
|
47
|
-
* быть исправлено человеком по другим основаниям, и выведенное значение не имеет права его
|
|
48
|
-
* вытеснять. Возвращает `null`, если заполнять нечего — чтобы вызывающий не переписывал файл зря.
|
|
49
|
-
*/
|
|
50
|
-
export declare function fillLedgerCost(row: LedgerCostRow, derived: DerivedRunCost): LedgerCostRow | null;
|
|
51
|
-
/** Три исхода на строку — и «нечего заполнять» отделено от «не нашли запись». */
|
|
52
|
-
export type FillOutcome = 'filled' | 'nothing-to-fill' | 'no-host-record';
|
|
53
|
-
/** Решение по одной строке: чем она разрешается и почему. */
|
|
54
|
-
export declare function decideFill(row: LedgerCostRow, record: unknown): {
|
|
55
|
-
outcome: FillOutcome;
|
|
56
|
-
next: LedgerCostRow | null;
|
|
57
|
-
};
|
|
58
|
-
//# sourceMappingURL=ledger-cost-fill.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ledger-cost-fill.d.ts","sourceRoot":"","sources":["../src/ledger-cost-fill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/B;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAUtE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,aAAa,GAAG,IAAI,CAUhG;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE1E,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAA;CAAE,CAKpH"}
|
package/dist/ledger-cost-fill.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Заполнение пустых полей стоимости в леджере прогонов ИЗ ЗАПИСИ ХОСТА.
|
|
3
|
-
*
|
|
4
|
-
* ЗАЧЕМ. Запись 3c026d5d: стоимость фичи на три четверти набита руками. ИЗМЕРЕНО 2026-09-19 по
|
|
5
|
-
* живому `.dz/feature-adr/run-cost-ledger.jsonl`: 393 строки, из них 293 без числа токенов.
|
|
6
|
-
* Числа при этом НЕ пропали — для прогонов Workflow они лежат в записи хоста
|
|
7
|
-
* `~/.claude/projects/<проект>/<сессия>/workflows/wf_<runId>.json`, где есть `totalTokens`,
|
|
8
|
-
* `durationMs` и `agentCount`. Песочница воркфлоу их видеть не может (у неё нет файловой системы
|
|
9
|
-
* и нет уведомления о завершении), поэтому соединение делается СНАРУЖИ и ПОСЛЕ прогона.
|
|
10
|
-
*
|
|
11
|
-
* ЧЕСТНАЯ ДОСЯГАЕМОСТЬ, и она меньше, чем подразумевает запись. Из 293 пустых строк:
|
|
12
|
-
* • 75 — прогоны Workflow (`auto:true`); только их и может достать это соединение,
|
|
13
|
-
* и лишь 25 из них несут `runId`, по которому запись хоста находится СЕГОДНЯ;
|
|
14
|
-
* • 218 — ручные круги ведущего, для которых записи хоста НЕ СУЩЕСТВУЕТ ПО ПОСТРОЕНИЮ:
|
|
15
|
-
* Workflow не запускался, и стоимость этим путём не восстановима в принципе.
|
|
16
|
-
* То есть механизм закрывает не «три четверти», а максимум 26% разрыва, а сегодня 9%.
|
|
17
|
-
* Остальное — задача ПИСАТЕЛЯ (проставлять пригодный `runId`) и отдельный вопрос про то,
|
|
18
|
-
* чем мерить ручные круги.
|
|
19
|
-
*
|
|
20
|
-
* ПРОВЕНАНС ОБЯЗАТЕЛЕН. Заполненное поле помечается `costSource: 'derived'`, потому что
|
|
21
|
-
* выведенное число и набранное руками — разные по доверию величины, и строка, где их нельзя
|
|
22
|
-
* различить, обесценивает обе.
|
|
23
|
-
*/
|
|
24
|
-
const isFiniteNumber = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
25
|
-
/**
|
|
26
|
-
* Прочитать стоимость из записи хоста. `null`, если запись не несёт обязательных чисел —
|
|
27
|
-
* частичная запись НЕ даёт частичного заполнения: половина выведенной строки хуже пустой,
|
|
28
|
-
* потому что выглядит как измерение.
|
|
29
|
-
*/
|
|
30
|
-
export function readHostRunCost(record) {
|
|
31
|
-
if (record === null || typeof record !== 'object')
|
|
32
|
-
return null;
|
|
33
|
-
const r = record;
|
|
34
|
-
if (!isFiniteNumber(r['totalTokens']) || !isFiniteNumber(r['durationMs']))
|
|
35
|
-
return null;
|
|
36
|
-
if (r['totalTokens'] < 0 || r['durationMs'] < 0)
|
|
37
|
-
return null;
|
|
38
|
-
return {
|
|
39
|
-
tokens: r['totalTokens'],
|
|
40
|
-
minutes: Math.round(r['durationMs'] / 60_000),
|
|
41
|
-
agents: isFiniteNumber(r['agentCount']) ? r['agentCount'] : null,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Заполнить ТОЛЬКО пустые поля. Уже набранное руками число НИКОГДА не перетирается: оно могло
|
|
46
|
-
* быть исправлено человеком по другим основаниям, и выведенное значение не имеет права его
|
|
47
|
-
* вытеснять. Возвращает `null`, если заполнять нечего — чтобы вызывающий не переписывал файл зря.
|
|
48
|
-
*/
|
|
49
|
-
export function fillLedgerCost(row, derived) {
|
|
50
|
-
const next = { ...row };
|
|
51
|
-
let filled = false;
|
|
52
|
-
if (!isFiniteNumber(row['tokens'])) {
|
|
53
|
-
next['tokens'] = derived.tokens;
|
|
54
|
-
filled = true;
|
|
55
|
-
}
|
|
56
|
-
if (!isFiniteNumber(row['minutes'])) {
|
|
57
|
-
next['minutes'] = derived.minutes;
|
|
58
|
-
filled = true;
|
|
59
|
-
}
|
|
60
|
-
if (!isFiniteNumber(row['agents']) && derived.agents !== null) {
|
|
61
|
-
next['agents'] = derived.agents;
|
|
62
|
-
filled = true;
|
|
63
|
-
}
|
|
64
|
-
if (!filled)
|
|
65
|
-
return null;
|
|
66
|
-
// Провенанс ставится ТОЛЬКО когда что-то действительно заполнено.
|
|
67
|
-
next['costSource'] = 'derived';
|
|
68
|
-
return next;
|
|
69
|
-
}
|
|
70
|
-
/** Решение по одной строке: чем она разрешается и почему. */
|
|
71
|
-
export function decideFill(row, record) {
|
|
72
|
-
const derived = record === undefined ? null : readHostRunCost(record);
|
|
73
|
-
if (derived === null)
|
|
74
|
-
return { outcome: 'no-host-record', next: null };
|
|
75
|
-
const next = fillLedgerCost(row, derived);
|
|
76
|
-
return next === null ? { outcome: 'nothing-to-fill', next: null } : { outcome: 'filled', next };
|
|
77
|
-
}
|
|
78
|
-
//# sourceMappingURL=ledger-cost-fill.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ledger-cost-fill.js","sourceRoot":"","sources":["../src/ledger-cost-fill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAmBH,MAAM,cAAc,GAAG,CAAC,CAAU,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEhG;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,CAAC,GAAG,MAAiC,CAAC;IAC5C,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACvF,IAAI,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,CAAC,CAAC,aAAa,CAAC;QACxB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC;QAC7C,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI;KACjE,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAkB,EAAE,OAAuB;IACxE,MAAM,IAAI,GAA4B,EAAE,GAAG,GAAG,EAAE,CAAC;IACjD,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC;IAAC,CAAC;IACvF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QAAC,IAAI,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC;IAAC,CAAC;IAC1F,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC;IAAC,CAAC;IAClH,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,kEAAkE;IAClE,IAAI,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IAC/B,OAAO,IAAqB,CAAC;AAC/B,CAAC;AAKD,6DAA6D;AAC7D,MAAM,UAAU,UAAU,CAAC,GAAkB,EAAE,MAAe;IAC5D,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IACtE,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACvE,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAClG,CAAC"}
|