@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
|
+
import { operationProgressReporter } from './operation-progress.mjs';
|
|
1
3
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
4
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
2
5
|
import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
|
|
3
6
|
import { constants } from "node:fs";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,14 +10,15 @@ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unli
|
|
|
7
10
|
import { spawn } from "node:child_process";
|
|
8
11
|
import net from "node:net";
|
|
9
12
|
import { isDeepStrictEqual } from "node:util";
|
|
10
|
-
import { ObservationClock } from "./observation-clock.mjs";
|
|
13
|
+
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
11
14
|
import { hookBudget } from "./native-hook-command.mjs";
|
|
12
15
|
import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
13
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
16
|
+
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
14
17
|
import path from "node:path";
|
|
15
18
|
import { observeModel } from "./model-observations.mjs";
|
|
16
|
-
import { AGY_VERSION_TIMEOUT_MS, AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
19
|
+
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
17
20
|
import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
21
|
+
import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
18
22
|
|
|
19
23
|
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
20
24
|
class DaemonError extends AgyError {}
|
|
@@ -53,15 +57,15 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
|
|
|
53
57
|
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
54
58
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
55
59
|
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
56
|
-
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"])
|
|
57
|
-
|
|
60
|
+
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) env[key] = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
|
|
61
|
+
const errorPayload = errorRecord;
|
|
58
62
|
async function forwardUsage(config, sessionId, result, tools, descendants) {
|
|
59
63
|
if (!config.ddFlowBin || !config.projectRoot) return;
|
|
60
64
|
const snapshot = usageSnapshot(result, tools), hasChildren = descendants.length > 0;
|
|
61
65
|
if (snapshot.total_tokens === null) return { status: "unavailable", reason: "provider_did_not_report_total_tokens" };
|
|
62
66
|
const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: hasChildren ? "unknown" : "physical_session", completeness: hasChildren ? "partial" : "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cache_read_tokens, totalTokens: snapshot.total_tokens }, tool_calls: snapshot.tool_calls };
|
|
63
67
|
const target = executable(config.ddFlowBin, ["agy", "usage", "ingest", "--project-root", config.projectRoot, "--json"]);
|
|
64
|
-
await
|
|
68
|
+
await commandText(target.command, target.args, { input: `${JSON.stringify(payload)}\n`, env: { DD_FLOW_HOME: config.ddFlowHome }, phase: "usage-ingest" });
|
|
65
69
|
return { status: "ingested" };
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -159,39 +163,11 @@ export async function qualifyAgyHooks(projectRoot, { stateDir = null, install =
|
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
|
|
162
|
-
const
|
|
163
|
-
return await
|
|
164
|
-
const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, checking = false, lastActivity = Date.now();
|
|
165
|
-
const clock = new ObservationClock({ timeoutMs, onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } });
|
|
166
|
-
const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearInterval(timer); client.destroy(); error ? reject(error) : resolve(result); };
|
|
167
|
-
const poll = Math.min(5_000, Math.max(25, Math.floor(timeoutMs / 20)));
|
|
168
|
-
timer = setInterval(async () => {
|
|
169
|
-
if (settled || checking) return;
|
|
170
|
-
checking = true;
|
|
171
|
-
try {
|
|
172
|
-
if (operation !== "session.prompt") {
|
|
173
|
-
if (clock.sample()) finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
let status = null;
|
|
177
|
-
try { status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000)); }
|
|
178
|
-
catch { /* The original RPC retains the authoritative failure. */ }
|
|
179
|
-
const observed = Date.parse(status?.last_activity_at ?? "");
|
|
180
|
-
const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
|
|
181
|
-
if (progressed) lastActivity = observed;
|
|
182
|
-
if (!clock.sample(progressed ? lastActivity : undefined, progressed ? lastActivity : undefined)) return;
|
|
183
|
-
finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
|
|
184
|
-
operation_id: request.id,
|
|
185
|
-
last_activity_at: status?.last_activity_at ?? null,
|
|
186
|
-
active_tree: status?.active_tree === true
|
|
187
|
-
}));
|
|
188
|
-
} finally { checking = false; }
|
|
189
|
-
}, poll);
|
|
190
|
-
client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; try { const response = JSON.parse(buffer.slice(0, index)); response.ok ? finish(null, response.result) : finish(new DaemonError(response.error.code, response.error.message, response.error.retryable, response.error.details)); } catch (error) { finish(new DaemonError("daemon_protocol_mismatch", error.message)); } }); client.on("error", error => finish(new DaemonError("daemon_not_running", error.message, true))); client.on("end", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true))); client.on("close", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true)));
|
|
191
|
-
});
|
|
166
|
+
const request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
167
|
+
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
|
|
192
168
|
}
|
|
193
169
|
|
|
194
|
-
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
170
|
+
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow","livenessTimeoutMs"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
195
171
|
// Called only after authorizeRetainedDaemonResume verifies the old daemon,
|
|
196
172
|
// provider processes and operation ledger. Native outcomes remain historical;
|
|
197
173
|
// their old liveness flags cannot describe the replacement process tree.
|
|
@@ -209,8 +185,12 @@ export function retainedAgyState(previous, sessionId) {
|
|
|
209
185
|
export async function startDaemon(options) {
|
|
210
186
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
211
187
|
const previous = await readState(paths.dir), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")), config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_AGY_BIN ?? "agy", provider: String(options.provider ?? "google"), model: String(options.model ?? "gemini-3.1-pro-high"), reasoning: String(options.reasoning ?? "high"), mode: String(options.mode ?? "accept-edits"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, entryPath: absolute(options.entryPath, "dd-agy entry"), daemonId: randomUUID(), resumeSessionId: options.sessionId ?? null, geminiDir: paths.gemini, appDataDir: "runtime", temporary: paths.temporary, noFlow };
|
|
188
|
+
config.livenessTimeoutMs = options.livenessTimeoutMs ?? 1_800_000;
|
|
189
|
+
validateObservationDuration(config.livenessTimeoutMs);
|
|
212
190
|
await prepareRuntimeOwner(config, paths.dir);
|
|
191
|
+
assertDaemonRestartable(previous);
|
|
213
192
|
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
193
|
+
assertDaemonRestartable(await readState(paths.dir));
|
|
214
194
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
215
195
|
if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
|
|
216
196
|
await removeSocket(paths); await prepare(paths, config);
|
|
@@ -221,7 +201,7 @@ export async function startDaemon(options) {
|
|
|
221
201
|
const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
222
202
|
try {
|
|
223
203
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
224
|
-
const
|
|
204
|
+
const startupClock = new ObservationClock({ timeoutMs: 30_000 }); while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100));
|
|
225
205
|
const persisted = await readState(paths.dir);
|
|
226
206
|
if (persisted?.shutdown_state === "failed") {
|
|
227
207
|
const failure = persisted.startup_error ?? {};
|
|
@@ -235,13 +215,13 @@ export async function startDaemon(options) {
|
|
|
235
215
|
throw error;
|
|
236
216
|
}
|
|
237
217
|
}
|
|
238
|
-
export const stopDaemon = async 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); };
|
|
218
|
+
export const stopDaemon = async 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(options.stateDir) }); };
|
|
239
219
|
|
|
240
220
|
export class Runtime {
|
|
241
221
|
constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.providerFinalization = Promise.resolve(); this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolObservations = new ToolObservations(); for (const reason of state.tool_observation_gaps ?? []) this.toolObservations.gaps.add(reason); for (const event of state.tool_observations ?? []) this.toolObservations.add(event); if (state.tool_calls?.length && !state.tool_observations) this.toolObservations.gaps.add("legacy_tools_not_attributed"); this.turnGeneration = state.turn_generation ?? 0; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = state.last_result ?? null; this.sessionObservations = new Map(state.session_observations ?? []); this.startupError = null; this.persisting = Promise.resolve(); this.draining = Promise.resolve(); this.lastActivityAt = state.last_activity_at ?? null; this.usageIngest = null; this.lastProviderEvent = null; }
|
|
242
|
-
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values(), ...this.toolObservations.sources.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
|
|
222
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values(), ...this.toolObservations.sources.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.catch(() => {}).then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
|
|
243
223
|
async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
244
|
-
args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "
|
|
224
|
+
args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "0", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
|
|
245
225
|
requireSessionIdentity(requestedId) {
|
|
246
226
|
const observedId = this.init?.conversation_id ?? null;
|
|
247
227
|
if (!requestedId || requestedId === observedId) return observedId;
|
|
@@ -249,20 +229,51 @@ export class Runtime {
|
|
|
249
229
|
harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
|
|
250
230
|
});
|
|
251
231
|
}
|
|
252
|
-
markActivity() { this.lastActivityAt = new Date().toISOString(); }
|
|
232
|
+
markActivity() { const current = this.active; this.lastActivityAt = new Date().toISOString(); this.progress = { cursor: (this.progress?.cursor ?? 0) + 1, observed_at: Date.now() }; void current?.publishProgress?.('native_progress', { session_id: this.init?.conversation_id, observed_at: this.lastActivityAt }).catch(error => { if(this.active === current) current.reject(error); }); }
|
|
233
|
+
markTerminalActivity(result) {
|
|
234
|
+
const current = this.active;
|
|
235
|
+
if (!current) return;
|
|
236
|
+
const key = createHash("sha256").update(JSON.stringify({ conversation_id: result.conversation_id ?? this.init?.conversation_id, status: result.status, num_turns: result.num_turns, error: result.error, response: result.response ?? result.text })).digest("hex");
|
|
237
|
+
current.terminalProgress ??= new Set();
|
|
238
|
+
if (current.terminalProgress.has(key) || current.terminalProgress.size >= 1_024) return;
|
|
239
|
+
current.terminalProgress.add(key); this.markActivity();
|
|
240
|
+
}
|
|
253
241
|
observeProviderActivity(event) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
242
|
+
if (!this.active) return;
|
|
243
|
+
const root = this.init?.conversation_id, step = event.step_update;
|
|
244
|
+
const id = step?.conversation_id ?? event.result?.conversation_id ?? root;
|
|
245
|
+
let parent = id; const seen = new Set();
|
|
246
|
+
while (parent && parent !== root && !seen.has(parent)) { seen.add(parent); const child = this.descendants.get(parent); if (child?.activity_generation !== this.active.generation) return; parent = child.parent_provider_session_id; }
|
|
247
|
+
if (!root || parent !== root) return;
|
|
248
|
+
if (event.event === "result") { if (["SUCCESS", "ERROR", "CANCELLED"].includes(event.result?.status)) this.markTerminalActivity(event.result); return; }
|
|
249
|
+
if (event.event !== "step_update") return;
|
|
250
|
+
const index = step?.step_index ?? (/^\d+$/.test(step?.step_id ?? "") ? Number(step.step_id) : null);
|
|
251
|
+
if (!Number.isInteger(index)) return;
|
|
252
|
+
const floor = this.sessionObservations.get(id)?.step_floor;
|
|
253
|
+
if (floor != null && index <= floor) return;
|
|
254
|
+
const tool = step.tool_call ?? step.tool_info ?? (step.step_type === "tool" ? step.tool_name : null);
|
|
255
|
+
if (!step.content && !step.text && !step.thought && !tool && !step.error && !(step.subagent_info?.subagents?.length)) return;
|
|
256
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({ content: step.content, text: step.text, thought: step.thought, tool_call: step.tool_call, tool_info: step.tool_info, tool_name: step.tool_name, ...(tool ? { state: step.state, status: step.status } : {}), error: step.error, subagent_info: step.subagent_info })).digest("hex");
|
|
257
|
+
this.progressSteps ??= new Map(); const prior = this.progressSteps.get(id);
|
|
258
|
+
if (prior && index < prior.index) return;
|
|
259
|
+
const snapshots = prior?.index === index ? prior.seen : new Set();
|
|
260
|
+
if (snapshots.has(fingerprint)) return;
|
|
261
|
+
// Native step snapshots can alternate/replay; scope retained dedup to the
|
|
262
|
+
// current step. Too many unique snapshots becomes uncertain, never a grant.
|
|
263
|
+
if (snapshots.size >= 1_024) return;
|
|
264
|
+
snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.markActivity();
|
|
258
265
|
}
|
|
259
266
|
async start(sessionId = null) {
|
|
267
|
+
if (this.drainingError) throw this.drainingError;
|
|
268
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
260
269
|
if (this.child && !this.exited) {
|
|
261
270
|
await this.readyPromise;
|
|
262
271
|
this.requireSessionIdentity(sessionId);
|
|
263
272
|
return this.init;
|
|
264
273
|
}
|
|
265
274
|
await this.providerFinalization;
|
|
275
|
+
if (this.drainingError) throw this.drainingError;
|
|
276
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
266
277
|
const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
|
|
267
278
|
let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
|
|
268
279
|
// The provider can reject before durable registration finishes. Keep a
|
|
@@ -273,19 +284,20 @@ export class Runtime {
|
|
|
273
284
|
const providerProcess = this.providerProcess;
|
|
274
285
|
try { await this.persist({ provider_process: providerProcess }); }
|
|
275
286
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
276
|
-
try { this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
287
|
+
try { assertDaemonDispatch(this.paths?.dir); this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
277
288
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
278
289
|
const providerChild = this.child;
|
|
279
290
|
let allowProviderFinalization;
|
|
280
291
|
const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
|
|
281
|
-
|
|
282
|
-
this.child.
|
|
292
|
+
const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
|
|
293
|
+
this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve); }, observationFailure);
|
|
294
|
+
this.diagnostics = streamNativeDiagnostics(this.child.stderr, chunk => this.journal("stderr", { text: chunk }), observationFailure);
|
|
283
295
|
this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
|
|
284
296
|
// A dead leader is not yet a finalized tree: stop must also join the
|
|
285
297
|
// pending drain, process-group check and final persistence/lease writes.
|
|
286
298
|
this.providerFinalization = new Promise((resolve, reject) => {
|
|
287
299
|
this.child.on("close", (code, signal) => {
|
|
288
|
-
void this.
|
|
300
|
+
void Promise.all([this.nativeDrain, this.diagnostics]).then(async () => {
|
|
289
301
|
const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
|
|
290
302
|
const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
|
|
291
303
|
if (beforeInit) readyReject(error);
|
|
@@ -310,15 +322,38 @@ export class Runtime {
|
|
|
310
322
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
|
|
311
323
|
finally { allowProviderFinalization(); }
|
|
312
324
|
let readyTimer;
|
|
313
|
-
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer =
|
|
325
|
+
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
|
|
314
326
|
finally { clearTimeout(readyTimer); }
|
|
315
327
|
}
|
|
316
328
|
async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
|
|
317
329
|
// Record an early provider rejection before journalling: a short-lived
|
|
318
330
|
// child can close while the asynchronous write is still pending.
|
|
319
|
-
if (
|
|
331
|
+
if (event.event === "result" && ["ERROR", "CANCELLED"].includes(event.result?.status)) {
|
|
332
|
+
this.lastTerminalFailure ??= this.state.last_terminal_failure;
|
|
333
|
+
const repeated = Boolean(this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === event.result.error
|
|
334
|
+
&& this.lastTerminalFailure.details.provider_session_id === (event.result.conversation_id || this.init?.conversation_id || null)
|
|
335
|
+
&& (this.active?.previousError === event.result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === event.result.error));
|
|
336
|
+
event.terminal_metadata = repeated ? this.lastTerminalFailure.details : { observed_at: new Date().toISOString(), provider_session_id: event.result.conversation_id || this.init?.conversation_id || null,
|
|
337
|
+
operation_id: this.active?.operationId ?? null, turn_generation: this.active?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" };
|
|
338
|
+
if (!this.init) this.startupError ??= agyTerminalFailure(event.result, event.terminal_metadata);
|
|
339
|
+
this.receivedTerminalMetadata = event.terminal_metadata;
|
|
340
|
+
}
|
|
320
341
|
this.observeProviderActivity(event); await this.journal("provider_event", { event }); if (event.event === "init") { this.init = { conversation_id: event.conversation_id, ...(event.init ?? {}) }; if (!this.init.conversation_id) throw new DaemonError("agy_init_missing", "Antigravity init has no conversation ID"); const requested = { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode, permission_mode: "always-proceed" }; this.profile = assertProfile(requested, observedProfile(this.init, requested)); await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: this.init.conversation_id, requested, observed: this.profile.observed, source: "native.init" }); await this.persist({ shutdown_state: "running", active_tree: false }); readyResolve(this.init); } else if (event.event === "step_update") this.observeStep(event.step_update ?? {}); else if (event.event === "result") { if (this.startupError) throw this.startupError; await this.finishTurn(event.result ?? {}); } } }
|
|
321
|
-
observeStep(step) {
|
|
342
|
+
observeStep(step) {
|
|
343
|
+
const sessionId = step.conversation_id ?? this.init?.conversation_id, observation = this.sessionObservations.get(sessionId) ?? {};
|
|
344
|
+
const fresh = Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1) && step.step_index > (observation.step_floor ?? -1);
|
|
345
|
+
const bound = sessionId === this.init?.conversation_id || this.descendants.get(sessionId)?.activity_generation === this.active?.generation;
|
|
346
|
+
// Tree liveness is broader than current-Turn model progress: fresh work
|
|
347
|
+
// from a retained child invalidates settlement without adopting this Turn.
|
|
348
|
+
if (fresh && (sessionId === this.init?.conversation_id || this.descendants.has(sessionId))) this.childActivity(sessionId);
|
|
349
|
+
if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) });
|
|
350
|
+
this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id));
|
|
351
|
+
for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) {
|
|
352
|
+
const prior = this.descendants.get(child.conversation_id);
|
|
353
|
+
this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown",
|
|
354
|
+
...(fresh && bound && this.active ? { activity_generation: this.active.generation } : {}) });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
322
357
|
toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
|
|
323
358
|
async captureChildTools() {
|
|
324
359
|
this.capturedToolFacts ??= new Map();
|
|
@@ -340,7 +375,7 @@ export class Runtime {
|
|
|
340
375
|
}
|
|
341
376
|
}
|
|
342
377
|
}
|
|
343
|
-
async finishTurn(result) {
|
|
378
|
+
async finishTurn(result, retainedMetadata = null) {
|
|
344
379
|
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
345
380
|
if (!["SUCCESS", "ERROR", "CANCELLED"].includes(result.status)) throw new DaemonError("agy_terminal_result_invalid", "Antigravity result has no recognized terminal status", false, { provider_result: result });
|
|
346
381
|
if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
|
|
@@ -353,12 +388,18 @@ export class Runtime {
|
|
|
353
388
|
return;
|
|
354
389
|
}
|
|
355
390
|
const current = this.active;
|
|
391
|
+
const terminalMetadata = retainedMetadata ?? this.receivedTerminalMetadata ?? (this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result.error
|
|
392
|
+
&& this.lastTerminalFailure.details.provider_session_id === (result.conversation_id || this.init?.conversation_id || null)
|
|
393
|
+
&& (current?.previousError === result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === result.error) ? this.lastTerminalFailure.details
|
|
394
|
+
: { observed_at: new Date().toISOString(), provider_session_id: result.conversation_id || this.init?.conversation_id || null, operation_id: current?.operationId ?? null,
|
|
395
|
+
turn_generation: current?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" });
|
|
396
|
+
this.receivedTerminalMetadata = null;
|
|
356
397
|
// The ordered stream returns one result per input. Model usage counters
|
|
357
398
|
// are not request IDs: native command rejections can report zero turns.
|
|
358
399
|
// prompt() drains earlier events before accepting another input.
|
|
359
400
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
360
401
|
await this.captureChildTools();
|
|
361
|
-
this.lastResult = result; this.
|
|
402
|
+
this.lastResult = result; this.markTerminalActivity(result);
|
|
362
403
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
363
404
|
const pendingTerminal = current && result.status === "SUCCESS";
|
|
364
405
|
if (pendingTerminal) current.terminalResult = result;
|
|
@@ -371,13 +412,9 @@ export class Runtime {
|
|
|
371
412
|
if (this.active !== current) return;
|
|
372
413
|
if (result.status === "ERROR" || result.status === "CANCELLED") {
|
|
373
414
|
this.active = null;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
: /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
|
|
378
|
-
: /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
|
|
379
|
-
: "agy_provider_failed";
|
|
380
|
-
return current.reject(new DaemonError(code, message, code === "agy_provider_rate_limited", { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
415
|
+
this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest });
|
|
416
|
+
await this.persist({ last_terminal_failure: errorRecord(this.lastTerminalFailure) });
|
|
417
|
+
return current.reject(this.lastTerminalFailure);
|
|
381
418
|
}
|
|
382
419
|
await this.completePendingTurn();
|
|
383
420
|
}
|
|
@@ -395,6 +432,9 @@ export class Runtime {
|
|
|
395
432
|
current.resolve(receipt);
|
|
396
433
|
}
|
|
397
434
|
receipt(result = this.lastResult) {
|
|
435
|
+
if (this.state.last_terminal_failure && !this.lastTerminalFailure) this.lastTerminalFailure = this.state.last_terminal_failure;
|
|
436
|
+
const terminalMetadata = this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result?.error
|
|
437
|
+
&& this.lastTerminalFailure.details.provider_session_id === (result?.conversation_id || this.init?.conversation_id || null) ? this.lastTerminalFailure.details : null;
|
|
398
438
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
399
439
|
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
400
440
|
const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
|
|
@@ -403,13 +443,14 @@ export class Runtime {
|
|
|
403
443
|
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
404
444
|
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
405
445
|
const terminalPending = this.active?.terminalResult === result;
|
|
406
|
-
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
446
|
+
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(terminalMetadata ? { terminal_metadata: terminalMetadata } : {}), ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
407
447
|
}
|
|
408
448
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
409
|
-
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
|
|
449
|
+
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
|
|
450
|
+
validateObservationDuration(livenessTimeoutMs);
|
|
410
451
|
if (!text) throw new DaemonError("prompt_required", "prompt is required");
|
|
411
452
|
await this.start(); await this.draining;
|
|
412
|
-
if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
|
|
453
|
+
if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
|
|
413
454
|
if (this.state.last_hook_rejection && !this.receipt().settled) throw new DaemonError("tree_not_settled", "Rejected hook does not prove that the prior Antigravity tree stopped", false, { primary_error: this.state.last_hook_rejection });
|
|
414
455
|
if (this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
|
|
415
456
|
let resolveTurn, rejectTurn;
|
|
@@ -417,27 +458,71 @@ export class Runtime {
|
|
|
417
458
|
// Cancellation can reject while admission persistence is still pending.
|
|
418
459
|
void reply.catch(() => {});
|
|
419
460
|
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
|
|
461
|
+
let resolveDispatchJournal;
|
|
462
|
+
const dispatchJournal = current.dispatchJournal = new Promise(resolve => { resolveDispatchJournal = resolve; });
|
|
463
|
+
this.promptObservation = current;
|
|
464
|
+
const finishDispatchJournal = () => {
|
|
465
|
+
current.dispatchJournalSettled = true; resolveDispatchJournal();
|
|
466
|
+
if (current.observerReturned && this.promptObservation === current) this.promptObservation = null;
|
|
467
|
+
};
|
|
420
468
|
// Reserve before persistence so different operation IDs cannot overlap.
|
|
469
|
+
current.publishProgress = operationProgressReporter();
|
|
421
470
|
this.active = current;
|
|
422
471
|
this.hookRequests = new Map();
|
|
423
472
|
const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
|
|
424
473
|
this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
|
|
425
|
-
this.lastProviderEvent = null;
|
|
474
|
+
this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
|
|
426
475
|
try {
|
|
427
476
|
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
428
477
|
assertDispatch();
|
|
478
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
429
479
|
if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
|
|
480
|
+
current.dispatched = true;
|
|
430
481
|
this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
|
|
431
|
-
if (error) {
|
|
482
|
+
if (error) { this.drainingError ??= nativeObservationError(error); current.dispatchObservationError = this.drainingError; current.reject(this.drainingError); finishDispatchJournal(); }
|
|
483
|
+
else void Promise.resolve().then(() => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: this.init?.conversation_id, native_request_id: current.generation })).then(finishDispatchJournal, cause => {
|
|
484
|
+
this.drainingError ??= nativeObservationError(cause); current.dispatchObservationError = this.drainingError;
|
|
485
|
+
current.reject(this.drainingError); finishDispatchJournal();
|
|
486
|
+
});
|
|
432
487
|
});
|
|
433
|
-
} catch (error) {
|
|
434
|
-
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (current.dispatched) { this.drainingError ??= nativeObservationError(error); error = this.drainingError; }
|
|
490
|
+
else if (this.active === current) this.active = null;
|
|
491
|
+
current.reject(error); finishDispatchJournal();
|
|
492
|
+
}
|
|
493
|
+
const clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal("observation_gap", { gap, operation_id: operationId }).catch(() => {}); } });
|
|
494
|
+
let checking = false;
|
|
495
|
+
const timer = setInterval(() => {
|
|
496
|
+
if (this.active !== current || checking || !clock.sample(this.progress)) return;
|
|
497
|
+
checking = true; const cursor = this.progress?.cursor;
|
|
498
|
+
void this.completePendingTurn().then(() => {
|
|
499
|
+
if (this.active !== current || this.progress?.cursor !== cursor || !clock.sample(this.progress)) return;
|
|
500
|
+
current.reject(new DaemonError("operation_observation_lost", "Antigravity native outcome is unknown after the productive silence window", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", last_activity_at: this.lastActivityAt ?? null }));
|
|
501
|
+
}).catch(error => {
|
|
502
|
+
if (this.active === current && this.progress?.cursor === cursor) current.reject(new DaemonError("operation_observation_lost", "Antigravity quiet reconciliation could not confirm the native outcome", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", observation_error: errorRecord(error) }));
|
|
503
|
+
}).finally(() => { checking = false; });
|
|
504
|
+
}, Math.max(10, Math.min(1_000, livenessTimeoutMs / 4)));
|
|
505
|
+
try {
|
|
506
|
+
const result = await reply;
|
|
507
|
+
await dispatchJournal;
|
|
508
|
+
if (current.dispatchObservationError) {
|
|
509
|
+
const error = current.dispatchObservationError;
|
|
510
|
+
if (error.code !== "operation_observation_lost") throw error;
|
|
511
|
+
throw Object.assign(new Error("Antigravity completed natively but its dispatch journal could not be saved", { cause: error.cause ?? error }), {
|
|
512
|
+
code: "native_outcome_observation_failed", details: { native_outcome: result, settled: false, observation_error: errorRecord(error) }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
} finally {
|
|
517
|
+
clearInterval(timer); current.observerReturned = true;
|
|
518
|
+
if (current.dispatchJournalSettled && this.promptObservation === current) this.promptObservation = null;
|
|
519
|
+
}
|
|
435
520
|
}
|
|
436
521
|
childActivity(sessionId) {
|
|
437
522
|
if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
|
|
438
523
|
const child = this.descendants.get(sessionId);
|
|
439
524
|
if (!child) return;
|
|
440
|
-
this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null
|
|
525
|
+
this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null });
|
|
441
526
|
}
|
|
442
527
|
async childForHook(conversationId) {
|
|
443
528
|
if (conversationId === this.init?.conversation_id) return null;
|
|
@@ -459,8 +544,7 @@ export class Runtime {
|
|
|
459
544
|
const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
|
|
460
545
|
daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
|
|
461
546
|
try {
|
|
462
|
-
await this.journal("hook_correction", { correction });
|
|
463
|
-
await this.persist({ last_hook_correction: correction });
|
|
547
|
+
await hookBudget(1_000, null).run(async () => { await this.journal("hook_correction", { correction }); await this.persist({ last_hook_correction: correction }); }, "correction_persistence");
|
|
464
548
|
current.hookCorrectionGeneration = current.generation;
|
|
465
549
|
return;
|
|
466
550
|
} catch (storage) { error = Object.assign(new Error("Could not persist native hook correction"), { code: "native_hook_receipt_invalid", details: { effect: "unknown", primary_error: correction, receipt_error: storage.message } }); }
|
|
@@ -468,7 +552,7 @@ export class Runtime {
|
|
|
468
552
|
const rejection = this.state.last_hook_rejection && this.state.last_hook_rejection.turn_generation === current?.generation ? this.state.last_hook_rejection : { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), details: error.details, event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
|
|
469
553
|
this.state.last_hook_rejection = rejection;
|
|
470
554
|
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
471
|
-
try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
|
|
555
|
+
try { await hookBudget(1_000, null).run(async () => { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }, "failure_persistence"); }
|
|
472
556
|
catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
|
|
473
557
|
}
|
|
474
558
|
async observeHookFailures() {
|
|
@@ -482,26 +566,49 @@ export class Runtime {
|
|
|
482
566
|
await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
|
|
483
567
|
}
|
|
484
568
|
}
|
|
485
|
-
async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!
|
|
569
|
+
async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); this.progressHooks ??= new Set(); const scoped = this.active && (!child || child.activity_generation === this.active.generation); const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]); if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true) && !this.progressHooks.has(progressKey) && this.progressHooks.size < 1_024) { this.progressHooks.add(progressKey); this.markActivity(); } if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
|
|
486
570
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
487
571
|
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured", ...(!payload.model ? { non_asserting: true } : {}) }, evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
488
572
|
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
489
573
|
}
|
|
490
574
|
async finalizeProviderForControl() {
|
|
491
575
|
const pending = this.providerFinalization;
|
|
492
|
-
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
576
|
+
try {
|
|
577
|
+
try { await pending; }
|
|
578
|
+
catch (error) {
|
|
579
|
+
// Control retries only retained physical/resource cleanup, never a
|
|
580
|
+
// missing mandatory stream record or a productive provider request.
|
|
581
|
+
if (this.providerFinalization !== pending) await this.providerFinalization;
|
|
582
|
+
else {
|
|
583
|
+
if (!this.retryProviderFinalization) throw error;
|
|
584
|
+
this.providerFinalization = this.retryProviderFinalization(5_000);
|
|
585
|
+
await this.providerFinalization;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await this.promptObservation?.dispatchJournal;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (!this.drainingError) throw error;
|
|
591
|
+
this.drainingError.cleanup_error = errorRecord(error);
|
|
592
|
+
this.drainingError.details = { ...this.drainingError.details, cleanup_error: this.drainingError.cleanup_error };
|
|
593
|
+
throw this.drainingError;
|
|
500
594
|
}
|
|
595
|
+
if (this.drainingError) throw this.drainingError;
|
|
596
|
+
}
|
|
597
|
+
async prepareStop(cancelTree) {
|
|
598
|
+
const current = this.active, priorResult = this.lastResult;
|
|
599
|
+
const emptyTree = !current && !this.state.active_tree && !this.state.unclaimed_activity && !priorResult && !this.descendants.size;
|
|
600
|
+
if (cancelTree) await this.cancel();
|
|
601
|
+
else if (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running");
|
|
602
|
+
// A physical kill is settlement, not a native clean terminal receipt.
|
|
603
|
+
// In particular, the previous Turn's SUCCESS cannot certify a cancelled
|
|
604
|
+
// in-flight Turn that exited without its own result.
|
|
605
|
+
const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
|
|
606
|
+
const clean = emptyTree || (currentResult && this.receipt().settled);
|
|
607
|
+
return { clean, ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
|
|
501
608
|
}
|
|
502
609
|
async cancel() { cancelPendingDispatch(this); if (!this.child) return { ...this.receipt(), settled: Boolean(this.exited) }; if (!this.exited) { try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); } await this.finalizeProviderForControl(); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
503
610
|
}
|
|
504
|
-
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.
|
|
611
|
+
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && !this.exited && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.finalizeProviderForControl(); }
|
|
505
612
|
async abortProvider(reason) {
|
|
506
613
|
const child = this.child, providerProcess = this.providerProcess;
|
|
507
614
|
await stopProcessGroup(child);
|
|
@@ -514,7 +621,27 @@ export class Runtime {
|
|
|
514
621
|
export async function serveDaemon(stateDir) {
|
|
515
622
|
const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
|
|
516
623
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
517
|
-
const runtime = new Runtime(paths, state); await removeSocket(paths);
|
|
624
|
+
const runtime = new Runtime(paths, state); await removeSocket(paths);
|
|
625
|
+
const server = net.createServer(socket => {
|
|
626
|
+
let buffer = "", consumed = false; socket.setEncoding("utf8");
|
|
627
|
+
socket.on("data", chunk => {
|
|
628
|
+
if (consumed) return;
|
|
629
|
+
buffer += chunk; if (!admitNativeRequestBuffer(buffer, socket, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
630
|
+
void (async () => {
|
|
631
|
+
let request;
|
|
632
|
+
try {
|
|
633
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
634
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} }));
|
|
635
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
636
|
+
socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
639
|
+
socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
640
|
+
}
|
|
641
|
+
})();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
518
645
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
|
|
519
646
|
try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
|
|
520
647
|
catch (error) {
|
|
@@ -528,9 +655,10 @@ export async function serveDaemon(stateDir) {
|
|
|
528
655
|
|
|
529
656
|
async function dispatch(runtime, request) {
|
|
530
657
|
if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
|
|
658
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
|
|
531
659
|
if (request.operation === "daemon.status") { await runtime.observeHookFailures(); return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, active_operation: runtime.active?.operationId ?? null, last_hook_rejection: runtime.state.last_hook_rejection ?? null, last_activity_at: runtime.lastActivityAt, provider_ready: Boolean(runtime.init && !runtime.exited), pid: process.pid, provider_pid: runtime.child?.pid ?? null, versions: runtime.state.versions, config: runtime.config, sessions: runtime.state.sessions ?? [], receipt: runtime.receipt() }; }
|
|
532
660
|
if (request.operation === "session.create") { if (params.sessionId) throw new DaemonError("invalid_create", "create does not accept a Session ID"); if (params.prompt) throw new DaemonError("invalid_create", "create does not execute a prompt; use session.prompt"); await runtime.start(); await runtime.zeroUsage(); return runtime.receipt(); }
|
|
533
|
-
if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch, request.id); }
|
|
661
|
+
if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch, request.id, params.livenessTimeoutMs ?? runtime.config.livenessTimeoutMs ?? 1_800_000); }
|
|
534
662
|
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
|
|
535
663
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
536
664
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
@@ -557,7 +685,8 @@ async function dispatch(runtime, request) {
|
|
|
557
685
|
}
|
|
558
686
|
catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
|
|
559
687
|
}
|
|
560
|
-
if (request.operation === "daemon.stop")
|
|
688
|
+
if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]]);
|
|
561
689
|
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
562
690
|
}
|
|
563
691
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
692
|
+
import { commandText } from "./process-json.mjs";
|