hippo-memory 1.52.1 → 1.52.3
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 +7 -1
- package/dist/api.js +27 -45
- package/dist/capture-error.js +2 -2
- package/dist/cli.js +22 -11
- package/dist/db.js +4 -3
- package/dist/half-life-migration.js +2 -3
- package/dist/reject-flow.js +5 -8
- package/dist/secret-detect.d.ts +1 -1
- package/dist/secret-detect.js +9 -1
- package/dist/store.d.ts +12 -1
- package/dist/store.js +95 -123
- package/dist/support-bundle.js +60 -41
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.js +5 -8
- 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
|
@@ -628,7 +628,7 @@ hippo watch "npm run build"
|
|
|
628
628
|
| `hippo dormant [<query>]` | List faded memories sleep kept instead of deleting |
|
|
629
629
|
| `hippo dormant restore <id>` | Bring a dormant memory back to active memory |
|
|
630
630
|
| `hippo dormant forget <id>` | Delete a dormant memory permanently |
|
|
631
|
-
| `hippo doctor [--json]` | Check the install: Node, store, schema, sleep, agent hooks; each problem names its fix |
|
|
631
|
+
| `hippo doctor [--json]` | Check the install: Node, store, schema, sleep, agent hooks; each problem names its fix. It never changes `hippo.db`, though SQLite may leave empty `hippo.db-wal` and `hippo.db-shm` files beside it |
|
|
632
632
|
| `hippo support-bundle [--out <file>] [--include-logs]` | Write a redacted JSON file for a support ticket: versions, doctor checks, config, store counts and log names, never memory text; `--include-logs` adds each log's last 200 lines, which can quote it |
|
|
633
633
|
| `hippo tokens [--days n]` | Estimated tokens of memory text handed to agents, per surface, and what skipping unchanged hook blocks saved |
|
|
634
634
|
| `hippo failures [--days n]` | Failed tool calls the capture-error hook saw, by outcome, and how many errors first happened in another session |
|
|
@@ -963,6 +963,12 @@ The interesting problems:
|
|
|
963
963
|
- **MemPalace-style spatial organization.** Could spatial structure (wings/halls/rooms) improve hippo's semantic layer?
|
|
964
964
|
- **AAAK-style compression for semantic memories.** Lossless token compression for context injection.
|
|
965
965
|
|
|
966
|
+
## Open source and commercial
|
|
967
|
+
|
|
968
|
+
Hippo is open core, and the line is drawn by who pays. Everything an individual developer or a self-hosted team needs is in this repository under MIT: the CLI, the MCP server, hooks, connectors, the dashboard, tenants, API keys, roles, per-key scope grants, the audit log and zero-touch memory. Code published here stays MIT and stays here.
|
|
969
|
+
|
|
970
|
+
A commercial edition for larger companies ships as a separate package under a commercial licence from KITFUNSO LTD. It adds SSO (OIDC and SAML sign-in), SCIM, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests for those features belong there, not here; see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
971
|
+
|
|
966
972
|
## License
|
|
967
973
|
|
|
968
974
|
MIT
|
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,
|
|
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';
|
|
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';
|
|
@@ -133,14 +133,14 @@ export function ambientSecretAdmit(e, currentProjectName) {
|
|
|
133
133
|
return false;
|
|
134
134
|
return origin === currentProjectName;
|
|
135
135
|
}
|
|
136
|
-
// The pinned-only branch needs pins and recent-N candidates, not the corpus.
|
|
137
|
-
function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admit) {
|
|
136
|
+
// The pinned-only branch needs pins and recent-N candidates, not the corpus; `recall` applies there only.
|
|
137
|
+
function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admit, recall) {
|
|
138
138
|
if (!pinnedOnly)
|
|
139
|
-
return loadAllEntries(hippoRoot, tenantId).filter(admit);
|
|
139
|
+
return { entries: loadAllEntries(hippoRoot, tenantId).filter(admit) };
|
|
140
140
|
// DF3's quality floor runs on the recent-N slice AFTER this load, so the load
|
|
141
141
|
// counts by it too, or it stops short of a store whose newest rows are junk.
|
|
142
142
|
const admitAmbient = (e) => admit(e) && (e.pinned || isContentWorthStoring(e.content));
|
|
143
|
-
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient);
|
|
143
|
+
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient, recall);
|
|
144
144
|
}
|
|
145
145
|
export function remember(ctx, opts) {
|
|
146
146
|
const detection = opts.untrusted ? detectInstruction(opts.content) : { flagged: false, reason: null };
|
|
@@ -1491,6 +1491,7 @@ export function auditList(ctx, opts) {
|
|
|
1491
1491
|
closeHippoDb(db);
|
|
1492
1492
|
}
|
|
1493
1493
|
}
|
|
1494
|
+
const finiteOr = (v, dflt, min) => Number.isFinite(v) && v >= min ? v : dflt;
|
|
1494
1495
|
/**
|
|
1495
1496
|
* Assemble a context bundle: recalled memories (pinned-only / strength-sorted
|
|
1496
1497
|
* fallback / hybrid search) + active task snapshot + session handoff + recent
|
|
@@ -1540,13 +1541,24 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1540
1541
|
// secrets, so WHICH rows reach this predicate is what loadAmbientEntries cares
|
|
1541
1542
|
// about below.
|
|
1542
1543
|
const admit = (e) => !e.superseded_by && ambientAdmit(e);
|
|
1543
|
-
//
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
? loadAmbientEntries(globalRoot, ctx.tenantId, pinnedOnly, includeRecent, admit)
|
|
1544
|
+
// Z1: decided before the ambient loads so the FTS candidate query below (pinned-only
|
|
1545
|
+
// branch) can piggyback on that connection instead of opening its own.
|
|
1546
|
+
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim()) && config.pinnedInject.promptRecall === true;
|
|
1547
|
+
const promptRecallTerms = promptRecallPending && config.pinnedInject.enabled
|
|
1548
|
+
? Array.from(promptTokens(opts.prompt ?? ''))
|
|
1549
1549
|
: [];
|
|
1550
|
+
const recallRequest = promptRecallTerms.length > 0
|
|
1551
|
+
? { terms: promptRecallTerms, limit: Math.floor(finiteOr(config.pinnedInject.promptRecallCandidates, 100, 1)) }
|
|
1552
|
+
: undefined;
|
|
1553
|
+
// Tenant-scoped loads (v1.11.1 lesson: NEVER resolveTenantId({}) here).
|
|
1554
|
+
const localLoad = hasLocal
|
|
1555
|
+
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly, includeRecent, admit, recallRequest)
|
|
1556
|
+
: { entries: [] };
|
|
1557
|
+
const globalLoad = hasGlobal
|
|
1558
|
+
? loadAmbientEntries(globalRoot, ctx.tenantId, pinnedOnly, includeRecent, admit, !isGlobalStoreRoot(ctx.hippoRoot) ? recallRequest : undefined)
|
|
1559
|
+
: { entries: [] };
|
|
1560
|
+
let localEntries = localLoad.entries;
|
|
1561
|
+
let globalEntries = globalLoad.entries;
|
|
1550
1562
|
// Computed below, after markRetrieved runs, so avgStrength reflects the
|
|
1551
1563
|
// post-retrieval strengths rather than a stale pre-mutation snapshot.
|
|
1552
1564
|
let ambientState;
|
|
@@ -1588,8 +1600,6 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1588
1600
|
limit: 5,
|
|
1589
1601
|
}).filter((e) => passesScopeFilterForRecall(rowScope(e), undefined))
|
|
1590
1602
|
: [];
|
|
1591
|
-
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim())
|
|
1592
|
-
&& loadConfig(ctx.hippoRoot).pinnedInject.promptRecall === true;
|
|
1593
1603
|
if (!promptRecallPending &&
|
|
1594
1604
|
localEntries.length === 0 &&
|
|
1595
1605
|
globalEntries.length === 0 &&
|
|
@@ -1659,49 +1669,21 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1659
1669
|
// the safe direction and is not worth extra bookkeeping to recover.
|
|
1660
1670
|
const recentBudget = Math.max(0, effBudget - pinnedReserve);
|
|
1661
1671
|
// Z1: gate the backfill on the prompt instead of recency (docs/plans/2026-09-26-z1-prompt-recall.md).
|
|
1662
|
-
const promptRecallOn =
|
|
1672
|
+
const promptRecallOn = promptRecallPending;
|
|
1663
1673
|
if (promptRecallOn) {
|
|
1664
1674
|
const rawMetric = pinnedCfg.pinnedInject.promptRecallMetric;
|
|
1665
1675
|
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
1676
|
const gate = {
|
|
1669
1677
|
metric,
|
|
1670
1678
|
threshold: finiteOr(pinnedCfg.pinnedInject.promptRecallThreshold, 0.04, 0),
|
|
1671
1679
|
minShared: finiteOr(pinnedCfg.pinnedInject.promptRecallMinShared, 2, 0),
|
|
1672
1680
|
maxItems: finiteOr(pinnedCfg.pinnedInject.promptRecallMaxItems, 5, 1),
|
|
1673
1681
|
};
|
|
1674
|
-
const candidateLimit = Math.floor(finiteOr(pinnedCfg.pinnedInject.promptRecallCandidates, 100, 1));
|
|
1675
1682
|
const p = promptTokens(opts.prompt ?? '');
|
|
1676
1683
|
if (p.size > 0) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
}
|
|
1684
|
+
// Candidates came off the ambient load's own connection (recallRequest above), not a fresh open.
|
|
1685
|
+
const localCandidates = localLoad.recall ?? [];
|
|
1686
|
+
const globalCandidates = globalLoad.recall ?? [];
|
|
1705
1687
|
const seenCandidateIds = new Set();
|
|
1706
1688
|
const candidateItems = [];
|
|
1707
1689
|
// Local wins the id collision (a global row synced into the local store).
|
package/dist/capture-error.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// stored, because error memories decay slowly and would crowd out real lessons; what is stored stays `observed`
|
|
3
3
|
// until outcome feedback confirms it. Every failure, stored or not, goes to the failure log (ROADMAP CD13).
|
|
4
4
|
import { createMemory } from './memory.js';
|
|
5
|
-
import { writeEntry,
|
|
5
|
+
import { writeEntry, loadContentsWithTag } from './store.js';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
import { closeHippoDb, openHippoDb } from './db.js';
|
|
8
8
|
import { recordFailure } from './failure-log.js';
|
|
@@ -94,7 +94,7 @@ function logFailure(hippoRoot, tenantId, payload, lesson, outcome) {
|
|
|
94
94
|
}
|
|
95
95
|
function storeLesson(hippoRoot, tenantId, text) {
|
|
96
96
|
const sig = failureSignature(text);
|
|
97
|
-
const repeat =
|
|
97
|
+
const repeat = loadContentsWithTag(hippoRoot, tenantId, 'auto-captured').some((content) => failureSignature(content) === sig);
|
|
98
98
|
if (repeat)
|
|
99
99
|
return 'duplicate';
|
|
100
100
|
const entry = createMemory(text, {
|
package/dist/cli.js
CHANGED
|
@@ -6472,17 +6472,28 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6472
6472
|
},
|
|
6473
6473
|
};
|
|
6474
6474
|
process.stdout.write(JSON.stringify(payload));
|
|
6475
|
-
if (finalStatic) {
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6475
|
+
if (finalStatic || recallBlock) {
|
|
6476
|
+
// One connection for both rows; each insert in its own try so one failing doesn't skip the other.
|
|
6477
|
+
withLedgerDb(hippoRoot, (db) => {
|
|
6478
|
+
if (finalStatic) {
|
|
6479
|
+
try {
|
|
6480
|
+
recordTokenUse(db, {
|
|
6481
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface, event: 'inject',
|
|
6482
|
+
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
6483
|
+
});
|
|
6484
|
+
}
|
|
6485
|
+
catch { /* best effort; see withLedgerDb doc comment */ }
|
|
6486
|
+
}
|
|
6487
|
+
if (recallBlock) {
|
|
6488
|
+
try {
|
|
6489
|
+
recordTokenUse(db, {
|
|
6490
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface: 'hook_recall', event: 'inject',
|
|
6491
|
+
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
6492
|
+
});
|
|
6493
|
+
}
|
|
6494
|
+
catch { /* best effort; see withLedgerDb doc comment */ }
|
|
6495
|
+
}
|
|
6496
|
+
});
|
|
6486
6497
|
}
|
|
6487
6498
|
}
|
|
6488
6499
|
else {
|
package/dist/db.js
CHANGED
|
@@ -2819,9 +2819,10 @@ function backfillFtsIndex(db) {
|
|
|
2819
2819
|
// SAFETY: this get() result's shape matches the single aliased `c` column
|
|
2820
2820
|
// named in the SELECT above.
|
|
2821
2821
|
const memCount = db.prepare(`SELECT COUNT(*) AS c FROM memories`).get()?.c ?? 0;
|
|
2822
|
-
// SAFETY: this get() result's shape matches the single aliased `c` column
|
|
2823
|
-
//
|
|
2824
|
-
const
|
|
2822
|
+
// SAFETY: this get() result's shape matches the single aliased `c` column named in the SELECT above.
|
|
2823
|
+
// memories_fts_docsize is FTS5's cheaper one-row-per-document shadow table; fall back for a foreign-built index.
|
|
2824
|
+
const ftsTable = tableExists(db, 'memories_fts_docsize') ? 'memories_fts_docsize' : 'memories_fts';
|
|
2825
|
+
const ftsCount = db.prepare(`SELECT COUNT(*) AS c FROM ${ftsTable}`).get()?.c ?? 0;
|
|
2825
2826
|
if (memCount === ftsCount)
|
|
2826
2827
|
return;
|
|
2827
2828
|
db.exec(`
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* (7 days when never recorded) to the configured `defaultHalfLifeDays`.
|
|
21
21
|
*/
|
|
22
22
|
import { deriveHalfLife } from './memory.js';
|
|
23
|
-
import {
|
|
23
|
+
import { openStore, selectAllEntries, HALF_LIFE_BASE_META_KEY } from './store.js';
|
|
24
24
|
import { openHippoDb, closeHippoDb, getMeta, setMeta } from './db.js';
|
|
25
25
|
import { appendAuditEvent } from './audit.js';
|
|
26
26
|
/** The base every store used before the base was recorded. */
|
|
@@ -65,8 +65,7 @@ function readBase(db) {
|
|
|
65
65
|
export function migrateDefaultHalfLife(hippoRoot, to, opts = {}) {
|
|
66
66
|
const dryRun = opts.dryRun ?? false;
|
|
67
67
|
const noop = (from) => ({ from, to, rescaled: 0, kept: 0, dryRun, halfLives: new Map() });
|
|
68
|
-
|
|
69
|
-
const db = openHippoDb(hippoRoot);
|
|
68
|
+
const db = openStore(hippoRoot);
|
|
70
69
|
try {
|
|
71
70
|
// Plan, write, audit and record the base under one write lock, so a concurrent write or sleep cannot interleave.
|
|
72
71
|
if (!dryRun)
|
package/dist/reject-flow.js
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* raw-archive.ts and dormant.ts. Nothing imports FROM this file except
|
|
12
12
|
* cli.ts and api.ts, so it introduces no cycle.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { closeHippoDb } from './db.js';
|
|
15
15
|
import { appendAuditEvent } from './audit.js';
|
|
16
16
|
import { archiveRawMemory } from './raw-archive.js';
|
|
17
17
|
import { purgeDormantByDigest } from './dormant.js';
|
|
18
|
-
import {
|
|
18
|
+
import { openStore, deleteEntryCore, purgeMirrorBestEffort, } from './store.js';
|
|
19
19
|
import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
|
|
20
20
|
/**
|
|
21
21
|
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
@@ -46,8 +46,7 @@ export function rejectValue(opts) {
|
|
|
46
46
|
// and pollute the listing.
|
|
47
47
|
throw new Error('reject --value requires non-empty content.');
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
const db = openHippoDb(opts.hippoRoot);
|
|
49
|
+
const db = openStore(opts.hippoRoot);
|
|
51
50
|
try {
|
|
52
51
|
let content;
|
|
53
52
|
if (opts.memoryId !== undefined) {
|
|
@@ -170,8 +169,7 @@ export function unrejectValue(hippoRoot, tenantId, digestOrPrefix, actor) {
|
|
|
170
169
|
if (digestOrPrefix.trim().length === 0) {
|
|
171
170
|
return { status: 'not_found' };
|
|
172
171
|
}
|
|
173
|
-
|
|
174
|
-
const db = openHippoDb(hippoRoot);
|
|
172
|
+
const db = openStore(hippoRoot);
|
|
175
173
|
try {
|
|
176
174
|
const all = listRejectedValues(db, tenantId);
|
|
177
175
|
const matches = all.filter((r) => r.digest.startsWith(digestOrPrefix));
|
|
@@ -201,8 +199,7 @@ export function unrejectValue(hippoRoot, tenantId, digestOrPrefix, actor) {
|
|
|
201
199
|
}
|
|
202
200
|
/** `hippo rejections` / `api.listRejections` — list tombstones for a tenant. */
|
|
203
201
|
export function listRejectionsForTenant(hippoRoot, tenantId) {
|
|
204
|
-
|
|
205
|
-
const db = openHippoDb(hippoRoot);
|
|
202
|
+
const db = openStore(hippoRoot);
|
|
206
203
|
try {
|
|
207
204
|
return listRejectedValues(db, tenantId);
|
|
208
205
|
}
|
package/dist/secret-detect.d.ts
CHANGED
|
@@ -38,6 +38,6 @@ export declare function detectSecret(entry: {
|
|
|
38
38
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
39
39
|
*/
|
|
40
40
|
export declare function redactSecrets(text: string): string;
|
|
41
|
-
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
41
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
|
|
42
42
|
export declare function redactSecretsStrict(text: string): string;
|
|
43
43
|
//# sourceMappingURL=secret-detect.d.ts.map
|
package/dist/secret-detect.js
CHANGED
|
@@ -28,6 +28,13 @@ const SECRET_PATTERNS = [
|
|
|
28
28
|
{ name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
29
29
|
{ name: 'stripe-key', re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}\b/ },
|
|
30
30
|
{ name: 'google-api-key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
31
|
+
// hk_ is hippo's own API key (src/auth.ts); npm, Hugging Face, GitLab and Slack webhook shapes follow gitleaks' rules.
|
|
32
|
+
{ name: 'hippo-api-key', re: /\bhk_[a-z2-7]{24}\.[a-z2-7]{32}\b/ },
|
|
33
|
+
{ name: 'npm-token', re: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
34
|
+
{ name: 'huggingface-token', re: /\bhf_[A-Za-z]{34}\b/ },
|
|
35
|
+
{ name: 'gitlab-token', re: /\bglpat-[\w-]{20,}/ },
|
|
36
|
+
{ name: 'google-oauth-token', re: /\bya29\.[\w-]{20,}/ },
|
|
37
|
+
{ name: 'slack-webhook', re: /\bhooks\.slack\.com\/(?:services|workflows|triggers)\/[A-Za-z0-9+/]{43,56}/ },
|
|
31
38
|
{ name: 'private-key-block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
32
39
|
// sk-... (OpenAI/Anthropic style) and sk_<vendor>_... shapes. Both require
|
|
33
40
|
// a key-ish noun somewhere in the content (co-occurrence guard) so prose
|
|
@@ -51,6 +58,7 @@ const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
|
|
|
51
58
|
// redactSecretsStrict-only: too noisy for whole-entry memory scanning, worth hiding once text leaves the machine.
|
|
52
59
|
const STRICT_ONLY_PATTERNS = [
|
|
53
60
|
/\bbearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
|
|
61
|
+
/\bauthorization["']?\s*[:=]\s*["']?basic\s+[A-Za-z0-9+/]{8,}={0,2}/gi,
|
|
54
62
|
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
|
|
55
63
|
];
|
|
56
64
|
/**
|
|
@@ -82,7 +90,7 @@ export function detectSecret(entry) {
|
|
|
82
90
|
export function redactSecrets(text) {
|
|
83
91
|
return redactText(text, false);
|
|
84
92
|
}
|
|
85
|
-
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
93
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
|
|
86
94
|
export function redactSecretsStrict(text) {
|
|
87
95
|
return redactText(text, true);
|
|
88
96
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ export declare function assertNonEmpty<T>(arr: readonly T[], name: string): void
|
|
|
109
109
|
export declare function getHippoRoot(cwd?: string, opts?: ResolveProjectIdentityOpts): string;
|
|
110
110
|
export declare function isInitialized(hippoRoot: string): boolean;
|
|
111
111
|
export declare function initStore(hippoRoot: string): void;
|
|
112
|
+
/** One open connection with init done on it, for callers who used to pay for `initStore` + a second `openHippoDb`. */
|
|
113
|
+
export declare function openStore(hippoRoot: string): DatabaseSyncLike;
|
|
112
114
|
/** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
|
|
113
115
|
export declare const HALF_LIFE_BASE_META_KEY = "default_half_life_base";
|
|
114
116
|
/**
|
|
@@ -411,7 +413,16 @@ export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEn
|
|
|
411
413
|
export declare function loadAllEntries(hippoRoot: string, tenantId?: string): MemoryEntry[];
|
|
412
414
|
/** Every memory row on an open connection, so a caller can read inside its own transaction. */
|
|
413
415
|
export declare function selectAllEntries(db: DatabaseSyncLike, tenantId?: string): MemoryEntry[];
|
|
414
|
-
export declare function
|
|
416
|
+
export declare function loadContentsWithTag(hippoRoot: string, tenantId: string, tag: string): string[];
|
|
417
|
+
export interface AmbientRecallRequest {
|
|
418
|
+
terms: string[];
|
|
419
|
+
limit: number;
|
|
420
|
+
}
|
|
421
|
+
export interface AmbientLoadResult {
|
|
422
|
+
entries: MemoryEntry[];
|
|
423
|
+
recall?: MemoryEntry[];
|
|
424
|
+
}
|
|
425
|
+
export declare function loadAmbientCandidates(hippoRoot: string, tenantId: string, recentNeeded: number, admit: (e: MemoryEntry) => boolean, recall?: AmbientRecallRequest): AmbientLoadResult;
|
|
415
426
|
/**
|
|
416
427
|
* Load likely search candidates directly from SQLite.
|
|
417
428
|
* Uses FTS5 when available, falls back to LIKE matching, then full-store fallback.
|
package/dist/store.js
CHANGED
|
@@ -114,6 +114,10 @@ export function isInitialized(hippoRoot) {
|
|
|
114
114
|
return fs.existsSync(path.join(hippoRoot, 'hippo.db'));
|
|
115
115
|
}
|
|
116
116
|
export function initStore(hippoRoot) {
|
|
117
|
+
closeHippoDb(openStore(hippoRoot));
|
|
118
|
+
}
|
|
119
|
+
/** One open connection with init done on it, for callers who used to pay for `initStore` + a second `openHippoDb`. */
|
|
120
|
+
export function openStore(hippoRoot) {
|
|
117
121
|
ensureMirrorDirectories(hippoRoot);
|
|
118
122
|
const db = openHippoDb(hippoRoot);
|
|
119
123
|
try {
|
|
@@ -122,9 +126,16 @@ export function initStore(hippoRoot) {
|
|
|
122
126
|
syncMirrorFiles(hippoRoot, db);
|
|
123
127
|
}
|
|
124
128
|
recordHalfLifeBaseForNewStore(db);
|
|
129
|
+
return db;
|
|
125
130
|
}
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
catch (error) {
|
|
132
|
+
try {
|
|
133
|
+
closeHippoDb(db);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Best effort only; surface the original init error.
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
/** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
|
|
@@ -1112,8 +1123,7 @@ function syncMirrorFiles(hippoRoot, db) {
|
|
|
1112
1123
|
}
|
|
1113
1124
|
/** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
|
|
1114
1125
|
export function loadIndex(hippoRoot) {
|
|
1115
|
-
|
|
1116
|
-
const db = openHippoDb(hippoRoot);
|
|
1126
|
+
const db = openStore(hippoRoot);
|
|
1117
1127
|
try {
|
|
1118
1128
|
return buildIndexFromDb(db);
|
|
1119
1129
|
}
|
|
@@ -1132,8 +1142,7 @@ export function loadIndex(hippoRoot) {
|
|
|
1132
1142
|
* is left untouched; only `rebuildIndex` writes it.
|
|
1133
1143
|
*/
|
|
1134
1144
|
export function saveIndex(hippoRoot, index) {
|
|
1135
|
-
|
|
1136
|
-
const db = openHippoDb(hippoRoot);
|
|
1145
|
+
const db = openStore(hippoRoot);
|
|
1137
1146
|
try {
|
|
1138
1147
|
db.exec('BEGIN');
|
|
1139
1148
|
try {
|
|
@@ -1205,10 +1214,9 @@ function stampOriginProjectForImport(hippoRoot, entry) {
|
|
|
1205
1214
|
};
|
|
1206
1215
|
}
|
|
1207
1216
|
export function writeEntry(hippoRoot, entry, opts) {
|
|
1208
|
-
|
|
1209
|
-
const stamped = stampOriginProject(hippoRoot, entry);
|
|
1210
|
-
const db = openHippoDb(hippoRoot);
|
|
1217
|
+
const db = openStore(hippoRoot);
|
|
1211
1218
|
try {
|
|
1219
|
+
const stamped = stampOriginProject(hippoRoot, entry);
|
|
1212
1220
|
writeEntryDbOnly(db, stamped, opts);
|
|
1213
1221
|
opts?.afterCommit?.();
|
|
1214
1222
|
writeEntryMirrors(hippoRoot, stamped);
|
|
@@ -1287,8 +1295,7 @@ export function writeEntryMirrors(hippoRoot, entry) {
|
|
|
1287
1295
|
* legacy single-tenant callers and the writeEntry/readEntry round-trip.
|
|
1288
1296
|
*/
|
|
1289
1297
|
export function readEntry(hippoRoot, id, tenantId) {
|
|
1290
|
-
|
|
1291
|
-
const db = openHippoDb(hippoRoot);
|
|
1298
|
+
const db = openStore(hippoRoot);
|
|
1292
1299
|
try {
|
|
1293
1300
|
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1294
1301
|
// MemoryRow's field set.
|
|
@@ -1311,8 +1318,7 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
|
1311
1318
|
if (ids.length === 0)
|
|
1312
1319
|
return [];
|
|
1313
1320
|
const capped = ids.slice(0, 500);
|
|
1314
|
-
|
|
1315
|
-
const db = openHippoDb(hippoRoot);
|
|
1321
|
+
const db = openStore(hippoRoot);
|
|
1316
1322
|
try {
|
|
1317
1323
|
const placeholders = capped.map(() => '?').join(',');
|
|
1318
1324
|
// T2: no ORDER BY meant row order followed SQLite's IN(...) scan order
|
|
@@ -1384,8 +1390,7 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
|
|
|
1384
1390
|
export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
|
|
1385
1391
|
if (!sessionId)
|
|
1386
1392
|
return [];
|
|
1387
|
-
|
|
1388
|
-
const db = openHippoDb(hippoRoot);
|
|
1393
|
+
const db = openStore(hippoRoot);
|
|
1389
1394
|
try {
|
|
1390
1395
|
const params = [];
|
|
1391
1396
|
let sql = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE kind = 'raw' AND source_session_id = ? AND superseded_by IS NULL`;
|
|
@@ -1430,8 +1435,7 @@ export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
|
|
|
1430
1435
|
export function countSessionRawMemories(hippoRoot, sessionId, tenantId, scope) {
|
|
1431
1436
|
if (!sessionId)
|
|
1432
1437
|
return 0;
|
|
1433
|
-
|
|
1434
|
-
const db = openHippoDb(hippoRoot);
|
|
1438
|
+
const db = openStore(hippoRoot);
|
|
1435
1439
|
try {
|
|
1436
1440
|
const params = [];
|
|
1437
1441
|
let sql = `SELECT COUNT(*) AS c FROM memories WHERE kind = 'raw' AND source_session_id = ? AND superseded_by IS NULL`;
|
|
@@ -1484,8 +1488,7 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
|
1484
1488
|
if (count <= 0)
|
|
1485
1489
|
return [];
|
|
1486
1490
|
const capped = Math.min(count, 200);
|
|
1487
|
-
|
|
1488
|
-
const db = openHippoDb(hippoRoot);
|
|
1491
|
+
const db = openStore(hippoRoot);
|
|
1489
1492
|
try {
|
|
1490
1493
|
const params = [];
|
|
1491
1494
|
let sql = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE kind = 'raw' AND superseded_by IS NULL`;
|
|
@@ -1518,8 +1521,7 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
|
1518
1521
|
* Used by `drillDown` (Task 3).
|
|
1519
1522
|
*/
|
|
1520
1523
|
export function loadChildrenOf(hippoRoot, parentId, tenantId) {
|
|
1521
|
-
|
|
1522
|
-
const db = openHippoDb(hippoRoot);
|
|
1524
|
+
const db = openStore(hippoRoot);
|
|
1523
1525
|
try {
|
|
1524
1526
|
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1525
1527
|
// MemoryRow's field set.
|
|
@@ -1587,8 +1589,7 @@ export function deleteEntryCore(db, id, opts) {
|
|
|
1587
1589
|
* existing caller.
|
|
1588
1590
|
*/
|
|
1589
1591
|
export function deleteEntry(hippoRoot, id, opts) {
|
|
1590
|
-
|
|
1591
|
-
const db = openHippoDb(hippoRoot);
|
|
1592
|
+
const db = openStore(hippoRoot);
|
|
1592
1593
|
try {
|
|
1593
1594
|
const result = deleteEntryCore(db, id, opts);
|
|
1594
1595
|
if (!result)
|
|
@@ -1622,8 +1623,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
1622
1623
|
const dormantMoves = opts?.dormant ?? [];
|
|
1623
1624
|
if (toWrite.length === 0 && toDeleteIds.length === 0 && dormantMoves.length === 0)
|
|
1624
1625
|
return [];
|
|
1625
|
-
|
|
1626
|
-
const db = openHippoDb(hippoRoot);
|
|
1626
|
+
const db = openStore(hippoRoot);
|
|
1627
1627
|
try {
|
|
1628
1628
|
// BEGIN IMMEDIATE (codex delta-review P2): the AT1 tombstone probes below
|
|
1629
1629
|
// READ before the first write. Under a deferred BEGIN, that read pins a
|
|
@@ -1789,8 +1789,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
1789
1789
|
* paths that surface results to a user MUST pass a resolved tenant.
|
|
1790
1790
|
*/
|
|
1791
1791
|
export function loadAllEntries(hippoRoot, tenantId) {
|
|
1792
|
-
|
|
1793
|
-
const db = openHippoDb(hippoRoot);
|
|
1792
|
+
const db = openStore(hippoRoot);
|
|
1794
1793
|
try {
|
|
1795
1794
|
return selectAllEntries(db, tenantId);
|
|
1796
1795
|
}
|
|
@@ -1807,15 +1806,26 @@ export function selectAllEntries(db, tenantId) {
|
|
|
1807
1806
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
1808
1807
|
return rows.map(rowToEntry);
|
|
1809
1808
|
}
|
|
1809
|
+
// Content of every tenant row tagged `tag`, without reading the rest of the store.
|
|
1810
|
+
// `instr` is a substring prefilter over the raw JSON; `includes` below re-checks exactly.
|
|
1811
|
+
export function loadContentsWithTag(hippoRoot, tenantId, tag) {
|
|
1812
|
+
const db = openStore(hippoRoot);
|
|
1813
|
+
try {
|
|
1814
|
+
/** SAFETY: rows' shape matches the two columns named in the SELECT below. */
|
|
1815
|
+
const rows = db.prepare(`SELECT content, tags_json FROM memories WHERE tenant_id = ? AND instr(tags_json, ?) > 0`).all(tenantId, JSON.stringify(tag));
|
|
1816
|
+
return rows.filter((r) => parseJsonArray(r.tags_json).includes(tag)).map((r) => r.content);
|
|
1817
|
+
}
|
|
1818
|
+
finally {
|
|
1819
|
+
closeHippoDb(db);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1810
1822
|
// The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
|
|
1811
|
-
// injection. One connection
|
|
1812
|
-
|
|
1813
|
-
export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit) {
|
|
1814
|
-
initStore(hippoRoot);
|
|
1823
|
+
// injection. One connection; `recall` piggybacks the Z1 FTS query on it too.
|
|
1824
|
+
export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
|
|
1815
1825
|
// A SQL LIMIT takes an integer; the Array.slice this replaced truncated one,
|
|
1816
1826
|
// and include_recent is any non-negative finite number at the HTTP edge.
|
|
1817
1827
|
const needed = Math.trunc(recentNeeded);
|
|
1818
|
-
const db =
|
|
1828
|
+
const db = openStore(hippoRoot);
|
|
1819
1829
|
try {
|
|
1820
1830
|
// SAFETY: every `where` below starts from MEMORY_SELECT_COLUMNS' table.
|
|
1821
1831
|
const run = (where, params) => db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE ${where}`).all(...params).map(rowToEntry);
|
|
@@ -1843,10 +1853,17 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit)
|
|
|
1843
1853
|
}
|
|
1844
1854
|
// loadAllEntries' order: rankedPinned's comparator can tie and Array.sort
|
|
1845
1855
|
// is stable, so input order is load-bearing downstream.
|
|
1846
|
-
|
|
1856
|
+
const entries = [...byId.values()].sort((a, b) => {
|
|
1847
1857
|
const byCreated = a.created.localeCompare(b.created);
|
|
1848
1858
|
return byCreated !== 0 ? byCreated : a.id.localeCompare(b.id);
|
|
1849
1859
|
});
|
|
1860
|
+
if (!recall)
|
|
1861
|
+
return { entries };
|
|
1862
|
+
const ftsQuery = pickRarestFtsQuery(db, recall.terms);
|
|
1863
|
+
const recallEntries = ftsQuery
|
|
1864
|
+
? loadRecallSearchEntriesFromDb(db, ftsQuery, recall.limit, tenantId, undefined, 'exact', false)
|
|
1865
|
+
: [];
|
|
1866
|
+
return { entries, recall: recallEntries };
|
|
1850
1867
|
}
|
|
1851
1868
|
finally {
|
|
1852
1869
|
closeHippoDb(db);
|
|
@@ -1860,8 +1877,7 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit)
|
|
|
1860
1877
|
* by tenant_id. Cross-tenant memories never surface. Omitted = no filter.
|
|
1861
1878
|
*/
|
|
1862
1879
|
export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId) {
|
|
1863
|
-
|
|
1864
|
-
const db = openHippoDb(hippoRoot);
|
|
1880
|
+
const db = openStore(hippoRoot);
|
|
1865
1881
|
try {
|
|
1866
1882
|
return loadSearchRows(db, query, limit, tenantId).map(rowToEntry);
|
|
1867
1883
|
}
|
|
@@ -1894,8 +1910,7 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
|
|
|
1894
1910
|
* undefined as "no tenant filter" for legacy callers.
|
|
1895
1911
|
*/
|
|
1896
1912
|
export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true) {
|
|
1897
|
-
|
|
1898
|
-
const db = openHippoDb(hippoRoot);
|
|
1913
|
+
const db = openStore(hippoRoot);
|
|
1899
1914
|
try {
|
|
1900
1915
|
return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded);
|
|
1901
1916
|
}
|
|
@@ -1941,8 +1956,7 @@ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
|
|
|
1941
1956
|
* Rebuild mirrors from SQLite, importing any legacy markdown files not already present.
|
|
1942
1957
|
*/
|
|
1943
1958
|
export function rebuildIndex(hippoRoot) {
|
|
1944
|
-
|
|
1945
|
-
const db = openHippoDb(hippoRoot);
|
|
1959
|
+
const db = openStore(hippoRoot);
|
|
1946
1960
|
try {
|
|
1947
1961
|
// SAFETY: rows' shape matches the single `id` column selected above.
|
|
1948
1962
|
const existingIds = new Set(db.prepare(`SELECT id FROM memories`).all().map((row) => row.id));
|
|
@@ -1993,8 +2007,7 @@ export function rebuildIndex(hippoRoot) {
|
|
|
1993
2007
|
}
|
|
1994
2008
|
}
|
|
1995
2009
|
export function updateStats(hippoRoot, delta) {
|
|
1996
|
-
|
|
1997
|
-
const db = openHippoDb(hippoRoot);
|
|
2010
|
+
const db = openStore(hippoRoot);
|
|
1998
2011
|
try {
|
|
1999
2012
|
// One atomic statement per counter, and only for counters the caller
|
|
2000
2013
|
// named: the read-modify-write this replaces both lost increments to a
|
|
@@ -2021,8 +2034,7 @@ export function updateStats(hippoRoot, delta) {
|
|
|
2021
2034
|
}
|
|
2022
2035
|
}
|
|
2023
2036
|
export function loadStats(hippoRoot) {
|
|
2024
|
-
|
|
2025
|
-
const db = openHippoDb(hippoRoot);
|
|
2037
|
+
const db = openStore(hippoRoot);
|
|
2026
2038
|
try {
|
|
2027
2039
|
return buildStatsFromDb(db);
|
|
2028
2040
|
}
|
|
@@ -2031,8 +2043,7 @@ export function loadStats(hippoRoot) {
|
|
|
2031
2043
|
}
|
|
2032
2044
|
}
|
|
2033
2045
|
export function appendConsolidationRun(hippoRoot, run) {
|
|
2034
|
-
|
|
2035
|
-
const db = openHippoDb(hippoRoot);
|
|
2046
|
+
const db = openStore(hippoRoot);
|
|
2036
2047
|
try {
|
|
2037
2048
|
db.prepare(`INSERT INTO consolidation_runs(timestamp, decayed, merged, removed) VALUES (?, ?, ?, ?)`).run(run.timestamp, run.decayed, run.merged, run.removed);
|
|
2038
2049
|
pruneConsolidationRuns(db, 50);
|
|
@@ -2044,8 +2055,7 @@ export function appendConsolidationRun(hippoRoot, run) {
|
|
|
2044
2055
|
}
|
|
2045
2056
|
/** Rows a tenant created since the last sleep (runs are host-wide), looking back at most 24 hours. */
|
|
2046
2057
|
export function countCreatedSinceLastSleep(hippoRoot, tenantId, now = new Date()) {
|
|
2047
|
-
|
|
2048
|
-
const db = openHippoDb(hippoRoot);
|
|
2058
|
+
const db = openStore(hippoRoot);
|
|
2049
2059
|
try {
|
|
2050
2060
|
const dayAgo = new Date(now.getTime() - 86_400_000).toISOString();
|
|
2051
2061
|
const row = db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE tenant_id = ?
|
|
@@ -2061,8 +2071,7 @@ export function countCreatedSinceLastSleep(hippoRoot, tenantId, now = new Date()
|
|
|
2061
2071
|
* Uses consolidation_runs timestamps to compute session intervals.
|
|
2062
2072
|
*/
|
|
2063
2073
|
export function loadSessionDecayContext(hippoRoot) {
|
|
2064
|
-
|
|
2065
|
-
const db = openHippoDb(hippoRoot);
|
|
2074
|
+
const db = openStore(hippoRoot);
|
|
2066
2075
|
try {
|
|
2067
2076
|
// Get recent consolidation timestamps (last 20)
|
|
2068
2077
|
// SAFETY: rows' shape matches the single `timestamp` column above.
|
|
@@ -2089,8 +2098,7 @@ export function loadSessionDecayContext(hippoRoot) {
|
|
|
2089
2098
|
* Increment the sleep counter. Called after each consolidation run.
|
|
2090
2099
|
*/
|
|
2091
2100
|
export function incrementSleepCount(hippoRoot) {
|
|
2092
|
-
|
|
2093
|
-
const db = openHippoDb(hippoRoot);
|
|
2101
|
+
const db = openStore(hippoRoot);
|
|
2094
2102
|
try {
|
|
2095
2103
|
const current = Number(getMeta(db, 'sleep_count', '0')) || 0;
|
|
2096
2104
|
setMeta(db, 'sleep_count', String(current + 1));
|
|
@@ -2127,8 +2135,7 @@ export function assertTenantId(fnName, value) {
|
|
|
2127
2135
|
}
|
|
2128
2136
|
export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
|
|
2129
2137
|
assertTenantId('saveActiveTaskSnapshot', tenantId);
|
|
2130
|
-
|
|
2131
|
-
const db = openHippoDb(hippoRoot);
|
|
2138
|
+
const db = openStore(hippoRoot);
|
|
2132
2139
|
const now = new Date().toISOString();
|
|
2133
2140
|
try {
|
|
2134
2141
|
db.exec('BEGIN');
|
|
@@ -2167,8 +2174,7 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
|
|
|
2167
2174
|
}
|
|
2168
2175
|
export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
|
|
2169
2176
|
assertTenantId('loadActiveTaskSnapshot', tenantId);
|
|
2170
|
-
|
|
2171
|
-
const db = openHippoDb(hippoRoot);
|
|
2177
|
+
const db = openStore(hippoRoot);
|
|
2172
2178
|
try {
|
|
2173
2179
|
// SAFETY: row's shape matches the ten columns named in the SELECT above.
|
|
2174
2180
|
const row = db.prepare(`
|
|
@@ -2243,8 +2249,7 @@ export function loadFreshActiveTaskSnapshot(hippoRoot, tenantId, opts = {}) {
|
|
|
2243
2249
|
}
|
|
2244
2250
|
export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cleared') {
|
|
2245
2251
|
assertTenantId('clearActiveTaskSnapshot', tenantId);
|
|
2246
|
-
|
|
2247
|
-
const db = openHippoDb(hippoRoot);
|
|
2252
|
+
const db = openStore(hippoRoot);
|
|
2248
2253
|
const now = new Date().toISOString();
|
|
2249
2254
|
try {
|
|
2250
2255
|
// SAFETY: active's shape matches the single `id` column selected above.
|
|
@@ -2272,8 +2277,7 @@ export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cl
|
|
|
2272
2277
|
*/
|
|
2273
2278
|
export function closeTaskSnapshotsForSession(hippoRoot, tenantId, sessionId, status = 'session-ended') {
|
|
2274
2279
|
assertTenantId('closeTaskSnapshotsForSession', tenantId);
|
|
2275
|
-
|
|
2276
|
-
const db = openHippoDb(hippoRoot);
|
|
2280
|
+
const db = openStore(hippoRoot);
|
|
2277
2281
|
const now = new Date().toISOString();
|
|
2278
2282
|
try {
|
|
2279
2283
|
const result = db.prepare(`UPDATE task_snapshots SET status = ?, updated_at = ? WHERE status = 'active' AND tenant_id = ? AND session_id = ?`).run(status, now, tenantId, sessionId);
|
|
@@ -2285,8 +2289,7 @@ export function closeTaskSnapshotsForSession(hippoRoot, tenantId, sessionId, sta
|
|
|
2285
2289
|
}
|
|
2286
2290
|
export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
2287
2291
|
assertTenantId('appendSessionEvent', tenantId);
|
|
2288
|
-
|
|
2289
|
-
const db = openHippoDb(hippoRoot);
|
|
2292
|
+
const db = openStore(hippoRoot);
|
|
2290
2293
|
const now = new Date().toISOString();
|
|
2291
2294
|
// v1.2: scope is wired through. Default-deny in api.recall + cmdRecall
|
|
2292
2295
|
// continuity reads applies to slack:private:* and 'unknown:legacy' rows.
|
|
@@ -2326,8 +2329,7 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
|
2326
2329
|
}
|
|
2327
2330
|
export function listSessionEvents(hippoRoot, tenantId, options = {}) {
|
|
2328
2331
|
assertTenantId('listSessionEvents', tenantId);
|
|
2329
|
-
|
|
2330
|
-
const db = openHippoDb(hippoRoot);
|
|
2332
|
+
const db = openStore(hippoRoot);
|
|
2331
2333
|
try {
|
|
2332
2334
|
const clauses = ['tenant_id = ?'];
|
|
2333
2335
|
const params = [tenantId];
|
|
@@ -2363,8 +2365,7 @@ export function listSessionEvents(hippoRoot, tenantId, options = {}) {
|
|
|
2363
2365
|
*/
|
|
2364
2366
|
export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
|
|
2365
2367
|
assertTenantId('findPromotableSessions', tenantId);
|
|
2366
|
-
|
|
2367
|
-
const db = openHippoDb(hippoRoot);
|
|
2368
|
+
const db = openStore(hippoRoot);
|
|
2368
2369
|
try {
|
|
2369
2370
|
// SAFETY: rows' shape matches the single `session_id` column selected
|
|
2370
2371
|
// above.
|
|
@@ -2384,8 +2385,7 @@ export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
|
|
|
2384
2385
|
*/
|
|
2385
2386
|
export function traceExistsForSession(hippoRoot, tenantId, session_id) {
|
|
2386
2387
|
assertTenantId('traceExistsForSession', tenantId);
|
|
2387
|
-
|
|
2388
|
-
const db = openHippoDb(hippoRoot);
|
|
2388
|
+
const db = openStore(hippoRoot);
|
|
2389
2389
|
try {
|
|
2390
2390
|
const row = db.prepare(`
|
|
2391
2391
|
SELECT 1 FROM memories
|
|
@@ -2399,8 +2399,7 @@ export function traceExistsForSession(hippoRoot, tenantId, session_id) {
|
|
|
2399
2399
|
}
|
|
2400
2400
|
}
|
|
2401
2401
|
export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
|
|
2402
|
-
|
|
2403
|
-
const db = openHippoDb(hippoRoot);
|
|
2402
|
+
const db = openStore(hippoRoot);
|
|
2404
2403
|
try {
|
|
2405
2404
|
// v0.28 — '*' is a sentinel meaning "no status filter, return all rows".
|
|
2406
2405
|
// Pre-v0.28 callers (cli/mcp/dashboard) always passed 'open' or default,
|
|
@@ -2458,8 +2457,7 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
|
|
|
2458
2457
|
}
|
|
2459
2458
|
}
|
|
2460
2459
|
export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new Date().toISOString()) {
|
|
2461
|
-
|
|
2462
|
-
const db = openHippoDb(hippoRoot);
|
|
2460
|
+
const db = openStore(hippoRoot);
|
|
2463
2461
|
try {
|
|
2464
2462
|
db.exec('BEGIN');
|
|
2465
2463
|
// Tenant guard (E2): a conflict is meaningful only within one tenant.
|
|
@@ -2574,8 +2572,7 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
2574
2572
|
* Returns the resolved conflict, or null if not found.
|
|
2575
2573
|
*/
|
|
2576
2574
|
export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = false, tenantId, opts) {
|
|
2577
|
-
|
|
2578
|
-
const db = openHippoDb(hippoRoot);
|
|
2575
|
+
const db = openStore(hippoRoot);
|
|
2579
2576
|
// When tenantId is set, the conflict lookup requires BOTH members in-tenant
|
|
2580
2577
|
// and every memories mutation carries AND tenant_id = ?. A cross-tenant probe
|
|
2581
2578
|
// then returns null, indistinguishable from a bad id. Omitted tenantId =
|
|
@@ -2798,8 +2795,7 @@ const HANDOFF_COLUMNS = 'id, session_id, repo_root, task_id, summary, next_actio
|
|
|
2798
2795
|
*/
|
|
2799
2796
|
export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
|
|
2800
2797
|
assertTenantId('saveSessionHandoff', tenantId);
|
|
2801
|
-
|
|
2802
|
-
const db = openHippoDb(hippoRoot);
|
|
2798
|
+
const db = openStore(hippoRoot);
|
|
2803
2799
|
const now = new Date().toISOString();
|
|
2804
2800
|
// v1.2: scope is wired through. Read-side default-deny in api.recall +
|
|
2805
2801
|
// cmdRecall continuity excludes slack:private:* and 'unknown:legacy'.
|
|
@@ -2827,8 +2823,7 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
|
|
|
2827
2823
|
/** Load the most recent handoff, optionally filtered by session ID. */
|
|
2828
2824
|
export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
|
|
2829
2825
|
assertTenantId('loadLatestHandoff', tenantId);
|
|
2830
|
-
|
|
2831
|
-
const db = openHippoDb(hippoRoot);
|
|
2826
|
+
const db = openStore(hippoRoot);
|
|
2832
2827
|
try {
|
|
2833
2828
|
const conditions = ['tenant_id = ?'];
|
|
2834
2829
|
const params = [tenantId];
|
|
@@ -2872,8 +2867,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
|
|
|
2872
2867
|
*/
|
|
2873
2868
|
export function loadHandoffById(hippoRoot, tenantId, id) {
|
|
2874
2869
|
assertTenantId('loadHandoffById', tenantId);
|
|
2875
|
-
|
|
2876
|
-
const db = openHippoDb(hippoRoot);
|
|
2870
|
+
const db = openStore(hippoRoot);
|
|
2877
2871
|
try {
|
|
2878
2872
|
// SAFETY: row's shape matches HANDOFF_COLUMNS.
|
|
2879
2873
|
const row = db.prepare(`
|
|
@@ -2890,8 +2884,7 @@ export function loadHandoffById(hippoRoot, tenantId, id) {
|
|
|
2890
2884
|
/** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
|
|
2891
2885
|
export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
|
|
2892
2886
|
assertTenantId('stampHandoffOutcome', tenantId);
|
|
2893
|
-
|
|
2894
|
-
const db = openHippoDb(hippoRoot);
|
|
2887
|
+
const db = openStore(hippoRoot);
|
|
2895
2888
|
try {
|
|
2896
2889
|
const result = db.prepare(`
|
|
2897
2890
|
UPDATE session_handoffs SET outcome = ?
|
|
@@ -3059,8 +3052,7 @@ export function createCard(hippoRoot, tenantId, input) {
|
|
|
3059
3052
|
if (input.budget !== undefined && !(Number.isSafeInteger(input.budget) && input.budget > 0)) {
|
|
3060
3053
|
throw new Error(`Invalid budget: ${input.budget} (expected a positive integer)`);
|
|
3061
3054
|
}
|
|
3062
|
-
|
|
3063
|
-
const db = openHippoDb(hippoRoot);
|
|
3055
|
+
const db = openStore(hippoRoot);
|
|
3064
3056
|
try {
|
|
3065
3057
|
const dependsOn = [...new Set(input.dependsOn ?? [])];
|
|
3066
3058
|
let id = '';
|
|
@@ -3112,8 +3104,7 @@ export function createCard(hippoRoot, tenantId, input) {
|
|
|
3112
3104
|
/** Returns the card row for id, or null if it does not exist under this tenant. */
|
|
3113
3105
|
export function loadCard(hippoRoot, tenantId, id) {
|
|
3114
3106
|
assertTenantId('loadCard', tenantId);
|
|
3115
|
-
|
|
3116
|
-
const db = openHippoDb(hippoRoot);
|
|
3107
|
+
const db = openStore(hippoRoot);
|
|
3117
3108
|
try {
|
|
3118
3109
|
return loadCardRow(db, tenantId, id);
|
|
3119
3110
|
}
|
|
@@ -3124,8 +3115,7 @@ export function loadCard(hippoRoot, tenantId, id) {
|
|
|
3124
3115
|
/** Lists cards for this tenant, optionally filtered to one status, newest-updated first. */
|
|
3125
3116
|
export function listCards(hippoRoot, tenantId, opts = {}) {
|
|
3126
3117
|
assertTenantId('listCards', tenantId);
|
|
3127
|
-
|
|
3128
|
-
const db = openHippoDb(hippoRoot);
|
|
3118
|
+
const db = openStore(hippoRoot);
|
|
3129
3119
|
try {
|
|
3130
3120
|
const conditions = ['tenant_id = ?'];
|
|
3131
3121
|
const params = [tenantId];
|
|
@@ -3146,8 +3136,7 @@ export function listCards(hippoRoot, tenantId, opts = {}) {
|
|
|
3146
3136
|
/** Returns this card's parent and child ids from card_deps. */
|
|
3147
3137
|
export function loadCardDeps(hippoRoot, tenantId, id) {
|
|
3148
3138
|
assertTenantId('loadCardDeps', tenantId);
|
|
3149
|
-
|
|
3150
|
-
const db = openHippoDb(hippoRoot);
|
|
3139
|
+
const db = openStore(hippoRoot);
|
|
3151
3140
|
try {
|
|
3152
3141
|
// SAFETY: rows' shape matches the single `parent` column named in the SELECT below.
|
|
3153
3142
|
const parents = db.prepare(`SELECT parent FROM card_deps WHERE tenant_id = ? AND child = ?`).all(tenantId, id).map((r) => r.parent);
|
|
@@ -3162,8 +3151,7 @@ export function loadCardDeps(hippoRoot, tenantId, id) {
|
|
|
3162
3151
|
/** Returns this card's run history, most recent first. */
|
|
3163
3152
|
export function loadCardRuns(hippoRoot, tenantId, id) {
|
|
3164
3153
|
assertTenantId('loadCardRuns', tenantId);
|
|
3165
|
-
|
|
3166
|
-
const db = openHippoDb(hippoRoot);
|
|
3154
|
+
const db = openStore(hippoRoot);
|
|
3167
3155
|
try {
|
|
3168
3156
|
// SAFETY: rows' shape matches CardRunRow.
|
|
3169
3157
|
const rows = db.prepare(`
|
|
@@ -3179,8 +3167,7 @@ export function loadCardRuns(hippoRoot, tenantId, id) {
|
|
|
3179
3167
|
/** Returns this card's comments, most recent first. */
|
|
3180
3168
|
export function loadCardComments(hippoRoot, tenantId, id) {
|
|
3181
3169
|
assertTenantId('loadCardComments', tenantId);
|
|
3182
|
-
|
|
3183
|
-
const db = openHippoDb(hippoRoot);
|
|
3170
|
+
const db = openStore(hippoRoot);
|
|
3184
3171
|
try {
|
|
3185
3172
|
// SAFETY: rows' shape matches CardCommentRow.
|
|
3186
3173
|
const rows = db.prepare(`
|
|
@@ -3196,8 +3183,7 @@ export function loadCardComments(hippoRoot, tenantId, id) {
|
|
|
3196
3183
|
/** Read side of the card <-> handoff round trip: the newest handoff filed against this card. */
|
|
3197
3184
|
export function loadLatestHandoffForCard(hippoRoot, tenantId, cardId) {
|
|
3198
3185
|
assertTenantId('loadLatestHandoffForCard', tenantId);
|
|
3199
|
-
|
|
3200
|
-
const db = openHippoDb(hippoRoot);
|
|
3186
|
+
const db = openStore(hippoRoot);
|
|
3201
3187
|
try {
|
|
3202
3188
|
// SAFETY: row's shape matches HANDOFF_COLUMNS.
|
|
3203
3189
|
const row = db.prepare(`
|
|
@@ -3216,8 +3202,7 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
|
|
|
3216
3202
|
if (runtime.trim() === '') {
|
|
3217
3203
|
throw new Error('runtime must not be empty');
|
|
3218
3204
|
}
|
|
3219
|
-
|
|
3220
|
-
const db = openHippoDb(hippoRoot);
|
|
3205
|
+
const db = openStore(hippoRoot);
|
|
3221
3206
|
try {
|
|
3222
3207
|
db.exec('BEGIN IMMEDIATE');
|
|
3223
3208
|
let runId = 0;
|
|
@@ -3259,8 +3244,7 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
|
|
|
3259
3244
|
export function heartbeatCard(hippoRoot, tenantId, id, runId) {
|
|
3260
3245
|
assertTenantId('heartbeatCard', tenantId);
|
|
3261
3246
|
assertRunId(runId);
|
|
3262
|
-
|
|
3263
|
-
const db = openHippoDb(hippoRoot);
|
|
3247
|
+
const db = openStore(hippoRoot);
|
|
3264
3248
|
try {
|
|
3265
3249
|
db.exec('BEGIN IMMEDIATE');
|
|
3266
3250
|
try {
|
|
@@ -3298,8 +3282,7 @@ export function blockCard(hippoRoot, tenantId, id, reason, runId) {
|
|
|
3298
3282
|
}
|
|
3299
3283
|
if (runId !== undefined)
|
|
3300
3284
|
assertRunId(runId);
|
|
3301
|
-
|
|
3302
|
-
const db = openHippoDb(hippoRoot);
|
|
3285
|
+
const db = openStore(hippoRoot);
|
|
3303
3286
|
try {
|
|
3304
3287
|
db.exec('BEGIN IMMEDIATE');
|
|
3305
3288
|
try {
|
|
@@ -3336,8 +3319,7 @@ export function reviewCard(hippoRoot, tenantId, id, runId) {
|
|
|
3336
3319
|
assertTenantId('reviewCard', tenantId);
|
|
3337
3320
|
if (runId !== undefined)
|
|
3338
3321
|
assertRunId(runId);
|
|
3339
|
-
|
|
3340
|
-
const db = openHippoDb(hippoRoot);
|
|
3322
|
+
const db = openStore(hippoRoot);
|
|
3341
3323
|
try {
|
|
3342
3324
|
db.exec('BEGIN IMMEDIATE');
|
|
3343
3325
|
try {
|
|
@@ -3373,8 +3355,7 @@ export function completeCard(hippoRoot, tenantId, id, outcome, runId) {
|
|
|
3373
3355
|
}
|
|
3374
3356
|
if (runId !== undefined)
|
|
3375
3357
|
assertRunId(runId);
|
|
3376
|
-
|
|
3377
|
-
const db = openHippoDb(hippoRoot);
|
|
3358
|
+
const db = openStore(hippoRoot);
|
|
3378
3359
|
try {
|
|
3379
3360
|
db.exec('BEGIN IMMEDIATE');
|
|
3380
3361
|
let promotedChildren = [];
|
|
@@ -3430,8 +3411,7 @@ export function completeCard(hippoRoot, tenantId, id, outcome, runId) {
|
|
|
3430
3411
|
/** Returns to ready every running card of the tenant whose lease has expired or is missing: clears its assignee, closes its live run as 'reclaimed' and leaves its handoffs alone, all in one write transaction. Returns the reclaimed card ids in id order. */
|
|
3431
3412
|
export function reclaimExpiredCards(hippoRoot, tenantId) {
|
|
3432
3413
|
assertTenantId('reclaimExpiredCards', tenantId);
|
|
3433
|
-
|
|
3434
|
-
const db = openHippoDb(hippoRoot);
|
|
3414
|
+
const db = openStore(hippoRoot);
|
|
3435
3415
|
try {
|
|
3436
3416
|
db.exec('BEGIN IMMEDIATE');
|
|
3437
3417
|
try {
|
|
@@ -3468,8 +3448,7 @@ export function addCardComment(hippoRoot, tenantId, cardId, author, body) {
|
|
|
3468
3448
|
if (body.trim() === '') {
|
|
3469
3449
|
throw new Error('body must not be empty');
|
|
3470
3450
|
}
|
|
3471
|
-
|
|
3472
|
-
const db = openHippoDb(hippoRoot);
|
|
3451
|
+
const db = openStore(hippoRoot);
|
|
3473
3452
|
try {
|
|
3474
3453
|
return insertCardComment(db, tenantId, cardId, author, body);
|
|
3475
3454
|
}
|
|
@@ -3496,8 +3475,7 @@ export function addCardComment(hippoRoot, tenantId, cardId, author, body) {
|
|
|
3496
3475
|
*/
|
|
3497
3476
|
export function loadDirtySummaries(hippoRoot, tenantId) {
|
|
3498
3477
|
assertTenantId('loadDirtySummaries', tenantId);
|
|
3499
|
-
|
|
3500
|
-
const db = openHippoDb(hippoRoot);
|
|
3478
|
+
const db = openStore(hippoRoot);
|
|
3501
3479
|
try {
|
|
3502
3480
|
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
3503
3481
|
// MemoryRow's field set.
|
|
@@ -3566,8 +3544,7 @@ export function markSummaryDirtyInTx(db, summaryId, tenantId, actor) {
|
|
|
3566
3544
|
*/
|
|
3567
3545
|
export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli') {
|
|
3568
3546
|
assertTenantId('markSummaryDirty', tenantId);
|
|
3569
|
-
|
|
3570
|
-
const db = openHippoDb(hippoRoot);
|
|
3547
|
+
const db = openStore(hippoRoot);
|
|
3571
3548
|
try {
|
|
3572
3549
|
// v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
|
|
3573
3550
|
// actual level in same round trip.
|
|
@@ -3614,8 +3591,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
3614
3591
|
* tenant-scoped via summary.tenantId.
|
|
3615
3592
|
*/
|
|
3616
3593
|
export function loadAllL2Summaries(hippoRoot) {
|
|
3617
|
-
|
|
3618
|
-
const db = openHippoDb(hippoRoot);
|
|
3594
|
+
const db = openStore(hippoRoot);
|
|
3619
3595
|
try {
|
|
3620
3596
|
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
3621
3597
|
// MemoryRow's field set.
|
|
@@ -3644,8 +3620,7 @@ export function loadAllL2Summaries(hippoRoot) {
|
|
|
3644
3620
|
* HIPPO_DAG_REBUILD_CAP takes most-recently-changed summaries first.
|
|
3645
3621
|
*/
|
|
3646
3622
|
export function loadAllDirtySummaries(hippoRoot) {
|
|
3647
|
-
|
|
3648
|
-
const db = openHippoDb(hippoRoot);
|
|
3623
|
+
const db = openStore(hippoRoot);
|
|
3649
3624
|
try {
|
|
3650
3625
|
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
3651
3626
|
// MemoryRow's field set.
|
|
@@ -3671,8 +3646,7 @@ export function loadAllDirtySummaries(hippoRoot) {
|
|
|
3671
3646
|
*/
|
|
3672
3647
|
export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
|
|
3673
3648
|
assertTenantId('loadChildrenOfSummary', tenantId);
|
|
3674
|
-
|
|
3675
|
-
const db = openHippoDb(hippoRoot);
|
|
3649
|
+
const db = openStore(hippoRoot);
|
|
3676
3650
|
try {
|
|
3677
3651
|
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
3678
3652
|
// MemoryRow's field set.
|
|
@@ -3705,8 +3679,7 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
|
|
|
3705
3679
|
*/
|
|
3706
3680
|
export function applyRebuildResult(hippoRoot, summary, patch) {
|
|
3707
3681
|
assertTenantId('applyRebuildResult', summary.tenantId);
|
|
3708
|
-
|
|
3709
|
-
const db = openHippoDb(hippoRoot);
|
|
3682
|
+
const db = openStore(hippoRoot);
|
|
3710
3683
|
try {
|
|
3711
3684
|
db.exec('SAVEPOINT rebuild_summary');
|
|
3712
3685
|
try {
|
|
@@ -3853,8 +3826,7 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
|
|
|
3853
3826
|
*/
|
|
3854
3827
|
export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, actor = 'cli', source = 'buildDag-clean') {
|
|
3855
3828
|
assertTenantId('clearSummaryDirtyAfterBuild', tenantId);
|
|
3856
|
-
|
|
3857
|
-
const db = openHippoDb(hippoRoot);
|
|
3829
|
+
const db = openStore(hippoRoot);
|
|
3858
3830
|
try {
|
|
3859
3831
|
// v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
|
|
3860
3832
|
// actual level so audit metadata stays accurate without an extra SELECT.
|
package/dist/support-bundle.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/** `hippo support-bundle`: one redacted JSON snapshot for a support ticket. Read-only; never touches memory content. */
|
|
1
|
+
/** `hippo support-bundle`: one redacted JSON snapshot for a support ticket. Read-only (SQLite may leave empty -wal and -shm files); never touches memory content. */
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
5
6
|
import { findHippoStoreDir, isGlobalStoreRoot, realpathOrResolve } from './project-identity.js';
|
|
6
7
|
import { getGlobalRoot } from './shared.js';
|
|
7
8
|
import { isInitialized } from './store.js';
|
|
@@ -189,18 +190,11 @@ function nativeRealPathKey(p) {
|
|
|
189
190
|
return null;
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const forms = new Set();
|
|
195
|
-
const add = (s) => { if (s.length >= 3)
|
|
196
|
-
forms.add(s); };
|
|
193
|
+
function collectHomeAliases(home, probes) {
|
|
194
|
+
const aliases = new Set();
|
|
197
195
|
for (const base of [home, realpathOrResolve(home), os.homedir(), realpathOrResolve(os.homedir())]) {
|
|
198
|
-
add(base);
|
|
199
|
-
add(path.resolve(base));
|
|
200
|
-
if (process.platform === 'win32') {
|
|
201
|
-
add(base.replace(/\\/g, '/'));
|
|
202
|
-
add(path.resolve(base).replace(/\\/g, '/'));
|
|
203
|
-
}
|
|
196
|
+
aliases.add(base);
|
|
197
|
+
aliases.add(path.resolve(base));
|
|
204
198
|
}
|
|
205
199
|
// The alias (e.g. a short-name temp root) can sit above the probe itself, so walk ancestors and
|
|
206
200
|
// compare native realpaths rather than checking each probe alone.
|
|
@@ -209,34 +203,56 @@ function collectHomeForms(home, probes) {
|
|
|
209
203
|
let dir = path.resolve(probe);
|
|
210
204
|
for (;;) {
|
|
211
205
|
const key = nativeRealPathKey(dir);
|
|
212
|
-
if (key !== null && homeKeys.has(key))
|
|
213
|
-
add(dir);
|
|
214
|
-
if (process.platform === 'win32')
|
|
215
|
-
add(dir.replace(/\\/g, '/'));
|
|
216
|
-
}
|
|
206
|
+
if (key !== null && homeKeys.has(key))
|
|
207
|
+
aliases.add(dir);
|
|
217
208
|
const parent = path.dirname(dir);
|
|
218
209
|
if (parent === dir)
|
|
219
210
|
break;
|
|
220
211
|
dir = parent;
|
|
221
212
|
}
|
|
222
213
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
214
|
+
return aliases;
|
|
215
|
+
}
|
|
216
|
+
// Node's own encoding, so it matches file URLs and ESM stack frames; a drive path's URL gains a leading slash.
|
|
217
|
+
function fileUrlPath(p) {
|
|
218
|
+
const pathname = pathToFileURL(p).pathname;
|
|
219
|
+
return /^\/[A-Za-z]:/.test(pathname) ? pathname.slice(1) : pathname;
|
|
220
|
+
}
|
|
221
|
+
// A home turns up as a path, as a file URL's percent-encoded path, and as a Claude Code project folder name.
|
|
222
|
+
const HOME_SPELLINGS = [
|
|
223
|
+
(p) => p,
|
|
224
|
+
fileUrlPath,
|
|
225
|
+
(p) => p.replace(/[^A-Za-z0-9]/g, '-'),
|
|
226
|
+
];
|
|
227
|
+
// A Windows separator can be written \, / or JSON-escaped (\\ in a log line quoting JSON), so any of them matches.
|
|
228
|
+
function spellingPattern(spelling) {
|
|
229
|
+
if (process.platform !== 'win32')
|
|
230
|
+
return escapeRegExp(spelling);
|
|
231
|
+
return spelling.split(/[\\/]+/).map(escapeRegExp).join('(?:\\\\+|/)');
|
|
233
232
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
/** One pattern for every alias of the home in every spelling, or null when none is long enough to swap safely. */
|
|
234
|
+
function buildHomePattern(home, probes) {
|
|
235
|
+
const spellings = new Set();
|
|
236
|
+
for (const alias of collectHomeAliases(home, probes)) {
|
|
237
|
+
for (const spell of HOME_SPELLINGS) {
|
|
238
|
+
const s = spell(alias);
|
|
239
|
+
if (s.length >= 3)
|
|
240
|
+
spellings.add(s);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Longest first: where one home nests in another (a test home under the real one), swap the longer.
|
|
244
|
+
const patterns = new Map();
|
|
245
|
+
for (const s of [...spellings].sort((a, b) => b.length - a.length)) {
|
|
246
|
+
const pattern = spellingPattern(s);
|
|
247
|
+
const key = process.platform === 'win32' ? pattern.toLowerCase() : pattern;
|
|
248
|
+
if (!patterns.has(key))
|
|
249
|
+
patterns.set(key, pattern);
|
|
250
|
+
}
|
|
251
|
+
if (patterns.size === 0)
|
|
252
|
+
return null;
|
|
253
|
+
// Whole names only: "<home> now" and "<home>." swap; "<home>ty", or "web-app" for a home of /app, do not.
|
|
254
|
+
const alternatives = [...patterns.values()].join('|');
|
|
255
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${alternatives})(?![\\p{L}\\p{N}_])`, process.platform === 'win32' ? 'giu' : 'gu');
|
|
240
256
|
}
|
|
241
257
|
const URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi;
|
|
242
258
|
function redactUrlsInString(text) {
|
|
@@ -255,37 +271,40 @@ function redactUrlsInString(text) {
|
|
|
255
271
|
}
|
|
256
272
|
});
|
|
257
273
|
}
|
|
258
|
-
function redactString(text,
|
|
259
|
-
|
|
274
|
+
function redactString(text, homePattern) {
|
|
275
|
+
const scrubbed = redactSecretsStrict(redactUrlsInString(text));
|
|
276
|
+
return homePattern === null ? scrubbed : scrubbed.replace(homePattern, '~');
|
|
260
277
|
}
|
|
261
|
-
function redactStrings(value,
|
|
278
|
+
function redactStrings(value, homePattern) {
|
|
262
279
|
if (isJsonString(value))
|
|
263
|
-
return redactString(value,
|
|
280
|
+
return redactString(value, homePattern);
|
|
264
281
|
if (Array.isArray(value))
|
|
265
|
-
return value.map((v) => redactStrings(v,
|
|
282
|
+
return value.map((v) => redactStrings(v, homePattern));
|
|
266
283
|
if (isJsonObject(value)) {
|
|
267
284
|
const out = {};
|
|
268
285
|
for (const [k, v] of Object.entries(value))
|
|
269
|
-
out[k] = redactStrings(v,
|
|
286
|
+
out[k] = redactStrings(v, homePattern);
|
|
270
287
|
return out;
|
|
271
288
|
}
|
|
272
289
|
return value;
|
|
273
290
|
}
|
|
274
291
|
/** Read-only: builds one redacted support-ticket snapshot. Never reads a memory content column. */
|
|
275
292
|
export function buildSupportBundle(opts) {
|
|
293
|
+
// Before doctor: its read-only open leaves an empty -wal that the store's file list would then report.
|
|
294
|
+
const stores = buildStores(opts);
|
|
276
295
|
const bundle = {
|
|
277
296
|
format: 'hippo-support-bundle/1',
|
|
278
297
|
createdAt: opts.now.toISOString(),
|
|
279
298
|
hippo: opts.version,
|
|
280
299
|
runtime: buildRuntime(),
|
|
281
300
|
doctor: JSON.parse(JSON.stringify(runDoctor(opts))),
|
|
282
|
-
stores
|
|
301
|
+
stores,
|
|
283
302
|
env: listSetEnvNames(),
|
|
284
303
|
logs: buildLogsSection(opts),
|
|
285
304
|
};
|
|
286
305
|
// Store paths found from the working folder arrive resolved; the global root arrives as HIPPO_HOME was typed, and log lines
|
|
287
306
|
// often quote the temp folder, which usually sits under the home.
|
|
288
|
-
const redacted = redactStrings(bundle,
|
|
307
|
+
const redacted = redactStrings(bundle, buildHomePattern(opts.home, [getGlobalRoot(), os.tmpdir()]));
|
|
289
308
|
// SAFETY: the value built above is always a JsonObject; redactStrings preserves object shape.
|
|
290
309
|
return redacted;
|
|
291
310
|
}
|
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.52.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.52.3";
|
|
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.52.
|
|
19
|
+
export const PACKAGE_VERSION = '1.52.3';
|
|
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/dist/working-memory.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* importance-ranked, and automatically evicted when the buffer
|
|
6
6
|
* exceeds WM_MAX_ENTRIES per scope.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { closeHippoDb } from './db.js';
|
|
9
|
+
import { openStore } from './store.js';
|
|
10
10
|
export const WM_MAX_ENTRIES = 20;
|
|
11
11
|
function rowToItem(row) {
|
|
12
12
|
let metadata = {};
|
|
@@ -38,8 +38,7 @@ function rowToItem(row) {
|
|
|
38
38
|
* Returns the new row ID.
|
|
39
39
|
*/
|
|
40
40
|
export function wmPush(hippoRoot, opts) {
|
|
41
|
-
|
|
42
|
-
const db = openHippoDb(hippoRoot);
|
|
41
|
+
const db = openStore(hippoRoot);
|
|
43
42
|
try {
|
|
44
43
|
const now = new Date().toISOString();
|
|
45
44
|
const importance = opts.importance ?? 0;
|
|
@@ -89,8 +88,7 @@ export function wmPush(hippoRoot, opts) {
|
|
|
89
88
|
* Read working memory entries, sorted by importance DESC.
|
|
90
89
|
*/
|
|
91
90
|
export function wmRead(hippoRoot, opts) {
|
|
92
|
-
|
|
93
|
-
const db = openHippoDb(hippoRoot);
|
|
91
|
+
const db = openStore(hippoRoot);
|
|
94
92
|
try {
|
|
95
93
|
const clauses = [];
|
|
96
94
|
const params = [];
|
|
@@ -124,8 +122,7 @@ export function wmRead(hippoRoot, opts) {
|
|
|
124
122
|
* Clear (delete) working memory entries. Returns the count deleted.
|
|
125
123
|
*/
|
|
126
124
|
export function wmClear(hippoRoot, opts) {
|
|
127
|
-
|
|
128
|
-
const db = openHippoDb(hippoRoot);
|
|
125
|
+
const db = openStore(hippoRoot);
|
|
129
126
|
try {
|
|
130
127
|
const clauses = [];
|
|
131
128
|
const params = [];
|
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.52.
|
|
5
|
+
"version": "1.52.3",
|
|
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.52.
|
|
3
|
+
"version": "1.52.3",
|
|
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",
|