hippo-memory 1.52.7 → 1.52.9
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 +159 -99
- package/dist/api.d.ts +52 -18
- package/dist/api.js +155 -86
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +40 -0
- package/dist/capture.js +141 -119
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +695 -702
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +4 -0
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- 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/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +13 -10
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +19 -5
- package/dist/hooks.js +131 -30
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +174 -213
- 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 +9 -3
- 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 +11 -3
- package/dist/shared.js +41 -31
- package/dist/store.d.ts +4 -5
- package/dist/store.js +26 -13
- 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/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/index.ts +1 -0
- 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-ui/assets/index-BgmA7Hwe.js +0 -61
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
|
|
@@ -989,9 +940,9 @@ async function executeTool(name, args, ctx) {
|
|
|
989
940
|
// Auto-detect query from git
|
|
990
941
|
let query = '';
|
|
991
942
|
try {
|
|
992
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
993
|
-
const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
994
|
-
const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
943
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
944
|
+
const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
945
|
+
const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
995
946
|
query = [branch, log, diff].filter(Boolean).join(' ');
|
|
996
947
|
}
|
|
997
948
|
catch { /* not a git repo */ }
|
|
@@ -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` : '';
|
|
@@ -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[];
|
package/dist/multihop.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { search } from './search.js';
|
|
1
|
+
import { fitBudget, search } from './search.js';
|
|
2
2
|
export function multihopSearch(query, entries, options = {}) {
|
|
3
|
-
const
|
|
3
|
+
const budget = options.budget ?? 4000;
|
|
4
|
+
// Pass 1 searches wide to find entities, so each return fits the caller's budget, as search() does.
|
|
5
|
+
const fit = (ordered) => fitBudget(ordered, budget, options.minResults ?? 1, options.cost);
|
|
6
|
+
const pass1 = search(query, entries, { ...options, budget: budget * 2 });
|
|
4
7
|
const topK = pass1.slice(0, 10);
|
|
5
8
|
if (topK.length === 0)
|
|
6
9
|
return [];
|
|
@@ -17,7 +20,7 @@ export function multihopSearch(query, entries, options = {}) {
|
|
|
17
20
|
.map((t) => t.split(':')[1])
|
|
18
21
|
.filter((e) => !queryLower.includes(e.toLowerCase()));
|
|
19
22
|
if (newEntities.length === 0)
|
|
20
|
-
return pass1;
|
|
23
|
+
return fit(pass1);
|
|
21
24
|
const followUpQuery = newEntities.join(' ') + ' ' + query;
|
|
22
25
|
const pass2 = search(followUpQuery, entries, options);
|
|
23
26
|
const merged = new Map();
|
|
@@ -30,6 +33,6 @@ export function multihopSearch(query, entries, options = {}) {
|
|
|
30
33
|
// T2 note: PLAIN stable score sort on purpose -- pass1/pass2 inputs are
|
|
31
34
|
// deterministically ordered (search() carries the content tail), stability
|
|
32
35
|
// inherits that, and ties keep pass-1 results ahead of pass-2 follow-ups.
|
|
33
|
-
return [...merged.values()].sort((a, b) => b.score - a.score);
|
|
36
|
+
return fit([...merged.values()].sort((a, b) => b.score - a.score));
|
|
34
37
|
}
|
|
35
38
|
//# sourceMappingURL=multihop.js.map
|
package/dist/physics-state.d.ts
CHANGED
|
@@ -26,10 +26,6 @@ export declare function savePhysicsState(db: DatabaseSyncLike, particles: Physic
|
|
|
26
26
|
* Returns the new particle (does not persist — caller must save).
|
|
27
27
|
*/
|
|
28
28
|
export declare function initializeParticle(entry: MemoryEntry, embedding: number[], now?: Date): PhysicsParticle;
|
|
29
|
-
/**
|
|
30
|
-
* Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
|
|
31
|
-
*/
|
|
32
|
-
export declare function deletePhysicsState(db: DatabaseSyncLike, memoryId: string): void;
|
|
33
29
|
/**
|
|
34
30
|
* Reset all physics states from original embeddings.
|
|
35
31
|
* Drops existing physics data and re-initializes from the embedding index.
|
package/dist/physics-state.js
CHANGED
|
@@ -129,12 +129,6 @@ export function initializeParticle(entry, embedding, now = evalNow()) {
|
|
|
129
129
|
lastSimulation: now.toISOString(),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
|
|
134
|
-
*/
|
|
135
|
-
export function deletePhysicsState(db, memoryId) {
|
|
136
|
-
db.prepare('DELETE FROM memory_physics WHERE memory_id = ?').run(memoryId);
|
|
137
|
-
}
|
|
138
132
|
/**
|
|
139
133
|
* Reset all physics states from original embeddings.
|
|
140
134
|
* Drops existing physics data and re-initializes from the embedding index.
|