@lmzhen/dsh-evolution-core 0.1.0-rc.70 → 0.1.0-rc.71
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 +87 -9
- package/lib/types/evolution-events.d.ts +34 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -732,6 +732,17 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
732
732
|
* append NEVER rewrites a corrupt log — the bytes stay untouched.
|
|
733
733
|
*/
|
|
734
734
|
const EVENT_LOG_VERSION = 1;
|
|
735
|
+
/** Active-log split point (rc.71): when the active log reaches this many events
|
|
736
|
+
* the older half is rotated into an archive; the active stays bounded so a
|
|
737
|
+
* single append stays O(active) instead of O(total-history). Tunable default —
|
|
738
|
+
* callers may override per append (the tests use small values). */
|
|
739
|
+
const EVENT_LOG_ROTATE_AT = 4e3;
|
|
740
|
+
/** Number of archives retained (rc.71): older archives are pruned at rotation,
|
|
741
|
+
* mirroring retainReports. The horizon covers the loop-analysis window. */
|
|
742
|
+
const EVENT_LOG_RETAIN_ARCHIVES = 10;
|
|
743
|
+
/** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
|
|
744
|
+
* `events.json` and never matches this glob. */
|
|
745
|
+
const EVENT_ARCHIVE_PREFIX = "events-";
|
|
735
746
|
function eventsFile(home) {
|
|
736
747
|
return join(home, "evolution", "events.json");
|
|
737
748
|
}
|
|
@@ -763,31 +774,74 @@ function parseEvolutionEvents(raw) {
|
|
|
763
774
|
* computed inside the transact, so two processes appending concurrently never
|
|
764
775
|
* collide. A malformed log is refused (bytes preserved) and the append fails.
|
|
765
776
|
* Returns the assigned seq.
|
|
777
|
+
*
|
|
778
|
+
* Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
|
|
779
|
+
* older half is copied into an archive inside the SAME transact (the archive
|
|
780
|
+
* path has its own lock, so no recursion) and the active is replaced with the
|
|
781
|
+
* newer half + the new event. seqs stay globally monotonic; a crash between
|
|
782
|
+
* archive write and active write leaves both copies, which the timeline merge
|
|
783
|
+
* dedupes by seq. An archive-write failure aborts the append (active keeps the
|
|
784
|
+
* full old content — no loss) and the caller's best-effort handling applies.
|
|
766
785
|
*/
|
|
767
|
-
async function appendEvolutionEvent(io, path, event) {
|
|
786
|
+
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
768
787
|
let assigned = 0;
|
|
769
|
-
await transactIo(io, path, (current) => {
|
|
788
|
+
await transactIo(io, path, async (current) => {
|
|
770
789
|
if (current !== null && current.trim() !== "") try {
|
|
771
790
|
JSON.parse(current);
|
|
772
791
|
} catch {
|
|
773
|
-
return
|
|
792
|
+
return current;
|
|
774
793
|
}
|
|
775
|
-
const
|
|
776
|
-
const maxSeq =
|
|
794
|
+
const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
|
|
795
|
+
const maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
777
796
|
const record = {
|
|
778
797
|
...event,
|
|
779
798
|
seq: maxSeq + 1,
|
|
780
799
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
781
800
|
};
|
|
782
801
|
assigned = record.seq;
|
|
783
|
-
return
|
|
802
|
+
return JSON.stringify({
|
|
784
803
|
version: 1,
|
|
785
|
-
events: [...
|
|
786
|
-
}, null, 2)
|
|
804
|
+
events: [...nextEvents, record]
|
|
805
|
+
}, null, 2);
|
|
787
806
|
});
|
|
788
807
|
if (assigned === 0) throw new Error(`evolution event log is malformed and was not touched: ${path}`);
|
|
789
808
|
return assigned;
|
|
790
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Split the active log at its midpoint when due: the older half is written to
|
|
812
|
+
* `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
|
|
813
|
+
* append so the active is never truncated without its copy), old archives are
|
|
814
|
+
* pruned, and the newer half is returned as the next active body. No-op when
|
|
815
|
+
* under the threshold.
|
|
816
|
+
*/
|
|
817
|
+
async function rotateIfDue(io, path, events, rotateAt) {
|
|
818
|
+
if (events.length < rotateAt) return events;
|
|
819
|
+
const mid = Math.ceil(events.length / 2);
|
|
820
|
+
const head = events.slice(0, mid);
|
|
821
|
+
const tail = events.slice(mid);
|
|
822
|
+
const anchor = tail[0]?.seq ?? events[events.length - 1]?.seq ?? 0;
|
|
823
|
+
const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
|
|
824
|
+
await io.writeText(archivePath, JSON.stringify({
|
|
825
|
+
version: 1,
|
|
826
|
+
events: head
|
|
827
|
+
}, null, 2));
|
|
828
|
+
await retainEventArchives(io, path);
|
|
829
|
+
return tail;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
|
|
833
|
+
* The name's numeric part is the last archived seq, so ordering is NUMERIC —
|
|
834
|
+
* lexicographic would rank `events-10` before `events-2`. Best-effort per
|
|
835
|
+
* removal; exported for the retention test.
|
|
836
|
+
*/
|
|
837
|
+
async function retainEventArchives(io, path) {
|
|
838
|
+
const dir = dirname(path);
|
|
839
|
+
const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json"));
|
|
840
|
+
const archiveSeq = (name) => Number.parseInt(name.slice(7, name.length - 5), 10) || 0;
|
|
841
|
+
names.sort((a, b) => archiveSeq(a) - archiveSeq(b));
|
|
842
|
+
const excess = names.slice(0, Math.max(0, names.length - 10));
|
|
843
|
+
for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
|
|
844
|
+
}
|
|
791
845
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
792
846
|
* corrupt content is flagged (and refused on append). */
|
|
793
847
|
async function readEvolutionEvents(io, path) {
|
|
@@ -813,6 +867,30 @@ async function readEvolutionEvents(io, path) {
|
|
|
813
867
|
};
|
|
814
868
|
}
|
|
815
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
872
|
+
* (active copy wins, duplicates only arise from the rotation crash window),
|
|
873
|
+
* sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
|
|
874
|
+
* malformed ARCHIVE is skipped (never bricks the boot) and still flagged.
|
|
875
|
+
*/
|
|
876
|
+
async function readEvolutionTimeline(io, path) {
|
|
877
|
+
const dir = dirname(path);
|
|
878
|
+
const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json")).sort();
|
|
879
|
+
let malformed = false;
|
|
880
|
+
const bySeq = /* @__PURE__ */ new Map();
|
|
881
|
+
for (const name of names) {
|
|
882
|
+
const read = await readEvolutionEvents(io, join(dir, name));
|
|
883
|
+
if (read.malformed) malformed = true;
|
|
884
|
+
for (const event of read.events) bySeq.set(event.seq, event);
|
|
885
|
+
}
|
|
886
|
+
const active = await readEvolutionEvents(io, path);
|
|
887
|
+
if (active.malformed) malformed = true;
|
|
888
|
+
for (const event of active.events) bySeq.set(event.seq, event);
|
|
889
|
+
return {
|
|
890
|
+
events: [...bySeq.values()].sort((a, b) => a.seq - b.seq),
|
|
891
|
+
malformed
|
|
892
|
+
};
|
|
893
|
+
}
|
|
816
894
|
//#endregion
|
|
817
895
|
//#region lib/types/prompts.js
|
|
818
896
|
/**
|
|
@@ -3130,4 +3208,4 @@ function evolutionHome(env = process.env) {
|
|
|
3130
3208
|
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "evolution");
|
|
3131
3209
|
}
|
|
3132
3210
|
//#endregion
|
|
3133
|
-
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_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, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_LOG_VERSION, EvolutionGateSet, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, 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, 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, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, foldCuratorFields, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutateUsage, mutationsFile, nodeEvolutionIo, normalizeUsageRecord, observeEvent, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, recordMutation, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, validateFrontmatter, verifyPromptBundle };
|
|
3211
|
+
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_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, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, 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, 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, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, foldCuratorFields, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutateUsage, mutationsFile, nodeEvolutionIo, normalizeUsageRecord, observeEvent, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, validateFrontmatter, verifyPromptBundle };
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type EvolutionIoLike } from './io.ts';
|
|
13
13
|
export declare const EVENT_LOG_VERSION = 1;
|
|
14
|
+
/** Active-log split point (rc.71): when the active log reaches this many events
|
|
15
|
+
* the older half is rotated into an archive; the active stays bounded so a
|
|
16
|
+
* single append stays O(active) instead of O(total-history). Tunable default —
|
|
17
|
+
* callers may override per append (the tests use small values). */
|
|
18
|
+
export declare const EVENT_LOG_ROTATE_AT = 4000;
|
|
19
|
+
/** Number of archives retained (rc.71): older archives are pruned at rotation,
|
|
20
|
+
* mirroring retainReports. The horizon covers the loop-analysis window. */
|
|
21
|
+
export declare const EVENT_LOG_RETAIN_ARCHIVES = 10;
|
|
22
|
+
/** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
|
|
23
|
+
* `events.json` and never matches this glob. */
|
|
24
|
+
export declare const EVENT_ARCHIVE_PREFIX = "events-";
|
|
14
25
|
export interface EvolutionEvent {
|
|
15
26
|
/** Global monotonic order key, assigned inside the append transact. */
|
|
16
27
|
seq: number;
|
|
@@ -42,8 +53,23 @@ export declare function parseEvolutionEvents(raw: string | null): EvolutionEvent
|
|
|
42
53
|
* computed inside the transact, so two processes appending concurrently never
|
|
43
54
|
* collide. A malformed log is refused (bytes preserved) and the append fails.
|
|
44
55
|
* Returns the assigned seq.
|
|
56
|
+
*
|
|
57
|
+
* Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
|
|
58
|
+
* older half is copied into an archive inside the SAME transact (the archive
|
|
59
|
+
* path has its own lock, so no recursion) and the active is replaced with the
|
|
60
|
+
* newer half + the new event. seqs stay globally monotonic; a crash between
|
|
61
|
+
* archive write and active write leaves both copies, which the timeline merge
|
|
62
|
+
* dedupes by seq. An archive-write failure aborts the append (active keeps the
|
|
63
|
+
* full old content — no loss) and the caller's best-effort handling applies.
|
|
64
|
+
*/
|
|
65
|
+
export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>, rotateAt?: number): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
|
|
68
|
+
* The name's numeric part is the last archived seq, so ordering is NUMERIC —
|
|
69
|
+
* lexicographic would rank `events-10` before `events-2`. Best-effort per
|
|
70
|
+
* removal; exported for the retention test.
|
|
45
71
|
*/
|
|
46
|
-
export declare function
|
|
72
|
+
export declare function retainEventArchives(io: EvolutionIoLike, path: string): Promise<void>;
|
|
47
73
|
export interface EventLogRead {
|
|
48
74
|
events: EvolutionEvent[];
|
|
49
75
|
/** True when the body is not valid JSON (syntax-level damage): refused on
|
|
@@ -55,4 +81,11 @@ export interface EventLogRead {
|
|
|
55
81
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
56
82
|
* corrupt content is flagged (and refused on append). */
|
|
57
83
|
export declare function readEvolutionEvents(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
|
|
84
|
+
/**
|
|
85
|
+
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
86
|
+
* (active copy wins, duplicates only arise from the rotation crash window),
|
|
87
|
+
* sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
|
|
88
|
+
* malformed ARCHIVE is skipped (never bricks the boot) and still flagged.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
|
|
58
91
|
//# sourceMappingURL=evolution-events.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-core",
|
|
3
3
|
"description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.71",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|