hippo-memory 1.51.0 → 1.52.1
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 +2 -2
- package/dist/api.d.ts +4 -0
- package/dist/api.js +80 -4
- package/dist/audit.d.ts +1 -0
- package/dist/audit.js +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +6 -1
- package/dist/capture.d.ts +2 -7
- package/dist/capture.js +23 -130
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +65 -30
- package/dist/config.d.ts +15 -0
- package/dist/config.js +6 -0
- package/dist/prompt-recall.d.ts +33 -0
- package/dist/prompt-recall.js +68 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +36 -10
- package/dist/token-ledger.d.ts +2 -1
- package/dist/token-ledger.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -724,9 +724,9 @@ For Claude Code, it also adds:
|
|
|
724
724
|
- a `SessionEnd` hook so `hippo sleep` runs automatically when the session exits
|
|
725
725
|
- a `SessionStart` hook that prints the previous session's consolidation output
|
|
726
726
|
- a `UserPromptSubmit` hook that runs `hippo context --pinned-only --include-recent 5 --format additional-context` every turn. It re-injects pinned memories (`hippo remember <text> --pin`) plus the last 5 writes, so fresh same-session lessons appear on the next prompt before you pin them. The block is rendered without live strength percentages, so it stays byte-identical while its memories do not change, and it is sent only when it changed since the session's last prompt: an unchanged block is skipped, resent every 10 skips (`pinnedInject.refreshTurns`, `0` never resends) and resent after compaction. `{"pinnedInject":{"skipUnchanged":false}}` sends it every turn as before. Opt out entirely with `{"pinnedInject":{"enabled":false}}` in `.hippo/config.json`.
|
|
727
|
-
- a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It saves a working-state snapshot (task/summary/next step)
|
|
727
|
+
- a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It saves a working-state snapshot (task/summary/next step) so mid-session compaction can't drop it; the `SessionEnd` hook still owns extracting durable memories.
|
|
728
728
|
- a second `SessionStart` hook (matcher `compact`) that runs `hippo compact-resume`, printing that snapshot plus the recent session trail back into context right after compaction.
|
|
729
|
-
- a `PostCompact` hook that runs `hippo post-compact`, which tells you what was saved ("Hippo saved your task snapshot
|
|
729
|
+
- a `PostCompact` hook that runs `hippo post-compact`, which tells you what was saved ("Hippo saved your task snapshot before compacting."). It prints nothing when nothing was saved.
|
|
730
730
|
- a `PostToolUseFailure` hook that runs `hippo capture-error`, which stores a failed tool call as an error memory. It skips interrupts, declined permissions and searches that found nothing, and stores a repeated failure once. It also logs every failure, stored or not, for `hippo failures`: the session, the tool and hashes of the error, never its text. A hash is not anonymous, since anyone who guesses an error's text can check it against the hash. The log keeps 90 days.
|
|
731
731
|
|
|
732
732
|
To remove: `hippo hook uninstall claude-code`
|
package/dist/api.d.ts
CHANGED
|
@@ -952,6 +952,8 @@ export interface ContextOpts {
|
|
|
952
952
|
* it just means every snapshot goes through the age check. Host-resolved
|
|
953
953
|
* (stdin payload, HIPPO_SESSION_ID, else the host's session var) so this stays host-agnostic. */
|
|
954
954
|
currentSessionId?: string | null;
|
|
955
|
+
/** Z1: raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
|
|
956
|
+
prompt?: string;
|
|
955
957
|
}
|
|
956
958
|
export interface ContextResultEntry {
|
|
957
959
|
entry: MemoryEntry;
|
|
@@ -959,6 +961,8 @@ export interface ContextResultEntry {
|
|
|
959
961
|
tokens: number;
|
|
960
962
|
isGlobal?: boolean;
|
|
961
963
|
isFreshTail?: boolean;
|
|
964
|
+
/** Z1: admitted by the prompt-recall gate, not the recent-N backfill or a pin. */
|
|
965
|
+
promptRecall?: boolean;
|
|
962
966
|
/** v39: the entry's owning project ('' = user-global, null = legacy row). */
|
|
963
967
|
origin?: string | null;
|
|
964
968
|
/** v39: how the origin relates to the active project. 'cross-project'
|
package/dist/api.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
11
|
-
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, } from './store.js';
|
|
11
|
+
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadRecallSearchEntriesFromDb, pickRarestFtsQuery, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, } from './store.js';
|
|
12
12
|
import { RejectedValueError } from './rejection.js';
|
|
13
13
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
14
14
|
import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
|
|
@@ -30,7 +30,8 @@ import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
|
30
30
|
import { scopeMatch } from './scope.js';
|
|
31
31
|
import { consolidate } from './consolidate.js';
|
|
32
32
|
import { loadConfig } from './config.js';
|
|
33
|
-
import { resolveProjectIdentity, classifyOriginProject } from './project-identity.js';
|
|
33
|
+
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot } from './project-identity.js';
|
|
34
|
+
import { promptTokens, contentTokens, gatePromptRecall, } from './prompt-recall.js';
|
|
34
35
|
import { detectSecret } from './secret-detect.js';
|
|
35
36
|
import { deduplicateStore } from './dedupe.js';
|
|
36
37
|
import { computeAmbientState } from './ambient.js';
|
|
@@ -1587,7 +1588,10 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1587
1588
|
limit: 5,
|
|
1588
1589
|
}).filter((e) => passesScopeFilterForRecall(rowScope(e), undefined))
|
|
1589
1590
|
: [];
|
|
1590
|
-
|
|
1591
|
+
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim())
|
|
1592
|
+
&& loadConfig(ctx.hippoRoot).pinnedInject.promptRecall === true;
|
|
1593
|
+
if (!promptRecallPending &&
|
|
1594
|
+
localEntries.length === 0 &&
|
|
1591
1595
|
globalEntries.length === 0 &&
|
|
1592
1596
|
!activeSnapshot &&
|
|
1593
1597
|
!sessionHandoff &&
|
|
@@ -1654,7 +1658,79 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1654
1658
|
// under-fills recents slightly -- it never displaces a pin -- so it is
|
|
1655
1659
|
// the safe direction and is not worth extra bookkeeping to recover.
|
|
1656
1660
|
const recentBudget = Math.max(0, effBudget - pinnedReserve);
|
|
1657
|
-
|
|
1661
|
+
// Z1: gate the backfill on the prompt instead of recency (docs/plans/2026-09-26-z1-prompt-recall.md).
|
|
1662
|
+
const promptRecallOn = Boolean(opts.prompt?.trim()) && pinnedCfg.pinnedInject.promptRecall === true;
|
|
1663
|
+
if (promptRecallOn) {
|
|
1664
|
+
const rawMetric = pinnedCfg.pinnedInject.promptRecallMetric;
|
|
1665
|
+
const metric = rawMetric === 'cosine' ? 'cosine' : 'jaccard';
|
|
1666
|
+
// Config values come from JSON with no runtime type check; Number.isFinite also rejects a string there.
|
|
1667
|
+
const finiteOr = (v, dflt, min) => Number.isFinite(v) && v >= min ? v : dflt;
|
|
1668
|
+
const gate = {
|
|
1669
|
+
metric,
|
|
1670
|
+
threshold: finiteOr(pinnedCfg.pinnedInject.promptRecallThreshold, 0.04, 0),
|
|
1671
|
+
minShared: finiteOr(pinnedCfg.pinnedInject.promptRecallMinShared, 2, 0),
|
|
1672
|
+
maxItems: finiteOr(pinnedCfg.pinnedInject.promptRecallMaxItems, 5, 1),
|
|
1673
|
+
};
|
|
1674
|
+
const candidateLimit = Math.floor(finiteOr(pinnedCfg.pinnedInject.promptRecallCandidates, 100, 1));
|
|
1675
|
+
const p = promptTokens(opts.prompt ?? '');
|
|
1676
|
+
if (p.size > 0) {
|
|
1677
|
+
const promptTermList = Array.from(p);
|
|
1678
|
+
// One open connection per store instead of loadRecallSearchEntries's own
|
|
1679
|
+
// initStore+open/close per call: store is already initialized (hasLocal/hasGlobal).
|
|
1680
|
+
let localCandidates = [];
|
|
1681
|
+
if (hasLocal) {
|
|
1682
|
+
const localDb = openHippoDb(ctx.hippoRoot);
|
|
1683
|
+
try {
|
|
1684
|
+
const ftsQuery = pickRarestFtsQuery(localDb, promptTermList);
|
|
1685
|
+
// An empty query would load the oldest rows, not matches.
|
|
1686
|
+
if (ftsQuery)
|
|
1687
|
+
localCandidates = loadRecallSearchEntriesFromDb(localDb, ftsQuery, candidateLimit, ctx.tenantId, undefined, 'exact', false);
|
|
1688
|
+
}
|
|
1689
|
+
finally {
|
|
1690
|
+
closeHippoDb(localDb);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
let globalCandidates = [];
|
|
1694
|
+
if (hasGlobal && !isGlobalStoreRoot(ctx.hippoRoot)) {
|
|
1695
|
+
const globalDb = openHippoDb(globalRoot);
|
|
1696
|
+
try {
|
|
1697
|
+
const ftsQuery = pickRarestFtsQuery(globalDb, promptTermList);
|
|
1698
|
+
if (ftsQuery)
|
|
1699
|
+
globalCandidates = loadRecallSearchEntriesFromDb(globalDb, ftsQuery, candidateLimit, ctx.tenantId, undefined, 'exact', false);
|
|
1700
|
+
}
|
|
1701
|
+
finally {
|
|
1702
|
+
closeHippoDb(globalDb);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const seenCandidateIds = new Set();
|
|
1706
|
+
const candidateItems = [];
|
|
1707
|
+
// Local wins the id collision (a global row synced into the local store).
|
|
1708
|
+
for (const e of localCandidates) {
|
|
1709
|
+
if (!admit(e) || e.pinned || !isContentWorthStoring(e.content) || seenCandidateIds.has(e.id))
|
|
1710
|
+
continue;
|
|
1711
|
+
seenCandidateIds.add(e.id);
|
|
1712
|
+
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: false });
|
|
1713
|
+
}
|
|
1714
|
+
for (const e of globalCandidates) {
|
|
1715
|
+
if (!admit(e) || e.pinned || !isContentWorthStoring(e.content) || seenCandidateIds.has(e.id))
|
|
1716
|
+
continue;
|
|
1717
|
+
seenCandidateIds.add(e.id);
|
|
1718
|
+
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: true });
|
|
1719
|
+
}
|
|
1720
|
+
const gated = gatePromptRecall(p, candidateItems, gate);
|
|
1721
|
+
for (const g of gated) {
|
|
1722
|
+
if (selectedIds.has(g.item.id))
|
|
1723
|
+
continue;
|
|
1724
|
+
const tokens = estimateTokens(g.item.entry.content);
|
|
1725
|
+
if (usedP + tokens > recentBudget)
|
|
1726
|
+
continue;
|
|
1727
|
+
selectedItems.push({ entry: g.item.entry, score: g.score, tokens, isGlobal: g.item.isGlobal, promptRecall: true });
|
|
1728
|
+
selectedIds.add(g.item.id);
|
|
1729
|
+
usedP += tokens;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
else if (includeRecent > 0) {
|
|
1658
1734
|
const recent = [
|
|
1659
1735
|
...localEntries.map((entry) => ({ entry, isGlobal: false })),
|
|
1660
1736
|
...globalEntries.map((entry) => ({ entry, isGlobal: true })),
|
package/dist/audit.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface AuditResult {
|
|
|
13
13
|
issues: AuditIssue[];
|
|
14
14
|
clean: number;
|
|
15
15
|
}
|
|
16
|
+
export declare const STOP_WORDS: Set<string>;
|
|
16
17
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
17
18
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
18
19
|
export declare function isContentWorthStoring(content: string): boolean;
|
package/dist/audit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canAutoDelete } from './memory.js';
|
|
2
|
-
const STOP_WORDS = new Set([
|
|
2
|
+
export const STOP_WORDS = new Set([
|
|
3
3
|
'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being',
|
|
4
4
|
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'it',
|
|
5
5
|
'this', 'that', 'and', 'or', 'but', 'not', 'no', 'so', 'if', 'do',
|
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
|
@@ -24,7 +24,12 @@ function hashKey(plaintext) {
|
|
|
24
24
|
// rate limit on /v1/* to bound key-id enumeration. Stored format identical to
|
|
25
25
|
// real hashes: scrypt$saltHex$hashHex.
|
|
26
26
|
const DUMMY_PLAINTEXT = 'hk_dummy_constant_padding_for_timing.dummy_secret_padding_for_timing_x';
|
|
27
|
-
|
|
27
|
+
// Precomputed `hashKey(DUMMY_PLAINTEXT)`: computing it at module load put one scrypt on every CLI start.
|
|
28
|
+
const DUMMY_HASH = 'scrypt$5b2117156f1ad78738fd8b6a5bace454$92e098fa28003b7c38ba8f1451bd8618260a4a3c5a64b87cc2b471567c0960e7';
|
|
29
|
+
// Not a secret (it's padding, not a real key hash): exposed only so a test can pin its shape.
|
|
30
|
+
export function _dummyHashForTests() {
|
|
31
|
+
return DUMMY_HASH;
|
|
32
|
+
}
|
|
28
33
|
function verifyKey(plaintext, stored) {
|
|
29
34
|
const parts = stored.split('$');
|
|
30
35
|
if (parts.length !== 3 || parts[0] !== 'scrypt')
|
package/dist/capture.d.ts
CHANGED
|
@@ -106,16 +106,11 @@ export declare function sanitizeLogMessage(message: string): string;
|
|
|
106
106
|
/** What one pre-compact run saved, reported to the user after compaction. */
|
|
107
107
|
export interface PreCompactReport {
|
|
108
108
|
snapshotSaved: boolean;
|
|
109
|
-
captured: number;
|
|
110
109
|
/** Claude Code session the run belonged to, when the payload named one. */
|
|
111
110
|
sessionId: string | null;
|
|
112
111
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
* saved. Without it the only sign was Claude Code's generic
|
|
116
|
-
* "PreCompact [...] completed successfully".
|
|
117
|
-
*/
|
|
118
|
-
export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapshotSaved' | 'captured'>): string | null;
|
|
112
|
+
/** Line shown after compaction, or null when nothing was saved (Claude Code's own generic message is the fallback). */
|
|
113
|
+
export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapshotSaved'>): string | null;
|
|
119
114
|
/**
|
|
120
115
|
* Where pre-compact leaves its report for `hippo post-compact`: next to the
|
|
121
116
|
* pre-compact log, so both hooks find it from the same `--log-file`.
|
package/dist/capture.js
CHANGED
|
@@ -21,7 +21,6 @@ import { defaultPreCompactLogPath } from './hooks.js';
|
|
|
21
21
|
import { redactSecrets } from './secret-detect.js';
|
|
22
22
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
23
23
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
24
|
-
import { loadConfig } from './config.js';
|
|
25
24
|
// Sentence-level patterns
|
|
26
25
|
//
|
|
27
26
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -469,62 +468,6 @@ function isDuplicate(content, existing) {
|
|
|
469
468
|
}
|
|
470
469
|
return false;
|
|
471
470
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Write already-extracted items to the store, deduped against existing
|
|
474
|
-
* tenant-scoped entries. Shared write path for `cmdCapture` (extracted from
|
|
475
|
-
* raw text inline) and `cmdPreCompact` (extracted from a pre-computed tail
|
|
476
|
-
* summary, no raw-text re-extraction). Mirrors the non-dry-run write loop in
|
|
477
|
-
* `cmdCaptureCore`: same layer/source/confidence, same embed-if-configured,
|
|
478
|
-
* fire-and-forget behaviour.
|
|
479
|
-
*
|
|
480
|
-
* Returns the fire-and-forget `embedMemory` promises alongside the counts
|
|
481
|
-
* (review round X6) so a caller that must not exit before embeddings settle
|
|
482
|
-
* — `cmdPreCompact`, which runs process.exit(0) right after — can await them
|
|
483
|
-
* with a bounded timeout instead of racing a detached write.
|
|
484
|
-
*/
|
|
485
|
-
function writeExtractedItems(hippoRoot, tenantId, extracted) {
|
|
486
|
-
if (extracted.length === 0)
|
|
487
|
-
return { captured: 0, skipped: 0, rejected: 0, embeds: [] };
|
|
488
|
-
const existing = loadAllEntries(hippoRoot, tenantId);
|
|
489
|
-
const embeds = [];
|
|
490
|
-
let captured = 0;
|
|
491
|
-
let skipped = 0;
|
|
492
|
-
let rejected = 0;
|
|
493
|
-
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
494
|
-
for (const item of extracted) {
|
|
495
|
-
if (isDuplicate(item.content, existing)) {
|
|
496
|
-
skipped++;
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
const entry = createMemory(item.content, {
|
|
500
|
-
layer: Layer.Episodic,
|
|
501
|
-
tags: item.tags,
|
|
502
|
-
source: 'capture',
|
|
503
|
-
confidence: 'observed',
|
|
504
|
-
tenantId,
|
|
505
|
-
baseHalfLifeDays,
|
|
506
|
-
});
|
|
507
|
-
// AT1 (plan §3 containment): a refusal is per-VALUE — one rejected
|
|
508
|
-
// extraction must not abort the rest of this transcript's captures.
|
|
509
|
-
try {
|
|
510
|
-
writeEntry(hippoRoot, entry);
|
|
511
|
-
}
|
|
512
|
-
catch (err) {
|
|
513
|
-
if (err instanceof RejectedValueError) {
|
|
514
|
-
rejected++;
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
throw err;
|
|
518
|
-
}
|
|
519
|
-
updateStats(hippoRoot, { remembered: 1 });
|
|
520
|
-
existing.push(entry); // within-batch dedup
|
|
521
|
-
if (isEmbeddingConfigured(hippoRoot)) {
|
|
522
|
-
embeds.push(embedMemory(hippoRoot, entry).catch(() => { }));
|
|
523
|
-
}
|
|
524
|
-
captured++;
|
|
525
|
-
}
|
|
526
|
-
return { captured, skipped, rejected, embeds };
|
|
527
|
-
}
|
|
528
471
|
/**
|
|
529
472
|
* Runtime shape guards used throughout this file wherever a value arrives
|
|
530
473
|
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
@@ -557,7 +500,7 @@ function errorMessage(cause) {
|
|
|
557
500
|
/** Leading markers of the command lines Claude Code writes with type 'user'. */
|
|
558
501
|
const CLAUDE_CODE_COMMAND_PREFIXES = ['<local-command-', '<command-name>', '<command-message>', '<command-args>'];
|
|
559
502
|
function isNonHumanUserLine(entry, content) {
|
|
560
|
-
if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true)
|
|
503
|
+
if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true || entry.promptSource === 'system')
|
|
561
504
|
return true;
|
|
562
505
|
const head = content.trimStart();
|
|
563
506
|
return CLAUDE_CODE_COMMAND_PREFIXES.some((p) => head.startsWith(p));
|
|
@@ -1121,20 +1064,11 @@ function isReadableFile(filePath) {
|
|
|
1121
1064
|
return false;
|
|
1122
1065
|
}
|
|
1123
1066
|
}
|
|
1124
|
-
/**
|
|
1125
|
-
* The line shown to the user after compaction, or null when nothing was
|
|
1126
|
-
* saved. Without it the only sign was Claude Code's generic
|
|
1127
|
-
* "PreCompact [...] completed successfully".
|
|
1128
|
-
*/
|
|
1067
|
+
/** Line shown after compaction, or null when nothing was saved (Claude Code's own generic message is the fallback). */
|
|
1129
1068
|
export function preCompactMessage(report) {
|
|
1130
|
-
if (!report.snapshotSaved
|
|
1069
|
+
if (!report.snapshotSaved)
|
|
1131
1070
|
return null;
|
|
1132
|
-
|
|
1133
|
-
if (report.snapshotSaved)
|
|
1134
|
-
parts.push('your task snapshot');
|
|
1135
|
-
if (report.captured > 0)
|
|
1136
|
-
parts.push(`${report.captured} new memor${report.captured === 1 ? 'y' : 'ies'}`);
|
|
1137
|
-
return `Hippo saved ${parts.join(' and ')} before compacting.${report.snapshotSaved ? ' The snapshot is restored into the new context.' : ''}`;
|
|
1071
|
+
return 'Hippo saved your task snapshot before compacting. The snapshot is restored into the new context.';
|
|
1138
1072
|
}
|
|
1139
1073
|
/**
|
|
1140
1074
|
* Where pre-compact leaves its report for `hippo post-compact`: next to the
|
|
@@ -1143,24 +1077,20 @@ export function preCompactMessage(report) {
|
|
|
1143
1077
|
export function preCompactReportPath(logFile) {
|
|
1144
1078
|
return path.join(path.dirname(logFile), 'pre-compact-last.json');
|
|
1145
1079
|
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Runs the PreCompact producer. Returns any `embedMemory` promises kicked
|
|
1148
|
-
* off along the way (empty on every skip path) so `cmdPreCompact` can await
|
|
1149
|
-
* them, bounded, before it exits (X6).
|
|
1150
|
-
*/
|
|
1080
|
+
/** Runs the PreCompact producer: saves a working-state snapshot into `report`. Never extracts memories; SessionEnd capture owns that. */
|
|
1151
1081
|
function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
1152
1082
|
// X3: the PreCompact hook fires in every Claude Code project, including
|
|
1153
1083
|
// ones that never ran `hippo init`, so gate before any store-opening call
|
|
1154
1084
|
// (saveActiveTaskSnapshot etc. call initStore, which would create one).
|
|
1155
1085
|
if (!isInitialized(hippoRoot)) {
|
|
1156
1086
|
appendPreCompactLog(logFile, 'skip: store not initialized');
|
|
1157
|
-
return
|
|
1087
|
+
return;
|
|
1158
1088
|
}
|
|
1159
1089
|
// Same hazard X4 guards below, different trigger: a read that timed out
|
|
1160
1090
|
// must not reach auto-discovery either, or it snapshots another session.
|
|
1161
1091
|
if (stdinTimedOut && (!stdinText || stdinText.trim() === '')) {
|
|
1162
1092
|
appendPreCompactLog(logFile, 'skip: no PreCompact payload arrived before the stdin wait window closed');
|
|
1163
|
-
return
|
|
1093
|
+
return;
|
|
1164
1094
|
}
|
|
1165
1095
|
// A true manual invocation has no stdin at all (TTY, or a non-TTY pipe
|
|
1166
1096
|
// that yielded an empty read) — that's the ONLY case newest-transcript
|
|
@@ -1186,7 +1116,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1186
1116
|
// `"transcript_path": null` (or the key missing entirely) — all fail
|
|
1187
1117
|
// the string check. Log and skip; never fall through to auto-discovery.
|
|
1188
1118
|
appendPreCompactLog(logFile, 'skip: malformed or incomplete PreCompact payload (missing string transcript_path)');
|
|
1189
|
-
return
|
|
1119
|
+
return;
|
|
1190
1120
|
}
|
|
1191
1121
|
if ('session_id' in payload && isStringValue(payload.session_id))
|
|
1192
1122
|
sessionId = payload.session_id;
|
|
@@ -1202,7 +1132,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1202
1132
|
// this check could gate on, so containment buys no real isolation.
|
|
1203
1133
|
if (payloadTranscriptPath !== null && !/\.jsonl$/i.test(payloadTranscriptPath)) {
|
|
1204
1134
|
appendPreCompactLog(logFile, `skip: payload transcript_path is not a .jsonl file: ${payloadTranscriptPath}`);
|
|
1205
|
-
return
|
|
1135
|
+
return;
|
|
1206
1136
|
}
|
|
1207
1137
|
// A payload transcript_path is EXCLUSIVE: never fall back to
|
|
1208
1138
|
// newest-transcript auto-discovery when it's missing/unreadable. That
|
|
@@ -1217,7 +1147,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1217
1147
|
}
|
|
1218
1148
|
else {
|
|
1219
1149
|
appendPreCompactLog(logFile, `skip: payload transcript_path unreadable: ${payloadTranscriptPath}`);
|
|
1220
|
-
return
|
|
1150
|
+
return;
|
|
1221
1151
|
}
|
|
1222
1152
|
}
|
|
1223
1153
|
else {
|
|
@@ -1225,7 +1155,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1225
1155
|
}
|
|
1226
1156
|
if (!transcriptPath) {
|
|
1227
1157
|
appendPreCompactLog(logFile, 'skip: no transcript resolved');
|
|
1228
|
-
return
|
|
1158
|
+
return;
|
|
1229
1159
|
}
|
|
1230
1160
|
let tail;
|
|
1231
1161
|
try {
|
|
@@ -1248,22 +1178,18 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1248
1178
|
}
|
|
1249
1179
|
catch (err) {
|
|
1250
1180
|
appendPreCompactLog(logFile, `skip: could not read transcript tail: ${errorMessage(err)}`);
|
|
1251
|
-
return
|
|
1181
|
+
return;
|
|
1252
1182
|
}
|
|
1253
1183
|
const summaryFull = summariseTranscript(tail);
|
|
1254
|
-
//
|
|
1255
|
-
// items become durable memories and must never carry raw secrets any more
|
|
1256
|
-
// than the snapshot fields may. (The pre-existing SessionEnd capture path
|
|
1257
|
-
// is deliberately unchanged.)
|
|
1184
|
+
// Snapshot fields must never carry raw secrets, same rule X9 applies below to task/next_step.
|
|
1258
1185
|
const scrubbedSummary = redactSecrets(summaryFull);
|
|
1259
|
-
const extracted = extractFromText(scrubbedSummary);
|
|
1260
1186
|
const rawTask = lastPlainUserMessage(tail);
|
|
1261
1187
|
const rawNextStep = lastAssistantTextBlock(tail);
|
|
1262
|
-
// Full skip only when
|
|
1263
|
-
//
|
|
1264
|
-
if (!rawTask.trim() && !summaryFull.trim() && !rawNextStep.trim()
|
|
1265
|
-
appendPreCompactLog(logFile, 'skip: empty summary
|
|
1266
|
-
return
|
|
1188
|
+
// Full skip only when every derived field is empty: never clobber a
|
|
1189
|
+
// user-authored active snapshot with junk.
|
|
1190
|
+
if (!rawTask.trim() && !summaryFull.trim() && !rawNextStep.trim()) {
|
|
1191
|
+
appendPreCompactLog(logFile, 'skip: empty summary');
|
|
1192
|
+
return;
|
|
1267
1193
|
}
|
|
1268
1194
|
const tenantId = resolveTenantId({});
|
|
1269
1195
|
// Per-field merge (X1): a tool-heavy tail whose only user turns are
|
|
@@ -1309,10 +1235,8 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1309
1235
|
const nextStep = scrubbedNextStep.trim()
|
|
1310
1236
|
? truncateCodePointSafe(scrubbedNextStep, PRE_COMPACT_NEXT_STEP_CAP)
|
|
1311
1237
|
: (fallback?.next_step ?? '');
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
// fields (cross-session tail with nothing derivable) skip the write so a
|
|
1315
|
-
// foreign session's junk never displaces the owning session's snapshot.
|
|
1238
|
+
// All-empty fields (cross-session tail with nothing derivable) skip the
|
|
1239
|
+
// write so a foreign session's junk never displaces the owning snapshot.
|
|
1316
1240
|
if (!task && !summary && !nextStep) {
|
|
1317
1241
|
appendPreCompactLog(logFile, 'skip: no snapshot content for this session (nothing derivable; fallback blocked or empty)');
|
|
1318
1242
|
}
|
|
@@ -1332,25 +1256,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1332
1256
|
appendPreCompactLog(logFile, `snapshot save failed: ${errorMessage(err)}`);
|
|
1333
1257
|
}
|
|
1334
1258
|
}
|
|
1335
|
-
// Capture extraction SECOND, own try/catch: a failure here self-heals at
|
|
1336
|
-
// the next SessionEnd capture (existing dedup absorbs the overlap).
|
|
1337
|
-
try {
|
|
1338
|
-
const { captured, skipped, rejected, embeds } = writeExtractedItems(hippoRoot, tenantId, extracted);
|
|
1339
|
-
report.captured = captured;
|
|
1340
|
-
appendPreCompactLog(logFile, `capture: ${captured} items captured, ${skipped} skipped` +
|
|
1341
|
-
(rejected > 0 ? `, ${rejected} rejected` : ''));
|
|
1342
|
-
return embeds;
|
|
1343
|
-
}
|
|
1344
|
-
catch (err) {
|
|
1345
|
-
appendPreCompactLog(logFile, `capture failed: ${errorMessage(err)}`);
|
|
1346
|
-
return [];
|
|
1347
|
-
}
|
|
1348
1259
|
}
|
|
1349
|
-
// X6: bound how long cmdPreCompact will wait for fire-and-forget embeddings
|
|
1350
|
-
// to settle before it exits. PreCompact runs under a hook timeout (30s in
|
|
1351
|
-
// the installer) — 3s leaves ample headroom while still giving embeddings a
|
|
1352
|
-
// real chance to finish instead of racing process.exit(0) unconditionally.
|
|
1353
|
-
const EMBED_SETTLE_TIMEOUT_MS = 3000;
|
|
1354
1260
|
/**
|
|
1355
1261
|
* PreCompact hook entry point. Exit code 2 on PreCompact BLOCKS compaction,
|
|
1356
1262
|
* so this verb must exit 0 on every path — malformed stdin, missing
|
|
@@ -1360,29 +1266,17 @@ const EMBED_SETTLE_TIMEOUT_MS = 3000;
|
|
|
1360
1266
|
*/
|
|
1361
1267
|
export async function cmdPreCompact(hippoRoot, options) {
|
|
1362
1268
|
const logFile = options.logFile ?? defaultPreCompactLogPath();
|
|
1363
|
-
|
|
1364
|
-
const report = { snapshotSaved: false, captured: 0, sessionId: null };
|
|
1269
|
+
const report = { snapshotSaved: false, sessionId: null };
|
|
1365
1270
|
try {
|
|
1366
|
-
|
|
1271
|
+
runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile, report);
|
|
1367
1272
|
}
|
|
1368
1273
|
catch (err) {
|
|
1369
1274
|
appendPreCompactLog(logFile, `pre-compact failed: ${errorMessage(err)}`);
|
|
1370
1275
|
}
|
|
1371
|
-
if (embeds.length > 0) {
|
|
1372
|
-
let timer;
|
|
1373
|
-
const timeout = new Promise((resolve) => {
|
|
1374
|
-
timer = setTimeout(() => resolve('timeout'), EMBED_SETTLE_TIMEOUT_MS);
|
|
1375
|
-
timer.unref?.();
|
|
1376
|
-
});
|
|
1377
|
-
const settled = Promise.allSettled(embeds).then(() => 'settled');
|
|
1378
|
-
const outcome = await Promise.race([settled, timeout]);
|
|
1379
|
-
clearTimeout(timer);
|
|
1380
|
-
appendPreCompactLog(logFile, outcome === 'settled' ? 'embeddings settled' : 'embeddings timeout');
|
|
1381
|
-
}
|
|
1382
1276
|
// Nothing goes to stdout: Claude Code passes PreCompact stdout to the
|
|
1383
1277
|
// summarising model as extra instructions. The PostCompact hook
|
|
1384
1278
|
// (`hippo post-compact`) tells the user instead, from this report.
|
|
1385
|
-
if (report.snapshotSaved
|
|
1279
|
+
if (report.snapshotSaved) {
|
|
1386
1280
|
try {
|
|
1387
1281
|
fs.writeFileSync(preCompactReportPath(logFile), JSON.stringify({ ...report, at: new Date().toISOString() }));
|
|
1388
1282
|
}
|
|
@@ -1431,7 +1325,6 @@ export function postCompactMessage(stdinText, logFile = defaultPreCompactLogPath
|
|
|
1431
1325
|
return null;
|
|
1432
1326
|
return preCompactMessage({
|
|
1433
1327
|
snapshotSaved: 'snapshotSaved' in report && report.snapshotSaved === true,
|
|
1434
|
-
captured: 'captured' in report && Number.isInteger(report.captured) ? Number(report.captured) : 0,
|
|
1435
1328
|
});
|
|
1436
1329
|
}
|
|
1437
1330
|
catch {
|
package/dist/cli.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export declare function printContextMarkdown(items: Array<{
|
|
|
63
63
|
isGlobal: boolean;
|
|
64
64
|
}>, totalTokens: number, framing?: string, opts?: {
|
|
65
65
|
showStrength?: boolean;
|
|
66
|
+
heading?: string;
|
|
66
67
|
}): void;
|
|
67
68
|
export declare function runCli(argv?: string[]): Promise<void>;
|
|
68
69
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,7 @@ import { execFileSync, execSync, spawn } from 'child_process';
|
|
|
41
41
|
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
42
42
|
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
43
43
|
import { detectSecret } from './secret-detect.js';
|
|
44
|
-
import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, } from './store.js';
|
|
44
|
+
import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, } from './store.js';
|
|
45
45
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
46
46
|
import { RejectedValueError } from './rejection.js';
|
|
47
47
|
import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
@@ -2935,6 +2935,8 @@ function cmdLastSleep(flags) {
|
|
|
2935
2935
|
// X8: session-event content is capped at print time only — the shared
|
|
2936
2936
|
// printSessionEvents stays untouched for every other caller.
|
|
2937
2937
|
const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
|
|
2938
|
+
// A snapshot older than this was not written for this compaction (pre-compact skipped), so restoring it is stale, not a resume.
|
|
2939
|
+
const COMPACT_RESUME_MAX_AGE_MS = 15 * 60_000;
|
|
2938
2940
|
function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
2939
2941
|
try {
|
|
2940
2942
|
// X3: gate on the non-exiting isInitialized check before any
|
|
@@ -2985,7 +2987,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
2985
2987
|
}
|
|
2986
2988
|
if (!suppressOutput) {
|
|
2987
2989
|
const tenantId = resolveTenantId({});
|
|
2988
|
-
const snapshot =
|
|
2990
|
+
const snapshot = loadFreshActiveTaskSnapshot(hippoRoot, tenantId, { maxAgeMs: COMPACT_RESUME_MAX_AGE_MS });
|
|
2989
2991
|
// X5: concurrent sessions must not cross-restore. Only suppress when
|
|
2990
2992
|
// BOTH ids are present and differ — either side missing, or a manual
|
|
2991
2993
|
// invocation with no payload session_id, still prints.
|
|
@@ -6326,12 +6328,16 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6326
6328
|
// hostSessionId(); absent both, undefined -- api.getContext then applies
|
|
6327
6329
|
// the pure freshness bound with no owner-match short-circuit.
|
|
6328
6330
|
let payloadSessionId;
|
|
6331
|
+
// Z1: the hook payload's raw prompt, read beside session_id (docs/plans/2026-09-26-z1-prompt-recall.md).
|
|
6332
|
+
let payloadPrompt;
|
|
6329
6333
|
if (stdinText && stdinText.trim() !== '') {
|
|
6330
6334
|
try {
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
+
// SAFETY: both fields are type-checked below before use; `?? {}` covers a JSON null payload.
|
|
6336
|
+
const { session_id: sid, prompt } = (JSON.parse(stdinText.trim()) ?? {});
|
|
6337
|
+
if (typeof sid === 'string' && sid.trim() !== '')
|
|
6338
|
+
payloadSessionId = sid;
|
|
6339
|
+
if (typeof prompt === 'string')
|
|
6340
|
+
payloadPrompt = prompt;
|
|
6335
6341
|
}
|
|
6336
6342
|
catch {
|
|
6337
6343
|
// Malformed/non-JSON stdin: fall through to the env fallback below.
|
|
@@ -6347,6 +6353,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6347
6353
|
includeRecent: parseCountFlag(flags['include-recent']),
|
|
6348
6354
|
crossProject,
|
|
6349
6355
|
currentSessionId,
|
|
6356
|
+
prompt: payloadPrompt,
|
|
6350
6357
|
};
|
|
6351
6358
|
const result = await api.getContext(ctx, opts);
|
|
6352
6359
|
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
@@ -6398,9 +6405,18 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6398
6405
|
}));
|
|
6399
6406
|
}
|
|
6400
6407
|
else if (format === 'additional-context') {
|
|
6401
|
-
//
|
|
6402
|
-
//
|
|
6403
|
-
const
|
|
6408
|
+
// Z1: split into a static block (snapshot/handoff/events/pins/recent-N,
|
|
6409
|
+
// TE2-skippable) and a prompt-recall block (never skipped, own heading).
|
|
6410
|
+
const staticEntries = mainEntries.filter((r) => !r.promptRecall);
|
|
6411
|
+
const staticCrossEntries = crossEntries.filter((r) => !r.promptRecall);
|
|
6412
|
+
const recallEntries = result.entries.filter((r) => r.promptRecall);
|
|
6413
|
+
const staticItems = staticEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6414
|
+
const recallItems = recallEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6415
|
+
// Header total is static-only once a recall section exists; otherwise byte-identical to today.
|
|
6416
|
+
const staticHeaderTokens = recallItems.length > 0
|
|
6417
|
+
? staticItems.reduce((sum, r) => sum + r.tokens, 0)
|
|
6418
|
+
: result.tokens;
|
|
6419
|
+
const staticBlock = captureConsole(() => {
|
|
6404
6420
|
if (result.activeSnapshot)
|
|
6405
6421
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6406
6422
|
if (result.sessionHandoff)
|
|
@@ -6408,49 +6424,66 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6408
6424
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6409
6425
|
printSessionEvents(result.recentEvents);
|
|
6410
6426
|
}
|
|
6411
|
-
if (
|
|
6427
|
+
if (staticItems.length > 0) {
|
|
6412
6428
|
// TE1: no live strength percentage, so an unchanged set of memories
|
|
6413
6429
|
// renders byte-identically turn after turn.
|
|
6414
|
-
printContextMarkdown(
|
|
6430
|
+
printContextMarkdown(staticItems, staticHeaderTokens, framing, { showStrength: false });
|
|
6415
6431
|
}
|
|
6416
|
-
printCrossProjectSection(
|
|
6432
|
+
printCrossProjectSection(staticCrossEntries);
|
|
6417
6433
|
});
|
|
6418
|
-
|
|
6434
|
+
const recallTokens = recallItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
6435
|
+
const recallBlock = recallItems.length > 0
|
|
6436
|
+
? captureConsole(() => printContextMarkdown(recallItems, recallTokens, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' }))
|
|
6437
|
+
: '';
|
|
6438
|
+
if (!staticBlock.trim() && !recallBlock.trim())
|
|
6419
6439
|
return;
|
|
6420
6440
|
const surface = pinnedOnly ? 'hook' : 'context';
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
//
|
|
6424
|
-
//
|
|
6425
|
-
|
|
6426
|
-
// (a manual run inside an agent's shell) must never suppress output.
|
|
6427
|
-
if (pinnedOnly && payloadSessionId !== undefined) {
|
|
6441
|
+
let sendStatic = staticBlock.trim().length > 0;
|
|
6442
|
+
// TE2: the per-prompt hook skips a static block identical to the one this
|
|
6443
|
+
// session already has, resent every refreshTurns skips. Hashed on the
|
|
6444
|
+
// static text alone so an unchanged pin set still skips while recall varies.
|
|
6445
|
+
if (sendStatic && pinnedOnly && payloadSessionId !== undefined) {
|
|
6428
6446
|
const injectCfg = loadConfig(hippoRoot).pinnedInject;
|
|
6429
6447
|
if (injectCfg.skipUnchanged !== false) {
|
|
6430
6448
|
const refreshTurns = Number.isFinite(injectCfg.refreshTurns) && injectCfg.refreshTurns >= 0
|
|
6431
6449
|
? injectCfg.refreshTurns
|
|
6432
6450
|
: 10;
|
|
6451
|
+
const staticHash = blockHash(staticBlock);
|
|
6433
6452
|
const last = withLedgerDb(hippoRoot, (db) => lastSentState(db, ctx.tenantId, payloadSessionId, surface));
|
|
6434
|
-
if (shouldSkipUnchanged(last ?? null,
|
|
6453
|
+
if (shouldSkipUnchanged(last ?? null, staticHash, refreshTurns)) {
|
|
6435
6454
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6436
6455
|
tenantId: ctx.tenantId, sessionId: payloadSessionId, surface, event: 'skip',
|
|
6437
|
-
items:
|
|
6456
|
+
items: staticItems.length, tokens: estimateTokens(staticBlock), hash: staticHash,
|
|
6438
6457
|
}));
|
|
6439
|
-
|
|
6458
|
+
sendStatic = false;
|
|
6440
6459
|
}
|
|
6441
6460
|
}
|
|
6442
6461
|
}
|
|
6462
|
+
const finalStatic = sendStatic ? staticBlock : '';
|
|
6463
|
+
const additionalContext = finalStatic && recallBlock
|
|
6464
|
+
? `${finalStatic}\n\n${recallBlock}`
|
|
6465
|
+
: finalStatic || recallBlock;
|
|
6466
|
+
if (!additionalContext.trim())
|
|
6467
|
+
return;
|
|
6443
6468
|
const payload = {
|
|
6444
6469
|
hookSpecificOutput: {
|
|
6445
6470
|
hookEventName: 'UserPromptSubmit',
|
|
6446
|
-
additionalContext
|
|
6471
|
+
additionalContext,
|
|
6447
6472
|
},
|
|
6448
6473
|
};
|
|
6449
6474
|
process.stdout.write(JSON.stringify(payload));
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6475
|
+
if (finalStatic) {
|
|
6476
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6477
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface, event: 'inject',
|
|
6478
|
+
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
6479
|
+
}));
|
|
6480
|
+
}
|
|
6481
|
+
if (recallBlock) {
|
|
6482
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6483
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface: 'hook_recall', event: 'inject',
|
|
6484
|
+
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
6485
|
+
}));
|
|
6486
|
+
}
|
|
6454
6487
|
}
|
|
6455
6488
|
else {
|
|
6456
6489
|
// markdown (default)
|
|
@@ -6575,7 +6608,8 @@ function printCrossProjectSection(items) {
|
|
|
6575
6608
|
export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
|
|
6576
6609
|
const now = evalNow();
|
|
6577
6610
|
const showStrength = opts.showStrength !== false;
|
|
6578
|
-
|
|
6611
|
+
const heading = opts.heading ?? 'Project Memory';
|
|
6612
|
+
console.log(`## ${heading} (${items.length} entries, ${totalTokens} tokens)\n`);
|
|
6579
6613
|
for (const item of items) {
|
|
6580
6614
|
const e = item.entry;
|
|
6581
6615
|
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
@@ -8742,6 +8776,7 @@ Commands:
|
|
|
8742
8776
|
--budget <n> Token budget (default: 1500)
|
|
8743
8777
|
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
8744
8778
|
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
8779
|
+
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on)
|
|
8745
8780
|
--format <fmt> Output format: markdown (default), json, or additional-context (Claude Code hook JSON)
|
|
8746
8781
|
--framing <mode> Framing: observe (default), suggest, assert
|
|
8747
8782
|
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
@@ -8953,7 +8988,7 @@ Commands:
|
|
|
8953
8988
|
last-sleep Print the last 'hippo sleep --log-file' output and clear it
|
|
8954
8989
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
8955
8990
|
--keep Print without clearing
|
|
8956
|
-
pre-compact PreCompact hook:
|
|
8991
|
+
pre-compact PreCompact hook: save a working-state snapshot before compaction
|
|
8957
8992
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
8958
8993
|
compact-resume SessionStart(compact) hook: re-print the snapshot + session trail
|
|
8959
8994
|
post-compact PostCompact hook: tell the user what pre-compact saved
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Config support for Hippo: reads .hippo/config.json with sane defaults.
|
|
3
3
|
*/
|
|
4
4
|
import { type PhysicsConfig } from './physics-config.js';
|
|
5
|
+
import type { PromptRecallMetric } from './prompt-recall.js';
|
|
5
6
|
export type DecayBasis = 'clock' | 'session' | 'adaptive';
|
|
6
7
|
export interface HippoConfig {
|
|
7
8
|
defaultHalfLifeDays: number;
|
|
@@ -69,6 +70,20 @@ export interface HippoConfig {
|
|
|
69
70
|
* sessions still see pinned rules near the latest turn. Default 10; 0
|
|
70
71
|
* never resends an unchanged block. */
|
|
71
72
|
refreshTurns: number;
|
|
73
|
+
/** Z1: gate the hook's backfill on the prompt's own content instead of
|
|
74
|
+
* the five newest memories. Default false: the eval failed its overlap gate
|
|
75
|
+
* (docs/evals/2026-09-26-z1-prompt-recall-result.md). */
|
|
76
|
+
promptRecall: boolean;
|
|
77
|
+
/** Z1: overlap metric for the prompt-recall gate. Default 'jaccard' (tuned, docs/evals/2026-09-26-z1-prompt-recall-result.md). */
|
|
78
|
+
promptRecallMetric: PromptRecallMetric;
|
|
79
|
+
/** Z1: minimum overlap score to admit a candidate. Default 0.04 (tuned). */
|
|
80
|
+
promptRecallThreshold: number;
|
|
81
|
+
/** Z1: minimum shared tokens to admit a candidate. Default 2. */
|
|
82
|
+
promptRecallMinShared: number;
|
|
83
|
+
/** Z1: max prompt-recall entries injected per prompt. Default 5 (tuned). */
|
|
84
|
+
promptRecallMaxItems: number;
|
|
85
|
+
/** Z1: FTS candidate pool size per store before gating. Default 100. */
|
|
86
|
+
promptRecallCandidates: number;
|
|
72
87
|
};
|
|
73
88
|
/** Memory scope isolation (v39): when true (default), ambient context
|
|
74
89
|
* (`hippo context`, the UserPromptSubmit hook, /v1/context, MCP
|
package/dist/config.js
CHANGED
|
@@ -47,6 +47,12 @@ const DEFAULT_CONFIG = {
|
|
|
47
47
|
budget: 1500,
|
|
48
48
|
skipUnchanged: true,
|
|
49
49
|
refreshTurns: 10,
|
|
50
|
+
promptRecall: false,
|
|
51
|
+
promptRecallMetric: 'jaccard',
|
|
52
|
+
promptRecallThreshold: 0.04,
|
|
53
|
+
promptRecallMinShared: 2,
|
|
54
|
+
promptRecallMaxItems: 5,
|
|
55
|
+
promptRecallCandidates: 100,
|
|
50
56
|
},
|
|
51
57
|
contextProjectIsolation: true,
|
|
52
58
|
extraction: {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PromptRecallMetric = 'jaccard' | 'cosine';
|
|
2
|
+
export interface PromptRecallGate {
|
|
3
|
+
metric: PromptRecallMetric;
|
|
4
|
+
threshold: number;
|
|
5
|
+
minShared: number;
|
|
6
|
+
maxItems: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const PROMPT_RECALL_MAX_CHARS = 4000;
|
|
9
|
+
/** Distinct content tokens: longer than 2 chars, not a stop word. */
|
|
10
|
+
export declare function contentTokens(text: string): Set<string>;
|
|
11
|
+
export declare function promptTokens(prompt: string): Set<string>;
|
|
12
|
+
export interface OverlapScore {
|
|
13
|
+
score: number;
|
|
14
|
+
shared: number;
|
|
15
|
+
}
|
|
16
|
+
/** Overlap of two token sets under `metric`. 0 when either set is empty. */
|
|
17
|
+
export declare function scoreOverlap(p: ReadonlySet<string>, m: ReadonlySet<string>, metric: PromptRecallMetric): OverlapScore;
|
|
18
|
+
/** Candidates that clear the gate, sorted score desc then id asc, capped at `gate.maxItems`. */
|
|
19
|
+
export declare function gatePromptRecall<T extends {
|
|
20
|
+
id: string;
|
|
21
|
+
tokens: ReadonlySet<string>;
|
|
22
|
+
}>(p: ReadonlySet<string>, candidates: readonly T[], gate: PromptRecallGate): Array<{
|
|
23
|
+
item: T;
|
|
24
|
+
score: number;
|
|
25
|
+
shared: number;
|
|
26
|
+
}>;
|
|
27
|
+
/** The prompt's content tokens as an FTS pre-select query, capped at `maxTerms`. */
|
|
28
|
+
export declare function promptRecallFtsQuery(p: ReadonlySet<string>, maxTerms?: number): string;
|
|
29
|
+
export declare const RAREST_TERM_COUNT = 8;
|
|
30
|
+
/** Terms sorted by ascending FTS doc count (rarest first), zero-count terms dropped, ties by term.
|
|
31
|
+
* Pure: `docCount` (an FTS lookup in the caller) does the only I/O. */
|
|
32
|
+
export declare function rarestPromptTerms(terms: Iterable<string>, docCount: (term: string) => number, maxTerms?: number): string[];
|
|
33
|
+
//# sourceMappingURL=prompt-recall.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Z1: recall gated on the hook prompt, not the five newest memories (pure, no I/O).
|
|
2
|
+
* See docs/plans/2026-09-26-z1-prompt-recall.md. */
|
|
3
|
+
import { tokenize } from './search.js';
|
|
4
|
+
import { STOP_WORDS } from './audit.js';
|
|
5
|
+
// Latency bound, not tuned: fixed in the prereg regardless of gate config.
|
|
6
|
+
export const PROMPT_RECALL_MAX_CHARS = 4000;
|
|
7
|
+
/** Distinct content tokens: longer than 2 chars, not a stop word. */
|
|
8
|
+
export function contentTokens(text) {
|
|
9
|
+
const out = new Set();
|
|
10
|
+
for (const t of tokenize(text)) {
|
|
11
|
+
if (t.length > 2 && !STOP_WORDS.has(t))
|
|
12
|
+
out.add(t);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function promptTokens(prompt) {
|
|
17
|
+
return contentTokens(prompt.slice(0, PROMPT_RECALL_MAX_CHARS));
|
|
18
|
+
}
|
|
19
|
+
/** Overlap of two token sets under `metric`. 0 when either set is empty. */
|
|
20
|
+
export function scoreOverlap(p, m, metric) {
|
|
21
|
+
if (p.size === 0 || m.size === 0)
|
|
22
|
+
return { score: 0, shared: 0 };
|
|
23
|
+
const [small, large] = p.size <= m.size ? [p, m] : [m, p];
|
|
24
|
+
let shared = 0;
|
|
25
|
+
for (const t of small)
|
|
26
|
+
if (large.has(t))
|
|
27
|
+
shared++;
|
|
28
|
+
const score = metric === 'jaccard'
|
|
29
|
+
? shared / (p.size + m.size - shared)
|
|
30
|
+
: shared / Math.sqrt(p.size * m.size);
|
|
31
|
+
return { score, shared };
|
|
32
|
+
}
|
|
33
|
+
/** Candidates that clear the gate, sorted score desc then id asc, capped at `gate.maxItems`. */
|
|
34
|
+
export function gatePromptRecall(p, candidates, gate) {
|
|
35
|
+
if (p.size === 0)
|
|
36
|
+
return [];
|
|
37
|
+
const kept = [];
|
|
38
|
+
for (const item of candidates) {
|
|
39
|
+
const { score, shared } = scoreOverlap(p, item.tokens, gate.metric);
|
|
40
|
+
if (score >= gate.threshold && shared >= gate.minShared)
|
|
41
|
+
kept.push({ item, score, shared });
|
|
42
|
+
}
|
|
43
|
+
kept.sort((a, b) => (b.score - a.score) || (a.item.id < b.item.id ? -1 : a.item.id > b.item.id ? 1 : 0));
|
|
44
|
+
return kept.slice(0, gate.maxItems);
|
|
45
|
+
}
|
|
46
|
+
/** The prompt's content tokens as an FTS pre-select query, capped at `maxTerms`. */
|
|
47
|
+
export function promptRecallFtsQuery(p, maxTerms = 32) {
|
|
48
|
+
const terms = [];
|
|
49
|
+
for (const t of p) {
|
|
50
|
+
if (terms.length >= maxTerms)
|
|
51
|
+
break;
|
|
52
|
+
terms.push(t);
|
|
53
|
+
}
|
|
54
|
+
return terms.join(' ');
|
|
55
|
+
}
|
|
56
|
+
// bm25 ranks every row matching any term, so fewer and rarer terms bound latency; 8 is a bound, not tuned.
|
|
57
|
+
export const RAREST_TERM_COUNT = 8;
|
|
58
|
+
/** Terms sorted by ascending FTS doc count (rarest first), zero-count terms dropped, ties by term.
|
|
59
|
+
* Pure: `docCount` (an FTS lookup in the caller) does the only I/O. */
|
|
60
|
+
export function rarestPromptTerms(terms, docCount, maxTerms = RAREST_TERM_COUNT) {
|
|
61
|
+
return Array.from(terms)
|
|
62
|
+
.map((t) => ({ t, c: docCount(t) }))
|
|
63
|
+
.filter((x) => x.c > 0)
|
|
64
|
+
.sort((a, b) => (a.c - b.c) || (a.t < b.t ? -1 : a.t > b.t ? 1 : 0))
|
|
65
|
+
.slice(0, maxTerms)
|
|
66
|
+
.map((x) => x.t);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=prompt-recall.js.map
|
package/dist/store.d.ts
CHANGED
|
@@ -445,6 +445,10 @@ export declare function loadSearchEntries(hippoRoot: string, query: string, limi
|
|
|
445
445
|
* undefined as "no tenant filter" for legacy callers.
|
|
446
446
|
*/
|
|
447
447
|
export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean): MemoryEntry[];
|
|
448
|
+
export declare function loadRecallSearchEntriesFromDb(db: DatabaseSyncLike, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean): MemoryEntry[];
|
|
449
|
+
/** Rarest-K prompt terms for this connection's FTS index, as a space-joined query string.
|
|
450
|
+
* Without FTS, returns the first 32 terms, as before rarest-term selection. */
|
|
451
|
+
export declare function pickRarestFtsQuery(db: DatabaseSyncLike, terms: readonly string[], maxTerms?: number): string;
|
|
448
452
|
/**
|
|
449
453
|
* Rebuild mirrors from SQLite, importing any legacy markdown files not already present.
|
|
450
454
|
*/
|
package/dist/store.js
CHANGED
|
@@ -13,6 +13,7 @@ import { rowToSessionHandoff, isHandoffOutcome } from './handoff.js';
|
|
|
13
13
|
import { CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
|
|
14
14
|
import { tokenize, markRetrieved } from './search.js';
|
|
15
15
|
import { isRecallBoostAblated } from './ablation.js';
|
|
16
|
+
import { rarestPromptTerms, RAREST_TERM_COUNT } from './prompt-recall.js';
|
|
16
17
|
import { appendAuditEvent } from './audit.js';
|
|
17
18
|
import { resolveTenantId } from './tenant.js';
|
|
18
19
|
import { deriveOriginProject, originFromSource, findHippoStoreDir, realpathOrResolve } from './project-identity.js';
|
|
@@ -1896,21 +1897,46 @@ export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH
|
|
|
1896
1897
|
initStore(hippoRoot);
|
|
1897
1898
|
const db = openHippoDb(hippoRoot);
|
|
1898
1899
|
try {
|
|
1899
|
-
|
|
1900
|
-
// 'exact' — api.recall semantics: narrow to m.scope = requested.
|
|
1901
|
-
// 'additive' — CLI --scope semantics (v1.25.0): default-admitted set
|
|
1902
|
-
// PLUS the requested scope; see RecallScopeFilter docs.
|
|
1903
|
-
const scopeFilter = requestedScope && requestedScope !== ''
|
|
1904
|
-
? explicitScopeMode === 'additive'
|
|
1905
|
-
? { mode: 'default-deny-or-exact', value: requestedScope }
|
|
1906
|
-
: { mode: 'exact', value: requestedScope }
|
|
1907
|
-
: { mode: 'default-deny' };
|
|
1908
|
-
return loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded).map(rowToEntry);
|
|
1900
|
+
return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded);
|
|
1909
1901
|
}
|
|
1910
1902
|
finally {
|
|
1911
1903
|
closeHippoDb(db);
|
|
1912
1904
|
}
|
|
1913
1905
|
}
|
|
1906
|
+
// Split out so callers with an already-open db (Z1 prompt-recall path) skip
|
|
1907
|
+
// the initStore+open/close cycle per store per call.
|
|
1908
|
+
export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true) {
|
|
1909
|
+
// 'exact' narrows to requestedScope; 'additive' adds it to the default-admitted set.
|
|
1910
|
+
const scopeFilter = requestedScope && requestedScope !== ''
|
|
1911
|
+
? explicitScopeMode === 'additive'
|
|
1912
|
+
? { mode: 'default-deny-or-exact', value: requestedScope }
|
|
1913
|
+
: { mode: 'exact', value: requestedScope }
|
|
1914
|
+
: { mode: 'default-deny' };
|
|
1915
|
+
return loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded).map(rowToEntry);
|
|
1916
|
+
}
|
|
1917
|
+
/** Rarest-K prompt terms for this connection's FTS index, as a space-joined query string.
|
|
1918
|
+
* Without FTS, returns the first 32 terms, as before rarest-term selection. */
|
|
1919
|
+
export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
|
|
1920
|
+
// The LIKE path has no bm25 ranking to bound, so it keeps the pre-rarest 32-term query.
|
|
1921
|
+
if (!isFtsAvailable(db))
|
|
1922
|
+
return terms.slice(0, 32).join(' ');
|
|
1923
|
+
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS temp.z1_rarest_vocab USING fts5vocab(main, 'memories_fts', 'row')`);
|
|
1924
|
+
// unicode61 splits `journal_mode` into two vocab terms; a term's count is its rarest part's (an upper bound).
|
|
1925
|
+
const partsOf = (t) => t.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
1926
|
+
const vocab = Array.from(new Set(terms.flatMap(partsOf)));
|
|
1927
|
+
if (vocab.length === 0)
|
|
1928
|
+
return '';
|
|
1929
|
+
// SAFETY: rows' shape matches the two columns named in the SELECT.
|
|
1930
|
+
const rows = db
|
|
1931
|
+
.prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${vocab.map(() => '?').join(', ')})`)
|
|
1932
|
+
.all(...vocab);
|
|
1933
|
+
const counts = new Map(rows.map((r) => [r.term, r.doc]));
|
|
1934
|
+
const docCount = (t) => {
|
|
1935
|
+
const parts = partsOf(t);
|
|
1936
|
+
return parts.length === 0 ? 0 : Math.min(...parts.map((x) => counts.get(x) ?? 0));
|
|
1937
|
+
};
|
|
1938
|
+
return rarestPromptTerms(terms, docCount, maxTerms).join(' ');
|
|
1939
|
+
}
|
|
1914
1940
|
/**
|
|
1915
1941
|
* Rebuild mirrors from SQLite, importing any legacy markdown files not already present.
|
|
1916
1942
|
*/
|
package/dist/token-ledger.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import type { DatabaseSyncLike } from './db.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Where a block of memory text was sent.
|
|
4
4
|
* - `hook`: the per-prompt `UserPromptSubmit` hook (`hippo context --pinned-only`).
|
|
5
|
+
* - `hook_recall`: the same hook's Z1 prompt-recall section (docs/plans/2026-09-26-z1-prompt-recall.md).
|
|
5
6
|
* - `context`, `recall`: the CLI commands.
|
|
6
7
|
* - `mcp_recall`, `mcp_context`: the MCP tools.
|
|
7
8
|
* - `http_recall`, `http_context`, `http_assemble`: the HTTP API.
|
|
8
9
|
*/
|
|
9
|
-
export type TokenSurface = 'hook' | 'context' | 'recall' | 'mcp_recall' | 'mcp_context' | 'http_recall' | 'http_context' | 'http_assemble';
|
|
10
|
+
export type TokenSurface = 'hook' | 'hook_recall' | 'context' | 'recall' | 'mcp_recall' | 'mcp_context' | 'http_recall' | 'http_context' | 'http_assemble';
|
|
10
11
|
/** All surfaces, in report order. */
|
|
11
12
|
export declare const TOKEN_SURFACES: readonly TokenSurface[];
|
|
12
13
|
/**
|
package/dist/token-ledger.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
22
|
/** All surfaces, in report order. */
|
|
23
23
|
export const TOKEN_SURFACES = [
|
|
24
|
-
'hook', 'context', 'recall', 'mcp_recall', 'mcp_context',
|
|
24
|
+
'hook', 'hook_recall', 'context', 'recall', 'mcp_recall', 'mcp_context',
|
|
25
25
|
'http_recall', 'http_context', 'http_assemble',
|
|
26
26
|
];
|
|
27
27
|
/** Rows older than this are pruned on write. */
|
package/dist/version.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PACKAGE_VERSION = "1.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.52.1";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.
|
|
19
|
+
export const PACKAGE_VERSION = '1.52.1';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.52.1",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hippo-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Zero runtime deps, SQLite, MCP server, and an opt-in hosted TypeSafe Jev reranker. Decay, retrieval strengthening, consolidation.",
|
|
5
5
|
"mcpName": "io.github.kitfunso/hippo-memory",
|
|
6
6
|
"type": "module",
|