dsh-daoing-memory 0.1.18 → 0.1.19

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/client.js CHANGED
@@ -8632,61 +8632,61 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8632
8632
  document.head.appendChild(tag);
8633
8633
  }
8634
8634
  var Workbench_module_css_default = {
8635
- "cardDetail": "vPI34G_cardDetail",
8636
- "btnDanger": "vPI34G_btnDanger",
8637
- "warnLine": "vPI34G_warnLine",
8638
- "badgePinned": "vPI34G_badgePinned",
8639
- "workbenchTabs": "vPI34G_workbenchTabs",
8640
- "cardMeta": "vPI34G_cardMeta",
8641
- "actions": "vPI34G_actions",
8642
- "statusCandidate": "vPI34G_statusCandidate",
8643
- "title": "vPI34G_title",
8635
+ "body": "vPI34G_body",
8644
8636
  "titleBlock": "vPI34G_titleBlock",
8645
- "cardHead": "vPI34G_cardHead",
8646
- "badge": "vPI34G_badge",
8647
- "badgeNegative": "vPI34G_badgeNegative",
8648
- "detailLabel": "vPI34G_detailLabel",
8649
- "toolbar": "vPI34G_toolbar",
8650
- "cardSub": "vPI34G_cardSub",
8651
- "statusOther": "vPI34G_statusOther",
8652
- "trust": "vPI34G_trust",
8653
- "header": "vPI34G_header",
8654
- "concernBg": "vPI34G_concernBg",
8655
- "panel": "vPI34G_panel",
8656
- "workbenchTabActive": "vPI34G_workbenchTabActive",
8637
+ "check": "vPI34G_check",
8638
+ "tabActive": "vPI34G_tabActive",
8639
+ "workbenchTabs": "vPI34G_workbenchTabs",
8640
+ "badgePositive": "vPI34G_badgePositive",
8657
8641
  "error": "vPI34G_error",
8642
+ "badgeNegative": "vPI34G_badgeNegative",
8643
+ "detailRow": "vPI34G_detailRow",
8658
8644
  "okLine": "vPI34G_okLine",
8659
- "page": "vPI34G_page",
8660
- "payloadFull": "vPI34G_payloadFull",
8661
- "payloadToggle": "vPI34G_payloadToggle",
8645
+ "tab": "vPI34G_tab",
8662
8646
  "badgeConflict": "vPI34G_badgeConflict",
8663
- "btn": "vPI34G_btn",
8664
- "badgePositive": "vPI34G_badgePositive",
8665
- "statusLive": "vPI34G_statusLive",
8647
+ "payloadToggle": "vPI34G_payloadToggle",
8648
+ "cellActions": "vPI34G_cellActions",
8649
+ "statusCandidate": "vPI34G_statusCandidate",
8650
+ "page": "vPI34G_page",
8666
8651
  "table": "vPI34G_table",
8667
- "statusChallenged": "vPI34G_statusChallenged",
8668
- "historyTitle": "vPI34G_historyTitle",
8669
- "workbenchTab": "vPI34G_workbenchTab",
8670
- "tab": "vPI34G_tab",
8671
- "field": "vPI34G_field",
8652
+ "trust": "vPI34G_trust",
8653
+ "warnLine": "vPI34G_warnLine",
8654
+ "cardMeta": "vPI34G_cardMeta",
8672
8655
  "valueCell": "vPI34G_valueCell",
8673
- "card": "vPI34G_card",
8674
- "tabActive": "vPI34G_tabActive",
8675
- "tabs": "vPI34G_tabs",
8656
+ "badge": "vPI34G_badge",
8676
8657
  "column": "vPI34G_column",
8677
8658
  "sectionTitle": "vPI34G_sectionTitle",
8659
+ "workbenchTabActive": "vPI34G_workbenchTabActive",
8660
+ "rules": "vPI34G_rules",
8661
+ "concernBg": "vPI34G_concernBg",
8662
+ "field": "vPI34G_field",
8663
+ "tabs": "vPI34G_tabs",
8664
+ "statusLive": "vPI34G_statusLive",
8665
+ "detailLabel": "vPI34G_detailLabel",
8666
+ "statusOther": "vPI34G_statusOther",
8667
+ "columns": "vPI34G_columns",
8668
+ "badgePinned": "vPI34G_badgePinned",
8669
+ "title": "vPI34G_title",
8678
8670
  "statline": "vPI34G_statline",
8679
- "check": "vPI34G_check",
8680
- "cardTitle": "vPI34G_cardTitle",
8671
+ "btn": "vPI34G_btn",
8672
+ "cardDetail": "vPI34G_cardDetail",
8673
+ "payloadFull": "vPI34G_payloadFull",
8674
+ "statusChallenged": "vPI34G_statusChallenged",
8675
+ "cardHead": "vPI34G_cardHead",
8676
+ "list": "vPI34G_list",
8677
+ "historyTitle": "vPI34G_historyTitle",
8678
+ "toolbar": "vPI34G_toolbar",
8681
8679
  "subtitle": "vPI34G_subtitle",
8682
- "detailRow": "vPI34G_detailRow",
8680
+ "cardSub": "vPI34G_cardSub",
8683
8681
  "form": "vPI34G_form",
8684
- "body": "vPI34G_body",
8685
- "list": "vPI34G_list",
8686
- "cellActions": "vPI34G_cellActions",
8682
+ "cardTitle": "vPI34G_cardTitle",
8683
+ "panel": "vPI34G_panel",
8684
+ "actions": "vPI34G_actions",
8685
+ "header": "vPI34G_header",
8686
+ "workbenchTab": "vPI34G_workbenchTab",
8687
8687
  "empty": "vPI34G_empty",
8688
- "columns": "vPI34G_columns",
8689
- "rules": "vPI34G_rules"
8688
+ "btnDanger": "vPI34G_btnDanger",
8689
+ "card": "vPI34G_card"
8690
8690
  };
8691
8691
  //#endregion
8692
8692
  //#region lib/types/client/ExperiencePage.js
@@ -9328,17 +9328,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9328
9328
  document.head.appendChild(tag);
9329
9329
  }
9330
9330
  var ReasonDialog_module_css_default = {
9331
- "btnConfirm": "HH3IWW_btnConfirm",
9332
- "title": "HH3IWW_title",
9333
- "hint": "HH3IWW_hint",
9331
+ "subtitle": "HH3IWW_subtitle",
9332
+ "overlay": "HH3IWW_overlay",
9334
9333
  "footer": "HH3IWW_footer",
9335
- "textarea": "HH3IWW_textarea",
9336
9334
  "buttons": "HH3IWW_buttons",
9337
- "subtitle": "HH3IWW_subtitle",
9335
+ "btnCancel": "HH3IWW_btnCancel",
9338
9336
  "dialog": "HH3IWW_dialog",
9339
- "overlay": "HH3IWW_overlay",
9337
+ "btnConfirm": "HH3IWW_btnConfirm",
9338
+ "hint": "HH3IWW_hint",
9340
9339
  "header": "HH3IWW_header",
9341
- "btnCancel": "HH3IWW_btnCancel"
9340
+ "title": "HH3IWW_title",
9341
+ "textarea": "HH3IWW_textarea"
9342
9342
  };
9343
9343
  //#endregion
9344
9344
  //#region lib/types/client/ReasonDialog.js
package/lib/index.js CHANGED
@@ -357,6 +357,35 @@ var MemoryStore = class {
357
357
  return scored.slice(0, topK);
358
358
  }
359
359
  /**
360
+ * Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
361
+ * Scores on gist+situation only (the lesson's identity) so identical lessons
362
+ * with divergent limits/paths still match. Returns the top-K matches (rough
363
+ * prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
364
+ */
365
+ findNearDuplicates(queryTokens, statuses, topK) {
366
+ if (queryTokens.size === 0 || topK <= 0) return [];
367
+ const sql = `
368
+ SELECT * FROM experiences
369
+ WHERE deleted = 0 AND status IN (${statuses.map(() => "?").join(", ")})
370
+ `;
371
+ const rows = this.db.prepare(sql).all(...statuses);
372
+ const scored = [];
373
+ for (const row of rows) {
374
+ const snapshot = this.rowToExperience(row);
375
+ const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation].join(" ")));
376
+ if (hay.size === 0) continue;
377
+ let hits = 0;
378
+ for (const token of queryTokens) if (hay.has(token)) hits += 1;
379
+ const score = hits / Math.sqrt(queryTokens.size * hay.size);
380
+ if (score > 0) scored.push({
381
+ snapshot,
382
+ score
383
+ });
384
+ }
385
+ scored.sort((a, b) => b.score - a.score);
386
+ return scored.slice(0, topK);
387
+ }
388
+ /**
360
389
  * Near-duplicate detection for the information-gain gate (007 flow-log fix).
361
390
  * Unlike recallCandidates it scores on gist+situation only (the lesson's
362
391
  * identity), so identical lessons with divergent limits/paths still match.
@@ -1045,7 +1074,7 @@ const INGEST_SOURCE_PRIOR = {
1045
1074
  }
1046
1075
  };
1047
1076
  /** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
1048
- function tokenOverlap(a, b) {
1077
+ function tokenOverlap$1(a, b) {
1049
1078
  if (a.size === 0 || b.size === 0) return 0;
1050
1079
  let hits = 0;
1051
1080
  for (const t of a) if (b.has(t)) hits += 1;
@@ -1074,7 +1103,7 @@ var MemoryCore = class MemoryCore {
1074
1103
  });
1075
1104
  }
1076
1105
  /** 生: refine one completed trajectory into a candidate (dual gate). */
1077
- refine(request, actor) {
1106
+ refine(request, actor, opts) {
1078
1107
  if (!((request.evidence.traceRef ?? "").trim() !== "" || (request.evidence.sessionRef ?? "").trim() !== "" || (request.evidence.note ?? "").trim() !== "")) return {
1079
1108
  accepted: false,
1080
1109
  reason: "rejected-evidence: every assertion needs an episodic evidence pointer (traceRef/sessionRef/note)"
@@ -1088,25 +1117,27 @@ var MemoryCore = class MemoryCore {
1088
1117
  accepted: false,
1089
1118
  reason: `rejected-complexity: trajectory below both gates (tokens < ${String(this.config.complexityTokenGate)}, steps < ${String(this.config.complexityStepGate)}, no failure, not human-marked)`
1090
1119
  };
1091
- const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(" ")));
1092
- const near = this.store.findNearDuplicate(dedupTokens, [
1093
- "candidate",
1094
- "live",
1095
- "challenged",
1096
- "archived",
1097
- "cold"
1098
- ]);
1099
- if (near !== void 0 && near.score >= this.config.duplicateOverlapGate) {
1100
- this.ledger("corroborate", "experience", near.snapshot.id, actor, {
1101
- family: request.family,
1102
- gist: request.gist,
1103
- score: near.score
1104
- });
1105
- return {
1106
- accepted: false,
1107
- reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead",
1108
- corroboratedId: near.snapshot.id
1109
- };
1120
+ if (opts?.dedup !== false) {
1121
+ const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(" ")));
1122
+ const near = this.store.findNearDuplicate(dedupTokens, [
1123
+ "candidate",
1124
+ "live",
1125
+ "challenged",
1126
+ "archived",
1127
+ "cold"
1128
+ ]);
1129
+ if (near !== void 0 && near.score >= this.config.duplicateOverlapGate) {
1130
+ this.ledger("corroborate", "experience", near.snapshot.id, actor, {
1131
+ family: request.family,
1132
+ gist: request.gist,
1133
+ score: near.score
1134
+ });
1135
+ return {
1136
+ accepted: false,
1137
+ reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead",
1138
+ corroboratedId: near.snapshot.id
1139
+ };
1140
+ }
1110
1141
  }
1111
1142
  const now = Date.now();
1112
1143
  const snapshot = {
@@ -1154,7 +1185,7 @@ var MemoryCore = class MemoryCore {
1154
1185
  * candidate carrying provenance (sourceType + sourceRef), the source-authority
1155
1186
  * prior, and the declared context scope. Candidates never recall until verified.
1156
1187
  */
1157
- ingest(request, actor) {
1188
+ ingest(request, actor, opts) {
1158
1189
  if ((request.sourceRef ?? "").trim() === "") throw new Error("memory: ingest requires a non-empty sourceRef provenance");
1159
1190
  const prior = INGEST_SOURCE_PRIOR[request.sourceType] ?? INGEST_SOURCE_PRIOR.other;
1160
1191
  const context = request.context ?? "";
@@ -1170,32 +1201,34 @@ var MemoryCore = class MemoryCore {
1170
1201
  continue;
1171
1202
  }
1172
1203
  const dedupTokens = new Set(tokenize([item.gist, ...item.situation].join(" ")));
1173
- const cross = this.store.findNearDuplicate(dedupTokens, [
1174
- "candidate",
1175
- "live",
1176
- "challenged",
1177
- "archived",
1178
- "cold"
1179
- ]);
1180
- if (cross !== void 0 && cross.score >= this.config.duplicateOverlapGate) {
1181
- this.ledger("corroborate", "experience", cross.snapshot.id, actor, {
1182
- family: item.family,
1183
- gist: item.gist,
1184
- score: cross.score,
1185
- via: "ingest"
1186
- });
1187
- rejected.push({
1188
- gist: item.gist,
1189
- reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead"
1190
- });
1191
- continue;
1192
- }
1193
- if (acceptedDedup.some((prev) => tokenOverlap(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
1194
- rejected.push({
1195
- gist: item.gist,
1196
- reason: "rejected-information-gain: near-duplicate of another item in this same ingest batch"
1197
- });
1198
- continue;
1204
+ if (opts?.dedup !== false) {
1205
+ const cross = this.store.findNearDuplicate(dedupTokens, [
1206
+ "candidate",
1207
+ "live",
1208
+ "challenged",
1209
+ "archived",
1210
+ "cold"
1211
+ ]);
1212
+ if (cross !== void 0 && cross.score >= this.config.duplicateOverlapGate) {
1213
+ this.ledger("corroborate", "experience", cross.snapshot.id, actor, {
1214
+ family: item.family,
1215
+ gist: item.gist,
1216
+ score: cross.score,
1217
+ via: "ingest"
1218
+ });
1219
+ rejected.push({
1220
+ gist: item.gist,
1221
+ reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead"
1222
+ });
1223
+ continue;
1224
+ }
1225
+ if (acceptedDedup.some((prev) => tokenOverlap$1(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
1226
+ rejected.push({
1227
+ gist: item.gist,
1228
+ reason: "rejected-information-gain: near-duplicate of another item in this same ingest batch"
1229
+ });
1230
+ continue;
1231
+ }
1199
1232
  }
1200
1233
  const now = Date.now();
1201
1234
  const snapshot = {
@@ -2346,6 +2379,24 @@ var MemoryCore = class MemoryCore {
2346
2379
  this.ledger("release-cold", "experience", next.id, actor, { revision: next.revision }, request.reason);
2347
2380
  return next;
2348
2381
  }
2382
+ /** Record a corroboration ledger event for a rejected near-duplicate.
2383
+ * The LLM semantic-dedup path routes a DUPLICATE verdict here so the
2384
+ * ledger stays consistent with the mechanical-gate path (refine/ingest). */
2385
+ markCorroborate(nearId, actor, req, via, score) {
2386
+ this.ledger("corroborate", "experience", nearId, actor, {
2387
+ family: req.family,
2388
+ gist: req.gist,
2389
+ score,
2390
+ via
2391
+ });
2392
+ }
2393
+ /** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
2394
+ * service layer asks here for the mechanical word-overlap top-K before it
2395
+ * runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
2396
+ nearDuplicateCandidates(gist, situation, statuses, topK) {
2397
+ const tokens = new Set(tokenize([gist, ...situation].join(" ")));
2398
+ return this.store.findNearDuplicates(tokens, statuses, topK);
2399
+ }
2349
2400
  /** Human-approved self-growth: merge new evidence into a live, human-approved
2350
2401
  * experience as an incremental new revision (revision+1, parent superseded).
2351
2402
  * Only approvedBy='human' + status='live' may evolve; agent/system-approved
@@ -3630,6 +3681,23 @@ function actorOf(agent) {
3630
3681
  const sessionId = agent.session?.id;
3631
3682
  return sessionId === void 0 ? "agent" : `agent:${sessionId}`;
3632
3683
  }
3684
+ /** Top-K coarse candidate pool for the LLM semantic-dedup prefilter. */
3685
+ const DEDUP_TOP_K = 5;
3686
+ /** Statuses scanned for a near-duplicate candidate pool. */
3687
+ const DEDUP_STATUSES = [
3688
+ "candidate",
3689
+ "live",
3690
+ "challenged",
3691
+ "archived",
3692
+ "cold"
3693
+ ];
3694
+ /** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
3695
+ function tokenOverlap(a, b) {
3696
+ if (a.size === 0 || b.size === 0) return 0;
3697
+ let hits = 0;
3698
+ for (const t of a) if (b.has(t)) hits += 1;
3699
+ return hits / Math.sqrt(a.size * b.size);
3700
+ }
3633
3701
  /**
3634
3702
  * Remote face of the memory library. All methods delegate to the core; the
3635
3703
  * core carries the 生·用·修·记 mechanism semantics.
@@ -4339,9 +4407,39 @@ let MemoryService = (() => {
4339
4407
  this.llm = llm;
4340
4408
  this.defaultModel = defaultModel;
4341
4409
  }
4342
- /** 生: refine a completed trajectory into an experience candidate. */
4343
- refine(agent, request) {
4344
- return this.core.refine(request, actorOf(agent));
4410
+ /** 生: refine a completed trajectory into an experience candidate.
4411
+ * Semantic-dedup: coarse word-overlap candidate set first; when it is
4412
+ * non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
4413
+ * (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
4414
+ async refine(agent, request) {
4415
+ const actor = actorOf(agent);
4416
+ const cands = this.core.nearDuplicateCandidates(request.gist, request.situation, DEDUP_STATUSES, DEDUP_TOP_K);
4417
+ if (cands.length === 0) return this.core.refine(request, actor);
4418
+ const verdict = await this.dedupVerdictWithLlm(agent, request, cands);
4419
+ if (verdict === "different") return this.core.refine(request, actor, { dedup: false });
4420
+ const best = cands[0];
4421
+ if (verdict === "merge" && best.snapshot.approvedBy === "human" && best.snapshot.status === "live") return {
4422
+ accepted: false,
4423
+ reason: "semantic-merge: merged into a human-approved live experience",
4424
+ corroboratedId: this.core.evolve({
4425
+ id: best.snapshot.id,
4426
+ reason: "semantic-merge: " + request.gist,
4427
+ gist: request.gist,
4428
+ situation: request.situation,
4429
+ path: request.path,
4430
+ reasoning: request.reasoning,
4431
+ limits: request.limits
4432
+ }, actor).id
4433
+ };
4434
+ this.core.markCorroborate(best.snapshot.id, actor, {
4435
+ family: request.family,
4436
+ gist: request.gist
4437
+ }, "refine", best.score);
4438
+ return {
4439
+ accepted: false,
4440
+ reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead",
4441
+ corroboratedId: best.snapshot.id
4442
+ };
4345
4443
  }
4346
4444
  /** 用: recall + adjudication + injection budget + negative channel. */
4347
4445
  recall(agent, request) {
@@ -4351,9 +4449,72 @@ let MemoryService = (() => {
4351
4449
  report(agent, request) {
4352
4450
  return this.core.report(request, actorOf(agent));
4353
4451
  }
4354
- /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1). */
4355
- ingest(agent, request) {
4356
- return this.core.ingest(request, actorOf(agent));
4452
+ /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
4453
+ * Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
4454
+ * the LLM judges [merge/duplicate/different] on the FULL lesson. */
4455
+ async ingest(agent, request) {
4456
+ const actor = actorOf(agent);
4457
+ const prior = this.core.ingest({
4458
+ ...request,
4459
+ experiences: []
4460
+ }, actor, { dedup: false }).sourcePrior;
4461
+ const accepted = [];
4462
+ const rejected = [];
4463
+ const acceptedDedup = [];
4464
+ for (const item of request.experiences) {
4465
+ if (item.kind === "negative" && (item.failureReason ?? "").trim() === "") {
4466
+ rejected.push({
4467
+ gist: item.gist,
4468
+ reason: "rejected-schema: negative experiences must carry the confirmed failureReason"
4469
+ });
4470
+ continue;
4471
+ }
4472
+ const cands = this.core.nearDuplicateCandidates(item.gist, item.situation, DEDUP_STATUSES, DEDUP_TOP_K);
4473
+ if (cands.length > 0) {
4474
+ const best = cands[0];
4475
+ const verdict = await this.dedupVerdictWithLlm(agent, item, cands);
4476
+ if (verdict !== "different") {
4477
+ if (verdict === "merge" && best.snapshot.approvedBy === "human" && best.snapshot.status === "live") this.core.evolve({
4478
+ id: best.snapshot.id,
4479
+ reason: "semantic-merge: " + item.gist,
4480
+ gist: item.gist,
4481
+ situation: item.situation,
4482
+ path: item.path,
4483
+ reasoning: item.reasoning,
4484
+ limits: item.limits
4485
+ }, actor);
4486
+ else this.core.markCorroborate(best.snapshot.id, actor, {
4487
+ family: item.family,
4488
+ gist: item.gist
4489
+ }, "ingest", best.score);
4490
+ rejected.push({
4491
+ gist: item.gist,
4492
+ reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead"
4493
+ });
4494
+ continue;
4495
+ }
4496
+ }
4497
+ const tokens = new Set(tokenize([item.gist, ...item.situation].join(" ")));
4498
+ if (acceptedDedup.some((prev) => tokenOverlap(tokens, prev) >= this.core.config.duplicateOverlapGate)) {
4499
+ rejected.push({
4500
+ gist: item.gist,
4501
+ reason: "rejected-information-gain: near-duplicate of another item in this same ingest batch"
4502
+ });
4503
+ continue;
4504
+ }
4505
+ const itemResult = this.core.ingest({
4506
+ ...request,
4507
+ experiences: [item]
4508
+ }, actor, { dedup: cands.length === 0 });
4509
+ for (const a of itemResult.accepted) accepted.push(a);
4510
+ for (const r of itemResult.rejected) rejected.push(r);
4511
+ acceptedDedup.push(tokens);
4512
+ }
4513
+ return {
4514
+ accepted,
4515
+ rejected,
4516
+ sourcePrior: prior
4517
+ };
4357
4518
  }
4358
4519
  /** 修: propose a revised draft for a challenged experience. */
4359
4520
  revise(agent, request) {
@@ -4403,6 +4564,58 @@ let MemoryService = (() => {
4403
4564
  } catch {}
4404
4565
  }
4405
4566
  /**
4567
+ * LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
4568
+ * Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
4569
+ * and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
4570
+ * (accept) when the LLM is unavailable or the model route is missing, so the
4571
+ * mechanical gate in core still governs without an LLM.
4572
+ */
4573
+ async dedupVerdictWithLlm(agent, lesson, cands) {
4574
+ const header = agent.session.requestHeader()?.config;
4575
+ let provider = header?.provider;
4576
+ let model = header?.model;
4577
+ if (provider === void 0 || model === void 0) {
4578
+ const sel = this.defaultModel?.currentSelection();
4579
+ if (sel !== void 0) {
4580
+ provider = sel.provider;
4581
+ model = sel.model;
4582
+ }
4583
+ }
4584
+ if (provider === void 0 || model === void 0) return "different";
4585
+ if (this.llm === void 0) return "different";
4586
+ const fmtLesson = (l) => "gist: " + l.gist + "\nsituation: " + l.situation.join(" | ") + "\npath:\n" + l.path.map((p) => " " + p.order + ". " + p.action).join("\n") + "\nreasoning: " + l.reasoning + "\nlimits: " + l.limits.join(" | ");
4587
+ const candText = cands.map((c, i) => "候选 " + (i + 1) + "(词面相似度 " + c.score.toFixed(3) + "):\n" + fmtLesson(c.snapshot)).join("\n\n");
4588
+ const systemPrompt = "你是记忆系统的语义去重裁判。给定一条\"新提炼的教训\"和若干\"已存在的候选经验\",判断它们是否指同一件事。\n只输出一个词:\n- merge:语义可合并,新教训是对候选经验的增量补充\n- duplicate:语义重复,新教训无新增信息\n- different:语义不同,是不同的经验\n只输出 merge/duplicate/different 之一,不要任何其它内容。";
4589
+ const messages = [createUserMessage({
4590
+ content: [{
4591
+ type: "text",
4592
+ text: "新提炼的教训:\n" + fmtLesson(lesson) + "\n\n候选经验:\n" + candText + "\n\n判定(merge/duplicate/different):"
4593
+ }],
4594
+ source: {
4595
+ kind: "plugin",
4596
+ plugin: "dsh-daoing-memory"
4597
+ }
4598
+ })];
4599
+ let text = "";
4600
+ const options = {
4601
+ provider,
4602
+ model,
4603
+ messages,
4604
+ system: systemPrompt,
4605
+ maxTokens: 50,
4606
+ sessionId: agent.session.id
4607
+ };
4608
+ try {
4609
+ for await (const chunk of this.llm.stream(options)) if (chunk.type === "text-delta") text += chunk.text;
4610
+ } catch {
4611
+ return "different";
4612
+ }
4613
+ const t = text.trim().toLowerCase();
4614
+ if (t.includes("merge")) return "merge";
4615
+ if (t.includes("duplicate")) return "duplicate";
4616
+ return "different";
4617
+ }
4618
+ /**
4406
4619
  * Call the LLM to summarize deletion records into extraction feedback rules.
4407
4620
  * Uses ctx.llm.stream() with the agent's current model route.
4408
4621
  */
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * @module dsh-daoing-memory/core
22
22
  */
23
- import type { ConcernTree, ConsolidateRequest, ConsolidateResult, DiaryAppendRequest, DiaryAppendResult, DiaryEntry, ExperienceListFilter, ExperienceSnapshot, ExtractionRecord, ExtractFactsRequest, ExtractFactsResult, FactEntry, EvolveExperienceRequest, HumanAddExperienceRequest, HumanAddFactRequest, HumanAckDiaryRequest, HumanArchiveExperienceRequest, HumanConfirmFactRequest, HumanDeleteConcernRequest, HumanDeleteExperienceRequest, HumanDeleteFactRequest, HumanEditExperienceRequest, HumanEditFactRequest, HumanPinRequest, HumanSetConcernStatusRequest, HumanReleaseColdRequest, IngestRequest, IngestResult, LedgerIntegrityResult, LedgerQueryRequest, MemoryExport, MemoryStats, RecallExperiencesRequest, RecallExperiencesResult, RefineExperienceRequest, RefineExperienceResult, ReportUseRequest, ReportUseResult, ReviseExperienceRequest, RollbackExperienceRequest, VerifyShadowRequest, VerifyShadowResult, SkillArtifact, SkillForm, SkillStatus, ReviewSkillRequest, PublishSkillRequest } from './types.ts';
23
+ import type { ConcernTree, ConsolidateRequest, ConsolidateResult, DiaryAppendRequest, DiaryAppendResult, DiaryEntry, ExperienceListFilter, ExperienceSnapshot, ExperienceStatus, ExtractionRecord, ExtractFactsRequest, ExtractFactsResult, FactEntry, EvolveExperienceRequest, HumanAddExperienceRequest, HumanAddFactRequest, HumanAckDiaryRequest, HumanArchiveExperienceRequest, HumanConfirmFactRequest, HumanDeleteConcernRequest, HumanDeleteExperienceRequest, HumanDeleteFactRequest, HumanEditExperienceRequest, HumanEditFactRequest, HumanPinRequest, HumanSetConcernStatusRequest, HumanReleaseColdRequest, IngestRequest, IngestResult, LedgerIntegrityResult, LedgerQueryRequest, MemoryExport, MemoryStats, RecallExperiencesRequest, RecallExperiencesResult, RefineExperienceRequest, RefineExperienceResult, ReportUseRequest, ReportUseResult, ReviseExperienceRequest, RollbackExperienceRequest, VerifyShadowRequest, VerifyShadowResult, SkillArtifact, SkillForm, SkillStatus, ReviewSkillRequest, PublishSkillRequest } from './types.ts';
24
24
  import { MemoryStore } from './store.ts';
25
25
  /** Tunable mechanism parameters, all overridable from the plugin config. */
26
26
  export interface MemoryCoreConfig {
@@ -82,13 +82,17 @@ export declare class MemoryCore {
82
82
  constructor(store: MemoryStore, config: MemoryCoreConfig);
83
83
  private ledger;
84
84
  /** 生: refine one completed trajectory into a candidate (dual gate). */
85
- refine(request: RefineExperienceRequest, actor: MemoryActor): RefineExperienceResult;
85
+ refine(request: RefineExperienceRequest, actor: MemoryActor, opts?: {
86
+ dedup?: boolean;
87
+ }): RefineExperienceResult;
86
88
  /**
87
89
  * 摄取归一: source-agnostic intake. Every extracted draft becomes an earned
88
90
  * candidate carrying provenance (sourceType + sourceRef), the source-authority
89
91
  * prior, and the declared context scope. Candidates never recall until verified.
90
92
  */
91
- ingest(request: IngestRequest, actor: MemoryActor): IngestResult;
93
+ ingest(request: IngestRequest, actor: MemoryActor, opts?: {
94
+ dedup?: boolean;
95
+ }): IngestResult;
92
96
  /** 用: recall → scope → adjudicate → budget → inject, plus the candidate probe channel. */
93
97
  recall(request: RecallExperiencesRequest, actor: MemoryActor): RecallExperiencesResult;
94
98
  /** 用·验: one use outcome → Beta update, attribution, quarantine, gates. */
@@ -234,6 +238,20 @@ export declare class MemoryCore {
234
238
  humanPromote(id: string, reason: string, actor: MemoryActor): ExperienceSnapshot;
235
239
  /** Human re-release (006 §3.2): move a cold-palace revision back to candidate. */
236
240
  humanReleaseCold(request: HumanReleaseColdRequest, actor: MemoryActor): ExperienceSnapshot;
241
+ /** Record a corroboration ledger event for a rejected near-duplicate.
242
+ * The LLM semantic-dedup path routes a DUPLICATE verdict here so the
243
+ * ledger stays consistent with the mechanical-gate path (refine/ingest). */
244
+ markCorroborate(nearId: string, actor: MemoryActor, req: {
245
+ family: string;
246
+ gist: string;
247
+ }, via: string, score: number): void;
248
+ /** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
249
+ * service layer asks here for the mechanical word-overlap top-K before it
250
+ * runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
251
+ nearDuplicateCandidates(gist: string, situation: string[], statuses: ExperienceStatus[], topK: number): {
252
+ snapshot: ExperienceSnapshot;
253
+ score: number;
254
+ }[];
237
255
  /** Human-approved self-growth: merge new evidence into a live, human-approved
238
256
  * experience as an incremental new revision (revision+1, parent superseded).
239
257
  * Only approvedBy='human' + status='live' may evolve; agent/system-approved
package/lib/types/core.js CHANGED
@@ -99,7 +99,7 @@ export class MemoryCore {
99
99
  }
100
100
  // ── 生: refine ────────────────────────────────────────────────────────────
101
101
  /** 生: refine one completed trajectory into a candidate (dual gate). */
102
- refine(request, actor) {
102
+ refine(request, actor, opts) {
103
103
  const evidenceOk = (request.evidence.traceRef ?? '').trim() !== ''
104
104
  || (request.evidence.sessionRef ?? '').trim() !== ''
105
105
  || (request.evidence.note ?? '').trim() !== '';
@@ -120,15 +120,21 @@ export class MemoryCore {
120
120
  // Information-gain gate (007 flow-log fix): match on the lesson identity
121
121
  // (gist + situation) across the whole library; a near-duplicate corroborates
122
122
  // instead of inflating the library into a running log.
123
- const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(' ')));
124
- const near = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
125
- if (near !== undefined && near.score >= this.config.duplicateOverlapGate) {
126
- this.ledger('corroborate', 'experience', near.snapshot.id, actor, {
127
- family: request.family,
128
- gist: request.gist,
129
- score: near.score,
130
- });
131
- return { accepted: false, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead', corroboratedId: near.snapshot.id };
123
+ // Information-gain gate: mechanical word-overlap is the DEFAULT (no-LLM)
124
+ // prefilter. When the service layer already ran LLM semantic dedup and
125
+ // decided DIFFERENT, it passes { dedup: false } to accept without a
126
+ // mechanical gate that would otherwise over-reject.
127
+ if (opts?.dedup !== false) {
128
+ const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(' ')));
129
+ const near = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
130
+ if (near !== undefined && near.score >= this.config.duplicateOverlapGate) {
131
+ this.ledger('corroborate', 'experience', near.snapshot.id, actor, {
132
+ family: request.family,
133
+ gist: request.gist,
134
+ score: near.score,
135
+ });
136
+ return { accepted: false, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead', corroboratedId: near.snapshot.id };
137
+ }
132
138
  }
133
139
  const now = Date.now();
134
140
  const snapshot = {
@@ -175,7 +181,7 @@ export class MemoryCore {
175
181
  * candidate carrying provenance (sourceType + sourceRef), the source-authority
176
182
  * prior, and the declared context scope. Candidates never recall until verified.
177
183
  */
178
- ingest(request, actor) {
184
+ ingest(request, actor, opts) {
179
185
  if ((request.sourceRef ?? '').trim() === '') {
180
186
  throw new Error('memory: ingest requires a non-empty sourceRef provenance');
181
187
  }
@@ -194,17 +200,19 @@ export class MemoryCore {
194
200
  // already accepted in this same batch. A near-duplicate corroborates
195
201
  // instead of inflating the library into a running log.
196
202
  const dedupTokens = new Set(tokenize([item.gist, ...item.situation].join(' ')));
197
- const cross = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
198
- if (cross !== undefined && cross.score >= this.config.duplicateOverlapGate) {
199
- this.ledger('corroborate', 'experience', cross.snapshot.id, actor, {
200
- family: item.family, gist: item.gist, score: cross.score, via: 'ingest',
201
- });
202
- rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead' });
203
- continue;
204
- }
205
- if (acceptedDedup.some(prev => tokenOverlap(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
206
- rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of another item in this same ingest batch' });
207
- continue;
203
+ if (opts?.dedup !== false) {
204
+ const cross = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
205
+ if (cross !== undefined && cross.score >= this.config.duplicateOverlapGate) {
206
+ this.ledger('corroborate', 'experience', cross.snapshot.id, actor, {
207
+ family: item.family, gist: item.gist, score: cross.score, via: 'ingest',
208
+ });
209
+ rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead' });
210
+ continue;
211
+ }
212
+ if (acceptedDedup.some(prev => tokenOverlap(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
213
+ rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of another item in this same ingest batch' });
214
+ continue;
215
+ }
208
216
  }
209
217
  const now = Date.now();
210
218
  const snapshot = {
@@ -1324,6 +1332,19 @@ export class MemoryCore {
1324
1332
  this.ledger('release-cold', 'experience', next.id, actor, { revision: next.revision }, request.reason);
1325
1333
  return next;
1326
1334
  }
1335
+ /** Record a corroboration ledger event for a rejected near-duplicate.
1336
+ * The LLM semantic-dedup path routes a DUPLICATE verdict here so the
1337
+ * ledger stays consistent with the mechanical-gate path (refine/ingest). */
1338
+ markCorroborate(nearId, actor, req, via, score) {
1339
+ this.ledger('corroborate', 'experience', nearId, actor, { family: req.family, gist: req.gist, score, via });
1340
+ }
1341
+ /** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
1342
+ * service layer asks here for the mechanical word-overlap top-K before it
1343
+ * runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
1344
+ nearDuplicateCandidates(gist, situation, statuses, topK) {
1345
+ const tokens = new Set(tokenize([gist, ...situation].join(' ')));
1346
+ return this.store.findNearDuplicates(tokens, statuses, topK);
1347
+ }
1327
1348
  /** Human-approved self-growth: merge new evidence into a live, human-approved
1328
1349
  * experience as an incremental new revision (revision+1, parent superseded).
1329
1350
  * Only approvedBy='human' + status='live' may evolve; agent/system-approved
@@ -36,14 +36,19 @@ export declare class MemoryService extends TypertRemoteService {
36
36
  model: string;
37
37
  };
38
38
  } | undefined);
39
- /** 生: refine a completed trajectory into an experience candidate. */
40
- refine(agent: Agent, request: RefineExperienceRequest): RefineExperienceResult;
39
+ /** 生: refine a completed trajectory into an experience candidate.
40
+ * Semantic-dedup: coarse word-overlap candidate set first; when it is
41
+ * non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
42
+ * (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
43
+ refine(agent: Agent, request: RefineExperienceRequest): Promise<RefineExperienceResult>;
41
44
  /** 用: recall + adjudication + injection budget + negative channel. */
42
45
  recall(agent: Agent, request: RecallExperiencesRequest): RecallExperiencesResult;
43
46
  /** 用·验: report one use outcome with attribution (V0 verification). */
44
47
  report(agent: Agent, request: ReportUseRequest): ReportUseResult;
45
- /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1). */
46
- ingest(agent: Agent, request: IngestRequest): IngestResult;
48
+ /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
49
+ * Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
50
+ * the LLM judges [merge/duplicate/different] on the FULL lesson. */
51
+ ingest(agent: Agent, request: IngestRequest): Promise<IngestResult>;
47
52
  /** 修: propose a revised draft for a challenged experience. */
48
53
  revise(agent: Agent, request: ReviseExperienceRequest): ExperienceSnapshot;
49
54
  /** V1 controlled re-enactment: shadow replay verification for a draft. */
@@ -65,6 +70,14 @@ export declare class MemoryService extends TypertRemoteService {
65
70
  * Uses the agent's current model route for the LLM call.
66
71
  */
67
72
  private maybeRunDeletionFeedbackSummarization;
73
+ /**
74
+ * LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
75
+ * Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
76
+ * and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
77
+ * (accept) when the LLM is unavailable or the model route is missing, so the
78
+ * mechanical gate in core still governs without an LLM.
79
+ */
80
+ private dedupVerdictWithLlm;
68
81
  /**
69
82
  * Call the LLM to summarize deletion records into extraction feedback rules.
70
83
  * Uses ctx.llm.stream() with the agent's current model route.
@@ -44,11 +44,26 @@ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
44
44
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
45
45
  import { join } from 'node:path';
46
46
  import { randomUUID } from 'node:crypto';
47
+ import { tokenize } from "./store.js";
47
48
  /** Derive the ledger actor label from the wire identity. */
48
49
  function actorOf(agent) {
49
50
  const sessionId = agent.session?.id;
50
51
  return sessionId === undefined ? 'agent' : `agent:${sessionId}`;
51
52
  }
53
+ /** Top-K coarse candidate pool for the LLM semantic-dedup prefilter. */
54
+ const DEDUP_TOP_K = 5;
55
+ /** Statuses scanned for a near-duplicate candidate pool. */
56
+ const DEDUP_STATUSES = ['candidate', 'live', 'challenged', 'archived', 'cold'];
57
+ /** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
58
+ function tokenOverlap(a, b) {
59
+ if (a.size === 0 || b.size === 0)
60
+ return 0;
61
+ let hits = 0;
62
+ for (const t of a)
63
+ if (b.has(t))
64
+ hits += 1;
65
+ return hits / Math.sqrt(a.size * b.size);
66
+ }
52
67
  /**
53
68
  * Remote face of the memory library. All methods delegate to the core; the
54
69
  * core carries the 生·用·修·记 mechanism semantics.
@@ -233,9 +248,38 @@ let MemoryService = (() => {
233
248
  this.llm = llm;
234
249
  this.defaultModel = defaultModel;
235
250
  }
236
- /** 生: refine a completed trajectory into an experience candidate. */
237
- refine(agent, request) {
238
- return this.core.refine(request, actorOf(agent));
251
+ /** 生: refine a completed trajectory into an experience candidate.
252
+ * Semantic-dedup: coarse word-overlap candidate set first; when it is
253
+ * non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
254
+ * (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
255
+ async refine(agent, request) {
256
+ const actor = actorOf(agent);
257
+ const cands = this.core.nearDuplicateCandidates(request.gist, request.situation, DEDUP_STATUSES, DEDUP_TOP_K);
258
+ if (cands.length === 0) {
259
+ // No coarse hit: accept via the mechanical gate (kept as no-LLM fallback).
260
+ return this.core.refine(request, actor);
261
+ }
262
+ const verdict = await this.dedupVerdictWithLlm(agent, request, cands);
263
+ if (verdict === 'different') {
264
+ // LLM judged semantically different: accept, skipping the mechanical gate
265
+ // that would otherwise over-reject on word overlap.
266
+ return this.core.refine(request, actor, { dedup: false });
267
+ }
268
+ const best = cands[0];
269
+ if (verdict === 'merge' && best.snapshot.approvedBy === 'human' && best.snapshot.status === 'live') {
270
+ const evolved = this.core.evolve({
271
+ id: best.snapshot.id,
272
+ reason: 'semantic-merge: ' + request.gist,
273
+ gist: request.gist,
274
+ situation: request.situation,
275
+ path: request.path,
276
+ reasoning: request.reasoning,
277
+ limits: request.limits,
278
+ }, actor);
279
+ return { accepted: false, reason: 'semantic-merge: merged into a human-approved live experience', corroboratedId: evolved.id };
280
+ }
281
+ this.core.markCorroborate(best.snapshot.id, actor, { family: request.family, gist: request.gist }, 'refine', best.score);
282
+ return { accepted: false, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead', corroboratedId: best.snapshot.id };
239
283
  }
240
284
  /** 用: recall + adjudication + injection budget + negative channel. */
241
285
  recall(agent, request) {
@@ -245,9 +289,58 @@ let MemoryService = (() => {
245
289
  report(agent, request) {
246
290
  return this.core.report(request, actorOf(agent));
247
291
  }
248
- /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1). */
249
- ingest(agent, request) {
250
- return this.core.ingest(request, actorOf(agent));
292
+ /** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
293
+ * Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
294
+ * the LLM judges [merge/duplicate/different] on the FULL lesson. */
295
+ async ingest(agent, request) {
296
+ const actor = actorOf(agent);
297
+ // Source-authority prior, delegated to core so it stays the single truth.
298
+ const prior = this.core.ingest({ ...request, experiences: [] }, actor, { dedup: false }).sourcePrior;
299
+ const accepted = [];
300
+ const rejected = [];
301
+ const acceptedDedup = [];
302
+ for (const item of request.experiences) {
303
+ if (item.kind === 'negative' && (item.failureReason ?? '').trim() === '') {
304
+ rejected.push({ gist: item.gist, reason: 'rejected-schema: negative experiences must carry the confirmed failureReason' });
305
+ continue;
306
+ }
307
+ const cands = this.core.nearDuplicateCandidates(item.gist, item.situation, DEDUP_STATUSES, DEDUP_TOP_K);
308
+ if (cands.length > 0) {
309
+ const best = cands[0];
310
+ const verdict = await this.dedupVerdictWithLlm(agent, item, cands);
311
+ if (verdict !== 'different') {
312
+ if (verdict === 'merge' && best.snapshot.approvedBy === 'human' && best.snapshot.status === 'live') {
313
+ this.core.evolve({
314
+ id: best.snapshot.id,
315
+ reason: 'semantic-merge: ' + item.gist,
316
+ gist: item.gist,
317
+ situation: item.situation,
318
+ path: item.path,
319
+ reasoning: item.reasoning,
320
+ limits: item.limits,
321
+ }, actor);
322
+ }
323
+ else {
324
+ this.core.markCorroborate(best.snapshot.id, actor, { family: item.family, gist: item.gist }, 'ingest', best.score);
325
+ }
326
+ rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead' });
327
+ continue;
328
+ }
329
+ }
330
+ // LLM judged different (or no coarse hit): accept. Within-batch dedup via tokenOverlap.
331
+ const tokens = new Set(tokenize([item.gist, ...item.situation].join(' ')));
332
+ if (acceptedDedup.some((prev) => tokenOverlap(tokens, prev) >= this.core.config.duplicateOverlapGate)) {
333
+ rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of another item in this same ingest batch' });
334
+ continue;
335
+ }
336
+ const itemResult = this.core.ingest({ ...request, experiences: [item] }, actor, { dedup: cands.length === 0 });
337
+ for (const a of itemResult.accepted)
338
+ accepted.push(a);
339
+ for (const r of itemResult.rejected)
340
+ rejected.push(r);
341
+ acceptedDedup.push(tokens);
342
+ }
343
+ return { accepted, rejected, sourcePrior: prior };
251
344
  }
252
345
  /** 修: propose a revised draft for a challenged experience. */
253
346
  revise(agent, request) {
@@ -310,6 +403,62 @@ let MemoryService = (() => {
310
403
  // Summarization failure is non-fatal — extraction proceeds without updated feedback.
311
404
  }
312
405
  }
406
+ /**
407
+ * LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
408
+ * Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
409
+ * and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
410
+ * (accept) when the LLM is unavailable or the model route is missing, so the
411
+ * mechanical gate in core still governs without an LLM.
412
+ */
413
+ async dedupVerdictWithLlm(agent, lesson, cands) {
414
+ // Try session-specific model first, fall back to the default model config.
415
+ const header = agent.session.requestHeader()?.config;
416
+ let provider = header?.provider;
417
+ let model = header?.model;
418
+ if (provider === undefined || model === undefined) {
419
+ const sel = this.defaultModel?.currentSelection();
420
+ if (sel !== undefined) {
421
+ provider = sel.provider;
422
+ model = sel.model;
423
+ }
424
+ }
425
+ if (provider === undefined || model === undefined)
426
+ return 'different';
427
+ if (this.llm === undefined)
428
+ return 'different';
429
+ const fmtLesson = (l) => 'gist: ' + l.gist + '\nsituation: ' + l.situation.join(' | ') + '\npath:\n' +
430
+ l.path.map((p) => ' ' + p.order + '. ' + p.action).join('\n') +
431
+ '\nreasoning: ' + l.reasoning + '\nlimits: ' + l.limits.join(' | ');
432
+ const candText = cands.map((c, i) => '候选 ' + (i + 1) + '(词面相似度 ' + c.score.toFixed(3) + '):\n' + fmtLesson(c.snapshot)).join('\n\n');
433
+ const systemPrompt = '你是记忆系统的语义去重裁判。给定一条"新提炼的教训"和若干"已存在的候选经验",判断它们是否指同一件事。\n' +
434
+ '只输出一个词:\n- merge:语义可合并,新教训是对候选经验的增量补充\n- duplicate:语义重复,新教训无新增信息\n- different:语义不同,是不同的经验\n' +
435
+ '只输出 merge/duplicate/different 之一,不要任何其它内容。';
436
+ const userPrompt = '新提炼的教训:\n' + fmtLesson(lesson) + '\n\n候选经验:\n' + candText + '\n\n判定(merge/duplicate/different):';
437
+ const messages = [createUserMessage({
438
+ content: [{ type: 'text', text: userPrompt }],
439
+ source: { kind: 'plugin', plugin: 'dsh-daoing-memory' },
440
+ })];
441
+ let text = '';
442
+ const options = {
443
+ provider, model, messages, system: systemPrompt,
444
+ maxTokens: 50, sessionId: agent.session.id,
445
+ };
446
+ try {
447
+ for await (const chunk of this.llm.stream(options)) {
448
+ if (chunk.type === 'text-delta')
449
+ text += chunk.text;
450
+ }
451
+ }
452
+ catch {
453
+ return 'different';
454
+ }
455
+ const t = text.trim().toLowerCase();
456
+ if (t.includes('merge'))
457
+ return 'merge';
458
+ if (t.includes('duplicate'))
459
+ return 'duplicate';
460
+ return 'different';
461
+ }
313
462
  /**
314
463
  * Call the LLM to summarize deletion records into extraction feedback rules.
315
464
  * Uses ctx.llm.stream() with the agent's current model route.
@@ -84,6 +84,16 @@ export declare class MemoryStore {
84
84
  snapshot: ExperienceSnapshot;
85
85
  score: number;
86
86
  }[];
87
+ /**
88
+ * Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
89
+ * Scores on gist+situation only (the lesson's identity) so identical lessons
90
+ * with divergent limits/paths still match. Returns the top-K matches (rough
91
+ * prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
92
+ */
93
+ findNearDuplicates(queryTokens: Set<string>, statuses: ExperienceStatus[], topK: number): {
94
+ snapshot: ExperienceSnapshot;
95
+ score: number;
96
+ }[];
87
97
  /**
88
98
  * Near-duplicate detection for the information-gain gate (007 flow-log fix).
89
99
  * Unlike recallCandidates it scores on gist+situation only (the lesson's
@@ -362,6 +362,37 @@ export class MemoryStore {
362
362
  scored.sort((a, b) => b.score - a.score);
363
363
  return scored.slice(0, topK);
364
364
  }
365
+ /**
366
+ * Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
367
+ * Scores on gist+situation only (the lesson's identity) so identical lessons
368
+ * with divergent limits/paths still match. Returns the top-K matches (rough
369
+ * prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
370
+ */
371
+ findNearDuplicates(queryTokens, statuses, topK) {
372
+ if (queryTokens.size === 0 || topK <= 0)
373
+ return [];
374
+ const sql = `
375
+ SELECT * FROM experiences
376
+ WHERE deleted = 0 AND status IN (${statuses.map(() => '?').join(', ')})
377
+ `;
378
+ const rows = this.db.prepare(sql).all(...statuses);
379
+ const scored = [];
380
+ for (const row of rows) {
381
+ const snapshot = this.rowToExperience(row);
382
+ const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation].join(' ')));
383
+ if (hay.size === 0)
384
+ continue;
385
+ let hits = 0;
386
+ for (const token of queryTokens)
387
+ if (hay.has(token))
388
+ hits += 1;
389
+ const score = hits / Math.sqrt(queryTokens.size * hay.size);
390
+ if (score > 0)
391
+ scored.push({ snapshot, score });
392
+ }
393
+ scored.sort((a, b) => b.score - a.score);
394
+ return scored.slice(0, topK);
395
+ }
365
396
  /**
366
397
  * Near-duplicate detection for the information-gain gate (007 flow-log fix).
367
398
  * Unlike recallCandidates it scores on gist+situation only (the lesson's
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-daoing-memory",
3
3
  "description": "Self-evolving memory for DeepSeek Harness (DSH): earned experiences, diary/fact semantic memory, concern tracking, and an append-only audit ledger.",
4
- "version": "0.1.18",
4
+ "version": "0.1.19",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "daoing",