hippo-memory 1.58.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/legacy.js +4 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.js +25 -9
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +3 -0
- package/dist/autolearn.js +5 -2
- package/dist/capture.js +8 -6
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +14 -8
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +356 -0
- package/dist/cli.js +1587 -1404
- package/dist/compaction-record.js +7 -6
- package/dist/config.js +12 -11
- package/dist/connectors/github/cli-impl.js +1 -0
- package/dist/consolidate.js +16 -4
- package/dist/dag.js +11 -9
- package/dist/dashboard.js +4 -2
- package/dist/db.js +16 -3
- package/dist/dedupe.js +1 -1
- package/dist/delivery-recorder.js +1 -0
- package/dist/doctor.js +24 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.js +3 -2
- package/dist/embeddings.js +10 -7
- package/dist/extract.js +20 -19
- package/dist/graph.js +3 -8
- package/dist/handoff.js +3 -0
- package/dist/hooks.js +3 -0
- package/dist/importers.js +6 -3
- package/dist/incidents.js +1 -0
- package/dist/judgment.js +5 -2
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +32 -13
- package/dist/memory.d.ts +5 -3
- package/dist/processes.js +1 -0
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +9 -4
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/recall-scope.d.ts +3 -3
- package/dist/recall-scope.js +5 -4
- package/dist/recall-trace.d.ts +2 -2
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +18 -10
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +223 -0
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +14 -1
- package/dist/rerankers/jev.js +30 -20
- package/dist/rerankers/llm.js +3 -3
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.js +1 -0
- package/dist/search.js +4 -2
- package/dist/secret-detect.js +1 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +38 -15
- package/dist/shared.js +21 -19
- package/dist/stdin.js +1 -0
- package/dist/store.d.ts +33 -1
- package/dist/store.js +123 -22
- package/dist/token-ledger.js +1 -0
- 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 +6 -2
package/dist/store.js
CHANGED
|
@@ -27,6 +27,7 @@ import { checkRejectionGuard, RejectedValueError, rejectionDigest, normalizeValu
|
|
|
27
27
|
// is the standard safe mutual-function-reference shape under NodeNext ESM.
|
|
28
28
|
import { archiveRawMemory } from './raw-archive.js';
|
|
29
29
|
import { insertDormantRow } from './dormant.js';
|
|
30
|
+
import { log } from './log.js';
|
|
30
31
|
/**
|
|
31
32
|
* Emit an audit event for a mutation against `db`. Wrapped so a broken audit
|
|
32
33
|
* log can never crash the surrounding mutation — the SQLite store is still the
|
|
@@ -58,7 +59,7 @@ export function auditRejectionRefusal(db, err, actor) {
|
|
|
58
59
|
audit(db, 'reject_refusal', err.entryId, { digest: err.digest, reason: err.reason }, actor, err.tenantId);
|
|
59
60
|
}
|
|
60
61
|
const INDEX_VERSION = 3;
|
|
61
|
-
const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
|
|
62
|
+
export const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
|
|
62
63
|
// F1 (v1.7.0): qualified-and-aliased columns for the FTS join in
|
|
63
64
|
// loadSearchRows. Every column is `m.<col> AS <col>` so rowToEntry's
|
|
64
65
|
// unqualified field reads keep working unchanged. The trailing
|
|
@@ -180,9 +181,9 @@ export function serializeEntry(entry) {
|
|
|
180
181
|
if (tenantId !== 'default') {
|
|
181
182
|
frontmatter['tenant_id'] = tenantId;
|
|
182
183
|
}
|
|
183
|
-
// v39:
|
|
184
|
-
// only undefined
|
|
185
|
-
if (entry.origin_project !== undefined
|
|
184
|
+
// v39: '' (user-global) and null (unknown, hidden by default) must both round-trip;
|
|
185
|
+
// only undefined (unstamped) is omitted, so a rebuild stamps nothing it can read.
|
|
186
|
+
if (entry.origin_project !== undefined) {
|
|
186
187
|
frontmatter['origin_project'] = entry.origin_project;
|
|
187
188
|
}
|
|
188
189
|
// Spread into a fresh object literal: dumpFrontmatter's Record<string,
|
|
@@ -241,7 +242,7 @@ export function deserializeEntry(raw) {
|
|
|
241
242
|
owner: data['owner'] === null || data['owner'] === undefined ? null : String(data['owner']),
|
|
242
243
|
artifact_ref: data['artifact_ref'] === null || data['artifact_ref'] === undefined ? null : String(data['artifact_ref']),
|
|
243
244
|
tenantId: data['tenant_id'] === null || data['tenant_id'] === undefined ? 'default' : String(data['tenant_id']),
|
|
244
|
-
origin_project:
|
|
245
|
+
origin_project: !('origin_project' in data) ? undefined : data['origin_project'] === null ? null : String(data['origin_project']),
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
248
|
function normalizeStringArray(value) {
|
|
@@ -312,7 +313,8 @@ function parseJsonArray(raw) {
|
|
|
312
313
|
const parsed = JSON.parse(raw);
|
|
313
314
|
return Array.isArray(parsed) ? parsed.map((item) => String(item)) : [];
|
|
314
315
|
}
|
|
315
|
-
catch {
|
|
316
|
+
catch (err) {
|
|
317
|
+
log.debug(`store: corrupt JSON array column read as empty: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
318
|
return [];
|
|
317
319
|
}
|
|
318
320
|
}
|
|
@@ -346,7 +348,8 @@ function parseJsonObject(raw) {
|
|
|
346
348
|
}
|
|
347
349
|
return {};
|
|
348
350
|
}
|
|
349
|
-
catch {
|
|
351
|
+
catch (err) {
|
|
352
|
+
log.debug(`store: corrupt JSON object column read as empty: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
353
|
return {};
|
|
351
354
|
}
|
|
352
355
|
}
|
|
@@ -748,12 +751,12 @@ export function purgeMirrorBestEffort(hippoRoot, id, isRaw, logPrefix) {
|
|
|
748
751
|
catch (secondErr) {
|
|
749
752
|
const msg = secondErr instanceof Error ? secondErr.message : String(secondErr);
|
|
750
753
|
if (isRaw) {
|
|
751
|
-
|
|
754
|
+
log.error(`${logPrefix}: mirror cleanup failed for ${id} (will retry via reaper on next open): ${msg}`);
|
|
752
755
|
}
|
|
753
756
|
else {
|
|
754
757
|
const leftover = getExistingEntryMirrorPaths(hippoRoot, id);
|
|
755
758
|
const pathsNote = leftover.length > 0 ? leftover.join(', ') : `${id}.md (path unresolved)`;
|
|
756
|
-
|
|
759
|
+
log.error(`${logPrefix}: mirror cleanup failed for ${id} - no automatic retry exists for this file, ` +
|
|
757
760
|
`delete it manually: ${pathsNote} (${msg})`);
|
|
758
761
|
}
|
|
759
762
|
return false;
|
|
@@ -809,7 +812,7 @@ function bootstrapLegacyStore(db, hippoRoot) {
|
|
|
809
812
|
}
|
|
810
813
|
}
|
|
811
814
|
if (rejectedCount > 0) {
|
|
812
|
-
|
|
815
|
+
log.warn(`bootstrapLegacyStore: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
|
|
813
816
|
}
|
|
814
817
|
const legacyIndex = loadLegacyIndexFile(hippoRoot);
|
|
815
818
|
setMeta(db, 'last_retrieval_ids', JSON.stringify(legacyIndex.last_retrieval_ids ?? []));
|
|
@@ -874,7 +877,8 @@ function loadLegacyIndexFile(hippoRoot) {
|
|
|
874
877
|
// that violates the shape falls through to the catch block's fallback.
|
|
875
878
|
return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
876
879
|
}
|
|
877
|
-
catch {
|
|
880
|
+
catch (err) {
|
|
881
|
+
log.debug(`store: unreadable index.json read as empty: ${err instanceof Error ? err.message : String(err)}`);
|
|
878
882
|
return { version: 1, entries: {}, last_retrieval_ids: [], last_trace_id: null };
|
|
879
883
|
}
|
|
880
884
|
}
|
|
@@ -895,7 +899,8 @@ function loadLegacyStatsFile(hippoRoot) {
|
|
|
895
899
|
// hand-edited or corrupted file even if this optimistic cast is wrong.
|
|
896
900
|
return JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
|
897
901
|
}
|
|
898
|
-
catch {
|
|
902
|
+
catch (err) {
|
|
903
|
+
log.debug(`store: unreadable stats.json read as zero: ${err instanceof Error ? err.message : String(err)}`);
|
|
899
904
|
return {
|
|
900
905
|
total_remembered: 0,
|
|
901
906
|
total_recalled: 0,
|
|
@@ -1077,7 +1082,7 @@ function mirrorBestEffort(what, write) {
|
|
|
1077
1082
|
write();
|
|
1078
1083
|
}
|
|
1079
1084
|
catch (err) {
|
|
1080
|
-
|
|
1085
|
+
log.warn(`${what} not refreshed (${err instanceof Error ? err.message : String(err)}); the database write succeeded`);
|
|
1081
1086
|
}
|
|
1082
1087
|
}
|
|
1083
1088
|
function syncMirrorFiles(hippoRoot, db) {
|
|
@@ -1174,7 +1179,7 @@ export function stampOriginProject(hippoRoot, entry) {
|
|
|
1174
1179
|
return { ...entry, origin_project: deriveOriginProject(path.dirname(hippoRoot)) };
|
|
1175
1180
|
}
|
|
1176
1181
|
/**
|
|
1177
|
-
* Import-time variant
|
|
1182
|
+
* Import-time variant for a mirror with no origin field (an explicit null stays null): used only where evidence exists
|
|
1178
1183
|
* for rows that predate the origin column - the legacy-markdown bootstrap and
|
|
1179
1184
|
* rebuildIndex import, which are the markdown-store equivalent of the v39 SQL
|
|
1180
1185
|
* backfill. Same evidence order as the migration: the provenance source
|
|
@@ -1183,7 +1188,7 @@ export function stampOriginProject(hippoRoot, entry) {
|
|
|
1183
1188
|
* owning project instead of becoming user-global (codex gating round 3 P1).
|
|
1184
1189
|
*/
|
|
1185
1190
|
function stampOriginProjectForImport(hippoRoot, entry) {
|
|
1186
|
-
if (entry.origin_project !== undefined
|
|
1191
|
+
if (entry.origin_project !== undefined)
|
|
1187
1192
|
return entry;
|
|
1188
1193
|
const fromSource = originFromSource(entry.source);
|
|
1189
1194
|
return {
|
|
@@ -1332,7 +1337,7 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
|
|
|
1332
1337
|
db?.exec('ROLLBACK');
|
|
1333
1338
|
}
|
|
1334
1339
|
catch { /* already rolled back; keep the original error */ }
|
|
1335
|
-
|
|
1340
|
+
log.warn(`retrieval stats not saved (${error instanceof Error ? error.message : String(error)})`);
|
|
1336
1341
|
found.clear();
|
|
1337
1342
|
}
|
|
1338
1343
|
finally {
|
|
@@ -1772,7 +1777,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
1772
1777
|
}
|
|
1773
1778
|
db.exec('COMMIT');
|
|
1774
1779
|
if (batchRejectedSkips > 0) {
|
|
1775
|
-
|
|
1780
|
+
log.warn(`batchWriteAndDelete: skipped ${batchRejectedSkips} write(s) whose content matches a rejected value (tombstone hit during the batch transaction)`);
|
|
1776
1781
|
}
|
|
1777
1782
|
// Sync mirrors once after all DB writes. Entries skipped above were
|
|
1778
1783
|
// never inserted — writing their markdown mirror would resurrect the
|
|
@@ -1853,6 +1858,11 @@ export function loadContentsWithTag(hippoRoot, tenantId, tag) {
|
|
|
1853
1858
|
closeHippoDb(db);
|
|
1854
1859
|
}
|
|
1855
1860
|
}
|
|
1861
|
+
const AMBIENT_SCOPED = 'superseded_by IS NULL AND tenant_id = ?';
|
|
1862
|
+
/** Exported so the plan test runs the exact SQL; idx_memories_pinned (db.ts v51) serves it. */
|
|
1863
|
+
export const AMBIENT_PINNED_WHERE = `pinned = 1 AND ${AMBIENT_SCOPED} ORDER BY created ASC, id ASC`;
|
|
1864
|
+
/** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
|
|
1865
|
+
export const AMBIENT_DRIFT_SQL = `SELECT 1 FROM memories WHERE ${AMBIENT_SCOPED} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`;
|
|
1856
1866
|
// The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
|
|
1857
1867
|
// injection. One connection; `recall` piggybacks the Z1 FTS query on it too.
|
|
1858
1868
|
export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
|
|
@@ -1864,14 +1874,14 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
|
|
|
1864
1874
|
// SAFETY: every `where` below starts from MEMORY_SELECT_COLUMNS' table.
|
|
1865
1875
|
const run = (where, params) => db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE ${where}`).all(...params).map(rowToEntry);
|
|
1866
1876
|
const byId = new Map();
|
|
1867
|
-
const scoped =
|
|
1868
|
-
for (const e of run(
|
|
1877
|
+
const scoped = AMBIENT_SCOPED;
|
|
1878
|
+
for (const e of run(AMBIENT_PINNED_WHERE, [tenantId])) {
|
|
1869
1879
|
if (admit(e))
|
|
1870
1880
|
byId.set(e.id, e);
|
|
1871
1881
|
}
|
|
1872
1882
|
if (needed > 0) {
|
|
1873
1883
|
// Text order is chronological only for canonical UTC ISO (memory.ts).
|
|
1874
|
-
const drifted = db.prepare(
|
|
1884
|
+
const drifted = db.prepare(AMBIENT_DRIFT_SQL).get(tenantId) !== undefined;
|
|
1875
1885
|
// `id DESC` mirrors getContext's comparator, not loadFreshRawMemories'
|
|
1876
1886
|
// cross-ingest-stable order: that would change what the hook injects.
|
|
1877
1887
|
const window = Math.max(needed * 4, 32);
|
|
@@ -1903,6 +1913,97 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
|
|
|
1903
1913
|
closeHippoDb(db);
|
|
1904
1914
|
}
|
|
1905
1915
|
}
|
|
1916
|
+
// calculateStrength's decay exponent with its reward factor, pins first: the strongest rows, without scoring each one in JS.
|
|
1917
|
+
const DECAY_RANK_SQL = `pinned DESC,
|
|
1918
|
+
CASE WHEN half_life_days > 0 THEN (julianday(?) - julianday(last_retrieved))
|
|
1919
|
+
/ (half_life_days * (1.0 + 0.5 * (COALESCE(outcome_positive, 0) - COALESCE(outcome_negative, 0))
|
|
1920
|
+
/ (COALESCE(outcome_positive, 0) + COALESCE(outcome_negative, 0) + 1.0))) END ASC NULLS LAST,
|
|
1921
|
+
id ASC`;
|
|
1922
|
+
/** Live tenant rows passing `filter`, at most `filter.cap`, in loadAllEntries' order; below the cap, every such row. */
|
|
1923
|
+
export function loadContextCandidates(hippoRoot, tenantId, filter) {
|
|
1924
|
+
const where = ['tenant_id = ?', `COALESCE(superseded_by, '') = ''`];
|
|
1925
|
+
const params = [tenantId];
|
|
1926
|
+
if (filter.exactScope) {
|
|
1927
|
+
where.push('scope = ?');
|
|
1928
|
+
params.push(filter.exactScope);
|
|
1929
|
+
}
|
|
1930
|
+
else {
|
|
1931
|
+
// isRestrictedScope's rule; NOT LIKE folds ASCII case as its /:private:/i does.
|
|
1932
|
+
where.push(`(scope IS NULL OR (scope NOT IN (${RECALL_DEFAULT_DENY_SCOPES.map(() => '?').join(', ')}) AND scope NOT LIKE '%:private:%'))`);
|
|
1933
|
+
params.push(...RECALL_DEFAULT_DENY_SCOPES);
|
|
1934
|
+
}
|
|
1935
|
+
if (filter.project !== undefined) {
|
|
1936
|
+
where.push(`(origin_project = '' OR origin_project = ?)`);
|
|
1937
|
+
params.push(filter.project);
|
|
1938
|
+
}
|
|
1939
|
+
const db = openStore(hippoRoot);
|
|
1940
|
+
try {
|
|
1941
|
+
// SAFETY: the outer SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set; the rank sort carries ids only.
|
|
1942
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (
|
|
1943
|
+
SELECT id FROM memories WHERE ${where.join(' AND ')} ORDER BY ${DECAY_RANK_SQL} LIMIT ?
|
|
1944
|
+
) ORDER BY created ASC, id ASC`).all(...params, filter.now.toISOString(), Math.max(0, Math.trunc(filter.cap)));
|
|
1945
|
+
return rows.map(rowToEntry);
|
|
1946
|
+
}
|
|
1947
|
+
finally {
|
|
1948
|
+
closeHippoDb(db);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/** Every tenant row as a StrengthRow, for whole-store health numbers; defaults match rowToEntry's. */
|
|
1952
|
+
export function loadStrengthRows(hippoRoot, tenantId) {
|
|
1953
|
+
const db = openStore(hippoRoot);
|
|
1954
|
+
try {
|
|
1955
|
+
// SAFETY: rows' shape matches the columns named in the SELECT below.
|
|
1956
|
+
const rows = db.prepare(`SELECT pinned, created, last_retrieved, half_life_days, retrieval_count, emotional_valence, outcome_positive, outcome_negative, tags_json
|
|
1957
|
+
FROM memories WHERE tenant_id = ?`).all(tenantId);
|
|
1958
|
+
return rows.map((row) => ({
|
|
1959
|
+
pinned: Boolean(row.pinned),
|
|
1960
|
+
created: row.created,
|
|
1961
|
+
last_retrieved: row.last_retrieved,
|
|
1962
|
+
half_life_days: Number(row.half_life_days ?? 7),
|
|
1963
|
+
retrieval_count: Number(row.retrieval_count ?? 0),
|
|
1964
|
+
emotional_valence: row.emotional_valence ?? 'neutral',
|
|
1965
|
+
outcome_positive: Number(row.outcome_positive ?? 0),
|
|
1966
|
+
outcome_negative: Number(row.outcome_negative ?? 0),
|
|
1967
|
+
tags: parseJsonArray(row.tags_json),
|
|
1968
|
+
}));
|
|
1969
|
+
}
|
|
1970
|
+
finally {
|
|
1971
|
+
closeHippoDb(db);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/** Text and source of tenant rows holding any of `words`; a row equal to a text apart from spacing holds its every word. */
|
|
1975
|
+
export function loadTextsHoldingWords(hippoRoot, tenantId, words) {
|
|
1976
|
+
const unique = [...new Set(words)];
|
|
1977
|
+
const out = [];
|
|
1978
|
+
const db = openStore(hippoRoot);
|
|
1979
|
+
try {
|
|
1980
|
+
// Chunked so one statement stays far under SQLite's bound-parameter limit.
|
|
1981
|
+
for (let i = 0; i < unique.length; i += 200) {
|
|
1982
|
+
const chunk = unique.slice(i, i + 200);
|
|
1983
|
+
// SAFETY: rows' shape matches the two columns named in the SELECT below.
|
|
1984
|
+
const rows = db.prepare(`SELECT content, source FROM memories WHERE tenant_id = ? AND (${chunk.map(() => 'instr(content, ?) > 0').join(' OR ')})`).all(tenantId, ...chunk);
|
|
1985
|
+
for (const row of rows)
|
|
1986
|
+
out.push({ content: row.content, source: row.source ?? 'cli' });
|
|
1987
|
+
}
|
|
1988
|
+
return out;
|
|
1989
|
+
}
|
|
1990
|
+
finally {
|
|
1991
|
+
closeHippoDb(db);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/** Row count and newest `created` per source, for peer listings that need no row; all tenants when `tenantId` is absent. */
|
|
1995
|
+
export function tallySources(hippoRoot, tenantId) {
|
|
1996
|
+
const db = openStore(hippoRoot);
|
|
1997
|
+
try {
|
|
1998
|
+
const where = tenantId !== undefined ? 'WHERE tenant_id = ?' : '';
|
|
1999
|
+
// SAFETY: rows' shape matches the four aliased columns in the SELECT below.
|
|
2000
|
+
return db.prepare(`SELECT COALESCE(source, 'cli') AS source, COUNT(*) AS count, MAX(created) AS latest, MIN(created || char(31) || id) AS first
|
|
2001
|
+
FROM memories ${where} GROUP BY COALESCE(source, 'cli')`).all(...(tenantId !== undefined ? [tenantId] : []));
|
|
2002
|
+
}
|
|
2003
|
+
finally {
|
|
2004
|
+
closeHippoDb(db);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1906
2007
|
/**
|
|
1907
2008
|
* Load likely search candidates directly from SQLite.
|
|
1908
2009
|
* Uses FTS5 when available, falls back to LIKE matching, then full-store fallback.
|
|
@@ -2019,7 +2120,7 @@ export function rebuildIndex(hippoRoot) {
|
|
|
2019
2120
|
}
|
|
2020
2121
|
}
|
|
2021
2122
|
if (rejectedCount > 0) {
|
|
2022
|
-
|
|
2123
|
+
log.warn(`rebuildIndex: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
|
|
2023
2124
|
}
|
|
2024
2125
|
db.exec('COMMIT');
|
|
2025
2126
|
}
|
|
@@ -3279,7 +3380,7 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
|
|
|
3279
3380
|
// tool) is the wrong one here; a direct audit() call is correct and
|
|
3280
3381
|
// commits with the rest of this savepoint.
|
|
3281
3382
|
audit(db, 'reject_refusal', summary.id, { digest: tombstone.digest, reason: tombstone.reason }, patch.actor, summary.tenantId);
|
|
3282
|
-
|
|
3383
|
+
log.warn(`applyRebuildResult: refused rebuild content for ${summary.id} — matches a rejected value ` +
|
|
3283
3384
|
`(digest ${tombstone.digest.slice(0, 12)}...); metadata updated, content unchanged`);
|
|
3284
3385
|
}
|
|
3285
3386
|
if (changed) {
|
package/dist/token-ledger.js
CHANGED
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.60.0";
|
|
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.60.0';
|
|
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) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.60.0",
|
|
6
6
|
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.60.0",
|
|
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.60.0",
|
|
4
4
|
"description": "Memory for AI agents that learns what is wrong and ranks it down. MCP server, hooks for Claude Code, OpenCode and Codex, AGENTS.md instructions for Codex, Cursor, OpenClaw and Pi. SQLite, zero runtime deps.",
|
|
5
5
|
"mcpName": "io.github.kitfunso/hippo-memory",
|
|
6
6
|
"type": "module",
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"baseline": "npm run build && npm test",
|
|
32
32
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && tsc -p tsconfig.benchmarks.json && tsc -p tsconfig.extensions.json",
|
|
33
|
+
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
33
34
|
"dev": "tsc --watch",
|
|
34
35
|
"pretest": "npm run build",
|
|
35
36
|
"test": "vitest run",
|
|
36
37
|
"test:watch": "vitest",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
37
39
|
"test:delivery-ledger": "vitest run tests/delivery-ledger",
|
|
38
40
|
"lint": "oxlint",
|
|
39
41
|
"sbom": "node scripts/sbom.mjs",
|
|
@@ -90,9 +92,11 @@
|
|
|
90
92
|
"devDependencies": {
|
|
91
93
|
"@oxlint/plugins": "^1.78.0",
|
|
92
94
|
"@types/node": "^22.16.0",
|
|
95
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
93
96
|
"oxlint": "^1.78.0",
|
|
94
97
|
"typescript": "^5.4.0",
|
|
95
|
-
"
|
|
98
|
+
"vite": "^8.3.2",
|
|
99
|
+
"vitest": "^5.0.3"
|
|
96
100
|
},
|
|
97
101
|
"peerDependenciesMeta": {
|
|
98
102
|
"@xenova/transformers": {
|