@deksden-com/dd-flow-cli 0.9.0-beta.121 → 0.9.0-beta.123
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 +17 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/hook-ingress.js +4 -3
- package/dist/cli/native-hook-ingress.js +34 -2
- package/dist/cli/run-cli.js +3 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +68 -6
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +21 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +5 -1
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
- package/dist/harness-runtime/lib/dd-zcode.mjs +40 -16
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/services/codex-capacity.js +131 -12
- package/dist/services/codex-hook-delivery.js +6 -2
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +27 -9
- package/dist/services/hooks.js +51 -59
- package/dist/services/lifecycle-command.js +1 -383
- package/dist/services/lifecycle-contract.js +1 -30
- package/dist/services/lifecycle-invocations.js +34 -13
- package/dist/services/merge-server.js +26 -9
- package/dist/services/review-copy.js +21 -0
- package/dist/services/run-controller-adapter.js +25 -3
- package/dist/services/run-controller-recovery.js +9 -2
- package/dist/services/run-controller.js +59 -20
- package/dist/services/run-observations.js +7 -2
- package/dist/services/runs.js +60 -47
- package/dist/services/stage-report-renderer.js +44 -2
- package/dist/services/verification-matrix.js +345 -0
- package/dist/services/vnext-code-review.js +30 -11
- package/dist/services/vnext-code.js +31 -10
- package/dist/services/vnext-merge.js +30 -7
- package/dist/services/vnext-plan-review.js +28 -7
- package/dist/services/vnext-plan.js +16 -3
- package/dist/services/work-registry.js +15 -1
- package/dist/storage/database.js +43 -9
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.123
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Continue confirmed Codex overloads in the retained Session with bounded backoff, exact owner admission and no concurrent productive Turns.
|
|
8
|
+
- Retain content-addressed verification evidence and reviewer input baselines across publication, relocation and review waves.
|
|
9
|
+
- Preserve canonical native model/tool observations and AGY child transcript evidence without rewriting historical results.
|
|
10
|
+
|
|
11
|
+
## 0.9.0-beta.122
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Generate a deterministic RUN-local verification matrix from accepted requirements, PLAN and exact check receipts; publish retained input/output packets and show their paths in shared Stage/Work instructions.
|
|
16
|
+
- Recognize one unambiguous lifecycle command through the shared Bash grammar instead of rejecting all shell composition; retain scoped authority, execution-directory and foreground checks.
|
|
17
|
+
- Share native hook deadlines through subprocess and SQLite admission, retain bounded phase diagnostics and the first ZCode failure, and prepare Stage archive/Git work outside the writer.
|
|
18
|
+
- Admit the tested ZCode ACP 0.52 bridge and retain explicitly configured native storage routes across daemon lifecycle calls.
|
|
19
|
+
|
|
3
20
|
## 0.9.0-beta.120
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.123",
|
|
4
|
+
"cli_commit": "0d75e9e7e18868fbe9eee8d51e4c4d06ab03a8cd",
|
|
5
|
+
"built_at": "2026-09-30T12:08:03.924Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "4.1.
|
|
8
|
-
"commit": "
|
|
7
|
+
"version": "4.1.2",
|
|
8
|
+
"commit": "bbb3845083d7c1984bb61d813dd6700fe695180a",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
|
package/dist/cli/hook-ingress.js
CHANGED
|
@@ -21,11 +21,12 @@ export async function runHookIngress(args) {
|
|
|
21
21
|
throw new AppError("usage", "--project-root requires a value", 2);
|
|
22
22
|
if (parsed.options.has("json") && parsed.options.get("json")?.[0] !== "__dd_flow_flag__")
|
|
23
23
|
throw new AppError("usage", "--json does not take a value", 2);
|
|
24
|
-
const budget = hookBudget(ingressBudgetMs);
|
|
24
|
+
const budget = hookBudget(ingressBudgetMs, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null);
|
|
25
25
|
const stdin = await readNativeHookInput(process.stdin, budget);
|
|
26
26
|
const payload = parseJsonObject(stdin || "{}", "native hook stdin");
|
|
27
27
|
const command = lifecycleCommandFromPayload(payload);
|
|
28
|
-
|
|
28
|
+
const analysis = command ? parseLifecycleCommand(command) : { kind: "none", unresolved: false };
|
|
29
|
+
if (event !== "PreToolUse" || !command || (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command))) {
|
|
29
30
|
process.stdout.write("{}\n");
|
|
30
31
|
return 0;
|
|
31
32
|
}
|
|
@@ -46,7 +47,7 @@ export async function runHookIngress(args) {
|
|
|
46
47
|
// A bounded CLI subprocess, not a detached writer: its successful reply follows COMMIT.
|
|
47
48
|
if (process.argv[1] === workerFile) {
|
|
48
49
|
try {
|
|
49
|
-
const stdin = await readNativeHookInput(process.stdin, hookBudget(ingressBudgetMs));
|
|
50
|
+
const stdin = await readNativeHookInput(process.stdin, hookBudget(ingressBudgetMs, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null));
|
|
50
51
|
const payload = parseJsonObject(stdin, "native hook stdin");
|
|
51
52
|
const started = performance.now();
|
|
52
53
|
const { verifyCodexHookDelivery } = await import("../services/codex-hook-delivery.js");
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
2
|
import { parseCommandArgs } from "../services/lifecycle-command.js";
|
|
3
|
+
import { channel } from "node:diagnostics_channel";
|
|
3
4
|
/** These ingress commands consume a prepared store; they never prepare it. */
|
|
4
5
|
export function isNativeHookIngress(args) {
|
|
5
6
|
return args[2] === "handle" && ((args[1] === "event" && ["agy", "grok", "droid", "opencode", "zcode"].includes(args[0] ?? ""))
|
|
6
7
|
|| args[0] === "zcode" && args[1] === "hook");
|
|
7
8
|
}
|
|
8
9
|
export async function dispatchNativeHook(args, stdin, env) {
|
|
10
|
+
const started = performance.now();
|
|
11
|
+
const deadline = env.DD_FLOW_HOOK_DEADLINE_AT === undefined ? undefined : Number(env.DD_FLOW_HOOK_DEADLINE_AT);
|
|
12
|
+
if (deadline !== undefined && !Number.isSafeInteger(deadline))
|
|
13
|
+
throw new AppError("hook_deadline_invalid", "Native hook deadline must be an absolute millisecond timestamp", 1, { effect: "unknown" });
|
|
14
|
+
const phase = (name) => {
|
|
15
|
+
if (deadline !== undefined && deadline <= Date.now())
|
|
16
|
+
throw new AppError("native_hook_timeout", "Native hook deadline expired", 1, { phase: name, effect: "unknown" });
|
|
17
|
+
process.stderr.write(`${JSON.stringify({ kind: "native_hook_phase", phase: name, elapsed_ms: Math.round(performance.now() - started), ...(deadline !== undefined ? { remaining_ms: Math.max(0, deadline - Date.now()) } : {}) })}\n`);
|
|
18
|
+
};
|
|
19
|
+
phase("import");
|
|
9
20
|
const parsed = parseCommandArgs(args.slice(3));
|
|
10
21
|
const projectRoot = parsed.options.get("project-root")?.[0];
|
|
11
22
|
if (!isNativeHookIngress(args) || parsed.positional.length || !projectRoot || projectRoot === "__dd_flow_flag__")
|
|
@@ -16,27 +27,48 @@ export async function dispatchNativeHook(args, stdin, env) {
|
|
|
16
27
|
const [hooks, { createContext }, { resolveDdFlowHome }] = await Promise.all([
|
|
17
28
|
import("../services/hooks.js"), import("../runtime/context.js"), import("../storage/paths.js")
|
|
18
29
|
]);
|
|
30
|
+
phase("imported");
|
|
19
31
|
let opened;
|
|
20
32
|
// Reuse the actual envelope validators and shell parser. They return for
|
|
21
33
|
// unrelated tools before their first DB access, without duplicating policy.
|
|
22
34
|
const context = {
|
|
23
35
|
env, ddFlowHome: resolveDdFlowHome(env), now: () => new Date().toISOString(),
|
|
24
|
-
get db() {
|
|
36
|
+
get db() {
|
|
37
|
+
if (!opened) {
|
|
38
|
+
phase("store_open");
|
|
39
|
+
opened = createContext(env, "hook");
|
|
40
|
+
phase("store_opened");
|
|
41
|
+
}
|
|
42
|
+
return opened.db;
|
|
43
|
+
}
|
|
25
44
|
};
|
|
26
45
|
const handler = args[0] === "agy" ? hooks.handleAgyEvent : args[0] === "grok" ? hooks.handleGrokEvent
|
|
27
46
|
: args[0] === "droid" ? hooks.handleDroidEvent : args[0] === "opencode" ? hooks.handleOpenCodeEvent
|
|
28
47
|
: args[1] === "hook" ? hooks.handleZcodeHook : hooks.handleZcodeEvent;
|
|
48
|
+
const timing = channel("dd-flow.sqlite.ingress");
|
|
49
|
+
// This command owns one synchronous store; there is no concurrent handler here.
|
|
50
|
+
let previousPhase;
|
|
51
|
+
const log = (value) => {
|
|
52
|
+
const record = value;
|
|
53
|
+
if (record.phase === previousPhase)
|
|
54
|
+
return;
|
|
55
|
+
previousPhase = record.phase;
|
|
56
|
+
process.stderr.write(`${JSON.stringify({ kind: "native_hook_phase", phase: record.phase, elapsed_ms: Math.round(performance.now() - started), ...(record.remaining_ms !== undefined ? { remaining_ms: record.remaining_ms } : {}) })}\n`);
|
|
57
|
+
};
|
|
58
|
+
timing.subscribe(log);
|
|
29
59
|
try {
|
|
60
|
+
phase("receipt");
|
|
30
61
|
return await handler(context, { projectRoot, stdin });
|
|
31
62
|
}
|
|
32
63
|
finally {
|
|
64
|
+
timing.unsubscribe(log);
|
|
33
65
|
opened?.db.close?.();
|
|
34
66
|
}
|
|
35
67
|
}
|
|
36
68
|
export async function runNativeHookIngress(args) {
|
|
37
69
|
try {
|
|
38
70
|
const { readNativeHookInput, hookBudget } = await import("../harness-runtime/lib/native-hook-command.mjs");
|
|
39
|
-
const stdin = await readNativeHookInput(process.stdin, hookBudget());
|
|
71
|
+
const stdin = await readNativeHookInput(process.stdin, hookBudget(25_000, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null));
|
|
40
72
|
process.stdout.write(`${JSON.stringify(await dispatchNativeHook(args, stdin, process.env))}\n`);
|
|
41
73
|
return 0;
|
|
42
74
|
}
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -75,6 +75,7 @@ import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScop
|
|
|
75
75
|
import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
|
|
76
76
|
import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
|
|
77
77
|
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
78
|
+
import { assertCapacityOwnerAdmission } from "../services/codex-capacity.js";
|
|
78
79
|
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
79
80
|
import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
80
81
|
import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
|
|
@@ -2656,6 +2657,8 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
|
|
|
2656
2657
|
const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
|
|
2657
2658
|
if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
|
|
2658
2659
|
throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
|
|
2660
|
+
if (!recovering)
|
|
2661
|
+
assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"));
|
|
2659
2662
|
}
|
|
2660
2663
|
const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
|
|
2661
2664
|
return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
|
|
@@ -15,7 +15,7 @@ async function hook(options) {
|
|
|
15
15
|
const payload = JSON.parse(await readNativeHookInput(process.stdin, budget) || "{}"), event = options.event;
|
|
16
16
|
try {
|
|
17
17
|
const observationMs = budget.remaining("observation");
|
|
18
|
-
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt:
|
|
18
|
+
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt: budget.deadlineAt }, observationMs, requestId);
|
|
19
19
|
if (identity.stale) return event === "PreToolUse" ? { decision: "deny", reason: "agy_stale_hook" } : {};
|
|
20
20
|
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
21
21
|
const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
|
|
@@ -28,7 +28,10 @@ async function hook(options) {
|
|
|
28
28
|
} catch (error) {
|
|
29
29
|
// A failed socket cannot be used to report its own failure. The daemon
|
|
30
30
|
// reads only receipts matching request IDs it actually accepted this turn.
|
|
31
|
-
|
|
31
|
+
const correctable = event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && error.details?.effect === "no_effect"
|
|
32
|
+
&& error.details?.recoverable === true && error.details?.disposition === "pre_cli_shell_refusal";
|
|
33
|
+
try { await writeJsonAtomic(path.join(options["state-dir"], "hook-failures", `${requestId}.json`), { request_id: requestId, observed_at: new Date().toISOString(), code: error.code ?? "agy_hook_rejected", message: error.message,
|
|
34
|
+
details: { ...error.details, effect: correctable ? "no_effect" : "unknown", ...(correctable ? { native_pre_cli_refusal: true } : {}) } }); }
|
|
32
35
|
catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
33
36
|
throw error;
|
|
34
37
|
}
|
|
@@ -49,4 +52,4 @@ try {
|
|
|
49
52
|
else if (family === "session") { if (command === "fork") throw Object.assign(new Error("Antigravity native headless fork is unsupported; use deterministic_replay"), { code: "agy_headless_fork_unsupported" }); const operation = command === "status" ? "inspect" : command; result = await callDaemon(common.stateDir, `session.${operation}`, { sessionId: options["session-id"], ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
|
|
50
53
|
else throw new Error("usage: dd-agy doctor | hooks install|check | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
|
|
51
54
|
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
|
|
52
|
-
} catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.code ?? "agy_hook_rejected" })}\n`); process.exit(0); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
|
|
55
|
+
} catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: `${error.code ?? "agy_hook_rejected"}: ${error.message ?? "Native hook refused this tool call"}` })}\n`); process.exit(0); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
|
-
import { ToolObservations, agyToolObservation } from "./tool-observations.mjs";
|
|
2
|
+
import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
6
|
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
7
|
import { spawn } from "node:child_process";
|
|
@@ -16,6 +18,29 @@ import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, conf
|
|
|
16
18
|
|
|
17
19
|
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
18
20
|
class DaemonError extends AgyError {}
|
|
21
|
+
export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId]) {
|
|
22
|
+
const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
|
|
23
|
+
let handle;
|
|
24
|
+
try {
|
|
25
|
+
if (!rootSessionId || !child.provider_session_id || !ownedSessionIds.includes(child.parent_provider_session_id) || !child.log_uri || !/^[\w-]+$/.test(child.provider_session_id)) throw new Error("unbound native locator");
|
|
26
|
+
const expected = path.join(runtimeRoot, "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
|
|
27
|
+
const canonical = path.join(await realpath(runtimeRoot), "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
|
|
28
|
+
if (![expected, canonical].includes(fileURLToPath(child.log_uri)) || await realpath(expected) !== canonical) throw new Error("foreign or symlink native locator");
|
|
29
|
+
const before = await lstat(expected);
|
|
30
|
+
if (!before.isFile()) throw new Error("native locator is not a regular file");
|
|
31
|
+
handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
32
|
+
const stat = await handle.stat();
|
|
33
|
+
if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
|
|
34
|
+
const maximum = 8 * 1024 * 1024, buffer = Buffer.alloc(Math.min(stat.size, maximum));
|
|
35
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
36
|
+
const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
|
|
37
|
+
const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
|
|
38
|
+
const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
|
|
39
|
+
if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
|
|
40
|
+
return events;
|
|
41
|
+
} catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
|
|
42
|
+
finally { await handle?.close(); }
|
|
43
|
+
}
|
|
19
44
|
const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
|
|
20
45
|
function locations(stateDir) {
|
|
21
46
|
const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
|
|
@@ -192,7 +217,7 @@ export async function startDaemon(options) {
|
|
|
192
217
|
const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
|
|
193
218
|
const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
194
219
|
const retained = retainedAgyState(previous, config.resumeSessionId);
|
|
195
|
-
await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
|
|
220
|
+
await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
|
|
196
221
|
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();
|
|
197
222
|
try {
|
|
198
223
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
@@ -214,7 +239,7 @@ export const stopDaemon = async options => { const timeoutMs = options.timeoutMs
|
|
|
214
239
|
|
|
215
240
|
export class Runtime {
|
|
216
241
|
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; }
|
|
217
|
-
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()], 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; }
|
|
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; }
|
|
218
243
|
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 }); } }
|
|
219
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", "6h", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
|
|
220
245
|
requireSessionIdentity(requestedId) {
|
|
@@ -295,6 +320,26 @@ export class Runtime {
|
|
|
295
320
|
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 ?? {}); } } }
|
|
296
321
|
observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); 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" }); } }
|
|
297
322
|
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
|
+
async captureChildTools() {
|
|
324
|
+
this.capturedToolFacts ??= new Map();
|
|
325
|
+
const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
|
|
326
|
+
for (const child of this.descendants.values()) {
|
|
327
|
+
const ancestors = new Set(); let parent = child.parent_provider_session_id;
|
|
328
|
+
while (parent && parent !== root && this.descendants.has(parent) && !ancestors.has(parent)) {
|
|
329
|
+
ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
|
|
330
|
+
}
|
|
331
|
+
const events = parent === root && root
|
|
332
|
+
? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors])
|
|
333
|
+
: [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
|
|
334
|
+
for (const event of events) {
|
|
335
|
+
this.toolObservations.add(event);
|
|
336
|
+
const key = JSON.stringify([event.session, event.call_id, event.reason]), encoded = JSON.stringify(event);
|
|
337
|
+
if (this.capturedToolFacts.get(key) === encoded) continue;
|
|
338
|
+
await this.journal("tool_observation", { schema_id: "dd-flow/tool-observation@1", event });
|
|
339
|
+
this.capturedToolFacts.set(key, encoded);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
298
343
|
async finishTurn(result) {
|
|
299
344
|
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
300
345
|
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 });
|
|
@@ -312,6 +357,7 @@ export class Runtime {
|
|
|
312
357
|
// are not request IDs: native command rejections can report zero turns.
|
|
313
358
|
// prompt() drains earlier events before accepting another input.
|
|
314
359
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
360
|
+
await this.captureChildTools();
|
|
315
361
|
this.lastResult = result; this.markActivity();
|
|
316
362
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
317
363
|
const pendingTerminal = current && result.status === "SUCCESS";
|
|
@@ -404,6 +450,21 @@ export class Runtime {
|
|
|
404
450
|
}
|
|
405
451
|
async rejectHook(error, event, payload, current = this.active) {
|
|
406
452
|
if (current && this.active !== current) return;
|
|
453
|
+
const correctable = current && event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid"
|
|
454
|
+
&& error.details?.effect === "no_effect" && error.details?.recoverable === true
|
|
455
|
+
&& error.details?.disposition === "pre_cli_shell_refusal" && error.details?.native_pre_cli_refusal === true;
|
|
456
|
+
if (correctable && current.hookCorrectionGeneration !== current.generation) {
|
|
457
|
+
// The request was correlated with this still-active Turn by
|
|
458
|
+
// observeHookFailures. Persist before allowing native same-Turn recovery.
|
|
459
|
+
const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
|
|
460
|
+
daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
|
|
461
|
+
try {
|
|
462
|
+
await this.journal("hook_correction", { correction });
|
|
463
|
+
await this.persist({ last_hook_correction: correction });
|
|
464
|
+
current.hookCorrectionGeneration = current.generation;
|
|
465
|
+
return;
|
|
466
|
+
} 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 } }); }
|
|
467
|
+
}
|
|
407
468
|
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 };
|
|
408
469
|
this.state.last_hook_rejection = rejection;
|
|
409
470
|
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
@@ -417,12 +478,13 @@ export class Runtime {
|
|
|
417
478
|
try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
|
|
418
479
|
catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
|
|
419
480
|
if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
|
|
481
|
+
this.hookRequests.delete(requestId);
|
|
420
482
|
await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
|
|
421
483
|
}
|
|
422
484
|
}
|
|
423
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 (!stale && prior.last_hook !== fingerprint) 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;
|
|
424
486
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
425
|
-
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
487
|
+
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" });
|
|
426
488
|
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 };
|
|
427
489
|
}
|
|
428
490
|
async finalizeProviderForControl() {
|
|
@@ -446,7 +508,7 @@ export class Runtime {
|
|
|
446
508
|
await this.finalizeProviderForControl();
|
|
447
509
|
await finishDaemonProcess(this.config, providerProcess, "failed", reason);
|
|
448
510
|
}
|
|
449
|
-
async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
511
|
+
async finishResource(state = "stopped", reason = "daemon_stopped") { await this.captureChildTools(); await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
450
512
|
}
|
|
451
513
|
|
|
452
514
|
export async function serveDaemon(stateDir) {
|
|
@@ -469,7 +531,7 @@ async function dispatch(runtime, request) {
|
|
|
469
531
|
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() }; }
|
|
470
532
|
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(); }
|
|
471
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); }
|
|
472
|
-
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
534
|
+
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
|
|
473
535
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
474
536
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
475
537
|
if (request.operation === "hook.observe") {
|
|
@@ -9,7 +9,7 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
10
|
import { errorRecord } from "./operation-errors.mjs";
|
|
11
11
|
import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
|
|
12
|
-
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
12
|
+
import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
13
13
|
|
|
14
14
|
const requestSchema = "dd-codex/daemon-request@1";
|
|
15
15
|
const responseSchema = "dd-codex/daemon-response@1";
|
|
@@ -38,7 +38,7 @@ async function observeCodexSessions(bridge, options, sessions, active, remember
|
|
|
38
38
|
}
|
|
39
39
|
const unsettled = [], observations = [];
|
|
40
40
|
for (const sessionId of sessions) {
|
|
41
|
-
const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId });
|
|
41
|
+
const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined });
|
|
42
42
|
for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
|
|
43
43
|
const status = observed.status?.type ?? observed.status;
|
|
44
44
|
const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
|
|
@@ -101,7 +101,7 @@ export async function serveDaemon(stateDir) {
|
|
|
101
101
|
bridge.close = async () => { await closeBridge(); await finishDaemonProcess(state.config, state.provider_process, "stopped", "daemon_stopped"); };
|
|
102
102
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
103
103
|
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.then(() => writeState(paths.state, snapshot)); return persist; };
|
|
104
|
-
const dispatch = async (operation, params) => {
|
|
104
|
+
const dispatch = async (operation, params, operationId) => {
|
|
105
105
|
const assertDispatch = captureDispatchGuard(bridge);
|
|
106
106
|
if (operation === "hook.verify") {
|
|
107
107
|
if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
|
|
@@ -125,7 +125,23 @@ export async function serveDaemon(stateDir) {
|
|
|
125
125
|
}
|
|
126
126
|
if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
|
|
127
127
|
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 }); } }
|
|
128
|
-
if (operation === "session.prompt") {
|
|
128
|
+
if (operation === "session.prompt") {
|
|
129
|
+
// Admission can wait for a permit. Observe again after it returns, not
|
|
130
|
+
// just before the caller's backoff, so no child Turn races this dispatch.
|
|
131
|
+
// Reserve before the first await: distinct operation IDs are independent
|
|
132
|
+
// journal entries and must not both pass the idle check while inspecting.
|
|
133
|
+
active = operation;
|
|
134
|
+
try {
|
|
135
|
+
const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
|
|
136
|
+
assertDispatch();
|
|
137
|
+
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 } });
|
|
138
|
+
if (/:capacity:[12]:session\.prompt$|:child:[a-f0-9]+:followup:[12]$/.test(operationId ?? ""))
|
|
139
|
+
await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
|
|
140
|
+
sessions.add(params.sessionId);
|
|
141
|
+
await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
|
|
142
|
+
}
|
|
143
|
+
finally { active = null; await save({ active_operation: null }); }
|
|
144
|
+
}
|
|
129
145
|
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
|
|
130
146
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
131
147
|
const owned = new Set(sessions);
|
|
@@ -143,7 +159,7 @@ export async function serveDaemon(stateDir) {
|
|
|
143
159
|
}
|
|
144
160
|
throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
|
|
145
161
|
};
|
|
146
|
-
await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}), () => dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
|
|
162
|
+
await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}, request.id), () => dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
|
|
147
163
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await save({ shutdown_state: "running", active_operation: null }); return await new Promise(() => {});
|
|
148
164
|
}
|
|
149
165
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -542,6 +542,8 @@ function nativeTurnError(bridge, sessionId, turn) {
|
|
|
542
542
|
function turnItemEvidence(turn) {
|
|
543
543
|
const items = turn?.items;
|
|
544
544
|
return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
|
|
545
|
+
items: items.map(item => ({ id: item?.id ?? null, type: item?.type ?? "unknown", status: item?.status ?? null })),
|
|
546
|
+
pending: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type) && !["completed", "failed", "declined"].includes(item?.status)),
|
|
545
547
|
possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
|
|
546
548
|
}
|
|
547
549
|
|
|
@@ -615,7 +617,9 @@ export async function inspectSession(options) {
|
|
|
615
617
|
|
|
616
618
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
617
619
|
const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
|
|
618
|
-
|
|
620
|
+
const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
|
|
621
|
+
const exact = terminal?.id === options.turnId && terminal?.status === "failed" ? { terminal_turn_id: terminal.id, terminal_status: terminal.status, provider_error: nativeTurnError(bridge, sessionId, terminal), native_turn_items: turnItemEvidence(terminal) } : {};
|
|
622
|
+
return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
|
|
619
623
|
}
|
|
620
624
|
|
|
621
625
|
export async function cancelSession(options) {
|
|
@@ -124,8 +124,19 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
const ZCODE_ROUTING_ENV = ["ZCODE_HOME", "ZCODE_STORAGE_DIR", "ZCODE_SESSION_DB_PATH"];
|
|
128
|
+
|
|
129
|
+
function nativeRoutingEnv() {
|
|
130
|
+
return Object.fromEntries(ZCODE_ROUTING_ENV.filter(key => process.env[key] !== undefined)
|
|
131
|
+
.map(key => [key, absolute(process.env[key], key)]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function sameNativeRouting(previous, config) {
|
|
135
|
+
return ZCODE_ROUTING_ENV.every(key => (previous?.env?.[key] ?? null) === (config.env?.[key] ?? null));
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
function sameConfig(state, config) {
|
|
128
|
-
return ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
139
|
+
return sameNativeRouting(state.config, config) && ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
export async function startDaemon(options) {
|
|
@@ -145,7 +156,7 @@ export async function startDaemon(options) {
|
|
|
145
156
|
ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
|
|
146
157
|
projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
|
|
147
158
|
resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
148
|
-
env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {},
|
|
159
|
+
env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
|
|
149
160
|
livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
|
|
150
161
|
requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
|
|
151
162
|
};
|
|
@@ -159,7 +170,10 @@ export async function startDaemon(options) {
|
|
|
159
170
|
} catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
|
|
160
171
|
|
|
161
172
|
const previous = await readState(paths.dir);
|
|
162
|
-
if (previous?.shutdown_state === "clean")
|
|
173
|
+
if (previous?.shutdown_state === "clean") {
|
|
174
|
+
if (!sameNativeRouting(previous.config, config)) throw new DaemonError("daemon_config_mismatch", "retained ZCode native storage routing changed");
|
|
175
|
+
await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
176
|
+
}
|
|
163
177
|
if (previous?.shutdown_state === "running" && previous.active_tree) {
|
|
164
178
|
throw new DaemonError("invalid_harness_crash", "the previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
|
|
165
179
|
}
|
|
@@ -237,6 +251,7 @@ export class DaemonRuntime {
|
|
|
237
251
|
const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
|
|
238
252
|
return {
|
|
239
253
|
...this.state.config, ...params, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
|
|
254
|
+
neverPromptedRootProof: this.neverPromptedRootProof(params.sessionId),
|
|
240
255
|
daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
|
|
241
256
|
assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
|
|
242
257
|
// The bridge owns the quiet timer; the daemon owns the durable status.
|
|
@@ -304,6 +319,12 @@ export class DaemonRuntime {
|
|
|
304
319
|
// A late identity is evidence, not permission to resurrect a completed
|
|
305
320
|
// request or overwrite another operation's ownership.
|
|
306
321
|
this.track(identity);
|
|
322
|
+
const allocated = this.sessions.get(identity.provider_session_id);
|
|
323
|
+
if (allocated && this.activeProductive === "session.create" && current?.allocation?.adapter_session_id === identity.adapter_session_id
|
|
324
|
+
&& allocated.prompt_dispatched === undefined) {
|
|
325
|
+
allocated.prompt_dispatched = false;
|
|
326
|
+
allocated.created_by_daemon_id = this.state.daemon_id;
|
|
327
|
+
}
|
|
307
328
|
if (!current || current.allocation?.adapter_session_id !== identity.adapter_session_id) {
|
|
308
329
|
await this.persist({});
|
|
309
330
|
return;
|
|
@@ -344,6 +365,8 @@ export class DaemonRuntime {
|
|
|
344
365
|
topology: topology ?? existing?.topology ?? null,
|
|
345
366
|
known_child_session_ids: [...new Set([...(existing?.known_child_session_ids ?? zcodeChildSessionIds(existing?.topology)), ...zcodeChildSessionIds(topology)])],
|
|
346
367
|
completed_turns: existing?.completed_turns ?? 0,
|
|
368
|
+
prompt_dispatched: existing?.prompt_dispatched,
|
|
369
|
+
created_by_daemon_id: existing?.created_by_daemon_id,
|
|
347
370
|
close_receipt: result.close?.closed === true ? result : existing?.close_receipt ?? null,
|
|
348
371
|
}); }
|
|
349
372
|
if (result?.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
|
|
@@ -397,10 +420,9 @@ export class DaemonRuntime {
|
|
|
397
420
|
|
|
398
421
|
async requireSettled(params) {
|
|
399
422
|
if (this.sessions.get(params.sessionId)?.close_receipt) throw new DaemonError("session_closed", "Closed Session requires explicit recovery");
|
|
400
|
-
const known = this.sessions.get(params.sessionId);
|
|
401
423
|
// A freshly created ZCode Session has no prior Turn and is not yet exposed
|
|
402
424
|
// by the provider's subagent index. Its first prompt is therefore safe.
|
|
403
|
-
if (
|
|
425
|
+
if (this.neverPromptedRootProof(params.sessionId)) return null;
|
|
404
426
|
const observed = await inspectSessionWithBridge(this.bridge, this.options(params));
|
|
405
427
|
const runningChildren = observed.subagents?.running ?? [];
|
|
406
428
|
if (this.treeRunning(observed)) {
|
|
@@ -410,6 +432,21 @@ export class DaemonRuntime {
|
|
|
410
432
|
return observed;
|
|
411
433
|
}
|
|
412
434
|
|
|
435
|
+
neverPromptedRootProof(sessionId) {
|
|
436
|
+
const known = this.sessions.get(sessionId);
|
|
437
|
+
return known?.prompt_dispatched === false && known.created_by_daemon_id === this.state.daemon_id
|
|
438
|
+
&& known.parent_provider_session_id === null && known.root_provider_session_id === sessionId
|
|
439
|
+
&& Array.isArray(known.known_child_session_ids) && known.known_child_session_ids.length === 0
|
|
440
|
+
? { source: "owned_daemon_create_without_prompt", daemon_id: this.state.daemon_id, provider_session_id: sessionId }
|
|
441
|
+
: null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async markPromptDispatched(sessionId) {
|
|
445
|
+
const known = this.sessions.get(sessionId);
|
|
446
|
+
if (known) known.prompt_dispatched = true;
|
|
447
|
+
await this.persist();
|
|
448
|
+
}
|
|
449
|
+
|
|
413
450
|
async dispatch(operation, params) {
|
|
414
451
|
if (operation === "daemon.status") return {
|
|
415
452
|
daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket,
|
|
@@ -447,11 +484,11 @@ export class DaemonRuntime {
|
|
|
447
484
|
dispatched_at: new Date().toISOString(),
|
|
448
485
|
request_timeout_ms: this.state.config.requestTimeoutMs,
|
|
449
486
|
};
|
|
450
|
-
const result = await this.productive(operation, async () => { await this.requireSettled(params); return await promptSessionWithBridge(this.bridge, { ...this.options(params), timeoutMs: request.request_timeout_ms }); }, request);
|
|
487
|
+
const result = await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await promptSessionWithBridge(this.bridge, { ...this.options(params), timeoutMs: request.request_timeout_ms }); }, request);
|
|
451
488
|
const session = this.sessions.get(result.provider_session_id); if (session) session.completed_turns = (session.completed_turns ?? 0) + 1;
|
|
452
489
|
await this.persist(); return result;
|
|
453
490
|
}
|
|
454
|
-
if (operation === "session.fork") return await this.productive(operation, async () => { await this.requireSettled(params); return await forkSessionWithBridge(this.bridge, this.options(params)); });
|
|
491
|
+
if (operation === "session.fork") return await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await forkSessionWithBridge(this.bridge, this.options(params)); });
|
|
455
492
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
456
493
|
const closed = this.sessions.get(params.sessionId)?.close_receipt;
|
|
457
494
|
if (closed) {
|
|
@@ -467,7 +504,8 @@ export class DaemonRuntime {
|
|
|
467
504
|
&& Array.isArray(receipt.topology.child_session_ids)) return receipt;
|
|
468
505
|
}
|
|
469
506
|
const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
|
|
470
|
-
const
|
|
507
|
+
const emptyRoot = this.neverPromptedRootProof(params.sessionId);
|
|
508
|
+
const observation = { ...closed, ...await observeClosedZcodeTree(this.bridge, params.sessionId, known, emptyRoot && closed.close?.closed === true ? {...emptyRoot,close_confirmed:true} : null), observed_at: new Date().toISOString() };
|
|
471
509
|
// Observations are not ownership inventory: their timestamps must not
|
|
472
510
|
// invalidate a drain fence on every read.
|
|
473
511
|
this.state.settlement_observations ??= {};
|
|
@@ -525,6 +563,20 @@ export class DaemonRuntime {
|
|
|
525
563
|
}
|
|
526
564
|
const options = this.options({ sessionId: session.provider_session_id, adapterSessionId: session.adapter_session_id });
|
|
527
565
|
let observed;
|
|
566
|
+
// A daemon-owned allocation with no productive dispatch cannot have
|
|
567
|
+
// children, and native ZCode has no durable read index for it yet. Close
|
|
568
|
+
// that resident directly; do not send cancellation for default stop.
|
|
569
|
+
const emptyRoot = !cancel && !this.activeProductive ? this.neverPromptedRootProof(session.provider_session_id) : null;
|
|
570
|
+
if (emptyRoot) {
|
|
571
|
+
const close = await this.bridge.request("zcode/session/close", { sessionId: session.adapter_session_id });
|
|
572
|
+
const observation = close?.closed === true
|
|
573
|
+
? await observeClosedZcodeTree(this.bridge, session.provider_session_id, [], { ...emptyRoot, close_confirmed: true })
|
|
574
|
+
: { settled: false, topology: { root_session_id: session.provider_session_id, child_session_ids: [], complete: false } };
|
|
575
|
+
this.track({ provider_session_id: session.provider_session_id, adapter_session_id: session.adapter_session_id, cancellation_requested: false, close_requested: true, close, ...observation });
|
|
576
|
+
await this.persist({ active_tree: observation.settled !== true });
|
|
577
|
+
if (observation.settled !== true) unsettled.push(session.provider_session_id);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
528
580
|
if (!cancel) observed = await inspectSessionWithBridge(this.bridge, options);
|
|
529
581
|
if (cancel || !observed || this.treeRunning(observed)) {
|
|
530
582
|
if (!cancel) unsettled.push(session.provider_session_id);
|