@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100

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.
Files changed (153) hide show
  1. package/CHANGELOG.md +561 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +91 -15
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1506 -334
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -3,6 +3,9 @@ import fs from "node:fs";
3
3
  import { parseJsonObject } from "../shared/json.js";
4
4
  import { resolveProjectRoot } from "../storage/paths.js";
5
5
  import { registerProject, requireProjectByRoot } from "./projects.js";
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";
6
9
  export function ingestZcodeUsage(context, input) {
7
10
  return ingestHarnessUsage(context, input, { harness: "zcode-acp", label: "ZCode", idPrefix: "ZUSG", sourceKind: "zcode_session_usage_v1" });
8
11
  }
@@ -18,6 +21,14 @@ export function ingestOpenCodeUsage(context, input) {
18
21
  export function ingestAgyUsage(context, input) {
19
22
  return ingestHarnessUsage(context, input, { harness: "antigravity-cli", label: "Antigravity", idPrefix: "AUSG", sourceKind: "antigravity_cli_session_usage_v1" });
20
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
+ }
21
32
  function ingestHarnessUsage(context, input, harness) {
22
33
  const payload = parseJsonObject(input.stdin || "{}", `${harness.label} usage snapshot`);
23
34
  const usage = object(payload.usage);
@@ -55,11 +66,11 @@ function ingestHarnessUsage(context, input, harness) {
55
66
  values.input, values.cacheRead, values.cacheWrite, values.output, values.reasoning, values.requests, values.errors,
56
67
  values.toolCalls, values.toolFailures, values.toolByName ? JSON.stringify(values.toolByName) : null, harness.sourceKind,
57
68
  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 };
69
+ return { ok: true, observed: inserted.changes === 1, duplicate: inserted.changes === 0, snapshot_id: id, session: publicSessionIdentity({ harness: harness.harness, provider_session_id: providerSessionId, session_id: providerSessionId }) };
59
70
  }
60
71
  export function checkpointSessionUsage(context, session, input) {
61
72
  const observedAt = context.now();
62
- const result = readCodexTranscript(session.transcript_path, session.session_id);
73
+ const result = readCodexTranscript(session.transcript_path, nativeSessionId(session));
63
74
  const source = sourceFacts(session.transcript_path);
64
75
  const snapshot = {
65
76
  id: `USG-${crypto.randomUUID()}`,
@@ -114,7 +125,7 @@ export function checkpointSessionUsage(context, session, input) {
114
125
  return snapshotForOutput(snapshot);
115
126
  }
116
127
  export function checkpointRunUsage(context, input) {
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
128
+ const sessions = context.db.all(`SELECT session_id, project_id, run_id, transcript_path, provider_session_id, parent_session_id, role, aspect_id, plan_item_id, session_kind
118
129
  FROM sessions WHERE project_id = ? AND run_id = ?`, [input.projectId, input.runId]);
119
130
  return sessions.map((session) => checkpointSessionUsage(context, session, input));
120
131
  }
@@ -136,7 +147,7 @@ export function recalculateRunUsage(context, input) {
136
147
  // after `work finish` but before the agent's actual terminal event.
137
148
  const accountingAt = observedAt;
138
149
  const links = context.db.all(`SELECT ws.id, ws.work_id, ws.session_id, ws.created_at, ws.completed_at,
139
- w.project_id, s.harness, s.transcript_path, s.parent_session_id, s.agent_id, s.provider_session_id
150
+ w.project_id, s.harness, s.transcript_path, s.parent_session_id, s.provider_parent_session_id, s.agent_id, s.provider_session_id
140
151
  FROM work_sessions ws
141
152
  JOIN works w ON w.work_id = ws.work_id
142
153
  LEFT JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
@@ -154,32 +165,38 @@ export function recalculateRunUsage(context, input) {
154
165
  const settledSessions = settledRunSessions(context, links, observedAt);
155
166
  const unsettledSessions = [...new Set(links.map((link) => link.session_id))].filter((sessionId) => !settledSessions.some((session) => session.session_id === sessionId));
156
167
  const usageFinal = final && unsettledSessions.length === 0;
157
- context.db.exec("BEGIN IMMEDIATE");
158
- try {
159
- context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
160
- for (const row of rows) {
161
- 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)
168
+ const persistRunProjection = !input.sessionId && !input.stage;
169
+ if (persistRunProjection) {
170
+ context.db.exec("SAVEPOINT run_usage");
171
+ try {
172
+ context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
173
+ for (const row of rows) {
174
+ 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)
162
175
  VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 'windowed_work_session', 1, ?, ?, ?)`, [
163
- `USG-${crypto.randomUUID()}`, input.projectId, input.runId, row.session_id, usageFinal ? "final" : "provisional", input.stage?.name ?? null, row.observed_at,
164
- row.tokens?.total_tokens ?? null, row.tokens?.input_tokens ?? null, row.tokens?.cache_read_input_tokens ?? null,
165
- row.tokens?.cache_read_input_tokens ?? null, row.tokens?.cache_write_input_tokens ?? null, row.tokens?.uncached_input_tokens ?? null,
166
- 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,
167
- row.status, row.diagnostic, observedAt
168
- ]);
169
- }
170
- for (const session of settledSessions) {
171
- context.db.run(`UPDATE sessions
176
+ `USG-${crypto.randomUUID()}`, input.projectId, input.runId, row.session_id, usageFinal ? "final" : "provisional", input.stage?.name ?? null, row.observed_at,
177
+ row.tokens?.total_tokens ?? null, row.tokens?.input_tokens ?? null, row.tokens?.cache_read_input_tokens ?? null,
178
+ row.tokens?.cache_read_input_tokens ?? null, row.tokens?.cache_write_input_tokens ?? null, row.tokens?.uncached_input_tokens ?? null,
179
+ 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,
180
+ row.status, row.diagnostic, observedAt
181
+ ]);
182
+ }
183
+ for (const session of settledSessions) {
184
+ context.db.run(`UPDATE sessions
172
185
  SET status = 'stopped', stop_reason = 'provider_turn_complete', updated_at = ?, stopped_at = ?
173
186
  WHERE project_id = ? AND run_id = ? AND session_id = ?
174
187
  AND status = 'idle'
175
188
  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]);
189
+ }
190
+ context.db.exec("RELEASE run_usage");
191
+ }
192
+ catch (error) {
193
+ context.db.exec("ROLLBACK TO run_usage");
194
+ context.db.exec("RELEASE run_usage");
195
+ throw error;
176
196
  }
177
- context.db.exec("COMMIT");
178
- }
179
- catch (error) {
180
- context.db.exec("ROLLBACK");
181
- throw error;
182
197
  }
198
+ const observations = persistRunProjection ? runObservations(context, input.projectId, input.runId) : null;
199
+ const legacyTools = toolCallsForLinks(context, links, accountingAt);
183
200
  return {
184
201
  ok: true,
185
202
  schema_id: "dd-flow/run-usage@2",
@@ -187,16 +204,29 @@ export function recalculateRunUsage(context, input) {
187
204
  status: usageFinal ? "final" : "provisional",
188
205
  scope: input.stage ? { kind: "stage", stage: input.stage.name, started_at: input.stage.startedAt, completed_at: input.stage.completedAt } : { kind: "run" },
189
206
  observed_at: observedAt,
207
+ persisted: persistRunProjection,
190
208
  totals: outputUsageTotals(totals),
191
- sessions: rows,
209
+ sessions: publicUsageRows(context, input.projectId, rows),
192
210
  session_reconciliation: {
193
- stopped: settledSessions.map((session) => session.session_id),
194
- unsettled: unsettledSessions
211
+ stopped: publicSessionIdentities(context, input.projectId, settledSessions.map((session) => session.session_id)),
212
+ unsettled: publicSessionIdentities(context, input.projectId, unsettledSessions)
195
213
  },
196
- tool_calls: toolCallsForLinks(context, links, accountingAt),
214
+ tool_calls: observations?.tools.observed_sessions ? observations.tools : legacyTools,
215
+ ...(observations ? { observations, legacy_tool_calls: legacyTools } : {}),
197
216
  coverage: rows.reduce((coverage, row) => ({ ...coverage, [row.status]: (coverage[row.status] ?? 0) + 1 }), {})
198
217
  };
199
218
  }
219
+ function publicUsageRows(context, projectId, rows) {
220
+ const identityFor = (storageId) => {
221
+ const row = context.db.get("SELECT harness, provider_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [projectId, storageId]);
222
+ return row ? publicSessionIdentity({ harness: row.harness, provider_session_id: row.provider_session_id, session_id: storageId }) : null;
223
+ };
224
+ return rows.map(({ session_id, provider_session_id: _providerSessionId, parent_session_id, ...row }) => ({
225
+ ...row,
226
+ session: identityFor(session_id),
227
+ ...(parent_session_id ? { parent_session: identityFor(parent_session_id) } : {})
228
+ }));
229
+ }
200
230
  function overlapsStage(link, stage, observedAt) {
201
231
  const linkStart = Date.parse(link.created_at);
202
232
  const linkEnd = Date.parse(link.completed_at ?? observedAt);
@@ -212,8 +242,20 @@ function clipToStage(link, stage, observedAt) {
212
242
  const completedAt = Date.parse(naturalEnd) > Date.parse(stageEnd) ? stageEnd : naturalEnd;
213
243
  return { ...link, created_at: startedAt, completed_at: completedAt };
214
244
  }
245
+ function nativeSessionId(session) {
246
+ return session.provider_session_id ?? session.session_id;
247
+ }
248
+ function publicSessionIdentities(context, projectId, storageIds) {
249
+ if (storageIds.length === 0)
250
+ return [];
251
+ const marks = storageIds.map(() => "?").join(", ");
252
+ const rows = context.db.all(`SELECT session_id, harness, provider_session_id FROM sessions WHERE project_id = ? AND session_id IN (${marks})`, [projectId, ...storageIds]);
253
+ const byStorage = new Map(rows.map((row) => [row.session_id, publicSessionIdentity(row)]));
254
+ return storageIds.flatMap((id) => byStorage.get(id) ?? []);
255
+ }
215
256
  function toolCallsForLinks(context, links, observedAt) {
216
257
  const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
258
+ let incomplete = false;
217
259
  const sessions = new Map();
218
260
  for (const link of links)
219
261
  sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
@@ -221,23 +263,25 @@ function toolCallsForLinks(context, links, observedAt) {
221
263
  const first = windows[0];
222
264
  const parsed = isExternalHarness(first.harness)
223
265
  ? 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)
224
- : readToolCallsForWindows(first.transcript_path, sessionId, windows, observedAt);
266
+ : readToolCallsForWindows(first.transcript_path, nativeSessionId(first), windows, observedAt);
225
267
  if (!parsed) {
226
268
  result.unavailable_sessions.push(sessionId);
227
269
  continue;
228
270
  }
271
+ if ("completeness" in parsed && parsed.completeness !== "complete")
272
+ incomplete = true;
229
273
  result.observed_sessions += 1;
230
274
  result.total += parsed.total;
231
275
  result.failures += parsed.failures;
232
276
  for (const [name, count] of Object.entries(parsed.by_tool))
233
277
  result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
234
278
  }
235
- result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length ? "partial" : "measured";
279
+ result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length || incomplete ? "partial" : "measured";
236
280
  return result;
237
281
  }
238
- function isExternalHarness(harness) { return harness === "zcode-acp" || harness === "grok-acp" || harness === "opencode-server" || harness === "antigravity-cli"; }
282
+ function isExternalHarness(harness) { return harness === "droid-cli" || harness === "zcode-acp" || harness === "grok-acp" || harness === "opencode-server" || harness === "antigravity-cli"; }
239
283
  function harnessSourceKind(harness) {
240
- 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";
284
+ 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";
241
285
  }
242
286
  function readHarnessToolCallsWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
243
287
  if (!providerSessionId)
@@ -275,9 +319,7 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
275
319
  return null;
276
320
  try {
277
321
  let sessionId;
278
- let total = 0;
279
- let failures = 0;
280
- const byTool = {};
322
+ const tools = new ToolObservations();
281
323
  for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
282
324
  if (!line.trim())
283
325
  continue;
@@ -286,6 +328,7 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
286
328
  event = JSON.parse(line);
287
329
  }
288
330
  catch {
331
+ tools.gaps.add("journal_invalid_record");
289
332
  continue;
290
333
  }
291
334
  const payload = object(event.payload);
@@ -294,20 +337,13 @@ function readToolCallsForWindows(transcriptPath, expectedSessionId, windows, obs
294
337
  if (event.type !== "response_item" || !payload)
295
338
  continue;
296
339
  const eventAt = stringValue(event.timestamp);
297
- if (eventAt && !windows.some((window) => eventInsideWindow(eventAt, window, observedAt)))
340
+ if (eventAt && windows.length && !windows.some((window) => eventInsideWindow(eventAt, window, observedAt)))
298
341
  continue;
299
- const type = stringValue(payload.type);
300
- if (type === "function_call" || type === "custom_tool_call") {
301
- const name = stringValue(payload.name) ?? "unknown";
302
- byTool[name] = (byTool[name] ?? 0) + 1;
303
- total += 1;
304
- }
305
- if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
306
- failures += 1;
342
+ tools.add(codexToolObservation(event, expectedSessionId));
307
343
  }
308
344
  if (sessionId && sessionId !== expectedSessionId)
309
345
  return null;
310
- return { total, failures, by_tool: Object.fromEntries(Object.entries(byTool).sort(([left], [right]) => left.localeCompare(right))) };
346
+ return tools.summary();
311
347
  }
312
348
  catch {
313
349
  return null;
@@ -319,37 +355,6 @@ function eventInsideWindow(eventAt, window, observedAt) {
319
355
  const end = Date.parse(window.completed_at ?? observedAt);
320
356
  return Number.isFinite(at) && (!Number.isFinite(start) || at >= start) && (!Number.isFinite(end) || at <= end);
321
357
  }
322
- function outputIndicatesFailure(payload) {
323
- if (payload.error === true || typeof payload.error === "string")
324
- return true;
325
- return containsFailure(payload.output);
326
- }
327
- function containsFailure(value) {
328
- if (Array.isArray(value))
329
- return value.some(containsFailure);
330
- if (typeof value === "string") {
331
- if (/(?:^|\n)exit=(?:[1-9]\d*)\b|Process exited with code [1-9]\d*/.test(value))
332
- return true;
333
- const trimmed = value.trim();
334
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
335
- try {
336
- return containsFailure(JSON.parse(trimmed));
337
- }
338
- catch {
339
- return false;
340
- }
341
- }
342
- return false;
343
- }
344
- const item = object(value);
345
- if (!item)
346
- return false;
347
- if (item.ok === false || item.is_error === true || item.isError === true || item.error === true || typeof item.error === "string")
348
- return true;
349
- if (typeof item.exit_code === "number" && item.exit_code !== 0)
350
- return true;
351
- return Object.values(item).some(containsFailure);
352
- }
353
358
  /** RUN accounting belongs to the physical provider Session, not to every Work link. */
354
359
  function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
355
360
  const sessions = new Map();
@@ -357,15 +362,15 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
357
362
  sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
358
363
  const physicalSessions = [...sessions.values()].filter((sessionLinks) => {
359
364
  const link = sessionLinks[0];
360
- if (!link.parent_session_id)
365
+ if (!link.provider_parent_session_id)
361
366
  return true;
362
- const parent = links.find((candidate) => candidate.session_id === link.parent_session_id);
367
+ const parent = links.find((candidate) => candidate.session_id === link.provider_parent_session_id);
363
368
  return !parent || !hasInclusiveUsageSnapshot(context, parent.project_id, parent.harness, parent.provider_session_id, observedAt);
364
369
  });
365
370
  return physicalSessions.map((sessionLinks) => {
366
371
  const link = sessionLinks[0];
367
372
  const startedAt = stage?.startedAt ?? sessionLinks.map((item) => item.created_at).sort()[0];
368
- const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, link.session_id);
373
+ const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, nativeSessionId(link));
369
374
  const naturalEnd = isExternalHarness(link.harness)
370
375
  ? observedAt
371
376
  : completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
@@ -373,7 +378,7 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
373
378
  const harnessUsage = isExternalHarness(link.harness)
374
379
  ? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
375
380
  : null;
376
- const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, link.session_id, startedAt, endedAt);
381
+ const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, nativeSessionId(link), startedAt, endedAt);
377
382
  return {
378
383
  work_session_id: link.id,
379
384
  work_id: link.work_id,
@@ -398,7 +403,7 @@ function hasInclusiveUsageSnapshot(context, projectId, harness, providerSessionI
398
403
  function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
399
404
  if (!providerSessionId)
400
405
  return { status: "not_observable", diagnostic: "provider_session_id_missing" };
401
- 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
406
+ 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
402
407
  FROM harness_usage_snapshots
403
408
  WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
404
409
  ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
@@ -407,6 +412,8 @@ function readHarnessUsageWindow(context, harness, projectId, providerSessionId,
407
412
  const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
408
413
  if (!last || !before || last === before)
409
414
  return { status: "not_yet_emitted", diagnostic: `${harness}_usage_window_incomplete` };
415
+ if (harness === "droid-cli" && (last.completeness !== "complete" || before.completeness !== "complete"))
416
+ return { status: "not_observable", diagnostic: "droid-cli_usage_window_partial" };
410
417
  const delta = (value, baseline) => value === null || baseline === null ? null : Math.max(0, value - baseline);
411
418
  const input = delta(last.input_tokens, before.input_tokens);
412
419
  const cacheRead = delta(last.cache_read_input_tokens, before.cache_read_input_tokens);
@@ -437,15 +444,16 @@ function settledRunSessions(context, links, observedAt) {
437
444
  return [];
438
445
  const link = sessionLinks[0];
439
446
  const sessionStartedAt = sessionLinks.map((item) => item.created_at).sort()[0];
447
+ const workCompletedAt = sessionLinks.map((item) => item.completed_at).sort().at(-1);
440
448
  if (isExternalHarness(link.harness) && link.provider_session_id) {
441
449
  const snapshot = context.db.get(`SELECT observed_at FROM harness_usage_snapshots
442
450
  WHERE project_id = ? AND harness = ? AND provider_session_id = ?
443
451
  AND completeness = 'complete' AND observed_at >= ? AND observed_at <= ?
444
- ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id, sessionStartedAt, observedAt]);
452
+ ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id, workCompletedAt, observedAt]);
445
453
  return snapshot ? [{ session_id: sessionId, completed_at: snapshot.observed_at }] : [];
446
454
  }
447
- const completed = completedProviderTurn(sessionLinks[0].transcript_path, sessionId);
448
- if (!completed || Date.parse(completed.completed_at) < Date.parse(sessionStartedAt))
455
+ const completed = completedProviderTurn(sessionLinks[0].transcript_path, nativeSessionId(sessionLinks[0]));
456
+ if (!completed || Date.parse(completed.completed_at) < Date.parse(workCompletedAt) || Date.parse(completed.completed_at) < Date.parse(sessionStartedAt))
449
457
  return [];
450
458
  return [{ session_id: sessionId, completed_at: completed.completed_at }];
451
459
  });
@@ -484,10 +492,10 @@ function completedProviderTurn(transcriptPath, expectedSessionId) {
484
492
  }
485
493
  }
486
494
  function usageReport(context, input, refresh) {
487
- 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
495
+ const sessions = context.db.all(`SELECT DISTINCT session_id, project_id, ? AS run_id, transcript_path, harness, provider_session_id, parent_session_id, role, aspect_id, plan_item_id, session_kind
488
496
  FROM sessions WHERE project_id = ? AND run_id = ?
489
497
  UNION
490
- 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
498
+ SELECT DISTINCT f.session_id, f.project_id, ? AS run_id, f.transcript_path, f.harness, f.provider_session_id, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
491
499
  FROM flow_session_segments s JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
492
500
  WHERE s.project_id = ? AND s.run_id = ?`, [input.runId, input.projectId, input.runId, input.runId, input.projectId, input.runId]);
493
501
  const selectedSessions = input.sessionId ? sessions.filter((session) => session.session_id === input.sessionId) : sessions;
@@ -500,13 +508,15 @@ function usageReport(context, input, refresh) {
500
508
  LEFT JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
501
509
  WHERE s.project_id = ? AND s.run_id = ? ORDER BY s.session_id, s.observed_at, s.id`, [input.projectId, input.runId]);
502
510
  const selectedRows = input.sessionId ? rows.filter((row) => row.session_id === input.sessionId) : rows;
503
- const deltas = usageDeltas(selectedRows);
511
+ const publicSessions = new Map(sessions.map((session) => [session.session_id, publicSessionIdentity({ harness: session.harness ?? "codex-desktop", provider_session_id: session.provider_session_id ?? null, session_id: session.session_id })]));
512
+ const rawDeltas = usageDeltas(selectedRows);
513
+ const deltas = rawDeltas.map(({ session_id, ...delta }) => ({ ...delta, session: publicSessions.get(session_id) ?? null }));
504
514
  const segments = context.db.all(`SELECT id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name
505
515
  FROM flow_session_segments WHERE project_id = ? AND run_id = ? ORDER BY started_at, id`, [input.projectId, input.runId]);
506
516
  const supported = new Set(["session", "role", "aspect", "plan-item", "stage", "protocol"]);
507
517
  const groupBy = supported.has(input.groupBy) ? input.groupBy : "session";
508
518
  const groups = new Map();
509
- for (const delta of deltas) {
519
+ for (const delta of rawDeltas) {
510
520
  const key = groupKey(delta, groupBy);
511
521
  const group = groups.get(key) ?? { tokens: emptyUsage(), snapshots: 0, statuses: {}, transition_buckets: 0 };
512
522
  group.snapshots += 1;
@@ -523,7 +533,9 @@ function usageReport(context, input, refresh) {
523
533
  run_id: input.runId,
524
534
  source: { kind: "codex_transcript_v1", parser_version: 1, refreshed_sessions: refreshed.length, checkpoint_source: refresh ? "refreshed" : "stored" },
525
535
  sessions: selectedSessions.map((session) => ({
526
- session_id: session.session_id, parent_session_id: session.parent_session_id ?? null, role: session.role ?? null,
536
+ session: publicSessionIdentity({ harness: session.harness ?? "codex-desktop", provider_session_id: session.provider_session_id ?? null, session_id: session.session_id }),
537
+ ...(session.parent_session_id ? { parent_session: publicSessions.get(session.parent_session_id) ?? null } : {}),
538
+ role: session.role ?? null,
527
539
  aspect_id: session.aspect_id ?? null, plan_item_id: session.plan_item_id ?? null, session_kind: session.session_kind ?? null
528
540
  })),
529
541
  groups: [...groups.entries()].map(([key, group]) => ({ key, ...group, tokens: outputUsageTotals(group.tokens) })),
@@ -531,7 +543,7 @@ function usageReport(context, input, refresh) {
531
543
  tool_calls: toolCallsForSessions(sessions),
532
544
  segments: segments.map((segment) => ({
533
545
  segment_id: segment.id,
534
- session_id: segment.session_id,
546
+ session: publicSessions.get(segment.session_id) ?? null,
535
547
  run_id: segment.run_id,
536
548
  protocol_id: segment.protocol_id,
537
549
  started_at: segment.started_at,
@@ -543,67 +555,31 @@ function usageReport(context, input, refresh) {
543
555
  }
544
556
  function toolCallsForSessions(sessions) {
545
557
  const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
558
+ let incomplete = false;
546
559
  for (const session of sessions) {
547
- const parsed = readToolCalls(session.transcript_path, session.session_id);
560
+ const parsed = readToolCalls(session.transcript_path, nativeSessionId(session));
548
561
  if (!parsed) {
549
562
  result.unavailable_sessions.push(session.session_id);
550
563
  continue;
551
564
  }
565
+ if (parsed.completeness !== "complete")
566
+ incomplete = true;
552
567
  result.observed_sessions += 1;
553
568
  result.total += parsed.total;
554
569
  result.failures += parsed.failures;
555
570
  for (const [name, count] of Object.entries(parsed.by_tool))
556
571
  result.by_tool[name] = (result.by_tool[name] ?? 0) + count;
557
572
  }
558
- result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length === 0 ? "measured" : "partial";
573
+ result.status = result.observed_sessions === 0 ? "unavailable" : result.unavailable_sessions.length || incomplete ? "partial" : "measured";
559
574
  return result;
560
575
  }
561
576
  function readToolCalls(transcriptPath, expectedSessionId) {
562
- if (!transcriptPath || !fs.existsSync(transcriptPath))
563
- return null;
564
- try {
565
- let sessionId;
566
- const byTool = {};
567
- let total = 0;
568
- let failures = 0;
569
- for (const line of fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/)) {
570
- if (!line.trim())
571
- continue;
572
- let event;
573
- try {
574
- event = JSON.parse(line);
575
- }
576
- catch {
577
- continue;
578
- }
579
- const payload = object(event.payload);
580
- if (!payload)
581
- continue;
582
- if (event.type === "session_meta")
583
- sessionId = stringValue(payload.id) ?? sessionId;
584
- if (event.type !== "response_item")
585
- continue;
586
- const type = stringValue(payload.type);
587
- if (type === "function_call" || type === "custom_tool_call") {
588
- const name = stringValue(payload.name) ?? "unknown";
589
- byTool[name] = (byTool[name] ?? 0) + 1;
590
- total += 1;
591
- }
592
- if ((type === "function_call_output" || type === "custom_tool_call_output") && outputIndicatesFailure(payload))
593
- failures += 1;
594
- }
595
- if (sessionId && sessionId !== expectedSessionId)
596
- return null;
597
- return { total, failures, by_tool: Object.fromEntries(Object.entries(byTool).sort(([left], [right]) => left.localeCompare(right))) };
598
- }
599
- catch {
600
- return null;
601
- }
577
+ return readToolCallsForWindows(transcriptPath, expectedSessionId, [], new Date().toISOString());
602
578
  }
603
579
  function segmentUsage(context, session, segment) {
604
580
  if (!session)
605
581
  return { status: "unavailable", diagnostic: "session_record_missing" };
606
- const result = readCodexTranscriptWindow(session.transcript_path, session.session_id, segment.started_at, segment.ended_at ?? context.now());
582
+ const result = readCodexTranscriptWindow(session.transcript_path, nativeSessionId(session), segment.started_at, segment.ended_at ?? context.now());
607
583
  return result.counter ? { status: result.status, tokens: result.counter.usage, token_event_at: result.counter.token_event_at } : { status: result.status, diagnostic: result.diagnostic ?? null };
608
584
  }
609
585
  function readCodexTranscript(transcriptPath, expectedSessionId) {