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 +45 -0
- package/dist/{chunk-6SJBRIGC.js → chunk-IMQOPTQX.js} +175 -10
- package/dist/{chunk-EUPZHNOY.js → chunk-RRLX4WCN.js} +116 -3
- package/dist/cli.js +4917 -331
- package/dist/core/recall/index.d.ts +23 -2
- package/dist/core/recall/index.js +1 -1
- package/dist/internal-recall-eval-server.js +964 -13
- package/package.json +1 -1
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-
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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,
|
|
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 =
|
|
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;
|