@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +32 -11
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +3 -3
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/hooks.js +150 -42
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +3 -11
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +81 -86
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +19 -64
- package/dist/services/run-recovery-runtime.js +24 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
package/dist/services/hooks.js
CHANGED
|
@@ -7,6 +7,7 @@ import { AppError } from "../shared/errors.js";
|
|
|
7
7
|
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
8
8
|
import { parseJsonObject } from "../shared/json.js";
|
|
9
9
|
import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
10
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
10
11
|
import { appendAudit } from "./audit.js";
|
|
11
12
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
12
13
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
@@ -549,21 +550,22 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
549
550
|
const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
|
|
550
551
|
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
|
|
551
552
|
const observedProfile = objectRecord(ddZcode.observedProfile);
|
|
553
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
552
554
|
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
553
555
|
const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
|
|
554
556
|
const persist = (scope) => {
|
|
555
557
|
if (scope)
|
|
556
558
|
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
557
|
-
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd:
|
|
558
|
-
const payload = { ...hook, session_id: providerSessionId, cwd:
|
|
559
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
560
|
+
const payload = { ...hook, session_id: providerSessionId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
559
561
|
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
|
|
560
562
|
const inserted = recordHookEvent(context, {
|
|
561
563
|
projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
|
|
562
564
|
daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? stringValue(hook.agentId) ?? null, turnId: null,
|
|
563
565
|
eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey,
|
|
564
|
-
transcriptPath: stringValue(hook.transcript_path) ?? null, cwd:
|
|
566
|
+
transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd
|
|
565
567
|
});
|
|
566
|
-
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd:
|
|
568
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
567
569
|
const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
|
|
568
570
|
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
|
|
569
571
|
};
|
|
@@ -602,13 +604,14 @@ export function handleGrokEvent(context, input) {
|
|
|
602
604
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
603
605
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
604
606
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
605
|
-
|
|
607
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
606
608
|
if (bootstrapStageStart)
|
|
607
609
|
registerProject(context, { root: expectedRoot });
|
|
608
610
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
609
611
|
const meta = objectRecord(hook._meta);
|
|
610
612
|
const ddGrok = objectRecord(meta.ddGrok);
|
|
611
613
|
const observedProfile = objectRecord(ddGrok.observedProfile);
|
|
614
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
612
615
|
const rootProviderSessionId = stringValue(ddGrok.rootProviderSessionId);
|
|
613
616
|
const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
|
|
614
617
|
if (!rootProviderSessionId || !providerSessionId)
|
|
@@ -622,23 +625,35 @@ export function handleGrokEvent(context, input) {
|
|
|
622
625
|
harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
|
|
623
626
|
promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
|
|
624
627
|
})).digest("hex");
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
628
|
+
const persist = (scope) => {
|
|
629
|
+
if (scope)
|
|
630
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
631
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
632
|
+
const payload = {
|
|
633
|
+
...hook, session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
634
|
+
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
|
|
635
|
+
};
|
|
636
|
+
const inserted = recordHookEvent(context, {
|
|
637
|
+
projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
|
|
638
|
+
daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
|
|
639
|
+
eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: observedCwd
|
|
640
|
+
});
|
|
641
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
642
|
+
return inserted;
|
|
629
643
|
};
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
projectId: project.id,
|
|
637
|
-
|
|
644
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
|
|
645
|
+
...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
|
|
646
|
+
if (!lifecycle)
|
|
647
|
+
return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
648
|
+
let inserted = false;
|
|
649
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
650
|
+
command, harness: "grok-acp", projectId: project.id,
|
|
651
|
+
identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
|
|
652
|
+
recordReceipt: scope => { inserted = persist(scope); return eventKey; }
|
|
638
653
|
});
|
|
639
|
-
|
|
640
|
-
...(
|
|
641
|
-
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
654
|
+
if (receipt.invocationId)
|
|
655
|
+
return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
656
|
+
return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
642
657
|
}
|
|
643
658
|
/** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
|
|
644
659
|
export function handleOpenCodeEvent(context, input) {
|
|
@@ -654,6 +669,65 @@ export function handleDroidEvent(context, input) {
|
|
|
654
669
|
throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
|
|
655
670
|
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
656
671
|
}
|
|
672
|
+
/** Stable project identity is not the native Session's execution workspace. */
|
|
673
|
+
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary) {
|
|
674
|
+
if (facts?.bootstrapStageStart)
|
|
675
|
+
return projectRoot;
|
|
676
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
677
|
+
if (!project)
|
|
678
|
+
return projectRoot;
|
|
679
|
+
let work;
|
|
680
|
+
if (facts?.workId) {
|
|
681
|
+
try {
|
|
682
|
+
work = resolveWorkReference(context, facts.workId, { projectId: project.id });
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2];
|
|
690
|
+
if (!reference)
|
|
691
|
+
return projectRoot;
|
|
692
|
+
const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
693
|
+
if (!run)
|
|
694
|
+
return projectRoot; // Hook ingress does not validate a mistyped CLI target.
|
|
695
|
+
if (work) {
|
|
696
|
+
// PROTOCOLIZE provisions the feature checkout after its coordinator
|
|
697
|
+
// Session has launched in the stable project root. Its transition Work
|
|
698
|
+
// must keep that physical route until the Stage is complete.
|
|
699
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'stage_work_memberships'")
|
|
700
|
+
&& context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage IN ('specify','protocolize') LIMIT 1", [project.id, run.id, work.work_id]))
|
|
701
|
+
return projectRoot;
|
|
702
|
+
const merge = context.db.get("SELECT target_workspace FROM merge_requests WHERE executor_work_id = ? AND run_id = ?", [work.work_id, run.id]);
|
|
703
|
+
if (merge)
|
|
704
|
+
return resolveProjectRoot(merge.target_workspace);
|
|
705
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'work_launches'")) {
|
|
706
|
+
const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching', 'start_requested')", [work.work_id]);
|
|
707
|
+
if (launch?.provider_session_id === providerSessionId && run.run_root) {
|
|
708
|
+
const copy = JSON.parse(launch.profile_json).review_copy?.root;
|
|
709
|
+
if (copy) {
|
|
710
|
+
const target = path.resolve(run.run_root, copy);
|
|
711
|
+
const relative = path.relative(run.run_root, target);
|
|
712
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`))
|
|
713
|
+
throw new AppError("read_only_workspace_unbound", "Review copy escapes its owning RUN", 1);
|
|
714
|
+
return resolveProjectRoot(target);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (facts?.stageName === "specify" || facts?.stageName === "protocolize")
|
|
720
|
+
return projectRoot;
|
|
721
|
+
if (facts?.stageName === "merge") {
|
|
722
|
+
const pending = context.db.all("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND run_id = ? AND status IN ('dispatching','active','action_required') ORDER BY updated_at DESC", [project.id, run.id]);
|
|
723
|
+
const targets = [...new Set(pending.map(item => resolveProjectRoot(item.target_workspace)))];
|
|
724
|
+
if (targets.length > 1)
|
|
725
|
+
throw new AppError("merge_workspace_ambiguous", "Current MERGE requests disagree on the native execution workspace", 1, { run_id: run.id, target_workspaces: targets });
|
|
726
|
+
if (targets.length === 1)
|
|
727
|
+
return targets[0];
|
|
728
|
+
}
|
|
729
|
+
return resolveProjectRoot(run.workspace_root);
|
|
730
|
+
}
|
|
657
731
|
function handleControlledToolEvent(context, input, event, harness) {
|
|
658
732
|
const agy = harness === "antigravity-cli";
|
|
659
733
|
const droid = harness === "droid-cli";
|
|
@@ -676,9 +750,7 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
676
750
|
throw new AppError(`${errorPrefix}_session_identity_invalid`, `${label} event is missing trusted physical identity`, 1);
|
|
677
751
|
}
|
|
678
752
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
679
|
-
|
|
680
|
-
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} Session directory does not match the controlled workspace`, 1, { expected: expectedRoot, actual: directory });
|
|
681
|
-
}
|
|
753
|
+
const observedCwd = resolveProjectRoot(directory);
|
|
682
754
|
if (droid && nativeParentId === providerSessionId)
|
|
683
755
|
throw new AppError("droid_session_identity_invalid", "A Droid Session cannot parent itself", 1);
|
|
684
756
|
const sessionId = storageSessionId(nativeSessionIdentity(harness, providerSessionId));
|
|
@@ -690,8 +762,8 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
690
762
|
const inserted = recordHookEvent(context, {
|
|
691
763
|
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
692
764
|
daemonId, sessionId, agentId: stringValue(session.agent_id) ?? null, turnId: stringValue(event.message_id) ?? null,
|
|
693
|
-
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd:
|
|
694
|
-
eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd:
|
|
765
|
+
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: observedCwd, tool_name: toolName, status: objectRecord(event.outcome).status },
|
|
766
|
+
eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
695
767
|
});
|
|
696
768
|
return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
|
|
697
769
|
}
|
|
@@ -699,10 +771,15 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
699
771
|
return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
|
|
700
772
|
}
|
|
701
773
|
const lifecycle = lifecycleFacts(command);
|
|
702
|
-
|
|
774
|
+
const auxiliary = continuationCommand(command);
|
|
775
|
+
if (!lifecycle && !auxiliary)
|
|
703
776
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
777
|
+
const expectedCwd = controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary);
|
|
778
|
+
if (observedCwd !== expectedCwd) {
|
|
779
|
+
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
780
|
+
}
|
|
704
781
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
705
|
-
|
|
782
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
706
783
|
if (bootstrapStageStart)
|
|
707
784
|
registerProject(context, { root: expectedRoot });
|
|
708
785
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
@@ -713,28 +790,55 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
713
790
|
}
|
|
714
791
|
}
|
|
715
792
|
const agentId = stringValue(session.agent_id);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
793
|
+
const persist = (scope) => {
|
|
794
|
+
if (scope)
|
|
795
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
796
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: observedCwd });
|
|
797
|
+
const payload = {
|
|
798
|
+
session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName, tool_input: rawInput, command,
|
|
799
|
+
...(agy ? { cwd_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}),
|
|
800
|
+
provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
|
|
801
|
+
mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
|
|
802
|
+
};
|
|
803
|
+
const inserted = recordHookEvent(context, {
|
|
804
|
+
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
805
|
+
daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
|
|
806
|
+
status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
807
|
+
});
|
|
808
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName, eventKey: baseKey });
|
|
809
|
+
return inserted;
|
|
721
810
|
};
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
811
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
|
|
812
|
+
if (agy && lifecycle) {
|
|
813
|
+
let inserted = false;
|
|
814
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
815
|
+
command, harness, projectId: project.id, identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
816
|
+
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
817
|
+
});
|
|
818
|
+
return { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
819
|
+
}
|
|
820
|
+
const inserted = context.db.writeTransaction(() => persist());
|
|
821
|
+
return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
730
822
|
}
|
|
731
823
|
/** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
|
|
732
824
|
export function handleAgyEvent(context, input) {
|
|
733
825
|
const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
|
|
734
826
|
if (event.schema_id !== "dd-flow/agy-tool-event@1")
|
|
735
827
|
throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
|
|
736
|
-
|
|
737
|
-
|
|
828
|
+
// workspacePaths lists available workspaces; its first element is not a
|
|
829
|
+
// native selected-cwd observation. The daemon's spawn cwd is a separate,
|
|
830
|
+
// launch-bound route, and the list only corroborates its membership.
|
|
831
|
+
const workspacePaths = Array.isArray(event.workspace_paths)
|
|
832
|
+
? event.workspace_paths.filter((value) => typeof value === "string" && path.isAbsolute(value)) : [];
|
|
833
|
+
const launchCwd = stringValue(event.launch_cwd);
|
|
834
|
+
const qualifiedLaunchCwd = launchCwd && path.isAbsolute(launchCwd) ? launchCwd : undefined;
|
|
835
|
+
const directory = qualifiedLaunchCwd ?? (workspacePaths.length === 1 ? workspacePaths[0] : undefined);
|
|
836
|
+
if (!directory && workspacePaths.length > 1)
|
|
837
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity did not identify the active route among multiple workspaces", 1, { workspace_paths: workspacePaths });
|
|
838
|
+
if (directory && qualifiedLaunchCwd && workspacePaths.length
|
|
839
|
+
&& !workspacePaths.some(value => resolveProjectRoot(value) === resolveProjectRoot(directory))) {
|
|
840
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity launch cwd is absent from the native workspace list", 1, { launch_cwd: directory, workspace_paths: workspacePaths });
|
|
841
|
+
}
|
|
738
842
|
const providerSessionId = stringValue(event.conversation_id);
|
|
739
843
|
const daemonId = stringValue(event.daemon_id);
|
|
740
844
|
const eventId = stringValue(event.event_id);
|
|
@@ -743,6 +847,9 @@ export function handleAgyEvent(context, input) {
|
|
|
743
847
|
throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
|
|
744
848
|
const rawInput = objectRecord(event.input);
|
|
745
849
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
850
|
+
if (command && (lifecycleFacts(command) || continuationCommand(command)) && !qualifiedLaunchCwd) {
|
|
851
|
+
throw new AppError("agy_workspace_route_unproven", "Managed Antigravity call has no launch-bound execution cwd", 1, { phase: "prepare", effect: "no_effect", recoverable: false, workspace_paths: workspacePaths });
|
|
852
|
+
}
|
|
746
853
|
const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
|
|
747
854
|
const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
|
|
748
855
|
if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
|
|
@@ -753,6 +860,7 @@ export function handleAgyEvent(context, input) {
|
|
|
753
860
|
daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
|
|
754
861
|
message_id: event.step_index === undefined ? null : String(event.step_index),
|
|
755
862
|
session: { provider_session_id: providerSessionId, parent_provider_session_id: event.parent_conversation_id, directory },
|
|
863
|
+
cwd_provenance: qualifiedLaunchCwd ? "launch_bound" : "workspace_paths_singleton",
|
|
756
864
|
profile: { provider: objectRecord(event.profile).provider, model: objectRecord(event.profile).model, variant: objectRecord(event.profile).reasoning, agent: objectRecord(event.profile).mode },
|
|
757
865
|
outcome: { status: stringValue(event.error) ? "error" : "completed" }
|
|
758
866
|
};
|
package/dist/services/lanes.js
CHANGED
|
@@ -23,14 +23,13 @@ export function prepareLaneLock(context, input, action) {
|
|
|
23
23
|
const { timeoutSeconds, pollIntervalSeconds } = prepareLaneWaitTiming(input);
|
|
24
24
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
25
25
|
const lane = normalizeLane(input.lane);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else if (["acquire", "wait", "wait-acquire"].includes(action))
|
|
26
|
+
const workspace = input.workspacePath !== undefined ? requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath) : undefined;
|
|
27
|
+
if (input.workspacePath === undefined && ["acquire", "wait", "wait-acquire"].includes(action))
|
|
29
28
|
throw new AppError("validation", "Lane lock requires a workspace path", 2);
|
|
30
29
|
if (action === "acquire" || action === "wait-acquire")
|
|
31
30
|
assertWorkerMayAcquireLaneLock(context, project.id, lane, input.workerId);
|
|
32
31
|
const lock = action === "heartbeat" || action === "release" ? requireOwnedActiveLock(context, project.id, lane, input.workerId, input.leaseToken) : undefined;
|
|
33
|
-
return { project, lane, ttlSeconds, timeoutSeconds, pollIntervalSeconds, lock };
|
|
32
|
+
return { project, lane, ttlSeconds, timeoutSeconds, pollIntervalSeconds, lock, workspace };
|
|
34
33
|
}
|
|
35
34
|
export function getLaneStatus(context, input) {
|
|
36
35
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -99,7 +98,12 @@ export function checkLaneWorkspace(context, input) {
|
|
|
99
98
|
};
|
|
100
99
|
}
|
|
101
100
|
export function acquireLaneLock(context, input) {
|
|
102
|
-
const
|
|
101
|
+
const prepared = prepareLaneLock(context, input, "acquire");
|
|
102
|
+
return context.db.writeTransaction(() => acquirePreparedLaneLock(context, input, prepared));
|
|
103
|
+
}
|
|
104
|
+
function acquirePreparedLaneLock(context, input, prepared) {
|
|
105
|
+
const { project, lane, ttlSeconds } = prepared;
|
|
106
|
+
assertPreparedLaneLock(context, prepared, input.workerId);
|
|
103
107
|
expireStaleLocks(context, project.id);
|
|
104
108
|
const active = activeLaneLock(context, project.id, lane);
|
|
105
109
|
if (active) {
|
|
@@ -141,6 +145,12 @@ export function acquireLaneLock(context, input) {
|
|
|
141
145
|
...(expired ? { expired_lock_id: expired.id } : {})
|
|
142
146
|
};
|
|
143
147
|
}
|
|
148
|
+
function assertPreparedLaneLock(context, prepared, workerId) {
|
|
149
|
+
if (JSON.stringify(laneRecord(context, prepared.project.id, prepared.lane)) !== JSON.stringify(prepared.workspace)
|
|
150
|
+
|| context.db.get("SELECT root FROM projects WHERE id = ?", [prepared.project.id])?.root !== prepared.project.root)
|
|
151
|
+
throw new AppError("lane_workspace_changed", "Lane registration changed after workspace preparation", 1);
|
|
152
|
+
assertWorkerMayAcquireLaneLock(context, prepared.project.id, prepared.lane, workerId);
|
|
153
|
+
}
|
|
144
154
|
export function heartbeatLaneLock(context, input) {
|
|
145
155
|
const { project, lock: preparedLock, ttlSeconds } = prepareLaneLock(context, input, "heartbeat");
|
|
146
156
|
const lock = preparedLock;
|
|
@@ -524,13 +534,15 @@ function ensureQueuedLaneWaiter(context, input) {
|
|
|
524
534
|
return waiter;
|
|
525
535
|
}
|
|
526
536
|
function tryAcquireQueuedLaneLock(context, input) {
|
|
527
|
-
context
|
|
528
|
-
|
|
537
|
+
const prepared = prepareLaneLock(context, input, "wait-acquire");
|
|
538
|
+
if (prepared.project.id !== input.projectId || prepared.lane !== input.lane)
|
|
539
|
+
throw new AppError("lane_workspace_changed", "Lane identity changed while waiting", 1);
|
|
540
|
+
return context.db.writeTransaction(() => {
|
|
541
|
+
assertPreparedLaneLock(context, prepared, input.workerId);
|
|
529
542
|
expireStaleLocks(context, input.projectId);
|
|
530
543
|
expireStaleWaiters(context, input.projectId);
|
|
531
544
|
const existingLock = activeLaneLock(context, input.projectId, input.lane);
|
|
532
545
|
if (existingLock?.worker_id === input.workerId) {
|
|
533
|
-
context.db.exec("COMMIT");
|
|
534
546
|
return {
|
|
535
547
|
acquired: true,
|
|
536
548
|
payload: {
|
|
@@ -548,7 +560,6 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
548
560
|
}
|
|
549
561
|
const latest = latestLaneWaiterForWorker(context, input.projectId, input.lane, input.workerId);
|
|
550
562
|
if (latest && ["cancelled", "expired"].includes(latest.status) && latest.updated_at >= input.invocationStartedAt) {
|
|
551
|
-
context.db.exec("COMMIT");
|
|
552
563
|
return {
|
|
553
564
|
acquired: false,
|
|
554
565
|
payload: {
|
|
@@ -572,14 +583,14 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
572
583
|
});
|
|
573
584
|
const head = firstQueuedLaneWaiter(context, input.projectId, input.lane);
|
|
574
585
|
if (!existingLock && head?.id === waiter.id) {
|
|
575
|
-
const lockResult =
|
|
586
|
+
const lockResult = acquirePreparedLaneLock(context, {
|
|
576
587
|
projectRoot: input.projectRoot,
|
|
577
588
|
lane: input.lane,
|
|
578
589
|
workerId: input.workerId,
|
|
579
590
|
workspacePath: input.workspacePath,
|
|
580
591
|
ttlSeconds: input.ttlSeconds,
|
|
581
592
|
reason: input.reason
|
|
582
|
-
});
|
|
593
|
+
}, prepared);
|
|
583
594
|
const now = context.now();
|
|
584
595
|
context.db.run(`UPDATE lane_waiters
|
|
585
596
|
SET status = 'acquired', acquired_at = ?, updated_at = ?
|
|
@@ -591,7 +602,6 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
591
602
|
reason: input.reason,
|
|
592
603
|
payload: { project_id: input.projectId, lane: input.lane, worker_id: input.workerId, waiter_id: waiter.id }
|
|
593
604
|
});
|
|
594
|
-
context.db.exec("COMMIT");
|
|
595
605
|
return {
|
|
596
606
|
acquired: true,
|
|
597
607
|
payload: {
|
|
@@ -608,7 +618,6 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
608
618
|
};
|
|
609
619
|
}
|
|
610
620
|
const position = queuedWaiterPosition(context, input.projectId, input.lane, waiter.id);
|
|
611
|
-
context.db.exec("COMMIT");
|
|
612
621
|
return {
|
|
613
622
|
acquired: false,
|
|
614
623
|
payload: {
|
|
@@ -622,11 +631,7 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
622
631
|
position
|
|
623
632
|
}
|
|
624
633
|
};
|
|
625
|
-
}
|
|
626
|
-
catch (error) {
|
|
627
|
-
context.db.exec("ROLLBACK");
|
|
628
|
-
throw error;
|
|
629
|
-
}
|
|
634
|
+
});
|
|
630
635
|
}
|
|
631
636
|
function resolveExistingPath(value) {
|
|
632
637
|
return resolveProjectRoot(value);
|
|
@@ -160,8 +160,16 @@ function invocationFromSegment(segment, heredoc, wrapped, inheritedEnv) {
|
|
|
160
160
|
index += 1;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
let executable = segment.words[index];
|
|
164
|
+
let nodeLauncher = false;
|
|
165
|
+
let launcher;
|
|
166
|
+
if (executable && path.basename(executable) === "node"
|
|
167
|
+
&& (path.basename(segment.words[index + 1] ?? "") === "cli.js" || segment.words[index + 1] === "$DD_FLOW_BIN")) {
|
|
168
|
+
launcher = executable;
|
|
169
|
+
executable = segment.words[++index];
|
|
170
|
+
nodeLauncher = true;
|
|
171
|
+
}
|
|
172
|
+
if (!executable || path.basename(executable) !== "dd-flow" && executable !== "$DD_FLOW_BIN" && !(nodeLauncher && path.basename(executable) === "cli.js"))
|
|
165
173
|
return null;
|
|
166
174
|
const argv = segment.words.slice(index + 1);
|
|
167
175
|
const operation = lifecycleOperation(argv);
|
|
@@ -170,6 +178,7 @@ function invocationFromSegment(segment, heredoc, wrapped, inheritedEnv) {
|
|
|
170
178
|
return {
|
|
171
179
|
operation,
|
|
172
180
|
executable,
|
|
181
|
+
...(launcher ? { launcher } : {}),
|
|
173
182
|
argv,
|
|
174
183
|
env,
|
|
175
184
|
args: parseCommandArgs(argv.slice(operation === "recovery_accept" ? 3 : 2)),
|