@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/README.md +111 -35
- package/README.zh.md +41 -19
- package/lib/client/form-specs.d.ts +94 -0
- package/lib/client/form-specs.js +144 -0
- package/lib/client.js +190 -2
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +108 -0
- package/lib/index.js +69 -8
- package/lib/mem.d.ts +37 -1
- package/lib/mem.js +74 -10
- package/lib/systemone.d.ts +38 -1
- package/lib/systemone.js +85 -9
- package/package.json +5 -1
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
|
|
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.
|
|
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",
|