@lmzhen/dsh-evolution-core 0.21.0 → 0.23.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,9 +1,9 @@
1
1
  import { parse } from "yaml";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
- import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
4
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
6
  import { homedir } from "node:os";
6
- import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { scopeOf } from "@deepseek-ai/dsh-scope";
9
9
  import { z } from "zod";
@@ -1157,6 +1157,34 @@ function bodyCost(content) {
1157
1157
  };
1158
1158
  }
1159
1159
  //#endregion
1160
+ //#region lib/types/guards.js
1161
+ /**
1162
+ * The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
1163
+ *
1164
+ * The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
1165
+ * times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
1166
+ * copies in evolution-policy and evolution-threat. This module is the single definition; callers
1167
+ * import it instead of spelling the clause again.
1168
+ *
1169
+ * @module @lmzhen/dsh-evolution-core
1170
+ */
1171
+ /**
1172
+ * Is this a non-null, non-array object (what JSON calls an object)?
1173
+ * @param value - the value to test.
1174
+ * @returns whether it is a plain object.
1175
+ */
1176
+ function isRecord(value) {
1177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1178
+ }
1179
+ /**
1180
+ * Read a value as a record, or undefined when it is not one.
1181
+ * @param value - the value to read.
1182
+ * @returns the value typed as a record, or undefined.
1183
+ */
1184
+ function asRecord(value) {
1185
+ return isRecord(value) ? value : void 0;
1186
+ }
1187
+ //#endregion
1160
1188
  //#region lib/types/probe.js
1161
1189
  function probePresent(value) {
1162
1190
  return {
@@ -1356,37 +1384,6 @@ function evolutionIoAdapter(provider) {
1356
1384
  }
1357
1385
  };
1358
1386
  }
1359
- /**
1360
- * F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
1361
- * to the same file proactively recycles our own leftover lock — the holder is
1362
- * us, so a leftover is stale by definition. Module-level by design: it must
1363
- * survive across `nodeEvolutionIo()` instances for the self-heal to be
1364
- * effective. (Not a pure function — the cross-call state is the intent.)
1365
- * V4-05: this set — not an mtime heuristic — is the ONLY signal that a
1366
- * same-pid lock is a leftover rather than a live in-process task, so it is the
1367
- * sole gate for the self-pid recycle branch. Exported (read-only in practice)
1368
- * so `io.spec.ts` can drive the self-heal path deterministically.
1369
- */
1370
- const pendingSelfCleanup = /* @__PURE__ */ new Map();
1371
- /** P2-7 (v11)/P3-10 (v14): record a failed release so the next write to this
1372
- * path self-heals it, keeping the map's 64-entry cap — a never-re-touched path
1373
- * must not pin it, while a path whose lock still EXISTS is the last to drop
1374
- * (dropping it disables the self-heal for a lock that is really there). */
1375
- const recordPendingSelfCleanup = async (lock, token) => {
1376
- if (pendingSelfCleanup.size >= 64) {
1377
- let droppable;
1378
- for (const candidate of pendingSelfCleanup.keys()) {
1379
- if (candidate === lock) continue;
1380
- if (await readFile(candidate, "utf8").then(() => false, () => true)) {
1381
- droppable = candidate;
1382
- break;
1383
- }
1384
- }
1385
- const victim = droppable ?? pendingSelfCleanup.keys().next().value;
1386
- if (victim !== void 0) pendingSelfCleanup.delete(victim);
1387
- }
1388
- pendingSelfCleanup.set(lock, token);
1389
- };
1390
1387
  const RENAME_RETRY_BASE_MS = 50;
1391
1388
  const RENAME_RETRY_MAX_DELAY_MS = 800;
1392
1389
  const RENAME_RETRY_MAX_ATTEMPTS = 6;
@@ -1514,73 +1511,14 @@ function isProcessAlive(pid) {
1514
1511
  return error?.code === "EPERM";
1515
1512
  }
1516
1513
  }
1517
- /** F-17 (v18): the write-lock protocol is a cross-module contract — the lock
1518
- * file is `<target>.lock` and its body is `<pid>:<token>`. This module creates
1519
- * them (`withWriteLock`) and `skill-store`'s probes/sweepers parse them, so both
1520
- * consume these two constants instead of repeating the literals. */
1521
- const LOCK_SUFFIX = ".lock";
1522
- /** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
1523
- * torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
1524
- * the pid part is intact, which is what the takeover probe needs. The capture
1525
- * group feeds `parseLockBody` (v20 A-1: skill-store's sweepers consume this
1526
- * helper instead of re-inlining a third regex copy — F-17 single-source). */
1527
- const LOCK_BODY_RE = /^(\d+):[0-9a-f]*$/;
1528
- /** Parse a writer-lock body into its holder pid. `null` when the body does
1529
- * not have the `pid:token` shape at all (e.g. a user support file named
1530
- * `*.lock`) — callers leave such files alone. A shape-matching body always
1531
- * yields a number, possibly `0` (state corruption / hand edit): there is no
1532
- * pid 0, so a `0` holder must be treated as DEAD by the caller —
1533
- * `isProcessAlive(0)` signals the caller's own process group on POSIX and
1534
- * answers true, so probe sites must guard `holder > 0` before probing. */
1535
- function parseLockBody(body) {
1536
- const match = LOCK_BODY_RE.exec(body.trim());
1537
- return match === null ? null : Number(match[1]);
1538
- }
1539
- /**
1540
- * V27 G0.2 (EVO-IO-01): the write lock named by this claim is no longer ours
1541
- * at the commit point — a takeover reclaimed it while we were inside the
1542
- * critical section. The lock layer converts this into a retry of the whole
1543
- * read-modify-write; it must never reach a caller as a successful write.
1544
- */
1545
- var LostWriteLock = class extends Error {
1546
- constructor() {
1547
- super("evolution-io: the write lock was reclaimed before the commit");
1548
- this.name = "LostWriteLock";
1549
- }
1550
- };
1514
+ /** How long an abandoned quarantine copy is kept for operator rescue. G2 S4: hoisted to module scope
1515
+ * (it used to live inside `nodeEvolutionIo`, in the region the lock protocol occupied). */
1516
+ const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
1551
1517
  /** V27 G1.4: every quarantine-copy name this family has ever produced —
1552
1518
  * `<file>.corrupt` (current fixed name), `<file>.corrupt.<epoch>`, and the
1553
1519
  * v10-05 legacy `<file>.corrupt-<epoch>-<rand>` series. A user support file
1554
1520
  * keeps a final extension (`.corrupt-backup.md`) and therefore stays. */
1555
1521
  const CORRUPT_COPY_RE = /\.corrupt(\.\d+|-\d+-[0-9a-z]+)?$/;
1556
- /** V27 G1.1: the three takeover windows, as protocol constants at ONE place
1557
- * (the inline copies that used to live in the acquisition loop, plus the dead
1558
- * branch's bare `1000`, are gone). */
1559
- /** A named holder that is gone, past this age, is reclaimed. Fits inside the
1560
- * `lockAttempts * 50ms` retry budget so a dead holder's lock is reachable
1561
- * within one budget (v19 gate arithmetic: budget >= 2 x threshold). */
1562
- const DEAD_LOCK_TAKEOVER_MS = 1e3;
1563
- /** No body at all: nothing attributes the lock to a holder, so this is the one
1564
- * branch that cannot probe liveness — it must outlast any plausible stall
1565
- * between create and body write (V27 G0.2: 1s was below what a loaded machine
1566
- * actually took, which let a peer delete a live holder's lock). */
1567
- const EMPTY_LOCK_TAKEOVER_MS = 3e4;
1568
- /** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
1569
- * and far below "forever". */
1570
- const LOCK_TEAR_TAKEOVER_MS = 36e5;
1571
- /** A2 (audit P1-2): even a lock whose holder pid probes ALIVE is reclaimable
1572
- * past this age. Liveness-by-pid cannot distinguish the original holder from
1573
- * an unrelated process the OS later assigned the same pid, so the plain alive
1574
- * probe let one recycled pid brick every writer of one state file forever.
1575
- * No write in this family holds a lock for more than minutes (the longest is
1576
- * the ~120s review window), so a day-old "alive" lock is a recycled pid, not
1577
- * a live writer. */
1578
- const ALIVE_LOCK_TAKEOVER_MS = 864e5;
1579
- /** P2-27 (v37): the commit-point ownership re-read. A transient read failure
1580
- * (EACCES/EMFILE/antivirus hold) must not abort a valid RMW, so the read is
1581
- * retried in place; only a still-unreadable lock fails the attempt. */
1582
- const OWNERSHIP_READ_ATTEMPTS = 3;
1583
- const OWNERSHIP_READ_RETRY_MS = 20;
1584
1522
  /**
1585
1523
  * V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
1586
1524
  * directory fsync failed — the bytes ARE visible, only their durability is
@@ -1593,39 +1531,12 @@ function isCommittedWarning(error) {
1593
1531
  return error?.committed === true;
1594
1532
  }
1595
1533
  /**
1596
- * V27 G1.1: the lock-takeover decision as ONE pure function, so the protocol is
1597
- * testable and exhaustive instead of being an inline expression inside the
1598
- * acquisition loop:
1599
- * - `none` the lock is fresh, or its holder is alive within the alive
1600
- * window → wait, never steal;
1601
- * - `dead` a named holder that is gone past the dead threshold, OR whose
1602
- * pid still probes alive but whose lock is older than the alive
1603
- * window (A2: a recycled pid must not hold the file forever);
1604
- * - `empty` no body at all: nothing attributes it to a holder, so only the
1605
- * wide `emptyAfterMs` window may reclaim it;
1606
- * - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
1607
- * tear threshold.
1608
- * The age thresholds compare against `nowMs - mtimeMs` with `>` so a lock whose
1609
- * age EQUALS the threshold is not yet reclaimed (the boundary the v19 gate fix
1610
- * pinned).
1611
- */
1612
- function decideTakeover(probe) {
1613
- const age = (probe.nowMs ?? Date.now()) - probe.mtimeMs;
1614
- const holder = Number(probe.body.split(":")[0] ?? "");
1615
- const namedHolder = Number.isInteger(holder) && holder > 0;
1616
- if (probe.body === "") return age > (probe.emptyAfterMs ?? 3e4) ? "empty" : "none";
1617
- if (!namedHolder) return age > (probe.corruptAfterMs ?? 36e5) ? "corrupt" : "none";
1618
- if (probe.alive(holder)) return age > (probe.aliveAfterMs ?? 864e5) ? "dead" : "none";
1619
- return age > (probe.deadAfterMs ?? 1e3) ? "dead" : "none";
1620
- }
1621
- /**
1622
- * Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
1623
- * (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
1624
- * while contention TESTS on a loaded runner may raise it (e.g. 240 ≈ 12s) —
1625
- * V10-06 integration: full-suite parallel load made 32-way takeover bursts
1626
- * exceed the default budget and fail loud.
1627
- */
1628
- function nodeEvolutionIo(lockAttempts = 40) {
1534
+ * Build the Node IO backend: plain fs promises, durable tmp + rename commits (crash atomicity) and
1535
+ * ONE concurrency guarantee of its own — writes to the same path are serialized in-process (see
1536
+ * `inProcessWrite`). G2 S4 removed the cross-process write lock and its takeover protocol; the
1537
+ * plugin path gets cross-process safety from the platform provider (`ctx.fs`).
1538
+ */
1539
+ function nodeEvolutionIo() {
1629
1540
  /**
1630
1541
  * v43 S1-4: delegates to the canonical `isMissingPath` (probe.ts) — one
1631
1542
  * definition for the whole family instead of a second copy of the same two
@@ -1640,201 +1551,34 @@ function nodeEvolutionIo(lockAttempts = 40) {
1640
1551
  /** True when the pid is alive (single source: `isProcessAlive`). */
1641
1552
  const isAlive = isProcessAlive;
1642
1553
  /**
1643
- * V10-06 (P1-1 integration fix): a takeover TICKET must never be reclaimed
1644
- * while its (live) holder can still be committing — the C-28 rename retry
1645
- * budget is ~2.35s under AV/indexer pressure, so the former 1s ticket age
1646
- * threshold let a contender steal an in-flight ticket, double-enter the
1647
- * critical section and drop one RMW (observed as 5/6 increments under
1648
- * full-suite load). 5s = retry budget + comfortable margin; a DEAD holder's
1649
- * ticket is still reclaimed immediately via the pid probe.
1650
- */
1651
- const TICKET_STALE_MS = 5e3;
1652
- /**
1653
- * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
1654
- * guards the atomic write. A >1s-old lock is taken over ONLY after probing
1655
- * the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
1656
- * slow writer no longer loses its lock to a peer at the threshold (the
1657
- * takeover is the only best-effort surface; the retry budget fails loud —
1658
- * rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
1659
- * rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
1660
- * attempts (500ms), and a fail-loud throw was observed instead of a clean
1661
- * serialization.
1662
- * 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
1663
- * task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
1664
- * lock contention, retried up to 40x and finally reported as
1665
- * "could not acquire write lock" while the real cause was hidden.
1666
- * 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
1667
- * retry budget (budget >= 2 x threshold), so a dead holder's lock is
1668
- * actually recoverable within one budget instead of being arithmetically
1669
- * unreachable.
1670
- * 0.3.21 (F-101), V4-04: takeover re-reads the lock right before acting and
1671
- * only proceeds when the content still names the dead pid — a peer that
1672
- * acquired the lock after our stale probe wrote its own pid, and deleting a
1673
- * LIVE lock is the double-hold (concurrent task) the probe must prevent. The
1674
- * re-read is necessary but not sufficient: two peers can both pass it, so the
1675
- * commit itself is an atomic rename to a unique name (only one peer's rename
1676
- * can succeed; the loser's source is gone). A naive rm lets the later peer
1677
- * delete the winner's freshly re-acquired live lock.
1678
- * 0.3.21 (F-367), V4-05: a self-pid lock is recycled ONLY when the failed
1679
- * release was recorded in pendingSelfCleanup — never on age alone, because an
1680
- * mtime over 1s is indistinguishable from a long task still executing in this
1681
- * process, and recycling that live lock would double-hold it. A failure to
1682
- * release in finally is recorded so the next write self-heals.
1683
- * V10-07 (P2-1): a third takeover shape — a TORN body (non-empty, no
1684
- * parseable pid) — is taken over after a wide 1h threshold with a
1685
- * console.warn; before this branch such a lock blocked every future writer
1686
- * forever.
1687
- * V27 G0.2 (EVO-IO-01): the CONTRACT for `task` is therefore: commit only
1688
- * after calling the `assertOwned` callback it receives, immediately before the
1689
- * rename (or delete) that publishes the result. A takeover may reclaim a lock
1690
- * whose holder is alive — an empty body cannot be attributed to a pid, and a
1691
- * peer can read a stale stat — and the holder cannot see it from the handle it
1692
- * opened (on POSIX its write lands on the UNLINKED inode). With the guard, a
1693
- * reclaimed claim aborts the RMW (`LostWriteLock`, converted into a retry
1694
- * here) instead of overwriting the current holder's result; without it, two
1695
- * writers commit and one update is silently lost.
1554
+ * G2 S4: the family write-lock protocol is retired. Concurrency is the PLATFORM’s job now — the
1555
+ * plugin path requires `ctx.fs`, whose writes are versioned — so this backend keeps exactly one
1556
+ * guarantee of its own: writes to the SAME path inside THIS process are serialized (a promise
1557
+ * chain per path: no timers, no lock files, no takeover windows). Cross-process RMW safety comes
1558
+ * from the provider; a standalone caller sharing a directory across processes must not rely on
1559
+ * this backend alone. The durable tmp + rename commit stays — crash atomicity and mutual
1560
+ * exclusion are different concerns.
1696
1561
  */
1697
- const withWriteLock = async (path, task) => {
1698
- const lock = `${path}${LOCK_SUFFIX}`;
1699
- let myClaim = "";
1700
- for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
1701
- try {
1702
- myClaim = `${process.pid}:${randomBytes(4).toString("hex")}`;
1703
- await writeFile(lock, myClaim, { flag: "wx" });
1704
- } catch (error) {
1705
- const code = error?.code;
1706
- if (code === "EEXIST" || code === "EPERM") {} else {
1707
- await rm(lock, { force: true }).catch(() => {});
1708
- throw error;
1709
- }
1710
- try {
1711
- const st = await stat(lock);
1712
- const holderRead = await readFile(lock, "utf8").then((body) => ({
1713
- ok: true,
1714
- body
1715
- }), () => ({
1716
- ok: false,
1717
- body: ""
1718
- }));
1719
- if (!holderRead.ok) continue;
1720
- const holderContent = holderRead.body;
1721
- const holder = Number(holderContent.split(":")[0] ?? "");
1722
- if (holder === process.pid && pendingSelfCleanup.has(lock)) {
1723
- const token = pendingSelfCleanup.get(lock);
1724
- if (await readFile(lock, "utf8").catch(() => "") === token) try {
1725
- await rm(lock, { force: true });
1726
- pendingSelfCleanup.delete(lock);
1727
- } catch {}
1728
- continue;
1729
- }
1730
- const decision = decideTakeover({
1731
- body: holderContent,
1732
- mtimeMs: st.mtimeMs,
1733
- alive: isAlive
1734
- });
1735
- if (decision !== "none") {
1736
- console.warn(`evolution-io: taking over write lock ${lock} (branch=stale${decision[0]?.toUpperCase()}${decision.slice(1)}, body=${JSON.stringify(holderContent)}, ageMs=${Date.now() - st.mtimeMs}, holderPid=${Number.isInteger(holder) && holder > 0 ? holder : "none"})`);
1737
- const currentRead = await readFile(lock, "utf8").then((body) => ({
1738
- ok: true,
1739
- body
1740
- }), () => ({
1741
- ok: false,
1742
- body: ""
1743
- }));
1744
- if (!currentRead.ok) continue;
1745
- if (currentRead.body === holderContent) {
1746
- const ticket = `${lock}.next`;
1747
- try {
1748
- const ticketBody = await readFile(ticket, "utf8").catch(() => "");
1749
- const ticketMtime = await stat(ticket).then((s) => s.mtimeMs, () => 0);
1750
- const ticketHolder = Number(ticketBody.split(":")[0] ?? "");
1751
- if ((!Number.isInteger(ticketHolder) || ticketHolder <= 0 || !isAlive(ticketHolder) || Date.now() - ticketMtime > TICKET_STALE_MS) && (ticketBody !== "" || Date.now() - ticketMtime > TICKET_STALE_MS)) await rm(ticket, { force: true }).catch(() => {});
1752
- } catch {}
1753
- try {
1754
- await writeFile(ticket, `${process.pid}:${randomBytes(4).toString("hex")}`, { flag: "wx" });
1755
- } catch {
1756
- continue;
1757
- }
1758
- const verifyRead = await readFile(lock, "utf8").then((body) => ({
1759
- ok: true,
1760
- body
1761
- }), () => ({
1762
- ok: false,
1763
- body: ""
1764
- }));
1765
- if (!verifyRead.ok) {
1766
- await rm(ticket, { force: true }).catch(() => {});
1767
- continue;
1768
- }
1769
- const verify = verifyRead.body;
1770
- if (verify === holderContent) {
1771
- const verifyHolder = Number(verify.split(":")[0] ?? "");
1772
- if (!(Number.isInteger(verifyHolder) && verifyHolder > 0 && isAlive(verifyHolder))) await rm(lock, { force: true }).catch(() => {});
1773
- }
1774
- await rm(ticket, { force: true }).catch(() => {});
1775
- continue;
1776
- }
1777
- }
1778
- } catch {
1779
- continue;
1780
- }
1781
- await new Promise((resolve) => setTimeout(resolve, 50));
1782
- continue;
1783
- }
1784
- const assertOwned = async () => {
1785
- let body = "";
1786
- let readable = false;
1787
- for (let read = 0; read < OWNERSHIP_READ_ATTEMPTS && !readable; read += 1) {
1788
- if (read > 0) await new Promise((resolve) => setTimeout(resolve, OWNERSHIP_READ_RETRY_MS));
1789
- const attempt = await readFile(lock, "utf8").then((value) => ({
1790
- ok: true,
1791
- value
1792
- }), () => ({
1793
- ok: false,
1794
- value: ""
1795
- }));
1796
- if (attempt.ok) {
1797
- body = attempt.value;
1798
- readable = true;
1799
- }
1800
- }
1801
- if (!readable) {
1802
- console.warn(`evolution-io: write lock ${lock} could not be read at the commit point (ownership unverified, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
1803
- throw new LostWriteLock();
1804
- }
1805
- if (body !== myClaim) {
1806
- console.warn(`evolution-io: write lock ${lock} was reclaimed by another writer before the commit (on disk now: ${JSON.stringify(body)}, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
1807
- throw new LostWriteLock();
1808
- }
1809
- };
1810
- try {
1811
- return await task(assertOwned);
1812
- } catch (error) {
1813
- if (error instanceof LostWriteLock) continue;
1814
- throw error;
1815
- } finally {
1816
- const mine = await readFile(lock, "utf8").then((body) => ({
1817
- ok: true,
1818
- body
1819
- }), (error) => ({
1820
- ok: false,
1821
- missing: isMissing(error)
1822
- }));
1823
- if (!mine.ok) {
1824
- if (!mine.missing) await recordPendingSelfCleanup(lock, myClaim);
1825
- } else if (mine.body === myClaim) await rm(lock, {
1826
- force: true,
1827
- maxRetries: 20,
1828
- retryDelay: 100
1829
- }).catch(() => recordPendingSelfCleanup(lock, myClaim));
1830
- }
1562
+ const queues = /* @__PURE__ */ new Map();
1563
+ const inProcessWrite = async (path, run) => {
1564
+ const result = (queues.get(path) ?? Promise.resolve()).then(run, run);
1565
+ const settled = result.then(() => {}, () => {});
1566
+ queues.set(path, settled);
1567
+ try {
1568
+ return await result;
1569
+ } finally {
1570
+ if (queues.get(path) === settled) queues.delete(path);
1831
1571
  }
1832
- throw new Error(`could not acquire write lock for ${path} after ${lockAttempts} attempts`);
1833
1572
  };
1834
- /** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
1835
- * `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
1836
- * Lazy: only the directory a write is about to touch gets swept, once per
1837
- * write, inside the write lock. */
1573
+ /**
1574
+ * Sweep transient files a crashed writer left behind, lazily: once per write, for the directory
1575
+ * that write is about to touch.
1576
+ * `<target>.<pid>.<rand>.tmp` — the durable-write tmp. This process's own leftover is recycled
1577
+ * immediately (a live writer's tmp is a current write, not a crash artifact); a foreign tmp
1578
+ * needs an age past an hour AND a dead holder.
1579
+ * `.corrupt*` — quarantine copies past their rescue window.
1580
+ * G2 S4: the lock and takeover-ticket branches are gone with the write-lock protocol.
1581
+ */
1838
1582
  const sweepStaleTmps = async (path) => {
1839
1583
  const dir = dirname(path);
1840
1584
  const base = basename(path);
@@ -1844,12 +1588,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
1844
1588
  } catch {
1845
1589
  return;
1846
1590
  }
1847
- const prefix = `${base}.`;
1848
- const lockName = `${base}${LOCK_SUFFIX}`;
1849
- const ticketName = `${lockName}.next`;
1850
- const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
1851
1591
  for (const name of entries) {
1852
- if (!name.startsWith(prefix) || name === lockName) continue;
1853
1592
  const tmpMatch = /^(.*)\.(\d+)\.([0-9a-f]+)\.tmp$/.exec(name);
1854
1593
  if (tmpMatch !== null && tmpMatch[1] === base) {
1855
1594
  const tmpPath = join(dir, name);
@@ -1861,19 +1600,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
1861
1600
  } catch {}
1862
1601
  continue;
1863
1602
  }
1864
- if (name === ticketName) {
1865
- const ticketPath = join(dir, name);
1866
- try {
1867
- const body = await readFile(ticketPath, "utf8").catch(() => "");
1868
- const holder = Number(body.split(":")[0] ?? "");
1869
- const st = await stat(ticketPath);
1870
- const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
1871
- const old = Date.now() - st.mtimeMs > TICKET_STALE_MS;
1872
- if (dead || old) await rm(ticketPath, { force: true });
1873
- } catch {}
1874
- continue;
1875
- }
1876
- if (CORRUPT_COPY_RE.test(name)) {
1603
+ if (name.startsWith(base + ".") && CORRUPT_COPY_RE.test(name)) {
1877
1604
  const corruptPath = join(dir, name);
1878
1605
  try {
1879
1606
  const st = await stat(corruptPath);
@@ -1893,16 +1620,14 @@ function nodeEvolutionIo(lockAttempts = 40) {
1893
1620
  },
1894
1621
  async writeText(path, content) {
1895
1622
  await mkdir(dirname(path), { recursive: true });
1896
- await withWriteLock(path, async (assertOwned) => {
1623
+ await inProcessWrite(path, async () => {
1897
1624
  await sweepStaleTmps(path);
1898
- const tmp = await writeDurableTmp(path, content);
1899
- await assertOwned();
1900
- await commitTmp(tmp, path);
1625
+ await commitTmp(await writeDurableTmp(path, content), path);
1901
1626
  });
1902
1627
  },
1903
1628
  async transact(path, task) {
1904
1629
  await mkdir(dirname(path), { recursive: true });
1905
- await withWriteLock(path, async (assertOwned) => {
1630
+ await inProcessWrite(path, async () => {
1906
1631
  await sweepStaleTmps(path);
1907
1632
  let current;
1908
1633
  try {
@@ -1913,14 +1638,11 @@ function nodeEvolutionIo(lockAttempts = 40) {
1913
1638
  }
1914
1639
  const next = await task(current);
1915
1640
  if (next === null) {
1916
- await assertOwned();
1917
1641
  await rm(path, { force: true });
1918
1642
  return;
1919
1643
  }
1920
1644
  if (next === current) return;
1921
- const tmp = await writeDurableTmp(path, next);
1922
- await assertOwned();
1923
- await commitTmp(tmp, path);
1645
+ await commitTmp(await writeDurableTmp(path, next), path);
1924
1646
  });
1925
1647
  },
1926
1648
  async remove(path) {
@@ -1983,6 +1705,121 @@ function nodeEvolutionIo(lockAttempts = 40) {
1983
1705
  }
1984
1706
  };
1985
1707
  }
1708
+ const defaultFs = {
1709
+ writeFileSync: (path, data) => {
1710
+ writeFileSync(path, data);
1711
+ },
1712
+ existsSync: (path) => existsSync(path),
1713
+ copyFileSync: (from, to) => {
1714
+ copyFileSync(from, to);
1715
+ },
1716
+ renameSync: (from, to) => {
1717
+ renameSync(from, to);
1718
+ },
1719
+ rmSync: (path, options) => {
1720
+ rmSync(path, options);
1721
+ },
1722
+ mtime: (path) => {
1723
+ try {
1724
+ return statSync(path).mtimeMs;
1725
+ } catch (error) {
1726
+ if (isMissingPath(error)) return null;
1727
+ throw error;
1728
+ }
1729
+ }
1730
+ };
1731
+ /**
1732
+ * S6.3 (E-40): atomically replace a set of files within one directory.
1733
+ * Each file is staged to a sibling `<name>.tmp`, then renamed into place
1734
+ * (atomic on the same filesystem); a pre-existing file keeps a single
1735
+ * `<name>.bak`. A failure at ANY staging step removes the staged temps and
1736
+ * leaves the previous files untouched — a half-written target is impossible
1737
+ * up to the staging and backup phases.
1738
+ *
1739
+ * F-211: the rename COMMIT phase does not carry the same guarantee. When a
1740
+ * filesystem refuses to overwrite the destination, the code removes it and
1741
+ * retries the rename; if the retry ALSO fails the destination is gone. The
1742
+ * single `.bak` created above still holds the previous content, so the target
1743
+ * is restored from it (best-effort) before the original rename error is
1744
+ * rethrown, so a failed commit never leaves the file missing.
1745
+ *
1746
+ * V4-17: the `.bak` is refreshed to the committed content after every
1747
+ * successful commit, so a later failed commit always recovers to the most
1748
+ * recent good generation (a once-at-first-install `.bak` could otherwise
1749
+ * restore content several installs old). The recovery error names the `.bak`
1750
+ * source (and mtime when available) so the caller knows the generation it
1751
+ * restored from.
1752
+ */
1753
+ function atomicWriteFiles(targetDir, writes, fs = defaultFs, logger = console.warn) {
1754
+ const stage = (name) => join(targetDir, `${name}.tmp`);
1755
+ const dupes = [...new Set(writes.map((write) => write.name).filter((name, index, all) => all.indexOf(name) !== index))];
1756
+ if (dupes.length > 0) throw new Error(`atomicWriteFiles: duplicate input names: ${dupes.join(", ")}`);
1757
+ try {
1758
+ for (const { name, content } of writes) fs.writeFileSync(stage(name), content);
1759
+ for (const { name } of writes) {
1760
+ const finalPath = join(targetDir, name);
1761
+ const bakPath = join(targetDir, `${name}.bak`);
1762
+ if (fs.existsSync(finalPath) && !fs.existsSync(bakPath)) fs.copyFileSync(finalPath, bakPath);
1763
+ }
1764
+ const committed = [];
1765
+ for (const { name } of writes) {
1766
+ const finalPath = join(targetDir, name);
1767
+ const bakPath = join(targetDir, `${name}.bak`);
1768
+ const tmp = stage(name);
1769
+ try {
1770
+ fs.renameSync(tmp, finalPath);
1771
+ committed.push(name);
1772
+ } catch {
1773
+ try {
1774
+ fs.rmSync(finalPath, { force: true });
1775
+ } catch (removeError) {
1776
+ const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
1777
+ throw new Error(`atomicWriteFiles: could not remove "${name}" to replace it (${removeError instanceof Error ? removeError.message : String(removeError)})${already}`);
1778
+ }
1779
+ try {
1780
+ fs.renameSync(tmp, finalPath);
1781
+ committed.push(name);
1782
+ } catch (renameError) {
1783
+ const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
1784
+ if (fs.existsSync(bakPath)) {
1785
+ try {
1786
+ fs.copyFileSync(bakPath, finalPath);
1787
+ } catch {
1788
+ throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually${already}`);
1789
+ }
1790
+ const bakMtime = fs.mtime?.(bakPath);
1791
+ const when = bakMtime !== null && bakMtime !== void 0 && Number.isFinite(bakMtime) ? ` (bak mtime ${new Date(bakMtime).toISOString()})` : "";
1792
+ throw new Error(`atomicWriteFiles: "${name}" was removed during commit and restored from the last good generation ${bakPath}${when}; original error: ${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
1793
+ }
1794
+ throw new Error(`${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
1795
+ }
1796
+ }
1797
+ }
1798
+ for (const { name } of writes) {
1799
+ const finalPath = join(targetDir, name);
1800
+ try {
1801
+ if (fs.existsSync(finalPath)) fs.copyFileSync(finalPath, join(targetDir, `${name}.bak`));
1802
+ } catch (refreshError) {
1803
+ logger(`atomicWriteFiles: committed "${name}" but failed to refresh its .bak (${refreshError instanceof Error ? refreshError.message : String(refreshError)}); a later failed commit will recover to an OLDER generation`);
1804
+ }
1805
+ }
1806
+ } catch (error) {
1807
+ const cleanupFailures = [];
1808
+ for (const { name } of writes) try {
1809
+ fs.rmSync(stage(name), {
1810
+ force: true,
1811
+ recursive: true
1812
+ });
1813
+ } catch (cleanupError) {
1814
+ cleanupFailures.push(`${name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1815
+ }
1816
+ if (cleanupFailures.length > 0) {
1817
+ const original = error instanceof Error ? error.message : String(error);
1818
+ throw new Error(`${original} (staged-temp cleanup also failed: ${cleanupFailures.join("; ")})`);
1819
+ }
1820
+ throw error;
1821
+ }
1822
+ }
1986
1823
  //#endregion
1987
1824
  //#region lib/types/usage.js
1988
1825
  /**
@@ -2093,7 +1930,7 @@ async function mutateUsage(root, io, task, options = {}) {
2093
1930
  let parsed = null;
2094
1931
  try {
2095
1932
  const probe = JSON.parse(current);
2096
- if (probe !== null && typeof probe === "object" && !Array.isArray(probe)) parsed = probe;
1933
+ if (isRecord(probe)) parsed = probe;
2097
1934
  } catch {
2098
1935
  parsed = null;
2099
1936
  }
@@ -3221,7 +3058,7 @@ async function readJsonBody(req) {
3221
3058
  */
3222
3059
  async function readJsonObject(req) {
3223
3060
  const body = await readJsonBody(req);
3224
- return body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
3061
+ return isRecord(body) ? body : null;
3225
3062
  }
3226
3063
  //#endregion
3227
3064
  //#region lib/types/env.js
@@ -3252,7 +3089,7 @@ function allowRowCollisions(env = process.env) {
3252
3089
  //#endregion
3253
3090
  //#region lib/types/errors.js
3254
3091
  /**
3255
- * THE error-code table: E-301…E-319 with the exact text each one emits (E-317…E-319 are the
3092
+ * THE error-code table: E-301…E-322 with the exact text each one emits (E-317…E-319 are the
3256
3093
  * write-admission gates' own refusals).
3257
3094
  *
3258
3095
  * The codes used to be spelled out at 33 call sites across two packages, so one
@@ -3289,6 +3126,7 @@ const EVOLUTION_ERRORS = Object.freeze({
3289
3126
  "e-317-skill-write-not-confirmed": "E-317: skill \"%a1%\" was not %a2% — the confirmation prompt was declined or dismissed, so nothing was written. The prompt appears on every create and bare delete; repeat the call only if the operator asks for it.",
3290
3127
  "e-318-skill-write-without-a-read": "E-318: skill \"%a1%\" was not read in this session, so this write is refused. Read it with the `skill` tool first (a read that failed does not count), then repeat the write.",
3291
3128
  "e-320-the-profile-home-is-not": "E-320: the profile home is not reported by this host, so the legacy settings document cannot be located. /evolution migrate reads `<profile home>/settings.yaml.imported`; run it from a profile-launched session.",
3129
+ "e-322-a-parameter-would-break-a": "E-322: %a1% must be at least %a2%, but this write sets it to %a3%. Nothing was written.",
3292
3130
  "e-321-no-io-provider-is": "E-321: no evolution-io provider is mounted, so the legacy settings document cannot be read. Mount the evolution-io row (evolution-host/evolution-all) and retry.",
3293
3131
  "e-319-skill-write-confirm-timed-out": "E-319: skill \"%a1%\" was not %a2% — the confirmation prompt went unanswered for %a3% second(s), so the write was cancelled. Nobody was there to decide: set `skillWriteConfirm` to `auto` to write without a prompt, keep `ask` to wait until someone answers, or raise `skillWriteConfirmTimeoutSeconds`."
3294
3132
  });
@@ -3485,35 +3323,6 @@ function clampedNumber(value, fallback, opts) {
3485
3323
  if (max !== void 0 && value > max) return fallback;
3486
3324
  return value;
3487
3325
  }
3488
- /**
3489
- * Clamp one numeric knob and warn ONCE per key.
3490
- *
3491
- * S2.9 (T3-10): `evolution-review` and `evolution-curator` each carried their own copy of this
3492
- * helper — their own once-per-key set and their own message — so the two settings readers of ONE
3493
- * policy row could drift apart. The logic lives here; each caller passes its own sink (and its own
3494
- * package prefix). A live field is read at every use, so the warning cannot be per-read.
3495
- *
3496
- * @param warned - the keys this caller already warned about.
3497
- * @param warn - the warning sink (each package prefixes its own name).
3498
- * @param name - the config key, for the warning.
3499
- * @param value - the value as supplied, or its volatile reference.
3500
- * @param fallback - the package default a corrected value falls back to.
3501
- * @param min - the smallest value the engine can act on.
3502
- * @param max - the largest value the carrier can hold, when bounded.
3503
- * @returns the value the engine may use.
3504
- */
3505
- function clampOnce(warned, warn, name, value, fallback, min, max) {
3506
- const current = typeof value === "object" ? value.get() : value;
3507
- const result = clampedNumber(current, fallback, max === void 0 ? { min } : {
3508
- min,
3509
- max
3510
- });
3511
- if (current !== void 0 && result !== current && !warned.has(name)) {
3512
- warned.add(name);
3513
- warn(`${name} provided an invalid value; falling back to the default`);
3514
- }
3515
- return result;
3516
- }
3517
3326
  //#endregion
3518
3327
  //#region lib/types/threats.js
3519
3328
  /**
@@ -4561,7 +4370,7 @@ async function recordMutation(root, io, record, cap = 500) {
4561
4370
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
4562
4371
  return current;
4563
4372
  }
4564
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
4373
+ if (isRecord(parsed)) {
4565
4374
  const version = parsed.version;
4566
4375
  if (typeof version === "number" && version > 1) {
4567
4376
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
@@ -4625,16 +4434,23 @@ function composePresetComposition(standardComposition, deltaComposition) {
4625
4434
  * test can keep — the entries are now data, and a divergence is impossible.
4626
4435
  */
4627
4436
  const ROW_OVERRIDES_URL = new URL("../row-overrides.json", import.meta.url);
4628
- let cachedOverrides;
4437
+ /** One cache per PATH: the installer reads the same file through its own root, and a single-slot
4438
+ * cache would hand the second caller the first caller's bytes. */
4439
+ const overridesCache = /* @__PURE__ */ new Map();
4629
4440
  /**
4630
4441
  * Read and validate the shared override table.
4442
+ * G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
4443
+ * runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
4444
+ * read would look for `scripts/../row-overrides.json` and fail.
4445
+ * @param path - the table to read.
4631
4446
  * @returns the entries, in file order. A malformed table fails LOUD: a composer
4632
4447
  * that silently skipped an entry would ship a preset running the platform
4633
4448
  * default while nothing downstream reported it.
4634
4449
  */
4635
- function loadRowOverrides() {
4636
- if (cachedOverrides !== void 0) return cachedOverrides;
4637
- const parsed = JSON.parse(readFileSync(fileURLToPath(ROW_OVERRIDES_URL), "utf8"));
4450
+ function loadRowOverrides(path = fileURLToPath(ROW_OVERRIDES_URL)) {
4451
+ const cached = overridesCache.get(path);
4452
+ if (cached !== void 0) return cached;
4453
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
4638
4454
  if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
4639
4455
  const entries = [];
4640
4456
  for (const [index, entry] of parsed.entries()) {
@@ -4647,7 +4463,7 @@ function loadRowOverrides() {
4647
4463
  missingReason: candidate.missingReason
4648
4464
  });
4649
4465
  }
4650
- cachedOverrides = entries;
4466
+ overridesCache.set(path, entries);
4651
4467
  return entries;
4652
4468
  }
4653
4469
  /**
@@ -4766,7 +4582,7 @@ function composePresetRow(baseComposition, deltaComposition, identity) {
4766
4582
  }
4767
4583
  /** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
4768
4584
  * preset the platform does not ship has to arrive inside an `insert` entry
4769
- * (`packages/boot/app-boot/tests/user-patches.spec.ts:47-64`). */
4585
+ * (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
4770
4586
  const PATCH_INSERT_KEY = "insert";
4771
4587
  /**
4772
4588
  * Wrap a composed row as the patch entry a profile patch needs.
@@ -6440,7 +6256,7 @@ function renderLongRunTable() {
6440
6256
  /**
6441
6257
  * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
6442
6258
  *
6443
- * O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
6259
+ * O-2 (v46 audit): the family probes 28 platform services optionally — the platform has no
6444
6260
  * formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
6445
6261
  * surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
6446
6262
  * them; this table is the single source it judges the rest from, and the arch guard cross-checks it
@@ -6450,7 +6266,7 @@ function renderLongRunTable() {
6450
6266
  * `client` probes run in the browser half, where the host has no view of the seats.
6451
6267
  * @module @lmzhen/dsh-evolution-core
6452
6268
  */
6453
- /** The 26 probes, in service-name order (the guard checks this list against the tree). */
6269
+ /** The 28 probes, in service-name order (the guard checks this list against the tree). */
6454
6270
  const PLATFORM_SERVICE_PROBES = Object.freeze([
6455
6271
  {
6456
6272
  service: "agents",
@@ -6487,6 +6303,16 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
6487
6303
  feature: "every durable read and write (the IO seam)",
6488
6304
  side: "host"
6489
6305
  },
6306
+ {
6307
+ service: "fs",
6308
+ feature: "the platform filesystem the evolution-io-fs backend maps the seam onto (locks + writeText(expected) version CAS)",
6309
+ side: "host"
6310
+ },
6311
+ {
6312
+ service: "hmr",
6313
+ feature: "the preset install read-merge-write commit (the platform HMR lock serializes it)",
6314
+ side: "host"
6315
+ },
6490
6316
  {
6491
6317
  service: "evolutionMemoryBudget",
6492
6318
  feature: "the memory-budget divergence section of this report",
@@ -6697,10 +6523,24 @@ const SERIALIZATIONS = [
6697
6523
  ];
6698
6524
  /** Parse + validate the table. A malformed table throws at import: a table the
6699
6525
  * gate cannot read must never degrade into "no declared write sites". */
6700
- function parseSites(raw) {
6701
- if (!Array.isArray(raw)) throw new Error("evolution-core: persisted-write-inventory.json must be an array of sites");
6526
+ function parseTable(raw) {
6527
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("evolution-core: persisted-write-inventory.json must be an object with { symbols, sites }");
6528
+ const table = raw;
6529
+ if (table.symbols === null || typeof table.symbols !== "object" || Array.isArray(table.symbols)) throw new Error("evolution-core: persisted-write-inventory.json needs a symbols legend (the roots every path is written against)");
6530
+ for (const [symbol, entry] of Object.entries(table.symbols)) {
6531
+ const problems = [];
6532
+ if (!symbol.startsWith("<") || !symbol.endsWith(">")) problems.push("symbol name (must be <name>)");
6533
+ if (entry === null || typeof entry !== "object") problems.push("entry");
6534
+ else {
6535
+ const symbolEntry = entry;
6536
+ if (typeof symbolEntry.resolver !== "string" || symbolEntry.resolver === "") problems.push("resolver");
6537
+ if (symbolEntry.segments !== void 0 && (!Array.isArray(symbolEntry.segments) || symbolEntry.segments.some((segment) => typeof segment !== "string"))) problems.push("segments");
6538
+ }
6539
+ if (problems.length > 0) throw new Error(`evolution-core: persisted-write-inventory.json symbol ${symbol} is malformed (missing/invalid: ${problems.join(", ")})`);
6540
+ }
6541
+ if (!Array.isArray(table.sites)) throw new Error("evolution-core: persisted-write-inventory.json needs a sites array");
6702
6542
  const sites = [];
6703
- for (const [index, entry] of raw.entries()) {
6543
+ for (const [index, entry] of table.sites.entries()) {
6704
6544
  const site = entry;
6705
6545
  const problems = [];
6706
6546
  if (typeof site.id !== "string" || site.id === "") problems.push("id");
@@ -6723,7 +6563,10 @@ function parseSites(raw) {
6723
6563
  });
6724
6564
  }
6725
6565
  if (new Set(sites.map((site) => site.id)).size !== sites.length) throw new Error("evolution-core: persisted-write-inventory.json declares duplicate site ids");
6726
- return sites;
6566
+ return {
6567
+ symbols: table.symbols,
6568
+ sites
6569
+ };
6727
6570
  }
6728
6571
  /** Instance keys held by writer services — the single source shared by the
6729
6572
  * claim site and the inventory row that declares it (rule N20 checks the row's
@@ -6732,6 +6575,7 @@ const INSTANCE_KEYS = {
6732
6575
  /** The per-home curator: report writing + the retention sweep. */
6733
6576
  curator: "evolution-curator" };
6734
6577
  let cachedSites;
6578
+ let cachedSymbols;
6735
6579
  /**
6736
6580
  * The declared persisted write sites, in file order.
6737
6581
  *
@@ -6755,8 +6599,17 @@ function persistedWriteSites() {
6755
6599
  const cause = error instanceof Error ? error.message : String(error);
6756
6600
  throw new Error(`evolution-core: persisted-write-inventory.json is unreadable (${cause}) — the package asset is missing or was not shipped; the write-inventory rules cannot be evaluated without it`);
6757
6601
  }
6758
- cachedSites = parseSites(JSON.parse(raw));
6759
- return cachedSites;
6602
+ const table = parseTable(JSON.parse(raw));
6603
+ cachedSites = table.sites;
6604
+ cachedSymbols = table.symbols;
6605
+ return table.sites;
6606
+ }
6607
+ /** The inventory's symbol legend — the roots every declared path is written against, with the
6608
+ * segments the gate checks against the resolver's source.
6609
+ * @internal no production caller — the gates read the JSON asset directly. */
6610
+ function writePathSymbols() {
6611
+ persistedWriteSites();
6612
+ return cachedSymbols ?? {};
6760
6613
  }
6761
6614
  /** Sites serialized by the per-home instance claim, with their instance keys.
6762
6615
  * @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
@@ -8009,6 +7862,13 @@ function versionRowCells(row) {
8009
7862
  tone: "tertiary",
8010
7863
  slot: "meta"
8011
7864
  });
7865
+ if (row.hash !== void 0 && row.hash !== "") title.push({
7866
+ key: "hash",
7867
+ literal: row.hash.slice(0, 7),
7868
+ tone: "tertiary",
7869
+ slot: "meta",
7870
+ note: row.hash
7871
+ });
8012
7872
  const meta = [];
8013
7873
  if (row.summary !== void 0 && row.summary !== "") meta.push({
8014
7874
  key: "summary",
@@ -8057,6 +7917,13 @@ function skillRowCells(skill) {
8057
7917
  tone: "tertiary",
8058
7918
  slot: "meta"
8059
7919
  }];
7920
+ if (skill.protectedBy !== null) meta.push({
7921
+ key: "protectedBy",
7922
+ literal: skill.protectedBy,
7923
+ tone: "tertiary",
7924
+ slot: "meta",
7925
+ note: skill.protectedBy
7926
+ });
8060
7927
  if (skill.age !== null) meta.push({
8061
7928
  key: "age",
8062
7929
  copy: ageCopyKey(skill.age),
@@ -9004,7 +8871,7 @@ function elapsedSince(at, nowMs) {
9004
8871
  * itself in `this.serial` (archive/restoreFromArchive deliberately do not).
9005
8872
  * 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
9006
8873
  * exclusion plus in-process crash ownership (tickets, takeover). Checked
9007
- * with `hasWriteLock` before any destructive move (archive/restore/
8874
+ * with `supportDirsUnlistable` before any destructive move (archive/restore/
9008
8875
  * snapshot); held inside transactIo by byte writers.
9009
8876
  * 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
9010
8877
  * must either live inside the serial section that commits the write, or
@@ -9241,15 +9108,6 @@ function skillDir(root, name) {
9241
9108
  function markerEntryName(marker) {
9242
9109
  return `.${marker}`;
9243
9110
  }
9244
- /** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
9245
- * active writer (skill body + the two marker writers). Single source with
9246
- * `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
9247
- * of the ghost-writer probe. */
9248
- const MARKER_LOCK_NAMES = [
9249
- `SKILL.md${LOCK_SUFFIX}`,
9250
- `.pinned${LOCK_SUFFIX}`,
9251
- `.hermes-managed${LOCK_SUFFIX}`
9252
- ];
9253
9111
  /** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
9254
9112
  const ARCHIVE_RETENTION_DAYS = 365;
9255
9113
  function markerPath(dir, marker) {
@@ -9328,7 +9186,6 @@ function validateSupportPath(filePath) {
9328
9186
  if (parts.length < 2) return "Provide a file name, not just a directory.";
9329
9187
  for (const part of parts.slice(1)) {
9330
9188
  if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
9331
- if (part.toLowerCase().endsWith(".lock")) return `Unsupported file name "${part}" — the .lock suffix is reserved for the writer-lock protocol.`;
9332
9189
  if (part.toLowerCase().endsWith(".corrupt") || part.toLowerCase().endsWith(".tmp")) return `Unsupported file name "${part}" — the .corrupt/.tmp suffixes are reserved for the state/IO protocols.`;
9333
9190
  const stem = part.split(".")[0]?.toLowerCase() ?? "";
9334
9191
  if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
@@ -9508,16 +9365,14 @@ var SkillLibrary = class {
9508
9365
  * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
9509
9366
  * holds nothing but this path's own write-lock file. The BR-5 rule from
9510
9367
  * setPinnedCore/createCore applies unchanged: a concurrent mover can land a
9511
- * full directory between the list probe and the remove, so anything beyond
9512
- * the lock file (support files, a fresh restore) must never be recursed
9513
- * away. Best-effort: a failed cleanup leaves the "not found" result
9368
+ * full directory between the list probe and the remove, so ANY content at all (support files, a
9369
+ * fresh restore) must never be recursed away. Best-effort: a failed cleanup leaves the "not found" result
9514
9370
  * unchanged (the operator-facing ghost-dir refusal is fail-loud already).
9515
9371
  */
9516
9372
  async cleanupGhostDir(skillFilePath) {
9517
9373
  const dir = dirname(skillFilePath);
9518
- const lockName = `${basename(skillFilePath)}${LOCK_SUFFIX}`;
9519
9374
  try {
9520
- if ((await this.io.list(dir)).some((entry) => entry !== lockName)) return;
9375
+ if ((await this.io.list(dir)).length > 0) return;
9521
9376
  await this.io.remove(dir);
9522
9377
  } catch {}
9523
9378
  }
@@ -10382,110 +10237,19 @@ var SkillLibrary = class {
10382
10237
  * P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
10383
10238
  * restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
10384
10239
  * after the move its transact commit re-creates `<dir>/…` (mkdir
10385
- * recursive) and the tree ends half-archived. The signal is the writer's
10386
- * own lock file, and its PLACEMENT (inside the moved directory) is why the
10387
- * mover must PROBE-and-REFUSE instead of acquiring it: an acquired lock
10388
- * would be renamed into `.archive` with the tree, stranding a phantom live
10389
- * lock (the v16 audit proved the probe→rename TOCTOU does exactly that,
10390
- * and restore would later move the residue back into the live root).
10391
- * Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
10392
- * each support dir (write_file's lock sits next to its file). Residual:
10393
- * NESTED support-subdir locks and the probe→rename TOCTOU itself remain
10394
- * fail-safe (renameWithRetry rides the write out; the writer's locked
10395
- * re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
10396
- * holder is GONE is not a writer — `decideTakeover` owns that verdict.
10240
+ * recursive) and the tree ends half-archived.
10241
+ * G2 S4: the writer-lock probe that used to answer this question is retired — writes go through
10242
+ * the provider's versioned writes, so a writer that raced the move fails its own write loudly
10243
+ * instead of landing on a moved-away path. What REMAINS load-bearing here is the fail-closed
10244
+ * listing rule, which is about the filesystem misbehaving rather than about writers.
10397
10245
  */
10398
- async hasWriteLock(dir) {
10399
- const markerLocks = MARKER_LOCK_NAMES.map((name) => join(dir, name));
10400
- for (const lock of markerLocks) if (await this.isWriterLock(lock)) return true;
10401
- for (const supportDir of SUPPORT_DIRS) {
10402
- let entries;
10403
- try {
10404
- entries = await this.io.list(join(dir, supportDir));
10405
- } catch {
10406
- return true;
10407
- }
10408
- for (const entry of entries) {
10409
- if (!entry.endsWith(".lock")) continue;
10410
- if (await this.isWriterLock(join(dir, supportDir, entry))) return true;
10411
- }
10412
- }
10413
- return false;
10414
- }
10415
- /** P2 (v17): a `*.lock` file counts as a writer lock only when its body
10416
- * carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
10417
- * (a creator between create and body write); anything else is user content
10418
- * (suffix-only matching refused archiving and deleted user files on restore).
10419
- * v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
10420
- * the lock the io layer refuses to reclaim, the 30s empty window included. */
10421
- async isWriterLock(lockPath) {
10422
- let body;
10423
- try {
10424
- body = await this.io.readText(lockPath);
10246
+ async supportDirsUnlistable(dir) {
10247
+ for (const supportDir of SUPPORT_DIRS) try {
10248
+ await this.io.list(join(dir, supportDir));
10425
10249
  } catch {
10426
10250
  return true;
10427
10251
  }
10428
- if (body === null) return false;
10429
- const trimmed = body.trim();
10430
- if (trimmed !== "" && !/^\d+$/.test(trimmed) && !LOCK_BODY_RE.test(trimmed)) return false;
10431
- let mtimeMs = null;
10432
- try {
10433
- mtimeMs = await this.io.mtime?.(lockPath) ?? null;
10434
- } catch {
10435
- mtimeMs = null;
10436
- }
10437
- return decideTakeover({
10438
- body,
10439
- mtimeMs: mtimeMs ?? Date.now(),
10440
- alive: isProcessAlive
10441
- }) === "none";
10442
- }
10443
- /** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
10444
- * A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
10445
- * (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
10446
- * a lock whose holder pid is NOT alive — a live writer's lock is never
10447
- * stolen by the sweep. A dead-pid residue would otherwise permanently
10448
- * refuse archive/restore. */
10449
- async deleteStrandedLocks(dir) {
10450
- for (const markerLock of MARKER_LOCK_NAMES) await this.sweepLockIfStranded(join(dir, markerLock));
10451
- for (const supportDir of SUPPORT_DIRS) {
10452
- let entries = [];
10453
- try {
10454
- entries = await this.io.list(join(dir, supportDir));
10455
- } catch {
10456
- continue;
10457
- }
10458
- for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
10459
- }
10460
- }
10461
- /** R2 follow-up (audit of A2): the recycled-pid window from io's
10462
- * `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
10463
- * whose holder pid probes alive but whose mtime is older than the alive
10464
- * window is a recycled pid, not a live writer (no write in this family
10465
- * holds a lock for more than minutes) — without this, one recycled pid
10466
- * blocked whole-tree snapshot recovery forever with a "retry once the
10467
- * write completes" message that could never become true. A backend without
10468
- * `mtime` keeps the conservative refuse-on-alive posture. */
10469
- async lockHolderAgedOut(lockPath) {
10470
- const mtime = await this.io.mtime?.(lockPath).catch(() => null);
10471
- return typeof mtime === "number" && Date.now() - mtime > 864e5;
10472
- }
10473
- /** Remove `lockPath` only when its body has the writer-lock `pid:token`
10474
- * shape AND the holder pid is not alive (or is an aged-out recycled pid —
10475
- * R2 follow-up); anything else (a user support file or a live writer's
10476
- * lock) is left untouched. */
10477
- async sweepLockIfStranded(lockPath) {
10478
- const lockProbe = await probeText(this.io, lockPath);
10479
- if (isUnknown(lockProbe)) {
10480
- console.warn(`skill-store: could not read ${lockPath} to check whether its holder is alive (${lockProbe.reason}); leaving the lock in place`);
10481
- return;
10482
- }
10483
- if (!isPresent(lockProbe)) return;
10484
- const body = lockProbe.value;
10485
- const pid = parseLockBody(body);
10486
- if (pid === null) return;
10487
- if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
10488
- await this.io.remove(lockPath).catch(() => {});
10252
+ return false;
10489
10253
  }
10490
10254
  /** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
10491
10255
  * only a single, non-traversing path component is safe. Dotfiles
@@ -10496,22 +10260,6 @@ var SkillLibrary = class {
10496
10260
  if (typeof name !== "string") return false;
10497
10261
  return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
10498
10262
  }
10499
- /** A1-7 (v18): a root-level lock whose holder is alive must refuse the
10500
- * restore; a dead residue is swept so a crashed writer cannot block
10501
- * recovery. A non-lock body shape is left alone (user file). */
10502
- async refuseLiveLockOrSweep(lockPath, label) {
10503
- let body;
10504
- try {
10505
- body = await this.io.readText(lockPath);
10506
- } catch (error) {
10507
- throw new Error(`snapshot restore refused: cannot verify ${label} (${error instanceof Error ? error.message : String(error)}); a live writer may hold it`);
10508
- }
10509
- if (body === null) return;
10510
- const pid = parseLockBody(body);
10511
- if (pid === null) return;
10512
- if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
10513
- await this.io.remove(lockPath).catch(() => {});
10514
- }
10515
10263
  async archive(rawName, options = {}) {
10516
10264
  const name = rawName.trim();
10517
10265
  const badName = this.badName(name, { allowReserved: true });
@@ -10558,9 +10306,9 @@ var SkillLibrary = class {
10558
10306
  message: `Skill "${name}" is a symlink; refusing to archive it.`
10559
10307
  };
10560
10308
  }
10561
- if (await this.hasWriteLock(dir)) return {
10309
+ if (await this.supportDirsUnlistable(dir)) return {
10562
10310
  ok: false,
10563
- message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
10311
+ message: `Skill "${name}" could not be checked for writers (a support directory could not be listed); retry archiving.`
10564
10312
  };
10565
10313
  const commitProtection = await this.deleteProtection(name, options);
10566
10314
  if (commitProtection) return {
@@ -11190,16 +10938,15 @@ var SkillLibrary = class {
11190
10938
  message: `Archived entry "${chosen}" is a symlink; refusing to restore it.`
11191
10939
  };
11192
10940
  }
11193
- if (await this.hasWriteLock(dest)) return {
10941
+ if (await this.supportDirsUnlistable(dest)) return {
11194
10942
  ok: false,
11195
- message: `Skill "${name}" is being written (write lock present); retry restoring once the write completes.`
10943
+ message: `Skill "${name}" could not be checked for writers (a support directory could not be listed); retry restoring.`
11196
10944
  };
11197
10945
  const moveFailure = await this.moveDir(source, dest);
11198
10946
  if (moveFailure !== void 0) return {
11199
10947
  ok: false,
11200
10948
  message: `Restore of "${name}" from .archive failed: ${moveFailure}`
11201
10949
  };
11202
- await this.deleteStrandedLocks(dest);
11203
10950
  for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
11204
10951
  const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
11205
10952
  if (isUnknown(restoredProbe)) console.warn(`evolution-skill-store: the restored SKILL.md of "${name}" could not be read back (${restoredProbe.reason}); the restore is recorded without after-bytes`);
@@ -11447,8 +11194,8 @@ var SkillLibrary = class {
11447
11194
  const names = await listNames(this.root, this.io);
11448
11195
  const skipped = [];
11449
11196
  const copyable = [];
11450
- for (const name of names) if (await this.hasWriteLock(this.dirOf(name))) {
11451
- console.warn(`skill-store: snapshot skipped "${name}" — a byte-writer holds its write lock; the skill is recorded as skipped in the manifest`);
11197
+ for (const name of names) if (await this.supportDirsUnlistable(this.dirOf(name))) {
11198
+ console.warn(`skill-store: snapshot skipped "${name}" — its support directory could not be listed; the skill is recorded as skipped in the manifest`);
11452
11199
  skipped.push(name);
11453
11200
  } else copyable.push(name);
11454
11201
  const copyFailure = (await Promise.allSettled(copyable.map(async (name) => {
@@ -11456,7 +11203,7 @@ var SkillLibrary = class {
11456
11203
  }))).find((result) => result.status === "rejected");
11457
11204
  if (copyFailure) throw copyFailure.reason;
11458
11205
  const suspect = [];
11459
- for (const name of copyable) if (await this.hasWriteLock(this.dirOf(name))) suspect.push(name);
11206
+ for (const name of copyable) if (await this.supportDirsUnlistable(this.dirOf(name))) suspect.push(name);
11460
11207
  if (suspect.length > 0) {
11461
11208
  for (const name of suspect) console.warn(`skill-store: snapshot demoted "${name}" to skipped — a write lock appeared while its copy ran; the copied bytes are suspect`);
11462
11209
  for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
@@ -11715,14 +11462,9 @@ var SkillLibrary = class {
11715
11462
  }
11716
11463
  for (const entry of rootEntries) {
11717
11464
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json" || entry === ".history") continue;
11718
- if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
11719
- await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
11720
- continue;
11721
- }
11722
11465
  const dir = join(this.root, entry);
11723
11466
  if (await this.io.exists(join(dir, "SKILL.md"))) {
11724
- await this.deleteStrandedLocks(dir);
11725
- if (await this.hasWriteLock(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
11467
+ if (await this.supportDirsUnlistable(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
11726
11468
  }
11727
11469
  }
11728
11470
  const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
@@ -11730,7 +11472,6 @@ var SkillLibrary = class {
11730
11472
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
11731
11473
  if (entry === ".history") continue;
11732
11474
  if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
11733
- if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
11734
11475
  await this.io.remove(join(this.root, entry));
11735
11476
  }
11736
11477
  for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
@@ -11742,10 +11483,7 @@ var SkillLibrary = class {
11742
11483
  await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
11743
11484
  } else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
11744
11485
  }
11745
- for (const entry of await this.io.list(this.root)) {
11746
- if (entry.startsWith(".")) continue;
11747
- await this.deleteStrandedLocks(join(this.root, entry));
11748
- }
11486
+ for (const entry of await this.io.list(this.root)) if (entry.startsWith(".")) continue;
11749
11487
  }
11750
11488
  };
11751
11489
  function newSkillLibrary(options) {
@@ -12360,6 +12098,7 @@ const PARAM_EXPOSURE = Object.freeze([
12360
12098
  control: "number",
12361
12099
  unit: "天",
12362
12100
  values: "",
12101
+ relation: { atLeast: "staleAfterDays" },
12363
12102
  card: "evolution-curator"
12364
12103
  },
12365
12104
  {
@@ -13001,6 +12740,23 @@ const PARAM_EXPOSURE = Object.freeze([
13001
12740
  values: "",
13002
12741
  card: "evolution-approval"
13003
12742
  },
12743
+ {
12744
+ id: "approvalConfirm",
12745
+ group: "write-caps",
12746
+ tier: "E3",
12747
+ authority: "cordis",
12748
+ owner: "evolution-approval",
12749
+ applies: "live",
12750
+ docAnchor: "PARAMETERS.md#write-caps",
12751
+ summary: "What the confirmation before an APPROVE does: write straight through, ask first, or treat an unanswered prompt as a cancel. The same three words skillWriteConfirm uses for a skill write.",
12752
+ label: "批准前先确认",
12753
+ hint: "点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。",
12754
+ control: "select",
12755
+ unit: "",
12756
+ values: "auto|ask|timeout",
12757
+ valueLabels: "直接执行|先确认|等待超时即取消",
12758
+ card: "evolution-approval"
12759
+ },
13004
12760
  {
13005
12761
  id: "qualityWarnThreshold",
13006
12762
  group: "deployment",
@@ -13271,14 +13027,6 @@ function readNumberParam(carrier, id) {
13271
13027
  * @module @lmzhen/dsh-evolution-core
13272
13028
  */
13273
13029
  /**
13274
- * Whether a parsed value is a plain mapping (a settings section).
13275
- * @param value - the parsed value.
13276
- * @returns true for a non-null, non-array object.
13277
- */
13278
- function isSection(value) {
13279
- return typeof value === "object" && value !== null && !Array.isArray(value);
13280
- }
13281
- /**
13282
13030
  * The key names one row may be written with: its own E3 rows, plus the deprecated
13283
13031
  * spellings that still resolve to them. An unknown key is refused instead of written —
13284
13032
  * the platform validates a write against the row's schema, so writing it would fail the
@@ -13309,7 +13057,7 @@ function planNamespaceMigration(documentText) {
13309
13057
  foreign: [],
13310
13058
  skipped: []
13311
13059
  };
13312
- if (!isSection(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13060
+ if (!isRecord(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13313
13061
  const legacy = new Map(NAMESPACE_MIGRATIONS.map((entry) => [entry.namespace, entry.rowId]));
13314
13062
  const entries = [];
13315
13063
  const empty = [];
@@ -13317,7 +13065,7 @@ function planNamespaceMigration(documentText) {
13317
13065
  for (const [namespace, section] of Object.entries(parsed)) {
13318
13066
  const rowId = legacy.get(namespace);
13319
13067
  if (rowId === void 0) continue;
13320
- if (!isSection(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13068
+ if (!isRecord(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13321
13069
  const allowed = writableKeys(rowId);
13322
13070
  const values = [];
13323
13071
  for (const [key, value] of Object.entries(section)) {
@@ -13443,6 +13191,114 @@ async function applyNamespaceMigration(plan, deps) {
13443
13191
  };
13444
13192
  }
13445
13193
  //#endregion
13194
+ //#region lib/types/live.js
13195
+ /**
13196
+ * The family's ONE live-config reader.
13197
+ *
13198
+ * A config field reaches a plugin in two shapes: the platform's schema parser turns a
13199
+ * `.volatile()` declaration into a live reference (`{ get() }`, routed through cosmokit's
13200
+ * `createVolatile`), while a hand-built or pre-parse config carries the plain value. Reading a
13201
+ * live reference as if it were a value is SILENT (a boolean compare is simply never true, a
13202
+ * number folds as NaN) — so the two shapes are unwrapped in exactly one place here, and every
13203
+ * reader below goes through it.
13204
+ *
13205
+ * This module owns no policy: clamping semantics stay in `clampedNumber` (numeric.ts), and the
13206
+ * once-per-key warning text/rhythm is the one `clampOnce` has always had (it moved here so the
13207
+ * read happens in one place; callers import it from the package barrel, so the move is invisible).
13208
+ *
13209
+ * @module @lmzhen/dsh-evolution-core
13210
+ */
13211
+ /**
13212
+ * Unwrap a live reference; return a plain value unchanged. The ONLY place that decides the shape.
13213
+ *
13214
+ * The discriminant is `typeof get === 'function'` rather than a bare `typeof value === 'object'`: a
13215
+ * config field may legitimately carry an object (a policy record), and calling `.get()` on that
13216
+ * used to throw where a plain read was meant.
13217
+ *
13218
+ * @param value - the field as supplied: live reference, plain value, or absent.
13219
+ * @returns the plain value, or `undefined` when the reference/field is absent.
13220
+ */
13221
+ function unwrapLive(value) {
13222
+ const raw = value;
13223
+ if (typeof raw !== "object" || raw === null) return raw === void 0 ? void 0 : raw;
13224
+ const candidate = raw;
13225
+ return typeof candidate.get === "function" ? candidate.get.call(raw) : raw;
13226
+ }
13227
+ /**
13228
+ * Read a numeric knob (live or plain) and clamp it, warning ONCE per key.
13229
+ *
13230
+ * @param value - the field as supplied: live reference, plain value, or absent.
13231
+ * @param fallback - the package default an invalid value falls back to.
13232
+ * @param opts - bounds plus the optional warning triple.
13233
+ * @returns the value the engine may use.
13234
+ */
13235
+ function liveNumber(value, fallback, opts = {}) {
13236
+ const current = unwrapLive(value);
13237
+ const { min, max } = opts;
13238
+ const result = clampedNumber(current, fallback, min === void 0 ? max === void 0 ? void 0 : { max } : max === void 0 ? { min } : {
13239
+ min,
13240
+ max
13241
+ });
13242
+ const name = opts.name;
13243
+ if (current !== void 0 && result !== current && name !== void 0 && opts.warn !== void 0) {
13244
+ const warned = opts.warned ?? /* @__PURE__ */ new Set();
13245
+ if (!warned.has(name)) {
13246
+ warned.add(name);
13247
+ opts.warn(`${name} provided an invalid value; falling back to the default`);
13248
+ }
13249
+ }
13250
+ return result;
13251
+ }
13252
+ /**
13253
+ * Clamp one numeric knob and warn ONCE per key — the pre-0.22 signature, kept as a thin adapter so
13254
+ * existing callers migrate one at a time instead of in one commit.
13255
+ *
13256
+ * @param warned - the keys this caller already warned about.
13257
+ * @param warn - the warning sink (each package prefixes its own name).
13258
+ * @param name - the config key, for the warning.
13259
+ * @param value - the value as supplied, or its live reference.
13260
+ * @param fallback - the package default a corrected value falls back to.
13261
+ * @param min - the smallest value the engine can act on.
13262
+ * @param max - the largest value the carrier can hold, when bounded.
13263
+ * @returns the value the engine may use.
13264
+ */
13265
+ function clampOnce(warned, warn, name, value, fallback, min, max) {
13266
+ return liveNumber(value, fallback, {
13267
+ min,
13268
+ ...max === void 0 ? {} : { max },
13269
+ warned,
13270
+ warn,
13271
+ name
13272
+ });
13273
+ }
13274
+ /**
13275
+ * Read a boolean flag (live or plain). Absent ⇒ `fallback` (default `false`).
13276
+ *
13277
+ * The boolean counterpart of {@link liveNumber}: `value === true` is never true for a live
13278
+ * reference, which is the silent failure this reader exists to prevent.
13279
+ */
13280
+ function liveBool(value, fallback = false) {
13281
+ return unwrapLive(value) ?? fallback;
13282
+ }
13283
+ /**
13284
+ * Read a string (live or plain). Blank/whitespace counts as unset — the same rule `evolutionRoot` uses
13285
+ * for `DSH_HOME`.
13286
+ */
13287
+ function liveString(value, fallback) {
13288
+ const current = unwrapLive(value)?.trim();
13289
+ return current === void 0 || current.length === 0 ? fallback : current;
13290
+ }
13291
+ /**
13292
+ * Read an enumerated string (live or plain); a value outside `allowed` falls back.
13293
+ *
13294
+ * Used for the mode-like knobs (review mode, trigger, citation policy, write-confirm mode) where a
13295
+ * bare `?? default` would accept a typo the schema never saw.
13296
+ */
13297
+ function liveEnum(value, allowed, fallback) {
13298
+ const current = unwrapLive(value);
13299
+ return current !== void 0 && allowed.includes(current) ? current : fallback;
13300
+ }
13301
+ //#endregion
13446
13302
  //#region lib/types/opt-in.js
13447
13303
  /**
13448
13304
  * Tool names only a session that mounted the family's MODEL rows can see.
@@ -13583,4 +13439,4 @@ function foldUntrustedLines(text, limit) {
13583
13439
  return limit > 0 ? marked.slice(0, limit) : marked;
13584
13440
  }
13585
13441
  //#endregion
13586
- export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LONG_RUNS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, declaredReportStamp, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, payloadText, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
13442
+ export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, atomicWriteFiles, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, compositionRowIds, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, declaredReportStamp, denseBlockSequence, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listDirectories, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeDesktopScope, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, rowBlockSpan, rowSiblingEnd, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, trimTrailingBlankLines, unhookedSupportPointers, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, writePathSymbols, yamlPlainScalarNeedsQuotes };