@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.133

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 (204) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1108 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +119 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +747 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1774 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +177 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1038 -192
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +781 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,101 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, open, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { withRunnerLock } from "./runner-lock.mjs";
5
+
6
+ const text = value => typeof value === "string" && value.trim() ? value.trim() : null;
7
+ const fields = ["provider", "model", "reasoning", "mode", "permission_mode"];
8
+ const observationScope = source => JSON.stringify([source?.channel ?? null, source?.scope ?? null, source?.attempt_id ?? null, source?.turn_id ?? null, source?.non_asserting === true]);
9
+ export const observedFields = value => Object.fromEntries(fields.map(key => [key, text(value?.[key])]));
10
+ export const modelObservationFile = journal => path.join(path.dirname(journal), "model-observations.jsonl");
11
+
12
+ /** Routing is allowed. Permission/autonomy changes remain integrity failures.
13
+ * Missing native fields are unknown, never proof that the request was applied. */
14
+ export function checkObservedProfile(requested, observed, { strict = false, required = [] } = {}) {
15
+ const unknown = fields.filter(key => requested?.[key] && !observed?.[key]);
16
+ const changes = fields.filter(key => requested?.[key] && observed?.[key] && requested[key] !== observed[key]);
17
+ const violations = changes.filter(key => strict || ["mode", "permission_mode"].includes(key));
18
+ const missing = required.filter(key => !observed?.[key]);
19
+ if (violations.length || missing.length) throw Object.assign(new Error("native execution profile integrity is not confirmed"), {
20
+ code: "profile_integrity_violation", details: { requested: observedFields(requested), observed: observedFields(observed), violations, missing }
21
+ });
22
+ return { requested: observedFields(requested), observed: observedFields(observed), status: unknown.length ? "incomplete" : changes.length ? "mixed" : "matched", matched: !unknown.length && !changes.length, unknown, changes };
23
+ }
24
+
25
+ export async function readModelObservations(journal) {
26
+ try { const text = await readFile(modelObservationFile(journal), "utf8"); return text.slice(0, text.lastIndexOf("\n") + 1).split("\n").filter(Boolean).map(JSON.parse); }
27
+ catch (error) { if (error.code === "ENOENT") return []; throw error; }
28
+ }
29
+
30
+ /** Only adapter-verified native facts enter this file. It records changes,
31
+ * not every tool or token; reading it never requires replaying native history. */
32
+ export async function observeModel({ journal, harness, sessionId, parentSessionId = null, requested = {}, observed = {}, source, evidence = "configured", reason = null, nativeTimestamp = null, previousModel = null }) {
33
+ if (!journal || !sessionId) return null;
34
+ if (!source || !["configured", "request", "response", "unavailable"].includes(evidence)) throw new Error("model observation requires a native source and evidence level");
35
+ if (typeof source === "string") source = { channel: source, ...(["configured", "response"].includes(evidence) ? { scope: evidence } : {}) };
36
+ const file = modelObservationFile(journal), profile = observedFields(observed);
37
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
38
+ return withRunnerLock(file, async () => {
39
+ const events = await readModelObservations(journal);
40
+ const replay = source?.event_sha256 ? events.find(item => item.harness === harness && item.session_id === sessionId && observationScope(item.source) === observationScope(source) && item.source?.event_sha256 === source.event_sha256) : null;
41
+ if (replay) return replay;
42
+ const previous = events.findLast(item => item.harness === harness && item.session_id === sessionId && observationScope(item.source) === observationScope(source));
43
+ const normalizedReason = text(reason)?.slice(0, 200) ?? null;
44
+ if (previous && JSON.stringify(previous.observed) === JSON.stringify(profile) && previous.reason === normalizedReason && previous.evidence === evidence
45
+ && previous.source?.raw_model === source?.raw_model && previous.source?.raw_provider === source?.raw_provider) return previous;
46
+ const previousKnown = events.findLast(item => item.harness === harness && item.session_id === sessionId && item.evidence === evidence && item.observed?.model
47
+ && (item.source?.scope ?? "session") === (source?.scope ?? "session") && (item.source?.attempt_id ?? null) === (source?.attempt_id ?? null));
48
+ const fromModel = text(previousModel) ?? previousKnown?.observed.model ?? null;
49
+ const previousRouting = events.findLast(item => item.harness === harness && item.session_id === sessionId && item.evidence === evidence && item.observed?.provider
50
+ && (item.source?.scope ?? "session") === (source?.scope ?? "session") && (item.source?.attempt_id ?? null) === (source?.attempt_id ?? null));
51
+ const changed = Boolean(profile.model && fromModel && (profile.model !== fromModel || profile.provider && previousRouting && profile.provider !== previousRouting.observed.provider));
52
+ const value = { schema_id: "dd-flow/model-observation@1", sequence: events.length + 1,
53
+ harness, session_id: sessionId, parent_session_id: parentSessionId,
54
+ kind: changed ? "model_changed" : !profile.model ? "model_observation_unavailable" : "model_observed",
55
+ previous_model: fromModel, requested: observedFields(requested), observed: profile,
56
+ source, evidence, reason: normalizedReason, native_timestamp: text(nativeTimestamp), observed_at: new Date().toISOString() };
57
+ value.id = createHash("sha256").update(JSON.stringify(value)).digest("hex");
58
+ const handle = await open(file, "a", 0o600);
59
+ try {
60
+ const bytes = await readFile(file); const committed = bytes.lastIndexOf(10) + 1;
61
+ if (committed < bytes.length) await handle.truncate(committed);
62
+ await handle.write(`${JSON.stringify(value)}\n`); await handle.sync();
63
+ } finally { await handle.close(); }
64
+ const directory = await open(path.dirname(file), "r");
65
+ try { await directory.sync(); } finally { await directory.close(); }
66
+ return value;
67
+ });
68
+ }
69
+
70
+ export function modelAttribution(events, expectedSessions = []) {
71
+ const sessions = new Map();
72
+ for (const event of events) {
73
+ const key = `${event.harness}:${event.session_id}`;
74
+ const session = sessions.get(key) ?? { harness: event.harness, session_id: event.session_id, parent_session_id: event.parent_session_id, requested: event.requested, observations: [] };
75
+ session.parent_session_id ??= event.parent_session_id;
76
+ if (!session.requested || !Object.values(session.requested).some(Boolean)) session.requested = event.requested;
77
+ session.observations.push(event); sessions.set(key, session);
78
+ }
79
+ const observedIds = new Set([...sessions.values()].map(session => session.session_id));
80
+ const missingSessions = expectedSessions.filter(session => typeof session === "string" ? !observedIds.has(session)
81
+ : !sessions.has(`${session.harness}:${session.session_id}`));
82
+ const models = [...new Set(events.map(event => event.observed?.model).filter(Boolean))];
83
+ const providers = [...new Set(events.map(event => event.observed?.provider).filter(Boolean))];
84
+ const incomplete = !events.length || missingSessions.length > 0 || [...sessions.values()].some(session => {
85
+ const known = session.observations.filter(event => event.observed?.model);
86
+ if (!known.length) return true;
87
+ const knownScopes = new Set(known.map(event => `${event.evidence}:${event.source?.scope ?? "session"}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`));
88
+ return session.observations.some(event => {
89
+ if (event.source?.non_asserting) return false;
90
+ if (["configured", "response"].includes(event.evidence) && !event.observed?.model) return true;
91
+ if (event.evidence !== "unavailable") return false;
92
+ const scope = event.source?.scope;
93
+ return !scope || !knownScopes.has(`configured:${scope}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`)
94
+ && !knownScopes.has(`response:${scope}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`);
95
+ });
96
+ });
97
+ return { models, providers, mixed: models.length > 1 || providers.length > 1 || events.some(event => event.observed?.model && event.requested?.model && event.observed.model !== event.requested.model
98
+ || event.observed?.provider && event.requested?.provider && event.observed.provider !== event.requested.provider),
99
+ observation_completeness: incomplete ? "incomplete" : "available_native_sources", transitions: events.filter(event => event.kind === "model_changed"),
100
+ sessions: [...sessions.values()], missing_sessions: missingSessions, usage_attribution: "session_totals_unattributed_to_model", note: "Native settings/events do not prove undisclosed server routing or per-model token costs." };
101
+ }
@@ -0,0 +1,13 @@
1
+ export const NATIVE_CHILD_CONTRACT_VERSION: "native-children@1";
2
+ export interface NativeChild {
3
+ session_id: string;
4
+ parent_session_id: string;
5
+ status: "running" | "unknown" | "completed" | "failed" | "cancelled" | "settled_by_root";
6
+ source: string;
7
+ provenance?: "root_child" | "parent_mismatch";
8
+ terminal_turn_id?: string;
9
+ terminal_observed_at?: string;
10
+ provider_error?: unknown;
11
+ native_turn_items?: { observed?: boolean; possible_effects?: boolean; pending?: boolean };
12
+ }
13
+ export function normalizeNativeChildren(receipt: Record<string, unknown>, rootSessionId: string): NativeChild[];
@@ -0,0 +1,78 @@
1
+ export const NATIVE_CHILD_CONTRACT_VERSION = "native-children@1";
2
+ import { errorRecord } from "./operation-errors.mjs";
3
+
4
+ function nativeFailure(code, message, exitCode, details) { return Object.assign(new Error(message), { code, exitCode, details }); }
5
+
6
+
7
+ /** Normalize provider-observed topology only; never parse assistant prose. */
8
+ export function normalizeNativeChildren(receipt, rootSessionId) {
9
+ const candidates = [];
10
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
11
+ receipt = object(receipt);
12
+ const list = (value) => Array.isArray(value) ? value : [];
13
+ const text = (value, keys) => { for (const key of keys) if (typeof value[key] === "string" && value[key]) return value[key]; return null; };
14
+ const settledTree = new Set(
15
+ list(receipt.tree_observations)
16
+ .filter(value => object(value).settled === true)
17
+ .map(value => text(object(value), ["provider_session_id", "session_id"]))
18
+ .filter(id => id !== null)
19
+ );
20
+ const add = (raw, source, scopedParent = null, fallback = "unknown") => {
21
+ const value = object(raw);
22
+ const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
23
+ const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
24
+ if (!id) return;
25
+ const outcomeError = object(value.outcome_error);
26
+ if (typeof outcomeError.code === "string") {
27
+ const record = errorRecord(outcomeError);
28
+ throw Object.assign(nativeFailure(record.code, record.message, 1, record.details), record, { details: { ...object(record.details), session_id: id, source } });
29
+ }
30
+ const state = String(value.status ?? value.state ?? fallback).toLowerCase();
31
+ if (state === "lost") throw nativeFailure("native_child_outcome_lost", "Native child ended without a recoverable outcome", 1, { session_id: id, parent_session_id: parent, source, native_status: state });
32
+ // Provider idle means no current activity, not a Work result. Only an
33
+ // explicit terminal result or a separately proven root settlement may be
34
+ // terminal here; unknown states stay unknown for reconciliation.
35
+ const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
36
+ const rootChild = parent === rootSessionId;
37
+ const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
38
+ candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
39
+ ...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
40
+ ...(text(value, ["terminal_observed_at"]) ? { terminal_observed_at: text(value, ["terminal_observed_at"]) } : {}),
41
+ ...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
42
+ ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
43
+ };
44
+ for (const value of list(receipt.descendants)) add(value, "adapter.descendants");
45
+ for (const value of list(receipt.children)) add(value, "opencode.session.children");
46
+ const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
47
+ for (const value of list(subagents.running)) add(value, "zcode/session/subagents", rootSessionId, "running");
48
+ for (const value of list(subagents.completed)) add(value, "zcode/session/subagents", rootSessionId, "completed");
49
+ for (const value of list(object(subagents.ended).items)) {
50
+ const status = String(object(value).status ?? "unknown");
51
+ if (!["success", "failed", "cancelled", "lost"].includes(status)) throw nativeFailure("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
52
+ add(value, "zcode/session/subagents", rootSessionId);
53
+ }
54
+ const unique = new Map();
55
+ const rank = { unknown: 0, running: 1, settled_by_root: 2, completed: 3, failed: 3, cancelled: 3 };
56
+ for (const child of candidates) {
57
+ const previous = unique.get(child.session_id);
58
+ if (previous && previous.parent_session_id !== child.parent_session_id) {
59
+ // Keep the contradictory parent visible to the existing provenance gate,
60
+ // independently of input order or which source happened to say success.
61
+ const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
62
+ unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
63
+ continue;
64
+ }
65
+ if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status) throw nativeFailure("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
66
+ if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
67
+ throw nativeFailure("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
68
+ if (previous && previous.status === child.status) {
69
+ unique.set(child.session_id, { ...previous, ...child,
70
+ ...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
71
+ ...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
72
+ ...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
73
+ continue;
74
+ }
75
+ if (!previous || rank[child.status] > rank[previous.status]) unique.set(child.session_id, child);
76
+ }
77
+ return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
78
+ }
@@ -0,0 +1,7 @@
1
+ export function invokeNativeHook(input: { bin: string; args: string[]; payload: unknown; home?: string | undefined; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }): Promise<Record<string, unknown> & { timing?: Record<string, number> }>;
2
+ export function hookBudget(timeoutMs?: number, deadlineAt?: number | null): { deadlineAt: number; remaining(phase: string, maximum?: number): number; run<T>(action: () => T | Promise<T>, phase: string): Promise<T> };
3
+ export function readNativeHookInput(stream: NodeJS.ReadStream, budget: { remaining(phase: string): number }, maxBytes?: number): Promise<string>;
4
+ export function acceptNativeHookRequest(stateDir: string, binding: Record<string, unknown>, current?: { requests: Map<string, { binding: Record<string, unknown> }>; accepting?: Map<string, Promise<Record<string, unknown>>> } | null): Promise<Record<string, unknown>>;
5
+ export function writeNativeHookFailure(stateDir: string, binding: Record<string, unknown> | undefined, error: unknown, budget: ReturnType<typeof hookBudget>): Promise<void>;
6
+ export function readNativeHookFailures(stateDir: string, requests: Map<string, { current: unknown; binding: Record<string, unknown> }>, current: unknown, reject: (error: Error & { code?: string; details?: Record<string, unknown> }) => void): Promise<void>;
7
+ export function runBoundedCommand(input: { bin: string; args: string[]; input?: string | null; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; timeoutCode?: string; inheritDeadline?: boolean; cleanupWithinBudget?: boolean; maintenanceProgress?: { action: string; processId: string | null; startupMs?: number; inactivityMs: number } | null; signal?: AbortSignal }): Promise<string>;
@@ -0,0 +1,251 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { reportedFailure } from "./process-json.mjs";
6
+ import { errorRecord, reportedError } from "./operation-errors.mjs";
7
+ import { writeJsonAtomic } from "./runner-events.mjs";
8
+
9
+ /** Retain accepted native requests in the existing operation hook ledger. */
10
+ export async function acceptNativeHookRequest(stateDir, binding, current = null) {
11
+ if (["operation_id", "daemon_id", "tool_call_id", "root_provider_session_id", "provider_session_id"].some(key => typeof binding[key] !== "string" || !binding[key])
12
+ || !Number.isSafeInteger(binding.turn_generation) || binding.turn_generation < 0
13
+ || !/^[a-f0-9]{64}$/.test(binding.command_sha256 ?? "")) throw Object.assign(new Error("Native hook has no current owned operation"), { code: "native_hook_unproven" });
14
+ const key = JSON.stringify([binding.provider_session_id, binding.tool_call_id, binding.event]);
15
+ const existing = current && [...current.requests.values()].find(value => JSON.stringify([value.binding.provider_session_id, value.binding.tool_call_id, value.binding.event]) === key)?.binding;
16
+ const same = value => {
17
+ if (value.command_sha256 !== binding.command_sha256 || value.native_cwd !== binding.native_cwd) throw Object.assign(new Error("Native tool identity was reused with changed hook input"), { code: "native_hook_identity_conflict" });
18
+ return value;
19
+ };
20
+ if (existing) return same(existing);
21
+ current && (current.accepting ??= new Map());
22
+ if (current?.accepting.has(key)) return same(await current.accepting.get(key));
23
+ const accepted = (async () => {
24
+ const directory = path.join(stateDir, "operations", createHash("sha256").update(binding.operation_id).digest("hex"));
25
+ const requested = JSON.parse(await readFile(path.join(directory, "requested.json"), "utf8"));
26
+ if (requested.operation_id !== binding.operation_id || requested.daemon_id !== binding.daemon_id || !["session.prompt", "session.create", "session.start"].includes(requested.operation)
27
+ || requested.operation === "session.prompt" && requested.session_id && requested.session_id !== binding.root_provider_session_id) throw Object.assign(new Error("Native hook operation does not match its retained owner"), { code: "native_hook_unproven" });
28
+ const receipt = { ...binding, schema_id: "dd-flow/hook-request@1", request_id: randomUUID() };
29
+ await writeJsonAtomic(path.join(directory, "hooks", `${receipt.request_id}.json`), receipt);
30
+ return receipt;
31
+ })();
32
+ current?.accepting.set(key, accepted);
33
+ try { return await accepted; } finally { current?.accepting.delete(key); }
34
+ }
35
+
36
+ export async function writeNativeHookFailure(stateDir, binding, error, budget) {
37
+ if (!binding?.request_id || !/^[A-Za-z0-9-]{1,128}$/.test(binding.request_id)) return;
38
+ await budget.run(() => writeJsonAtomic(path.join(stateDir, "hook-failures", `${binding.request_id}.json`), {
39
+ ...errorRecord(error), request_id: binding.request_id, observed_at: new Date().toISOString(), native_hook_binding: binding,
40
+ }), "failure_receipt");
41
+ }
42
+
43
+ /** Read only requests accepted for this exact live operation, never scan a mailbox. */
44
+ export async function readNativeHookFailures(stateDir, requests, current, reject) {
45
+ for (const [id, accepted] of requests) {
46
+ if (accepted.current !== current) continue;
47
+ let value;
48
+ try { value = JSON.parse(await readFile(path.join(stateDir, "hook-failures", `${id}.json`), "utf8")); }
49
+ catch (error) { if (error.code === "ENOENT") continue; value = { code: "native_hook_receipt_invalid", message: error.message }; }
50
+ if (value.request_id !== id || !value.native_hook_binding || JSON.stringify(value.native_hook_binding) !== JSON.stringify(accepted.binding) || typeof value.code !== "string" || typeof value.message !== "string") {
51
+ if (!accepted.diagnostic) {
52
+ accepted.diagnostic = { code: "native_hook_receipt_invalid", message: "Accepted hook request has an unreadable or unbound failure receipt", request_id: id, operation_id: accepted.binding.operation_id, effect: "unknown", receipt_bound: false };
53
+ process.stderr.write(`${JSON.stringify({ native_hook_diagnostic: accepted.diagnostic })}\n`);
54
+ }
55
+ continue;
56
+ }
57
+ requests.delete(id);
58
+ const error = reportedError(value, "Native hook failed");
59
+ error.details = { ...error.details, native_hook_binding: accepted.binding };
60
+ reject(error);
61
+ }
62
+ }
63
+
64
+ /** The caller supplies its qualified native limit; phases share that deadline. */
65
+ export function hookBudget(timeoutMs = 25_000, deadlineAt = process.env.DD_FLOW_HOOK_DEADLINE_AT === undefined ? null : Number(process.env.DD_FLOW_HOOK_DEADLINE_AT)) {
66
+ if (deadlineAt !== null && (!Number.isSafeInteger(deadlineAt) || deadlineAt < 0)) throw Object.assign(new Error("Invalid native hook deadline"), { code: "hook_deadline_invalid", details: { effect: "unknown" } });
67
+ const started = performance.now();
68
+ const duration = Math.max(0, Math.min(timeoutMs, Number.isFinite(deadlineAt) ? deadlineAt - Date.now() : timeoutMs));
69
+ const remaining = (phase, maximum = duration) => {
70
+ const value = duration - (performance.now() - started);
71
+ if (value <= 0) throw Object.assign(new Error(`Native hook deadline exceeded during ${phase}`), { code: "native_hook_timeout", details: { phase, elapsed_ms: Math.round(performance.now() - started), effect: "unknown" } });
72
+ return Math.max(1, Math.floor(Math.min(maximum, value)));
73
+ };
74
+ return { deadlineAt: Math.floor(Date.now() + duration), remaining, async run(action, phase) {
75
+ const timeoutMs = remaining(phase);
76
+ let timer;
77
+ try { return await Promise.race([Promise.resolve().then(action), new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error(`Native hook deadline exceeded during ${phase}`), { code: "native_hook_timeout", details: { phase, effect: "unknown" } })), timeoutMs); })]); }
78
+ finally { clearTimeout(timer); }
79
+ } };
80
+ }
81
+
82
+ /** Bound native hook ingress before parsing or opening adapter state. */
83
+ export function readNativeHookInput(stream, budget, maxBytes = 1_048_576) {
84
+ return new Promise((resolve, reject) => {
85
+ const chunks = []; let bytes = 0, done = false;
86
+ const cleanup = () => { clearTimeout(timer); stream.off("data", onData); stream.off("end", onEnd); stream.off("error", onError); };
87
+ const finish = (error, value) => { if (done) return; done = true; cleanup(); if (error) { stream.destroy(); reject(error); } else resolve(value); };
88
+ const onData = chunk => {
89
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
90
+ bytes += buffer.length;
91
+ if (bytes > maxBytes) finish(Object.assign(new Error("Native hook input exceeds 1 MiB"), { code: "native_hook_input_too_large" }));
92
+ else chunks.push(buffer);
93
+ };
94
+ const onEnd = () => finish(null, Buffer.concat(chunks).toString("utf8"));
95
+ const onError = error => finish(error);
96
+ let timeoutMs;
97
+ try { timeoutMs = budget.remaining("input"); } catch (error) { reject(error); return; }
98
+ const timer = setTimeout(() => finish(Object.assign(new Error("Native hook deadline exceeded during input"), { code: "native_hook_timeout", details: { phase: "input", effect: "unknown" } })), timeoutMs);
99
+ stream.on("data", onData).once("end", onEnd).once("error", onError);
100
+ stream.resume();
101
+ });
102
+ }
103
+
104
+ export async function terminateCommandTree(child, { platform = process.platform, launch = spawn, timeoutMs = 400 } = {}) {
105
+ if (!child.pid) return;
106
+ if (platform !== "win32") { process.kill(-child.pid, "SIGKILL"); return; }
107
+ // Node's child.kill on Windows only terminates the leader. taskkill is the
108
+ // native subtree primitive; bound its own lifetime as well as the request.
109
+ await new Promise((resolve, reject) => {
110
+ const killer = launch("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
111
+ let settled = false, timer;
112
+ const finish = error => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(); };
113
+ timer = setTimeout(() => { killer.kill("SIGKILL"); killer.unref(); finish(Object.assign(new Error("Windows subtree cleanup timed out"), { code: "process_cleanup_timeout" })); }, timeoutMs);
114
+ killer.once("error", finish);
115
+ killer.once("close", code => finish(code === 0 ? null : Object.assign(new Error(`taskkill exited ${code}`), { code: "process_cleanup_failed" })));
116
+ });
117
+ }
118
+
119
+ /** Preserve the native hook protocol across the CLI process boundary. */
120
+ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}, timeoutMs = 15_000, timeoutCode = "native_hook_timeout", inheritDeadline = true, cleanupWithinBudget = false, maintenanceProgress = null, signal }) {
121
+ if (signal?.aborted) throw Object.assign(new Error("Bounded CLI request cancelled before launch"), { code: "process_maintenance_cancelled", details: { effect: "no_effect" } });
122
+ const inherited = inheritDeadline ? env.DD_FLOW_HOOK_DEADLINE_AT ?? process.env.DD_FLOW_HOOK_DEADLINE_AT : undefined;
123
+ if (inherited !== undefined) {
124
+ const deadline = Number(inherited);
125
+ if (!Number.isSafeInteger(deadline) || deadline < 0) throw Object.assign(new Error("Invalid native hook deadline"), { code: "hook_deadline_invalid", details: { effect: "no_effect" } });
126
+ timeoutMs = Math.min(timeoutMs, deadline - Date.now());
127
+ }
128
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw Object.assign(new Error("Native hook budget exhausted before subprocess launch"), { code: timeoutCode, details: { phase: "subprocess", effect: "no_effect" } });
129
+ if (maintenanceProgress && (!Number.isFinite(maintenanceProgress.inactivityMs) || maintenanceProgress.inactivityMs <= 0
130
+ || maintenanceProgress.startupMs !== undefined && (!Number.isFinite(maintenanceProgress.startupMs) || maintenanceProgress.startupMs <= 0)
131
+ || typeof maintenanceProgress.action !== "string" || !(maintenanceProgress.processId === null || typeof maintenanceProgress.processId === "string")))
132
+ throw Object.assign(new Error("Invalid maintenance progress budget"), { code: "process_maintenance_timeout", details: { effect: "no_effect", native_dispatch_started: false } });
133
+ const started = performance.now();
134
+ // Transport/physical cleanup has its own small tail inside the outer limit.
135
+ const cleanupMs = Math.min(500, timeoutMs / 5);
136
+ const commandMs = cleanupWithinBudget ? Math.max(1, timeoutMs - cleanupMs) : timeoutMs;
137
+ const deadlineAt = Math.floor(Date.now() + Math.max(0, commandMs - Math.min(250, commandMs / 10)));
138
+ const script = /\.[cm]?js$/.test(bin);
139
+ const output = await new Promise((resolve, reject) => {
140
+ const child = spawn(script ? process.execPath : bin, [...(script ? [bin] : []), ...args], {
141
+ cwd, env: { ...process.env, ...env, DD_FLOW_HOOK_DEADLINE_AT: String(deadlineAt) }, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"]
142
+ });
143
+ let stdout = "", stderr = "", failure, cleanupTimer, settled = false, termination;
144
+ let timer, phaseBuffer = "", lastProgress = started;
145
+ const progressPhases = new Set();
146
+ let importsReady = maintenanceProgress?.startupMs === undefined;
147
+ const silenceMs = () => importsReady ? maintenanceProgress?.inactivityMs : maintenanceProgress.startupMs;
148
+ const commandDeadline = started + commandMs;
149
+ const finish = (error, value) => {
150
+ if (settled) return;
151
+ settled = true; clearTimeout(timer); clearTimeout(cleanupTimer); signal?.removeEventListener("abort", abort);
152
+ if (error) {
153
+ const diagnostics = hookDiagnostics(stderr);
154
+ if (diagnostics.length) error.details = { ...error.details, hook_diagnostics: diagnostics };
155
+ reject(error);
156
+ } else resolve(value);
157
+ };
158
+ const stop = error => {
159
+ if (termination || settled) return;
160
+ failure ??= error;
161
+ clearTimeout(timer);
162
+ failure.details = { ...failure.details, failure_at: new Date().toISOString(), elapsed_ms: Math.round(performance.now() - started), effect: "unknown", subprocess_pid: child.pid ?? null };
163
+ termination = terminateCommandTree(child).catch(cleanup => {
164
+ if (cleanup.code === "ESRCH") return;
165
+ failure.details.cleanup_error = { code: cleanup.code, message: cleanup.message };
166
+ failure.details.cleanup_unconfirmed = true;
167
+ child.kill("SIGKILL");
168
+ });
169
+ cleanupTimer ??= setTimeout(() => {
170
+ failure.details.cleanup_unconfirmed = true;
171
+ child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy(); child.unref();
172
+ finish(failure);
173
+ }, cleanupWithinBudget ? cleanupMs : 500);
174
+ };
175
+ // A useful, request-bound phase advances the silence window, not ownership
176
+ // authority. The child storage deadline and parent uncertainty horizon stay
177
+ // fixed; repeated phases, logging and polling cannot keep a writer alive.
178
+ const arm = () => {
179
+ clearTimeout(timer);
180
+ const now = performance.now();
181
+ const remaining = Math.min(commandDeadline - now, maintenanceProgress ? lastProgress + silenceMs() - now : Infinity);
182
+ if (remaining <= 0) { stop(Object.assign(new Error("Bounded CLI request timed out"), { code: timeoutCode,
183
+ details: maintenanceProgress ? { phase: importsReady ? "storage" : "startup", inactivity_ms: silenceMs(),
184
+ ownership_horizon_ms: commandMs } : {} })); return; }
185
+ timer = setTimeout(arm, Math.ceil(remaining));
186
+ };
187
+ arm();
188
+ const abort = () => stop(Object.assign(new Error("Bounded CLI request cancelled"), { code: "process_maintenance_cancelled" }));
189
+ signal?.addEventListener("abort", abort, { once: true });
190
+ if (signal?.aborted) abort();
191
+ child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk; if (stdout.length > 1_048_576) stop(new Error("Native hook response exceeds protocol limit")); });
192
+ // Match the existing response bound: a legal bounded error tree may exceed
193
+ // 64KiB when pretty-printed, plus maintenance phases preceding the receipt.
194
+ child.stderr.setEncoding("utf8").on("data", chunk => {
195
+ stderr = (stderr + chunk).slice(-1_048_576);
196
+ if (!maintenanceProgress || termination || settled) return;
197
+ phaseBuffer += chunk;
198
+ const lines = phaseBuffer.split("\n"); phaseBuffer = lines.pop().slice(-16_384);
199
+ for (const line of lines) {
200
+ if (line.length > 16_384) continue;
201
+ let phase; try { phase = JSON.parse(line); } catch { continue; }
202
+ if (phase?.kind !== "process_maintenance_phase" || phase.action !== maintenanceProgress.action
203
+ || (phase.process_id ?? null) !== maintenanceProgress.processId || phase.native_dispatch_started !== false
204
+ || !["request", "imports_ready", "store_open", "store_validation", "lookup", "writer_reservation", "writer_reserved", "write", "commit", "committed", "ack"].includes(phase.phase)
205
+ || phase.store_kind !== undefined && !["run", "resource"].includes(phase.store_kind)) continue;
206
+ const key = `${phase.store_kind ?? ""}:${phase.phase}`;
207
+ if (progressPhases.has(key)) continue;
208
+ // Delayed observation cannot revive an already-expired silence window.
209
+ const now = performance.now();
210
+ if (now >= commandDeadline || now - lastProgress >= silenceMs()) { arm(); return; }
211
+ // Cold imports are not a stalled storage operation. Readiness is an
212
+ // actual boundary, never a timer heartbeat; the ownership horizon stays fixed.
213
+ if (["imports_ready", "store_open"].includes(phase.phase)) importsReady = true;
214
+ progressPhases.add(key); lastProgress = now; arm();
215
+ }
216
+ });
217
+ child.stdin.on("error", error => { failure ??= error; });
218
+ child.on("error", error => finish(failure ?? error));
219
+ child.on("close", code => {
220
+ if (failure) { void (termination ?? Promise.resolve()).then(() => finish(failure)); return; }
221
+ if (code !== 0) {
222
+ return finish(reportedFailure(stdout, stderr, "Native hook admission failed", "native_hook_failed"));
223
+ }
224
+ finish(null, stdout);
225
+ });
226
+ child.stdin.end(input);
227
+ });
228
+ return output;
229
+ }
230
+
231
+ /** Only our structured phase allowlist crosses a timeout, never raw stderr/env. */
232
+ function hookDiagnostics(stderr) {
233
+ return stderr.split("\n").flatMap(line => {
234
+ try {
235
+ const value = JSON.parse(line);
236
+ if (!["native_hook_phase", "process_maintenance_phase"].includes(value?.kind)) return [];
237
+ const phase = typeof value.phase === "string" && /^[a-z_]{1,48}$/.test(value.phase) ? value.phase : null;
238
+ if (!phase) return [];
239
+ return [{ phase, ...Object.fromEntries(["elapsed_ms", "remaining_ms", "sqlite_extended_code"].filter(key => Number.isFinite(value[key])).map(key => [key, value[key]])), ...(value.kind === "process_maintenance_phase" ? { ...Object.fromEntries(["action", "process_id", "operation_id", "lease_expires_at", "store_kind", "maintenance_effect", "attempt_kind", "role"].filter(key => typeof value[key] === "string" && /^[A-Za-z0-9:._-]{1,128}$/.test(value[key])).map(key => [key, value[key]])), ...(typeof value.native_dispatch_started === "boolean" ? { native_dispatch_started: value.native_dispatch_started } : {}) } : {}) }];
240
+ } catch { return []; }
241
+ }).slice(-16);
242
+ }
243
+
244
+ export async function invokeNativeHook({ bin, args, payload, home, cwd, env, timeoutMs = 15_000 }) {
245
+ const output = await runBoundedCommand({ bin, args, input: `${JSON.stringify(payload)}\n`, cwd, env: { ...env, DD_FLOW_HOME: home }, timeoutMs });
246
+ let result;
247
+ try { result = JSON.parse(output); } catch { throw Object.assign(new Error("Invalid native hook JSON response"), { code: "native_hook_response_invalid" }); }
248
+ if (result?.ok === false && typeof result.error?.code === "string") throw Object.assign(new Error(result.error.message ?? "Native hook failed"), { code: result.error.code, details: result.error.details });
249
+ if (!result || typeof result !== "object" || Array.isArray(result) || result.ok === false) throw Object.assign(new Error("Native hook did not return a successful protocol object"), { code: "native_hook_response_invalid" });
250
+ return result;
251
+ }
@@ -0,0 +1,45 @@
1
+ // Async iteration backpressures the native pipe while its existing evidence
2
+ // sink drains. Bound one JSON reply, never lifetime diagnostics.
3
+ export const MAX_NATIVE_LINE_BYTES = 8 * 1024 * 1024;
4
+ export async function readNativeLines(stream, onLine, onError) {
5
+ let buffer = "";
6
+ stream.setEncoding("utf8");
7
+ try {
8
+ for await (const chunk of stream.iterator({ destroyOnReturn: false })) {
9
+ buffer += chunk;
10
+ let at;
11
+ while ((at = buffer.indexOf("\n")) >= 0) {
12
+ const line = buffer.slice(0, at); buffer = buffer.slice(at + 1);
13
+ assertLine(line);
14
+ if (line.trim()) await onLine(line);
15
+ }
16
+ assertLine(buffer);
17
+ }
18
+ if (buffer.trim()) { assertLine(buffer); await onLine(buffer); }
19
+ } catch (cause) { stream.pause(); await observationFailure(cause, onError); }
20
+ }
21
+ export function assertNativeLine(line, phase = "reply") {
22
+ if (Buffer.byteLength(line) > MAX_NATIVE_LINE_BYTES) throw Object.assign(new Error(`Native JSON ${phase} exceeds the bounded capture limit`), { code: phase === "request" ? "native_request_limit" : "operation_observation_lost", details: { reason: `native_${phase}_limit`, limit_bytes: MAX_NATIVE_LINE_BYTES, ...(phase === "request" ? { effect: "no_effect" } : { native_outcome: "unknown" }) } });
23
+ }
24
+ const assertLine = assertNativeLine;
25
+ export function admitNativeRequestBuffer(buffer, socket, schema) {
26
+ try { assertNativeLine(buffer, "request"); return true; }
27
+ catch (error) { socket.pause(); socket.end(`${JSON.stringify({ schema_id: schema, id: null, ok: false, error: { code: error.code, message: error.message, details: error.details } })}\n`, () => socket.destroy()); return false; }
28
+ }
29
+ export function retainNativeReplyBuffer(buffer, finish) {
30
+ try { assertNativeLine(buffer); return true; }
31
+ catch (error) { finish(error); return false; }
32
+ }
33
+ export async function streamNativeDiagnostics(stream, onChunk, onError) {
34
+ stream.setEncoding("utf8");
35
+ try { for await (const chunk of stream.iterator({ destroyOnReturn: false })) await onChunk(String(chunk)); }
36
+ catch (cause) { stream.pause(); await observationFailure(cause, onError); }
37
+ }
38
+ async function observationFailure(cause, onError) {
39
+ const error = nativeObservationError(cause);
40
+ try { await onError(error); }
41
+ catch (publication) { error.cleanup_error ??= { code: publication.code ?? null, message: publication.message }; }
42
+ }
43
+ export function nativeObservationError(cause, details = {}) {
44
+ return !cause.code || /^[A-Z_]+$/.test(cause.code) ? Object.assign(new Error("Native evidence reader failed", { cause }), { code: "operation_observation_lost", details: { native_outcome: "unknown", observation_error: { code: cause.code ?? null, message: cause.message }, ...details } }) : cause;
45
+ }
@@ -0,0 +1,18 @@
1
+ export const OBSERVATION_CLOCK_CONTRACT_VERSION: "operation-progress@1";
2
+ export function validateObservationDuration(value: number, label?: string): number;
3
+ export interface ObservationState {
4
+ policy: string; timeout_ms: number; started_at: number; cursor?: unknown;
5
+ observed_at: number | null; elapsed_ms: number; uncertainty_elapsed_ms: number;
6
+ gaps: number; observation_lost: boolean;
7
+ }
8
+ export class ObservationClock {
9
+ constructor(options: { timeoutMs: number; gapMs?: number; uncertaintyMs?: number;
10
+ wall?: () => number; monotonic?: () => number; onGap?: (value: Record<string, unknown>) => void;
11
+ saved?: ObservationState | null });
12
+ observationLost: boolean;
13
+ sample(progress?: unknown, progressAt?: number): boolean;
14
+ state(): ObservationState;
15
+ }
16
+ export function observedTimeout(callback: (clock: ObservationClock) => void, timeoutMs: number,
17
+ options?: { gapMs?: number; uncertaintyMs?: number; wall?: () => number; monotonic?: () => number;
18
+ onGap?: (value: Record<string, unknown>) => void; progress?: () => unknown; saved?: ObservationState | null }): ReturnType<typeof setInterval>;
@@ -0,0 +1,92 @@
1
+ export const OBSERVATION_CLOCK_CONTRACT_VERSION = "operation-progress@1";
2
+ export function validateObservationDuration(value, label = "observation duration") {
3
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647)
4
+ throw new RangeError(`${label} must be integer milliseconds in 1..2147483647`);
5
+ return value;
6
+ }
7
+
8
+ /** Only observed monotonic time is silence. A clock gap is uncertainty, never
9
+ * progress or proof of provider failure. Durable snapshots retain that episode. */
10
+ export class ObservationClock {
11
+ constructor({ timeoutMs, gapMs = 60_000, uncertaintyMs = 120_000, wall = Date.now,
12
+ monotonic = () => performance.now(), onGap = () => {}, saved = null }) {
13
+ validateObservationDuration(timeoutMs); validateObservationDuration(gapMs, "gap duration");
14
+ validateObservationDuration(uncertaintyMs, "observation recovery duration");
15
+ Object.assign(this, { timeoutMs, gapMs, uncertaintyMs, wall, monotonic, onGap });
16
+ this.lastWall = wall(); this.lastMono = monotonic(); this.startedWall = this.lastWall;
17
+ this.elapsed = 0; this.progress = undefined; this.progressAt = null;
18
+ this.uncertain = false; this.uncertaintyElapsed = 0; this.gaps = 0; this.observationLost = false;
19
+ if (saved) {
20
+ if (saved.policy !== OBSERVATION_CLOCK_CONTRACT_VERSION || saved.timeout_ms !== timeoutMs
21
+ || !Number.isFinite(saved.started_at) || !Number.isSafeInteger(saved.gaps) || saved.gaps < 0
22
+ || saved.observed_at !== null && !Number.isFinite(saved.observed_at)
23
+ || typeof saved.observation_lost !== "boolean"
24
+ || !Number.isFinite(saved.elapsed_ms) || saved.elapsed_ms < 0
25
+ || !Number.isFinite(saved.uncertainty_elapsed_ms) || saved.uncertainty_elapsed_ms < 0)
26
+ throw new RangeError("invalid retained observation clock");
27
+ this.startedWall = saved.started_at; this.elapsed = saved.elapsed_ms;
28
+ this.progress = saved.cursor; this.progressAt = saved.observed_at;
29
+ // A reconstructed observer cannot prove silence during its absence.
30
+ this.uncertain = true; this.gaps = saved.gaps + 1;
31
+ this.uncertaintyElapsed = saved.uncertainty_elapsed_ms;
32
+ this.observationLost = saved.observation_lost || this.gaps > 1 || this.uncertaintyElapsed >= uncertaintyMs;
33
+ }
34
+ }
35
+ sample(progress, progressAt) {
36
+ if (progress && typeof progress === "object" && Object.hasOwn(progress, "cursor")) {
37
+ progressAt = progress.observed_at; progress = progress.cursor;
38
+ }
39
+ const wall = this.wall(), mono = this.monotonic();
40
+ const delta = mono - this.lastMono, wallDelta = wall - this.lastWall;
41
+ const gap = !Number.isFinite(delta) || !Number.isFinite(wallDelta)
42
+ || delta > this.gapMs || wallDelta > this.gapMs || delta < 0 || wallDelta < 0;
43
+ if (gap) {
44
+ this.uncertain = true; this.gaps++;
45
+ this.onGap({ source: "observer_clock", classification: "observation_gap", confirmed_sleep: false,
46
+ started_at: Number.isFinite(this.lastWall) ? this.lastWall : null, observed_at: Number.isFinite(wall) ? wall : null,
47
+ wall_delta_ms: wallDelta, monotonic_delta_ms: delta });
48
+ } else {
49
+ this.elapsed += delta;
50
+ if (this.uncertain) this.uncertaintyElapsed += delta;
51
+ }
52
+ this.lastWall = wall; this.lastMono = mono;
53
+ const validCursor = typeof progress === "string" || typeof progress === "boolean"
54
+ || typeof progress === "number" && Number.isFinite(progress);
55
+ const changed = validCursor && progress !== this.progress;
56
+ const timestamped = progressAt !== undefined;
57
+ const validTime = !timestamped ? wall >= this.startedWall && (this.progressAt === null || wall >= this.progressAt)
58
+ : Number.isFinite(progressAt) && progressAt >= this.startedWall
59
+ && progressAt <= wall && (this.progressAt === null || progressAt >= this.progressAt);
60
+ if (changed && validTime && !this.observationLost) {
61
+ const eventAt = timestamped ? progressAt : wall;
62
+ // A delayed pre-gap event cannot establish that observation has recovered.
63
+ const recovers = !this.uncertain || eventAt >= wall - Math.max(0, gap ? 0 : delta);
64
+ this.progress = progress; this.progressAt = eventAt;
65
+ this.elapsed = Math.max(0, wall - eventAt);
66
+ if (recovers) { this.uncertain = false; this.gaps = 0; this.uncertaintyElapsed = 0; }
67
+ }
68
+ if (changed && timestamped && Number.isFinite(progressAt) && !validTime && progressAt > wall) {
69
+ if (!this.uncertain) this.onGap({ source: "observer_clock", classification: "clock_skew",
70
+ confirmed_sleep: false, observed_at: wall, signal_at: progressAt });
71
+ this.uncertain = true;
72
+ }
73
+ if (this.uncertain && (this.gaps > 1 || this.uncertaintyElapsed >= this.uncertaintyMs)) this.observationLost = true;
74
+ return this.observationLost || !this.uncertain && this.elapsed >= this.timeoutMs;
75
+ }
76
+ state() {
77
+ return { policy: OBSERVATION_CLOCK_CONTRACT_VERSION, timeout_ms: this.timeoutMs,
78
+ started_at: this.startedWall, cursor: this.progress, observed_at: this.progressAt,
79
+ elapsed_ms: this.elapsed, uncertainty_elapsed_ms: this.uncertaintyElapsed,
80
+ gaps: this.gaps, observation_lost: this.observationLost };
81
+ }
82
+ }
83
+
84
+ /** Node timer handle, compatible with existing clearTimeout cleanup. */
85
+ export function observedTimeout(callback, timeoutMs, options = {}) {
86
+ const clock = new ObservationClock({ timeoutMs, ...options });
87
+ const timer = setInterval(() => {
88
+ const snapshot = options.progress?.();
89
+ if (clock.sample(snapshot)) { clearInterval(timer); callback(clock); }
90
+ }, Math.max(1, Math.min(1_000, timeoutMs / 4)));
91
+ return timer;
92
+ }
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ // The existing runner operation owns its child driver requests, including calls
4
+ // nested in Judge/HITL/fanout. Async context keeps parallel executions separate.
5
+ export const operationContext = new AsyncLocalStorage();