@deksden-com/dd-flow-cli 0.9.0-beta.152 → 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 +114 -16
- 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 +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 +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-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 -7
- 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 +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 +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 +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -3,27 +3,29 @@ import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-p
|
|
|
3
3
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
4
4
|
import { operationProgressReporter } from './operation-progress.mjs';
|
|
5
5
|
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
6
|
-
import { inspectAgyNative, stopAgyNativeTree, ownedAgyEndpoint, readAgyNativeQualification, AGY_NATIVE_INSPECTION_CONTRACT } from './agy-native-inspection.mjs';
|
|
6
|
+
import { inspectAgyNative, inspectAgyNativeState, stopAgyNativeTree, ownedAgyEndpoint, readAgyNativeQualification, AGY_NATIVE_INSPECTION_CONTRACT } from './agy-native-inspection.mjs';
|
|
7
|
+
import { agyNativeInventory } from './agy-native-inventory.mjs';
|
|
7
8
|
import { processSnapshot } from './process-snapshot.mjs';
|
|
8
9
|
import { streamRequestKey } from './provider-overload-policy.mjs';
|
|
9
10
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
10
|
-
import { errorRecord
|
|
11
|
-
import { ToolObservations, agyToolObservation,
|
|
11
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
12
|
+
import { ToolObservations, agyToolObservation, createAgyTranscriptObserver } from "./tool-observations.mjs";
|
|
13
|
+
import { readAgyTranscriptSnapshot } from "./agy-transcript-stream.mjs";
|
|
12
14
|
import { constants } from "node:fs";
|
|
13
15
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
15
|
-
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
17
|
+
import { appendFile, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
16
18
|
import { spawn } from "node:child_process";
|
|
17
19
|
import net from "node:net";
|
|
18
20
|
import { isDeepStrictEqual } from "node:util";
|
|
19
21
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
20
|
-
import { hookBudget } from "./native-hook-command.mjs";
|
|
22
|
+
import { hookBudget, retainNativeWorkStart, isStandaloneWorkStart } from "./native-hook-command.mjs";
|
|
23
|
+
import { parseLifecycleCommand } from "./lifecycle-command.mjs";
|
|
21
24
|
import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
22
25
|
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
23
26
|
import path from "node:path";
|
|
24
27
|
import { observeModel } from "./model-observations.mjs";
|
|
25
28
|
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
|
|
26
|
-
import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
|
|
27
29
|
import { agyTerminalEnvelope } from "./agy-terminal-envelope.mjs";
|
|
28
30
|
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
29
31
|
import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
@@ -35,20 +37,22 @@ class DaemonError extends AgyError {}
|
|
|
35
37
|
// classify services. This transcript format has no tool-call IDs: parallel
|
|
36
38
|
// calls therefore retain unknown resources, never FIFO-derived authority.
|
|
37
39
|
export function agyTranscriptFacts(text, source, sessionId) {
|
|
38
|
-
const
|
|
39
|
-
const result = () => ({ children, resources: [...resources.values()] });
|
|
40
|
-
if (!source?.locator || !source?.sha256 || !sessionId) return result();
|
|
41
|
-
const lines = text.split('\n');
|
|
40
|
+
const reducer = createAgyTranscriptFacts(source, sessionId), lines = text.split('\n');
|
|
42
41
|
for (let index = 0; index < lines.length; index++) {
|
|
43
42
|
const line = lines[index];
|
|
44
43
|
if (!line && index === lines.length - 1) continue;
|
|
45
44
|
let event;
|
|
46
|
-
try { event = JSON.parse(line); } catch {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
try { event = JSON.parse(line); } catch { if (index < lines.length - 1) reducer.gap(); break; }
|
|
46
|
+
reducer.add(event);
|
|
47
|
+
}
|
|
48
|
+
return reducer.finish();
|
|
49
|
+
}
|
|
50
|
+
export function createAgyTranscriptFacts(source, sessionId) {
|
|
51
|
+
const children = [], resources = new Map(), calls = [];
|
|
52
|
+
let invalid = false;
|
|
53
|
+
function add(event) {
|
|
54
|
+
if (invalid || !source?.locator || !source?.sha256 || !sessionId) return;
|
|
55
|
+
if (!event || typeof event !== 'object' || Array.isArray(event) || event.conversation_id && event.conversation_id !== sessionId) { invalid = true; return; }
|
|
52
56
|
const content = typeof event.content === 'string' ? event.content : '';
|
|
53
57
|
const native_source = { ...source, step_index: event.step_index };
|
|
54
58
|
if (event.source === 'SYSTEM' && event.type === 'SYSTEM_MESSAGE' && event.status === 'DONE') {
|
|
@@ -57,11 +61,11 @@ export function agyTranscriptFacts(text, source, sessionId) {
|
|
|
57
61
|
const prior = resources.get(terminal.task_id);
|
|
58
62
|
if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped', ...(terminal.command_outcome ? { native_outcome: terminal.command_outcome } : {}), terminal_source: native_source }));
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
return;
|
|
61
65
|
}
|
|
62
|
-
if (event.source !== 'MODEL')
|
|
63
|
-
if (event.type === 'PLANNER_RESPONSE') { calls.length = 0; if (Array.isArray(event.tool_calls) && event.tool_calls.length === 1) calls.push(event.tool_calls[0]);
|
|
64
|
-
if (event.type !== 'GENERIC')
|
|
66
|
+
if (event.source !== 'MODEL') return;
|
|
67
|
+
if (event.type === 'PLANNER_RESPONSE') { calls.length = 0; if (Array.isArray(event.tool_calls) && event.tool_calls.length === 1) calls.push(event.tool_calls[0]); return; }
|
|
68
|
+
if (event.type !== 'GENERIC') return;
|
|
65
69
|
const call = calls.shift();
|
|
66
70
|
if (call?.name === 'manage_task' && event.status === 'DONE') {
|
|
67
71
|
const terminal = agyTerminalEnvelope(event, call), prior = resources.get(terminal?.task_id);
|
|
@@ -69,22 +73,22 @@ export function agyTranscriptFacts(text, source, sessionId) {
|
|
|
69
73
|
}
|
|
70
74
|
if (call?.name === 'invoke_subagent' && event.status === 'DONE') {
|
|
71
75
|
const match = content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nCreated the following subagent:\n(\{[\s\S]*?\})\nThe subagent will send you a message/);
|
|
72
|
-
if (!match)
|
|
76
|
+
if (!match) return;
|
|
73
77
|
try {
|
|
74
78
|
const child = JSON.parse(match[1]);
|
|
75
79
|
if (/^[\w-]+$/.test(child.conversationId) && typeof child.logAbsoluteUri === 'string') children.push({ provider_session_id: child.conversationId, parent_provider_session_id: sessionId, log_uri: child.logAbsoluteUri, workspace_uris: child.workspaceUris ?? [], status: 'unknown', native_source });
|
|
76
80
|
} catch { /* No partial/guessed native edge. */ }
|
|
77
81
|
}
|
|
78
|
-
if (event.status !== 'RUNNING')
|
|
82
|
+
if (event.status !== 'RUNNING') return;
|
|
79
83
|
const task = content.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
|
|
80
|
-
if (!task || !task[1].startsWith(`${sessionId}/`))
|
|
84
|
+
if (!task || !task[1].startsWith(`${sessionId}/`)) return;
|
|
81
85
|
let daemon = call?.name === 'run_command' ? call.args?.IsDaemon : null;
|
|
82
86
|
if (typeof daemon === 'string') { try { daemon = JSON.parse(daemon); } catch { daemon = null; } }
|
|
83
|
-
const resource = { resource_id: task[1], provider_session_id: sessionId, classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
|
|
87
|
+
const resource = { resource_id: task[1], provider_session_id: sessionId, ...(call?.name === 'schedule' ? { kind: 'schedule' } : {}), classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
|
|
84
88
|
...(typeof daemon === 'boolean' ? { native_classification: { tool: 'run_command', IsDaemon: daemon } } : {}) };
|
|
85
89
|
resources.set(resource.resource_id, mergeAgyResource(resources.get(resource.resource_id), resource));
|
|
86
90
|
}
|
|
87
|
-
return
|
|
91
|
+
return { add, gap: () => { invalid = true; }, finish: () => ({ children, resources: [...resources.values()], ...(invalid ? { gap: 'agy_transcript_invalid_record' } : {}) }) };
|
|
88
92
|
}
|
|
89
93
|
export function mergeAgyResource(prior, next) {
|
|
90
94
|
if (!prior) return next;
|
|
@@ -104,7 +108,7 @@ export function mergeAgyResource(prior, next) {
|
|
|
104
108
|
return { ...prior, ...next };
|
|
105
109
|
}
|
|
106
110
|
export const agyTranscriptResources = (text, source, sessionId) => agyTranscriptFacts(text, source, sessionId).resources;
|
|
107
|
-
export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null) {
|
|
111
|
+
export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null, onProgress = null) {
|
|
108
112
|
const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
|
|
109
113
|
let handle;
|
|
110
114
|
try {
|
|
@@ -117,15 +121,20 @@ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ow
|
|
|
117
121
|
handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
118
122
|
const stat = await handle.stat();
|
|
119
123
|
if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
let observer, inspector;
|
|
125
|
+
const snapshot = await readAgyTranscriptSnapshot(handle, { locator: child.log_uri, onProgress,
|
|
126
|
+
assertIdentity: async current => { const pathStat = await lstat(expected); if (!pathStat.isFile() || current.dev !== pathStat.dev || current.ino !== pathStat.ino || await realpath(expected) !== canonical) throw new Error('native locator changed during read'); },
|
|
127
|
+
onRecord: (record, { line, ...source }) => {
|
|
128
|
+
observer ??= createAgyTranscriptObserver(child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
|
|
129
|
+
inspector ??= inspect?.(source);
|
|
130
|
+
observer.add(record, line); inspector?.add(record);
|
|
131
|
+
} });
|
|
132
|
+
observer ??= createAgyTranscriptObserver(child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, snapshot.source);
|
|
133
|
+
if (!snapshot.partial_tail) inspector?.finish();
|
|
134
|
+
const events = observer.finish();
|
|
135
|
+
if (snapshot.partial_tail) events.push({ session, reason: "agy_transcript_partial_tail" });
|
|
127
136
|
return events;
|
|
128
|
-
} catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
|
|
137
|
+
} catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : error.message === 'agy_transcript_invalid_record' ? 'agy_transcript_invalid_record' : "agy_transcript_locator_rejected" }]; }
|
|
129
138
|
finally { await handle?.close(); }
|
|
130
139
|
}
|
|
131
140
|
const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
|
|
@@ -141,6 +150,7 @@ async function readState(dir) { try { return JSON.parse(await readFile(locations
|
|
|
141
150
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
142
151
|
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
143
152
|
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) env[key] = undefined; return managedAgyEnv({ ...env, ...managedFlowEnv(config) }); }
|
|
153
|
+
|
|
144
154
|
const errorPayload = errorRecord;
|
|
145
155
|
async function forwardUsage(config, sessionId, result, tools, descendants) {
|
|
146
156
|
if (!config.ddFlowBin || !config.projectRoot) return;
|
|
@@ -328,7 +338,7 @@ export class Runtime {
|
|
|
328
338
|
if (!current) return;
|
|
329
339
|
const key = createHash("sha256").update(JSON.stringify({ conversation_id: result.conversation_id ?? this.init?.conversation_id, status: result.status, num_turns: result.num_turns, error: result.error, response: result.response ?? result.text })).digest("hex");
|
|
330
340
|
current.terminalProgress ??= new Set();
|
|
331
|
-
if (current.terminalProgress.has(key)
|
|
341
|
+
if (current.terminalProgress.has(key)) return;
|
|
332
342
|
current.terminalProgress.add(key); this.markActivity();
|
|
333
343
|
}
|
|
334
344
|
observeProviderActivity(event) {
|
|
@@ -353,8 +363,7 @@ export class Runtime {
|
|
|
353
363
|
const snapshots = prior?.index === index ? prior.seen : new Set();
|
|
354
364
|
if (snapshots.has(fingerprint)) return;
|
|
355
365
|
// Native step snapshots can alternate/replay; scope retained dedup to the
|
|
356
|
-
// current step.
|
|
357
|
-
if (snapshots.size >= 1_024) return;
|
|
366
|
+
// current step. Unique later updates remain useful activity at any size.
|
|
358
367
|
snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.invalidateSettlement(id); this.markActivity();
|
|
359
368
|
}
|
|
360
369
|
async start(sessionId = null) {
|
|
@@ -463,7 +472,7 @@ export class Runtime {
|
|
|
463
472
|
}
|
|
464
473
|
}
|
|
465
474
|
toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
|
|
466
|
-
async captureChildTools() {
|
|
475
|
+
async captureChildTools(onProgress = null) {
|
|
467
476
|
this.capturedToolFacts ??= new Map();
|
|
468
477
|
this.nativeResources ??= new Map(this.state.native_resources_entries?.map(item => [item.resource_id, item]) ?? []);
|
|
469
478
|
const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
|
|
@@ -478,8 +487,10 @@ export class Runtime {
|
|
|
478
487
|
ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
|
|
479
488
|
}
|
|
480
489
|
const events = parent === root && root
|
|
481
|
-
? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors],
|
|
482
|
-
const
|
|
490
|
+
? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors], source => {
|
|
491
|
+
const reducer = createAgyTranscriptFacts(source, child.provider_session_id);
|
|
492
|
+
return { add: reducer.add, finish: () => {
|
|
493
|
+
const facts = reducer.finish();
|
|
483
494
|
for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, mergeAgyResource(this.nativeResources.get(resource.resource_id), resource));
|
|
484
495
|
if (facts.gap) { this.state.native_topology_error = facts.gap; this.invalidateSettlement(root); }
|
|
485
496
|
for (const discovered of facts.children) {
|
|
@@ -497,7 +508,8 @@ export class Runtime {
|
|
|
497
508
|
this.descendants.set(value.provider_session_id, value);
|
|
498
509
|
queue.push(value);
|
|
499
510
|
}
|
|
500
|
-
|
|
511
|
+
} };
|
|
512
|
+
}, onProgress && (cursor => onProgress(`${child.provider_session_id}:${cursor}`)))
|
|
501
513
|
: [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
|
|
502
514
|
for (const event of events) {
|
|
503
515
|
this.toolObservations.add(event);
|
|
@@ -542,10 +554,7 @@ export class Runtime {
|
|
|
542
554
|
return;
|
|
543
555
|
}
|
|
544
556
|
this.lastResult = result; this.markTerminalActivity(result);
|
|
545
|
-
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
546
|
-
const pendingTerminal = current && result.status === "SUCCESS";
|
|
547
557
|
if (current) { current.terminalResult = result; current.terminalMetadata = terminalMetadata; current.terminalPersisted = false; }
|
|
548
|
-
if (pendingTerminal && rootStop?.source_bound === true && rootStop.fullyIdle === true) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" });
|
|
549
558
|
this.state.terminal_observation_pending = true;
|
|
550
559
|
try {
|
|
551
560
|
await this.persist({ active_tree: true, last_result: result, last_terminal_metadata: terminalMetadata });
|
|
@@ -571,14 +580,6 @@ export class Runtime {
|
|
|
571
580
|
if (this.active !== current) return;
|
|
572
581
|
this.active = null;
|
|
573
582
|
const settled = this.receipt().settled;
|
|
574
|
-
if (result.status === "ERROR" && settled && rootStop?.fullyIdle === true && !this.state.last_hook_rejection && current.operationId && this.paths.runtime) {
|
|
575
|
-
const root = this.init.conversation_id, transcript = path.join(this.paths.runtime, "brain", root, ".system_generated", "logs", "transcript.jsonl");
|
|
576
|
-
await captureAgyTranscript(this.paths.runtime, { provider_session_id: root, parent_provider_session_id: root, log_uri: new URL(`file://${transcript}`).href }, root, [root], (text, source) => {
|
|
577
|
-
const boundary = recoveredAgyErrorBoundary(text, result.error, current.stepFloor, source, root);
|
|
578
|
-
const hook = current.nativeHookEvents?.get(boundary?.invocation_step ?? boundary?.boundary_step);
|
|
579
|
-
if (boundary && hook) terminalMetadata.recovered_error_boundary = { ...boundary, receipt: { ...boundary.receipt, native_hook_event_key: hook, daemon_instance: this.config.daemonId }, source };
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
583
|
this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, native_turn_items: { observed: settled, possible_effects: true, pending: !settled }, provider_error_scope: "terminal_result", usage_ingest: this.usageIngest });
|
|
583
584
|
try { await this.persist({ active_tree: !settled, last_terminal_failure: errorRecord(this.lastTerminalFailure) }); }
|
|
584
585
|
catch (error) { return this.failTerminalObservation(current, result, terminalMetadata, error); }
|
|
@@ -602,6 +603,17 @@ export class Runtime {
|
|
|
602
603
|
this.active = null;
|
|
603
604
|
current.resolve(receipt);
|
|
604
605
|
}
|
|
606
|
+
reconcilePendingHook(current) {
|
|
607
|
+
if (!current || this.active !== current) return Promise.resolve();
|
|
608
|
+
if (current.hookReconciliation) return current.hookReconciliation;
|
|
609
|
+
if (!current.pendingHookReconciliation) return Promise.resolve();
|
|
610
|
+
current.pendingHookReconciliation = false;
|
|
611
|
+
current.hookReconciliation = this.completePendingTurn().catch(error => {
|
|
612
|
+
current.observationError ??= error;
|
|
613
|
+
if (this.active === current) { this.drainingError ??= error; current.reject(error); }
|
|
614
|
+
}).finally(() => { current.hookReconciliation = null; });
|
|
615
|
+
return current.hookReconciliation;
|
|
616
|
+
}
|
|
605
617
|
failTerminalObservation(current, result, metadata, cause) {
|
|
606
618
|
this.state.terminal_observation_pending = true;
|
|
607
619
|
const details = { ...metadata, native_outcome: result, settled: false, terminal_persisted: current?.terminalPersisted === true, observation_error: errorRecord(cause), native_turn_items: { observed: false, possible_effects: true, pending: true } };
|
|
@@ -712,7 +724,6 @@ export class Runtime {
|
|
|
712
724
|
// proof (or verified prior clean retirement before new activity) closes it.
|
|
713
725
|
const retained = this.state.retained_tree_settlement;
|
|
714
726
|
const rootSettled = this.nativeProof?.turn_generation === this.turnGeneration && this.nativeProof.connection_id === this.connectionId && this.nativeProof.revision === (this.nativeRevision ?? 0)
|
|
715
|
-
|| observation?.stop?.source_bound === true && observation.stop?.fullyIdle === true && observation.stop.generation === this.turnGeneration
|
|
716
727
|
|| retained?.evidence === "verified_clean_daemon_shutdown" && retained.session_id === root && retained.turn_generation === this.turnGeneration && !this.child;
|
|
717
728
|
const terminalPending = this.active?.terminalResult === result;
|
|
718
729
|
// A positively inspected owned scope before its first Turn has no model
|
|
@@ -724,7 +735,8 @@ export class Runtime {
|
|
|
724
735
|
if (clock) clock.sample(this.progress);
|
|
725
736
|
const exhausted = clock && (clock.observationLost || clock.elapsed >= clock.timeoutMs);
|
|
726
737
|
const nativeSettlement = { confirmed: Boolean(settled), state: settled ? 'ready' : exhausted ? 'unproven' : this.nativeInspection?.state ?? 'unproven', reason: settled ? 'current_root_idle_proven' : exhausted ? 'observation_budget_exhausted' : this.nativeInspection?.reason ?? 'current_root_idle_proof_missing', capability: AGY_NATIVE_INSPECTION_CONTRACT, operation_id: terminalMetadata?.request_proof?.operation_id, request_key: streamRequestKey(terminalMetadata?.request_proof), provider_session_id: root, daemon_instance: this.config.daemonId, connection_epoch: this.connectionId, generation: this.turnGeneration, ...(this.nativeProof ? { source: this.nativeProof.source, method: this.nativeProof.method, proof_sha256: createHash('sha256').update(JSON.stringify(this.nativeProof)).digest('hex') } : {}), ...(clock ? { observation_clock: clock.state() } : {}) };
|
|
727
|
-
|
|
738
|
+
const hookDiagnostics = (this.state.native_hook_diagnostics ?? []).filter(item => item.turn_generation === this.turnGeneration);
|
|
739
|
+
return nativeExecutionReceipt({ harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(terminalMetadata ? { terminal_metadata: terminalMetadata, ...(terminalMetadata.turn_id ? { terminal_turn_id: terminalMetadata.turn_id } : {}) } : {}), ...(assistantText === null ? {} : { assistant_text: assistantText }), ...(hookDiagnostics.length ? { native_hook_diagnostics: hookDiagnostics } : {}), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], ...settledNativeTree(root, [...this.descendants.values()], settled, !this.state.unclaimed_activity && (!this.active || terminalPending) && rootSettled && ["SUCCESS", "ERROR", "CANCELLED"].includes(result?.status)), native_turn_items: { observed: Boolean(terminalMetadata?.request_proof), possible_effects: true, pending: !settled, settled, ...(terminalMetadata?.request_proof ? { request_proof: terminalMetadata.request_proof } : {}) }, transcript_path: observation?.transcript_path ?? null, settled, native_settled: settled, physical_stopped: this.exited === true, native_settlement: nativeSettlement, }, { daemonId: this.config.daemonId, generation: this.turnGeneration, revision: this.nativeRevision ?? 0, resources: { coverage: "partial", entries: [...(this.nativeResources?.values() ?? this.state.native_resources_entries ?? [])], ...(this.state.native_retirement_notifications?.length ? { pending_notifications: this.state.native_retirement_notifications } : {}) } });
|
|
728
740
|
}
|
|
729
741
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
730
742
|
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
|
|
@@ -735,14 +747,13 @@ export class Runtime {
|
|
|
735
747
|
if (!this.nativeObserver && !this.nativeQualification?.qualified) throw new DaemonError('agy_native_inspection_unqualified', 'AGY whole-tree inspection requires a matching native qualification receipt before productive dispatch', false, { capability: AGY_NATIVE_INSPECTION_CONTRACT });
|
|
736
748
|
await this.reconcileRetainedAdmission();
|
|
737
749
|
if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
|
|
738
|
-
if (this.state.
|
|
739
|
-
if (this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
|
|
750
|
+
if (this.state.unclaimed_activity || ((this.state.active_tree || this.lastResult) && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
|
|
740
751
|
let resolveTurn, rejectTurn;
|
|
741
752
|
const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
|
|
742
753
|
// Cancellation can reject while admission persistence is still pending.
|
|
743
754
|
void reply.catch(() => {});
|
|
744
755
|
this.connectionId ??= randomUUID();
|
|
745
|
-
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false };
|
|
756
|
+
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false, guard: assertDispatch };
|
|
746
757
|
this.nativeAbort?.abort(); this.nativeProof = null; this.nativeInspection = null;
|
|
747
758
|
current.clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal('observation_gap', { gap, operation_id: operationId }).catch(() => {}); } });
|
|
748
759
|
this.observationEpisode = current;
|
|
@@ -756,11 +767,15 @@ export class Runtime {
|
|
|
756
767
|
// Reserve before persistence so different operation IDs cannot overlap.
|
|
757
768
|
current.publishProgress = operationProgressReporter();
|
|
758
769
|
this.active = current;
|
|
759
|
-
this.
|
|
770
|
+
this.lastNativeHookOperation = current;
|
|
771
|
+
// A callback's failure receipt may arrive after its Turn has returned.
|
|
772
|
+
// Keep its accepted request identity; it is an observation binding, not
|
|
773
|
+
// proof of which native Turn originally produced the callback.
|
|
774
|
+
this.hookRequests ??= new Map();
|
|
760
775
|
const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
|
|
761
776
|
current.stepFloor = prior.last_step_index ?? -1;
|
|
762
777
|
this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
|
|
763
|
-
this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new
|
|
778
|
+
this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Map(); this.progress = undefined;
|
|
764
779
|
try {
|
|
765
780
|
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
766
781
|
assertDispatch();
|
|
@@ -783,7 +798,15 @@ export class Runtime {
|
|
|
783
798
|
const clock = current.clock;
|
|
784
799
|
let checking = false;
|
|
785
800
|
const timer = setInterval(() => {
|
|
801
|
+
if (this.active === current && [...(this.hookRequests?.values() ?? [])].some(binding => binding.current && binding.current !== current)) {
|
|
802
|
+
void this.observeHookFailures().catch(() => { /* Retry the same retained request on the next observation. */ });
|
|
803
|
+
}
|
|
786
804
|
if (this.active === current && current.terminalResult && this.nativeInspection?.state === 'inspection_pending' && !this.nativeFlight) this.queueNativeInspection();
|
|
805
|
+
if (this.active === current && current.pendingHookReconciliation && !checking) {
|
|
806
|
+
checking = true;
|
|
807
|
+
void this.reconcilePendingHook(current).finally(() => { checking = false; });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
787
810
|
if (this.active !== current || checking || !clock.sample(this.progress)) return;
|
|
788
811
|
checking = true; const cursor = this.progress?.cursor;
|
|
789
812
|
void this.completePendingTurn().then(() => {
|
|
@@ -856,56 +879,169 @@ export class Runtime {
|
|
|
856
879
|
if (observation?.stop) this.sessionObservations.set(id, { ...observation, stop: null });
|
|
857
880
|
}
|
|
858
881
|
}
|
|
859
|
-
|
|
882
|
+
qualifiedChildForHook(conversationId) {
|
|
883
|
+
// A fully source-bound in-memory ancestry is already the result of the
|
|
884
|
+
// observation loop. Avoid re-reading every transcript on each child hook.
|
|
885
|
+
const retained = this.descendants.get(conversationId), seenRetained = new Set();
|
|
886
|
+
let node = retained, expected = conversationId, qualified = Boolean(node && !this.state.native_topology_error);
|
|
887
|
+
while (qualified) {
|
|
888
|
+
const source = node?.native_source, parent = node?.parent_provider_session_id;
|
|
889
|
+
let sourceBound = source?.kind === 'agy.step_update.subagent_info'
|
|
890
|
+
&& source.parent_provider_session_id === parent && Number.isSafeInteger(source.step_index) && source.step_index >= 0;
|
|
891
|
+
if (!sourceBound && typeof source?.locator === 'string' && /^[a-f0-9]{64}$/.test(source?.sha256 ?? '') && typeof parent === 'string') {
|
|
892
|
+
try {
|
|
893
|
+
const locator = fileURLToPath(source.locator);
|
|
894
|
+
sourceBound = locator.endsWith(path.sep + path.join('brain', parent, '.system_generated', 'logs', 'transcript.jsonl'));
|
|
895
|
+
} catch { /* An invalid locator never grants a fast-path identity. */ }
|
|
896
|
+
}
|
|
897
|
+
if (!node || node.provider_session_id !== expected || !sourceBound || !parent || seenRetained.has(expected)
|
|
898
|
+
|| source.parent_provider_session_id && source.parent_provider_session_id !== parent) { qualified = false; break; }
|
|
899
|
+
seenRetained.add(node.provider_session_id);
|
|
900
|
+
if (parent === this.init?.conversation_id) break;
|
|
901
|
+
expected = parent;
|
|
902
|
+
node = this.descendants.get(parent);
|
|
903
|
+
if (!node) qualified = false;
|
|
904
|
+
}
|
|
905
|
+
return qualified ? retained : null;
|
|
906
|
+
}
|
|
907
|
+
async childForHook(conversationId, onProgress = null) {
|
|
860
908
|
if (conversationId === this.init?.conversation_id) return null;
|
|
861
|
-
|
|
909
|
+
const retained = this.qualifiedChildForHook(conversationId);
|
|
910
|
+
if (retained) return retained;
|
|
911
|
+
await this.captureChildTools(onProgress);
|
|
862
912
|
const deadline = Date.now() + 1_000;
|
|
863
|
-
while (!this.descendants.has(conversationId) && Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 25)); await this.captureChildTools(); }
|
|
913
|
+
while (!this.descendants.has(conversationId) && Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 25)); await this.captureChildTools(onProgress); }
|
|
864
914
|
const child = this.descendants.get(conversationId);
|
|
865
915
|
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
866
916
|
const seen = new Set([conversationId]); let current = child;
|
|
867
|
-
|
|
917
|
+
for (;;) {
|
|
918
|
+
if (this.state.native_topology_error || current.native_source?.parent_provider_session_id && current.native_source.parent_provider_session_id !== current.parent_provider_session_id)
|
|
919
|
+
throw new DaemonError("agy_child_parent_unqualified", "Antigravity native ancestry is contradictory");
|
|
920
|
+
if (current.parent_provider_session_id === this.init?.conversation_id) break;
|
|
868
921
|
const parent = this.descendants.get(current.parent_provider_session_id);
|
|
869
|
-
if (
|
|
922
|
+
if (!current.native_source || !parent || seen.has(parent.provider_session_id)) throw new DaemonError("agy_child_parent_unqualified", "Antigravity worker has no source-bound ancestry to the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: current.parent_provider_session_id });
|
|
870
923
|
seen.add(parent.provider_session_id); current = parent;
|
|
871
924
|
}
|
|
872
925
|
if (this.state.native_topology_error) throw new DaemonError("agy_child_parent_unqualified", "Antigravity native ancestry is contradictory");
|
|
873
926
|
return child;
|
|
874
927
|
}
|
|
875
|
-
async
|
|
876
|
-
if (
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
928
|
+
async registerWorkStart(event, payload, callbackRequestId = null) {
|
|
929
|
+
if (event !== "PreToolUse" || !this.config.runtime_owner || !this.config.ddFlowBin || !this.init?.conversation_id
|
|
930
|
+
|| typeof payload.conversationId !== "string" || !payload.conversationId
|
|
931
|
+
|| !Number.isSafeInteger(payload.stepIdx) || payload.stepIdx < 0
|
|
932
|
+
|| payload.executionNum !== undefined && (!Number.isSafeInteger(payload.executionNum) || payload.executionNum < 0)
|
|
933
|
+
|| !["run_command", "run_terminal_command", "RunTerminalCommand"].includes(payload.toolCall?.name)) return null;
|
|
934
|
+
const input = payload.toolCall?.args ?? {}, command = input.command ?? input.cmd ?? input.CommandLine;
|
|
935
|
+
if (!isStandaloneWorkStart(command)) return null;
|
|
936
|
+
assertDaemonDispatch(this.paths.dir);
|
|
937
|
+
const tool = createHash("sha256").update(JSON.stringify([this.config.daemonId, payload.conversationId, payload.stepIdx, payload.executionNum ?? null, event])).digest("hex");
|
|
938
|
+
this.retainedWorkStarts ??= new Map();
|
|
939
|
+
this.retainedWorkActivity ??= new Set();
|
|
940
|
+
if (!this.retainedWorkActivity.has(tool)) {
|
|
941
|
+
this.retainedWorkActivity.add(tool);
|
|
942
|
+
// A callback is activity even before its ancestry is published. It
|
|
943
|
+
// invalidates old idle, but cannot identify or finish a creator prompt.
|
|
944
|
+
this.childActivity(payload.conversationId);
|
|
888
945
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
946
|
+
return await retainNativeWorkStart(this.paths.dir, { daemon_id: this.config.daemonId, source_harness: "antigravity-cli",
|
|
947
|
+
root_provider_session_id: this.init.conversation_id, provider_session_id: payload.conversationId, parent_provider_session_id: null,
|
|
948
|
+
tool_call_id: tool, event, native_command: command, native_cwd: this.config.cwd,
|
|
949
|
+
...(callbackRequestId ? { callback_request_id: callbackRequestId } : {}),
|
|
950
|
+
native_payload: { schema_id: "dd-flow/agy-tool-event@1", event_id: tool, phase: "before", daemon_id: this.config.daemonId,
|
|
951
|
+
launch_cwd: this.config.cwd, conversation_id: payload.conversationId, execution_num: payload.executionNum, step_index: payload.stepIdx,
|
|
952
|
+
tool: payload.toolCall.name, input, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath,
|
|
953
|
+
profile: payload.model ? observedProfile(payload) : {} } }, this.active ?? this.lastNativeHookOperation, this.retainedWorkStarts);
|
|
954
|
+
}
|
|
955
|
+
async verifyWorkStart(params) {
|
|
956
|
+
const retained = this.retainedWorkStarts?.get(params.requestId);
|
|
957
|
+
if (!retained || !isDeepStrictEqual(retained.binding, params.binding)) throw new DaemonError("native_hook_unproven", "Unknown native Work start callback");
|
|
958
|
+
retained.guard(); assertDaemonDispatch(this.paths.dir);
|
|
959
|
+
if (retained.binding.root_provider_session_id !== this.init?.conversation_id) throw new DaemonError("native_hook_unproven", "Native Work start root changed");
|
|
960
|
+
const publish = operationProgressReporter();
|
|
961
|
+
const child = await this.childForHook(retained.binding.provider_session_id,
|
|
962
|
+
cursor => publish('verification_progress', { native_request_id: cursor, session_id: retained.binding.provider_session_id }));
|
|
963
|
+
if (retained.binding.provider_session_id !== this.init.conversation_id && this.qualifiedChildForHook(retained.binding.provider_session_id) !== child)
|
|
964
|
+
throw new DaemonError("native_hook_unproven", "Native Work start child has no source-bound ancestry");
|
|
965
|
+
retained.guard();
|
|
966
|
+
const parent = child?.parent_provider_session_id ?? null;
|
|
967
|
+
const verified = { ...retained.binding, parent_provider_session_id: parent, native_ancestry: "confirmed",
|
|
968
|
+
native_payload: { ...retained.binding.native_payload, parent_conversation_id: parent } };
|
|
969
|
+
if (retained.binding.callback_request_id) this.hookRequests?.delete(retained.binding.callback_request_id);
|
|
970
|
+
return { verified: true, registeredHookBinding: retained.binding, nativeHookBinding: verified };
|
|
971
|
+
}
|
|
972
|
+
async recordUnconfirmedHookFailure(error, event, payload, observer, requestId = null) {
|
|
973
|
+
// The request was observed while this operation was active, but AGY does
|
|
974
|
+
// not expose the native creator Turn. Preserve the refusal without
|
|
975
|
+
// attributing it to (or aborting) this potentially unrelated prompt.
|
|
976
|
+
const key = typeof requestId === 'string' && /^[A-Za-z0-9-]{1,128}$/.test(requestId) ? requestId : null;
|
|
977
|
+
const scope = key && JSON.stringify([key, observer?.operationId ?? null, observer?.generation ?? null]);
|
|
978
|
+
this.unconfirmedHookFlights ??= new Map();
|
|
979
|
+
if (scope && this.unconfirmedHookFlights.has(scope)) return await this.unconfirmedHookFlights.get(scope);
|
|
980
|
+
const flight = (async () => {
|
|
981
|
+
let diagnostic = key && (this.state.native_hook_diagnostics ?? []).find(item => item.request_id === key
|
|
982
|
+
&& item.operation_id === (observer?.operationId ?? null) && item.turn_generation === (observer?.generation ?? null));
|
|
983
|
+
try {
|
|
984
|
+
if (!diagnostic) {
|
|
985
|
+
diagnostic = { code: 'native_hook_failed', message: 'Native hook failed without a verified creator Turn',
|
|
986
|
+
...(key ? { request_id: key } : {}), primary_error: errorRecord(error), event, conversation_id: payload.conversationId ?? null,
|
|
987
|
+
daemon_id: this.config.daemonId, operation_id: observer?.operationId ?? null,
|
|
988
|
+
turn_generation: observer?.generation ?? null, native_creator: 'unconfirmed' };
|
|
989
|
+
// Do not grow in-memory state when durable journal writing fails.
|
|
990
|
+
await this.journal('unconfirmed_hook_failure', diagnostic);
|
|
991
|
+
this.state.native_hook_diagnostics = [...(this.state.native_hook_diagnostics ?? []), diagnostic];
|
|
992
|
+
}
|
|
993
|
+
await this.persist(); return true;
|
|
994
|
+
} catch { return false; /* Optional diagnostic cannot erase a native operation. */ }
|
|
995
|
+
})();
|
|
996
|
+
if (scope) this.unconfirmedHookFlights.set(scope, flight);
|
|
997
|
+
try { return await flight; }
|
|
998
|
+
finally { if (scope && this.unconfirmedHookFlights.get(scope) === flight) this.unconfirmedHookFlights.delete(scope); }
|
|
894
999
|
}
|
|
895
1000
|
async observeHookFailures() {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this.hookRequests
|
|
903
|
-
|
|
904
|
-
|
|
1001
|
+
if (!this.hookRequests?.size) return;
|
|
1002
|
+
if (this.hookFailuresFlight) return await this.hookFailuresFlight;
|
|
1003
|
+
const flight = (async () => {
|
|
1004
|
+
let names;
|
|
1005
|
+
try { names = new Set(await readdir(path.join(this.paths.dir, "hook-failures"))); }
|
|
1006
|
+
catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
1007
|
+
for (const [requestId, binding] of this.hookRequests ?? []) {
|
|
1008
|
+
if (!binding.current || !names.has(`${requestId}.json`)) continue;
|
|
1009
|
+
let failure;
|
|
1010
|
+
try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
|
|
1011
|
+
catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
|
|
1012
|
+
if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
|
|
1013
|
+
if (binding.current !== this.active) {
|
|
1014
|
+
if (failure.request_id !== requestId) continue;
|
|
1015
|
+
// The callback request was accepted while this observer operation was
|
|
1016
|
+
// active. AGY has no native creator ID, so this is diagnostic only:
|
|
1017
|
+
// never mutate either the old receipt or the current Turn.
|
|
1018
|
+
await this.journal("late_hook_receipt", { request_id: requestId, daemon_id: this.config.daemonId,
|
|
1019
|
+
observer_operation_id: binding.current.operationId ?? null, observer_turn_generation: binding.current.generation ?? null,
|
|
1020
|
+
native_creator: "unconfirmed", event: binding.event, failure: errorRecord(failure) });
|
|
1021
|
+
this.hookRequests.delete(requestId);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const saved = await this.recordUnconfirmedHookFailure(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current, requestId);
|
|
1025
|
+
if (saved) this.hookRequests.delete(requestId);
|
|
1026
|
+
}
|
|
1027
|
+
})();
|
|
1028
|
+
this.hookFailuresFlight = flight;
|
|
1029
|
+
try { await flight; } finally { if (this.hookFailuresFlight === flight) this.hookFailuresFlight = null; }
|
|
905
1030
|
}
|
|
906
1031
|
async observeHook(event, payload, budget = null) {
|
|
1032
|
+
const hookOwner = this.active, hookGeneration = this.turnGeneration;
|
|
907
1033
|
const conversationId = payload.conversationId ?? this.init?.conversation_id;
|
|
908
|
-
const
|
|
1034
|
+
const shell = ["run_command", "run_terminal_command", "RunTerminalCommand"].includes(payload.toolCall?.name);
|
|
1035
|
+
const args = payload.toolCall?.args ?? {}, command = args.command ?? args.cmd ?? args.CommandLine;
|
|
1036
|
+
const observationOnly = Boolean(this.config.runtime_owner && this.config.ddFlowBin && (event === "Stop" || !shell
|
|
1037
|
+
|| typeof command === "string" && parseLifecycleCommand(command).participation === "none"));
|
|
1038
|
+
// Ordinary native facts cannot grant Work authority. Keep them cheap;
|
|
1039
|
+
// independent observation verifies ancestry/idle after the callback ACK.
|
|
1040
|
+
const child = observationOnly ? this.qualifiedChildForHook(conversationId) : await this.childForHook(conversationId); budget?.remaining("identity");
|
|
1041
|
+
if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) {
|
|
1042
|
+
await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, reason: "owner_changed_during_identity" });
|
|
1043
|
+
return { stale: true };
|
|
1044
|
+
}
|
|
909
1045
|
const prior = this.sessionObservations.get(conversationId) ?? {};
|
|
910
1046
|
const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null;
|
|
911
1047
|
const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null;
|
|
@@ -915,37 +1051,52 @@ export class Runtime {
|
|
|
915
1051
|
if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; }
|
|
916
1052
|
const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex");
|
|
917
1053
|
if (event === "Stop" && prior.last_stop_hook === fingerprint && prior.last_stop_generation === this.turnGeneration) return { stale: true };
|
|
918
|
-
this.progressHooks ??= new
|
|
1054
|
+
this.progressHooks ??= new Map();
|
|
919
1055
|
const scoped = this.active && (!child || child.activity_generation === this.active.generation);
|
|
920
|
-
|
|
921
|
-
|
|
1056
|
+
// Native hook payloads/step updates carry no creator prompt identity.
|
|
1057
|
+
// A late old step above the new floor is indistinguishable from a new one:
|
|
1058
|
+
// Stop remains a hint, never a whole-tree or child idle grant.
|
|
1059
|
+
const sourceBound = false;
|
|
922
1060
|
const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]);
|
|
923
|
-
if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true)
|
|
1061
|
+
if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true)) {
|
|
1062
|
+
let progress = this.progressHooks.get(conversationId);
|
|
1063
|
+
if (!progress || progress.step !== step) { progress = { step, seen: new Set() }; this.progressHooks.set(conversationId, progress); }
|
|
1064
|
+
if (!progress.seen.has(progressKey)) { progress.seen.add(progressKey); this.markActivity(); }
|
|
1065
|
+
}
|
|
924
1066
|
if (conversationId) this.sessionObservations.set(conversationId, { ...prior,
|
|
925
1067
|
...(step !== null ? { last_step_index: step, last_execution_num: execution ?? (step === prior.last_step_index ? prior.last_execution_num : null) } : {}),
|
|
926
1068
|
last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}),
|
|
927
1069
|
...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}),
|
|
928
1070
|
...(event === "Stop" ? { last_stop_hook: fingerprint, last_stop_generation: this.turnGeneration, stop: { fullyIdle: payload.fullyIdle === true, source_bound: sourceBound, terminationReason: payload.terminationReason ?? null, executionNum: execution } } : {}) });
|
|
929
1071
|
if (event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId);
|
|
930
|
-
if (child && event === "Stop" && sourceBound && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" });
|
|
931
|
-
const terminalPending = this.active?.terminalResult === this.lastResult;
|
|
932
|
-
if (!child && event === "Stop" && sourceBound && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" });
|
|
933
1072
|
const parent = child?.parent_provider_session_id ?? null;
|
|
934
1073
|
if (event === "Stop" && !stale) {
|
|
935
1074
|
const observation = this.sessionObservations.get(conversationId);
|
|
936
1075
|
this.sessionObservations.set(conversationId, { ...observation, stop: { ...observation.stop, generation: this.turnGeneration } });
|
|
937
1076
|
}
|
|
938
1077
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
939
|
-
|
|
1078
|
+
// An omitted model carries no profile evidence. The hook journal below
|
|
1079
|
+
// retains that fact; do not read/lock/fsync the model history for every
|
|
1080
|
+
// ordinary tool or Stop callback. Actual model observations stay intact.
|
|
1081
|
+
if (payload.model && !observationOnly) await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured" }, evidence: "configured" });
|
|
1082
|
+
if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) {
|
|
1083
|
+
await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, reason: "owner_changed_during_model_observation" });
|
|
1084
|
+
return { stale: true };
|
|
1085
|
+
}
|
|
940
1086
|
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex");
|
|
941
|
-
await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation:
|
|
942
|
-
if (
|
|
1087
|
+
await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: hookGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, ...(payload.model ? {} : { model_observation: "unavailable" }), persistence_timing: this.persistTiming ?? null });
|
|
1088
|
+
if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) return { stale: true };
|
|
943
1089
|
this.queueNativeInspection();
|
|
944
|
-
|
|
1090
|
+
// A provider may wait for this hook's ACK before emitting the very event
|
|
1091
|
+
// needed to finish the turn. The fact above is persisted before ACK;
|
|
1092
|
+
// completion reconciliation belongs to the daemon, not the callback.
|
|
1093
|
+
if (hookOwner && this.active === hookOwner && this.turnGeneration === hookGeneration) hookOwner.pendingHookReconciliation = true;
|
|
1094
|
+
return { observationOnly, event_id: eventId, source_bound: sourceBound, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
945
1095
|
}
|
|
946
1096
|
async finalizeProviderForControl() {
|
|
947
1097
|
const pending = this.providerFinalization;
|
|
948
1098
|
try {
|
|
1099
|
+
while (this.active?.pendingHookReconciliation || this.active?.hookReconciliation) await this.reconcilePendingHook(this.active);
|
|
949
1100
|
try { await pending; }
|
|
950
1101
|
catch (error) {
|
|
951
1102
|
// Control retries only retained physical/resource cleanup, never a
|
|
@@ -958,6 +1109,7 @@ export class Runtime {
|
|
|
958
1109
|
}
|
|
959
1110
|
}
|
|
960
1111
|
await this.promptObservation?.dispatchJournal;
|
|
1112
|
+
await this.observeHookFailures();
|
|
961
1113
|
} catch (error) {
|
|
962
1114
|
if (!this.drainingError) throw error;
|
|
963
1115
|
this.drainingError.cleanup_error = errorRecord(error);
|
|
@@ -975,29 +1127,109 @@ export class Runtime {
|
|
|
975
1127
|
// In particular, the previous Turn's SUCCESS cannot certify a cancelled
|
|
976
1128
|
// in-flight Turn that exited without its own result.
|
|
977
1129
|
const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
|
|
978
|
-
if (currentResult && this.receipt().settled) await this.retireNativeServices();
|
|
979
1130
|
const clean = currentResult && this.receipt().settled;
|
|
980
1131
|
return { ...this.receipt(), clean, settled: Boolean(clean), native_settled: Boolean(clean), ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
|
|
981
1132
|
}
|
|
982
1133
|
async retireNativeServices() {
|
|
1134
|
+
if (!this.nativeQualification?.qualified) return;
|
|
1135
|
+
this.nativeResources ??= new Map((this.state.native_resources_entries ?? []).map(item => [item.resource_id, item]));
|
|
983
1136
|
const services = [...(this.nativeResources?.values() ?? [])].filter(item => item.classification === 'support_service' && item.state !== 'stopped');
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1137
|
+
const schedules = [...(this.nativeResources?.values() ?? [])].filter(item => item.state !== 'stopped' &&
|
|
1138
|
+
(item.kind === 'schedule' || item.classification === 'unknown'));
|
|
1139
|
+
// Unknown task semantics can be reconciled by exact native handle inventory,
|
|
1140
|
+
// not by inventing a shell outcome or trusting a PID printed to stdout.
|
|
987
1141
|
const revision = this.nativeRevision ?? 0;
|
|
988
1142
|
const endpoint = this.nativeStopper ? null : await ownedAgyEndpoint(this.nativeLaunch);
|
|
989
1143
|
const input = { endpoint, sessionId: this.init.conversation_id, cwd: this.config.cwd,
|
|
990
1144
|
csrfToken: this.nativeCsrfToken, remainingMs: 10_000, rootRelation: this.nativeQualification.rootRelation };
|
|
991
|
-
const
|
|
992
|
-
|
|
1145
|
+
const clock = new ObservationClock({ timeoutMs: input.remainingMs });
|
|
1146
|
+
let progress = 0;
|
|
1147
|
+
const requestInput = () => {
|
|
1148
|
+
if (clock.sample()) throw new DaemonError('native_resources_unproven', 'AGY native retirement observation budget exhausted');
|
|
1149
|
+
const remainingMs = Math.max(1, Math.ceil(clock.timeoutMs - clock.elapsed));
|
|
1150
|
+
return { ...input, remainingMs };
|
|
1151
|
+
};
|
|
1152
|
+
const inventory = async () => {
|
|
1153
|
+
if (this.state.native_topology_error) throw new DaemonError('native_resources_unproven', 'AGY native topology is incomplete');
|
|
1154
|
+
const root = this.init.conversation_id, nodes = [{ provider_session_id: root, parent_provider_session_id: '' }, ...this.descendants.values()];
|
|
1155
|
+
for (const node of nodes.slice(1)) {
|
|
1156
|
+
const seen = new Set([node.provider_session_id]); let parent = node.parent_provider_session_id;
|
|
1157
|
+
while (parent !== root) {
|
|
1158
|
+
if (!parent || seen.has(parent)) throw new DaemonError('native_resources_unproven', 'AGY native resource ancestry is unbound');
|
|
1159
|
+
seen.add(parent); parent = this.descendants.get(parent)?.parent_provider_session_id;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const result = [];
|
|
1163
|
+
for (const node of nodes) {
|
|
1164
|
+
const snapshot = await (this.nativeStateObserver ?? inspectAgyNativeState)({ ...requestInput(),
|
|
1165
|
+
sessionId: node.provider_session_id, rootSessionId: root, parentSessionId: node.parent_provider_session_id });
|
|
1166
|
+
result.push({ session_id: node.provider_session_id, snapshot, ...agyNativeInventory(snapshot, node.provider_session_id) });
|
|
1167
|
+
clock.sample(++progress); // A completed node read, not observer polling.
|
|
1168
|
+
}
|
|
1169
|
+
return result;
|
|
1170
|
+
};
|
|
1171
|
+
// Historical transcript envelopes need not explain whether a timer fired
|
|
1172
|
+
// or was cancelled. Fresh native absence proves retirement, not success.
|
|
1173
|
+
// The transcript ledger is partial. It cannot decide whether a fresh native
|
|
1174
|
+
// inventory is needed: even a missing ledger may hide an active daemon task.
|
|
1175
|
+
let states = await inventory();
|
|
1176
|
+
const required = states.find(state => !state.idle || state.tasks.some(task => task.isDaemon !== true));
|
|
1177
|
+
if (required) {
|
|
1178
|
+
this.childActivity(required.session_id);
|
|
1179
|
+
throw new DaemonError('native_resources_unproven', 'AGY required execution became active before native resource retirement');
|
|
1180
|
+
}
|
|
1181
|
+
const notificationKey = (session, trajectory, message) => JSON.stringify([session, trajectory, message.id, message.sender, message.recipient]);
|
|
1182
|
+
const notifications = new Map((this.state.native_retirement_notifications ?? []).filter(item => item.owner_daemon_id === this.config.daemonId)
|
|
1183
|
+
.map(item => [notificationKey(item.session_id, item.trajectory_id, item), item]));
|
|
1184
|
+
if (states.some(state => state.messages.some(message => !notifications.has(notificationKey(state.session_id, state.snapshot.trajectoryId, message)))))
|
|
1185
|
+
throw new DaemonError('native_resources_unproven', 'AGY has pending delivery before native resource retirement');
|
|
1186
|
+
const stopSenders = new Set(['system', ...states.map(state => state.session_id), ...states.flatMap(state => state.tasks.map(task => task.taskId))]);
|
|
1187
|
+
const trajectories = new Map(states.map(state => [state.session_id, state.snapshot.trajectoryId]));
|
|
1188
|
+
const activeTasks = states.some(state => state.snapshot.hasActiveChildren === true || state.tasks.length > 0);
|
|
1189
|
+
const stopped = services.length || activeTasks
|
|
1190
|
+
? await (this.nativeStopper ?? stopAgyNativeTree)(requestInput(), [...this.descendants.keys()]) : null;
|
|
1191
|
+
if (stopped) clock.sample(++progress);
|
|
1192
|
+
const observed = await (this.nativeObserver ?? inspectAgyNative)(requestInput());
|
|
1193
|
+
clock.sample(++progress);
|
|
993
1194
|
if (!this.nativeStopper && !isDeepStrictEqual(await ownedAgyEndpoint(this.nativeLaunch), endpoint)
|
|
994
1195
|
|| !observed.idle || revision !== (this.nativeRevision ?? 0) || !this.receipt().settled)
|
|
995
1196
|
throw new DaemonError('native_resources_unproven', 'AGY native service retirement lost its owner/execution fence');
|
|
1197
|
+
{
|
|
1198
|
+
states = await inventory();
|
|
1199
|
+
if (states.some(state => !state.idle || trajectories.get(state.session_id) !== state.snapshot.trajectoryId ||
|
|
1200
|
+
state.tasks.length) ||
|
|
1201
|
+
observed.trajectory_id && observed.trajectory_id !== trajectories.get(this.init.conversation_id) ||
|
|
1202
|
+
revision !== (this.nativeRevision ?? 0) || !this.receipt().settled ||
|
|
1203
|
+
!this.nativeStopper && !isDeepStrictEqual(await ownedAgyEndpoint(this.nativeLaunch), endpoint))
|
|
1204
|
+
throw new DaemonError('native_resources_unproven', 'AGY schedule retirement lacks fresh idle inventory and required delivery');
|
|
1205
|
+
for (const state of states) for (const message of state.messages) {
|
|
1206
|
+
const key = notificationKey(state.session_id, state.snapshot.trajectoryId, message);
|
|
1207
|
+
if (notifications.has(key)) continue;
|
|
1208
|
+
if (!stopped || typeof message.id !== 'string' || !message.id || message.recipient !== state.session_id || !stopSenders.has(message.sender))
|
|
1209
|
+
throw new DaemonError('native_resources_unproven', 'AGY post-stop delivery has no owned native control provenance');
|
|
1210
|
+
// Native cancellation creates notices after required execution is
|
|
1211
|
+
// already settled. They remain pending technical evidence until the
|
|
1212
|
+
// fenced provider is closed; do not prompt a model or claim delivery.
|
|
1213
|
+
notifications.set(key, { id: message.id, sender: message.sender, recipient: message.recipient, session_id: state.session_id,
|
|
1214
|
+
trajectory_id: state.snapshot.trajectoryId, owner_daemon_id: this.config.daemonId, generation: this.turnGeneration, revision,
|
|
1215
|
+
status: 'pending', disposition: 'after_owned_native_stop', method: stopped.method });
|
|
1216
|
+
}
|
|
1217
|
+
for (const schedule of schedules) {
|
|
1218
|
+
const state = states.find(item => item.session_id === schedule.provider_session_id);
|
|
1219
|
+
if (!state || state.tasks.some(task => task.taskId === schedule.resource_id))
|
|
1220
|
+
throw new DaemonError('native_resources_unproven', 'AGY schedule owner or retirement is unproven');
|
|
1221
|
+
this.nativeResources.set(schedule.resource_id, { ...schedule, state: 'stopped', native_retirement: {
|
|
1222
|
+
method: state.snapshot.method, reason: 'native_task_inventory_absent', revision,
|
|
1223
|
+
trajectory_id: state.snapshot.trajectoryId, observed_at: state.snapshot.observed_at } });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
996
1226
|
// This is native handle retirement, not physical cleanup. Shared shutdown
|
|
997
1227
|
// still re-observes the retained local PID/birth inventory after close.
|
|
998
1228
|
for (const service of services) this.nativeResources.set(service.resource_id, { ...service, state: 'stopped',
|
|
999
1229
|
native_retirement: { method: stopped.method, stopped_session_ids: stopped.stopped_session_ids, revision } });
|
|
1000
|
-
await this.persist({ native_resources_entries: [...this.nativeResources.values()] });
|
|
1230
|
+
await this.persist({ native_resources_entries: [...this.nativeResources.values()], native_retirement_notifications: [...notifications.values()] });
|
|
1231
|
+
if (revision !== (this.nativeRevision ?? 0) || !this.receipt().settled)
|
|
1232
|
+
throw new DaemonError('native_resources_unproven', 'AGY resource retirement was fenced during publication');
|
|
1001
1233
|
}
|
|
1002
1234
|
async cancel() { if (!this.active && this.receipt().settled) return { ...this.receipt(), cancelled: true }; this.nativeAbort?.abort(); this.nativeProof = null; this.nativeRevision = (this.nativeRevision ?? 0) + 1; this.state.native_execution_revision = this.nativeRevision; cancelPendingDispatch(this); if (!this.child) return this.receipt(); if (!this.exited) { try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); } await this.finalizeProviderForControl(); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true };
|
|
1003
1235
|
}
|
|
@@ -1059,30 +1291,55 @@ async function dispatch(runtime, request) {
|
|
|
1059
1291
|
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); void runtime.reconcileRetainedAdmission().catch(error => { runtime.nativeInspection = { state: 'unproven', reason: error.details?.reason ?? error.code ?? 'native_admission_unproven' }; }); runtime.queueNativeInspection(); await runtime.persist({ native_observation_episode: runtime.episodeSnapshot() }); return runtime.receipt(); }
|
|
1060
1292
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
1061
1293
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
1294
|
+
if (request.operation === "hook.verify") {
|
|
1295
|
+
if (params.daemonId !== runtime.config.daemonId) throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon");
|
|
1296
|
+
assertDaemonDispatch(runtime.paths.dir);
|
|
1297
|
+
if (params.binding?.native_admission === "assigned_work_start") return await runtime.verifyWorkStart(params);
|
|
1298
|
+
const retained = runtime.verifiedHookFacts?.get(params.requestId);
|
|
1299
|
+
if (!retained || !isDeepStrictEqual(retained.binding, params.binding)) throw new DaemonError("native_hook_unproven", "Hook fact is not bound to a verified native delivery");
|
|
1300
|
+
retained.guard();
|
|
1301
|
+
return { verified: true, nativeHookBinding: retained.binding };
|
|
1302
|
+
}
|
|
1303
|
+
if (request.operation === "hook.complete") {
|
|
1304
|
+
if (params.daemonId !== runtime.config.daemonId)
|
|
1305
|
+
throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon");
|
|
1306
|
+
if (typeof params.requestId !== 'string' || !/^[A-Za-z0-9-]{1,128}$/.test(params.requestId))
|
|
1307
|
+
throw new DaemonError("agy_hook_binding_invalid", "Antigravity callback request ID is invalid");
|
|
1308
|
+
// Administrative ACK only: the success of a callback is not proof that a
|
|
1309
|
+
// Work mutation occurred or that a native agent Turn is complete.
|
|
1310
|
+
return { retired: runtime.hookRequests?.delete(params.requestId) === true };
|
|
1311
|
+
}
|
|
1062
1312
|
if (request.operation === "hook.observe") {
|
|
1063
1313
|
// Check the process binding before correlating a request with an active
|
|
1064
1314
|
// turn. A stale/foreign daemon must not poison an unrelated operation.
|
|
1065
1315
|
if (runtime.config.hookBinding === "environment-v1" && params.daemonId !== runtime.config.daemonId) throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon", false, { expected_daemon_id: runtime.config.daemonId, observed_daemon_id: params.daemonId ?? null });
|
|
1066
1316
|
const current = runtime.active, budget = hookBudget(25_000, params.deadlineAt);
|
|
1067
1317
|
runtime.hookRequests ??= new Map();
|
|
1068
|
-
if (/^[A-Za-z0-9-]{1,128}$/.test(request.id)) runtime.hookRequests.set(request.id, { current, event: params.event, payload: { conversationId: params.payload?.conversationId, toolCall: { name: params.payload?.toolCall?.name } } });
|
|
1318
|
+
if (/^[A-Za-z0-9-]{1,128}$/.test(request.id)) runtime.hookRequests.set(request.id, { current: current ?? runtime.lastNativeHookOperation, event: params.event, payload: { conversationId: params.payload?.conversationId, toolCall: { name: params.payload?.toolCall?.name } } });
|
|
1069
1319
|
try {
|
|
1320
|
+
const deferred = await budget.run(() => runtime.registerWorkStart(params.event, params.payload ?? {}, request.id), "registration");
|
|
1321
|
+
if (deferred) return { fastAck: true, nativeHookBinding: deferred };
|
|
1070
1322
|
const result = await budget.run(async () => {
|
|
1071
1323
|
if (runtime.hookRequests.has(request.id) && current?.operationId) {
|
|
1072
1324
|
const directory = path.join(runtime.paths.dir, "operations", createHash("sha256").update(current.operationId).digest("hex"), "hooks");
|
|
1073
1325
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1074
|
-
await writeJson(path.join(directory, `${request.id}.json`), { schema_id: "dd-flow/hook-request@1", request_id: request.id, daemon_id: runtime.config.daemonId, operation_id: current.operationId, turn_generation: current.generation, event: params.event, conversation_id: params.payload?.conversationId ?? null, tool: params.payload?.toolCall?.name ?? null });
|
|
1326
|
+
await writeJson(path.join(directory, `${request.id}.json`), { schema_id: "dd-flow/hook-request@1", request_id: request.id, daemon_id: runtime.config.daemonId, operation_id: current.operationId, turn_generation: current.generation, native_creator: "unconfirmed", event: params.event, conversation_id: params.payload?.conversationId ?? null, tool: params.payload?.toolCall?.name ?? null });
|
|
1075
1327
|
}
|
|
1076
1328
|
budget.remaining("observation");
|
|
1077
1329
|
return await runtime.observeHook(params.event, params.payload ?? {}, budget);
|
|
1078
1330
|
}, "observation");
|
|
1331
|
+
// The AGY stream does not prove which native prompt produced a step.
|
|
1332
|
+
// An unseen old step can exceed a newer prompt's observed floor. Do not
|
|
1333
|
+
// mint an optional CLI admission from event order; legacy hook performs
|
|
1334
|
+
// its existing scoped checks until a native creator binding exists.
|
|
1079
1335
|
await runtime.observeHookFailures(); budget.remaining("observation");
|
|
1080
|
-
if (
|
|
1336
|
+
if (result.stale) runtime.hookRequests.delete(request.id);
|
|
1081
1337
|
return result;
|
|
1082
1338
|
}
|
|
1083
|
-
catch (error) { await runtime.
|
|
1339
|
+
catch (error) { await runtime.recordUnconfirmedHookFailure(error, params.event, params.payload ?? {}, current, request.id); throw error; }
|
|
1084
1340
|
}
|
|
1085
1341
|
if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]], async () => {
|
|
1342
|
+
await runtime.retireNativeServices();
|
|
1086
1343
|
await runtime.captureChildTools();
|
|
1087
1344
|
return runtime.receipt().native_resources;
|
|
1088
1345
|
});
|