@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 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 Promise.resolve(current);
792
+ return current;
774
793
  }
775
- const events = parseEvolutionEvents(current);
776
- const maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
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 Promise.resolve(JSON.stringify({
802
+ return JSON.stringify({
784
803
  version: 1,
785
- events: [...events, record]
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 appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>): Promise<number>;
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.70",
4
+ "version": "0.1.0-rc.71",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },