@lmzhen/dsh-evolution-feedback 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 +88 -12
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
//#region ../evolution-core/src/io.ts
|
|
@@ -41,6 +41,14 @@ function evolutionIoAdapter(provider) {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
/** Active-log split point (rc.71): when the active log reaches this many events
|
|
45
|
+
* the older half is rotated into an archive; the active stays bounded so a
|
|
46
|
+
* single append stays O(active) instead of O(total-history). Tunable default —
|
|
47
|
+
* callers may override per append (the tests use small values). */
|
|
48
|
+
const EVENT_LOG_ROTATE_AT = 4e3;
|
|
49
|
+
/** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
|
|
50
|
+
* `events.json` and never matches this glob. */
|
|
51
|
+
const EVENT_ARCHIVE_PREFIX = "events-";
|
|
44
52
|
function eventsFile(home) {
|
|
45
53
|
return join(home, "evolution", "events.json");
|
|
46
54
|
}
|
|
@@ -72,31 +80,74 @@ function parseEvolutionEvents(raw) {
|
|
|
72
80
|
* computed inside the transact, so two processes appending concurrently never
|
|
73
81
|
* collide. A malformed log is refused (bytes preserved) and the append fails.
|
|
74
82
|
* Returns the assigned seq.
|
|
83
|
+
*
|
|
84
|
+
* Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
|
|
85
|
+
* older half is copied into an archive inside the SAME transact (the archive
|
|
86
|
+
* path has its own lock, so no recursion) and the active is replaced with the
|
|
87
|
+
* newer half + the new event. seqs stay globally monotonic; a crash between
|
|
88
|
+
* archive write and active write leaves both copies, which the timeline merge
|
|
89
|
+
* dedupes by seq. An archive-write failure aborts the append (active keeps the
|
|
90
|
+
* full old content — no loss) and the caller's best-effort handling applies.
|
|
75
91
|
*/
|
|
76
|
-
async function appendEvolutionEvent(io, path, event) {
|
|
92
|
+
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
77
93
|
let assigned = 0;
|
|
78
|
-
await transactIo(io, path, (current) => {
|
|
94
|
+
await transactIo(io, path, async (current) => {
|
|
79
95
|
if (current !== null && current.trim() !== "") try {
|
|
80
96
|
JSON.parse(current);
|
|
81
97
|
} catch {
|
|
82
|
-
return
|
|
98
|
+
return current;
|
|
83
99
|
}
|
|
84
|
-
const
|
|
85
|
-
const maxSeq =
|
|
100
|
+
const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
|
|
101
|
+
const maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
86
102
|
const record = {
|
|
87
103
|
...event,
|
|
88
104
|
seq: maxSeq + 1,
|
|
89
105
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
90
106
|
};
|
|
91
107
|
assigned = record.seq;
|
|
92
|
-
return
|
|
108
|
+
return JSON.stringify({
|
|
93
109
|
version: 1,
|
|
94
|
-
events: [...
|
|
95
|
-
}, null, 2)
|
|
110
|
+
events: [...nextEvents, record]
|
|
111
|
+
}, null, 2);
|
|
96
112
|
});
|
|
97
113
|
if (assigned === 0) throw new Error(`evolution event log is malformed and was not touched: ${path}`);
|
|
98
114
|
return assigned;
|
|
99
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Split the active log at its midpoint when due: the older half is written to
|
|
118
|
+
* `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
|
|
119
|
+
* append so the active is never truncated without its copy), old archives are
|
|
120
|
+
* pruned, and the newer half is returned as the next active body. No-op when
|
|
121
|
+
* under the threshold.
|
|
122
|
+
*/
|
|
123
|
+
async function rotateIfDue(io, path, events, rotateAt) {
|
|
124
|
+
if (events.length < rotateAt) return events;
|
|
125
|
+
const mid = Math.ceil(events.length / 2);
|
|
126
|
+
const head = events.slice(0, mid);
|
|
127
|
+
const tail = events.slice(mid);
|
|
128
|
+
const anchor = tail[0]?.seq ?? events[events.length - 1]?.seq ?? 0;
|
|
129
|
+
const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
|
|
130
|
+
await io.writeText(archivePath, JSON.stringify({
|
|
131
|
+
version: 1,
|
|
132
|
+
events: head
|
|
133
|
+
}, null, 2));
|
|
134
|
+
await retainEventArchives(io, path);
|
|
135
|
+
return tail;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
|
|
139
|
+
* The name's numeric part is the last archived seq, so ordering is NUMERIC —
|
|
140
|
+
* lexicographic would rank `events-10` before `events-2`. Best-effort per
|
|
141
|
+
* removal; exported for the retention test.
|
|
142
|
+
*/
|
|
143
|
+
async function retainEventArchives(io, path) {
|
|
144
|
+
const dir = dirname(path);
|
|
145
|
+
const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json"));
|
|
146
|
+
const archiveSeq = (name) => Number.parseInt(name.slice(7, name.length - 5), 10) || 0;
|
|
147
|
+
names.sort((a, b) => archiveSeq(a) - archiveSeq(b));
|
|
148
|
+
const excess = names.slice(0, Math.max(0, names.length - 10));
|
|
149
|
+
for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
|
|
150
|
+
}
|
|
100
151
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
101
152
|
* corrupt content is flagged (and refused on append). */
|
|
102
153
|
async function readEvolutionEvents(io, path) {
|
|
@@ -122,6 +173,30 @@ async function readEvolutionEvents(io, path) {
|
|
|
122
173
|
};
|
|
123
174
|
}
|
|
124
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
178
|
+
* (active copy wins, duplicates only arise from the rotation crash window),
|
|
179
|
+
* sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
|
|
180
|
+
* malformed ARCHIVE is skipped (never bricks the boot) and still flagged.
|
|
181
|
+
*/
|
|
182
|
+
async function readEvolutionTimeline(io, path) {
|
|
183
|
+
const dir = dirname(path);
|
|
184
|
+
const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json")).sort();
|
|
185
|
+
let malformed = false;
|
|
186
|
+
const bySeq = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const name of names) {
|
|
188
|
+
const read = await readEvolutionEvents(io, join(dir, name));
|
|
189
|
+
if (read.malformed) malformed = true;
|
|
190
|
+
for (const event of read.events) bySeq.set(event.seq, event);
|
|
191
|
+
}
|
|
192
|
+
const active = await readEvolutionEvents(io, path);
|
|
193
|
+
if (active.malformed) malformed = true;
|
|
194
|
+
for (const event of active.events) bySeq.set(event.seq, event);
|
|
195
|
+
return {
|
|
196
|
+
events: [...bySeq.values()].sort((a, b) => a.seq - b.seq),
|
|
197
|
+
malformed
|
|
198
|
+
};
|
|
199
|
+
}
|
|
125
200
|
//#endregion
|
|
126
201
|
//#region ../evolution-core/src/prompts.ts
|
|
127
202
|
/**
|
|
@@ -393,13 +468,14 @@ var EvolutionFeedback = class {
|
|
|
393
468
|
if (!path || !eventsPath) return;
|
|
394
469
|
await this.mutate(async () => {
|
|
395
470
|
const rawEvents = await io.readText(eventsPath);
|
|
396
|
-
|
|
471
|
+
const archiveNames = (await io.list(dirname(eventsPath))).filter((name) => name.startsWith("events-") && name.endsWith(".json"));
|
|
472
|
+
if ((rawEvents === null || rawEvents.trim() === "") && archiveNames.length === 0) {
|
|
397
473
|
const aggregate = parseAggregate(await io.readText(path));
|
|
398
474
|
if (aggregate) try {
|
|
399
475
|
await migrateFeedbackEvents(io, eventsPath, aggregate);
|
|
400
476
|
} catch {}
|
|
401
477
|
}
|
|
402
|
-
const { events } = await
|
|
478
|
+
const { events } = await readEvolutionTimeline(io, eventsPath);
|
|
403
479
|
const cache = parseCache(await io.readText(path));
|
|
404
480
|
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
405
481
|
const truth = cache ? foldWithDelta(cache, events) : foldFeedbackState(events);
|
|
@@ -472,7 +548,7 @@ var EvolutionFeedback = class {
|
|
|
472
548
|
if (!path || !eventsPath || !recordIo) return Promise.resolve();
|
|
473
549
|
return this.mutate(async () => {
|
|
474
550
|
try {
|
|
475
|
-
const { events } = await
|
|
551
|
+
const { events } = await readEvolutionTimeline(recordIo, eventsPath);
|
|
476
552
|
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
477
553
|
if (maxSeq === 0) return;
|
|
478
554
|
await recordIo.writeText(path, JSON.stringify({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-feedback",
|
|
3
3
|
"description": "Feedback-to-quality scoring for self-evolution (community build)",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.71",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
41
|
-
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.
|
|
40
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.71",
|
|
41
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.71"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
45
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
46
|
-
"@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.
|
|
47
|
-
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.
|
|
45
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.71",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.71",
|
|
47
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.71"
|
|
48
48
|
}
|
|
49
49
|
}
|