@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 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 = Array.from(mergedCandidates.values()).map((candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, aroundDate, query.aroundRadius)).sort((left, right) => right.score - left.score);
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, aroundDate, aroundRadius) {
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 = aroundDate ? gaussianRecency(candidate.entry.created_at, aroundDate, normalizeAroundRadius(aroundRadius)) : recencyScore(candidate.entry.created_at, candidate.entry.expiry);
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 information about people, places, systems, or how things work. Use decision for standing rules, constraints, or chosen approaches. Use preference for stated wants, values, or opinions. Use lesson for non-obvious insights learned from specific experience. Use milestone for notable one-time events worth remembering (a move, a launch, a life change, a hire, a trip). Use relationship for meaningful connections between people, groups, or systems.";
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
- lines.push("Episode Matches");
8392
- if (result.episodes.length === 0) {
8393
- lines.push("None.");
8394
- } else {
8395
- for (const [index, episode] of result.episodes.entries()) {
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
- for (const [index, entry] of result.entries.entries()) {
8409
- lines.push(
8410
- `${index + 1}. ${entry.entry.id} | ${entry.entry.type} | ${entry.entry.subject} | score ${entry.score.toFixed(2)} | importance ${entry.entry.importance}`
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 entries and episodes, entries forces semantic recall, and episodes forces temporal session recall."
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, and mode=episodes for time-bounded 'what happened' questions. Time periods are parsed from the query text. Session-start recall is already handled automatically.",
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/store/claim-extraction.ts
8714
- var SELF_REFERENTIAL_ENTITIES = /* @__PURE__ */ new Set(["i", "me", "the_user", "myself", "user", "we", "our_team", "the_project", "this_project"]);
8715
- async function extractClaimKey(entry, entityHints, llm, config) {
8716
- if (!config.enabled || !config.eligibleTypes.includes(entry.type)) {
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
- const response = await llm.completeJson(buildClaimExtractionSystemPrompt(entityHints), buildClaimExtractionUserPrompt(entry));
8720
- if (response.no_claim === true) {
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 confidence = normalizeConfidence(response.confidence);
8724
- if (confidence < config.confidenceThreshold) {
9147
+ const normalizedPhrase = ["source", "of", "truth"];
9148
+ if (attributeTokens.length === normalizedPhrase.length && startsWithTokens(attributeTokens, normalizedPhrase)) {
8725
9149
  return null;
8726
9150
  }
8727
- const rawEntity = typeof response.entity === "string" ? response.entity.trim() : "";
8728
- const rawAttribute = typeof response.attribute === "string" ? response.attribute.trim() : "";
8729
- const entity = normalizeEntity(rawEntity, entityHints);
8730
- const attribute = normalizeClaimKeyPart(rawAttribute);
8731
- if (!entity || !attribute) {
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
- claimKey: `${entity}/${attribute}`,
8736
- confidence,
8737
- rawEntity,
8738
- rawAttribute
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 buildClaimExtractionSystemPrompt(entityHints) {
8745
- const normalizedHints = Array.from(new Set(entityHints.map((entityHint) => normalizeClaimKeyPart(entityHint)).filter((entityHint) => entityHint.length > 0)));
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 the claim key for a knowledge entry.",
8748
- "A claim key identifies the specific slot this fact occupies: entity/attribute in lowercase snake_case.",
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
- "Rules:",
8751
- "- entity: the primary noun this fact is about - the thing being described. Could be a person, project, system, tool, service, concept, dataset, organization, or any other identifiable noun.",
8752
- "- attribute: the specific property or aspect of that entity being stated. Should be narrow enough that two entries with the same entity/attribute are likely describing the same slot of knowledge.",
8753
- "- Format: entity/attribute (both lowercase snake_case)",
8754
- "- If the entry describes multiple unrelated facts, narrative content, or a vague opinion with no single dominant slot, set no_claim to true.",
8755
- "- Confidence: 0.0 to 1.0, how precisely this claim key captures the entry's single dominant slot. Use 0.9+ only when the slot is unambiguous.",
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
- `Known entities already in the knowledge base: ${normalizedHints.length > 0 ? normalizedHints.join(", ") : "(none)"}`,
8758
- `If the entry is clearly about one of these existing entities, use that name. Do not invent a new entity name when an existing one matches (e.g., don't create "react_router_v7" if "react_router" already exists and the entry is about React Router).`,
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, entityHints) {
8773
- const normalizedValue = normalizeClaimKeyPart(value);
9700
+ function normalizeEntity(value, hints) {
9701
+ const normalizedValue = normalizeClaimKeySegment(value);
8774
9702
  if (normalizedValue.length === 0) {
8775
9703
  return "";
8776
9704
  }
8777
- const normalizedHints = Array.from(new Set(entityHints.map((entityHint) => normalizeClaimKeyPart(entityHint)).filter((entityHint) => entityHint.length > 0)));
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 normalizeClaimKeyPart(value) {
8787
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
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: normalizeOptionalString(input.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) && (preparedEntries.length > 1 || preparedEntries.some((entry) => entry.input.supersedes !== void 0))) {
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
- entityHints = await getEntityHints(claimExtraction.db);
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
- options.onWarning?.(`Claim extraction hint lookup failed: ${formatPipelineError(error)}`);
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
- if (preparedEntry.input.claim_key) {
10138
+ const claimKey = preparedEntry.input.claim_key;
10139
+ if (!claimKey || preparedEntry.input.supersedes) {
9056
10140
  continue;
9057
10141
  }
9058
- try {
9059
- const extracted = await extractClaimKey(
9060
- {
9061
- type: preparedEntry.input.type,
9062
- subject: preparedEntry.input.subject,
9063
- content: preparedEntry.input.content
9064
- },
9065
- entityHints,
9066
- claimExtraction.llm,
9067
- claimExtraction.config
9068
- );
9069
- if (extracted?.claimKey) {
9070
- preparedEntry.input.claim_key = extracted.claimKey;
9071
- }
9072
- } catch (error) {
9073
- options.onWarning?.(`Claim extraction failed for "${preparedEntry.input.subject}": ${formatPipelineError(error)}`);
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 hasTransactionSupport(db) {
9078
- return typeof db.withTransaction === "function";
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 decision, preference, person, system, or risk directly."
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 a transient progress snapshot."
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 knowledge entry in agenr long-term memory. Call immediately after decisions, preferences, lessons, and durable facts - but apply the future-session test first: will a fresh session need this to make a better decision, or are you just logging what happened?\n\nStore: architecture decisions, workflow constraints, recurring problems, user preferences, durable technical facts, operational lessons, important open risks.\n\nDo not store: version shipping events (changelogs are the record), issue/PR filing records (the tracker is the record), phase plans or release sequencing (stale within a session), progress snapshots (stale within minutes), prompt file locations or build logistics.\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.",
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 claimKey = readStringParam6(params, "claimKey");
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 && claimKey === void 0 && validFrom === void 0 && validTo === 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
- ...claimKey !== void 0 ? { claim_key: claimKey } : {},
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
- ...claimKey !== void 0 ? { claimKey } : {},
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.6.0",
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 two recall kinds behind one tool: use mode=entries for exact facts, decisions, thresholds, and versions; use mode=episodes or auto for what-happened questions tied to a time period.",
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
- "Store decisions, preferences, lessons, durable facts, and important open risks with agenr_store immediately after they happen. Apply the future-session test: will a fresh session need this to make a better decision?"
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 = Array.isArray(args.entries) ? args.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 = Array.isArray(args.entries) ? args.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((entry) => {
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 updateEntry(executor, id, fields) {
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 = "6";
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
- const currentVersion = await getSchemaVersion(db);
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 ${buildActiveEntryClause("e")}
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}`);