@lmzhen/dsh-evolution-merge 0.39.1 → 0.40.0

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/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { dirname, join } from "node:path";
1
+ import { dirname, isAbsolute, join } from "node:path";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { pairKeyOf, screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { DEFAULT_SKILL_LIMITS, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, readUnitJournal, registerOwnerSettings, reportsSweepLockTarget, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, sweepReports, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
5
+ import { DEFAULT_SKILL_LIMITS, LeaseStore, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, readUnitJournal, registerOwnerSettings, reportsSweepLockTarget, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, sweepReports, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
6
6
  import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { chmod, mkdir, open, readFile, stat, unlink } from "node:fs/promises";
@@ -1183,60 +1183,64 @@ async function readV4Readiness(io, home) {
1183
1183
  return await inspect(io, home, parsed.body, parsed.rows);
1184
1184
  }
1185
1185
  /** Atomic readiness + V4 row write under one state-file lock. No production caller in 1b-2a. */
1186
- async function transactMergeState(io, home, fn) {
1186
+ async function transactMergeState(io, home, fn, fence) {
1187
1187
  let result;
1188
- await transactIo(io, schema4StatePath(home), async (current) => {
1189
- if (current === null) throw new V4BlockedError(blocked$2("merge-state-v4-required", "schema4 state is missing"));
1190
- let value;
1191
- try {
1192
- value = JSON.parse(current);
1193
- } catch {
1194
- throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not JSON"));
1195
- }
1196
- if (!object(value)) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not an object"));
1197
- if (typeof value.schemaVersion === "number" && value.schemaVersion > 4) throw new V4BlockedError(blocked$2("merge-state-forward-only", "state schema " + String(value.schemaVersion) + " is newer than this build"));
1198
- if (value.schemaVersion === 3) throw new V4BlockedError(blocked$2("merge-state-normalize-required", "schema3 state requires v4 normalize"));
1199
- if (value.schemaVersion !== 4) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not schema4"));
1200
- const parsed = parse(current);
1201
- if (parsed === null) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "schema4 state has an invalid row"));
1202
- const ready = await inspect(io, home, parsed.body, parsed.rows);
1203
- if (!ready.ok) throw new V4BlockedError(ready);
1204
- let rows = parsed.rows;
1205
- const apply = (write) => {
1206
- const next = applyV4Write(rows, write, (/* @__PURE__ */ new Date()).toISOString());
1207
- if (next.ok) rows = next.rows;
1208
- return next;
1209
- };
1210
- result = await fn({
1211
- rows: () => rows,
1212
- createVerify: (row) => apply({
1213
- kind: "createVerify",
1214
- row
1215
- }),
1216
- createSkipped: (row) => apply({
1217
- kind: "createSkipped",
1218
- row
1219
- }),
1220
- transition: (id, event, expectedVersion) => apply({
1221
- kind: "transition",
1222
- id,
1223
- event,
1224
- expectedVersion
1225
- }),
1226
- remove: (id, terminal, unit, expectedVersion) => apply({
1227
- kind: "remove",
1228
- id,
1229
- terminal,
1230
- ...unit === void 0 ? {} : { unit },
1231
- expectedVersion
1232
- })
1188
+ const run = async () => {
1189
+ await transactIo(io, schema4StatePath(home), async (current) => {
1190
+ if (current === null) throw new V4BlockedError(blocked$2("merge-state-v4-required", "schema4 state is missing"));
1191
+ let value;
1192
+ try {
1193
+ value = JSON.parse(current);
1194
+ } catch {
1195
+ throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not JSON"));
1196
+ }
1197
+ if (!object(value)) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not an object"));
1198
+ if (typeof value.schemaVersion === "number" && value.schemaVersion > 4) throw new V4BlockedError(blocked$2("merge-state-forward-only", "state schema " + String(value.schemaVersion) + " is newer than this build"));
1199
+ if (value.schemaVersion === 3) throw new V4BlockedError(blocked$2("merge-state-normalize-required", "schema3 state requires v4 normalize"));
1200
+ if (value.schemaVersion !== 4) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "merge state is not schema4"));
1201
+ const parsed = parse(current);
1202
+ if (parsed === null) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "schema4 state has an invalid row"));
1203
+ const ready = await inspect(io, home, parsed.body, parsed.rows);
1204
+ if (!ready.ok) throw new V4BlockedError(ready);
1205
+ let rows = parsed.rows;
1206
+ const apply = (write) => {
1207
+ const next = applyV4Write(rows, write, (/* @__PURE__ */ new Date()).toISOString());
1208
+ if (next.ok) rows = next.rows;
1209
+ return next;
1210
+ };
1211
+ result = await fn({
1212
+ rows: () => rows,
1213
+ createVerify: (row) => apply({
1214
+ kind: "createVerify",
1215
+ row
1216
+ }),
1217
+ createSkipped: (row) => apply({
1218
+ kind: "createSkipped",
1219
+ row
1220
+ }),
1221
+ transition: (id, event, expectedVersion) => apply({
1222
+ kind: "transition",
1223
+ id,
1224
+ event,
1225
+ expectedVersion
1226
+ }),
1227
+ remove: (id, terminal, unit, expectedVersion) => apply({
1228
+ kind: "remove",
1229
+ id,
1230
+ terminal,
1231
+ ...unit === void 0 ? {} : { unit },
1232
+ expectedVersion
1233
+ })
1234
+ });
1235
+ return JSON.stringify({
1236
+ ...parsed.body,
1237
+ schemaVersion: 4,
1238
+ rows
1239
+ }, null, 2) + "\n";
1233
1240
  });
1234
- return JSON.stringify({
1235
- ...parsed.body,
1236
- schemaVersion: 4,
1237
- rows
1238
- }, null, 2) + "\n";
1239
- });
1241
+ };
1242
+ if (fence !== void 0) await fence.run(run);
1243
+ else await run();
1240
1244
  if (result === void 0) throw new V4BlockedError(blocked$2("merge-state-needs-repair", "v4 transaction did not produce a result"));
1241
1245
  return result;
1242
1246
  }
@@ -1804,160 +1808,164 @@ async function readStateReadiness(io, home) {
1804
1808
  return blocked("merge-state-migration-required", "v2 state requires v3 migration");
1805
1809
  }
1806
1810
  /** One readiness/migration gate; used by startup and every write entry. */
1807
- async function ensureMergeStateReady(io, home) {
1811
+ async function ensureMergeStateReady(io, home, fence) {
1808
1812
  let outcome;
1809
1813
  let migrated = false;
1810
1814
  try {
1811
- await transactIo(io, mergeStatePath$1(home), async (current) => {
1812
- if (current === null) {
1813
- outcome = {
1814
- ok: true,
1815
- schema: 3,
1816
- migrated: false,
1817
- state: {
1818
- schemaVersion: 3,
1819
- rows: []
1820
- },
1821
- quarantine: [],
1822
- report: await readReport(io, home)
1823
- };
1824
- return null;
1825
- }
1826
- const body = parseBody(current);
1827
- if (body === null) {
1828
- outcome = blocked("merge-state-needs-repair", "state file is not a JSON object");
1829
- return current;
1830
- }
1831
- const version = body.schemaVersion;
1832
- if (typeof version === "number" && version > 3) {
1833
- outcome = blocked("merge-state-forward-only", "state schema " + String(version) + " is newer than this build");
1834
- return current;
1835
- }
1836
- if (version === 3) {
1837
- const state = v3State(body);
1838
- if (state === null) {
1839
- outcome = blocked("merge-state-needs-repair", "v3 state has an invalid row");
1815
+ const run = async () => {
1816
+ await transactIo(io, mergeStatePath$1(home), async (current) => {
1817
+ if (current === null) {
1818
+ outcome = {
1819
+ ok: true,
1820
+ schema: 3,
1821
+ migrated: false,
1822
+ state: {
1823
+ schemaVersion: 3,
1824
+ rows: []
1825
+ },
1826
+ quarantine: [],
1827
+ report: await readReport(io, home)
1828
+ };
1829
+ return null;
1830
+ }
1831
+ const body = parseBody(current);
1832
+ if (body === null) {
1833
+ outcome = blocked("merge-state-needs-repair", "state file is not a JSON object");
1840
1834
  return current;
1841
1835
  }
1842
- const journal = await readJournal$1(io, home);
1843
- if (journal !== null) {
1844
- outcome = await completeJournal(io, home, journal, current);
1836
+ const version = body.schemaVersion;
1837
+ if (typeof version === "number" && version > 3) {
1838
+ outcome = blocked("merge-state-forward-only", "state schema " + String(version) + " is newer than this build");
1845
1839
  return current;
1846
1840
  }
1847
- const report = await readReport(io, home);
1848
- const marker = readMarker(body);
1849
- if (body.migration !== void 0 && marker === null) {
1850
- outcome = blocked("merge-state-needs-repair", "migration marker is invalid");
1841
+ if (version === 3) {
1842
+ const state = v3State(body);
1843
+ if (state === null) {
1844
+ outcome = blocked("merge-state-needs-repair", "v3 state has an invalid row");
1845
+ return current;
1846
+ }
1847
+ const journal = await readJournal$1(io, home);
1848
+ if (journal !== null) {
1849
+ outcome = await completeJournal(io, home, journal, current);
1850
+ return current;
1851
+ }
1852
+ const report = await readReport(io, home);
1853
+ const marker = readMarker(body);
1854
+ if (body.migration !== void 0 && marker === null) {
1855
+ outcome = blocked("merge-state-needs-repair", "migration marker is invalid");
1856
+ return current;
1857
+ }
1858
+ if (report !== null && report.ready !== true) {
1859
+ outcome = blocked("merge-state-migration-blocked", "migration report is not ready");
1860
+ return current;
1861
+ }
1862
+ if (marker !== null && rowsSha(state.rows) === marker.targetSha256) {
1863
+ if (report === null) {
1864
+ outcome = blocked("merge-state-migration-blocked", "migration report is missing");
1865
+ return current;
1866
+ }
1867
+ for (const entry of marker.staging) if (!await io.exists(stagingIndexPath(home, entry.rowId))) await writeStagingIndex(io, home, entry);
1868
+ }
1869
+ outcome = {
1870
+ ok: true,
1871
+ schema: 3,
1872
+ migrated: false,
1873
+ state,
1874
+ quarantine: [],
1875
+ report
1876
+ };
1851
1877
  return current;
1852
1878
  }
1853
- if (report !== null && report.ready !== true) {
1854
- outcome = blocked("merge-state-migration-blocked", "migration report is not ready");
1879
+ const mapped = migrateV2ToV3(body);
1880
+ if (!mapped.ok) {
1881
+ outcome = blocked("merge-state-migration-blocked", mapped.message);
1855
1882
  return current;
1856
1883
  }
1857
- if (marker !== null && rowsSha(state.rows) === marker.targetSha256) {
1858
- if (report === null) {
1859
- outcome = blocked("merge-state-migration-blocked", "migration report is missing");
1860
- return current;
1861
- }
1862
- for (const entry of marker.staging) if (!await io.exists(stagingIndexPath(home, entry.rowId))) await writeStagingIndex(io, home, entry);
1884
+ const sourceSha256 = hash(current);
1885
+ const backup = v2BackupPath(home);
1886
+ const existingBackup = await readTextOrNull(io, backup);
1887
+ if (existingBackup !== null && existingBackup !== current) {
1888
+ outcome = blocked("merge-state-migration-blocked", "v2 backup exists with different bytes");
1889
+ return current;
1890
+ }
1891
+ if (existingBackup === null) await transactIo(io, backup, () => current);
1892
+ try {
1893
+ await chmod(backup, 292);
1894
+ } catch {
1895
+ outcome = blocked("merge-state-migration-blocked", "v2 backup is not read-only");
1896
+ return current;
1897
+ }
1898
+ const staging = [];
1899
+ for (const row of body.rows) {
1900
+ if (typeof row.id !== "string" || typeof row.pairKey !== "string" || typeof row.stagingRef !== "string") continue;
1901
+ if (row.state !== "pending" && row.state !== "approved") continue;
1902
+ staging.push({
1903
+ rowId: row.id,
1904
+ pairKey: row.pairKey,
1905
+ ref: row.stagingRef,
1906
+ expiresAt: typeof row.stagingExpiresAt === "number" ? row.stagingExpiresAt : Date.now() + 7 * 864e5,
1907
+ ...typeof row.runId === "string" ? { runId: row.runId } : {},
1908
+ createdAt: typeof row.updatedAt === "string" && !Number.isNaN(Date.parse(row.updatedAt)) ? Date.parse(row.updatedAt) : Date.now()
1909
+ });
1863
1910
  }
1911
+ const marker = {
1912
+ migratedFromV2: true,
1913
+ sourceSha256,
1914
+ targetSha256: rowsSha(mapped.state.rows),
1915
+ staging,
1916
+ at: now()
1917
+ };
1918
+ const targetText = json$1({
1919
+ ...mapped.state,
1920
+ migration: marker
1921
+ });
1922
+ const targetSha256 = hash(targetText);
1923
+ const journal = {
1924
+ version: 1,
1925
+ sourceSha256,
1926
+ targetSha256,
1927
+ rows: mapped.state.rows.length,
1928
+ quarantine: mapped.quarantine,
1929
+ staging,
1930
+ backupPath: backup,
1931
+ at: now()
1932
+ };
1933
+ await transactIo(io, migrationJournalPath(home), () => json$1(journal));
1934
+ for (const entry of staging) await writeStagingIndex(io, home, entry);
1935
+ if (mapped.quarantine.length > 0) await transactIo(io, quarantinePath(home), () => json$1(mapped.quarantine));
1936
+ const prepared = {
1937
+ version: 1,
1938
+ ready: false,
1939
+ migrated: true,
1940
+ sourceSha256,
1941
+ targetSha256,
1942
+ rows: mapped.state.rows.length,
1943
+ quarantined: mapped.quarantine.length,
1944
+ backupReadOnly: true,
1945
+ at: now(),
1946
+ steps: [
1947
+ "backup",
1948
+ "journal",
1949
+ "staging-index",
1950
+ "quarantine",
1951
+ "v3-write"
1952
+ ]
1953
+ };
1954
+ await writeReport(io, home, prepared);
1955
+ migrated = true;
1864
1956
  outcome = {
1865
1957
  ok: true,
1866
1958
  schema: 3,
1867
- migrated: false,
1868
- state,
1869
- quarantine: [],
1870
- report
1959
+ migrated: true,
1960
+ state: mapped.state,
1961
+ quarantine: mapped.quarantine,
1962
+ report: prepared
1871
1963
  };
1872
- return current;
1873
- }
1874
- const mapped = migrateV2ToV3(body);
1875
- if (!mapped.ok) {
1876
- outcome = blocked("merge-state-migration-blocked", mapped.message);
1877
- return current;
1878
- }
1879
- const sourceSha256 = hash(current);
1880
- const backup = v2BackupPath(home);
1881
- const existingBackup = await readTextOrNull(io, backup);
1882
- if (existingBackup !== null && existingBackup !== current) {
1883
- outcome = blocked("merge-state-migration-blocked", "v2 backup exists with different bytes");
1884
- return current;
1885
- }
1886
- if (existingBackup === null) await transactIo(io, backup, () => current);
1887
- try {
1888
- await chmod(backup, 292);
1889
- } catch {
1890
- outcome = blocked("merge-state-migration-blocked", "v2 backup is not read-only");
1891
- return current;
1892
- }
1893
- const staging = [];
1894
- for (const row of body.rows) {
1895
- if (typeof row.id !== "string" || typeof row.pairKey !== "string" || typeof row.stagingRef !== "string") continue;
1896
- if (row.state !== "pending" && row.state !== "approved") continue;
1897
- staging.push({
1898
- rowId: row.id,
1899
- pairKey: row.pairKey,
1900
- ref: row.stagingRef,
1901
- expiresAt: typeof row.stagingExpiresAt === "number" ? row.stagingExpiresAt : Date.now() + 7 * 864e5,
1902
- ...typeof row.runId === "string" ? { runId: row.runId } : {},
1903
- createdAt: typeof row.updatedAt === "string" && !Number.isNaN(Date.parse(row.updatedAt)) ? Date.parse(row.updatedAt) : Date.now()
1904
- });
1905
- }
1906
- const marker = {
1907
- migratedFromV2: true,
1908
- sourceSha256,
1909
- targetSha256: rowsSha(mapped.state.rows),
1910
- staging,
1911
- at: now()
1912
- };
1913
- const targetText = json$1({
1914
- ...mapped.state,
1915
- migration: marker
1964
+ return targetText;
1916
1965
  });
1917
- const targetSha256 = hash(targetText);
1918
- const journal = {
1919
- version: 1,
1920
- sourceSha256,
1921
- targetSha256,
1922
- rows: mapped.state.rows.length,
1923
- quarantine: mapped.quarantine,
1924
- staging,
1925
- backupPath: backup,
1926
- at: now()
1927
- };
1928
- await transactIo(io, migrationJournalPath(home), () => json$1(journal));
1929
- for (const entry of staging) await writeStagingIndex(io, home, entry);
1930
- if (mapped.quarantine.length > 0) await transactIo(io, quarantinePath(home), () => json$1(mapped.quarantine));
1931
- const prepared = {
1932
- version: 1,
1933
- ready: false,
1934
- migrated: true,
1935
- sourceSha256,
1936
- targetSha256,
1937
- rows: mapped.state.rows.length,
1938
- quarantined: mapped.quarantine.length,
1939
- backupReadOnly: true,
1940
- at: now(),
1941
- steps: [
1942
- "backup",
1943
- "journal",
1944
- "staging-index",
1945
- "quarantine",
1946
- "v3-write"
1947
- ]
1948
- };
1949
- await writeReport(io, home, prepared);
1950
- migrated = true;
1951
- outcome = {
1952
- ok: true,
1953
- schema: 3,
1954
- migrated: true,
1955
- state: mapped.state,
1956
- quarantine: mapped.quarantine,
1957
- report: prepared
1958
- };
1959
- return targetText;
1960
- });
1966
+ };
1967
+ if (fence !== void 0) await fence.run(run);
1968
+ else await run();
1961
1969
  } catch (error) {
1962
1970
  return blocked("merge-state-migration-blocked", "state migration failed: " + (error instanceof Error ? error.message : String(error)));
1963
1971
  }
@@ -2304,11 +2312,22 @@ async function presentStage(io, home, entry) {
2304
2312
  state: "present"
2305
2313
  });
2306
2314
  }
2315
+ function safeRunTree(home, ref) {
2316
+ const name = ref.trim();
2317
+ if (name === "" || name.includes("\\") || isAbsolute(name)) return null;
2318
+ const parts = name.split("/");
2319
+ if (parts.some((part) => part === "" || part === "." || part === "..")) return null;
2320
+ return join(home, "merge", "runs", ...parts);
2321
+ }
2307
2322
  async function dropStage(io, home, rowId, ref, onError) {
2308
2323
  const index = join(home, "merge", "staging-index", encodeURIComponent(rowId) + ".json");
2309
2324
  await io.remove(index).catch((error) => onError?.(index, error));
2310
2325
  if (ref !== void 0 && ref !== "") {
2311
- const tree = join(home, "merge", "runs", ref);
2326
+ const tree = safeRunTree(home, ref);
2327
+ if (tree === null) {
2328
+ onError?.(join(home, "merge", "runs"), /* @__PURE__ */ new Error("invalid staging ref: " + JSON.stringify(ref)));
2329
+ return;
2330
+ }
2312
2331
  await io.remove(tree).catch((error) => onError?.(tree, error));
2313
2332
  }
2314
2333
  }
@@ -2374,7 +2393,12 @@ async function cleanupStage(io, home, now, rows, limit = 100) {
2374
2393
  for (const run of runNames.slice(0, limit)) {
2375
2394
  const prefix = run + "/";
2376
2395
  if ([...refs].some((ref) => ref === run || ref.startsWith(prefix))) continue;
2377
- await io.remove(join(runRoot, run)).catch((error) => fail(join(runRoot, run), error));
2396
+ const tree = safeRunTree(home, run);
2397
+ if (tree === null) {
2398
+ fail(join(runRoot, run), /* @__PURE__ */ new Error("invalid run dir name: " + JSON.stringify(run)));
2399
+ continue;
2400
+ }
2401
+ await io.remove(tree).catch((error) => fail(tree, error));
2378
2402
  orphans.push(run);
2379
2403
  }
2380
2404
  return {
@@ -2421,7 +2445,7 @@ async function readPairLedger(io, home) {
2421
2445
  };
2422
2446
  }
2423
2447
  /** 1b-2a write skeleton. Production stays on the v3 gate until 1b-2b. */
2424
- function pairLedger(io, home) {
2448
+ function pairLedger(io, home, fence) {
2425
2449
  return {
2426
2450
  read: () => readPairLedger(io, home),
2427
2451
  transact: async (fn) => {
@@ -2429,7 +2453,7 @@ function pairLedger(io, home) {
2429
2453
  const normalized = await normalizePairLedger(io, home);
2430
2454
  if (!normalized.ok) throw new Error(normalized.message ?? "normalize failed");
2431
2455
  }
2432
- return await transactMergeState(io, home, fn);
2456
+ return await transactMergeState(io, home, fn, fence);
2433
2457
  },
2434
2458
  normalize: () => normalizePairLedger(io, home)
2435
2459
  };
@@ -2572,8 +2596,39 @@ async function recordPair(io, home, entry) {
2572
2596
  function runMetaPath(home, runId) {
2573
2597
  return join(home, "merge", "run-meta", encodeURIComponent(runId) + ".json");
2574
2598
  }
2575
- async function writeRunMeta(io, home, meta) {
2576
- await transactIo(io, runMetaPath(home, meta.runId), () => JSON.stringify(meta, null, 2) + "\n");
2599
+ async function writeRunMeta(io, home, meta, fence) {
2600
+ if (meta.revision !== void 0) {
2601
+ const currentRevision = (await readRunMeta(io, home, meta.runId))?.revision ?? 0;
2602
+ if (currentRevision !== meta.revision) throw new Error("run-meta revision conflict: expected " + String(meta.revision) + ", found " + String(currentRevision));
2603
+ }
2604
+ const write = async () => {
2605
+ await transactIo(io, runMetaPath(home, meta.runId), (current) => {
2606
+ let next = meta;
2607
+ let previousRevision = 0;
2608
+ if (current !== null) try {
2609
+ const prev = JSON.parse(current);
2610
+ previousRevision = typeof prev.revision === "number" ? prev.revision : 0;
2611
+ if (meta.revision !== void 0 && meta.revision !== previousRevision) throw new Error("run-meta revision conflict: expected " + String(meta.revision) + ", found " + String(previousRevision));
2612
+ if (prev.degraded === true) next = {
2613
+ ...next,
2614
+ degraded: true
2615
+ };
2616
+ const terminal = prev.phase === "settled" || prev.phase === "cancelled" || prev.phase === "undone";
2617
+ const undoFromSettled = prev.phase === "settled" && next.phase === "undone";
2618
+ if (terminal && next.phase !== prev.phase && !undoFromSettled) next = {
2619
+ ...next,
2620
+ phase: prev.phase,
2621
+ ...prev.stopReason === void 0 ? {} : { stopReason: prev.stopReason }
2622
+ };
2623
+ } catch {}
2624
+ return JSON.stringify({
2625
+ ...next,
2626
+ revision: previousRevision + 1
2627
+ }, null, 2) + "\n";
2628
+ });
2629
+ };
2630
+ if (fence !== void 0) await fence.run(write);
2631
+ else await write();
2577
2632
  }
2578
2633
  async function readRunMeta(io, home, runId) {
2579
2634
  const raw = await readTextOrNull(io, runMetaPath(home, runId));
@@ -3087,6 +3142,7 @@ function releaseClaim(current, token) {
3087
3142
  //#region lib/types/target-claim.js
3088
3143
  /** Durable target claim: one approve writer per merge target at a time. */
3089
3144
  const PROCESS_STARTED_AT = Date.now() - process.uptime() * 1e3;
3145
+ const TARGET_CLAIM_TTL_MS = 30 * 6e4;
3090
3146
  function claimPath(home, target) {
3091
3147
  return join(home, "merge", "claims", contentHash(target) + ".json");
3092
3148
  }
@@ -3105,20 +3161,26 @@ function parseClaim(raw) {
3105
3161
  async function withGuard(path, fn) {
3106
3162
  const guard = path + ".guard";
3107
3163
  await mkdir(dirname(path), { recursive: true });
3108
- for (let attempt = 0; attempt < 500; attempt += 1) try {
3109
- const handle = await open(guard, "wx");
3110
- await handle.writeFile(String(process.pid));
3111
- await handle.close();
3164
+ for (let attempt = 0; attempt < 500; attempt += 1) {
3165
+ let handle;
3112
3166
  try {
3113
- return await fn();
3114
- } finally {
3115
- await unlink(guard).catch(() => {});
3167
+ handle = await open(guard, "wx");
3168
+ await handle.writeFile(String(process.pid));
3169
+ await handle.close();
3170
+ handle = void 0;
3171
+ try {
3172
+ return await fn();
3173
+ } finally {
3174
+ await unlink(guard).catch(() => {});
3175
+ }
3176
+ } catch (error) {
3177
+ await handle?.close().catch(() => {});
3178
+ if (error.code !== "EEXIST") await unlink(guard).catch(() => {});
3179
+ if (error.code !== "EEXIST") throw error;
3180
+ const pid = Number(await readFile(guard, "utf8").catch(() => ""));
3181
+ if (Number.isFinite(pid) && pid > 0 && !isProcessAlive(pid)) await unlink(guard).catch(() => {});
3182
+ await new Promise((resolve) => setTimeout(resolve, 10));
3116
3183
  }
3117
- } catch (error) {
3118
- if (error.code !== "EEXIST") throw error;
3119
- const pid = Number(await readFile(guard, "utf8").catch(() => ""));
3120
- if (Number.isFinite(pid) && pid > 0 && !isProcessAlive(pid)) await unlink(guard).catch(() => {});
3121
- await new Promise((resolve) => setTimeout(resolve, 10));
3122
3184
  }
3123
3185
  throw new Error("target-claim-guard-timeout");
3124
3186
  }
@@ -3149,7 +3211,7 @@ async function acquireTargetClaim(io, home, target, now) {
3149
3211
  let granted;
3150
3212
  await compareAndSwap(io, path, (raw) => {
3151
3213
  const current = parseClaim(raw);
3152
- const claim = acquireClaim(current !== void 0 && current.pid === process.pid && current.startedAt !== void 0 && Math.abs(current.startedAt - PROCESS_STARTED_AT) > 1e3 || current !== void 0 && current.expiresAt > now && current.pid !== void 0 && current.pid !== process.pid && !isProcessAlive(current.pid) ? void 0 : current, now, Number.MAX_SAFE_INTEGER, token, process.pid, PROCESS_STARTED_AT);
3214
+ const claim = acquireClaim(current !== void 0 && current.pid === process.pid && current.startedAt !== void 0 && Math.abs(current.startedAt - PROCESS_STARTED_AT) > 1e3 || current !== void 0 && current.expiresAt > now && current.pid !== void 0 && current.pid !== process.pid && !isProcessAlive(current.pid) ? void 0 : current, now, TARGET_CLAIM_TTL_MS, token, process.pid, PROCESS_STARTED_AT);
3153
3215
  if (claim === void 0) return raw;
3154
3216
  granted = claim;
3155
3217
  return JSON.stringify(claim) + "\n";
@@ -6230,31 +6292,42 @@ async function withMergeRun(options) {
6230
6292
  ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId }
6231
6293
  });
6232
6294
  const startedAt = handle.startedAt;
6295
+ const writeMeta = async (meta) => {
6296
+ await writeRunMeta(options.io, options.home, meta, options.fence);
6297
+ };
6233
6298
  const stage = (name, progress) => options.registry.stage(handle.id, name, progress);
6234
- await stage("screening", options.total === void 0 ? void 0 : {
6235
- done: 0,
6236
- total: options.total
6237
- });
6238
- await writeRunMeta(options.io, options.home, {
6239
- version: 1,
6240
- runId: handle.id,
6241
- pid: process.pid,
6242
- ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
6243
- ...options.pairKey === void 0 ? {} : { pairKey: options.pairKey },
6244
- ...options.stagingRef === void 0 ? {} : { stagingRef: options.stagingRef },
6245
- ...options.pairKey === void 0 ? {} : { pairs: [{
6246
- pairKey: options.pairKey,
6247
- state: "pending"
6248
- }] },
6249
- pairCount: options.pairCount ?? (options.pairKey === void 0 ? 0 : 1),
6250
- phase: "running",
6251
- approval: options.approval.mergeApproval ? "pending" : "approved",
6252
- createdAt: new Date(startedAt).toISOString(),
6253
- updatedAt: new Date(options.now()).toISOString()
6254
- });
6299
+ try {
6300
+ await stage("screening", options.total === void 0 ? void 0 : {
6301
+ done: 0,
6302
+ total: options.total
6303
+ });
6304
+ await writeMeta({
6305
+ version: 1,
6306
+ runId: handle.id,
6307
+ pid: process.pid,
6308
+ ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
6309
+ ...options.pairKey === void 0 ? {} : { pairKey: options.pairKey },
6310
+ ...options.stagingRef === void 0 ? {} : { stagingRef: options.stagingRef },
6311
+ ...options.pairKey === void 0 ? {} : { pairs: [{
6312
+ pairKey: options.pairKey,
6313
+ state: "pending"
6314
+ }] },
6315
+ pairCount: options.pairCount ?? (options.pairKey === void 0 ? 0 : 1),
6316
+ phase: "running",
6317
+ approval: options.approval.mergeApproval ? "pending" : "approved",
6318
+ createdAt: new Date(startedAt).toISOString(),
6319
+ updatedAt: new Date(options.now()).toISOString()
6320
+ });
6321
+ } catch (error) {
6322
+ await options.registry.settle(handle.id, {
6323
+ state: "failed",
6324
+ failure: "merge run failed before body: " + (error instanceof Error ? error.message : String(error))
6325
+ }).catch(() => {});
6326
+ throw error;
6327
+ }
6255
6328
  const writeFailureMeta = async (stopReason) => {
6256
6329
  try {
6257
- await writeRunMeta(options.io, options.home, {
6330
+ await writeMeta({
6258
6331
  version: 1,
6259
6332
  runId: handle.id,
6260
6333
  pid: process.pid,
@@ -6308,13 +6381,23 @@ async function withMergeRun(options) {
6308
6381
  timedOut = true;
6309
6382
  let drainTimer;
6310
6383
  const drained = await Promise.race([bodyOutcome, new Promise((resolve) => {
6311
- drainTimer = setTimeout(() => resolve({ kind: "drain-timeout" }), DETACHED_DRAIN_MS);
6384
+ drainTimer = setTimeout(() => resolve({ kind: "drain-timeout" }), options.drainMs ?? DETACHED_DRAIN_MS);
6312
6385
  })]);
6313
6386
  if (drainTimer !== void 0) clearTimeout(drainTimer);
6314
- outcome = drained.kind === "drain-timeout" ? {
6315
- kind: "error",
6316
- error: /* @__PURE__ */ new Error("timeout: body did not stop within 30s drain window")
6317
- } : drained;
6387
+ if (drained.kind === "drain-timeout") {
6388
+ await options.registry.settle(handle.id, {
6389
+ state: "failed",
6390
+ failure: "timeout: body did not stop within 30s drain window"
6391
+ }).catch((error) => {
6392
+ (options.warn ?? ((message) => {
6393
+ console.warn(message);
6394
+ }))("evolution-merge: drain-timeout settle failed for " + handle.id + ": " + (error instanceof Error ? error.message : String(error)));
6395
+ });
6396
+ outcome = {
6397
+ kind: "error",
6398
+ error: /* @__PURE__ */ new Error("timeout: body did not stop within 30s drain window")
6399
+ };
6400
+ } else outcome = drained;
6318
6401
  }
6319
6402
  }
6320
6403
  draft = outcome.kind === "draft" ? outcome.value : emptyDraft("run body failed: " + (outcome.error instanceof Error ? outcome.error.message : String(outcome.error)));
@@ -6398,10 +6481,12 @@ async function withMergeRun(options) {
6398
6481
  pairKey: options.pairKey,
6399
6482
  state: "pending"
6400
6483
  }];
6401
- await writeRunMeta(options.io, options.home, {
6484
+ const currentMeta = await readRunMeta(options.io, options.home, handle.id);
6485
+ await writeMeta({
6402
6486
  version: 1,
6403
6487
  runId: handle.id,
6404
6488
  pid: process.pid,
6489
+ ...currentMeta?.revision === void 0 ? {} : { revision: currentMeta.revision },
6405
6490
  ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
6406
6491
  ...options.pairKey === void 0 ? {} : { pairKey: options.pairKey },
6407
6492
  ...options.stagingRef === void 0 ? {} : { stagingRef: options.stagingRef },
@@ -6428,7 +6513,11 @@ async function withMergeRun(options) {
6428
6513
  };
6429
6514
  };
6430
6515
  if (options.detached === true) {
6431
- finish().catch(() => {});
6516
+ finish().catch((error) => {
6517
+ (options.warn ?? ((message) => {
6518
+ console.warn(message);
6519
+ }))("evolution-merge: detached run " + handle.id + " finish failed: " + (error instanceof Error ? error.message : String(error)));
6520
+ });
6432
6521
  return {
6433
6522
  ok: true,
6434
6523
  accepted: true,
@@ -6444,6 +6533,7 @@ async function runScanCommand(options) {
6444
6533
  io: options.io,
6445
6534
  home: options.home,
6446
6535
  registry: options.registry,
6536
+ ...options.fence === void 0 ? {} : { fence: options.fence },
6447
6537
  command: "scan",
6448
6538
  approval: {
6449
6539
  platformWrite: options.platformWrite,
@@ -6476,8 +6566,8 @@ async function runScanCommand(options) {
6476
6566
  }
6477
6567
  //#endregion
6478
6568
  //#region lib/types/pending-actions.js
6479
- async function act(io, home, now, id, type) {
6480
- return await pairLedger(io, home).transact((tx) => {
6569
+ async function act(io, home, now, id, type, fence) {
6570
+ return await pairLedger(io, home, fence).transact((tx) => {
6481
6571
  const row = tx.rows().find((item) => item.id === id);
6482
6572
  if (row === void 0) return {
6483
6573
  ok: false,
@@ -6519,24 +6609,36 @@ async function act(io, home, now, id, type) {
6519
6609
  };
6520
6610
  });
6521
6611
  }
6522
- async function approvePending(io, home, now, id) {
6523
- return await act(io, home, now, id, "approve");
6612
+ async function approvePending(io, home, now, id, fence) {
6613
+ return await act(io, home, now, id, "approve", fence);
6524
6614
  }
6525
- async function rejectPending(io, home, now, id) {
6526
- return await act(io, home, now, id, "reject");
6615
+ async function rejectPending(io, home, now, id, fence) {
6616
+ return await act(io, home, now, id, "reject", fence);
6527
6617
  }
6528
6618
  //#endregion
6529
6619
  //#region lib/types/domain/row-run.js
6530
6620
  /** Attach the in-flight child run to a row before its round2 starts (P1-8). */
6531
- async function attachRunToRow(io, home, rowId, runId) {
6532
- if (!await pairLedger(io, home).transact((tx) => {
6533
- const current = tx.rows().find((row) => row.id === rowId);
6534
- if (current === void 0) return false;
6535
- return tx.transition(rowId, {
6536
- type: "run-attached",
6537
- runId
6538
- }, current.version).ok;
6539
- })) throw new Error("row is not attachable to run: " + rowId);
6621
+ async function attachRunToRow(io, home, rowId, runId, isActive, fence) {
6622
+ const leaseStore = new LeaseStore({
6623
+ io,
6624
+ home,
6625
+ ttlMs: 3e4
6626
+ });
6627
+ const lease = await leaseStore.acquire("foreground", "row-attach:" + rowId);
6628
+ if (!lease.ok) throw new Error("row attach is busy: " + rowId);
6629
+ try {
6630
+ if (!await pairLedger(io, home, fence).transact(async (tx) => {
6631
+ const current = tx.rows().find((row) => row.id === rowId);
6632
+ if (current === void 0) return false;
6633
+ if (current.runId !== void 0 && current.runId !== runId && await isActive?.(current.runId) === true) return false;
6634
+ return tx.transition(rowId, {
6635
+ type: "run-attached",
6636
+ runId
6637
+ }, current.version).ok;
6638
+ })) throw new Error("row is not attachable to run: " + rowId);
6639
+ } finally {
6640
+ await leaseStore.release(lease.token);
6641
+ }
6540
6642
  }
6541
6643
  //#endregion
6542
6644
  //#region lib/types/domain/failure-route.js
@@ -6555,7 +6657,7 @@ function casExpiredHook(code) {
6555
6657
  }
6556
6658
  //#endregion
6557
6659
  //#region lib/types/round2-outcome.js
6558
- async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false) {
6660
+ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false, fence) {
6559
6661
  const reportExtra = {
6560
6662
  ...outcome.manifestFiles === void 0 ? {} : { manifestFiles: outcome.manifestFiles },
6561
6663
  ...outcome.criteria === void 0 ? {} : { criteria: outcome.criteria },
@@ -6568,7 +6670,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6568
6670
  };
6569
6671
  const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
6570
6672
  const finalize = async () => {
6571
- if (!(await pairLedger(io, home).transact((tx) => {
6673
+ if (!(await pairLedger(io, home, fence).transact((tx) => {
6572
6674
  const current = tx.rows().find((item) => item.id === row.id);
6573
6675
  return current === void 0 ? {
6574
6676
  ok: false,
@@ -6632,7 +6734,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6632
6734
  };
6633
6735
  if (failure.code === "merge-over-limit" || failure.code === "limit") {
6634
6736
  const reason = "merge-over-limit";
6635
- if (await pairLedger(io, home).transact((tx) => {
6737
+ if (await pairLedger(io, home, fence).transact((tx) => {
6636
6738
  const current = tx.rows().find((item) => item.id === row.id);
6637
6739
  return current === void 0 ? false : tx.transition(row.id, {
6638
6740
  type: "skipped",
@@ -6665,7 +6767,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6665
6767
  retryable: false,
6666
6768
  retryFrom: "none"
6667
6769
  };
6668
- await pairLedger(io, home).transact((tx) => {
6770
+ await pairLedger(io, home, fence).transact((tx) => {
6669
6771
  const current = tx.rows().find((item) => item.id === row.id);
6670
6772
  if (current === void 0) return false;
6671
6773
  return tx.transition(row.id, {
@@ -6691,7 +6793,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6691
6793
  };
6692
6794
  }
6693
6795
  if (failure.code === "commit-failed" || failure.code === "validation-failed") {
6694
- if (await pairLedger(io, home).transact((tx) => {
6796
+ if (await pairLedger(io, home, fence).transact((tx) => {
6695
6797
  const current = tx.rows().find((item) => item.id === row.id);
6696
6798
  return current === void 0 ? false : tx.remove(row.id, "superseded", void 0, current.version).ok;
6697
6799
  })) {
@@ -6705,7 +6807,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6705
6807
  }
6706
6808
  }
6707
6809
  if (failure.code === "source-gone") {
6708
- if (await pairLedger(io, home).transact((tx) => {
6810
+ if (await pairLedger(io, home, fence).transact((tx) => {
6709
6811
  const current = tx.rows().find((item) => item.id === row.id);
6710
6812
  return current === void 0 ? false : tx.remove(row.id, "superseded", void 0, current.version).ok;
6711
6813
  })) await dropStage(io, home, row.id, row.stagingRef);
@@ -6723,7 +6825,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
6723
6825
  }
6724
6826
  };
6725
6827
  }
6726
- if (await pairLedger(io, home).transact((tx) => {
6828
+ if (await pairLedger(io, home, fence).transact((tx) => {
6727
6829
  const current = tx.rows().find((item) => item.id === row.id);
6728
6830
  if (current === void 0) return null;
6729
6831
  const moved = tx.transition(row.id, {
@@ -6784,7 +6886,7 @@ async function runApproveCommand(options) {
6784
6886
  if (options.agentless === true) return failure("approve", "e-305", errorText("e-305-this-invocation-carries-no", { a1: "approve" }), "在 GUI 或 CLI 的会话里运行 /evolution merge approve " + before.id);
6785
6887
  const parentRunId = options.parentRunId ?? before.runId;
6786
6888
  if (parentRunId === void 0) return failure("approve", "merge-parent-required", "缺 parent run id(E-305 提示:该行可能由无会话调用创建,或父 run 已丢失)", "在 agent 会话中执行 /evolution merge approve " + before.id, before.runId);
6787
- const taken = await approvePending(options.io, options.home, options.now, options.rowId);
6889
+ const taken = await approvePending(options.io, options.home, options.now, options.rowId, options.fence);
6788
6890
  if (!taken.ok) return failure("approve", "approve-" + taken.reason, taken.message, "重试;若行状态已变化,运行 /evolution merge status " + options.rowId, before.runId);
6789
6891
  let row = taken.row;
6790
6892
  const decision = row.decision;
@@ -6792,7 +6894,7 @@ async function runApproveCommand(options) {
6792
6894
  if (options.ports.stage !== void 0) {
6793
6895
  const staged = await options.ports.stage(row, decision);
6794
6896
  if (!staged.ok) return failure("approve", "stage-failed", staged.message, "检查暂存目录后重试;不影响已记录的决策", row.runId);
6795
- const fused = await pairLedger(options.io, options.home).transact((tx) => {
6897
+ const fused = await pairLedger(options.io, options.home, options.fence).transact((tx) => {
6796
6898
  const current = tx.rows().find((item) => item.id === row.id);
6797
6899
  if (current === void 0) return null;
6798
6900
  const moved = tx.transition(row.id, {
@@ -6810,6 +6912,7 @@ async function runApproveCommand(options) {
6810
6912
  io: options.io,
6811
6913
  home: options.home,
6812
6914
  registry: options.registry,
6915
+ ...options.fence === void 0 ? {} : { fence: options.fence },
6813
6916
  command: "approve",
6814
6917
  approval: {
6815
6918
  platformWrite: options.platformWrite,
@@ -6821,7 +6924,7 @@ async function runApproveCommand(options) {
6821
6924
  now: options.now,
6822
6925
  ...options.detached === true ? { detached: true } : {},
6823
6926
  async body(ctx) {
6824
- await attachRunToRow(options.io, options.home, row.id, ctx.runId);
6927
+ await attachRunToRow(options.io, options.home, row.id, ctx.runId, options.isOwnerActive, options.fence);
6825
6928
  const stage = (name) => ctx.stage(name, {
6826
6929
  done: 0,
6827
6930
  total: 1
@@ -6854,7 +6957,7 @@ async function runApproveCommand(options) {
6854
6957
  },
6855
6958
  notes: []
6856
6959
  };
6857
- const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true);
6960
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true, options.fence);
6858
6961
  return {
6859
6962
  ...base,
6860
6963
  stopReason: record.merged ? "completed" : "failed",
@@ -6891,6 +6994,7 @@ async function runResumeCommand(options) {
6891
6994
  io: options.io,
6892
6995
  home: options.home,
6893
6996
  registry: options.registry,
6997
+ ...options.fence === void 0 ? {} : { fence: options.fence },
6894
6998
  approval: options.approval,
6895
6999
  platformWrite: options.platformWrite,
6896
7000
  now: options.now,
@@ -6926,7 +7030,7 @@ async function runResumeCommand(options) {
6926
7030
  if (selected.state === "approved" && options.ports.stage !== void 0) {
6927
7031
  const staged = await options.ports.stage(selected, decision);
6928
7032
  if (!staged.ok) return failure("resume", "stage-failed", staged.message, "检查暂存目录后重试;不影响已记录的决策", selected.runId);
6929
- const fused = await pairLedger(options.io, options.home).transact((tx) => {
7033
+ const fused = await pairLedger(options.io, options.home, options.fence).transact((tx) => {
6930
7034
  const current = tx.rows().find((item) => item.id === selected.id);
6931
7035
  if (current === void 0) return null;
6932
7036
  const moved = tx.transition(selected.id, {
@@ -6955,7 +7059,7 @@ async function runResumeCommand(options) {
6955
7059
  now: options.now,
6956
7060
  ...options.detached === true ? { detached: true } : {},
6957
7061
  async body(ctx) {
6958
- await attachRunToRow(options.io, options.home, row.id, ctx.runId);
7062
+ await attachRunToRow(options.io, options.home, row.id, ctx.runId, options.isOwnerActive, options.fence);
6959
7063
  await ctx.stage("author", {
6960
7064
  done: 0,
6961
7065
  total: 1
@@ -6975,7 +7079,7 @@ async function runResumeCommand(options) {
6975
7079
  }
6976
7080
  };
6977
7081
  }
6978
- const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true);
7082
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true, options.fence);
6979
7083
  return {
6980
7084
  stopReason: record.merged ? "completed" : "failed",
6981
7085
  scope: {
@@ -9229,7 +9333,7 @@ async function hostOnlyFailure(verb, capabilities) {
9229
9333
  * same-target run would collide with (the pair itself is guarded by run-meta).
9230
9334
  */
9231
9335
  function busyTarget(rows, target, excludeRowId) {
9232
- const row = rows.find((item) => item.id !== excludeRowId && (item.state === "approved" || item.state === "fuse") && item.decision?.target === target);
9336
+ const row = rows.find((item) => item.id !== excludeRowId && (item.state === "approved" || item.state === "fuse") && item.decision?.target === target && !(item.failure !== void 0 && item.failure.retryable !== true));
9233
9337
  return row === void 0 ? void 0 : {
9234
9338
  id: row.id,
9235
9339
  ...row.runId === void 0 ? {} : { runId: row.runId }
@@ -9249,8 +9353,8 @@ function forcedDecisionFor(a, b) {
9249
9353
  bSummary: "B: " + sorted[1] + " 的现有技能面。"
9250
9354
  };
9251
9355
  }
9252
- async function removeRuntimeRow(io, home, id, event, unitId) {
9253
- return await pairLedger(io, home).transact((tx) => {
9356
+ async function removeRuntimeRow(io, home, id, event, unitId, fence) {
9357
+ return await pairLedger(io, home, fence).transact((tx) => {
9254
9358
  const current = tx.rows().find((item) => item.id === id);
9255
9359
  if (current === void 0) return false;
9256
9360
  const unit = event === "committed" ? {
@@ -9272,6 +9376,7 @@ function unitFor(deps) {
9272
9376
  });
9273
9377
  }
9274
9378
  function createMergeRuntime(deps) {
9379
+ const ledger = () => pairLedger(deps.io, deps.home, deps.fence);
9275
9380
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
9276
9381
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
9277
9382
  const paramsOf = () => {
@@ -9288,6 +9393,7 @@ function createMergeRuntime(deps) {
9288
9393
  now: deps.now,
9289
9394
  ...deps.model === void 0 ? {} : { model: deps.model },
9290
9395
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9396
+ ...deps.fence === void 0 ? {} : { fence: deps.fence },
9291
9397
  ...timeout === void 0 ? {} : { timeoutMinutes: timeout }
9292
9398
  });
9293
9399
  const stage = deps.stage;
@@ -9336,7 +9442,7 @@ function createMergeRuntime(deps) {
9336
9442
  const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
9337
9443
  let cleared = false;
9338
9444
  if (existing?.state === "skipped" || existing?.state === "fuse" && existing.failure?.retryable === false) {
9339
- if (!await removeRuntimeRow(deps.io, deps.home, existing.id, "superseded")) return fail("pair", "row-changed", "清理可替换行时发现并发修改:" + existing.id, "运行 /evolution merge status 后重试");
9445
+ if (!await removeRuntimeRow(deps.io, deps.home, existing.id, "superseded", void 0, deps.fence)) return fail("pair", "row-changed", "清理可替换行时发现并发修改:" + existing.id, "运行 /evolution merge status 后重试");
9340
9446
  cleared = true;
9341
9447
  }
9342
9448
  if (existing?.state === "verify" && existing.runId !== void 0 && await deps.isOwnerActive?.(existing.runId) === true) return fail("pair", "merge-pair-in-flight", "同一 pair 已在运行 " + existing.runId, "等待该 run 结束,或运行 /evolution merge status " + existing.runId, existing.runId);
@@ -9511,7 +9617,7 @@ function createMergeRuntime(deps) {
9511
9617
  if (command.verb === "reject") {
9512
9618
  const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
9513
9619
  if (row === void 0) return fail("reject", "row-not-found", "找不到合并行 " + command.id, "运行 /evolution merge pending 查看待处理行", command.id);
9514
- if (!await pairLedger(deps.io, deps.home).transact((tx) => {
9620
+ if (!await ledger().transact((tx) => {
9515
9621
  const current = tx.rows().find((item) => item.id === row.id);
9516
9622
  if (current === void 0 || current.state !== "pending") return false;
9517
9623
  return tx.transition(row.id, {
@@ -9593,7 +9699,7 @@ function createMergeRuntime(deps) {
9593
9699
  const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
9594
9700
  if (row === void 0) return fail("discard", "row-not-found", "找不到合并行 " + command.id, "运行 /evolution merge status 查看当前队列", command.id);
9595
9701
  if (row.state !== "pending" && row.state !== "approved" && row.state !== "fuse") return fail("discard", "row-not-discardable", "该行状态是 " + row.state + ",不可丢弃:" + command.id, "只有 pending/approved/fuse 行可丢弃", row.runId);
9596
- if (!await removeRuntimeRow(deps.io, deps.home, row.id, "superseded")) return fail("discard", "row-changed", "丢弃时发现并发修改:" + command.id, "运行 /evolution merge status " + command.id + " 后重试");
9702
+ if (!await removeRuntimeRow(deps.io, deps.home, row.id, "superseded", void 0, deps.fence)) return fail("discard", "row-changed", "丢弃时发现并发修改:" + command.id, "运行 /evolution merge status " + command.id + " 后重试");
9597
9703
  return {
9598
9704
  ok: true,
9599
9705
  text: "已丢弃合并行:" + command.id
@@ -9612,6 +9718,7 @@ function createMergeRuntime(deps) {
9612
9718
  port: skillLibraryUndoPort(deps.skills),
9613
9719
  now: deps.now,
9614
9720
  target: command.target,
9721
+ ...deps.fence === void 0 ? {} : { fence: deps.fence },
9615
9722
  ...unit === void 0 ? {} : { finalizeUnit: (id) => unit.finalize.call(unit, id) }
9616
9723
  });
9617
9724
  return result.ok ? {
@@ -9622,7 +9729,7 @@ function createMergeRuntime(deps) {
9622
9729
  if (command.verb === "skipped" && command.restore !== void 0) {
9623
9730
  const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.restore);
9624
9731
  if (row === void 0 || row.state !== "skipped") return fail("skipped", "row-not-found", "找不到可恢复的 skipped 行:" + command.restore, "运行 /evolution merge skipped 查看跳过队列", command.restore);
9625
- if (!await removeRuntimeRow(deps.io, deps.home, row.id, "restored")) return fail("skipped", "row-changed", "恢复时发现并发修改:" + row.id, "运行 /evolution merge status 后重试");
9732
+ if (!await removeRuntimeRow(deps.io, deps.home, row.id, "restored", void 0, deps.fence)) return fail("skipped", "row-changed", "恢复时发现并发修改:" + row.id, "运行 /evolution merge status 后重试");
9626
9733
  return {
9627
9734
  ok: true,
9628
9735
  text: "已恢复参与筛选:" + row.id
@@ -9632,7 +9739,7 @@ function createMergeRuntime(deps) {
9632
9739
  if (command.clear !== void 0) {
9633
9740
  const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.clear);
9634
9741
  if (cleared === void 0) return fail("resume", "row-not-found", "找不到要清除的行:" + command.clear, "运行 /evolution merge status 查看当前队列");
9635
- if (!await removeRuntimeRow(deps.io, deps.home, cleared.id, "superseded")) return fail("resume", "row-changed", "清除时发现并发修改:" + cleared.id, "运行 /evolution merge status 后重试");
9742
+ if (!await removeRuntimeRow(deps.io, deps.home, cleared.id, "superseded", void 0, deps.fence)) return fail("resume", "row-changed", "清除时发现并发修改:" + cleared.id, "运行 /evolution merge status 后重试");
9636
9743
  return {
9637
9744
  ok: true,
9638
9745
  text: "已清除合并行:" + cleared.id
@@ -9753,12 +9860,12 @@ function createMergeRuntime(deps) {
9753
9860
  if (targetCardNow !== void 0 && (targetCardNow.protectionUnknown === true || targetCardNow.protectedBy != null)) return fail("approve", "target-protected", "目标技能「" + target + "」在决策后变为受保护,拒绝提交", "解除保护或改选其它目标", candidate.runId);
9754
9861
  const missingSource = [candidate.a, candidate.b].find((name) => !cards.some((card) => card.name === name && card.managed === true));
9755
9862
  if (missingSource !== void 0) {
9756
- if (!await removeRuntimeRow(deps.io, deps.home, candidate.id, "superseded")) return fail("approve", "row-changed", "source-gone,但行已被并发修改,未清理:" + candidate.id, "运行 /evolution merge status " + candidate.id + " 后重试");
9863
+ if (!await removeRuntimeRow(deps.io, deps.home, candidate.id, "superseded", void 0, deps.fence)) return fail("approve", "row-changed", "source-gone,但行已被并发修改,未清理:" + candidate.id, "运行 /evolution merge status " + candidate.id + " 后重试");
9757
9864
  return fail("approve", "source-gone", missingSource + " 已不在 family-managed 技能库,已从 active 队列清除", "重新 pair 该技能对;剩余行用 /evolution merge discard " + candidate.id, candidate.runId);
9758
9865
  }
9759
9866
  const sourceExpected = candidate.decision?.sourceRevisionRef ?? candidate.sourceRevisionRef;
9760
9867
  if (revisionMismatch(candidate.decision?.targetRevisionRef ?? candidate.targetRevisionRef, targetRevisionRefOf(cards, candidate.decision.target)) !== null) {
9761
- await pairLedger(deps.io, deps.home).transact((tx) => {
9868
+ await ledger().transact((tx) => {
9762
9869
  const current = tx.rows().find((item) => item.id === candidate.id);
9763
9870
  return current === void 0 ? false : tx.transition(candidate.id, {
9764
9871
  type: "expired",
@@ -9772,7 +9879,7 @@ function createMergeRuntime(deps) {
9772
9879
  candidate.a,
9773
9880
  candidate.b
9774
9881
  ])) {
9775
- await pairLedger(deps.io, deps.home).transact((tx) => {
9882
+ await ledger().transact((tx) => {
9776
9883
  const current = tx.rows().find((item) => item.id === candidate.id);
9777
9884
  return current === void 0 ? false : tx.transition(candidate.id, {
9778
9885
  type: "expired",
@@ -9782,7 +9889,7 @@ function createMergeRuntime(deps) {
9782
9889
  return fail("approve", "source-changed", "源技能内容在决策后变化,拒绝照跑", "重新 pair,或 /evolution merge discard " + candidate.id, candidate.runId);
9783
9890
  }
9784
9891
  if (revisionMismatch(sourceExpected, sourceRevisionRefOf(cards, candidate.a, candidate.b)) !== null) {
9785
- await pairLedger(deps.io, deps.home).transact((tx) => {
9892
+ await ledger().transact((tx) => {
9786
9893
  const current = tx.rows().find((item) => item.id === candidate.id);
9787
9894
  return current === void 0 ? false : tx.transition(candidate.id, {
9788
9895
  type: "expired",
@@ -9793,7 +9900,7 @@ function createMergeRuntime(deps) {
9793
9900
  }
9794
9901
  const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
9795
9902
  if (candidate.sourceRevision !== void 0 && currentRevision !== void 0 && currentRevision !== candidate.sourceRevision) {
9796
- if (!await pairLedger(deps.io, deps.home).transact((tx) => {
9903
+ if (!await ledger().transact((tx) => {
9797
9904
  const current = tx.rows().find((item) => item.id === candidate.id);
9798
9905
  return current === void 0 ? false : tx.transition(candidate.id, {
9799
9906
  type: "expired",
@@ -9804,7 +9911,7 @@ function createMergeRuntime(deps) {
9804
9911
  }
9805
9912
  const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
9806
9913
  if (candidate.targetRevision !== void 0 && currentTargetRevision !== void 0 && currentTargetRevision !== candidate.targetRevision) {
9807
- if (!await pairLedger(deps.io, deps.home).transact((tx) => {
9914
+ if (!await ledger().transact((tx) => {
9808
9915
  const current = tx.rows().find((item) => item.id === candidate.id);
9809
9916
  return current === void 0 ? false : tx.transition(candidate.id, {
9810
9917
  type: "expired",
@@ -9820,7 +9927,7 @@ function createMergeRuntime(deps) {
9820
9927
  if (await deps.isBlocked?.(candidate.decision.target) === true) return fail("approve", "target-blocked", "target 被范围门拦下:" + candidate.decision.target, "运行 /evolution scope 查看范围门", candidate.runId);
9821
9928
  }
9822
9929
  if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
9823
- if (!await pairLedger(deps.io, deps.home).transact((tx) => {
9930
+ if (!await ledger().transact((tx) => {
9824
9931
  const current = tx.rows().find((item) => item.id === candidate.id);
9825
9932
  return current === void 0 ? false : tx.transition(candidate.id, {
9826
9933
  type: "expired",
@@ -10487,6 +10594,7 @@ async function runPairCommand(options) {
10487
10594
  io: options.io,
10488
10595
  home: options.home,
10489
10596
  registry: options.registry,
10597
+ ...options.fence === void 0 ? {} : { fence: options.fence },
10490
10598
  approval: options.approval,
10491
10599
  platformWrite: options.platformWrite,
10492
10600
  now: options.now,
@@ -10614,7 +10722,7 @@ function apply(ctx, rawConfig = {}) {
10614
10722
  quarantine: [],
10615
10723
  report: null
10616
10724
  };
10617
- const state = await ensureMergeStateReady(io, home);
10725
+ const state = await ensureMergeStateReady(io, home, ctx.get("evolutionMutationAuthority")?.homeFence);
10618
10726
  if (!state.ok) return v4.code === "merge-state-forward-only" || v4.code === "merge-state-needs-repair" ? {
10619
10727
  ok: false,
10620
10728
  code: v4.code,
@@ -10631,12 +10739,14 @@ function apply(ctx, rawConfig = {}) {
10631
10739
  code: "merge-state-migration-blocked",
10632
10740
  message: error instanceof Error ? error.message : String(error)
10633
10741
  }));
10742
+ const homeFence = ctx.get("evolutionMutationAuthority")?.homeFence;
10634
10743
  const runs = newRunRegistry({
10635
10744
  io,
10636
10745
  home,
10637
10746
  warn: (message) => {
10638
10747
  ctx.logger.warn(message);
10639
- }
10748
+ },
10749
+ ...homeFence === void 0 ? {} : { fence: homeFence }
10640
10750
  });
10641
10751
  ctx.effect(() => {
10642
10752
  const host = ctx;
@@ -10655,11 +10765,13 @@ function apply(ctx, rawConfig = {}) {
10655
10765
  limitsSnapshot,
10656
10766
  platformWrite: getPlatformWrite()
10657
10767
  });
10768
+ const mutationAuthority = ctx.get("evolutionMutationAuthority");
10658
10769
  const skills = newSkillLibrary({
10659
10770
  io,
10660
10771
  ctx,
10661
10772
  authority: authorityFrom({ origin: "subagent" }),
10662
- limits: limitsSnapshot.skillLimits
10773
+ limits: limitsSnapshot.skillLimits,
10774
+ ...mutationAuthority?.mutationLock === void 0 ? {} : { mutationLock: mutationAuthority.mutationLock }
10663
10775
  });
10664
10776
  ready.then((value) => {
10665
10777
  if (!value.ok) {
@@ -10671,18 +10783,22 @@ function apply(ctx, rawConfig = {}) {
10671
10783
  home,
10672
10784
  backend: skillLibraryUnitBackend(skills),
10673
10785
  claim: targetClaimUnitPort(io, home, () => Date.now()),
10674
- now: () => Date.now()
10786
+ now: () => Date.now(),
10787
+ isActive: async (runId) => await runs.hasOwner(runId),
10788
+ ...mutationAuthority?.homeFence === void 0 ? {} : { fence: mutationAuthority.homeFence }
10675
10789
  });
10676
10790
  const cards = async () => await readSkillCards(skills);
10677
10791
  (async () => {
10678
10792
  const loaded = await runs.load();
10679
10793
  if (!loaded.ok) ctx.logger.warn("merge run index: " + (loaded.note ?? "unreadable"));
10794
+ const activeRuns = /* @__PURE__ */ new Set();
10795
+ for (const record of runs.runs().filter((record) => record.state === "running")) if (await runs.hasOwner(record.id)) activeRuns.add(record.id);
10680
10796
  const reports = await recoverCommitUnits({
10681
10797
  io,
10682
10798
  home,
10683
10799
  unit,
10684
10800
  now: () => Date.now(),
10685
- isActive: () => false,
10801
+ isActive: (runId) => activeRuns.has(runId),
10686
10802
  runs
10687
10803
  });
10688
10804
  if (reports.length > 0) ctx.logger.info("merge unit recovery: " + reports.map((report) => report.runId + "/" + report.unitId + "=" + report.action).join(", "));
@@ -10888,6 +11004,7 @@ function apply(ctx, rawConfig = {}) {
10888
11004
  now: () => Date.now(),
10889
11005
  getParams,
10890
11006
  getPlatformWrite,
11007
+ ...mutationAuthority?.homeFence === void 0 ? {} : { fence: mutationAuthority.homeFence },
10891
11008
  ...defaultModel === void 0 ? {} : { model: defaultModel.currentSelection().model },
10892
11009
  channel,
10893
11010
  parent: void 0,
@@ -10952,4 +11069,4 @@ function apply(ctx, rawConfig = {}) {
10952
11069
  });
10953
11070
  }
10954
11071
  //#endregion
10955
- export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_LIFECYCLE_VERBS, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStageForCards, createStaging, dropRow, dropRowV3, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromRoot, ensureStaging, forcedDecisionFor, formatFacts, formatQueue, formatRunDetail, formatRunsView, formatSnapshotQueue, formatSnapshotStatus, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, liveRowFactsOf, loadMergeState, loadStateSchema, makeMergeRoutes, manifestFilesOf, mergeCommandHelp, mergeDoctor, mergeDoctorAvailability, mergeDoctorFact, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, missingManifestFile, moveRow, mutateMergeState, pairKey, parseAuthoringManifest, parseDecision, parseMergeCommand, pendingExpiry, persistedExpiry, promptHashOf, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, retryMergeReport, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, sourceBlock, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, transactRows, transitionLegacyForProjection, undoMergeReceipt, unitFor, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
11072
+ export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_LIFECYCLE_VERBS, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, busyTarget, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStageForCards, createStaging, dropRow, dropRowV3, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromRoot, ensureStaging, forcedDecisionFor, formatFacts, formatQueue, formatRunDetail, formatRunsView, formatSnapshotQueue, formatSnapshotStatus, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, liveRowFactsOf, loadMergeState, loadStateSchema, makeMergeRoutes, manifestFilesOf, mergeCommandHelp, mergeDoctor, mergeDoctorAvailability, mergeDoctorFact, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, missingManifestFile, moveRow, mutateMergeState, pairKey, parseAuthoringManifest, parseDecision, parseMergeCommand, pendingExpiry, persistedExpiry, promptHashOf, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, retryMergeReport, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, sourceBlock, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, transactRows, transitionLegacyForProjection, undoMergeReceipt, unitFor, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };