fix(runs): count a model run once when the terminal node carries its result on
ci / check (push) Successful in 1m35s
ci / publish (push) Successful in 1m33s

A terminal node's result is the record it received, so after post_upgrade the run
listed the same post-upgrade check twice and counted its cost twice (run view, run
list and model costs). Identical calls now count once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
homelab-updaterandClaude Opus 5.5 committed 2026-10-07 11:31:30 +02:00
1 parent d05b6c27f3
commit bcd144fbdc
2 files changed
+25 -2

No files matched your search

+14
View File
@@ -27,4 +27,18 @@ describe('workflowModelCalls', () => {
expect(calls[2]).toMatchObject({ instanceId: 'run-1', workflowId: 'update-radarr', summary: 'Clean start.', actions: ['hl ps'], at: '2026-09-22T21:35:00.000Z' });
expect(totalCostUsd(calls)).toBeCloseTo(0.82);
});
it('counts a model run once when a later node carries its result on, as the terminal node does', () => {
const postUpgrade = { action: 'model.postUpgrade', state: 'completed', report: { outcome: 'nothing_to_do', summary: 'Clean start.', actions: ['hl ps'], model: 'sonnet', costUsd: 0.2 } };
const notes = { verdict: 'attention', summary: 'A migration runs.', model: 'opus', costUsd: 0.12 };
const calls = workflowModelCalls({ id: 'run-1', workflowId: 'update-radarr' }, [
attempt(1, 'release_notes', notes),
attempt(2, 'gate', notes),
attempt(3, 'post_upgrade', postUpgrade),
attempt(4, 'terminal_updated', postUpgrade),
]);
expect(calls.map((call) => call.nodeId)).toEqual(['release_notes', 'post_upgrade']);
expect(totalCostUsd(calls)).toBeCloseTo(0.32);
});
});
+11 -2
View File
@@ -22,9 +22,19 @@ const cost = (value: unknown): number => (typeof value === 'number' && Number.is
/**
* The model runs a workflow run made, read from its node attempts: a model action's result carries `report`
* (app/privileged-action-executor-adapter), the release notes operation's result carries `verdict` and `model`.
* A node that passes the record on (a terminal node's result is its input) repeats that result; the same call counts once.
*/
export function workflowModelCalls(instance: { readonly id: string; readonly workflowId: string }, attempts: readonly NodeAttempt[]): WorkflowModelCall[] {
return attempts.flatMap((attempt): WorkflowModelCall[] => {
const seen = new Set<string>();
return attempts.flatMap((attempt) => modelCallOf(instance, attempt)).filter(({ nodeId: _nodeId, at: _at, ...call }) => {
const key = JSON.stringify(call);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function modelCallOf(instance: { readonly id: string; readonly workflowId: string }, attempt: NodeAttempt): WorkflowModelCall[] {
const result = record(attempt.result);
const at = attempt.finishedAt ?? attempt.startedAt;
const base = { instanceId: instance.id, workflowId: instance.workflowId, nodeId: attempt.nodeId, at };
@@ -47,7 +57,6 @@ export function workflowModelCalls(instance: { readonly id: string; readonly wor
}];
}
return [];
});
}
export function totalCostUsd(calls: readonly WorkflowModelCall[]): number {