@agenr/agenr-plugin 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/dist/index.js +1499 -140
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7156,6 +7156,12 @@ function createNoopRecallTraceSink() {
|
|
|
7156
7156
|
|
|
7157
7157
|
// ../../src/core/recall/search.ts
|
|
7158
7158
|
var MIN_VECTOR_ONLY_EVIDENCE = 0.3;
|
|
7159
|
+
var HISTORICAL_STATE_FLAT_RECENCY = 0.5;
|
|
7160
|
+
var HISTORICAL_PREDECESSOR_BOOST = 0.08;
|
|
7161
|
+
var HISTORICAL_RETIRED_PREDECESSOR_BOOST = 0.06;
|
|
7162
|
+
var HISTORICAL_OLDER_STATE_BOOST = 0.08;
|
|
7163
|
+
var HISTORICAL_TOPIC_SHARED_PREFIX_MIN = 2;
|
|
7164
|
+
var HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN = 0.6;
|
|
7159
7165
|
async function recall(query, ports, options = {}) {
|
|
7160
7166
|
const text = query.text.trim();
|
|
7161
7167
|
const limit = normalizeLimit(query.limit);
|
|
@@ -7207,10 +7213,26 @@ async function recall(query, ports, options = {}) {
|
|
|
7207
7213
|
]);
|
|
7208
7214
|
const mergeStartedAt = Date.now();
|
|
7209
7215
|
const mergedCandidates = mergeCandidates(vectorCandidates, ftsCandidates);
|
|
7216
|
+
await expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, {
|
|
7217
|
+
activeEntryIds: Array.from(mergedCandidates.keys()),
|
|
7218
|
+
rankingProfile: query.rankingProfile
|
|
7219
|
+
});
|
|
7210
7220
|
summary.candidateCounts.merged = mergedCandidates.size;
|
|
7211
7221
|
summary.timings.mergeCandidatesMs = elapsedMs(mergeStartedAt);
|
|
7212
7222
|
const scoreStartedAt = Date.now();
|
|
7213
|
-
const scored =
|
|
7223
|
+
const scored = applyHistoricalLineageBoosts(
|
|
7224
|
+
Array.from(mergedCandidates.values()).map(
|
|
7225
|
+
(candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, {
|
|
7226
|
+
aroundDate,
|
|
7227
|
+
aroundRadius: query.aroundRadius,
|
|
7228
|
+
rankingProfile: query.rankingProfile
|
|
7229
|
+
})
|
|
7230
|
+
),
|
|
7231
|
+
{
|
|
7232
|
+
aroundDate,
|
|
7233
|
+
rankingProfile: query.rankingProfile
|
|
7234
|
+
}
|
|
7235
|
+
).sort((left, right) => right.score - left.score);
|
|
7214
7236
|
summary.timings.scoreCandidatesMs = elapsedMs(scoreStartedAt);
|
|
7215
7237
|
const thresholdStartedAt = Date.now();
|
|
7216
7238
|
const thresholded = scored.filter((result) => hasSufficientReturnEvidence(result) && result.score >= threshold);
|
|
@@ -7306,10 +7328,10 @@ function finishRecallTrace(summary, trace, noResultReason) {
|
|
|
7306
7328
|
}
|
|
7307
7329
|
trace.reportSummary(summary);
|
|
7308
7330
|
}
|
|
7309
|
-
function scoreMergedCandidate(candidate, queryText, queryEmbedding,
|
|
7331
|
+
function scoreMergedCandidate(candidate, queryText, queryEmbedding, params) {
|
|
7310
7332
|
const vector = candidate.vectorSim ?? cosineSimilarity(candidate.entry.embedding ?? [], queryEmbedding);
|
|
7311
7333
|
const lexical = computeLexicalScore(queryText, candidate.entry.subject, candidate.entry.content);
|
|
7312
|
-
const recency =
|
|
7334
|
+
const recency = resolveRecencyScore(candidate.entry, params);
|
|
7313
7335
|
const importance = importanceScore(candidate.entry.importance);
|
|
7314
7336
|
const scored = scoreCandidate({
|
|
7315
7337
|
vectorSim: vector,
|
|
@@ -7323,6 +7345,97 @@ function scoreMergedCandidate(candidate, queryText, queryEmbedding, aroundDate,
|
|
|
7323
7345
|
scores: scored.scores
|
|
7324
7346
|
};
|
|
7325
7347
|
}
|
|
7348
|
+
async function expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, params) {
|
|
7349
|
+
if (params.rankingProfile !== "historical_state" || mergedCandidates.size === 0 || !ports.fetchPredecessors) {
|
|
7350
|
+
return;
|
|
7351
|
+
}
|
|
7352
|
+
const predecessors = await ports.fetchPredecessors({
|
|
7353
|
+
activeEntryIds: params.activeEntryIds
|
|
7354
|
+
});
|
|
7355
|
+
for (const entry of predecessors) {
|
|
7356
|
+
if (mergedCandidates.has(entry.id)) {
|
|
7357
|
+
continue;
|
|
7358
|
+
}
|
|
7359
|
+
mergedCandidates.set(entry.id, {
|
|
7360
|
+
entry,
|
|
7361
|
+
vectorSim: cosineSimilarity(entry.embedding ?? [], queryEmbedding)
|
|
7362
|
+
});
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
function resolveRecencyScore(entry, params) {
|
|
7366
|
+
if (params.aroundDate) {
|
|
7367
|
+
return gaussianRecency(entry.created_at, params.aroundDate, normalizeAroundRadius(params.aroundRadius));
|
|
7368
|
+
}
|
|
7369
|
+
if (params.rankingProfile === "historical_state") {
|
|
7370
|
+
return HISTORICAL_STATE_FLAT_RECENCY;
|
|
7371
|
+
}
|
|
7372
|
+
return recencyScore(entry.created_at, entry.expiry);
|
|
7373
|
+
}
|
|
7374
|
+
function applyHistoricalLineageBoosts(candidates, params) {
|
|
7375
|
+
if (params.rankingProfile !== "historical_state") {
|
|
7376
|
+
return candidates;
|
|
7377
|
+
}
|
|
7378
|
+
const entries = candidates.map((candidate) => candidate.entry);
|
|
7379
|
+
return candidates.map((candidate) => {
|
|
7380
|
+
const bonus = resolveHistoricalLineageBonus(candidate.entry, entries, params.aroundDate);
|
|
7381
|
+
if (bonus <= 0) {
|
|
7382
|
+
return candidate;
|
|
7383
|
+
}
|
|
7384
|
+
return {
|
|
7385
|
+
...candidate,
|
|
7386
|
+
score: Math.min(1, candidate.score + bonus)
|
|
7387
|
+
};
|
|
7388
|
+
});
|
|
7389
|
+
}
|
|
7390
|
+
function resolveHistoricalLineageBonus(entry, entries, aroundDate) {
|
|
7391
|
+
if (entries.some((peer) => peer.id !== entry.id && entry.superseded_by === peer.id)) {
|
|
7392
|
+
return HISTORICAL_PREDECESSOR_BOOST;
|
|
7393
|
+
}
|
|
7394
|
+
if (aroundDate) {
|
|
7395
|
+
return 0;
|
|
7396
|
+
}
|
|
7397
|
+
const activePeers = entries.filter((peer) => peer.id !== entry.id && isPotentialCurrentPeer(peer) && isOlderHistoricalPeer(entry, peer));
|
|
7398
|
+
if (activePeers.length === 0) {
|
|
7399
|
+
return 0;
|
|
7400
|
+
}
|
|
7401
|
+
return entry.retired ? HISTORICAL_RETIRED_PREDECESSOR_BOOST : HISTORICAL_OLDER_STATE_BOOST;
|
|
7402
|
+
}
|
|
7403
|
+
function isPotentialCurrentPeer(entry) {
|
|
7404
|
+
return !entry.retired && entry.superseded_by === void 0;
|
|
7405
|
+
}
|
|
7406
|
+
function isOlderHistoricalPeer(left, right) {
|
|
7407
|
+
return createdAtMs(left.created_at) < createdAtMs(right.created_at) && sharesHistoricalLineage(left, right);
|
|
7408
|
+
}
|
|
7409
|
+
function sharesHistoricalLineage(left, right) {
|
|
7410
|
+
if (left.claim_key && right.claim_key && left.claim_key === right.claim_key) {
|
|
7411
|
+
return true;
|
|
7412
|
+
}
|
|
7413
|
+
return sharesHistoricalTopic(left, right);
|
|
7414
|
+
}
|
|
7415
|
+
function sharesHistoricalTopic(left, right) {
|
|
7416
|
+
const leftTokens = tokenize(left.subject);
|
|
7417
|
+
const rightTokens = tokenize(right.subject);
|
|
7418
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) {
|
|
7419
|
+
return false;
|
|
7420
|
+
}
|
|
7421
|
+
const sharedPrefixCount = countSharedPrefixTokens(leftTokens, rightTokens);
|
|
7422
|
+
return sharedPrefixCount >= HISTORICAL_TOPIC_SHARED_PREFIX_MIN && sharedPrefixCount / leftTokens.length >= HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN;
|
|
7423
|
+
}
|
|
7424
|
+
function createdAtMs(value) {
|
|
7425
|
+
const timestamp = new Date(value).getTime();
|
|
7426
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
7427
|
+
}
|
|
7428
|
+
function countSharedPrefixTokens(leftTokens, rightTokens) {
|
|
7429
|
+
const length = Math.min(leftTokens.length, rightTokens.length);
|
|
7430
|
+
let sharedPrefixCount = 0;
|
|
7431
|
+
for (let index = 0; index < length; index += 1) {
|
|
7432
|
+
if (leftTokens[index] !== rightTokens[index]) {
|
|
7433
|
+
break;
|
|
7434
|
+
}
|
|
7435
|
+
sharedPrefixCount += 1;
|
|
7436
|
+
}
|
|
7437
|
+
return sharedPrefixCount;
|
|
7438
|
+
}
|
|
7326
7439
|
function hasSufficientReturnEvidence(candidate) {
|
|
7327
7440
|
if (candidate.scores.lexical > 0) {
|
|
7328
7441
|
return true;
|
|
@@ -7972,12 +8085,36 @@ var EPISODE_FRESHNESS_NOTICE = "Episodes cover consolidated prior sessions only;
|
|
|
7972
8085
|
var EPISODE_SEMANTIC_FALLBACK_NOTICE = "Semantic episode search unavailable - showing temporal results only.";
|
|
7973
8086
|
var EPISODE_SEMANTIC_UNAVAILABLE_NOTICE = "Semantic episode search unavailable - no semantic episode results could be returned.";
|
|
7974
8087
|
var ENTRY_FILTER_NOTICE = "Threshold, type filters, and tag filters were applied to entries only.";
|
|
8088
|
+
var HISTORICAL_STATE_PATTERNS = [
|
|
8089
|
+
"what was the previous",
|
|
8090
|
+
"what was the earlier",
|
|
8091
|
+
"what did we use before",
|
|
8092
|
+
"what was the old",
|
|
8093
|
+
"what changed",
|
|
8094
|
+
"changed from",
|
|
8095
|
+
"replaced by",
|
|
8096
|
+
"before we switched",
|
|
8097
|
+
"before we migrated",
|
|
8098
|
+
"previous approach",
|
|
8099
|
+
"earlier plan",
|
|
8100
|
+
"old workflow"
|
|
8101
|
+
];
|
|
8102
|
+
var HISTORICAL_STATE_REGEX_PATTERNS = [
|
|
8103
|
+
/\bwhat\b.*\bused?\b.*\bbefore\b/u,
|
|
8104
|
+
/\bwhat\b.*\bworkflow\b.*\bbefore\b/u,
|
|
8105
|
+
/\bwhat\b.*\bplan\b.*\bearlier\b/u,
|
|
8106
|
+
/\bwhat\b.*\bplan\b.*\bbefore\b/u
|
|
8107
|
+
];
|
|
7975
8108
|
async function runUnifiedRecall(input, deps) {
|
|
7976
8109
|
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
7977
8110
|
const requested = normalizeMode(input.mode);
|
|
7978
8111
|
const parsedTimeWindow = parseTemporalWindow(input.text, now);
|
|
7979
8112
|
const hasEntryFilters = hasEntryScopedFilters(input);
|
|
7980
8113
|
const topicAnchor = hasTopicAnchor(input.text, hasEntryFilters);
|
|
8114
|
+
const historicalStatePattern = detectHistoricalStatePattern(input.text);
|
|
8115
|
+
if (historicalStatePattern) {
|
|
8116
|
+
deps.debugLog?.(`[agenr] unified recall matched historical-state pattern=${JSON.stringify(historicalStatePattern)} query=${JSON.stringify(input.text)}`);
|
|
8117
|
+
}
|
|
7981
8118
|
const routing = routeRecall({
|
|
7982
8119
|
requested,
|
|
7983
8120
|
text: input.text,
|
|
@@ -7989,6 +8126,7 @@ async function runUnifiedRecall(input, deps) {
|
|
|
7989
8126
|
text: input.text,
|
|
7990
8127
|
limit: input.limit,
|
|
7991
8128
|
requested,
|
|
8129
|
+
detectedIntent: routing.detectedIntent,
|
|
7992
8130
|
parsedTimeWindow,
|
|
7993
8131
|
topicAnchor,
|
|
7994
8132
|
embedQuery: deps.embedQuery
|
|
@@ -8033,11 +8171,12 @@ function routeRecall(params) {
|
|
|
8033
8171
|
const lower = params.text.trim().toLowerCase();
|
|
8034
8172
|
const factual = /^(when did|when was|what decision|what preference|what(?:'s| is) the default|which version|what threshold)\b/.test(lower);
|
|
8035
8173
|
const narrative = /\b(what happened|what were we doing|what was going on|summarize|catch me up)\b/.test(lower);
|
|
8174
|
+
const historicalState = detectHistoricalStatePattern(params.text) !== void 0;
|
|
8036
8175
|
const topicAnchor = hasTopicAnchor(params.text, params.hasEntryFilters);
|
|
8037
8176
|
if (params.requested === "entries") {
|
|
8038
8177
|
return {
|
|
8039
8178
|
requested: params.requested,
|
|
8040
|
-
detectedIntent: factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
|
|
8179
|
+
detectedIntent: historicalState ? "historical_state" : factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
|
|
8041
8180
|
queried: ["entries"],
|
|
8042
8181
|
reason: "Explicit mode=entries override."
|
|
8043
8182
|
};
|
|
@@ -8045,11 +8184,19 @@ function routeRecall(params) {
|
|
|
8045
8184
|
if (params.requested === "episodes") {
|
|
8046
8185
|
return {
|
|
8047
8186
|
requested: params.requested,
|
|
8048
|
-
detectedIntent: params.parsedTimeWindow ? "temporal_narrative" : "mixed",
|
|
8187
|
+
detectedIntent: historicalState ? "historical_state" : params.parsedTimeWindow ? "temporal_narrative" : "mixed",
|
|
8049
8188
|
queried: ["episodes"],
|
|
8050
8189
|
reason: params.parsedTimeWindow ? "Explicit mode=episodes override with a resolved time window." : "Explicit mode=episodes override without a resolved time window."
|
|
8051
8190
|
};
|
|
8052
8191
|
}
|
|
8192
|
+
if (historicalState) {
|
|
8193
|
+
return {
|
|
8194
|
+
requested: params.requested,
|
|
8195
|
+
detectedIntent: "historical_state",
|
|
8196
|
+
queried: ["entries", "episodes"],
|
|
8197
|
+
reason: params.parsedTimeWindow ? "The query asks about a previous state or transition and includes a supported time expression, so both entries and episodes were queried." : "The query asks about a previous state or transition, so both entries and episodes were queried."
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8053
8200
|
if (factual && params.parsedTimeWindow) {
|
|
8054
8201
|
return {
|
|
8055
8202
|
requested: params.requested,
|
|
@@ -8099,7 +8246,7 @@ function routeRecall(params) {
|
|
|
8099
8246
|
}
|
|
8100
8247
|
async function buildEpisodeQueryPlan(params) {
|
|
8101
8248
|
const notices = [];
|
|
8102
|
-
const shouldUseSemantic = params.parsedTimeWindow ? params.topicAnchor : params.requested === "episodes";
|
|
8249
|
+
const shouldUseSemantic = params.detectedIntent === "historical_state" || (params.parsedTimeWindow ? params.topicAnchor : params.requested === "episodes");
|
|
8103
8250
|
let embedding;
|
|
8104
8251
|
if (shouldUseSemantic) {
|
|
8105
8252
|
embedding = await maybeEmbedEpisodeQuery(params.text, params.embedQuery);
|
|
@@ -8141,17 +8288,18 @@ async function maybeRunEntryRecall(params) {
|
|
|
8141
8288
|
}
|
|
8142
8289
|
return {
|
|
8143
8290
|
kind: "results",
|
|
8144
|
-
results: await recall(buildEntryRecallInput(params.input, params.parsedTimeWindow), params.deps.recall)
|
|
8291
|
+
results: await recall(buildEntryRecallInput(params.input, params.parsedTimeWindow, params.routing), params.deps.recall, params.deps.recallOptions)
|
|
8145
8292
|
};
|
|
8146
8293
|
}
|
|
8147
|
-
function buildEntryRecallInput(input, parsedTimeWindow) {
|
|
8294
|
+
function buildEntryRecallInput(input, parsedTimeWindow, routing) {
|
|
8148
8295
|
const request = {
|
|
8149
8296
|
text: input.text,
|
|
8150
8297
|
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
8151
8298
|
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
8152
8299
|
...input.types && input.types.length > 0 ? { types: input.types } : {},
|
|
8153
8300
|
...input.tags && input.tags.length > 0 ? { tags: input.tags } : {},
|
|
8154
|
-
...input.sessionKey ? { sessionKey: input.sessionKey } : {}
|
|
8301
|
+
...input.sessionKey ? { sessionKey: input.sessionKey } : {},
|
|
8302
|
+
...routing.detectedIntent === "historical_state" ? { rankingProfile: "historical_state" } : {}
|
|
8155
8303
|
};
|
|
8156
8304
|
if (!parsedTimeWindow) {
|
|
8157
8305
|
return request;
|
|
@@ -8168,6 +8316,15 @@ function buildEntryRecallInput(input, parsedTimeWindow) {
|
|
|
8168
8316
|
aroundRadius: radiusDays
|
|
8169
8317
|
};
|
|
8170
8318
|
}
|
|
8319
|
+
function detectHistoricalStatePattern(text) {
|
|
8320
|
+
const lower = text.trim().toLowerCase();
|
|
8321
|
+
const explicitPattern = HISTORICAL_STATE_PATTERNS.find((pattern) => lower.includes(pattern));
|
|
8322
|
+
if (explicitPattern) {
|
|
8323
|
+
return explicitPattern;
|
|
8324
|
+
}
|
|
8325
|
+
const regexPattern = HISTORICAL_STATE_REGEX_PATTERNS.find((pattern) => pattern.test(lower));
|
|
8326
|
+
return regexPattern?.source;
|
|
8327
|
+
}
|
|
8171
8328
|
function normalizeMode(value) {
|
|
8172
8329
|
return value === "entries" || value === "episodes" ? value : "auto";
|
|
8173
8330
|
}
|
|
@@ -8209,7 +8366,7 @@ var EPISODE_ACTIVITY_LEVELS = ["substantial", "minimal", "none"];
|
|
|
8209
8366
|
|
|
8210
8367
|
// ../../src/adapters/openclaw/tools/shared.ts
|
|
8211
8368
|
import { failedTextResult, readStringParam } from "openclaw/plugin-sdk/agent-runtime";
|
|
8212
|
-
var ENTRY_TYPE_DESCRIPTION = "Knowledge type to store. Use fact for durable
|
|
8369
|
+
var ENTRY_TYPE_DESCRIPTION = "Knowledge type to store. Use fact for durable truth about a person, system, place, or how something works. Use decision for a standing rule, constraint, policy, or chosen approach future sessions should follow - not a progress update or completed action. Use preference for what someone likes, wants, values, or wants avoided. Use lesson for a non-obvious takeaway learned from experience that should change future behavior. Use milestone for a rare one-time event with durable future significance - not ordinary execution progress. Use relationship for a meaningful durable connection between people, groups, or systems.";
|
|
8213
8370
|
var EXPIRY_DESCRIPTION = "Lifetime bucket: core (always injected at session start, use sparingly), permanent (durable and recalled on demand), or temporary (short-horizon).";
|
|
8214
8371
|
var UPDATE_EXPIRY_DESCRIPTION = `${EXPIRY_DESCRIPTION} Accepted values: ${EXPIRY_LEVELS.join(", ")}.`;
|
|
8215
8372
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
@@ -8388,29 +8545,15 @@ function formatUnifiedRecallResults(result) {
|
|
|
8388
8545
|
lines.push(`${result.timeWindow.start} -> ${result.timeWindow.end} (${result.timeWindow.timezone}) from ${JSON.stringify(result.timeWindow.resolvedFrom)}`);
|
|
8389
8546
|
lines.push("");
|
|
8390
8547
|
}
|
|
8391
|
-
|
|
8392
|
-
if (
|
|
8393
|
-
lines
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
lines.push(
|
|
8397
|
-
`${index + 1}. ${episode.episode.id} | ${episode.episode.source} | ${episode.episode.startedAt} -> ${episode.episode.endedAt ?? episode.episode.startedAt} | score ${episode.score.toFixed(2)}`
|
|
8398
|
-
);
|
|
8399
|
-
lines.push(` ${index < 3 ? episode.episode.summary.trim() : truncate(episode.episode.summary.trim(), 220)}`);
|
|
8400
|
-
lines.push(` why_matched=${describeEpisodeMatch(episode)}`);
|
|
8401
|
-
}
|
|
8402
|
-
}
|
|
8403
|
-
lines.push("");
|
|
8404
|
-
lines.push("Entry Matches");
|
|
8405
|
-
if (result.entries.length === 0) {
|
|
8406
|
-
lines.push("None.");
|
|
8548
|
+
const renderEntriesFirst = result.routing.detectedIntent === "historical_state";
|
|
8549
|
+
if (renderEntriesFirst) {
|
|
8550
|
+
appendEntryMatches(lines, result);
|
|
8551
|
+
lines.push("");
|
|
8552
|
+
appendEpisodeMatches(lines, result);
|
|
8407
8553
|
} else {
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
);
|
|
8412
|
-
lines.push(` ${truncate(entry.entry.content, 220)}`);
|
|
8413
|
-
}
|
|
8554
|
+
appendEpisodeMatches(lines, result);
|
|
8555
|
+
lines.push("");
|
|
8556
|
+
appendEntryMatches(lines, result);
|
|
8414
8557
|
}
|
|
8415
8558
|
if (result.notices.length > 0) {
|
|
8416
8559
|
lines.push("");
|
|
@@ -8421,6 +8564,33 @@ function formatUnifiedRecallResults(result) {
|
|
|
8421
8564
|
}
|
|
8422
8565
|
return lines.join("\n");
|
|
8423
8566
|
}
|
|
8567
|
+
function appendEntryMatches(lines, result) {
|
|
8568
|
+
lines.push("Entry Matches");
|
|
8569
|
+
if (result.entries.length === 0) {
|
|
8570
|
+
lines.push("None.");
|
|
8571
|
+
return;
|
|
8572
|
+
}
|
|
8573
|
+
for (const [index, entry] of result.entries.entries()) {
|
|
8574
|
+
lines.push(
|
|
8575
|
+
`${index + 1}. ${entry.entry.id} | ${entry.entry.type} | ${entry.entry.subject} | score ${entry.score.toFixed(2)} | importance ${entry.entry.importance}`
|
|
8576
|
+
);
|
|
8577
|
+
lines.push(` ${truncate(entry.entry.content, 220)}`);
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
function appendEpisodeMatches(lines, result) {
|
|
8581
|
+
lines.push("Episode Matches");
|
|
8582
|
+
if (result.episodes.length === 0) {
|
|
8583
|
+
lines.push("None.");
|
|
8584
|
+
return;
|
|
8585
|
+
}
|
|
8586
|
+
for (const [index, episode] of result.episodes.entries()) {
|
|
8587
|
+
lines.push(
|
|
8588
|
+
`${index + 1}. ${episode.episode.id} | ${episode.episode.source} | ${episode.episode.startedAt} -> ${episode.episode.endedAt ?? episode.episode.startedAt} | score ${episode.score.toFixed(2)}`
|
|
8589
|
+
);
|
|
8590
|
+
lines.push(` ${index < 3 ? episode.episode.summary.trim() : truncate(episode.episode.summary.trim(), 220)}`);
|
|
8591
|
+
lines.push(` why_matched=${describeEpisodeMatch(episode)}`);
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8424
8594
|
function formatUnifiedRecallLogSummary(result) {
|
|
8425
8595
|
const entrySubjects = result.entries.map((entry) => entry.entry.subject.trim()).filter((subject) => subject.length > 0);
|
|
8426
8596
|
const displayed = entrySubjects.slice(0, RESULT_SUBJECT_LOG_LIMIT).map((subject) => JSON.stringify(truncate(subject, 80)));
|
|
@@ -8513,12 +8683,12 @@ var RECALL_TOOL_PARAMETERS = {
|
|
|
8513
8683
|
properties: {
|
|
8514
8684
|
query: {
|
|
8515
8685
|
type: "string",
|
|
8516
|
-
description: "What you need to remember. Use a focused natural-language query rather than a broad 'everything' search."
|
|
8686
|
+
description: "What you need to remember. Use a focused natural-language query rather than a broad 'everything' search. Phrase prior-state asks directly, for example 'what was the previous approach' or 'what changed from X to Y'."
|
|
8517
8687
|
},
|
|
8518
8688
|
mode: {
|
|
8519
8689
|
type: "string",
|
|
8520
8690
|
enum: [...RECALL_MODES],
|
|
8521
|
-
description: "Recall mode: auto routes between
|
|
8691
|
+
description: "Recall mode: auto routes between exact entry recall, historical-state recall, and episodes; entries forces semantic recall; episodes forces temporal or semantic session recall."
|
|
8522
8692
|
},
|
|
8523
8693
|
limit: {
|
|
8524
8694
|
type: "integer",
|
|
@@ -8552,7 +8722,7 @@ function createAgenrRecallTool(ctx, servicesPromise, logger) {
|
|
|
8552
8722
|
return {
|
|
8553
8723
|
name: "agenr_recall",
|
|
8554
8724
|
label: "Agenr Recall",
|
|
8555
|
-
description: "Retrieve knowledge from agenr long-term memory. Use mode=auto for the normal path, mode=entries for exact facts and decisions
|
|
8725
|
+
description: "Retrieve knowledge from agenr long-term memory. Use mode=auto for the normal path, including historical-state questions like what was the previous approach or what changed from X to Y; use mode=entries for exact facts and decisions; use mode=episodes for time-bounded 'what happened' questions. Time periods are parsed from the query text. Session-start recall is already handled automatically.",
|
|
8556
8726
|
parameters: RECALL_TOOL_PARAMETERS,
|
|
8557
8727
|
async execute(_toolCallId, rawParams) {
|
|
8558
8728
|
try {
|
|
@@ -8598,6 +8768,9 @@ function createAgenrRecallTool(ctx, servicesPromise, logger) {
|
|
|
8598
8768
|
recall: services.recall,
|
|
8599
8769
|
embeddingAvailable: services.embeddingStatus.available,
|
|
8600
8770
|
embeddingError: services.embeddingStatus.error,
|
|
8771
|
+
debugLog: (message) => {
|
|
8772
|
+
logger.debug?.(message);
|
|
8773
|
+
},
|
|
8601
8774
|
embedQuery: services.embeddingStatus.available ? async (text) => {
|
|
8602
8775
|
const vectors = await services.embedding.embed([text]);
|
|
8603
8776
|
return vectors[0] ?? [];
|
|
@@ -8710,52 +8883,655 @@ import { failedTextResult as failedTextResult3, readNumberParam as readNumberPar
|
|
|
8710
8883
|
// ../../src/core/store/pipeline.ts
|
|
8711
8884
|
import { randomUUID } from "crypto";
|
|
8712
8885
|
|
|
8713
|
-
// ../../src/core/
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8886
|
+
// ../../src/core/supersession.ts
|
|
8887
|
+
function validateSupersessionRules(oldEntry, newEntry) {
|
|
8888
|
+
if (oldEntry.type !== newEntry.type) {
|
|
8889
|
+
return {
|
|
8890
|
+
ok: false,
|
|
8891
|
+
reason: "type_mismatch"
|
|
8892
|
+
};
|
|
8893
|
+
}
|
|
8894
|
+
if (oldEntry.type === "milestone") {
|
|
8895
|
+
return {
|
|
8896
|
+
ok: false,
|
|
8897
|
+
reason: "milestone"
|
|
8898
|
+
};
|
|
8899
|
+
}
|
|
8900
|
+
if (oldEntry.expiry === "core") {
|
|
8901
|
+
return {
|
|
8902
|
+
ok: false,
|
|
8903
|
+
reason: "core_expiry"
|
|
8904
|
+
};
|
|
8905
|
+
}
|
|
8906
|
+
return {
|
|
8907
|
+
ok: true
|
|
8908
|
+
};
|
|
8909
|
+
}
|
|
8910
|
+
function describeSupersessionRuleFailure(reason) {
|
|
8911
|
+
switch (reason) {
|
|
8912
|
+
case "type_mismatch":
|
|
8913
|
+
return "Supersession requires both entries to have the same type.";
|
|
8914
|
+
case "milestone":
|
|
8915
|
+
return "Milestone entries are never superseded automatically.";
|
|
8916
|
+
case "core_expiry":
|
|
8917
|
+
return "Core-expiry entries are never superseded automatically.";
|
|
8918
|
+
}
|
|
8919
|
+
}
|
|
8920
|
+
|
|
8921
|
+
// ../../src/core/claim-key.ts
|
|
8922
|
+
var UNKNOWN_SEGMENT = "unknown";
|
|
8923
|
+
var SELF_REFERENTIAL_ENTITIES = /* @__PURE__ */ new Set(["i", "me", "myself", "the_user", "user", "we", "our_team", "the_project", "this_project"]);
|
|
8924
|
+
var GENERIC_ATTRIBUTES = /* @__PURE__ */ new Set(["info", "details", "config", "stuff", "thing", "data"]);
|
|
8925
|
+
var COMPACTION_RELATION_TOKENS = /* @__PURE__ */ new Set([
|
|
8926
|
+
"after",
|
|
8927
|
+
"before",
|
|
8928
|
+
"depend",
|
|
8929
|
+
"depends",
|
|
8930
|
+
"follows",
|
|
8931
|
+
"follow",
|
|
8932
|
+
"keep",
|
|
8933
|
+
"keeps",
|
|
8934
|
+
"maintain",
|
|
8935
|
+
"maintains",
|
|
8936
|
+
"need",
|
|
8937
|
+
"needs",
|
|
8938
|
+
"precede",
|
|
8939
|
+
"precedes",
|
|
8940
|
+
"preserve",
|
|
8941
|
+
"preserves",
|
|
8942
|
+
"require",
|
|
8943
|
+
"required",
|
|
8944
|
+
"requires",
|
|
8945
|
+
"retain",
|
|
8946
|
+
"retains"
|
|
8947
|
+
]);
|
|
8948
|
+
var COMPACTION_BREAK_TOKENS = /* @__PURE__ */ new Set(["about", "across", "and", "between", "during", "for", "from", "into", "onto", "or", "to", "with"]);
|
|
8949
|
+
var COMPACTION_WEAK_LEADING_TOKENS = /* @__PURE__ */ new Set(["actual", "authoritative", "canonical", "concrete", "current", "durable", "existing", "real"]);
|
|
8950
|
+
var ACTION_CONDITION_TOKENS = /* @__PURE__ */ new Set(["activate", "activation", "apply", "fire", "launch", "run", "start", "trigger"]);
|
|
8951
|
+
var TRAILING_OBJECT_COMPACTION_PREPOSITIONS = /* @__PURE__ */ new Set(["about", "for", "from", "into", "onto", "to", "with"]);
|
|
8952
|
+
var TRAILING_OBJECT_TRANSFER_HEADS = /* @__PURE__ */ new Set([
|
|
8953
|
+
"access",
|
|
8954
|
+
"boundary",
|
|
8955
|
+
"condition",
|
|
8956
|
+
"contract",
|
|
8957
|
+
"guide",
|
|
8958
|
+
"path",
|
|
8959
|
+
"policy",
|
|
8960
|
+
"preference",
|
|
8961
|
+
"process",
|
|
8962
|
+
"rule",
|
|
8963
|
+
"schedule",
|
|
8964
|
+
"support",
|
|
8965
|
+
"surface",
|
|
8966
|
+
"window",
|
|
8967
|
+
"workflow"
|
|
8968
|
+
]);
|
|
8969
|
+
var STABLE_ATTRIBUTE_HEADS = /* @__PURE__ */ new Set([
|
|
8970
|
+
"access",
|
|
8971
|
+
"boundary",
|
|
8972
|
+
"condition",
|
|
8973
|
+
"contract",
|
|
8974
|
+
"default",
|
|
8975
|
+
"dependency",
|
|
8976
|
+
"guide",
|
|
8977
|
+
"mode",
|
|
8978
|
+
"order",
|
|
8979
|
+
"path",
|
|
8980
|
+
"policy",
|
|
8981
|
+
"preference",
|
|
8982
|
+
"preservation",
|
|
8983
|
+
"process",
|
|
8984
|
+
"requirement",
|
|
8985
|
+
"rule",
|
|
8986
|
+
"schedule",
|
|
8987
|
+
"setting",
|
|
8988
|
+
"status",
|
|
8989
|
+
"strategy",
|
|
8990
|
+
"support",
|
|
8991
|
+
"surface",
|
|
8992
|
+
"timezone",
|
|
8993
|
+
"truth",
|
|
8994
|
+
"version",
|
|
8995
|
+
"window",
|
|
8996
|
+
"workflow"
|
|
8997
|
+
]);
|
|
8998
|
+
function normalizeClaimKeySegment(value) {
|
|
8999
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
9000
|
+
}
|
|
9001
|
+
function normalizeClaimKey(value) {
|
|
9002
|
+
const trimmed = value.trim();
|
|
9003
|
+
if (trimmed.length === 0) {
|
|
9004
|
+
return { ok: false, reason: "empty" };
|
|
9005
|
+
}
|
|
9006
|
+
const slashCount = Array.from(trimmed).filter((character) => character === "/").length;
|
|
9007
|
+
if (slashCount === 0) {
|
|
9008
|
+
return { ok: false, reason: "missing_separator" };
|
|
9009
|
+
}
|
|
9010
|
+
if (slashCount !== 1) {
|
|
9011
|
+
return { ok: false, reason: "too_many_segments" };
|
|
9012
|
+
}
|
|
9013
|
+
const [rawEntity = "", rawAttribute = ""] = trimmed.split("/");
|
|
9014
|
+
const entity = normalizeClaimKeySegment(rawEntity);
|
|
9015
|
+
if (entity.length === 0) {
|
|
9016
|
+
return { ok: false, reason: "empty_entity" };
|
|
9017
|
+
}
|
|
9018
|
+
const attribute = normalizeClaimKeySegment(rawAttribute);
|
|
9019
|
+
if (attribute.length === 0) {
|
|
9020
|
+
return { ok: false, reason: "empty_attribute" };
|
|
9021
|
+
}
|
|
9022
|
+
if (entity === UNKNOWN_SEGMENT && attribute === UNKNOWN_SEGMENT) {
|
|
9023
|
+
return { ok: false, reason: "unknown_pair" };
|
|
9024
|
+
}
|
|
9025
|
+
return {
|
|
9026
|
+
ok: true,
|
|
9027
|
+
value: {
|
|
9028
|
+
claimKey: `${entity}/${attribute}`,
|
|
9029
|
+
entity,
|
|
9030
|
+
attribute
|
|
9031
|
+
}
|
|
9032
|
+
};
|
|
9033
|
+
}
|
|
9034
|
+
function compactClaimKey(claimKey) {
|
|
9035
|
+
const normalized = normalizeClaimKey(claimKey);
|
|
9036
|
+
if (!normalized.ok) {
|
|
8717
9037
|
return null;
|
|
8718
9038
|
}
|
|
8719
|
-
|
|
8720
|
-
|
|
9039
|
+
let attributeTokens = normalized.value.attribute.split("_").filter((token) => token.length > 0);
|
|
9040
|
+
const entityTokens = normalized.value.entity.split("_").filter((token) => token.length > 0);
|
|
9041
|
+
const reasons = [];
|
|
9042
|
+
if (entityTokens.length > 0 && startsWithTokens(attributeTokens, entityTokens) && attributeTokens.length > entityTokens.length) {
|
|
9043
|
+
attributeTokens = attributeTokens.slice(entityTokens.length);
|
|
9044
|
+
reasons.push("removed duplicated entity prefix from attribute");
|
|
9045
|
+
}
|
|
9046
|
+
if (entityTokens.length > 0 && attributeTokens.length > entityTokens.length + 1 && endsWithTokens(attributeTokens, entityTokens) && TRAILING_OBJECT_COMPACTION_PREPOSITIONS.has(attributeTokens[attributeTokens.length - entityTokens.length - 1] ?? "")) {
|
|
9047
|
+
attributeTokens = attributeTokens.slice(0, attributeTokens.length - entityTokens.length - 1);
|
|
9048
|
+
reasons.push("removed duplicated entity suffix from attribute");
|
|
9049
|
+
}
|
|
9050
|
+
const sourceOfTruthCompaction = compactSourceOfTruthAttribute(attributeTokens);
|
|
9051
|
+
if (sourceOfTruthCompaction) {
|
|
9052
|
+
attributeTokens = sourceOfTruthCompaction.attributeTokens;
|
|
9053
|
+
reasons.push(sourceOfTruthCompaction.reason);
|
|
9054
|
+
} else {
|
|
9055
|
+
const relationCompaction = compactRelationAttribute(attributeTokens);
|
|
9056
|
+
if (relationCompaction) {
|
|
9057
|
+
attributeTokens = relationCompaction.attributeTokens;
|
|
9058
|
+
reasons.push(relationCompaction.reason);
|
|
9059
|
+
} else {
|
|
9060
|
+
const trailingObjectCompaction = compactTrailingObjectAttribute(attributeTokens);
|
|
9061
|
+
if (trailingObjectCompaction) {
|
|
9062
|
+
attributeTokens = trailingObjectCompaction.attributeTokens;
|
|
9063
|
+
reasons.push(trailingObjectCompaction.reason);
|
|
9064
|
+
}
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
const attribute = attributeTokens.join("_");
|
|
9068
|
+
if (attribute.length === 0) {
|
|
9069
|
+
return {
|
|
9070
|
+
claimKey: normalized.value.claimKey,
|
|
9071
|
+
entity: normalized.value.entity,
|
|
9072
|
+
attribute: normalized.value.attribute,
|
|
9073
|
+
compactedFrom: null,
|
|
9074
|
+
reason: null
|
|
9075
|
+
};
|
|
9076
|
+
}
|
|
9077
|
+
const compactedClaimKey = `${normalized.value.entity}/${attribute}`;
|
|
9078
|
+
return {
|
|
9079
|
+
claimKey: compactedClaimKey,
|
|
9080
|
+
entity: normalized.value.entity,
|
|
9081
|
+
attribute,
|
|
9082
|
+
compactedFrom: compactedClaimKey !== normalized.value.claimKey ? normalized.value.claimKey : null,
|
|
9083
|
+
reason: reasons.length > 0 ? joinCompactionReasons(reasons) : null
|
|
9084
|
+
};
|
|
9085
|
+
}
|
|
9086
|
+
function validateExtractedClaimKey(claimKey) {
|
|
9087
|
+
if (SELF_REFERENTIAL_ENTITIES.has(claimKey.entity)) {
|
|
9088
|
+
return {
|
|
9089
|
+
ok: false,
|
|
9090
|
+
reason: "self_referential_entity",
|
|
9091
|
+
value: claimKey
|
|
9092
|
+
};
|
|
9093
|
+
}
|
|
9094
|
+
if (GENERIC_ATTRIBUTES.has(claimKey.attribute)) {
|
|
9095
|
+
return {
|
|
9096
|
+
ok: false,
|
|
9097
|
+
reason: "generic_attribute",
|
|
9098
|
+
value: claimKey
|
|
9099
|
+
};
|
|
9100
|
+
}
|
|
9101
|
+
if (isValueShapedAttribute(claimKey.attribute)) {
|
|
9102
|
+
return {
|
|
9103
|
+
ok: false,
|
|
9104
|
+
reason: "value_shaped_attribute",
|
|
9105
|
+
value: claimKey
|
|
9106
|
+
};
|
|
9107
|
+
}
|
|
9108
|
+
return {
|
|
9109
|
+
ok: true,
|
|
9110
|
+
value: claimKey
|
|
9111
|
+
};
|
|
9112
|
+
}
|
|
9113
|
+
function describeClaimKeyNormalizationFailure(reason) {
|
|
9114
|
+
switch (reason) {
|
|
9115
|
+
case "empty":
|
|
9116
|
+
return "claim key was empty";
|
|
9117
|
+
case "missing_separator":
|
|
9118
|
+
return "claim key must contain exactly one '/'";
|
|
9119
|
+
case "too_many_segments":
|
|
9120
|
+
return "claim key must contain exactly one '/'";
|
|
9121
|
+
case "empty_entity":
|
|
9122
|
+
return "claim key entity was empty after normalization";
|
|
9123
|
+
case "empty_attribute":
|
|
9124
|
+
return "claim key attribute was empty after normalization";
|
|
9125
|
+
case "unknown_pair":
|
|
9126
|
+
return 'claim key "unknown/unknown" is not allowed';
|
|
9127
|
+
}
|
|
9128
|
+
}
|
|
9129
|
+
function describeExtractedClaimKeyRejection(reason, claimKey) {
|
|
9130
|
+
switch (reason) {
|
|
9131
|
+
case "self_referential_entity":
|
|
9132
|
+
return `entity "${claimKey.entity}" is self-referential`;
|
|
9133
|
+
case "generic_attribute":
|
|
9134
|
+
return `attribute "${claimKey.attribute}" is too generic`;
|
|
9135
|
+
case "value_shaped_attribute":
|
|
9136
|
+
return `attribute "${claimKey.attribute}" looks value-shaped`;
|
|
9137
|
+
}
|
|
9138
|
+
}
|
|
9139
|
+
function isValueShapedAttribute(attribute) {
|
|
9140
|
+
return /^\d+(?:_\d+)*$/u.test(attribute) || /^v\d+(?:_\d+)*$/u.test(attribute);
|
|
9141
|
+
}
|
|
9142
|
+
function compactSourceOfTruthAttribute(attributeTokens) {
|
|
9143
|
+
const sourceOfTruthIndex = findSourceOfTruthPhraseIndex(attributeTokens);
|
|
9144
|
+
if (sourceOfTruthIndex === -1) {
|
|
8721
9145
|
return null;
|
|
8722
9146
|
}
|
|
8723
|
-
const
|
|
8724
|
-
if (
|
|
9147
|
+
const normalizedPhrase = ["source", "of", "truth"];
|
|
9148
|
+
if (attributeTokens.length === normalizedPhrase.length && startsWithTokens(attributeTokens, normalizedPhrase)) {
|
|
8725
9149
|
return null;
|
|
8726
9150
|
}
|
|
8727
|
-
const
|
|
8728
|
-
const
|
|
8729
|
-
const
|
|
8730
|
-
const
|
|
8731
|
-
|
|
9151
|
+
const before = attributeTokens.slice(0, sourceOfTruthIndex);
|
|
9152
|
+
const after = attributeTokens.slice(sourceOfTruthIndex + normalizedPhrase.length);
|
|
9153
|
+
const leadingAllowed = before.every((token) => COMPACTION_WEAK_LEADING_TOKENS.has(token));
|
|
9154
|
+
const hasMixedStableFamily = before.some((token) => STABLE_ATTRIBUTE_HEADS.has(token)) || after.some((token) => STABLE_ATTRIBUTE_HEADS.has(token));
|
|
9155
|
+
const hasConjunctionNoise = before.includes("and") || before.includes("or") || after.includes("and") || after.includes("or");
|
|
9156
|
+
if (!leadingAllowed || hasMixedStableFamily || hasConjunctionNoise) {
|
|
8732
9157
|
return null;
|
|
8733
9158
|
}
|
|
8734
9159
|
return {
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
9160
|
+
attributeTokens: normalizedPhrase,
|
|
9161
|
+
reason: "collapsed source-of-truth phrasing into the stable canonical slot"
|
|
9162
|
+
};
|
|
9163
|
+
}
|
|
9164
|
+
function compactRelationAttribute(attributeTokens) {
|
|
9165
|
+
const relationIndex = attributeTokens.findIndex((token) => COMPACTION_RELATION_TOKENS.has(token));
|
|
9166
|
+
if (relationIndex === -1) {
|
|
9167
|
+
return null;
|
|
9168
|
+
}
|
|
9169
|
+
const relation = attributeTokens[relationIndex] ?? "";
|
|
9170
|
+
const left = attributeTokens.slice(0, relationIndex);
|
|
9171
|
+
const right = attributeTokens.slice(relationIndex + 1);
|
|
9172
|
+
if (left.length === 0 && right.length === 0) {
|
|
9173
|
+
return null;
|
|
9174
|
+
}
|
|
9175
|
+
if (isRequirementRelation(relation)) {
|
|
9176
|
+
const conditionAction = extractConditionAction(right);
|
|
9177
|
+
if (conditionAction) {
|
|
9178
|
+
return {
|
|
9179
|
+
attributeTokens: [conditionAction, "condition"],
|
|
9180
|
+
reason: `collapsed a sentence-like ${conditionAction} requirement into a stable condition slot`
|
|
9181
|
+
};
|
|
9182
|
+
}
|
|
9183
|
+
const requirementFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
9184
|
+
if (!requirementFocus) {
|
|
9185
|
+
return null;
|
|
9186
|
+
}
|
|
9187
|
+
return {
|
|
9188
|
+
attributeTokens: [...requirementFocus, "requirement"],
|
|
9189
|
+
reason: "collapsed a sentence-like requirement phrase into a stable requirement slot"
|
|
9190
|
+
};
|
|
9191
|
+
}
|
|
9192
|
+
if (isOrderingRelation(relation)) {
|
|
9193
|
+
const orderingFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
9194
|
+
if (!orderingFocus) {
|
|
9195
|
+
return null;
|
|
9196
|
+
}
|
|
9197
|
+
return {
|
|
9198
|
+
attributeTokens: [...orderingFocus, "order"],
|
|
9199
|
+
reason: "collapsed a sentence-like ordering phrase into a stable order slot"
|
|
9200
|
+
};
|
|
9201
|
+
}
|
|
9202
|
+
if (isPreservationRelation(relation)) {
|
|
9203
|
+
const preservationFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
9204
|
+
if (!preservationFocus) {
|
|
9205
|
+
return null;
|
|
9206
|
+
}
|
|
9207
|
+
return {
|
|
9208
|
+
attributeTokens: [...preservationFocus, "preservation"],
|
|
9209
|
+
reason: "collapsed a sentence-like preservation phrase into a stable preservation slot"
|
|
9210
|
+
};
|
|
9211
|
+
}
|
|
9212
|
+
return null;
|
|
9213
|
+
}
|
|
9214
|
+
function compactTrailingObjectAttribute(attributeTokens) {
|
|
9215
|
+
const prepositionIndex = attributeTokens.findIndex((token) => TRAILING_OBJECT_COMPACTION_PREPOSITIONS.has(token));
|
|
9216
|
+
if (prepositionIndex <= 0 || prepositionIndex >= attributeTokens.length - 1) {
|
|
9217
|
+
return null;
|
|
9218
|
+
}
|
|
9219
|
+
const left = trimWeakLeadingTokens(attributeTokens.slice(0, prepositionIndex));
|
|
9220
|
+
const right = attributeTokens.slice(prepositionIndex + 1);
|
|
9221
|
+
if (left.length === 0 || left.length > 3 || left.includes("and") || left.includes("or") || left.some((token) => COMPACTION_RELATION_TOKENS.has(token))) {
|
|
9222
|
+
return null;
|
|
9223
|
+
}
|
|
9224
|
+
const head = left[left.length - 1];
|
|
9225
|
+
if (!head || !TRAILING_OBJECT_TRANSFER_HEADS.has(head)) {
|
|
9226
|
+
return null;
|
|
9227
|
+
}
|
|
9228
|
+
const objectFocus = extractCompactionFocus(right, 2);
|
|
9229
|
+
if (!objectFocus) {
|
|
9230
|
+
return null;
|
|
9231
|
+
}
|
|
9232
|
+
const headCore = extractStableHeadCore(left, 2);
|
|
9233
|
+
if (!headCore) {
|
|
9234
|
+
return null;
|
|
9235
|
+
}
|
|
9236
|
+
return {
|
|
9237
|
+
attributeTokens: [...objectFocus, ...headCore],
|
|
9238
|
+
reason: "collapsed a trailing object phrase into a compact stable slot name"
|
|
8739
9239
|
};
|
|
8740
9240
|
}
|
|
9241
|
+
function findSourceOfTruthPhraseIndex(tokens) {
|
|
9242
|
+
for (let index = 0; index <= tokens.length - 3; index += 1) {
|
|
9243
|
+
if (tokens[index] === "source" && tokens[index + 1] === "of" && tokens[index + 2] === "truth") {
|
|
9244
|
+
return index;
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
return -1;
|
|
9248
|
+
}
|
|
9249
|
+
function extractConditionAction(tokens) {
|
|
9250
|
+
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
9251
|
+
const token = tokens[index];
|
|
9252
|
+
if (token && ACTION_CONDITION_TOKENS.has(token)) {
|
|
9253
|
+
return token;
|
|
9254
|
+
}
|
|
9255
|
+
}
|
|
9256
|
+
return null;
|
|
9257
|
+
}
|
|
9258
|
+
function extractCompactionFocus(tokens, limit) {
|
|
9259
|
+
const compactable = trimWeakLeadingTokens(tokens).filter((token) => token.length > 0);
|
|
9260
|
+
const segments = splitTokensOnBreaks(compactable).filter((segment) => segment.length > 0);
|
|
9261
|
+
const preferredSegment = segments[0];
|
|
9262
|
+
if (!preferredSegment || preferredSegment.length === 0) {
|
|
9263
|
+
return null;
|
|
9264
|
+
}
|
|
9265
|
+
return preferredSegment.slice(0, limit);
|
|
9266
|
+
}
|
|
9267
|
+
function extractStableHeadCore(tokens, limit) {
|
|
9268
|
+
const compactable = trimWeakLeadingTokens(tokens).filter((token) => token.length > 0);
|
|
9269
|
+
const head = compactable[compactable.length - 1];
|
|
9270
|
+
if (!head || !STABLE_ATTRIBUTE_HEADS.has(head)) {
|
|
9271
|
+
return null;
|
|
9272
|
+
}
|
|
9273
|
+
return compactable.slice(Math.max(0, compactable.length - limit));
|
|
9274
|
+
}
|
|
9275
|
+
function splitTokensOnBreaks(tokens) {
|
|
9276
|
+
const segments = [];
|
|
9277
|
+
let current = [];
|
|
9278
|
+
for (const token of tokens) {
|
|
9279
|
+
if (COMPACTION_BREAK_TOKENS.has(token)) {
|
|
9280
|
+
if (current.length > 0) {
|
|
9281
|
+
segments.push(current);
|
|
9282
|
+
current = [];
|
|
9283
|
+
}
|
|
9284
|
+
continue;
|
|
9285
|
+
}
|
|
9286
|
+
current.push(token);
|
|
9287
|
+
}
|
|
9288
|
+
if (current.length > 0) {
|
|
9289
|
+
segments.push(current);
|
|
9290
|
+
}
|
|
9291
|
+
return segments;
|
|
9292
|
+
}
|
|
9293
|
+
function trimWeakLeadingTokens(tokens) {
|
|
9294
|
+
let start = 0;
|
|
9295
|
+
while (start < tokens.length && COMPACTION_WEAK_LEADING_TOKENS.has(tokens[start] ?? "")) {
|
|
9296
|
+
start += 1;
|
|
9297
|
+
}
|
|
9298
|
+
return tokens.slice(start);
|
|
9299
|
+
}
|
|
9300
|
+
function joinCompactionReasons(reasons) {
|
|
9301
|
+
if (reasons.length <= 1) {
|
|
9302
|
+
return reasons[0] ?? "";
|
|
9303
|
+
}
|
|
9304
|
+
return `${reasons.slice(0, -1).join(", ")} and ${reasons[reasons.length - 1]}`;
|
|
9305
|
+
}
|
|
9306
|
+
function isRequirementRelation(token) {
|
|
9307
|
+
return token === "depend" || token === "depends" || token === "need" || token === "needs" || token === "required" || token === "require" || token === "requires";
|
|
9308
|
+
}
|
|
9309
|
+
function isOrderingRelation(token) {
|
|
9310
|
+
return token === "after" || token === "before" || token === "follow" || token === "follows" || token === "precede" || token === "precedes";
|
|
9311
|
+
}
|
|
9312
|
+
function isPreservationRelation(token) {
|
|
9313
|
+
return token === "keep" || token === "keeps" || token === "maintain" || token === "maintains" || token === "preserve" || token === "preserves" || token === "retain" || token === "retains";
|
|
9314
|
+
}
|
|
9315
|
+
function startsWithTokens(tokens, prefix) {
|
|
9316
|
+
return prefix.every((token, index) => tokens[index] === token);
|
|
9317
|
+
}
|
|
9318
|
+
function endsWithTokens(tokens, suffix) {
|
|
9319
|
+
return suffix.every((token, index) => tokens[tokens.length - suffix.length + index] === token);
|
|
9320
|
+
}
|
|
9321
|
+
|
|
9322
|
+
// ../../src/core/store/claim-extraction.ts
|
|
9323
|
+
var SELF_REFERENTIAL_ENTITIES2 = /* @__PURE__ */ new Set(["i", "me", "the_user", "myself", "user", "we", "our_team", "the_project", "this_project"]);
|
|
9324
|
+
var USER_REFERENTIAL_ENTITIES = /* @__PURE__ */ new Set(["i", "me", "myself", "the_user", "user"]);
|
|
9325
|
+
var PROJECT_REFERENTIAL_ENTITIES = /* @__PURE__ */ new Set(["the_project", "this_project"]);
|
|
9326
|
+
var DETERMINISTIC_ATTRIBUTE_HEADS = /* @__PURE__ */ new Set([
|
|
9327
|
+
"budget",
|
|
9328
|
+
"city",
|
|
9329
|
+
"config",
|
|
9330
|
+
"deadline",
|
|
9331
|
+
"email",
|
|
9332
|
+
"employer",
|
|
9333
|
+
"language",
|
|
9334
|
+
"limit",
|
|
9335
|
+
"location",
|
|
9336
|
+
"mode",
|
|
9337
|
+
"model",
|
|
9338
|
+
"name",
|
|
9339
|
+
"owner",
|
|
9340
|
+
"plan",
|
|
9341
|
+
"policy",
|
|
9342
|
+
"preference",
|
|
9343
|
+
"priority",
|
|
9344
|
+
"quota",
|
|
9345
|
+
"region",
|
|
9346
|
+
"role",
|
|
9347
|
+
"schedule",
|
|
9348
|
+
"setting",
|
|
9349
|
+
"status",
|
|
9350
|
+
"strategy",
|
|
9351
|
+
"team",
|
|
9352
|
+
"theme",
|
|
9353
|
+
"timezone",
|
|
9354
|
+
"version",
|
|
9355
|
+
"window"
|
|
9356
|
+
]);
|
|
9357
|
+
var MAX_ENTITY_HINTS = 12;
|
|
9358
|
+
var MAX_CLAIM_KEY_EXAMPLES = 8;
|
|
9359
|
+
var DEFAULT_REPAIR_CONFIDENCE = 0.86;
|
|
9360
|
+
async function previewClaimKeyExtraction(entry, llm, config, options = {}) {
|
|
9361
|
+
if (!config.enabled || !config.eligibleTypes.includes(entry.type)) {
|
|
9362
|
+
return null;
|
|
9363
|
+
}
|
|
9364
|
+
const normalizedHints = normalizeClaimExtractionHints(options.hints ?? {});
|
|
9365
|
+
let attempt;
|
|
9366
|
+
try {
|
|
9367
|
+
attempt = await attemptClaimExtraction(entry, normalizedHints, llm);
|
|
9368
|
+
} catch (error) {
|
|
9369
|
+
const repaired = tryDeterministicClaimKeyRepair(entry, normalizedHints);
|
|
9370
|
+
if (repaired) {
|
|
9371
|
+
return repaired;
|
|
9372
|
+
}
|
|
9373
|
+
throw error;
|
|
9374
|
+
}
|
|
9375
|
+
if (attempt.response.no_claim === true) {
|
|
9376
|
+
options.onPreviewOutcome?.(buildPreviewOutcome("no_claim", attempt));
|
|
9377
|
+
return null;
|
|
9378
|
+
}
|
|
9379
|
+
const candidate = buildClaimExtractionCandidate(entry, attempt.response, normalizedHints, options.onWarning);
|
|
9380
|
+
if (candidate) {
|
|
9381
|
+
options.onPreviewOutcome?.({
|
|
9382
|
+
outcome: "candidate",
|
|
9383
|
+
confidence: candidate.confidence,
|
|
9384
|
+
rawEntity: candidate.rawEntity,
|
|
9385
|
+
rawAttribute: candidate.rawAttribute,
|
|
9386
|
+
path: attempt.path
|
|
9387
|
+
});
|
|
9388
|
+
return {
|
|
9389
|
+
claimKey: candidate.claimKey,
|
|
9390
|
+
confidence: candidate.confidence,
|
|
9391
|
+
rawEntity: candidate.rawEntity,
|
|
9392
|
+
rawAttribute: candidate.rawAttribute,
|
|
9393
|
+
path: attempt.path,
|
|
9394
|
+
...candidate.compactedFrom ? {
|
|
9395
|
+
compactedFrom: candidate.compactedFrom,
|
|
9396
|
+
compactionReason: candidate.compactionReason
|
|
9397
|
+
} : {}
|
|
9398
|
+
};
|
|
9399
|
+
}
|
|
9400
|
+
options.onPreviewOutcome?.(buildPreviewOutcome("rejected_candidate", attempt));
|
|
9401
|
+
return tryDeterministicClaimKeyRepair(entry, normalizedHints);
|
|
9402
|
+
}
|
|
9403
|
+
async function extractClaimKey(entry, llm, config, options = {}) {
|
|
9404
|
+
const preview = await previewClaimKeyExtraction(entry, llm, config, options);
|
|
9405
|
+
if (!preview) {
|
|
9406
|
+
return null;
|
|
9407
|
+
}
|
|
9408
|
+
if (preview.path === "deterministic_repair" || preview.confidence >= config.confidenceThreshold) {
|
|
9409
|
+
return preview;
|
|
9410
|
+
}
|
|
9411
|
+
const deterministicRepair = tryDeterministicClaimKeyRepair(entry, normalizeClaimExtractionHints(options.hints ?? {}));
|
|
9412
|
+
if (deterministicRepair) {
|
|
9413
|
+
return deterministicRepair;
|
|
9414
|
+
}
|
|
9415
|
+
return null;
|
|
9416
|
+
}
|
|
8741
9417
|
async function getEntityHints(db) {
|
|
8742
9418
|
return db.getDistinctClaimKeyPrefixes();
|
|
8743
9419
|
}
|
|
8744
|
-
function
|
|
8745
|
-
|
|
9420
|
+
async function runBatchClaimExtraction(results, ports, config, _concurrency = 10, onWarning) {
|
|
9421
|
+
if (!config.enabled) {
|
|
9422
|
+
return /* @__PURE__ */ new Map();
|
|
9423
|
+
}
|
|
9424
|
+
const hintState = await loadClaimExtractionHintState(ports.db);
|
|
9425
|
+
const llm = ports.createLlm();
|
|
9426
|
+
const extractedEntries = /* @__PURE__ */ new Map();
|
|
9427
|
+
for (const result of results) {
|
|
9428
|
+
for (const entry of result.entries) {
|
|
9429
|
+
if (entry.claim_key) {
|
|
9430
|
+
recordClaimKeyHint(hintState, entry.claim_key);
|
|
9431
|
+
continue;
|
|
9432
|
+
}
|
|
9433
|
+
if (!config.eligibleTypes.includes(entry.type)) {
|
|
9434
|
+
continue;
|
|
9435
|
+
}
|
|
9436
|
+
try {
|
|
9437
|
+
const extracted = await extractClaimKey(
|
|
9438
|
+
{
|
|
9439
|
+
type: entry.type,
|
|
9440
|
+
subject: entry.subject,
|
|
9441
|
+
content: entry.content
|
|
9442
|
+
},
|
|
9443
|
+
llm,
|
|
9444
|
+
config,
|
|
9445
|
+
{
|
|
9446
|
+
hints: buildEntryHints(hintState, entry),
|
|
9447
|
+
onWarning
|
|
9448
|
+
}
|
|
9449
|
+
);
|
|
9450
|
+
if (extracted?.claimKey) {
|
|
9451
|
+
entry.claim_key = extracted.claimKey;
|
|
9452
|
+
recordClaimKeyHint(hintState, extracted.claimKey);
|
|
9453
|
+
extractedEntries.set(entry, extracted);
|
|
9454
|
+
}
|
|
9455
|
+
} catch {
|
|
9456
|
+
}
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
return extractedEntries;
|
|
9460
|
+
}
|
|
9461
|
+
function buildClaimExtractionSystemPrompt(hints, promptMode) {
|
|
9462
|
+
const metadataHints = [hints.userEntity ? `user_id=${hints.userEntity}` : null, hints.projectEntity ? `project=${hints.projectEntity}` : null].filter(
|
|
9463
|
+
(value) => value !== null
|
|
9464
|
+
);
|
|
9465
|
+
const groundingHints = [
|
|
9466
|
+
hints.tags.length > 0 ? `tags=${hints.tags.join(", ")}` : null,
|
|
9467
|
+
hints.sourceContext ? `source_context=${hints.sourceContext}` : null
|
|
9468
|
+
].filter((value) => value !== null);
|
|
9469
|
+
const retryInstructions = promptMode === "json_retry" ? [
|
|
9470
|
+
"",
|
|
9471
|
+
"Your previous answer was invalid JSON.",
|
|
9472
|
+
"Reply with exactly one JSON object and nothing else.",
|
|
9473
|
+
"Do not use markdown fences, commentary, or trailing text."
|
|
9474
|
+
] : [];
|
|
8746
9475
|
return [
|
|
8747
|
-
"You are a knowledge entry classifier. Extract
|
|
8748
|
-
"A claim key
|
|
9476
|
+
"You are a knowledge entry classifier. Extract one stable claim key for a durable knowledge entry.",
|
|
9477
|
+
"A claim key names the durable slot this entry updates: entity/attribute in lowercase snake_case.",
|
|
9478
|
+
"The goal is stable slot naming, not a paraphrase of the current value.",
|
|
9479
|
+
"",
|
|
9480
|
+
"Stability rules:",
|
|
9481
|
+
"- Prefer stable slot names over transient wording.",
|
|
9482
|
+
"- Choose attribute names that still make sense if the value changes.",
|
|
9483
|
+
"- Prefer short noun-like slot names over sentence-like attribute phrases.",
|
|
9484
|
+
"- When a candidate sounds like a rule or explanation sentence, compress it into the reusable slot it governs.",
|
|
9485
|
+
"- Prefer concrete entities over pronouns, deictic phrases, or self-referential placeholders.",
|
|
9486
|
+
"- Reuse an existing entity or full claim-key example when it clearly matches the same slot.",
|
|
9487
|
+
"- Stay domain-general. The same rules apply to people, devices, services, projects, places, organizations, products, datasets, policies, and preferences.",
|
|
9488
|
+
"- If the entry states a durable rule, default, workflow, guardrail, source-of-truth rule, architecture boundary, or process constraint plus rationale, extract the primary durable slot rather than the supporting rationale.",
|
|
9489
|
+
"- Do not return no_claim just because the entry explains why the rule exists. The durable policy or system slot is usually still the target.",
|
|
9490
|
+
"- Avoid full action clauses like requires_x_to_y, preserves_x_across_y, or x_precedes_y when a shorter stable slot such as trigger_condition, context_preservation, source_of_truth, or handoff_order would carry the same durable meaning.",
|
|
9491
|
+
"",
|
|
9492
|
+
"Return no_claim when:",
|
|
9493
|
+
"- The entry is narrative, multi-fact, or mostly a story about what happened.",
|
|
9494
|
+
"- The entry is an event or milestone without one continuing slot.",
|
|
9495
|
+
"- The entity is ambiguous or can only be named with a pronoun or vague placeholder.",
|
|
9496
|
+
"- The entry does not express one durable property, preference, decision, configuration, relationship, or other stable slot.",
|
|
9497
|
+
"- When unsure, prefer no_claim over inventing a weak key.",
|
|
9498
|
+
"",
|
|
9499
|
+
"Positive examples:",
|
|
9500
|
+
`- "Jim's timezone is America/Chicago." -> jim/timezone`,
|
|
9501
|
+
'- "Jim prefers oat milk in coffee." -> jim/coffee_preference',
|
|
9502
|
+
'- "Pixel 8 is set to dark mode." -> pixel_8/theme_mode',
|
|
9503
|
+
'- "Postgres max_connections is 200." -> postgres/max_connections',
|
|
9504
|
+
'- "Agenr defaults to gpt-5.4-mini." -> agenr/default_model',
|
|
9505
|
+
'- "Mac mini updates should stay manual so debugging stays predictable." -> mac_mini/manual_update_policy',
|
|
9506
|
+
'- "Use the warehouse inventory sheet as the source of truth for stock counts." -> stock_counts/source_of_truth',
|
|
9507
|
+
'- "The repo workflow is defined by AGENTS.md, even when older notes disagree." -> repo_workflow/source_of_truth',
|
|
9508
|
+
'- "Agenr keeps pure logic in src/core and adapters outside it so future hosts can plug in cleanly." -> agenr/core_adapter_boundary',
|
|
9509
|
+
'- "The before-prompt-build hook only triggers after a real agent turn or message." -> before_prompt_build_hook/trigger_condition',
|
|
9510
|
+
'- "Durable memory preserves context across sessions." -> durable_memory/context_preservation',
|
|
8749
9511
|
"",
|
|
8750
|
-
"
|
|
8751
|
-
"-
|
|
8752
|
-
"-
|
|
8753
|
-
"-
|
|
8754
|
-
"-
|
|
8755
|
-
"-
|
|
9512
|
+
"Negative examples:",
|
|
9513
|
+
"- Bad: jim/america_chicago -> Good: jim/timezone",
|
|
9514
|
+
"- Bad: project_x/details -> Good: project_x/deploy_strategy",
|
|
9515
|
+
"- Bad: we/deployment_process -> Good: platform_team/deploy_strategy",
|
|
9516
|
+
"- Bad: jim/oat_milk -> Good: jim/coffee_preference",
|
|
9517
|
+
"- Bad: release_notes/because_rollbacks_are_hard -> Good: release_process/source_of_truth",
|
|
9518
|
+
"- Bad: openclaw/requires_real_agent_turn_or_message_to_trigger -> Good: openclaw/trigger_condition",
|
|
9519
|
+
"- Bad: session_continuity/durable_memory_preserves_context_across_sessions -> Good: session_continuity/context_preservation",
|
|
9520
|
+
"- Bad: incident_story/we_spent_two_hours_debugging -> Good: no_claim",
|
|
8756
9521
|
"",
|
|
8757
|
-
|
|
8758
|
-
|
|
9522
|
+
"Field rules:",
|
|
9523
|
+
"- entity: the main concrete thing being described. It can be a person, device, service, product, organization, workflow area, or other durable system/process anchor.",
|
|
9524
|
+
"- attribute: the narrow stable slot on that entity. For policy/process entries, name the governing slot such as source_of_truth, default_mode, update_policy, architecture_boundary, deploy_strategy, or escalation_workflow.",
|
|
9525
|
+
"- Confidence: 0.0 to 1.0. Use 0.9+ only when the slot is unambiguous and durable.",
|
|
9526
|
+
"",
|
|
9527
|
+
`Known entity hints: ${hints.entityHints.length > 0 ? hints.entityHints.join(", ") : "(none)"}`,
|
|
9528
|
+
`Known claim-key examples: ${hints.claimKeyExamples.length > 0 ? hints.claimKeyExamples.join(", ") : "(none)"}`,
|
|
9529
|
+
`Current entry metadata hints: ${metadataHints.length > 0 ? metadataHints.join(", ") : "(none)"}`,
|
|
9530
|
+
`Current entry grounding clues: ${groundingHints.length > 0 ? groundingHints.join(", ") : "(none)"}`,
|
|
9531
|
+
'If project metadata is present, it may resolve phrases like "the project" when that mapping is obvious.',
|
|
9532
|
+
'If user metadata is present, it may resolve phrases like "the user", "I", or "me" when that mapping is obvious.',
|
|
9533
|
+
"Tags and source_context are local grounding clues, not proof. Use them to pick the right durable slot only when the entry content already supports that slot.",
|
|
9534
|
+
...retryInstructions,
|
|
8759
9535
|
"",
|
|
8760
9536
|
'Respond with JSON: { "entity": string, "attribute": string, "confidence": number, "no_claim"?: boolean }'
|
|
8761
9537
|
].join("\n");
|
|
@@ -8763,28 +9539,270 @@ function buildClaimExtractionSystemPrompt(entityHints) {
|
|
|
8763
9539
|
function buildClaimExtractionUserPrompt(entry) {
|
|
8764
9540
|
return [`Entry type: ${entry.type}`, `Subject: ${entry.subject}`, `Content: ${entry.content}`].join("\n");
|
|
8765
9541
|
}
|
|
9542
|
+
async function attemptClaimExtraction(entry, hints, llm) {
|
|
9543
|
+
const userPrompt = buildClaimExtractionUserPrompt(entry);
|
|
9544
|
+
try {
|
|
9545
|
+
return {
|
|
9546
|
+
path: "model",
|
|
9547
|
+
response: await llm.completeJson(buildClaimExtractionSystemPrompt(hints, "standard"), userPrompt)
|
|
9548
|
+
};
|
|
9549
|
+
} catch (error) {
|
|
9550
|
+
if (!isMalformedJsonError(error)) {
|
|
9551
|
+
throw error;
|
|
9552
|
+
}
|
|
9553
|
+
}
|
|
9554
|
+
return {
|
|
9555
|
+
path: "json_retry",
|
|
9556
|
+
response: await llm.completeJson(buildClaimExtractionSystemPrompt(hints, "json_retry"), userPrompt)
|
|
9557
|
+
};
|
|
9558
|
+
}
|
|
9559
|
+
function buildClaimExtractionCandidate(entry, response, hints, onWarning) {
|
|
9560
|
+
const confidence = normalizeConfidence(response.confidence);
|
|
9561
|
+
const rawEntity = typeof response.entity === "string" ? response.entity.trim() : "";
|
|
9562
|
+
const rawAttribute = typeof response.attribute === "string" ? response.attribute.trim() : "";
|
|
9563
|
+
const entity = normalizeEntity(rawEntity, hints);
|
|
9564
|
+
const attribute = normalizeClaimKeySegment(rawAttribute);
|
|
9565
|
+
const normalizedClaimKey = normalizeClaimKey(`${entity}/${attribute}`);
|
|
9566
|
+
if (!normalizedClaimKey.ok) {
|
|
9567
|
+
onWarning?.(`Claim extraction dropped claim key for "${entry.subject}": ${describeClaimKeyNormalizationFailure(normalizedClaimKey.reason)}.`);
|
|
9568
|
+
return null;
|
|
9569
|
+
}
|
|
9570
|
+
const compactedClaimKey = compactClaimKey(normalizedClaimKey.value.claimKey);
|
|
9571
|
+
if (!compactedClaimKey) {
|
|
9572
|
+
onWarning?.(`Claim extraction dropped claim key for "${entry.subject}": claim key could not be compacted safely.`);
|
|
9573
|
+
return null;
|
|
9574
|
+
}
|
|
9575
|
+
const validatedClaimKey = validateExtractedClaimKey(compactedClaimKey);
|
|
9576
|
+
if (!validatedClaimKey.ok) {
|
|
9577
|
+
onWarning?.(
|
|
9578
|
+
`Claim extraction rejected "${validatedClaimKey.value.claimKey}" for "${entry.subject}": ${describeExtractedClaimKeyRejection(validatedClaimKey.reason, validatedClaimKey.value)}.`
|
|
9579
|
+
);
|
|
9580
|
+
return null;
|
|
9581
|
+
}
|
|
9582
|
+
return {
|
|
9583
|
+
claimKey: validatedClaimKey.value.claimKey,
|
|
9584
|
+
confidence,
|
|
9585
|
+
rawEntity,
|
|
9586
|
+
rawAttribute,
|
|
9587
|
+
compactedFrom: compactedClaimKey.compactedFrom,
|
|
9588
|
+
compactionReason: compactedClaimKey.reason
|
|
9589
|
+
};
|
|
9590
|
+
}
|
|
9591
|
+
function tryDeterministicClaimKeyRepair(entry, hints) {
|
|
9592
|
+
const repaired = parsePossessiveClaim(entry.subject) ?? parsePossessiveStatement(entry.content);
|
|
9593
|
+
if (!repaired) {
|
|
9594
|
+
return null;
|
|
9595
|
+
}
|
|
9596
|
+
const attribute = normalizeClaimKeySegment(repaired.attribute);
|
|
9597
|
+
if (!looksLikeDeterministicAttribute(attribute)) {
|
|
9598
|
+
return null;
|
|
9599
|
+
}
|
|
9600
|
+
const entity = normalizeEntity(repaired.entity, hints);
|
|
9601
|
+
const normalizedClaimKey = normalizeClaimKey(`${entity}/${attribute}`);
|
|
9602
|
+
if (!normalizedClaimKey.ok) {
|
|
9603
|
+
return null;
|
|
9604
|
+
}
|
|
9605
|
+
const validatedClaimKey = validateExtractedClaimKey(normalizedClaimKey.value);
|
|
9606
|
+
if (!validatedClaimKey.ok) {
|
|
9607
|
+
return null;
|
|
9608
|
+
}
|
|
9609
|
+
return {
|
|
9610
|
+
claimKey: validatedClaimKey.value.claimKey,
|
|
9611
|
+
confidence: DEFAULT_REPAIR_CONFIDENCE,
|
|
9612
|
+
rawEntity: repaired.entity,
|
|
9613
|
+
rawAttribute: repaired.attribute,
|
|
9614
|
+
path: "deterministic_repair"
|
|
9615
|
+
};
|
|
9616
|
+
}
|
|
9617
|
+
async function loadClaimExtractionHintState(db) {
|
|
9618
|
+
const [entityHintResult, claimKeyExampleResult] = await Promise.allSettled([getEntityHints(db), getClaimKeyExamples(db)]);
|
|
9619
|
+
return createHintState({
|
|
9620
|
+
entityHints: entityHintResult.status === "fulfilled" ? entityHintResult.value : [],
|
|
9621
|
+
claimKeyExamples: claimKeyExampleResult.status === "fulfilled" ? claimKeyExampleResult.value : []
|
|
9622
|
+
});
|
|
9623
|
+
}
|
|
9624
|
+
async function getClaimKeyExamples(db) {
|
|
9625
|
+
if (typeof db.getClaimKeyExamples !== "function") {
|
|
9626
|
+
return [];
|
|
9627
|
+
}
|
|
9628
|
+
return db.getClaimKeyExamples(MAX_CLAIM_KEY_EXAMPLES);
|
|
9629
|
+
}
|
|
9630
|
+
function createHintState(input) {
|
|
9631
|
+
const claimKeyExamples = normalizeClaimKeyExamples(input.claimKeyExamples ?? []);
|
|
9632
|
+
const entityHints = limitUnique(
|
|
9633
|
+
[
|
|
9634
|
+
...normalizeEntityHints(input.entityHints ?? []),
|
|
9635
|
+
...claimKeyExamples.flatMap((claimKey) => {
|
|
9636
|
+
const normalizedClaimKey = normalizeClaimKey(claimKey);
|
|
9637
|
+
return normalizedClaimKey.ok ? [normalizedClaimKey.value.entity] : [];
|
|
9638
|
+
})
|
|
9639
|
+
],
|
|
9640
|
+
MAX_ENTITY_HINTS
|
|
9641
|
+
);
|
|
9642
|
+
return {
|
|
9643
|
+
entityHints,
|
|
9644
|
+
claimKeyExamples
|
|
9645
|
+
};
|
|
9646
|
+
}
|
|
9647
|
+
function buildEntryHints(state, entry) {
|
|
9648
|
+
return {
|
|
9649
|
+
entityHints: [...state.entityHints],
|
|
9650
|
+
claimKeyExamples: [...state.claimKeyExamples],
|
|
9651
|
+
userId: entry.user_id,
|
|
9652
|
+
project: entry.project,
|
|
9653
|
+
tags: entry.tags,
|
|
9654
|
+
sourceContext: entry.source_context
|
|
9655
|
+
};
|
|
9656
|
+
}
|
|
9657
|
+
function recordClaimKeyHint(state, claimKey) {
|
|
9658
|
+
const normalizedClaimKey = normalizeClaimKey(claimKey);
|
|
9659
|
+
if (!normalizedClaimKey.ok) {
|
|
9660
|
+
return;
|
|
9661
|
+
}
|
|
9662
|
+
state.claimKeyExamples = prependUnique(state.claimKeyExamples, normalizedClaimKey.value.claimKey, MAX_CLAIM_KEY_EXAMPLES);
|
|
9663
|
+
state.entityHints = prependUnique(state.entityHints, normalizedClaimKey.value.entity, MAX_ENTITY_HINTS);
|
|
9664
|
+
}
|
|
9665
|
+
function normalizeClaimExtractionHints(hints) {
|
|
9666
|
+
const claimKeyExamples = normalizeClaimKeyExamples(hints.claimKeyExamples ?? []);
|
|
9667
|
+
return {
|
|
9668
|
+
entityHints: limitUnique(
|
|
9669
|
+
[
|
|
9670
|
+
...normalizeEntityHints(hints.entityHints ?? []),
|
|
9671
|
+
...claimKeyExamples.flatMap((claimKey) => {
|
|
9672
|
+
const normalizedClaimKey = normalizeClaimKey(claimKey);
|
|
9673
|
+
return normalizedClaimKey.ok ? [normalizedClaimKey.value.entity] : [];
|
|
9674
|
+
})
|
|
9675
|
+
],
|
|
9676
|
+
MAX_ENTITY_HINTS
|
|
9677
|
+
),
|
|
9678
|
+
claimKeyExamples,
|
|
9679
|
+
userEntity: normalizeMetadataEntity(hints.userId),
|
|
9680
|
+
projectEntity: normalizeMetadataEntity(hints.project),
|
|
9681
|
+
tags: normalizeHintTags(hints.tags ?? []),
|
|
9682
|
+
sourceContext: normalizeSourceContextHint(hints.sourceContext)
|
|
9683
|
+
};
|
|
9684
|
+
}
|
|
9685
|
+
function buildPreviewOutcome(outcome, attempt) {
|
|
9686
|
+
return {
|
|
9687
|
+
outcome,
|
|
9688
|
+
confidence: normalizeConfidence(attempt.response.confidence),
|
|
9689
|
+
rawEntity: typeof attempt.response.entity === "string" ? attempt.response.entity.trim() : "",
|
|
9690
|
+
rawAttribute: typeof attempt.response.attribute === "string" ? attempt.response.attribute.trim() : "",
|
|
9691
|
+
path: attempt.path
|
|
9692
|
+
};
|
|
9693
|
+
}
|
|
8766
9694
|
function normalizeConfidence(value) {
|
|
8767
9695
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
8768
9696
|
return 0;
|
|
8769
9697
|
}
|
|
8770
9698
|
return Math.min(1, Math.max(0, value));
|
|
8771
9699
|
}
|
|
8772
|
-
function normalizeEntity(value,
|
|
8773
|
-
const normalizedValue =
|
|
9700
|
+
function normalizeEntity(value, hints) {
|
|
9701
|
+
const normalizedValue = normalizeClaimKeySegment(value);
|
|
8774
9702
|
if (normalizedValue.length === 0) {
|
|
8775
9703
|
return "";
|
|
8776
9704
|
}
|
|
8777
|
-
|
|
8778
|
-
if (SELF_REFERENTIAL_ENTITIES.has(normalizedValue)) {
|
|
8779
|
-
if (normalizedHints.length === 1) {
|
|
8780
|
-
return normalizedHints[0] ?? normalizedValue;
|
|
8781
|
-
}
|
|
9705
|
+
if (!SELF_REFERENTIAL_ENTITIES2.has(normalizedValue)) {
|
|
8782
9706
|
return normalizedValue;
|
|
8783
9707
|
}
|
|
9708
|
+
if (USER_REFERENTIAL_ENTITIES.has(normalizedValue) && hints.userEntity) {
|
|
9709
|
+
return hints.userEntity;
|
|
9710
|
+
}
|
|
9711
|
+
if (PROJECT_REFERENTIAL_ENTITIES.has(normalizedValue) && hints.projectEntity) {
|
|
9712
|
+
return hints.projectEntity;
|
|
9713
|
+
}
|
|
9714
|
+
const concreteCandidates = limitUnique(
|
|
9715
|
+
[hints.projectEntity, hints.userEntity, ...hints.entityHints].filter(
|
|
9716
|
+
(candidate) => typeof candidate === "string" && candidate.length > 0
|
|
9717
|
+
),
|
|
9718
|
+
MAX_ENTITY_HINTS
|
|
9719
|
+
);
|
|
9720
|
+
if (concreteCandidates.length === 1) {
|
|
9721
|
+
return concreteCandidates[0] ?? normalizedValue;
|
|
9722
|
+
}
|
|
9723
|
+
if (hints.entityHints.length === 1) {
|
|
9724
|
+
return hints.entityHints[0] ?? normalizedValue;
|
|
9725
|
+
}
|
|
8784
9726
|
return normalizedValue;
|
|
8785
9727
|
}
|
|
8786
|
-
function
|
|
8787
|
-
return
|
|
9728
|
+
function normalizeEntityHints(entityHints) {
|
|
9729
|
+
return limitUnique(
|
|
9730
|
+
entityHints.map((entityHint) => normalizeClaimKeySegment(entityHint)).filter((entityHint) => entityHint.length > 0 && !SELF_REFERENTIAL_ENTITIES2.has(entityHint)),
|
|
9731
|
+
MAX_ENTITY_HINTS
|
|
9732
|
+
);
|
|
9733
|
+
}
|
|
9734
|
+
function normalizeClaimKeyExamples(claimKeyExamples) {
|
|
9735
|
+
return limitUnique(
|
|
9736
|
+
claimKeyExamples.flatMap((claimKeyExample) => {
|
|
9737
|
+
const normalizedClaimKey = normalizeClaimKey(claimKeyExample);
|
|
9738
|
+
return normalizedClaimKey.ok ? [normalizedClaimKey.value.claimKey] : [];
|
|
9739
|
+
}),
|
|
9740
|
+
MAX_CLAIM_KEY_EXAMPLES
|
|
9741
|
+
);
|
|
9742
|
+
}
|
|
9743
|
+
function normalizeMetadataEntity(value) {
|
|
9744
|
+
if (typeof value !== "string") {
|
|
9745
|
+
return void 0;
|
|
9746
|
+
}
|
|
9747
|
+
const normalized = normalizeClaimKeySegment(value);
|
|
9748
|
+
if (normalized.length === 0 || SELF_REFERENTIAL_ENTITIES2.has(normalized) || !/[a-z]/u.test(normalized)) {
|
|
9749
|
+
return void 0;
|
|
9750
|
+
}
|
|
9751
|
+
return normalized;
|
|
9752
|
+
}
|
|
9753
|
+
function normalizeHintTags(tags) {
|
|
9754
|
+
return limitUnique(
|
|
9755
|
+
tags.map((tag) => normalizeClaimKeySegment(tag)).filter((tag) => tag.length > 0),
|
|
9756
|
+
8
|
|
9757
|
+
);
|
|
9758
|
+
}
|
|
9759
|
+
function normalizeSourceContextHint(value) {
|
|
9760
|
+
const trimmed = value?.trim();
|
|
9761
|
+
if (!trimmed) {
|
|
9762
|
+
return void 0;
|
|
9763
|
+
}
|
|
9764
|
+
return trimmed.length <= 160 ? trimmed : `${trimmed.slice(0, 157).trimEnd()}...`;
|
|
9765
|
+
}
|
|
9766
|
+
function isMalformedJsonError(error) {
|
|
9767
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9768
|
+
return /json|unexpected token|unexpected end|unexpected non-whitespace|unterminated|position \d+/iu.test(message);
|
|
9769
|
+
}
|
|
9770
|
+
function parsePossessiveClaim(subject) {
|
|
9771
|
+
const match = /^\s*(?<entity>[^.!?\n]+?)[’']s\s+(?<attribute>[^.!?\n]+?)\s*$/iu.exec(subject);
|
|
9772
|
+
if (!match?.groups) {
|
|
9773
|
+
return null;
|
|
9774
|
+
}
|
|
9775
|
+
return {
|
|
9776
|
+
entity: stripTrailingPunctuation(match.groups.entity),
|
|
9777
|
+
attribute: stripTrailingPunctuation(match.groups.attribute)
|
|
9778
|
+
};
|
|
9779
|
+
}
|
|
9780
|
+
function parsePossessiveStatement(content) {
|
|
9781
|
+
const match = /^\s*(?<entity>[^.!?\n]+?)[’']s\s+(?<attribute>[^.!?\n]+?)\s+(?:is|are|was|were)\b/iu.exec(content);
|
|
9782
|
+
if (!match?.groups) {
|
|
9783
|
+
return null;
|
|
9784
|
+
}
|
|
9785
|
+
return {
|
|
9786
|
+
entity: stripTrailingPunctuation(match.groups.entity),
|
|
9787
|
+
attribute: stripTrailingPunctuation(match.groups.attribute)
|
|
9788
|
+
};
|
|
9789
|
+
}
|
|
9790
|
+
function stripTrailingPunctuation(value) {
|
|
9791
|
+
return value.trim().replace(/[\s"'“”‘’.,:;!?]+$/gu, "").trim();
|
|
9792
|
+
}
|
|
9793
|
+
function looksLikeDeterministicAttribute(attribute) {
|
|
9794
|
+
const parts = attribute.split("_").filter((part) => part.length > 0);
|
|
9795
|
+
if (parts.length === 0 || parts.length > 4) {
|
|
9796
|
+
return false;
|
|
9797
|
+
}
|
|
9798
|
+
const head = parts[parts.length - 1];
|
|
9799
|
+
return typeof head === "string" && DETERMINISTIC_ATTRIBUTE_HEADS.has(head);
|
|
9800
|
+
}
|
|
9801
|
+
function prependUnique(values, value, limit) {
|
|
9802
|
+
return limitUnique([value, ...values], limit);
|
|
9803
|
+
}
|
|
9804
|
+
function limitUnique(values, limit) {
|
|
9805
|
+
return Array.from(new Set(values.filter((value) => value.length > 0))).slice(0, limit);
|
|
8788
9806
|
}
|
|
8789
9807
|
|
|
8790
9808
|
// ../../src/core/store/embedding-text.ts
|
|
@@ -8809,6 +9827,7 @@ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-
|
|
|
8809
9827
|
function validateEntriesWithIndexes(inputs) {
|
|
8810
9828
|
const valid = [];
|
|
8811
9829
|
const errors = [];
|
|
9830
|
+
const warnings = [];
|
|
8812
9831
|
const rejectedInputIndexes = [];
|
|
8813
9832
|
for (const [index, input] of inputs.entries()) {
|
|
8814
9833
|
const subject = normalizeString(input.subject);
|
|
@@ -8848,11 +9867,6 @@ function validateEntriesWithIndexes(inputs) {
|
|
|
8848
9867
|
rejectedInputIndexes.push(index);
|
|
8849
9868
|
continue;
|
|
8850
9869
|
}
|
|
8851
|
-
if (input.claim_key !== void 0 && typeof input.claim_key !== "string") {
|
|
8852
|
-
errors.push(`Entry ${index} has an invalid claim key.`);
|
|
8853
|
-
rejectedInputIndexes.push(index);
|
|
8854
|
-
continue;
|
|
8855
|
-
}
|
|
8856
9870
|
if (input.valid_from !== void 0 && !isIsoTimestamp(input.valid_from)) {
|
|
8857
9871
|
errors.push(`Entry ${index} has an invalid valid_from timestamp.`);
|
|
8858
9872
|
rejectedInputIndexes.push(index);
|
|
@@ -8863,6 +9877,21 @@ function validateEntriesWithIndexes(inputs) {
|
|
|
8863
9877
|
rejectedInputIndexes.push(index);
|
|
8864
9878
|
continue;
|
|
8865
9879
|
}
|
|
9880
|
+
let normalizedClaimKey;
|
|
9881
|
+
if (input.claim_key !== void 0) {
|
|
9882
|
+
if (typeof input.claim_key !== "string") {
|
|
9883
|
+
warnings.push(`Entry ${index} provided a non-string claim key and it was dropped.`);
|
|
9884
|
+
} else {
|
|
9885
|
+
const claimKey = normalizeClaimKey(input.claim_key);
|
|
9886
|
+
if (claimKey.ok) {
|
|
9887
|
+
normalizedClaimKey = claimKey.value.claimKey;
|
|
9888
|
+
} else {
|
|
9889
|
+
warnings.push(
|
|
9890
|
+
`Entry ${index} provided invalid claim key ${JSON.stringify(input.claim_key)} and it was dropped: ${describeClaimKeyNormalizationFailure(claimKey.reason)}.`
|
|
9891
|
+
);
|
|
9892
|
+
}
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
8866
9895
|
valid.push({
|
|
8867
9896
|
inputIndex: index,
|
|
8868
9897
|
input: {
|
|
@@ -8878,7 +9907,7 @@ function validateEntriesWithIndexes(inputs) {
|
|
|
8878
9907
|
project: normalizeOptionalString(input.project),
|
|
8879
9908
|
created_at: normalizeOptionalString(input.created_at),
|
|
8880
9909
|
supersedes: normalizeOptionalString(input.supersedes),
|
|
8881
|
-
claim_key:
|
|
9910
|
+
claim_key: normalizedClaimKey,
|
|
8882
9911
|
valid_from: normalizeOptionalString(input.valid_from),
|
|
8883
9912
|
valid_to: normalizeOptionalString(input.valid_to)
|
|
8884
9913
|
}
|
|
@@ -8888,7 +9917,8 @@ function validateEntriesWithIndexes(inputs) {
|
|
|
8888
9917
|
valid,
|
|
8889
9918
|
rejected: errors.length,
|
|
8890
9919
|
rejectedInputIndexes,
|
|
8891
|
-
errors
|
|
9920
|
+
errors,
|
|
9921
|
+
warnings
|
|
8892
9922
|
};
|
|
8893
9923
|
}
|
|
8894
9924
|
function clampImportance(value) {
|
|
@@ -8922,11 +9952,16 @@ function isIsoTimestamp(value) {
|
|
|
8922
9952
|
}
|
|
8923
9953
|
|
|
8924
9954
|
// ../../src/core/store/pipeline.ts
|
|
9955
|
+
var AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE = 0.9;
|
|
9956
|
+
var AUTO_SUPERSESSION_ELIGIBLE_PATHS = /* @__PURE__ */ new Set(["model", "json_retry"]);
|
|
8925
9957
|
async function storeEntriesDetailed(inputs, db, embedding, options = {}) {
|
|
8926
9958
|
if (inputs.length === 0) {
|
|
8927
9959
|
return { stored: 0, skipped: 0, rejected: 0, details: [] };
|
|
8928
9960
|
}
|
|
8929
9961
|
const plan = await buildStorePlan(inputs, db);
|
|
9962
|
+
for (const warning of plan.warnings) {
|
|
9963
|
+
options.onWarning?.(warning);
|
|
9964
|
+
}
|
|
8930
9965
|
if (plan.pendingEntries.length === 0) {
|
|
8931
9966
|
return {
|
|
8932
9967
|
stored: 0,
|
|
@@ -8951,9 +9986,9 @@ async function storeEntriesDetailed(inputs, db, embedding, options = {}) {
|
|
|
8951
9986
|
};
|
|
8952
9987
|
}
|
|
8953
9988
|
const pendingEntries = plan.pendingEntries;
|
|
8954
|
-
await maybeExtractClaimKeys(pendingEntries, options);
|
|
9989
|
+
const extractedClaimKeys = await maybeExtractClaimKeys(pendingEntries, options);
|
|
8955
9990
|
const embeddings = await resolvePendingEmbeddings(inputs, pendingEntries, embedding, options.precomputedEmbeddings);
|
|
8956
|
-
await persistEntries(db, pendingEntries, embeddings, options.onWarning);
|
|
9991
|
+
await persistEntries(db, pendingEntries, embeddings, extractedClaimKeys, options.claimExtraction?.config, options.onWarning);
|
|
8957
9992
|
return {
|
|
8958
9993
|
stored: pendingEntries.length,
|
|
8959
9994
|
skipped: plan.skipped,
|
|
@@ -8990,9 +10025,11 @@ async function embedPendingEntries(entries, embedding) {
|
|
|
8990
10025
|
}
|
|
8991
10026
|
return vectors;
|
|
8992
10027
|
}
|
|
8993
|
-
async function persistEntries(db, preparedEntries, embeddings, onWarning) {
|
|
10028
|
+
async function persistEntries(db, preparedEntries, embeddings, extractedClaimKeys, claimExtractionConfig, onWarning) {
|
|
8994
10029
|
const writeBatch = async (targetDb) => {
|
|
8995
10030
|
let stored = 0;
|
|
10031
|
+
const autoSupersessionPlans = await planAutoSupersession(targetDb, preparedEntries, extractedClaimKeys, claimExtractionConfig);
|
|
10032
|
+
const emittedWarnings = /* @__PURE__ */ new Set();
|
|
8996
10033
|
for (const [index, preparedEntry] of preparedEntries.entries()) {
|
|
8997
10034
|
const embedding = embeddings[index] ?? [];
|
|
8998
10035
|
const entry = buildEntry(preparedEntry, embedding);
|
|
@@ -9004,11 +10041,27 @@ async function persistEntries(db, preparedEntries, embeddings, onWarning) {
|
|
|
9004
10041
|
onWarning?.(`Stored entry ${entryId} but could not supersede ${supersededEntryId} because the target was missing or inactive.`);
|
|
9005
10042
|
}
|
|
9006
10043
|
}
|
|
10044
|
+
const autoSupersessionPlan = autoSupersessionPlans.get(preparedEntry.inputIndex);
|
|
10045
|
+
if (autoSupersessionPlan?.kind === "link" && autoSupersessionPlan.oldEntryId) {
|
|
10046
|
+
const superseded = await targetDb.supersedeEntry(autoSupersessionPlan.oldEntryId, entryId, "update");
|
|
10047
|
+
if (!superseded) {
|
|
10048
|
+
onWarning?.(
|
|
10049
|
+
`Stored entry ${entryId} with claim_key "${preparedEntry.input.claim_key}" but could not auto-supersede ${autoSupersessionPlan.oldEntryId} because the target was missing or inactive.`
|
|
10050
|
+
);
|
|
10051
|
+
}
|
|
10052
|
+
}
|
|
10053
|
+
if (autoSupersessionPlan?.warning && !emittedWarnings.has(autoSupersessionPlan.warning)) {
|
|
10054
|
+
emittedWarnings.add(autoSupersessionPlan.warning);
|
|
10055
|
+
onWarning?.(autoSupersessionPlan.warning);
|
|
10056
|
+
}
|
|
9007
10057
|
stored += 1;
|
|
9008
10058
|
}
|
|
9009
10059
|
return stored;
|
|
9010
10060
|
};
|
|
9011
|
-
if (hasTransactionSupport(db) &&
|
|
10061
|
+
if (hasTransactionSupport(db) && preparedEntries.some((entry) => entry.input.supersedes !== void 0 || entry.input.claim_key !== void 0)) {
|
|
10062
|
+
return db.withTransaction(writeBatch);
|
|
10063
|
+
}
|
|
10064
|
+
if (hasTransactionSupport(db) && preparedEntries.length > 1) {
|
|
9012
10065
|
return db.withTransaction(writeBatch);
|
|
9013
10066
|
}
|
|
9014
10067
|
return writeBatch(db);
|
|
@@ -9043,39 +10096,147 @@ function buildEntry(preparedEntry, embedding) {
|
|
|
9043
10096
|
async function maybeExtractClaimKeys(preparedEntries, options) {
|
|
9044
10097
|
const claimExtraction = options.claimExtraction;
|
|
9045
10098
|
if (!claimExtraction || preparedEntries.length === 0) {
|
|
9046
|
-
return;
|
|
10099
|
+
return /* @__PURE__ */ new Map();
|
|
9047
10100
|
}
|
|
9048
|
-
let entityHints = [];
|
|
9049
10101
|
try {
|
|
9050
|
-
|
|
10102
|
+
const extractedEntries = await runBatchClaimExtraction(
|
|
10103
|
+
[
|
|
10104
|
+
{
|
|
10105
|
+
entries: preparedEntries.map((preparedEntry) => preparedEntry.input)
|
|
10106
|
+
}
|
|
10107
|
+
],
|
|
10108
|
+
{
|
|
10109
|
+
createLlm: () => claimExtraction.llm,
|
|
10110
|
+
db: claimExtraction.db
|
|
10111
|
+
},
|
|
10112
|
+
claimExtraction.config,
|
|
10113
|
+
1,
|
|
10114
|
+
options.onWarning
|
|
10115
|
+
);
|
|
10116
|
+
const extractedClaimKeys = /* @__PURE__ */ new Map();
|
|
10117
|
+
for (const preparedEntry of preparedEntries) {
|
|
10118
|
+
const extracted = extractedEntries.get(preparedEntry.input);
|
|
10119
|
+
if (extracted) {
|
|
10120
|
+
extractedClaimKeys.set(preparedEntry.inputIndex, extracted);
|
|
10121
|
+
}
|
|
10122
|
+
}
|
|
10123
|
+
return extractedClaimKeys;
|
|
9051
10124
|
} catch (error) {
|
|
9052
|
-
|
|
10125
|
+
const subject = preparedEntries[0]?.input.subject ?? "batch";
|
|
10126
|
+
options.onWarning?.(`Claim extraction failed for "${subject}": ${formatPipelineError(error)}`);
|
|
10127
|
+
return /* @__PURE__ */ new Map();
|
|
9053
10128
|
}
|
|
10129
|
+
}
|
|
10130
|
+
function hasTransactionSupport(db) {
|
|
10131
|
+
return typeof db.withTransaction === "function";
|
|
10132
|
+
}
|
|
10133
|
+
async function planAutoSupersession(db, preparedEntries, extractedClaimKeys, claimExtractionConfig) {
|
|
10134
|
+
const plans = /* @__PURE__ */ new Map();
|
|
10135
|
+
const preparedEntriesByClaimKey = groupPreparedEntriesByClaimKey(preparedEntries);
|
|
10136
|
+
const siblingCache = /* @__PURE__ */ new Map();
|
|
9054
10137
|
for (const preparedEntry of preparedEntries) {
|
|
9055
|
-
|
|
10138
|
+
const claimKey = preparedEntry.input.claim_key;
|
|
10139
|
+
if (!claimKey || preparedEntry.input.supersedes) {
|
|
9056
10140
|
continue;
|
|
9057
10141
|
}
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
10142
|
+
const siblings = await getClaimKeySiblings(db, siblingCache, claimKey);
|
|
10143
|
+
if (siblings.length === 0) {
|
|
10144
|
+
continue;
|
|
10145
|
+
}
|
|
10146
|
+
const batchSiblingCount = preparedEntriesByClaimKey.get(claimKey)?.length ?? 0;
|
|
10147
|
+
if (batchSiblingCount > 1) {
|
|
10148
|
+
plans.set(preparedEntry.inputIndex, {
|
|
10149
|
+
kind: "skip",
|
|
10150
|
+
warning: `Skipped auto-supersession for claim_key "${claimKey}" because this store batch contains ${batchSiblingCount} entries for the same slot.`
|
|
10151
|
+
});
|
|
10152
|
+
continue;
|
|
10153
|
+
}
|
|
10154
|
+
if (siblings.length > 1) {
|
|
10155
|
+
plans.set(preparedEntry.inputIndex, {
|
|
10156
|
+
kind: "skip",
|
|
10157
|
+
warning: `Skipped auto-supersession for claim_key "${claimKey}" because ${siblings.length} active siblings already exist for that slot.`
|
|
10158
|
+
});
|
|
10159
|
+
continue;
|
|
10160
|
+
}
|
|
10161
|
+
const sibling = siblings[0];
|
|
10162
|
+
if (!sibling) {
|
|
10163
|
+
continue;
|
|
10164
|
+
}
|
|
10165
|
+
if (!isAutoSupersessionEligible(preparedEntry, extractedClaimKeys, claimExtractionConfig)) {
|
|
10166
|
+
plans.set(preparedEntry.inputIndex, {
|
|
10167
|
+
kind: "skip",
|
|
10168
|
+
warning: buildAutoSupersessionEligibilityWarning(preparedEntry, extractedClaimKeys.get(preparedEntry.inputIndex))
|
|
10169
|
+
});
|
|
10170
|
+
continue;
|
|
10171
|
+
}
|
|
10172
|
+
const supersessionValidation = validateSupersessionRules(sibling, {
|
|
10173
|
+
type: preparedEntry.input.type,
|
|
10174
|
+
expiry: preparedEntry.input.expiry ?? "temporary"
|
|
10175
|
+
});
|
|
10176
|
+
if (!supersessionValidation.ok) {
|
|
10177
|
+
plans.set(preparedEntry.inputIndex, {
|
|
10178
|
+
kind: "skip",
|
|
10179
|
+
warning: buildAutoSupersessionRuleWarning(preparedEntry, sibling, supersessionValidation.reason)
|
|
10180
|
+
});
|
|
10181
|
+
continue;
|
|
9074
10182
|
}
|
|
10183
|
+
plans.set(preparedEntry.inputIndex, {
|
|
10184
|
+
kind: "link",
|
|
10185
|
+
oldEntryId: sibling.id
|
|
10186
|
+
});
|
|
9075
10187
|
}
|
|
10188
|
+
return plans;
|
|
9076
10189
|
}
|
|
9077
|
-
function
|
|
9078
|
-
|
|
10190
|
+
function groupPreparedEntriesByClaimKey(preparedEntries) {
|
|
10191
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
10192
|
+
for (const preparedEntry of preparedEntries) {
|
|
10193
|
+
const claimKey = preparedEntry.input.claim_key;
|
|
10194
|
+
if (!claimKey) {
|
|
10195
|
+
continue;
|
|
10196
|
+
}
|
|
10197
|
+
const existing = grouped.get(claimKey) ?? [];
|
|
10198
|
+
existing.push(preparedEntry);
|
|
10199
|
+
grouped.set(claimKey, existing);
|
|
10200
|
+
}
|
|
10201
|
+
return grouped;
|
|
10202
|
+
}
|
|
10203
|
+
async function getClaimKeySiblings(db, cache, claimKey) {
|
|
10204
|
+
const cached = cache.get(claimKey);
|
|
10205
|
+
if (cached) {
|
|
10206
|
+
return cached;
|
|
10207
|
+
}
|
|
10208
|
+
const siblings = await db.findActiveEntriesByClaimKey(claimKey);
|
|
10209
|
+
cache.set(claimKey, siblings);
|
|
10210
|
+
return siblings;
|
|
10211
|
+
}
|
|
10212
|
+
function isAutoSupersessionEligible(preparedEntry, extractedClaimKeys, claimExtractionConfig) {
|
|
10213
|
+
if (preparedEntry.claimKeySource === "manual") {
|
|
10214
|
+
return true;
|
|
10215
|
+
}
|
|
10216
|
+
const extractedClaimKey = extractedClaimKeys.get(preparedEntry.inputIndex);
|
|
10217
|
+
if (!extractedClaimKey || !claimExtractionConfig) {
|
|
10218
|
+
return false;
|
|
10219
|
+
}
|
|
10220
|
+
if (!AUTO_SUPERSESSION_ELIGIBLE_PATHS.has(extractedClaimKey.path)) {
|
|
10221
|
+
return false;
|
|
10222
|
+
}
|
|
10223
|
+
return extractedClaimKey.confidence >= Math.max(claimExtractionConfig.confidenceThreshold, AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE);
|
|
10224
|
+
}
|
|
10225
|
+
function buildAutoSupersessionEligibilityWarning(preparedEntry, extractedClaimKey) {
|
|
10226
|
+
const claimKey = preparedEntry.input.claim_key ?? "(missing)";
|
|
10227
|
+
if (preparedEntry.claimKeySource === "manual") {
|
|
10228
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the claim-key provenance was not eligible for automatic linking.`;
|
|
10229
|
+
}
|
|
10230
|
+
if (extractedClaimKey) {
|
|
10231
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the extracted claim key came from ${extractedClaimKey.path} at confidence ${extractedClaimKey.confidence.toFixed(2)}. Only explicit/manual claim keys or model-extracted keys at ${AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE.toFixed(2)}+ auto-link.`;
|
|
10232
|
+
}
|
|
10233
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the claim-key provenance was not explicit or a tracked high-confidence extraction.`;
|
|
10234
|
+
}
|
|
10235
|
+
function buildAutoSupersessionRuleWarning(preparedEntry, sibling, reason) {
|
|
10236
|
+
if (reason === "type_mismatch") {
|
|
10237
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${preparedEntry.input.claim_key}" but skipped auto-supersession because the matching active entry is type "${sibling.type}" and the new entry is type "${preparedEntry.input.type}". ${describeSupersessionRuleFailure(reason)}`;
|
|
10238
|
+
}
|
|
10239
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${preparedEntry.input.claim_key}" but skipped auto-supersession: ${describeSupersessionRuleFailure(reason)}`;
|
|
9079
10240
|
}
|
|
9080
10241
|
async function buildStorePlan(inputs, db) {
|
|
9081
10242
|
const validation = validateEntriesWithIndexes(inputs);
|
|
@@ -9088,7 +10249,8 @@ async function buildStorePlan(inputs, db) {
|
|
|
9088
10249
|
input,
|
|
9089
10250
|
inputIndex,
|
|
9090
10251
|
contentHash: computeContentHash(input.content, input.source_file),
|
|
9091
|
-
normContentHash: computeNormContentHash(input.content)
|
|
10252
|
+
normContentHash: computeNormContentHash(input.content),
|
|
10253
|
+
claimKeySource: input.claim_key ? "manual" : void 0
|
|
9092
10254
|
}));
|
|
9093
10255
|
const afterBatchContentHash = dedupePreparedEntries(preparedEntries, "contentHash", "content_hash", details);
|
|
9094
10256
|
const existingHashes = await db.findExistingHashes(afterBatchContentHash.map((entry) => entry.contentHash));
|
|
@@ -9100,7 +10262,8 @@ async function buildStorePlan(inputs, db) {
|
|
|
9100
10262
|
pendingEntries,
|
|
9101
10263
|
skipped: details.filter((detail) => detail.outcome === "skipped").length,
|
|
9102
10264
|
rejected: validation.rejected,
|
|
9103
|
-
details
|
|
10265
|
+
details,
|
|
10266
|
+
warnings: validation.warnings
|
|
9104
10267
|
};
|
|
9105
10268
|
}
|
|
9106
10269
|
function dedupePreparedEntries(entries, field, reason, details) {
|
|
@@ -9156,11 +10319,11 @@ var STORE_TOOL_PARAMETERS = {
|
|
|
9156
10319
|
},
|
|
9157
10320
|
subject: {
|
|
9158
10321
|
type: "string",
|
|
9159
|
-
description: "Short subject line future recall can match. Name the
|
|
10322
|
+
description: "Short subject line future recall can match. Name the durable takeaway, person, system, rule, relationship, or milestone directly."
|
|
9160
10323
|
},
|
|
9161
10324
|
content: {
|
|
9162
10325
|
type: "string",
|
|
9163
|
-
description: "What a fresh session should remember. Store the durable takeaway, not
|
|
10326
|
+
description: "What a fresh session should remember. Store the durable takeaway, not the activity log, canonical record, or transient progress snapshot."
|
|
9164
10327
|
},
|
|
9165
10328
|
importance: {
|
|
9166
10329
|
type: "integer",
|
|
@@ -9205,7 +10368,7 @@ function createAgenrStoreTool(ctx, servicesPromise, logger) {
|
|
|
9205
10368
|
return {
|
|
9206
10369
|
name: "agenr_store",
|
|
9207
10370
|
label: "Agenr Store",
|
|
9208
|
-
description: "Store a new
|
|
10371
|
+
description: "Store a new durable memory entry in agenr. Apply the future-session test first: will a fresh future session make a better decision because this was stored, or are you just logging that something happened?\n\nIf another system is already the canonical record - such as version control, a task or ticket tracker, a calendar, a signed document, a chat or email thread, or a database/CRM - usually do not store that record here. Store only the durable takeaway: the standing implication, rule, lesson, preference, risk, or relationship.\n\nType guide: fact = durable truth about a person, system, place, or how something works. decision = a standing rule, constraint, policy, or chosen approach future sessions should follow. preference = what someone likes, wants, values, or wants avoided. lesson = a non-obvious takeaway learned from experience that should change future behavior. milestone = a rare one-time event with durable future significance, not ordinary execution progress. relationship = a meaningful durable connection between people, groups, or systems.\n\nUsually do not store: 'I merged PR #123.', 'I filed a ticket with support.', 'We had a meeting at 3 PM.', 'I sent the contract for signature.', 'We spent two hours debugging the outage.' Do store the takeaway instead: 'Always use the structured export path because raw sync corrupts timestamps.' 'Jim prefers text-first updates and dislikes surprise calls.' 'Service restarts fail unless config Y is enabled.' 'The office Wi-Fi name is Acorn-5G.'\n\nDo not use decision as a catch-all for important activity updates. Do not store plans, checklists, speculative future state, progress snapshots, session narration, or rephrased recalled material.\n\nWhen replacing an existing fact, pass `supersedes` with the old entry's ID. When storing a slot-like fact (for example, a library version or a rollout strategy), pass `claimKey` to enable future supersession detection.\n\nDo not ask before storing - but do ask whether future-you actually needs it.",
|
|
9209
10372
|
parameters: STORE_TOOL_PARAMETERS,
|
|
9210
10373
|
async execute(_toolCallId, rawParams) {
|
|
9211
10374
|
try {
|
|
@@ -9410,25 +10573,32 @@ function createAgenrUpdateTool(ctx, servicesPromise, logger) {
|
|
|
9410
10573
|
const subject = readStringParam6(params, "subject");
|
|
9411
10574
|
const importance = readNumberParam3(params, "importance", { integer: true, strict: true });
|
|
9412
10575
|
const expiry = parseExpiry(readStringParam6(params, "expiry"));
|
|
9413
|
-
const
|
|
10576
|
+
const claimKeyInput = readStringParam6(params, "claimKey");
|
|
9414
10577
|
const validFrom = readStringParam6(params, "validFrom");
|
|
9415
10578
|
const validTo = readStringParam6(params, "validTo");
|
|
10579
|
+
const normalizedClaimKey = claimKeyInput === void 0 ? void 0 : (() => {
|
|
10580
|
+
const claimKey = normalizeClaimKey(claimKeyInput);
|
|
10581
|
+
if (!claimKey.ok) {
|
|
10582
|
+
throw new Error("claimKey must use canonical entity/attribute format.");
|
|
10583
|
+
}
|
|
10584
|
+
return claimKey.value.claimKey;
|
|
10585
|
+
})();
|
|
9416
10586
|
logToolCall(
|
|
9417
10587
|
logger,
|
|
9418
10588
|
"agenr_update",
|
|
9419
10589
|
ctx,
|
|
9420
10590
|
`target=${formatTargetSelector(id, subject)}${importance !== void 0 ? ` importance=${importance}` : ""}${expiry !== void 0 ? ` expiry=${expiry}` : ""}`,
|
|
9421
|
-
sanitizeUpdateToolParams({ id, subject, importance, expiry, claimKey, validFrom, validTo })
|
|
10591
|
+
sanitizeUpdateToolParams({ id, subject, importance, expiry, claimKey: normalizedClaimKey, validFrom, validTo })
|
|
9422
10592
|
);
|
|
9423
10593
|
const services = await servicesPromise;
|
|
9424
10594
|
const entry = await resolveTargetEntry(services, params);
|
|
9425
|
-
if (importance === void 0 && expiry === void 0 &&
|
|
10595
|
+
if (importance === void 0 && expiry === void 0 && normalizedClaimKey === void 0 && validFrom === void 0 && validTo === void 0) {
|
|
9426
10596
|
throw new Error("Provide at least one update field: importance, expiry, claimKey, validFrom, or validTo.");
|
|
9427
10597
|
}
|
|
9428
10598
|
const updated = await services.entries.updateEntry(entry.id, {
|
|
9429
10599
|
...importance !== void 0 ? { importance } : {},
|
|
9430
10600
|
...expiry !== void 0 ? { expiry } : {},
|
|
9431
|
-
...
|
|
10601
|
+
...normalizedClaimKey !== void 0 ? { claim_key: normalizedClaimKey } : {},
|
|
9432
10602
|
...validFrom !== void 0 ? { valid_from: validFrom } : {},
|
|
9433
10603
|
...validTo !== void 0 ? { valid_to: validTo } : {}
|
|
9434
10604
|
});
|
|
@@ -9445,7 +10615,7 @@ function createAgenrUpdateTool(ctx, servicesPromise, logger) {
|
|
|
9445
10615
|
sessionKey: ctx.sessionKey,
|
|
9446
10616
|
...importance !== void 0 ? { importance } : {},
|
|
9447
10617
|
...expiry !== void 0 ? { expiry } : {},
|
|
9448
|
-
...
|
|
10618
|
+
...normalizedClaimKey !== void 0 ? { claimKey: normalizedClaimKey } : {},
|
|
9449
10619
|
...validFrom !== void 0 ? { validFrom } : {},
|
|
9450
10620
|
...validTo !== void 0 ? { validTo } : {}
|
|
9451
10621
|
});
|
|
@@ -9470,7 +10640,7 @@ function registerAgenrOpenClawTools(api, servicesPromise, logger) {
|
|
|
9470
10640
|
var openclaw_plugin_default = {
|
|
9471
10641
|
id: "agenr",
|
|
9472
10642
|
name: "agenr",
|
|
9473
|
-
version: "1.
|
|
10643
|
+
version: "1.7.0",
|
|
9474
10644
|
description: "agenr memory plugin for OpenClaw",
|
|
9475
10645
|
kind: "memory",
|
|
9476
10646
|
contracts: {
|
|
@@ -9723,7 +10893,7 @@ function buildAgenrMemoryPromptSection({
|
|
|
9723
10893
|
const lines = [
|
|
9724
10894
|
"## Memory Recall",
|
|
9725
10895
|
"Before answering anything about prior work, decisions, preferences, people, dates, unfinished work, or past sessions, call agenr_recall first. Session-start recall is automatic; use agenr_recall mid-session when you need context you do not already have.",
|
|
9726
|
-
"agenr_recall supports
|
|
10896
|
+
"agenr_recall supports exact fact recall plus historical and episodic recall behind one tool: use mode=entries for exact facts, decisions, thresholds, and versions; use mode=auto for prior-state questions like what was the previous approach, what did we use before, or what changed from X to Y; use mode=episodes when you explicitly want session narrative recall.",
|
|
9727
10897
|
"For temporal narrative questions, put the time phrase in the query itself: examples include yesterday, last week, this month, 2 weeks ago, or in March.",
|
|
9728
10898
|
"One focused agenr_recall call with the right scope beats several broad ones.",
|
|
9729
10899
|
"Memory authority, strongest to weakest:",
|
|
@@ -9735,7 +10905,20 @@ function buildAgenrMemoryPromptSection({
|
|
|
9735
10905
|
];
|
|
9736
10906
|
if (availableTools.has(MEMORY_TOOL_NAMES.store)) {
|
|
9737
10907
|
lines.push(
|
|
9738
|
-
"
|
|
10908
|
+
"Use agenr_store for durable memory, not for logging. Apply the future-session test: will a fresh future session make a better decision because this was stored, or are you just recording that something happened?"
|
|
10909
|
+
);
|
|
10910
|
+
lines.push(
|
|
10911
|
+
"If another system already holds the canonical record - such as version control, a task or ticket tracker, a calendar, a signed document, a chat or email thread, or a database/CRM - usually do not store that record. Store only the durable takeaway: the standing rule, implication, lesson, preference, risk, or relationship."
|
|
10912
|
+
);
|
|
10913
|
+
lines.push(
|
|
10914
|
+
"Type guide: fact = durable truth about a person, system, place, or how something works; decision = standing rule, constraint, policy, or chosen approach future sessions should follow; preference = what someone likes, wants, values, or wants avoided; lesson = non-obvious takeaway from experience that should change future behavior; milestone = rare one-time event with durable future significance, not ordinary task completion."
|
|
10915
|
+
);
|
|
10916
|
+
lines.push("Do not use decision as a catch-all for important activity updates.");
|
|
10917
|
+
lines.push(
|
|
10918
|
+
"Usually do not store: 'I merged PR #123.', 'I filed a support ticket.', 'We had a meeting at 3 PM.', 'I sent the contract for signature.', or 'We spent two hours debugging the outage.'"
|
|
10919
|
+
);
|
|
10920
|
+
lines.push(
|
|
10921
|
+
"Do store the durable takeaway instead: 'Always use the structured export path because raw sync corrupts timestamps.' (decision or lesson), 'Jim prefers text-first updates and dislikes surprise calls.' (preference), 'Service restarts fail unless config Y is enabled.' (lesson), 'The office Wi-Fi name is Acorn-5G.' (fact)."
|
|
9739
10922
|
);
|
|
9740
10923
|
lines.push("Do not store progress snapshots or current-state narration about what is happening right now as durable memory.");
|
|
9741
10924
|
lines.push("Do not store plans, checklists, or speculative future state as facts or decisions.");
|
|
@@ -10921,6 +12104,26 @@ function firstStringArgValue(args, max) {
|
|
|
10921
12104
|
}
|
|
10922
12105
|
return void 0;
|
|
10923
12106
|
}
|
|
12107
|
+
function extractAgenrStoreEntries(args) {
|
|
12108
|
+
const nestedEntries = Array.isArray(args.entries) ? args.entries.flatMap((entry) => {
|
|
12109
|
+
const record = asRecord3(entry);
|
|
12110
|
+
return record ? [record] : [];
|
|
12111
|
+
}) : [];
|
|
12112
|
+
if (nestedEntries.length > 0) {
|
|
12113
|
+
return nestedEntries;
|
|
12114
|
+
}
|
|
12115
|
+
if (getString(args.type) || getString(args.subject) || getString(args.content) || getString(args.claimKey) || getString(args.claim_key) || getString(args.supersedes)) {
|
|
12116
|
+
return [args];
|
|
12117
|
+
}
|
|
12118
|
+
return [];
|
|
12119
|
+
}
|
|
12120
|
+
function summarizeAgenrStoreEntry(entry) {
|
|
12121
|
+
const type = getString(entry.type) ?? "unknown";
|
|
12122
|
+
const subject = getString(entry.subject) ?? "(no subject)";
|
|
12123
|
+
const claimKey = getString(entry.claimKey) ?? getString(entry.claim_key);
|
|
12124
|
+
const claimKeySuffix = claimKey ? ` claim_key=${JSON.stringify(truncateInline(claimKey.trim(), 120))}` : "";
|
|
12125
|
+
return `${type}: "${truncateInline(subject, 60)}"${claimKeySuffix}`;
|
|
12126
|
+
}
|
|
10924
12127
|
function toolIdentifier(toolName, args) {
|
|
10925
12128
|
const normalizedToolName = toolName.trim().toLowerCase();
|
|
10926
12129
|
if (normalizedToolName === "read" || normalizedToolName === "edit" || normalizedToolName === "write") {
|
|
@@ -10942,7 +12145,7 @@ function toolIdentifier(toolName, args) {
|
|
|
10942
12145
|
return targetUrl ? `${action} ${targetUrl}` : action;
|
|
10943
12146
|
}
|
|
10944
12147
|
if (normalizedToolName === "agenr_store") {
|
|
10945
|
-
const entries =
|
|
12148
|
+
const entries = extractAgenrStoreEntries(args);
|
|
10946
12149
|
return `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`;
|
|
10947
12150
|
}
|
|
10948
12151
|
if (normalizedToolName === "agenr_recall") {
|
|
@@ -11037,19 +12240,11 @@ function summarizeToolCall(call, options) {
|
|
|
11037
12240
|
return `[called message: ${truncateInline(action, 200)} to ${truncateInline(target, 200)}]`;
|
|
11038
12241
|
}
|
|
11039
12242
|
if (normalizedToolName === "agenr_store") {
|
|
11040
|
-
const entries =
|
|
12243
|
+
const entries = extractAgenrStoreEntries(args);
|
|
11041
12244
|
if (entries.length === 0) {
|
|
11042
12245
|
return "[attempted brain store: (empty)]";
|
|
11043
12246
|
}
|
|
11044
|
-
const summaries = entries.slice(0, 3).map(
|
|
11045
|
-
const record = asRecord3(entry);
|
|
11046
|
-
if (!record) {
|
|
11047
|
-
return null;
|
|
11048
|
-
}
|
|
11049
|
-
const type = getString(record.type) ?? "unknown";
|
|
11050
|
-
const subject = getString(record.subject) ?? "(no subject)";
|
|
11051
|
-
return `${type}: "${truncateInline(subject, 60)}"`;
|
|
11052
|
-
}).filter((summary) => summary !== null);
|
|
12247
|
+
const summaries = entries.slice(0, 3).map(summarizeAgenrStoreEntry);
|
|
11053
12248
|
const countSuffix = entries.length > 3 ? ` (+${entries.length - 3} more)` : "";
|
|
11054
12249
|
return `[attempted brain store: ${summaries.join(", ")}${countSuffix}]`;
|
|
11055
12250
|
}
|
|
@@ -13489,8 +14684,9 @@ var AUTH_METHOD_DEFINITIONS = [
|
|
|
13489
14684
|
var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
|
|
13490
14685
|
var DEFAULT_CONFIG_DIR = path7.join(os2.homedir(), ".agenr");
|
|
13491
14686
|
var DEFAULT_DB_NAME = "knowledge.db";
|
|
14687
|
+
var DEFAULT_CLAIM_EXTRACTION_CONCURRENCY = 10;
|
|
13492
14688
|
var DEFAULT_CLAIM_EXTRACTION_CONFIDENCE_THRESHOLD = 0.8;
|
|
13493
|
-
var DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES = ["fact", "preference", "decision"];
|
|
14689
|
+
var DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES = ["fact", "preference", "decision", "lesson"];
|
|
13494
14690
|
function resolveConfigDir() {
|
|
13495
14691
|
return process.env.AGENR_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
13496
14692
|
}
|
|
@@ -13516,7 +14712,8 @@ function resolveClaimExtractionConfig(config) {
|
|
|
13516
14712
|
return {
|
|
13517
14713
|
enabled: config?.claimExtraction?.enabled ?? true,
|
|
13518
14714
|
confidenceThreshold: normalizeClaimExtractionConfidence(config?.claimExtraction?.confidenceThreshold),
|
|
13519
|
-
eligibleTypes: normalizeClaimExtractionEligibleTypes(config?.claimExtraction?.eligibleTypes)
|
|
14715
|
+
eligibleTypes: normalizeClaimExtractionEligibleTypes(config?.claimExtraction?.eligibleTypes),
|
|
14716
|
+
concurrency: normalizeClaimExtractionConcurrency(config?.claimExtraction?.concurrency)
|
|
13520
14717
|
};
|
|
13521
14718
|
}
|
|
13522
14719
|
function readConfig(options = {}) {
|
|
@@ -13590,6 +14787,13 @@ function normalizeClaimExtractionEligibleTypes(value) {
|
|
|
13590
14787
|
const normalized = Array.from(new Set(value.filter((candidate) => ENTRY_TYPES.includes(candidate))));
|
|
13591
14788
|
return normalized.length > 0 ? normalized : [...DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES];
|
|
13592
14789
|
}
|
|
14790
|
+
function normalizeClaimExtractionConcurrency(value) {
|
|
14791
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
14792
|
+
return DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
|
|
14793
|
+
}
|
|
14794
|
+
const normalized = Math.trunc(value);
|
|
14795
|
+
return normalized > 0 ? normalized : DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
|
|
14796
|
+
}
|
|
13593
14797
|
|
|
13594
14798
|
// ../../src/adapters/db/client.ts
|
|
13595
14799
|
import fs9 from "fs/promises";
|
|
@@ -14493,7 +15697,26 @@ async function getDistinctClaimKeyPrefixes(executor) {
|
|
|
14493
15697
|
return typeof prefix === "string" && prefix.length > 0 ? [prefix] : [];
|
|
14494
15698
|
});
|
|
14495
15699
|
}
|
|
14496
|
-
async function
|
|
15700
|
+
async function getClaimKeyExamples2(executor, limit = 8) {
|
|
15701
|
+
const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 8;
|
|
15702
|
+
const result = await executor.execute({
|
|
15703
|
+
sql: `
|
|
15704
|
+
SELECT claim_key
|
|
15705
|
+
FROM entries
|
|
15706
|
+
WHERE claim_key IS NOT NULL
|
|
15707
|
+
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
15708
|
+
GROUP BY claim_key
|
|
15709
|
+
ORDER BY COUNT(*) DESC, MAX(importance) DESC, MAX(created_at) DESC, claim_key ASC
|
|
15710
|
+
LIMIT ?
|
|
15711
|
+
`,
|
|
15712
|
+
args: [normalizedLimit]
|
|
15713
|
+
});
|
|
15714
|
+
return result.rows.flatMap((row) => {
|
|
15715
|
+
const claimKey = row.claim_key;
|
|
15716
|
+
return typeof claimKey === "string" && claimKey.length > 0 ? [claimKey] : [];
|
|
15717
|
+
});
|
|
15718
|
+
}
|
|
15719
|
+
async function updateEntry(executor, id, fields, options) {
|
|
14497
15720
|
const assignments = [];
|
|
14498
15721
|
const args = [];
|
|
14499
15722
|
if (fields.importance !== void 0) {
|
|
@@ -14527,7 +15750,7 @@ async function updateEntry(executor, id, fields) {
|
|
|
14527
15750
|
UPDATE entries
|
|
14528
15751
|
SET ${assignments.join(", ")}
|
|
14529
15752
|
WHERE id = ?
|
|
14530
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
15753
|
+
AND ${options?.includeInactive === true ? "1 = 1" : ACTIVE_ENTRY_CLAUSE}
|
|
14531
15754
|
`,
|
|
14532
15755
|
args
|
|
14533
15756
|
});
|
|
@@ -14629,7 +15852,7 @@ function normalizeInteger(value, fallback) {
|
|
|
14629
15852
|
}
|
|
14630
15853
|
|
|
14631
15854
|
// ../../src/adapters/db/schema.ts
|
|
14632
|
-
var SCHEMA_VERSION = "
|
|
15855
|
+
var SCHEMA_VERSION = "7";
|
|
14633
15856
|
var VECTOR_INDEX_NAME = "idx_entries_embedding";
|
|
14634
15857
|
var EPISODE_VECTOR_INDEX_NAME = "idx_episodes_embedding";
|
|
14635
15858
|
var BULK_WRITE_STATE_META_KEY = "bulk_write_state";
|
|
@@ -14778,6 +16001,7 @@ var CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL = `
|
|
|
14778
16001
|
entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
14779
16002
|
reasoning TEXT NOT NULL DEFAULT '',
|
|
14780
16003
|
recall_delta TEXT,
|
|
16004
|
+
details_json TEXT,
|
|
14781
16005
|
created_at TEXT NOT NULL
|
|
14782
16006
|
)
|
|
14783
16007
|
`;
|
|
@@ -14793,6 +16017,35 @@ var CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL = `
|
|
|
14793
16017
|
CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_created_at
|
|
14794
16018
|
ON surgeon_run_actions(created_at)
|
|
14795
16019
|
`;
|
|
16020
|
+
var CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL = `
|
|
16021
|
+
CREATE TABLE IF NOT EXISTS surgeon_run_proposals (
|
|
16022
|
+
id TEXT PRIMARY KEY,
|
|
16023
|
+
run_id TEXT NOT NULL REFERENCES surgeon_runs(id),
|
|
16024
|
+
group_id TEXT NOT NULL,
|
|
16025
|
+
issue_kind TEXT NOT NULL,
|
|
16026
|
+
scope TEXT NOT NULL,
|
|
16027
|
+
entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
16028
|
+
current_claim_keys TEXT NOT NULL DEFAULT '[]',
|
|
16029
|
+
proposed_claim_keys TEXT NOT NULL DEFAULT '[]',
|
|
16030
|
+
rationale TEXT NOT NULL DEFAULT '',
|
|
16031
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
16032
|
+
source TEXT NOT NULL DEFAULT '',
|
|
16033
|
+
eligible_for_apply INTEGER NOT NULL DEFAULT 0,
|
|
16034
|
+
created_at TEXT NOT NULL
|
|
16035
|
+
)
|
|
16036
|
+
`;
|
|
16037
|
+
var CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL = `
|
|
16038
|
+
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_run_id
|
|
16039
|
+
ON surgeon_run_proposals(run_id)
|
|
16040
|
+
`;
|
|
16041
|
+
var CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL = `
|
|
16042
|
+
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_group_id
|
|
16043
|
+
ON surgeon_run_proposals(group_id)
|
|
16044
|
+
`;
|
|
16045
|
+
var CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL = `
|
|
16046
|
+
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_created_at
|
|
16047
|
+
ON surgeon_run_proposals(created_at)
|
|
16048
|
+
`;
|
|
14796
16049
|
var CREATE_META_TABLE_SQL = `
|
|
14797
16050
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
14798
16051
|
key TEXT PRIMARY KEY,
|
|
@@ -14911,6 +16164,10 @@ var SCHEMA_STATEMENTS = [
|
|
|
14911
16164
|
CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL,
|
|
14912
16165
|
CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL,
|
|
14913
16166
|
CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL,
|
|
16167
|
+
CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL,
|
|
16168
|
+
CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL,
|
|
16169
|
+
CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL,
|
|
16170
|
+
CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL,
|
|
14914
16171
|
CREATE_META_TABLE_SQL,
|
|
14915
16172
|
CREATE_ENTRIES_CONTENT_HASH_INDEX_SQL,
|
|
14916
16173
|
CREATE_ENTRIES_NORM_CONTENT_HASH_INDEX_SQL,
|
|
@@ -14932,10 +16189,15 @@ var SCHEMA_STATEMENTS = [
|
|
|
14932
16189
|
];
|
|
14933
16190
|
async function initSchema(db) {
|
|
14934
16191
|
await db.execute("PRAGMA foreign_keys = ON");
|
|
14935
|
-
|
|
16192
|
+
let currentVersion = await getSchemaVersion(db);
|
|
14936
16193
|
await assertSupportedSchemaState(db, currentVersion);
|
|
14937
16194
|
if (currentVersion === "5") {
|
|
14938
16195
|
await migrateV5ToV6(db);
|
|
16196
|
+
currentVersion = "6";
|
|
16197
|
+
}
|
|
16198
|
+
if (currentVersion === "6") {
|
|
16199
|
+
await migrateV6ToV7(db);
|
|
16200
|
+
currentVersion = "7";
|
|
14939
16201
|
}
|
|
14940
16202
|
const hadEntriesFts = await tableExists(db, "entries_fts");
|
|
14941
16203
|
for (const statement of SCHEMA_STATEMENTS) {
|
|
@@ -14959,7 +16221,7 @@ async function initSchema(db) {
|
|
|
14959
16221
|
await ensureVectorIndexes(db);
|
|
14960
16222
|
}
|
|
14961
16223
|
async function assertSupportedSchemaState(db, currentVersion) {
|
|
14962
|
-
if (currentVersion && currentVersion !== "5" && currentVersion !== SCHEMA_VERSION) {
|
|
16224
|
+
if (currentVersion && currentVersion !== "5" && currentVersion !== "6" && currentVersion !== SCHEMA_VERSION) {
|
|
14963
16225
|
throw new Error(
|
|
14964
16226
|
`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.`
|
|
14965
16227
|
);
|
|
@@ -14987,6 +16249,21 @@ async function migrateV5ToV6(db) {
|
|
|
14987
16249
|
}
|
|
14988
16250
|
}
|
|
14989
16251
|
}
|
|
16252
|
+
async function migrateV6ToV7(db) {
|
|
16253
|
+
if (await tableExists(db, "surgeon_run_actions")) {
|
|
16254
|
+
try {
|
|
16255
|
+
await db.execute("ALTER TABLE surgeon_run_actions ADD COLUMN details_json TEXT");
|
|
16256
|
+
} catch (error) {
|
|
16257
|
+
if (!(error instanceof Error && /duplicate column/i.test(error.message))) {
|
|
16258
|
+
throw error;
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
}
|
|
16262
|
+
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL);
|
|
16263
|
+
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL);
|
|
16264
|
+
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL);
|
|
16265
|
+
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL);
|
|
16266
|
+
}
|
|
14990
16267
|
async function rebuildFts(db) {
|
|
14991
16268
|
await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
|
|
14992
16269
|
}
|
|
@@ -15204,6 +16481,10 @@ var LibsqlDatabase = class _LibsqlDatabase {
|
|
|
15204
16481
|
async getDistinctClaimKeyPrefixes() {
|
|
15205
16482
|
return getDistinctClaimKeyPrefixes(this.executor);
|
|
15206
16483
|
}
|
|
16484
|
+
/** Lists bounded full claim-key examples ordered for extraction hinting. */
|
|
16485
|
+
async getClaimKeyExamples(limit) {
|
|
16486
|
+
return getClaimKeyExamples2(this.executor, limit);
|
|
16487
|
+
}
|
|
15207
16488
|
/** Updates mutable entry fields such as importance, expiry, and temporal metadata. */
|
|
15208
16489
|
async updateEntry(id, fields) {
|
|
15209
16490
|
return updateEntry(this.executor, id, fields);
|
|
@@ -15529,9 +16810,14 @@ var RECALL_CANDIDATE_SELECT_COLUMNS = `
|
|
|
15529
16810
|
e.importance,
|
|
15530
16811
|
e.expiry,
|
|
15531
16812
|
e.embedding,
|
|
16813
|
+
e.superseded_by,
|
|
16814
|
+
e.claim_key,
|
|
16815
|
+
e.retired,
|
|
15532
16816
|
e.created_at
|
|
15533
16817
|
`;
|
|
15534
16818
|
var FTS_TIERS = ["exact", "all_tokens", "any_tokens"];
|
|
16819
|
+
var PREDECESSOR_EXPANSION_LIMIT_PER_SEED = 8;
|
|
16820
|
+
var PREDECESSOR_EXPANSION_MAX_RESULTS = 40;
|
|
15535
16821
|
function createRecallAdapter(executor, embeddingPort) {
|
|
15536
16822
|
return new LibsqlRecallAdapter(executor, embeddingPort);
|
|
15537
16823
|
}
|
|
@@ -15631,6 +16917,74 @@ var LibsqlRecallAdapter = class {
|
|
|
15631
16917
|
}
|
|
15632
16918
|
return Array.from(matches.values()).sort((left, right) => compareFtsCandidates(left, right)).slice(0, params.limit);
|
|
15633
16919
|
}
|
|
16920
|
+
/**
|
|
16921
|
+
* Finds historical predecessors scoped to a seed set of active candidate IDs.
|
|
16922
|
+
*
|
|
16923
|
+
* Direct supersession links are preferred. Same-claim-key siblings are used as
|
|
16924
|
+
* the structural lineage path, with retired same-subject entries preserved as
|
|
16925
|
+
* a weaker fallback when explicit slot identity is unavailable.
|
|
16926
|
+
*/
|
|
16927
|
+
async fetchPredecessors(params) {
|
|
16928
|
+
const normalizedIds = normalizeStrings(params.activeEntryIds);
|
|
16929
|
+
if (normalizedIds.length === 0) {
|
|
16930
|
+
return [];
|
|
16931
|
+
}
|
|
16932
|
+
const placeholders = normalizedIds.map(() => "?").join(", ");
|
|
16933
|
+
const expansionLimit = normalizePredecessorExpansionLimit(normalizedIds.length);
|
|
16934
|
+
const result = await this.executor.execute({
|
|
16935
|
+
sql: `
|
|
16936
|
+
WITH seed AS (
|
|
16937
|
+
SELECT id, subject, claim_key
|
|
16938
|
+
FROM entries
|
|
16939
|
+
WHERE id IN (${placeholders})
|
|
16940
|
+
),
|
|
16941
|
+
seed_subjects AS (
|
|
16942
|
+
SELECT DISTINCT subject
|
|
16943
|
+
FROM seed
|
|
16944
|
+
WHERE TRIM(subject) <> ''
|
|
16945
|
+
),
|
|
16946
|
+
seed_claim_keys AS (
|
|
16947
|
+
SELECT DISTINCT claim_key
|
|
16948
|
+
FROM seed
|
|
16949
|
+
WHERE claim_key IS NOT NULL
|
|
16950
|
+
),
|
|
16951
|
+
lineage AS (
|
|
16952
|
+
SELECT
|
|
16953
|
+
${RECALL_CANDIDATE_SELECT_COLUMNS},
|
|
16954
|
+
CASE
|
|
16955
|
+
WHEN e.superseded_by IN (SELECT id FROM seed) THEN 0
|
|
16956
|
+
WHEN e.claim_key IS NOT NULL
|
|
16957
|
+
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
|
|
16958
|
+
AND (e.retired = 1 OR e.superseded_by IS NOT NULL) THEN 1
|
|
16959
|
+
WHEN e.claim_key IS NOT NULL
|
|
16960
|
+
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys) THEN 2
|
|
16961
|
+
WHEN e.retired = 1
|
|
16962
|
+
AND e.subject IN (SELECT subject FROM seed_subjects) THEN 3
|
|
16963
|
+
ELSE 4
|
|
16964
|
+
END AS lineage_priority
|
|
16965
|
+
FROM entries AS e
|
|
16966
|
+
WHERE e.id NOT IN (SELECT id FROM seed)
|
|
16967
|
+
AND (
|
|
16968
|
+
e.superseded_by IN (SELECT id FROM seed)
|
|
16969
|
+
OR (
|
|
16970
|
+
e.claim_key IS NOT NULL
|
|
16971
|
+
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
|
|
16972
|
+
)
|
|
16973
|
+
OR (
|
|
16974
|
+
e.retired = 1
|
|
16975
|
+
AND e.subject IN (SELECT subject FROM seed_subjects)
|
|
16976
|
+
)
|
|
16977
|
+
)
|
|
16978
|
+
)
|
|
16979
|
+
SELECT *
|
|
16980
|
+
FROM lineage
|
|
16981
|
+
ORDER BY lineage_priority ASC, created_at ASC, id ASC
|
|
16982
|
+
LIMIT ?
|
|
16983
|
+
`,
|
|
16984
|
+
args: [...normalizedIds, expansionLimit]
|
|
16985
|
+
});
|
|
16986
|
+
return result.rows.map((row) => mapRecallCandidateRow(row));
|
|
16987
|
+
}
|
|
15634
16988
|
/** Hydrates full entries for the final ranked result set. */
|
|
15635
16989
|
async hydrateEntries(ids) {
|
|
15636
16990
|
const normalizedIds = normalizeStrings(ids);
|
|
@@ -15643,8 +16997,7 @@ var LibsqlRecallAdapter = class {
|
|
|
15643
16997
|
SELECT
|
|
15644
16998
|
${ENTRY_SELECT_COLUMNS2}
|
|
15645
16999
|
FROM entries AS e
|
|
15646
|
-
WHERE ${
|
|
15647
|
-
AND e.id IN (${placeholders})
|
|
17000
|
+
WHERE e.id IN (${placeholders})
|
|
15648
17001
|
`,
|
|
15649
17002
|
args: normalizedIds
|
|
15650
17003
|
});
|
|
@@ -15735,9 +17088,15 @@ function mapRecallCandidateRow(row) {
|
|
|
15735
17088
|
importance: readNumber(row, "importance", 0),
|
|
15736
17089
|
expiry,
|
|
15737
17090
|
embedding: readEmbedding(row, "embedding"),
|
|
17091
|
+
superseded_by: readOptionalString(row, "superseded_by"),
|
|
17092
|
+
claim_key: readOptionalString(row, "claim_key"),
|
|
17093
|
+
retired: readBoolean(row, "retired"),
|
|
15738
17094
|
created_at: readRequiredString(row, "created_at")
|
|
15739
17095
|
};
|
|
15740
17096
|
}
|
|
17097
|
+
function normalizePredecessorExpansionLimit(seedCount) {
|
|
17098
|
+
return Math.min(PREDECESSOR_EXPANSION_MAX_RESULTS, seedCount * PREDECESSOR_EXPANSION_LIMIT_PER_SEED);
|
|
17099
|
+
}
|
|
15741
17100
|
function wrapVectorError(error) {
|
|
15742
17101
|
const message = error instanceof Error ? error.message : String(error);
|
|
15743
17102
|
return new Error(`Vector search is unavailable: ${message}`);
|