hippo-memory 1.52.8 → 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 +158 -98
- package/dist/api.d.ts +51 -18
- package/dist/api.js +121 -76
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/capture.d.ts +37 -0
- package/dist/capture.js +111 -81
- package/dist/cli.js +627 -654
- 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/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/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 +5 -3
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +122 -27
- 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/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 +35 -30
- package/dist/store.d.ts +1 -0
- package/dist/store.js +4 -0
- 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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Typed judgment over capture candidates via TypeSafe's Jev (System One).
|
|
2
|
+
* Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
|
|
3
|
+
* every captured memory currently lands on a flat schema_fit of 0.5. */
|
|
4
|
+
import { ConfidenceLevel, EmotionalValence } from './memory.js';
|
|
5
|
+
export type JudgedKind = 'error' | 'decision' | 'convention' | 'preference' | 'trivia';
|
|
6
|
+
export interface Judgment {
|
|
7
|
+
/** Jev noul, 0..1. Maps to `schema_fit` on the written entry. */
|
|
8
|
+
durable: number;
|
|
9
|
+
kind: JudgedKind;
|
|
10
|
+
valence: EmotionalValence;
|
|
11
|
+
confidence: ConfidenceLevel;
|
|
12
|
+
/** Jev's calibrated confidence on the kind choice, 0..1. */
|
|
13
|
+
kindConfidence: number;
|
|
14
|
+
}
|
|
15
|
+
export interface JudgeOptions {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
/** Injected for testing — defaults to the real fetch. */
|
|
19
|
+
fetcher?: typeof fetch;
|
|
20
|
+
/** Bounded parallelism for `judgeAll`. */
|
|
21
|
+
concurrency?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Absent key means hippo keeps its pre-Jev behaviour and makes no HTTP call. */
|
|
24
|
+
export declare function judgmentApiKey(): string | undefined;
|
|
25
|
+
/** `null` on any failure, so a Jev outage degrades capture to today's
|
|
26
|
+
* behaviour instead of blocking the write. */
|
|
27
|
+
export declare function judge(content: string, opts: JudgeOptions): Promise<Judgment | null>;
|
|
28
|
+
/** Judge many candidates under a bounded concurrency pool, order preserved. */
|
|
29
|
+
export declare function judgeAll(contents: readonly string[], opts: JudgeOptions): Promise<(Judgment | null)[]>;
|
|
30
|
+
//# sourceMappingURL=judgment.d.ts.map
|
package/dist/judgment.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Typed judgment over capture candidates via TypeSafe's Jev (System One).
|
|
2
|
+
* Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
|
|
3
|
+
* every captured memory currently lands on a flat schema_fit of 0.5. */
|
|
4
|
+
const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
5
|
+
const DEFAULT_MODEL = 'jev-1.13.0';
|
|
6
|
+
const MAX_CONCURRENCY = 8;
|
|
7
|
+
const RETRY_STATUS = new Set([429, 529]);
|
|
8
|
+
const QUESTIONS = {
|
|
9
|
+
durable: {
|
|
10
|
+
type: 'noul',
|
|
11
|
+
instructions: 'This text was extracted from an AI coding agent transcript as a candidate memory. It is worth storing long-term only if it would still be useful to a future agent working on this codebase weeks from now: a durable preference, a convention, a decision with a reason, or a gotcha that will recur. Transient chatter, one-off status, restatements of code already in the repo, and anything only true inside this one session are not worth storing.',
|
|
12
|
+
},
|
|
13
|
+
kind: {
|
|
14
|
+
type: 'choice',
|
|
15
|
+
instructions: 'Classify what kind of durable knowledge this is.',
|
|
16
|
+
criteria: {
|
|
17
|
+
error: 'A failure, gotcha, or thing that went wrong, and why.',
|
|
18
|
+
decision: 'A choice that was made, ideally with its reason.',
|
|
19
|
+
convention: 'A rule, standard, or way this project does things.',
|
|
20
|
+
preference: 'A stated preference of the user or team.',
|
|
21
|
+
trivia: 'None of the above; incidental detail with no reuse value.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
valence: {
|
|
25
|
+
type: 'choice',
|
|
26
|
+
instructions: 'Classify the emotional charge of this memory for replay priority.',
|
|
27
|
+
criteria: {
|
|
28
|
+
critical: 'A costly failure or a rule whose violation causes real damage.',
|
|
29
|
+
negative: 'Something that went wrong, or a warning.',
|
|
30
|
+
positive: 'Something that worked, or a confirmed good approach.',
|
|
31
|
+
neutral: 'Plain fact with no success or failure charge.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const KINDS = ['error', 'decision', 'convention', 'preference', 'trivia'];
|
|
36
|
+
const VALENCES = ['critical', 'negative', 'positive', 'neutral'];
|
|
37
|
+
/** Absent key means hippo keeps its pre-Jev behaviour and makes no HTTP call. */
|
|
38
|
+
export function judgmentApiKey() {
|
|
39
|
+
const key = process.env.TYPESAFE_API_KEY?.trim();
|
|
40
|
+
return key ? key : undefined;
|
|
41
|
+
}
|
|
42
|
+
function oneOf(value, allowed) {
|
|
43
|
+
return allowed.find((option) => option === value) ?? null;
|
|
44
|
+
}
|
|
45
|
+
/** `verified` is unreachable: that tier means a human or a test confirmed it. */
|
|
46
|
+
function toConfidenceTier(kindConfidence) {
|
|
47
|
+
if (kindConfidence >= 0.8)
|
|
48
|
+
return 'observed';
|
|
49
|
+
return 'inferred';
|
|
50
|
+
}
|
|
51
|
+
async function postOnce(content, opts) {
|
|
52
|
+
const fetchFn = opts.fetcher ?? fetch;
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetchFn(ENDPOINT, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': 'application/json',
|
|
59
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
state: content,
|
|
63
|
+
model: opts.model ?? DEFAULT_MODEL,
|
|
64
|
+
questions: QUESTIONS,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (RETRY_STATUS.has(res.status))
|
|
72
|
+
return { retryable: true };
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
return null;
|
|
75
|
+
return { res };
|
|
76
|
+
}
|
|
77
|
+
/** `null` on any failure, so a Jev outage degrades capture to today's
|
|
78
|
+
* behaviour instead of blocking the write. */
|
|
79
|
+
export async function judge(content, opts) {
|
|
80
|
+
const trimmed = content.trim();
|
|
81
|
+
if (trimmed.length < 3)
|
|
82
|
+
return null;
|
|
83
|
+
let attempt = await postOnce(trimmed, opts);
|
|
84
|
+
if (attempt && 'retryable' in attempt) {
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
86
|
+
attempt = await postOnce(trimmed, opts);
|
|
87
|
+
}
|
|
88
|
+
if (!attempt || 'retryable' in attempt)
|
|
89
|
+
return null;
|
|
90
|
+
let data;
|
|
91
|
+
try {
|
|
92
|
+
// SAFETY: the documented Jev response is `{ answers: { <name>: Answer } }`
|
|
93
|
+
// keyed by the question names posted above; every field read below is
|
|
94
|
+
// optional-chained and range-checked before use, so a lie here returns null.
|
|
95
|
+
data = await attempt.res.json();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const durable = data.answers?.durable?.noul;
|
|
101
|
+
const kind = oneOf(data.answers?.kind?.choice, KINDS);
|
|
102
|
+
const valence = oneOf(data.answers?.valence?.choice, VALENCES);
|
|
103
|
+
if (durable === undefined || durable < 0 || durable > 1 || !kind || !valence)
|
|
104
|
+
return null;
|
|
105
|
+
const kindConfidence = data.answers?.kind?.confidence ?? 0;
|
|
106
|
+
return { durable, kind, valence, confidence: toConfidenceTier(kindConfidence), kindConfidence };
|
|
107
|
+
}
|
|
108
|
+
/** Judge many candidates under a bounded concurrency pool, order preserved. */
|
|
109
|
+
export async function judgeAll(contents, opts) {
|
|
110
|
+
const out = Array.from({ length: contents.length }, () => null);
|
|
111
|
+
const limit = Math.max(1, opts.concurrency ?? MAX_CONCURRENCY);
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
const worker = async () => {
|
|
114
|
+
while (cursor < contents.length) {
|
|
115
|
+
const i = cursor++;
|
|
116
|
+
out[i] = await judge(contents[i], opts);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
await Promise.all(Array.from({ length: Math.min(limit, contents.length) }, worker));
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=judgment.js.map
|
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` : '';
|
|
@@ -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
|