@deksden-com/dd-flow-cli 0.9.0-beta.143 → 0.9.0-beta.145
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 +65 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +9 -3
- package/dist/cli/command-metadata.js +33 -14
- package/dist/cli/help.js +32 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +74 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +70 -32
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +12 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +11 -4
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-fanout.js +21 -4
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +48 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-start.js +26 -19
- package/dist/services/descriptor-stage-coordinator.js +30 -10
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +104 -93
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +8 -4
- package/dist/services/frozen-engine.js +113 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +2 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +25 -4
- package/dist/services/lifecycle-invocations.js +33 -5
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +28 -2
- package/dist/services/memory-init-controller-start.js +19 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +21 -3
- package/dist/services/memory-init-native-capacity.js +13 -0
- package/dist/services/memory-init-preflight.js +11 -1
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-layer.js +68 -4
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +77 -23
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +74 -9
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-controller-process.js +2 -6
- package/dist/services/run-controller-recovery.js +6 -1
- package/dist/services/run-controller.js +131 -80
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +9 -7
- package/dist/services/stage-pause.js +108 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-fanout.js +21 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +80 -33
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +8 -4
|
@@ -8,6 +8,7 @@ import { AppError, errorRecord } from "../shared/errors.js";
|
|
|
8
8
|
import { getResourceDatabase } from "../storage/database.js";
|
|
9
9
|
import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
|
|
10
10
|
import { managedLeaseMonitor } from "./managed-lease-monitor.js";
|
|
11
|
+
import { physicalStartup } from "./physical-startup.js";
|
|
11
12
|
import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus, prepareRuntimeScopeFence, parseRuntimeBudget } from "./runtime-budget.js";
|
|
12
13
|
import { controlRuntimeScopeRuns } from "./runtime-scope-control.js";
|
|
13
14
|
import { prepareRuntimeScopeResume, recordRuntimeScopeResume } from "./runtime-scope-resume.js";
|
|
@@ -138,8 +139,7 @@ export function prepareRuntimeScopeServing(context, scopeId, generation) {
|
|
|
138
139
|
}
|
|
139
140
|
return { key, token };
|
|
140
141
|
}
|
|
141
|
-
function requireStartingScopeWorker(db, key, token) {
|
|
142
|
-
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
142
|
+
function requireStartingScopeWorker(db, key, token, row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token])) {
|
|
143
143
|
if (!row)
|
|
144
144
|
throw new AppError("runtime_scope_owner_unknown", "Scope worker claim changed", 1);
|
|
145
145
|
const worker = JSON.parse(row.metadata_json);
|
|
@@ -150,29 +150,55 @@ function requireStartingScopeWorker(db, key, token) {
|
|
|
150
150
|
export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
151
151
|
const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
|
|
152
152
|
const db = getResourceDatabase(resourceHome(context));
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
db.
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
const startup = physicalStartup(context.databaseDeadlineAt);
|
|
154
|
+
let claimedOwner = undefined;
|
|
155
|
+
const assertStartupCurrent = () => {
|
|
156
|
+
const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
157
|
+
const fence = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
|
|
158
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
159
|
+
const current = row ? JSON.parse(row.metadata_json) : null;
|
|
160
|
+
if (!control || JSON.parse(control.metadata_json).generation !== generation || fence || !current || current.token !== token || current.status !== (claimedOwner ? "running" : "starting") || current.owner?.pid !== claimedOwner?.pid || current.owner?.pid_started_at !== claimedOwner?.pid_started_at)
|
|
161
|
+
throw new AppError("runtime_scope_control_superseded", "Scope startup no longer owns this control", 1);
|
|
162
|
+
};
|
|
163
|
+
const starting = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
164
|
+
let worker = requireStartingScopeWorker(db, key, token, starting);
|
|
165
|
+
let retainedMetadata = starting.metadata_json;
|
|
166
|
+
let claimedByThisServe = false;
|
|
167
|
+
const persistWorker = () => {
|
|
168
|
+
const next = JSON.stringify(worker);
|
|
169
|
+
if (db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ? AND metadata_json = ?", [next, context.now(), key, token, retainedMetadata]).changes !== 1)
|
|
170
|
+
throw new AppError("runtime_scope_owner_unknown", "Scope owner changed before its retained result could be saved", 1);
|
|
171
|
+
retainedMetadata = next;
|
|
172
|
+
};
|
|
161
173
|
let heartbeat;
|
|
162
174
|
let lease;
|
|
163
175
|
// A fence must never be held indefinitely by an observer that cannot obtain
|
|
164
176
|
// fresh provider evidence. A later operator request continues the retained
|
|
165
177
|
// budget; this owner records why it stopped without claiming settlement.
|
|
166
|
-
|
|
167
|
-
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
|
|
178
|
+
let observationCleanup;
|
|
168
179
|
let cleanupFailure;
|
|
169
180
|
const result = await (async () => {
|
|
170
181
|
try {
|
|
182
|
+
const owner = await startup.birth(assertStartupCurrent);
|
|
183
|
+
const claim = db.writeTransaction(() => {
|
|
184
|
+
assertStartupCurrent();
|
|
185
|
+
const current = requireStartingScopeWorker(db, key, token);
|
|
186
|
+
current.owner = owner;
|
|
187
|
+
current.status = "running";
|
|
188
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(current), context.now(), key, token]);
|
|
189
|
+
return { worker: current, metadata: JSON.stringify(current) };
|
|
190
|
+
});
|
|
191
|
+
worker = claim.worker;
|
|
192
|
+
retainedMetadata = claim.metadata;
|
|
193
|
+
claimedOwner = owner;
|
|
194
|
+
claimedByThisServe = true;
|
|
195
|
+
const observation = observationCleanup = recoveryObservationBudget(worker.snapshot);
|
|
196
|
+
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
|
|
171
197
|
const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
|
|
172
198
|
worker.process_id = record.id;
|
|
173
199
|
worker.process_lease_token = record.lease_token;
|
|
174
|
-
const confirmed = confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
|
|
175
|
-
|
|
200
|
+
const confirmed = await startup.observe(() => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid }), assertStartupCurrent, record.lease_expires_at);
|
|
201
|
+
persistWorker();
|
|
176
202
|
lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), confirmed.lease_expires_at);
|
|
177
203
|
let timerFailure;
|
|
178
204
|
const assertScopeCurrent = () => {
|
|
@@ -233,7 +259,7 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
|
233
259
|
await lease.ensureConfirmed(assertScopeCurrent);
|
|
234
260
|
observation.observe(outcome);
|
|
235
261
|
worker.snapshot = { ...outcome, recovery_observation: observation.state() };
|
|
236
|
-
|
|
262
|
+
persistWorker();
|
|
237
263
|
if (worker.snapshot.drain?.physical_settled === true) {
|
|
238
264
|
worker.status = "completed";
|
|
239
265
|
break;
|
|
@@ -259,18 +285,19 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
|
259
285
|
lease?.close();
|
|
260
286
|
if (heartbeat)
|
|
261
287
|
clearInterval(heartbeat);
|
|
262
|
-
|
|
263
|
-
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation:
|
|
288
|
+
observationCleanup?.exhausted();
|
|
289
|
+
worker.snapshot = { ...(worker.snapshot ?? {}), ...(observationCleanup ? { recovery_observation: observationCleanup.state() } : {}) };
|
|
264
290
|
let persistenceFailure;
|
|
265
291
|
try {
|
|
266
|
-
|
|
292
|
+
persistWorker();
|
|
267
293
|
}
|
|
268
294
|
catch (error) {
|
|
269
295
|
persistenceFailure = error;
|
|
270
296
|
process.stderr.write(`${JSON.stringify({ primary: worker.error, primary_error: worker.snapshot.error, phase: "persist_scope_owner", secondary: errorRecord(error) })}\n`);
|
|
271
297
|
}
|
|
272
298
|
try {
|
|
273
|
-
|
|
299
|
+
const current = claimedByThisServe && db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
300
|
+
if (current && current.metadata_json === retainedMetadata && !persistenceFailure && worker.process_id && worker.process_lease_token)
|
|
274
301
|
finishManagedProcess(context, { id: worker.process_id, leaseToken: worker.process_lease_token, state: worker.status === "failed" ? "failed" : "stopped", reason: "scope_owner_exited" });
|
|
275
302
|
}
|
|
276
303
|
catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
2
3
|
import { continuationPolicies, flowKinds } from "../domain/contracts.js";
|
|
3
4
|
import { AppError } from "../shared/errors.js";
|
|
4
5
|
import { hookSessionIdentity } from "./hooks.js";
|
|
@@ -23,25 +24,7 @@ export function publicFlowSession(context, session) {
|
|
|
23
24
|
delete publicFields.parent_session_id;
|
|
24
25
|
return { ...publicFields, harness_id: identity.harness_id, session_id: identity.session_id, parent_session: parent ? publicSessionIdentity(parent) : null };
|
|
25
26
|
}
|
|
26
|
-
export
|
|
27
|
-
const duplicate = context.db.get("SELECT id FROM flow_session_segments WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
28
|
-
if (duplicate)
|
|
29
|
-
return { changed: false, segment_id: duplicate.id };
|
|
30
|
-
const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
|
|
31
|
-
WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
|
|
32
|
-
const now = context.now();
|
|
33
|
-
const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
|
|
34
|
-
const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
|
|
35
|
-
if (open && open.run_id === runId && open.protocol_id === protocolId) {
|
|
36
|
-
return { changed: false, segment_id: open.id };
|
|
37
|
-
}
|
|
38
|
-
if (open)
|
|
39
|
-
context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE id = ? AND ended_at IS NULL", [now, open.id]);
|
|
40
|
-
const result = context.db.run(`INSERT INTO flow_session_segments
|
|
41
|
-
(project_id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name, event_key)
|
|
42
|
-
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
|
|
43
|
-
return { changed: true, segment_id: Number(result.lastInsertRowid) };
|
|
44
|
-
}
|
|
27
|
+
export { recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
45
28
|
export function registerFlowJob(context, input) {
|
|
46
29
|
const jobId = input.jobId ?? `JOB-${input.runId}-${input.planItemId}`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
47
30
|
const now = context.now();
|
|
@@ -167,11 +150,7 @@ export function stoppedMergeWorkerState(context, projectId, workerId) {
|
|
|
167
150
|
status: latest?.status ?? null
|
|
168
151
|
};
|
|
169
152
|
}
|
|
170
|
-
export
|
|
171
|
-
return context.db.all(`SELECT * FROM sessions
|
|
172
|
-
WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
|
|
173
|
-
ORDER BY updated_at DESC`, [projectId]);
|
|
174
|
-
}
|
|
153
|
+
export { activeFlowSessionsForProject } from "./hook-session-observation.js";
|
|
175
154
|
export function flowSessionsForProject(context, projectId, filter = {}) {
|
|
176
155
|
if (filter.sessionId) {
|
|
177
156
|
return context.db.all("SELECT * FROM sessions WHERE project_id = ? AND (session_id = ? OR provider_session_id = ?) ORDER BY updated_at DESC", [projectId, filter.sessionId, filter.sessionId]);
|
package/dist/services/setup.js
CHANGED
|
@@ -60,7 +60,7 @@ function atomicJson(file, value) {
|
|
|
60
60
|
fs.unlinkSync(temporary);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
function readSetupRequest(file) {
|
|
63
|
+
function readSetupRequest(file, validateExecutor = true) {
|
|
64
64
|
const value = regularJson(file);
|
|
65
65
|
const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
|
|
66
66
|
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
@@ -77,7 +77,7 @@ function readSetupRequest(file) {
|
|
|
77
77
|
|| !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
|
|
78
78
|
|| !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
|
|
79
79
|
refuse("invalid_request_record");
|
|
80
|
-
if (Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
80
|
+
if (validateExecutor && Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
81
81
|
try {
|
|
82
82
|
prepareOperatorProfile(request.executor);
|
|
83
83
|
}
|
|
@@ -85,7 +85,7 @@ function readSetupRequest(file) {
|
|
|
85
85
|
refuse("invalid_request_executor");
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
if (Object.hasOwn(request, "profileCreation")) {
|
|
88
|
+
if (validateExecutor && Object.hasOwn(request, "profileCreation")) {
|
|
89
89
|
try {
|
|
90
90
|
validateProfileCreationIntent(request.profileCreation);
|
|
91
91
|
}
|
|
@@ -225,22 +225,72 @@ export function commitSetupOnboarding(home, requestId, selection) {
|
|
|
225
225
|
return result;
|
|
226
226
|
}
|
|
227
227
|
/** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
|
|
228
|
+
function validateInstallation(root, installation) {
|
|
229
|
+
if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
|
|
230
|
+
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
231
|
+
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
232
|
+
refuse("invalid_installation_selection");
|
|
233
|
+
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
234
|
+
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
235
|
+
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
236
|
+
refuse("invalid_installation_selection");
|
|
237
|
+
}
|
|
238
|
+
/** Independently committed canonical data, never activation or old code
|
|
239
|
+
* admission. A missing old engine cache is irrelevant to comparing documents.
|
|
240
|
+
* Same canon commit/version with different archive bytes is not disambiguated
|
|
241
|
+
* by picking the newest CLI or whichever request happens to be listed first.
|
|
242
|
+
*/
|
|
243
|
+
export function readCommittedCanonRelease(home, version, commit, expectedInstallationSha256) {
|
|
244
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
245
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version) || !/^[a-f0-9]{40}$/.test(commit)
|
|
246
|
+
|| expectedInstallationSha256 !== undefined && !/^[a-f0-9]{64}$/.test(expectedInstallationSha256))
|
|
247
|
+
refuse("invalid_canonical_baseline_selection");
|
|
248
|
+
for (const target of [root, directory])
|
|
249
|
+
if (fs.existsSync(target) && !fs.lstatSync(target).isDirectory())
|
|
250
|
+
refuse("unsafe_setup_directory");
|
|
251
|
+
if (!fs.existsSync(directory))
|
|
252
|
+
return null;
|
|
253
|
+
const matches = [];
|
|
254
|
+
for (const name of fs.readdirSync(directory).filter(name => name.endsWith(".json")).sort()) {
|
|
255
|
+
const request = readSetupRequest(path.join(directory, name), false);
|
|
256
|
+
if (`${request.request_id}.json` !== name)
|
|
257
|
+
refuse("request_identity_conflict");
|
|
258
|
+
if (request.phase !== "committed" || request.installation.manifest?.canon?.version !== version || request.installation.manifest.canon.commit !== commit)
|
|
259
|
+
continue;
|
|
260
|
+
if (expectedInstallationSha256 !== undefined && request.installation.manifest_sha256 !== expectedInstallationSha256)
|
|
261
|
+
continue;
|
|
262
|
+
validateInstallation(root, request.installation);
|
|
263
|
+
const binding = { installation: request.installation,
|
|
264
|
+
...(Object.hasOwn(request, "executor") ? { executor: request.executor } : {}),
|
|
265
|
+
...(request.profileCreation ? { profileCreation: request.profileCreation } : {}),
|
|
266
|
+
...(request.deferredOnboarding ? { deferredOnboarding: true } : {}) };
|
|
267
|
+
if (!["install", "update"].some(action => hybridDigest({ action, ...binding }) === request.identity))
|
|
268
|
+
refuse("request_identity_conflict");
|
|
269
|
+
matches.push(request.installation);
|
|
270
|
+
}
|
|
271
|
+
if (!matches.length)
|
|
272
|
+
return null;
|
|
273
|
+
if (new Set(matches.map(installation => `${installation.manifest.canon.archive.sha256}:${installation.manifest.canon.archive.bytes}`)).size !== 1)
|
|
274
|
+
refuse("ambiguous_canonical_baseline");
|
|
275
|
+
const selected = matches.sort((left, right) => left.manifest_sha256.localeCompare(right.manifest_sha256, "en"))[0];
|
|
276
|
+
try {
|
|
277
|
+
verifyPublishedReleaseCache(path.dirname(selected.canon_root), selected.manifest.canon.archive);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error.code === "ENOENT")
|
|
281
|
+
return null;
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return structuredClone(selected);
|
|
285
|
+
}
|
|
228
286
|
export function readSetupStatus(home) {
|
|
229
287
|
const root = path.resolve(home);
|
|
230
288
|
if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
|
|
231
289
|
refuse("unsafe_setup_home");
|
|
232
290
|
const file = path.join(root, "installation.json");
|
|
233
291
|
const installation = fs.existsSync(file) ? regularJson(file) : null;
|
|
234
|
-
if (installation)
|
|
235
|
-
|
|
236
|
-
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
237
|
-
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
238
|
-
refuse("invalid_installation_selection");
|
|
239
|
-
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
240
|
-
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
241
|
-
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
242
|
-
refuse("invalid_installation_selection");
|
|
243
|
-
}
|
|
292
|
+
if (installation)
|
|
293
|
+
validateInstallation(root, installation);
|
|
244
294
|
let readiness = installation ? "installed" : "not_installed";
|
|
245
295
|
if (installation) {
|
|
246
296
|
try {
|
|
@@ -9,7 +9,7 @@ import { finishVnextMerge, startVnextMerge } from "./vnext-merge.js";
|
|
|
9
9
|
import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
|
|
10
10
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
11
11
|
import { sdlcHandlerIdentities as ids } from "../domain/stage-handler-identities.js";
|
|
12
|
-
import { memoryInitStageHandlers } from "./memory-stage-dispatch.js";
|
|
12
|
+
import { memoryInitStageHandlers, memoryUpgradeStageHandlers } from "./memory-stage-dispatch.js";
|
|
13
13
|
/** Engine-owned code, not a plugin registration surface or executable descriptor expression. */
|
|
14
14
|
export const sdlcStageHandlers = Object.freeze({
|
|
15
15
|
specify: Object.freeze({ ...ids.specify, entry: launchVnextSpecify, finish: finishVnextSpecifyStage }),
|
|
@@ -23,8 +23,8 @@ export const sdlcStageHandlers = Object.freeze({
|
|
|
23
23
|
/** A fresh inventory cannot mutate the registered handler functions. */
|
|
24
24
|
export function bundledHandlerContext(blobs) {
|
|
25
25
|
return {
|
|
26
|
-
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.entryId)),
|
|
27
|
-
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.finishId)), blobs
|
|
26
|
+
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers)].map(handler => handler.entryId)),
|
|
27
|
+
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers)].map(handler => handler.finishId)), blobs
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
/** Handler identity is also stage/flow scoped: a known CODE handler is not a memory handler. */
|
|
@@ -40,8 +40,9 @@ export function admitBundledFlow(input, sha256, blobs) {
|
|
|
40
40
|
throw new AppError("flow_handler_scope_invalid", "SDLC handlers require the registered seven-stage layout and transitions", 2, { phase: "prepare", effect: "no_effect" });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
if (flow.descriptor.id === "mb-init") {
|
|
44
|
-
const
|
|
43
|
+
if (flow.descriptor.id === "mb-init" || flow.descriptor.id === "mb-upgrade") {
|
|
44
|
+
const upgrade = flow.descriptor.id === "mb-upgrade";
|
|
45
|
+
const stages = Object.keys(upgrade ? memoryUpgradeStageHandlers : memoryInitStageHandlers);
|
|
45
46
|
const edges = stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" }));
|
|
46
47
|
// Historical packs have only accepted transitions; diagnostic VERIFY uses
|
|
47
48
|
// the same REVIEW handler without granting another route or stage.
|
|
@@ -59,12 +60,13 @@ export function admitBundledFlow(input, sha256, blobs) {
|
|
|
59
60
|
schemaId = JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id;
|
|
60
61
|
}
|
|
61
62
|
catch { /* Invalid schema cannot authorize this stage handler. */ }
|
|
62
|
-
if (schemaId !== `dd-flow/mb-init-${stage.id}-result@1`)
|
|
63
|
+
if (schemaId !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : `dd-flow/mb-init-${stage.id}-result@1`))
|
|
63
64
|
throw new AppError("flow_handler_scope_invalid", "Memory init stage requires its own registered result schema", 2, { phase: "prepare", effect: "no_effect", stage: stage.id });
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
for (const stage of flow.descriptor.stages) {
|
|
67
|
-
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
|
|
68
|
+
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
|
|
69
|
+
: flow.descriptor.id === "mb-upgrade" ? Object.values(memoryUpgradeStageHandlers) : [];
|
|
68
70
|
const handler = handlers.find(candidate => candidate.stage === stage.id);
|
|
69
71
|
if (!handler || stage.entry_handler !== handler.entryId || stage.finish_handler !== handler.finishId) {
|
|
70
72
|
throw new AppError("flow_handler_scope_invalid", "Registered handlers do not belong to this flow stage", 2, { phase: "prepare", effect: "no_effect", flow_id: flow.descriptor.id, stage: stage.id });
|
|
@@ -17,7 +17,7 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
|
17
17
|
import { runtimeCommand } from "./runtime-command.js";
|
|
18
18
|
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
19
19
|
import { latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
20
|
-
import {
|
|
20
|
+
import { acceptedIntentReviewFingerprint, prepareAcceptedIntentSources, validatePreparedAcceptedIntentReview } from "./accepted-intent.js";
|
|
21
21
|
export { flowCommand } from "./runtime-command.js";
|
|
22
22
|
export function prepareStagePause(context, input) {
|
|
23
23
|
if (typeof input.question !== "string" || !input.question.trim())
|
|
@@ -116,7 +116,31 @@ export function resumeStageAfterUser(context, input) {
|
|
|
116
116
|
// reserving SQLite's writer; an unreferenced candidate has no flow authority.
|
|
117
117
|
publishImmutableInput(answerPath, Buffer.from(input.answer), "hitl_answer_conflict");
|
|
118
118
|
const answerPacket = prepareStagePacket(answerPath, Buffer.from(input.answer));
|
|
119
|
-
|
|
119
|
+
// Capture the exact continuation inputs and already-issued commands before
|
|
120
|
+
// accepting the answer. A publication retry must not issue new authority.
|
|
121
|
+
const retryCommands = lifecycleRetryCommands(context, stage.stage);
|
|
122
|
+
const resumeSourcePath = path.join(path.dirname(pause.question_path), `resume-${crypto.createHash("sha256").update(`${pause.id}\0${input.answer}`).digest("hex")}.json`);
|
|
123
|
+
let review;
|
|
124
|
+
let reviewPreparationError;
|
|
125
|
+
const previousAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
|
|
126
|
+
if (["plan-review", "code-review"].includes(stage.stage)) {
|
|
127
|
+
try {
|
|
128
|
+
const home = requireRunHome(run), source = latestRunVerificationMatrix(context, project.id, run.id, stage.stage, "input");
|
|
129
|
+
const reviewContext = path.join(home, stage.stage === "plan-review" ? "04-plan-review/work-context.json" : "06-code-review/review-context.json");
|
|
130
|
+
const retainedContext = fs.existsSync(reviewContext) ? JSON.parse(fs.readFileSync(reviewContext, "utf8")) : null;
|
|
131
|
+
const currentSources = acceptedIntentReviewFingerprint(source?.metadata.accepted_sources) ? prepareAcceptedIntentSources(context, { projectId: project.id, runId: run.id, home, stageDirectory: "intake", marked: true }).sources : [];
|
|
132
|
+
review = { home, source, retainedContext, currentSources };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!(error instanceof AppError))
|
|
136
|
+
throw error;
|
|
137
|
+
reviewPreparationError = error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return context.db.writeTransaction(() => {
|
|
141
|
+
const currentAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
|
|
142
|
+
if (currentAnswer !== previousAnswer)
|
|
143
|
+
throw new AppError("runtime_state_changed", "Accepted inputs changed during resume preparation", 1);
|
|
120
144
|
const identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
|
|
121
145
|
if (workSession.session_id !== identity.sessionId) {
|
|
122
146
|
bindSessionForResume(context, work.work_id, identity, now);
|
|
@@ -130,49 +154,56 @@ export function resumeStageAfterUser(context, input) {
|
|
|
130
154
|
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: answerPacket });
|
|
131
155
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
|
|
132
156
|
context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
intentInstructions.push(`
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
const prompt = evidence.prompt;
|
|
158
|
+
const intentInstructions = [];
|
|
159
|
+
if (["plan-review", "code-review"].includes(stage.stage)) {
|
|
160
|
+
try {
|
|
161
|
+
if (reviewPreparationError)
|
|
162
|
+
throw reviewPreparationError;
|
|
163
|
+
if (review) {
|
|
164
|
+
const id = context.db.get("SELECT id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.pause_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, pause.id]).id;
|
|
165
|
+
const identity = `accepted_hitl:${String(id).padStart(12, "0")}:${pause.id}`;
|
|
166
|
+
const sources = [...review.currentSources, { role: `${identity}:0-question`, sha256: crypto.createHash("sha256").update(evidence.question).digest("hex") }, { role: `${identity}:1-answer`, sha256: crypto.createHash("sha256").update(input.answer).digest("hex") }];
|
|
167
|
+
validatePreparedAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home: review.home, stage: stage.stage, decisionFrozen: Boolean(review.retainedContext?.accepted_decision) }, review.source?.metadata.accepted_sources, review.retainedContext?.accepted_intent_review_fingerprint, sources);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof AppError))
|
|
172
|
+
throw error;
|
|
173
|
+
intentInstructions.push(`Accepted-input review boundary: ${error.code}: ${error.message}`);
|
|
174
|
+
if (error.code === "accepted_intent_review_ack_required")
|
|
175
|
+
intentInstructions.push(`Apply/classify this own-stage clarification through the existing coordinator correction/repair route. Include this exact field in the decision: ${JSON.stringify({ accepted_intent_acknowledgement: error.details.accepted_intent_acknowledgement })}. Independent reviewer results prove only the earlier retained inputs; do not claim they reviewed the new answer.`);
|
|
176
|
+
else
|
|
177
|
+
intentInstructions.push("Do not accept stale independent review as coverage of this answer; report the actionable upstream revision/evidence blocker.");
|
|
178
|
+
}
|
|
152
179
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
""
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
const continuation = [
|
|
181
|
+
prompt.trimEnd(),
|
|
182
|
+
"",
|
|
183
|
+
"<hitl_resume>",
|
|
184
|
+
`- Pause: ${pause.id}`,
|
|
185
|
+
`- Question source: ${pause.question_path}`,
|
|
186
|
+
`- Answer source: ${answerPath}`,
|
|
187
|
+
"- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
|
|
188
|
+
...intentInstructions,
|
|
189
|
+
...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
|
|
190
|
+
"",
|
|
191
|
+
"<user_question>",
|
|
192
|
+
evidence.question,
|
|
193
|
+
"</user_question>",
|
|
194
|
+
"",
|
|
195
|
+
"<user_answer>",
|
|
196
|
+
input.answer,
|
|
197
|
+
"</user_answer>",
|
|
198
|
+
"</hitl_resume>",
|
|
199
|
+
""
|
|
200
|
+
].join("\n");
|
|
201
|
+
const result = { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, resume_source_path: resumeSourcePath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
202
|
+
const scope = context.env.DD_FLOW_INVOCATION_SCOPE ? JSON.parse(context.env.DD_FLOW_INVOCATION_SCOPE) : null;
|
|
203
|
+
const bytes = Buffer.from(`${JSON.stringify({ invocation_id: context.env.DD_FLOW_CURRENT_INVOCATION ?? null, generation: scope?.generation ?? null, result })}\n`);
|
|
204
|
+
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: { file: resumeSourcePath, bytes, sha256: crypto.createHash("sha256").update(bytes).digest("hex") } });
|
|
205
|
+
return result;
|
|
206
|
+
});
|
|
176
207
|
}
|
|
177
208
|
export function stagePauseCommand(context, input) {
|
|
178
209
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -180,6 +211,39 @@ export function stagePauseCommand(context, input) {
|
|
|
180
211
|
const questionInputPath = prepareAgentOutputFile(path.join(requireRunHome(run), "works", input.workId, "question-input.md"));
|
|
181
212
|
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
182
213
|
}
|
|
214
|
+
/** Recover only the exact resume committed with this invocation; never resume again. */
|
|
215
|
+
export function recoverCommittedStageResume(context, input) {
|
|
216
|
+
const project = requireProjectByRoot(context, input.projectRoot), run = requireRun(context, project.id, input.runId);
|
|
217
|
+
const work = requireWork(context, project.id, run.id, input.workId);
|
|
218
|
+
const index = JSON.parse(run.index_json);
|
|
219
|
+
const current = index.stage_runs.find(stage => stage.stage === input.stage);
|
|
220
|
+
if (!current || current.status !== "running" || index.current_stage && index.current_stage !== input.stage || work.status !== "running")
|
|
221
|
+
return null;
|
|
222
|
+
const resumed = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.work_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, input.stage, work.work_id]);
|
|
223
|
+
if (!resumed)
|
|
224
|
+
return null;
|
|
225
|
+
const acceptedPause = JSON.parse(resumed.payload_json);
|
|
226
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.attempt') = ? AND json_extract(payload_json, '$.file') LIKE '%/resume-%.json' ORDER BY id DESC LIMIT 257", [project.id, run.id, input.stage, current.attempt]);
|
|
227
|
+
if (rows.length > 256)
|
|
228
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt count exceeds its bound", 1);
|
|
229
|
+
let result = null;
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
const packet = JSON.parse(row.payload_json);
|
|
232
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
233
|
+
if (bytes.length > 8 * 1024 * 1024 || bytes.toString("base64") !== packet.base64 || crypto.createHash("sha256").update(bytes).digest("hex") !== packet.sha256)
|
|
234
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt custody differs from its committed bytes", 1);
|
|
235
|
+
const retained = JSON.parse(bytes.toString("utf8"));
|
|
236
|
+
if (retained.invocation_id !== input.id)
|
|
237
|
+
continue;
|
|
238
|
+
const value = retained.result;
|
|
239
|
+
if (retained.generation !== input.generation || !value || value.run_id !== run.id || value.work_id !== work.work_id || value.stage !== input.stage
|
|
240
|
+
|| value.pause_id !== acceptedPause.pause_id || value.answer_path !== acceptedPause.answer_path || value.resume_source_path !== packet.file || typeof value.worker_prompt_markdown !== "string"
|
|
241
|
+
|| result && JSON.stringify(result) !== JSON.stringify(value))
|
|
242
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt does not match the current invocation and accepted pause", 1);
|
|
243
|
+
result = value;
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
183
247
|
/** Render separate file-write and lifecycle-call instructions for HITL. */
|
|
184
248
|
export function stagePauseCommandTemplate(command) {
|
|
185
249
|
const questionFile = command.match(/--question-file\s+(?:'([^']*)'|"([^"]*)"|(\S+))/)?.slice(1).find(Boolean) ?? "<question-file-from-command>";
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
const canonicalOnlyFlowNames = ["mb-init.md", "mb-upgrade.md", "mb-distill.md"];
|
|
5
|
+
export const canonicalOnlyFlowNames = Object.freeze(["mb-init.md", "mb-upgrade.md", "mb-distill.md"]);
|
|
6
6
|
export function resolveStatusProjectRoot(input) {
|
|
7
7
|
if (input.requestedRoot) {
|
|
8
8
|
const absolute = path.resolve(input.requestedRoot);
|
|
@@ -105,7 +105,7 @@ export function verifiedNativeWorkFailure(context, input) {
|
|
|
105
105
|
}
|
|
106
106
|
export function fanoutState(input) {
|
|
107
107
|
if (!input.hasWork)
|
|
108
|
-
return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
|
|
108
|
+
return input.registeredEmpty ? "children_settled" : input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
|
|
109
109
|
if (input.capacityRequired && !input.capacityKnown)
|
|
110
110
|
return "capacity_required";
|
|
111
111
|
if (input.created || input.running)
|
|
@@ -133,14 +133,17 @@ export function readFanoutDescriptor(stageRoot) {
|
|
|
133
133
|
* productive assignments, including reviewer/correction waves.
|
|
134
134
|
*/
|
|
135
135
|
export function memoryStageFanoutDescriptor(context, input) {
|
|
136
|
-
if (!["compose", "review"].includes(input.stage))
|
|
136
|
+
if (!["compose", "upgrade", "review"].includes(input.stage))
|
|
137
137
|
return null;
|
|
138
138
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
139
139
|
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
140
|
-
if (run
|
|
140
|
+
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
|
|
141
|
+
return null;
|
|
142
|
+
const authoringStage = run.flow_kind === "mb-init" ? "compose" : "upgrade";
|
|
143
|
+
if (input.stage !== authoringStage && input.stage !== "review")
|
|
141
144
|
return null;
|
|
142
145
|
const bundle = readFlowRunExecutionBundle(context, input);
|
|
143
|
-
if (!bundle || bundle.descriptor.descriptor.id !==
|
|
146
|
+
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind)
|
|
144
147
|
throw new AppError("stage_graph_compatibility_required", "Memory graph requires its admitted RUN bundle", 1);
|
|
145
148
|
const index = JSON.parse(run.index_json);
|
|
146
149
|
if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
|
|
@@ -153,7 +156,7 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
153
156
|
if (id === root.workId)
|
|
154
157
|
continue;
|
|
155
158
|
const work = context.db.get("SELECT parent_work_id,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [project.id, input.runId, id]);
|
|
156
|
-
const schemas = input.stage ===
|
|
159
|
+
const schemas = input.stage === authoringStage ? ["dd-flow/memory-composition-work-result@1"] : ["dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"];
|
|
157
160
|
if (!work || work.parent_work_id !== root.workId || !schemas.includes(work.result_schema ?? ""))
|
|
158
161
|
throw new AppError("stage_graph_conflict", "Memory graph contains an unassigned Work", 1);
|
|
159
162
|
const custody = readWorkBatchCustody(context, id);
|
|
@@ -161,11 +164,11 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
161
164
|
|| custody.scope.cycle !== scope.cycle || custody.scope.stage !== input.stage || custody.scope.parent_work_id !== root.workId)
|
|
162
165
|
throw new AppError("stage_graph_conflict", "Memory graph contains foreign allocation custody", 1);
|
|
163
166
|
}
|
|
164
|
-
if (input.stage ===
|
|
167
|
+
if (input.stage === authoringStage) {
|
|
165
168
|
const children = context.db.all("SELECT work_id FROM works WHERE project_id=? AND run_id=? AND parent_work_id=?", [project.id, input.runId, root.workId]).filter(work => members.has(work.work_id));
|
|
166
169
|
const registered = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? LIMIT 1", [project.id, input.runId, scope.attempt, scope.cycle]);
|
|
167
170
|
if (children.length || registered) {
|
|
168
|
-
const registration = selectMemoryCompositionRegistration(context, { ...scope, stage:
|
|
171
|
+
const registration = selectMemoryCompositionRegistration(context, { ...scope, stage: authoringStage, parent_work_id: root.workId });
|
|
169
172
|
const ids = new Set(Object.values(registration.work_ids));
|
|
170
173
|
if (ids.size !== children.length || children.some(work => !ids.has(work.work_id)))
|
|
171
174
|
throw new AppError("stage_graph_conflict", "Memory composition differs from its registered graph", 1);
|
|
@@ -185,7 +188,8 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
185
188
|
const index = JSON.parse(run.index_json);
|
|
186
189
|
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
|
|
187
190
|
const stageRoot = path.join(run.run_root, bundle ? bundle.descriptor.stage(input.stage).directory : vnextStageDirectory(input.stage));
|
|
188
|
-
|
|
191
|
+
const memoryDescriptor = memoryStageFanoutDescriptor(context, input);
|
|
192
|
+
let descriptor = memoryDescriptor ?? readFanoutDescriptor(stageRoot);
|
|
189
193
|
// Registration is authoritative even if projection publication was interrupted,
|
|
190
194
|
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
191
195
|
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
@@ -211,6 +215,14 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
211
215
|
const ready = { works: all.works.filter(work => work.ready) };
|
|
212
216
|
const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
|
|
213
217
|
const hasWork = all.works.length > 0;
|
|
218
|
+
// Only SQL-owned memory registration can attest a deliberately empty
|
|
219
|
+
// graph. A mutable fanout.json must never turn missing Work into success.
|
|
220
|
+
let registeredEmpty = false;
|
|
221
|
+
if (memoryDescriptor && input.stage === "upgrade" && !hasWork
|
|
222
|
+
&& context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='upgrade' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 1", [project.id, input.runId, currentScope.attempt, currentScope.cycle, descriptor.parent_work_id])) {
|
|
223
|
+
const registered = selectMemoryCompositionRegistration(context, { ...currentScope, stage: "upgrade", parent_work_id: descriptor.parent_work_id });
|
|
224
|
+
registeredEmpty = Object.keys(registered.work_ids).length === 0;
|
|
225
|
+
}
|
|
214
226
|
const capacityKnown = capacity.available_slots !== null;
|
|
215
227
|
return {
|
|
216
228
|
ok: true,
|
|
@@ -221,7 +233,7 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
221
233
|
// PLAN-REVIEW has a deterministic dispatcher. CODE and CODE-REVIEW do
|
|
222
234
|
// not: their coordinator first materializes the Work graph from its
|
|
223
235
|
// authoritative prompt. An empty graph is therefore not a success.
|
|
224
|
-
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
236
|
+
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, registeredEmpty, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
225
237
|
capacity,
|
|
226
238
|
graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
|
|
227
239
|
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
|