@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
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 +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -11,9 +11,9 @@ import net from "node:net";
|
|
|
11
11
|
import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { assertStartupChild, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
14
|
-
import { acceptNativeHookRequest,
|
|
15
|
-
import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
|
|
16
|
-
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
14
|
+
import { acceptNativeHookRequest, readNativeHookFailures, writeNativeHookFailure } from "./native-hook-command.mjs";
|
|
15
|
+
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
16
|
+
import { errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
AcpBridge,
|
|
@@ -225,7 +225,7 @@ export class DaemonRuntime {
|
|
|
225
225
|
const adapter = params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId;
|
|
226
226
|
const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
|
|
227
227
|
return {
|
|
228
|
-
...this.state.config, ...params, operationId: this.state.active_request?.request_id, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
|
|
228
|
+
...this.state.config, ...params, operationId: this.state.active_request?.request_id, nativeHookOwner: this.nativeHookOperation ?? null, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
|
|
229
229
|
neverPromptedRootProof: this.neverPromptedRootProof(params.sessionId),
|
|
230
230
|
daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
|
|
231
231
|
assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
|
|
@@ -369,7 +369,8 @@ export class DaemonRuntime {
|
|
|
369
369
|
if (this.activeProductive) throw new DaemonError("operation_busy", `${this.activeProductive} is already running`, true);
|
|
370
370
|
this.activeProductive = name;
|
|
371
371
|
this.activeGeneration = this.controlGeneration ?? 0;
|
|
372
|
-
const current = this.nativeHookOperation = { operationId: request?.operation_id, generation: this.activeGeneration, requests: new Map() };
|
|
372
|
+
const current = this.nativeHookOperation = { operationId: request?.operation_id, rootProviderSessionId: request?.session_id, generation: this.activeGeneration, requests: new Map() };
|
|
373
|
+
current.valid = () => current.generation === (this.controlGeneration ?? 0);
|
|
373
374
|
let rejectHook;
|
|
374
375
|
const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
|
|
375
376
|
void hookFailed.catch(() => {}); // A hook can fail while the dispatch receipt is still persisting.
|
|
@@ -379,6 +380,12 @@ export class DaemonRuntime {
|
|
|
379
380
|
if (name === "session.prompt") this.state.last_native_failure = null;
|
|
380
381
|
await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
|
|
381
382
|
const result = await Promise.race([task(), hookFailed]);
|
|
383
|
+
// The native tool cannot finish before its hook returns. Drain any
|
|
384
|
+
// already-written failure before discarding this operation's requests.
|
|
385
|
+
await this.observeHookFailures(current);
|
|
386
|
+
if (current.error) throw current.error;
|
|
387
|
+
const hookDiagnostics = (this.bridge.lifecycleDiagnostics ?? []).filter(item => item.operation_id === current.operationId && item.generation === current.generation);
|
|
388
|
+
if (hookDiagnostics.length) result.native_hook_diagnostics = [...new Map([...(result.native_hook_diagnostics ?? []), ...hookDiagnostics].map(item => [JSON.stringify(item), item])).values()];
|
|
382
389
|
this.track(result);
|
|
383
390
|
const running = this.treeRunning(result);
|
|
384
391
|
await this.persist({ active_tree: running, active_operation: null, active_request: null });
|
|
@@ -393,12 +400,12 @@ export class DaemonRuntime {
|
|
|
393
400
|
if (name === "session.prompt" && response?.method === "session/prompt" && error.details.provider_session_id)
|
|
394
401
|
this.state.last_native_failure = { provider_session_id: error.details.provider_session_id, request_id: response.request_id };
|
|
395
402
|
try {
|
|
396
|
-
await
|
|
403
|
+
await this.persist({
|
|
397
404
|
active_operation: null,
|
|
398
405
|
active_request: active?.allocation
|
|
399
406
|
? { ...active, outcome: "unknown", error: errorPayload(error) }
|
|
400
407
|
: null,
|
|
401
|
-
})
|
|
408
|
+
});
|
|
402
409
|
} catch (cleanupError) {
|
|
403
410
|
// Preserve the operation's primary failure if durable cleanup itself
|
|
404
411
|
// is unavailable. The caller will retain the outcome as unknown and
|
|
@@ -406,15 +413,50 @@ export class DaemonRuntime {
|
|
|
406
413
|
if (error && typeof error === "object") error.cleanup_error = errorPayload(cleanupError);
|
|
407
414
|
}
|
|
408
415
|
throw error;
|
|
409
|
-
} finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.activeProductive = null; this.activeGeneration = null; }
|
|
416
|
+
} finally { clearInterval(timer); if (!current.retainNativeHooks) current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.activeProductive = null; this.activeGeneration = null; }
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
async observeHookFailures(current = this.nativeHookOperation) {
|
|
413
|
-
if (!current || this.nativeHookOperation !== current
|
|
414
|
-
current.reading
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
}
|
|
420
|
+
if (!current || this.nativeHookOperation !== current) return;
|
|
421
|
+
while (current.reading) {
|
|
422
|
+
await current.reading;
|
|
423
|
+
if (this.nativeHookOperation !== current) return;
|
|
424
|
+
}
|
|
425
|
+
const reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.acceptHookFailure(current, error));
|
|
426
|
+
current.reading = reading;
|
|
427
|
+
try { await reading; } finally { if (current.reading === reading) current.reading = null; }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
acceptHookFailure(current, error) {
|
|
431
|
+
if (this.nativeHookOperation !== current) return;
|
|
432
|
+
const unconfirmed = error.details?.native_hook_binding?.native_creator === "unconfirmed";
|
|
433
|
+
if (!unconfirmed && isProvenShellCorrection(error)) return;
|
|
434
|
+
if (unconfirmed || isNativeHookObservationLoss(error)) {
|
|
435
|
+
const diagnostic = { ...errorRecord(error), session_id: this.state.active_request?.adapter_session_id ?? null,
|
|
436
|
+
operation_id: current.operationId, generation: current.generation, delivery_stage: "hook_observation", completeness: "incomplete" };
|
|
437
|
+
this.bridge.lifecycleDiagnostics ??= [];
|
|
438
|
+
this.bridge.lifecycleDiagnostics.push(diagnostic);
|
|
439
|
+
try { void Promise.resolve(this.bridge.journal?.write("native_hook_observation_failure", diagnostic)).catch(() => {}); } catch { /* diagnostic is optional */ }
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
current.reject(error);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
hookOwner(binding) {
|
|
446
|
+
if (!binding?.request_id) return null;
|
|
447
|
+
const turns = this.bridge.nativeHookOwnership?.turns?.values() ?? [];
|
|
448
|
+
const owners = new Set([this.nativeHookOperation, ...[...turns].filter(turn => !turn.invalid).map(turn => turn.owner)]);
|
|
449
|
+
for (const owner of owners) {
|
|
450
|
+
if (!owner || owner.operationId !== binding.operation_id || owner.generation !== binding.turn_generation || owner.generation !== (this.controlGeneration ?? 0)) continue;
|
|
451
|
+
const accepted = owner.requests.get(binding.request_id);
|
|
452
|
+
if (accepted?.current !== owner || JSON.stringify(accepted.binding) !== JSON.stringify(binding)) continue;
|
|
453
|
+
if (binding.source_harness === "zcode-acp") {
|
|
454
|
+
const proof = this.bridge.nativeHookOwnership?.fact(binding.provider_session_id, binding.tool_call_id, binding.native_command);
|
|
455
|
+
if (!proof || proof.owner !== owner || proof.parent !== binding.parent_provider_session_id) continue;
|
|
456
|
+
}
|
|
457
|
+
return owner;
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
418
460
|
}
|
|
419
461
|
|
|
420
462
|
async requireSettled(params) {
|
|
@@ -491,23 +533,67 @@ export class DaemonRuntime {
|
|
|
491
533
|
};
|
|
492
534
|
if (operation === "hook.resolve") {
|
|
493
535
|
const sessionId = String(params.sessionId ?? "");
|
|
494
|
-
const
|
|
536
|
+
const payload = params.payload, command = payload?.tool_input?.command ?? payload?.tool_input?.cmd;
|
|
537
|
+
const tool = payload?.tool_use_id ?? payload?.tool_call_id;
|
|
538
|
+
const source = this.bridge.nativeHookOwnership;
|
|
539
|
+
let proof = source?.fact(sessionId, tool, command);
|
|
540
|
+
const tracked = this.sessions.get(sessionId);
|
|
541
|
+
if (!proof && (!tracked || tracked.parent_provider_session_id) && source && tool && typeof command === "string") {
|
|
542
|
+
await source.wait(sessionId, tool, command, params.hookDeadlineAt);
|
|
543
|
+
proof = source.fact(sessionId, tool, command);
|
|
544
|
+
}
|
|
545
|
+
const session = proof ? { root_provider_session_id: proof.root, parent_provider_session_id: proof.parent } : tracked;
|
|
495
546
|
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 });
|
|
496
547
|
const identity = { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} };
|
|
497
|
-
const payload = params.payload, command = payload?.tool_input?.command ?? payload?.tool_input?.cmd;
|
|
498
548
|
if (payload?.hook_event_name === "PreToolUse" && typeof command === "string" && (["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) || continuationCommand(command))) {
|
|
499
|
-
|
|
500
|
-
|
|
549
|
+
// A retained child can only be attributed through the exact observed
|
|
550
|
+
// native turn and tool. Otherwise preserve the legacy handler without
|
|
551
|
+
// guessing that the currently active prompt created it.
|
|
552
|
+
if (sessionId !== session.root_provider_session_id && !proof) return identity;
|
|
553
|
+
const current = proof?.owner ?? this.nativeHookOperation, active = this.state.active_request;
|
|
501
554
|
const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === active?.adapter_session_id);
|
|
502
555
|
const root = active?.session_id ?? active?.allocation?.provider_session_id;
|
|
503
|
-
if (!current ||
|
|
504
|
-
|
|
505
|
-
|
|
556
|
+
if (!current || current.generation !== (this.controlGeneration ?? 0) || !tool || !proof && (!active || !pending || session.root_provider_session_id !== root)) throw new DaemonError("native_hook_unproven", "Native hook has no exact owned Session operation");
|
|
557
|
+
assertDaemonDispatch(this.paths.dir);
|
|
558
|
+
const nativePayload = proof ? { ...proof.message, _meta: { ...(proof.message._meta ?? {}), ddZcode: { daemonId: this.state.daemon_id, rootProviderSessionId: proof.root, parentProviderSessionId: proof.parent } } } : null;
|
|
559
|
+
const binding = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: session.root_provider_session_id, parent_provider_session_id: session.parent_provider_session_id ?? null, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null, native_creator: proof ? "confirmed" : "unconfirmed", ...(proof ? { source_harness: "zcode-acp", native_command: command, native_payload: nativePayload } : {}) }, current);
|
|
560
|
+
if (current.generation !== (this.controlGeneration ?? 0) || proof && source.fact(sessionId, tool, command) !== proof || !proof && this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
|
|
506
561
|
current.requests.set(binding.request_id, { current, binding }); identity.nativeHookBinding = binding;
|
|
562
|
+
const parsed = parseLifecycleCommand(command);
|
|
563
|
+
if (proof && parsed.kind === "standalone" && parsed.invocation.operation === "work_start" && commandOption(parsed.invocation, "invocation-id")) identity.fastAck = true;
|
|
507
564
|
}
|
|
508
565
|
return identity;
|
|
509
566
|
}
|
|
510
|
-
if (operation === "hook.
|
|
567
|
+
if (operation === "hook.verify") {
|
|
568
|
+
if (params.daemonId !== this.state.daemon_id) throw new DaemonError("native_hook_unproven", "Native hook daemon identity differs");
|
|
569
|
+
const owner = this.hookOwner(params.binding);
|
|
570
|
+
if (!owner) throw new DaemonError("native_hook_unproven", "Native hook has no matching retained operation");
|
|
571
|
+
assertDaemonDispatch(this.paths.dir);
|
|
572
|
+
return { verified: true, nativeHookBinding: params.binding };
|
|
573
|
+
}
|
|
574
|
+
if (operation === "hook.failed") {
|
|
575
|
+
if (params.binding !== undefined || params.failure !== undefined) {
|
|
576
|
+
const binding = params.binding, failure = params.failure, current = this.hookOwner(binding);
|
|
577
|
+
// Join an in-flight file observation before checking the request. A
|
|
578
|
+
// direct fallback and its receipt must not be delivered twice.
|
|
579
|
+
while (current?.reading) await current.reading;
|
|
580
|
+
const accepted = current?.requests.get(binding?.request_id);
|
|
581
|
+
if (!current || this.hookOwner(binding) !== current || accepted?.current !== current
|
|
582
|
+
|| JSON.stringify(accepted.binding) !== JSON.stringify(binding) || typeof failure?.code !== "string" || typeof failure?.message !== "string")
|
|
583
|
+
throw new DaemonError("native_hook_unproven", "Hook failure has no exact current owned delivery");
|
|
584
|
+
assertDaemonDispatch(this.paths.dir);
|
|
585
|
+
if (this.nativeHookOperation !== current) {
|
|
586
|
+
await writeNativeHookFailure(this.paths.dir, binding, reportedError(failure, "Native hook failed"));
|
|
587
|
+
return { observed: true, retained: true };
|
|
588
|
+
}
|
|
589
|
+
current.requests.delete(binding.request_id);
|
|
590
|
+
const error = reportedError(failure, "Native hook failed");
|
|
591
|
+
error.details = { ...error.details, native_hook_binding: binding };
|
|
592
|
+
this.acceptHookFailure(current, error);
|
|
593
|
+
return { observed: true };
|
|
594
|
+
}
|
|
595
|
+
await this.observeHookFailures(); return { observed: true };
|
|
596
|
+
}
|
|
511
597
|
if (operation === "session.create") {
|
|
512
598
|
const request = {
|
|
513
599
|
operation_id: operationId,
|
|
@@ -668,6 +754,13 @@ export class DaemonRuntime {
|
|
|
668
754
|
await this.persist({ native_stop_proof: { daemon_id: this.state.daemon_id, sessions: [...this.sessions.keys()], observed_at: new Date().toISOString() } });
|
|
669
755
|
}
|
|
670
756
|
commitDaemonShutdown(this.paths.dir);
|
|
757
|
+
if (!cancel) {
|
|
758
|
+
// The native tree has been observed idle. Publish that execution proof
|
|
759
|
+
// before attempting to close the independent bridge process. A close
|
|
760
|
+
// failure is cleanup debt and must not erase the already observed turn.
|
|
761
|
+
await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null });
|
|
762
|
+
return { daemon_id: this.state.daemon_id, clean: true, settled: true, native_settled: true };
|
|
763
|
+
}
|
|
671
764
|
// A cooperative close can make the native Session non-resident while its
|
|
672
765
|
// original ACP prompt is still pending. For an explicitly whole-RUN
|
|
673
766
|
// cancellation this daemon owns the bridge process group, so retire that
|
|
@@ -698,7 +791,7 @@ export class DaemonRuntime {
|
|
|
698
791
|
}
|
|
699
792
|
|
|
700
793
|
export function zcodeBridgeEnv(config, daemonId) {
|
|
701
|
-
return { ...config.env, ...managedFlowEnv(config, daemonId) };
|
|
794
|
+
return { ...config.env, ...managedFlowEnv(config, daemonId), DD_ZCODE_STATE_DIR: !config.noFlow && daemonId ? config.runtime_owner?.state_dir : undefined };
|
|
702
795
|
}
|
|
703
796
|
|
|
704
797
|
export async function serveDaemon(stateDir) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
2
2
|
import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { ZcodeNativeHookOwnership } from "./zcode-native-hook-ownership.mjs";
|
|
4
|
+
import { invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
5
|
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -136,6 +137,7 @@ export class AcpBridge {
|
|
|
136
137
|
this.options = options;
|
|
137
138
|
this.nextId = 1;
|
|
138
139
|
this.pending = new Map();
|
|
140
|
+
this.nativeHookOwnership = new ZcodeNativeHookOwnership();
|
|
139
141
|
this.allocationObservers = new Map();
|
|
140
142
|
this.lateResponses = new Map();
|
|
141
143
|
this.expiredRequests = new Map();
|
|
@@ -146,6 +148,7 @@ export class AcpBridge {
|
|
|
146
148
|
this.connectionId = randomUUID();
|
|
147
149
|
this.receivedSequence = 0;
|
|
148
150
|
this.lifecycleNotifications = new Set();
|
|
151
|
+
this.lifecycleDiagnostics = [];
|
|
149
152
|
this.notificationError = null;
|
|
150
153
|
this.bridgeStderr = "";
|
|
151
154
|
this.toolObservations = new ToolObservations();
|
|
@@ -230,7 +233,6 @@ export class AcpBridge {
|
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
if (!isResponse || request) this.journal.writeInbound(message, request?.method);
|
|
233
|
-
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
|
|
234
236
|
const update = message.params?.update;
|
|
235
237
|
// ZCode emits this as `session/update`; Grok Build wraps the same ACP
|
|
236
238
|
// payload in `_x.ai/session_notification`. Preserve either provider's
|
|
@@ -256,32 +258,18 @@ export class AcpBridge {
|
|
|
256
258
|
void this.notifications.catch(error => { this.notificationError ??= error; });
|
|
257
259
|
}
|
|
258
260
|
}
|
|
261
|
+
if (message.method === "zcode/session/native_turn_started") {
|
|
262
|
+
const pending = this.pending.get(message.params?.requestId);
|
|
263
|
+
for (const routed of this.nativeHookOwnership.bind(message.params, pending)) { this.observeAttributedActivity(routed.message); this.dispatchLifecycleNotification(routed.message, routed.observer, routed.owner); }
|
|
264
|
+
}
|
|
265
|
+
this.observeAttributedActivity(message);
|
|
259
266
|
this.observeToolCall(message);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const correctable = current && correctableLifecycleErrors.has(error);
|
|
267
|
-
if (current && !correctable) {
|
|
268
|
-
this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
|
|
269
|
-
this.notificationError = this.lifecycleFailure;
|
|
270
|
-
}
|
|
271
|
-
// A missing/foreign authority is not a recoverable CLI validation
|
|
272
|
-
// result. Stop admitting productive requests immediately; the managed
|
|
273
|
-
// controller still owns physical stop/drain and evidence capture.
|
|
274
|
-
if (current && !correctable) this.rejectAll(this.notificationError);
|
|
275
|
-
try {
|
|
276
|
-
await hookBudget(1_000, null).run(() => this.journal.write("notification_failure", { ...errorRecord(error),
|
|
277
|
-
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" }), "failure_persistence");
|
|
278
|
-
} catch (persistenceError) {
|
|
279
|
-
if (current) this.notificationError ??= persistenceError;
|
|
280
|
-
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
this.lifecycleNotifications.add(pending);
|
|
284
|
-
void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
|
|
267
|
+
const pendingPrompts = [...this.pending.values()].filter(item => item.method === "session/prompt" && item.sessionId === message.params?.sessionId);
|
|
268
|
+
const owned = this.nativeHookOwnership.observe(message, pendingPrompts.length > 0, this.nativeHookOwnership.isManaged(message.params?.sessionId, this.pending));
|
|
269
|
+
if (owned !== null) {
|
|
270
|
+
for (const routed of owned) this.dispatchLifecycleNotification(routed.message, routed.observer, routed.owner);
|
|
271
|
+
} else if (message.method === "session/update" && this.options.onLifecycleNotification) {
|
|
272
|
+
this.dispatchLifecycleNotification(message, this.options.onLifecycleNotification);
|
|
285
273
|
}
|
|
286
274
|
if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
|
|
287
275
|
if (message.id !== undefined) {
|
|
@@ -293,6 +281,7 @@ export class AcpBridge {
|
|
|
293
281
|
return;
|
|
294
282
|
}
|
|
295
283
|
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(message.id);
|
|
284
|
+
if (pending.method === "session/prompt") this.discardUnboundIfIdle(pending.sessionId);
|
|
296
285
|
if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message, pending.method), grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
297
286
|
if (message.error) {
|
|
298
287
|
const data = message.error.data && typeof message.error.data === "object" && !Array.isArray(message.error.data) ? message.error.data : null;
|
|
@@ -332,6 +321,46 @@ export class AcpBridge {
|
|
|
332
321
|
});
|
|
333
322
|
}
|
|
334
323
|
|
|
324
|
+
observeAttributedActivity(message) {
|
|
325
|
+
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message) && this.nativeHookOwnership.isCurrent(message, this.pending)) this.observeActivity(message.params.sessionId, message.params.update);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
discardUnboundIfIdle(sessionId) {
|
|
329
|
+
if (![...this.pending.values()].some(item => item.method === "session/prompt" && item.sessionId === sessionId)) this.nativeHookOwnership.discardUnbound(sessionId);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
dispatchLifecycleNotification(message, observer, owner = null) {
|
|
333
|
+
const update = message.params?.update;
|
|
334
|
+
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
335
|
+
// A validated no-effect rejection is returned through the invocation
|
|
336
|
+
// ledger. It is evidence, not a poisoned ACP Session.
|
|
337
|
+
const current = this.options.onLifecycleNotification === observer && [...this.pending.values()].some(request => request.method === "session/prompt" && request.sessionId === message.params?.sessionId && (!owner || request.nativeHookOwner === owner));
|
|
338
|
+
const correctable = current && correctableLifecycleErrors.has(error);
|
|
339
|
+
// This callback observes an already-delivered native tool event. Its
|
|
340
|
+
// own timeout is not the result of the CLI command or the Turn.
|
|
341
|
+
const observationTimeout = current && error.code === "native_hook_timeout";
|
|
342
|
+
const diagnostic = { ...errorRecord(error), session_id: message.params?.sessionId ?? null,
|
|
343
|
+
tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" };
|
|
344
|
+
if (observationTimeout) this.lifecycleDiagnostics.push(diagnostic);
|
|
345
|
+
if (current && !correctable && !observationTimeout) {
|
|
346
|
+
this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
|
|
347
|
+
this.notificationError = this.lifecycleFailure;
|
|
348
|
+
}
|
|
349
|
+
// A missing/foreign authority is not a recoverable CLI validation
|
|
350
|
+
// result. Stop admitting productive requests immediately; the managed
|
|
351
|
+
// controller still owns physical stop/drain and evidence capture.
|
|
352
|
+
if (current && !correctable && !observationTimeout) this.rejectAll(this.notificationError);
|
|
353
|
+
try {
|
|
354
|
+
await this.journal.write("notification_failure", diagnostic);
|
|
355
|
+
} catch (persistenceError) {
|
|
356
|
+
if (current && !observationTimeout) this.notificationError ??= persistenceError;
|
|
357
|
+
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
this.lifecycleNotifications.add(pending);
|
|
361
|
+
void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
|
|
362
|
+
}
|
|
363
|
+
|
|
335
364
|
observeToolCall(message) {
|
|
336
365
|
const rootSessionId = this.modelSessions.get(message.params?.sessionId)?.sessionId;
|
|
337
366
|
this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
|
|
@@ -364,7 +393,7 @@ export class AcpBridge {
|
|
|
364
393
|
const rejectObservation = cause => {
|
|
365
394
|
if (this.pending.get(pending.id) !== pending) return;
|
|
366
395
|
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer);
|
|
367
|
-
this.pending.delete(pending.id); pending.reject(nativeObservationError(cause));
|
|
396
|
+
this.pending.delete(pending.id); this.discardUnboundIfIdle(pending.sessionId); pending.reject(nativeObservationError(cause));
|
|
368
397
|
};
|
|
369
398
|
void pending.publish("native_progress", { native_request_id: String(pending.id), session_id: adapterId, observed_at: new Date(observedAt).toISOString() }).catch(rejectObservation);
|
|
370
399
|
this.sessionActivity.set(adapterId, observedAt);
|
|
@@ -430,7 +459,7 @@ export class AcpBridge {
|
|
|
430
459
|
return new Promise((resolve, reject) => {
|
|
431
460
|
const finish = (error) => {
|
|
432
461
|
const pending = this.pending.get(id); if (!pending) return;
|
|
433
|
-
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(id); reject(error);
|
|
462
|
+
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(id); if (pending.method === "session/prompt") this.discardUnboundIfIdle(pending.sessionId); reject(error);
|
|
434
463
|
};
|
|
435
464
|
// Productive Turns use the sliding session-activity timeout below. A
|
|
436
465
|
// second wall-clock timeout would kill an active, long-running Turn.
|
|
@@ -484,7 +513,7 @@ export class AcpBridge {
|
|
|
484
513
|
void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
|
|
485
514
|
});
|
|
486
515
|
}, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
|
|
487
|
-
this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
|
|
516
|
+
this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, nativeHookOwner: method === "session/prompt" ? this.dispatchNativeHookOwner ?? null : null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
|
|
488
517
|
if (method === "session/prompt") {
|
|
489
518
|
void (async () => {
|
|
490
519
|
const dispatch = { operation_id: this.pending.get(id)?.operationId, session_id: params.sessionId, native_request_id: id,
|
|
@@ -520,6 +549,7 @@ export class AcpBridge {
|
|
|
520
549
|
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
|
|
521
550
|
}
|
|
522
551
|
this.pending.clear();
|
|
552
|
+
this.nativeHookOwnership.buffer.clear();
|
|
523
553
|
}
|
|
524
554
|
async flush() {
|
|
525
555
|
await this.terminalOutcomes;
|
|
@@ -536,8 +566,7 @@ export class AcpBridge {
|
|
|
536
566
|
await Promise.all([this.nativeDrain, this.diagnostics]);
|
|
537
567
|
await this.terminalOutcomes;
|
|
538
568
|
await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
|
|
539
|
-
|
|
540
|
-
else await this.journal.flush();
|
|
569
|
+
await this.journal.flush();
|
|
541
570
|
});
|
|
542
571
|
}
|
|
543
572
|
}
|
|
@@ -648,7 +677,6 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
648
677
|
}
|
|
649
678
|
if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
|
|
650
679
|
cursors.add(cursor);
|
|
651
|
-
if (cursors.size > 1_024) fail("ZCode child pagination exceeds its bounded directory limit", "native_topology_incomplete");
|
|
652
680
|
clock.sample(cursor);
|
|
653
681
|
}
|
|
654
682
|
}
|
|
@@ -714,7 +742,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
714
742
|
const clock = new ObservationClock({ timeoutMs: 30_000 });
|
|
715
743
|
try {
|
|
716
744
|
for (const sessionId of pending) {
|
|
717
|
-
if (
|
|
745
|
+
if (clock.sample()) throw new Error("Closed tree observation timed out");
|
|
718
746
|
const resident = await bridge.request("zcode/session/resident", { sessionId }, 5_000);
|
|
719
747
|
if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
|
|
720
748
|
residents.push(resident);
|
|
@@ -1001,6 +1029,12 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
1001
1029
|
["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
|
|
1002
1030
|
// CP253 installed artifact: exact same-child Read continuation and retained proof.
|
|
1003
1031
|
["b18dfdbedaa9330e765315bce0f1a609792d6c91", "dd-zcode-harness@2"],
|
|
1032
|
+
// Plan108 native 0.17.0: exact input/turn binding and a late child shell
|
|
1033
|
+
// event after the root reply. Clean rebuild retained identical code bytes.
|
|
1034
|
+
["ac3e1063934fd516b7e69e900dc196eaafd3e7a9", "dd-zcode-harness@2"],
|
|
1035
|
+
// Reviewed late-progress repair: unchanged request/turn contract retains
|
|
1036
|
+
// ac3e106 native evidence; changed delivery is covered by scoped regressions.
|
|
1037
|
+
["f9a3c5948ffbb0a31ec00bf8986610e17b8b9d77", "dd-zcode-harness@2"],
|
|
1004
1038
|
]);
|
|
1005
1039
|
const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
|
|
1006
1040
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
@@ -1087,7 +1121,9 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1087
1121
|
bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
|
|
1088
1122
|
const identity = await controlledIdentity(bridge, { ...options, cwd }); forward = flowForwarder(options, identity.providerSessionId, bridge);
|
|
1089
1123
|
await bridge.journal.flush();
|
|
1090
|
-
|
|
1124
|
+
const observer = await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId);
|
|
1125
|
+
bridge.configure({ onLifecycleNotification: observer });
|
|
1126
|
+
bridge.nativeHookOwnership?.register(options.nativeHookOwner?.operationId, observer, identity.adapterSessionId);
|
|
1091
1127
|
const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
|
|
1092
1128
|
options.assertDispatch?.();
|
|
1093
1129
|
let initial_usage_error = null;
|
|
@@ -1097,8 +1133,10 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1097
1133
|
// never turn a long native child request into a fabricated provider failure.
|
|
1098
1134
|
const timeoutMs = null;
|
|
1099
1135
|
const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
|
|
1136
|
+
const diagnosticsCursor = bridge.lifecycleDiagnostics?.length ?? 0;
|
|
1100
1137
|
options.assertDispatch?.();
|
|
1101
1138
|
bridge.dispatchOperationId = options.operationId;
|
|
1139
|
+
bridge.dispatchNativeHookOwner = options.nativeHookOwner ?? null;
|
|
1102
1140
|
const managed = options.operationId ? { schema_id: "dd-flow/managed-provider-recovery@1", owner: "controller", operation_id: options.operationId, root_operation_id: options.operationId.replace(/:capacity:\d+:session\.prompt$/, "") } : null;
|
|
1103
1141
|
const acknowledged = value => managed && value && Object.keys(managed).every(key => value[key] === managed[key]);
|
|
1104
1142
|
if (managed && (bridge.recoveryCapability?.schema_id !== managed.schema_id || bridge.recoveryCapability?.owner !== "controller")) fail("ZCode bridge does not acknowledge controller-owned provider recovery", "provider_recovery_unavailable");
|
|
@@ -1155,7 +1193,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1155
1193
|
await cancelTree(bridge, identity.adapterSessionId, evidence.subagents, identity.providerSessionId);
|
|
1156
1194
|
fail("background ZCode subagents cannot outlive the one-shot dd-zcode connection; the live child tree was cancelled");
|
|
1157
1195
|
}
|
|
1158
|
-
|
|
1196
|
+
const native_hook_diagnostics = (bridge.lifecycleDiagnostics ?? []).slice(diagnosticsCursor).filter(item => item.session_id === identity.adapterSessionId);
|
|
1197
|
+
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, assistant_text: bridge.assistantTextSince(identity.adapterSessionId, textCursor), turn, profile: profile_receipt, evidence: publicEvidence(evidence), ...(native_hook_diagnostics.length ? { native_hook_diagnostics } : {}) });
|
|
1159
1198
|
}
|
|
1160
1199
|
|
|
1161
1200
|
export async function inspectSession(options) {
|
|
@@ -6,6 +6,7 @@ export function delegationContract(harness: string): string;
|
|
|
6
6
|
export const lifecycleRetryInstruction: string;
|
|
7
7
|
export const lifecycleCommandCompletionInstruction: string;
|
|
8
8
|
export const stageLifecycleInstruction: string;
|
|
9
|
+
export const acceptedResultInstruction: string;
|
|
9
10
|
export function workerDelegationTask(input: { workId: string; startCommand: string; launchPolicy?: string }): string;
|
|
10
11
|
export function renderDelegationInstructions(input: DelegationInput): string;
|
|
11
12
|
export function renderCapacityInstructions(input: { harness: string; maximum: number }): string;
|
|
@@ -71,12 +71,16 @@ export function delegationContract(harness) {
|
|
|
71
71
|
|
|
72
72
|
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending. When the result provides prompt_delivery, read its complete plain Markdown file at prompt_delivery.path before working; it contains the current instructions, unlike the historical prompt_path. If delivery publication is pending, stop for reconciliation without repeating the accepted command.";
|
|
73
73
|
|
|
74
|
-
export const
|
|
74
|
+
export const nonLifecycleInputCorrectionInstruction = "For a non-lifecycle command explicitly reporting phase=prepare, effect=no_effect and recoverable=true, correct only its rejected arguments/JSON or an already-authorized missing precondition, then retry that non-lifecycle operation. Read that exact command's --help when its options are unclear. This includes check/integration input rejected before dispatch: an unsupported option or invalid JSON is not a failed check or an unknown Git effect. For example, remove an unsupported --stage from interactive integrate, or retain the current authorized interactive scope before reading git-inputs. This does not permit changing accepted data under an old request ID, expanding scope, replaying lifecycle commands without their issued successor, repeating dispatched checks/integration with failed, partial, committed or unknown effects, or bypassing ownership, recovery, storage, trust or operator-control refusals. An error lacking any of the three explicit preparation fields does not grant this correction path.";
|
|
75
|
+
|
|
76
|
+
export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. On any other lifecycle failure report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute. ${nonLifecycleInputCorrectionInstruction}`;
|
|
77
|
+
export const acceptedResultInstruction = "dd-flow's accepted Work/Stage result is authoritative; a native completion message or a result file alone is not acceptance. After dd-flow accepts your Work result, stop writing files, running tools and creating tasks, and end this Turn. A late native error, idle child card or failed session shutdown does not revoke an accepted result: do not repeat the Work or send another model prompt just to close it. Report technical_notes and cleanup_pending separately; the controller owns resource retirement. If another assigned Work or the Stage gate is still incomplete, continue only that remaining assignment through the issued graph. A possible conflicting writer requires controller reconciliation before dependent work, not an invented completion or a duplicate worker.";
|
|
75
78
|
|
|
76
79
|
export const stageLifecycleInstruction = [
|
|
77
80
|
"The project root identifies this RUN; use the Stage packet's declared execution workspace and absolute artifact paths. Do not change a native Session's working directory to a workspace provisioned for a later Stage.",
|
|
78
81
|
"In a controller-managed fanout, the coordinator ends the entry Turn after the Work graph is returned; the controller dispatches children. A completed child or Work is not Stage success: settle the declared graph and Stage gate before finishing.",
|
|
79
82
|
"Execute each runtime-issued lifecycle operation and its arguments exactly. One literal lifecycle call may be composed with ordinary shell commands; preserve conditions, stdin and redirects, and never replay the surrounding shell chain after failure. Invocation IDs are internal authority; never add, copy or invent one.",
|
|
83
|
+
acceptedResultInstruction,
|
|
80
84
|
lifecycleRetryInstruction
|
|
81
85
|
].join(" ");
|
|
82
86
|
|
|
@@ -97,6 +101,7 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
|
|
|
97
101
|
"Continue only if it succeeds and returns the authoritative Work packet.",
|
|
98
102
|
lifecycleRetryInstruction,
|
|
99
103
|
"After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
|
|
104
|
+
acceptedResultInstruction,
|
|
100
105
|
"Do not start another Work, create a child, change dependencies, ask the user, or treat a quiet sibling as failed. You cannot ask the user or pause the parent Stage. Missing facts go through this Work's declared result/failure contract; only the coordinator handles Stage HITL."
|
|
101
106
|
].join("\n");
|
|
102
107
|
}
|
|
@@ -120,10 +125,11 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
|
|
|
120
125
|
adapter.launch,
|
|
121
126
|
"Retain the frozen coordinator model and reasoning for native children; do not choose a different model, reasoning level or permission mode.",
|
|
122
127
|
"You are the coordinator. The JSON assignments below contain child-only messages, not instructions for you to execute. Pass each message verbatim as the native tool's task/message/prompt argument. For context=fresh, select empty conversation context before launch; if unavailable, report the limitation and stop. Use only fields exposed by your actual native tool schema; if the native tool is absent, report that error without substituting a shell command.",
|
|
123
|
-
"Every child must be a direct child of this current Session. Wait
|
|
128
|
+
"Every child must be a direct child of this current Session. Wait while assigned Work remains in progress; once dd-flow has accepted all assigned results, return without waiting merely for native session retirement. One failed child is evidence for the coordinator, not a reason to cancel its siblings.",
|
|
124
129
|
adapter.partial ?? "If a spawn is refused, stop issuing new spawns and retain every successful child handle. Wait for all already-launched children, then release completed children using the adapter's exposed native close/release mechanism when supported. A failed batch does not undo successful spawns. Report completed/launched assignments, assignments not launched and the exact refusal; never claim no children launched when any spawn succeeded. Return after this partial wave; the controller will issue remaining Work when capacity is available. Do not replay an ambiguous spawn.",
|
|
125
130
|
"Do not finish this Stage, repair it, or start a successor in this Turn. Do not invent provider or internal session IDs; lifecycle evidence comes from the adapter.",
|
|
126
131
|
adapter.wait,
|
|
132
|
+
acceptedResultInstruction,
|
|
127
133
|
"Child assignments (JSON data):",
|
|
128
134
|
JSON.stringify(assignments, null, 2)
|
|
129
135
|
].join("\n\n");
|
|
@@ -148,13 +154,14 @@ export function renderWaitInstructions({ harness, stage }) {
|
|
|
148
154
|
return [
|
|
149
155
|
`Native child Work for ${stage} is still running.`,
|
|
150
156
|
adapter.wait,
|
|
157
|
+
acceptedResultInstruction,
|
|
151
158
|
"Do not create a Session, Work, or additional child agent. Do not cancel siblings merely because one child failed, and do not finish this Stage."
|
|
152
159
|
].join("\n");
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
export function renderAdapterPolicy({ harness }) {
|
|
156
163
|
const adapter = requireAdapter(harness);
|
|
157
|
-
return [adapter.launch, adapter.wait, "
|
|
164
|
+
return [adapter.launch, adapter.wait, "Native launch identity comes from adapter observations; never synthesize provider or internal Session IDs.", acceptedResultInstruction].join(" ");
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
export function renderRecoveryProbeInstructions({ harness, command }) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
// Internal transport, not operation authorization. The RUN owner registers and
|
|
6
|
+
// confirms this wrapper's PID/group, then releases stdin after its final fence.
|
|
7
|
+
// fd3 privately carries fixed argv and only permitted environment values; these
|
|
8
|
+
// resolved values never enter policy or persisted configuration. fd4 carries raw stdout to the
|
|
9
|
+
// owner in memory. Public stdout/stderr never contain provider output or env.
|
|
10
|
+
const outputLimit = 1024 * 1024;
|
|
11
|
+
const text = value => typeof value === 'string' && value.length > 0 && value.length <= 16384 && !value.includes('\0');
|
|
12
|
+
const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
|
|
13
|
+
const emit = value => process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
14
|
+
process.stdout.on('error', () => {}); // Observer loss is not command completion.
|
|
15
|
+
process.stderr.on('error', () => {});
|
|
16
|
+
async function bounded(stream, limit) {
|
|
17
|
+
const chunks = []; let bytes = 0;
|
|
18
|
+
for await (const chunk of stream) {
|
|
19
|
+
bytes += chunk.length;
|
|
20
|
+
if (bytes > limit) throw new Error('bounded_input');
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
return Buffer.concat(chunks);
|
|
24
|
+
}
|
|
25
|
+
let config = null, admitted = false, started = false;
|
|
26
|
+
const result = (exitCode, signal, error, outputAvailable = true, truncated = false) => {
|
|
27
|
+
emit({ schema_id: 'dd-flow/delivery-command-result@1', operation_id: config?.operation_id ?? null,
|
|
28
|
+
step_id: config?.step_id ?? null, admitted, started, exit_code: exitCode, signal, error,
|
|
29
|
+
output_available: outputAvailable, stdout_truncated: truncated });
|
|
30
|
+
process.exitCode = error ? 1 : 0;
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const bytes = await bounded(fs.createReadStream('', { fd: 3, autoClose: true }), 3 * outputLimit);
|
|
34
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
35
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
36
|
+
|| Object.keys(value).sort().join() !== 'argv,environment,operation_id,permitted_environment_variables,schema_id,step_id'
|
|
37
|
+
|| value.schema_id !== 'dd-flow/delivery-command@1' || !id(value.operation_id) || !id(value.step_id)
|
|
38
|
+
|| !Array.isArray(value.argv) || !value.argv.length || value.argv.length > 128 || !value.argv.every(text)
|
|
39
|
+
|| !Array.isArray(value.permitted_environment_variables) || value.permitted_environment_variables.length > 128
|
|
40
|
+
|| !value.permitted_environment_variables.every(name => typeof name === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(name))
|
|
41
|
+
|| new Set(value.permitted_environment_variables).size !== value.permitted_environment_variables.length
|
|
42
|
+
|| !value.environment || typeof value.environment !== 'object' || Array.isArray(value.environment)
|
|
43
|
+
|| Object.entries(value.environment).some(([name, content]) => !value.permitted_environment_variables.includes(name)
|
|
44
|
+
|| typeof content !== 'string' || content.includes('\0') || content.length > 16384)) throw new Error('invalid_configuration');
|
|
45
|
+
// Fail before readiness if the private output channel was not installed.
|
|
46
|
+
const channel = fs.fstatSync(4);
|
|
47
|
+
if (!channel.isFIFO() && !channel.isSocket()) throw new Error('private_pipe_required');
|
|
48
|
+
fs.writeSync(4, Buffer.alloc(0));
|
|
49
|
+
config = value;
|
|
50
|
+
emit({ schema_id: 'dd-flow/delivery-command-ready@1', operation_id: config.operation_id, step_id: config.step_id });
|
|
51
|
+
const admission = await bounded(process.stdin, 32);
|
|
52
|
+
if (!admission.equals(Buffer.from('admitted\n'))) result(null, null, 'admission_missing');
|
|
53
|
+
else {
|
|
54
|
+
admitted = true;
|
|
55
|
+
const environment = { ...config.environment };
|
|
56
|
+
environment.DD_FLOW_DELIVERY_OPERATION_ID = config.operation_id;
|
|
57
|
+
environment.DD_FLOW_DELIVERY_STEP_ID = config.step_id;
|
|
58
|
+
const output = fs.createWriteStream('', { fd: 4, autoClose: true });
|
|
59
|
+
let available = true, truncated = false, count = 0;
|
|
60
|
+
const flushed = new Promise(resolve => { output.once('finish', resolve); output.on('error', () => { available = false; resolve(); }); });
|
|
61
|
+
const child = spawn(config.argv[0], config.argv.slice(1), { env: environment, shell: false, windowsHide: true,
|
|
62
|
+
detached: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
63
|
+
started = Boolean(child.pid);
|
|
64
|
+
let spawnFailed = false;
|
|
65
|
+
child.once('error', () => { spawnFailed = true; });
|
|
66
|
+
child.stdout.on('error', () => { available = false; });
|
|
67
|
+
child.stderr.on('error', () => { available = false; });
|
|
68
|
+
child.stderr.resume(); // Never persist or echo authenticated stderr.
|
|
69
|
+
child.stdout.on('data', bytes => {
|
|
70
|
+
const remaining = Math.max(0, outputLimit - count);
|
|
71
|
+
if (bytes.length > remaining) truncated = true;
|
|
72
|
+
if (remaining && available) output.write(bytes.subarray(0, remaining));
|
|
73
|
+
count += bytes.length;
|
|
74
|
+
});
|
|
75
|
+
const completed = await new Promise(resolve => child.once('close', (code, signal) => resolve({ code, signal })));
|
|
76
|
+
output.end(); await flushed;
|
|
77
|
+
result(completed.code, completed.signal, spawnFailed ? 'spawn_failed' : !available ? 'private_output_unavailable'
|
|
78
|
+
: truncated ? 'output_limit_exceeded' : completed.code !== 0 || completed.signal ? 'command_failed' : null, available, truncated);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Input may contain secrets accidentally. Never print it or an exception
|
|
82
|
+
// whose message/stack could contain an authenticated argv or raw payload.
|
|
83
|
+
result(null, null, admitted ? 'transport_failed' : 'configuration_or_admission_invalid', false);
|
|
84
|
+
}
|