@lmzhen/dsh-evolution-core 0.22.0 → 0.24.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/README.md +20 -0
- package/lib/index.js +454 -705
- package/lib/types/constants.d.ts +1 -1
- package/lib/types/errors.d.ts +28 -27
- package/lib/types/guards.d.ts +23 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/io.d.ts +44 -103
- package/lib/types/params.d.ts +10 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/preset-composition.d.ts +84 -1
- package/lib/types/skill-row-cells.d.ts +2 -0
- package/lib/types/skill-store.d.ts +9 -49
- package/package.json +1 -1
- package/persisted-write-inventory.json +3 -3
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 {
|
|
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";
|
|
@@ -531,7 +531,7 @@ const PLATFORM_CATALOG_DESCRIPTION_DEFAULT = 500;
|
|
|
531
531
|
* be copied into validateFrontmatter AND the patch path, and neither copy named
|
|
532
532
|
* where the content should go — the upstream cap message names the destination
|
|
533
533
|
* directories, and that is the part a model actually acts on. */
|
|
534
|
-
const CONTENT_SPLIT_HINT = "
|
|
534
|
+
const CONTENT_SPLIT_HINT = "建议拆成更小的 SKILL.md,把支撑文件放进 references/ 或 templates/。";
|
|
535
535
|
/** V27 G2.4: the largest millisecond delay a timer accepts. `AbortSignal.timeout`
|
|
536
536
|
* (and `setTimeout`) coerce anything larger to 1ms after a Node warning, so a
|
|
537
537
|
* timeout configured above this ceiling silently collapses to "immediately
|
|
@@ -1052,16 +1052,16 @@ function shrinksOverLimit(next, current, limit) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMITS, current) {
|
|
1054
1054
|
const parsed = parseFrontmatter(content);
|
|
1055
|
-
if (!parsed) return "SKILL.md
|
|
1055
|
+
if (!parsed) return "SKILL.md 必须以 `---` 行开头,用另一个完全相同的 `---` 行结束 frontmatter(只容忍结尾的 `\\r`),并在其下包含正文。";
|
|
1056
1056
|
const unreadable = parsed.platformStringSplit.filter((entry) => entry.kind === "sequence" || entry.kind === "mapping");
|
|
1057
|
-
if (unreadable.length > 0) return `Frontmatter
|
|
1058
|
-
if (!parsed.frontmatter.name) return "Frontmatter
|
|
1059
|
-
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return
|
|
1060
|
-
if (parsed.frontmatter.name.length > limits.maxNameLength) return
|
|
1061
|
-
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name
|
|
1062
|
-
if (!parsed.frontmatter.description) return "Frontmatter
|
|
1063
|
-
if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `
|
|
1064
|
-
if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md
|
|
1057
|
+
if (unreadable.length > 0) return `Frontmatter 字段 ${unreadable.map((entry) => `"${entry.key}" (a YAML ${entry.kind})`).join(", ")} 必须是字符串:平台技能目录会把这种字段当作缺失,并忽略整个文件。请写成纯文本,或用双引号包住取值以保持字面内容。`;
|
|
1058
|
+
if (!parsed.frontmatter.name) return "Frontmatter 必须包含 name 字段。";
|
|
1059
|
+
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `技能名「${parsed.frontmatter.name}」不合法:请只用小写字母、数字和连字符。`;
|
|
1060
|
+
if (parsed.frontmatter.name.length > limits.maxNameLength) return `技能名超过 ${limits.maxNameLength} 个字符。`;
|
|
1061
|
+
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter 里的 name「${parsed.frontmatter.name}」与目标技能「${expectedName}」不一致。`;
|
|
1062
|
+
if (!parsed.frontmatter.description) return "Frontmatter 必须包含 description 字段。";
|
|
1063
|
+
if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `description 超过 ${limits.maxDescriptionLength} 个字符。`;
|
|
1064
|
+
if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md 内容超过 ${limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`;
|
|
1065
1065
|
return null;
|
|
1066
1066
|
}
|
|
1067
1067
|
/**
|
|
@@ -1076,8 +1076,8 @@ function authoringFeedback(frontmatter) {
|
|
|
1076
1076
|
const over60 = description.length > 60;
|
|
1077
1077
|
const hasColon = description.includes(":");
|
|
1078
1078
|
const lines = [];
|
|
1079
|
-
lines.push(over60 ?
|
|
1080
|
-
if (hasColon) lines.push("
|
|
1079
|
+
lines.push(over60 ? `描述为 ${description.length}/60 个字符 —— 超过 60 字符的作者规范上限(Hermes 标准;技能目录会按平台配置的上限截断)。` : `描述为 ${description.length}/60 个字符 —— 在作者规范上限之内。`);
|
|
1080
|
+
if (hasColon) lines.push("描述里含冒号 —— 请用双引号把整个取值包起来。");
|
|
1081
1081
|
return {
|
|
1082
1082
|
descriptionChars: description.length,
|
|
1083
1083
|
over60,
|
|
@@ -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
|
-
/**
|
|
1518
|
-
*
|
|
1519
|
-
|
|
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
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
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
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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
|
-
/**
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
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
|
|
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
|
|
1623
|
+
await inProcessWrite(path, async () => {
|
|
1897
1624
|
await sweepStaleTmps(path);
|
|
1898
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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-
|
|
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
|
|
@@ -3265,32 +3102,33 @@ function allowRowCollisions(env = process.env) {
|
|
|
3265
3102
|
* @module @lmzhen/dsh-evolution-core
|
|
3266
3103
|
*/
|
|
3267
3104
|
const EVOLUTION_ERRORS = Object.freeze({
|
|
3268
|
-
"e-305-this-invocation-carries-no": "E-305:
|
|
3269
|
-
"e-306-this-deployment-stages-foreground": "E-306:
|
|
3270
|
-
"e-301-approval-service-not-mounted": "E-301:
|
|
3271
|
-
"e-301-approval-service-not-mounted-2": "E-301:
|
|
3272
|
-
"e-304-the-mounted-approval-service": "E-304:
|
|
3273
|
-
"e-302-curator-service-not-mounted": "E-302:
|
|
3274
|
-
"e-305-the-invocation-agent-exposes": "E-305:
|
|
3275
|
-
"e-306-this-deployment-stages-foreground-2": "E-306:
|
|
3276
|
-
"e-303-replay-service-not-mounted": "E-303:
|
|
3277
|
-
"e-307-the-settings-service-could": "E-307:
|
|
3278
|
-
"e-308-unknown-parameter-group-group": "E-308:
|
|
3279
|
-
"e-311-no-settings-service-is": "E-311:
|
|
3280
|
-
"e-312-error-instanceof-Error-error": "E-312: %a1%
|
|
3281
|
-
"e-313-unknown-parameter-id-evolution": "E-313:
|
|
3282
|
-
"e-314-id-is-a-deployment": "E-314:
|
|
3283
|
-
"e-315-id-has-no-user": "E-315:
|
|
3284
|
-
"e-307-the-settings-service-could-2": "E-307:
|
|
3285
|
-
"e-316-namespace-namespace-is-not": "E-316: namespace \"%a1%\"
|
|
3286
|
-
"e-309-revision-conflict-you-sent": "E-309:
|
|
3287
|
-
"e-309-revision-conflict-message-Re": "E-309:
|
|
3288
|
-
"e-310-the-settings-service-refused": "E-310:
|
|
3289
|
-
"e-317-skill-write-not-confirmed": "E-317:
|
|
3290
|
-
"e-318-skill-write-without-a-read": "E-318:
|
|
3291
|
-
"e-320-the-profile-home-is-not": "E-320:
|
|
3292
|
-
"e-
|
|
3293
|
-
"e-
|
|
3105
|
+
"e-305-this-invocation-carries-no": "E-305: 本次调用没有携带 agent —— `%a1%` 需要由会话支撑的调用(请在 GUI 或 CLI 的会话里运行)。",
|
|
3106
|
+
"e-306-this-deployment-stages-foreground": "E-306: 本部署会暂存前台写入,但 `/evolution %a1%` 无法通过技能运行器回放 —— 没有可暂存的内容。请在批准策略为 'never' 的会话里运行,或在 evolution-approval 行上设置 `stageForeground: false`,然后重试该命令。",
|
|
3107
|
+
"e-301-approval-service-not-mounted": "E-301: 批准服务未挂载:待批准写入无法列出或回放。挂载 evolution-approval 行(随 evolution-host/evolution-all 提供)后,运行 /evolution doctor 可查看已挂载的服务。",
|
|
3108
|
+
"e-301-approval-service-not-mounted-2": "E-301: 批准服务未挂载。下一步:evolution-approval 行随 evolution-host/evolution-all 一起提供 —— 运行 /evolution doctor 查看已挂载的服务。",
|
|
3109
|
+
"e-304-the-mounted-approval-service": "E-304: 已挂载的批准服务早于本能力发布 —— 请改为驳回该记录。",
|
|
3110
|
+
"e-302-curator-service-not-mounted": "E-302: 策展服务未挂载。下一步:挂载 evolution-curator 行(evolution-host/evolution-all),并运行 /evolution doctor。",
|
|
3111
|
+
"e-305-the-invocation-agent-exposes": "E-305: 调用 agent 既没有暴露 `followup` 也没有 `inject` —— %a1% 没有投递通道。",
|
|
3112
|
+
"e-306-this-deployment-stages-foreground-2": "E-306: 本部署会暂存前台写入,但本次调用没有 agent 会话 —— `/evolution restructure` 会暂存一条没有会话归属的记录。请在 GUI 或 CLI 的会话里运行,或在 `stageForeground: false` 行上设置,然后重试该命令。",
|
|
3113
|
+
"e-303-replay-service-not-mounted": "E-303: 回放服务未挂载。下一步:挂载 evolution-replay 行(evolution-host/evolution-all),并运行 /evolution doctor。",
|
|
3114
|
+
"e-307-the-settings-service-could": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution params 需要用户层来区分覆盖值与部署值;运行 /evolution doctor 查看已挂载的服务。",
|
|
3115
|
+
"e-308-unknown-parameter-group-group": "E-308: 未知的参数分组「%a1%」—— 已知分组:%a2%。",
|
|
3116
|
+
"e-311-no-settings-service-is": "E-311: 没有挂载设置服务,因此没有可写入的用户层。请挂载 settings 行(packages/settings/settings-file)后重试;本命令从不修改 cordis.yml —— 部署值属于部署。/evolution doctor 会列出已挂载的服务。",
|
|
3117
|
+
"e-312-error-instanceof-Error-error": "E-312: %a1% —— /evolution params 会列出规范 id。",
|
|
3118
|
+
"e-313-unknown-parameter-id-evolution": "E-313: 未知参数「%a1%」—— /evolution params 会列出每个已注册 id 及其层级与用户层。",
|
|
3119
|
+
"e-314-id-is-a-deployment": "E-314: 「%a1%」是部署参数(层级 %a2%,归属 %a3%)—— 它写在 cordis.yml 的插件行或策略行上,而不是从会话写入。/evolution params 会显示每一行由谁可写。",
|
|
3120
|
+
"e-315-id-has-no-user": "E-315: 「%a1%」没有用户层 —— 归属 %a2% 没有发布设置命名空间,因此该值留在部署层。",
|
|
3121
|
+
"e-307-the-settings-service-could-2": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution policy set 需要当前修订号才能安全写入;运行 /evolution doctor 查看已挂载的服务。",
|
|
3122
|
+
"e-316-namespace-namespace-is-not": "E-316: namespace \"%a1%\" 未注册 —— 归属 %a2% 在本组合中未挂载,因此无法从这里写入它的参数。 /evolution params 会把那些行标为 'unregistered'.",
|
|
3123
|
+
"e-309-revision-conflict-you-sent": "E-309: 修订冲突 —— 你提交的是 %a1%,文档当前为 %a2%。请用 /evolution params --json 重新读取后重试。",
|
|
3124
|
+
"e-309-revision-conflict-message-Re": "E-309: 修订冲突 —— %a1%。请用 /evolution params --json 重新读取后重试。",
|
|
3125
|
+
"e-310-the-settings-service-refused": "E-310: 设置服务拒绝了这次写入:%a1% —— 归属插件的规则优先(跨字段成对约束,或只能收紧的上限)。/evolution params 会显示当前值。",
|
|
3126
|
+
"e-317-skill-write-not-confirmed": "E-317: 技能 \"%a1%\" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。",
|
|
3127
|
+
"e-318-skill-write-without-a-read": "E-318: 本会话没有读取技能 \"%a1%\",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。",
|
|
3128
|
+
"e-320-the-profile-home-is-not": "E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。",
|
|
3129
|
+
"e-322-a-parameter-would-break-a": "E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。",
|
|
3130
|
+
"e-321-no-io-provider-is": "E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。",
|
|
3131
|
+
"e-319-skill-write-confirm-timed-out": "E-319: 技能 \"%a1%\" 未被%a2% —— 确认提示在 %a3% 秒内无人应答,因此该写入被取消。没有人在场决定:把 `skillWriteConfirm` 设为 `auto` 可免提示直接写入,保留 `ask` 则一直等到有人应答,或提高 `skillWriteConfirmTimeoutSeconds`。"
|
|
3294
3132
|
});
|
|
3295
3133
|
/**
|
|
3296
3134
|
* Render one table entry, substituting its interpolation slots.
|
|
@@ -3983,7 +3821,7 @@ var MemoryStore = class {
|
|
|
3983
3821
|
const chars = entries.join(ENTRY_DELIMITER).length;
|
|
3984
3822
|
if (this.failureCount > this.maxFailures) return {
|
|
3985
3823
|
ok: false,
|
|
3986
|
-
message:
|
|
3824
|
+
message: `本轮记忆整理已失败 ${this.failureCount} 次。请停止重试记忆调用,继续处理用户的任务。${previewEntries(entries)}`,
|
|
3987
3825
|
entries,
|
|
3988
3826
|
chars,
|
|
3989
3827
|
limit: this.limitFor(target)
|
|
@@ -4046,10 +3884,10 @@ var MemoryStore = class {
|
|
|
4046
3884
|
const oversized = await this.oversizedFile(target);
|
|
4047
3885
|
if (!oversized) return null;
|
|
4048
3886
|
const backup = await this.backupFile(target);
|
|
4049
|
-
const suffix = backup ? `
|
|
3887
|
+
const suffix = backup ? ` 已把备份保存到 ${basename(backup)}。` : "";
|
|
4050
3888
|
return {
|
|
4051
3889
|
ok: false,
|
|
4052
|
-
message:
|
|
3890
|
+
message: `记忆文件为 ${oversized.size} 字节(上限 ${oversized.limit * READ_GUARD_FACTOR})—— 跳过读取。${suffix} 请手动修复该文件后重试。`,
|
|
4053
3891
|
entries: [],
|
|
4054
3892
|
chars: 0,
|
|
4055
3893
|
limit: this.limitFor(target)
|
|
@@ -4082,7 +3920,7 @@ var MemoryStore = class {
|
|
|
4082
3920
|
const size = await this.io.size?.(fileFor(this.root, target));
|
|
4083
3921
|
if (typeof size === "number" && size > SHRINK_LOAD_CEILING) return {
|
|
4084
3922
|
ok: false,
|
|
4085
|
-
message:
|
|
3923
|
+
message: `记忆文件为 ${size} 字节 —— 即使只做删除批处理也过大、无法加载。请手动修复该文件后重试。`,
|
|
4086
3924
|
entries: [],
|
|
4087
3925
|
chars: 0,
|
|
4088
3926
|
limit: this.limitFor(target)
|
|
@@ -4096,7 +3934,7 @@ var MemoryStore = class {
|
|
|
4096
3934
|
});
|
|
4097
3935
|
return outcome ?? {
|
|
4098
3936
|
ok: false,
|
|
4099
|
-
message: "
|
|
3937
|
+
message: "内部错误:记忆写事务没有调用任务;未执行任何写入",
|
|
4100
3938
|
entries: [],
|
|
4101
3939
|
chars: 0,
|
|
4102
3940
|
limit: this.limitFor(target)
|
|
@@ -4108,7 +3946,7 @@ var MemoryStore = class {
|
|
|
4108
3946
|
async addChained(target, facts) {
|
|
4109
3947
|
if (!facts.trim()) return {
|
|
4110
3948
|
ok: false,
|
|
4111
|
-
message: "
|
|
3949
|
+
message: "内容不能为空。",
|
|
4112
3950
|
entries: [],
|
|
4113
3951
|
chars: 0,
|
|
4114
3952
|
limit: this.limitFor(target)
|
|
@@ -4125,7 +3963,7 @@ var MemoryStore = class {
|
|
|
4125
3963
|
if (!content) return {
|
|
4126
3964
|
result: {
|
|
4127
3965
|
ok: false,
|
|
4128
|
-
message: "
|
|
3966
|
+
message: "内容不能为空。",
|
|
4129
3967
|
entries: [],
|
|
4130
3968
|
chars: 0,
|
|
4131
3969
|
limit: this.limitFor(target)
|
|
@@ -4152,7 +3990,7 @@ var MemoryStore = class {
|
|
|
4152
3990
|
if (hasEntryDelimiter(prefixed)) return {
|
|
4153
3991
|
result: {
|
|
4154
3992
|
ok: false,
|
|
4155
|
-
message: "
|
|
3993
|
+
message: "该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。",
|
|
4156
3994
|
entries: [],
|
|
4157
3995
|
chars: 0,
|
|
4158
3996
|
limit: this.limitFor(target)
|
|
@@ -4165,7 +4003,7 @@ var MemoryStore = class {
|
|
|
4165
4003
|
return {
|
|
4166
4004
|
result: {
|
|
4167
4005
|
ok: true,
|
|
4168
|
-
message:
|
|
4006
|
+
message: `该条目已存在(未重复添加)。${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
|
|
4169
4007
|
entries,
|
|
4170
4008
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4171
4009
|
limit: this.limitFor(target)
|
|
@@ -4177,14 +4015,14 @@ var MemoryStore = class {
|
|
|
4177
4015
|
const total = next.join(ENTRY_DELIMITER).length;
|
|
4178
4016
|
const addLimit = this.limitFor(target);
|
|
4179
4017
|
if (addLimit > 0 && total > addLimit) return {
|
|
4180
|
-
result: this.failure(target,
|
|
4018
|
+
result: this.failure(target, `添加该条目会超过 ${addLimit} 字符上限。请先合并或删除过期条目,再重试。`, entries),
|
|
4181
4019
|
write: null
|
|
4182
4020
|
};
|
|
4183
4021
|
this.resetFailures();
|
|
4184
4022
|
return {
|
|
4185
4023
|
result: {
|
|
4186
4024
|
ok: true,
|
|
4187
|
-
message:
|
|
4025
|
+
message: `已添加条目。${this.storageHint(target, total)}`,
|
|
4188
4026
|
entries: next,
|
|
4189
4027
|
chars: total,
|
|
4190
4028
|
limit: this.limitFor(target)
|
|
@@ -4261,10 +4099,9 @@ var MemoryStore = class {
|
|
|
4261
4099
|
if (shrinkOnly) return null;
|
|
4262
4100
|
const limit = this.limitFor(target);
|
|
4263
4101
|
const entries = normalizeEntries(raw);
|
|
4264
|
-
const over = entries.filter((entry) => entry.length > limit).length;
|
|
4265
4102
|
return {
|
|
4266
4103
|
ok: false,
|
|
4267
|
-
message: `${
|
|
4104
|
+
message: `${entries.filter((entry) => entry.length > limit).length} 条记忆超过了配置的 ${target}CharLimit(${limit});它们是在更高上限下写入的。请提高上限或删除条目 —— 删除类操作仍然可用。`,
|
|
4268
4105
|
entries,
|
|
4269
4106
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4270
4107
|
limit
|
|
@@ -4273,7 +4110,7 @@ var MemoryStore = class {
|
|
|
4273
4110
|
const backup = await this.backupFile(target);
|
|
4274
4111
|
return {
|
|
4275
4112
|
ok: false,
|
|
4276
|
-
message:
|
|
4113
|
+
message: `检测到记忆文件被外部改动。${backup ? ` 已把备份保存到 ${basename(backup)}。` : ""} 请先处理该漂移再重试。`,
|
|
4277
4114
|
entries: [],
|
|
4278
4115
|
chars: 0,
|
|
4279
4116
|
limit: this.limitFor(target)
|
|
@@ -4285,7 +4122,7 @@ var MemoryStore = class {
|
|
|
4285
4122
|
async applyBatchChained(target, operations) {
|
|
4286
4123
|
if (operations.length === 0) return {
|
|
4287
4124
|
ok: false,
|
|
4288
|
-
message: "operations
|
|
4125
|
+
message: "operations 列表为空。",
|
|
4289
4126
|
entries: [],
|
|
4290
4127
|
chars: 0,
|
|
4291
4128
|
limit: this.limitFor(target)
|
|
@@ -4310,7 +4147,7 @@ var MemoryStore = class {
|
|
|
4310
4147
|
if (!body) return {
|
|
4311
4148
|
result: {
|
|
4312
4149
|
ok: false,
|
|
4313
|
-
message:
|
|
4150
|
+
message: `第 ${position} 个操作(add):必须提供 facts。未应用任何操作。${previewEntries(entries)}`,
|
|
4314
4151
|
entries,
|
|
4315
4152
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4316
4153
|
limit: this.limitFor(target)
|
|
@@ -4321,7 +4158,7 @@ var MemoryStore = class {
|
|
|
4321
4158
|
if (threat) return {
|
|
4322
4159
|
result: {
|
|
4323
4160
|
ok: false,
|
|
4324
|
-
message:
|
|
4161
|
+
message: `第 ${position} 个操作:${threat}${previewEntries(entries)}`,
|
|
4325
4162
|
entries,
|
|
4326
4163
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4327
4164
|
limit: this.limitFor(target)
|
|
@@ -4332,7 +4169,7 @@ var MemoryStore = class {
|
|
|
4332
4169
|
if (hasEntryDelimiter(entryBody)) return {
|
|
4333
4170
|
result: {
|
|
4334
4171
|
ok: false,
|
|
4335
|
-
message:
|
|
4172
|
+
message: `第 ${position} 个操作(add):该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。${previewEntries(entries)}`,
|
|
4336
4173
|
entries,
|
|
4337
4174
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4338
4175
|
limit: this.limitFor(target)
|
|
@@ -4346,7 +4183,7 @@ var MemoryStore = class {
|
|
|
4346
4183
|
if (rawAction !== "remove" && rawAction !== "replace") return {
|
|
4347
4184
|
result: {
|
|
4348
4185
|
ok: false,
|
|
4349
|
-
message:
|
|
4186
|
+
message: `第 ${position} 个操作:未知动作「${String(rawAction)}」(应为 add/remove/replace)。未应用任何操作。${previewEntries(entries)}`,
|
|
4350
4187
|
entries,
|
|
4351
4188
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4352
4189
|
limit: this.limitFor(target)
|
|
@@ -4357,7 +4194,7 @@ var MemoryStore = class {
|
|
|
4357
4194
|
if (!needle) return {
|
|
4358
4195
|
result: {
|
|
4359
4196
|
ok: false,
|
|
4360
|
-
message:
|
|
4197
|
+
message: `第 ${position} 个操作(${op.action}):必须提供 old_text。未应用任何操作。${previewEntries(entries)}`,
|
|
4361
4198
|
entries,
|
|
4362
4199
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4363
4200
|
limit: this.limitFor(target)
|
|
@@ -4369,13 +4206,13 @@ var MemoryStore = class {
|
|
|
4369
4206
|
matchIndex
|
|
4370
4207
|
})).filter(({ entry }) => entry.includes(needle));
|
|
4371
4208
|
if (matches.length === 0) return {
|
|
4372
|
-
result: this.failure(target,
|
|
4209
|
+
result: this.failure(target, `第 ${position} 个操作:找不到匹配 "${needle}" 的条目。未应用任何操作。`, entries),
|
|
4373
4210
|
write: null
|
|
4374
4211
|
};
|
|
4375
4212
|
if (new Set(matches.map((m) => m.entry)).size > 1) return {
|
|
4376
4213
|
result: {
|
|
4377
4214
|
ok: false,
|
|
4378
|
-
message:
|
|
4215
|
+
message: `第 ${position} 个操作:「${needle}」匹配到多条不同条目。未应用任何操作。${previewEntries(entries)}`,
|
|
4379
4216
|
entries,
|
|
4380
4217
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4381
4218
|
limit: this.limitFor(target)
|
|
@@ -4389,7 +4226,7 @@ var MemoryStore = class {
|
|
|
4389
4226
|
if (!body) return {
|
|
4390
4227
|
result: {
|
|
4391
4228
|
ok: false,
|
|
4392
|
-
message:
|
|
4229
|
+
message: `第 ${position} 个操作(replace):必须提供 facts。${previewEntries(entries)}`,
|
|
4393
4230
|
entries,
|
|
4394
4231
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4395
4232
|
limit: this.limitFor(target)
|
|
@@ -4400,7 +4237,7 @@ var MemoryStore = class {
|
|
|
4400
4237
|
if (threat) return {
|
|
4401
4238
|
result: {
|
|
4402
4239
|
ok: false,
|
|
4403
|
-
message:
|
|
4240
|
+
message: `第 ${position} 个操作:${threat}${previewEntries(entries)}`,
|
|
4404
4241
|
entries,
|
|
4405
4242
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4406
4243
|
limit: this.limitFor(target)
|
|
@@ -4411,7 +4248,7 @@ var MemoryStore = class {
|
|
|
4411
4248
|
if (hasEntryDelimiter(entryBody)) return {
|
|
4412
4249
|
result: {
|
|
4413
4250
|
ok: false,
|
|
4414
|
-
message:
|
|
4251
|
+
message: `第 ${position} 个操作(replace):该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。${previewEntries(entries)}`,
|
|
4415
4252
|
entries,
|
|
4416
4253
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
4417
4254
|
limit: this.limitFor(target)
|
|
@@ -4424,14 +4261,14 @@ var MemoryStore = class {
|
|
|
4424
4261
|
const total = working.join(ENTRY_DELIMITER).length;
|
|
4425
4262
|
const batchLimit = this.limitFor(target);
|
|
4426
4263
|
if (batchLimit > 0 && total > batchLimit) return {
|
|
4427
|
-
result: this.failure(target,
|
|
4264
|
+
result: this.failure(target, `批处理结果(${total} 字符)超过 ${batchLimit} 上限。请在同一个批里再删除或缩短一些条目。`, entries),
|
|
4428
4265
|
write: null
|
|
4429
4266
|
};
|
|
4430
4267
|
this.resetFailures();
|
|
4431
4268
|
return {
|
|
4432
4269
|
result: {
|
|
4433
4270
|
ok: true,
|
|
4434
|
-
message:
|
|
4271
|
+
message: `已应用 ${operations.length} 个操作。${this.storageHint(target, total)}`,
|
|
4435
4272
|
entries: working,
|
|
4436
4273
|
chars: total,
|
|
4437
4274
|
limit: this.limitFor(target)
|
|
@@ -4532,7 +4369,7 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
4532
4369
|
console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
|
|
4533
4370
|
return current;
|
|
4534
4371
|
}
|
|
4535
|
-
if (
|
|
4372
|
+
if (isRecord(parsed)) {
|
|
4536
4373
|
const version = parsed.version;
|
|
4537
4374
|
if (typeof version === "number" && version > 1) {
|
|
4538
4375
|
console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
|
|
@@ -4596,16 +4433,23 @@ function composePresetComposition(standardComposition, deltaComposition) {
|
|
|
4596
4433
|
* test can keep — the entries are now data, and a divergence is impossible.
|
|
4597
4434
|
*/
|
|
4598
4435
|
const ROW_OVERRIDES_URL = new URL("../row-overrides.json", import.meta.url);
|
|
4599
|
-
|
|
4436
|
+
/** One cache per PATH: the installer reads the same file through its own root, and a single-slot
|
|
4437
|
+
* cache would hand the second caller the first caller's bytes. */
|
|
4438
|
+
const overridesCache = /* @__PURE__ */ new Map();
|
|
4600
4439
|
/**
|
|
4601
4440
|
* Read and validate the shared override table.
|
|
4441
|
+
* G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
|
|
4442
|
+
* runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
|
|
4443
|
+
* read would look for `scripts/../row-overrides.json` and fail.
|
|
4444
|
+
* @param path - the table to read.
|
|
4602
4445
|
* @returns the entries, in file order. A malformed table fails LOUD: a composer
|
|
4603
4446
|
* that silently skipped an entry would ship a preset running the platform
|
|
4604
4447
|
* default while nothing downstream reported it.
|
|
4605
4448
|
*/
|
|
4606
|
-
function loadRowOverrides() {
|
|
4607
|
-
|
|
4608
|
-
|
|
4449
|
+
function loadRowOverrides(path = fileURLToPath(ROW_OVERRIDES_URL)) {
|
|
4450
|
+
const cached = overridesCache.get(path);
|
|
4451
|
+
if (cached !== void 0) return cached;
|
|
4452
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
4609
4453
|
if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
|
|
4610
4454
|
const entries = [];
|
|
4611
4455
|
for (const [index, entry] of parsed.entries()) {
|
|
@@ -4618,7 +4462,7 @@ function loadRowOverrides() {
|
|
|
4618
4462
|
missingReason: candidate.missingReason
|
|
4619
4463
|
});
|
|
4620
4464
|
}
|
|
4621
|
-
|
|
4465
|
+
overridesCache.set(path, entries);
|
|
4622
4466
|
return entries;
|
|
4623
4467
|
}
|
|
4624
4468
|
/**
|
|
@@ -4737,7 +4581,7 @@ function composePresetRow(baseComposition, deltaComposition, identity) {
|
|
|
4737
4581
|
}
|
|
4738
4582
|
/** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
|
|
4739
4583
|
* preset the platform does not ship has to arrive inside an `insert` entry
|
|
4740
|
-
* (`
|
|
4584
|
+
* (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
|
|
4741
4585
|
const PATCH_INSERT_KEY = "insert";
|
|
4742
4586
|
/**
|
|
4743
4587
|
* Wrap a composed row as the patch entry a profile patch needs.
|
|
@@ -6116,7 +5960,7 @@ function newRunRegistry(options) {
|
|
|
6116
5960
|
const parsed = parseIndex(raw);
|
|
6117
5961
|
if (!parsed.ok) return {
|
|
6118
5962
|
ok: false,
|
|
6119
|
-
note: "
|
|
5963
|
+
note: "运行索引读不出来(不是 JSON,或没有 runs 数组)—— 上一次的运行历史未知(报告不受影响)"
|
|
6120
5964
|
};
|
|
6121
5965
|
const wasRunning = new Set([...foreign.values()].filter((row) => row.state === "running").map((row) => row.id));
|
|
6122
5966
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6411,7 +6255,7 @@ function renderLongRunTable() {
|
|
|
6411
6255
|
/**
|
|
6412
6256
|
* Every platform service the family reaches for with `ctx.get`, and what degrades without it.
|
|
6413
6257
|
*
|
|
6414
|
-
* O-2 (v46 audit): the family probes
|
|
6258
|
+
* O-2 (v46 audit): the family probes 28 platform services optionally — the platform has no
|
|
6415
6259
|
* formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
|
|
6416
6260
|
* surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
|
|
6417
6261
|
* them; this table is the single source it judges the rest from, and the arch guard cross-checks it
|
|
@@ -6421,7 +6265,7 @@ function renderLongRunTable() {
|
|
|
6421
6265
|
* `client` probes run in the browser half, where the host has no view of the seats.
|
|
6422
6266
|
* @module @lmzhen/dsh-evolution-core
|
|
6423
6267
|
*/
|
|
6424
|
-
/** The
|
|
6268
|
+
/** The 28 probes, in service-name order (the guard checks this list against the tree). */
|
|
6425
6269
|
const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
6426
6270
|
{
|
|
6427
6271
|
service: "agents",
|
|
@@ -6458,6 +6302,16 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
6458
6302
|
feature: "every durable read and write (the IO seam)",
|
|
6459
6303
|
side: "host"
|
|
6460
6304
|
},
|
|
6305
|
+
{
|
|
6306
|
+
service: "fs",
|
|
6307
|
+
feature: "the platform filesystem the evolution-io-fs backend maps the seam onto (locks + writeText(expected) version CAS)",
|
|
6308
|
+
side: "host"
|
|
6309
|
+
},
|
|
6310
|
+
{
|
|
6311
|
+
service: "hmr",
|
|
6312
|
+
feature: "the preset install read-merge-write commit (the platform HMR lock serializes it)",
|
|
6313
|
+
side: "host"
|
|
6314
|
+
},
|
|
6461
6315
|
{
|
|
6462
6316
|
service: "evolutionMemoryBudget",
|
|
6463
6317
|
feature: "the memory-budget divergence section of this report",
|
|
@@ -8007,6 +7861,13 @@ function versionRowCells(row) {
|
|
|
8007
7861
|
tone: "tertiary",
|
|
8008
7862
|
slot: "meta"
|
|
8009
7863
|
});
|
|
7864
|
+
if (row.hash !== void 0 && row.hash !== "") title.push({
|
|
7865
|
+
key: "hash",
|
|
7866
|
+
literal: row.hash.slice(0, 7),
|
|
7867
|
+
tone: "tertiary",
|
|
7868
|
+
slot: "meta",
|
|
7869
|
+
note: row.hash
|
|
7870
|
+
});
|
|
8010
7871
|
const meta = [];
|
|
8011
7872
|
if (row.summary !== void 0 && row.summary !== "") meta.push({
|
|
8012
7873
|
key: "summary",
|
|
@@ -8055,6 +7916,13 @@ function skillRowCells(skill) {
|
|
|
8055
7916
|
tone: "tertiary",
|
|
8056
7917
|
slot: "meta"
|
|
8057
7918
|
}];
|
|
7919
|
+
if (skill.protectedBy !== null) meta.push({
|
|
7920
|
+
key: "protectedBy",
|
|
7921
|
+
literal: skill.protectedBy,
|
|
7922
|
+
tone: "tertiary",
|
|
7923
|
+
slot: "meta",
|
|
7924
|
+
note: skill.protectedBy
|
|
7925
|
+
});
|
|
8058
7926
|
if (skill.age !== null) meta.push({
|
|
8059
7927
|
key: "age",
|
|
8060
7928
|
copy: ageCopyKey(skill.age),
|
|
@@ -8710,7 +8578,7 @@ function contentRetentionFeedback(input) {
|
|
|
8710
8578
|
if (before === null || after === null) return null;
|
|
8711
8579
|
if (before.length < 200) return null;
|
|
8712
8580
|
if (after.length >= before.length * .5) return null;
|
|
8713
|
-
return
|
|
8581
|
+
return `新内容保留了上一版正文的 ${Math.round(after.length / before.length * 100)}%(${after.length}/${before.length} 字符);被替换的版本已保留在该技能的历史里。`;
|
|
8714
8582
|
}
|
|
8715
8583
|
/**
|
|
8716
8584
|
* Read one skill's versions, oldest first.
|
|
@@ -9002,7 +8870,7 @@ function elapsedSince(at, nowMs) {
|
|
|
9002
8870
|
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
9003
8871
|
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
9004
8872
|
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
9005
|
-
* with `
|
|
8873
|
+
* with `supportDirsUnlistable` before any destructive move (archive/restore/
|
|
9006
8874
|
* snapshot); held inside transactIo by byte writers.
|
|
9007
8875
|
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
9008
8876
|
* must either live inside the serial section that commits the write, or
|
|
@@ -9078,7 +8946,7 @@ function anchorRefusal(name, verdict) {
|
|
|
9078
8946
|
ok: false,
|
|
9079
8947
|
stale: true,
|
|
9080
8948
|
anchor: verdict,
|
|
9081
|
-
message: verdict === "missing" ?
|
|
8949
|
+
message: verdict === "missing" ? `技能「${name}」不存在。` : `技能 "${name}" 在读取之后被改动过;为避免覆盖更新的内容,本次写入被拒绝。`
|
|
9082
8950
|
};
|
|
9083
8951
|
}
|
|
9084
8952
|
/**
|
|
@@ -9101,7 +8969,7 @@ function anchorUnverifiable(name, filePath) {
|
|
|
9101
8969
|
ok: false,
|
|
9102
8970
|
stale: true,
|
|
9103
8971
|
anchor: "drift",
|
|
9104
|
-
message: filePath === null ?
|
|
8972
|
+
message: filePath === null ? `无法读取技能 "${name}" 以校验暂存内容。` : `无法读取技能 "${name}" 的支撑文件 "${filePath}" 以校验暂存内容。`
|
|
9105
8973
|
};
|
|
9106
8974
|
}
|
|
9107
8975
|
function anchorRefusalFile(name, filePath, verdict) {
|
|
@@ -9109,7 +8977,7 @@ function anchorRefusalFile(name, filePath, verdict) {
|
|
|
9109
8977
|
ok: false,
|
|
9110
8978
|
stale: true,
|
|
9111
8979
|
anchor: verdict,
|
|
9112
|
-
message: verdict === "missing" ?
|
|
8980
|
+
message: verdict === "missing" ? `文件「${filePath}」不在技能「${name}」里。` : `技能 "${name}" 的支撑文件 "${filePath}" 在读取之后被改动过;为避免覆盖更新的内容,本次写入被拒绝。`
|
|
9113
8981
|
};
|
|
9114
8982
|
}
|
|
9115
8983
|
/**
|
|
@@ -9127,7 +8995,7 @@ function anchorRefusalFile(name, filePath, verdict) {
|
|
|
9127
8995
|
function unreadableTarget(label, error) {
|
|
9128
8996
|
return {
|
|
9129
8997
|
ok: false,
|
|
9130
|
-
message:
|
|
8998
|
+
message: `读不出「${label}」:${error?.code === "EISDIR" ? "the path is a DIRECTORY, not a file" : error instanceof Error ? error.message : String(error)}。没有写入任何内容。`
|
|
9131
8999
|
};
|
|
9132
9000
|
}
|
|
9133
9001
|
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
@@ -9239,15 +9107,6 @@ function skillDir(root, name) {
|
|
|
9239
9107
|
function markerEntryName(marker) {
|
|
9240
9108
|
return `.${marker}`;
|
|
9241
9109
|
}
|
|
9242
|
-
/** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
|
|
9243
|
-
* active writer (skill body + the two marker writers). Single source with
|
|
9244
|
-
* `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
|
|
9245
|
-
* of the ghost-writer probe. */
|
|
9246
|
-
const MARKER_LOCK_NAMES = [
|
|
9247
|
-
`SKILL.md${LOCK_SUFFIX}`,
|
|
9248
|
-
`.pinned${LOCK_SUFFIX}`,
|
|
9249
|
-
`.hermes-managed${LOCK_SUFFIX}`
|
|
9250
|
-
];
|
|
9251
9110
|
/** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
|
|
9252
9111
|
const ARCHIVE_RETENTION_DAYS = 365;
|
|
9253
9112
|
function markerPath(dir, marker) {
|
|
@@ -9320,16 +9179,15 @@ function validateRestructureTarget(filePath) {
|
|
|
9320
9179
|
}
|
|
9321
9180
|
function validateSupportPath(filePath) {
|
|
9322
9181
|
const normalized = filePath.replace(/\\/g, "/");
|
|
9323
|
-
if (normalized.includes("..")) return "
|
|
9182
|
+
if (normalized.includes("..")) return "不允许路径穿越。";
|
|
9324
9183
|
const parts = normalized.split("/").filter(Boolean);
|
|
9325
9184
|
if (parts.length === 0 || !SUPPORT_DIRS.includes(parts[0])) return `file_path must be under one of: ${SUPPORT_DIRS.join(", ")}.`;
|
|
9326
|
-
if (parts.length < 2) return "
|
|
9185
|
+
if (parts.length < 2) return "请给出文件名,不能只给目录。";
|
|
9327
9186
|
for (const part of parts.slice(1)) {
|
|
9328
|
-
if (!SUPPORT_FILE_NAME_RE.test(part)) return
|
|
9329
|
-
if (part.toLowerCase().endsWith(".
|
|
9330
|
-
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.`;
|
|
9187
|
+
if (!SUPPORT_FILE_NAME_RE.test(part)) return `不支持的文件名「${part}」:请只用小写字母、数字、点、连字符和下划线(首字符须为字母或数字)。`;
|
|
9188
|
+
if (part.toLowerCase().endsWith(".corrupt") || part.toLowerCase().endsWith(".tmp")) return `不支持的文件名「${part}」:.corrupt/.tmp 后缀由状态与 IO 协议保留。`;
|
|
9331
9189
|
const stem = part.split(".")[0]?.toLowerCase() ?? "";
|
|
9332
|
-
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return
|
|
9190
|
+
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `不支持的文件名「${part}」:这是 Windows 保留设备名。`;
|
|
9333
9191
|
}
|
|
9334
9192
|
return null;
|
|
9335
9193
|
}
|
|
@@ -9478,7 +9336,7 @@ var SkillLibrary = class {
|
|
|
9478
9336
|
const o = outcome;
|
|
9479
9337
|
if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
|
|
9480
9338
|
ok: false,
|
|
9481
|
-
message: "
|
|
9339
|
+
message: "内部错误:写事务没有调用任务;未执行任何写入"
|
|
9482
9340
|
};
|
|
9483
9341
|
if (o.ghostDir === true) await this.cleanupGhostDir(path);
|
|
9484
9342
|
let result = o.result;
|
|
@@ -9498,7 +9356,7 @@ var SkillLibrary = class {
|
|
|
9498
9356
|
if (o.write !== null && o.event) this.notifyMutation(o.event);
|
|
9499
9357
|
return durabilityWarning === "" || !result.ok ? result : {
|
|
9500
9358
|
...result,
|
|
9501
|
-
message: `${result.message}
|
|
9359
|
+
message: `${result.message} (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`
|
|
9502
9360
|
};
|
|
9503
9361
|
}
|
|
9504
9362
|
/**
|
|
@@ -9506,16 +9364,14 @@ var SkillLibrary = class {
|
|
|
9506
9364
|
* found no SKILL.md — remove the possibly-resurrected directory ONLY when it
|
|
9507
9365
|
* holds nothing but this path's own write-lock file. The BR-5 rule from
|
|
9508
9366
|
* setPinnedCore/createCore applies unchanged: a concurrent mover can land a
|
|
9509
|
-
* full directory between the list probe and the remove, so
|
|
9510
|
-
*
|
|
9511
|
-
* away. Best-effort: a failed cleanup leaves the "not found" result
|
|
9367
|
+
* full directory between the list probe and the remove, so ANY content at all (support files, a
|
|
9368
|
+
* fresh restore) must never be recursed away. Best-effort: a failed cleanup leaves the "not found" result
|
|
9512
9369
|
* unchanged (the operator-facing ghost-dir refusal is fail-loud already).
|
|
9513
9370
|
*/
|
|
9514
9371
|
async cleanupGhostDir(skillFilePath) {
|
|
9515
9372
|
const dir = dirname(skillFilePath);
|
|
9516
|
-
const lockName = `${basename(skillFilePath)}${LOCK_SUFFIX}`;
|
|
9517
9373
|
try {
|
|
9518
|
-
if ((await this.io.list(dir)).
|
|
9374
|
+
if ((await this.io.list(dir)).length > 0) return;
|
|
9519
9375
|
await this.io.remove(dir);
|
|
9520
9376
|
} catch {}
|
|
9521
9377
|
}
|
|
@@ -9663,7 +9519,7 @@ var SkillLibrary = class {
|
|
|
9663
9519
|
*/
|
|
9664
9520
|
badName(name, opts = {}) {
|
|
9665
9521
|
const normalized = name.trim();
|
|
9666
|
-
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return
|
|
9522
|
+
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `技能名「${normalized}」不合法:请只用小写字母、数字和连字符(不超过 ${this.limits.maxNameLength} 个)。`;
|
|
9667
9523
|
if (!opts.allowReserved && WIN32_RESERVED_DEVICE_NAMES.has(normalized)) return `"${normalized}" is a Windows reserved device name and cannot be used as a skill name.`;
|
|
9668
9524
|
return null;
|
|
9669
9525
|
}
|
|
@@ -9922,7 +9778,7 @@ var SkillLibrary = class {
|
|
|
9922
9778
|
};
|
|
9923
9779
|
if (origin === "background_review") return {
|
|
9924
9780
|
ok: false,
|
|
9925
|
-
message: "
|
|
9781
|
+
message: "只有前台(用户或主智能体)可以固定或取消固定技能。"
|
|
9926
9782
|
};
|
|
9927
9783
|
let durabilityWarning = "";
|
|
9928
9784
|
const dir = this.dirOf(normalized);
|
|
@@ -9930,17 +9786,17 @@ var SkillLibrary = class {
|
|
|
9930
9786
|
const existing = await this.io.exists(marker);
|
|
9931
9787
|
if (pinned && existing) return {
|
|
9932
9788
|
ok: true,
|
|
9933
|
-
message:
|
|
9789
|
+
message: `技能「${normalized}」已被固定。`,
|
|
9934
9790
|
path: dir
|
|
9935
9791
|
};
|
|
9936
9792
|
if (!pinned && !existing) return {
|
|
9937
9793
|
ok: true,
|
|
9938
|
-
message:
|
|
9794
|
+
message: `技能「${normalized}」未被固定,无需处理。`,
|
|
9939
9795
|
path: dir
|
|
9940
9796
|
};
|
|
9941
9797
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
9942
9798
|
ok: false,
|
|
9943
|
-
message:
|
|
9799
|
+
message: `技能「${normalized}」不存在。`
|
|
9944
9800
|
};
|
|
9945
9801
|
if (pinned) {
|
|
9946
9802
|
try {
|
|
@@ -9954,14 +9810,14 @@ var SkillLibrary = class {
|
|
|
9954
9810
|
await this.cleanupGhostDir(join(dir, "SKILL.md"));
|
|
9955
9811
|
return {
|
|
9956
9812
|
ok: false,
|
|
9957
|
-
message:
|
|
9813
|
+
message: `固定过程中技能「${normalized}」被并发归档;残留标记已移除:等搬运完成后重试。`
|
|
9958
9814
|
};
|
|
9959
9815
|
}
|
|
9960
9816
|
} else await this.io.remove(marker);
|
|
9961
9817
|
await this.audit(normalized, pinned ? "pin" : "unpin", null, null, pinned ? "pinned" : "unpinned");
|
|
9962
9818
|
return {
|
|
9963
9819
|
ok: true,
|
|
9964
|
-
message: `${pinned ?
|
|
9820
|
+
message: `${pinned ? `技能 "${normalized}" 已固定:免受删除、后台复核与生命周期影响。` : `技能 "${normalized}" 已取消固定。`}${durabilityWarning === "" ? "" : ` (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`}`,
|
|
9965
9821
|
path: dir
|
|
9966
9822
|
};
|
|
9967
9823
|
}
|
|
@@ -9983,7 +9839,7 @@ var SkillLibrary = class {
|
|
|
9983
9839
|
const norm = normalizeFrontmatter(content);
|
|
9984
9840
|
if (norm.issues.length > 0) return {
|
|
9985
9841
|
ok: false,
|
|
9986
|
-
message:
|
|
9842
|
+
message: `技能「${normalized}」的 frontmatter 无法自动修复:${norm.issues[0]}:请用双引号包住取值后重试。`
|
|
9987
9843
|
};
|
|
9988
9844
|
const finalContent = norm.changed ? norm.content : content;
|
|
9989
9845
|
if (norm.changed) {
|
|
@@ -10001,7 +9857,7 @@ var SkillLibrary = class {
|
|
|
10001
9857
|
const dir = this.dirOf(normalized);
|
|
10002
9858
|
if (await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
10003
9859
|
ok: false,
|
|
10004
|
-
message:
|
|
9860
|
+
message: `技能「${normalized}」已存在。`
|
|
10005
9861
|
};
|
|
10006
9862
|
let rootEntries;
|
|
10007
9863
|
try {
|
|
@@ -10010,17 +9866,17 @@ var SkillLibrary = class {
|
|
|
10010
9866
|
const cause = error instanceof Error ? error.message : String(error);
|
|
10011
9867
|
return {
|
|
10012
9868
|
ok: false,
|
|
10013
|
-
message:
|
|
9869
|
+
message: `无法检查技能「${normalized}」是否与大小写变体冲突:列出 ${this.root} 失败(${cause})。拒绝创建 —— 仅大小写不同的第二个目录在跨平台时无法撤销。`
|
|
10014
9870
|
};
|
|
10015
9871
|
}
|
|
10016
9872
|
for (const entry of rootEntries) if (typeof entry === "string" && entry !== normalized && entry.toLowerCase() === normalized.toLowerCase()) return {
|
|
10017
9873
|
ok: false,
|
|
10018
|
-
message:
|
|
9874
|
+
message: `技能「${normalized}」与已存在的大小写变体目录「${entry}」冲突(技能名只允许小写):请重命名其中一个。`
|
|
10019
9875
|
};
|
|
10020
9876
|
const protection = await this.writeProtection(normalized, origin);
|
|
10021
9877
|
if (protection) return {
|
|
10022
9878
|
ok: false,
|
|
10023
|
-
message:
|
|
9879
|
+
message: `技能「${normalized}」受保护(${protection})。`
|
|
10024
9880
|
};
|
|
10025
9881
|
const onDisk = finalContent.trimEnd() + "\n";
|
|
10026
9882
|
const createPath = join(dir, "SKILL.md");
|
|
@@ -10055,18 +9911,18 @@ var SkillLibrary = class {
|
|
|
10055
9911
|
}
|
|
10056
9912
|
if (!taskRan) return {
|
|
10057
9913
|
ok: false,
|
|
10058
|
-
message: "
|
|
9914
|
+
message: "内部错误:创建事务没有调用任务;未写入任何文件"
|
|
10059
9915
|
};
|
|
10060
9916
|
if (existsAtCommit) return {
|
|
10061
9917
|
ok: false,
|
|
10062
|
-
message:
|
|
9918
|
+
message: `技能「${normalized}」已存在。`
|
|
10063
9919
|
};
|
|
10064
9920
|
let markerWarning = "";
|
|
10065
9921
|
if (origin !== "foreground") {
|
|
10066
9922
|
try {
|
|
10067
9923
|
await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
10068
9924
|
} catch (error) {
|
|
10069
|
-
if (isCommittedOnly(error)) markerWarning = `
|
|
9925
|
+
if (isCommittedOnly(error)) markerWarning = `hermes-managed 标记已落盘,但其目录 fsync 失败 —— 持久性未确认:${error instanceof Error ? error.message : String(error)}`;
|
|
10070
9926
|
else markerWarning = `hermes-managed marker write failed (${error instanceof Error ? error.message : String(error)}); the skill landed but is NOT lifecycle-managed — recreate or pin it manually`;
|
|
10071
9927
|
}
|
|
10072
9928
|
if (!await this.io.exists(createPath)) {
|
|
@@ -10074,7 +9930,7 @@ var SkillLibrary = class {
|
|
|
10074
9930
|
await this.cleanupGhostDir(createPath);
|
|
10075
9931
|
return {
|
|
10076
9932
|
ok: false,
|
|
10077
|
-
message:
|
|
9933
|
+
message: `创建过程中技能「${normalized}」被并发归档;残留标记已移除:等搬运完成后重试一次。`
|
|
10078
9934
|
};
|
|
10079
9935
|
}
|
|
10080
9936
|
}
|
|
@@ -10084,10 +9940,10 @@ var SkillLibrary = class {
|
|
|
10084
9940
|
name: normalized,
|
|
10085
9941
|
skillDir: dir
|
|
10086
9942
|
});
|
|
10087
|
-
const warnings = [createDurabilityWarning !== "" ?
|
|
9943
|
+
const warnings = [createDurabilityWarning !== "" ? `写入已落盘,但目录 fsync 失败 —— 持久性未确认:${createDurabilityWarning}` : "", markerWarning].filter((warning) => warning !== "");
|
|
10088
9944
|
return {
|
|
10089
9945
|
ok: true,
|
|
10090
|
-
message:
|
|
9946
|
+
message: `已创建技能「${normalized}」。${warnings.length === 0 ? "" : ` (警告:${warnings.join(";")})`}`,
|
|
10091
9947
|
path: dir,
|
|
10092
9948
|
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
10093
9949
|
};
|
|
@@ -10107,7 +9963,7 @@ var SkillLibrary = class {
|
|
|
10107
9963
|
const protection = await this.writeProtection(name, origin);
|
|
10108
9964
|
if (protection) return {
|
|
10109
9965
|
ok: false,
|
|
10110
|
-
message:
|
|
9966
|
+
message: `技能「${name}」受保护(${protection})。`
|
|
10111
9967
|
};
|
|
10112
9968
|
const limitProbe = await probeText(this.io, path);
|
|
10113
9969
|
if (isUnknown(limitProbe)) console.warn(`skill-store: could not re-read ${path} for the shrink exemption (${limitProbe.reason}); validating without it — an over-limit repair is refused, not accepted`);
|
|
@@ -10120,7 +9976,7 @@ var SkillLibrary = class {
|
|
|
10120
9976
|
const norm = normalizeFrontmatter(content);
|
|
10121
9977
|
if (norm.issues.length > 0) return {
|
|
10122
9978
|
ok: false,
|
|
10123
|
-
message:
|
|
9979
|
+
message: `技能「${name}」的 frontmatter 无法自动修复:${norm.issues[0]}:请用双引号包住取值后重试。`
|
|
10124
9980
|
};
|
|
10125
9981
|
const finalContent = norm.changed ? norm.content : content;
|
|
10126
9982
|
if (norm.changed) {
|
|
@@ -10144,14 +10000,14 @@ var SkillLibrary = class {
|
|
|
10144
10000
|
if (current === null) return {
|
|
10145
10001
|
result: {
|
|
10146
10002
|
ok: false,
|
|
10147
|
-
message:
|
|
10003
|
+
message: `技能「${name}」不存在。`
|
|
10148
10004
|
},
|
|
10149
10005
|
write: null
|
|
10150
10006
|
};
|
|
10151
10007
|
if (finalContent.trimEnd() === current.trimEnd()) return {
|
|
10152
10008
|
result: {
|
|
10153
10009
|
ok: true,
|
|
10154
|
-
message:
|
|
10010
|
+
message: `技能「${name}」未变更:提供的内容与当前文件一致,未写入。`,
|
|
10155
10011
|
noop: true,
|
|
10156
10012
|
path: dir
|
|
10157
10013
|
},
|
|
@@ -10161,7 +10017,7 @@ var SkillLibrary = class {
|
|
|
10161
10017
|
return {
|
|
10162
10018
|
result: {
|
|
10163
10019
|
ok: true,
|
|
10164
|
-
message:
|
|
10020
|
+
message: `技能「${name}」已更新。`,
|
|
10165
10021
|
path: dir,
|
|
10166
10022
|
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
10167
10023
|
},
|
|
@@ -10195,12 +10051,12 @@ var SkillLibrary = class {
|
|
|
10195
10051
|
const skillMd = join(dir, "SKILL.md");
|
|
10196
10052
|
if (!await this.io.exists(skillMd)) return {
|
|
10197
10053
|
ok: false,
|
|
10198
|
-
message:
|
|
10054
|
+
message: `技能「${name}」不存在。`
|
|
10199
10055
|
};
|
|
10200
10056
|
const protection = await this.writeProtection(name, origin);
|
|
10201
10057
|
if (protection) return {
|
|
10202
10058
|
ok: false,
|
|
10203
|
-
message:
|
|
10059
|
+
message: `技能「${name}」受保护(${protection})。`
|
|
10204
10060
|
};
|
|
10205
10061
|
let target = skillMd;
|
|
10206
10062
|
let patchLabel = "SKILL.md";
|
|
@@ -10218,7 +10074,7 @@ var SkillLibrary = class {
|
|
|
10218
10074
|
if (md === null) return {
|
|
10219
10075
|
result: {
|
|
10220
10076
|
ok: false,
|
|
10221
|
-
message:
|
|
10077
|
+
message: `文件不存在:${patchLabel}`
|
|
10222
10078
|
},
|
|
10223
10079
|
write: null
|
|
10224
10080
|
};
|
|
@@ -10240,7 +10096,7 @@ var SkillLibrary = class {
|
|
|
10240
10096
|
if (patched === null) return {
|
|
10241
10097
|
result: {
|
|
10242
10098
|
ok: false,
|
|
10243
|
-
message:
|
|
10099
|
+
message: `在「${name}/${patchLabel}」里找不到 old_string(或 replaceAll 的模糊匹配预算已用尽)。整篇重写请改用 update。`
|
|
10244
10100
|
},
|
|
10245
10101
|
write: null
|
|
10246
10102
|
};
|
|
@@ -10253,7 +10109,7 @@ var SkillLibrary = class {
|
|
|
10253
10109
|
if (validation) return {
|
|
10254
10110
|
result: {
|
|
10255
10111
|
ok: false,
|
|
10256
|
-
message:
|
|
10112
|
+
message: `补丁被拒绝:${validation}`
|
|
10257
10113
|
},
|
|
10258
10114
|
write: null
|
|
10259
10115
|
};
|
|
@@ -10261,7 +10117,7 @@ var SkillLibrary = class {
|
|
|
10261
10117
|
if (norm.issues.length > 0) return {
|
|
10262
10118
|
result: {
|
|
10263
10119
|
ok: false,
|
|
10264
|
-
message:
|
|
10120
|
+
message: `补丁被拒绝:frontmatter 无法自动修复(${norm.issues[0]})。`
|
|
10265
10121
|
},
|
|
10266
10122
|
write: null
|
|
10267
10123
|
};
|
|
@@ -10272,7 +10128,7 @@ var SkillLibrary = class {
|
|
|
10272
10128
|
if (revalidated) return {
|
|
10273
10129
|
result: {
|
|
10274
10130
|
ok: false,
|
|
10275
|
-
message:
|
|
10131
|
+
message: `补丁被拒绝:${revalidated}`
|
|
10276
10132
|
},
|
|
10277
10133
|
write: null
|
|
10278
10134
|
};
|
|
@@ -10281,7 +10137,7 @@ var SkillLibrary = class {
|
|
|
10281
10137
|
if (Buffer.byteLength(writeContent, "utf8") > this.limits.maxSkillFileBytes && target !== skillMd) return {
|
|
10282
10138
|
result: {
|
|
10283
10139
|
ok: false,
|
|
10284
|
-
message:
|
|
10140
|
+
message: `修补后的文件超过 ${this.limits.maxSkillFileBytes} 字节。`
|
|
10285
10141
|
},
|
|
10286
10142
|
write: null
|
|
10287
10143
|
};
|
|
@@ -10290,7 +10146,7 @@ var SkillLibrary = class {
|
|
|
10290
10146
|
if (overLimit && target === skillMd && !repairing) return {
|
|
10291
10147
|
result: {
|
|
10292
10148
|
ok: false,
|
|
10293
|
-
message:
|
|
10149
|
+
message: `修补后的内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
|
|
10294
10150
|
},
|
|
10295
10151
|
write: null
|
|
10296
10152
|
};
|
|
@@ -10299,7 +10155,7 @@ var SkillLibrary = class {
|
|
|
10299
10155
|
if (supportOverCap && this.supportFileCharPolicy() === "enforce") return {
|
|
10300
10156
|
result: {
|
|
10301
10157
|
ok: false,
|
|
10302
|
-
message:
|
|
10158
|
+
message: `修补后的文件内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
|
|
10303
10159
|
},
|
|
10304
10160
|
write: null
|
|
10305
10161
|
};
|
|
@@ -10314,7 +10170,7 @@ var SkillLibrary = class {
|
|
|
10314
10170
|
if (writeContent.trimEnd() === md.trimEnd()) return {
|
|
10315
10171
|
result: {
|
|
10316
10172
|
ok: true,
|
|
10317
|
-
message:
|
|
10173
|
+
message: `技能「${name}」未变更:old_string 与替换内容相同(${patchLabel}),未写入。`,
|
|
10318
10174
|
noop: true,
|
|
10319
10175
|
path: dir
|
|
10320
10176
|
},
|
|
@@ -10324,7 +10180,7 @@ var SkillLibrary = class {
|
|
|
10324
10180
|
return {
|
|
10325
10181
|
result: {
|
|
10326
10182
|
ok: true,
|
|
10327
|
-
message:
|
|
10183
|
+
message: `技能「${name}」已修补(${patchLabel})。${patchNote}${capAdvisory}`,
|
|
10328
10184
|
path: dir,
|
|
10329
10185
|
...normalizedFields ? { normalizedFrontmatterFields: normalizedFields } : {}
|
|
10330
10186
|
},
|
|
@@ -10369,9 +10225,9 @@ var SkillLibrary = class {
|
|
|
10369
10225
|
const reason = error instanceof Error ? error.message : String(error);
|
|
10370
10226
|
try {
|
|
10371
10227
|
await this.io.remove(dest);
|
|
10372
|
-
return
|
|
10228
|
+
return `复制成功,但源目录删不掉(${reason});已回滚复制出的目录树`;
|
|
10373
10229
|
} catch {
|
|
10374
|
-
return
|
|
10230
|
+
return `复制成功,但源目录删不掉(${reason}),且复制出的目录树也无法回滚 —— 该目录树现在同时存在于两处;请手动清理`;
|
|
10375
10231
|
}
|
|
10376
10232
|
}
|
|
10377
10233
|
}
|
|
@@ -10380,110 +10236,19 @@ var SkillLibrary = class {
|
|
|
10380
10236
|
* P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
|
|
10381
10237
|
* restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
|
|
10382
10238
|
* after the move its transact commit re-creates `<dir>/…` (mkdir
|
|
10383
|
-
* recursive) and the tree ends half-archived.
|
|
10384
|
-
*
|
|
10385
|
-
*
|
|
10386
|
-
*
|
|
10387
|
-
*
|
|
10388
|
-
* and restore would later move the residue back into the live root).
|
|
10389
|
-
* Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
|
|
10390
|
-
* each support dir (write_file's lock sits next to its file). Residual:
|
|
10391
|
-
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
10392
|
-
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
10393
|
-
* re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
|
|
10394
|
-
* holder is GONE is not a writer — `decideTakeover` owns that verdict.
|
|
10239
|
+
* recursive) and the tree ends half-archived.
|
|
10240
|
+
* G2 S4: the writer-lock probe that used to answer this question is retired — writes go through
|
|
10241
|
+
* the provider's versioned writes, so a writer that raced the move fails its own write loudly
|
|
10242
|
+
* instead of landing on a moved-away path. What REMAINS load-bearing here is the fail-closed
|
|
10243
|
+
* listing rule, which is about the filesystem misbehaving rather than about writers.
|
|
10395
10244
|
*/
|
|
10396
|
-
async
|
|
10397
|
-
const
|
|
10398
|
-
|
|
10399
|
-
for (const supportDir of SUPPORT_DIRS) {
|
|
10400
|
-
let entries;
|
|
10401
|
-
try {
|
|
10402
|
-
entries = await this.io.list(join(dir, supportDir));
|
|
10403
|
-
} catch {
|
|
10404
|
-
return true;
|
|
10405
|
-
}
|
|
10406
|
-
for (const entry of entries) {
|
|
10407
|
-
if (!entry.endsWith(".lock")) continue;
|
|
10408
|
-
if (await this.isWriterLock(join(dir, supportDir, entry))) return true;
|
|
10409
|
-
}
|
|
10410
|
-
}
|
|
10411
|
-
return false;
|
|
10412
|
-
}
|
|
10413
|
-
/** P2 (v17): a `*.lock` file counts as a writer lock only when its body
|
|
10414
|
-
* carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
|
|
10415
|
-
* (a creator between create and body write); anything else is user content
|
|
10416
|
-
* (suffix-only matching refused archiving and deleted user files on restore).
|
|
10417
|
-
* v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
|
|
10418
|
-
* the lock the io layer refuses to reclaim, the 30s empty window included. */
|
|
10419
|
-
async isWriterLock(lockPath) {
|
|
10420
|
-
let body;
|
|
10421
|
-
try {
|
|
10422
|
-
body = await this.io.readText(lockPath);
|
|
10245
|
+
async supportDirsUnlistable(dir) {
|
|
10246
|
+
for (const supportDir of SUPPORT_DIRS) try {
|
|
10247
|
+
await this.io.list(join(dir, supportDir));
|
|
10423
10248
|
} catch {
|
|
10424
10249
|
return true;
|
|
10425
10250
|
}
|
|
10426
|
-
|
|
10427
|
-
const trimmed = body.trim();
|
|
10428
|
-
if (trimmed !== "" && !/^\d+$/.test(trimmed) && !LOCK_BODY_RE.test(trimmed)) return false;
|
|
10429
|
-
let mtimeMs = null;
|
|
10430
|
-
try {
|
|
10431
|
-
mtimeMs = await this.io.mtime?.(lockPath) ?? null;
|
|
10432
|
-
} catch {
|
|
10433
|
-
mtimeMs = null;
|
|
10434
|
-
}
|
|
10435
|
-
return decideTakeover({
|
|
10436
|
-
body,
|
|
10437
|
-
mtimeMs: mtimeMs ?? Date.now(),
|
|
10438
|
-
alive: isProcessAlive
|
|
10439
|
-
}) === "none";
|
|
10440
|
-
}
|
|
10441
|
-
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
|
|
10442
|
-
* A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
|
|
10443
|
-
* (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
|
|
10444
|
-
* a lock whose holder pid is NOT alive — a live writer's lock is never
|
|
10445
|
-
* stolen by the sweep. A dead-pid residue would otherwise permanently
|
|
10446
|
-
* refuse archive/restore. */
|
|
10447
|
-
async deleteStrandedLocks(dir) {
|
|
10448
|
-
for (const markerLock of MARKER_LOCK_NAMES) await this.sweepLockIfStranded(join(dir, markerLock));
|
|
10449
|
-
for (const supportDir of SUPPORT_DIRS) {
|
|
10450
|
-
let entries = [];
|
|
10451
|
-
try {
|
|
10452
|
-
entries = await this.io.list(join(dir, supportDir));
|
|
10453
|
-
} catch {
|
|
10454
|
-
continue;
|
|
10455
|
-
}
|
|
10456
|
-
for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
|
|
10457
|
-
}
|
|
10458
|
-
}
|
|
10459
|
-
/** R2 follow-up (audit of A2): the recycled-pid window from io's
|
|
10460
|
-
* `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
|
|
10461
|
-
* whose holder pid probes alive but whose mtime is older than the alive
|
|
10462
|
-
* window is a recycled pid, not a live writer (no write in this family
|
|
10463
|
-
* holds a lock for more than minutes) — without this, one recycled pid
|
|
10464
|
-
* blocked whole-tree snapshot recovery forever with a "retry once the
|
|
10465
|
-
* write completes" message that could never become true. A backend without
|
|
10466
|
-
* `mtime` keeps the conservative refuse-on-alive posture. */
|
|
10467
|
-
async lockHolderAgedOut(lockPath) {
|
|
10468
|
-
const mtime = await this.io.mtime?.(lockPath).catch(() => null);
|
|
10469
|
-
return typeof mtime === "number" && Date.now() - mtime > 864e5;
|
|
10470
|
-
}
|
|
10471
|
-
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
10472
|
-
* shape AND the holder pid is not alive (or is an aged-out recycled pid —
|
|
10473
|
-
* R2 follow-up); anything else (a user support file or a live writer's
|
|
10474
|
-
* lock) is left untouched. */
|
|
10475
|
-
async sweepLockIfStranded(lockPath) {
|
|
10476
|
-
const lockProbe = await probeText(this.io, lockPath);
|
|
10477
|
-
if (isUnknown(lockProbe)) {
|
|
10478
|
-
console.warn(`skill-store: could not read ${lockPath} to check whether its holder is alive (${lockProbe.reason}); leaving the lock in place`);
|
|
10479
|
-
return;
|
|
10480
|
-
}
|
|
10481
|
-
if (!isPresent(lockProbe)) return;
|
|
10482
|
-
const body = lockProbe.value;
|
|
10483
|
-
const pid = parseLockBody(body);
|
|
10484
|
-
if (pid === null) return;
|
|
10485
|
-
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
|
|
10486
|
-
await this.io.remove(lockPath).catch(() => {});
|
|
10251
|
+
return false;
|
|
10487
10252
|
}
|
|
10488
10253
|
/** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
|
|
10489
10254
|
* only a single, non-traversing path component is safe. Dotfiles
|
|
@@ -10494,22 +10259,6 @@ var SkillLibrary = class {
|
|
|
10494
10259
|
if (typeof name !== "string") return false;
|
|
10495
10260
|
return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
|
|
10496
10261
|
}
|
|
10497
|
-
/** A1-7 (v18): a root-level lock whose holder is alive must refuse the
|
|
10498
|
-
* restore; a dead residue is swept so a crashed writer cannot block
|
|
10499
|
-
* recovery. A non-lock body shape is left alone (user file). */
|
|
10500
|
-
async refuseLiveLockOrSweep(lockPath, label) {
|
|
10501
|
-
let body;
|
|
10502
|
-
try {
|
|
10503
|
-
body = await this.io.readText(lockPath);
|
|
10504
|
-
} catch (error) {
|
|
10505
|
-
throw new Error(`snapshot restore refused: cannot verify ${label} (${error instanceof Error ? error.message : String(error)}); a live writer may hold it`);
|
|
10506
|
-
}
|
|
10507
|
-
if (body === null) return;
|
|
10508
|
-
const pid = parseLockBody(body);
|
|
10509
|
-
if (pid === null) return;
|
|
10510
|
-
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`);
|
|
10511
|
-
await this.io.remove(lockPath).catch(() => {});
|
|
10512
|
-
}
|
|
10513
10262
|
async archive(rawName, options = {}) {
|
|
10514
10263
|
const name = rawName.trim();
|
|
10515
10264
|
const badName = this.badName(name, { allowReserved: true });
|
|
@@ -10521,17 +10270,17 @@ var SkillLibrary = class {
|
|
|
10521
10270
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
10522
10271
|
if (md === null) return {
|
|
10523
10272
|
ok: false,
|
|
10524
|
-
message:
|
|
10273
|
+
message: `技能「${name}」不存在。`
|
|
10525
10274
|
};
|
|
10526
10275
|
const protection = await this.deleteProtection(name, options);
|
|
10527
10276
|
if (protection) return {
|
|
10528
10277
|
ok: false,
|
|
10529
|
-
message: protection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` :
|
|
10278
|
+
message: protection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `技能「${name}」受保护(${protection})。`
|
|
10530
10279
|
};
|
|
10531
10280
|
if (options.absorbedInto) {
|
|
10532
10281
|
if (options.absorbedInto.trim() === name) return {
|
|
10533
10282
|
ok: false,
|
|
10534
|
-
message: "absorbed_into
|
|
10283
|
+
message: "absorbed_into 不能是正在归档的技能本身(不能吸收到自己)。"
|
|
10535
10284
|
};
|
|
10536
10285
|
const intoBad = this.badName(options.absorbedInto.trim());
|
|
10537
10286
|
if (intoBad) return {
|
|
@@ -10553,22 +10302,22 @@ var SkillLibrary = class {
|
|
|
10553
10302
|
if (this.io.isSymlink) {
|
|
10554
10303
|
if (await this.io.isSymlink(dir) === true) return {
|
|
10555
10304
|
ok: false,
|
|
10556
|
-
message:
|
|
10305
|
+
message: `技能「${name}」是符号链接;拒绝归档。`
|
|
10557
10306
|
};
|
|
10558
10307
|
}
|
|
10559
|
-
if (await this.
|
|
10308
|
+
if (await this.supportDirsUnlistable(dir)) return {
|
|
10560
10309
|
ok: false,
|
|
10561
|
-
message:
|
|
10310
|
+
message: `无法检查技能「${name}」是否有写入方(有一个支撑目录列不出来):请重试归档。`
|
|
10562
10311
|
};
|
|
10563
10312
|
const commitProtection = await this.deleteProtection(name, options);
|
|
10564
10313
|
if (commitProtection) return {
|
|
10565
10314
|
ok: false,
|
|
10566
|
-
message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` :
|
|
10315
|
+
message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `技能「${name}」受保护(${commitProtection})。`
|
|
10567
10316
|
};
|
|
10568
10317
|
const moveFailure = await this.moveDir(dir, dest);
|
|
10569
10318
|
if (moveFailure !== void 0) return {
|
|
10570
10319
|
ok: false,
|
|
10571
|
-
message:
|
|
10320
|
+
message: `技能「${name}」归档失败:${moveFailure}。`
|
|
10572
10321
|
};
|
|
10573
10322
|
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
10574
10323
|
try {
|
|
@@ -10585,7 +10334,7 @@ var SkillLibrary = class {
|
|
|
10585
10334
|
});
|
|
10586
10335
|
return {
|
|
10587
10336
|
ok: true,
|
|
10588
|
-
message:
|
|
10337
|
+
message: `技能「${name}」已归档到 .archive。`,
|
|
10589
10338
|
path: dest
|
|
10590
10339
|
};
|
|
10591
10340
|
}
|
|
@@ -10683,7 +10432,7 @@ var SkillLibrary = class {
|
|
|
10683
10432
|
const mode = options.mode ?? "append";
|
|
10684
10433
|
if (normalizedSources.length === 0) return {
|
|
10685
10434
|
ok: false,
|
|
10686
|
-
message: "
|
|
10435
|
+
message: "合并至少需要一个互不相同的来源技能。"
|
|
10687
10436
|
};
|
|
10688
10437
|
for (const name of [targetName, ...normalizedSources]) {
|
|
10689
10438
|
const bad = this.badName(name);
|
|
@@ -10696,7 +10445,7 @@ var SkillLibrary = class {
|
|
|
10696
10445
|
const targetProtection = await this.writeProtection(targetName, origin);
|
|
10697
10446
|
if (targetProtection) return {
|
|
10698
10447
|
ok: false,
|
|
10699
|
-
message:
|
|
10448
|
+
message: `技能「${targetName}」受保护(${targetProtection})。`
|
|
10700
10449
|
};
|
|
10701
10450
|
return await this.serial(async () => {
|
|
10702
10451
|
const referenceWrites = [];
|
|
@@ -10708,17 +10457,17 @@ var SkillLibrary = class {
|
|
|
10708
10457
|
const protection = await this.deleteProtection(source);
|
|
10709
10458
|
if (protection) return {
|
|
10710
10459
|
ok: false,
|
|
10711
|
-
message:
|
|
10460
|
+
message: `技能「${source}」受保护(${protection})。`
|
|
10712
10461
|
};
|
|
10713
10462
|
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
10714
10463
|
if (!sourceMd) return {
|
|
10715
10464
|
ok: false,
|
|
10716
|
-
message:
|
|
10465
|
+
message: `技能「${source}」不存在。`
|
|
10717
10466
|
};
|
|
10718
10467
|
const parsed = parseFrontmatter(sourceMd);
|
|
10719
10468
|
if (!parsed) return {
|
|
10720
10469
|
ok: false,
|
|
10721
|
-
message:
|
|
10470
|
+
message: `技能「${source}」没有有效的 frontmatter;拒绝合并。`
|
|
10722
10471
|
};
|
|
10723
10472
|
const carriesSupport = await this.countSupportDirs(source) > 0;
|
|
10724
10473
|
const refs = supportRefs(parsed.body);
|
|
@@ -10727,7 +10476,7 @@ var SkillLibrary = class {
|
|
|
10727
10476
|
const decision = await this.planSourceRehoming(source, targetName, parsed.body, rehomed.map((file) => file.to));
|
|
10728
10477
|
if (decision.kind === "blocked") return {
|
|
10729
10478
|
ok: false,
|
|
10730
|
-
message:
|
|
10479
|
+
message: `合并被拒绝:${carriesSupport ? `source "${source}" carries support files` : `source "${source}" body references support files (${refs.join(", ")})`} 会被落下。请改用 mode:'reference',或直接归档整个包。${decision.note}`
|
|
10731
10480
|
};
|
|
10732
10481
|
rehomed.push(...decision.files);
|
|
10733
10482
|
appendedBody = decision.rewritten;
|
|
@@ -10739,17 +10488,17 @@ var SkillLibrary = class {
|
|
|
10739
10488
|
const protection = await this.deleteProtection(source);
|
|
10740
10489
|
if (protection) return {
|
|
10741
10490
|
ok: false,
|
|
10742
|
-
message:
|
|
10491
|
+
message: `技能「${source}」受保护(${protection})。`
|
|
10743
10492
|
};
|
|
10744
10493
|
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
10745
10494
|
if (!sourceMd) return {
|
|
10746
10495
|
ok: false,
|
|
10747
|
-
message:
|
|
10496
|
+
message: `技能「${source}」不存在。`
|
|
10748
10497
|
};
|
|
10749
10498
|
const parsed = parseFrontmatter(sourceMd);
|
|
10750
10499
|
if (!parsed) return {
|
|
10751
10500
|
ok: false,
|
|
10752
|
-
message:
|
|
10501
|
+
message: `技能「${source}」没有有效的 frontmatter;拒绝降级。`
|
|
10753
10502
|
};
|
|
10754
10503
|
const refs = supportRefs(parsed.body);
|
|
10755
10504
|
let demotedBody = parsed.body;
|
|
@@ -10757,7 +10506,7 @@ var SkillLibrary = class {
|
|
|
10757
10506
|
const decision = await this.planSourceRehoming(source, targetName, parsed.body, rehomed.map((file) => file.to));
|
|
10758
10507
|
if (decision.kind === "blocked") return {
|
|
10759
10508
|
ok: false,
|
|
10760
|
-
message:
|
|
10509
|
+
message: `合并被拒绝:来源技能「${source}」的正文引用了会被落下的支撑文件(${refs.join(", ")}):请改为归档整个包。${decision.note}`
|
|
10761
10510
|
};
|
|
10762
10511
|
rehomed.push(...decision.files);
|
|
10763
10512
|
demotedBody = decision.rewritten;
|
|
@@ -10774,7 +10523,7 @@ var SkillLibrary = class {
|
|
|
10774
10523
|
try {
|
|
10775
10524
|
if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
|
|
10776
10525
|
ok: false,
|
|
10777
|
-
message:
|
|
10526
|
+
message: `技能「${targetName}」不存在。`
|
|
10778
10527
|
};
|
|
10779
10528
|
for (const source of normalizedSources) {
|
|
10780
10529
|
if (plannedSourceBytes.has(source)) {
|
|
@@ -10837,7 +10586,7 @@ var SkillLibrary = class {
|
|
|
10837
10586
|
if (!result.ok) throw new Error(result.message);
|
|
10838
10587
|
return {
|
|
10839
10588
|
ok: true,
|
|
10840
|
-
message:
|
|
10589
|
+
message: `已把 ${normalizedSources.join(", ")} 合并进「${targetName}」。`,
|
|
10841
10590
|
path: targetDir
|
|
10842
10591
|
};
|
|
10843
10592
|
} catch (error) {
|
|
@@ -10850,11 +10599,11 @@ var SkillLibrary = class {
|
|
|
10850
10599
|
}
|
|
10851
10600
|
if (failedRestores.length > 0) return {
|
|
10852
10601
|
ok: false,
|
|
10853
|
-
message:
|
|
10602
|
+
message: `合并失败(${reason});已回滚,但以下未回滚:${failedRestores.join(", ")}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`
|
|
10854
10603
|
};
|
|
10855
10604
|
return {
|
|
10856
10605
|
ok: false,
|
|
10857
|
-
message:
|
|
10606
|
+
message: `合并失败,已回滚:${reason}`
|
|
10858
10607
|
};
|
|
10859
10608
|
}
|
|
10860
10609
|
});
|
|
@@ -10883,21 +10632,21 @@ var SkillLibrary = class {
|
|
|
10883
10632
|
};
|
|
10884
10633
|
if (moves.length === 0) return {
|
|
10885
10634
|
ok: false,
|
|
10886
|
-
message: "
|
|
10635
|
+
message: "重构至少需要一次章节搬移。"
|
|
10887
10636
|
};
|
|
10888
10637
|
if (moves.length > 5) return {
|
|
10889
10638
|
ok: false,
|
|
10890
|
-
message:
|
|
10639
|
+
message: `重构超过 5 次搬移。`
|
|
10891
10640
|
};
|
|
10892
10641
|
for (const move of moves) {
|
|
10893
10642
|
const raw = move;
|
|
10894
10643
|
if (raw === null || typeof raw !== "object") return {
|
|
10895
10644
|
ok: false,
|
|
10896
|
-
message: "
|
|
10645
|
+
message: "每次重构搬移都必须是带 heading 的对象。"
|
|
10897
10646
|
};
|
|
10898
10647
|
if (typeof move.heading !== "string" || !move.heading.trim()) return {
|
|
10899
10648
|
ok: false,
|
|
10900
|
-
message: "
|
|
10649
|
+
message: "每次重构搬移都需要非空的 heading。"
|
|
10901
10650
|
};
|
|
10902
10651
|
const targetIssue = validateRestructureTarget(move.toFile);
|
|
10903
10652
|
if (targetIssue) return {
|
|
@@ -10909,25 +10658,25 @@ var SkillLibrary = class {
|
|
|
10909
10658
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
10910
10659
|
if (!md) return {
|
|
10911
10660
|
ok: false,
|
|
10912
|
-
message:
|
|
10661
|
+
message: `技能「${name}」不存在。`
|
|
10913
10662
|
};
|
|
10914
10663
|
const block = frontmatterBlock(md);
|
|
10915
10664
|
if (!block) return {
|
|
10916
10665
|
ok: false,
|
|
10917
|
-
message: "SKILL.md
|
|
10666
|
+
message: "SKILL.md 没有有效的 frontmatter;拒绝重构。"
|
|
10918
10667
|
};
|
|
10919
10668
|
const header = block.lines.slice(0, block.end + 1).join("\n");
|
|
10920
10669
|
const plan = planRestructureSections(md.slice(header.length).replace(/\r\n/g, "\n"), moves);
|
|
10921
10670
|
if ("error" in plan) return {
|
|
10922
10671
|
ok: false,
|
|
10923
|
-
message:
|
|
10672
|
+
message: `重构被拒绝:${plan.error}`
|
|
10924
10673
|
};
|
|
10925
10674
|
const newMd = `${header}${plan.body}`.replace(/\r\n/g, "\n");
|
|
10926
10675
|
const finalMd = block.nl === "\r\n" ? newMd.replace(/\n/g, "\r\n") : newMd;
|
|
10927
10676
|
const newMdCheck = validateFrontmatter(finalMd, name, this.limits);
|
|
10928
10677
|
if (newMdCheck) return {
|
|
10929
10678
|
ok: false,
|
|
10930
|
-
message:
|
|
10679
|
+
message: `重构被拒绝:${newMdCheck}`
|
|
10931
10680
|
};
|
|
10932
10681
|
const listing = (this.limits.citationPolicy ?? "verify") === "refuse" ? null : await this.listSupportFiles(name);
|
|
10933
10682
|
const files = listing !== null && isPresent(listing) ? listing.value : null;
|
|
@@ -10936,7 +10685,7 @@ var SkillLibrary = class {
|
|
|
10936
10685
|
if (refs.length === 0) continue;
|
|
10937
10686
|
if (files === null) return {
|
|
10938
10687
|
ok: false,
|
|
10939
|
-
message:
|
|
10688
|
+
message: `重构被拒绝:章节「## ${section.heading}」引用了会留在原地的支撑文件(${refs.join(", ")}):请拆开该章节,或连同它的文件一起搬。`
|
|
10940
10689
|
};
|
|
10941
10690
|
const report = resolveCitations({
|
|
10942
10691
|
content: section.text,
|
|
@@ -10947,7 +10696,7 @@ var SkillLibrary = class {
|
|
|
10947
10696
|
const missing = report.dangling.map((ref) => ref.target ?? ref.raw).join(", ");
|
|
10948
10697
|
return {
|
|
10949
10698
|
ok: false,
|
|
10950
|
-
message:
|
|
10699
|
+
message: `重构被拒绝:章节「## ${section.heading}」引用的支撑文件不存在(${missing}):请修正引用,或先创建该文件。`
|
|
10951
10700
|
};
|
|
10952
10701
|
}
|
|
10953
10702
|
}
|
|
@@ -10988,7 +10737,7 @@ var SkillLibrary = class {
|
|
|
10988
10737
|
if (!result.ok) return result;
|
|
10989
10738
|
return {
|
|
10990
10739
|
ok: true,
|
|
10991
|
-
message:
|
|
10740
|
+
message: `已重构「${name}」:把 ${plan.sections.length} 个章节搬到 references/。`,
|
|
10992
10741
|
path: dir
|
|
10993
10742
|
};
|
|
10994
10743
|
}
|
|
@@ -11010,12 +10759,12 @@ var SkillLibrary = class {
|
|
|
11010
10759
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
11011
10760
|
if (!md) return {
|
|
11012
10761
|
ok: false,
|
|
11013
|
-
message:
|
|
10762
|
+
message: `技能「${name}」不存在。`
|
|
11014
10763
|
};
|
|
11015
10764
|
const protection = plan.protection === "write" ? await this.writeProtection(name, plan.origin) : plan.protection === "delete" ? await this.deleteProtection(name) : null;
|
|
11016
10765
|
if (protection) return {
|
|
11017
10766
|
ok: false,
|
|
11018
|
-
message:
|
|
10767
|
+
message: `技能「${name}」受保护(${protection})。`
|
|
11019
10768
|
};
|
|
11020
10769
|
const landing = [];
|
|
11021
10770
|
for (const write of plan.writes) {
|
|
@@ -11025,12 +10774,12 @@ var SkillLibrary = class {
|
|
|
11025
10774
|
} catch (error) {
|
|
11026
10775
|
return {
|
|
11027
10776
|
ok: false,
|
|
11028
|
-
message:
|
|
10777
|
+
message: `拒绝目录树变更:无法安全预读 ${write.target}(${error instanceof Error ? error.message : String(error)});未执行任何写入`
|
|
11029
10778
|
};
|
|
11030
10779
|
}
|
|
11031
10780
|
if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
|
|
11032
10781
|
ok: false,
|
|
11033
|
-
message:
|
|
10782
|
+
message: `写入超过 ${this.limits.maxSkillFileBytes} 字节:${write.target}`
|
|
11034
10783
|
};
|
|
11035
10784
|
const threat = this.contentThreatBlock(write.content);
|
|
11036
10785
|
if (threat) return {
|
|
@@ -11085,11 +10834,11 @@ var SkillLibrary = class {
|
|
|
11085
10834
|
const reason = error instanceof Error ? error.message : String(error);
|
|
11086
10835
|
if (stuck.length === 0) return {
|
|
11087
10836
|
ok: false,
|
|
11088
|
-
message:
|
|
10837
|
+
message: `目录树变更失败,已回滚:${reason}`
|
|
11089
10838
|
};
|
|
11090
10839
|
return {
|
|
11091
10840
|
ok: false,
|
|
11092
|
-
message:
|
|
10841
|
+
message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:请从 .backups 快照恢复这些目标(或重新应用这次变更)。`
|
|
11093
10842
|
};
|
|
11094
10843
|
}
|
|
11095
10844
|
const skillMdWrite = landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md");
|
|
@@ -11103,7 +10852,7 @@ var SkillLibrary = class {
|
|
|
11103
10852
|
});
|
|
11104
10853
|
return {
|
|
11105
10854
|
ok: true,
|
|
11106
|
-
message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded
|
|
10855
|
+
message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`,
|
|
11107
10856
|
path: dir
|
|
11108
10857
|
};
|
|
11109
10858
|
}
|
|
@@ -11122,7 +10871,7 @@ var SkillLibrary = class {
|
|
|
11122
10871
|
const dest = this.dirOf(name);
|
|
11123
10872
|
if (await this.io.exists(dest)) return {
|
|
11124
10873
|
ok: false,
|
|
11125
|
-
message: await this.io.exists(join(dest, "SKILL.md")) ?
|
|
10874
|
+
message: await this.io.exists(join(dest, "SKILL.md")) ? `技能「${name}」已存在于当前根目录;拒绝覆盖。` : `技能目录「${name}」已存在于当前根目录,但没有 SKILL.md;恢复前请先移除或修复它。`
|
|
11126
10875
|
};
|
|
11127
10876
|
let caseVariant;
|
|
11128
10877
|
try {
|
|
@@ -11130,7 +10879,7 @@ var SkillLibrary = class {
|
|
|
11130
10879
|
} catch {}
|
|
11131
10880
|
if (caseVariant !== void 0) return {
|
|
11132
10881
|
ok: false,
|
|
11133
|
-
message:
|
|
10882
|
+
message: `技能「${caseVariant}」(同名、仅大小写不同)已存在于当前根目录;把「${name}」恢复到它旁边会造成含义不清的重复:请恢复为「${caseVariant}」,或先移除该变体。`
|
|
11134
10883
|
};
|
|
11135
10884
|
const archiveRoot = join(this.root, ".archive");
|
|
11136
10885
|
let entries;
|
|
@@ -11139,7 +10888,7 @@ var SkillLibrary = class {
|
|
|
11139
10888
|
} catch {
|
|
11140
10889
|
return {
|
|
11141
10890
|
ok: false,
|
|
11142
|
-
message: "
|
|
10891
|
+
message: "没有可用的技能归档。"
|
|
11143
10892
|
};
|
|
11144
10893
|
}
|
|
11145
10894
|
const candidates = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse();
|
|
@@ -11174,30 +10923,29 @@ var SkillLibrary = class {
|
|
|
11174
10923
|
if (!chosen) {
|
|
11175
10924
|
if (unreadable.length === 0) return {
|
|
11176
10925
|
ok: false,
|
|
11177
|
-
message:
|
|
10926
|
+
message: `技能「${name}」不在 .archive 里。`
|
|
11178
10927
|
};
|
|
11179
10928
|
return {
|
|
11180
10929
|
ok: false,
|
|
11181
|
-
message:
|
|
10930
|
+
message: `.archive 里没有「${name}」对应的 frontmatter 名,但有 ${unreadable.length} 个名字相似的归档项:${unreadable.join(", ")}。这些归档项没有可读的 SKILL.md(或名字对不上),按名字恢复无法区分它们:请把目录改成技能名后再次恢复,或改用快照恢复。`
|
|
11182
10931
|
};
|
|
11183
10932
|
}
|
|
11184
10933
|
const source = join(archiveRoot, chosen);
|
|
11185
10934
|
if (this.io.isSymlink) {
|
|
11186
10935
|
if (await this.io.isSymlink(source) === true) return {
|
|
11187
10936
|
ok: false,
|
|
11188
|
-
message:
|
|
10937
|
+
message: `归档项「${chosen}」是符号链接;拒绝恢复。`
|
|
11189
10938
|
};
|
|
11190
10939
|
}
|
|
11191
|
-
if (await this.
|
|
10940
|
+
if (await this.supportDirsUnlistable(dest)) return {
|
|
11192
10941
|
ok: false,
|
|
11193
|
-
message:
|
|
10942
|
+
message: `无法检查技能「${name}」是否有写入方(有一个支撑目录列不出来):请重试恢复。`
|
|
11194
10943
|
};
|
|
11195
10944
|
const moveFailure = await this.moveDir(source, dest);
|
|
11196
10945
|
if (moveFailure !== void 0) return {
|
|
11197
10946
|
ok: false,
|
|
11198
|
-
message:
|
|
10947
|
+
message: `从 .archive 恢复「${name}」失败:${moveFailure}`
|
|
11199
10948
|
};
|
|
11200
|
-
await this.deleteStrandedLocks(dest);
|
|
11201
10949
|
for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
|
|
11202
10950
|
const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
|
|
11203
10951
|
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`);
|
|
@@ -11209,7 +10957,7 @@ var SkillLibrary = class {
|
|
|
11209
10957
|
});
|
|
11210
10958
|
return {
|
|
11211
10959
|
ok: true,
|
|
11212
|
-
message:
|
|
10960
|
+
message: `技能「${name}」已从 .archive 恢复。`,
|
|
11213
10961
|
path: dest
|
|
11214
10962
|
};
|
|
11215
10963
|
}
|
|
@@ -11226,12 +10974,12 @@ var SkillLibrary = class {
|
|
|
11226
10974
|
const dir = this.dirOf(name);
|
|
11227
10975
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
11228
10976
|
ok: false,
|
|
11229
|
-
message:
|
|
10977
|
+
message: `技能「${name}」不存在。`
|
|
11230
10978
|
};
|
|
11231
10979
|
const protection = await this.writeProtection(name, origin);
|
|
11232
10980
|
if (protection) return {
|
|
11233
10981
|
ok: false,
|
|
11234
|
-
message:
|
|
10982
|
+
message: `技能「${name}」受保护(${protection})。`
|
|
11235
10983
|
};
|
|
11236
10984
|
const validation = validateSupportPath(filePath);
|
|
11237
10985
|
if (validation) return {
|
|
@@ -11240,7 +10988,7 @@ var SkillLibrary = class {
|
|
|
11240
10988
|
};
|
|
11241
10989
|
if (Buffer.byteLength(content, "utf8") > this.limits.maxSkillFileBytes) return {
|
|
11242
10990
|
ok: false,
|
|
11243
|
-
message:
|
|
10991
|
+
message: `支撑文件超过 ${this.limits.maxSkillFileBytes} 字节。`
|
|
11244
10992
|
};
|
|
11245
10993
|
const threat = this.contentThreatBlock(content);
|
|
11246
10994
|
if (threat) return {
|
|
@@ -11260,14 +11008,14 @@ var SkillLibrary = class {
|
|
|
11260
11008
|
if (overCap && !repairing && this.supportFileCharPolicy() === "enforce") return {
|
|
11261
11009
|
result: {
|
|
11262
11010
|
ok: false,
|
|
11263
|
-
message:
|
|
11011
|
+
message: `支撑文件「${filePath}」的内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
|
|
11264
11012
|
},
|
|
11265
11013
|
write: null
|
|
11266
11014
|
};
|
|
11267
11015
|
if (current !== null && content.trimEnd() === current.trimEnd()) return {
|
|
11268
11016
|
result: {
|
|
11269
11017
|
ok: true,
|
|
11270
|
-
message:
|
|
11018
|
+
message: `支撑文件「${filePath}」未变更:提供的内容与当前文件一致,未写入。`,
|
|
11271
11019
|
noop: true,
|
|
11272
11020
|
path: target
|
|
11273
11021
|
},
|
|
@@ -11276,7 +11024,7 @@ var SkillLibrary = class {
|
|
|
11276
11024
|
return {
|
|
11277
11025
|
result: {
|
|
11278
11026
|
ok: true,
|
|
11279
|
-
message:
|
|
11027
|
+
message: `支撑文件「${filePath}」已写入「${name}」。${capAdvisory}`,
|
|
11280
11028
|
path: target
|
|
11281
11029
|
},
|
|
11282
11030
|
write: content,
|
|
@@ -11310,12 +11058,12 @@ var SkillLibrary = class {
|
|
|
11310
11058
|
const dir = this.dirOf(name);
|
|
11311
11059
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
11312
11060
|
ok: false,
|
|
11313
|
-
message:
|
|
11061
|
+
message: `技能「${name}」不存在。`
|
|
11314
11062
|
};
|
|
11315
11063
|
const protection = await this.writeProtection(name, origin);
|
|
11316
11064
|
if (protection) return {
|
|
11317
11065
|
ok: false,
|
|
11318
|
-
message:
|
|
11066
|
+
message: `技能「${name}」受保护(${protection})。`
|
|
11319
11067
|
};
|
|
11320
11068
|
const validation = validateSupportPath(filePath);
|
|
11321
11069
|
if (validation) return {
|
|
@@ -11327,7 +11075,7 @@ var SkillLibrary = class {
|
|
|
11327
11075
|
if (anchor !== void 0) return anchorRefusalFile(name, filePath, "missing");
|
|
11328
11076
|
return {
|
|
11329
11077
|
ok: false,
|
|
11330
|
-
message:
|
|
11078
|
+
message: `文件「${filePath}」不在技能「${name}」里。`
|
|
11331
11079
|
};
|
|
11332
11080
|
}
|
|
11333
11081
|
const beforeProbe = await probeText(this.io, target);
|
|
@@ -11351,7 +11099,7 @@ var SkillLibrary = class {
|
|
|
11351
11099
|
});
|
|
11352
11100
|
if (!ran.done) return {
|
|
11353
11101
|
ok: false,
|
|
11354
|
-
message: "
|
|
11102
|
+
message: "内部错误:删除事务没有调用任务;没有删除任何东西"
|
|
11355
11103
|
};
|
|
11356
11104
|
} else if (verdict === "match") await this.io.remove(target);
|
|
11357
11105
|
if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
|
|
@@ -11364,7 +11112,7 @@ var SkillLibrary = class {
|
|
|
11364
11112
|
});
|
|
11365
11113
|
return {
|
|
11366
11114
|
ok: true,
|
|
11367
|
-
message:
|
|
11115
|
+
message: `支撑文件「${filePath}」已从「${name}」移除。`,
|
|
11368
11116
|
path: target
|
|
11369
11117
|
};
|
|
11370
11118
|
}
|
|
@@ -11445,8 +11193,8 @@ var SkillLibrary = class {
|
|
|
11445
11193
|
const names = await listNames(this.root, this.io);
|
|
11446
11194
|
const skipped = [];
|
|
11447
11195
|
const copyable = [];
|
|
11448
|
-
for (const name of names) if (await this.
|
|
11449
|
-
console.warn(`skill-store: snapshot skipped "${name}" —
|
|
11196
|
+
for (const name of names) if (await this.supportDirsUnlistable(this.dirOf(name))) {
|
|
11197
|
+
console.warn(`skill-store: snapshot skipped "${name}" — its support directory could not be listed; the skill is recorded as skipped in the manifest`);
|
|
11450
11198
|
skipped.push(name);
|
|
11451
11199
|
} else copyable.push(name);
|
|
11452
11200
|
const copyFailure = (await Promise.allSettled(copyable.map(async (name) => {
|
|
@@ -11454,7 +11202,7 @@ var SkillLibrary = class {
|
|
|
11454
11202
|
}))).find((result) => result.status === "rejected");
|
|
11455
11203
|
if (copyFailure) throw copyFailure.reason;
|
|
11456
11204
|
const suspect = [];
|
|
11457
|
-
for (const name of copyable) if (await this.
|
|
11205
|
+
for (const name of copyable) if (await this.supportDirsUnlistable(this.dirOf(name))) suspect.push(name);
|
|
11458
11206
|
if (suspect.length > 0) {
|
|
11459
11207
|
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`);
|
|
11460
11208
|
for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
|
|
@@ -11582,7 +11330,7 @@ var SkillLibrary = class {
|
|
|
11582
11330
|
out.push({
|
|
11583
11331
|
path: join(backupRoot, name),
|
|
11584
11332
|
createdAt: "",
|
|
11585
|
-
reason: "
|
|
11333
|
+
reason: "清单读不出来或缺失(孤儿快照)"
|
|
11586
11334
|
});
|
|
11587
11335
|
continue;
|
|
11588
11336
|
}
|
|
@@ -11624,12 +11372,12 @@ var SkillLibrary = class {
|
|
|
11624
11372
|
const listed = await this.listSnapshots();
|
|
11625
11373
|
if (isUnknown(listed)) return {
|
|
11626
11374
|
ok: false,
|
|
11627
|
-
message:
|
|
11375
|
+
message: `拒绝快照恢复:快照目录读不出来(${listed.reason})—— 未做任何改动。`
|
|
11628
11376
|
};
|
|
11629
11377
|
const latest = isPresent(listed) ? listed.value[0] : void 0;
|
|
11630
11378
|
if (!latest) return {
|
|
11631
11379
|
ok: false,
|
|
11632
|
-
message: "
|
|
11380
|
+
message: "没有可用的技能快照。"
|
|
11633
11381
|
};
|
|
11634
11382
|
let preRollbackPath;
|
|
11635
11383
|
try {
|
|
@@ -11637,7 +11385,7 @@ var SkillLibrary = class {
|
|
|
11637
11385
|
} catch (error) {
|
|
11638
11386
|
return {
|
|
11639
11387
|
ok: false,
|
|
11640
|
-
message:
|
|
11388
|
+
message: `在任何清理之前就拒绝了快照恢复:生成回滚前快照失败(${error instanceof Error ? error.message : String(error)})—— 活动目录树未改变。请解决快照写入失败后重试。`
|
|
11641
11389
|
};
|
|
11642
11390
|
}
|
|
11643
11391
|
let snapshotExtras;
|
|
@@ -11646,7 +11394,7 @@ var SkillLibrary = class {
|
|
|
11646
11394
|
} catch (error) {
|
|
11647
11395
|
return {
|
|
11648
11396
|
ok: false,
|
|
11649
|
-
message:
|
|
11397
|
+
message: `在任何清理之前就拒绝了快照恢复:读取快照清单或附属文件失败(${error instanceof Error ? error.message : String(error)})—— 活动目录树未改变。请解决读取失败后重试。`
|
|
11650
11398
|
};
|
|
11651
11399
|
}
|
|
11652
11400
|
try {
|
|
@@ -11657,17 +11405,17 @@ var SkillLibrary = class {
|
|
|
11657
11405
|
await this.restoreSnapshotIntoRoot(preRollbackPath);
|
|
11658
11406
|
return {
|
|
11659
11407
|
ok: false,
|
|
11660
|
-
message:
|
|
11408
|
+
message: `快照恢复失败(${reason});活动目录树已回滚到回滚前的快照。`
|
|
11661
11409
|
};
|
|
11662
11410
|
} catch (rollbackError) {
|
|
11663
11411
|
const rb = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
11664
11412
|
if (isPreClearRefusal(reason) && isPreClearRefusal(rb)) return {
|
|
11665
11413
|
ok: false,
|
|
11666
|
-
message:
|
|
11414
|
+
message: `在任何清理之前就拒绝了快照恢复; 活动目录树未改变。\n- 目标:${reason}\n- 回滚前快照:${rb}\n请解决原因(有存活的技能写入,或快照不完整)后重试 —— 无需手动抢救。`
|
|
11667
11415
|
};
|
|
11668
11416
|
return {
|
|
11669
11417
|
ok: false,
|
|
11670
|
-
message:
|
|
11418
|
+
message: `快照恢复失败(${reason}),且回滚前快照的恢复也失败(${rb})。请手动抢救:${preRollbackPath}(回滚前)、${latest.path}(目标)。`
|
|
11671
11419
|
};
|
|
11672
11420
|
}
|
|
11673
11421
|
}
|
|
@@ -11677,7 +11425,7 @@ var SkillLibrary = class {
|
|
|
11677
11425
|
});
|
|
11678
11426
|
return {
|
|
11679
11427
|
ok: true,
|
|
11680
|
-
message:
|
|
11428
|
+
message: `已从 ${latest.path} 恢复技能目录树`,
|
|
11681
11429
|
path: latest.path,
|
|
11682
11430
|
...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
|
|
11683
11431
|
};
|
|
@@ -11713,14 +11461,9 @@ var SkillLibrary = class {
|
|
|
11713
11461
|
}
|
|
11714
11462
|
for (const entry of rootEntries) {
|
|
11715
11463
|
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json" || entry === ".history") continue;
|
|
11716
|
-
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
|
|
11717
|
-
await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
|
|
11718
|
-
continue;
|
|
11719
|
-
}
|
|
11720
11464
|
const dir = join(this.root, entry);
|
|
11721
11465
|
if (await this.io.exists(join(dir, "SKILL.md"))) {
|
|
11722
|
-
await this.
|
|
11723
|
-
if (await this.hasWriteLock(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
|
|
11466
|
+
if (await this.supportDirsUnlistable(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
|
|
11724
11467
|
}
|
|
11725
11468
|
}
|
|
11726
11469
|
const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
|
|
@@ -11728,7 +11471,6 @@ var SkillLibrary = class {
|
|
|
11728
11471
|
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
|
|
11729
11472
|
if (entry === ".history") continue;
|
|
11730
11473
|
if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
|
|
11731
|
-
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
|
|
11732
11474
|
await this.io.remove(join(this.root, entry));
|
|
11733
11475
|
}
|
|
11734
11476
|
for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
|
|
@@ -11740,10 +11482,7 @@ var SkillLibrary = class {
|
|
|
11740
11482
|
await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
|
|
11741
11483
|
} else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
|
|
11742
11484
|
}
|
|
11743
|
-
for (const entry of await this.io.list(this.root))
|
|
11744
|
-
if (entry.startsWith(".")) continue;
|
|
11745
|
-
await this.deleteStrandedLocks(join(this.root, entry));
|
|
11746
|
-
}
|
|
11485
|
+
for (const entry of await this.io.list(this.root)) if (entry.startsWith(".")) continue;
|
|
11747
11486
|
}
|
|
11748
11487
|
};
|
|
11749
11488
|
function newSkillLibrary(options) {
|
|
@@ -12358,6 +12097,7 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
12358
12097
|
control: "number",
|
|
12359
12098
|
unit: "天",
|
|
12360
12099
|
values: "",
|
|
12100
|
+
relation: { atLeast: "staleAfterDays" },
|
|
12361
12101
|
card: "evolution-curator"
|
|
12362
12102
|
},
|
|
12363
12103
|
{
|
|
@@ -12999,6 +12739,23 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
12999
12739
|
values: "",
|
|
13000
12740
|
card: "evolution-approval"
|
|
13001
12741
|
},
|
|
12742
|
+
{
|
|
12743
|
+
id: "approvalConfirm",
|
|
12744
|
+
group: "write-caps",
|
|
12745
|
+
tier: "E3",
|
|
12746
|
+
authority: "cordis",
|
|
12747
|
+
owner: "evolution-approval",
|
|
12748
|
+
applies: "live",
|
|
12749
|
+
docAnchor: "PARAMETERS.md#write-caps",
|
|
12750
|
+
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.",
|
|
12751
|
+
label: "批准前先确认",
|
|
12752
|
+
hint: "点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。",
|
|
12753
|
+
control: "select",
|
|
12754
|
+
unit: "",
|
|
12755
|
+
values: "auto|ask|timeout",
|
|
12756
|
+
valueLabels: "直接执行|先确认|等待超时即取消",
|
|
12757
|
+
card: "evolution-approval"
|
|
12758
|
+
},
|
|
13002
12759
|
{
|
|
13003
12760
|
id: "qualityWarnThreshold",
|
|
13004
12761
|
group: "deployment",
|
|
@@ -13269,14 +13026,6 @@ function readNumberParam(carrier, id) {
|
|
|
13269
13026
|
* @module @lmzhen/dsh-evolution-core
|
|
13270
13027
|
*/
|
|
13271
13028
|
/**
|
|
13272
|
-
* Whether a parsed value is a plain mapping (a settings section).
|
|
13273
|
-
* @param value - the parsed value.
|
|
13274
|
-
* @returns true for a non-null, non-array object.
|
|
13275
|
-
*/
|
|
13276
|
-
function isSection(value) {
|
|
13277
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13278
|
-
}
|
|
13279
|
-
/**
|
|
13280
13029
|
* The key names one row may be written with: its own E3 rows, plus the deprecated
|
|
13281
13030
|
* spellings that still resolve to them. An unknown key is refused instead of written —
|
|
13282
13031
|
* the platform validates a write against the row's schema, so writing it would fail the
|
|
@@ -13307,7 +13056,7 @@ function planNamespaceMigration(documentText) {
|
|
|
13307
13056
|
foreign: [],
|
|
13308
13057
|
skipped: []
|
|
13309
13058
|
};
|
|
13310
|
-
if (!
|
|
13059
|
+
if (!isRecord(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
|
|
13311
13060
|
const legacy = new Map(NAMESPACE_MIGRATIONS.map((entry) => [entry.namespace, entry.rowId]));
|
|
13312
13061
|
const entries = [];
|
|
13313
13062
|
const empty = [];
|
|
@@ -13315,7 +13064,7 @@ function planNamespaceMigration(documentText) {
|
|
|
13315
13064
|
for (const [namespace, section] of Object.entries(parsed)) {
|
|
13316
13065
|
const rowId = legacy.get(namespace);
|
|
13317
13066
|
if (rowId === void 0) continue;
|
|
13318
|
-
if (!
|
|
13067
|
+
if (!isRecord(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
|
|
13319
13068
|
const allowed = writableKeys(rowId);
|
|
13320
13069
|
const values = [];
|
|
13321
13070
|
for (const [key, value] of Object.entries(section)) {
|
|
@@ -13324,7 +13073,7 @@ function planNamespaceMigration(documentText) {
|
|
|
13324
13073
|
skipped.push({
|
|
13325
13074
|
namespace,
|
|
13326
13075
|
key,
|
|
13327
|
-
reason: "row `" + rowId + "`
|
|
13076
|
+
reason: "row `" + rowId + "` 没有这个参数"
|
|
13328
13077
|
});
|
|
13329
13078
|
continue;
|
|
13330
13079
|
}
|
|
@@ -13376,7 +13125,7 @@ function classifyNamespaceMigration(plan, read) {
|
|
|
13376
13125
|
for (const value of entry.values) skipped.push({
|
|
13377
13126
|
namespace: entry.namespace,
|
|
13378
13127
|
key: value.source,
|
|
13379
|
-
reason: "row `" + entry.rowId + "`
|
|
13128
|
+
reason: "row `" + entry.rowId + "` 在本组合中不提供服务"
|
|
13380
13129
|
});
|
|
13381
13130
|
continue;
|
|
13382
13131
|
}
|
|
@@ -13689,4 +13438,4 @@ function foldUntrustedLines(text, limit) {
|
|
|
13689
13438
|
return limit > 0 ? marked.slice(0, limit) : marked;
|
|
13690
13439
|
}
|
|
13691
13440
|
//#endregion
|
|
13692
|
-
export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT,
|
|
13441
|
+
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 };
|