@lmzhen/dsh-evolution-core 0.3.16 → 0.3.17
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 +92 -23
- package/lib/types/constants.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/serial.d.ts +10 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -64,45 +64,81 @@ function nodeEvolutionIo() {
|
|
|
64
64
|
};
|
|
65
65
|
/**
|
|
66
66
|
* Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
|
|
67
|
-
* guards the atomic write. A >
|
|
67
|
+
* guards the atomic write. A >1s-old lock is taken over ONLY after probing
|
|
68
68
|
* the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
|
|
69
|
-
* slow writer no longer loses its lock to a peer at the
|
|
69
|
+
* slow writer no longer loses its lock to a peer at the threshold (the
|
|
70
70
|
* takeover is the only best-effort surface; the retry budget fails loud —
|
|
71
71
|
* rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
|
|
72
72
|
* rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
|
|
73
73
|
* attempts (500ms), and a fail-loud throw was observed instead of a clean
|
|
74
74
|
* serialization.
|
|
75
|
+
* 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
|
|
76
|
+
* task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
|
|
77
|
+
* lock contention, retried up to 40x and finally reported as
|
|
78
|
+
* "could not acquire write lock" while the real cause was hidden.
|
|
79
|
+
* 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
|
|
80
|
+
* retry budget (budget >= 2 x threshold), so a dead holder's lock is
|
|
81
|
+
* actually recoverable within one budget instead of being arithmetically
|
|
82
|
+
* unreachable.
|
|
75
83
|
*/
|
|
76
84
|
const withWriteLock = async (path, task) => {
|
|
77
85
|
const lock = `${path}.lock`;
|
|
78
|
-
for (let attempt = 0; attempt < 40; attempt += 1)
|
|
79
|
-
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
86
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
80
87
|
try {
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} catch {}
|
|
96
|
-
continue;
|
|
88
|
+
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const code = error?.code;
|
|
91
|
+
if (code !== "EEXIST" && code !== "EPERM") throw error;
|
|
92
|
+
try {
|
|
93
|
+
const st = await stat(lock);
|
|
94
|
+
if (Date.now() - st.mtimeMs > 1e3) {
|
|
95
|
+
const holder = Number(await readFile(lock, "utf8").catch(() => ""));
|
|
96
|
+
if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
|
|
97
|
+
try {
|
|
98
|
+
await rm(lock, { force: true });
|
|
99
|
+
} catch {}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
97
102
|
}
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
98
105
|
}
|
|
99
|
-
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
100
107
|
continue;
|
|
101
108
|
}
|
|
102
|
-
|
|
109
|
+
try {
|
|
110
|
+
return await task();
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(lock, { force: true }).catch(() => {});
|
|
113
|
+
}
|
|
103
114
|
}
|
|
104
115
|
throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
|
|
105
116
|
};
|
|
117
|
+
/** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
|
|
118
|
+
* `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
|
|
119
|
+
* Lazy: only the directory a write is about to touch gets swept, once per
|
|
120
|
+
* write, inside the write lock. */
|
|
121
|
+
const sweepStaleTmps = async (path) => {
|
|
122
|
+
const dir = dirname(path);
|
|
123
|
+
const base = basename(path);
|
|
124
|
+
let entries;
|
|
125
|
+
try {
|
|
126
|
+
entries = await readdir(dir);
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const prefix = `${base}.`;
|
|
131
|
+
for (const name of entries) {
|
|
132
|
+
if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
|
|
133
|
+
const tmpPath = join(dir, name);
|
|
134
|
+
const holder = Number(name.slice(prefix.length, name.length - 4).split(".")[0] ?? "");
|
|
135
|
+
try {
|
|
136
|
+
const st = await stat(tmpPath);
|
|
137
|
+
const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
|
|
138
|
+
if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
106
142
|
return {
|
|
107
143
|
async readText(path) {
|
|
108
144
|
try {
|
|
@@ -115,6 +151,7 @@ function nodeEvolutionIo() {
|
|
|
115
151
|
async writeText(path, content) {
|
|
116
152
|
await mkdir(dirname(path), { recursive: true });
|
|
117
153
|
await withWriteLock(path, async () => {
|
|
154
|
+
await sweepStaleTmps(path);
|
|
118
155
|
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
119
156
|
await writeFile(tmp, content, "utf8");
|
|
120
157
|
await rename(tmp, path);
|
|
@@ -123,6 +160,7 @@ function nodeEvolutionIo() {
|
|
|
123
160
|
async transact(path, task) {
|
|
124
161
|
await mkdir(dirname(path), { recursive: true });
|
|
125
162
|
await withWriteLock(path, async () => {
|
|
163
|
+
await sweepStaleTmps(path);
|
|
126
164
|
let current;
|
|
127
165
|
try {
|
|
128
166
|
current = await readFile(path, "utf8");
|
|
@@ -494,6 +532,19 @@ const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
|
494
532
|
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
495
533
|
const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
496
534
|
const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
|
|
535
|
+
/** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
|
|
536
|
+
* never carry — single source for plan-validator, evolution-policy and the
|
|
537
|
+
* threat scanner (they used to each hardcode the list). */
|
|
538
|
+
const FORBIDDEN_CONTROL_KEYS = [
|
|
539
|
+
"policy",
|
|
540
|
+
"threshold",
|
|
541
|
+
"prompt_hash",
|
|
542
|
+
"model_route",
|
|
543
|
+
"evolution_config"
|
|
544
|
+
];
|
|
545
|
+
/** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
|
|
546
|
+
* scanner cover. */
|
|
547
|
+
const EVOLUTION_WRITE_TOOLS = ["memory", "skill_manage"];
|
|
497
548
|
/** Hermes authoring quality bar for descriptions (the 60-char Rule). The
|
|
498
549
|
* platform's own index limit stays in validateFrontmatter; this bar is the
|
|
499
550
|
* target the authoring standard names, enforced as ADVISORY feedback.
|
|
@@ -2362,6 +2413,24 @@ function redactSecrets(text) {
|
|
|
2362
2413
|
return out;
|
|
2363
2414
|
}
|
|
2364
2415
|
//#endregion
|
|
2416
|
+
//#region lib/types/serial.js
|
|
2417
|
+
/**
|
|
2418
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
2419
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
2420
|
+
* one file never interleave inside this process. The durable cross-process
|
|
2421
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
2422
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
2423
|
+
* memory-files — one factory now).
|
|
2424
|
+
*/
|
|
2425
|
+
function makeSerialQueue() {
|
|
2426
|
+
let chain = Promise.resolve();
|
|
2427
|
+
return (task) => {
|
|
2428
|
+
const run = chain.then(task, task);
|
|
2429
|
+
chain = run.then(() => void 0, () => void 0);
|
|
2430
|
+
return run;
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
//#endregion
|
|
2365
2434
|
//#region lib/types/skill-health.js
|
|
2366
2435
|
/**
|
|
2367
2436
|
* Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
|
|
@@ -4301,4 +4370,4 @@ function evolutionHome(env = process.env) {
|
|
|
4301
4370
|
return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
|
|
4302
4371
|
}
|
|
4303
4372
|
//#endregion
|
|
4304
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
|
|
4373
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
|
50
50
|
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
51
51
|
export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
52
52
|
export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
|
|
53
|
+
/** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
|
|
54
|
+
* never carry — single source for plan-validator, evolution-policy and the
|
|
55
|
+
* threat scanner (they used to each hardcode the list). */
|
|
56
|
+
export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config"];
|
|
57
|
+
/** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
|
|
58
|
+
* scanner cover. */
|
|
59
|
+
export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
|
|
53
60
|
/** Hermes authoring quality bar for descriptions (the 60-char Rule). The
|
|
54
61
|
* platform's own index limit stays in validateFrontmatter; this bar is the
|
|
55
62
|
* target the authoring standard names, enforced as ADVISORY feedback.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './preset-composition.ts';
|
|
|
19
19
|
export * from './prompts.ts';
|
|
20
20
|
export * from './quality.ts';
|
|
21
21
|
export * from './redact.ts';
|
|
22
|
+
export * from './serial.ts';
|
|
22
23
|
export * from './skill-health.ts';
|
|
23
24
|
export * from './signals.ts';
|
|
24
25
|
export * from './drift-signals.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
3
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
4
|
+
* one file never interleave inside this process. The durable cross-process
|
|
5
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
6
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
7
|
+
* memory-files — one factory now).
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeSerialQueue(): <T>(task: () => Promise<T>) => Promise<T>;
|
|
10
|
+
//# sourceMappingURL=serial.d.ts.map
|
package/package.json
CHANGED