@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { SETUP_STARTUP_MS } from './setup-progress.mjs';
|
|
5
5
|
|
|
6
6
|
import { checkObservedProfile } from "./model-observations.mjs";
|
|
7
|
-
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
7
|
+
import { classifyProviderError, safeProviderError, streamRequestKey } from "./provider-overload-policy.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
10
10
|
export const AGY_VERSION_TIMEOUT_MS = 30_000;
|
|
@@ -14,14 +14,22 @@ export class AgyError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function agyTerminalFailure(result, metadata = {}) {
|
|
17
|
-
|
|
17
|
+
let native = typeof result.error === "object" && result.error ? result.error : { message: result.error ?? "Antigravity returned ERROR" };
|
|
18
|
+
if (typeof result.error === "string" && /^AGY_ERROR\s*[: ]/.test(result.error)) {
|
|
19
|
+
try {
|
|
20
|
+
const envelope = JSON.parse(result.error.replace(/^AGY_ERROR\s*:?\s*/, ""));
|
|
21
|
+
const error = envelope.error ?? envelope;
|
|
22
|
+
if (error && typeof error === "object" && !Array.isArray(error)) native = error;
|
|
23
|
+
} catch { /* A malformed envelope remains a native failure, never evidence. */ }
|
|
24
|
+
}
|
|
18
25
|
const category = classifyProviderError(native);
|
|
19
26
|
const legacy = { provider_quota_exhausted: "agy_provider_quota_exhausted", provider_limit_unknown: "agy_provider_limit_unknown", provider_auth_failed: "agy_provider_account", provider_failed: "agy_provider_failed" };
|
|
20
27
|
let code = result.status === "CANCELLED" ? "agy_turn_cancelled" : legacy[category] ?? category;
|
|
21
28
|
// A local generation/usage count is not a native request identity.
|
|
22
29
|
const identity = result.turn_id ?? result.request_id;
|
|
23
30
|
const turn = typeof identity === "string" && identity ? identity : null;
|
|
24
|
-
const
|
|
31
|
+
const stream = streamRequestKey(metadata.request_proof);
|
|
32
|
+
const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited"].includes(category) && !turn && !stream;
|
|
25
33
|
if (proofMissing) code = "provider_overload_evidence_unavailable";
|
|
26
34
|
return new AgyError(code, String(native.message ?? "Antigravity returned ERROR"), code === "provider_capacity_transient" || code === "provider_rate_limited", {
|
|
27
35
|
provider_result: result, ...metadata, provider_error: safeProviderError(native), native_source: "agy.result", terminal_status: result.status === "ERROR" ? "failed" : "cancelled",
|
|
@@ -57,6 +65,12 @@ export function observedProfile(init = {}, requested = {}) {
|
|
|
57
65
|
|
|
58
66
|
export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed, { required: requested.permission_mode ? ["permission_mode"] : [] }); }
|
|
59
67
|
|
|
68
|
+
export function agyModelArgs(model, reasoning) {
|
|
69
|
+
const encoded = /^gemini-.+-(low|medium|high)$/.exec(model)?.[1];
|
|
70
|
+
if (encoded && encoded !== reasoning) throw new AgyError("agy_profile_invalid", "Requested reasoning conflicts with the selected Gemini model");
|
|
71
|
+
return ["--model", model, ...(encoded ? [] : ["--effort", reasoning])];
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
export async function doctor(options = {}) {
|
|
61
75
|
const bin = options.bin ?? process.env.DD_AGY_BIN ?? "agy", temporary = await mkdtemp(path.join(os.tmpdir(), "dd-agy-doctor-"));
|
|
62
76
|
try {
|
|
@@ -7,12 +7,13 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
|
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
9
9
|
import net from "node:net";
|
|
10
|
-
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
10
|
+
import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
11
11
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
12
12
|
import { parseCapacityOperation } from "./provider-overload-policy.mjs";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
15
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
16
|
+
import { nativeExecutionReceipt } from "./native-children.mjs";
|
|
16
17
|
import { errorRecord } from "./operation-errors.mjs";
|
|
17
18
|
import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, codexReadPhase, readCodexHistory, retainedCodexTurn, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
|
|
18
19
|
import { assertDaemonOwnership, assertStartupChild, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
@@ -77,7 +78,7 @@ export async function inspectCodexTree(bridge, options, { remember = () => {}, a
|
|
|
77
78
|
options = { ...options, readPhase: options.readPhase ?? codexReadPhase(bridge, options) };
|
|
78
79
|
const sessions = new Set([options.sessionId]);
|
|
79
80
|
const observed = await observeCodexSessions(bridge, options, sessions, active, remember);
|
|
80
|
-
return { ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
|
|
81
|
+
return nativeExecutionReceipt({ ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) }, { daemonId: options.daemonId });
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export async function cancelCodexTree(bridge, options, { remember = () => {}, active = () => null, timeoutMs = 5_000 } = {}) {
|
|
@@ -120,32 +121,44 @@ export async function startDaemon(options) {
|
|
|
120
121
|
}
|
|
121
122
|
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(locations(options.stateDir).state) }); }
|
|
122
123
|
|
|
124
|
+
export async function prepareCodexStop({ bridge, state, sessions, active, save, stateDir, cancelTree }) {
|
|
125
|
+
if (cancelTree) cancelPendingDispatch(bridge);
|
|
126
|
+
if (active() && !cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
|
|
127
|
+
let revision;
|
|
128
|
+
await waitForSettlement({
|
|
129
|
+
knownSessions: () => [...sessions],
|
|
130
|
+
observe: () => { revision = state.native_execution_revision; return observeCodexSessions(bridge, state.config, sessions, active); },
|
|
131
|
+
...(cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
|
|
132
|
+
});
|
|
133
|
+
if (active() || state.native_execution_revision !== revision) throw Object.assign(new Error("native activity changed during settlement observation"), { code: "tree_not_settled" });
|
|
134
|
+
commitDaemonShutdown(stateDir);
|
|
135
|
+
await save({ active_tree: false });
|
|
136
|
+
return { clean: true, settled: true, native_settled: true, native_settlement: { confirmed: true, revision } };
|
|
137
|
+
}
|
|
138
|
+
|
|
123
139
|
export async function serveDaemon(stateDir) {
|
|
124
140
|
const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
|
|
125
141
|
const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
|
|
126
142
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
127
143
|
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
|
|
144
|
+
bridge.options.onNativeActivity = () => {
|
|
145
|
+
state.native_execution_revision = (state.native_execution_revision ?? 0) + 1;
|
|
146
|
+
if (state.active_tree !== true) void save({ active_tree: true }).catch(error => { bridge.rejectAll(error); });
|
|
147
|
+
};
|
|
128
148
|
const dispatch = async (operation, params, operationId) => {
|
|
129
149
|
const assertDispatch = captureDispatchGuard(bridge);
|
|
130
150
|
if (operation === "hook.verify") {
|
|
131
151
|
if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
|
|
132
152
|
return { ...bridge.verifyNativeHook(params), daemonId: state.daemon_id, ddFlowHome: state.config.ddFlowHome, projectRoot: state.config.projectRoot };
|
|
133
153
|
}
|
|
134
|
-
if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, shutdown_state: state.shutdown_state };
|
|
154
|
+
if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, active_tree: state.active_tree ?? null, shutdown_state: state.shutdown_state };
|
|
135
155
|
// The runner may need to retire an orphaned daemon after its caller was
|
|
136
156
|
// interrupted. `daemon.stop` is the one control operation that remains
|
|
137
157
|
// available while a request is marked active; callers must first prove the
|
|
138
158
|
// provider Session itself is idle.
|
|
139
159
|
if (operation === "daemon.stop") {
|
|
140
160
|
return await completeDaemonShutdown(paths.dir, state, save, async () => {
|
|
141
|
-
|
|
142
|
-
if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
|
|
143
|
-
await waitForSettlement({
|
|
144
|
-
knownSessions: () => [...sessions],
|
|
145
|
-
observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
|
|
146
|
-
...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
|
|
147
|
-
});
|
|
148
|
-
return { clean: true };
|
|
161
|
+
return await prepareCodexStop({ bridge, state, sessions, active: () => active, save, stateDir: paths.dir, cancelTree: params.cancelTree });
|
|
149
162
|
}, [
|
|
150
163
|
["provider", () => bridge.close()],
|
|
151
164
|
["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
|
|
@@ -154,7 +167,7 @@ export async function serveDaemon(stateDir) {
|
|
|
154
167
|
}
|
|
155
168
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
|
|
156
169
|
if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
|
|
157
|
-
if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
|
|
170
|
+
if (operation === "session.create") { active = operation; try { await save({ active_operation: active, active_tree: true }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
|
|
158
171
|
if (operation === "session.prompt") {
|
|
159
172
|
parseCapacityOperation(operationId);
|
|
160
173
|
// Admission can wait for a permit. Observe again after it returns, not
|
|
@@ -163,6 +176,7 @@ export async function serveDaemon(stateDir) {
|
|
|
163
176
|
// journal entries and must not both pass the idle check while inspecting.
|
|
164
177
|
active = operation;
|
|
165
178
|
try {
|
|
179
|
+
await save({ active_operation: active, active_tree: true });
|
|
166
180
|
const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
|
|
167
181
|
assertDispatch();
|
|
168
182
|
if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
|
|
@@ -173,7 +187,7 @@ export async function serveDaemon(stateDir) {
|
|
|
173
187
|
}
|
|
174
188
|
finally { active = null; await save({ active_operation: null }); }
|
|
175
189
|
}
|
|
176
|
-
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
|
|
190
|
+
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active, active_tree: true }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
|
|
177
191
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
178
192
|
const owned = new Set(sessions);
|
|
179
193
|
for (const id of owned) for (const child of bridge.directChildren?.(id) ?? []) owned.add(child.provider_session_id);
|
|
@@ -180,6 +180,10 @@ export class CodexBridge {
|
|
|
180
180
|
|
|
181
181
|
receive(line) {
|
|
182
182
|
let message; try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
|
|
183
|
+
// Read-induced idle/status notifications are not new execution. Native
|
|
184
|
+
// turn/item events and newly active threads invalidate an idle proof.
|
|
185
|
+
if (/^(turn|item)\//.test(message.method ?? "")
|
|
186
|
+
|| message.method === "thread/status/changed" && message.params?.status?.type === "active") this.options.onNativeActivity?.();
|
|
183
187
|
this.observeItemPhase(message);
|
|
184
188
|
try { this.observeTopology(message); }
|
|
185
189
|
catch (error) { this.topologyError = error; this.rejectAll(error); return; }
|
|
@@ -66,7 +66,7 @@ export async function serveDaemon(stateDir) {
|
|
|
66
66
|
runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
|
|
67
67
|
try { inspected = await runtime.inspect(params.sessionId, { ignoreActive: true }); }
|
|
68
68
|
catch (error) { inspected = { harness: "droid-cli", provider_session_id: params.sessionId, descendants: [...runtime.topology.values()], inspection_error: errorRecord(error) }; }
|
|
69
|
-
return { ...inspected, cancelled: true, settled: cleanup.settled, cleanup };
|
|
69
|
+
return { ...inspected, cancelled: true, settled: inspected.settled === true && cleanup.native_settled === true, native_settled: inspected.settled === true && cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
|
|
70
70
|
}
|
|
71
71
|
if (operation === "daemon.stop") {
|
|
72
72
|
stopping = true;
|
|
@@ -78,9 +78,10 @@ export async function serveDaemon(stateDir) {
|
|
|
78
78
|
if (runtime.rootId) try { await runtime.inspect(runtime.rootId, { ignoreActive: true }); } catch (error) { cleanup.inspection_error = errorRecord(error); }
|
|
79
79
|
// Native evidence failures remain visible, but cannot strand copied
|
|
80
80
|
// credentials or the daemon after the owned process tree is gone.
|
|
81
|
-
const clean =
|
|
82
|
-
return { clean, cleanup };
|
|
81
|
+
const clean = cleanup.native_settled === true && !cleanup.inspection_error;
|
|
82
|
+
return { clean, settled: cleanup.native_settled === true, native_settled: cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
|
|
83
83
|
}, [
|
|
84
|
+
["provider", async () => { if (!runtime.closed) throw new DroidError("provider_process_stop_incomplete", "Droid provider has not closed"); }],
|
|
84
85
|
["auth", () => removeDroidAuth(state.config)],
|
|
85
86
|
["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
|
|
86
87
|
]);
|
|
@@ -22,7 +22,7 @@ function metadataReader(factory) { if (!metadataReaders.has(factory)) metadataRe
|
|
|
22
22
|
import { confirmDaemonProcess, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
23
23
|
import { renderAdapterPolicy, lifecycleRetryInstruction } from "./delegation-instructions.mjs";
|
|
24
24
|
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
25
|
-
import { settledNativeTree } from "./native-children.mjs";
|
|
25
|
+
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
26
26
|
|
|
27
27
|
export const DROID_CONTRACT = "dd-droid-harness@1";
|
|
28
28
|
export const DROID_PROTOCOL = "1.201.0";
|
|
@@ -256,7 +256,7 @@ export class DroidRuntime {
|
|
|
256
256
|
}
|
|
257
257
|
async observeSettings(id, settings, source, extra = {}) {
|
|
258
258
|
const observed = this.profile(settings);
|
|
259
|
-
const event = await observeModel({ journal: this.config.journal, harness: "droid-cli", sessionId: id, parentSessionId: id === this.rootId ? null : this.rootId,
|
|
259
|
+
const event = await observeModel({ journal: this.config.journal, harness: "droid-cli", sessionId: id, parentSessionId: id === this.rootId ? null : this.topology.get(id)?.parent_provider_session_id ?? this.rootId,
|
|
260
260
|
requested: this.config, observed, source, ...extra });
|
|
261
261
|
if (id === this.rootId) this.observedProfile = observed;
|
|
262
262
|
this.modelEventIds ??= new Map();
|
|
@@ -267,8 +267,9 @@ export class DroidRuntime {
|
|
|
267
267
|
return observed;
|
|
268
268
|
}
|
|
269
269
|
validateMetadata(id, native) {
|
|
270
|
-
|
|
271
|
-
if (id !== this.rootId && !this.topology.has(
|
|
270
|
+
const parent = native?.header.callingSessionId;
|
|
271
|
+
if (!native || path.resolve(native.header.cwd) !== this.config.cwd || native.header.id !== id || (id !== this.rootId && (id === parent || parent !== this.rootId && !this.topology.has(parent) || this.topology.has(id) && this.topology.get(id).parent_provider_session_id !== parent))) throw new DroidError("session_identity_mismatch", "Native model observation is outside the owned Session tree");
|
|
272
|
+
if (id !== this.rootId && !this.topology.has(id)) this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parent, tool_use_id: native.header.callingToolUseId, status: "unknown", source: "native.metadata" });
|
|
272
273
|
}
|
|
273
274
|
async observeModels() {
|
|
274
275
|
if (!this.rootId || !this.loaded) return;
|
|
@@ -276,9 +277,12 @@ export class DroidRuntime {
|
|
|
276
277
|
const info = await stat(catalog).catch(error => { if (error.code === "ENOENT") return null; throw error; });
|
|
277
278
|
if (info && info.mtimeMs !== this.modelCatalogMtime) {
|
|
278
279
|
const value = await readJson(catalog);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
const tasks = Array.isArray(value?.invocations) ? value.invocations : [];
|
|
281
|
+
for (let size = -1; size !== this.topology.size;) { size = this.topology.size;
|
|
282
|
+
for (const task of tasks) if (task?.childSessionId && (task.parentSessionId === this.rootId || this.topology.has(task.parentSessionId))) {
|
|
283
|
+
const native = await this.metadata.read(task.childSessionId);
|
|
284
|
+
if (native) this.validateMetadata(task.childSessionId, native);
|
|
285
|
+
}
|
|
282
286
|
}
|
|
283
287
|
this.modelCatalogMtime = info.mtimeMs;
|
|
284
288
|
}
|
|
@@ -286,7 +290,7 @@ export class DroidRuntime {
|
|
|
286
290
|
const routing = await readDroidRoutingLog(log, this.state.model_log_cursor);
|
|
287
291
|
for (const transition of routing.events) {
|
|
288
292
|
const native = await this.metadata.read(transition.session_id);
|
|
289
|
-
if (!native || (transition.session_id !== this.rootId && native.header.callingSessionId !== this.rootId)) continue;
|
|
293
|
+
if (!native || (transition.session_id !== this.rootId && native.header.callingSessionId !== this.rootId && !this.topology.has(native.header.callingSessionId))) continue;
|
|
290
294
|
this.validateMetadata(transition.session_id, native);
|
|
291
295
|
await this.observeSettings(transition.session_id, { ...native.settings, modelId: transition.model, model: transition.model },
|
|
292
296
|
{ path: log, channel: "native_router", event_sha256: transition.event_sha256 }, { reason: transition.reason, nativeTimestamp: transition.native_timestamp, previousModel: transition.previous_model });
|
|
@@ -439,39 +443,72 @@ export class DroidRuntime {
|
|
|
439
443
|
return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: binding.assistant_text ?? "", turn_id: binding.turn_id, recovered_from: "native.agent_turn_outcome" };
|
|
440
444
|
}
|
|
441
445
|
async refreshTopology() {
|
|
442
|
-
let catalog;
|
|
443
|
-
try { catalog = await
|
|
444
|
-
catch (error) { if (!(error instanceof SyntaxError)) throw error; this.topologyIncomplete = true; return [...this.topology.values()]; }
|
|
446
|
+
let catalog, missing = false;
|
|
447
|
+
try { catalog = JSON.parse(await readFile(path.join(this.paths.factory, "task-invocations.json"), "utf8")); }
|
|
448
|
+
catch (error) { if (error.code === "ENOENT") missing = true; else { if (!(error instanceof SyntaxError)) throw error; this.topologyIncomplete = true; return [...this.topology.values()]; } }
|
|
445
449
|
this.topologyIncomplete = false;
|
|
446
|
-
const invocations = catalog?.invocations ?? [];
|
|
447
450
|
const root = this.rootId ? await readDroidSession(this.paths.factory, this.rootId) : null;
|
|
448
|
-
const
|
|
451
|
+
const taskTools = native => new Map((native?.records ?? []).flatMap(record => record.message?.content ?? []).filter(block => block.type === "tool_use" && block.name === "Task").map(block => [block.id, block]));
|
|
452
|
+
const validCatalog = catalog && !Array.isArray(catalog) && Array.isArray(catalog.invocations);
|
|
453
|
+
if (!validCatalog) {
|
|
454
|
+
const noChildren = !this.topology.size && !taskTools(root).size;
|
|
455
|
+
const noDispatch = !this.active && !this.lastResult && !this.state.active_tree && (!root || root.complete && root.records.every(record => record.type === "session_start"));
|
|
456
|
+
const completeNoTask = root?.complete && terminal(root.outcomes.at(-1)?.reason) && root.tool_calls.completeness === "complete";
|
|
457
|
+
this.topologyIncomplete = !(missing && noChildren && (noDispatch || completeNoTask));
|
|
458
|
+
return [...this.topology.values()];
|
|
459
|
+
}
|
|
460
|
+
const invocations = catalog.invocations, owned = new Set([this.rootId, ...this.topology.keys()]);
|
|
461
|
+
// Resolve ancestry before validating records so malformed foreign entries
|
|
462
|
+
// do not block this tree, while malformed reachable children do.
|
|
463
|
+
for (let size = -1; size !== owned.size;) { size = owned.size; for (const task of invocations) if (owned.has(task?.parentSessionId) && task?.childSessionId) owned.add(task.childSessionId); }
|
|
464
|
+
const parents = new Map([[this.rootId, root]]);
|
|
465
|
+
for (const id of owned) if (id !== this.rootId) parents.set(id, await readDroidSession(this.paths.factory, id));
|
|
466
|
+
for (const [parentId, native] of parents) {
|
|
467
|
+
if (!native?.complete) this.topologyIncomplete = true;
|
|
468
|
+
for (const toolId of taskTools(native).keys()) if (!invocations.some(task => task?.parentSessionId === parentId && task?.parentToolUseId === toolId)) this.topologyIncomplete = true;
|
|
469
|
+
}
|
|
449
470
|
const groups = new Map();
|
|
450
471
|
for (const task of invocations) {
|
|
451
|
-
if (task
|
|
472
|
+
if (!task || typeof task !== "object") { this.topologyIncomplete = true; continue; }
|
|
473
|
+
if (!owned.has(task.parentSessionId) && !owned.has(task.childSessionId) && ![...parents.values()].some(native => taskTools(native).has(task.parentToolUseId))) continue;
|
|
474
|
+
if (![task.parentSessionId, task.childSessionId, task.parentToolUseId, task.taskInvocationId, task.status].every(value => typeof value === "string" && value.length)) { this.topologyIncomplete = true; continue; }
|
|
452
475
|
const tasks = groups.get(task.childSessionId) ?? []; tasks.push(task); groups.set(task.childSessionId, tasks);
|
|
453
476
|
}
|
|
477
|
+
for (const id of this.topology.keys()) if (!groups.has(id)) this.topologyIncomplete = true;
|
|
454
478
|
for (const [id, tasks] of groups) {
|
|
455
|
-
const native =
|
|
479
|
+
const native = parents.get(id), parentId = tasks[0].parentSessionId, rootTools = taskTools(parents.get(parentId));
|
|
456
480
|
if (!native || !native.settings) { this.topologyIncomplete = true; continue; }
|
|
457
|
-
if (native.header.callingSessionId !== this.
|
|
481
|
+
if (id === this.rootId || id === parentId || tasks.some(task => task.parentSessionId !== parentId) || native.header.callingSessionId !== parentId || path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_child_identity_invalid", "Task invocation disagrees with physical child parent/workspace");
|
|
482
|
+
if (new Set(tasks.map(task => task.taskInvocationId)).size !== tasks.length) this.topologyIncomplete = true;
|
|
458
483
|
const creationTool = native.header.callingToolUseId;
|
|
459
484
|
for (const task of tasks) {
|
|
485
|
+
if (!rootTools.has(task.parentToolUseId)) {
|
|
486
|
+
const block = (parents.get(parentId)?.records ?? []).flatMap(record => record.message?.content ?? []).find(block => block.type === "tool_use" && block.id === task.parentToolUseId);
|
|
487
|
+
if (block) throw new DroidError("droid_child_identity_invalid", "Task invocation is bound to a non-Task tool");
|
|
488
|
+
this.topologyIncomplete = true; continue;
|
|
489
|
+
}
|
|
460
490
|
if (task.parentToolUseId === creationTool) continue;
|
|
461
491
|
const tool = rootTools.get(task.parentToolUseId);
|
|
462
492
|
if (tool?.name !== "Task" || tool.input?.resume !== id) throw new DroidError("droid_child_identity_invalid", "Repeated child invocation has no matching native Task resume evidence");
|
|
463
493
|
}
|
|
464
494
|
await this.observeSettings(id, native.settings, { path: native.transcript_path, sha256: native.source_sha256 });
|
|
465
|
-
const prior = this.topology.get(id);
|
|
466
495
|
// Task resume adds a Turn to the same physical child, not another
|
|
467
496
|
// Session. Its transcript retains the original creation edge. Work
|
|
468
497
|
// reassignment is checked by dd-flow's physical Session binding.
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
const
|
|
498
|
+
const order = [...rootTools.keys()];
|
|
499
|
+
const ordered = tasks.toSorted((a, b) => order.indexOf(a.parentToolUseId) - order.indexOf(b.parentToolUseId));
|
|
500
|
+
const latest = ordered.at(-1); const running = !terminal(latest.status);
|
|
472
501
|
const outcome = native.outcomes.at(-1), nativeTurn = outcome && terminal(outcome.reason) && typeof outcome.turnId === "string" ? outcome.turnId : null;
|
|
473
|
-
const
|
|
474
|
-
|
|
502
|
+
const parentResult = (parents.get(parentId)?.records ?? []).flatMap(record => record.message?.content ?? []).some(block => block.type === "tool_result" && block.tool_use_id === latest.parentToolUseId);
|
|
503
|
+
const exactTurn = nativeTurn && (tasks.length === 1 || latest.turnId === nativeTurn);
|
|
504
|
+
// A resumed Task's previous terminal Turn is not its current outcome.
|
|
505
|
+
// The matching native Task result proves completion without attributing
|
|
506
|
+
// a previous error/Turn ID to this invocation.
|
|
507
|
+
const evidence = !running && nativeTurn && (tasks.length === 1 || parentResult || exactTurn) ? {
|
|
508
|
+
...(exactTurn ? { terminal_turn_id: nativeTurn } : {}), native_source: "droid.agent_turn_outcome",
|
|
509
|
+
native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 },
|
|
510
|
+
...(exactTurn && outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
|
|
511
|
+
this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parentId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: running ? "running" : status(latest.status), source: "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
|
|
475
512
|
}
|
|
476
513
|
return [...this.topology.values()];
|
|
477
514
|
}
|
|
@@ -491,9 +528,10 @@ export class DroidRuntime {
|
|
|
491
528
|
async inspect(id, { ignoreActive = false, ingestUsage = true } = {}) {
|
|
492
529
|
this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
|
|
493
530
|
const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
|
|
494
|
-
const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
|
|
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
|
|
531
|
+
const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && source?.tool_calls.pending === 0 && children.every(child => terminal(child.status) && child.native_turn_items?.observed === true && child.native_turn_items.pending === false);
|
|
532
|
+
const rootSettled = source?.complete && source.tool_calls.completeness === "complete" && source.tool_calls.pending === 0 && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
|
|
533
|
+
return nativeExecutionReceipt({ 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,
|
|
534
|
+
...(this.topologyIncomplete ? { native_settlement: { confirmed: false, state: "unproven", reason: "native_task_inventory_incomplete" } } : {}) }, { daemonId: this.config.daemonId });
|
|
497
535
|
}
|
|
498
536
|
async observeHook(payload, eventId, deadlineAt = null) {
|
|
499
537
|
try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
|
|
@@ -508,7 +546,8 @@ export class DroidRuntime {
|
|
|
508
546
|
const foreign = message => new DroidError("droid_hook_identity_invalid", message, { scope: "foreign_hook", provider_session_id: id });
|
|
509
547
|
if (!native || path.resolve(payload.cwd) !== this.config.cwd || await realpath(payload.transcript_path) !== await realpath(native.transcript_path)) throw foreign("Hook is outside its controlled native Session/workspace");
|
|
510
548
|
const parent = native.header.callingSessionId ?? null;
|
|
511
|
-
if (parent && parent !== this.rootId) throw foreign("Hook child belongs to another root");
|
|
549
|
+
if (parent && parent !== this.rootId && !this.topology.has(parent)) throw foreign("Hook child belongs to another root");
|
|
550
|
+
if (parent && (id === parent || this.topology.has(id) && this.topology.get(id).parent_provider_session_id !== parent)) throw foreign("Hook child ancestry changed");
|
|
512
551
|
if (!parent && this.rootId && id !== this.rootId) throw foreign("Hook came from an unrelated root");
|
|
513
552
|
if (path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_hook_identity_invalid", "Owned native Session workspace changed");
|
|
514
553
|
if (parent && !terminal(this.topology.get(id)?.status)) this.topology.set(id, { ...(this.topology.get(id) ?? {}), provider_session_id: id, parent_provider_session_id: parent, tool_use_id: native.header.callingToolUseId, status: "running", source: "droid.session_start" });
|
|
@@ -521,7 +560,7 @@ export class DroidRuntime {
|
|
|
521
560
|
if (!this.config.ddFlowBin || payload.tool_name !== "Execute") return {};
|
|
522
561
|
// Usage is ingested by inspect/turn completion, never ahead of admission.
|
|
523
562
|
try {
|
|
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"
|
|
563
|
+
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"));
|
|
525
564
|
} catch (error) {
|
|
526
565
|
if (event === "PreToolUse" && isProvenShellCorrection(error)) {
|
|
527
566
|
const refusal = errorRecord(error);
|
|
@@ -576,8 +615,11 @@ export class DroidRuntime {
|
|
|
576
615
|
let topologyError = null;
|
|
577
616
|
try { await this.refreshTopology(); }
|
|
578
617
|
catch (error) { topologyError = errorRecord(error); await this.journal("closed_tree_topology_error", { error: topologyError }); }
|
|
579
|
-
|
|
580
|
-
const
|
|
618
|
+
const root = this.rootId ? await readDroidSession(this.paths.factory, this.rootId) : null;
|
|
619
|
+
const nativeSettled = !forced && !topologyError && !this.topologyIncomplete
|
|
620
|
+
&& (!this.rootId || root?.complete && (terminal(root.outcomes.at(-1)?.reason) || !this.active && !this.lastResult && root.records.every(record => record.type === "session_start")) && root.tool_calls.pending === 0)
|
|
621
|
+
&& [...this.topology.values()].every(child => terminal(child.status) && child.native_turn_items?.observed === true && child.native_turn_items.pending === false);
|
|
622
|
+
const cleanup = { settled: true, physical_stopped: true, native_settled: nativeSettled, forced, observed_processes: this.ownedProcesses.length, ...(topologyError ? { topology_error: topologyError } : {}) };
|
|
581
623
|
await this.save({ active_tree: false, cleanup });
|
|
582
624
|
return cleanup;
|
|
583
625
|
}
|
|
@@ -21,7 +21,7 @@ import path from "node:path";
|
|
|
21
21
|
|
|
22
22
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
23
23
|
import { safeProviderError } from "./provider-overload-policy.mjs";
|
|
24
|
-
import { settledNativeTree } from "./native-children.mjs";
|
|
24
|
+
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
25
25
|
import { observeModel, readModelObservations } from "./model-observations.mjs";
|
|
26
26
|
import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
|
|
27
27
|
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
@@ -187,8 +187,53 @@ export async function stopDaemon(options) { const timeoutMs = options.timeoutMs
|
|
|
187
187
|
export class Runtime {
|
|
188
188
|
constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
|
|
189
189
|
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
async reduceTerminal(outcome) {
|
|
191
|
+
const session = this.sessions.get(outcome.session_id), turn = session?.native_prompt_operation;
|
|
192
|
+
if (outcome.method !== "session/prompt" || !turn || typeof outcome.connection_id !== "string" || !outcome.connection_id
|
|
193
|
+
|| !Number.isSafeInteger(outcome.request_id) || outcome.request_id < 1 || !Number.isSafeInteger(outcome.native_sequence) || outcome.native_sequence < 1 || turn.daemon_id !== this.state.daemon_id
|
|
194
|
+
|| turn.connection_id !== outcome.connection_id || turn.operation_id !== outcome.operation_id || turn.generation !== outcome.generation
|
|
195
|
+
|| turn.native_request_id !== outcome.request_id) return;
|
|
196
|
+
const message = outcome.terminal_response;
|
|
197
|
+
const kind = message?.error ? "failed" : message?.result?.stopReason;
|
|
198
|
+
if (!["failed", "end_turn", "cancelled"].includes(kind)) return;
|
|
199
|
+
const receipt = { kind, ...turn, request_id: outcome.request_id, native_sequence: outcome.native_sequence };
|
|
200
|
+
if (session.native_root_receipt && (session.native_root_receipt.kind !== kind || session.native_root_receipt.request_id !== outcome.request_id)) {
|
|
201
|
+
this.state.observation_error = { code: "native_terminal_outcome_conflict", message: "Conflicting native terminal outcomes for the same prompt" };
|
|
202
|
+
await this.persist();
|
|
203
|
+
throw new DaemonError(this.state.observation_error.code, this.state.observation_error.message);
|
|
204
|
+
}
|
|
205
|
+
if (session.native_root_receipt) return;
|
|
206
|
+
if (session.native_activity && session.native_activity.connection_id === outcome.connection_id && session.native_activity.sequence > outcome.native_sequence) {
|
|
207
|
+
return await this.invalidateTerminal(session);
|
|
208
|
+
}
|
|
209
|
+
session.native_root_receipt = receipt;
|
|
210
|
+
await this.persist();
|
|
211
|
+
}
|
|
212
|
+
async invalidateTerminal(session) {
|
|
213
|
+
session.native_root_receipt = null;
|
|
214
|
+
this.state.observation_error = { code: "native_activity_after_terminal", message: "Productive root activity followed its native terminal outcome" };
|
|
215
|
+
await this.persist();
|
|
216
|
+
}
|
|
217
|
+
async observeRootActivity(message, observation) {
|
|
218
|
+
const session = this.sessions.get(message?.params?.sessionId), turn = session?.native_prompt_operation;
|
|
219
|
+
if (!observation?.productive || !turn || session.parent_provider_session_id || turn.connection_id !== observation.connection_id) return;
|
|
220
|
+
if (session.native_activity?.connection_id !== observation.connection_id || session.native_activity.sequence < observation.sequence) session.native_activity = { connection_id: observation.connection_id, sequence: observation.sequence };
|
|
221
|
+
const receipt = session.native_root_receipt;
|
|
222
|
+
if (receipt?.connection_id === observation.connection_id && observation.sequence > receipt.native_sequence) await this.invalidateTerminal(session);
|
|
223
|
+
}
|
|
224
|
+
async drainTerminal() { await this.bridge.terminalOutcomes; await this.persisting; if (this.bridge.terminalProjectionError) throw this.bridge.terminalProjectionError; }
|
|
225
|
+
settledRoot(id) { return !this.active && !this.rootRunning({ provider_session_id: id }) && this.topology(id).every(child => !this.rootRunning({ provider_session_id: child.provider_session_id }) && !child.outcome_error && !child.activity_status); }
|
|
226
|
+
async restoreTerminals() {
|
|
227
|
+
if (!this.state.config?.journal) return;
|
|
228
|
+
let contents;
|
|
229
|
+
try { contents = await readFile(this.state.config.journal, "utf8"); } catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
230
|
+
for (const line of contents.split("\n").filter(Boolean)) {
|
|
231
|
+
const event = JSON.parse(line);
|
|
232
|
+
if (["native_terminal_outcome", "native_terminal_after_hook_failure"].includes(event.kind)) await this.reduceTerminal(event.payload);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, rootSettled: id => this.settledRoot(id), liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
|
|
236
|
+
track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { ...session, native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
|
|
192
237
|
recordDescendant(value, root, source, fallback) {
|
|
193
238
|
const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId ?? value?.child_session_id ?? value?.subagent_id;
|
|
194
239
|
const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? value?.parent_session_id ?? root;
|
|
@@ -302,13 +347,14 @@ export class Runtime {
|
|
|
302
347
|
}
|
|
303
348
|
return modelFacts;
|
|
304
349
|
}
|
|
305
|
-
topology(root) { if (this.state.observation_error) { const error = this.state.observation_error; throw new DaemonError(error.code, error.message, false, error.details); } return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
|
|
350
|
+
topology(root) { if (this.state.observation_error) { const error = this.state.observation_error; throw new DaemonError(error.code, error.message, false, error.details); } return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root || this.sessions.get(child.provider_session_id)?.root_provider_session_id === root); }
|
|
306
351
|
observeToolCall(message) { this.toolObservations ??= new ToolObservations(); this.toolObservations.add(acpToolObservation(message, { harness: "grok-acp" })); const sessionId = message?.params?.sessionId; if (sessionId) this.toolUsage.set(sessionId, this.toolObservations.summary(sessionId)); }
|
|
307
352
|
running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
|
|
308
353
|
treeRunning(result) {
|
|
309
354
|
const evidence = result?.evidence ?? result;
|
|
310
355
|
const children = evidence?.subagents?.subagents;
|
|
311
|
-
return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error)
|
|
356
|
+
return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error)
|
|
357
|
+
|| this.topology(result?.provider_session_id).some(child => this.rootRunning({ provider_session_id: child.provider_session_id }) || child.outcome_error || child.activity_status);
|
|
312
358
|
}
|
|
313
359
|
rootRunning(result) {
|
|
314
360
|
const status = String(result?.info?.status ?? result?.info?.state ?? result?.info?.data?.status ?? "unknown").toLowerCase();
|
|
@@ -316,7 +362,7 @@ export class Runtime {
|
|
|
316
362
|
// Grok may retire a completed child Session before session/info can be
|
|
317
363
|
// inspected. A retained TaskOutput for that exact attempt is native
|
|
318
364
|
// terminal proof; missing session/info alone is not.
|
|
319
|
-
if (status === "unknown" &&
|
|
365
|
+
if (status === "unknown" && ["x.ai/tool/TaskOutput", "x.ai/subagent/event"].includes(child?.source) && child.attempt_id && !child.outcome_error && !child.activity_status
|
|
320
366
|
&& ["completed", "failed", "cancelled"].includes(child.status)) return false;
|
|
321
367
|
// Grok 1.0.17 omits status from session/info. Only a durable native
|
|
322
368
|
// creation/end_turn receipt can then establish an idle root. Dispatch
|
|
@@ -326,6 +372,7 @@ export class Runtime {
|
|
|
326
372
|
return !["completed", "failed", "cancelled", "stopped", "idle"].includes(status);
|
|
327
373
|
}
|
|
328
374
|
async refreshTree(cancel = false) {
|
|
375
|
+
await this.drainTerminal();
|
|
329
376
|
const running = [];
|
|
330
377
|
for (const session of [...this.sessions.values()].filter(item => !item.parent_provider_session_id)) {
|
|
331
378
|
const id = session.provider_session_id;
|
|
@@ -357,15 +404,21 @@ export class Runtime {
|
|
|
357
404
|
const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
|
|
358
405
|
// Only a new native turn invalidates its own root proof. Creation, fork
|
|
359
406
|
// and local archive export must not erase an unrelated Session's receipt.
|
|
360
|
-
if (name === "session.prompt" && this.sessions.has(sessionId))
|
|
407
|
+
if (name === "session.prompt" && this.sessions.has(sessionId)) {
|
|
408
|
+
const session = this.sessions.get(sessionId);
|
|
409
|
+
session.native_root_receipt = null;
|
|
410
|
+
this.bridge.dispatchGeneration = current.generation;
|
|
411
|
+
session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation };
|
|
412
|
+
}
|
|
361
413
|
try {
|
|
362
414
|
await this.persist({ active_tree: true, active_operation: name });
|
|
363
415
|
assertDaemonDispatch(this.paths?.dir);
|
|
364
416
|
const before = new Map(this.descendants);
|
|
365
417
|
const result = await Promise.race([task(), hookFailed]);
|
|
418
|
+
await this.drainTerminal();
|
|
366
419
|
this.track(result, before);
|
|
367
420
|
const session = this.sessions.get(result.provider_session_id);
|
|
368
|
-
if (session &&
|
|
421
|
+
if (session && ["session.create", "session.fork"].includes(name)) session.native_root_receipt = { kind: "created", daemon_id: this.state.daemon_id };
|
|
369
422
|
// Archive requires an idle tree before entering this exclusive section
|
|
370
423
|
// and performs no native work. Its file receipt is not tree evidence.
|
|
371
424
|
const running = name === "session.archive" ? false : this.treeRunning(result);
|
|
@@ -375,11 +428,7 @@ export class Runtime {
|
|
|
375
428
|
// A matched JSON-RPC error completes this native prompt request. It is
|
|
376
429
|
// root-turn proof, not proof that any descendants have stopped. A lost
|
|
377
430
|
// response, bridge error, or failure before dispatch earns no receipt.
|
|
378
|
-
|
|
379
|
-
if (name === "session.prompt" && response?.method === "session/prompt" && response.session_id === sessionId) {
|
|
380
|
-
const session = this.sessions.get(sessionId);
|
|
381
|
-
if (session) session.native_root_receipt = { kind: "failed", daemon_id: this.state.daemon_id, request_id: response.request_id };
|
|
382
|
-
}
|
|
431
|
+
try { await this.drainTerminal(); } catch (storage) { error.cleanup_error ??= errorRecord(storage); }
|
|
383
432
|
try { await hookBudget(1_000, null).run(() => this.persist({ active_tree: true, active_operation: null }), "failure_persistence"); }
|
|
384
433
|
catch (storage) { error.cleanup_error = errorRecord(storage); }
|
|
385
434
|
throw error;
|
|
@@ -416,7 +465,7 @@ export class Runtime {
|
|
|
416
465
|
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);
|
|
417
466
|
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); }
|
|
418
467
|
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; }); }
|
|
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) } : {}) }; }
|
|
468
|
+
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 nativeExecutionReceipt({ ...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) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 }); }
|
|
420
469
|
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; }
|
|
421
470
|
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); }); }
|
|
422
471
|
if (operation === "daemon.stop") {
|
|
@@ -427,7 +476,7 @@ export class Runtime {
|
|
|
427
476
|
observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
|
|
428
477
|
...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
|
|
429
478
|
});
|
|
430
|
-
return { clean: true };
|
|
479
|
+
return { clean: true, settled: true, native_settled: true };
|
|
431
480
|
}, [
|
|
432
481
|
["provider", () => this.bridge.close()],
|
|
433
482
|
["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
|
|
@@ -441,10 +490,11 @@ export class Runtime {
|
|
|
441
490
|
|
|
442
491
|
export async function serveDaemon(stateDir) {
|
|
443
492
|
const paths = locations(stateDir); const state = await readState(paths.dir); if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible"); let runtime;
|
|
444
|
-
const bridge = new AcpBridge({ ...state.config, initializeTimeoutMs: 60_000, commandArgs: ["agent", "--no-leader", "--always-approve", "--model", state.config.model, "--reasoning-effort", state.config.reasoning, "stdio"], env: isolatedEnv(state.config), clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow", onNotification: async (message) => {
|
|
493
|
+
const bridge = new AcpBridge({ ...state.config, initializeTimeoutMs: 60_000, commandArgs: ["agent", "--no-leader", "--always-approve", "--model", state.config.model, "--reasoning-effort", state.config.reasoning, "stdio"], env: isolatedEnv(state.config), clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow", onNotification: async (message, observation) => {
|
|
445
494
|
const sessionId = message?.params?.sessionId;
|
|
446
495
|
if (!runtime || !sessionId) return;
|
|
447
496
|
try {
|
|
497
|
+
await runtime.observeRootActivity(message, observation);
|
|
448
498
|
runtime.observeToolCall(message);
|
|
449
499
|
for (const fact of runtime.observeSubagentEvent(message)) {
|
|
450
500
|
const source = { channel: "x.ai/subagent_spawned", scope: "configured", attempt_id: fact.attemptId, parent_prompt_id: fact.parentPromptId,
|
|
@@ -463,6 +513,13 @@ export async function serveDaemon(stateDir) {
|
|
|
463
513
|
const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
|
|
464
514
|
await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
|
|
465
515
|
} }); const initialized = await startManagedBridge({ config: state.config, state, bridge, kind: "grok-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); runtime = new Runtime(paths, state, bridge, initialized);
|
|
516
|
+
bridge.configure({ onTerminalOutcome: outcome => runtime.reduceTerminal(outcome), onPromptDispatch: async dispatch => {
|
|
517
|
+
const turn = runtime.sessions.get(dispatch.session_id)?.native_prompt_operation;
|
|
518
|
+
if (!turn || turn.operation_id !== dispatch.operation_id || turn.connection_id !== dispatch.connection_id || turn.generation !== dispatch.generation) throw new DaemonError("native_dispatch_binding_mismatch", "Native prompt dispatch differs from retained operation");
|
|
519
|
+
turn.native_request_id = dispatch.native_request_id;
|
|
520
|
+
await runtime.persist();
|
|
521
|
+
} });
|
|
522
|
+
await runtime.restoreTerminals();
|
|
466
523
|
for (const fact of await readModelObservations(state.config.journal)) {
|
|
467
524
|
if (fact.harness === "grok-acp" && fact.source?.channel === "x.ai/subagent_spawned" && runtime.sessions.has(fact.session_id)
|
|
468
525
|
&& (runtime.descendants.get(fact.session_id)?.attempt_id ?? null) === (fact.source.attempt_id ?? null) && fact.observed?.model) {
|