@lmzhen/dsh-evolution-feedback 0.3.81 → 0.3.83

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @deepseek-ai/dsh-evolution-feedback
1
+ # @lmzhen/dsh-evolution-feedback
2
2
 
3
3
  Feedback-to-quality scoring for self-evolution
4
4
 
@@ -8,7 +8,7 @@ Feedback-to-quality scoring for self-evolution
8
8
 
9
9
  #### What the model sees
10
10
 
11
- `@deepseek-ai/dsh-evolution-feedback` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
11
+ `@lmzhen/dsh-evolution-feedback` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
12
12
 
13
13
  #### Token effect
14
14
 
package/lib/index.js CHANGED
@@ -30,6 +30,17 @@ const CACHE_VERSION = 2;
30
30
  * live in the ACTIVE log (bounded by `EVENT_LOG_ROTATE_AT`), so the next fold
31
31
  * is complete. Package-private tunable, not a config surface. */
32
32
  const CACHE_SNAP_EVERY = 1024;
33
+ /** C-events-dispatch-1 (v43 audit): the timeline read flags a DROPPED band — an
34
+ * unreadable archive (EISDIR/EACCES) or a body this v1 reader cannot interpret
35
+ * (damaged, or a future version). The boot cache stores the fold BASELINE
36
+ * (`lastSeq`), and `foldWithDelta` only folds `seq > lastSeq`, so folding a
37
+ * truncated read back into it would seal the dropped band against every later
38
+ * boot — the loss becomes unrecoverable without deleting the cache by hand.
39
+ * Both cache writers withhold the refresh when that flag is set (the cache is
40
+ * disposable by contract, so the last COMPLETE fold simply stays) and say so
41
+ * through this ONE message, deduped to a single warn per process (V5-32) because
42
+ * `refold()` re-runs `restore()` before every quality push. */
43
+ const TRUNCATED_TIMELINE_WARN = "evolution-feedback: the evolution event timeline is TRUNCATED - at least one segment (an unreadable archive, or a body this v1 reader cannot interpret, e.g. a future version) was dropped, so the folded counts are incomplete; the boot cache was NOT updated (a truncated lastSeq would seal the missing band against every later boot)";
33
44
  var EvolutionFeedback = class EvolutionFeedback {
34
45
  state = {
35
46
  skills: {},
@@ -46,6 +57,21 @@ var EvolutionFeedback = class EvolutionFeedback {
46
57
  * unpersisted note there, and reverting to it resurrects a value the log
47
58
  * never held). Seeded from the fold truth, updated on successful appends. */
48
59
  durableNote = /* @__PURE__ */ new Map();
60
+ /** PLAN S1.4 (2026-09-16): targets with a feedback append still IN FLIGHT,
61
+ * split by table, as a COUNT per target. record() increments before queueing
62
+ * the append and the append task decrements when it settles (landed or rolled
63
+ * back, finally), dropping the entry only at ZERO — the count covers exactly
64
+ * the optimistic window rc.66 meant to protect, including two appends for the
65
+ * same target in flight at once (review C-P2-1: a per-target boolean marker
66
+ * was cleared by the first settle, so a merge between the two appends folded
67
+ * the second append's increment away). restore() lets memory win ONLY for these targets — a settled
68
+ * target folds from the log truth, so another process's feedback for the
69
+ * same target (audit P1-3) is never overwritten by this process's stale
70
+ * record. */
71
+ pendingAppends = {
72
+ skills: /* @__PURE__ */ new Map(),
73
+ sessions: /* @__PURE__ */ new Map()
74
+ };
49
75
  /** P2-32 (v11): process-level bound — every feedbacked session would
50
76
  * otherwise keep one entry for the whole process lifetime (a name + note
51
77
  * string per session); cap this map and drop the earliest-INSERTED entry on
@@ -74,6 +100,29 @@ var EvolutionFeedback = class EvolutionFeedback {
74
100
  attachIo(io) {
75
101
  this.io = io;
76
102
  }
103
+ /** PLAN S4.6 (2026-09-16): whether a durable backend is attached — the
104
+ * plugin's quality push (`apply`) consults this before writing the
105
+ * feedback pair into the skill-usage sidecar. Same no-io posture as
106
+ * `record`/`refold`: without `ctx.evolutionIo` (when mounted) nothing may
107
+ * turn the optimistic in-memory aggregate into a durable side effect. */
108
+ get durable() {
109
+ return this.io !== void 0 && !!this.eventsPath;
110
+ }
111
+ /** V5-32 posture applied to the read side (C-events-dispatch-1, v43): an
112
+ * event a repeated read keeps reporting must not warn on each read —
113
+ * `refold()` calls `restore()` before every quality push. Shares the bounded
114
+ * `warnedMessages` set (and its FIFO eviction) with the append-failure warn.
115
+ * @param message - the warn text; identical text warns once per process.
116
+ */
117
+ warnOnce(message) {
118
+ if (this.warnedMessages.has(message)) return;
119
+ if (this.warnedMessages.size >= this.WARNED_CAP) {
120
+ const oldest = this.warnedMessages.values().next().value;
121
+ if (oldest !== void 0) this.warnedMessages.delete(oldest);
122
+ }
123
+ this.warnedMessages.add(message);
124
+ this.warn(message);
125
+ }
77
126
  /** Durable-note map key (V4-41): a target shares one record per mode. */
78
127
  noteKey(mode, target) {
79
128
  return `${mode}\u0000${target}`;
@@ -99,20 +148,31 @@ var EvolutionFeedback = class EvolutionFeedback {
99
148
  }
100
149
  const initial = await readEvolutionTimeline(io, eventsPath, archiveNames);
101
150
  const lateArchives = (await listEventArchives(io, eventsPath)).filter((name) => !archiveNames.includes(name));
102
- const events = lateArchives.length === 0 ? initial.events : (await readEvolutionTimeline(io, eventsPath, [...archiveNames, ...lateArchives])).events;
151
+ const later = lateArchives.length === 0 ? null : await readEvolutionTimeline(io, eventsPath, [...archiveNames, ...lateArchives]);
152
+ const events = later?.events ?? initial.events;
153
+ const truncated = initial.malformed || later?.malformed === true;
103
154
  const cache = parseCache(rawCache, this.warn);
104
155
  const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
105
156
  const floor = events[0]?.seq ?? 0;
106
157
  const usableCache = cache && cache.lastSeq >= floor - 1 ? cache : null;
107
158
  const truth = usableCache ? foldWithDelta(usableCache, events, this.warn) : foldFeedbackState(events, this.warn);
159
+ const pickPending = (mode) => {
160
+ const picked = {};
161
+ for (const [target, inFlight] of this.pendingAppends[mode]) {
162
+ if (inFlight <= 0) continue;
163
+ const record = this.state[mode][target];
164
+ if (record) picked[target] = record;
165
+ }
166
+ return picked;
167
+ };
108
168
  this.state = {
109
169
  skills: {
110
170
  ...truth.skills,
111
- ...this.state.skills
171
+ ...pickPending("skills")
112
172
  },
113
173
  sessions: {
114
174
  ...truth.sessions,
115
- ...this.state.sessions
175
+ ...pickPending("sessions")
116
176
  }
117
177
  };
118
178
  const skillEntries = Object.entries(truth.skills);
@@ -129,7 +189,8 @@ var EvolutionFeedback = class EvolutionFeedback {
129
189
  if (oldest === void 0) break;
130
190
  this.durableNote.delete(oldest);
131
191
  }
132
- if (maxSeq > 0 && (!cache || cache.lastSeq < maxSeq)) try {
192
+ if (truncated) this.warnOnce(TRUNCATED_TIMELINE_WARN);
193
+ else if (maxSeq > 0 && (!cache || cache.lastSeq < maxSeq)) try {
133
194
  await io.writeText(path, JSON.stringify({
134
195
  version: CACHE_VERSION,
135
196
  lastSeq: maxSeq,
@@ -175,6 +236,8 @@ var EvolutionFeedback = class EvolutionFeedback {
175
236
  const recordIo = this.io;
176
237
  const eventsPath = this.eventsPath;
177
238
  if (!recordIo || !eventsPath) return;
239
+ const pending = this.pendingAppends[mode];
240
+ pending.set(target, (pending.get(target) ?? 0) + 1);
178
241
  this.mutate(async () => {
179
242
  try {
180
243
  const seq = await appendEvolutionEvent(recordIo, eventsPath, {
@@ -215,6 +278,10 @@ var EvolutionFeedback = class EvolutionFeedback {
215
278
  this.warn(message);
216
279
  }
217
280
  if (rollback) this.onRollback?.(target, kind);
281
+ } finally {
282
+ const remaining = (pending.get(target) ?? 1) - 1;
283
+ if (remaining > 0) pending.set(target, remaining);
284
+ else pending.delete(target);
218
285
  }
219
286
  });
220
287
  }
@@ -255,7 +322,11 @@ var EvolutionFeedback = class EvolutionFeedback {
255
322
  const recordIo = this.io;
256
323
  if (!path || !eventsPath || !recordIo) return;
257
324
  try {
258
- const { events } = await readEvolutionTimeline(recordIo, eventsPath);
325
+ const { events, malformed } = await readEvolutionTimeline(recordIo, eventsPath);
326
+ if (malformed) {
327
+ this.warnOnce(TRUNCATED_TIMELINE_WARN);
328
+ return;
329
+ }
259
330
  const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
260
331
  if (maxSeq === 0) return;
261
332
  let rawCache;
@@ -584,6 +655,7 @@ function apply(ctx, rawConfig = {}) {
584
655
  const skillUsage = skillCtx.skillUsage;
585
656
  const pushQuality = (target, kind) => {
586
657
  if (kind !== "skill") return;
658
+ if (!feedback.durable) return;
587
659
  feedback.refold().then(() => {
588
660
  const score = feedback.score(target, "skill");
589
661
  const warn = score < qualityWarnThreshold;
@@ -50,6 +50,18 @@ export declare class EvolutionFeedback {
50
50
  * unpersisted note there, and reverting to it resurrects a value the log
51
51
  * never held). Seeded from the fold truth, updated on successful appends. */
52
52
  private readonly durableNote;
53
+ /** PLAN S1.4 (2026-09-16): targets with a feedback append still IN FLIGHT,
54
+ * split by table, as a COUNT per target. record() increments before queueing
55
+ * the append and the append task decrements when it settles (landed or rolled
56
+ * back, finally), dropping the entry only at ZERO — the count covers exactly
57
+ * the optimistic window rc.66 meant to protect, including two appends for the
58
+ * same target in flight at once (review C-P2-1: a per-target boolean marker
59
+ * was cleared by the first settle, so a merge between the two appends folded
60
+ * the second append's increment away). restore() lets memory win ONLY for these targets — a settled
61
+ * target folds from the log truth, so another process's feedback for the
62
+ * same target (audit P1-3) is never overwritten by this process's stale
63
+ * record. */
64
+ private readonly pendingAppends;
53
65
  /** P2-32 (v11): process-level bound — every feedbacked session would
54
66
  * otherwise keep one entry for the whole process lifetime (a name + note
55
67
  * string per session); cap this map and drop the earliest-INSERTED entry on
@@ -71,6 +83,19 @@ export declare class EvolutionFeedback {
71
83
  constructor(io?: IoLike, home?: string, pathOverride?: string, warn?: (message: string) => void);
72
84
  /** Bind the evolution IO backend after construction (S6.4 deferred binding). */
73
85
  attachIo(io: IoLike): void;
86
+ /** PLAN S4.6 (2026-09-16): whether a durable backend is attached — the
87
+ * plugin's quality push (`apply`) consults this before writing the
88
+ * feedback pair into the skill-usage sidecar. Same no-io posture as
89
+ * `record`/`refold`: without `ctx.evolutionIo` (when mounted) nothing may
90
+ * turn the optimistic in-memory aggregate into a durable side effect. */
91
+ get durable(): boolean;
92
+ /** V5-32 posture applied to the read side (C-events-dispatch-1, v43): an
93
+ * event a repeated read keeps reporting must not warn on each read —
94
+ * `refold()` calls `restore()` before every quality push. Shares the bounded
95
+ * `warnedMessages` set (and its FIFO eviction) with the append-failure warn.
96
+ * @param message - the warn text; identical text warns once per process.
97
+ */
98
+ private warnOnce;
74
99
  /** Durable-note map key (V4-41): a target shares one record per mode. */
75
100
  private noteKey;
76
101
  private mutate;
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.3.81",
4
+ "version": "0.3.83",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,16 +27,16 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.3.81"
30
+ "@lmzhen/dsh-evolution-core": "^0.3.83"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
- "@lmzhen/dsh-evolution-io": "^0.3.81",
35
- "@lmzhen/dsh-skill-usage": "^0.3.81"
34
+ "@lmzhen/dsh-evolution-io": "^0.3.83",
35
+ "@lmzhen/dsh-skill-usage": "^0.3.83"
36
36
  },
37
37
  "devDependencies": {
38
- "@lmzhen/dsh-evolution-io": "^0.3.81",
39
- "@lmzhen/dsh-evolution-io-node": "^0.3.81",
40
- "@lmzhen/dsh-skill-usage": "^0.3.81"
38
+ "@lmzhen/dsh-evolution-io": "^0.3.83",
39
+ "@lmzhen/dsh-evolution-io-node": "^0.3.83",
40
+ "@lmzhen/dsh-skill-usage": "^0.3.83"
41
41
  }
42
42
  }