@deksden-com/dd-flow-cli 0.7.0 → 0.8.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.
- package/CHANGELOG.md +666 -0
- package/README.md +7 -2
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +88 -10
- package/dist/cli/run-cli.js +523 -28
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/domain/validation.js +1 -1
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/compatibility.schema.json +32 -0
- package/dist/schemas/flow-contract.schema.json +9 -5
- package/dist/schemas/flow-run.schema.json +16 -123
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/stage-start-response.schema.json +4 -2
- package/dist/schemas/status-report.schema.json +76 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +10 -2
- package/dist/services/code-checks.js +244 -0
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +12 -12
- package/dist/services/engines.js +1 -1
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +774 -18
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -1
- package/dist/services/merge-queue.js +5 -5
- package/dist/services/merge-worker.js +2 -2
- package/dist/services/migrations.js +2 -2
- package/dist/services/plan-runtime.js +1 -1
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +17 -11
- package/dist/services/protocols.js +8 -8
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +504 -51
- package/dist/services/schema-validation.js +21 -3
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +198 -75
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +305 -0
- package/dist/services/vnext-code.js +686 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +552 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +522 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +263 -34
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
package/dist/services/usage.js
CHANGED
|
@@ -1,8 +1,66 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { parseJsonObject } from "../shared/json.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
6
|
+
export function ingestZcodeUsage(context, input) {
|
|
7
|
+
return ingestHarnessUsage(context, input, { harness: "zcode-acp", label: "ZCode", idPrefix: "ZUSG", sourceKind: "zcode_session_usage_v1" });
|
|
8
|
+
}
|
|
9
|
+
/** Store a Grok Build ACP usage snapshot. Root snapshots may include child usage. */
|
|
10
|
+
export function ingestGrokUsage(context, input) {
|
|
11
|
+
return ingestHarnessUsage(context, input, { harness: "grok-acp", label: "Grok Build", idPrefix: "GUSG", sourceKind: "grok_session_usage_v1" });
|
|
12
|
+
}
|
|
13
|
+
/** Store an OpenCode cumulative Session usage snapshot. */
|
|
14
|
+
export function ingestOpenCodeUsage(context, input) {
|
|
15
|
+
return ingestHarnessUsage(context, input, { harness: "opencode-server", label: "OpenCode", idPrefix: "OUSG", sourceKind: "opencode_session_usage_v1" });
|
|
16
|
+
}
|
|
17
|
+
/** Store an Antigravity CLI cumulative conversation usage snapshot. */
|
|
18
|
+
export function ingestAgyUsage(context, input) {
|
|
19
|
+
return ingestHarnessUsage(context, input, { harness: "antigravity-cli", label: "Antigravity", idPrefix: "AUSG", sourceKind: "antigravity_cli_session_usage_v1" });
|
|
20
|
+
}
|
|
21
|
+
function ingestHarnessUsage(context, input, harness) {
|
|
22
|
+
const payload = parseJsonObject(input.stdin || "{}", `${harness.label} usage snapshot`);
|
|
23
|
+
const usage = object(payload.usage);
|
|
24
|
+
const providerSessionId = stringValue(payload.provider_session_id) ?? stringValue(usage?.sessionId);
|
|
25
|
+
if (!usage || !providerSessionId)
|
|
26
|
+
throw new Error(`${harness.label} usage snapshot requires usage and provider_session_id`);
|
|
27
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
28
|
+
registerProject(context, { root: projectRoot });
|
|
29
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
30
|
+
const observedAt = stringValue(payload.observed_at) ?? context.now();
|
|
31
|
+
const requestUsageMeasured = stringValue(usage.requestUsageStatus) === "measured";
|
|
32
|
+
const total = tokenField(usage, requestUsageMeasured ? ["requestTotalTokens"] : ["totalTokens", "total_tokens"]);
|
|
33
|
+
if (total === undefined || total === null)
|
|
34
|
+
throw new Error(`${harness.label} usage snapshot has no total token count`);
|
|
35
|
+
const toolUsage = object(payload.tool_calls);
|
|
36
|
+
const values = {
|
|
37
|
+
total,
|
|
38
|
+
input: tokenField(usage, requestUsageMeasured ? ["requestInputTokens"] : ["inputTokens", "input_tokens"]) ?? null,
|
|
39
|
+
cacheRead: tokenField(usage, requestUsageMeasured ? ["requestCacheReadTokens"] : ["cacheReadTokens", "cachedReadTokens", "cacheReadInputTokens", "cache_read_input_tokens"]) ?? null,
|
|
40
|
+
cacheWrite: tokenField(usage, requestUsageMeasured ? ["requestCacheCreationTokens"] : ["cacheCreationTokens", "cacheCreationInputTokens", "cache_write_input_tokens"]) ?? null,
|
|
41
|
+
output: tokenField(usage, requestUsageMeasured ? ["requestOutputTokens"] : ["outputTokens", "output_tokens"]) ?? null,
|
|
42
|
+
reasoning: tokenField(usage, requestUsageMeasured ? ["requestReasoningTokens"] : ["reasoningTokens", "reasoning_output_tokens"]) ?? null,
|
|
43
|
+
requests: tokenField(usage, requestUsageMeasured ? ["requestCount"] : ["modelRequestCount", "modelCalls", "request_count"]) ?? null,
|
|
44
|
+
errors: tokenField(usage, ["modelErrorCount", "error_count"]) ?? null,
|
|
45
|
+
toolCalls: toolUsage ? tokenField(toolUsage, ["total"]) ?? null : null,
|
|
46
|
+
toolFailures: toolUsage ? tokenField(toolUsage, ["failures"]) ?? null : null,
|
|
47
|
+
toolByName: object(toolUsage?.by_tool) ?? object(toolUsage?.by_name) ?? null
|
|
48
|
+
};
|
|
49
|
+
const id = `${harness.idPrefix}-${crypto.createHash("sha256").update(JSON.stringify({ project: project.id, providerSessionId, observedAt, values })).digest("hex")}`;
|
|
50
|
+
const inserted = context.db.run(`INSERT OR IGNORE INTO harness_usage_snapshots
|
|
51
|
+
(id, project_id, harness, provider_session_id, daemon_id, observed_at, total_tokens, input_tokens,
|
|
52
|
+
cache_read_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens,
|
|
53
|
+
request_count, error_count, tool_calls, tool_failures, tool_by_name_json, source_kind, usage_scope, completeness, created_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, project.id, harness.harness, providerSessionId, stringValue(payload.daemon_id) ?? null, observedAt, values.total,
|
|
55
|
+
values.input, values.cacheRead, values.cacheWrite, values.output, values.reasoning, values.requests, values.errors,
|
|
56
|
+
values.toolCalls, values.toolFailures, values.toolByName ? JSON.stringify(values.toolByName) : null, harness.sourceKind,
|
|
57
|
+
stringValue(payload.usage_scope) ?? "physical_session", stringValue(payload.completeness) ?? "complete", context.now()]);
|
|
58
|
+
return { ok: true, observed: inserted.changes === 1, duplicate: inserted.changes === 0, snapshot_id: id, provider_session_id: providerSessionId, harness: harness.harness };
|
|
59
|
+
}
|
|
3
60
|
export function checkpointSessionUsage(context, session, input) {
|
|
4
61
|
const observedAt = context.now();
|
|
5
62
|
const result = readCodexTranscript(session.transcript_path, session.session_id);
|
|
63
|
+
const source = sourceFacts(session.transcript_path);
|
|
6
64
|
const snapshot = {
|
|
7
65
|
id: `USG-${crypto.randomUUID()}`,
|
|
8
66
|
...session,
|
|
@@ -19,6 +77,10 @@ export function checkpointSessionUsage(context, session, input) {
|
|
|
19
77
|
output_tokens: result.counter?.usage.output_tokens ?? null,
|
|
20
78
|
reasoning_output_tokens: result.counter?.usage.reasoning_output_tokens ?? null,
|
|
21
79
|
source_kind: "codex_transcript_v1",
|
|
80
|
+
source_locator: source.locator,
|
|
81
|
+
source_sha256: source.sha256,
|
|
82
|
+
source_size_bytes: source.size,
|
|
83
|
+
source_mtime_ms: source.mtime,
|
|
22
84
|
token_event_at: result.counter?.token_event_at ?? null,
|
|
23
85
|
turn_id: result.counter?.turn_id ?? null,
|
|
24
86
|
turn_attribution: result.counter?.turn_attribution ?? "unavailable",
|
|
@@ -27,23 +89,25 @@ export function checkpointSessionUsage(context, session, input) {
|
|
|
27
89
|
diagnostic_code: result.diagnostic ?? null
|
|
28
90
|
};
|
|
29
91
|
const previous = context.db.get(`SELECT s.*, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
|
|
30
|
-
FROM
|
|
31
|
-
LEFT JOIN
|
|
92
|
+
FROM usage s
|
|
93
|
+
LEFT JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
32
94
|
WHERE s.project_id = ? AND s.run_id = ? AND s.session_id = ? AND s.checkpoint = ?
|
|
33
95
|
ORDER BY s.observed_at DESC, s.id DESC LIMIT 1`, [session.project_id, session.run_id, session.session_id, input.checkpoint]);
|
|
34
96
|
if (previous && sameUsageSnapshot(previous, snapshot)) {
|
|
35
97
|
return snapshotForOutput(previous);
|
|
36
98
|
}
|
|
37
|
-
context.db.run(`INSERT INTO
|
|
99
|
+
context.db.run(`INSERT INTO usage
|
|
38
100
|
(id, project_id, run_id, session_id, checkpoint, stage, stage_attempt, observed_at,
|
|
39
101
|
total_tokens, input_tokens, cached_input_tokens, cache_read_input_tokens, cache_write_input_tokens, uncached_input_tokens,
|
|
40
102
|
output_tokens, reasoning_output_tokens,
|
|
41
|
-
source_kind,
|
|
42
|
-
|
|
103
|
+
source_kind, source_locator, source_sha256, source_size_bytes, source_mtime_ms,
|
|
104
|
+
token_event_at, turn_id, turn_attribution, parser_version, extraction_status, diagnostic_code, created_at)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
43
106
|
snapshot.id, snapshot.project_id, snapshot.run_id, snapshot.session_id, snapshot.checkpoint, snapshot.stage,
|
|
44
107
|
snapshot.stage_attempt, snapshot.observed_at, snapshot.total_tokens, snapshot.input_tokens,
|
|
45
108
|
snapshot.cached_input_tokens, snapshot.cache_read_input_tokens, snapshot.cache_write_input_tokens,
|
|
46
109
|
snapshot.uncached_input_tokens, snapshot.output_tokens, snapshot.reasoning_output_tokens, snapshot.source_kind,
|
|
110
|
+
snapshot.source_locator, snapshot.source_sha256, snapshot.source_size_bytes, snapshot.source_mtime_ms,
|
|
47
111
|
snapshot.token_event_at, snapshot.turn_id, snapshot.turn_attribution, snapshot.parser_version,
|
|
48
112
|
snapshot.extraction_status, snapshot.diagnostic_code, observedAt
|
|
49
113
|
]);
|
|
@@ -51,22 +115,400 @@ export function checkpointSessionUsage(context, session, input) {
|
|
|
51
115
|
}
|
|
52
116
|
export function checkpointRunUsage(context, input) {
|
|
53
117
|
const sessions = context.db.all(`SELECT session_id, project_id, run_id, transcript_path, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
54
|
-
FROM
|
|
118
|
+
FROM sessions WHERE project_id = ? AND run_id = ?`, [input.projectId, input.runId]);
|
|
55
119
|
return sessions.map((session) => checkpointSessionUsage(context, session, input));
|
|
56
120
|
}
|
|
57
121
|
export function usageForRun(context, input) {
|
|
122
|
+
return usageReport(context, input, true);
|
|
123
|
+
}
|
|
124
|
+
/** Reads saved checkpoints only; reporting must not create a new measurement. */
|
|
125
|
+
export function storedUsageForRun(context, input) {
|
|
126
|
+
return usageReport(context, input, false);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The vNext controller owns this operation. It rereads source transcripts and
|
|
130
|
+
* replaces the current RUN projection; it does not build a checkpoint ledger.
|
|
131
|
+
*/
|
|
132
|
+
export function recalculateRunUsage(context, input) {
|
|
133
|
+
const observedAt = context.now();
|
|
134
|
+
const runRow = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
135
|
+
const runCompletedAt = runRow ? terminalTimestamp(runRow.index_json) : null;
|
|
136
|
+
const accountingAt = input.stage?.completedAt ?? runCompletedAt ?? observedAt;
|
|
137
|
+
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.session_id, ws.created_at, ws.completed_at,
|
|
138
|
+
w.project_id, s.harness, s.transcript_path, s.parent_session_id, s.agent_id, s.provider_session_id
|
|
139
|
+
FROM work_sessions ws
|
|
140
|
+
JOIN works w ON w.work_id = ws.work_id
|
|
141
|
+
LEFT JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
142
|
+
WHERE w.project_id = ? AND w.run_id = ?
|
|
143
|
+
ORDER BY ws.created_at, ws.id`, [input.projectId, input.runId]).filter((link) => (!input.sessionId || link.session_id === input.sessionId) && (!input.stage || overlapsStage(link, input.stage, observedAt)))
|
|
144
|
+
.map((link) => input.stage ? clipToStage(link, input.stage, observedAt) : link);
|
|
145
|
+
if (input.sessionId && links.length === 0)
|
|
146
|
+
throw new Error(`Session is not associated with RUN: ${input.sessionId}`);
|
|
147
|
+
const final = !context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND status IN ('created', 'running') LIMIT 1", [input.projectId, input.runId]);
|
|
148
|
+
const rows = usageRowsForPhysicalSessions(context, links, accountingAt, input.stage);
|
|
149
|
+
const totals = emptyUsage();
|
|
150
|
+
for (const row of rows)
|
|
151
|
+
if (row.tokens)
|
|
152
|
+
addUsage(totals, row.tokens);
|
|
153
|
+
const settledSessions = settledRunSessions(context, links, observedAt);
|
|
154
|
+
const unsettledSessions = [...new Set(links.map((link) => link.session_id))].filter((sessionId) => !settledSessions.some((session) => session.session_id === sessionId));
|
|
155
|
+
const usageFinal = final && unsettledSessions.length === 0;
|
|
156
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
157
|
+
try {
|
|
158
|
+
context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
context.db.run(`INSERT INTO usage (id, project_id, run_id, session_id, checkpoint, stage, stage_attempt, observed_at, total_tokens, input_tokens, cached_input_tokens, cache_read_input_tokens, cache_write_input_tokens, uncached_input_tokens, output_tokens, reasoning_output_tokens, source_kind, source_locator, source_sha256, source_size_bytes, source_mtime_ms, token_event_at, turn_id, turn_attribution, parser_version, extraction_status, diagnostic_code, created_at)
|
|
161
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 'windowed_work_session', 1, ?, ?, ?)`, [
|
|
162
|
+
`USG-${crypto.randomUUID()}`, input.projectId, input.runId, row.session_id, usageFinal ? "final" : "provisional", input.stage?.name ?? null, row.observed_at,
|
|
163
|
+
row.tokens?.total_tokens ?? null, row.tokens?.input_tokens ?? null, row.tokens?.cache_read_input_tokens ?? null,
|
|
164
|
+
row.tokens?.cache_read_input_tokens ?? null, row.tokens?.cache_write_input_tokens ?? null, row.tokens?.uncached_input_tokens ?? null,
|
|
165
|
+
row.tokens?.output_tokens ?? null, row.tokens?.reasoning_output_tokens ?? null, row.source_kind, row.source.locator, row.source.sha256, row.source.size, row.source.mtime,
|
|
166
|
+
row.status, row.diagnostic, observedAt
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
for (const session of settledSessions) {
|
|
170
|
+
context.db.run(`UPDATE sessions
|
|
171
|
+
SET status = 'stopped', stop_reason = 'provider_turn_complete', updated_at = ?, stopped_at = ?
|
|
172
|
+
WHERE project_id = ? AND run_id = ? AND session_id = ?
|
|
173
|
+
AND status = 'idle'
|
|
174
|
+
AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')`, [observedAt, session.completed_at, input.projectId, input.runId, session.session_id, session.session_id]);
|
|
175
|
+
}
|
|
176
|
+
context.db.exec("COMMIT");
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
context.db.exec("ROLLBACK");
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
schema_id: "dd-flow/run-usage@2",
|
|
185
|
+
run_id: input.runId,
|
|
186
|
+
status: usageFinal ? "final" : "provisional",
|
|
187
|
+
scope: input.stage ? { kind: "stage", stage: input.stage.name, started_at: input.stage.startedAt, completed_at: input.stage.completedAt } : { kind: "run" },
|
|
188
|
+
observed_at: observedAt,
|
|
189
|
+
totals: outputUsageTotals(totals),
|
|
190
|
+
sessions: rows,
|
|
191
|
+
session_reconciliation: {
|
|
192
|
+
stopped: settledSessions.map((session) => session.session_id),
|
|
193
|
+
unsettled: unsettledSessions
|
|
194
|
+
},
|
|
195
|
+
tool_calls: toolCallsForLinks(context, links, accountingAt),
|
|
196
|
+
coverage: rows.reduce((coverage, row) => ({ ...coverage, [row.status]: (coverage[row.status] ?? 0) + 1 }), {})
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function overlapsStage(link, stage, observedAt) {
|
|
200
|
+
const linkStart = Date.parse(link.created_at);
|
|
201
|
+
const linkEnd = Date.parse(link.completed_at ?? observedAt);
|
|
202
|
+
const stageStart = Date.parse(stage.startedAt);
|
|
203
|
+
const stageEnd = Date.parse(stage.completedAt ?? observedAt);
|
|
204
|
+
return (!Number.isFinite(linkEnd) || !Number.isFinite(stageStart) || linkEnd >= stageStart)
|
|
205
|
+
&& (!Number.isFinite(linkStart) || !Number.isFinite(stageEnd) || linkStart <= stageEnd);
|
|
206
|
+
}
|
|
207
|
+
function clipToStage(link, stage, observedAt) {
|
|
208
|
+
const startedAt = Date.parse(link.created_at) < Date.parse(stage.startedAt) ? stage.startedAt : link.created_at;
|
|
209
|
+
const naturalEnd = link.completed_at ?? observedAt;
|
|
210
|
+
const stageEnd = stage.completedAt ?? observedAt;
|
|
211
|
+
const completedAt = Date.parse(naturalEnd) > Date.parse(stageEnd) ? stageEnd : naturalEnd;
|
|
212
|
+
return { ...link, created_at: startedAt, completed_at: completedAt };
|
|
213
|
+
}
|
|
214
|
+
function toolCallsForLinks(context, links, observedAt) {
|
|
215
|
+
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
216
|
+
const sessions = new Map();
|
|
217
|
+
for (const link of links)
|
|
218
|
+
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
219
|
+
for (const [sessionId, windows] of sessions) {
|
|
220
|
+
const first = windows[0];
|
|
221
|
+
const parsed = isExternalHarness(first.harness)
|
|
222
|
+
? readHarnessToolCallsWindow(context, first.harness, first.project_id, first.provider_session_id, windows.map((item) => item.created_at).sort()[0], windows.map((item) => item.completed_at ?? observedAt).sort().at(-1), observedAt)
|
|
223
|
+
: readToolCallsForWindows(first.transcript_path, sessionId, windows, observedAt);
|
|
224
|
+
if (!parsed) {
|
|
225
|
+
result.unavailable_sessions.push(sessionId);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
result.observed_sessions += 1;
|
|
229
|
+
result.total += parsed.total;
|
|
230
|
+
result.failures += parsed.failures;
|
|
231
|
+
for (const [name, count] of Object.entries(parsed.by_tool))
|
|
232
|
+
result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
|
|
233
|
+
}
|
|
234
|
+
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length ? "partial" : "measured";
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function isExternalHarness(harness) { return harness === "zcode-acp" || harness === "grok-acp" || harness === "opencode-server" || harness === "antigravity-cli"; }
|
|
238
|
+
function harnessSourceKind(harness) {
|
|
239
|
+
return harness === "grok-acp" ? "grok_session_usage_v1" : harness === "zcode-acp" ? "zcode_session_usage_v1" : harness === "antigravity-cli" ? "antigravity_cli_session_usage_v1" : "opencode_session_usage_v1";
|
|
240
|
+
}
|
|
241
|
+
function readHarnessToolCallsWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
|
|
242
|
+
if (!providerSessionId)
|
|
243
|
+
return null;
|
|
244
|
+
const snapshots = context.db.all(`SELECT observed_at, tool_calls, tool_failures, tool_by_name_json FROM harness_usage_snapshots
|
|
245
|
+
WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
|
|
246
|
+
ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
|
|
247
|
+
const last = snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt))
|
|
248
|
+
?? snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(endedAt)).at(-1);
|
|
249
|
+
const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
|
|
250
|
+
if (!last || !before || last === before || last.tool_calls === null || before.tool_calls === null)
|
|
251
|
+
return null;
|
|
252
|
+
const parse = (value) => {
|
|
253
|
+
if (!value)
|
|
254
|
+
return {};
|
|
255
|
+
try {
|
|
256
|
+
return Object.fromEntries(Object.entries(JSON.parse(value)).filter((entry) => typeof entry[1] === "number"));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const latestByTool = parse(last.tool_by_name_json);
|
|
263
|
+
const baselineByTool = parse(before.tool_by_name_json);
|
|
264
|
+
const byTool = {};
|
|
265
|
+
for (const name of Object.keys(latestByTool).sort()) {
|
|
266
|
+
const count = Math.max(0, latestByTool[name] - (baselineByTool[name] ?? 0));
|
|
267
|
+
if (count > 0)
|
|
268
|
+
byTool[name] = count;
|
|
269
|
+
}
|
|
270
|
+
return { total: Math.max(0, last.tool_calls - before.tool_calls), failures: Math.max(0, (last.tool_failures ?? 0) - (before.tool_failures ?? 0)), by_tool: byTool };
|
|
271
|
+
}
|
|
272
|
+
function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, observedAt) {
|
|
273
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
274
|
+
return null;
|
|
275
|
+
try {
|
|
276
|
+
let sessionId;
|
|
277
|
+
let total = 0;
|
|
278
|
+
let failures = 0;
|
|
279
|
+
const byTool = {};
|
|
280
|
+
for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
|
|
281
|
+
if (!line.trim())
|
|
282
|
+
continue;
|
|
283
|
+
let event;
|
|
284
|
+
try {
|
|
285
|
+
event = JSON.parse(line);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const payload = object(event.payload);
|
|
291
|
+
if (event.type === "session_meta")
|
|
292
|
+
sessionId ??= stringValue(payload?.id);
|
|
293
|
+
if (event.type !== "response_item" || !payload)
|
|
294
|
+
continue;
|
|
295
|
+
const eventAt = stringValue(event.timestamp);
|
|
296
|
+
if (eventAt && !windows.some((window) => eventInsideWindow(eventAt, window, observedAt)))
|
|
297
|
+
continue;
|
|
298
|
+
const type = stringValue(payload.type);
|
|
299
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
300
|
+
const name = stringValue(payload.name) ?? "unknown";
|
|
301
|
+
byTool[name] = (byTool[name] ?? 0) + 1;
|
|
302
|
+
total += 1;
|
|
303
|
+
}
|
|
304
|
+
if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
|
|
305
|
+
failures += 1;
|
|
306
|
+
}
|
|
307
|
+
if (sessionId && sessionId !== expectedSessionId)
|
|
308
|
+
return null;
|
|
309
|
+
return { total, failures, by_tool: Object.fromEntries(Object.entries(byTool).sort(([left], [right]) => left.localeCompare(right))) };
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function eventInsideWindow(eventAt, window, observedAt) {
|
|
316
|
+
const at = Date.parse(eventAt);
|
|
317
|
+
const start = Date.parse(window.created_at);
|
|
318
|
+
const end = Date.parse(window.completed_at ?? observedAt);
|
|
319
|
+
return Number.isFinite(at) && (!Number.isFinite(start) || at >= start) && (!Number.isFinite(end) || at <= end);
|
|
320
|
+
}
|
|
321
|
+
function outputIndicatesFailure(payload) {
|
|
322
|
+
if (payload.error === true || typeof payload.error === "string")
|
|
323
|
+
return true;
|
|
324
|
+
return containsFailure(payload.output);
|
|
325
|
+
}
|
|
326
|
+
function containsFailure(value) {
|
|
327
|
+
if (Array.isArray(value))
|
|
328
|
+
return value.some(containsFailure);
|
|
329
|
+
if (typeof value === "string") {
|
|
330
|
+
if (/(?:^|\n)exit=(?:[1-9]\d*)\b|Process exited with code [1-9]\d*/.test(value))
|
|
331
|
+
return true;
|
|
332
|
+
const trimmed = value.trim();
|
|
333
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
334
|
+
try {
|
|
335
|
+
return containsFailure(JSON.parse(trimmed));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const item = object(value);
|
|
344
|
+
if (!item)
|
|
345
|
+
return false;
|
|
346
|
+
if (item.ok === false || item.is_error === true || item.isError === true || item.error === true || typeof item.error === "string")
|
|
347
|
+
return true;
|
|
348
|
+
if (typeof item.exit_code === "number" && item.exit_code !== 0)
|
|
349
|
+
return true;
|
|
350
|
+
return Object.values(item).some(containsFailure);
|
|
351
|
+
}
|
|
352
|
+
/** RUN accounting belongs to the physical provider Session, not to every Work link. */
|
|
353
|
+
function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
354
|
+
const sessions = new Map();
|
|
355
|
+
for (const link of links)
|
|
356
|
+
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
357
|
+
const physicalSessions = [...sessions.values()].filter((sessionLinks) => {
|
|
358
|
+
const link = sessionLinks[0];
|
|
359
|
+
if (!link.parent_session_id)
|
|
360
|
+
return true;
|
|
361
|
+
const parent = links.find((candidate) => candidate.session_id === link.parent_session_id);
|
|
362
|
+
return !parent || !hasInclusiveUsageSnapshot(context, parent.project_id, parent.harness, parent.provider_session_id, observedAt);
|
|
363
|
+
});
|
|
364
|
+
return physicalSessions.map((sessionLinks) => {
|
|
365
|
+
const link = sessionLinks[0];
|
|
366
|
+
const startedAt = stage?.startedAt ?? sessionLinks.map((item) => item.created_at).sort()[0];
|
|
367
|
+
const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, link.session_id);
|
|
368
|
+
const naturalEnd = isExternalHarness(link.harness)
|
|
369
|
+
? observedAt
|
|
370
|
+
: completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
|
|
371
|
+
const endedAt = stage?.completedAt ?? (Date.parse(naturalEnd) > Date.parse(observedAt) ? observedAt : naturalEnd);
|
|
372
|
+
const harnessUsage = isExternalHarness(link.harness)
|
|
373
|
+
? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
|
|
374
|
+
: null;
|
|
375
|
+
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, link.session_id, startedAt, endedAt);
|
|
376
|
+
return {
|
|
377
|
+
work_session_id: link.id,
|
|
378
|
+
work_id: link.work_id,
|
|
379
|
+
session_id: link.session_id,
|
|
380
|
+
provider_session_id: link.provider_session_id,
|
|
381
|
+
agent_id: link.agent_id,
|
|
382
|
+
parent_session_id: link.parent_session_id,
|
|
383
|
+
status: parsed.status,
|
|
384
|
+
diagnostic: parsed.diagnostic ?? null,
|
|
385
|
+
observed_at: observedAt,
|
|
386
|
+
tokens: parsed.counter?.usage ?? null,
|
|
387
|
+
source: harnessUsage ? { locator: link.provider_session_id ? `${link.harness}:${link.provider_session_id}` : null, sha256: null, size: null, mtime: null } : sourceFacts(link.transcript_path),
|
|
388
|
+
source_kind: harnessUsage ? harnessSourceKind(link.harness) : "codex_transcript_v1"
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function hasInclusiveUsageSnapshot(context, projectId, harness, providerSessionId, observedAt) {
|
|
393
|
+
if (!providerSessionId)
|
|
394
|
+
return false;
|
|
395
|
+
return Boolean(context.db.get("SELECT 1 FROM harness_usage_snapshots WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND usage_scope = 'execution_tree_inclusive' AND observed_at <= ? LIMIT 1", [projectId, harness, providerSessionId, observedAt]));
|
|
396
|
+
}
|
|
397
|
+
function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
|
|
398
|
+
if (!providerSessionId)
|
|
399
|
+
return { status: "not_observable", diagnostic: "provider_session_id_missing" };
|
|
400
|
+
const snapshots = context.db.all(`SELECT observed_at, total_tokens, input_tokens, cache_read_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens
|
|
401
|
+
FROM harness_usage_snapshots
|
|
402
|
+
WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
|
|
403
|
+
ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
|
|
404
|
+
const last = snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt))
|
|
405
|
+
?? snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(endedAt)).at(-1);
|
|
406
|
+
const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
|
|
407
|
+
if (!last || !before || last === before)
|
|
408
|
+
return { status: "not_yet_emitted", diagnostic: `${harness}_usage_window_incomplete` };
|
|
409
|
+
const delta = (value, baseline) => value === null || baseline === null ? null : Math.max(0, value - baseline);
|
|
410
|
+
const input = delta(last.input_tokens, before.input_tokens);
|
|
411
|
+
const cacheRead = delta(last.cache_read_input_tokens, before.cache_read_input_tokens);
|
|
412
|
+
return {
|
|
413
|
+
status: "measured",
|
|
414
|
+
counter: {
|
|
415
|
+
usage: {
|
|
416
|
+
total_tokens: Math.max(0, last.total_tokens - before.total_tokens),
|
|
417
|
+
input_tokens: input,
|
|
418
|
+
cache_read_input_tokens: cacheRead,
|
|
419
|
+
cache_write_input_tokens: delta(last.cache_write_input_tokens, before.cache_write_input_tokens),
|
|
420
|
+
uncached_input_tokens: input === null || cacheRead === null ? null : Math.max(0, input - cacheRead),
|
|
421
|
+
output_tokens: delta(last.output_tokens, before.output_tokens),
|
|
422
|
+
reasoning_output_tokens: delta(last.reasoning_output_tokens, before.reasoning_output_tokens)
|
|
423
|
+
},
|
|
424
|
+
token_event_at: last.observed_at,
|
|
425
|
+
turn_id: null,
|
|
426
|
+
turn_attribution: "unavailable"
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function terminalTimestamp(indexJson) {
|
|
431
|
+
try {
|
|
432
|
+
const index = JSON.parse(indexJson);
|
|
433
|
+
return ["done", "blocked", "cancelled", "failed"].includes(index.status ?? "") && typeof index.completed_at === "string" ? index.completed_at : null;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function settledRunSessions(context, links, observedAt) {
|
|
440
|
+
const sessions = new Map();
|
|
441
|
+
for (const link of links)
|
|
442
|
+
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
443
|
+
return [...sessions.entries()].flatMap(([sessionId, sessionLinks]) => {
|
|
444
|
+
if (sessionLinks.some((link) => !link.completed_at))
|
|
445
|
+
return [];
|
|
446
|
+
const link = sessionLinks[0];
|
|
447
|
+
const latestWorkFinish = sessionLinks.map((item) => Date.parse(item.completed_at)).filter(Number.isFinite).sort((left, right) => right - left)[0];
|
|
448
|
+
if (isExternalHarness(link.harness) && link.provider_session_id && latestWorkFinish !== undefined) {
|
|
449
|
+
const snapshot = context.db.get(`SELECT observed_at FROM harness_usage_snapshots
|
|
450
|
+
WHERE project_id = ? AND harness = ? AND provider_session_id = ?
|
|
451
|
+
AND observed_at >= ? AND observed_at <= ?
|
|
452
|
+
ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id, new Date(latestWorkFinish).toISOString(), observedAt]);
|
|
453
|
+
return snapshot ? [{ session_id: sessionId, completed_at: snapshot.observed_at }] : [];
|
|
454
|
+
}
|
|
455
|
+
const completed = completedProviderTurn(sessionLinks[0].transcript_path, sessionId);
|
|
456
|
+
if (!completed || (latestWorkFinish !== undefined && Date.parse(completed.completed_at) < latestWorkFinish))
|
|
457
|
+
return [];
|
|
458
|
+
return [{ session_id: sessionId, completed_at: completed.completed_at }];
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/** The final lifecycle event must be task_complete; a later task_started means the Session is live again. */
|
|
462
|
+
function completedProviderTurn(transcriptPath, expectedSessionId) {
|
|
463
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
464
|
+
return null;
|
|
465
|
+
try {
|
|
466
|
+
let sessionId;
|
|
467
|
+
let latestLifecycle = null;
|
|
468
|
+
for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
|
|
469
|
+
if (!line.trim())
|
|
470
|
+
continue;
|
|
471
|
+
let event;
|
|
472
|
+
try {
|
|
473
|
+
event = JSON.parse(line);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const payload = object(event.payload);
|
|
479
|
+
if (event.type === "session_meta")
|
|
480
|
+
sessionId ??= stringValue(payload?.id);
|
|
481
|
+
const type = event.type === "event_msg" ? stringValue(payload?.type) : null;
|
|
482
|
+
const at = stringValue(event.timestamp);
|
|
483
|
+
if ((type === "task_started" || type === "task_complete") && at)
|
|
484
|
+
latestLifecycle = { type, at };
|
|
485
|
+
}
|
|
486
|
+
if (sessionId && sessionId !== expectedSessionId)
|
|
487
|
+
return null;
|
|
488
|
+
return latestLifecycle?.type === "task_complete" ? { completed_at: latestLifecycle.at } : null;
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function usageReport(context, input, refresh) {
|
|
58
495
|
const sessions = context.db.all(`SELECT DISTINCT session_id, project_id, ? AS run_id, transcript_path, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
59
|
-
FROM
|
|
496
|
+
FROM sessions WHERE project_id = ? AND run_id = ?
|
|
60
497
|
UNION
|
|
61
498
|
SELECT DISTINCT f.session_id, f.project_id, ? AS run_id, f.transcript_path, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
|
|
62
|
-
FROM flow_session_segments s JOIN
|
|
499
|
+
FROM flow_session_segments s JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
63
500
|
WHERE s.project_id = ? AND s.run_id = ?`, [input.runId, input.projectId, input.runId, input.runId, input.projectId, input.runId]);
|
|
64
|
-
const
|
|
501
|
+
const selectedSessions = input.sessionId ? sessions.filter((session) => session.session_id === input.sessionId) : sessions;
|
|
502
|
+
if (input.sessionId && selectedSessions.length === 0) {
|
|
503
|
+
throw new Error(`Session is not associated with RUN: ${input.sessionId}`);
|
|
504
|
+
}
|
|
505
|
+
const refreshed = refresh ? selectedSessions.map((session) => checkpointSessionUsage(context, session, { checkpoint: "manual_sync" })) : [];
|
|
65
506
|
const rows = context.db.all(`SELECT s.*, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
|
|
66
|
-
FROM
|
|
67
|
-
LEFT JOIN
|
|
507
|
+
FROM usage s
|
|
508
|
+
LEFT JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
68
509
|
WHERE s.project_id = ? AND s.run_id = ? ORDER BY s.session_id, s.observed_at, s.id`, [input.projectId, input.runId]);
|
|
69
|
-
const
|
|
510
|
+
const selectedRows = input.sessionId ? rows.filter((row) => row.session_id === input.sessionId) : rows;
|
|
511
|
+
const deltas = usageDeltas(selectedRows);
|
|
70
512
|
const segments = context.db.all(`SELECT id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name
|
|
71
513
|
FROM flow_session_segments WHERE project_id = ? AND run_id = ? ORDER BY started_at, id`, [input.projectId, input.runId]);
|
|
72
514
|
const supported = new Set(["session", "role", "aspect", "plan-item", "stage", "protocol"]);
|
|
@@ -87,13 +529,14 @@ export function usageForRun(context, input) {
|
|
|
87
529
|
ok: true,
|
|
88
530
|
schema_id: "dd-flow/run-usage@1",
|
|
89
531
|
run_id: input.runId,
|
|
90
|
-
source: { kind: "codex_transcript_v1", parser_version: 1, refreshed_sessions: refreshed.length },
|
|
91
|
-
sessions:
|
|
532
|
+
source: { kind: "codex_transcript_v1", parser_version: 1, refreshed_sessions: refreshed.length, checkpoint_source: refresh ? "refreshed" : "stored" },
|
|
533
|
+
sessions: selectedSessions.map((session) => ({
|
|
92
534
|
session_id: session.session_id, parent_session_id: session.parent_session_id ?? null, role: session.role ?? null,
|
|
93
535
|
aspect_id: session.aspect_id ?? null, plan_item_id: session.plan_item_id ?? null, session_kind: session.session_kind ?? null
|
|
94
536
|
})),
|
|
95
537
|
groups: [...groups.entries()].map(([key, group]) => ({ key, ...group, tokens: outputUsageTotals(group.tokens) })),
|
|
96
538
|
deltas,
|
|
539
|
+
tool_calls: toolCallsForSessions(sessions),
|
|
97
540
|
segments: segments.map((segment) => ({
|
|
98
541
|
segment_id: segment.id,
|
|
99
542
|
session_id: segment.session_id,
|
|
@@ -101,11 +544,70 @@ export function usageForRun(context, input) {
|
|
|
101
544
|
protocol_id: segment.protocol_id,
|
|
102
545
|
started_at: segment.started_at,
|
|
103
546
|
ended_at: segment.ended_at,
|
|
104
|
-
usage: segmentUsage(context,
|
|
547
|
+
usage: segmentUsage(context, selectedSessions.find((session) => session.session_id === segment.session_id), segment)
|
|
105
548
|
})),
|
|
106
|
-
coverage: coverageForRows(
|
|
549
|
+
coverage: coverageForRows(selectedRows)
|
|
107
550
|
};
|
|
108
551
|
}
|
|
552
|
+
function toolCallsForSessions(sessions) {
|
|
553
|
+
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
554
|
+
for (const session of sessions) {
|
|
555
|
+
const parsed = readToolCalls(session.transcript_path, session.session_id);
|
|
556
|
+
if (!parsed) {
|
|
557
|
+
result.unavailable_sessions.push(session.session_id);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
result.observed_sessions += 1;
|
|
561
|
+
result.total += parsed.total;
|
|
562
|
+
result.failures += parsed.failures;
|
|
563
|
+
for (const [name, count] of Object.entries(parsed.by_tool))
|
|
564
|
+
result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
|
|
565
|
+
}
|
|
566
|
+
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length === 0 ? "measured" : "partial";
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
function readToolCalls(transcriptPath, expectedSessionId) {
|
|
570
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
571
|
+
return null;
|
|
572
|
+
try {
|
|
573
|
+
let sessionId;
|
|
574
|
+
const byTool = {};
|
|
575
|
+
let total = 0;
|
|
576
|
+
let failures = 0;
|
|
577
|
+
for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
|
|
578
|
+
if (!line.trim())
|
|
579
|
+
continue;
|
|
580
|
+
let event;
|
|
581
|
+
try {
|
|
582
|
+
event = JSON.parse(line);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const payload = object(event.payload);
|
|
588
|
+
if (!payload)
|
|
589
|
+
continue;
|
|
590
|
+
if (event.type === "session_meta")
|
|
591
|
+
sessionId = stringValue(payload.id) ?? sessionId;
|
|
592
|
+
if (event.type !== "response_item")
|
|
593
|
+
continue;
|
|
594
|
+
const type = stringValue(payload.type);
|
|
595
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
596
|
+
const name = stringValue(payload.name) ?? "unknown";
|
|
597
|
+
byTool[name] = (byTool[name] ?? 0) + 1;
|
|
598
|
+
total += 1;
|
|
599
|
+
}
|
|
600
|
+
if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
|
|
601
|
+
failures += 1;
|
|
602
|
+
}
|
|
603
|
+
if (sessionId && sessionId !== expectedSessionId)
|
|
604
|
+
return null;
|
|
605
|
+
return { total, failures, by_tool: Object.fromEntries(Object.entries(byTool).sort(([left], [right]) => left.localeCompare(right))) };
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
109
611
|
function segmentUsage(context, session, segment) {
|
|
110
612
|
if (!session)
|
|
111
613
|
return { status: "unavailable", diagnostic: "session_record_missing" };
|
|
@@ -134,7 +636,7 @@ function readCodexTranscript(transcriptPath, expectedSessionId) {
|
|
|
134
636
|
}
|
|
135
637
|
const payload = object(event.payload);
|
|
136
638
|
if (event.type === "session_meta")
|
|
137
|
-
sessionId
|
|
639
|
+
sessionId ??= stringValue(payload?.id);
|
|
138
640
|
if (event.type === "event_msg" && payload?.type === "task_started")
|
|
139
641
|
nearestTurn = stringValue(payload.turn_id) ?? nearestTurn;
|
|
140
642
|
if (event.type !== "event_msg" || payload?.type !== "token_count")
|
|
@@ -195,7 +697,7 @@ function readTranscriptEvents(transcriptPath, expectedSessionId) {
|
|
|
195
697
|
}
|
|
196
698
|
const payload = object(event.payload);
|
|
197
699
|
if (event.type === "session_meta")
|
|
198
|
-
sessionId
|
|
700
|
+
sessionId ??= stringValue(payload?.id);
|
|
199
701
|
if (event.type === "event_msg" && payload?.type === "task_started")
|
|
200
702
|
nearestTurn = stringValue(payload.turn_id) ?? nearestTurn;
|
|
201
703
|
if (event.type !== "event_msg" || payload?.type !== "token_count")
|
|
@@ -293,6 +795,12 @@ function snapshotForOutput(snapshot) {
|
|
|
293
795
|
delete safe.cached_input_tokens;
|
|
294
796
|
return { schema_id: "dd-flow/session-usage-snapshot@1", ...safe };
|
|
295
797
|
}
|
|
798
|
+
function sourceFacts(file) {
|
|
799
|
+
if (!file || !fs.existsSync(file))
|
|
800
|
+
return { locator: file, sha256: null, size: null, mtime: null };
|
|
801
|
+
const stat = fs.statSync(file);
|
|
802
|
+
return { locator: file, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"), size: stat.size, mtime: Math.trunc(stat.mtimeMs) };
|
|
803
|
+
}
|
|
296
804
|
function sameUsageSnapshot(previous, current) {
|
|
297
805
|
return previous.stage === current.stage
|
|
298
806
|
&& previous.stage_attempt === current.stage_attempt
|