agenr 1.6.0 → 1.7.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.0] - 2026-04-04
4
+
5
+ Claim-key quality foundations, recall integration, auto-supersession, surgeon quality passes, and OpenClaw plugin publishing polish.
6
+
7
+ ### Added
8
+
9
+ - **Claim-key Phase 2a foundation.** Shared normalization/validation pipeline for claim keys, stronger post-extraction validation, and explicit claim-key preservation work for re-ingest flows.
10
+ - **Claim-key Phase 2b extraction quality.** Improved extraction quality with better prompting/hints and related quality tuning for missing-key backfill.
11
+ - **Claim-key Phase 2c recall integration.** Recall lineage expansion now uses claim keys as a structural signal.
12
+ - **Claim-key Phase 2d store-time auto-supersession.** New entries can auto-link to prior siblings on the same claim key under safety gates.
13
+ - **Claim-key Phase 2e surgeon quality pass.** New surgeon claim-key quality pass with missing-key backfill, supported promotion lanes, compaction, grounded-family promotion, stable-slot promotion refinement, and entity-family convergence scaffolding.
14
+ - **Shadow-mode sibling-slot-resonance instrumentation.** Deterministic diagnostic instrumentation for threshold-only supported cohorts, persisted in surgeon run details and summaries without changing live auto-apply behavior.
15
+
16
+ ### Changed
17
+
18
+ - **Claim-key quality promotion policy.** Rebalanced supported promotion, compact canonicalization, grounded backfill behavior, and grounded-family/stable-slot promotion rules to surface stronger candidates while preserving unresolved boundaries.
19
+ - **Surgeon progress/liveness reporting.** Improved preview concurrency and progress reporting during claim-key-quality runs.
20
+ - **Planning and review docs.** Added and updated internal plan/review docs covering claim-key quality sequencing, grounded-family promotion analysis, threshold-only cohort audit, and shadow-mode follow-up.
21
+
22
+ ### Validation
23
+
24
+ Changes since last push to `origin/master`:
25
+
26
+ - feat: add historical-state recall routing
27
+ - Add unified recall path to recall eval seam
28
+ - feat: make historical recall lineage-aware
29
+ - fix: phase 2a claim key foundation
30
+ - feat: improve claim-key extraction quality
31
+ - fix: tighten agenr_store durable memory guidance
32
+ - feat: use claim keys in recall lineage expansion
33
+ - feat: add store-time claim-key auto-supersession
34
+ - feat: add surgeon claim-key quality pass
35
+ - Improve surgeon liveness progress reporting
36
+ - Improve claim-key-quality preview concurrency
37
+ - fix: tune claim-key-quality missing-key backfill
38
+ - fix: improve grounded claim-key backfill quality
39
+ - fix: promote supported claim-key proposals
40
+ - fix: compact canonical claim-key candidates
41
+ - fix: rebalance post-compaction claim-key promotion
42
+ - feat: add claim-key entity family convergence
43
+ - fix: promote grounded family missing-key candidates
44
+ - docs: add threshold-only supported cohort audit
45
+ - feat: add shadow sibling-slot resonance instrumentation
46
+ - Align surgeon presets with claim-key quality plan
47
+
3
48
  ## [1.6.0] - 2026-04-02
4
49
 
5
50
  Store nudge, memory guidance improvements, plugin rename, and dead code cleanup.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildLexicalPlan,
3
3
  cosineSimilarity
4
- } from "./chunk-EUPZHNOY.js";
4
+ } from "./chunk-RRLX4WCN.js";
5
5
 
6
6
  // src/adapters/db/client.ts
7
7
  import fs from "fs/promises";
@@ -905,7 +905,26 @@ async function getDistinctClaimKeyPrefixes(executor) {
905
905
  return typeof prefix === "string" && prefix.length > 0 ? [prefix] : [];
906
906
  });
907
907
  }
908
- async function updateEntry(executor, id, fields) {
908
+ async function getClaimKeyExamples(executor, limit = 8) {
909
+ const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 8;
910
+ const result = await executor.execute({
911
+ sql: `
912
+ SELECT claim_key
913
+ FROM entries
914
+ WHERE claim_key IS NOT NULL
915
+ AND ${ACTIVE_ENTRY_CLAUSE}
916
+ GROUP BY claim_key
917
+ ORDER BY COUNT(*) DESC, MAX(importance) DESC, MAX(created_at) DESC, claim_key ASC
918
+ LIMIT ?
919
+ `,
920
+ args: [normalizedLimit]
921
+ });
922
+ return result.rows.flatMap((row) => {
923
+ const claimKey = row.claim_key;
924
+ return typeof claimKey === "string" && claimKey.length > 0 ? [claimKey] : [];
925
+ });
926
+ }
927
+ async function updateEntry(executor, id, fields, options) {
909
928
  const assignments = [];
910
929
  const args = [];
911
930
  if (fields.importance !== void 0) {
@@ -939,7 +958,7 @@ async function updateEntry(executor, id, fields) {
939
958
  UPDATE entries
940
959
  SET ${assignments.join(", ")}
941
960
  WHERE id = ?
942
- AND ${ACTIVE_ENTRY_CLAUSE}
961
+ AND ${options?.includeInactive === true ? "1 = 1" : ACTIVE_ENTRY_CLAUSE}
943
962
  `,
944
963
  args
945
964
  });
@@ -1041,7 +1060,7 @@ function normalizeInteger(value, fallback) {
1041
1060
  }
1042
1061
 
1043
1062
  // src/adapters/db/schema.ts
1044
- var SCHEMA_VERSION = "6";
1063
+ var SCHEMA_VERSION = "7";
1045
1064
  var VECTOR_INDEX_NAME = "idx_entries_embedding";
1046
1065
  var EPISODE_VECTOR_INDEX_NAME = "idx_episodes_embedding";
1047
1066
  var BULK_WRITE_STATE_META_KEY = "bulk_write_state";
@@ -1190,6 +1209,7 @@ var CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL = `
1190
1209
  entry_ids TEXT NOT NULL DEFAULT '[]',
1191
1210
  reasoning TEXT NOT NULL DEFAULT '',
1192
1211
  recall_delta TEXT,
1212
+ details_json TEXT,
1193
1213
  created_at TEXT NOT NULL
1194
1214
  )
1195
1215
  `;
@@ -1205,6 +1225,35 @@ var CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL = `
1205
1225
  CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_created_at
1206
1226
  ON surgeon_run_actions(created_at)
1207
1227
  `;
1228
+ var CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL = `
1229
+ CREATE TABLE IF NOT EXISTS surgeon_run_proposals (
1230
+ id TEXT PRIMARY KEY,
1231
+ run_id TEXT NOT NULL REFERENCES surgeon_runs(id),
1232
+ group_id TEXT NOT NULL,
1233
+ issue_kind TEXT NOT NULL,
1234
+ scope TEXT NOT NULL,
1235
+ entry_ids TEXT NOT NULL DEFAULT '[]',
1236
+ current_claim_keys TEXT NOT NULL DEFAULT '[]',
1237
+ proposed_claim_keys TEXT NOT NULL DEFAULT '[]',
1238
+ rationale TEXT NOT NULL DEFAULT '',
1239
+ confidence REAL NOT NULL DEFAULT 0,
1240
+ source TEXT NOT NULL DEFAULT '',
1241
+ eligible_for_apply INTEGER NOT NULL DEFAULT 0,
1242
+ created_at TEXT NOT NULL
1243
+ )
1244
+ `;
1245
+ var CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL = `
1246
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_run_id
1247
+ ON surgeon_run_proposals(run_id)
1248
+ `;
1249
+ var CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL = `
1250
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_group_id
1251
+ ON surgeon_run_proposals(group_id)
1252
+ `;
1253
+ var CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL = `
1254
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_created_at
1255
+ ON surgeon_run_proposals(created_at)
1256
+ `;
1208
1257
  var CREATE_META_TABLE_SQL = `
1209
1258
  CREATE TABLE IF NOT EXISTS _meta (
1210
1259
  key TEXT PRIMARY KEY,
@@ -1323,6 +1372,10 @@ var SCHEMA_STATEMENTS = [
1323
1372
  CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL,
1324
1373
  CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL,
1325
1374
  CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL,
1375
+ CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL,
1376
+ CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL,
1377
+ CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL,
1378
+ CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL,
1326
1379
  CREATE_META_TABLE_SQL,
1327
1380
  CREATE_ENTRIES_CONTENT_HASH_INDEX_SQL,
1328
1381
  CREATE_ENTRIES_NORM_CONTENT_HASH_INDEX_SQL,
@@ -1344,10 +1397,15 @@ var SCHEMA_STATEMENTS = [
1344
1397
  ];
1345
1398
  async function initSchema(db) {
1346
1399
  await db.execute("PRAGMA foreign_keys = ON");
1347
- const currentVersion = await getSchemaVersion(db);
1400
+ let currentVersion = await getSchemaVersion(db);
1348
1401
  await assertSupportedSchemaState(db, currentVersion);
1349
1402
  if (currentVersion === "5") {
1350
1403
  await migrateV5ToV6(db);
1404
+ currentVersion = "6";
1405
+ }
1406
+ if (currentVersion === "6") {
1407
+ await migrateV6ToV7(db);
1408
+ currentVersion = "7";
1351
1409
  }
1352
1410
  const hadEntriesFts = await tableExists(db, "entries_fts");
1353
1411
  for (const statement of SCHEMA_STATEMENTS) {
@@ -1371,7 +1429,7 @@ async function initSchema(db) {
1371
1429
  await ensureVectorIndexes(db);
1372
1430
  }
1373
1431
  async function assertSupportedSchemaState(db, currentVersion) {
1374
- if (currentVersion && currentVersion !== "5" && currentVersion !== SCHEMA_VERSION) {
1432
+ if (currentVersion && currentVersion !== "5" && currentVersion !== "6" && currentVersion !== SCHEMA_VERSION) {
1375
1433
  throw new Error(
1376
1434
  `Unsupported agenr database schema version "${currentVersion}". This build only supports schema version ${SCHEMA_VERSION}. Create a fresh database with \`agenr db reset\` or manually migrate the data into a new database.`
1377
1435
  );
@@ -1399,6 +1457,21 @@ async function migrateV5ToV6(db) {
1399
1457
  }
1400
1458
  }
1401
1459
  }
1460
+ async function migrateV6ToV7(db) {
1461
+ if (await tableExists(db, "surgeon_run_actions")) {
1462
+ try {
1463
+ await db.execute("ALTER TABLE surgeon_run_actions ADD COLUMN details_json TEXT");
1464
+ } catch (error) {
1465
+ if (!(error instanceof Error && /duplicate column/i.test(error.message))) {
1466
+ throw error;
1467
+ }
1468
+ }
1469
+ }
1470
+ await db.execute(CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL);
1471
+ await db.execute(CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL);
1472
+ await db.execute(CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL);
1473
+ await db.execute(CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL);
1474
+ }
1402
1475
  async function rebuildFts(db) {
1403
1476
  await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
1404
1477
  }
@@ -1628,6 +1701,10 @@ var LibsqlDatabase = class _LibsqlDatabase {
1628
1701
  async getDistinctClaimKeyPrefixes() {
1629
1702
  return getDistinctClaimKeyPrefixes(this.executor);
1630
1703
  }
1704
+ /** Lists bounded full claim-key examples ordered for extraction hinting. */
1705
+ async getClaimKeyExamples(limit) {
1706
+ return getClaimKeyExamples(this.executor, limit);
1707
+ }
1631
1708
  /** Updates mutable entry fields such as importance, expiry, and temporal metadata. */
1632
1709
  async updateEntry(id, fields) {
1633
1710
  return updateEntry(this.executor, id, fields);
@@ -1765,8 +1842,9 @@ var DEFAULT_SURGEON_CONTEXT_LIMIT = 0;
1765
1842
  var DEFAULT_SURGEON_RETIREMENT_PROTECT_RECALLED_DAYS = 14;
1766
1843
  var DEFAULT_SURGEON_RETIREMENT_PROTECT_MIN_IMPORTANCE = 9;
1767
1844
  var DEFAULT_SURGEON_SKIP_RECENTLY_EVALUATED_DAYS = 7;
1845
+ var DEFAULT_CLAIM_EXTRACTION_CONCURRENCY = 10;
1768
1846
  var DEFAULT_CLAIM_EXTRACTION_CONFIDENCE_THRESHOLD = 0.8;
1769
- var DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES = ["fact", "preference", "decision"];
1847
+ var DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES = ["fact", "preference", "decision", "lesson"];
1770
1848
  function isAgenrAuthMethod(value) {
1771
1849
  return AUTH_METHOD_SET.has(value);
1772
1850
  }
@@ -1809,7 +1887,8 @@ function resolveClaimExtractionConfig(config) {
1809
1887
  return {
1810
1888
  enabled: config?.claimExtraction?.enabled ?? true,
1811
1889
  confidenceThreshold: normalizeClaimExtractionConfidence(config?.claimExtraction?.confidenceThreshold),
1812
- eligibleTypes: normalizeClaimExtractionEligibleTypes(config?.claimExtraction?.eligibleTypes)
1890
+ eligibleTypes: normalizeClaimExtractionEligibleTypes(config?.claimExtraction?.eligibleTypes),
1891
+ concurrency: normalizeClaimExtractionConcurrency(config?.claimExtraction?.concurrency)
1813
1892
  };
1814
1893
  }
1815
1894
  function readConfig(options = {}) {
@@ -1905,6 +1984,13 @@ function normalizeClaimExtractionEligibleTypes(value) {
1905
1984
  const normalized = Array.from(new Set(value.filter((candidate) => ENTRY_TYPES.includes(candidate))));
1906
1985
  return normalized.length > 0 ? normalized : [...DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES];
1907
1986
  }
1987
+ function normalizeClaimExtractionConcurrency(value) {
1988
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1989
+ return DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
1990
+ }
1991
+ const normalized = Math.trunc(value);
1992
+ return normalized > 0 ? normalized : DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
1993
+ }
1908
1994
 
1909
1995
  // src/core/store/embedding-text.ts
1910
1996
  function composeEmbeddingText(entry) {
@@ -2121,9 +2207,14 @@ var RECALL_CANDIDATE_SELECT_COLUMNS = `
2121
2207
  e.importance,
2122
2208
  e.expiry,
2123
2209
  e.embedding,
2210
+ e.superseded_by,
2211
+ e.claim_key,
2212
+ e.retired,
2124
2213
  e.created_at
2125
2214
  `;
2126
2215
  var FTS_TIERS = ["exact", "all_tokens", "any_tokens"];
2216
+ var PREDECESSOR_EXPANSION_LIMIT_PER_SEED = 8;
2217
+ var PREDECESSOR_EXPANSION_MAX_RESULTS = 40;
2127
2218
  function createRecallAdapter(executor, embeddingPort) {
2128
2219
  return new LibsqlRecallAdapter(executor, embeddingPort);
2129
2220
  }
@@ -2223,6 +2314,74 @@ var LibsqlRecallAdapter = class {
2223
2314
  }
2224
2315
  return Array.from(matches.values()).sort((left, right) => compareFtsCandidates(left, right)).slice(0, params.limit);
2225
2316
  }
2317
+ /**
2318
+ * Finds historical predecessors scoped to a seed set of active candidate IDs.
2319
+ *
2320
+ * Direct supersession links are preferred. Same-claim-key siblings are used as
2321
+ * the structural lineage path, with retired same-subject entries preserved as
2322
+ * a weaker fallback when explicit slot identity is unavailable.
2323
+ */
2324
+ async fetchPredecessors(params) {
2325
+ const normalizedIds = normalizeStrings(params.activeEntryIds);
2326
+ if (normalizedIds.length === 0) {
2327
+ return [];
2328
+ }
2329
+ const placeholders = normalizedIds.map(() => "?").join(", ");
2330
+ const expansionLimit = normalizePredecessorExpansionLimit(normalizedIds.length);
2331
+ const result = await this.executor.execute({
2332
+ sql: `
2333
+ WITH seed AS (
2334
+ SELECT id, subject, claim_key
2335
+ FROM entries
2336
+ WHERE id IN (${placeholders})
2337
+ ),
2338
+ seed_subjects AS (
2339
+ SELECT DISTINCT subject
2340
+ FROM seed
2341
+ WHERE TRIM(subject) <> ''
2342
+ ),
2343
+ seed_claim_keys AS (
2344
+ SELECT DISTINCT claim_key
2345
+ FROM seed
2346
+ WHERE claim_key IS NOT NULL
2347
+ ),
2348
+ lineage AS (
2349
+ SELECT
2350
+ ${RECALL_CANDIDATE_SELECT_COLUMNS},
2351
+ CASE
2352
+ WHEN e.superseded_by IN (SELECT id FROM seed) THEN 0
2353
+ WHEN e.claim_key IS NOT NULL
2354
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
2355
+ AND (e.retired = 1 OR e.superseded_by IS NOT NULL) THEN 1
2356
+ WHEN e.claim_key IS NOT NULL
2357
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys) THEN 2
2358
+ WHEN e.retired = 1
2359
+ AND e.subject IN (SELECT subject FROM seed_subjects) THEN 3
2360
+ ELSE 4
2361
+ END AS lineage_priority
2362
+ FROM entries AS e
2363
+ WHERE e.id NOT IN (SELECT id FROM seed)
2364
+ AND (
2365
+ e.superseded_by IN (SELECT id FROM seed)
2366
+ OR (
2367
+ e.claim_key IS NOT NULL
2368
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
2369
+ )
2370
+ OR (
2371
+ e.retired = 1
2372
+ AND e.subject IN (SELECT subject FROM seed_subjects)
2373
+ )
2374
+ )
2375
+ )
2376
+ SELECT *
2377
+ FROM lineage
2378
+ ORDER BY lineage_priority ASC, created_at ASC, id ASC
2379
+ LIMIT ?
2380
+ `,
2381
+ args: [...normalizedIds, expansionLimit]
2382
+ });
2383
+ return result.rows.map((row) => mapRecallCandidateRow(row));
2384
+ }
2226
2385
  /** Hydrates full entries for the final ranked result set. */
2227
2386
  async hydrateEntries(ids) {
2228
2387
  const normalizedIds = normalizeStrings(ids);
@@ -2235,8 +2394,7 @@ var LibsqlRecallAdapter = class {
2235
2394
  SELECT
2236
2395
  ${ENTRY_SELECT_COLUMNS}
2237
2396
  FROM entries AS e
2238
- WHERE ${buildActiveEntryClause("e")}
2239
- AND e.id IN (${placeholders})
2397
+ WHERE e.id IN (${placeholders})
2240
2398
  `,
2241
2399
  args: normalizedIds
2242
2400
  });
@@ -2327,9 +2485,15 @@ function mapRecallCandidateRow(row) {
2327
2485
  importance: readNumber(row, "importance", 0),
2328
2486
  expiry,
2329
2487
  embedding: readEmbedding(row, "embedding"),
2488
+ superseded_by: readOptionalString(row, "superseded_by"),
2489
+ claim_key: readOptionalString(row, "claim_key"),
2490
+ retired: readBoolean(row, "retired"),
2330
2491
  created_at: readRequiredString(row, "created_at")
2331
2492
  };
2332
2493
  }
2494
+ function normalizePredecessorExpansionLimit(seedCount) {
2495
+ return Math.min(PREDECESSOR_EXPANSION_MAX_RESULTS, seedCount * PREDECESSOR_EXPANSION_LIMIT_PER_SEED);
2496
+ }
2333
2497
  function wrapVectorError(error) {
2334
2498
  const message = error instanceof Error ? error.message : String(error);
2335
2499
  return new Error(`Vector search is unavailable: ${message}`);
@@ -2358,6 +2522,7 @@ export {
2358
2522
  DEFAULT_SURGEON_RETIREMENT_PROTECT_RECALLED_DAYS,
2359
2523
  DEFAULT_SURGEON_RETIREMENT_PROTECT_MIN_IMPORTANCE,
2360
2524
  DEFAULT_SURGEON_SKIP_RECENTLY_EVALUATED_DAYS,
2525
+ DEFAULT_CLAIM_EXTRACTION_CONCURRENCY,
2361
2526
  isAgenrAuthMethod,
2362
2527
  authMethodToProvider,
2363
2528
  getAuthMethodDefinition,
@@ -370,6 +370,12 @@ function createNoopRecallTraceSink() {
370
370
 
371
371
  // src/core/recall/search.ts
372
372
  var MIN_VECTOR_ONLY_EVIDENCE = 0.3;
373
+ var HISTORICAL_STATE_FLAT_RECENCY = 0.5;
374
+ var HISTORICAL_PREDECESSOR_BOOST = 0.08;
375
+ var HISTORICAL_RETIRED_PREDECESSOR_BOOST = 0.06;
376
+ var HISTORICAL_OLDER_STATE_BOOST = 0.08;
377
+ var HISTORICAL_TOPIC_SHARED_PREFIX_MIN = 2;
378
+ var HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN = 0.6;
373
379
  async function recall(query, ports, options = {}) {
374
380
  const text = query.text.trim();
375
381
  const limit = normalizeLimit(query.limit);
@@ -421,10 +427,26 @@ async function recall(query, ports, options = {}) {
421
427
  ]);
422
428
  const mergeStartedAt = Date.now();
423
429
  const mergedCandidates = mergeCandidates(vectorCandidates, ftsCandidates);
430
+ await expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, {
431
+ activeEntryIds: Array.from(mergedCandidates.keys()),
432
+ rankingProfile: query.rankingProfile
433
+ });
424
434
  summary.candidateCounts.merged = mergedCandidates.size;
425
435
  summary.timings.mergeCandidatesMs = elapsedMs(mergeStartedAt);
426
436
  const scoreStartedAt = Date.now();
427
- const scored = Array.from(mergedCandidates.values()).map((candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, aroundDate, query.aroundRadius)).sort((left, right) => right.score - left.score);
437
+ const scored = applyHistoricalLineageBoosts(
438
+ Array.from(mergedCandidates.values()).map(
439
+ (candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, {
440
+ aroundDate,
441
+ aroundRadius: query.aroundRadius,
442
+ rankingProfile: query.rankingProfile
443
+ })
444
+ ),
445
+ {
446
+ aroundDate,
447
+ rankingProfile: query.rankingProfile
448
+ }
449
+ ).sort((left, right) => right.score - left.score);
428
450
  summary.timings.scoreCandidatesMs = elapsedMs(scoreStartedAt);
429
451
  const thresholdStartedAt = Date.now();
430
452
  const thresholded = scored.filter((result) => hasSufficientReturnEvidence(result) && result.score >= threshold);
@@ -520,10 +542,10 @@ function finishRecallTrace(summary, trace, noResultReason) {
520
542
  }
521
543
  trace.reportSummary(summary);
522
544
  }
523
- function scoreMergedCandidate(candidate, queryText, queryEmbedding, aroundDate, aroundRadius) {
545
+ function scoreMergedCandidate(candidate, queryText, queryEmbedding, params) {
524
546
  const vector = candidate.vectorSim ?? cosineSimilarity(candidate.entry.embedding ?? [], queryEmbedding);
525
547
  const lexical = computeLexicalScore(queryText, candidate.entry.subject, candidate.entry.content);
526
- const recency = aroundDate ? gaussianRecency(candidate.entry.created_at, aroundDate, normalizeAroundRadius(aroundRadius)) : recencyScore(candidate.entry.created_at, candidate.entry.expiry);
548
+ const recency = resolveRecencyScore(candidate.entry, params);
527
549
  const importance = importanceScore(candidate.entry.importance);
528
550
  const scored = scoreCandidate({
529
551
  vectorSim: vector,
@@ -537,6 +559,97 @@ function scoreMergedCandidate(candidate, queryText, queryEmbedding, aroundDate,
537
559
  scores: scored.scores
538
560
  };
539
561
  }
562
+ async function expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, params) {
563
+ if (params.rankingProfile !== "historical_state" || mergedCandidates.size === 0 || !ports.fetchPredecessors) {
564
+ return;
565
+ }
566
+ const predecessors = await ports.fetchPredecessors({
567
+ activeEntryIds: params.activeEntryIds
568
+ });
569
+ for (const entry of predecessors) {
570
+ if (mergedCandidates.has(entry.id)) {
571
+ continue;
572
+ }
573
+ mergedCandidates.set(entry.id, {
574
+ entry,
575
+ vectorSim: cosineSimilarity(entry.embedding ?? [], queryEmbedding)
576
+ });
577
+ }
578
+ }
579
+ function resolveRecencyScore(entry, params) {
580
+ if (params.aroundDate) {
581
+ return gaussianRecency(entry.created_at, params.aroundDate, normalizeAroundRadius(params.aroundRadius));
582
+ }
583
+ if (params.rankingProfile === "historical_state") {
584
+ return HISTORICAL_STATE_FLAT_RECENCY;
585
+ }
586
+ return recencyScore(entry.created_at, entry.expiry);
587
+ }
588
+ function applyHistoricalLineageBoosts(candidates, params) {
589
+ if (params.rankingProfile !== "historical_state") {
590
+ return candidates;
591
+ }
592
+ const entries = candidates.map((candidate) => candidate.entry);
593
+ return candidates.map((candidate) => {
594
+ const bonus = resolveHistoricalLineageBonus(candidate.entry, entries, params.aroundDate);
595
+ if (bonus <= 0) {
596
+ return candidate;
597
+ }
598
+ return {
599
+ ...candidate,
600
+ score: Math.min(1, candidate.score + bonus)
601
+ };
602
+ });
603
+ }
604
+ function resolveHistoricalLineageBonus(entry, entries, aroundDate) {
605
+ if (entries.some((peer) => peer.id !== entry.id && entry.superseded_by === peer.id)) {
606
+ return HISTORICAL_PREDECESSOR_BOOST;
607
+ }
608
+ if (aroundDate) {
609
+ return 0;
610
+ }
611
+ const activePeers = entries.filter((peer) => peer.id !== entry.id && isPotentialCurrentPeer(peer) && isOlderHistoricalPeer(entry, peer));
612
+ if (activePeers.length === 0) {
613
+ return 0;
614
+ }
615
+ return entry.retired ? HISTORICAL_RETIRED_PREDECESSOR_BOOST : HISTORICAL_OLDER_STATE_BOOST;
616
+ }
617
+ function isPotentialCurrentPeer(entry) {
618
+ return !entry.retired && entry.superseded_by === void 0;
619
+ }
620
+ function isOlderHistoricalPeer(left, right) {
621
+ return createdAtMs(left.created_at) < createdAtMs(right.created_at) && sharesHistoricalLineage(left, right);
622
+ }
623
+ function sharesHistoricalLineage(left, right) {
624
+ if (left.claim_key && right.claim_key && left.claim_key === right.claim_key) {
625
+ return true;
626
+ }
627
+ return sharesHistoricalTopic(left, right);
628
+ }
629
+ function sharesHistoricalTopic(left, right) {
630
+ const leftTokens = tokenize(left.subject);
631
+ const rightTokens = tokenize(right.subject);
632
+ if (leftTokens.length === 0 || rightTokens.length === 0) {
633
+ return false;
634
+ }
635
+ const sharedPrefixCount = countSharedPrefixTokens(leftTokens, rightTokens);
636
+ return sharedPrefixCount >= HISTORICAL_TOPIC_SHARED_PREFIX_MIN && sharedPrefixCount / leftTokens.length >= HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN;
637
+ }
638
+ function createdAtMs(value) {
639
+ const timestamp = new Date(value).getTime();
640
+ return Number.isFinite(timestamp) ? timestamp : 0;
641
+ }
642
+ function countSharedPrefixTokens(leftTokens, rightTokens) {
643
+ const length = Math.min(leftTokens.length, rightTokens.length);
644
+ let sharedPrefixCount = 0;
645
+ for (let index = 0; index < length; index += 1) {
646
+ if (leftTokens[index] !== rightTokens[index]) {
647
+ break;
648
+ }
649
+ sharedPrefixCount += 1;
650
+ }
651
+ return sharedPrefixCount;
652
+ }
540
653
  function hasSufficientReturnEvidence(candidate) {
541
654
  if (candidate.scores.lexical > 0) {
542
655
  return true;