@lmzhen/dsh-evolution-core 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +79 -37
- package/lib/types/io.d.ts +18 -3
- package/lib/types/skill-store.d.ts +17 -3
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -328,7 +328,10 @@ const LOCK_BODY_RE = /^(\d+):[0-9a-f]*$/;
|
|
|
328
328
|
/** Parse a writer-lock body into its holder pid. `null` when the body does
|
|
329
329
|
* not have the `pid:token` shape at all (e.g. a user support file named
|
|
330
330
|
* `*.lock`) — callers leave such files alone. A shape-matching body always
|
|
331
|
-
* yields a number
|
|
331
|
+
* yields a number, possibly `0` (state corruption / hand edit): there is no
|
|
332
|
+
* pid 0, so a `0` holder must be treated as DEAD by the caller —
|
|
333
|
+
* `isProcessAlive(0)` signals the caller's own process group on POSIX and
|
|
334
|
+
* answers true, so probe sites must guard `holder > 0` before probing. */
|
|
332
335
|
function parseLockBody(body) {
|
|
333
336
|
const match = LOCK_BODY_RE.exec(body.trim());
|
|
334
337
|
return match === null ? null : Number(match[1]);
|
|
@@ -365,6 +368,14 @@ const EMPTY_LOCK_TAKEOVER_MS = 3e4;
|
|
|
365
368
|
/** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
|
|
366
369
|
* and far below "forever". */
|
|
367
370
|
const LOCK_TEAR_TAKEOVER_MS = 36e5;
|
|
371
|
+
/** A2 (audit P1-2): even a lock whose holder pid probes ALIVE is reclaimable
|
|
372
|
+
* past this age. Liveness-by-pid cannot distinguish the original holder from
|
|
373
|
+
* an unrelated process the OS later assigned the same pid, so the plain alive
|
|
374
|
+
* probe let one recycled pid brick every writer of one state file forever.
|
|
375
|
+
* No write in this family holds a lock for more than minutes (the longest is
|
|
376
|
+
* the ~120s review window), so a day-old "alive" lock is a recycled pid, not
|
|
377
|
+
* a live writer. */
|
|
378
|
+
const ALIVE_LOCK_TAKEOVER_MS = 864e5;
|
|
368
379
|
/** P2-27 (v37): the commit-point ownership re-read. A transient read failure
|
|
369
380
|
* (EACCES/EMFILE/antivirus hold) must not abort a valid RMW, so the read is
|
|
370
381
|
* retried in place; only a still-unreadable lock fails the attempt. */
|
|
@@ -385,8 +396,11 @@ function isCommittedWarning(error) {
|
|
|
385
396
|
* V27 G1.1: the lock-takeover decision as ONE pure function, so the protocol is
|
|
386
397
|
* testable and exhaustive instead of being an inline expression inside the
|
|
387
398
|
* acquisition loop:
|
|
388
|
-
* - `none` the lock is fresh, or its holder is alive
|
|
389
|
-
*
|
|
399
|
+
* - `none` the lock is fresh, or its holder is alive within the alive
|
|
400
|
+
* window → wait, never steal;
|
|
401
|
+
* - `dead` a named holder that is gone past the dead threshold, OR whose
|
|
402
|
+
* pid still probes alive but whose lock is older than the alive
|
|
403
|
+
* window (A2: a recycled pid must not hold the file forever);
|
|
390
404
|
* - `empty` no body at all: nothing attributes it to a holder, so only the
|
|
391
405
|
* wide `emptyAfterMs` window may reclaim it;
|
|
392
406
|
* - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
|
|
@@ -401,7 +415,7 @@ function decideTakeover(probe) {
|
|
|
401
415
|
const namedHolder = Number.isInteger(holder) && holder > 0;
|
|
402
416
|
if (probe.body === "") return age > (probe.emptyAfterMs ?? 3e4) ? "empty" : "none";
|
|
403
417
|
if (!namedHolder) return age > (probe.corruptAfterMs ?? 36e5) ? "corrupt" : "none";
|
|
404
|
-
if (probe.alive(holder)) return "none";
|
|
418
|
+
if (probe.alive(holder)) return age > (probe.aliveAfterMs ?? 864e5) ? "dead" : "none";
|
|
405
419
|
return age > (probe.deadAfterMs ?? 1e3) ? "dead" : "none";
|
|
406
420
|
}
|
|
407
421
|
/**
|
|
@@ -1692,6 +1706,9 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1692
1706
|
let refuseMessage = "";
|
|
1693
1707
|
let parsedBody = null;
|
|
1694
1708
|
await transactIo(io, path, async (current) => {
|
|
1709
|
+
parsedBody = null;
|
|
1710
|
+
refuseMessage = "";
|
|
1711
|
+
assigned = 0;
|
|
1695
1712
|
if (current !== null && current.trim() !== "") {
|
|
1696
1713
|
let shape;
|
|
1697
1714
|
try {
|
|
@@ -3887,9 +3904,10 @@ const SECRET_PATTERNS = [
|
|
|
3887
3904
|
["stripe key", /[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g],
|
|
3888
3905
|
["github fine-grained token", /github_pat_[A-Za-z0-9_]{20,}/g],
|
|
3889
3906
|
["google api key", /AIza[0-9A-Za-z_-]{30,}/g],
|
|
3890
|
-
["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi]
|
|
3907
|
+
["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi],
|
|
3908
|
+
["basic credential", /\bBasic[\s]+[a-z0-9._~+/=]{16,}/gi]
|
|
3891
3909
|
];
|
|
3892
|
-
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]*[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]*)?)\\b([\"\\']?[\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
|
|
3910
|
+
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]*[_\\-])?((?:token|api[_-]?key|secret|password|passwd|authorization)(?:[_\\-][\\w-]*)?)\\b([\"\\']?[\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
|
|
3893
3911
|
const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,63}:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
|
|
3894
3912
|
const PEM_PRIVATE_KEY_PATTERN = new RegExp(`-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----[\\s\\S]*?-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----`, "g");
|
|
3895
3913
|
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]*)?)\s*:(?:\r)?$/i;
|
|
@@ -4996,15 +5014,18 @@ function frontmatterBlock(content) {
|
|
|
4996
5014
|
* strict catalog cannot load". `frontmatterCatalogInvalid` publishes it and
|
|
4997
5015
|
* `normalizeFrontmatter` decides each rewrite with the same predicate
|
|
4998
5016
|
* (`yamlPlainScalarNeedsQuotes`), so the audit verdict and the write path can
|
|
4999
|
-
* never disagree.
|
|
5000
|
-
*
|
|
5001
|
-
|
|
5002
|
-
|
|
5017
|
+
* never disagree. A9 (audit P2-12): the scan is ENDING-AGNOSTIC — lines are
|
|
5018
|
+
* split on `\n` with the per-line CR stripped, exactly the iteration the
|
|
5019
|
+
* rewrite path uses. The former split on the block's FIRST-LINE ending turned
|
|
5020
|
+
* a mixed-ending block (first line CRLF, entries LF) into one unsplit chunk
|
|
5021
|
+
* whose entries were all skipped, so an unquoted ` #` value read as CLEAN
|
|
5022
|
+
* while the catalog silently dropped it and the next edit would have quoted
|
|
5023
|
+
* it. Only single-line `key: value` entries are judged.
|
|
5024
|
+
*/
|
|
5025
|
+
function unsafeFrontmatterEntries(block) {
|
|
5003
5026
|
const found = [];
|
|
5004
|
-
for (const
|
|
5005
|
-
const
|
|
5006
|
-
if (clean.includes("\n") || clean.includes("\r")) continue;
|
|
5007
|
-
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(clean);
|
|
5027
|
+
for (const rawLine of block.split("\n")) {
|
|
5028
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(withoutCr(rawLine));
|
|
5008
5029
|
if (!match) continue;
|
|
5009
5030
|
const key = match[1];
|
|
5010
5031
|
if (key === void 0) continue;
|
|
@@ -5153,7 +5174,7 @@ function readFrontmatterBlock(content) {
|
|
|
5153
5174
|
return {
|
|
5154
5175
|
frontmatter,
|
|
5155
5176
|
body,
|
|
5156
|
-
unsafeValues: unsafeFrontmatterEntries(found.block
|
|
5177
|
+
unsafeValues: unsafeFrontmatterEntries(found.block),
|
|
5157
5178
|
strictFailed: strict === null,
|
|
5158
5179
|
platformStringSplit: strict?.split ?? []
|
|
5159
5180
|
};
|
|
@@ -6199,20 +6220,28 @@ var SkillLibrary = class {
|
|
|
6199
6220
|
* Structure-health facts for one skill (rc.73 A1, 008 design): body
|
|
6200
6221
|
* chars/density from SKILL.md, support groups from countSupportDirs, plus
|
|
6201
6222
|
* optional usage counts (A2 churn dimension) when the caller has them.
|
|
6202
|
-
* Derived, never persisted
|
|
6223
|
+
* Derived, never persisted. CONTRACT: `null` whenever the skill cannot be
|
|
6224
|
+
* read — a missing file AND any read failure (EACCES/EIO/…) both answer
|
|
6225
|
+
* null, so a whole health view degrades one ROW instead of throwing out of
|
|
6226
|
+
* its per-skill loop (A6, audit P2-10: the former code absorbed only
|
|
6227
|
+
* missing/EISDIR and let a transient win32 hold kill the entire view).
|
|
6203
6228
|
*/
|
|
6204
6229
|
async assessHealth(rawName, thresholds = DEFAULT_HEALTH_THRESHOLDS, counts) {
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6230
|
+
try {
|
|
6231
|
+
const name = rawName.trim();
|
|
6232
|
+
const content = await this.read(name);
|
|
6233
|
+
if (content === null) return null;
|
|
6234
|
+
return assessStructureHealth({
|
|
6235
|
+
skillName: name,
|
|
6236
|
+
bodyChars: content.length,
|
|
6237
|
+
bodyText: content,
|
|
6238
|
+
supportGroups: await this.countSupportDirs(name),
|
|
6239
|
+
patchCount: counts?.patchCount,
|
|
6240
|
+
readCount: counts?.readCount
|
|
6241
|
+
}, thresholds);
|
|
6242
|
+
} catch {
|
|
6243
|
+
return null;
|
|
6244
|
+
}
|
|
6216
6245
|
}
|
|
6217
6246
|
/** Best-effort audit trail entry; never blocks the mutation. */
|
|
6218
6247
|
async audit(skillName, action, before, after, summary) {
|
|
@@ -6388,13 +6417,13 @@ var SkillLibrary = class {
|
|
|
6388
6417
|
ok: false,
|
|
6389
6418
|
message: `Skill "${normalized}" already exists.`
|
|
6390
6419
|
};
|
|
6420
|
+
let markerWarning = "";
|
|
6391
6421
|
if (origin !== "foreground") {
|
|
6392
|
-
let markerDurabilityWarning = "";
|
|
6393
6422
|
try {
|
|
6394
6423
|
await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
6395
6424
|
} catch (error) {
|
|
6396
|
-
if (
|
|
6397
|
-
|
|
6425
|
+
if (isCommittedOnly(error)) markerWarning = `the hermes-managed marker landed but its directory fsync failed — durability unconfirmed: ${error instanceof Error ? error.message : String(error)}`;
|
|
6426
|
+
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`;
|
|
6398
6427
|
}
|
|
6399
6428
|
if (!await this.io.exists(createPath)) {
|
|
6400
6429
|
await this.io.remove(markerPath(dir, "hermes-managed")).catch(() => {});
|
|
@@ -6404,7 +6433,6 @@ var SkillLibrary = class {
|
|
|
6404
6433
|
message: `Skill "${normalized}" was archived concurrently while being created; the partial marker was removed — retry once the mover settles.`
|
|
6405
6434
|
};
|
|
6406
6435
|
}
|
|
6407
|
-
if (markerDurabilityWarning !== "") createDurabilityWarning = createDurabilityWarning === "" ? markerDurabilityWarning : `${createDurabilityWarning}; marker: ${markerDurabilityWarning}`;
|
|
6408
6436
|
}
|
|
6409
6437
|
await this.audit(normalized, "create", null, onDisk, "created");
|
|
6410
6438
|
this.notifyMutation({
|
|
@@ -6412,9 +6440,10 @@ var SkillLibrary = class {
|
|
|
6412
6440
|
name: normalized,
|
|
6413
6441
|
skillDir: dir
|
|
6414
6442
|
});
|
|
6443
|
+
const warnings = [createDurabilityWarning !== "" ? `the write landed but the directory fsync failed — durability unconfirmed: ${createDurabilityWarning}` : "", markerWarning].filter((warning) => warning !== "");
|
|
6415
6444
|
return {
|
|
6416
6445
|
ok: true,
|
|
6417
|
-
message: `Skill "${normalized}" created.${
|
|
6446
|
+
message: `Skill "${normalized}" created.${warnings.length === 0 ? "" : ` (warning: ${warnings.join("; ")})`}`,
|
|
6418
6447
|
path: dir,
|
|
6419
6448
|
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
6420
6449
|
};
|
|
@@ -6772,15 +6801,28 @@ var SkillLibrary = class {
|
|
|
6772
6801
|
for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
|
|
6773
6802
|
}
|
|
6774
6803
|
}
|
|
6804
|
+
/** R2 follow-up (audit of A2): the recycled-pid window from io's
|
|
6805
|
+
* `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
|
|
6806
|
+
* whose holder pid probes alive but whose mtime is older than the alive
|
|
6807
|
+
* window is a recycled pid, not a live writer (no write in this family
|
|
6808
|
+
* holds a lock for more than minutes) — without this, one recycled pid
|
|
6809
|
+
* blocked whole-tree snapshot recovery forever with a "retry once the
|
|
6810
|
+
* write completes" message that could never become true. A backend without
|
|
6811
|
+
* `mtime` keeps the conservative refuse-on-alive posture. */
|
|
6812
|
+
async lockHolderAgedOut(lockPath) {
|
|
6813
|
+
const mtime = await this.io.mtime?.(lockPath).catch(() => null);
|
|
6814
|
+
return typeof mtime === "number" && Date.now() - mtime > 864e5;
|
|
6815
|
+
}
|
|
6775
6816
|
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
6776
|
-
* shape AND the holder pid is not alive
|
|
6777
|
-
* or a live writer's
|
|
6817
|
+
* shape AND the holder pid is not alive (or is an aged-out recycled pid —
|
|
6818
|
+
* R2 follow-up); anything else (a user support file or a live writer's
|
|
6819
|
+
* lock) is left untouched. */
|
|
6778
6820
|
async sweepLockIfStranded(lockPath) {
|
|
6779
6821
|
const body = await this.io.readText(lockPath).catch(() => null);
|
|
6780
6822
|
if (body === null) return;
|
|
6781
6823
|
const pid = parseLockBody(body);
|
|
6782
6824
|
if (pid === null) return;
|
|
6783
|
-
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) return;
|
|
6825
|
+
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
|
|
6784
6826
|
await this.io.remove(lockPath).catch(() => {});
|
|
6785
6827
|
}
|
|
6786
6828
|
/** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
|
|
@@ -6805,7 +6847,7 @@ var SkillLibrary = class {
|
|
|
6805
6847
|
if (body === null) return;
|
|
6806
6848
|
const pid = parseLockBody(body);
|
|
6807
6849
|
if (pid === null) return;
|
|
6808
|
-
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
|
|
6850
|
+
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`);
|
|
6809
6851
|
await this.io.remove(lockPath).catch(() => {});
|
|
6810
6852
|
}
|
|
6811
6853
|
async archive(rawName, options = {}) {
|
|
@@ -7975,4 +8017,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
|
|
|
7975
8017
|
return false;
|
|
7976
8018
|
}
|
|
7977
8019
|
//#endregion
|
|
7978
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveExecOrigins, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, transactTaskGuard, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
|
8020
|
+
export { ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveExecOrigins, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, transactTaskGuard, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
package/lib/types/io.d.ts
CHANGED
|
@@ -173,7 +173,10 @@ export declare const LOCK_BODY_RE: RegExp;
|
|
|
173
173
|
/** Parse a writer-lock body into its holder pid. `null` when the body does
|
|
174
174
|
* not have the `pid:token` shape at all (e.g. a user support file named
|
|
175
175
|
* `*.lock`) — callers leave such files alone. A shape-matching body always
|
|
176
|
-
* yields a number
|
|
176
|
+
* yields a number, possibly `0` (state corruption / hand edit): there is no
|
|
177
|
+
* pid 0, so a `0` holder must be treated as DEAD by the caller —
|
|
178
|
+
* `isProcessAlive(0)` signals the caller's own process group on POSIX and
|
|
179
|
+
* answers true, so probe sites must guard `holder > 0` before probing. */
|
|
177
180
|
export declare function parseLockBody(body: string): number | null;
|
|
178
181
|
/**
|
|
179
182
|
* V27 G0.2 (EVO-IO-01): the write lock named by this claim is no longer ours
|
|
@@ -199,6 +202,14 @@ export declare const EMPTY_LOCK_TAKEOVER_MS = 30000;
|
|
|
199
202
|
/** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
|
|
200
203
|
* and far below "forever". */
|
|
201
204
|
export declare const LOCK_TEAR_TAKEOVER_MS = 3600000;
|
|
205
|
+
/** A2 (audit P1-2): even a lock whose holder pid probes ALIVE is reclaimable
|
|
206
|
+
* past this age. Liveness-by-pid cannot distinguish the original holder from
|
|
207
|
+
* an unrelated process the OS later assigned the same pid, so the plain alive
|
|
208
|
+
* probe let one recycled pid brick every writer of one state file forever.
|
|
209
|
+
* No write in this family holds a lock for more than minutes (the longest is
|
|
210
|
+
* the ~120s review window), so a day-old "alive" lock is a recycled pid, not
|
|
211
|
+
* a live writer. */
|
|
212
|
+
export declare const ALIVE_LOCK_TAKEOVER_MS = 86400000;
|
|
202
213
|
/**
|
|
203
214
|
* V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
|
|
204
215
|
* directory fsync failed — the bytes ARE visible, only their durability is
|
|
@@ -224,13 +235,17 @@ interface TakeoverProbe {
|
|
|
224
235
|
deadAfterMs?: number;
|
|
225
236
|
emptyAfterMs?: number;
|
|
226
237
|
corruptAfterMs?: number;
|
|
238
|
+
aliveAfterMs?: number;
|
|
227
239
|
}
|
|
228
240
|
/**
|
|
229
241
|
* V27 G1.1: the lock-takeover decision as ONE pure function, so the protocol is
|
|
230
242
|
* testable and exhaustive instead of being an inline expression inside the
|
|
231
243
|
* acquisition loop:
|
|
232
|
-
* - `none` the lock is fresh, or its holder is alive
|
|
233
|
-
*
|
|
244
|
+
* - `none` the lock is fresh, or its holder is alive within the alive
|
|
245
|
+
* window → wait, never steal;
|
|
246
|
+
* - `dead` a named holder that is gone past the dead threshold, OR whose
|
|
247
|
+
* pid still probes alive but whose lock is older than the alive
|
|
248
|
+
* window (A2: a recycled pid must not hold the file forever);
|
|
234
249
|
* - `empty` no body at all: nothing attributes it to a holder, so only the
|
|
235
250
|
* wide `emptyAfterMs` window may reclaim it;
|
|
236
251
|
* - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
|
|
@@ -372,7 +372,11 @@ export declare class SkillLibrary {
|
|
|
372
372
|
* Structure-health facts for one skill (rc.73 A1, 008 design): body
|
|
373
373
|
* chars/density from SKILL.md, support groups from countSupportDirs, plus
|
|
374
374
|
* optional usage counts (A2 churn dimension) when the caller has them.
|
|
375
|
-
* Derived, never persisted
|
|
375
|
+
* Derived, never persisted. CONTRACT: `null` whenever the skill cannot be
|
|
376
|
+
* read — a missing file AND any read failure (EACCES/EIO/…) both answer
|
|
377
|
+
* null, so a whole health view degrades one ROW instead of throwing out of
|
|
378
|
+
* its per-skill loop (A6, audit P2-10: the former code absorbed only
|
|
379
|
+
* missing/EISDIR and let a transient win32 hold kill the entire view).
|
|
376
380
|
*/
|
|
377
381
|
assessHealth(rawName: string, thresholds?: SkillHealthThresholds, counts?: {
|
|
378
382
|
patchCount?: number;
|
|
@@ -436,9 +440,19 @@ export declare class SkillLibrary {
|
|
|
436
440
|
* stolen by the sweep. A dead-pid residue would otherwise permanently
|
|
437
441
|
* refuse archive/restore. */
|
|
438
442
|
private deleteStrandedLocks;
|
|
443
|
+
/** R2 follow-up (audit of A2): the recycled-pid window from io's
|
|
444
|
+
* `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
|
|
445
|
+
* whose holder pid probes alive but whose mtime is older than the alive
|
|
446
|
+
* window is a recycled pid, not a live writer (no write in this family
|
|
447
|
+
* holds a lock for more than minutes) — without this, one recycled pid
|
|
448
|
+
* blocked whole-tree snapshot recovery forever with a "retry once the
|
|
449
|
+
* write completes" message that could never become true. A backend without
|
|
450
|
+
* `mtime` keeps the conservative refuse-on-alive posture. */
|
|
451
|
+
private lockHolderAgedOut;
|
|
439
452
|
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
440
|
-
* shape AND the holder pid is not alive
|
|
441
|
-
* or a live writer's
|
|
453
|
+
* shape AND the holder pid is not alive (or is an aged-out recycled pid —
|
|
454
|
+
* R2 follow-up); anything else (a user support file or a live writer's
|
|
455
|
+
* lock) is left untouched. */
|
|
442
456
|
private sweepLockIfStranded;
|
|
443
457
|
/** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
|
|
444
458
|
* only a single, non-traversing path component is safe. Dotfiles
|
package/package.json
CHANGED