@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.110
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 +520 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +388 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- 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/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -4,10 +4,42 @@ import { appendAudit } from "./audit.js";
|
|
|
4
4
|
import { requireProjectByRoot } from "./projects.js";
|
|
5
5
|
import { persistProtocolState, protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
7
|
+
import { ensureLaneWorkspace, heartbeatLaneLock, laneRecord, prepareLaneLock, prepareLaneWaitTiming, prepareLaneWorkspace, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
8
8
|
import { stopMergeWorker, stoppedMergeWorkerState } from "./sessions.js";
|
|
9
9
|
import { getProtocolBranchContext, requireClaimableBranchBundle } from "./branch-context.js";
|
|
10
10
|
const mergeLockTtlSeconds = 300;
|
|
11
|
+
/** SQL evidence comes from the same read snapshot as filesystem preparation.
|
|
12
|
+
* Bundle membership scans only branch metadata; unrelated RUN writes do not
|
|
13
|
+
* invalidate an individual job's accepted inputs. */
|
|
14
|
+
function prepareMergePublication(context, root, workerId, prepare, protocolIds, mode, requireLock = true) {
|
|
15
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(root));
|
|
16
|
+
return context.db.readSnapshot(() => {
|
|
17
|
+
const value = prepare(), ids = JSON.stringify(protocolIds(value));
|
|
18
|
+
const observe = () => ({
|
|
19
|
+
root: context.db.get("SELECT root FROM projects WHERE id = ?", [project.id]),
|
|
20
|
+
lane: context.db.get("SELECT workspace_path, expected_branch, status FROM lanes WHERE project_id = ? AND name = 'merge'", [project.id]),
|
|
21
|
+
lock: context.db.get("SELECT id, worker_id, lease_token FROM lane_locks WHERE project_id = ? AND lane = 'merge' AND status = 'active'", [project.id]),
|
|
22
|
+
worker: workerId ? stoppedMergeWorkerState(context, project.id, workerId) : null,
|
|
23
|
+
queue: mode === "head" ? context.db.all("SELECT * FROM merge_queue WHERE project_id = ? AND status IN ('ready','requeued') ORDER BY created_at, id LIMIT 1", [project.id])
|
|
24
|
+
: context.db.all(`SELECT * FROM merge_queue WHERE project_id = ? ${mode === "bundle" ? "" : "AND protocol_id IN (SELECT value FROM json_each(?))"} ORDER BY id`, mode === "bundle" ? [project.id] : [project.id, ids]),
|
|
25
|
+
protocols: context.db.all("SELECT * FROM protocols WHERE project_id = ? AND id IN (SELECT value FROM json_each(?)) ORDER BY id", [project.id, ids]),
|
|
26
|
+
states: context.db.all("SELECT protocol_id, payload_json FROM audit_events WHERE id IN (SELECT MAX(id) FROM audit_events WHERE project_id = ? AND event_type = 'protocol.state_projection' AND protocol_id IN (SELECT value FROM json_each(?)) GROUP BY protocol_id) ORDER BY protocol_id", [project.id, ids]),
|
|
27
|
+
runs: context.db.all("SELECT id, subject_id, status, verdict, next_action, index_json, created_at, updated_at, completed_at FROM runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id IN (SELECT value FROM json_each(?)) ORDER BY id", [project.id, ids]),
|
|
28
|
+
...(mode === "bundle" ? {
|
|
29
|
+
membership: context.db.all("SELECT id, status, stage, next_action, workspace_json, route_json, active_def_json FROM protocols WHERE project_id = ? ORDER BY id", [project.id]),
|
|
30
|
+
worktrees: context.db.all("SELECT protocol_id, integration_branch, feature_branch, base_ref, worktree_path, status FROM worktree_records WHERE project_id = ? ORDER BY protocol_id", [project.id]),
|
|
31
|
+
sessions: context.db.all("SELECT session_id, protocol_id, worker_id, flow_kind, status, current_stage, next_action FROM sessions WHERE project_id = ? AND status IN ('pending','active','waiting_user','blocked','stopping') ORDER BY session_id", [project.id])
|
|
32
|
+
} : {})
|
|
33
|
+
});
|
|
34
|
+
const expected = JSON.stringify(observe());
|
|
35
|
+
return { value, assertCurrent: () => {
|
|
36
|
+
if (JSON.stringify(observe()) !== expected)
|
|
37
|
+
throw new AppError("merge_preparation_changed", "Merge ownership or input authority changed during preparation", 1, { effect: "no_effect" });
|
|
38
|
+
if (requireLock && !context.db.get("SELECT 1 FROM lane_locks WHERE project_id = ? AND lane = 'merge' AND status = 'active' AND worker_id = ? AND expires_at > ?", [project.id, workerId, context.now()]))
|
|
39
|
+
throw new AppError("lane_lock_not_active", "Prepared merge lost its live lane lease", 1, { effect: "no_effect" });
|
|
40
|
+
} };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
11
43
|
export function getMergeQueueStatus(context, input) {
|
|
12
44
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
13
45
|
return {
|
|
@@ -18,7 +50,10 @@ export function getMergeQueueStatus(context, input) {
|
|
|
18
50
|
export function getMergeBundleStatus(context, input) {
|
|
19
51
|
return getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
20
52
|
}
|
|
21
|
-
export function
|
|
53
|
+
export function prepareMergeClaim(context, input) {
|
|
54
|
+
requireMergeText(input.workerId, "worker-id");
|
|
55
|
+
if (input.protocolId !== undefined)
|
|
56
|
+
requireMergeText(input.protocolId, "protocol-id");
|
|
22
57
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
23
58
|
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
24
59
|
if (stopState.stopped) {
|
|
@@ -34,23 +69,29 @@ export function claimNextMergeJob(context, input) {
|
|
|
34
69
|
workerId: input.workerId,
|
|
35
70
|
workspacePath: input.workspacePath
|
|
36
71
|
});
|
|
72
|
+
const job = input.protocolId !== undefined
|
|
73
|
+
? queueJobByProtocol(context, project.id, input.protocolId)
|
|
74
|
+
: context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND status IN ('ready', 'requeued') ORDER BY created_at ASC, id ASC LIMIT 1", [project.id]);
|
|
75
|
+
if (input.protocolId !== undefined && (!job || !["ready", "requeued"].includes(job.status))) {
|
|
76
|
+
throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, { protocol_id: input.protocolId, status: job?.status ?? "missing", allowed: ["ready", "requeued"] });
|
|
77
|
+
}
|
|
78
|
+
const transition = job ? prepareClaimedProtocolTransition(context, project.id, job.protocol_id) : null;
|
|
79
|
+
return { project, job, transition };
|
|
80
|
+
}
|
|
81
|
+
export function claimNextMergeJob(context, input) {
|
|
82
|
+
const prepared = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeClaim(context, input), value => value.job ? [value.job.protocol_id] : [], "head");
|
|
83
|
+
const { project, job, transition } = prepared.value;
|
|
37
84
|
const now = context.now();
|
|
38
85
|
let claimedProtocolId = null;
|
|
39
|
-
context.db.
|
|
40
|
-
|
|
41
|
-
const job = context.db.get(`SELECT * FROM merge_queue
|
|
42
|
-
WHERE project_id = ? AND status IN ('ready', 'requeued')
|
|
43
|
-
ORDER BY created_at ASC, id ASC
|
|
44
|
-
LIMIT 1`, [project.id]);
|
|
86
|
+
const early = context.db.writeTransaction(() => {
|
|
87
|
+
prepared.assertCurrent();
|
|
45
88
|
if (!job) {
|
|
46
|
-
context.db.exec("COMMIT");
|
|
47
89
|
return mergeQueueResult(null, { ok: true, outcome: "empty", claimed: false });
|
|
48
90
|
}
|
|
49
91
|
const update = context.db.run(`UPDATE merge_queue
|
|
50
92
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
51
93
|
WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
|
|
52
94
|
if (update.changes !== 1) {
|
|
53
|
-
context.db.exec("COMMIT");
|
|
54
95
|
return mergeQueueResult(null, { ok: true, outcome: "empty", claimed: false });
|
|
55
96
|
}
|
|
56
97
|
appendAudit(context, {
|
|
@@ -59,14 +100,12 @@ export function claimNextMergeJob(context, input) {
|
|
|
59
100
|
eventType: "merge_queue.claimed",
|
|
60
101
|
payload: { protocol_id: job.protocol_id, worker_id: input.workerId }
|
|
61
102
|
});
|
|
62
|
-
transitionClaimedProtocolToIntegration(context,
|
|
103
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
63
104
|
claimedProtocolId = job.protocol_id;
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
105
|
+
return null;
|
|
106
|
+
});
|
|
107
|
+
if (early)
|
|
108
|
+
return early;
|
|
70
109
|
return mergeQueueResult(claimedProtocolId ? queueJobByProtocol(context, project.id, claimedProtocolId) : null, {
|
|
71
110
|
ok: true,
|
|
72
111
|
outcome: claimedProtocolId ? "claimed" : "empty",
|
|
@@ -74,32 +113,13 @@ export function claimNextMergeJob(context, input) {
|
|
|
74
113
|
});
|
|
75
114
|
}
|
|
76
115
|
export function claimMergeJob(context, input) {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim another merge job", 1, {
|
|
81
|
-
worker_id: input.workerId,
|
|
82
|
-
status: stopState.status,
|
|
83
|
-
reason: stopState.reason
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
requireLaneLockOwner(context, {
|
|
87
|
-
projectRoot: project.root,
|
|
88
|
-
lane: "merge",
|
|
89
|
-
workerId: input.workerId,
|
|
90
|
-
workspacePath: input.workspacePath
|
|
91
|
-
});
|
|
116
|
+
requireMergeText(input.protocolId, "protocol-id");
|
|
117
|
+
const prepared = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeClaim(context, input), () => [input.protocolId], "target");
|
|
118
|
+
const { project, job: selectedJob, transition: selectedTransition } = prepared.value;
|
|
92
119
|
const now = context.now();
|
|
93
|
-
context.db.
|
|
94
|
-
|
|
95
|
-
const job =
|
|
96
|
-
if (!job || !["ready", "requeued"].includes(job.status)) {
|
|
97
|
-
throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, {
|
|
98
|
-
protocol_id: input.protocolId,
|
|
99
|
-
status: job?.status ?? "missing",
|
|
100
|
-
allowed: ["ready", "requeued"]
|
|
101
|
-
});
|
|
102
|
-
}
|
|
120
|
+
return context.db.writeTransaction(() => {
|
|
121
|
+
prepared.assertCurrent();
|
|
122
|
+
const job = selectedJob, transition = selectedTransition;
|
|
103
123
|
const update = context.db.run(`UPDATE merge_queue
|
|
104
124
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
105
125
|
WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
|
|
@@ -112,16 +132,12 @@ export function claimMergeJob(context, input) {
|
|
|
112
132
|
eventType: "merge_queue.claimed",
|
|
113
133
|
payload: { protocol_id: job.protocol_id, worker_id: input.workerId, targeted: true }
|
|
114
134
|
});
|
|
115
|
-
transitionClaimedProtocolToIntegration(context,
|
|
116
|
-
context.db.exec("COMMIT");
|
|
135
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
117
136
|
return mergeQueueResult(queueJobByProtocol(context, project.id, job.protocol_id), { ok: true, outcome: "claimed", claimed: true, targeted: true });
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
context.db.exec("ROLLBACK");
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
137
|
+
});
|
|
123
138
|
}
|
|
124
|
-
export function
|
|
139
|
+
export function prepareMergeBundleClaim(context, input) {
|
|
140
|
+
requireMergeText(input.workerId, "worker-id");
|
|
125
141
|
const { project, protocolIds } = requireClaimableBranchBundle(context, {
|
|
126
142
|
projectRoot: input.projectRoot,
|
|
127
143
|
workspacePath: input.workspacePath
|
|
@@ -140,9 +156,16 @@ export function claimMergeBundle(context, input) {
|
|
|
140
156
|
workerId: input.workerId,
|
|
141
157
|
workspacePath: input.workspacePath
|
|
142
158
|
});
|
|
159
|
+
const transitions = protocolIds.map((protocolId) => prepareClaimedProtocolTransition(context, project.id, protocolId));
|
|
160
|
+
return { project, protocolIds, transitions };
|
|
161
|
+
}
|
|
162
|
+
export function claimMergeBundle(context, input) {
|
|
163
|
+
const prepared = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeBundleClaim(context, input), value => value.protocolIds, "bundle");
|
|
164
|
+
const { project, protocolIds, transitions } = prepared.value;
|
|
143
165
|
const now = context.now();
|
|
144
|
-
context.db.
|
|
145
|
-
|
|
166
|
+
context.db.writeTransaction(() => {
|
|
167
|
+
prepared.assertCurrent();
|
|
168
|
+
// Claim every queue row before publishing any filesystem state.
|
|
146
169
|
for (const protocolId of protocolIds) {
|
|
147
170
|
const update = context.db.run(`UPDATE merge_queue
|
|
148
171
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
@@ -153,7 +176,10 @@ export function claimMergeBundle(context, input) {
|
|
|
153
176
|
protocol_ids: protocolIds
|
|
154
177
|
});
|
|
155
178
|
}
|
|
156
|
-
|
|
179
|
+
}
|
|
180
|
+
for (const transition of transitions) {
|
|
181
|
+
const protocolId = transition.protocol.id;
|
|
182
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
157
183
|
appendAudit(context, {
|
|
158
184
|
protocolId,
|
|
159
185
|
projectId: project.id,
|
|
@@ -166,12 +192,7 @@ export function claimMergeBundle(context, input) {
|
|
|
166
192
|
eventType: "merge_bundle.claimed",
|
|
167
193
|
payload: { project_id: project.id, worker_id: input.workerId, protocol_ids: protocolIds, workspace_path: input.workspacePath }
|
|
168
194
|
});
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
context.db.exec("ROLLBACK");
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
195
|
+
});
|
|
175
196
|
return mergeBundleResult(context, {
|
|
176
197
|
ok: true,
|
|
177
198
|
outcome: "claimed",
|
|
@@ -182,22 +203,38 @@ export function claimMergeBundle(context, input) {
|
|
|
182
203
|
protocolIds
|
|
183
204
|
});
|
|
184
205
|
}
|
|
185
|
-
export
|
|
206
|
+
export function prepareMergeQueueWait(context, input) {
|
|
207
|
+
requireMergeText(input.workerId, "worker-id");
|
|
208
|
+
if (typeof input.acquireLock !== "boolean")
|
|
209
|
+
throw new AppError("validation", "acquire-lock must be boolean", 2);
|
|
210
|
+
const { timeoutSeconds, pollIntervalSeconds } = prepareLaneWaitTiming(input);
|
|
186
211
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
187
|
-
const timeoutSeconds = input.timeoutSeconds ?? 0;
|
|
188
|
-
const pollIntervalSeconds = input.pollIntervalSeconds ?? 10;
|
|
189
|
-
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
190
|
-
throw new AppError("validation", "--timeout must be zero or a positive number", 2);
|
|
191
|
-
}
|
|
192
|
-
if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds <= 0) {
|
|
193
|
-
throw new AppError("validation", "--poll-interval must be a positive number", 2);
|
|
194
|
-
}
|
|
195
212
|
if (input.acquireLock && pollIntervalSeconds >= mergeLockTtlSeconds) {
|
|
196
213
|
throw new AppError("validation", "--poll-interval must be lower than the merge lock TTL when --acquire-lock true", 2, {
|
|
197
214
|
lock_ttl_seconds: mergeLockTtlSeconds
|
|
198
215
|
});
|
|
199
216
|
}
|
|
200
217
|
const initialStopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
218
|
+
if (!initialStopState.stopped) {
|
|
219
|
+
const next = context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND status IN ('ready', 'requeued') ORDER BY created_at ASC, id ASC LIMIT 1", [project.id]);
|
|
220
|
+
if (next)
|
|
221
|
+
prepareClaimedProtocolTransition(context, project.id, next.protocol_id);
|
|
222
|
+
if (input.acquireLock) {
|
|
223
|
+
if (laneRecord(context, project.id, "merge")) {
|
|
224
|
+
prepareLaneLock(context, { ...input, lane: "merge", reason: "merge queue wait-next", ttlSeconds: mergeLockTtlSeconds }, "wait-acquire");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
prepareLaneWorkspace(context, { ...input, lane: "merge" });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
requireLaneLockOwner(context, { ...input, lane: "merge" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { project, timeoutSeconds, pollIntervalSeconds, initialStopState };
|
|
235
|
+
}
|
|
236
|
+
export async function waitNextMergeJob(context, input) {
|
|
237
|
+
const { project, timeoutSeconds, pollIntervalSeconds, initialStopState } = prepareMergeQueueWait(context, input);
|
|
201
238
|
if (initialStopState.stopped) {
|
|
202
239
|
return mergeQueueResult(null, {
|
|
203
240
|
ok: true,
|
|
@@ -325,7 +362,9 @@ export async function waitNextMergeJob(context, input) {
|
|
|
325
362
|
await delay(pollIntervalSeconds * 1000);
|
|
326
363
|
}
|
|
327
364
|
}
|
|
328
|
-
export function
|
|
365
|
+
export function prepareMergeJobCompletion(context, input) {
|
|
366
|
+
requireMergeText(input.summary, "summary");
|
|
367
|
+
requireMergeText(input.workerId, "worker-id");
|
|
329
368
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
330
369
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
331
370
|
const job = requireClaimedJob(context, protocol.project_id, input.protocolId, input.workerId);
|
|
@@ -335,14 +374,18 @@ export function completeMergeJob(context, input) {
|
|
|
335
374
|
workerId: input.workerId,
|
|
336
375
|
workspacePath: input.workspacePath
|
|
337
376
|
});
|
|
338
|
-
const now = context.now();
|
|
339
|
-
context.db.run(`UPDATE merge_queue
|
|
340
|
-
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
341
|
-
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
342
377
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
343
378
|
const flowContract = flowContractForState(state);
|
|
344
379
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
345
380
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
381
|
+
return { protocol, job, state, flowContract, targetStage, targetStatus };
|
|
382
|
+
}
|
|
383
|
+
export function completeMergeJob(context, input) {
|
|
384
|
+
const { protocol, job, state, flowContract, targetStage, targetStatus } = prepareMergeJobCompletion(context, input);
|
|
385
|
+
const now = context.now();
|
|
386
|
+
context.db.run(`UPDATE merge_queue
|
|
387
|
+
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
388
|
+
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
346
389
|
persistProtocolState(context, protocol, {
|
|
347
390
|
...state,
|
|
348
391
|
stage: targetStage,
|
|
@@ -365,7 +408,9 @@ export function completeMergeJob(context, input) {
|
|
|
365
408
|
stop_after_current
|
|
366
409
|
});
|
|
367
410
|
}
|
|
368
|
-
export function
|
|
411
|
+
export function prepareMergeBundleCompletion(context, input) {
|
|
412
|
+
requireMergeText(input.summary, "summary");
|
|
413
|
+
requireMergeText(input.workerId, "worker-id");
|
|
369
414
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
370
415
|
requireLaneLockOwner(context, {
|
|
371
416
|
projectRoot: project.root,
|
|
@@ -380,19 +425,21 @@ export function completeMergeBundle(context, input) {
|
|
|
380
425
|
workspace_path: input.workspacePath
|
|
381
426
|
});
|
|
382
427
|
}
|
|
428
|
+
const jobs = protocolIds.map((protocolId) => prepareMergeJobCompletion(context, { ...input, protocolId }));
|
|
429
|
+
return { project, protocolIds, jobs };
|
|
430
|
+
}
|
|
431
|
+
export function completeMergeBundle(context, input) {
|
|
432
|
+
const publication = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeBundleCompletion(context, input), value => value.protocolIds, "bundle");
|
|
433
|
+
const prepared = publication.value;
|
|
383
434
|
const now = context.now();
|
|
384
|
-
context.db.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
435
|
+
context.db.writeTransaction(() => {
|
|
436
|
+
publication.assertCurrent();
|
|
437
|
+
const { project, protocolIds, jobs } = prepared;
|
|
438
|
+
for (const { protocol, job, state, flowContract, targetStage, targetStatus } of jobs) {
|
|
439
|
+
const protocolId = protocol.id;
|
|
389
440
|
context.db.run(`UPDATE merge_queue
|
|
390
441
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
391
442
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
392
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
393
|
-
const flowContract = flowContractForState(state);
|
|
394
|
-
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
395
|
-
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
396
443
|
persistProtocolState(context, protocol, {
|
|
397
444
|
...state,
|
|
398
445
|
stage: targetStage,
|
|
@@ -412,12 +459,8 @@ export function completeMergeBundle(context, input) {
|
|
|
412
459
|
eventType: "merge_bundle.completed",
|
|
413
460
|
payload: { project_id: project.id, worker_id: input.workerId, summary: input.summary, protocol_ids: protocolIds }
|
|
414
461
|
});
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
context.db.exec("ROLLBACK");
|
|
419
|
-
throw error;
|
|
420
|
-
}
|
|
462
|
+
});
|
|
463
|
+
const { project, protocolIds } = prepared;
|
|
421
464
|
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle complete after stop request");
|
|
422
465
|
return mergeBundleResult(context, {
|
|
423
466
|
ok: true,
|
|
@@ -430,7 +473,9 @@ export function completeMergeBundle(context, input) {
|
|
|
430
473
|
stop_after_current
|
|
431
474
|
});
|
|
432
475
|
}
|
|
433
|
-
export function
|
|
476
|
+
export function prepareMergeJobNote(context, input) {
|
|
477
|
+
requireMergeText(input.summary, "summary");
|
|
478
|
+
requireMergeText(input.workerId, "worker-id");
|
|
434
479
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
435
480
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
436
481
|
const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
|
|
@@ -449,6 +494,10 @@ export function noteMergeJob(context, input) {
|
|
|
449
494
|
claimed_by_session_id: job.claimed_by_session_id
|
|
450
495
|
});
|
|
451
496
|
}
|
|
497
|
+
return { protocol, job };
|
|
498
|
+
}
|
|
499
|
+
export function noteMergeJob(context, input) {
|
|
500
|
+
const { protocol, job } = prepareMergeJobNote(context, input);
|
|
452
501
|
const now = context.now();
|
|
453
502
|
context.db.run(`UPDATE merge_queue
|
|
454
503
|
SET last_reason = ?, updated_at = ?
|
|
@@ -461,7 +510,11 @@ export function noteMergeJob(context, input) {
|
|
|
461
510
|
});
|
|
462
511
|
return mergeQueueResult(queueJobByProtocol(context, protocol.project_id, protocol.id), { ok: true, outcome: "noted" });
|
|
463
512
|
}
|
|
464
|
-
export function
|
|
513
|
+
export function prepareMergeJobFailure(context, input) {
|
|
514
|
+
requireMergeText(input.reason, "reason");
|
|
515
|
+
requireMergeText(input.workerId, "worker-id");
|
|
516
|
+
if (typeof input.requeue !== "boolean")
|
|
517
|
+
throw new AppError("validation", "requeue must be boolean", 2);
|
|
465
518
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
466
519
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
467
520
|
const job = requireClaimedJob(context, protocol.project_id, input.protocolId, input.workerId);
|
|
@@ -471,13 +524,17 @@ export function failMergeJob(context, input) {
|
|
|
471
524
|
workerId: input.workerId,
|
|
472
525
|
workspacePath: input.workspacePath
|
|
473
526
|
});
|
|
527
|
+
const state = input.requeue ? readProtocolRuntimeState(context, protocol).state : null;
|
|
528
|
+
return { protocol, job, state };
|
|
529
|
+
}
|
|
530
|
+
export function failMergeJob(context, input) {
|
|
531
|
+
const { protocol, job, state } = prepareMergeJobFailure(context, input);
|
|
474
532
|
const now = context.now();
|
|
475
533
|
const status = input.requeue ? "requeued" : "failed";
|
|
476
534
|
context.db.run(`UPDATE merge_queue
|
|
477
535
|
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
478
536
|
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
479
|
-
if (
|
|
480
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
537
|
+
if (state) {
|
|
481
538
|
persistProtocolState(context, protocol, {
|
|
482
539
|
...state,
|
|
483
540
|
stage: "ready_for_merge",
|
|
@@ -500,7 +557,11 @@ export function failMergeJob(context, input) {
|
|
|
500
557
|
stop_after_current
|
|
501
558
|
});
|
|
502
559
|
}
|
|
503
|
-
export function
|
|
560
|
+
export function prepareMergeBundleFailure(context, input) {
|
|
561
|
+
requireMergeText(input.reason, "reason");
|
|
562
|
+
requireMergeText(input.workerId, "worker-id");
|
|
563
|
+
if (typeof input.requeue !== "boolean")
|
|
564
|
+
throw new AppError("validation", "requeue must be boolean", 2);
|
|
504
565
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
505
566
|
requireLaneLockOwner(context, {
|
|
506
567
|
projectRoot: project.root,
|
|
@@ -515,18 +576,23 @@ export function failMergeBundle(context, input) {
|
|
|
515
576
|
workspace_path: input.workspacePath
|
|
516
577
|
});
|
|
517
578
|
}
|
|
579
|
+
const jobs = protocolIds.map((protocolId) => prepareMergeJobFailure(context, { ...input, protocolId }));
|
|
580
|
+
return { project, protocolIds, jobs };
|
|
581
|
+
}
|
|
582
|
+
export function failMergeBundle(context, input) {
|
|
583
|
+
const publication = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeBundleFailure(context, input), value => value.protocolIds, "bundle");
|
|
584
|
+
const prepared = publication.value;
|
|
518
585
|
const now = context.now();
|
|
519
586
|
const status = input.requeue ? "requeued" : "failed";
|
|
520
|
-
context.db.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
587
|
+
context.db.writeTransaction(() => {
|
|
588
|
+
publication.assertCurrent();
|
|
589
|
+
const { project, protocolIds, jobs } = prepared;
|
|
590
|
+
for (const { protocol, job, state } of jobs) {
|
|
591
|
+
const protocolId = protocol.id;
|
|
525
592
|
context.db.run(`UPDATE merge_queue
|
|
526
593
|
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
527
594
|
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
528
|
-
if (
|
|
529
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
595
|
+
if (state) {
|
|
530
596
|
persistProtocolState(context, protocol, {
|
|
531
597
|
...state,
|
|
532
598
|
stage: "ready_for_merge",
|
|
@@ -549,12 +615,8 @@ export function failMergeBundle(context, input) {
|
|
|
549
615
|
reason: input.reason,
|
|
550
616
|
payload: { project_id: project.id, worker_id: input.workerId, requeue: input.requeue, protocol_ids: protocolIds }
|
|
551
617
|
});
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
catch (error) {
|
|
555
|
-
context.db.exec("ROLLBACK");
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
618
|
+
});
|
|
619
|
+
const { project, protocolIds } = prepared;
|
|
558
620
|
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle fail after stop request");
|
|
559
621
|
return mergeBundleResult(context, {
|
|
560
622
|
ok: true,
|
|
@@ -566,47 +628,58 @@ export function failMergeBundle(context, input) {
|
|
|
566
628
|
stop_after_current
|
|
567
629
|
});
|
|
568
630
|
}
|
|
569
|
-
export function
|
|
631
|
+
export function prepareMergeQueueCancellation(context, input) {
|
|
632
|
+
requireMergeText(input.reason, "reason");
|
|
633
|
+
if (input.workerId !== undefined)
|
|
634
|
+
requireMergeText(input.workerId, "worker-id");
|
|
635
|
+
if (typeof input.force !== "boolean")
|
|
636
|
+
throw new AppError("validation", "force must be boolean", 2);
|
|
570
637
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
571
638
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
572
639
|
const reason = input.reason.trim();
|
|
573
|
-
|
|
574
|
-
|
|
640
|
+
const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
|
|
641
|
+
if (!job) {
|
|
642
|
+
throw new AppError("not_found", "Merge queue job is not found", 1, { protocol_id: protocol.id });
|
|
575
643
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (!
|
|
581
|
-
throw new AppError("
|
|
582
|
-
}
|
|
583
|
-
if (job.status === "cancelled") {
|
|
584
|
-
context.db.exec("COMMIT");
|
|
585
|
-
return mergeQueueResult(job, { ok: true, outcome: "cancelled", cancelled: false, reason: "already_cancelled" });
|
|
586
|
-
}
|
|
587
|
-
if (job.status === "claimed" && !input.force) {
|
|
588
|
-
if (!input.workerId) {
|
|
589
|
-
throw new AppError("merge_job_claimed", "Claimed jobs require --worker-id or --force", 1, { protocol_id: protocol.id });
|
|
590
|
-
}
|
|
591
|
-
if (job.claimed_by_session_id !== input.workerId) {
|
|
592
|
-
throw new AppError("merge_job_claimed_by_other_session", "Merge job is claimed by another worker", 1, {
|
|
593
|
-
protocol_id: protocol.id,
|
|
594
|
-
claimed_by_session_id: job.claimed_by_session_id
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
requireLaneLockOwner(context, {
|
|
598
|
-
projectRoot: protocol.project_root,
|
|
599
|
-
lane: "merge",
|
|
600
|
-
workerId: input.workerId,
|
|
601
|
-
workspacePath: input.workspacePath
|
|
602
|
-
});
|
|
644
|
+
if (job.status === "cancelled") {
|
|
645
|
+
return { protocol, job, reason };
|
|
646
|
+
}
|
|
647
|
+
if (job.status === "claimed" && !input.force) {
|
|
648
|
+
if (!input.workerId) {
|
|
649
|
+
throw new AppError("merge_job_claimed", "Claimed jobs require --worker-id or --force", 1, { protocol_id: protocol.id });
|
|
603
650
|
}
|
|
604
|
-
if (
|
|
605
|
-
throw new AppError("
|
|
651
|
+
if (job.claimed_by_session_id !== input.workerId) {
|
|
652
|
+
throw new AppError("merge_job_claimed_by_other_session", "Merge job is claimed by another worker", 1, {
|
|
606
653
|
protocol_id: protocol.id,
|
|
607
|
-
|
|
654
|
+
claimed_by_session_id: job.claimed_by_session_id
|
|
608
655
|
});
|
|
609
656
|
}
|
|
657
|
+
requireLaneLockOwner(context, {
|
|
658
|
+
projectRoot: protocol.project_root,
|
|
659
|
+
lane: "merge",
|
|
660
|
+
workerId: input.workerId,
|
|
661
|
+
workspacePath: input.workspacePath
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (["merged", "failed"].includes(job.status) && !input.force) {
|
|
665
|
+
throw new AppError("merge_job_terminal", `Merge job is ${job.status}; cancellation requires --force --reason`, 1, {
|
|
666
|
+
protocol_id: protocol.id,
|
|
667
|
+
status: job.status
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return { protocol, job, reason };
|
|
671
|
+
}
|
|
672
|
+
export function cancelMergeQueueJob(context, input) {
|
|
673
|
+
const prepared = prepareMergePublication(context, input.projectRoot, input.workerId, () => prepareMergeQueueCancellation(context, input), value => [value.protocol.id], "target", false);
|
|
674
|
+
const { protocol, job, reason } = prepared.value;
|
|
675
|
+
const now = context.now();
|
|
676
|
+
const early = context.db.writeTransaction(() => {
|
|
677
|
+
prepared.assertCurrent();
|
|
678
|
+
if (job.status === "claimed" && !input.force && !context.db.get("SELECT 1 FROM lane_locks WHERE project_id = ? AND lane = 'merge' AND status = 'active' AND worker_id = ? AND expires_at > ?", [protocol.project_id, input.workerId, context.now()]))
|
|
679
|
+
throw new AppError("lane_lock_not_active", "Prepared cancellation lost its live lane lease", 1);
|
|
680
|
+
if (job.status === "cancelled") {
|
|
681
|
+
return mergeQueueResult(job, { ok: true, outcome: "cancelled", cancelled: false, reason: "already_cancelled" });
|
|
682
|
+
}
|
|
610
683
|
context.db.run(`UPDATE merge_queue
|
|
611
684
|
SET status = 'cancelled', last_reason = ?, updated_at = ?
|
|
612
685
|
WHERE id = ?`, [reason, now, job.id]);
|
|
@@ -618,17 +691,19 @@ export function cancelMergeQueueJob(context, input) {
|
|
|
618
691
|
forced: input.force,
|
|
619
692
|
payload: { protocol_id: protocol.id, from: job.status, worker_id: input.workerId ?? null }
|
|
620
693
|
});
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
throw error;
|
|
626
|
-
}
|
|
694
|
+
return null;
|
|
695
|
+
});
|
|
696
|
+
if (early)
|
|
697
|
+
return early;
|
|
627
698
|
return mergeQueueResult(queueJobByProtocol(context, protocol.project_id, protocol.id), { ok: true, outcome: "cancelled", cancelled: true });
|
|
628
699
|
}
|
|
629
700
|
export function queueForProject(context, projectId) {
|
|
630
701
|
return context.db.all(`SELECT * FROM merge_queue WHERE project_id = ? ORDER BY created_at ASC, id ASC`, [projectId]);
|
|
631
702
|
}
|
|
703
|
+
function requireMergeText(value, parameter) {
|
|
704
|
+
if (typeof value !== "string" || !value.trim())
|
|
705
|
+
throw new AppError("validation", `--${parameter} must be nonempty text`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter });
|
|
706
|
+
}
|
|
632
707
|
function queueJobByProtocol(context, projectId, protocolId) {
|
|
633
708
|
return context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
634
709
|
}
|
|
@@ -688,7 +763,7 @@ function requireClaimedJob(context, projectId, protocolId, sessionId) {
|
|
|
688
763
|
}
|
|
689
764
|
return job;
|
|
690
765
|
}
|
|
691
|
-
function
|
|
766
|
+
function prepareClaimedProtocolTransition(context, projectId, protocolId) {
|
|
692
767
|
const protocol = requireProtocol(context, protocolId, projectId);
|
|
693
768
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
694
769
|
const flowContract = flowContractForState(state);
|
|
@@ -707,6 +782,10 @@ function transitionClaimedProtocolToIntegration(context, projectId, protocolId,
|
|
|
707
782
|
diagnostics: blockingDiagnostics
|
|
708
783
|
});
|
|
709
784
|
}
|
|
785
|
+
return { protocol, state, flowContract };
|
|
786
|
+
}
|
|
787
|
+
function transitionClaimedProtocolToIntegration(context, prepared, workerId, now) {
|
|
788
|
+
const { protocol, state, flowContract } = prepared;
|
|
710
789
|
persistProtocolState(context, protocol, {
|
|
711
790
|
...state,
|
|
712
791
|
stage: "integration",
|
|
@@ -715,10 +794,10 @@ function transitionClaimedProtocolToIntegration(context, projectId, protocolId,
|
|
|
715
794
|
updated_at: now
|
|
716
795
|
});
|
|
717
796
|
appendAudit(context, {
|
|
718
|
-
protocolId,
|
|
797
|
+
protocolId: protocol.id,
|
|
719
798
|
projectId: protocol.project_id,
|
|
720
799
|
eventType: "protocol.integration_claimed",
|
|
721
|
-
payload: { protocol_id:
|
|
800
|
+
payload: { protocol_id: protocol.id, worker_id: workerId, flow_contract_id: flowContract.id }
|
|
722
801
|
});
|
|
723
802
|
}
|
|
724
803
|
function releaseLaneLockIfOwned(context, input) {
|