@deksden-com/dd-flow-cli 0.9.0-beta.29 → 0.9.0-beta.31
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 +14 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +39 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.31
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Treat an authoritative fully-idle Antigravity root Stop as settlement evidence
|
|
8
|
+
for otherwise unobserved native children, while preserving their distinct
|
|
9
|
+
`settled_by_root` status rather than claiming individual completion.
|
|
10
|
+
|
|
11
|
+
## 0.9.0-beta.30
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Bound Antigravity prompt liveness from the most recent native activity and preserve clean recovery settlement after an expired prompt.
|
|
16
|
+
|
|
3
17
|
## 0.9.0-beta.29
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.31",
|
|
4
|
+
"cli_commit": "f21c8317e762f978272536faff6822921dffeff5",
|
|
5
|
+
"built_at": "2026-09-07T16:20:23.716Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.6",
|
|
8
8
|
"commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
|
|
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import net from "node:net";
|
|
6
|
-
import {
|
|
6
|
+
import { ObservationClock } from "./observation-clock.mjs";
|
|
7
7
|
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { observeModel } from "./model-observations.mjs";
|
|
@@ -74,25 +74,33 @@ async function prepare(paths, config) {
|
|
|
74
74
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
75
75
|
const paths = locations(stateDir), request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
76
76
|
return await new Promise((resolve, reject) => {
|
|
77
|
-
const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, lastActivity = Date.now();
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
77
|
+
const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, checking = false, lastActivity = Date.now();
|
|
78
|
+
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(() => {}); } });
|
|
79
|
+
const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearInterval(timer); client.destroy(); error ? reject(error) : resolve(result); };
|
|
80
|
+
const poll = Math.min(5_000, Math.max(25, Math.floor(timeoutMs / 20)));
|
|
81
|
+
timer = setInterval(async () => {
|
|
82
|
+
if (settled || checking) return;
|
|
83
|
+
checking = true;
|
|
84
84
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
if (operation !== "session.prompt") {
|
|
86
|
+
if (clock.sample()) finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let status = null;
|
|
90
|
+
try { status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000)); }
|
|
91
|
+
catch { /* The original RPC retains the authoritative failure. */ }
|
|
92
|
+
const observed = Date.parse(status?.last_activity_at ?? "");
|
|
93
|
+
const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
|
|
94
|
+
if (progressed) lastActivity = observed;
|
|
95
|
+
if (!clock.sample(progressed ? lastActivity : undefined)) return;
|
|
96
|
+
finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
|
|
97
|
+
operation_id: request.id,
|
|
98
|
+
last_activity_at: status?.last_activity_at ?? null,
|
|
99
|
+
active_tree: status?.active_tree === true
|
|
100
|
+
}));
|
|
101
|
+
} finally { checking = false; }
|
|
102
|
+
}, poll);
|
|
103
|
+
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)));
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
106
|
|
|
@@ -214,13 +222,17 @@ export class Runtime {
|
|
|
214
222
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
215
223
|
this.lastResult = result; this.markActivity();
|
|
216
224
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
217
|
-
|
|
225
|
+
const pendingTerminal = current && result.status === "SUCCESS";
|
|
226
|
+
if (pendingTerminal) current.terminalResult = result;
|
|
227
|
+
if (pendingTerminal && rootStop?.fullyIdle === true) 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" });
|
|
228
|
+
const terminalReceipt = pendingTerminal ? this.receipt(result) : null;
|
|
229
|
+
await this.persist({ active_tree: pendingTerminal ? !terminalReceipt.settled : result.status === "RUNNING" || rootStop?.fullyIdle === false, last_result: result });
|
|
218
230
|
try { this.usageIngest = await forwardUsage(this.config, result.conversation_id ?? this.init.conversation_id, result, this.toolSnapshot(), [...this.descendants.values()]); }
|
|
219
231
|
catch (error) { this.usageIngest = { status: "failed", error: error.message ?? String(error) }; await this.journal("usage_ingest_failed", this.usageIngest); }
|
|
220
232
|
if (!current) return;
|
|
221
233
|
if (this.active !== current) return;
|
|
222
|
-
this.active = null;
|
|
223
234
|
if (result.status === "ERROR" || result.status === "CANCELLED") {
|
|
235
|
+
this.active = null;
|
|
224
236
|
const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
|
|
225
237
|
const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
|
|
226
238
|
: /rate.?limit|too many requests|\b429\b/.test(normalized) ? "agy_provider_rate_limited"
|
|
@@ -228,17 +240,18 @@ export class Runtime {
|
|
|
228
240
|
: "agy_provider_failed";
|
|
229
241
|
return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
230
242
|
}
|
|
231
|
-
current.resolve(
|
|
243
|
+
if (terminalReceipt.settled) { this.active = null; current.resolve(terminalReceipt); }
|
|
232
244
|
}
|
|
233
245
|
receipt(result = this.lastResult) {
|
|
234
246
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
235
247
|
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
236
|
-
const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled"].includes(child.status));
|
|
248
|
+
const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
|
|
237
249
|
// A terminal root result is the provider's authoritative end-of-turn
|
|
238
250
|
// observation when AGY does not emit a root Stop hook. A received
|
|
239
251
|
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
240
252
|
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
241
|
-
|
|
253
|
+
const terminalPending = this.active?.terminalResult === result;
|
|
254
|
+
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.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
242
255
|
}
|
|
243
256
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
244
257
|
async prompt(text, assertDispatch = captureDispatchGuard(this)) { if (!text) throw new DaemonError("prompt_required", "prompt is required"); await this.start(); await this.draining; if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running"); if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled"); const root = this.init?.conversation_id; const prior = this.sessionObservations.get(root) ?? {}; this.turnGeneration++; this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null }); this.lastProviderEvent = null; await this.persist({ active_tree: true }); return await new Promise((resolve, reject) => { assertDispatch(); this.active = { resolve, reject, generation: this.turnGeneration, previousError: this.lastResult?.error ?? null }; this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => { if (error) { this.active = null; reject(error); } }); }); }
|
|
@@ -250,10 +263,10 @@ export class Runtime {
|
|
|
250
263
|
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
251
264
|
return child;
|
|
252
265
|
}
|
|
253
|
-
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); 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; 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, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); const parent = child?.parent_provider_session_id ?? null;
|
|
266
|
+
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); 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; 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, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); 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 settled = terminalPending ? this.receipt(this.lastResult) : null; const completed = settled?.settled ? this.active : null; if (completed) this.active = null; const parent = child?.parent_provider_session_id ?? null;
|
|
254
267
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
255
268
|
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" });
|
|
256
|
-
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, 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 }); return { event_id: eventId, daemon_id: this.config.daemonId, 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 };
|
|
269
|
+
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, 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(completed ? { active_tree: false } : {}); 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 }); completed?.resolve(settled); return { event_id: eventId, daemon_id: this.config.daemonId, 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 };
|
|
257
270
|
}
|
|
258
271
|
async cancel() { cancelPendingDispatch(this); if (!this.child || this.exited) return { ...this.receipt(), settled: Boolean(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.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
259
272
|
}
|