@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
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 +31 -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 +274 -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 +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- 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 +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- 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 +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- 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 +250 -33
- 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 -7
- package/dist/services/retained-native-hook.js +274 -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 +25 -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 +257 -44
- 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 +8 -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 +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
package/dist/cli.js
CHANGED
|
@@ -53,12 +53,26 @@ if (args[0] === "upgrade" || args[0] === "flow" && args[1] === "mb-upgrade") {
|
|
|
53
53
|
const { runMemoryUpgradeCli } = await import("./cli/memory-upgrade.js");
|
|
54
54
|
process.exit(await runMemoryUpgradeCli(args, process.env));
|
|
55
55
|
}
|
|
56
|
-
if (args[0] === "review" || args[0] === "flow" && ["mb-audit", "project-review"].includes(args[1] ?? "")) {
|
|
56
|
+
if (args[0] === "review" || args[0] === "flow" && ["mb-audit", "project-review", "mb-distill", "review-fix"].includes(args[1] ?? "")) {
|
|
57
57
|
publishSetup('ingress_entered');
|
|
58
58
|
const { runDiagnosticCli } = await import("./cli/diagnostic.js");
|
|
59
59
|
publishSetup('imports_ready');
|
|
60
60
|
process.exit(await runDiagnosticCli(args, process.env));
|
|
61
61
|
}
|
|
62
|
+
if (args[0] === "flow" && args[1] === "mb-fix") {
|
|
63
|
+
publishSetup('ingress_entered');
|
|
64
|
+
const { runMemoryFixCli } = await import("./cli/memory-fix.js");
|
|
65
|
+
publishSetup('imports_ready');
|
|
66
|
+
process.exit(await runMemoryFixCli(args, process.env));
|
|
67
|
+
}
|
|
68
|
+
if (args[0] === "flow" && args[1] === "interactive") {
|
|
69
|
+
const { runInteractiveCli } = await import("./cli/interactive.js");
|
|
70
|
+
process.exit(await runInteractiveCli(args, process.env));
|
|
71
|
+
}
|
|
72
|
+
if (args[0] === "flow" && ["release", "deploy", "publish"].includes(args[1] ?? "")) {
|
|
73
|
+
const { runDeliveryCli } = await import("./cli/delivery.js");
|
|
74
|
+
process.exit(await runDeliveryCli(args, process.env));
|
|
75
|
+
}
|
|
62
76
|
if (args.slice(0, 3).join(" ") === "runtime storage admit") {
|
|
63
77
|
const { runMemoryStorageAdmissionCli } = await import("./cli/memory-storage-admission.js");
|
|
64
78
|
process.exit(runMemoryStorageAdmissionCli(args));
|
|
@@ -75,10 +89,10 @@ if (args.slice(0, 3).join(" ") === "runtime flow admit") {
|
|
|
75
89
|
const { runMemoryFlowAdmissionCli } = await import("./cli/memory-flow-admission.js");
|
|
76
90
|
process.exit(await runMemoryFlowAdmissionCli(args));
|
|
77
91
|
}
|
|
78
|
-
const
|
|
92
|
+
const codexHook = args.slice(0, 3).join(" ") === "codex hook handle";
|
|
79
93
|
const exitCode = isRuntimeMaintenanceIngress(args, process.env)
|
|
80
94
|
? await runRuntimeMaintenanceIngress(args)
|
|
81
|
-
:
|
|
95
|
+
: codexHook
|
|
82
96
|
? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
|
|
83
97
|
: isNativeHookIngress(args)
|
|
84
98
|
? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const deliveryFlowStages = {
|
|
2
|
+
release: ["preflight", "version-decision", "prepare", "verify", "publish", "finish"],
|
|
3
|
+
deploy: ["preflight", "target-decision", "prepare", "deploy", "verify", "finish"],
|
|
4
|
+
publish: ["preflight", "target-decision", "prepare", "verify", "publish", "finish"]
|
|
5
|
+
};
|
|
6
|
+
export function isDeliveryFlow(value) {
|
|
7
|
+
return typeof value === "string" && Object.hasOwn(deliveryFlowStages, value);
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Distillation's semantic catalog, not executable handler registration. */
|
|
2
|
+
export const distillStages = ["preflight", "extract", "review", "promote", "verify", "finish"];
|
|
3
|
+
const domain = [
|
|
4
|
+
"structure-navigation", "delivery-protocols", "deferrals-closure", "scenarios-evidence", "ui-guides",
|
|
5
|
+
"system-adr-contracts", "engineering-code-standards", "operations-release", "agent-workflow-prompts", "templates-metadata"
|
|
6
|
+
];
|
|
7
|
+
const process = [
|
|
8
|
+
"user-intent-intake", "project-grounding", "flow-routing", "planning-decomposition", "execution-orchestration",
|
|
9
|
+
"implementation-context-coding", "debugging-failure-recovery", "review-verification-testing", "evidence-acceptance-closure",
|
|
10
|
+
"delivery-release-operations", "memory-documentation-lifecycle", "memory-freshness-self-improvement"
|
|
11
|
+
];
|
|
12
|
+
export const distillAspects = Object.freeze([
|
|
13
|
+
...domain.map((name, index) => aspect("domain", name, index)),
|
|
14
|
+
...process.map((name, index) => aspect("process", name, index))
|
|
15
|
+
]);
|
|
16
|
+
function aspect(lens, name, index) {
|
|
17
|
+
const number = String(index + 1).padStart(2, "0");
|
|
18
|
+
return Object.freeze({ id: `${lens}-${number}`, lens, name,
|
|
19
|
+
criterion: `support/dd-flow/mb-distill/${lens === "domain" ? "aspects" : "process-aspects"}/${number}-${name}.md` });
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const interactiveStages = ["preflight", "interactive", "consolidation", "hardening", "review", "integration"];
|
|
@@ -17,22 +17,37 @@ async function hook(options) {
|
|
|
17
17
|
try {
|
|
18
18
|
const observationMs = budget.remaining("observation");
|
|
19
19
|
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt: budget.deadlineAt }, observationMs, requestId);
|
|
20
|
+
if (identity.fastAck === true) {
|
|
21
|
+
const binding = identity.nativeHookBinding, args = payload.toolCall?.args ?? {};
|
|
22
|
+
if (event !== "PreToolUse" || binding?.native_admission !== "assigned_work_start" || binding.source_harness !== "antigravity-cli"
|
|
23
|
+
|| binding.daemon_id !== options["daemon-id"] || binding.provider_session_id !== payload.conversationId
|
|
24
|
+
|| binding.native_command !== (args.command ?? args.cmd ?? args.CommandLine)) throw Object.assign(new Error("Deferred native callback binding differs"), { code: "native_hook_unproven" });
|
|
25
|
+
return { decision: "allow" };
|
|
26
|
+
}
|
|
20
27
|
if (identity.stale) return event === "PreToolUse" ? { decision: "deny", reason: "agy_stale_hook" } : {};
|
|
21
|
-
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
28
|
+
if (!identity.observationOnly && (event === "PreToolUse" || event === "PostToolUse")) {
|
|
22
29
|
const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
|
|
23
30
|
const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, launch_cwd: identity.launch_cwd, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, execution_num: payload.executionNum, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
|
|
24
31
|
await invokeNativeHook({ bin: options["dd-flow-bin"] ?? identity.dd_flow_bin,
|
|
25
32
|
args: ["agy", "event", "handle", "--project-root", options["project-root"] ?? identity.project_root, "--json"],
|
|
26
33
|
payload: normalized, home: options["dd-flow-home"] ?? identity.dd_flow_home, timeoutMs: budget.remaining("admission") });
|
|
27
34
|
}
|
|
35
|
+
// A successful callback can retire its exact observation request. This is
|
|
36
|
+
// best-effort housekeeping: lost IPC neither denies the native tool nor
|
|
37
|
+
// invents an execution result; the daemon retains the request if uncertain.
|
|
38
|
+
try { await callDaemon(options["state-dir"], "hook.complete", { daemonId: options["daemon-id"], requestId }, Math.min(500, budget.remaining("retirement"))); }
|
|
39
|
+
catch { /* The request remains available for a late failure receipt. */ }
|
|
28
40
|
return event === "PreToolUse" ? { decision: "allow" } : {};
|
|
29
41
|
} catch (error) {
|
|
30
42
|
// A failed socket cannot be used to report its own failure. The daemon
|
|
31
43
|
// reads only receipts matching request IDs it actually accepted this turn.
|
|
32
44
|
const correctable = event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && error.details?.effect === "no_effect"
|
|
33
45
|
&& error.details?.recoverable === true && error.details?.disposition === "pre_cli_shell_refusal";
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
// The operation deadline may already be exhausted. It cannot be reused to
|
|
47
|
+
// persist the failure caused by that deadline: the daemon consumes only a
|
|
48
|
+
// receipt for a request it actually registered for its current turn.
|
|
49
|
+
try { await writeJsonAtomic(path.join(options["state-dir"], "hook-failures", `${requestId}.json`), { request_id: requestId, observed_at: new Date().toISOString(), code: error.code ?? "agy_hook_rejected", message: error.message,
|
|
50
|
+
details: { ...error.details, effect: correctable ? "no_effect" : "unknown", ...(correctable ? { native_pre_cli_refusal: true } : {}) } }); }
|
|
36
51
|
catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
37
52
|
throw error;
|
|
38
53
|
}
|
|
@@ -10,15 +10,25 @@ function parse(argv) { const positional = []; const options = {}; for (let index
|
|
|
10
10
|
function common(options, operation) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
|
|
11
11
|
async function hook(options) {
|
|
12
12
|
const budget = hookBudget();
|
|
13
|
-
const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload }, budget.remaining("identity"));
|
|
13
|
+
const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload, hookDeadlineAt: budget.deadlineAt }, budget.remaining("identity"));
|
|
14
|
+
const binding = identity.nativeHookBinding, input = payload.tool_input ?? payload.toolInput;
|
|
15
|
+
// Only the daemon's exact, retained child delivery can use this short ACK;
|
|
16
|
+
// the issued CLI command still verifies its assignment and materializes it.
|
|
17
|
+
if (identity.fastAck === true && binding?.source_harness === "grok-acp" && binding.request_id
|
|
18
|
+
&& binding.provider_session_id === (payload.sessionId ?? payload.session_id)
|
|
19
|
+
&& binding.provider_session_id !== binding.root_provider_session_id
|
|
20
|
+
&& binding.native_command === (input?.command ?? input?.cmd)) return { decision: "continue" };
|
|
14
21
|
const enriched = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: identity } }; const args = ["grok", "event", "handle", "--project-root", options["project-root"], "--json"]; const command = options["dd-flow-bin"];
|
|
15
22
|
try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission") }); }
|
|
16
23
|
catch (error) {
|
|
17
24
|
if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
let storageError;
|
|
26
|
+
try { await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error); }
|
|
27
|
+
catch (storage) { storageError = storage; process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
28
|
+
if (identity.nativeHookBinding && storageError?.code !== "native_hook_identity_conflict") try {
|
|
29
|
+
await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id,
|
|
30
|
+
...(storageError ? { binding: identity.nativeHookBinding, failure: errorRecord(error) } : {}) }, storageError ? 5_000 : budget.remaining("failure_notification"));
|
|
31
|
+
} catch { /* The durable receipt is polled by the owner or retained for reconciliation. */ }
|
|
22
32
|
throw error;
|
|
23
33
|
}
|
|
24
34
|
}
|
|
@@ -34,18 +34,26 @@ function common(options, operation) {
|
|
|
34
34
|
async function hook(options) {
|
|
35
35
|
const budget = hookBudget();
|
|
36
36
|
const payload = JSON.parse((await readNativeHookInput(process.stdin, budget)) || "{}");
|
|
37
|
-
const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload }, budget.remaining("identity"));
|
|
37
|
+
const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload, hookDeadlineAt: budget.deadlineAt }, budget.remaining("identity"));
|
|
38
|
+
const binding = identity.nativeHookBinding, input = payload.tool_input ?? payload.toolInput;
|
|
39
|
+
// This only acknowledges an already retained child delivery. The actual
|
|
40
|
+
// Work CLI still materializes and validates its issued invocation via CAS.
|
|
41
|
+
if (identity.fastAck === true && binding?.source_harness === "zcode-acp" && binding.request_id
|
|
42
|
+
&& binding.provider_session_id === (payload.session_id ?? payload.sessionId)
|
|
43
|
+
&& binding.provider_session_id !== binding.root_provider_session_id
|
|
44
|
+
&& binding.native_command === (input?.command ?? input?.cmd)) return {};
|
|
38
45
|
const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
|
|
39
46
|
|
|
40
47
|
try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission"), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } }); }
|
|
41
48
|
catch (error) {
|
|
42
49
|
if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
let storageError;
|
|
51
|
+
try { await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error); }
|
|
52
|
+
catch (storage) { storageError = storage; process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
53
|
+
if (identity.nativeHookBinding && storageError?.code !== "native_hook_identity_conflict") try {
|
|
54
|
+
await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id,
|
|
55
|
+
...(storageError ? { binding: identity.nativeHookBinding, failure: errorRecord(error) } : {}) }, storageError ? 5_000 : budget.remaining("failure_notification"));
|
|
56
|
+
} catch { /* The durable receipt is polled by the owner or retained for reconciliation. */ }
|
|
49
57
|
throw error;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -6,14 +6,22 @@ import { agyTerminalEnvelope, ownedAgyOutput } from "./agy-terminal-envelope.mjs
|
|
|
6
6
|
export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null, sessionId = null) {
|
|
7
7
|
let rows;
|
|
8
8
|
try { rows = text.trim().split("\n").filter(Boolean).map(JSON.parse); } catch { return null; }
|
|
9
|
+
const reducer = createAgyErrorBoundary(error, floor, source, sessionId);
|
|
10
|
+
for (const row of rows) reducer.add(row);
|
|
11
|
+
return reducer.finish();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Stateful equivalent for an owned, validated JSONL snapshot. */
|
|
15
|
+
export function createAgyErrorBoundary(error, floor = -1, source = null, sessionId = null) {
|
|
9
16
|
let previous = -1, lastError = null, boundary = null, invocation = null, call = null;
|
|
10
|
-
const launches = new Map();
|
|
11
|
-
|
|
12
|
-
if (
|
|
17
|
+
const launches = new Map(); let invalid = false;
|
|
18
|
+
function add(row) {
|
|
19
|
+
if (invalid) return;
|
|
20
|
+
if (!Number.isInteger(row.step_index) || row.step_index <= previous) { invalid = true; return; }
|
|
13
21
|
previous = row.step_index;
|
|
14
|
-
if (row.step_index <= (floor ?? -1))
|
|
15
|
-
if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; call = null; launches.clear();
|
|
16
|
-
if (row.type === "ERROR_MESSAGE") { lastError = row; boundary = null; invocation = null;
|
|
22
|
+
if (row.step_index <= (floor ?? -1)) return;
|
|
23
|
+
if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; call = null; launches.clear(); return; }
|
|
24
|
+
if (row.type === "ERROR_MESSAGE") { lastError = row; boundary = null; invocation = null; return; }
|
|
17
25
|
if (Array.isArray(row.tool_calls)) {
|
|
18
26
|
invocation = null;
|
|
19
27
|
call = row.tool_calls.length === 1 ? row.tool_calls[0] : null;
|
|
@@ -29,7 +37,7 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
|
|
|
29
37
|
if (row.type === 'GENERIC' && row.status === 'RUNNING') {
|
|
30
38
|
const task = row.content?.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
|
|
31
39
|
if (task && invocation && (!sessionId || task[1].startsWith(`${sessionId}/`))) launches.set(task[1], launches.has(task[1]) ? null : { invocation, step: row.step_index });
|
|
32
|
-
call = null; invocation = null;
|
|
40
|
+
call = null; invocation = null; return;
|
|
33
41
|
}
|
|
34
42
|
const envelope = agyTerminalEnvelope(row, call);
|
|
35
43
|
let launchStep = null;
|
|
@@ -38,11 +46,11 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
|
|
|
38
46
|
invocation = launch && launch.step < row.step_index ? launch.invocation : null;
|
|
39
47
|
launchStep = launch?.step ?? null;
|
|
40
48
|
}
|
|
41
|
-
if (!lastError || envelope?.exit_code !== 0)
|
|
49
|
+
if (!lastError || envelope?.exit_code !== 0) return;
|
|
42
50
|
const retained = envelope.output_reference && source && sessionId ? ownedAgyOutput(envelope.output_reference, source, sessionId, envelope.task_id) : null;
|
|
43
51
|
const full = retained ? agyTerminalEnvelope({ type: 'GENERIC', status: 'DONE', content: retained.text }) : null;
|
|
44
52
|
const output = (retained ? full?.exit_code === 0 ? full.output : null : envelope.output)?.replaceAll("\r", "");
|
|
45
|
-
if (!output)
|
|
53
|
+
if (!output) return;
|
|
46
54
|
for (const match of output.matchAll(/^\s*\{/gm)) {
|
|
47
55
|
let receipt;
|
|
48
56
|
const candidate = output.slice(match.index).trim();
|
|
@@ -74,6 +82,6 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
|
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
|
-
return boundary && lastError?.error === error ? boundary : null;
|
|
85
|
+
return { add, finish: () => !invalid && boundary && lastError?.error === error ? boundary : null };
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -160,14 +160,23 @@ async function agyNativeRequest({ endpoint, sessionId, cwd, remainingMs, signal,
|
|
|
160
160
|
if (!/^127\.0\.0\.1:\d+$/.test(endpoint?.target ?? '') || Number(endpoint.target.split(':')[1]) < 1 || Number(endpoint.target.split(':')[1]) > 65535) throw invalid('native_endpoint_nonloopback');
|
|
161
161
|
if (!Number.isFinite(remainingMs) || remainingMs < 1) throw invalid('observation_budget_exhausted');
|
|
162
162
|
const deadline = Date.now() + remainingMs;
|
|
163
|
-
const client = new Client(endpoint.target, grpc.credentials.createInsecure(), { 'grpc.max_receive_message_length':
|
|
163
|
+
const client = new Client(endpoint.target, grpc.credentials.createInsecure(), { 'grpc.max_receive_message_length': -1, 'grpc.max_send_message_length': 64 * 1024, 'grpc.enable_retries': 0 });
|
|
164
164
|
let active;
|
|
165
165
|
const headers = new grpc.Metadata();
|
|
166
166
|
if (typeof csrfToken === 'string' && csrfToken) headers.set('x-codeium-csrf-token', csrfToken);
|
|
167
167
|
const abort = () => active?.cancel();
|
|
168
|
+
// Native error details may contain request or authentication material. Keep
|
|
169
|
+
// the method and numeric status for diagnosis without copying those bytes.
|
|
170
|
+
const grpcError = (name, error) => {
|
|
171
|
+
const code = Number.isInteger(error?.code) ? error.code : null;
|
|
172
|
+
const failure = invalid(`grpc_${code ?? 'unknown'}`);
|
|
173
|
+
failure.details.grpc_method = agyNativeService[name].path;
|
|
174
|
+
failure.details.grpc_code = code;
|
|
175
|
+
return failure;
|
|
176
|
+
};
|
|
168
177
|
const unary = (name, request) => new Promise((resolve, reject) => {
|
|
169
178
|
if (signal?.aborted) return reject(invalid('inspection_cancelled'));
|
|
170
|
-
active = client[name](request, headers, { deadline }, (error, value) => error ? reject(
|
|
179
|
+
active = client[name](request, headers, { deadline }, (error, value) => error ? reject(grpcError(name, error)) : value ? resolve(value) : reject(invalid('grpc_message_missing')));
|
|
171
180
|
});
|
|
172
181
|
signal?.addEventListener('abort', abort, { once: true });
|
|
173
182
|
try {
|
|
@@ -191,7 +200,7 @@ async function agyNativeRequest({ endpoint, sessionId, cwd, remainingMs, signal,
|
|
|
191
200
|
const snapshot = JSON.parse(JSON.stringify(update, (key, value) => key.startsWith('$') ? undefined : value));
|
|
192
201
|
done = true; resolve({ ...snapshot, observed_at: new Date().toISOString(), method: agyNativeService.state.path }); active.cancel();
|
|
193
202
|
});
|
|
194
|
-
active.on('error', error => { if (!done) { done = true; reject(
|
|
203
|
+
active.on('error', error => { if (!done) { done = true; reject(grpcError('state', error)); } });
|
|
195
204
|
active.on('end', () => { if (!done) { done = true; reject(invalid('native_state_snapshot_missing')); } });
|
|
196
205
|
});
|
|
197
206
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A new StreamAgentStateUpdates subscription starts with a full snapshot.
|
|
2
|
+
// An omitted protobuf update is an empty list; a partial indexed list is not.
|
|
3
|
+
export const agyNativeFullList = (update, key) => !update ||
|
|
4
|
+
Array.isArray(update[key]) && update.totalLength === update[key].length &&
|
|
5
|
+
update.indices?.length === update.totalLength && update.indices.every((value, offset) => value === offset);
|
|
6
|
+
|
|
7
|
+
export function agyNativeInventory(snapshot, sessionId) {
|
|
8
|
+
if (snapshot?.conversationId !== sessionId || !snapshot.trajectoryId || snapshot.error ||
|
|
9
|
+
snapshot.method !== '/exa.language_server_pb.LanguageServerService/StreamAgentStateUpdates' ||
|
|
10
|
+
!agyNativeFullList(snapshot.backgroundTasksUpdate, 'backgroundTasks') ||
|
|
11
|
+
!agyNativeFullList(snapshot.pendingAgentMessagesUpdate, 'pendingAgentMessages'))
|
|
12
|
+
throw Object.assign(new Error('AGY native task inventory is incomplete or unbound'), { code: 'native_resources_unproven' });
|
|
13
|
+
const tasks = snapshot.backgroundTasksUpdate?.backgroundTasks?.map(item => item.taskSnapshot) ?? [];
|
|
14
|
+
if (tasks.some(task => !task || typeof task.taskId !== 'string' || !task.taskId.startsWith(`${sessionId}/task-`) || !/\/task-\d+$/.test(task.taskId) || !task.toolName) ||
|
|
15
|
+
new Set(tasks.map(task => task.taskId)).size !== tasks.length)
|
|
16
|
+
throw Object.assign(new Error('AGY native task identity is unproven'), { code: 'native_resources_unproven' });
|
|
17
|
+
return { tasks, messages: snapshot.pendingAgentMessagesUpdate?.pendingAgentMessages ?? [],
|
|
18
|
+
idle: snapshot.fullyIdle === true && snapshot.status === 1 && snapshot.executorLoopStatus === 1 && snapshot.hasActiveChildren !== true };
|
|
19
|
+
}
|
|
@@ -2,6 +2,7 @@ import { constants, openSync, closeSync, fstatSync, lstatSync, readSync, realpat
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
6
|
|
|
6
7
|
const taskId = '[\\w-]+/task-\\d+';
|
|
7
8
|
|
|
@@ -18,16 +19,22 @@ export function ownedAgyOutput(reference, source, sessionId, ownerTask = null) {
|
|
|
18
19
|
if (!number || ![`steps/${number}/output.txt`, `tasks/task-${number}.log`].includes(relative)) return null;
|
|
19
20
|
}
|
|
20
21
|
const before = lstatSync(file);
|
|
21
|
-
if (!before.isFile()
|
|
22
|
+
if (!before.isFile()) return null;
|
|
22
23
|
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
23
24
|
const stat = fstatSync(fd);
|
|
24
25
|
if (!stat.isFile() || stat.ino !== before.ino || stat.dev !== before.dev) return null;
|
|
25
|
-
const bytes = Buffer.
|
|
26
|
-
let offset = 0;
|
|
27
|
-
while (offset <
|
|
26
|
+
const bytes = Buffer.allocUnsafe(64 * 1024), decoder = new StringDecoder('utf8'), hash = createHash('sha256');
|
|
27
|
+
let offset = 0, text = '';
|
|
28
|
+
while (offset < stat.size) {
|
|
29
|
+
const count = readSync(fd, bytes, 0, Math.min(bytes.length, stat.size - offset), offset); if (!count) return null;
|
|
30
|
+
const chunk = bytes.subarray(0, count); hash.update(chunk); text += decoder.write(chunk); offset += count;
|
|
31
|
+
}
|
|
32
|
+
text += decoder.end();
|
|
28
33
|
const after = fstatSync(fd);
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
const finalPath = lstatSync(file);
|
|
35
|
+
if (!finalPath.isFile() || finalPath.dev !== stat.dev || finalPath.ino !== stat.ino || realpathSync(file) !== file
|
|
36
|
+
|| stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs) return null;
|
|
37
|
+
return { text, sha256: hash.digest('hex'), locator: reference };
|
|
31
38
|
} catch { return null; }
|
|
32
39
|
finally { if (fd !== undefined) closeSync(fd); }
|
|
33
40
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
|
|
4
|
+
// The caller opens and validates the owned no-symlink handle and must discard
|
|
5
|
+
// tentative callback results if this function rejects. One complete JSONL
|
|
6
|
+
// record, rather than the whole transcript, is the largest retained value.
|
|
7
|
+
export async function readAgyTranscriptSnapshot(handle, { locator, assertIdentity, onRecord, onProgress }) {
|
|
8
|
+
if (typeof assertIdentity !== 'function' || typeof onRecord !== 'function') throw new TypeError('snapshot requires owner validation and a record consumer');
|
|
9
|
+
const original = await handle.stat();
|
|
10
|
+
const unchanged = async () => {
|
|
11
|
+
const current = await handle.stat();
|
|
12
|
+
if (!current.isFile() || current.dev !== original.dev || current.ino !== original.ino || current.size !== original.size
|
|
13
|
+
|| current.mtimeMs !== original.mtimeMs || current.ctimeMs !== original.ctimeMs) throw new Error('agy_transcript_snapshot_changed');
|
|
14
|
+
await assertIdentity(current);
|
|
15
|
+
};
|
|
16
|
+
if (!original.isFile()) throw new Error('agy_transcript_snapshot_changed');
|
|
17
|
+
await unchanged();
|
|
18
|
+
const buffer = Buffer.allocUnsafe(64 * 1024), digest = createHash('sha256');
|
|
19
|
+
let offset = 0, complete = 0, completeHash = digest.copy();
|
|
20
|
+
while (offset < original.size) {
|
|
21
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, original.size - offset), offset);
|
|
22
|
+
if (!bytesRead) throw new Error('agy_transcript_snapshot_changed');
|
|
23
|
+
const chunk = buffer.subarray(0, bytesRead), lastNewline = chunk.lastIndexOf(10);
|
|
24
|
+
if (lastNewline >= 0) {
|
|
25
|
+
digest.update(chunk.subarray(0, lastNewline + 1));
|
|
26
|
+
complete = offset + lastNewline + 1; completeHash = digest.copy();
|
|
27
|
+
digest.update(chunk.subarray(lastNewline + 1));
|
|
28
|
+
} else digest.update(chunk);
|
|
29
|
+
offset += bytesRead;
|
|
30
|
+
await onProgress?.(`digest:${original.dev}:${original.ino}:${original.size}:${original.mtimeMs}:${offset}`);
|
|
31
|
+
}
|
|
32
|
+
const source = { locator, byte_start: 0, byte_end: complete, sha256: completeHash.digest('hex') };
|
|
33
|
+
await unchanged();
|
|
34
|
+
const decoder = new StringDecoder('utf8'); let pending = '', line = 0;
|
|
35
|
+
offset = 0;
|
|
36
|
+
while (offset < complete) {
|
|
37
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, complete - offset), offset);
|
|
38
|
+
if (!bytesRead) throw new Error('agy_transcript_snapshot_changed');
|
|
39
|
+
pending += decoder.write(buffer.subarray(0, bytesRead)); offset += bytesRead;
|
|
40
|
+
await onProgress?.(`records:${source.sha256}:${offset}`);
|
|
41
|
+
let newline;
|
|
42
|
+
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
43
|
+
const text = pending.slice(0, newline); pending = pending.slice(newline + 1); line += 1;
|
|
44
|
+
if (!text.trim()) continue;
|
|
45
|
+
let record;
|
|
46
|
+
try { record = JSON.parse(text); } catch { throw new Error('agy_transcript_invalid_record'); }
|
|
47
|
+
await onRecord(record, { ...source, line });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
pending += decoder.end();
|
|
51
|
+
if (pending) throw new Error('agy_transcript_snapshot_changed');
|
|
52
|
+
await unchanged();
|
|
53
|
+
return { source, partial_tail: complete !== original.size };
|
|
54
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export function recoverDaemonReply(root: string, id: string, options?: { timeoutMs?: number | null; signal?: AbortSignal; binding?: Record<string, unknown>; onProgress?: (value: Record<string, unknown>) => void; onOutcome?: (value: Record<string, unknown>) => void; onPendingSettlement?: (value: Record<string, unknown>) => Promise<void>; observationClock?: import("./observation-clock.mjs").ObservationClock }): Promise<Record<string, unknown>>;
|
|
2
2
|
export function retainedDaemonReply(root: string, id: string, binding?: Record<string, unknown>): Promise<{ ready: boolean; outcome: unknown; value?: unknown; settlement?: Record<string, unknown> | null; reason?: string } | null>;
|
|
3
|
+
export function withDaemonProgressObserver<T>(observer: (value: Record<string, unknown>) => void, action: () => Promise<T>): Promise<T>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
2
4
|
import { readFile } from 'node:fs/promises';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import { hashJson, writeJsonAtomic } from './runner-events.mjs';
|
|
5
7
|
import { ObservationClock } from './observation-clock.mjs';
|
|
6
8
|
import { canonicalNativeOperation, nativeOperationWait, validateDaemonWait } from './adapter-timeouts.mjs';
|
|
7
|
-
import { assertNativeLine
|
|
9
|
+
import { assertNativeLine } from './native-streams.mjs';
|
|
8
10
|
import { operationDirectory, readOperationRecord, readOperationProgress, readOperationWaiting, suspendOperationClock } from './operation-progress.mjs';
|
|
9
11
|
|
|
10
12
|
const productive = new Set(['session.create', 'session.prompt', 'session.start', 'session.fork', 'session.resume']);
|
|
13
|
+
const progressObserver = new AsyncLocalStorage();
|
|
14
|
+
export const withDaemonProgressObserver = (observer, action) => progressObserver.run(observer, action);
|
|
11
15
|
const fault = (code, message, details) => Object.assign(new Error(message), { code, retryable: false, details });
|
|
12
16
|
export async function retainedDaemonReply(root, id, binding = {}) {
|
|
13
17
|
const requested = await readOperationRecord(root, id, 'requested.json');
|
|
@@ -84,9 +88,12 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
|
|
|
84
88
|
validateDaemonWait(harness, request.operation, timeoutMs);
|
|
85
89
|
assertNativeLine(JSON.stringify(request), 'request');
|
|
86
90
|
const durable = productive.has(request.operation);
|
|
91
|
+
const verification = request.operation === 'hook.verify' && request.params?.binding?.native_admission === 'assigned_work_start';
|
|
92
|
+
if (verification) request = { ...request, id: randomUUID() }; // Read-only proof is fresh, never a replay of prior authority.
|
|
93
|
+
const observable = durable || verification, onProgress = progressObserver.getStore();
|
|
87
94
|
const binding = { operation: request.operation, params_sha256: hashJson({ operation: request.operation, params: request.params ?? {} }) };
|
|
88
95
|
let initialDaemon;
|
|
89
|
-
if (
|
|
96
|
+
if (observable) try {
|
|
90
97
|
const state = JSON.parse(await readFile(path.join(stateDir,'daemon.json'),'utf8'));
|
|
91
98
|
initialDaemon = state.daemon_id ?? state.config?.daemonId ?? null;
|
|
92
99
|
binding.owner_id = state.config?.runtime_owner?.owner_id ?? null;
|
|
@@ -119,12 +126,13 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
|
|
|
119
126
|
if (done || polling) return pollFlight; polling = true;
|
|
120
127
|
pollFlight = (async () => {
|
|
121
128
|
try {
|
|
122
|
-
const progress =
|
|
129
|
+
const progress = observable ? await readOperationProgress(stateDir, request.id, { ...binding, ...(frozenDaemon === undefined ? {} : { daemon_id: frozenDaemon }) }) : null;
|
|
123
130
|
if (done) return;
|
|
124
131
|
if (progress && progress.sequence > lastSequence) {
|
|
125
132
|
frozenDaemon ??= progress.daemon_id; lastSequence = progress.sequence;
|
|
126
133
|
binding.generation ??= progress.generation;
|
|
127
134
|
latestProgress = {cursor:progress.sequence,observed_at:Date.parse(progress.observed_at)};
|
|
135
|
+
if (verification && progress.phase === 'verification_progress') onProgress?.(progress);
|
|
128
136
|
process.stderr.write(`${JSON.stringify({ kind: 'operation_progress', ...progress })}\n`);
|
|
129
137
|
}
|
|
130
138
|
if (durable && disconnected) {
|
|
@@ -148,7 +156,7 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
|
|
|
148
156
|
client.on('end', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
|
|
149
157
|
client.on('close', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
|
|
150
158
|
client.on('data', chunk => { void (async () => {
|
|
151
|
-
buffer += chunk;
|
|
159
|
+
buffer += chunk;
|
|
152
160
|
const at = buffer.indexOf('\n'); if (at < 0) return;
|
|
153
161
|
try {
|
|
154
162
|
const response = JSON.parse(buffer.slice(0, at));
|
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, readdir, link, unlink, open } from "node:fs/promises";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { hashJson, writeJsonAtomic } from "./runner-events.mjs";
|
|
7
|
-
import { errorRecord, isObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
7
|
+
import { errorRecord, isNativeHookObservationLoss, isObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
8
8
|
import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation, stopProcessGroup } from "./managed-daemon.mjs";
|
|
9
9
|
import { processSnapshot } from "./process-snapshot.mjs";
|
|
10
10
|
import { NATIVE_SHUTDOWN_CONTRACT, NATIVE_SHUTDOWN_PHASES } from "./native-children.mjs";
|
|
@@ -90,6 +90,12 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
|
|
|
90
90
|
commitDaemonShutdown(root);
|
|
91
91
|
if (reconcileResources && !shutdown.phases.owned_resources) {
|
|
92
92
|
const revision = state.native_execution_revision, generation = dispatchGenerations.get(root) ?? shutdown.generation;
|
|
93
|
+
// A fresh attempt cannot reuse a previous inventory while reconciliation
|
|
94
|
+
// is in flight or after the new attempt fails.
|
|
95
|
+
if (shutdown.phases.native_resources_reconciled) {
|
|
96
|
+
shutdown = { ...shutdown, phases: { ...shutdown.phases, native_resources_reconciled: false } };
|
|
97
|
+
await persist({ shutdown });
|
|
98
|
+
}
|
|
93
99
|
const resources = await reconcileResources();
|
|
94
100
|
const prior = shutdown.result?.native_resources;
|
|
95
101
|
if ((state.daemon_id ?? state.config?.daemonId) !== daemonId || state.native_execution_revision !== revision
|
|
@@ -98,7 +104,9 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
|
|
|
98
104
|
|| resources.generation !== prior?.generation || resources.revision !== prior?.revision || !Array.isArray(resources.entries)
|
|
99
105
|
|| resources.entries.some(item => item.owner_daemon_id !== daemonId || item.generation !== resources.generation))
|
|
100
106
|
throw Object.assign(new Error('native resource reconciliation lost its owner/execution fence'), { code: 'native_resources_unproven' });
|
|
101
|
-
shutdown = { ...shutdown,
|
|
107
|
+
shutdown = { ...shutdown, phases: { ...shutdown.phases, native_resources_reconciled: true },
|
|
108
|
+
native_resource_reconciliation: { daemon_id: daemonId, dispatch_generation: generation, native_revision: revision },
|
|
109
|
+
result: { ...shutdown.result, native_resources: resources } };
|
|
102
110
|
await persist({ shutdown });
|
|
103
111
|
}
|
|
104
112
|
for (const [phase, action] of phases) {
|
|
@@ -249,8 +257,12 @@ export async function inspectDaemonOperation(root, id) {
|
|
|
249
257
|
catch (error) { hookDiagnostics.push({ code: "native_hook_receipt_invalid", message: error.message, request_id: binding.request_id, effect: "unknown", receipt_bound: false }); continue; }
|
|
250
258
|
if (failure?.request_id !== binding.request_id || typeof failure.code !== "string" || typeof failure.message !== "string") continue;
|
|
251
259
|
if (binding.command_sha256 && JSON.stringify(failure.native_hook_binding) !== JSON.stringify(binding)) continue;
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
// AGY can observe a callback while a newer prompt is active without
|
|
261
|
+
// knowing the native creator. Keep its original refusal as diagnostic,
|
|
262
|
+
// never as this operation's primary failure or automatic correction.
|
|
263
|
+
const unconfirmedCreator = binding.native_creator === "unconfirmed";
|
|
264
|
+
const correction = !unconfirmedCreator && isProvenShellCorrection(failure);
|
|
265
|
+
(unconfirmedCreator ? hookDiagnostics : correction ? hookCorrections : isNativeHookObservationLoss(failure) ? hookDiagnostics : hookFailures).push({ ...errorRecord(failure), observed_at: failure.observed_at ?? null, details: { ...failure.details, native_hook_binding: binding, effect: correction ? "no_effect" : "unknown" } });
|
|
254
266
|
}
|
|
255
267
|
hookFailures.sort((a, b) => String(a.observed_at ?? "").localeCompare(String(b.observed_at ?? "")));
|
|
256
268
|
// Admission failure is causal evidence, not proof that the native tree stopped.
|
|
@@ -342,6 +354,8 @@ export async function durableDaemonDispatch(root, request, action, observe) {
|
|
|
342
354
|
return settlement.state === "not_required" || !result || typeof result !== "object" || Array.isArray(result) ? result : { ...result, settlement };
|
|
343
355
|
}
|
|
344
356
|
const control = ["daemon.stop", "session.cancel"].includes(request.operation);
|
|
357
|
+
if (request.operation === 'hook.verify' && request.params?.binding?.native_admission === 'assigned_work_start')
|
|
358
|
+
return await journalDispatch(root, request, () => withOperationProgress(root, request.id, action), false);
|
|
345
359
|
if (!productive.has(request.operation) && !control) return await action();
|
|
346
360
|
const generation = dispatchGenerations.get(root) ?? 0;
|
|
347
361
|
// Once retirement is committed there is no productive dispatch to cancel.
|
|
@@ -397,7 +411,7 @@ export async function durableDaemonStart(options, action) {
|
|
|
397
411
|
return await journalDispatch(root, request, () => action(options));
|
|
398
412
|
}
|
|
399
413
|
|
|
400
|
-
async function journalDispatch(root, request, action) {
|
|
414
|
+
async function journalDispatch(root, request, action, allowReplay = true) {
|
|
401
415
|
const directory = location(root, request.id), fingerprint = hashJson({ operation: request.operation, params: request.params ?? {} });
|
|
402
416
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
403
417
|
// Publish a complete request atomically and exclusively. Writing directly with
|
|
@@ -411,6 +425,7 @@ async function journalDispatch(root, request, action) {
|
|
|
411
425
|
await link(temporary, path.join(directory, "requested.json"));
|
|
412
426
|
} catch (error) {
|
|
413
427
|
if (error.code !== "EEXIST") throw error;
|
|
428
|
+
if (!allowReplay) throw Object.assign(new Error('Native verification requires a fresh observation'), { code: 'operation_conflict' });
|
|
414
429
|
const saved = await inspectDaemonOperation(root, request.id);
|
|
415
430
|
if (saved.params_sha256 !== fingerprint || (saved.invocation_sha256 ?? null) !== (request.invocation_sha256 ?? null)) throw Object.assign(new Error("operation id was reused with different parameters or invocation"), { code: "operation_conflict", details: { operation_id: request.id } });
|
|
416
431
|
if (saved.state === "completed") return saved.result;
|
|
@@ -4,3 +4,4 @@ export declare function qualifyAgyHooks(projectRoot: string, options?: { stateDi
|
|
|
4
4
|
workspaceHooksSha256: string;
|
|
5
5
|
workspaceHooksInstalled: string;
|
|
6
6
|
}>;
|
|
7
|
+
export declare function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
|