hippo-memory 1.52.8 → 1.53.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/README.md +185 -101
- package/dist/agent-memories/apply.d.ts +47 -0
- package/dist/agent-memories/apply.js +253 -0
- package/dist/agent-memories/claude-code.d.ts +11 -0
- package/dist/agent-memories/claude-code.js +113 -0
- package/dist/agent-memories/codex.d.ts +3 -0
- package/dist/agent-memories/codex.js +47 -0
- package/dist/agent-memories/copilot.d.ts +3 -0
- package/dist/agent-memories/copilot.js +125 -0
- package/dist/agent-memories/files.d.ts +37 -0
- package/dist/agent-memories/files.js +77 -0
- package/dist/agent-memories/folder-store.d.ts +17 -0
- package/dist/agent-memories/folder-store.js +44 -0
- package/dist/agent-memories/gemini.d.ts +3 -0
- package/dist/agent-memories/gemini.js +103 -0
- package/dist/agent-memories/git.d.ts +8 -0
- package/dist/agent-memories/git.js +11 -0
- package/dist/agent-memories/keys.d.ts +9 -0
- package/dist/agent-memories/keys.js +20 -0
- package/dist/agent-memories/legacy.d.ts +17 -0
- package/dist/agent-memories/legacy.js +45 -0
- package/dist/agent-memories/markdown.d.ts +13 -0
- package/dist/agent-memories/markdown.js +123 -0
- package/dist/agent-memories/openclaw.d.ts +3 -0
- package/dist/agent-memories/openclaw.js +42 -0
- package/dist/agent-memories/plan.d.ts +78 -0
- package/dist/agent-memories/plan.js +123 -0
- package/dist/agent-memories/qwen-code.d.ts +5 -0
- package/dist/agent-memories/qwen-code.js +50 -0
- package/dist/agent-memories/report.d.ts +52 -0
- package/dist/agent-memories/report.js +88 -0
- package/dist/agent-memories/source.d.ts +16 -0
- package/dist/agent-memories/source.js +32 -0
- package/dist/agent-memories/sync.d.ts +33 -0
- package/dist/agent-memories/sync.js +336 -0
- package/dist/agent-memories/tools.d.ts +33 -0
- package/dist/agent-memories/tools.js +19 -0
- package/dist/agent-memories/types.d.ts +42 -0
- package/dist/agent-memories/types.js +2 -0
- package/dist/api.d.ts +53 -20
- package/dist/api.js +141 -97
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +68 -2
- package/dist/capture.d.ts +48 -22
- package/dist/capture.js +186 -161
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +750 -797
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/compaction-items.d.ts +18 -0
- package/dist/compaction-items.js +60 -0
- package/dist/compaction-record.d.ts +94 -0
- package/dist/compaction-record.js +546 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +13 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +87 -34
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/db.d.ts +5 -1
- package/dist/db.js +46 -14
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +69 -4
- package/dist/dormant.d.ts +9 -3
- package/dist/dormant.js +26 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +5 -3
- package/dist/hooks.d.ts +18 -2
- package/dist/hooks.js +128 -32
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +35 -3
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +44 -36
- package/dist/store.d.ts +9 -2
- package/dist/store.js +25 -2
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,8 @@ import { loadAllEntries, writeEntry, strengthenRetrieved, readEntry, loadFreshAc
|
|
|
16
16
|
import { shareMemory, listPeers, getGlobalRoot, initGlobal } from '../shared.js';
|
|
17
17
|
import { consolidate } from '../consolidate.js';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
|
-
import { fetchGitLog, extractLessons, partitionLessons,
|
|
19
|
+
import { fetchGitLog, extractLessons, partitionLessons, isGitRepo } from '../autolearn.js';
|
|
20
|
+
import { dropHeldCopies, duplicateKey, storedTextKeys } from '../same-text.js';
|
|
20
21
|
import { loadConfig } from '../config.js';
|
|
21
22
|
import { confidenceLabel } from '../memory.js';
|
|
22
23
|
import { resolveTenantId } from '../tenant.js';
|
|
@@ -74,6 +75,7 @@ function isJsonObjectRecord(v) {
|
|
|
74
75
|
return v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
75
76
|
}
|
|
76
77
|
import { formatHandoffEvidenceLine } from '../handoff.js';
|
|
78
|
+
import { assembleCost, assembleText, drillCost, drillText, printedTokens } from '../context-render.js';
|
|
77
79
|
function formatContinuityBlock(block) {
|
|
78
80
|
const lines = ['## Continuity'];
|
|
79
81
|
if (block.activeSnapshot) {
|
|
@@ -123,19 +125,54 @@ function formatContinuityBlock(block) {
|
|
|
123
125
|
}
|
|
124
126
|
return lines.join('\n');
|
|
125
127
|
}
|
|
126
|
-
|
|
128
|
+
const NO_MEMORIES = 'No relevant memories found.';
|
|
129
|
+
function memoriesHeading(count) {
|
|
130
|
+
return `Found ${count} memories:\n`;
|
|
131
|
+
}
|
|
132
|
+
function formatMemory(r) {
|
|
133
|
+
const conf = confidenceLabel(r.entry).text;
|
|
134
|
+
const tags = r.entry.tags.length > 0 ? ` tags: ${r.entry.tags.join(', ')}` : '';
|
|
135
|
+
return `[${conf}]${tags} (strength=${r.entry.strength.toFixed(2)})\n${r.entry.content}\n`;
|
|
136
|
+
}
|
|
137
|
+
function formatMemories(results) {
|
|
127
138
|
if (results.length === 0)
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
return NO_MEMORIES;
|
|
140
|
+
return [memoriesHeading(results.length), ...results.map(formatMemory)].join('\n');
|
|
141
|
+
}
|
|
142
|
+
/** What a memory costs the budget: the text formatMemories prints for it. */
|
|
143
|
+
const memoryCost = (r) => printedTokens(formatMemory(r));
|
|
144
|
+
// The widest heading or the empty-list line, whichever costs more, so either prints inside the budget.
|
|
145
|
+
function memoriesReserve(budget) {
|
|
146
|
+
return Math.max(printedTokens(memoriesHeading(budget)), estimateTokens(NO_MEMORIES));
|
|
147
|
+
}
|
|
148
|
+
// Rows the ranked list already shows drop out of this section, so pricing every row bounds what it prints.
|
|
149
|
+
function tailSection(rows) {
|
|
150
|
+
if (rows.length === 0)
|
|
151
|
+
return '';
|
|
152
|
+
const lines = ['', '## Fresh tail / substituted summaries'];
|
|
153
|
+
for (const r of rows) {
|
|
154
|
+
const tag = r.isSummary ? '[summary]' : '[tail]';
|
|
155
|
+
const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
|
|
156
|
+
if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
|
|
157
|
+
lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
lines.push(`- ${tag} ${r.id}: ${head}`);
|
|
161
|
+
}
|
|
137
162
|
}
|
|
138
|
-
return lines.join('\n');
|
|
163
|
+
return '\n' + lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
// J3.2: the hint depends on the query alone, so api.recall's copy is the one shown; JSON.stringify fences the phrase.
|
|
166
|
+
function planningSection(r) {
|
|
167
|
+
if (r.planningFallacyHint) {
|
|
168
|
+
const h = r.planningFallacyHint;
|
|
169
|
+
return `## Planning fallacy hint\nClass: ${h.classTag}\n${h.baserateSummary}\n(detected: ${JSON.stringify(h.detectedPhrase)})\n\n---\n\n`;
|
|
170
|
+
}
|
|
171
|
+
if (r.planningFallacyWatching) {
|
|
172
|
+
const w = r.planningFallacyWatching;
|
|
173
|
+
return `## Planning fallacy watch\nReason: ${w.reason}\n${w.suggestion}\n(detected: ${JSON.stringify(w.detectedPhrase)})\n\n---\n\n`;
|
|
174
|
+
}
|
|
175
|
+
return '';
|
|
139
176
|
}
|
|
140
177
|
// ── Tool definitions ──
|
|
141
178
|
const TOOLS = [
|
|
@@ -512,6 +549,7 @@ async function executeTool(name, args, ctx) {
|
|
|
512
549
|
// never actually saw. Real MCP tracing is the reserved 'mcp'
|
|
513
550
|
// pipeline value (schema v40) — a follow-up, not v1 scope.
|
|
514
551
|
suppressRecallTrace: true,
|
|
552
|
+
keepHeldCopies: true,
|
|
515
553
|
...recallExtra,
|
|
516
554
|
});
|
|
517
555
|
// Existing physics/hybrid scorer continues to drive user-visible
|
|
@@ -532,10 +570,26 @@ async function executeTool(name, args, ctx) {
|
|
|
532
570
|
? allEntries.filter((e) => e.scope === explicitScope)
|
|
533
571
|
: allEntries.filter((e) => passesScopeFilterForRecall(e.scope ?? null, undefined));
|
|
534
572
|
const droppedPreRankCountMcp = allEntries.length - entries.length;
|
|
573
|
+
// Sections are paid in print order, ahead of the memories and after the heading; one that does not fit is dropped whole.
|
|
574
|
+
let left = budget - memoriesReserve(budget);
|
|
575
|
+
const pays = (piece) => {
|
|
576
|
+
const tokens = estimateTokens(piece);
|
|
577
|
+
if (tokens > left)
|
|
578
|
+
return false;
|
|
579
|
+
left -= tokens;
|
|
580
|
+
return true;
|
|
581
|
+
};
|
|
582
|
+
const planPiece = planningSection(apiResult);
|
|
583
|
+
const showPlan = planPiece !== '' && pays(planPiece);
|
|
584
|
+
const tailRows = apiResult.results.filter((r) => r.isFreshTail || r.isSummary);
|
|
585
|
+
const showTail = tailRows.length > 0 && pays(tailSection(tailRows));
|
|
586
|
+
const continuityPiece = includeContinuity && apiResult.continuity ? `\n\n${formatContinuityBlock(apiResult.continuity)}` : '';
|
|
587
|
+
const showContinuity = continuityPiece !== '' && pays(continuityPiece);
|
|
535
588
|
const usePhysics = config.physics?.enabled !== false;
|
|
589
|
+
const fit = { budget: Math.max(0, left), cost: memoryCost, hippoRoot };
|
|
536
590
|
let results = usePhysics
|
|
537
|
-
? await physicsSearch(query, entries, {
|
|
538
|
-
: await hybridSearch(query, entries,
|
|
591
|
+
? await physicsSearch(query, entries, { ...fit, physicsConfig: config.physics })
|
|
592
|
+
: await hybridSearch(query, entries, fit);
|
|
539
593
|
// v1.12.13 / C5 — droppedByBudget for MCP is an UPPER BOUND. The
|
|
540
594
|
// difference (entries.length - results.length) lumps three things
|
|
541
595
|
// together: rows hybridSearch/physicsSearch internally dropped because
|
|
@@ -553,7 +607,7 @@ async function executeTool(name, args, ctx) {
|
|
|
553
607
|
// compute droppedByBudget = scoredCount - results.length, with the
|
|
554
608
|
// remainder (entries.length - scoredCount) attributed to
|
|
555
609
|
// droppedPreRank or a new "noQueryMatch" counter.
|
|
556
|
-
const
|
|
610
|
+
const droppedByBudgetFor = (shown) => Math.max(0, entries.length - shown);
|
|
557
611
|
// v1.7.4 -- dlPFC goal-stack boost on the MCP physics/hybrid result
|
|
558
612
|
// list BEFORE formatMemories. MCP's user-visible primary ordering does
|
|
559
613
|
// NOT come from api.recall (apiResult above), so the boost has to run
|
|
@@ -572,26 +626,71 @@ async function executeTool(name, args, ctx) {
|
|
|
572
626
|
closeHippoDb(dbForBoost);
|
|
573
627
|
}
|
|
574
628
|
}
|
|
575
|
-
|
|
629
|
+
// J1, J2 and C5: MCP ranks its own list (its top-1 can differ from api.recall's), so its hints and Cutoff block are its own.
|
|
630
|
+
const anchorRing = process.env.HIPPO_ANCHORING !== 'off' && sessionId
|
|
631
|
+
? getOrCreateRing(sessionRecallHistoryMcp, buildSessionKey(tenantId, sessionId))
|
|
632
|
+
: null;
|
|
633
|
+
const queryHash = hashQueryText(query);
|
|
634
|
+
const render = (cut) => {
|
|
635
|
+
const list = dropHeldCopies(cut, (r) => r.entry); // after every cut, so a merged row cut here never hides its sources
|
|
636
|
+
const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
|
|
637
|
+
const availability = process.env.HIPPO_AVAILABILITY !== 'off'
|
|
638
|
+
? detectAvailabilityBias({
|
|
639
|
+
topK: list.map((r) => ({ id: r.entry.id, created: r.entry.created })),
|
|
640
|
+
pool: entries.map((e) => ({ id: e.id, created: e.created })),
|
|
641
|
+
})
|
|
642
|
+
: null;
|
|
643
|
+
const shownIds = new Set(list.map((r) => r.entry.id));
|
|
644
|
+
const shownKeys = storedTextKeys(list.map((r) => r.entry));
|
|
645
|
+
const tail = showTail
|
|
646
|
+
? dropHeldCopies(tailRows.filter((r) => !shownIds.has(r.id) && !shownKeys.has(duplicateKey(r.content))), (r) => r)
|
|
647
|
+
: [];
|
|
648
|
+
const s = buildSuppressionSummary({
|
|
649
|
+
totalCandidates: totalCandidatesCountMcp,
|
|
650
|
+
droppedPreRank: droppedPreRankCountMcp + cut.length - list.length, // the bucket CLI and API recall put hidden copies in
|
|
651
|
+
droppedByBudget: droppedByBudgetFor(cut.length),
|
|
652
|
+
summarySubstitutionsAdded: tail.filter((r) => r.isSummary).length,
|
|
653
|
+
freshTailAdded: tail.filter((r) => r.isFreshTail && !r.isSummary).length,
|
|
654
|
+
suppressedByInterference: anchoring?.reason === 'memory_dominance' ? 1 : 0,
|
|
655
|
+
});
|
|
656
|
+
// Anchoring is the stronger pull, so it prints first; the Cutoff block sits above the list, where the agent reads it.
|
|
657
|
+
let text = anchoring ? `## Anchoring hint\n${anchoring.summary}\n[anchored_on: ${anchoring.memoryId}]\n\n---\n\n` : '';
|
|
658
|
+
if (availability)
|
|
659
|
+
text += `## Availability bias\n${availability.summary}\n\n---\n\n`;
|
|
660
|
+
if (showPlan)
|
|
661
|
+
text += planPiece;
|
|
662
|
+
const cutoffClauses = [];
|
|
663
|
+
if (s.droppedByBudget > 0)
|
|
664
|
+
cutoffClauses.push(`${s.droppedByBudget} dropped to fit limit`);
|
|
665
|
+
if (s.droppedPreRank > 0)
|
|
666
|
+
cutoffClauses.push(`${s.droppedPreRank} filtered pre-rank`);
|
|
667
|
+
if (s.summarySubstitutionsAdded > 0)
|
|
668
|
+
cutoffClauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
|
|
669
|
+
if (s.freshTailAdded > 0)
|
|
670
|
+
cutoffClauses.push(`${s.freshTailAdded} fresh-tail added`);
|
|
671
|
+
if (s.suppressedByInterference > 0)
|
|
672
|
+
cutoffClauses.push(`${s.suppressedByInterference} suppressed by interference`);
|
|
673
|
+
if (cutoffClauses.length > 0) {
|
|
674
|
+
text += `## Cutoff\nShowing ${list.length} of ${s.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n\n---\n\n`;
|
|
675
|
+
}
|
|
676
|
+
// v1.6.3: the fresh-tail and summary rows api.recall produced follow the ranked list, or the MCP fields go unanswered.
|
|
677
|
+
text += formatMemories(list) + tailSection(tail) + (showContinuity ? continuityPiece : '');
|
|
678
|
+
return { anchoring, availability, text, list };
|
|
679
|
+
};
|
|
680
|
+
let rendered = render(results);
|
|
681
|
+
// The hints, Cutoff block and heading vary with the list, so the lowest-ranked entry goes until the whole response fits.
|
|
682
|
+
while (results.length > 1 && estimateTokens(rendered.text) > budget) {
|
|
683
|
+
results = results.slice(0, -1);
|
|
684
|
+
rendered = render(results);
|
|
685
|
+
}
|
|
686
|
+
const { anchoring: mcpAnchoringHint, availability: mcpAvailabilityHint, list: shown } = rendered;
|
|
687
|
+
const retrievedIds = shown.map((r) => r.entry.id);
|
|
576
688
|
strengthenRetrieved(hippoRoot, retrievedIds);
|
|
577
689
|
lastRecalledIds.set(resolveClientKey(ctx), retrievedIds);
|
|
578
|
-
// v0.33 / J1 — MCP per-pipeline anchoring detector. UNLIKE J3.2's
|
|
579
|
-
// planningFallacyHint (which is pipeline-invariant because it
|
|
580
|
-
// depends only on queryText + predictions table state), the
|
|
581
|
-
// anchoring hint depends on (a) per-pipeline top-1 ranking (MCP's
|
|
582
|
-
// physics/hybrid winner can differ from api.recall's BM25 winner)
|
|
583
|
-
// and (b) per-pipeline ring buffer. So MCP computes its OWN hint
|
|
584
|
-
// against MCP's own top-1, mirroring the C5 per-pipeline rule.
|
|
585
|
-
let mcpAnchoringHint = null;
|
|
586
690
|
if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const queryHash = hashQueryText(query);
|
|
591
|
-
const topId = results[0]?.entry.id ?? null;
|
|
592
|
-
mcpAnchoringHint = detectAnchoring(snapshotRing(ring), queryHash, topId);
|
|
593
|
-
appendRecall(ring, queryHash, topId, mcpAnchoringHint?.memoryId);
|
|
594
|
-
// Pipeline-local audit emission (lockstep with CLI / api.recall).
|
|
691
|
+
if (anchorRing) {
|
|
692
|
+
// Appended after the final detect: anchoredOn feeds the cooldown for the next recall on this session.
|
|
693
|
+
appendRecall(anchorRing, queryHash, shown[0]?.entry.id ?? null, mcpAnchoringHint?.memoryId);
|
|
595
694
|
if (mcpAnchoringHint?.reason === 'memory_dominance') {
|
|
596
695
|
const dbForAudit = openHippoDb(hippoRoot);
|
|
597
696
|
try {
|
|
@@ -650,160 +749,25 @@ async function executeTool(name, args, ctx) {
|
|
|
650
749
|
}
|
|
651
750
|
}
|
|
652
751
|
}
|
|
653
|
-
// v0.32 / J3.2 — auto-injection of reference-class baserate hint
|
|
654
|
-
// when the query carries a forward-prediction phrase. Read from
|
|
655
|
-
// apiResult.planningFallacyHint (already computed inside api.recall
|
|
656
|
-
// with the caller identity threaded through ctx.actor.subject -
|
|
657
|
-
// auth-resolved actor under HTTP-MCP, 'mcp' for stdio). The hint is
|
|
658
|
-
// pipeline-INVARIANT — same (hippoRoot, tenantId, query) inputs
|
|
659
|
-
// produce the same hint regardless of which downstream search
|
|
660
|
-
// pipeline (api.recall band vs physics/hybrid) renders the memory
|
|
661
|
-
// list, so re-computing here would double the audit emission for
|
|
662
|
-
// identical telemetry. C5 per-pipeline rule does NOT apply here
|
|
663
|
-
// because the hint depends on queryText, not on the matched memory
|
|
664
|
-
// set. Prepend BEFORE the memory list so the agent sees it first.
|
|
665
|
-
// v0.33 / J1: Anchoring hint goes ABOVE planning-fallacy hint
|
|
666
|
-
// (anchoring is the stronger cognitive-pull warning).
|
|
667
|
-
// v1.13.3 / C5 follow-up — Build MCP-pipeline suppressionSummary BEFORE
|
|
668
|
-
// the response is assembled so the Cutoff block can render at TOP
|
|
669
|
-
// alongside the other Track J hints. The dogfood
|
|
670
|
-
// (docs/dogfood/2026-05-27-track-j-warnings.md) showed the v1.13.0-v1.13.2
|
|
671
|
-
// bottom-placement was dark: a fresh sub-agent summarised the visible
|
|
672
|
-
// memories with zero mention of the dropped pool. Top-placement + plain-
|
|
673
|
-
// English rewrite fixes the read-rate without any system-prompt addendum.
|
|
674
|
-
const physicsIds = new Set(results.map((r) => r.entry.id));
|
|
675
|
-
const tailOrSummary = apiResult.results.filter((r) => (r.isFreshTail || r.isSummary) && !physicsIds.has(r.id));
|
|
676
|
-
const freshTailAddedMcp = tailOrSummary.filter((r) => r.isFreshTail && !r.isSummary).length;
|
|
677
|
-
const summarySubsAddedMcp = tailOrSummary.filter((r) => r.isSummary).length;
|
|
678
|
-
// v0.33 / J1: suppressedByInterference bumped on MCP's R2 fire.
|
|
679
|
-
const mcpSuppressedByInterference = mcpAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
|
|
680
|
-
const mcpSuppressionSummary = buildSuppressionSummary({
|
|
681
|
-
totalCandidates: totalCandidatesCountMcp,
|
|
682
|
-
droppedPreRank: droppedPreRankCountMcp,
|
|
683
|
-
droppedByBudget: droppedByBudgetCountMcp,
|
|
684
|
-
summarySubstitutionsAdded: summarySubsAddedMcp,
|
|
685
|
-
freshTailAdded: freshTailAddedMcp,
|
|
686
|
-
suppressedByInterference: mcpSuppressedByInterference,
|
|
687
|
-
});
|
|
688
|
-
// v1.13.x / J2 — MCP per-pipeline availability/recency-bias detector.
|
|
689
|
-
// Like the anchoring hint above (and unlike J3.2's pipeline-invariant
|
|
690
|
-
// planningFallacyHint), this depends on MCP's OWN returned top-K and the
|
|
691
|
-
// scope-filtered candidate pool (entries) it was drawn from, so MCP
|
|
692
|
-
// computes its own hint here. Soft warning only. Gated by
|
|
693
|
-
// HIPPO_AVAILABILITY=off; audit emission is pipeline-local (actor =
|
|
694
|
-
// auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio).
|
|
695
|
-
let mcpAvailabilityHint = null;
|
|
696
|
-
if (process.env.HIPPO_AVAILABILITY !== 'off') {
|
|
697
|
-
mcpAvailabilityHint = detectAvailabilityBias({
|
|
698
|
-
topK: results.map((r) => ({ id: r.entry.id, created: r.entry.created })),
|
|
699
|
-
pool: entries.map((e) => ({ id: e.id, created: e.created })),
|
|
700
|
-
});
|
|
701
|
-
if (mcpAvailabilityHint) {
|
|
702
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
703
|
-
try {
|
|
704
|
-
appendAuditEvent(dbForAudit, {
|
|
705
|
-
tenantId,
|
|
706
|
-
actor: ctx?.actor ?? 'mcp',
|
|
707
|
-
op: 'recall_availability_detected',
|
|
708
|
-
metadata: {
|
|
709
|
-
recent_fraction: mcpAvailabilityHint.recentFraction,
|
|
710
|
-
older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
|
|
711
|
-
returned_count: mcpAvailabilityHint.returnedCount,
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
finally {
|
|
716
|
-
closeHippoDb(dbForAudit);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
let response = '';
|
|
721
|
-
if (mcpAnchoringHint) {
|
|
722
|
-
response =
|
|
723
|
-
`## Anchoring hint\n` +
|
|
724
|
-
`${mcpAnchoringHint.summary}\n` +
|
|
725
|
-
`[anchored_on: ${mcpAnchoringHint.memoryId}]\n` +
|
|
726
|
-
`\n---\n\n`;
|
|
727
|
-
}
|
|
728
|
-
// v1.13.x / J2 — availability/recency-bias hint, rendered below the
|
|
729
|
-
// anchoring hint and above the planning-fallacy hint. Soft warning only.
|
|
730
752
|
if (mcpAvailabilityHint) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
// (no_class_match / tiebreak). Mutually exclusive with the hint
|
|
747
|
-
// block above. Suggestion text directs the user toward an action
|
|
748
|
-
// (typically: tag a prediction class) that would unblock the
|
|
749
|
-
// hint next time.
|
|
750
|
-
const w = apiResult.planningFallacyWatching;
|
|
751
|
-
const safePhrase = JSON.stringify(w.detectedPhrase);
|
|
752
|
-
response +=
|
|
753
|
-
`## Planning fallacy watch\n` +
|
|
754
|
-
`Reason: ${w.reason}\n` +
|
|
755
|
-
`${w.suggestion}\n` +
|
|
756
|
-
`(detected: ${safePhrase})\n` +
|
|
757
|
-
`\n---\n\n`;
|
|
758
|
-
}
|
|
759
|
-
// v1.13.3 / C5 follow-up — Cutoff block (was "WYSIATI:" line at bottom
|
|
760
|
-
// in v1.13.0-v1.13.2). Top placement so the agent reads the cutoff
|
|
761
|
-
// before scrolling the result list. "Cutoff" is plain English; the old
|
|
762
|
-
// "WYSIATI:" acronym was opaque to agents without Kahneman context per
|
|
763
|
-
// the 2026-05-27 dogfood Trial 1.
|
|
764
|
-
const sMcp = mcpSuppressionSummary;
|
|
765
|
-
const cutoffClauses = [];
|
|
766
|
-
if (sMcp.droppedByBudget > 0)
|
|
767
|
-
cutoffClauses.push(`${sMcp.droppedByBudget} dropped to fit limit`);
|
|
768
|
-
if (sMcp.droppedPreRank > 0)
|
|
769
|
-
cutoffClauses.push(`${sMcp.droppedPreRank} filtered pre-rank`);
|
|
770
|
-
if (sMcp.summarySubstitutionsAdded > 0)
|
|
771
|
-
cutoffClauses.push(`${sMcp.summarySubstitutionsAdded} summary substitutions added`);
|
|
772
|
-
if (sMcp.freshTailAdded > 0)
|
|
773
|
-
cutoffClauses.push(`${sMcp.freshTailAdded} fresh-tail added`);
|
|
774
|
-
if (sMcp.suppressedByInterference > 0)
|
|
775
|
-
cutoffClauses.push(`${sMcp.suppressedByInterference} suppressed by interference`);
|
|
776
|
-
if (cutoffClauses.length > 0) {
|
|
777
|
-
response +=
|
|
778
|
-
`## Cutoff\n` +
|
|
779
|
-
`Showing ${results.length} of ${sMcp.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n` +
|
|
780
|
-
`\n---\n\n`;
|
|
781
|
-
}
|
|
782
|
-
response += formatMemories(results, hippoRoot);
|
|
783
|
-
// v1.6.3 codex P2 fix. The physics/hybrid scorer drives the primary
|
|
784
|
-
// ranked block above, so user-visible ordering is preserved. But
|
|
785
|
-
// when the v1.5.0+/v1.5.2 RecallOpts are passed, we MUST also surface
|
|
786
|
-
// the fresh-tail and substituted-summary items apiRecall produced —
|
|
787
|
-
// otherwise the advertised MCP fields are silently ignored. Append
|
|
788
|
-
// them as their own section, deduplicated against the physics ranking.
|
|
789
|
-
if (tailOrSummary.length > 0) {
|
|
790
|
-
const lines = ['', '## Fresh tail / substituted summaries'];
|
|
791
|
-
for (const r of tailOrSummary) {
|
|
792
|
-
const tag = r.isSummary ? '[summary]' : '[tail]';
|
|
793
|
-
const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
|
|
794
|
-
if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
|
|
795
|
-
lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
lines.push(`- ${tag} ${r.id}: ${head}`);
|
|
799
|
-
}
|
|
753
|
+
const dbForAudit = openHippoDb(hippoRoot);
|
|
754
|
+
try {
|
|
755
|
+
appendAuditEvent(dbForAudit, {
|
|
756
|
+
tenantId,
|
|
757
|
+
actor: ctx?.actor ?? 'mcp',
|
|
758
|
+
op: 'recall_availability_detected',
|
|
759
|
+
metadata: {
|
|
760
|
+
recent_fraction: mcpAvailabilityHint.recentFraction,
|
|
761
|
+
older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
|
|
762
|
+
returned_count: mcpAvailabilityHint.returnedCount,
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
closeHippoDb(dbForAudit);
|
|
800
768
|
}
|
|
801
|
-
response += '\n' + lines.join('\n');
|
|
802
|
-
}
|
|
803
|
-
if (includeContinuity && apiResult.continuity) {
|
|
804
|
-
response += '\n\n' + formatContinuityBlock(apiResult.continuity);
|
|
805
769
|
}
|
|
806
|
-
return
|
|
770
|
+
return rendered.text;
|
|
807
771
|
}
|
|
808
772
|
case 'hippo_assemble': {
|
|
809
773
|
const sessionId = String(args.session_id || '');
|
|
@@ -830,14 +794,9 @@ async function executeTool(name, args, ctx) {
|
|
|
830
794
|
const r = apiAssemble(apiCtx, sessionId, {
|
|
831
795
|
summarizeOlder,
|
|
832
796
|
...assembleExtra,
|
|
797
|
+
cost: assembleCost(sessionId),
|
|
833
798
|
});
|
|
834
|
-
|
|
835
|
-
lines.push(`Session ${r.sessionId} — ${r.items.length} items, ${r.tokens} tokens (raw=${r.totalRaw}, summarized=${r.summarized}, evicted=${r.evicted})`);
|
|
836
|
-
for (const it of r.items) {
|
|
837
|
-
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
838
|
-
lines.push(` ${prefix} ${it.createdAt} ${it.id} - ${it.content}`);
|
|
839
|
-
}
|
|
840
|
-
return lines.join('\n');
|
|
799
|
+
return assembleText(r);
|
|
841
800
|
}
|
|
842
801
|
case 'hippo_drill': {
|
|
843
802
|
const summaryId = String(args.summary_id || '');
|
|
@@ -868,7 +827,7 @@ async function executeTool(name, args, ctx) {
|
|
|
868
827
|
drillExtra.budget = budget;
|
|
869
828
|
if (depth !== undefined)
|
|
870
829
|
drillExtra.depth = depth;
|
|
871
|
-
const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra });
|
|
830
|
+
const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, cost: drillCost });
|
|
872
831
|
if ('failure' in r) {
|
|
873
832
|
// v1.6.4: only not_drillable is caller-actionable. not_found
|
|
874
833
|
// intentionally collapses cross-tenant + scope-blocked + missing
|
|
@@ -879,15 +838,7 @@ async function executeTool(name, args, ctx) {
|
|
|
879
838
|
}
|
|
880
839
|
return `No drillable summary at id=${summaryId}.`;
|
|
881
840
|
}
|
|
882
|
-
|
|
883
|
-
lines.push(`Summary ${r.summary.id} — ${r.summary.descendantCount} descendants${r.summary.earliestAt ? ` (${r.summary.earliestAt} -> ${r.summary.latestAt})` : ''}`);
|
|
884
|
-
lines.push(` ${r.summary.content}`);
|
|
885
|
-
lines.push('');
|
|
886
|
-
lines.push(`Children (${r.children.length}/${r.totalChildren}${r.truncated ? ', truncated' : ''}):`);
|
|
887
|
-
for (const c of r.children) {
|
|
888
|
-
lines.push(` [L${c.dagLevel}] ${c.id} - ${c.content}`);
|
|
889
|
-
}
|
|
890
|
-
return lines.join('\n');
|
|
841
|
+
return drillText(r);
|
|
891
842
|
}
|
|
892
843
|
case 'hippo_predict_baserate': {
|
|
893
844
|
// J3 reference-class / planning-fallacy detector. Reads from the E2
|
|
@@ -1031,13 +982,6 @@ async function executeTool(name, args, ctx) {
|
|
|
1031
982
|
return true;
|
|
1032
983
|
return classifyOriginProject(e.origin_project, mcpProjectName) !== 'cross-project';
|
|
1033
984
|
});
|
|
1034
|
-
const usePhysicsCtx = config.physics?.enabled !== false;
|
|
1035
|
-
const results = usePhysicsCtx
|
|
1036
|
-
? await physicsSearch(query, entries, { budget, hippoRoot, physicsConfig: config.physics })
|
|
1037
|
-
: await hybridSearch(query, entries, { budget, hippoRoot });
|
|
1038
|
-
const retrievedIds = results.map((r) => r.entry.id);
|
|
1039
|
-
strengthenRetrieved(hippoRoot, retrievedIds);
|
|
1040
|
-
lastRecalledIds.set(resolveClientKey(ctx), retrievedIds);
|
|
1041
985
|
// DF1 (docs/plans/2026-08-23-df1-snapshot-lifecycle.md, T2): bounded
|
|
1042
986
|
// read, no session id available on this surface (freshness bound
|
|
1043
987
|
// only) — an orphaned snapshot must age out here too, not just on the
|
|
@@ -1061,8 +1005,23 @@ async function executeTool(name, args, ctx) {
|
|
|
1061
1005
|
'',
|
|
1062
1006
|
].join('\n')
|
|
1063
1007
|
: '';
|
|
1064
|
-
|
|
1065
|
-
|
|
1008
|
+
// The snapshot prints first, so it is paid first after the heading; context keeps no hit past the budget, even the top one.
|
|
1009
|
+
let left = budget - memoriesReserve(budget);
|
|
1010
|
+
if (left < 0)
|
|
1011
|
+
return ''; // not even the heading fits, so nothing prints, as at budget 0
|
|
1012
|
+
const snapshotPiece = snapshotText ? `${snapshotText}\n` : '';
|
|
1013
|
+
const showSnapshot = snapshotPiece !== '' && estimateTokens(snapshotPiece) <= left;
|
|
1014
|
+
if (showSnapshot)
|
|
1015
|
+
left -= estimateTokens(snapshotPiece);
|
|
1016
|
+
const usePhysicsCtx = config.physics?.enabled !== false;
|
|
1017
|
+
const fit = { budget: left, minResults: 0, cost: memoryCost, hippoRoot };
|
|
1018
|
+
const results = dropHeldCopies(usePhysicsCtx
|
|
1019
|
+
? await physicsSearch(query, entries, { ...fit, physicsConfig: config.physics })
|
|
1020
|
+
: await hybridSearch(query, entries, fit), (r) => r.entry);
|
|
1021
|
+
const retrievedIds = results.map((r) => r.entry.id);
|
|
1022
|
+
strengthenRetrieved(hippoRoot, retrievedIds);
|
|
1023
|
+
lastRecalledIds.set(resolveClientKey(ctx), retrievedIds);
|
|
1024
|
+
return (showSnapshot ? snapshotPiece : '') + formatMemories(results);
|
|
1066
1025
|
}
|
|
1067
1026
|
case 'hippo_status': {
|
|
1068
1027
|
const entries = loadAllEntries(hippoRoot, tenantId);
|
|
@@ -1108,8 +1067,9 @@ async function executeTool(name, args, ctx) {
|
|
|
1108
1067
|
let added = 0;
|
|
1109
1068
|
let skipped = 0;
|
|
1110
1069
|
let rejected = 0;
|
|
1070
|
+
const keys = storedTextKeys(loadAllEntries(hippoRoot, tenantId));
|
|
1111
1071
|
for (const lesson of lessons) {
|
|
1112
|
-
if (
|
|
1072
|
+
if (keys.has(duplicateKey(lesson))) {
|
|
1113
1073
|
skipped++;
|
|
1114
1074
|
continue;
|
|
1115
1075
|
}
|
|
@@ -1133,6 +1093,7 @@ async function executeTool(name, args, ctx) {
|
|
|
1133
1093
|
}
|
|
1134
1094
|
throw err;
|
|
1135
1095
|
}
|
|
1096
|
+
keys.add(duplicateKey(lesson));
|
|
1136
1097
|
added++;
|
|
1137
1098
|
}
|
|
1138
1099
|
const rejectedSuffix = rejected > 0 ? `, ${rejected} rejected values skipped` : '';
|
package/dist/memory.d.ts
CHANGED
|
@@ -213,8 +213,17 @@ export declare function resolveConfidence(entry: MemoryEntry, now?: Date): Confi
|
|
|
213
213
|
* on an older base (src/half-life-migration.ts).
|
|
214
214
|
*/
|
|
215
215
|
export declare const DEFAULT_HALF_LIFE_DAYS = 365;
|
|
216
|
-
export declare const
|
|
217
|
-
export declare
|
|
216
|
+
export declare const COMPACTION_MEMORY_TAG = "compaction-memory";
|
|
217
|
+
export declare const COMPACTION_SOURCE_PREFIX = "compaction:";
|
|
218
|
+
/** A row with `tag` and a source starting `sourcePrefix` is kept for good. Both, since merge copies source tags onto rows whose source is 'consolidation'. */
|
|
219
|
+
export interface KeepPair {
|
|
220
|
+
readonly tag: string;
|
|
221
|
+
readonly sourcePrefix: string;
|
|
222
|
+
}
|
|
223
|
+
export declare const KEEP_PAIRS: readonly KeepPair[];
|
|
224
|
+
export declare const AUTO_DELETABLE_SQL: string;
|
|
225
|
+
export declare function isKeptForGood(entry: Pick<MemoryEntry, 'tags' | 'source' | 'superseded_by'>): boolean;
|
|
226
|
+
export declare function canAutoDelete(entry: Pick<MemoryEntry, 'pinned' | 'kind' | 'tags' | 'source' | 'superseded_by'>): boolean;
|
|
218
227
|
export interface CreateMemoryOptions {
|
|
219
228
|
layer?: Layer;
|
|
220
229
|
tags?: string[];
|
|
@@ -239,6 +248,14 @@ export interface CreateMemoryOptions {
|
|
|
239
248
|
}
|
|
240
249
|
/** Create a new memory entry with defaults. Untyped JavaScript callers that omit the options get the compiled default half-life. */
|
|
241
250
|
export declare function createMemory(content: string, options: CreateMemoryOptions): MemoryEntry;
|
|
251
|
+
/** The row that replaces `old`: a supersede never changes where a memory belongs, so source, scope, session and a stamped origin carry over. */
|
|
252
|
+
export declare function createSuccessor(old: MemoryEntry, content: string, opts: {
|
|
253
|
+
tenantId: string;
|
|
254
|
+
baseHalfLifeDays: number;
|
|
255
|
+
layer?: Layer;
|
|
256
|
+
tags?: string[];
|
|
257
|
+
pinned?: boolean;
|
|
258
|
+
}): MemoryEntry;
|
|
242
259
|
/**
|
|
243
260
|
* Compute how well new content fits existing knowledge patterns.
|
|
244
261
|
* Returns 0..1 where:
|
package/dist/memory.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { isDecayAblated, isOutcomeSlowAblated, isRecallBoostAblated, evalNow, } from './ablation.js';
|
|
7
|
+
import { AGENT_MEMORY_TOOLS, toolSourcePrefix } from './agent-memories/tools.js';
|
|
7
8
|
export var Layer;
|
|
8
9
|
(function (Layer) {
|
|
9
10
|
Layer["Buffer"] = "buffer";
|
|
@@ -314,10 +315,22 @@ export function resolveConfidence(entry, now = evalNow()) {
|
|
|
314
315
|
* on an older base (src/half-life-migration.ts).
|
|
315
316
|
*/
|
|
316
317
|
export const DEFAULT_HALF_LIFE_DAYS = 365;
|
|
317
|
-
|
|
318
|
-
export const
|
|
318
|
+
export const COMPACTION_MEMORY_TAG = 'compaction-memory';
|
|
319
|
+
export const COMPACTION_SOURCE_PREFIX = 'compaction:';
|
|
320
|
+
export const KEEP_PAIRS = [
|
|
321
|
+
{ tag: COMPACTION_MEMORY_TAG, sourcePrefix: COMPACTION_SOURCE_PREFIX },
|
|
322
|
+
...AGENT_MEMORY_TOOLS.map((t) => ({ tag: t.tag, sourcePrefix: toolSourcePrefix(t.id) })),
|
|
323
|
+
];
|
|
324
|
+
const sqlText = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
325
|
+
// json_each matches the tag as a whole element; substr, not LIKE, keeps the prefix case-sensitive like startsWith.
|
|
326
|
+
const keepPairSql = (p) => `(COALESCE(superseded_by, '') = '' AND EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(tags_json) THEN tags_json ELSE '[]' END) WHERE value = ${sqlText(p.tag)}) AND substr(source, 1, ${p.sourcePrefix.length}) = ${sqlText(p.sourcePrefix)})`;
|
|
327
|
+
// Pinned and kept rows stay (a superseded row is not kept: its successor carries the tag and source); raw rows leave only through archiveRawMemory. The SQL twin guards the DELETE itself.
|
|
328
|
+
export const AUTO_DELETABLE_SQL = `pinned = 0 AND kind != 'raw'${KEEP_PAIRS.map((p) => ` AND NOT ${keepPairSql(p)}`).join('')}`;
|
|
329
|
+
export function isKeptForGood(entry) {
|
|
330
|
+
return !entry.superseded_by && KEEP_PAIRS.some((p) => entry.tags.includes(p.tag) && entry.source.startsWith(p.sourcePrefix));
|
|
331
|
+
}
|
|
319
332
|
export function canAutoDelete(entry) {
|
|
320
|
-
return !entry.pinned && entry.kind !== 'raw';
|
|
333
|
+
return !entry.pinned && entry.kind !== 'raw' && !isKeptForGood(entry);
|
|
321
334
|
}
|
|
322
335
|
export function createMemory(content, options = {}) {
|
|
323
336
|
const trimmed = content.trim();
|
|
@@ -373,6 +386,25 @@ export function createMemory(content, options = {}) {
|
|
|
373
386
|
entry.strength = calculateStrength(entry);
|
|
374
387
|
return entry;
|
|
375
388
|
}
|
|
389
|
+
/** The row that replaces `old`: a supersede never changes where a memory belongs, so source, scope, session and a stamped origin carry over. */
|
|
390
|
+
export function createSuccessor(old, content, opts) {
|
|
391
|
+
const next = createMemory(content, {
|
|
392
|
+
layer: opts.layer ?? old.layer,
|
|
393
|
+
tags: opts.tags ?? [...old.tags],
|
|
394
|
+
pinned: opts.pinned ?? old.pinned,
|
|
395
|
+
source: old.source,
|
|
396
|
+
confidence: 'verified',
|
|
397
|
+
tenantId: opts.tenantId,
|
|
398
|
+
scope: old.scope,
|
|
399
|
+
source_session_id: old.source_session_id,
|
|
400
|
+
baseHalfLifeDays: opts.baseHalfLifeDays,
|
|
401
|
+
});
|
|
402
|
+
// A legacy null origin has nothing to carry, so the store stamps it from its own location.
|
|
403
|
+
if (typeof old.origin_project === 'string') {
|
|
404
|
+
next.origin_project = old.origin_project;
|
|
405
|
+
}
|
|
406
|
+
return next;
|
|
407
|
+
}
|
|
376
408
|
/**
|
|
377
409
|
* Compute how well new content fits existing knowledge patterns.
|
|
378
410
|
* Returns 0..1 where:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type MemoryEntry } from './memory.js';
|
|
2
|
+
/** The row that replaces a merged row once its retired texts leave: undefined when it holds none, null when nothing else is left. */
|
|
3
|
+
export declare function mergedSuccessor(row: MemoryEntry, retired: (text: string) => boolean, retiredIds: ReadonlySet<string>): MemoryEntry | null | undefined;
|
|
4
|
+
/** Sleep's check on a merged row: drops texts whose source was superseded since the merge, or that a rejection now covers. */
|
|
5
|
+
export declare function successorAfterRetirement(row: MemoryEntry, byId: ReadonlyMap<string, MemoryEntry>, rejected: (text: string) => boolean): MemoryEntry | null | undefined;
|
|
6
|
+
//# sourceMappingURL=merged-row.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// A sleep-merged row copies its sources' texts, so retiring a text (reject, or superseding its source) must reach the row too.
|
|
2
|
+
// SHORTCUT: reject rewrites merged rows at once, live and dormant; supersede and `resolve --reject-loser` wait for the next sleep's check, which sees a dormant row only once restored.
|
|
3
|
+
import { generateId } from './memory.js';
|
|
4
|
+
import { duplicateKey, heldTexts, mergedText } from './same-text.js';
|
|
5
|
+
/** The row that replaces a merged row once its retired texts leave: undefined when it holds none, null when nothing else is left. */
|
|
6
|
+
export function mergedSuccessor(row, retired, retiredIds) {
|
|
7
|
+
const texts = heldTexts(row);
|
|
8
|
+
const kept = texts.filter((t) => !retired(t));
|
|
9
|
+
if (kept.length === texts.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (kept.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const count = `${kept.length} related ${kept.length === 1 ? 'memory' : 'memories'}`;
|
|
14
|
+
const header = row.content.slice(0, row.content.indexOf('\n\n')).replace(/\d+ related memor(?:y|ies)/, count);
|
|
15
|
+
// A new id, as a fresh merge would get; age and half-life carry over so the rewrite buys the row no extra life.
|
|
16
|
+
return {
|
|
17
|
+
...row,
|
|
18
|
+
id: generateId('sem'),
|
|
19
|
+
content: mergedText(header, kept),
|
|
20
|
+
parents: row.parents.filter((id) => !retiredIds.has(id)),
|
|
21
|
+
conflicts_with: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Sleep's check on a merged row: drops texts whose source was superseded since the merge, or that a rejection now covers. */
|
|
25
|
+
export function successorAfterRetirement(row, byId, rejected) {
|
|
26
|
+
if (row.source !== 'consolidation' || row.superseded_by)
|
|
27
|
+
return undefined;
|
|
28
|
+
const sources = row.parents.flatMap((id) => byId.get(id) ?? []);
|
|
29
|
+
const retiredIds = new Set(sources.filter((s) => Boolean(s.superseded_by) || rejected(s.content)).map((s) => s.id));
|
|
30
|
+
const live = new Set(sources.filter((s) => !retiredIds.has(s.id)).map((s) => duplicateKey(s.content)));
|
|
31
|
+
const gone = new Set(sources.filter((s) => retiredIds.has(s.id)).map((s) => duplicateKey(s.content)));
|
|
32
|
+
const retired = (text) => (gone.has(duplicateKey(text)) && !live.has(duplicateKey(text))) || rejected(text);
|
|
33
|
+
return mergedSuccessor(row, retired, retiredIds);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=merged-row.js.map
|
package/dist/multihop.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { MemoryEntry } from './memory.js';
|
|
2
|
-
import { type SearchResult } from './search.js';
|
|
2
|
+
import { type ResultCost, type SearchResult } from './search.js';
|
|
3
3
|
export declare function multihopSearch(query: string, entries: MemoryEntry[], options?: {
|
|
4
4
|
budget?: number;
|
|
5
5
|
now?: Date;
|
|
6
6
|
hippoRoot?: string;
|
|
7
7
|
minResults?: number;
|
|
8
|
+
cost?: ResultCost;
|
|
8
9
|
includeSuperseded?: boolean;
|
|
9
10
|
asOf?: string;
|
|
10
11
|
}): SearchResult[];
|