opencode-swarm 7.154.0 → 7.154.2
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/dist/background/completion-observer.d.ts +10 -1
- package/dist/background/pending-delegations.d.ts +47 -0
- package/dist/background/pr-subscriptions.d.ts +7 -0
- package/dist/cli/coder-settlement-xqs49g07.js +54 -0
- package/dist/cli/{config-doctor-8p9y543t.js → config-doctor-69eebk90.js} +2 -2
- package/dist/cli/{core-1h6x6k59.js → core-dyp14hcb.js} +2 -2
- package/dist/cli/{curation-policy-9r902jw4.js → curation-policy-jm9jsxtt.js} +6 -6
- package/dist/cli/curator-drift-h57z6tm3.js +1 -1
- package/dist/cli/{curator-yj00z1qz.js → curator-he8n9z9n.js} +35 -33
- package/dist/cli/{curator-llm-factory-ypwf3a90.js → curator-llm-factory-5tq9c4v5.js} +35 -33
- package/dist/cli/{evidence-summary-service-npq04240.js → evidence-summary-service-05s5y8d6.js} +13 -13
- package/dist/cli/{gate-evidence-4b6t0ycn.js → gate-evidence-3ptr9af3.js} +7 -7
- package/dist/cli/guardrail-explain-tebk344k.js +67 -0
- package/dist/cli/{guardrail-log-eyecsekk.js → guardrail-log-x3gz3x17.js} +5 -5
- package/dist/cli/{guardrail-reset-269jdwhd.js → guardrail-reset-716pdyf5.js} +35 -33
- package/dist/cli/{hive-promoter-5936am48.js → hive-promoter-nvwpj2t9.js} +35 -33
- package/dist/cli/{index-10zvxqra.js → index-07n87ctw.js} +1 -1
- package/dist/cli/{index-yve4e8fx.js → index-14bz2t7b.js} +1 -1
- package/dist/cli/{index-nkr634n6.js → index-2w6vy9a9.js} +1 -1
- package/dist/cli/{index-6yc06dx9.js → index-399e8aze.js} +1 -1
- package/dist/cli/{index-garsang6.js → index-4xqa0sgz.js} +1 -1
- package/dist/cli/{index-tnm9bb8b.js → index-57t97rtr.js} +1 -1
- package/dist/cli/{index-s70tmqn3.js → index-6qwp46k0.js} +5 -5
- package/dist/cli/{index-68w5jxv5.js → index-6rcx6vr6.js} +4 -4
- package/dist/cli/{index-wfxg3s7n.js → index-6rns7b2m.js} +96 -23
- package/dist/cli/{index-3mhgv8d6.js → index-7tw116n6.js} +2 -2
- package/dist/cli/{index-wpstzk85.js → index-8v69hfpt.js} +4 -4
- package/dist/cli/index-9avdncr2.js +875 -0
- package/dist/cli/{index-1tdsddab.js → index-bxvkn0ye.js} +2281 -2919
- package/dist/cli/index-ddszzne4.js +42 -0
- package/dist/cli/{index-4y7g04py.js → index-dm9tc6qb.js} +7 -7
- package/dist/cli/{index-4g00jhbs.js → index-dsqat5pg.js} +3 -3
- package/dist/cli/{index-snnps1vm.js → index-f6qmar4k.js} +5 -5
- package/dist/cli/{index-y64jrbch.js → index-fv9ddfta.js} +2 -2
- package/dist/cli/{index-ttqnhxxq.js → index-h1g8w4v0.js} +2 -2
- package/dist/cli/{index-s0a4ewkb.js → index-j6q3k628.js} +2 -2
- package/dist/cli/{index-c809a69b.js → index-kemxyzc0.js} +1 -1
- package/dist/cli/{index-pcj7jk4t.js → index-mnxtkt8s.js} +4 -4
- package/dist/cli/{index-hk4wa0v1.js → index-n0gqre56.js} +6 -6
- package/dist/cli/{index-7jnh1sgb.js → index-nh4axsxc.js} +228 -2
- package/dist/cli/{index-2mbs5q26.js → index-p7ey2w90.js} +7 -7
- package/dist/cli/{index-48m4841x.js → index-pj2nycpg.js} +1 -1
- package/dist/cli/{index-3db7xza0.js → index-r0dactw2.js} +1 -1
- package/dist/cli/{index-2kzxkx50.js → index-v2ryvr8k.js} +37 -35
- package/dist/cli/{index-mddgkqg9.js → index-v9585wjc.js} +2 -2
- package/dist/cli/{index-dwh02jmv.js → index-vhhdj49x.js} +10 -10
- package/dist/cli/{index-vtdwkfbv.js → index-xagq35tf.js} +1 -1
- package/dist/cli/{index-c6dfyzdh.js → index-xdy28ngt.js} +1 -1
- package/dist/cli/{index-3hdnfmhp.js → index-xq2v52tt.js} +2 -2
- package/dist/cli/{index-qcz4xprd.js → index-xt6jx41g.js} +4 -4
- package/dist/cli/{index-p4y7jefb.js → index-ynvw5kxc.js} +2 -2
- package/dist/cli/index.js +35 -33
- package/dist/cli/{knowledge-escalator-bxpzeth0.js → knowledge-escalator-s0tye368.js} +12 -12
- package/dist/cli/{knowledge-events-jxdpg2hj.js → knowledge-events-eyy9yf6b.js} +10 -10
- package/dist/cli/{knowledge-link-nwxykjya.js → knowledge-link-rb11f06y.js} +5 -5
- package/dist/cli/{knowledge-store-336hyzs8.js → knowledge-store-zjq5mz2d.js} +6 -6
- package/dist/cli/{knowledge-validator-9wkvnsv0.js → knowledge-validator-yw5qqqpd.js} +8 -8
- package/dist/cli/{pending-delegations-0fmj1qy6.js → pending-delegations-rdzjq3nw.js} +16 -3
- package/dist/cli/{pr-subscriptions-1ndwgtbw.js → pr-subscriptions-2f5fne6z.js} +4 -4
- package/dist/cli/{runner-06dvm53x.js → runner-yaqrj388.js} +6 -6
- package/dist/cli/{scan-cursor-3xxqtxwx.js → scan-cursor-8f0s883y.js} +7 -7
- package/dist/cli/{schema-jdx83m4m.js → schema-2xw94dh3.js} +1 -1
- package/dist/cli/{scope-persistence-wgxn2kba.js → scope-persistence-tap2q94x.js} +9 -9
- package/dist/cli/{skill-generator-vmxsc2fm.js → skill-generator-2xbfryqb.js} +14 -14
- package/dist/cli/{telemetry-q2km2zrz.js → telemetry-r8e8zey7.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-gsay5210.js → worktree-collision-ownership-5gwey8ms.js} +10 -9
- package/dist/cli/{worktree-isolation-peb4bkg3.js → worktree-isolation-xsy14t5a.js} +35 -33
- package/dist/evaluation/contracts.d.ts +4 -4
- package/dist/evaluation/gate-ground-truth.d.ts +1 -1
- package/dist/index.js +523 -523
- package/dist/telemetry.d.ts +1 -0
- package/dist/utils/canonical-root.d.ts +2 -0
- package/dist/workflow/coder-settlement.d.ts +14 -0
- package/package.json +1 -1
- package/dist/cli/guardrail-explain-dyxbhrdj.js +0 -65
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
removeWorktreeProvisioningOwner
|
|
4
|
+
} from "./index-4xqa0sgz.js";
|
|
5
|
+
import {
|
|
6
|
+
SOURCE_WORKTREE_GONE_STAGE,
|
|
7
|
+
getTaskWorkflowSnapshot,
|
|
8
|
+
readWorkflowWalFile,
|
|
9
|
+
reconcileLandedMerge,
|
|
10
|
+
withTaskEvidenceTransaction,
|
|
11
|
+
writeWorkflowWalFile
|
|
12
|
+
} from "./index-8v69hfpt.js";
|
|
13
|
+
import {
|
|
14
|
+
canonicalRootKeyFresh
|
|
15
|
+
} from "./index-ddszzne4.js";
|
|
16
|
+
import {
|
|
17
|
+
captureWorkspaceSnapshot,
|
|
18
|
+
changedFilesSinceSnapshot
|
|
19
|
+
} from "./index-vnn7qq8c.js";
|
|
20
|
+
import {
|
|
21
|
+
appendCoreEventSync
|
|
22
|
+
} from "./index-xdy28ngt.js";
|
|
23
|
+
import {
|
|
24
|
+
isPathWithinDeclaredScope
|
|
25
|
+
} from "./index-556ja8an.js";
|
|
26
|
+
import {
|
|
27
|
+
validateSwarmPath
|
|
28
|
+
} from "./index-4cgee5vh.js";
|
|
29
|
+
import {
|
|
30
|
+
init_git_executable,
|
|
31
|
+
resolveGitExecutable
|
|
32
|
+
} from "./index-9e6fdz2h.js";
|
|
33
|
+
import {
|
|
34
|
+
advisoryWarn,
|
|
35
|
+
init_warning_buffer
|
|
36
|
+
} from "./index-4rhhvd1a.js";
|
|
37
|
+
import {
|
|
38
|
+
tryAcquireLock
|
|
39
|
+
} from "./index-xagq35tf.js";
|
|
40
|
+
import {
|
|
41
|
+
criticalWarn,
|
|
42
|
+
init_logger
|
|
43
|
+
} from "./index-zgwm4ryv.js";
|
|
44
|
+
import {
|
|
45
|
+
__require
|
|
46
|
+
} from "./index-a76rekgs.js";
|
|
47
|
+
|
|
48
|
+
// src/workflow/coder-settlement.ts
|
|
49
|
+
import { spawnSync } from "child_process";
|
|
50
|
+
import { randomUUID } from "crypto";
|
|
51
|
+
import { existsSync } from "fs";
|
|
52
|
+
import { readdir } from "fs/promises";
|
|
53
|
+
|
|
54
|
+
// src/gate-evidence-classification.ts
|
|
55
|
+
import * as path from "path";
|
|
56
|
+
function normalizeCandidate(value) {
|
|
57
|
+
if (typeof value !== "string" || value.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
if ([...value].some((character) => {
|
|
60
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
61
|
+
return codePoint <= 31 || codePoint === 127;
|
|
62
|
+
}))
|
|
63
|
+
return null;
|
|
64
|
+
if (path.win32.isAbsolute(value) || path.posix.isAbsolute(value))
|
|
65
|
+
return null;
|
|
66
|
+
const normalized = value.replace(/\\/g, "/");
|
|
67
|
+
const segments = normalized.split("/");
|
|
68
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
function isExactMarkdownPath(value) {
|
|
74
|
+
const normalized = normalizeCandidate(value);
|
|
75
|
+
return normalized !== null && path.posix.extname(normalized) === ".md";
|
|
76
|
+
}
|
|
77
|
+
function isMarkdownOnlyDeclaredScope(value) {
|
|
78
|
+
return Array.isArray(value) && value.length > 0 && value.every(isExactMarkdownPath);
|
|
79
|
+
}
|
|
80
|
+
function isMarkdownOnlyTaskChange(declaredFiles, observedFiles) {
|
|
81
|
+
if (!Array.isArray(declaredFiles) || !Array.isArray(observedFiles))
|
|
82
|
+
return false;
|
|
83
|
+
if (!isMarkdownOnlyDeclaredScope(declaredFiles) || observedFiles.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
if (!observedFiles.every(isExactMarkdownPath))
|
|
86
|
+
return false;
|
|
87
|
+
const declared = new Set(declaredFiles.map(normalizeCandidate).filter((value) => value !== null));
|
|
88
|
+
return observedFiles.every((value) => {
|
|
89
|
+
const normalized = normalizeCandidate(value);
|
|
90
|
+
return normalized !== null && declared.has(normalized);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/workflow/coder-settlement.ts
|
|
95
|
+
init_warning_buffer();
|
|
96
|
+
init_git_executable();
|
|
97
|
+
init_logger();
|
|
98
|
+
var LIVE_DISPATCHES_SYMBOL = Symbol.for("opencode-swarm.workflow.coder-settlement.live-dispatches");
|
|
99
|
+
var globalRegistry = globalThis;
|
|
100
|
+
if (!globalRegistry[LIVE_DISPATCHES_SYMBOL]) {
|
|
101
|
+
globalRegistry[LIVE_DISPATCHES_SYMBOL] = new Set;
|
|
102
|
+
}
|
|
103
|
+
var liveDispatches = globalRegistry[LIVE_DISPATCHES_SYMBOL];
|
|
104
|
+
var runtimeId = randomUUID();
|
|
105
|
+
var MAX_LIVE_DISPATCHES = 512;
|
|
106
|
+
function dispatchKey(directory, taskId, transitionId) {
|
|
107
|
+
return `${canonicalRootKeyFresh(directory)}\x00${taskId}\x00${transitionId}`;
|
|
108
|
+
}
|
|
109
|
+
function walPath(directory, taskId) {
|
|
110
|
+
return validateSwarmPath(directory, `coder-settlements/${taskId}.json`);
|
|
111
|
+
}
|
|
112
|
+
async function readWal(filePath, taskId) {
|
|
113
|
+
return readWorkflowWalFile("coder-settlement", filePath, taskId);
|
|
114
|
+
}
|
|
115
|
+
async function withSettlementLock(directory, taskId, actor, operation) {
|
|
116
|
+
const target = `coder-settlements/${taskId}.json`;
|
|
117
|
+
const lock = await tryAcquireLock(directory, target, actor, `coder-settlement-${taskId}-${Date.now()}`);
|
|
118
|
+
if (!lock.acquired) {
|
|
119
|
+
throw new Error(`CODER_SETTLEMENT_LOCKED: task ${taskId}`);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return await operation();
|
|
123
|
+
} finally {
|
|
124
|
+
await lock.lock._release?.().catch(() => {
|
|
125
|
+
return;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function writeWal(filePath, wal) {
|
|
130
|
+
await writeWorkflowWalFile("coder-settlement", filePath, wal);
|
|
131
|
+
}
|
|
132
|
+
async function appendSettlementEvent(directory, action, wal, extra) {
|
|
133
|
+
try {
|
|
134
|
+
appendCoreEventSync(directory, {
|
|
135
|
+
type: "coder_settlement",
|
|
136
|
+
action,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
taskId: wal.taskId,
|
|
139
|
+
transitionId: wal.transitionId,
|
|
140
|
+
actor: wal.actor,
|
|
141
|
+
...extra ?? {}
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
criticalWarn(`[coder-settlement] lifecycle event write failed (${action} ${wal.taskId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function baselineAttributionDoomed(baseline) {
|
|
148
|
+
if (baseline.gitHead === null)
|
|
149
|
+
return true;
|
|
150
|
+
return Array.isArray(baseline.changedFiles) && baseline.changedFiles.length > 0;
|
|
151
|
+
}
|
|
152
|
+
function doomedReason(baseline) {
|
|
153
|
+
if (baseline.gitHead === null)
|
|
154
|
+
return "launch workspace has no git baseline (not a git repository, or git unavailable at dispatch)";
|
|
155
|
+
return `launch baseline was dirty with ${String(baseline.changedFiles?.length ?? 0)} pre-existing uncommitted/untracked change(s)`;
|
|
156
|
+
}
|
|
157
|
+
function isProcessAlive(processId) {
|
|
158
|
+
if (processId === process.pid)
|
|
159
|
+
return true;
|
|
160
|
+
try {
|
|
161
|
+
process.kill(processId, 0);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return error.code === "EPERM";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function scopedObservedFiles(directory, context) {
|
|
168
|
+
const baseline = { ...context.baseline, directory };
|
|
169
|
+
const observed = changedFilesSinceSnapshot(directory, baseline);
|
|
170
|
+
if (!observed || !context.declaredFiles)
|
|
171
|
+
return null;
|
|
172
|
+
return observed.filter((filePath) => isPathWithinDeclaredScope(filePath, context.declaredFiles ?? [], directory));
|
|
173
|
+
}
|
|
174
|
+
async function commitPrepared(directory, wal) {
|
|
175
|
+
return withTaskEvidenceTransaction(directory, wal.taskId, wal.actor, async (transaction) => {
|
|
176
|
+
let lockedWal = await readWal(walPath(directory, wal.taskId), wal.taskId);
|
|
177
|
+
if (lockedWal === null)
|
|
178
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
179
|
+
if (lockedWal.transitionId !== wal.transitionId) {
|
|
180
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
181
|
+
}
|
|
182
|
+
const snapshot = getTaskWorkflowSnapshot(transaction.read());
|
|
183
|
+
const evidenceAlreadySettled = lockedWal.accepted === true ? snapshot.authoritative && snapshot.state === (lockedWal.settlementFailed === true ? "rework_required" : "coder_delegated") && snapshot.generation === lockedWal.expectedGeneration + 1 && snapshot.lastOutcome === (lockedWal.settlementFailed === true ? "accepted_mutation_failed" : "accepted_mutation") && snapshot.lastTransitionId === lockedWal.transitionId : snapshot.generation === lockedWal.expectedGeneration && snapshot.lastOutcome === "dispatch_no_mutation" && snapshot.lastTransitionId === lockedWal.transitionId;
|
|
184
|
+
if (evidenceAlreadySettled) {
|
|
185
|
+
if (lockedWal.state !== "COMMITTED") {
|
|
186
|
+
lockedWal = {
|
|
187
|
+
...lockedWal,
|
|
188
|
+
state: "COMMITTED",
|
|
189
|
+
cleanupComplete: lockedWal.worktree === undefined
|
|
190
|
+
};
|
|
191
|
+
await writeWal(walPath(directory, wal.taskId), lockedWal);
|
|
192
|
+
await appendSettlementEvent(directory, "settled", lockedWal, {
|
|
193
|
+
resumed: true
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
evidence: transaction.read(),
|
|
198
|
+
accepted: lockedWal.accepted === true,
|
|
199
|
+
alreadyApplied: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (lockedWal.state !== "PREPARED") {
|
|
203
|
+
throw new Error("CODER_SETTLEMENT_NOT_PREPARED");
|
|
204
|
+
}
|
|
205
|
+
const evidence = await transaction.transition(lockedWal.accepted ? {
|
|
206
|
+
type: "accepted_mutation",
|
|
207
|
+
agentType: "coder",
|
|
208
|
+
context: {
|
|
209
|
+
testEngineerExempt: lockedWal.testEngineerExempt === true,
|
|
210
|
+
settlementFailed: lockedWal.settlementFailed === true
|
|
211
|
+
},
|
|
212
|
+
expectedGeneration: lockedWal.expectedGeneration,
|
|
213
|
+
transitionId: lockedWal.transitionId
|
|
214
|
+
} : {
|
|
215
|
+
type: "dispatch_no_mutation",
|
|
216
|
+
agentType: "coder",
|
|
217
|
+
expectedGeneration: lockedWal.expectedGeneration,
|
|
218
|
+
transitionId: lockedWal.transitionId
|
|
219
|
+
});
|
|
220
|
+
lockedWal = {
|
|
221
|
+
...lockedWal,
|
|
222
|
+
state: "COMMITTED",
|
|
223
|
+
cleanupComplete: lockedWal.worktree === undefined
|
|
224
|
+
};
|
|
225
|
+
await writeWal(walPath(directory, wal.taskId), lockedWal);
|
|
226
|
+
await appendSettlementEvent(directory, "settled", lockedWal);
|
|
227
|
+
return {
|
|
228
|
+
evidence,
|
|
229
|
+
accepted: lockedWal.accepted === true,
|
|
230
|
+
alreadyApplied: false
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function beginCoderSettlement(options) {
|
|
235
|
+
if (options.context.baseline.gitHead === null) {
|
|
236
|
+
const rootBaseline = captureWorkspaceSnapshot(options.directory);
|
|
237
|
+
if (rootBaseline.gitHead !== null) {
|
|
238
|
+
throw new Error(`CODER_SETTLEMENT_BASELINE_UNAVAILABLE: the launch baseline for task ${options.taskId} has no git HEAD \u2014 the observation directory (${options.context.baseline.directory}) is not a git repository or git is unavailable there. ` + "An unregistered worktree lane (a lane directory that git worktree list does not know) produces exactly this. " + 'The task was not dispatched and no settlement state was created; retry the dispatch, or set worktree.policy "disabled" to run coders in the primary tree.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const filePath = walPath(options.directory, options.taskId);
|
|
242
|
+
let ownsActiveDispatch = false;
|
|
243
|
+
await withSettlementLock(options.directory, options.taskId, options.actor, () => withTaskEvidenceTransaction(options.directory, options.taskId, options.actor, async (transaction) => {
|
|
244
|
+
const snapshot = getTaskWorkflowSnapshot(transaction.read());
|
|
245
|
+
if (snapshot.generation !== options.expectedGeneration) {
|
|
246
|
+
throw new Error(`TASK_WORKFLOW_GENERATION_MISMATCH: expected ${options.expectedGeneration}, found ${snapshot.generation}`);
|
|
247
|
+
}
|
|
248
|
+
const existing = await readWal(filePath, options.taskId);
|
|
249
|
+
if (existing !== null) {
|
|
250
|
+
if ((existing.state === "DISPATCHED" || existing.state === "PREPARED") && existing.transitionId !== options.transitionId) {
|
|
251
|
+
throw new Error(`CODER_SETTLEMENT_IN_PROGRESS: transition ${existing.transitionId} owns task ${options.taskId}, so transition ${options.transitionId} cannot dispatch it (${filePath}, state ${existing.state}). Wait for the owning transition to settle or run /swarm recover ${options.taskId} (or /swarm reset-session), then retry; do not remove the WAL by hand.`);
|
|
252
|
+
}
|
|
253
|
+
if (existing.transitionId === options.transitionId) {
|
|
254
|
+
const immutableMatches = existing.actor === options.actor && existing.expectedGeneration === options.expectedGeneration && JSON.stringify(existing.context) === JSON.stringify(options.context) && JSON.stringify(existing.worktree) === JSON.stringify(options.worktree);
|
|
255
|
+
if (!immutableMatches) {
|
|
256
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
257
|
+
}
|
|
258
|
+
if (existing.state === "ABORTED") {
|
|
259
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
260
|
+
}
|
|
261
|
+
ownsActiveDispatch = existing.state === "DISPATCHED" && existing.runtimeId === runtimeId;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await transaction.transition({
|
|
266
|
+
type: "dispatch_attempted",
|
|
267
|
+
agentType: "coder",
|
|
268
|
+
expectedGeneration: options.expectedGeneration,
|
|
269
|
+
transitionId: options.transitionId
|
|
270
|
+
});
|
|
271
|
+
await writeWal(filePath, {
|
|
272
|
+
version: 1,
|
|
273
|
+
state: "DISPATCHED",
|
|
274
|
+
taskId: options.taskId,
|
|
275
|
+
transitionId: options.transitionId,
|
|
276
|
+
actor: options.actor,
|
|
277
|
+
processId: process.pid,
|
|
278
|
+
runtimeId,
|
|
279
|
+
expectedGeneration: options.expectedGeneration,
|
|
280
|
+
context: options.context,
|
|
281
|
+
...options.worktree ? { worktree: options.worktree } : {},
|
|
282
|
+
recordedAt: new Date().toISOString()
|
|
283
|
+
});
|
|
284
|
+
ownsActiveDispatch = true;
|
|
285
|
+
await appendSettlementEvent(options.directory, "dispatched", {
|
|
286
|
+
taskId: options.taskId,
|
|
287
|
+
transitionId: options.transitionId,
|
|
288
|
+
actor: options.actor
|
|
289
|
+
});
|
|
290
|
+
}));
|
|
291
|
+
if (ownsActiveDispatch) {
|
|
292
|
+
const key = dispatchKey(options.directory, options.taskId, options.transitionId);
|
|
293
|
+
if (!liveDispatches.has(key) && liveDispatches.size >= MAX_LIVE_DISPATCHES) {
|
|
294
|
+
throw new Error("CODER_SETTLEMENT_CAPACITY_EXCEEDED");
|
|
295
|
+
}
|
|
296
|
+
liveDispatches.add(key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function settleCoderDispatch(options) {
|
|
300
|
+
return withSettlementLock(options.directory, options.taskId, "coder-settlement", async () => {
|
|
301
|
+
const filePath = walPath(options.directory, options.taskId);
|
|
302
|
+
const wal = await readWal(filePath, options.taskId);
|
|
303
|
+
if (wal === null)
|
|
304
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
305
|
+
if (wal.transitionId !== options.transitionId) {
|
|
306
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
307
|
+
}
|
|
308
|
+
if (wal.state === "COMMITTED") {
|
|
309
|
+
if (wal.accepted !== options.accepted || wal.testEngineerExempt !== options.testEngineerExempt || wal.settlementFailed !== (options.settlementFailed === true)) {
|
|
310
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
311
|
+
}
|
|
312
|
+
return commitPrepared(options.directory, wal);
|
|
313
|
+
}
|
|
314
|
+
if (wal.state === "ABORTED") {
|
|
315
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
316
|
+
}
|
|
317
|
+
if (wal.state === "PREPARED" && (wal.accepted !== options.accepted || wal.testEngineerExempt !== options.testEngineerExempt || wal.settlementFailed !== (options.settlementFailed === true))) {
|
|
318
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
319
|
+
}
|
|
320
|
+
const prepared = {
|
|
321
|
+
...wal,
|
|
322
|
+
state: "PREPARED",
|
|
323
|
+
accepted: options.accepted,
|
|
324
|
+
testEngineerExempt: options.testEngineerExempt,
|
|
325
|
+
settlementFailed: options.settlementFailed === true
|
|
326
|
+
};
|
|
327
|
+
if (wal.state !== "PREPARED")
|
|
328
|
+
await writeWal(filePath, prepared);
|
|
329
|
+
liveDispatches.delete(dispatchKey(options.directory, options.taskId, options.transitionId));
|
|
330
|
+
try {
|
|
331
|
+
return await commitPrepared(options.directory, prepared);
|
|
332
|
+
} finally {
|
|
333
|
+
const finalWal = await readWal(filePath, options.taskId);
|
|
334
|
+
if (finalWal !== null && finalWal.state === "COMMITTED") {
|
|
335
|
+
liveDispatches.delete(dispatchKey(options.directory, options.taskId, options.transitionId));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function abortCoderSettlement(options) {
|
|
341
|
+
const { outcome, worktree } = await withSettlementLock(options.directory, options.taskId, "coder-abort", async () => abortDispatchedWalUnderLock(options.directory, options.taskId, options.transitionId, options.reason));
|
|
342
|
+
if (worktree && outcome !== "not-dispatched") {
|
|
343
|
+
try {
|
|
344
|
+
await completeCoderSettlementCleanup(options.directory, options.taskId, options.transitionId);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
criticalWarn(`[coder-settlement] aborted settlement worktree cleanup failed for task ${options.taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return outcome;
|
|
350
|
+
}
|
|
351
|
+
async function transferCoderSettlementToBackground(options) {
|
|
352
|
+
return withSettlementLock(options.directory, options.taskId, "coder-background-transfer", async () => {
|
|
353
|
+
const filePath = walPath(options.directory, options.taskId);
|
|
354
|
+
const wal = await readWal(filePath, options.taskId);
|
|
355
|
+
if (wal === null)
|
|
356
|
+
return "missing";
|
|
357
|
+
if (wal.taskId !== options.taskId || wal.transitionId !== options.transitionId) {
|
|
358
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
359
|
+
}
|
|
360
|
+
if (wal.state === "PREPARED") {
|
|
361
|
+
throw new Error("CODER_SETTLEMENT_IN_PROGRESS");
|
|
362
|
+
}
|
|
363
|
+
if (wal.state !== "DISPATCHED") {
|
|
364
|
+
liveDispatches.delete(dispatchKey(options.directory, options.taskId, options.transitionId));
|
|
365
|
+
return "already-terminal";
|
|
366
|
+
}
|
|
367
|
+
const reason = "ownership transferred to durable background completion";
|
|
368
|
+
await writeWal(filePath, {
|
|
369
|
+
...wal,
|
|
370
|
+
state: "ABORTED",
|
|
371
|
+
abortReason: reason,
|
|
372
|
+
cleanupComplete: true
|
|
373
|
+
});
|
|
374
|
+
liveDispatches.delete(dispatchKey(options.directory, options.taskId, options.transitionId));
|
|
375
|
+
await appendSettlementEvent(options.directory, "aborted", wal, {
|
|
376
|
+
reason,
|
|
377
|
+
ownership: "background-delegation"
|
|
378
|
+
});
|
|
379
|
+
return "transferred";
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function abortDispatchedWalUnderLock(directory, taskId, transitionId, reason) {
|
|
383
|
+
const filePath = walPath(directory, taskId);
|
|
384
|
+
const wal = await readWal(filePath, taskId);
|
|
385
|
+
if (wal === null)
|
|
386
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
387
|
+
if (wal.transitionId !== transitionId) {
|
|
388
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
389
|
+
}
|
|
390
|
+
if (wal.state === "ABORTED") {
|
|
391
|
+
return { outcome: "already-aborted", worktree: wal.worktree !== undefined };
|
|
392
|
+
}
|
|
393
|
+
if (wal.state !== "DISPATCHED") {
|
|
394
|
+
return { outcome: "not-dispatched", worktree: false };
|
|
395
|
+
}
|
|
396
|
+
await writeWal(filePath, {
|
|
397
|
+
...wal,
|
|
398
|
+
state: "ABORTED",
|
|
399
|
+
abortReason: reason
|
|
400
|
+
});
|
|
401
|
+
liveDispatches.delete(dispatchKey(directory, taskId, transitionId));
|
|
402
|
+
await appendSettlementEvent(directory, "aborted", wal, { reason });
|
|
403
|
+
return { outcome: "aborted", worktree: wal.worktree !== undefined };
|
|
404
|
+
}
|
|
405
|
+
async function abortCoderSettlementIfDoomed(options) {
|
|
406
|
+
const filePath = walPath(options.directory, options.taskId);
|
|
407
|
+
let wal;
|
|
408
|
+
try {
|
|
409
|
+
wal = await readWal(filePath, options.taskId);
|
|
410
|
+
} catch {
|
|
411
|
+
return "not-doomed";
|
|
412
|
+
}
|
|
413
|
+
if (wal === null)
|
|
414
|
+
return "not-doomed";
|
|
415
|
+
if (wal.state === "ABORTED")
|
|
416
|
+
return "already-aborted";
|
|
417
|
+
if (wal.transitionId !== options.transitionId || wal.state !== "DISPATCHED") {
|
|
418
|
+
return "not-doomed";
|
|
419
|
+
}
|
|
420
|
+
if (!baselineAttributionDoomed(wal.context.baseline))
|
|
421
|
+
return "not-doomed";
|
|
422
|
+
const outcome = await abortCoderSettlement({
|
|
423
|
+
directory: options.directory,
|
|
424
|
+
taskId: options.taskId,
|
|
425
|
+
transitionId: options.transitionId,
|
|
426
|
+
reason: doomedReason(wal.context.baseline)
|
|
427
|
+
});
|
|
428
|
+
return outcome === "aborted" || outcome === "already-aborted" ? outcome : "not-doomed";
|
|
429
|
+
}
|
|
430
|
+
async function recordCoderMergeProvenance(options) {
|
|
431
|
+
await withSettlementLock(options.directory, options.taskId, "coder-merge-provenance", async () => {
|
|
432
|
+
const filePath = walPath(options.directory, options.taskId);
|
|
433
|
+
const wal = await readWal(filePath, options.taskId);
|
|
434
|
+
if (wal === null)
|
|
435
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
436
|
+
if (wal.taskId !== options.taskId || wal.transitionId !== options.transitionId) {
|
|
437
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
438
|
+
}
|
|
439
|
+
if (wal.state !== "DISPATCHED") {
|
|
440
|
+
const sameProvenance = JSON.stringify(wal.mergeProvenance) === JSON.stringify(options.provenance) && JSON.stringify(wal.observedFiles) === JSON.stringify(options.observedFiles);
|
|
441
|
+
if (!sameProvenance)
|
|
442
|
+
throw new Error("CODER_SETTLEMENT_IDEMPOTENCY_CONFLICT");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await writeWal(filePath, {
|
|
446
|
+
...wal,
|
|
447
|
+
mergeProvenance: options.provenance,
|
|
448
|
+
observedFiles: [...options.observedFiles]
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function probeBranchExists(directory, branchName) {
|
|
453
|
+
const result = spawnSync(resolveGitExecutable(), [
|
|
454
|
+
"-C",
|
|
455
|
+
directory,
|
|
456
|
+
"show-ref",
|
|
457
|
+
"--verify",
|
|
458
|
+
"--quiet",
|
|
459
|
+
`refs/heads/${branchName}`
|
|
460
|
+
], {
|
|
461
|
+
cwd: directory,
|
|
462
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
463
|
+
timeout: 5000,
|
|
464
|
+
windowsHide: true
|
|
465
|
+
});
|
|
466
|
+
if (result.error)
|
|
467
|
+
throw result.error;
|
|
468
|
+
if (result.status === 0)
|
|
469
|
+
return true;
|
|
470
|
+
if (result.status === 1)
|
|
471
|
+
return false;
|
|
472
|
+
throw new Error(`CODER_SETTLEMENT_CLEANUP_UNCERTAIN: git show-ref exited with status ${String(result.status)}`);
|
|
473
|
+
}
|
|
474
|
+
async function cleanupRecoveredWorktree(directory, descriptor) {
|
|
475
|
+
const branchExists = () => _internals.branchExists(directory, descriptor.branchName);
|
|
476
|
+
const hasResidue = existsSync(descriptor.worktreePath) || branchExists();
|
|
477
|
+
const {
|
|
478
|
+
awaitingMergeByCallID,
|
|
479
|
+
cleanupStandardWorktreeForCallId,
|
|
480
|
+
standardWorktreeByCallID
|
|
481
|
+
} = await import("./worktree-isolation-xsy14t5a.js");
|
|
482
|
+
standardWorktreeByCallID.delete(descriptor.callID);
|
|
483
|
+
if (hasResidue)
|
|
484
|
+
awaitingMergeByCallID.set(descriptor.callID, {
|
|
485
|
+
callID: descriptor.callID,
|
|
486
|
+
parentSessionID: descriptor.parentSessionId,
|
|
487
|
+
taskId: descriptor.taskId,
|
|
488
|
+
...descriptor.planTaskId ? { planTaskId: descriptor.planTaskId } : {},
|
|
489
|
+
branch: descriptor.branchName,
|
|
490
|
+
worktreePath: descriptor.worktreePath,
|
|
491
|
+
mergeStrategy: descriptor.mergeStrategy,
|
|
492
|
+
queuedAt: Date.now()
|
|
493
|
+
});
|
|
494
|
+
if (hasResidue) {
|
|
495
|
+
await cleanupStandardWorktreeForCallId(descriptor.callID, "success", directory, descriptor.worktreeDir ?? undefined);
|
|
496
|
+
}
|
|
497
|
+
if (existsSync(descriptor.worktreePath) || branchExists()) {
|
|
498
|
+
throw new Error("CODER_SETTLEMENT_WORKTREE_CLEANUP_UNVERIFIED");
|
|
499
|
+
}
|
|
500
|
+
const provisioningOwner = descriptor.provisioningOwner ?? (descriptor.reservationId !== undefined && descriptor.generation !== undefined ? {
|
|
501
|
+
reservationId: descriptor.reservationId,
|
|
502
|
+
generation: descriptor.generation,
|
|
503
|
+
branchName: descriptor.branchName
|
|
504
|
+
} : undefined);
|
|
505
|
+
if (!_internals.removeWorktreeProvisioningOwner(directory, descriptor.callID, provisioningOwner)) {
|
|
506
|
+
throw new Error("CODER_SETTLEMENT_OWNER_CLEANUP_FAILED");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function completeCoderSettlementCleanup(directory, taskId, transitionId) {
|
|
510
|
+
await withSettlementLock(directory, taskId, "coder-cleanup", async () => {
|
|
511
|
+
const filePath = walPath(directory, taskId);
|
|
512
|
+
const wal = await readWal(filePath, taskId);
|
|
513
|
+
if (wal === null)
|
|
514
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
515
|
+
if (wal.taskId !== taskId || wal.transitionId !== transitionId) {
|
|
516
|
+
throw new Error("CODER_SETTLEMENT_WAL_REPLACED");
|
|
517
|
+
}
|
|
518
|
+
if (wal.state !== "COMMITTED" && wal.state !== "ABORTED") {
|
|
519
|
+
throw new Error("CODER_SETTLEMENT_NOT_COMMITTED");
|
|
520
|
+
}
|
|
521
|
+
if (!wal.worktree || wal.cleanupComplete === true)
|
|
522
|
+
return;
|
|
523
|
+
await cleanupRecoveredWorktree(directory, wal.worktree);
|
|
524
|
+
await writeWal(filePath, { ...wal, cleanupComplete: true });
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function selfHealMissingLaneWorktree(directory, filePath, taskId, transitionId, descriptor, detail) {
|
|
528
|
+
if (existsSync(descriptor.worktreePath))
|
|
529
|
+
return false;
|
|
530
|
+
if (_internals.branchExists(directory, descriptor.branchName))
|
|
531
|
+
return false;
|
|
532
|
+
const reason = `CODER_SETTLEMENT_SELF_HEALED_MISSING_WORKTREE: ${detail}`;
|
|
533
|
+
const aborted = await abortDispatchedWalUnderLock(directory, taskId, transitionId, reason);
|
|
534
|
+
if (aborted.outcome === "not-dispatched")
|
|
535
|
+
return false;
|
|
536
|
+
try {
|
|
537
|
+
await cleanupRecoveredWorktree(directory, descriptor);
|
|
538
|
+
const healedWal = await readWal(filePath, taskId);
|
|
539
|
+
if (healedWal !== null) {
|
|
540
|
+
await writeWal(filePath, { ...healedWal, cleanupComplete: true });
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
criticalWarn(`[coder-settlement] self-healed task ${taskId} but residual cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
544
|
+
}
|
|
545
|
+
const message = `STALE_CODER_SETTLEMENT_SELF_HEALED: task ${taskId} was blocked by a coder settlement whose lane worktree "${descriptor.worktreePath}" and lane branch "${descriptor.branchName}" no longer exist. Nothing was recoverable, so the write-ahead log at "${filePath}" was marked terminal (ABORTED) and the task was unblocked. Re-dispatch the task if its work still needs doing.`;
|
|
546
|
+
criticalWarn(message);
|
|
547
|
+
advisoryWarn(message);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
async function recoverCoderSettlement(directory, taskId) {
|
|
551
|
+
return withSettlementLock(directory, taskId, "coder-recovery", async () => {
|
|
552
|
+
const filePath = walPath(directory, taskId);
|
|
553
|
+
const existingWal = await readWal(filePath, taskId);
|
|
554
|
+
if (existingWal === null)
|
|
555
|
+
return null;
|
|
556
|
+
let wal = existingWal;
|
|
557
|
+
if (wal.state === "ABORTED")
|
|
558
|
+
return null;
|
|
559
|
+
if (wal.state === "COMMITTED") {
|
|
560
|
+
if (wal.worktree && wal.cleanupComplete !== true) {
|
|
561
|
+
await cleanupRecoveredWorktree(directory, wal.worktree);
|
|
562
|
+
await writeWal(filePath, { ...wal, cleanupComplete: true });
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const key = dispatchKey(directory, taskId, wal.transitionId);
|
|
567
|
+
if (liveDispatches.has(key) || wal.processId !== process.pid && isProcessAlive(wal.processId)) {
|
|
568
|
+
throw new Error(`CODER_DISPATCH_IN_PROGRESS: transition ${wal.transitionId} still owns task ${taskId} (${filePath}, state ${wal.state}, pid ${wal.processId}). Wait for that dispatch to settle or run /swarm recover ${taskId} (or /swarm reset-session) to unwedge it; do not remove the WAL by hand.`);
|
|
569
|
+
}
|
|
570
|
+
if (wal.state === "DISPATCHED") {
|
|
571
|
+
if (wal.worktree) {
|
|
572
|
+
const worktree = wal.worktree;
|
|
573
|
+
const observed2 = wal.observedFiles ?? scopedObservedFiles(worktree.worktreePath, wal.context);
|
|
574
|
+
if (observed2 === null) {
|
|
575
|
+
throw new Error(`CODER_SETTLEMENT_RECOVERY_UNCERTAIN: isolated task ${taskId} changes could not be attributed safely`);
|
|
576
|
+
}
|
|
577
|
+
if (wal.mergeProvenance) {
|
|
578
|
+
const landed = await reconcileLandedMerge(directory, wal.mergeProvenance);
|
|
579
|
+
if (landed.landed) {
|
|
580
|
+
wal = {
|
|
581
|
+
...wal,
|
|
582
|
+
state: "PREPARED",
|
|
583
|
+
observedFiles: observed2,
|
|
584
|
+
accepted: observed2.length > 0,
|
|
585
|
+
testEngineerExempt: isMarkdownOnlyTaskChange(wal.context.declaredFiles, observed2)
|
|
586
|
+
};
|
|
587
|
+
await writeWal(filePath, wal);
|
|
588
|
+
const committed2 = await commitPrepared(directory, wal);
|
|
589
|
+
await cleanupRecoveredWorktree(directory, worktree);
|
|
590
|
+
await writeWal(filePath, {
|
|
591
|
+
...wal,
|
|
592
|
+
state: "COMMITTED",
|
|
593
|
+
cleanupComplete: true
|
|
594
|
+
});
|
|
595
|
+
return committed2;
|
|
596
|
+
}
|
|
597
|
+
if (landed.error) {
|
|
598
|
+
throw new Error(`CODER_SETTLEMENT_RECOVERY_UNCERTAIN: ${landed.error}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const {
|
|
602
|
+
awaitingMergeByCallID,
|
|
603
|
+
finishStandardWorktreeDispatch,
|
|
604
|
+
standardWorktreeByCallID
|
|
605
|
+
} = await import("./worktree-isolation-xsy14t5a.js");
|
|
606
|
+
const descriptor = wal.worktree;
|
|
607
|
+
const reconstructedDispatch = {
|
|
608
|
+
callID: descriptor.callID,
|
|
609
|
+
parentSessionID: descriptor.parentSessionId,
|
|
610
|
+
taskId: descriptor.taskId,
|
|
611
|
+
...descriptor.planTaskId ? { planTaskId: descriptor.planTaskId } : {},
|
|
612
|
+
handle: {
|
|
613
|
+
worktreePath: descriptor.worktreePath,
|
|
614
|
+
branchName: descriptor.branchName,
|
|
615
|
+
purpose: "lane",
|
|
616
|
+
id: descriptor.worktreeId,
|
|
617
|
+
sessionId: descriptor.worktreeSessionId
|
|
618
|
+
},
|
|
619
|
+
mergeStrategy: descriptor.mergeStrategy,
|
|
620
|
+
laneIndex: descriptor.laneIndex,
|
|
621
|
+
...descriptor.worktreeDir ? { worktree_dir: descriptor.worktreeDir } : {}
|
|
622
|
+
};
|
|
623
|
+
standardWorktreeByCallID.delete(descriptor.callID);
|
|
624
|
+
awaitingMergeByCallID.set(descriptor.callID, {
|
|
625
|
+
callID: descriptor.callID,
|
|
626
|
+
parentSessionID: descriptor.parentSessionId,
|
|
627
|
+
taskId: descriptor.taskId,
|
|
628
|
+
...descriptor.planTaskId ? { planTaskId: descriptor.planTaskId } : {},
|
|
629
|
+
branch: descriptor.branchName,
|
|
630
|
+
worktreePath: descriptor.worktreePath,
|
|
631
|
+
mergeStrategy: descriptor.mergeStrategy,
|
|
632
|
+
queuedAt: Date.now()
|
|
633
|
+
});
|
|
634
|
+
let recovered = null;
|
|
635
|
+
const mergeResult = await finishStandardWorktreeDispatch(directory, reconstructedDispatch, undefined, descriptor.callID, {
|
|
636
|
+
operationId: wal.transitionId,
|
|
637
|
+
...wal.mergeProvenance ? { resume: wal.mergeProvenance } : {},
|
|
638
|
+
onBeforeMerge: async (provenance) => {
|
|
639
|
+
wal = {
|
|
640
|
+
...wal,
|
|
641
|
+
mergeProvenance: provenance,
|
|
642
|
+
observedFiles: observed2
|
|
643
|
+
};
|
|
644
|
+
await writeWal(filePath, wal);
|
|
645
|
+
},
|
|
646
|
+
onMerged: async () => {
|
|
647
|
+
wal = {
|
|
648
|
+
...wal,
|
|
649
|
+
state: "PREPARED",
|
|
650
|
+
accepted: observed2.length > 0,
|
|
651
|
+
testEngineerExempt: isMarkdownOnlyTaskChange(wal.context.declaredFiles, observed2)
|
|
652
|
+
};
|
|
653
|
+
await writeWal(filePath, wal);
|
|
654
|
+
recovered = await commitPrepared(directory, wal);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
if (mergeResult.outcome !== "merged" || recovered === null) {
|
|
658
|
+
if (mergeResult.outcome === "failed" && mergeResult.stage === SOURCE_WORKTREE_GONE_STAGE && await selfHealMissingLaneWorktree(directory, filePath, taskId, wal.transitionId, descriptor, mergeResult.message)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
throw new Error(`CODER_SETTLEMENT_MERGE_RECOVERY_REQUIRED: ${mergeResult.outcome === "failed" ? mergeResult.message : mergeResult.outcome}`);
|
|
662
|
+
}
|
|
663
|
+
await cleanupRecoveredWorktree(directory, descriptor);
|
|
664
|
+
const committedWal = await readWal(filePath, taskId);
|
|
665
|
+
if (committedWal === null) {
|
|
666
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
667
|
+
}
|
|
668
|
+
await writeWal(filePath, {
|
|
669
|
+
...committedWal,
|
|
670
|
+
cleanupComplete: true
|
|
671
|
+
});
|
|
672
|
+
return recovered;
|
|
673
|
+
}
|
|
674
|
+
const rawObserved = changedFilesSinceSnapshot(directory, wal.context.baseline);
|
|
675
|
+
let observed;
|
|
676
|
+
if (rawObserved === null) {
|
|
677
|
+
if (baselineAttributionDoomed(wal.context.baseline)) {
|
|
678
|
+
const aborted = await abortDispatchedWalUnderLock(directory, taskId, wal.transitionId, doomedReason(wal.context.baseline));
|
|
679
|
+
if (aborted.outcome === "aborted" || aborted.outcome === "already-aborted") {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
observed = null;
|
|
684
|
+
} else if (wal.context.declaredFiles === null) {
|
|
685
|
+
observed = [];
|
|
686
|
+
} else {
|
|
687
|
+
observed = rawObserved.filter((filePath2) => isPathWithinDeclaredScope(filePath2, wal.context.declaredFiles ?? [], directory));
|
|
688
|
+
}
|
|
689
|
+
if (observed === null) {
|
|
690
|
+
throw new Error(`CODER_SETTLEMENT_RECOVERY_UNCERTAIN: task ${taskId} workspace changes could not be attributed safely`);
|
|
691
|
+
}
|
|
692
|
+
wal = {
|
|
693
|
+
...wal,
|
|
694
|
+
state: "PREPARED",
|
|
695
|
+
accepted: observed.length > 0,
|
|
696
|
+
testEngineerExempt: isMarkdownOnlyTaskChange(wal.context.declaredFiles, observed)
|
|
697
|
+
};
|
|
698
|
+
await writeWal(filePath, wal);
|
|
699
|
+
}
|
|
700
|
+
const committed = await commitPrepared(directory, wal);
|
|
701
|
+
if (wal.worktree) {
|
|
702
|
+
await cleanupRecoveredWorktree(directory, wal.worktree);
|
|
703
|
+
const committedWal = await readWal(filePath, taskId);
|
|
704
|
+
if (committedWal === null) {
|
|
705
|
+
throw new Error("CODER_SETTLEMENT_WAL_MISSING");
|
|
706
|
+
}
|
|
707
|
+
await writeWal(filePath, { ...committedWal, cleanupComplete: true });
|
|
708
|
+
}
|
|
709
|
+
return committed;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function releaseCoderDispatchOwnership(directory, taskId, transitionId) {
|
|
713
|
+
liveDispatches.delete(dispatchKey(directory, taskId, transitionId));
|
|
714
|
+
}
|
|
715
|
+
async function assertNoUnsettledCoderDispatch(directory, taskId) {
|
|
716
|
+
const filePath = walPath(directory, taskId);
|
|
717
|
+
const wal = await readWal(filePath, taskId);
|
|
718
|
+
if (wal === null)
|
|
719
|
+
return;
|
|
720
|
+
if (wal.state === "DISPATCHED" || wal.state === "PREPARED") {
|
|
721
|
+
throw new Error(`CODER_SETTLEMENT_RECOVERY_REQUIRED: transition ${wal.transitionId} must settle before task ${taskId} can change plan status (${filePath}, state ${wal.state}). Run /swarm recover ${taskId} (or /swarm reset-session), then retry.`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
var MAX_SETTLEMENT_WAL_SCAN = 200;
|
|
725
|
+
var SETTLEMENT_WAL_FILE_PATTERN = /^[\w.\-:]+\.json$/;
|
|
726
|
+
async function listCoderSettlementWalStates(directory) {
|
|
727
|
+
let entries;
|
|
728
|
+
try {
|
|
729
|
+
const dirPath = validateSwarmPath(directory, "coder-settlements");
|
|
730
|
+
entries = await readdir(dirPath);
|
|
731
|
+
} catch {
|
|
732
|
+
return { states: [], truncated: false };
|
|
733
|
+
}
|
|
734
|
+
const matching = entries.filter((entry) => SETTLEMENT_WAL_FILE_PATTERN.test(entry));
|
|
735
|
+
const states = [];
|
|
736
|
+
for (const entry of matching.sort().slice(0, MAX_SETTLEMENT_WAL_SCAN)) {
|
|
737
|
+
const taskId = entry.slice(0, -".json".length);
|
|
738
|
+
try {
|
|
739
|
+
const wal = await readWal(walPath(directory, taskId), taskId);
|
|
740
|
+
if (wal === null)
|
|
741
|
+
continue;
|
|
742
|
+
states.push({
|
|
743
|
+
taskId,
|
|
744
|
+
state: wal.state,
|
|
745
|
+
transitionId: wal.transitionId,
|
|
746
|
+
processId: wal.processId,
|
|
747
|
+
recordedAt: wal.recordedAt,
|
|
748
|
+
accepted: wal.accepted === true ? true : undefined,
|
|
749
|
+
declaredFiles: wal.context?.declaredFiles ?? null,
|
|
750
|
+
ownedInProcess: liveDispatches.has(dispatchKey(directory, taskId, wal.transitionId)),
|
|
751
|
+
ownedByLiveForeignPid: wal.processId !== process.pid && isProcessAlive(wal.processId)
|
|
752
|
+
});
|
|
753
|
+
} catch {
|
|
754
|
+
states.push({
|
|
755
|
+
taskId,
|
|
756
|
+
state: "unreadable",
|
|
757
|
+
ownedInProcess: false,
|
|
758
|
+
ownedByLiveForeignPid: false
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { states, truncated: matching.length > MAX_SETTLEMENT_WAL_SCAN };
|
|
763
|
+
}
|
|
764
|
+
async function recoverStaleCoderSettlements(directory, options) {
|
|
765
|
+
const { states: listed, truncated } = await listCoderSettlementWalStates(directory);
|
|
766
|
+
const filter = options?.taskIds ? new Set(options.taskIds) : undefined;
|
|
767
|
+
const results = [];
|
|
768
|
+
for (const entry of listed) {
|
|
769
|
+
if (filter && !filter.has(entry.taskId))
|
|
770
|
+
continue;
|
|
771
|
+
if (entry.state === "unreadable") {
|
|
772
|
+
results.push({ taskId: entry.taskId, outcome: "unreadable_wal" });
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (entry.state === "ABORTED" || entry.state === "COMMITTED") {
|
|
776
|
+
try {
|
|
777
|
+
await _internals.recoverCoderSettlement(directory, entry.taskId);
|
|
778
|
+
results.push({
|
|
779
|
+
taskId: entry.taskId,
|
|
780
|
+
outcome: "already_terminal",
|
|
781
|
+
state: entry.state
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
results.push({
|
|
785
|
+
taskId: entry.taskId,
|
|
786
|
+
outcome: "error",
|
|
787
|
+
message: error instanceof Error ? error.message : String(error)
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (entry.ownedInProcess && options?.force !== true) {
|
|
793
|
+
results.push({
|
|
794
|
+
taskId: entry.taskId,
|
|
795
|
+
outcome: "owned_in_process",
|
|
796
|
+
transitionId: entry.transitionId ?? ""
|
|
797
|
+
});
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (entry.ownedByLiveForeignPid) {
|
|
801
|
+
results.push({
|
|
802
|
+
taskId: entry.taskId,
|
|
803
|
+
outcome: "owned_by_live_foreign_pid",
|
|
804
|
+
processId: entry.processId ?? -1
|
|
805
|
+
});
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const forced = entry.ownedInProcess && options?.force === true;
|
|
809
|
+
if (forced && entry.transitionId) {
|
|
810
|
+
releaseCoderDispatchOwnership(directory, entry.taskId, entry.transitionId);
|
|
811
|
+
await appendSettlementEvent(directory, "recovered", {
|
|
812
|
+
taskId: entry.taskId,
|
|
813
|
+
transitionId: entry.transitionId,
|
|
814
|
+
actor: "swarm-recovery"
|
|
815
|
+
}, { forced: true, stage: "ownership-release" });
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const recovered = await _internals.recoverCoderSettlement(directory, entry.taskId);
|
|
819
|
+
if (recovered === null) {
|
|
820
|
+
let current = null;
|
|
821
|
+
try {
|
|
822
|
+
current = await readWal(walPath(directory, entry.taskId), entry.taskId);
|
|
823
|
+
} catch {
|
|
824
|
+
current = null;
|
|
825
|
+
}
|
|
826
|
+
if (current && (current.state === "COMMITTED" || current.state === "ABORTED")) {
|
|
827
|
+
results.push({
|
|
828
|
+
taskId: entry.taskId,
|
|
829
|
+
outcome: "already_terminal",
|
|
830
|
+
state: current.state
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
results.push({
|
|
835
|
+
taskId: entry.taskId,
|
|
836
|
+
outcome: "error",
|
|
837
|
+
message: "settlement WAL disappeared during recovery"
|
|
838
|
+
});
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
await appendSettlementEvent(directory, "recovered", {
|
|
842
|
+
taskId: entry.taskId,
|
|
843
|
+
transitionId: entry.transitionId ?? "",
|
|
844
|
+
actor: "swarm-recovery"
|
|
845
|
+
}, { forced, accepted: recovered.accepted });
|
|
846
|
+
results.push({
|
|
847
|
+
taskId: entry.taskId,
|
|
848
|
+
outcome: "recovered",
|
|
849
|
+
accepted: recovered.accepted,
|
|
850
|
+
forced
|
|
851
|
+
});
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
854
|
+
await appendSettlementEvent(directory, "recovery-failed", {
|
|
855
|
+
taskId: entry.taskId,
|
|
856
|
+
transitionId: entry.transitionId ?? "",
|
|
857
|
+
actor: "swarm-recovery"
|
|
858
|
+
}, { forced, error: message });
|
|
859
|
+
results.push({
|
|
860
|
+
taskId: entry.taskId,
|
|
861
|
+
outcome: "error",
|
|
862
|
+
message
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { results, truncated };
|
|
867
|
+
}
|
|
868
|
+
var _internals = {
|
|
869
|
+
liveDispatches,
|
|
870
|
+
removeWorktreeProvisioningOwner,
|
|
871
|
+
branchExists: probeBranchExists,
|
|
872
|
+
recoverCoderSettlement
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
export { beginCoderSettlement, settleCoderDispatch, abortCoderSettlement, transferCoderSettlementToBackground, abortCoderSettlementIfDoomed, recordCoderMergeProvenance, completeCoderSettlementCleanup, recoverCoderSettlement, releaseCoderDispatchOwnership, assertNoUnsettledCoderDispatch, listCoderSettlementWalStates, recoverStaleCoderSettlements, _internals };
|