@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139
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 +19 -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 +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -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/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/runtime-process.js +1 -1
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +56 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- 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 +53 -22
- package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
- package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
- package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
- 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-children.d.mts +2 -0
- package/dist/harness-runtime/lib/native-children.mjs +20 -1
- 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 +32 -9
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- 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 +26 -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/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +18 -2
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- 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/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -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/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +93 -15
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/managed-processes.js +31 -10
- 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 +296 -0
- package/dist/services/memory-init-preflight.js +330 -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 +181 -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 +178 -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/merge-server.js +2 -4
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
- package/dist/services/recovery-snapshot-database.js +417 -2
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- 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-adapter.js +1 -1
- 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 +5 -5
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +158 -64
- package/dist/services/run-recovery-guard.js +5 -3
- package/dist/services/run-recovery-runtime.js +3 -3
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +413 -30
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +6 -6
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +73 -11
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +7 -5
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/vnext-protocolize.js +20 -6
- package/dist/services/vnext-workspace-policy.js +7 -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
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
|
@@ -4,6 +4,7 @@ import { ToolObservations, openCodeToolObservation } from "./tool-observations.m
|
|
|
4
4
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
5
5
|
import { assertNativeLine } from "./native-streams.mjs";
|
|
6
6
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
7
|
+
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
7
8
|
|
|
8
9
|
export const OPENCODE_PROTOCOL = 2;
|
|
9
10
|
export class OpenCodeError extends Error {
|
|
@@ -27,17 +28,16 @@ export function normalizeMessage(message, sessionID) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function providerFailure(native, sessionId, turnId) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const code = quota ? "provider_quota_exhausted" : status === 401 || status === 403 ? "provider_auth_failed"
|
|
33
|
-
: /overload|server.?busy/i.test(`${type} ${message}`) || status === 503 ? "provider_capacity_transient"
|
|
34
|
-
: /rate.?limit|too many requests/i.test(message) ? "provider_rate_limited"
|
|
35
|
-
: status === 429 ? "provider_limit_unknown" : "opencode_provider_failed";
|
|
31
|
+
const status = native?.status, type = String(native?.type ?? "unknown");
|
|
32
|
+
const category = classifyProviderError(native), code = category === "provider_failed" ? "opencode_provider_failed" : category;
|
|
36
33
|
// Never forward response.body or provider credentials into controller/Judge.
|
|
37
34
|
return new OpenCodeError(code, `OpenCode native execution failed (${type}${status ? `, HTTP ${status}` : ""})`,
|
|
38
35
|
["provider_capacity_transient", "provider_rate_limited"].includes(code), {
|
|
39
36
|
provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_error_type: type,
|
|
40
37
|
native_turn_id: turnId, terminal_status: "failed",
|
|
38
|
+
provider_error: safeProviderError(native),
|
|
39
|
+
will_retry: native?.willRetry === true, native_retry_active: native?.willRetry === true,
|
|
40
|
+
native_turn_items: { observed: true, possible_effects: true, pending: false },
|
|
41
41
|
...(Number.isInteger(status) ? { http_status: status } : {}), observed_at: new Date().toISOString(), native_source: "opencode.v2.session.execution" });
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -48,11 +48,16 @@ export class OpenCodeClient {
|
|
|
48
48
|
baseUrl: this.baseUrl.href, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }
|
|
49
49
|
}));
|
|
50
50
|
}
|
|
51
|
-
async call(method, input, { timeoutMs, progress, onDispatched, productive = false } = {}) {
|
|
51
|
+
async call(method, input, { timeoutMs, progress, onDispatched, productive = false, signal } = {}) {
|
|
52
52
|
const observation = this.readObservation?.();
|
|
53
53
|
timeoutMs ??= observation?.timeoutMs ?? this.timeoutMs; progress ??= observation?.progress;
|
|
54
54
|
const api = await this.ready, controller = new AbortController();
|
|
55
|
-
|
|
55
|
+
let timer;
|
|
56
|
+
const abort = () => { controller.abort(); clearInterval(timer); };
|
|
57
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
58
|
+
if (signal?.aborted) abort();
|
|
59
|
+
timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
|
|
60
|
+
if (signal?.aborted) clearInterval(timer);
|
|
56
61
|
try {
|
|
57
62
|
const keys = method.split("."), name = keys.pop(), domain = keys.reduce((value, key) => value?.[key], api);
|
|
58
63
|
if (typeof domain?.[name] !== "function") throw new OpenCodeError("opencode_capability_missing", `OpenCode v2 does not expose ${method}`);
|
|
@@ -68,7 +73,7 @@ export class OpenCodeClient {
|
|
|
68
73
|
if (productive || controller.signal.aborted) throw Object.assign(lost(`OpenCode observer lost during ${method}`, { method }), { cause });
|
|
69
74
|
throw Object.assign(new OpenCodeError("opencode_unavailable", `OpenCode ${method} observation failed`, true,
|
|
70
75
|
{ method, native_type: cause._tag ?? cause.name ?? null, native_reason: cause.reason ?? null }), { cause });
|
|
71
|
-
} finally { clearInterval(timer); }
|
|
76
|
+
} finally { clearInterval(timer); signal?.removeEventListener("abort", abort); }
|
|
72
77
|
}
|
|
73
78
|
async health() {
|
|
74
79
|
const info = await this.call("server.info", undefined, { timeoutMs: 5_000 });
|
|
@@ -95,7 +100,8 @@ export class OpenCodeClient {
|
|
|
95
100
|
async children(id) { return (await this.pages("session.list", { parentID: id })).map(info => ({ ...info, directory: info.location?.directory })); }
|
|
96
101
|
async messages(id) { return (await this.pages("message.list", { sessionID: id })).map(message => normalizeMessage(message, id)); }
|
|
97
102
|
inbox(id) { return this.call("session.inbox.list", { sessionID: id }); }
|
|
98
|
-
async
|
|
103
|
+
async shellDirectory(id) { const api = await this.ready; return typeof api.session?.get === "function" ? (await this.inspect(id)).directory : this.directory; }
|
|
104
|
+
async shells(id) { const directory = await this.shellDirectory(id); const result = await this.call("shell.list", { location: { directory } }); return result.data.filter(item => item.metadata?.sessionID === id).map(item => ({ ...item, location: item.location ?? { directory } })); }
|
|
99
105
|
async pending(id) {
|
|
100
106
|
const [inbox, forms, permissions] = await Promise.all([this.inbox(id), this.call("session.form.list", { sessionID: id }), this.call("permission.list", { sessionID: id })]);
|
|
101
107
|
const value = { inbox, forms: forms.data ?? forms, permissions: permissions.data ?? permissions };
|
|
@@ -117,7 +123,7 @@ export class OpenCodeClient {
|
|
|
117
123
|
const result = await this.call("session.interrupt", { sessionID: id, resume: false }, { productive: true });
|
|
118
124
|
const api = await this.ready;
|
|
119
125
|
if (typeof api.shell?.list === "function") for (const shell of await this.shells(id)) {
|
|
120
|
-
if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location:
|
|
126
|
+
if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: shell.location }, { productive: true });
|
|
121
127
|
}
|
|
122
128
|
return result;
|
|
123
129
|
}
|
|
@@ -127,7 +133,7 @@ export class OpenCodeClient {
|
|
|
127
133
|
import(data) { return this.call("session.import", data, { productive: true }); }
|
|
128
134
|
|
|
129
135
|
async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
130
|
-
save, onModelMessages, onObservationError, assertDispatch, onDispatched, livenessTimeoutMs = this.timeoutMs }) {
|
|
136
|
+
save, onModelMessages, onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
|
|
131
137
|
validateObservationDuration(livenessTimeoutMs);
|
|
132
138
|
const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
|
|
133
139
|
if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
|
|
@@ -144,6 +150,21 @@ export class OpenCodeClient {
|
|
|
144
150
|
const publish = operationProgressReporter(), known = new Set([id, ...baseline.keys()]), signatures = new Map(), shellCursors = new Map(), liveSeen = new Set();
|
|
145
151
|
let progress = 0, marker, stream;
|
|
146
152
|
const liveParts = new Map(), liveShells = new Map(), backgroundShells = new Set();
|
|
153
|
+
const completedTools = new Set(), outputReads = new Map(), shellLocations = new Map();
|
|
154
|
+
const observationErrors = { count: 0 };
|
|
155
|
+
const observeOutput = (shellID, location) => {
|
|
156
|
+
if (outputReads.has(shellID) || !location?.directory) return;
|
|
157
|
+
const cursor = shellCursors.get(shellID) ?? 0;
|
|
158
|
+
const pending = this.call("shell.output", { id: shellID, location, cursor, limit: 65536 }, { signal: streamAbort.signal })
|
|
159
|
+
.then(async output => { const data = output.data ?? output; if (!streamAbort.signal.aborted && data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); } })
|
|
160
|
+
.catch(async error => {
|
|
161
|
+
if (streamAbort.signal.aborted) return;
|
|
162
|
+
const diagnostic = { method: "shell.output", shell_id: shellID, native_type: error.details?.native_type ?? error.cause?._tag ?? null, http_status: error.cause?.status ?? null };
|
|
163
|
+
observationErrors.count++; observationErrors.first ??= diagnostic; observationErrors.last = diagnostic;
|
|
164
|
+
if (observationErrors.count === 1) await onObservationError?.(error);
|
|
165
|
+
}).finally(() => outputReads.delete(shellID));
|
|
166
|
+
outputReads.set(shellID, pending); void pending.catch(() => {});
|
|
167
|
+
};
|
|
147
168
|
const advance = async (observedAt = Date.now()) => { marker = { cursor: ++progress, observed_at: observedAt }; await publish("native_progress", { session_id: id, observed_at: new Date(marker.observed_at).toISOString() }); };
|
|
148
169
|
let connected;
|
|
149
170
|
const connection = new Promise(resolve => { connected = resolve; });
|
|
@@ -157,7 +178,8 @@ export class OpenCodeClient {
|
|
|
157
178
|
&& ["session.text.delta", "session.reasoning.delta", "session.tool.input.delta", "session.tool.progress"].includes(event.type)) {
|
|
158
179
|
liveSeen.add(event.id); if (liveSeen.size > 1024) liveSeen.delete(liveSeen.values().next().value);
|
|
159
180
|
const key = `${data.sessionID}:${data.assistantMessageID}:${data.id ?? ""}`, signature = requestDigest(data.metadata ?? null);
|
|
160
|
-
if (event.type === "session.tool.progress" &&
|
|
181
|
+
if (event.type === "session.tool.progress" && completedTools.has(key)) continue;
|
|
182
|
+
if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string" && !completedTools.has(key)) liveShells.set(key, data.metadata.shellID);
|
|
161
183
|
if (typeof data.delta === "string" && data.delta.length || event.type === "session.tool.progress" && liveParts.get(key) !== signature) {
|
|
162
184
|
liveParts.set(key, signature); await advance(event.created);
|
|
163
185
|
}
|
|
@@ -189,6 +211,7 @@ export class OpenCodeClient {
|
|
|
189
211
|
}
|
|
190
212
|
for (;;) {
|
|
191
213
|
try {
|
|
214
|
+
await checkHookFailures?.();
|
|
192
215
|
const events = await this.logs(id, state.cursor);
|
|
193
216
|
let delivered = state.delivered === true, terminal = state.terminal;
|
|
194
217
|
for (const event of events) {
|
|
@@ -198,6 +221,7 @@ export class OpenCodeClient {
|
|
|
198
221
|
if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
|
|
199
222
|
state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
|
|
200
223
|
}
|
|
224
|
+
let runningTools = false;
|
|
201
225
|
const observe = async (session, visited = new Set()) => {
|
|
202
226
|
if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
|
|
203
227
|
const messages = await this.messages(session);
|
|
@@ -211,19 +235,24 @@ export class OpenCodeClient {
|
|
|
211
235
|
signatures.set(key, { signature: next, seen, highWater: Math.max(before?.highWater ?? 0, encoded.length) });
|
|
212
236
|
}
|
|
213
237
|
for (const part of item.parts) {
|
|
214
|
-
|
|
238
|
+
const toolKey = `${session}:${item.info.id}:${part.id}`;
|
|
239
|
+
if (part.type === "tool" && ["running", "streaming", "pending"].includes(part.state?.status)) runningTools = true;
|
|
240
|
+
if (part.type === "tool" && !["running", "streaming", "pending"].includes(part.state?.status)) { completedTools.add(toolKey); liveShells.delete(toolKey); }
|
|
241
|
+
let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(toolKey) : null;
|
|
215
242
|
if (!shellID && part.tool === "shell" && part.state?.status === "running") {
|
|
216
243
|
// Tool progress metadata is volatile in the CLI. Reconcile
|
|
217
244
|
// only an unambiguous native shell in this current message.
|
|
218
245
|
const candidates = (await this.shells(session)).filter(shell => shell.time.started >= item.info.created);
|
|
219
|
-
if (candidates.length === 1) shellID = candidates[0].id;
|
|
246
|
+
if (candidates.length === 1) { shellID = candidates[0].id; shellLocations.set(shellID, candidates[0].location); }
|
|
220
247
|
}
|
|
221
248
|
if (!shellID) continue;
|
|
222
249
|
const cursor = shellCursors.get(shellID) ?? 0;
|
|
223
250
|
shellCursors.set(shellID, cursor); // Zero-output shells still own their native notification.
|
|
224
251
|
if (part.tool === "shell" && part.state?.metadata?.status === "running") backgroundShells.add(shellID);
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
if (!completedTools.has(toolKey)) {
|
|
253
|
+
if (!shellLocations.has(shellID)) shellLocations.set(shellID, (await this.shells(session)).find(shell => shell.id === shellID)?.location);
|
|
254
|
+
observeOutput(shellID, shellLocations.get(shellID));
|
|
255
|
+
}
|
|
227
256
|
}
|
|
228
257
|
}
|
|
229
258
|
for (const child of await this.children(session)) {
|
|
@@ -258,7 +287,7 @@ export class OpenCodeClient {
|
|
|
258
287
|
state = { ...state, delivered: true, terminal: { type: `session.execution.${terminal.info.outcome}`, error: terminal.info.error } }; await save?.(state);
|
|
259
288
|
}
|
|
260
289
|
}
|
|
261
|
-
const statuses = await this.statuses(); let unsettled =
|
|
290
|
+
const statuses = await this.statuses(); let unsettled = runningTools;
|
|
262
291
|
// A background child's native completion notification is delivered
|
|
263
292
|
// asynchronously. An idle parent before that delivery is not the
|
|
264
293
|
// completion of the coordinating operation.
|
|
@@ -278,18 +307,20 @@ export class OpenCodeClient {
|
|
|
278
307
|
if (typeof api.shell?.list === "function") unsettled ||= (await this.shells(session)).some(shell => shell.status === "running");
|
|
279
308
|
}
|
|
280
309
|
if (state.delivered && state.terminal && !unsettled) {
|
|
310
|
+
await checkHookFailures?.();
|
|
281
311
|
if (state.terminal.type === "session.execution.failed") throw providerFailure(state.terminal.error, id, messageId);
|
|
282
312
|
if (state.terminal.type === "session.execution.interrupted") throw new OpenCodeError("opencode_execution_interrupted", "OpenCode execution was interrupted", false, { provider_session_id: id, turn_id: messageId });
|
|
283
313
|
const final = current.findLast(item => item.info.completed !== undefined && item.info.finish && item.info.finish !== "tool-calls");
|
|
284
314
|
if (!final) throw lost("OpenCode terminal has no current assistant response", { session_id: id, message_id: messageId });
|
|
285
315
|
if (final.info.error) throw providerFailure(final.info.error, id, messageId);
|
|
286
|
-
return { ...final, admission_message_id: messageId, log_cursor: state.cursor };
|
|
316
|
+
return { ...final, admission_message_id: messageId, log_cursor: state.cursor, ...(observationErrors.count ? { observation_errors: observationErrors } : {}) };
|
|
287
317
|
}
|
|
288
318
|
} catch (error) {
|
|
319
|
+
await checkHookFailures?.();
|
|
289
320
|
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)) throw error;
|
|
290
321
|
await onObservationError?.(error);
|
|
291
322
|
}
|
|
292
|
-
if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state() });
|
|
323
|
+
if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
|
|
293
324
|
await delay(Math.max(5, Math.min(500, livenessTimeoutMs / 4)));
|
|
294
325
|
}
|
|
295
326
|
} finally { clearInterval(connectionTimer); this.readObservation = priorObservation; streamAbort.abort(); await stream; }
|
|
@@ -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";
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
} from "./dd-zcode.mjs";
|
|
29
31
|
|
|
30
32
|
const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
|
|
33
|
+
import { settledNativeTree } from "./native-children.mjs";
|
|
31
34
|
const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
|
|
32
35
|
const STATE_SCHEMA = "dd-zcode/daemon-state@1";
|
|
33
36
|
// ACP silence starts diagnostics; it is not a provider-failure deadline.
|
|
@@ -90,7 +93,7 @@ async function removeSocket(socket, dir) {
|
|
|
90
93
|
const errorPayload = errorRecord;
|
|
91
94
|
|
|
92
95
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
93
|
-
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
96
|
+
const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
|
|
94
97
|
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'zcode', timeoutMs });
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -110,22 +113,21 @@ function sameConfig(state, config) {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
export async function startDaemon(options) {
|
|
116
|
+
if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined, projectRoot: undefined };
|
|
113
117
|
const paths = locations(options.stateDir);
|
|
114
118
|
const hasFlowContext = Boolean(options.ddFlowHome || options.projectRoot || options.ddFlowBin);
|
|
115
|
-
if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot)) {
|
|
116
|
-
throw new DaemonError("flow_context_incomplete", "--dd-flow-home and --project-root must be provided together for ZCode lifecycle forwarding");
|
|
119
|
+
if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot || !options.ddFlowBin)) {
|
|
120
|
+
throw new DaemonError("flow_context_incomplete", "--dd-flow-home, --dd-flow-bin and --project-root must be provided together for ZCode lifecycle forwarding");
|
|
117
121
|
}
|
|
118
122
|
const config = {
|
|
119
123
|
cwd: absolute(options.cwd, "--cwd"),
|
|
120
124
|
journal: absolute(options.journal, "--journal"),
|
|
121
125
|
bin: options.bin ?? null,
|
|
122
126
|
zcodePath: options.zcodePath ?? null,
|
|
123
|
-
|
|
124
|
-
// for the trusted event bridge unless a pinned executable was supplied.
|
|
125
|
-
ddFlowBin: hasFlowContext ? (options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : "dd-flow") : null,
|
|
127
|
+
ddFlowBin: hasFlowContext ? absolute(options.ddFlowBin, "--dd-flow-bin") : null,
|
|
126
128
|
ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
|
|
127
129
|
projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
|
|
128
|
-
resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
130
|
+
resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
129
131
|
env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
|
|
130
132
|
livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
|
|
131
133
|
requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
|
|
@@ -373,6 +375,7 @@ export class DaemonRuntime {
|
|
|
373
375
|
current.reject = error => { if (this.nativeHookOperation !== current || current.error) return; current.error = error; this.bridge.lifecycleFailure ??= error; this.bridge.notificationError = this.bridge.lifecycleFailure; this.bridge.rejectAll(this.bridge.lifecycleFailure); rejectHook(this.bridge.lifecycleFailure); };
|
|
374
376
|
const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
|
|
375
377
|
try {
|
|
378
|
+
if (name === "session.prompt") this.state.last_native_failure = null;
|
|
376
379
|
await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
|
|
377
380
|
const result = await Promise.race([task(), hookFailed]);
|
|
378
381
|
this.track(result);
|
|
@@ -385,6 +388,9 @@ export class DaemonRuntime {
|
|
|
385
388
|
// after a late/partial create: the native Session existed, but the
|
|
386
389
|
// daemon state no longer carried the only correlation to it.
|
|
387
390
|
const active = this.state?.active_request;
|
|
391
|
+
const response = error?.details?.terminal_response;
|
|
392
|
+
if (name === "session.prompt" && response?.method === "session/prompt" && error.details.provider_session_id)
|
|
393
|
+
this.state.last_native_failure = { provider_session_id: error.details.provider_session_id, request_id: response.request_id };
|
|
388
394
|
try {
|
|
389
395
|
await hookBudget(1_000, null).run(() => this.persist({
|
|
390
396
|
active_operation: null,
|
|
@@ -571,7 +577,14 @@ export class DaemonRuntime {
|
|
|
571
577
|
if (this.sessions.has(result.provider_session_id)) this.track(result, result.subagents);
|
|
572
578
|
else if (result.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
|
|
573
579
|
await this.persist({ active_tree: this.treeRunning(result) || Boolean(this.activeProductive) });
|
|
574
|
-
|
|
580
|
+
const settled = !this.treeRunning(result) && !this.activeProductive;
|
|
581
|
+
const failure = this.state.last_native_failure;
|
|
582
|
+
const nativeChildren = (result.evidence ?? result).subagents;
|
|
583
|
+
const childIds = [...new Set([...(this.sessions.get(result.provider_session_id)?.known_child_session_ids ?? []), ...zcodeChildSessionIds(nativeChildren)])];
|
|
584
|
+
const childStates = new Map([...(nativeChildren?.ended?.items ?? nativeChildren?.completed ?? []), ...(nativeChildren?.running ?? []).map(child => ({ ...child, status: "running" }))].map(child => [child.childSessionId ?? child.sessionId, child.status]));
|
|
585
|
+
const rootSettled = !this.activeProductive && failure?.provider_session_id === result.provider_session_id && (result.evidence ?? result).read?.projection?.status === "idle";
|
|
586
|
+
return { ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
|
|
587
|
+
...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) };
|
|
575
588
|
}
|
|
576
589
|
if (operation === "session.cancel") {
|
|
577
590
|
if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
|
|
@@ -683,8 +696,7 @@ export class DaemonRuntime {
|
|
|
683
696
|
}
|
|
684
697
|
|
|
685
698
|
export function zcodeBridgeEnv(config, daemonId) {
|
|
686
|
-
return { ...config.env,
|
|
687
|
-
DD_FLOW_RUNTIME_OWNER: config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined };
|
|
699
|
+
return { ...config.env, ...managedFlowEnv(config, daemonId) };
|
|
688
700
|
}
|
|
689
701
|
|
|
690
702
|
export async function serveDaemon(stateDir) {
|
|
@@ -15,6 +15,7 @@ import { commandText } from "./process-json.mjs";
|
|
|
15
15
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
16
16
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
17
17
|
import { safeNativeFailure } from "./native-children.mjs";
|
|
18
|
+
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
18
19
|
|
|
19
20
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
20
21
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -218,9 +219,12 @@ export class AcpBridge {
|
|
|
218
219
|
// ZCode emits this as `session/update`; Grok Build wraps the same ACP
|
|
219
220
|
// payload in `_x.ai/session_notification`. Preserve either provider's
|
|
220
221
|
// native error before its generic JSON-RPC terminal response arrives.
|
|
221
|
-
if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state"
|
|
222
|
+
if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state") {
|
|
222
223
|
for (const pending of this.pending.values()) {
|
|
223
|
-
if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId
|
|
224
|
+
if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) {
|
|
225
|
+
pending.nativeRetryActive = update.type !== "failed";
|
|
226
|
+
if (update.type === "failed") pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
|
|
227
|
+
}
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
|
|
@@ -283,11 +287,17 @@ export class AcpBridge {
|
|
|
283
287
|
const error = new Error(messageText);
|
|
284
288
|
const nativeCode = typeof data?.code === "string" ? data.code : typeof data?.native_code === "string" ? data.native_code : null;
|
|
285
289
|
const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
const nativeFailure = { ...(provider ?? {}), ...(data ?? {}),
|
|
291
|
+
code: [provider?.code, data?.code, data?.native_code].filter(value => typeof value === "string").join(" "), message: messageText };
|
|
292
|
+
const category = pending.method === "session/prompt" ? classifyProviderError(nativeFailure) : "provider_failed";
|
|
293
|
+
error.code = unknown ? nativeCode : category === "provider_failed" ? "acp_request_failed" : category;
|
|
294
|
+
error.retryable = ["provider_rate_limited", "provider_capacity_transient"].includes(error.code);
|
|
288
295
|
error.details = { acp_code: message.error.code ?? null,
|
|
289
296
|
observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
|
|
290
297
|
terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
|
|
298
|
+
...(pending.method === "session/prompt" ? { native_source: "acp.error_response", terminal_status: "failed", turn_id: String(message.id), native_turn_id: String(message.id), provider_error: safeProviderError(nativeFailure),
|
|
299
|
+
native_retry_active: pending.nativeRetryActive === true, will_retry: nativeFailure.willRetry === true,
|
|
300
|
+
native_turn_items: { observed: this.toolSummary(pending.sessionId).completeness === "complete", possible_effects: this.toolSummary(pending.sessionId).total > 0, pending: this.toolSummary(pending.sessionId).pending > 0 } } : {}),
|
|
291
301
|
...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
|
|
292
302
|
pending.reject(error);
|
|
293
303
|
}
|
|
@@ -1046,6 +1056,9 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1046
1056
|
idleSessionId: identity.adapterSessionId,
|
|
1047
1057
|
idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
|
|
1048
1058
|
inactivityMode: "diagnostic",
|
|
1059
|
+
}).catch(error => {
|
|
1060
|
+
if (error.details?.terminal_response?.session_id === identity.adapterSessionId) error.details.provider_session_id = identity.providerSessionId;
|
|
1061
|
+
throw error;
|
|
1049
1062
|
});
|
|
1050
1063
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
1051
1064
|
const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type DelegationWork = { work_id: string; start_command: string; launch_policy?: string };
|
|
2
2
|
export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[]; totalWorks?: number; remainingWorks?: number };
|
|
3
3
|
export function delegationAdapter(harness: string): string | null;
|
|
4
|
+
export function renderNativeChildFollowupInstruction(harness: string, childId: string): string | null;
|
|
4
5
|
export function delegationContract(harness: string): string;
|
|
5
6
|
export const lifecycleRetryInstruction: string;
|
|
6
7
|
export const lifecycleCommandCompletionInstruction: string;
|
|
@@ -37,6 +37,17 @@ export function delegationAdapter(harness) {
|
|
|
37
37
|
return Object.hasOwn(aliases, harness) ? aliases[harness] : null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Only documented same-child APIs may continue an existing native worker. */
|
|
41
|
+
export function renderNativeChildFollowupInstruction(harness, childId) {
|
|
42
|
+
if (typeof childId !== "string" || !childId) return null;
|
|
43
|
+
const adapter = delegationAdapter(harness), target = JSON.stringify(childId);
|
|
44
|
+
if (adapter === "codex") return `Use collaboration.followup_task with target: ${target} and message: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Do not spawn a replacement.`;
|
|
45
|
+
if (adapter === "opencode") return `Use the native subagent tool with sessionID: ${target} and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Keep the same model and reasoning. Do not spawn a replacement.`;
|
|
46
|
+
if (adapter === "droid") return `Use the native Task tool with resume: ${target}, subagent_type: "dd-flow-worker", and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Omit model and complexity overrides. Do not spawn a replacement.`;
|
|
47
|
+
if (adapter === "grok") return `Use the native send_subagent_message tool with subagent_id: ${target}, delivery: "queue", and text: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Retain its accepted message_id and wait on the same subagent. If admission is uncertain, do not resend. Do not spawn a replacement.`;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
function requireAdapter(harness) {
|
|
41
52
|
const key = delegationAdapter(harness);
|
|
42
53
|
if (!key) throw Object.assign(new Error(`Unsupported delegation adapter: ${harness}`), { code: "delegation_contract_unsupported" });
|
|
@@ -49,7 +60,7 @@ export function delegationContract(harness) {
|
|
|
49
60
|
return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
|
|
50
61
|
}
|
|
51
62
|
|
|
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.";
|
|
63
|
+
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
64
|
|
|
54
65
|
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
66
|
|
|
@@ -71,7 +82,9 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
|
|
|
71
82
|
"Your first technical action must be this exact lifecycle command:",
|
|
72
83
|
startCommand,
|
|
73
84
|
"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
|
-
"
|
|
85
|
+
startCommand.startsWith("cd '")
|
|
86
|
+
? "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."
|
|
87
|
+
: "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
|
|
75
88
|
"Continue only if it succeeds and returns the authoritative Work packet.",
|
|
76
89
|
lifecycleRetryInstruction,
|
|
77
90
|
"After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
|
|
@@ -109,7 +122,7 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
|
|
|
109
122
|
|
|
110
123
|
export function renderCapacityInstructions({ harness, maximum }) {
|
|
111
124
|
const adapter = requireAdapter(harness);
|
|
112
|
-
if (!Number.isInteger(maximum) || maximum < 1) throw new Error("
|
|
125
|
+
if (!Number.isInteger(maximum) || maximum < 1 || maximum > 15) throw new Error("Native capacity limit must be an integer from 1 through 15");
|
|
113
126
|
return [
|
|
114
127
|
`Adapter delegation contract: ${delegationContract(harness)}.`,
|
|
115
128
|
"This is a technical native-subagent capacity qualification, not product work.",
|
|
@@ -25,7 +25,8 @@ export function maintenanceRetryable(error) {
|
|
|
25
25
|
let retryable = false;
|
|
26
26
|
// Inspect only bounded typed causes from our maintenance transport, never text.
|
|
27
27
|
for (let depth = 0; error && depth < 8; depth++, error = error.cause ?? error.details?.cause) {
|
|
28
|
-
if (error.details?.cleanup_unconfirmed === true
|
|
28
|
+
if (error.details?.cleanup_unconfirmed === true || error.cleanup_error || error.details?.cleanup_error
|
|
29
|
+
|| error.details?.store_cleanup_errors?.length || error.details?.transaction_cleanup_error || error.details?.transaction_close_error) return false;
|
|
29
30
|
if (error.details?.effect === "unknown") return false;
|
|
30
31
|
if (["process_lease_lost", "controller_lease_lost", "process_maintenance_receipt_invalid", "runtime_owner_mismatch"].includes(error.code)) return false;
|
|
31
32
|
if (error.code === "process_ownership_unknown" && error.details?.reason === "physical_identity_unavailable" && ["process_confirmation", "observer_registration"].includes(error.details.phase) && error.details.effect === "no_effect" && error.details.native_dispatch_started === false) retryable = true;
|
|
@@ -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
|
}
|