@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
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 +526 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -1,14 +1,129 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
3
6
|
import { writeJsonAtomic } from "../storage/paths.js";
|
|
4
7
|
/** Deterministic JSON -> Markdown/HTML projection shared by every stage. */
|
|
5
8
|
export function writeStageReport(root, report) {
|
|
6
9
|
const json = path.join(root, "stage-report.json");
|
|
7
10
|
const markdown = path.join(root, "stage-report.md");
|
|
8
11
|
const html = path.join(root, "stage-report.html");
|
|
12
|
+
const normalized = normalizeStageReport(report);
|
|
13
|
+
writeJsonAtomic(json, normalized);
|
|
14
|
+
const text = renderStageReportMarkdown(normalized);
|
|
15
|
+
fs.writeFileSync(markdown, text);
|
|
16
|
+
fs.writeFileSync(html, renderStageReportHtml(normalized, text));
|
|
17
|
+
return { json, markdown, html };
|
|
18
|
+
}
|
|
19
|
+
export function prepareStageReport(report) {
|
|
20
|
+
const normalized = normalizeStageReport(report);
|
|
21
|
+
const json = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
22
|
+
const markdown = renderStageReportMarkdown(normalized);
|
|
23
|
+
const html = renderStageReportHtml(normalized, markdown);
|
|
24
|
+
return { normalized, bytes: { json, markdown, html }, sha256: crypto.createHash("sha256").update(json).digest("hex") };
|
|
25
|
+
}
|
|
26
|
+
export function prepareStagePacket(file, bytes) {
|
|
27
|
+
if (fs.existsSync(file) && !bytes.equals(fs.readFileSync(file)))
|
|
28
|
+
throw new AppError("stage_packet_conflict", "An existing Stage packet differs from prepared bytes", 2, { file });
|
|
29
|
+
return { file, bytes, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
30
|
+
}
|
|
31
|
+
export function acceptStagePacket(context, input) {
|
|
32
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
33
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
34
|
+
const attempt = index?.stage_runs?.find((item) => item.stage === input.stage)?.attempt;
|
|
35
|
+
if (!attempt)
|
|
36
|
+
throw new AppError("stage_packet_owner_missing", "Stage packet has no owning attempt", 1, { run_id: input.runId, stage: input.stage });
|
|
37
|
+
const payload = { run_id: input.runId, stage: input.stage, attempt, file: input.prepared.file, sha256: input.prepared.sha256, base64: input.prepared.bytes.toString("base64") };
|
|
38
|
+
context.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.stage_packet', 0, ?, ?)", [input.projectId, JSON.stringify(payload), context.now()]);
|
|
39
|
+
context.db.afterCommit(() => publishStagePacket(context, input.projectId, input.runId, input.stage, input.prepared.file));
|
|
40
|
+
}
|
|
41
|
+
export function publishStagePacket(context, projectId, runId, stage, file) {
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
withProjectionLock(`${file}.publish-lock`, () => {
|
|
44
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
45
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
46
|
+
const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
|
|
47
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
|
|
48
|
+
const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && item.file === file);
|
|
49
|
+
if (!packet)
|
|
50
|
+
return;
|
|
51
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
52
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== packet.sha256)
|
|
53
|
+
throw new AppError("stage_packet_invalid", "Stored Stage packet hash does not match its bytes", 1, { file });
|
|
54
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(file, bytes, { flag: "wx" });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== "EEXIST")
|
|
60
|
+
throw error;
|
|
61
|
+
if (!bytes.equals(fs.readFileSync(file)))
|
|
62
|
+
throw new AppError("stage_packet_conflict", "Accepted Stage packet differs from the published file", 1, { file, run_id: runId, stage, attempt: currentAttempt });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** SQL receipt and exact bytes are committed with the owning Stage transition. */
|
|
67
|
+
export function acceptStageReport(context, input) {
|
|
68
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
69
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
70
|
+
const attempt = index?.stage_runs?.find((item) => item.stage === input.stage)?.attempt;
|
|
71
|
+
if (!attempt)
|
|
72
|
+
throw new AppError("stage_report_owner_missing", "Stage report has no owning attempt", 1, { run_id: input.runId, stage: input.stage });
|
|
73
|
+
const payload = { run_id: input.runId, stage: input.stage, attempt, root: input.root, sha256: input.prepared.sha256, bytes: input.prepared.bytes };
|
|
74
|
+
context.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.stage_report', 0, ?, ?)", [input.projectId, JSON.stringify(payload), context.now()]);
|
|
75
|
+
context.db.afterCommit(() => publishStageReport(context, input.projectId, input.runId, input.stage, input.root));
|
|
76
|
+
}
|
|
77
|
+
/** Replays the latest committed receipt, never an obsolete attempt. */
|
|
78
|
+
export function publishStageReport(context, projectId, runId, stage, root) {
|
|
79
|
+
withProjectionLock(`${root}.report-publish-lock`, () => {
|
|
80
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
81
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
82
|
+
const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
|
|
83
|
+
const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
|
|
84
|
+
const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.root === root && item.attempt === currentAttempt);
|
|
85
|
+
if (!receipt)
|
|
86
|
+
return;
|
|
87
|
+
fs.mkdirSync(root, { recursive: true });
|
|
88
|
+
for (const [name, bytes] of Object.entries(receipt.bytes)) {
|
|
89
|
+
const file = path.join(root, `stage-report.${name === "json" ? "json" : name === "markdown" ? "md" : "html"}`);
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(file, bytes, { flag: "wx" });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== "EEXIST")
|
|
95
|
+
throw error;
|
|
96
|
+
if (fs.readFileSync(file, "utf8") !== bytes)
|
|
97
|
+
throw new AppError("stage_report_conflict", "Accepted Stage report differs from the published file", 1, { run_id: runId, stage, attempt: currentAttempt, file });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Admission repair for predecessor reports; the stage being retried is archived separately. */
|
|
103
|
+
export function reconcileRunStageReports(context, projectId, runId, exceptStage) {
|
|
104
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const receipt = JSON.parse(row.payload_json);
|
|
108
|
+
if (receipt.run_id !== runId || typeof receipt.stage !== "string" || typeof receipt.root !== "string" || receipt.stage === exceptStage || seen.has(receipt.stage))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(receipt.stage);
|
|
111
|
+
publishStageReport(context, projectId, runId, receipt.stage, receipt.root);
|
|
112
|
+
}
|
|
113
|
+
const packets = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
|
|
114
|
+
const packetFiles = new Set();
|
|
115
|
+
for (const row of packets) {
|
|
116
|
+
const packet = JSON.parse(row.payload_json);
|
|
117
|
+
if (packet.run_id !== runId || typeof packet.stage !== "string" || typeof packet.file !== "string" || packet.stage === exceptStage || packetFiles.has(packet.file))
|
|
118
|
+
continue;
|
|
119
|
+
packetFiles.add(packet.file);
|
|
120
|
+
publishStagePacket(context, projectId, runId, packet.stage, packet.file);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function normalizeStageReport(report) {
|
|
9
124
|
const semantic = object(report.semantic);
|
|
10
125
|
const sourceArtifacts = object(report.artifacts);
|
|
11
|
-
|
|
126
|
+
return {
|
|
12
127
|
...report,
|
|
13
128
|
schema_id: "dd-flow/stage-report@2",
|
|
14
129
|
summary: string(report.summary) || string(semantic.result) || "Stage completed.",
|
|
@@ -27,11 +142,6 @@ export function writeStageReport(root, report) {
|
|
|
27
142
|
html: string(sourceArtifacts.html) || "stage-report.html"
|
|
28
143
|
}
|
|
29
144
|
};
|
|
30
|
-
writeJsonAtomic(json, normalized);
|
|
31
|
-
const text = renderStageReportMarkdown(normalized);
|
|
32
|
-
fs.writeFileSync(markdown, text);
|
|
33
|
-
fs.writeFileSync(html, renderStageReportHtml(normalized, text));
|
|
34
|
-
return { json, markdown, html };
|
|
35
145
|
}
|
|
36
146
|
export function renderStageReportMarkdown(report) {
|
|
37
147
|
const semantic = object(report.semantic);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
export function stageAttempt(context, projectId, runId, stage) {
|
|
3
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
4
|
+
if (!run)
|
|
5
|
+
throw new AppError("not_found", "Stage graph RUN is unavailable", 1);
|
|
6
|
+
const stages = JSON.parse(run.index_json).stage_runs ?? [];
|
|
7
|
+
return [...stages].reverse().find(item => item.stage === stage)?.attempt ?? "try-001";
|
|
8
|
+
}
|
|
9
|
+
export function bindStageWork(context, input) {
|
|
10
|
+
context.db.run("INSERT OR IGNORE INTO stage_work_memberships (project_id, run_id, work_id, stage, attempt, cycle) VALUES (?, ?, ?, ?, ?, ?)", [input.projectId, input.runId, input.workId, input.stage, input.attempt ?? stageAttempt(context, input.projectId, input.runId, input.stage), input.cycle ?? stageCycle(context, input.projectId, input.runId, input.stage)]);
|
|
11
|
+
}
|
|
12
|
+
export function stageCycle(context, projectId, runId, stage) {
|
|
13
|
+
if (stage !== "code" && stage !== "code-review")
|
|
14
|
+
return "";
|
|
15
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
16
|
+
const cycle = row ? JSON.parse(row.index_json).variables?.["merge.source_repair"]?.cycle : undefined;
|
|
17
|
+
return typeof cycle === "string" ? cycle : stage === "code-review" ? "initial" : "";
|
|
18
|
+
}
|
|
19
|
+
export function stageWorkIds(context, projectId, runId, stage) {
|
|
20
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stage_work_memberships'"))
|
|
21
|
+
throw new AppError("stage_graph_compatibility_required", "Stage Work membership is unavailable; use the RUN's pinned engine", 1);
|
|
22
|
+
return new Set(context.db.all("SELECT work_id FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [projectId, runId, stage, stageAttempt(context, projectId, runId, stage), stageCycle(context, projectId, runId, stage)]).map(row => row.work_id));
|
|
23
|
+
}
|
|
24
|
+
/** Generic Work APIs also serve non-stage runs; null preserves that contract. */
|
|
25
|
+
export function currentStageWorkIds(context, projectId, runId) {
|
|
26
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
27
|
+
if (!row)
|
|
28
|
+
return null;
|
|
29
|
+
const index = JSON.parse(row.index_json);
|
|
30
|
+
const stage = [...(index.stage_runs ?? [])].reverse().find(item => item.status === "running")?.stage;
|
|
31
|
+
return stage ? stageWorkIds(context, projectId, runId, stage) : null;
|
|
32
|
+
}
|
|
33
|
+
export function stageGraphScope(context, projectId, runId, stage) {
|
|
34
|
+
return { project_id: projectId, run_id: runId, attempt: stageAttempt(context, projectId, runId, stage), cycle: stageCycle(context, projectId, runId, stage) };
|
|
35
|
+
}
|
package/dist/services/usage.js
CHANGED
|
@@ -4,6 +4,8 @@ import { parseJsonObject } from "../shared/json.js";
|
|
|
4
4
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
5
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
6
6
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
7
|
+
import { runObservations } from "./run-observations.js";
|
|
8
|
+
import { ToolObservations, codexToolObservation } from "../harness-runtime/lib/tool-observations.mjs";
|
|
7
9
|
export function ingestZcodeUsage(context, input) {
|
|
8
10
|
return ingestHarnessUsage(context, input, { harness: "zcode-acp", label: "ZCode", idPrefix: "ZUSG", sourceKind: "zcode_session_usage_v1" });
|
|
9
11
|
}
|
|
@@ -19,6 +21,14 @@ export function ingestOpenCodeUsage(context, input) {
|
|
|
19
21
|
export function ingestAgyUsage(context, input) {
|
|
20
22
|
return ingestHarnessUsage(context, input, { harness: "antigravity-cli", label: "Antigravity", idPrefix: "AUSG", sourceKind: "antigravity_cli_session_usage_v1" });
|
|
21
23
|
}
|
|
24
|
+
/** Droid counters must describe one physical Session; inclusive roots double-count children. */
|
|
25
|
+
export function ingestDroidUsage(context, input) {
|
|
26
|
+
const payload = parseJsonObject(input.stdin || "{}", "Droid usage snapshot");
|
|
27
|
+
if (payload.usage_scope !== "physical_session" || !["complete", "partial"].includes(String(payload.completeness))) {
|
|
28
|
+
throw new Error("Droid usage requires physical_session scope and complete or partial completeness");
|
|
29
|
+
}
|
|
30
|
+
return ingestHarnessUsage(context, input, { harness: "droid-cli", label: "Droid", idPrefix: "DUSG", sourceKind: "droid_session_usage_v1" });
|
|
31
|
+
}
|
|
22
32
|
function ingestHarnessUsage(context, input, harness) {
|
|
23
33
|
const payload = parseJsonObject(input.stdin || "{}", `${harness.label} usage snapshot`);
|
|
24
34
|
const usage = object(payload.usage);
|
|
@@ -157,8 +167,7 @@ export function recalculateRunUsage(context, input) {
|
|
|
157
167
|
const usageFinal = final && unsettledSessions.length === 0;
|
|
158
168
|
const persistRunProjection = !input.sessionId && !input.stage;
|
|
159
169
|
if (persistRunProjection) {
|
|
160
|
-
context.db.
|
|
161
|
-
try {
|
|
170
|
+
context.db.writeTransaction(() => {
|
|
162
171
|
context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
|
|
163
172
|
for (const row of rows) {
|
|
164
173
|
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)
|
|
@@ -177,13 +186,10 @@ export function recalculateRunUsage(context, input) {
|
|
|
177
186
|
AND status = 'idle'
|
|
178
187
|
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]);
|
|
179
188
|
}
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
context.db.exec("ROLLBACK");
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
189
|
+
});
|
|
186
190
|
}
|
|
191
|
+
const observations = persistRunProjection ? runObservations(context, input.projectId, input.runId) : null;
|
|
192
|
+
const legacyTools = toolCallsForLinks(context, links, accountingAt);
|
|
187
193
|
return {
|
|
188
194
|
ok: true,
|
|
189
195
|
schema_id: "dd-flow/run-usage@2",
|
|
@@ -198,7 +204,8 @@ export function recalculateRunUsage(context, input) {
|
|
|
198
204
|
stopped: publicSessionIdentities(context, input.projectId, settledSessions.map((session) => session.session_id)),
|
|
199
205
|
unsettled: publicSessionIdentities(context, input.projectId, unsettledSessions)
|
|
200
206
|
},
|
|
201
|
-
tool_calls:
|
|
207
|
+
tool_calls: observations?.tools.observed_sessions ? observations.tools : legacyTools,
|
|
208
|
+
...(observations ? { observations, legacy_tool_calls: legacyTools } : {}),
|
|
202
209
|
coverage: rows.reduce((coverage, row) => ({ ...coverage, [row.status]: (coverage[row.status] ?? 0) + 1 }), {})
|
|
203
210
|
};
|
|
204
211
|
}
|
|
@@ -241,6 +248,7 @@ function publicSessionIdentities(context, projectId, storageIds) {
|
|
|
241
248
|
}
|
|
242
249
|
function toolCallsForLinks(context, links, observedAt) {
|
|
243
250
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
251
|
+
let incomplete = false;
|
|
244
252
|
const sessions = new Map();
|
|
245
253
|
for (const link of links)
|
|
246
254
|
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
@@ -253,18 +261,20 @@ function toolCallsForLinks(context, links, observedAt) {
|
|
|
253
261
|
result.unavailable_sessions.push(sessionId);
|
|
254
262
|
continue;
|
|
255
263
|
}
|
|
264
|
+
if ("completeness" in parsed && parsed.completeness !== "complete")
|
|
265
|
+
incomplete = true;
|
|
256
266
|
result.observed_sessions += 1;
|
|
257
267
|
result.total += parsed.total;
|
|
258
268
|
result.failures += parsed.failures;
|
|
259
269
|
for (const [name, count] of Object.entries(parsed.by_tool))
|
|
260
270
|
result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
|
|
261
271
|
}
|
|
262
|
-
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length ? "partial" : "measured";
|
|
272
|
+
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length || incomplete ? "partial" : "measured";
|
|
263
273
|
return result;
|
|
264
274
|
}
|
|
265
|
-
function isExternalHarness(harness) { return harness === "zcode-acp" || harness === "grok-acp" || harness === "opencode-server" || harness === "antigravity-cli"; }
|
|
275
|
+
function isExternalHarness(harness) { return harness === "droid-cli" || harness === "zcode-acp" || harness === "grok-acp" || harness === "opencode-server" || harness === "antigravity-cli"; }
|
|
266
276
|
function harnessSourceKind(harness) {
|
|
267
|
-
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";
|
|
277
|
+
return harness === "droid-cli" ? "droid_session_usage_v1" : 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";
|
|
268
278
|
}
|
|
269
279
|
function readHarnessToolCallsWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
|
|
270
280
|
if (!providerSessionId)
|
|
@@ -302,9 +312,7 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
|
|
|
302
312
|
return null;
|
|
303
313
|
try {
|
|
304
314
|
let sessionId;
|
|
305
|
-
|
|
306
|
-
let failures = 0;
|
|
307
|
-
const byTool = {};
|
|
315
|
+
const tools = new ToolObservations();
|
|
308
316
|
for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
|
|
309
317
|
if (!line.trim())
|
|
310
318
|
continue;
|
|
@@ -313,6 +321,7 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
|
|
|
313
321
|
event = JSON.parse(line);
|
|
314
322
|
}
|
|
315
323
|
catch {
|
|
324
|
+
tools.gaps.add("journal_invalid_record");
|
|
316
325
|
continue;
|
|
317
326
|
}
|
|
318
327
|
const payload = object(event.payload);
|
|
@@ -321,20 +330,13 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
|
|
|
321
330
|
if (event.type !== "response_item" || !payload)
|
|
322
331
|
continue;
|
|
323
332
|
const eventAt = stringValue(event.timestamp);
|
|
324
|
-
if (eventAt && !windows.some((window) => eventInsideWindow(eventAt, window, observedAt)))
|
|
333
|
+
if (eventAt && windows.length && !windows.some((window) => eventInsideWindow(eventAt, window, observedAt)))
|
|
325
334
|
continue;
|
|
326
|
-
|
|
327
|
-
if (type === "function_call" || type === "custom_tool_call") {
|
|
328
|
-
const name = stringValue(payload.name) ?? "unknown";
|
|
329
|
-
byTool[name] = (byTool[name] ?? 0) + 1;
|
|
330
|
-
total += 1;
|
|
331
|
-
}
|
|
332
|
-
if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
|
|
333
|
-
failures += 1;
|
|
335
|
+
tools.add(codexToolObservation(event, expectedSessionId));
|
|
334
336
|
}
|
|
335
337
|
if (sessionId && sessionId !== expectedSessionId)
|
|
336
338
|
return null;
|
|
337
|
-
return
|
|
339
|
+
return tools.summary();
|
|
338
340
|
}
|
|
339
341
|
catch {
|
|
340
342
|
return null;
|
|
@@ -346,37 +348,6 @@ function eventInsideWindow(eventAt, window, observedAt) {
|
|
|
346
348
|
const end = Date.parse(window.completed_at ?? observedAt);
|
|
347
349
|
return Number.isFinite(at) && (!Number.isFinite(start) || at >= start) && (!Number.isFinite(end) || at <= end);
|
|
348
350
|
}
|
|
349
|
-
function outputIndicatesFailure(payload) {
|
|
350
|
-
if (payload.error === true || typeof payload.error === "string")
|
|
351
|
-
return true;
|
|
352
|
-
return containsFailure(payload.output);
|
|
353
|
-
}
|
|
354
|
-
function containsFailure(value) {
|
|
355
|
-
if (Array.isArray(value))
|
|
356
|
-
return value.some(containsFailure);
|
|
357
|
-
if (typeof value === "string") {
|
|
358
|
-
if (/(?:^|\n)exit=(?:[1-9]\d*)\b|Process exited with code [1-9]\d*/.test(value))
|
|
359
|
-
return true;
|
|
360
|
-
const trimmed = value.trim();
|
|
361
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
362
|
-
try {
|
|
363
|
-
return containsFailure(JSON.parse(trimmed));
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
const item = object(value);
|
|
372
|
-
if (!item)
|
|
373
|
-
return false;
|
|
374
|
-
if (item.ok === false || item.is_error === true || item.isError === true || item.error === true || typeof item.error === "string")
|
|
375
|
-
return true;
|
|
376
|
-
if (typeof item.exit_code === "number" && item.exit_code !== 0)
|
|
377
|
-
return true;
|
|
378
|
-
return Object.values(item).some(containsFailure);
|
|
379
|
-
}
|
|
380
351
|
/** RUN accounting belongs to the physical provider Session, not to every Work link. */
|
|
381
352
|
function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
382
353
|
const sessions = new Map();
|
|
@@ -425,7 +396,7 @@ function hasInclusiveUsageSnapshot(context, projectId, harness, providerSessionI
|
|
|
425
396
|
function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
|
|
426
397
|
if (!providerSessionId)
|
|
427
398
|
return { status: "not_observable", diagnostic: "provider_session_id_missing" };
|
|
428
|
-
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
|
|
399
|
+
const snapshots = context.db.all(`SELECT completeness, observed_at, total_tokens, input_tokens, cache_read_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens
|
|
429
400
|
FROM harness_usage_snapshots
|
|
430
401
|
WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
|
|
431
402
|
ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
|
|
@@ -434,6 +405,8 @@ function readHarnessUsageWindow(context, harness, projectId, providerSessionId,
|
|
|
434
405
|
const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
|
|
435
406
|
if (!last || !before || last === before)
|
|
436
407
|
return { status: "not_yet_emitted", diagnostic: `${harness}_usage_window_incomplete` };
|
|
408
|
+
if (harness === "droid-cli" && (last.completeness !== "complete" || before.completeness !== "complete"))
|
|
409
|
+
return { status: "not_observable", diagnostic: "droid-cli_usage_window_partial" };
|
|
437
410
|
const delta = (value, baseline) => value === null || baseline === null ? null : Math.max(0, value - baseline);
|
|
438
411
|
const input = delta(last.input_tokens, before.input_tokens);
|
|
439
412
|
const cacheRead = delta(last.cache_read_input_tokens, before.cache_read_input_tokens);
|
|
@@ -575,62 +548,26 @@ function usageReport(context, input, refresh) {
|
|
|
575
548
|
}
|
|
576
549
|
function toolCallsForSessions(sessions) {
|
|
577
550
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
551
|
+
let incomplete = false;
|
|
578
552
|
for (const session of sessions) {
|
|
579
553
|
const parsed = readToolCalls(session.transcript_path, nativeSessionId(session));
|
|
580
554
|
if (!parsed) {
|
|
581
555
|
result.unavailable_sessions.push(session.session_id);
|
|
582
556
|
continue;
|
|
583
557
|
}
|
|
558
|
+
if (parsed.completeness !== "complete")
|
|
559
|
+
incomplete = true;
|
|
584
560
|
result.observed_sessions += 1;
|
|
585
561
|
result.total += parsed.total;
|
|
586
562
|
result.failures += parsed.failures;
|
|
587
563
|
for (const [name, count] of Object.entries(parsed.by_tool))
|
|
588
564
|
result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
|
|
589
565
|
}
|
|
590
|
-
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length
|
|
566
|
+
result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length || incomplete ? "partial" : "measured";
|
|
591
567
|
return result;
|
|
592
568
|
}
|
|
593
569
|
function readToolCalls(transcriptPath, expectedSessionId) {
|
|
594
|
-
|
|
595
|
-
return null;
|
|
596
|
-
try {
|
|
597
|
-
let sessionId;
|
|
598
|
-
const byTool = {};
|
|
599
|
-
let total = 0;
|
|
600
|
-
let failures = 0;
|
|
601
|
-
for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
|
|
602
|
-
if (!line.trim())
|
|
603
|
-
continue;
|
|
604
|
-
let event;
|
|
605
|
-
try {
|
|
606
|
-
event = JSON.parse(line);
|
|
607
|
-
}
|
|
608
|
-
catch {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
const payload = object(event.payload);
|
|
612
|
-
if (!payload)
|
|
613
|
-
continue;
|
|
614
|
-
if (event.type === "session_meta")
|
|
615
|
-
sessionId = stringValue(payload.id) ?? sessionId;
|
|
616
|
-
if (event.type !== "response_item")
|
|
617
|
-
continue;
|
|
618
|
-
const type = stringValue(payload.type);
|
|
619
|
-
if (type === "function_call" || type === "custom_tool_call") {
|
|
620
|
-
const name = stringValue(payload.name) ?? "unknown";
|
|
621
|
-
byTool[name] = (byTool[name] ?? 0) + 1;
|
|
622
|
-
total += 1;
|
|
623
|
-
}
|
|
624
|
-
if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
|
|
625
|
-
failures += 1;
|
|
626
|
-
}
|
|
627
|
-
if (sessionId && sessionId !== expectedSessionId)
|
|
628
|
-
return null;
|
|
629
|
-
return { total, failures, by_tool: Object.fromEntries(Object.entries(byTool).sort(([left], [right]) => left.localeCompare(right))) };
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
570
|
+
return readToolCallsForWindows(transcriptPath, expectedSessionId, [], new Date().toISOString());
|
|
634
571
|
}
|
|
635
572
|
function segmentUsage(context, session, segment) {
|
|
636
573
|
if (!session)
|