@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74
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/dist/build-info.json +3 -3
- package/dist/cli/help.js +2 -0
- package/dist/cli/run-cli.js +15 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
- package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
- package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
- package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
- package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
- package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
- package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/services/code-checks.js +120 -19
- package/dist/services/controller-fanout.js +11 -0
- package/dist/services/engines.js +40 -9
- package/dist/services/eval-snapshots.js +30 -47
- package/dist/services/harness-adapter.js +12 -6
- package/dist/services/lifecycle-invocations.js +12 -8
- package/dist/services/native-session-control.js +6 -0
- package/dist/services/projects.js +6 -2
- package/dist/services/recovery-observation-budget.js +9 -1
- package/dist/services/run-control-worker.js +19 -0
- package/dist/services/run-control.js +87 -13
- package/dist/services/run-controller-adapter.js +134 -7
- package/dist/services/run-controller.js +11 -3
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/runs.js +1 -11
- package/dist/services/schema-validation.js +1 -1
- package/dist/services/stage-lifecycle.js +14 -2
- package/dist/services/usage.js +20 -87
- package/dist/services/vnext-code-review.js +35 -27
- package/dist/services/vnext-code.js +62 -26
- package/dist/services/vnext-merge.js +8 -8
- package/dist/services/vnext-plan-review.js +16 -16
- package/dist/services/vnext-plan.js +51 -11
- package/dist/services/work-registry.js +48 -27
- package/dist/shared/errors.js +12 -0
- package/package.json +1 -1
|
@@ -23,10 +23,21 @@ import {
|
|
|
23
23
|
const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
|
|
24
24
|
const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
|
|
25
25
|
const STATE_SCHEMA = "dd-zcode/daemon-state@1";
|
|
26
|
-
//
|
|
27
|
-
// turn without emitting an ACP update. This is an idle deadline, not a
|
|
28
|
-
// wall-clock cap: every observed update resets it.
|
|
26
|
+
// ACP silence starts diagnostics; it is not a provider-failure deadline.
|
|
29
27
|
export const DEFAULT_LIVENESS_TIMEOUT_MS = 600_000;
|
|
28
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = null;
|
|
29
|
+
|
|
30
|
+
function positiveDuration(value, name, fallback) {
|
|
31
|
+
const duration = value ?? fallback;
|
|
32
|
+
if (!Number.isFinite(duration) || duration <= 0) throw new DaemonError("invalid_duration", `${name} must be a positive finite duration`, false, { name, value: duration });
|
|
33
|
+
return duration;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function optionalDuration(value, name, fallback) {
|
|
37
|
+
if (value === null) return null;
|
|
38
|
+
if (value === undefined && fallback === null) return null;
|
|
39
|
+
return positiveDuration(value, name, fallback);
|
|
40
|
+
}
|
|
30
41
|
|
|
31
42
|
class DaemonError extends Error {
|
|
32
43
|
constructor(code, message, retryable = false, details) {
|
|
@@ -114,7 +125,7 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
|
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
function sameConfig(state, config) {
|
|
117
|
-
return ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
128
|
+
return ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
export async function startDaemon(options) {
|
|
@@ -135,7 +146,8 @@ export async function startDaemon(options) {
|
|
|
135
146
|
projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
|
|
136
147
|
resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
137
148
|
env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {},
|
|
138
|
-
livenessTimeoutMs: options.livenessTimeoutMs
|
|
149
|
+
livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
|
|
150
|
+
requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
|
|
139
151
|
};
|
|
140
152
|
await prepareRuntimeOwner(config, paths.dir);
|
|
141
153
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 });
|
|
@@ -230,7 +242,96 @@ export class DaemonRuntime {
|
|
|
230
242
|
const known = this.sessions.get(params.sessionId);
|
|
231
243
|
const adapter = params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId;
|
|
232
244
|
const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
|
|
233
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
...this.state.config, ...params, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
|
|
247
|
+
daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
|
|
248
|
+
assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
|
|
249
|
+
// The bridge owns the quiet timer; the daemon owns the durable status.
|
|
250
|
+
// Do not infer provider state or cancel here.
|
|
251
|
+
onInactivity: async ({ requestId, sessionId, lastActivityAt, idleTimeoutMs }) => {
|
|
252
|
+
const request = this.state.active_request;
|
|
253
|
+
// `sessionId` is ACP identity; the durable request's `session_id` is
|
|
254
|
+
// native/provider identity. These strings can coincide but are not
|
|
255
|
+
// interchangeable.
|
|
256
|
+
if (!request || request.adapter_session_id !== sessionId) return;
|
|
257
|
+
const adapterSessionId = request.adapter_session_id;
|
|
258
|
+
let diagnostics;
|
|
259
|
+
try {
|
|
260
|
+
// Both are non-mutating native observations. In particular, do not
|
|
261
|
+
// inspect/resume a running child: some ZCode children are known to
|
|
262
|
+
// be non-resident and unsubscribable while their request is alive.
|
|
263
|
+
const [resident, topology] = await Promise.all([
|
|
264
|
+
this.bridge.request("zcode/session/resident", { sessionId: adapterSessionId }, 5_000),
|
|
265
|
+
this.bridge.request("zcode/session/retainedSubagents", { sessionId: adapterSessionId }, 5_000),
|
|
266
|
+
]);
|
|
267
|
+
diagnostics = {
|
|
268
|
+
observed_at: new Date().toISOString(), adapter_session_id: adapterSessionId,
|
|
269
|
+
resident: resident?.resident === true,
|
|
270
|
+
child_session_ids: zcodeChildSessionIds(topology?.topology ?? topology),
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
diagnostics = { observed_at: new Date().toISOString(), adapter_session_id: adapterSessionId, observation_error: errorPayload(error) };
|
|
274
|
+
}
|
|
275
|
+
// The probe is asynchronous. Do not recreate a completed or
|
|
276
|
+
// reconnected request with a stale callback snapshot.
|
|
277
|
+
const current = this.state.active_request;
|
|
278
|
+
if (!current || current.request_id !== request.request_id || current.adapter_session_id !== adapterSessionId || this.activeGeneration !== request.generation) return;
|
|
279
|
+
await this.persist({ active_request: {
|
|
280
|
+
...current,
|
|
281
|
+
observation: { state: "quiet", request_id: requestId, detected_at: new Date().toISOString(), last_activity_at: new Date(lastActivityAt).toISOString(), quiet_threshold_ms: idleTimeoutMs, diagnostics },
|
|
282
|
+
} });
|
|
283
|
+
},
|
|
284
|
+
// Native create publishes its identities before post-create work. Keep
|
|
285
|
+
// that allocation in the same durable request record so recovery can
|
|
286
|
+
// distinguish "no Session allocated" from "allocated, later step lost".
|
|
287
|
+
onSessionAliasAllocated: async (identity) => {
|
|
288
|
+
const current = this.state.active_request;
|
|
289
|
+
await this.persist({
|
|
290
|
+
active_request: {
|
|
291
|
+
...(current ?? {
|
|
292
|
+
request_id: randomUUID(),
|
|
293
|
+
session_id: params.sessionId ?? null,
|
|
294
|
+
adapter_session_id: identity.adapter_session_id,
|
|
295
|
+
generation: this.controlGeneration ?? 0,
|
|
296
|
+
dispatched_at: new Date().toISOString(),
|
|
297
|
+
}),
|
|
298
|
+
allocation: {
|
|
299
|
+
adapter_session_id: identity.adapter_session_id,
|
|
300
|
+
provider_session_id: null,
|
|
301
|
+
cwd: identity.cwd,
|
|
302
|
+
allocated_at: new Date().toISOString(),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
onSessionAllocated: async (identity) => {
|
|
308
|
+
const current = this.state.active_request;
|
|
309
|
+
// A late identity is evidence, not permission to resurrect a completed
|
|
310
|
+
// request or overwrite another operation's ownership.
|
|
311
|
+
this.track(identity);
|
|
312
|
+
if (!current || current.allocation?.adapter_session_id !== identity.adapter_session_id) {
|
|
313
|
+
await this.persist({});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await this.persist({
|
|
317
|
+
active_request: {
|
|
318
|
+
...(current ?? {
|
|
319
|
+
request_id: randomUUID(),
|
|
320
|
+
session_id: params.sessionId ?? null,
|
|
321
|
+
adapter_session_id: params.adapterSessionId ?? params.sessionId ?? null,
|
|
322
|
+
generation: this.controlGeneration ?? 0,
|
|
323
|
+
dispatched_at: new Date().toISOString(),
|
|
324
|
+
}),
|
|
325
|
+
allocation: {
|
|
326
|
+
adapter_session_id: identity.adapter_session_id,
|
|
327
|
+
provider_session_id: identity.provider_session_id,
|
|
328
|
+
cwd: identity.cwd,
|
|
329
|
+
allocated_at: new Date().toISOString(),
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
};
|
|
234
335
|
}
|
|
235
336
|
|
|
236
337
|
track(result, topology = result?.evidence?.subagents) {
|
|
@@ -265,19 +366,36 @@ export class DaemonRuntime {
|
|
|
265
366
|
return evidence?.read?.projection?.status !== "idle" || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
|
|
266
367
|
}
|
|
267
368
|
|
|
268
|
-
async productive(name, task) {
|
|
369
|
+
async productive(name, task, request = null) {
|
|
269
370
|
if (this.activeProductive) throw new DaemonError("operation_busy", `${this.activeProductive} is already running`, true);
|
|
270
371
|
this.activeProductive = name;
|
|
271
372
|
this.activeGeneration = this.controlGeneration ?? 0;
|
|
272
373
|
try {
|
|
273
|
-
await this.persist({ active_tree: true, active_operation: name });
|
|
374
|
+
await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
|
|
274
375
|
const result = await task();
|
|
275
376
|
this.track(result);
|
|
276
377
|
const running = this.treeRunning(result);
|
|
277
|
-
await this.persist({ active_tree: running, active_operation: null });
|
|
278
|
-
return result;
|
|
378
|
+
await this.persist({ active_tree: running, active_operation: null, active_request: null });
|
|
379
|
+
return { ...result, settled: !running };
|
|
279
380
|
} catch (error) {
|
|
280
|
-
|
|
381
|
+
// Keep an allocated identity visible until the controller seals the
|
|
382
|
+
// outcome. Clearing it here was the source of `provider_identity_unknown`
|
|
383
|
+
// after a late/partial create: the native Session existed, but the
|
|
384
|
+
// daemon state no longer carried the only correlation to it.
|
|
385
|
+
const active = this.state?.active_request;
|
|
386
|
+
try {
|
|
387
|
+
await this.persist({
|
|
388
|
+
active_operation: null,
|
|
389
|
+
active_request: active?.allocation
|
|
390
|
+
? { ...active, outcome: "unknown", error: errorPayload(error) }
|
|
391
|
+
: null,
|
|
392
|
+
});
|
|
393
|
+
} catch (cleanupError) {
|
|
394
|
+
// Preserve the operation's primary failure if durable cleanup itself
|
|
395
|
+
// is unavailable. The caller will retain the outcome as unknown and
|
|
396
|
+
// the next recovery pass can retry the state write.
|
|
397
|
+
if (error && typeof error === "object") error.cleanup_error = errorPayload(cleanupError);
|
|
398
|
+
}
|
|
281
399
|
throw error;
|
|
282
400
|
} finally { this.activeProductive = null; this.activeGeneration = null; }
|
|
283
401
|
}
|
|
@@ -303,7 +421,7 @@ export class DaemonRuntime {
|
|
|
303
421
|
cwd: this.state.cwd, journal: this.state.journal, versions: this.state.versions,
|
|
304
422
|
started_at: this.state.started_at, updated_at: this.state.updated_at,
|
|
305
423
|
shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status,
|
|
306
|
-
active_tree: this.state.active_tree, active_operation: this.activeProductive,
|
|
424
|
+
active_tree: this.state.active_tree, active_operation: this.activeProductive, active_request: this.state.active_request ?? null,
|
|
307
425
|
sessions: [...this.sessions.values()], config: this.state.config,
|
|
308
426
|
};
|
|
309
427
|
if (operation === "hook.resolve") {
|
|
@@ -312,10 +430,29 @@ export class DaemonRuntime {
|
|
|
312
430
|
if (!session?.root_provider_session_id) throw new DaemonError("hook_identity_unknown", "ZCode hook Session is not a tracked controlled Session", false, { session_id: sessionId });
|
|
313
431
|
return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} };
|
|
314
432
|
}
|
|
315
|
-
if (operation === "session.create")
|
|
433
|
+
if (operation === "session.create") {
|
|
434
|
+
const request = {
|
|
435
|
+
request_id: randomUUID(),
|
|
436
|
+
session_id: params.sessionId ?? null,
|
|
437
|
+
adapter_session_id: params.adapterSessionId ?? params.sessionId ?? null,
|
|
438
|
+
generation: this.controlGeneration ?? 0,
|
|
439
|
+
dispatched_at: new Date().toISOString(),
|
|
440
|
+
request_timeout_ms: this.state.config.requestTimeoutMs,
|
|
441
|
+
};
|
|
442
|
+
return await this.productive(operation, async () => await createSessionWithBridge(this.bridge, this.options(params), this.initialized), request);
|
|
443
|
+
}
|
|
316
444
|
if (operation === "session.prompt") {
|
|
317
445
|
if (this.sessions.get(params.sessionId)?.close_receipt) throw new DaemonError("session_closed", "Closed Session requires explicit recovery before another prompt");
|
|
318
|
-
const
|
|
446
|
+
const known = this.sessions.get(params.sessionId);
|
|
447
|
+
const request = {
|
|
448
|
+
request_id: randomUUID(),
|
|
449
|
+
session_id: params.sessionId,
|
|
450
|
+
adapter_session_id: params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId,
|
|
451
|
+
generation: this.controlGeneration ?? 0,
|
|
452
|
+
dispatched_at: new Date().toISOString(),
|
|
453
|
+
request_timeout_ms: this.state.config.requestTimeoutMs,
|
|
454
|
+
};
|
|
455
|
+
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);
|
|
319
456
|
const session = this.sessions.get(result.provider_session_id); if (session) session.completed_turns = (session.completed_turns ?? 0) + 1;
|
|
320
457
|
await this.persist(); return result;
|
|
321
458
|
}
|
|
@@ -327,6 +464,13 @@ export class DaemonRuntime {
|
|
|
327
464
|
const topology = closed.topology;
|
|
328
465
|
if (topology?.root_session_id !== params.sessionId || !Array.isArray(topology.child_session_ids) || topology.child_session_ids.some(id => typeof id !== "string" || !id)) return { ...closed, settled: false, observation_error: { code: "topology_incomplete", message: "closed Session has no native topology identity" } };
|
|
329
466
|
const previous = this.state.settlement_observations?.[params.sessionId];
|
|
467
|
+
// A confirmed closed tree cannot resume through this daemon. Retain
|
|
468
|
+
// that terminal evidence even after its bridge has been retired.
|
|
469
|
+
for (const receipt of [previous, closed]) {
|
|
470
|
+
if (receipt?.close?.closed === true && receipt.settled === true
|
|
471
|
+
&& receipt.topology?.complete === true && receipt.topology.root_session_id === params.sessionId
|
|
472
|
+
&& Array.isArray(receipt.topology.child_session_ids)) return receipt;
|
|
473
|
+
}
|
|
330
474
|
const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
|
|
331
475
|
const observation = { ...closed, ...await observeClosedZcodeTree(this.bridge, params.sessionId, known), observed_at: new Date().toISOString() };
|
|
332
476
|
// Observations are not ownership inventory: their timestamps must not
|
|
@@ -412,14 +556,36 @@ export class DaemonRuntime {
|
|
|
412
556
|
}
|
|
413
557
|
}
|
|
414
558
|
if (unsettled.length) throw new DaemonError("tree_not_settled", "daemon still owns running Sessions", false, { sessions: unsettled });
|
|
559
|
+
// A cooperative close can make the native Session non-resident while its
|
|
560
|
+
// original ACP prompt is still pending. For an explicitly whole-RUN
|
|
561
|
+
// cancellation this daemon owns the bridge process group, so retire that
|
|
562
|
+
// group now rather than reporting a clean-looking but still live tree.
|
|
563
|
+
// A per-child cancellation never takes this path.
|
|
564
|
+
let bridgeClosed = false;
|
|
565
|
+
if (this.activeProductive && cancel) {
|
|
566
|
+
try {
|
|
567
|
+
if (typeof this.bridge.close === "function") await this.bridge.close();
|
|
568
|
+
bridgeClosed = true;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
415
573
|
const deadline = Date.now() + 5000;
|
|
416
574
|
while (this.activeProductive && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
417
575
|
if (this.activeProductive) throw new DaemonError("tree_not_settled", "productive operation did not settle after cancellation");
|
|
418
|
-
|
|
419
|
-
|
|
576
|
+
// The owned bridge process group is the physical stop barrier. A native
|
|
577
|
+
// closed/resident receipt only proves API state and cannot seal recovery.
|
|
578
|
+
// A bridge without a close method occurs only in in-memory diagnostics: it
|
|
579
|
+
// owns no OS process boundary. Production AcpBridge always has one.
|
|
580
|
+
try { if (!bridgeClosed && typeof this.bridge.close === "function") await this.bridge.close(); }
|
|
581
|
+
catch (error) { throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false); }
|
|
582
|
+
await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null, physical_settlement: { confirmed_at: new Date().toISOString(), bridge_pid: this.bridge.child?.pid ?? null } });
|
|
583
|
+
return { daemon_id: this.state.daemon_id, stopped: true, clean: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
|
|
420
584
|
}
|
|
421
585
|
|
|
422
586
|
async shutdown() {
|
|
587
|
+
// prepareStop has already closed the owned bridge. Signal shutdown may get
|
|
588
|
+
// here directly, so close remains idempotent.
|
|
423
589
|
await this.bridge.close();
|
|
424
590
|
await this.persist({ active_tree: false, shutdown_state: "clean", stopped_at: new Date().toISOString() });
|
|
425
591
|
const resourceProcess = this.state.resource_process;
|