@deksden-com/dd-flow-cli 0.9.0-beta.141 → 0.9.0-beta.143

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,22 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.143
4
+
5
+ ### Patch Changes
6
+
7
+ - 60daf29: Keep an early AGY provider rejection as the readiness failure rather than a stream-drain cleanup failure. Preserve strict journal and physical cleanup errors.
8
+ - d86445c: Observe accepted Codex Turns while persisted history is unavailable, preserving exact native outcomes, cancellation identity and conservative tree settlement. Keep RPC errors intact in compact journals.
9
+
10
+ Treat successful metadata-only history responses as incomplete in cancellation and final result admission; require exact current Turn item evidence before bypassing retained-output/profile safeguards. Reject incomplete cancellation records and terminal settlement without a valid Turn identity. Prefer native completed output over stale snapshots of the same Turn.
11
+
12
+ ## 0.9.0-beta.142
13
+
14
+ ### Patch Changes
15
+
16
+ - Wait for the owned OpenCode v2 listener through typed SDK transport startup failures and expose bounded read-only scope inventory batches for validated retirement-proof reuse.
17
+ - Retry no-effect physical confirmation on the same held check, bootstrap or service process before dispatch, without replaying commands or extending exhausted authority.
18
+ - Backfilled from published artifact commit `19ca01d6`: Keep native Session observation stable, use bounded shared Codex read phases, scope check evidence to declared inputs, retain accepted intake/HITL review sources, and reject stale review coverage without productive replay. These changes were already shipped; beta.143 does not count them as new work.
19
+
3
20
  ## 0.9.0-beta.141
4
21
 
5
22
  ### Patch Changes
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.141",
4
- "cli_commit": "c370c6e9beaefba2216bbf5bbadac02e4827768f",
5
- "built_at": "2026-10-17T22:37:48.634Z",
3
+ "cli_version": "0.9.0-beta.143",
4
+ "cli_commit": "2294710b9c9b678193798301d5a81048506d672d",
5
+ "built_at": "2026-10-20T02:52:47.825Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.2",
8
- "commit": "1ddedf9d3f086e046f8e7b194c739ec386e7c8b9",
8
+ "commit": "a1cfe8b06329c691c314064b6ea6a3da6628cceb",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
- "repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp231-release",
11
- "memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp231-release/.memory-bank",
12
- "flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp231-release/.memory-bank/dd-flow",
10
+ "repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-plan092",
11
+ "memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-plan092/.memory-bank",
12
+ "flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-plan092/.memory-bank/dd-flow",
13
13
  "layout": "dot_memory_bank"
14
14
  }
15
15
  }
@@ -156,7 +156,7 @@ export const commandInputs = {
156
156
  "run control pause": route(0, `${control} request-id`, "grace-ms wait-ms escalate", "force"),
157
157
  "run control stop": route(0, `${control} request-id`, "grace-ms wait-ms escalate", "force"),
158
158
  "runtime scope fence": route(0, "scope-id request-id"),
159
- "runtime scope status": route(0, "scope-id"),
159
+ "runtime scope status": route(0, "", "scope-id scope-ids-json"),
160
160
  "runtime scope stop": route(0, "scope-id request-id"),
161
161
  "runtime scope control": route(0, "scope-id request-id mode", "manifest-json", "", "", { mode: ["pause", "stop"] }),
162
162
  "runtime scope serve": route(0, "scope-id generation"),
@@ -143,6 +143,7 @@ export const runtimeOptionDescriptions = {
143
143
  schema: "Registered JSON schema name against which the supplied file is validated.",
144
144
  "schema-dir": "Explicit schema directory for validation; not a workflow handler registry or permission to execute code.",
145
145
  "scope-id": "Exact retained runtime/EVAL scope identity; operations must not affect neighboring scopes.",
146
+ "scope-ids-json": "Read-only JSON array of 1..16 unique retained scope IDs; returns ordered fresh inventories without fencing or stopping them.",
146
147
  "semantic-file": "Stage semantic result artifact; applicable schema and finish checks are selected independently.",
147
148
  session: "Physical Session selector for source-backed usage aggregation.",
148
149
  "session-id": "Exact retained physical Session identity for registration, status, stop or usage synchronization.",
package/dist/cli/help.js CHANGED
@@ -169,6 +169,7 @@ never registers a project, selects a model automatically or creates a runtime DB
169
169
  ["runtime scope", `Usage:
170
170
  dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID --json
171
171
  dd-flow runtime scope status --scope-id EVAL_ID --json
172
+ dd-flow runtime scope status --scope-ids-json '["EVAL_ID","QUAL_ID"]' --json
172
173
  dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID --json
173
174
  dd-flow runtime scope control --scope-id EVAL_ID --request-id ID --mode pause|stop [--manifest-json JSON] --json
174
175
  dd-flow runtime scope resume --scope-id EVAL_ID --request-id ID --generation N --capture-key SHA256 --json
@@ -92,7 +92,7 @@ import { prepareCommandRoute } from "./command-inputs.js";
92
92
  import { parseLifecycleCommand } from "../services/lifecycle-command.js";
93
93
  import { dispatchRuntimeProcess, prepareRuntimeProcess } from "./runtime-process.js";
94
94
  import { reconcileFlowRunHealth } from "../services/run-health.js";
95
- import { fenceRuntimeBudget, runtimeBudgetStatus, prepareRuntimeScopeControl, prepareRuntimeScopeFence } from "../services/runtime-budget.js";
95
+ import { fenceRuntimeBudget, runtimeBudgetStatus, prepareRuntimeScopeStatus, prepareRuntimeScopeControl, prepareRuntimeScopeFence } from "../services/runtime-budget.js";
96
96
  import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
97
97
  import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
98
98
  import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
@@ -1017,6 +1017,8 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
1017
1017
  return prepareError(error, { parameter: "from/output" });
1018
1018
  }
1019
1019
  }
1020
+ if (route.key === "runtime scope status")
1021
+ prepareRuntimeScopeStatus(optionalOption(parsed, "scope-id"), optionalOption(parsed, "scope-ids-json"));
1020
1022
  if (["runtime scope fence", "runtime scope stop"].includes(route.key))
1021
1023
  prepareRuntimeScopeFence(requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"));
1022
1024
  if (route.key === "runtime scope resume")
@@ -2778,8 +2780,12 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
2778
2780
  return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
2779
2781
  }
2780
2782
  if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
2781
- if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
2782
- throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
2783
+ if ([...parsed.options.keys()].some(key => !["scope-id", "scope-ids-json"].includes(key)) || parsed.options.size !== 1 || [...parsed.options.values()].some(values => values.length !== 1))
2784
+ throw new AppError("usage", "Use exactly one of --scope-id or --scope-ids-json", 2);
2785
+ const ids = prepareRuntimeScopeStatus(optionalOption(parsed, "scope-id"), optionalOption(parsed, "scope-ids-json"));
2786
+ if (parsed.options.has("scope-ids-json")) {
2787
+ return { ok: true, scopes: ids.map(id => runtimeBudgetStatus(context, id)) };
2788
+ }
2783
2789
  return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
2784
2790
  }
2785
2791
  if (parsed.positional.length !== 1 || parsed.positional[0] !== "fence" || [...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
@@ -294,7 +294,7 @@ export class Runtime {
294
294
  let allowProviderFinalization;
295
295
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
296
296
  const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
297
- this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve); }, observationFailure);
297
+ this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve, readyReject); }, observationFailure);
298
298
  this.diagnostics = streamNativeDiagnostics(this.child.stderr, chunk => this.journal("stderr", { text: chunk }), observationFailure);
299
299
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
300
300
  // A dead leader is not yet a finalized tree: stop must also join the
@@ -329,7 +329,7 @@ export class Runtime {
329
329
  try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), SETUP_STARTUP_MS, { progress: () => this.lastActivityAt }); })]); }
330
330
  finally { clearTimeout(readyTimer); }
331
331
  }
332
- async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
332
+ async drain(readyResolve, readyReject) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
333
333
  // Record an early provider rejection before journalling: a short-lived
334
334
  // child can close while the asynchronous write is still pending.
335
335
  if (event.event === "result" && ["ERROR", "CANCELLED"].includes(event.result?.status)) {
@@ -344,7 +344,12 @@ export class Runtime {
344
344
  }
345
345
  this.observeProviderActivity(event);
346
346
  if (event.event === "result" && this.init && !this.startupError) { await this.finishTurn(event.result ?? {}, event.terminal_metadata, event); continue; }
347
- 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 ?? {}); } } }
347
+ 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") {
348
+ // A successfully read/journalled provider refusal rejects readiness, not
349
+ // the stream drain that physical cleanup must still join.
350
+ if (this.startupError) { readyReject(this.startupError); continue; }
351
+ await this.finishTurn(event.result ?? {});
352
+ } } }
348
353
  observeStep(step) {
349
354
  const sessionId = step.conversation_id ?? this.init?.conversation_id, observation = this.sessionObservations.get(sessionId) ?? {};
350
355
  const fresh = Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1) && step.step_index > (observation.step_floor ?? -1);
@@ -14,7 +14,7 @@ import path from "node:path";
14
14
  import { spawn } from "node:child_process";
15
15
  import { waitForSettlement } from "./session-settlement.mjs";
16
16
  import { errorRecord } from "./operation-errors.mjs";
17
- import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
17
+ import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, codexReadPhase, readCodexHistory, retainedCodexTurn, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
18
18
  import { assertDaemonOwnership, assertStartupChild, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
19
19
 
20
20
  export function codexBridgeEnv(config, daemonId) { return { ...config.env, ...managedFlowEnv(config, daemonId) }; }
@@ -28,10 +28,11 @@ async function readState(file) { try { return JSON.parse(await readFile(file, "u
28
28
  async function writeState(file, value) { const temp = `${file}.${process.pid}.tmp`; await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temp, file); }
29
29
  async function removeSocket(socket) { try { const info = await lstat(socket); if (!info.isSocket()) throw Object.assign(new Error("refusing to remove non-socket daemon path"), { code: "unsafe_socket" }); await unlink(socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
30
30
  const errorPayload = errorRecord;
31
- const terminalTurn = turn => ["completed", "interrupted", "failed", "cancelled"].includes(turn?.status);
32
- export function settledCodexSession(status, latestTurn, active) {
31
+ const terminalTurn = turn => typeof turn?.id === "string" && turn.id.trim().length > 0 && ["completed", "interrupted", "failed", "cancelled"].includes(turn.status);
32
+ export function settledCodexSession(status, latestTurn, active, historyAvailable = true) {
33
+ if (latestTurn && !terminalTurn(latestTurn)) return false;
33
34
  if (status === "idle") return true;
34
- if (status === "notLoaded") return !latestTurn || terminalTurn(latestTurn);
35
+ if (status === "notLoaded") return terminalTurn(latestTurn) || historyAvailable && !latestTurn;
35
36
  // `systemError` alone is not evidence that the provider stopped. A current
36
37
  // terminal Turn plus no daemon operation is the narrow settled case.
37
38
  return status === "systemError" && !active && terminalTurn(latestTurn);
@@ -49,26 +50,46 @@ async function observeCodexSessions(bridge, options, sessions, active, remember
49
50
  const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined });
50
51
  for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
51
52
  const status = observed.status?.type ?? observed.status;
52
- const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
53
- const settled = settledCodexSession(status, stored?.data?.[0] ?? null, active());
53
+ const currentId = bridge.currentTurns?.get(sessionId) ?? (sessionId === options.sessionId ? options.turnId : undefined);
54
+ const retained = retainedCodexTurn(bridge, sessionId, currentId);
55
+ const compactTurn = observed.thread?.turns?.findLast(turn => !currentId || turn?.id === currentId);
56
+ const stored = ["notLoaded", "systemError"].includes(status) || currentId && !terminalTurn(retained)
57
+ ? await readCodexHistory(bridge, "thread/turns/list", { threadId: sessionId, limit: 20, sortDirection: "desc", itemsView: "summary" }, options.readPhase) : null;
58
+ const hydrated = stored && !stored.available
59
+ ? await readCodexHistory(bridge, "thread/read", { threadId: sessionId, includeTurns: true }, options.readPhase) : null;
60
+ const hydratedAvailable = hydrated?.available && Array.isArray(hydrated.result.thread.turns);
61
+ const hydratedTurn = hydratedAvailable ? hydrated.result.thread.turns.findLast(turn => !currentId || turn?.id === currentId) : null;
62
+ const malformedHistory = [stored?.result?.data, hydrated?.result?.thread.turns, observed.thread?.turns].some(turns =>
63
+ turns?.some(turn => typeof turn?.id !== "string" || !turn.id.trim() || !["inProgress", "completed", "interrupted", "failed", "cancelled"].includes(turn.status)));
64
+ const latest = retained && terminalTurn(retained) ? retained
65
+ : malformedHistory ? { status: "unknown" }
66
+ : stored?.result?.data.find(turn => !currentId || turn?.id === currentId) ?? hydratedTurn ?? compactTurn ?? retained
67
+ ?? (currentId ? { id: currentId, status: "unknown" } : null);
68
+ const observedStatus = hydrated?.result?.thread.status ?? observed.status;
69
+ const settled = settledCodexSession(observedStatus?.type ?? observedStatus, latest, active(), (stored?.available ?? true) || hydratedAvailable);
54
70
  if (!settled) unsettled.push(sessionId);
55
- observations.push({ ...observed, settled });
71
+ observations.push({ ...observed, status: observedStatus, settled });
56
72
  }
57
73
  return { sessions: unsettled.reverse(), active: Boolean(active()), observations };
58
74
  }
59
75
 
60
76
  export async function inspectCodexTree(bridge, options, { remember = () => {}, active = () => null } = {}) {
77
+ options = { ...options, readPhase: options.readPhase ?? codexReadPhase(bridge, options) };
61
78
  const sessions = new Set([options.sessionId]);
62
79
  const observed = await observeCodexSessions(bridge, options, sessions, active, remember);
63
80
  return { ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
64
81
  }
65
82
 
66
83
  export async function cancelCodexTree(bridge, options, { remember = () => {}, active = () => null, timeoutMs = 5_000 } = {}) {
84
+ // Zero requests one best-effort cancellation/observation pass, not an
85
+ // invalid zero-length RPC window or permission to skip owned children.
86
+ const readBudget = timeoutMs === 0 ? CODEX_REQUEST_TIMEOUT_MS : timeoutMs;
87
+ options = { ...options, readPhase: options.readPhase ?? codexReadPhase(bridge, { ...options, timeoutMs: readBudget, livenessTimeoutMs: readBudget }) };
67
88
  const sessions = new Set([options.sessionId]);
68
89
  const settlement = await waitForSettlement({
69
90
  timeoutMs, knownSessions: () => [...sessions],
70
91
  cancel: async sessionId => {
71
- try { await cancelSessionWithBridge(bridge, { ...options, sessionId, turnId: null }); }
92
+ try { await cancelSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined }); }
72
93
  catch (error) { if (error.code !== "turn_not_active") throw error; }
73
94
  },
74
95
  observe: () => observeCodexSessions(bridge, options, sessions, active, remember)