@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
|
@@ -13,7 +13,8 @@ import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from
|
|
|
13
13
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
14
14
|
import { publishSetup } from './setup-progress.mjs';
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
export const CODEX_REQUEST_TIMEOUT_MS = 120_000;
|
|
17
|
+
export const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
17
18
|
const CODEX_SESSION_CONFIG = Object.freeze({
|
|
18
19
|
bypass_hook_trust: true,
|
|
19
20
|
"features.plugins": false,
|
|
@@ -280,7 +281,7 @@ export class CodexBridge {
|
|
|
280
281
|
const correction = isProvenShellCorrection(error);
|
|
281
282
|
error.details = { ...error.details, native_hook_binding: { provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
|
|
282
283
|
hook_run_id: run.id, source: "codex.hook/completed" }, provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
|
|
283
|
-
hook_run_id: run.id, effect:
|
|
284
|
+
hook_run_id: run.id, effect: error.details?.effect ?? "unknown", native_turn_items: { observed: false, possible_effects: true, pending: true }, duration_ms: run.durationMs };
|
|
284
285
|
if (correction) {
|
|
285
286
|
try { void Promise.resolve(this.journal.write("native_hook_correction", errorRecord(error))).catch(storage => { error.cleanup_error ??= errorRecord(storage); }); }
|
|
286
287
|
catch (storage) { error.cleanup_error ??= errorRecord(storage); }
|
|
@@ -409,7 +410,7 @@ export class CodexBridge {
|
|
|
409
410
|
this.send({ jsonrpc: "2.0", id: message.id, error });
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
request(method, params = {}, timeoutMs = this.options.timeoutMs ??
|
|
413
|
+
request(method, params = {}, timeoutMs = this.options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS) {
|
|
413
414
|
const turnFloor = method === "turn/start" ? new Set([this.currentTurns?.get(params.threadId), ...[...this.turns].filter(([, value]) => value.threadId === params.threadId).map(([id]) => id)].filter(Boolean)) : null;
|
|
414
415
|
if (method === "turn/start" && params.threadId) {
|
|
415
416
|
// Only one productive root Turn is admitted by the daemon fence. Drop
|
|
@@ -576,7 +577,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
576
577
|
const started = await bridge.request("turn/start", { threadId: sessionId, input: [{ type: "text", text: prompt }], cwd, model: options.model ?? null, effort: options.reasoning ?? null, approvalPolicy: "never", sandboxPolicy: { type: "dangerFullAccess" } }, 30_000);
|
|
577
578
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
578
579
|
let completed;
|
|
579
|
-
try { completed =
|
|
580
|
+
try { completed = isTerminalTurnStatus(turn?.status) ? terminalTurn({ turn }, sessionId, turnId, bridge)
|
|
581
|
+
: await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock, turn?.status); }
|
|
580
582
|
catch (error) {
|
|
581
583
|
const retained = bridge.turns?.get(turnId);
|
|
582
584
|
if (retained?.value?.threadId === sessionId && retained.value.turn?.id === turnId && retained.value.turn.status === "completed") {
|
|
@@ -624,11 +626,20 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
|
624
626
|
}, 30_000);
|
|
625
627
|
}
|
|
626
628
|
|
|
627
|
-
async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) })) {
|
|
629
|
+
async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) }), startStatus) {
|
|
628
630
|
// app-server can briefly expose the new Turn as `interrupted` between the
|
|
629
631
|
// turn/start response and its turn/started notification. Give that snapshot
|
|
630
632
|
// one event-loop grace window before treating failure as authoritative.
|
|
631
633
|
const failureNotBefore = Date.now() + 1_000;
|
|
634
|
+
const reconcileSnapshot = async (status, threadStatus) => {
|
|
635
|
+
if (!isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore)) return false;
|
|
636
|
+
if (status === "completed" || startStatus !== "inProgress" || bridge.turns.get(turnId)?.status === "completed") return true;
|
|
637
|
+
// A provisional failure can contradict the accepted start before its
|
|
638
|
+
// delayed notification arrives. Recheck this Turn, never start another.
|
|
639
|
+
const current = await readTurn(bridge, sessionId, turnId, true);
|
|
640
|
+
return bridge.turns.get(turnId)?.status === "completed" || current?.turn?.status === status
|
|
641
|
+
&& isAuthoritativeTerminal(bridge, turnId, status, current.threadStatus, failureNotBefore);
|
|
642
|
+
};
|
|
632
643
|
let nextStoredReadAt = Date.now();
|
|
633
644
|
while (true) {
|
|
634
645
|
bridge.assertHookHealthy?.(sessionId);
|
|
@@ -636,20 +647,20 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
636
647
|
const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
|
|
637
648
|
if (expired || Date.now() >= nextStoredReadAt) {
|
|
638
649
|
const stored = await readTurn(bridge, sessionId, turnId, false);
|
|
639
|
-
if (stored?.turn &&
|
|
650
|
+
if (stored?.turn && await reconcileSnapshot(stored.turn.status, stored.threadStatus)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
|
|
640
651
|
// A loaded app-server can retain a stale `active` Thread state after a
|
|
641
652
|
// provider-side interruption. The persisted turn page is independent of
|
|
642
653
|
// that live subscription and is the compact authority for its terminal
|
|
643
654
|
// status; unlike `includeTurns`, it never hydrates the whole history.
|
|
644
655
|
const persisted = await readPersistedTurn(bridge, sessionId, turnId);
|
|
645
|
-
if (persisted &&
|
|
656
|
+
if (persisted && await reconcileSnapshot(persisted.status, stored?.threadStatus)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
|
|
646
657
|
// Desktop may unload an idle Thread immediately after an interrupted
|
|
647
658
|
// Turn. Reload it once before polling again: otherwise the compact
|
|
648
659
|
// status remains `notLoaded` forever and hides the terminal Turn.
|
|
649
660
|
if (stored?.threadStatus === "notLoaded") {
|
|
650
661
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
651
662
|
const hydrated = await readTurn(bridge, sessionId, turnId, true);
|
|
652
|
-
if (hydrated?.turn &&
|
|
663
|
+
if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
|
|
653
664
|
}
|
|
654
665
|
// Some app-server versions correctly omit turns for a compact read.
|
|
655
666
|
// Once the thread is idle, hydrate exactly once to recover the terminal
|
|
@@ -660,7 +671,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
660
671
|
// an inProgress Turn after emitting the final agent message; treating
|
|
661
672
|
// that stale Turn as terminal incorrectly aborts an otherwise usable
|
|
662
673
|
// worker session.
|
|
663
|
-
if (hydrated?.turn &&
|
|
674
|
+
if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
|
|
664
675
|
}
|
|
665
676
|
// Some app-server builds emit a final agent message but omit both the
|
|
666
677
|
// terminal event and the compact turn record. Wait briefly, hydrate once,
|
|
@@ -671,8 +682,9 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
671
682
|
// app-server reports the Thread idle.
|
|
672
683
|
if (final?.turnId === turnId && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
|
|
673
684
|
const hydrated = await readTurn(bridge, sessionId, turnId, true);
|
|
674
|
-
if (hydrated?.turn &&
|
|
675
|
-
|
|
685
|
+
if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
|
|
686
|
+
if (hydrated?.threadStatus === "idle" && !isTerminalTurnStatus(hydrated.turn?.status))
|
|
687
|
+
return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
|
|
676
688
|
}
|
|
677
689
|
nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
|
|
678
690
|
}
|
|
@@ -722,7 +734,7 @@ export function turnItemEvidence(turn, bridge, sessionId) {
|
|
|
722
734
|
possible_effects: items.some(item => classifyCodexItem(item?.type) !== "non_tool") } : { observed: false };
|
|
723
735
|
}
|
|
724
736
|
|
|
725
|
-
export { terminalCodexOverload } from "./
|
|
737
|
+
export { terminalProviderOverload as terminalCodexOverload } from "./provider-overload-policy.mjs";
|
|
726
738
|
|
|
727
739
|
function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
|
|
728
740
|
function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
2
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
3
3
|
import { admitNativeRequestBuffer, nativeObservationError } from "./native-streams.mjs";
|
|
4
|
+
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { chmod, lstat, mkdir, open, unlink, realpath } from "node:fs/promises";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
@@ -16,7 +17,7 @@ import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmD
|
|
|
16
17
|
const REQUEST = "dd-droid/daemon-request@1", RESPONSE = "dd-droid/daemon-response@1";
|
|
17
18
|
async function removeSocket(file) { try { if (!(await lstat(file)).isSocket()) throw new DroidError("unsafe_socket", "Refusing to replace non-socket daemon path"); await unlink(file); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
18
19
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, operationId = process.env.DD_EVAL_OPERATION_ID ?? randomUUID()) {
|
|
19
|
-
const request = { schema_id: REQUEST, id: operationId, operation, params };
|
|
20
|
+
const request = prepareDaemonRequest({ schema_id: REQUEST, id: operationId, operation, params });
|
|
20
21
|
return await callDaemonObserved({ stateDir, socket: droidPaths(stateDir).socket, request, responseSchema: RESPONSE, harness: 'droid', timeoutMs });
|
|
21
22
|
}
|
|
22
23
|
function sameConfig(a, b) { return ["cwd", "bin", "model", "reasoning", "mode", "provider", "ddFlowBin", "ddFlowHome", "resourceHome", "projectRoot", "noFlow","inactivityMs"].every(key => a?.[key] === b[key]); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedFlowEnv } from './managed-flow-env.mjs';
|
|
1
2
|
import { processSnapshot } from "./process-snapshot.mjs";
|
|
2
3
|
import { ToolObservations, droidToolObservations, journalToolObservations } from "./tool-observations.mjs";
|
|
3
4
|
export { processSnapshot } from "./process-snapshot.mjs";
|
|
@@ -10,7 +11,7 @@ import path from "node:path";
|
|
|
10
11
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
11
12
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
12
13
|
import { writeJsonAtomic } from "./runner-events.mjs";
|
|
13
|
-
import { errorRecord, reportedError, isObservationLoss } from "./operation-errors.mjs";
|
|
14
|
+
import { errorRecord, reportedError, isObservationLoss, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
14
15
|
import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
15
16
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
16
17
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
@@ -20,6 +21,8 @@ const metadataReaders = new Map();
|
|
|
20
21
|
function metadataReader(factory) { if (!metadataReaders.has(factory)) metadataReaders.set(factory, new DroidMetadataReader(factory)); return metadataReaders.get(factory); }
|
|
21
22
|
import { confirmDaemonProcess, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
22
23
|
import { renderAdapterPolicy, lifecycleRetryInstruction } from "./delegation-instructions.mjs";
|
|
24
|
+
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
25
|
+
import { settledNativeTree } from "./native-children.mjs";
|
|
23
26
|
|
|
24
27
|
export const DROID_CONTRACT = "dd-droid-harness@1";
|
|
25
28
|
export const DROID_PROTOCOL = "1.201.0";
|
|
@@ -44,7 +47,7 @@ export function droidEnvironment(config) {
|
|
|
44
47
|
// This adapter deliberately uses the operator's existing Factory login.
|
|
45
48
|
// Undefined is a tombstone: downstream env overlays must not resurrect keys.
|
|
46
49
|
for (const key of Object.keys(env)) if (key.endsWith("API_KEY")) env[key] = undefined;
|
|
47
|
-
return env;
|
|
50
|
+
return { ...env, ...managedFlowEnv(config) };
|
|
48
51
|
}
|
|
49
52
|
export async function prepareDroidHome(config) {
|
|
50
53
|
const paths = droidPaths(config.stateDir);
|
|
@@ -95,6 +98,19 @@ export async function readDroidSession(factory, id) {
|
|
|
95
98
|
const terminal = value => ["completed", "cancelled", "failed", "process_exit", "error"].includes(value);
|
|
96
99
|
const status = value => value === "completed" ? "completed" : ["cancelled", "process_exit"].includes(value) ? "cancelled" : ["failed", "error"].includes(value) ? "failed" : "unknown";
|
|
97
100
|
|
|
101
|
+
export function droidTerminalFailure(outcome, sessionId, settled) {
|
|
102
|
+
if (!["failed", "error"].includes(outcome?.reason) || !outcome.error || typeof outcome.error !== "object") return null;
|
|
103
|
+
const code = classifyProviderError(outcome.error);
|
|
104
|
+
if (code === "provider_failed") return null;
|
|
105
|
+
const turnId = outcome.turnId;
|
|
106
|
+
return Object.assign(new DroidError(code, "Droid native provider request failed", {
|
|
107
|
+
provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_turn_id: turnId,
|
|
108
|
+
terminal_status: "failed", native_source: "droid.agent_turn_outcome", observed_at: new Date().toISOString(),
|
|
109
|
+
provider_error: safeProviderError(outcome.error), will_retry: outcome.error.willRetry === true, native_retry_active: outcome.error.willRetry === true,
|
|
110
|
+
native_turn_items: { observed: settled === true, possible_effects: true, pending: settled !== true }
|
|
111
|
+
}), { retryable: code === "provider_capacity_transient" || code === "provider_rate_limited" });
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
|
|
99
115
|
function descendants(rows, rootPid) { const ids = new Set([rootPid]); for (;;) { const size = ids.size; for (const row of rows) if (ids.has(row.ppid)) ids.add(row.pid); if (size === ids.size) return rows.filter(row => ids.has(row.pid)); } }
|
|
100
116
|
|
|
@@ -360,15 +376,18 @@ export class DroidRuntime {
|
|
|
360
376
|
});
|
|
361
377
|
}
|
|
362
378
|
const outcome = await completed; await this.draining;
|
|
379
|
+
let receipt;
|
|
363
380
|
try {
|
|
364
|
-
|
|
365
|
-
return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
|
|
381
|
+
receipt = await this.inspect(id, { ignoreActive: true });
|
|
366
382
|
} catch (error) {
|
|
367
383
|
// A terminal native outcome is durable independently of a later
|
|
368
384
|
// filesystem/topology observation. Preserve it and let settlement
|
|
369
385
|
// reconcile; do not convert it into a failed prompt.
|
|
370
386
|
return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error) };
|
|
371
387
|
}
|
|
388
|
+
const providerFailure = droidTerminalFailure(outcome, id, receipt.settled);
|
|
389
|
+
if (providerFailure) throw providerFailure;
|
|
390
|
+
return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
|
|
372
391
|
} catch (error) {
|
|
373
392
|
if (current.outcome && isObservationLoss(error)) error = Object.assign(new Error("Droid completed natively but its dispatch acknowledgement was lost", { cause: error }), {
|
|
374
393
|
code: "native_outcome_observation_failed", details: { native_outcome: { provider_session_id: id, turn_id: turnId, result: current.outcome }, settled: false, observation_error: errorRecord(error) }
|
|
@@ -399,6 +418,8 @@ export class DroidRuntime {
|
|
|
399
418
|
catch (error) {
|
|
400
419
|
receipt = { provider_session_id: binding.provider_session_id, adapter_session_id: binding.provider_session_id, settled: false, settlement_observation_error: errorRecord(error) };
|
|
401
420
|
}
|
|
421
|
+
const failure = droidTerminalFailure(outcome, binding.provider_session_id, receipt.settled);
|
|
422
|
+
if (failure) throw failure;
|
|
402
423
|
// Native outcome and tree settlement are independent. Retain the former
|
|
403
424
|
// even while children are running; settled:false keeps capacity reserved.
|
|
404
425
|
const current = this.active;
|
|
@@ -448,7 +469,9 @@ export class DroidRuntime {
|
|
|
448
469
|
const ordered = tasks.toSorted((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
449
470
|
const latest = ordered.at(-1); const running = tasks.some(task => !terminal(task.status));
|
|
450
471
|
const closed = running && prior?.source === "adapter.tree_closed";
|
|
451
|
-
|
|
472
|
+
const outcome = native.outcomes.at(-1), nativeTurn = outcome && terminal(outcome.reason) && typeof outcome.turnId === "string" ? outcome.turnId : null;
|
|
473
|
+
const evidence = !running && nativeTurn ? { terminal_turn_id: nativeTurn, native_source: "droid.agent_turn_outcome", native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 }, ...(outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
|
|
474
|
+
this.topology.set(id, { provider_session_id: id, parent_provider_session_id: this.rootId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: closed ? prior.status : running ? "running" : status(latest.status), source: closed ? prior.source : "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
|
|
452
475
|
}
|
|
453
476
|
return [...this.topology.values()];
|
|
454
477
|
}
|
|
@@ -469,7 +492,8 @@ export class DroidRuntime {
|
|
|
469
492
|
this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
|
|
470
493
|
const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
|
|
471
494
|
const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
|
|
472
|
-
|
|
495
|
+
const rootSettled = (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
|
|
496
|
+
return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
|
|
473
497
|
}
|
|
474
498
|
async observeHook(payload, eventId, deadlineAt = null) {
|
|
475
499
|
try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
|
|
@@ -499,15 +523,14 @@ export class DroidRuntime {
|
|
|
499
523
|
try {
|
|
500
524
|
return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } }, budget.remaining("admission", 15_000));
|
|
501
525
|
} catch (error) {
|
|
502
|
-
|
|
503
|
-
if (event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && details?.phase === "prepare" && details.effect === "no_effect" && details.recoverable === true && details.disposition === "pre_cli_shell_refusal" && (details.refusal_event_key || details.event_key)) {
|
|
526
|
+
if (event === "PreToolUse" && isProvenShellCorrection(error)) {
|
|
504
527
|
const refusal = errorRecord(error);
|
|
505
528
|
await this.journal("hook_rejected", { error: refusal, event_id: eventId, session_id: id });
|
|
506
529
|
await this.save();
|
|
507
530
|
return { continue: false, stopReason: error.message };
|
|
508
531
|
}
|
|
509
532
|
// Identity is proven above; only this owned admission can fail its turn.
|
|
510
|
-
this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: "unknown" } });
|
|
533
|
+
this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: error.details?.effect ?? "unknown", native_turn_items: { observed: false, possible_effects: true, pending: true } } });
|
|
511
534
|
this.rejectPending(this.fatalError);
|
|
512
535
|
try { await budget.run(async () => { await this.save({ fatal_error: errorRecord(this.fatalError) }); await this.journal("hook_rejected", { error: errorRecord(this.fatalError) }); }, "failure_persistence"); }
|
|
513
536
|
catch (storage) { this.fatalError.details.receipt_error = errorRecord(storage); }
|
|
@@ -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 { canonicalNativeOperation } from './adapter-timeouts.mjs';
|
|
4
5
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
6
|
+
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
5
7
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
6
8
|
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
7
9
|
import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
|
|
@@ -18,6 +20,8 @@ import os from "node:os";
|
|
|
18
20
|
import path from "node:path";
|
|
19
21
|
|
|
20
22
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
23
|
+
import { safeProviderError } from "./provider-overload-policy.mjs";
|
|
24
|
+
import { settledNativeTree } from "./native-children.mjs";
|
|
21
25
|
import { observeModel, readModelObservations } from "./model-observations.mjs";
|
|
22
26
|
import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
|
|
23
27
|
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
@@ -39,7 +43,7 @@ const errorPayload = errorRecord;
|
|
|
39
43
|
function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
|
|
40
44
|
|
|
41
45
|
function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
|
|
42
|
-
export function isolatedEnv(config) {
|
|
46
|
+
export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config) }; }
|
|
43
47
|
function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
|
|
44
48
|
|
|
45
49
|
async function writeIsolatedConfig(paths) {
|
|
@@ -151,7 +155,7 @@ async function materializeSessionArchive(paths, archiveDir) {
|
|
|
151
155
|
|
|
152
156
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
153
157
|
operation = canonicalNativeOperation('grok', operation);
|
|
154
|
-
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
158
|
+
const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
|
|
155
159
|
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'grok', timeoutMs });
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -239,6 +243,10 @@ export class Runtime {
|
|
|
239
243
|
parent_provider_session_id: parent,
|
|
240
244
|
status,
|
|
241
245
|
source,
|
|
246
|
+
...(terminal(status) && typeof attempt === "string" && attempt ? { terminal_turn_id: attempt,
|
|
247
|
+
...(typeof value.observed_at === "string" ? { terminal_observed_at: value.observed_at } : {}),
|
|
248
|
+
...(value.provider_error && typeof value.provider_error === "object" ? { provider_error: safeProviderError(value.provider_error), native_source: "grok.subagent.event" } : {}),
|
|
249
|
+
...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) } : {}),
|
|
242
250
|
...(attempt || descendant?.attempt_id ? { attempt_id: attempt ?? descendant.attempt_id } : {}),
|
|
243
251
|
...(newAttempt && descendant?.attempt_id ? { retired_attempt_ids: [...retired, descendant.attempt_id] } : retired.length ? { retired_attempt_ids: retired } : {}),
|
|
244
252
|
...(!newAttempt && !terminal(status) && descendant?.outcome_error ? { outcome_error: descendant.outcome_error } : {})
|
|
@@ -408,7 +416,7 @@ export class Runtime {
|
|
|
408
416
|
if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; }, null, operationId);
|
|
409
417
|
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
|
|
410
418
|
if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
|
|
411
|
-
if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); return { ...result, settled
|
|
419
|
+
if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return { ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }; }
|
|
412
420
|
if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
|
|
413
421
|
if (operation === "session.archive") { await this.requireSettled(); return await this.productive(operation, async () => { let session = this.sessions.get(params.sessionId); if (!session?.cwd) { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); session = this.sessions.get(result.provider_session_id); } return await exportSessionArchive(this.paths, session.provider_session_id, absolute(session.cwd, "Grok Build session cwd"), params.archiveDir, this.state.versions); }); }
|
|
414
422
|
if (operation === "daemon.stop") {
|
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
5
|
+
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
4
6
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
5
7
|
import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
|
|
6
8
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
@@ -10,13 +12,15 @@ import { spawn } from "node:child_process";
|
|
|
10
12
|
import net from "node:net";
|
|
11
13
|
import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
12
14
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
13
|
-
import { errorRecord } from "./operation-errors.mjs";
|
|
15
|
+
import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
14
16
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
15
17
|
import os from "node:os";
|
|
16
18
|
import path from "node:path";
|
|
17
19
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
18
20
|
import { writeJsonAtomic } from "./runner-events.mjs";
|
|
19
21
|
import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
|
|
22
|
+
import { safeProviderError } from "./provider-overload-policy.mjs";
|
|
23
|
+
import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
|
|
20
24
|
import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
21
25
|
|
|
22
26
|
const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@2", ARCHIVE_SCHEMA = "dd-opencode/session-archive@2";
|
|
@@ -40,13 +44,13 @@ const writeJson = writeJsonAtomic;
|
|
|
40
44
|
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
41
45
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
42
46
|
function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
|
|
43
|
-
function isolatedEnv(config) {
|
|
47
|
+
export function isolatedEnv(config) {
|
|
44
48
|
const env = { ...process.env };
|
|
45
49
|
for (const name of ["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_PASSWORD", "OPENCODE_PTY_HANDOFF", "OPENCODE_SERVER", "OPENCODE_SIMULATE"]) delete env[name];
|
|
46
50
|
return { ...env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome,
|
|
47
51
|
XDG_STATE_HOME: config.stateHome, OPENCODE_DB: config.database, OPENCODE_CONFIG_DIR: config.adapterDir,
|
|
48
52
|
OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_SERVER_PASSWORD: config.password,
|
|
49
|
-
...(config
|
|
53
|
+
...managedFlowEnv(config),
|
|
50
54
|
DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "none" };
|
|
51
55
|
}
|
|
52
56
|
export async function run(bin, args, config, options = {}) {
|
|
@@ -63,7 +67,7 @@ const errorPayload = errorRecord;
|
|
|
63
67
|
|
|
64
68
|
async function writePlugin(paths, config) {
|
|
65
69
|
const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
66
|
-
const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot })});\n`;
|
|
70
|
+
const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
|
|
67
71
|
const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
|
|
68
72
|
}
|
|
69
73
|
async function prepare(paths, config, authSource) {
|
|
@@ -136,7 +140,7 @@ async function importArchive(config, client) {
|
|
|
136
140
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
137
141
|
const state = await readState(stateDir);
|
|
138
142
|
if (state && state.schema_id !== STATE_SCHEMA) throw new DaemonError("legacy_opencode_state", "Use archive/opencode-v1-final-beta134 for v1 states; the v2 adapter cannot dispatch to a legacy daemon", false, { effect: "no_effect" });
|
|
139
|
-
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
143
|
+
const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
|
|
140
144
|
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
|
|
141
145
|
}
|
|
142
146
|
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow","configSource","authSource"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
@@ -187,13 +191,15 @@ export class Runtime {
|
|
|
187
191
|
}
|
|
188
192
|
async describe(id) {
|
|
189
193
|
const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]);
|
|
190
|
-
if (!info.directory || path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace");
|
|
191
194
|
const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
|
|
195
|
+
if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
|
|
196
|
+
|| existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
|
|
192
197
|
const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent,
|
|
193
198
|
root_provider_session_id: parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id, cwd: info.directory };
|
|
194
199
|
this.sessions.set(id, record);
|
|
195
200
|
for (const child of children) {
|
|
196
|
-
|
|
201
|
+
const retained = this.sessions.get(child.id);
|
|
202
|
+
if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
|
|
197
203
|
this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID, root_provider_session_id: record.root_provider_session_id, cwd: child.directory });
|
|
198
204
|
}
|
|
199
205
|
const pending = new Map();
|
|
@@ -202,6 +208,18 @@ export class Runtime {
|
|
|
202
208
|
pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
|
|
203
209
|
}
|
|
204
210
|
const busy = session => Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0);
|
|
211
|
+
for (const child of children) {
|
|
212
|
+
const history = await this.client.messages(child.id);
|
|
213
|
+
const index = history.findLastIndex(message => message.info.role === "user" && message.info.sessionID === child.id);
|
|
214
|
+
const terminal = index >= 0 && history.slice(index + 1).findLast(message => message.info.role === "idle" && message.info.sessionID === child.id);
|
|
215
|
+
if (!terminal || busy(child.id)) continue;
|
|
216
|
+
const outcome = terminal.info.outcome;
|
|
217
|
+
if (!["succeeded", "failed", "interrupted"].includes(outcome)) continue;
|
|
218
|
+
Object.assign(child, { status: outcome === "succeeded" ? "completed" : outcome === "failed" ? "failed" : "cancelled",
|
|
219
|
+
terminal_turn_id: history[index].info.id, native_source: "opencode.v2.session.execution",
|
|
220
|
+
native_turn_items: { observed: true, possible_effects: true, pending: false },
|
|
221
|
+
...(terminal.info.error ? { provider_error: safeProviderError(terminal.info.error) } : {}) });
|
|
222
|
+
}
|
|
205
223
|
const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
|
|
206
224
|
const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
|
|
207
225
|
const inputIndex = messages.findLastIndex(message => message.info.role === "user");
|
|
@@ -212,6 +230,8 @@ export class Runtime {
|
|
|
212
230
|
if (this.state.config.journal) journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools);
|
|
213
231
|
return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
|
|
214
232
|
assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
|
|
233
|
+
route_conformance: { matches: [info, ...children].every(session => session.directory && path.resolve(session.directory) === path.resolve(this.state.config.cwd)), expected: this.state.config.cwd, actual: info.directory ?? null, actual_provenance: "native_session_readback" },
|
|
234
|
+
route_violations: [info, ...children].filter(session => !session.directory || path.resolve(session.directory) !== path.resolve(this.state.config.cwd)).map(session => ({ provider_session_id: session.id, expected: this.state.config.cwd, actual: session.directory ?? null, actual_provenance: "native_session_readback" })),
|
|
215
235
|
settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
|
|
216
236
|
tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
|
|
217
237
|
tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
|
|
@@ -235,6 +255,7 @@ export class Runtime {
|
|
|
235
255
|
}
|
|
236
256
|
async requireSettled(id) {
|
|
237
257
|
const value = await this.describe(id);
|
|
258
|
+
if (!value.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session has an unexpected workspace", false, { effect: "no_effect", ...value.route_violations[0], native_outcome: value.terminal_status, settled: value.settled });
|
|
238
259
|
if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status });
|
|
239
260
|
const config = this.state.config;
|
|
240
261
|
if (config.provider && config.model) checkObservedProfile({ ...config, reasoning: config.variant, mode: value.parent_provider_session_id ? "dd-flow-worker" : config.agent }, value.observed_profile);
|
|
@@ -254,30 +275,87 @@ export class Runtime {
|
|
|
254
275
|
} catch (cause) {
|
|
255
276
|
const error = completed ? new DaemonError("operation_observation_lost", "OpenCode returned but its durable observation could not be saved; do not resend", false, { result, cause: errorPayload(cause) }) : cause;
|
|
256
277
|
const failures = [];
|
|
257
|
-
try { await this.persist({ active_tree: error.details?.effect === "no_effect" ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
|
|
278
|
+
try { await this.persist({ active_tree: error.details?.effect === "no_effect" && error.details?.native_turn_items?.possible_effects !== true ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
|
|
258
279
|
try { await this.journal(`${name}.failed`, null); } catch (secondary) { failures.push(errorPayload(secondary)); }
|
|
259
280
|
if (failures.length) error.details = { ...error.details, observation_errors: failures };
|
|
260
281
|
throw error;
|
|
261
282
|
} finally { this.active = null; }
|
|
262
283
|
}
|
|
263
284
|
async cancelTree(id, seen = new Set()) {
|
|
285
|
+
if (!this.sessions.has(id)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
|
|
264
286
|
if (seen.has(id)) return [];
|
|
287
|
+
const native = await this.client.inspect(id), retained = this.sessions.get(id);
|
|
288
|
+
if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
|
|
265
289
|
seen.add(id);
|
|
266
290
|
const errors = []; let children = [];
|
|
267
291
|
try { children = await this.client.children(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) }); }
|
|
268
292
|
for (const child of children) {
|
|
269
|
-
if (child.parentID !== id ||
|
|
293
|
+
if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
|
|
270
294
|
errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
|
|
271
295
|
}
|
|
272
|
-
|
|
296
|
+
this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: id, root_provider_session_id: this.sessions.get(id)?.root_provider_session_id ?? id, cwd: child.directory });
|
|
297
|
+
try { errors.push(...await this.cancelTree(child.id, seen)); }
|
|
298
|
+
catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
|
|
273
299
|
}
|
|
274
300
|
try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
|
|
275
301
|
return errors;
|
|
276
302
|
}
|
|
303
|
+
async observeHookFailures(current = this.nativeHookOperation) {
|
|
304
|
+
if (!current || this.nativeHookOperation !== current) return;
|
|
305
|
+
if (current.reading) return await current.reading;
|
|
306
|
+
current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
|
|
307
|
+
try { await current.reading; } finally { current.reading = null; }
|
|
308
|
+
}
|
|
309
|
+
retainHookFailure(current, error) {
|
|
310
|
+
if (this.nativeHookOperation !== current || isProvenShellCorrection(error)) return;
|
|
311
|
+
error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
|
|
312
|
+
current.failure ??= error;
|
|
313
|
+
}
|
|
277
314
|
async dispatch(operation, params, operationId = null) {
|
|
278
315
|
const assertDispatch = captureDispatchGuard(this);
|
|
279
316
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
280
317
|
if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
|
|
318
|
+
if (operation === "hook.resolve") {
|
|
319
|
+
const current = this.nativeHookOperation, sessionId = params.sessionId, payload = params.payload;
|
|
320
|
+
if (!current || !sessionId || typeof payload?.input?.command !== "string" || typeof payload.tool_call_id !== "string" || !payload.tool_call_id || typeof payload.message_id !== "string" || !payload.message_id || !["before", "after"].includes(payload.phase))
|
|
321
|
+
throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
|
|
322
|
+
let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
|
|
323
|
+
try {
|
|
324
|
+
const tree = await this.childTree(current.sessionId);
|
|
325
|
+
if (sessionId !== current.sessionId && !tree.some(child => child.id === sessionId && !child.fork)) throw new DaemonError("foreign_session", "OpenCode hook Session is outside the owned native tree");
|
|
326
|
+
owned = true;
|
|
327
|
+
const native = await this.client.inspect(sessionId), retained = this.sessions.get(sessionId), child = tree.find(item => item.id === sessionId);
|
|
328
|
+
if (native.id !== sessionId || child && (native.parentID !== child.parentID || native.fork) || retained && (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("native_hook_identity_conflict", "OpenCode hook Session ancestry changed");
|
|
329
|
+
if (!native.directory) throw new DaemonError("native_hook_unproven", "OpenCode hook Session directory is unavailable");
|
|
330
|
+
if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
|
|
331
|
+
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
|
|
332
|
+
const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
|
|
333
|
+
provider_session_id: sessionId, root_provider_session_id: current.sessionId, tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
|
|
334
|
+
command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
|
|
335
|
+
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
|
|
336
|
+
current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
|
|
337
|
+
id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
|
|
338
|
+
model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
|
|
339
|
+
} };
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (owned) this.retainHookFailure(current, error);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (operation === "hook.verify") {
|
|
346
|
+
const current = this.nativeHookOperation, binding = params.binding;
|
|
347
|
+
if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
|
|
348
|
+
|| JSON.stringify(current.requests.get(binding?.request_id)?.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode hook generation is no longer current");
|
|
349
|
+
return { current: true };
|
|
350
|
+
}
|
|
351
|
+
if (operation === "hook.failed") {
|
|
352
|
+
if (params.failure) {
|
|
353
|
+
await this.dispatch("hook.verify", { binding: params.binding });
|
|
354
|
+
if (JSON.stringify(params.failure.details?.native_hook_binding) !== JSON.stringify(params.binding) || !params.failure.details?.receipt_error || typeof params.failure.code !== "string" || typeof params.failure.message !== "string") throw new DaemonError("native_hook_receipt_invalid", "OpenCode fallback hook error is not bound");
|
|
355
|
+
this.retainHookFailure(this.nativeHookOperation, reportedError(params.failure, "Native hook failed"));
|
|
356
|
+
}
|
|
357
|
+
await this.observeHookFailures(); return { observed: true };
|
|
358
|
+
}
|
|
281
359
|
if (operation === "daemon.doctor") {
|
|
282
360
|
const info = await this.client.health();
|
|
283
361
|
await this.client.call("model.list", { location: { directory: this.state.config.cwd } });
|
|
@@ -309,26 +387,33 @@ export class Runtime {
|
|
|
309
387
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
310
388
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
311
389
|
this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
|
|
312
|
-
|
|
390
|
+
const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
|
|
391
|
+
this.state.hook_generation = current.generation; this.nativeHookOperation = current;
|
|
392
|
+
const checkHookFailures = async () => { await this.observeHookFailures(current); if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode prompt generation changed"); if (current.failure) throw current.failure; };
|
|
393
|
+
let reply;
|
|
394
|
+
try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
313
395
|
save: async receipt => { this.state.operations[key] = receipt; await this.persist(); },
|
|
314
|
-
assertDispatch, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
|
|
396
|
+
assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
|
|
315
397
|
onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
|
|
316
398
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
317
399
|
onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
|
|
318
|
-
});
|
|
400
|
+
}); await checkHookFailures(); }
|
|
401
|
+
finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
|
|
402
|
+
const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
|
|
319
403
|
const expectedAgent = this.sessions.get(params.sessionId)?.parent_provider_session_id ? "dd-flow-worker" : this.state.config.agent;
|
|
320
404
|
const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: reply.info.mode ?? null };
|
|
321
405
|
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, reply_profile);
|
|
322
406
|
let result;
|
|
323
407
|
try { result = await this.describe(params.sessionId); }
|
|
324
|
-
catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
|
|
408
|
+
catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, terminal_turn_id: reply.admission_message_id, terminal_status: "completed", native_outcome: "succeeded", observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
|
|
325
409
|
result.assistant_text = assistant_text;
|
|
410
|
+
if (!result.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session moved during execution", false, { ...result.route_violations[0], native_outcome: result.terminal_status, settled: result.settled, result });
|
|
326
411
|
result.response_message_id = reply.info.id;
|
|
327
412
|
result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
|
|
328
413
|
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
|
|
329
414
|
if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
330
415
|
const info = await this.client.fork(params.sessionId, params.messageId);
|
|
331
|
-
const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
|
|
416
|
+
const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.fork ? null : info.parentID ?? params.sessionId, root_provider_session_id: info.fork ? info.id : this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
|
|
332
417
|
this.sessions.set(info.id, created);
|
|
333
418
|
try {
|
|
334
419
|
const fork = await this.describe(info.id);
|