@fonlan/dsh-gitmemo 0.4.1 → 0.5.1

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/systemone.js CHANGED
@@ -52,6 +52,8 @@ export const NOUL_CRITERIA = {
52
52
  };
53
53
  /** The judgement asked of the model in `score` mode. */
54
54
  export const SCORE_INSTRUCTION = "How relevant is the `candidate` memory record to the `task`? Rate how reusable its conclusions, decisions, constraints, or findings are for this exact task.";
55
+ /** Accepted `policy` values, for config validation and tool schemas. */
56
+ export const GATE_POLICIES = ["rerank", "filter"];
55
57
  /** Trim a candidate summary before it is sent to the model. */
56
58
  function clamp(text, limit) {
57
59
  const collapsed = text.replace(/\s+/g, " ").trim();
@@ -153,7 +155,29 @@ export function parseUsage(payload) {
153
155
  * @returns kept/dropped hash lists and per-candidate verdicts.
154
156
  */
155
157
  export function decide(candidates, values, options) {
158
+ // `decide` is the low-level primitive: with no explicit policy it applies the
159
+ // literal threshold semantics, and with no explicit bound it applies none. The
160
+ // product policy (rerank by default, 25 % drop cap) lives in the config layer,
161
+ // which passes both in explicitly — the same split as `minKeep`.
162
+ const policy = options.policy ?? "filter";
156
163
  const cutoff = options.mode === "noul" ? options.threshold : options.scoreMin;
164
+ if (policy === "rerank") {
165
+ // Reordering cannot lose a memory: report every judged candidate as kept and
166
+ // let `ordered` carry the ranking. The threshold verdict is still visible in
167
+ // each verdict's `value`.
168
+ const all = candidates.map((candidate, index) => ({
169
+ hash: candidate.hash,
170
+ value: values[index] ?? null,
171
+ keep: true
172
+ }));
173
+ return {
174
+ kept: candidates.map((c) => c.hash),
175
+ dropped: [],
176
+ verdicts: all,
177
+ floored: false,
178
+ bounded: false
179
+ };
180
+ }
157
181
  const verdicts = candidates.map((candidate, index) => {
158
182
  const value = values[index];
159
183
  if (value === undefined)
@@ -179,12 +203,40 @@ export function decide(candidates, values, options) {
179
203
  kept = verdicts.filter((v) => v.keep).map((v) => v.hash);
180
204
  floored = true;
181
205
  }
206
+ // Bound the damage: a threshold that rejects most of a page is far more
207
+ // likely to be mis-calibrated than the page is to be mostly irrelevant, and
208
+ // the cost of the two errors is not symmetric — a dropped memory is gone,
209
+ // a kept one only costs context. Restore the best-scoring rejects until the
210
+ // drop share is within `maxDropFraction`.
211
+ let bounded = false;
212
+ const maxDropFraction = options.maxDropFraction ?? 1;
213
+ if (candidates.length > 0 && maxDropFraction < 1) {
214
+ const allowedDrops = Math.max(0, Math.floor(candidates.length * Math.max(0, maxDropFraction)));
215
+ const dropCount = candidates.length - kept.length;
216
+ if (dropCount > allowedDrops) {
217
+ const restoreCount = dropCount - allowedDrops;
218
+ const rejects = verdicts
219
+ .map((verdict, index) => ({ verdict, index }))
220
+ .filter((entry) => !entry.verdict.keep)
221
+ .sort((a, b) => {
222
+ const av = a.verdict.value ?? Number.NEGATIVE_INFINITY;
223
+ const bv = b.verdict.value ?? Number.NEGATIVE_INFINITY;
224
+ return bv - av || a.index - b.index;
225
+ })
226
+ .slice(0, restoreCount);
227
+ for (const entry of rejects)
228
+ entry.verdict.keep = true;
229
+ kept = verdicts.filter((v) => v.keep).map((v) => v.hash);
230
+ bounded = true;
231
+ }
232
+ }
182
233
  const keptSet = new Set(kept);
183
234
  return {
184
235
  kept,
185
236
  dropped: candidates.map((c) => c.hash).filter((hash) => !keptSet.has(hash)),
186
237
  verdicts,
187
- floored
238
+ floored,
239
+ bounded
188
240
  };
189
241
  }
190
242
  /** Combine a caller signal with a timeout, keeping both cancellation sources. */
@@ -206,12 +258,15 @@ function withTimeout(signal, timeoutMs) {
206
258
  */
207
259
  export async function evaluateGate(task, candidates, options, signal) {
208
260
  const mode = options.mode;
261
+ const policy = options.policy ?? "rerank";
209
262
  const base = {
210
263
  mode,
211
- model: options.model
264
+ model: options.model,
265
+ policy,
266
+ bounded: false
212
267
  };
213
268
  if (candidates.length === 0) {
214
- return { ...base, kept: [], dropped: [], verdicts: [], judged: 0, untouched: 0, degraded: false };
269
+ return { ...base, kept: [], dropped: [], verdicts: [], ordered: [], judged: 0, untouched: 0, degraded: false };
215
270
  }
216
271
  const judged = candidates.slice(0, Math.max(1, options.maxCandidates));
217
272
  const untouched = candidates.slice(judged.length);
@@ -220,6 +275,7 @@ export async function evaluateGate(task, candidates, options, signal) {
220
275
  kept: candidates.map((c) => c.hash),
221
276
  dropped: [],
222
277
  verdicts: candidates.map((c) => ({ hash: c.hash, value: null, keep: true })),
278
+ ordered: candidates.map((c) => c.hash),
223
279
  judged: judged.length,
224
280
  untouched: untouched.length,
225
281
  degraded: true,
@@ -260,24 +316,44 @@ export async function evaluateGate(task, candidates, options, signal) {
260
316
  return keepAll("system-one request failed: " + detail);
261
317
  }
262
318
  const values = parseAnswers(payload, mode, judged.length);
263
- const decision = decide(judged, values, options);
319
+ const decision = decide(judged, values, { ...options, policy });
264
320
  // Candidates past `maxCandidates` were never evaluated, so they are never
265
321
  // removed: the gate may only drop what it actually judged. Leaving them out
266
322
  // of the kept set would silently truncate every page larger than the cap.
267
323
  const keptSet = new Set([...decision.kept, ...untouched.map((c) => c.hash)]);
324
+ const verdicts = [
325
+ ...decision.verdicts,
326
+ ...untouched.map((c) => ({ hash: c.hash, value: null, keep: true }))
327
+ ];
328
+ // Best value first; nulls (unjudged / unreadable) keep page order at the end.
329
+ const ordered = candidates
330
+ .map((candidate, index) => ({ candidate, index, value: verdicts[index]?.value ?? null }))
331
+ .sort((a, b) => {
332
+ if (a.value === b.value)
333
+ return a.index - b.index;
334
+ if (a.value === null)
335
+ return 1;
336
+ if (b.value === null)
337
+ return -1;
338
+ return b.value - a.value;
339
+ })
340
+ .map((entry) => entry.candidate.hash);
268
341
  const usage = parseUsage(payload);
269
342
  return {
270
343
  ...base,
344
+ ordered,
345
+ bounded: decision.bounded,
271
346
  kept: [...candidates.map((c) => c.hash).filter((hash) => keptSet.has(hash))],
272
347
  dropped: decision.dropped,
273
- verdicts: [
274
- ...decision.verdicts,
275
- ...untouched.map((c) => ({ hash: c.hash, value: null, keep: true }))
276
- ],
348
+ verdicts,
277
349
  judged: judged.length,
278
350
  untouched: untouched.length,
279
351
  degraded: false,
280
352
  ...(usage === undefined ? {} : { usage }),
281
- ...(decision.floored ? { reason: "no candidate scored above the threshold; retained top " + decision.kept.length } : {})
353
+ ...(decision.floored
354
+ ? { reason: "no candidate scored above the threshold; retained top " + decision.kept.length }
355
+ : decision.bounded
356
+ ? { reason: "threshold would have dropped more than maxDropFraction; restored the best-scoring rejects" }
357
+ : {})
282
358
  };
283
359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-gitmemo",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Git-backed long-term memory for DeepSeek Harness: a local .mem git repo (single main branch, immutable entries, structured commit messages) with mem_search / mem_read / mem_write / mem_delete / mem_replace tools and fully automatic legacy migration (no CLI needed; the CLI remains for explicit control), auto-managed by the root agent with zero manual steps.",
5
5
  "author": "fonlan",
6
6
  "publishConfig": {
@@ -48,6 +48,10 @@
48
48
  "bundle": "tsdown",
49
49
  "watch": "tsdown --watch",
50
50
  "test": "npm run build && node --test",
51
+ "eval:lme": "node evals/longmemeval/run_retrieval.mjs",
52
+ "eval:lme:oracle": "node evals/longmemeval/run_retrieval.mjs --split oracle",
53
+ "eval:lme:s": "node --max-old-space-size=8192 evals/longmemeval/run_retrieval.mjs --split s --concurrency 8",
54
+ "eval:omni:setup": "node evals/omnimemeval/setup.mjs",
51
55
  "prepublishOnly": "npm run build",
52
56
  "release:patch": "npm version patch && git push && git push --tags",
53
57
  "release:minor": "npm version minor && git push && git push --tags",