@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { managedFlowEnv } from './managed-flow-env.mjs';
|
|
1
2
|
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
2
3
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
3
4
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
4
5
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
6
|
+
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
5
7
|
import { spawn } from "node:child_process";
|
|
6
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
9
|
import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
promptSessionWithBridge,
|
|
25
27
|
zcodeChildSessionIds,
|
|
26
28
|
observeClosedZcodeTree,
|
|
29
|
+
readZcodeSubagents,
|
|
27
30
|
} from "./dd-zcode.mjs";
|
|
28
31
|
|
|
29
32
|
const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
|
|
@@ -89,7 +92,7 @@ async function removeSocket(socket, dir) {
|
|
|
89
92
|
const errorPayload = errorRecord;
|
|
90
93
|
|
|
91
94
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
92
|
-
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
95
|
+
const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
|
|
93
96
|
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'zcode', timeoutMs });
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -109,22 +112,21 @@ function sameConfig(state, config) {
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
export async function startDaemon(options) {
|
|
115
|
+
if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined, projectRoot: undefined };
|
|
112
116
|
const paths = locations(options.stateDir);
|
|
113
117
|
const hasFlowContext = Boolean(options.ddFlowHome || options.projectRoot || options.ddFlowBin);
|
|
114
|
-
if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot)) {
|
|
115
|
-
throw new DaemonError("flow_context_incomplete", "--dd-flow-home and --project-root must be provided together for ZCode lifecycle forwarding");
|
|
118
|
+
if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot || !options.ddFlowBin)) {
|
|
119
|
+
throw new DaemonError("flow_context_incomplete", "--dd-flow-home, --dd-flow-bin and --project-root must be provided together for ZCode lifecycle forwarding");
|
|
116
120
|
}
|
|
117
121
|
const config = {
|
|
118
122
|
cwd: absolute(options.cwd, "--cwd"),
|
|
119
123
|
journal: absolute(options.journal, "--journal"),
|
|
120
124
|
bin: options.bin ?? null,
|
|
121
125
|
zcodePath: options.zcodePath ?? null,
|
|
122
|
-
|
|
123
|
-
// for the trusted event bridge unless a pinned executable was supplied.
|
|
124
|
-
ddFlowBin: hasFlowContext ? (options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : "dd-flow") : null,
|
|
126
|
+
ddFlowBin: hasFlowContext ? absolute(options.ddFlowBin, "--dd-flow-bin") : null,
|
|
125
127
|
ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
|
|
126
128
|
projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
|
|
127
|
-
resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
129
|
+
resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
128
130
|
env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
|
|
129
131
|
livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
|
|
130
132
|
requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
|
|
@@ -203,6 +205,7 @@ export async function stopDaemon(options) {
|
|
|
203
205
|
export class DaemonRuntime {
|
|
204
206
|
constructor(paths, state, bridge, initialized) {
|
|
205
207
|
this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized;
|
|
208
|
+
this.state.capabilities = { ...(this.state.capabilities ?? {}), retained_child_observation: true };
|
|
206
209
|
this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item]));
|
|
207
210
|
this.liveAdapters = new Set();
|
|
208
211
|
this.activeProductive = null; this.server = null; this.persisting = Promise.resolve();
|
|
@@ -439,13 +442,46 @@ export class DaemonRuntime {
|
|
|
439
442
|
|
|
440
443
|
async dispatch(operation, params, operationId = null) {
|
|
441
444
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
445
|
+
if (operation === "session.observe-children") {
|
|
446
|
+
assertDaemonDispatch(this.paths.dir);
|
|
447
|
+
const root = this.sessions.get(params.sessionId);
|
|
448
|
+
if (!root || root.close_receipt) throw new DaemonError("session_identity_mismatch", "Retained observation requires a registered open root Session");
|
|
449
|
+
const generation = this.controlGeneration ?? 0;
|
|
450
|
+
const active = this.state.active_request;
|
|
451
|
+
const activeIdentity = value => JSON.stringify([value?.operation_id ?? null, value?.session_id ?? null, value?.adapter_session_id ?? null, value?.generation ?? null]);
|
|
452
|
+
const capturedIdentity = activeIdentity(active);
|
|
453
|
+
if (active && active.session_id !== params.sessionId) throw new DaemonError("session_identity_mismatch", "Retained observation has a foreign active root");
|
|
454
|
+
const cacheScope = `${params.sessionId}:${generation}`;
|
|
455
|
+
if (this.childObservationScope !== cacheScope) { this.childObservationScope = cacheScope; this.childTerminalCache = new Map(); this.childObservationKeys = new Set(); }
|
|
456
|
+
const subagents = await readZcodeSubagents(this.bridge, root.adapter_session_id, 30_000, true, this.childTerminalCache);
|
|
457
|
+
if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "Observation generation was retired");
|
|
458
|
+
if (activeIdentity(this.state.active_request) !== capturedIdentity) throw new DaemonError("operation_cancelled", "Observation belongs to a retired active Turn");
|
|
459
|
+
assertDaemonDispatch(this.paths.dir);
|
|
460
|
+
const receipt = { schema_id: "dd-zcode/child-observation@1", daemon_id: this.state.daemon_id, provider_session_id: params.sessionId,
|
|
461
|
+
generation, active_operation_id: active?.operation_id ?? null, subagents };
|
|
462
|
+
const failed = (subagents.ended?.items ?? subagents.completed ?? []).filter(child => child.status === "failed" && child.terminal_turn_id);
|
|
463
|
+
const fresh = failed.filter(child => !this.childObservationKeys.has(`${child.childSessionId}:${child.terminal_turn_id}`));
|
|
464
|
+
if (fresh.length) {
|
|
465
|
+
await this.bridge.journal.write("native_child_observation", { ...receipt, subagents: { running: [], ended: { total: fresh.length, items: fresh } } });
|
|
466
|
+
await this.bridge.journal.flush();
|
|
467
|
+
if (this.state.config?.journal) {
|
|
468
|
+
const journal = await open(this.state.config.journal, "a");
|
|
469
|
+
try { await journal.sync(); } finally { await journal.close(); }
|
|
470
|
+
}
|
|
471
|
+
for (const child of fresh) this.childObservationKeys.add(`${child.childSessionId}:${child.terminal_turn_id}`);
|
|
472
|
+
}
|
|
473
|
+
if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "Observation generation was retired before ACK");
|
|
474
|
+
if (activeIdentity(this.state.active_request) !== capturedIdentity) throw new DaemonError("operation_cancelled", "Observation Turn changed before ACK");
|
|
475
|
+
assertDaemonDispatch(this.paths.dir);
|
|
476
|
+
return receipt;
|
|
477
|
+
}
|
|
442
478
|
if (operation === "daemon.status") return {
|
|
443
479
|
daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket,
|
|
444
480
|
cwd: this.state.cwd, journal: this.state.journal, versions: this.state.versions,
|
|
445
481
|
started_at: this.state.started_at, updated_at: this.state.updated_at,
|
|
446
482
|
shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status,
|
|
447
483
|
active_tree: this.state.active_tree, active_operation: this.activeProductive, active_request: this.state.active_request ?? null,
|
|
448
|
-
sessions: [...this.sessions.values()], config: this.state.config,
|
|
484
|
+
sessions: [...this.sessions.values()], config: this.state.config, capabilities: { retained_child_observation: true },
|
|
449
485
|
};
|
|
450
486
|
if (operation === "hook.resolve") {
|
|
451
487
|
const sessionId = String(params.sessionId ?? "");
|
|
@@ -648,8 +684,7 @@ export class DaemonRuntime {
|
|
|
648
684
|
}
|
|
649
685
|
|
|
650
686
|
export function zcodeBridgeEnv(config, daemonId) {
|
|
651
|
-
return { ...config.env,
|
|
652
|
-
DD_FLOW_RUNTIME_OWNER: config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined };
|
|
687
|
+
return { ...config.env, ...managedFlowEnv(config, daemonId) };
|
|
653
688
|
}
|
|
654
689
|
|
|
655
690
|
export async function serveDaemon(stateDir) {
|
|
@@ -544,7 +544,7 @@ async function resume(bridge, sessionId, cwd) {
|
|
|
544
544
|
|
|
545
545
|
/** Ended children are paginated by the native API (20 by default). A partial
|
|
546
546
|
* directory is never settlement evidence, including during teardown. */
|
|
547
|
-
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
|
|
547
|
+
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false, terminalCache = new Map()) {
|
|
548
548
|
const clock = new ObservationClock({ timeoutMs });
|
|
549
549
|
const cursors = new Set(), ended = new Map();
|
|
550
550
|
let first, cursor;
|
|
@@ -575,6 +575,8 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
575
575
|
if (child.status !== "failed" || !Number.isSafeInteger(child.startedAt) || child.startedAt <= 0
|
|
576
576
|
|| !Number.isSafeInteger(child.endedAt) || child.endedAt < child.startedAt || child.endedAt > 8.64e15) { items.push(child); continue; }
|
|
577
577
|
// Closed children must never be loaded merely to explain their failure.
|
|
578
|
+
const cacheKey = JSON.stringify([child.childSessionId, child.startedAt, child.endedAt]);
|
|
579
|
+
if (terminalCache.has(cacheKey)) { items.push(terminalCache.get(cacheKey)); continue; }
|
|
578
580
|
let terminal;
|
|
579
581
|
try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, startedAt: child.startedAt, endedAt: child.endedAt }, timeoutMs); }
|
|
580
582
|
catch (error) { items.push({ ...child, terminal_metadata_unavailable: error.code ?? "terminal_metadata_unavailable" }); continue; }
|
|
@@ -584,8 +586,10 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
584
586
|
|| terminal.startedAt !== child.startedAt || terminal.endedAt !== child.endedAt
|
|
585
587
|
|| !Number.isFinite(observedAt) || observedAt < child.startedAt || observedAt > child.endedAt) fail("ZCode terminal failure metadata has foreign identity", "native_child_turn_conflict");
|
|
586
588
|
const providerError = safeNativeFailure(terminal.provider_error);
|
|
587
|
-
|
|
588
|
-
...(providerError ? { provider_error: providerError } : {}) }
|
|
589
|
+
const item = { ...child, terminal_turn_id: terminal.terminal_turn_id, terminal_observed_at: terminal.terminal_observed_at,
|
|
590
|
+
...(providerError ? { provider_error: providerError } : {}) };
|
|
591
|
+
terminalCache.set(cacheKey, item);
|
|
592
|
+
items.push(item);
|
|
589
593
|
clock.sample(child.childSessionId);
|
|
590
594
|
}
|
|
591
595
|
return { ...first, ended: { total: first.ended.total, items } };
|
|
@@ -49,7 +49,7 @@ export function delegationContract(harness) {
|
|
|
49
49
|
return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
52
|
+
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
53
53
|
|
|
54
54
|
export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
|
|
55
55
|
|
|
@@ -71,7 +71,9 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
|
|
|
71
71
|
"Your first technical action must be this exact lifecycle command:",
|
|
72
72
|
startCommand,
|
|
73
73
|
"Execute the issued operation and target exactly. Lifecycle invocation IDs are internal runtime authority and are intentionally omitted; never add or invent them. Preserve any explicit native receipt handle that the command does contain.",
|
|
74
|
-
"
|
|
74
|
+
startCommand.startsWith("cd '")
|
|
75
|
+
? "Execute the literal cd shell prefix exactly; it routes this command into its runtime-owned private execution copy. Do not invent native spawn fields. After start, obey the packet's read_only/write scopes and prefix subsequent issued lifecycle commands with the same literal cd route. A review copy never grants permission to write reviewed inputs. The runtime independently checks the CLI process directory."
|
|
76
|
+
: "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
|
|
75
77
|
"Continue only if it succeeds and returns the authoritative Work packet.",
|
|
76
78
|
lifecycleRetryInstruction,
|
|
77
79
|
"After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
|
|
@@ -109,7 +111,7 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
|
|
|
109
111
|
|
|
110
112
|
export function renderCapacityInstructions({ harness, maximum }) {
|
|
111
113
|
const adapter = requireAdapter(harness);
|
|
112
|
-
if (!Number.isInteger(maximum) || maximum < 1) throw new Error("
|
|
114
|
+
if (!Number.isInteger(maximum) || maximum < 1 || maximum > 15) throw new Error("Native capacity limit must be an integer from 1 through 15");
|
|
113
115
|
return [
|
|
114
116
|
`Adapter delegation contract: ${delegationContract(harness)}.`,
|
|
115
117
|
"This is a technical native-subagent capacity qualification, not product work.",
|
|
@@ -255,6 +255,7 @@ export function parseCommandArgs(argv) {
|
|
|
255
255
|
const positional = [], options = new Map();
|
|
256
256
|
for (let index = 0; index < argv.length; index++) {
|
|
257
257
|
const value = argv[index];
|
|
258
|
+
if (value === "--") { positional.push(...argv.slice(index + 1)); break; }
|
|
258
259
|
if (value?.startsWith("--")) {
|
|
259
260
|
const equals = value.indexOf("=");
|
|
260
261
|
if (equals > 2) { const key = value.slice(2, equals); options.set(key, [...(options.get(key) ?? []), value.slice(equals + 1)]); continue; }
|
|
@@ -8,5 +8,6 @@ export function confirmDaemonProcess(config: RuntimeConfig, record: ProcessBindi
|
|
|
8
8
|
export function assertDaemonOwnership(stateDir: string, dispatch?: Record<string, unknown>, assertCurrent?: () => void): Promise<void>;
|
|
9
9
|
export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined): Promise<void>;
|
|
10
10
|
export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
|
|
11
|
+
export function cleanupFailedDaemonStart(config: RuntimeConfig, record: ProcessBinding | null, child: ChildProcess | null, original: Error & { details?: Record<string, unknown> }, readState: () => Promise<Record<string, unknown>>, daemonId: string): Promise<void>;
|
|
11
12
|
export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
12
13
|
export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
|
|
@@ -61,14 +61,16 @@ export async function prepareRuntimeOwner(config, stateDir, env = process.env) {
|
|
|
61
61
|
export async function assertDaemonOwnership(stateDir, dispatch = {}, assertCurrent = () => {}, progress = async () => {}, waiting = async () => {}) {
|
|
62
62
|
const gate = async () => {
|
|
63
63
|
assertCurrent();
|
|
64
|
+
let confirmed = false;
|
|
64
65
|
for (const lease of leases.values()) {
|
|
65
66
|
if (lease.stateDir !== path.resolve(stateDir)) continue;
|
|
66
67
|
await reconfirmLease(lease, assertCurrent);
|
|
67
68
|
assertCurrent();
|
|
69
|
+
confirmed = true;
|
|
68
70
|
}
|
|
71
|
+
if (confirmed) await progress('ownership_confirmed');
|
|
69
72
|
};
|
|
70
73
|
await gate();
|
|
71
|
-
await progress('ownership_confirmed');
|
|
72
74
|
const state = JSON.parse(await readFile(path.join(stateDir, "daemon.json"), "utf8"));
|
|
73
75
|
const owner = bindRuntimeOwner({ runtime_owner: state.config?.runtime_owner ?? null }, {});
|
|
74
76
|
if (!owner) return;
|
|
@@ -264,7 +266,7 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
|
|
|
264
266
|
const poisonKey = token => JSON.stringify([path.resolve(resourceHome), options.id, token ?? null]);
|
|
265
267
|
if (action !== "status" && options.id) {
|
|
266
268
|
const poison = maintenancePoison.get(poisonKey(options["lease-token"])) ?? maintenancePoison.get(poisonKey(null));
|
|
267
|
-
if (poison) throw poison;
|
|
269
|
+
if (poison && !(action === "finish" && poison.finish === JSON.stringify(options))) throw poison.error;
|
|
268
270
|
}
|
|
269
271
|
const args = ["runtime", "process", action];
|
|
270
272
|
for (const [key, value] of Object.entries(options)) if (value !== null && value !== undefined) args.push(`--${key}`, String(value));
|
|
@@ -275,9 +277,13 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
|
|
|
275
277
|
try { output = await runBoundedCommand({ bin: ddFlowBin, args: [...args, "--json"], cwd: config.cwd,
|
|
276
278
|
env: { ...(config.env ?? {}), DD_FLOW_HOME: ddFlowHome, DD_FLOW_RESOURCE_HOME: resourceHome }, timeoutMs,
|
|
277
279
|
timeoutCode: action === "heartbeat" ? "process_heartbeat_timeout" : "process_maintenance_timeout", inheritDeadline: false, cleanupWithinBudget: true,
|
|
278
|
-
|
|
280
|
+
// Storage silence is not loss of authority: the original command/lease
|
|
281
|
+
// deadline remains finite, including slow I/O and multiple bounded reads.
|
|
282
|
+
maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: timeoutMs }, signal }); }
|
|
279
283
|
catch (error) {
|
|
280
|
-
if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]),
|
|
284
|
+
if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), {
|
|
285
|
+
error, ...(action === "finish" && error.code === "process_maintenance_timeout" ? { finish: JSON.stringify(options) } : {})
|
|
286
|
+
});
|
|
281
287
|
throw error;
|
|
282
288
|
}
|
|
283
289
|
let result;
|
|
@@ -393,7 +399,9 @@ export async function heartbeatDaemonProcess(config, record) {
|
|
|
393
399
|
if (lease.closing) throw Object.assign(new Error("Managed process is closing"), { code: "process_closing" });
|
|
394
400
|
if (lease.lost) throw lease.error;
|
|
395
401
|
if (lease.error && !maintenanceRetryable(lease.error)) throw lease.error;
|
|
396
|
-
if (!renewalRemaining(lease.renewal)) throw lease.error
|
|
402
|
+
if (!renewalRemaining(lease.renewal)) throw lease.error ?? Object.assign(new Error("Managed lease authority has expired before heartbeat dispatch"), {
|
|
403
|
+
code: "process_ownership_unknown", details: { process_id: record.id, reason: "lease_authority_expired" }
|
|
404
|
+
});
|
|
397
405
|
if (lease.pending) return await lease.pending;
|
|
398
406
|
lease.pending = (async () => {
|
|
399
407
|
try {
|
|
@@ -425,7 +433,7 @@ export async function finishDaemonProcess(config, process, state = "stopped", re
|
|
|
425
433
|
// A predecessor owns its transport fence; finish gets an independent
|
|
426
434
|
// settlement window only after that flight physically settled.
|
|
427
435
|
const settlement = createRenewalState(); settlement.uncertainSince = performance.now();
|
|
428
|
-
const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement);
|
|
436
|
+
const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement, true);
|
|
429
437
|
if (result?.ok !== true) throw Object.assign(new Error("managed process finish was not accepted"), { code: "process_lease_lost", details: { process_id: process.id } });
|
|
430
438
|
leases.delete(key);
|
|
431
439
|
}
|
|
@@ -453,11 +461,16 @@ async function reconfirmLease(lease, assertCurrent) {
|
|
|
453
461
|
return;
|
|
454
462
|
}
|
|
455
463
|
}
|
|
456
|
-
async function retryBookkeeping(action, renewal = createRenewalState()) {
|
|
464
|
+
async function retryBookkeeping(action, renewal = createRenewalState(), repeatFinish = false) {
|
|
457
465
|
renewal.uncertainSince ??= performance.now();
|
|
458
466
|
for (;;) {
|
|
459
467
|
try { return await action(renewalRemaining(renewal)); }
|
|
460
|
-
catch (error) {
|
|
468
|
+
catch (error) {
|
|
469
|
+
// Exact finish is an idempotent terminal-state CAS, not productive IO.
|
|
470
|
+
// Its old helper may still commit the same finish; all other writes stay fenced.
|
|
471
|
+
if (!(maintenanceRetryable(error) || repeatFinish && error.code === "process_maintenance_timeout" && error.details?.cleanup_unconfirmed === true) || !renewalFailure(renewal)) throw error;
|
|
472
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(renewalDelay(renewal), renewalRemaining(renewal))));
|
|
473
|
+
}
|
|
461
474
|
}
|
|
462
475
|
}
|
|
463
476
|
|
|
@@ -482,7 +495,10 @@ function requireTimelyMaintenance(renewal, action, receipt, record) {
|
|
|
482
495
|
export async function cleanupFailedStart(config, record, child, original) {
|
|
483
496
|
try {
|
|
484
497
|
await stopProcessGroup(child);
|
|
498
|
+
const stopped = child?.pid ? { pid: child.pid, pgid: process.platform === "win32" ? null : child.pid,
|
|
499
|
+
settled: true, physical_exit: "ESRCH", observed_at: new Date().toISOString() } : undefined;
|
|
485
500
|
await finishDaemonProcess(config, record, "failed", original.code ?? "startup_failed");
|
|
501
|
+
return stopped;
|
|
486
502
|
} catch (cleanup) {
|
|
487
503
|
original.details = { ...original.details, cleanup_failed: errorRecord(cleanup), process_id: record?.id ?? null, pid: child?.pid ?? null };
|
|
488
504
|
}
|
|
@@ -491,7 +507,7 @@ export async function cleanupFailedStart(config, record, child, original) {
|
|
|
491
507
|
/** The provider has its own process group. A failed daemon start must settle
|
|
492
508
|
* that exact retained process as well as the daemon; PID-only cleanup is unsafe. */
|
|
493
509
|
export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild, original, readState, daemonId) {
|
|
494
|
-
await cleanupFailedStart(config, daemonRecord, daemonChild, original);
|
|
510
|
+
const stopped = await cleanupFailedStart(config, daemonRecord, daemonChild, original);
|
|
495
511
|
if (!daemonRecord) return;
|
|
496
512
|
try {
|
|
497
513
|
const state = await readState();
|
|
@@ -502,6 +518,15 @@ export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild
|
|
|
502
518
|
}
|
|
503
519
|
const provider = state.provider_process;
|
|
504
520
|
if (provider) await runtimeProcess(config, "stop", { id: provider.id, "lease-token": provider.lease_token });
|
|
521
|
+
// Confirmation can fail before the registry learns the child's PID. The
|
|
522
|
+
// owned child handle still proves its physical exit, not a productive ACK.
|
|
523
|
+
if (stopped && !original.details?.cleanup_failed && !original.details?.provider_cleanup_failed && state.pid === stopped.pid && !provider && state.provider_pid == null && owner) {
|
|
524
|
+
original.details = { ...original.details, startup_child_cleanup: {
|
|
525
|
+
...stopped, daemon_id: daemonId, process_id: daemonRecord.id,
|
|
526
|
+
owner_id: owner.owner_id, project_id: owner.project_id, run_id: owner.run_id,
|
|
527
|
+
operation_id: owner.operation_id, state_dir: owner.state_dir
|
|
528
|
+
} };
|
|
529
|
+
}
|
|
505
530
|
} catch (cleanup) {
|
|
506
531
|
original.details = { ...original.details, provider_cleanup_failed: errorRecord(cleanup) };
|
|
507
532
|
}
|
|
@@ -587,29 +612,39 @@ async function stopOwnedChild(child, graceMs, detached, settlementMs) {
|
|
|
587
612
|
|
|
588
613
|
/** A daemon acknowledgement is not a completed stop until its endpoint closes. */
|
|
589
614
|
export async function confirmDaemonStopped(request, socket, timeoutMs = 30_000, { readState } = {}) {
|
|
590
|
-
const deadline = performance.now() + timeoutMs;
|
|
591
615
|
const expected = await readState?.();
|
|
592
616
|
const identity = state => state?.daemon_id ?? state?.config?.daemonId;
|
|
593
617
|
if (readState && (!identity(expected) || !Number.isSafeInteger(expected?.pid) || expected.pid < 1)) throw Object.assign(new Error('daemon stop has no retained incarnation/PID proof'), { code: 'daemon_cleanup_identity_mismatch' });
|
|
594
618
|
const failure = async observer => {
|
|
595
619
|
const state = await readState?.();
|
|
596
|
-
if (expected && identity(state) !== identity(expected)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
|
|
620
|
+
if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
|
|
597
621
|
const primary = state?.shutdown_state === "cleanup_failed" ? state.cleanup_error : null;
|
|
598
622
|
if (primary) throw Object.assign(new Error(primary.message), primary, { details: { ...primary.details, socket, phase: "confirm_daemon_stop", observer_error: errorRecord(observer) } });
|
|
599
623
|
throw observer;
|
|
600
624
|
};
|
|
601
|
-
let result;
|
|
625
|
+
let result, observationError;
|
|
602
626
|
try { result = await request(); }
|
|
603
|
-
catch (error) {
|
|
604
|
-
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (!expected || !["daemon_timeout", "daemon_not_running", "daemon_connection_closed", "operation_observation_lost"].includes(error.code)) return await failure(error);
|
|
629
|
+
observationError = error;
|
|
630
|
+
}
|
|
631
|
+
if (!observationError && result?.stopped !== true) return await failure(Object.assign(new Error("stop acknowledgement does not prove shutdown"), { code: "daemon_stop_incomplete" }));
|
|
632
|
+
// The RPC wait and physical retirement are separate phases. A lost ACK
|
|
633
|
+
// consumes the former, not the existing confirmation budget.
|
|
634
|
+
const deadline = performance.now() + timeoutMs;
|
|
605
635
|
for (;;) {
|
|
606
636
|
const state = await readState?.();
|
|
607
|
-
if (expected && identity(state) !== identity(expected)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
|
|
637
|
+
if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
|
|
638
|
+
if (state?.shutdown_state === "cleanup_failed") return await failure(observationError ?? Object.assign(new Error("daemon cleanup failed"), { code: "daemon_stop_incomplete" }));
|
|
608
639
|
let endpointPresent = true;
|
|
609
640
|
try { await lstat(socket); } catch (error) { if (error.code === "ENOENT") endpointPresent = false; else throw error; }
|
|
610
641
|
let daemonPresent = false;
|
|
611
642
|
if (expected?.pid) { try { process.kill(expected.pid, 0); daemonPresent = true; } catch (error) { if (error.code !== "ESRCH") daemonPresent = true; } }
|
|
612
643
|
if (!endpointPresent && !daemonPresent) {
|
|
644
|
+
if (observationError) {
|
|
645
|
+
if (state?.shutdown_state === "clean" && state.active_tree === false && !state.cleanup_error) return { stopped: true, clean: true };
|
|
646
|
+
return await failure(observationError);
|
|
647
|
+
}
|
|
613
648
|
if (readState && result.clean === true && (state?.shutdown_state !== 'clean' || state.active_tree !== false)) return await failure(Object.assign(new Error('daemon clean ACK conflicts with its retained state'), { code: 'daemon_stop_incomplete' }));
|
|
614
649
|
return result;
|
|
615
650
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Final native overlay: launch-bound routing, never inherited invocation authority. */
|
|
2
|
+
export function managedFlowEnv(config, daemonId = config.daemonId) {
|
|
3
|
+
const managed = !config.noFlow && Boolean(config.ddFlowHome && config.ddFlowBin && daemonId);
|
|
4
|
+
return {
|
|
5
|
+
DD_FLOW_DAEMON_ID: managed ? daemonId : undefined,
|
|
6
|
+
DD_FLOW_HOME: managed ? config.ddFlowHome : undefined,
|
|
7
|
+
DD_FLOW_BIN: managed ? config.ddFlowBin : undefined,
|
|
8
|
+
DD_FLOW_RESOURCE_HOME: config.resourceHome ?? undefined,
|
|
9
|
+
DD_FLOW_RUNTIME_OWNER: managed && config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined,
|
|
10
|
+
DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined,
|
|
11
|
+
DD_FLOW_SESSION_ID: undefined, DD_FLOW_WORK_ID: undefined, DD_FLOW_CONTROLLER_TOKEN: undefined,
|
|
12
|
+
DD_FLOW_CONTROL_TOKEN: undefined, DD_FLOW_SCOPE_CONTROL_TOKEN: undefined,
|
|
13
|
+
DD_FLOW_HOOK_DEADLINE_AT: undefined,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -62,7 +62,7 @@ export async function readNativeHookFailures(stateDir, requests, current, reject
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
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
|
|
65
|
+
export function hookBudget(timeoutMs = 25_000, deadlineAt = process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null) {
|
|
66
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
67
|
const started = performance.now();
|
|
68
68
|
const duration = Math.max(0, Math.min(timeoutMs, Number.isFinite(deadlineAt) ? deadlineAt - Date.now() : timeoutMs));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export function nativeCommandFingerprint(input: { executable: string; args: string[]; cwd: string; env: Record<string, string | undefined>; operationId: string }): string;
|
|
2
|
+
export function prepareDaemonRequest<T extends { id: string; operation: string; params?: Record<string, unknown> }>(request: T): T & { invocation_sha256?: string };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { hashJson } from './runner-events.mjs';
|
|
6
|
+
|
|
7
|
+
const environmentKeys = ['HOME', 'CODEX_HOME', 'DD_FLOW_CONFIG_HOME', 'DD_FLOW_RESOURCE_HOME', 'DD_FLOW_HOME', 'DD_FLOW_BIN', 'DD_FLOW_ENGINE_HOME'];
|
|
8
|
+
const fail = reason => { throw Object.assign(new Error('Native request proof does not match its prepared command'), {
|
|
9
|
+
code: 'native_request_proof_conflict', details: { reason, effect: 'no_effect' }
|
|
10
|
+
}); };
|
|
11
|
+
|
|
12
|
+
/** Independent transport identity: prepared by the caller before launching
|
|
13
|
+
* the adapter, and recomputed by the adapter's shared request builder. */
|
|
14
|
+
export function nativeCommandFingerprint({ executable, args, cwd, env, operationId }) {
|
|
15
|
+
return hashJson({ executable, args, cwd, operation_id: operationId,
|
|
16
|
+
environment: Object.fromEntries(environmentKeys.map(key => [key, env[key] ?? null])) });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Called after the adapter's own argument preparation but before its RPC or
|
|
20
|
+
* daemon-start action. No provider response can supply this fingerprint. */
|
|
21
|
+
export function prepareDaemonRequest(request) {
|
|
22
|
+
const file = process.env.DD_FLOW_NATIVE_REQUEST_PROOF_FILE;
|
|
23
|
+
const expected = process.env.DD_FLOW_NATIVE_COMMAND_SHA256;
|
|
24
|
+
const operation = process.env.DD_FLOW_NATIVE_REQUEST_OPERATION;
|
|
25
|
+
if (file === undefined && expected === undefined) return request;
|
|
26
|
+
// Control helpers may inspect status before their owned stop RPC. Status
|
|
27
|
+
// cannot consume the productive/control request's independently bound proof.
|
|
28
|
+
if (operation && request.operation !== operation) {
|
|
29
|
+
if (request.operation === 'daemon.status' && operation === 'daemon.stop') return request;
|
|
30
|
+
fail('operation_changed');
|
|
31
|
+
}
|
|
32
|
+
const ownerText = process.env.DD_FLOW_NATIVE_REQUEST_PROOF_OWNER;
|
|
33
|
+
// Never forward caller proof authority into the daemon/provider process.
|
|
34
|
+
delete process.env.DD_FLOW_NATIVE_REQUEST_PROOF_FILE;
|
|
35
|
+
delete process.env.DD_FLOW_NATIVE_COMMAND_SHA256;
|
|
36
|
+
delete process.env.DD_FLOW_NATIVE_REQUEST_OPERATION;
|
|
37
|
+
delete process.env.DD_FLOW_NATIVE_REQUEST_PROOF_OWNER;
|
|
38
|
+
if (!file || !path.isAbsolute(file) || !/^[a-f0-9]{64}$/.test(expected ?? '')
|
|
39
|
+
|| typeof request.id !== 'string' || !request.id || typeof request.operation !== 'string' || request.operation !== operation) fail('invalid_owner');
|
|
40
|
+
const command = nativeCommandFingerprint({ executable: process.argv[1], args: process.argv.slice(2),
|
|
41
|
+
cwd: process.cwd(), env: process.env, operationId: request.id });
|
|
42
|
+
if (command !== expected || fs.realpathSync(path.dirname(file)) !== path.dirname(file)) fail('command_changed');
|
|
43
|
+
let owner;
|
|
44
|
+
try { owner = JSON.parse(ownerText ?? 'null'); } catch { fail('invalid_parent_owner'); }
|
|
45
|
+
const parent = fs.lstatSync(path.dirname(file));
|
|
46
|
+
if (!parent.isDirectory() || parent.dev !== owner?.dev || parent.ino !== owner?.ino) fail('proof_parent_changed');
|
|
47
|
+
const proof = { schema_id: 'dd-flow/native-request-proof@1', operation_id: request.id, operation: request.operation,
|
|
48
|
+
command_sha256: command, params_sha256: hashJson({ operation: request.operation, params: request.params ?? {} }),
|
|
49
|
+
session_id: request.params?.sessionId ?? null };
|
|
50
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
51
|
+
const fd = fs.openSync(temporary, 'wx', 0o600);
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(fd, JSON.stringify(proof)); fs.fsyncSync(fd);
|
|
54
|
+
const current = fs.lstatSync(path.dirname(file));
|
|
55
|
+
if (parent.dev !== current.dev || parent.ino !== current.ino || !current.isDirectory()) fail('proof_parent_changed');
|
|
56
|
+
try { fs.linkSync(temporary, file); }
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code !== 'EEXIST') throw error;
|
|
59
|
+
const stat = fs.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 16 * 1024
|
|
61
|
+
|| hashJson(JSON.parse(fs.readFileSync(file, 'utf8'))) !== hashJson(proof)) fail('retained_request_changed');
|
|
62
|
+
}
|
|
63
|
+
} finally { fs.closeSync(fd); fs.unlinkSync(temporary); }
|
|
64
|
+
return { ...request, invocation_sha256: command };
|
|
65
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { managedFlowEnv } from './managed-flow-env.mjs';
|
|
2
3
|
import * as Plugin from "../vendor/opencode-plugin.mjs";
|
|
3
4
|
import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
5
|
|
|
@@ -26,10 +27,13 @@ export function makeOpenCodePlugin(binding, invoke = invokeNativeHook) {
|
|
|
26
27
|
args: ["opencode", "event", "handle", "--project-root", binding.projectRoot, "--json"],
|
|
27
28
|
payload, home: binding.ddFlowHome, timeoutMs: budget.remaining("admission", 15000) });
|
|
28
29
|
await ctx.shell.hook("create.before", event => {
|
|
30
|
+
Object.assign(event.env, managedFlowEnv(binding));
|
|
29
31
|
delete event.env.OPENCODE_PASSWORD; delete event.env.OPENCODE_SERVER_PASSWORD;
|
|
30
32
|
});
|
|
31
33
|
await ctx.tool.hook("execute.before", async event => {
|
|
32
|
-
if (event.tool !== "shell"
|
|
34
|
+
if (event.tool !== "shell") return;
|
|
35
|
+
if (event.input.timeout === undefined) event.input.timeout = 0;
|
|
36
|
+
if (!binding.ddFlowBin) return;
|
|
33
37
|
const budget = hookBudget(), payload = await envelope("before", event, budget);
|
|
34
38
|
event.input = { ...event.input, workdir: payload.session.directory };
|
|
35
39
|
const result = await deliver({ ...payload, input: event.input }, budget);
|
|
@@ -64,7 +64,9 @@ export function publishOperationProgress(root, id, phase, metadata = {}) {
|
|
|
64
64
|
const prior = await readOperationProgress(root, id);
|
|
65
65
|
const observedAt = metadata.observed_at ?? new Date().toISOString();
|
|
66
66
|
if (prior && Date.parse(observedAt)<Date.parse(prior.observed_at)) return prior;
|
|
67
|
-
|
|
67
|
+
// A later successful ownership gate is fresh liveness evidence while admission waits.
|
|
68
|
+
if (prior && phase === prior.phase && phase !== 'native_progress'
|
|
69
|
+
&& (phase !== 'ownership_confirmed' || Date.parse(observedAt) <= Date.parse(prior.observed_at))) return prior;
|
|
68
70
|
if (prior && phase === 'native_progress' && Date.parse(observedAt) - Date.parse(prior.observed_at) < 1_000) return prior;
|
|
69
71
|
const value = { schema_id: OPERATION_PROGRESS_CONTRACT,
|
|
70
72
|
...Object.fromEntries(fields.map(field => [field, requested[field] ?? null])),
|
package/dist/runtime/context.js
CHANGED
|
@@ -11,7 +11,7 @@ export function createRouterContext(env, deadlineAt) {
|
|
|
11
11
|
}
|
|
12
12
|
export function createContext(env, mode = "initialize", importPermitId, options = {}) {
|
|
13
13
|
const ddFlowHome = resolveDdFlowHome(env);
|
|
14
|
-
if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize")
|
|
14
|
+
if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize" || options.resourceDatabaseMode === "create_new")
|
|
15
15
|
assertImportAdmission(ddFlowHome, importPermitId);
|
|
16
16
|
return {
|
|
17
17
|
ddFlowHome,
|