@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 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
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.29",
4
- "cli_commit": "8b0e5fe4a8991172c6d6d5f0963a0adf5bc2228d",
5
- "built_at": "2026-09-07T13:07:57.099Z",
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 { observedTimeout } from "./observation-clock.mjs";
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 finish = (error, result) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(result); };
79
- const arm = () => { timer = observedTimeout(async () => {
80
- // Prompt RPCs return only at the end. Native events update this durable
81
- // timestamp, so a moving timestamp proves productive provider activity.
82
- if (operation !== "session.prompt") return finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
83
- let status = null;
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
- status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000));
86
- const observed = Date.parse(status.last_activity_at ?? "");
87
- if (status.active_tree && Number.isFinite(observed) && observed > lastActivity) { lastActivity = observed; return arm(); }
88
- } catch { /* The original RPC retains the authoritative failure. */ }
89
- finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
90
- operation_id: request.id,
91
- last_activity_at: status?.last_activity_at ?? null,
92
- active_tree: status?.active_tree === true
93
- }));
94
- }, timeoutMs, { onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } }); };
95
- arm(); 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)));
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
- await this.persist({ active_tree: result.status === "RUNNING" || rootStop?.fullyIdle === false, last_result: result });
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(this.receipt(result));
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
- 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 && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.29",
3
+ "version": "0.9.0-beta.31",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {