hippo-memory 1.52.7 → 1.52.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -99
- package/dist/api.d.ts +52 -18
- package/dist/api.js +155 -86
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +40 -0
- package/dist/capture.js +141 -119
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +695 -702
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +4 -0
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +13 -10
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +19 -5
- package/dist/hooks.js +131 -30
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +174 -213
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +9 -3
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +11 -3
- package/dist/shared.js +41 -31
- package/dist/store.d.ts +4 -5
- package/dist/store.js +26 -13
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/index.ts +1 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/dedupe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Store-level deduplication. Scans for
|
|
3
|
-
*
|
|
2
|
+
* Store-level deduplication. Scans for memories with the same text apart
|
|
3
|
+
* from spacing, keeps the stronger copy (by strength + retrieval count),
|
|
4
4
|
* removes the rest.
|
|
5
5
|
*
|
|
6
6
|
* Extracted from cli.ts in Episode A (v1.11.3) so `api.sleep` can dedupe
|
|
@@ -25,6 +25,7 @@ import { loadAllEntries, deleteEntry } from './store.js';
|
|
|
25
25
|
import { compareEntryIdentity } from './compare.js';
|
|
26
26
|
import { canAutoDelete } from './memory.js';
|
|
27
27
|
import { derivationPartitionKey } from './recall-scope.js';
|
|
28
|
+
import { duplicateKey } from './same-text.js';
|
|
28
29
|
/** Quantization step for strength-tie comparisons. The historical 0.01
|
|
29
30
|
* epsilon (see `strengthBucket` below) applied via rounding instead of a
|
|
30
31
|
* raw abs-diff threshold, so the tiebreak is transitive. */
|
|
@@ -55,16 +56,15 @@ export function strengthBucket(strength) {
|
|
|
55
56
|
return Number.isFinite(s) ? Math.round(s / STRENGTH_TIE_EPSILON) : 0;
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
|
-
* Scan the store for
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* Scan the store for duplicates and remove the weaker copy: same text apart
|
|
60
|
+
* from spacing, since a near-duplicate can differ in a value (port, version,
|
|
61
|
+
* path, name), AND the same tenant: the scan is partitioned by
|
|
61
62
|
* tenantId, so byte-identical content in two tenants is never a duplicate
|
|
62
63
|
* pair (the tenant boundary is an isolation boundary; cross-tenant removal
|
|
63
64
|
* was the v1.32.0 known-issue data-loss bug).
|
|
64
|
-
* Keeps the one with higher strength (or more retrievals if tied).
|
|
65
|
+
* Keeps the one with higher strength (or more retrievals if tied). `threshold` is accepted for old callers and ignored.
|
|
65
66
|
*/
|
|
66
67
|
export function deduplicateStore(hippoRoot, options = {}) {
|
|
67
|
-
const threshold = options.threshold ?? 0.7;
|
|
68
68
|
const dryRun = options.dryRun ?? false;
|
|
69
69
|
// Only current distilled rows compete: raw rows are append-only (the delete
|
|
70
70
|
// trigger would abort sleep mid-loop) and superseded rows are history, as in consolidate.ts.
|
|
@@ -111,15 +111,16 @@ export function deduplicateStore(hippoRoot, options = {}) {
|
|
|
111
111
|
return retrievalDiff;
|
|
112
112
|
return compareEntryIdentity(a, b);
|
|
113
113
|
});
|
|
114
|
+
const texts = tenantEntries.map((e) => duplicateKey(e.content));
|
|
114
115
|
for (let i = 0; i < tenantEntries.length; i++) {
|
|
115
116
|
if (removed.has(tenantEntries[i].id))
|
|
116
117
|
continue;
|
|
117
118
|
for (let j = i + 1; j < tenantEntries.length; j++) {
|
|
118
119
|
if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]))
|
|
119
120
|
continue;
|
|
120
|
-
|
|
121
|
-
if (similarity <= threshold)
|
|
121
|
+
if (texts[j] !== texts[i])
|
|
122
122
|
continue;
|
|
123
|
+
const similarity = textOverlap(tenantEntries[i].content, tenantEntries[j].content);
|
|
123
124
|
removed.add(tenantEntries[j].id);
|
|
124
125
|
pairs.push({
|
|
125
126
|
kept: tenantEntries[i].id,
|
package/dist/doctor.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface DoctorReport {
|
|
|
19
19
|
/** Inputs for {@link runDoctor}; defaults come from the process. */
|
|
20
20
|
export interface DoctorOpts {
|
|
21
21
|
cwd?: string;
|
|
22
|
-
/** Home directory used to find agent configuration (~/.claude). */
|
|
22
|
+
/** Home directory used to find agent configuration (~/.claude, ~/.codex). */
|
|
23
23
|
home?: string;
|
|
24
24
|
version: string;
|
|
25
25
|
nodeVersion?: string;
|
package/dist/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getGlobalRoot } from './shared.js';
|
|
|
12
12
|
import { isInitialized } from './store.js';
|
|
13
13
|
import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVersion, countTableRows, IncompatibleBinaryError } from './db.js';
|
|
14
14
|
import { isEmbeddingAvailable } from './embeddings.js';
|
|
15
|
+
import { CODEX_TRUST_LINE, codexHomeDir, isCodexPresent, isJsonObject } from './hooks.js';
|
|
15
16
|
/** Minimum Node.js version hippo supports (package.json engines). */
|
|
16
17
|
export const MIN_NODE = '22.16.0';
|
|
17
18
|
function versionAtLeast(actual, min) {
|
|
@@ -34,6 +35,29 @@ function readJson(file) {
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
// Trust lives in Codex's own config.toml rows; doctor reads only the hooks file and reminds.
|
|
39
|
+
function codexCheck(home) {
|
|
40
|
+
const file = path.join(codexHomeDir(home), 'hooks.json');
|
|
41
|
+
const parsed = readJson(file);
|
|
42
|
+
// Codex drops every hook in a hooks.json it cannot parse, hippo's included.
|
|
43
|
+
if (fs.existsSync(file) && !isJsonObject(parsed)) {
|
|
44
|
+
return { id: 'codex', status: 'warn', detail: "Codex's hooks.json is not a JSON object, so Codex runs no hook from it", fix: 'repair hooks.json, then run: hippo hook install codex' };
|
|
45
|
+
}
|
|
46
|
+
const text = JSON.stringify(parsed ?? '');
|
|
47
|
+
const codexHooks = [
|
|
48
|
+
['hippo context --pinned-only', 'per-prompt memory'],
|
|
49
|
+
['hippo compact-resume', 'resume after compaction'],
|
|
50
|
+
];
|
|
51
|
+
const missing = codexHooks.filter(([marker]) => !text.includes(marker)).map(([, what]) => what);
|
|
52
|
+
if (missing.length === 0)
|
|
53
|
+
return { id: 'codex', status: 'pass', detail: `Codex: hippo memory hooks installed. ${CODEX_TRUST_LINE}` };
|
|
54
|
+
return {
|
|
55
|
+
id: 'codex',
|
|
56
|
+
status: 'warn',
|
|
57
|
+
detail: missing.length === codexHooks.length ? "Codex found, but hippo's memory hooks are not installed" : `Codex: hippo hooks missing for ${missing.join(', ')}`,
|
|
58
|
+
fix: 'hippo hook install codex (then trust the hooks once in /hooks)',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
37
61
|
// Migration 46 creates failure_log; a read-only open no longer creates it on an older store.
|
|
38
62
|
const FAILURE_LOG_SCHEMA = 46;
|
|
39
63
|
/** The failed-tool-call count over the last 7 days, or why it could not be read. */
|
|
@@ -123,8 +147,15 @@ export function runDoctor(opts) {
|
|
|
123
147
|
const since = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
124
148
|
try {
|
|
125
149
|
// SAFETY: COUNT/SUM aggregate row.
|
|
126
|
-
const row = db.prepare(`SELECT COUNT(
|
|
127
|
-
|
|
150
|
+
const row = db.prepare(`SELECT COUNT(CASE WHEN event = 'inject' THEN 1 END) AS n,
|
|
151
|
+
COALESCE(SUM(CASE WHEN event = 'inject' THEN tokens END), 0) AS t,
|
|
152
|
+
COALESCE(SUM(CASE WHEN event = 'reread' THEN tokens END), 0) AS r
|
|
153
|
+
FROM token_ledger WHERE ts >= ?`).get(since);
|
|
154
|
+
checks.push({
|
|
155
|
+
id: 'tokens',
|
|
156
|
+
status: 'info',
|
|
157
|
+
detail: `${Number(row?.n ?? 0)} memory blocks sent to agents in 7 days, about ${Number(row?.t ?? 0)} tokens sent and ${Number(row?.r ?? 0)} re-read by later model calls (hippo tokens for detail)`,
|
|
158
|
+
});
|
|
128
159
|
}
|
|
129
160
|
catch {
|
|
130
161
|
checks.push({ id: 'tokens', status: 'info', detail: 'no token ledger yet (created on the next write)' });
|
|
@@ -176,6 +207,8 @@ export function runDoctor(opts) {
|
|
|
176
207
|
else {
|
|
177
208
|
checks.push({ id: 'claude-code', status: 'info', detail: 'Claude Code not found; other agents can use hippo over MCP (hippo mcp)' });
|
|
178
209
|
}
|
|
210
|
+
if (isCodexPresent(home))
|
|
211
|
+
checks.push(codexCheck(home));
|
|
179
212
|
checks.push({ id: 'embeddings', status: 'info', detail: isEmbeddingAvailable() ? 'local embeddings available (hybrid search)' : 'embeddings not installed; recall uses BM25 (optional: hippo embed --help)' });
|
|
180
213
|
return { ok: !checks.some((c) => c.status === 'fail'), version: opts.version, store, checks };
|
|
181
214
|
}
|
package/dist/dormant.d.ts
CHANGED
|
@@ -71,6 +71,10 @@ export interface DormantSnapshot {
|
|
|
71
71
|
* has no dormant memory with that id (another tenant's id reads as absent).
|
|
72
72
|
*/
|
|
73
73
|
export declare function readDormantSnapshot(db: DatabaseSyncLike, tenantId: string, id: string): DormantSnapshot | null;
|
|
74
|
+
/** Every dormant memory of a tenant whose snapshot still reads back. */
|
|
75
|
+
export declare function listDormantSnapshots(db: DatabaseSyncLike, tenantId: string): DormantSnapshot[];
|
|
76
|
+
/** Put `entry` in place of a tenant's dormant memory `id`, keeping when and why that one went dormant. */
|
|
77
|
+
export declare function replaceDormantEntry(db: DatabaseSyncLike, tenantId: string, id: string, entry: MemoryEntry): void;
|
|
74
78
|
/** Whether a tenant has a dormant memory with this id (snapshot readable or not). */
|
|
75
79
|
export declare function hasDormantRow(db: DatabaseSyncLike, tenantId: string, id: string): boolean;
|
|
76
80
|
/** Delete a tenant's dormant memory. Returns false when there was none. */
|
package/dist/dormant.js
CHANGED
|
@@ -74,8 +74,23 @@ export function readDormantSnapshot(db, tenantId, id) {
|
|
|
74
74
|
// SAFETY: row's shape matches the seven columns named in the SELECT.
|
|
75
75
|
const row = db.prepare(`SELECT tenant_id, id, content, entry_json, reason, strength, dormant_at
|
|
76
76
|
FROM dormant_memories WHERE tenant_id = ? AND id = ?`).get(tenantId, id);
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
return row ? toSnapshot(row) : null;
|
|
78
|
+
}
|
|
79
|
+
/** Every dormant memory of a tenant whose snapshot still reads back. */
|
|
80
|
+
export function listDormantSnapshots(db, tenantId) {
|
|
81
|
+
// SAFETY: rows' shape matches the seven columns named in the SELECT.
|
|
82
|
+
const rows = db.prepare(`SELECT tenant_id, id, content, entry_json, reason, strength, dormant_at
|
|
83
|
+
FROM dormant_memories WHERE tenant_id = ?`).all(tenantId);
|
|
84
|
+
return rows.flatMap((row) => toSnapshot(row) ?? []);
|
|
85
|
+
}
|
|
86
|
+
function toSnapshot(row) {
|
|
87
|
+
const entry = parseSnapshot(row);
|
|
88
|
+
return entry ? { entry, reason: row.reason, strength: row.strength, dormantAt: row.dormant_at } : null;
|
|
89
|
+
}
|
|
90
|
+
/** Put `entry` in place of a tenant's dormant memory `id`, keeping when and why that one went dormant. */
|
|
91
|
+
export function replaceDormantEntry(db, tenantId, id, entry) {
|
|
92
|
+
db.prepare(`UPDATE dormant_memories SET id = ?, content = ?, entry_json = ? WHERE tenant_id = ? AND id = ?`)
|
|
93
|
+
.run(entry.id, entry.content, JSON.stringify(entry), tenantId, id);
|
|
79
94
|
}
|
|
80
95
|
/** Whether a tenant has a dormant memory with this id (snapshot readable or not). */
|
|
81
96
|
export function hasDormantRow(db, tenantId, id) {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - API provider `id` is `${kind}:${model}`; switching to/from an API embedder
|
|
20
20
|
* (or a dimension change) flips the identity and triggers the existing
|
|
21
21
|
* reindex-on-change path.
|
|
22
|
-
* - `resolveEmbeddingProvider`
|
|
22
|
+
* - `resolveEmbeddingProvider` throws on an invalid config (unknown provider,
|
|
23
|
+
* bad apiBaseUrl); `embedMemory` turns that into a warning. `isAvailable()` is provider-aware
|
|
23
24
|
* (local -> dependency installed; api -> key present). `embed()` MAY throw on
|
|
24
25
|
* a hard transport/auth failure so a reindex can abort atomically; hot paths
|
|
25
26
|
* wrap it and fall back to BM25.
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - API provider `id` is `${kind}:${model}`; switching to/from an API embedder
|
|
20
20
|
* (or a dimension change) flips the identity and triggers the existing
|
|
21
21
|
* reindex-on-change path.
|
|
22
|
-
* - `resolveEmbeddingProvider`
|
|
22
|
+
* - `resolveEmbeddingProvider` throws on an invalid config (unknown provider,
|
|
23
|
+
* bad apiBaseUrl); `embedMemory` turns that into a warning. `isAvailable()` is provider-aware
|
|
23
24
|
* (local -> dependency installed; api -> key present). `embed()` MAY throw on
|
|
24
25
|
* a hard transport/auth failure so a reindex can abort atomically; hot paths
|
|
25
26
|
* wrap it and fall back to BM25.
|
package/dist/embeddings.js
CHANGED
|
@@ -12,6 +12,7 @@ import { openHippoDb, closeHippoDb, getMeta, setMeta } from './db.js';
|
|
|
12
12
|
import { initializeParticle, savePhysicsState, loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
13
13
|
import { loadConfig } from './config.js';
|
|
14
14
|
import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
15
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
15
16
|
// Use createRequire for synchronous module resolution check in ESM
|
|
16
17
|
const _require = createRequire(import.meta.url);
|
|
17
18
|
// Cached availability check
|
|
@@ -460,11 +461,29 @@ async function withEmbedLock(hippoRoot, fn) {
|
|
|
460
461
|
resolve();
|
|
461
462
|
}
|
|
462
463
|
}
|
|
464
|
+
// A bad key fails every write; one warning tells the user, N would bury the command's own output.
|
|
465
|
+
let _embedFailureWarned = false;
|
|
466
|
+
function warnEmbedFailureOnce(source, rawMessage) {
|
|
467
|
+
if (_embedFailureWarned)
|
|
468
|
+
return;
|
|
469
|
+
_embedFailureWarned = true;
|
|
470
|
+
// Strict scrub: this line can land in a hook log file, and an API may echo the key back in its error body.
|
|
471
|
+
const message = redactSecretsStrict(rawMessage).replace(/\s+/g, ' ').replace(/\.+$/, '');
|
|
472
|
+
console.error(`hippo: embedding failed (${source}): ${message}. Memories are stored without embeddings until this is fixed.`);
|
|
473
|
+
}
|
|
463
474
|
/**
|
|
464
475
|
* Embed a single memory entry and cache the result in the embedding index.
|
|
465
476
|
*/
|
|
466
477
|
export async function embedMemory(hippoRoot, entry, model) {
|
|
467
|
-
|
|
478
|
+
let provider;
|
|
479
|
+
try {
|
|
480
|
+
provider = resolveEmbeddingProvider(hippoRoot, { model });
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
// Callers fire and forget, so this must resolve: a bad config warns once instead of rejecting.
|
|
484
|
+
warnEmbedFailureOnce('config', err instanceof Error ? err.message : String(err));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
468
487
|
if (!provider.isAvailable())
|
|
469
488
|
return;
|
|
470
489
|
return withEmbedLock(hippoRoot, async () => {
|
|
@@ -513,8 +532,9 @@ export async function embedMemory(hippoRoot, entry, model) {
|
|
|
513
532
|
// Physics init is best-effort — don't break embedding
|
|
514
533
|
}
|
|
515
534
|
}
|
|
516
|
-
catch {
|
|
517
|
-
// Provider failure (API down / bad key). Best-effort: leave the index as-is.
|
|
535
|
+
catch (err) {
|
|
536
|
+
// Provider failure (API down / bad key). Best-effort: leave the index as-is, but say so once.
|
|
537
|
+
warnEmbedFailureOnce(provider.kind, err instanceof Error ? err.message : String(err));
|
|
518
538
|
}
|
|
519
539
|
}).catch((err) => {
|
|
520
540
|
console.error(`hippo: skipped embedding ${entry.id} (${err instanceof Error ? err.message : String(err)}); run 'hippo embed' to backfill`);
|
|
@@ -172,6 +172,7 @@ function runHippo(args, cwd) {
|
|
|
172
172
|
encoding: 'utf8',
|
|
173
173
|
timeout: 30_000,
|
|
174
174
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
175
|
+
windowsHide: true,
|
|
175
176
|
});
|
|
176
177
|
// `encoding: 'utf8'` above selects the ExecFileSyncOptionsWithStringEncoding
|
|
177
178
|
// overload, so `result` is always a `string` here — no runtime check needed.
|
package/dist/extract.js
CHANGED
|
@@ -3,6 +3,7 @@ import { writeEntry } from './store.js';
|
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { RejectedValueError } from './rejection.js';
|
|
5
5
|
import { redactSecrets } from './secret-detect.js';
|
|
6
|
+
import { neverAutoShareTags } from './shared.js';
|
|
6
7
|
function isJsonString(value) {
|
|
7
8
|
return typeof value === 'string';
|
|
8
9
|
}
|
|
@@ -81,7 +82,10 @@ export async function extractFacts(text, opts) {
|
|
|
81
82
|
}
|
|
82
83
|
const INHERITABLE_PREFIXES = ['conv:', 'session:', 'scope:', 'path:'];
|
|
83
84
|
export function storeExtractedFacts(hippoRoot, source, facts) {
|
|
84
|
-
const inheritedTags =
|
|
85
|
+
const inheritedTags = [
|
|
86
|
+
...source.tags.filter((t) => INHERITABLE_PREFIXES.some((p) => t.startsWith(p))),
|
|
87
|
+
...neverAutoShareTags([source]),
|
|
88
|
+
];
|
|
85
89
|
const entries = [];
|
|
86
90
|
let rejected = 0;
|
|
87
91
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Kahneman 2003 inside-vs-outside view).
|
|
12
12
|
*
|
|
13
13
|
* Iteration signal: the `recall_autodebias_hint_no_class_match` audit op
|
|
14
|
-
* (emitted by
|
|
14
|
+
* (emitted by computePlanningFallacyOutput when a phrase matches but no class
|
|
15
15
|
* resolves) is the telemetry channel for deciding whether to add an
|
|
16
16
|
* embedding-based detector in J3.3.
|
|
17
17
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Kahneman 2003 inside-vs-outside view).
|
|
12
12
|
*
|
|
13
13
|
* Iteration signal: the `recall_autodebias_hint_no_class_match` audit op
|
|
14
|
-
* (emitted by
|
|
14
|
+
* (emitted by computePlanningFallacyOutput when a phrase matches but no class
|
|
15
15
|
* resolves) is the telemetry channel for deciding whether to add an
|
|
16
16
|
* embedding-based detector in J3.3.
|
|
17
17
|
*
|
package/dist/graph-recall.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SearchResult } from './search.js';
|
|
1
|
+
import { type ResultCost, type SearchResult } from './search.js';
|
|
2
2
|
/** Hard cap on `--hops` (a higher value just walks more of a finite graph; this bounds
|
|
3
3
|
* worst-case work and keeps the flag honest). */
|
|
4
4
|
export declare const MAX_HOPS = 3;
|
|
@@ -23,6 +23,8 @@ export interface GraphExpandOpts {
|
|
|
23
23
|
asOf?: string;
|
|
24
24
|
/** Token budget for the augmented set (defaults to 4000, matching recall's default). */
|
|
25
25
|
budget?: number;
|
|
26
|
+
/** Budget cost per result; defaults to the memory text. */
|
|
27
|
+
cost?: ResultCost;
|
|
26
28
|
/** The recall --min-results floor: this many top base rows are kept regardless of
|
|
27
29
|
* budget, so graph expansion never violates the floor. Defaults to 1. */
|
|
28
30
|
minResults?: number;
|
package/dist/graph-recall.js
CHANGED
|
@@ -61,12 +61,9 @@ function loadByIdsChunked(root, tenantId, ids) {
|
|
|
61
61
|
}
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
* stores. Pure reads.
|
|
68
|
-
*/
|
|
69
|
-
function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, hitsByOrigin, opts) {
|
|
64
|
+
/** Traverse one store's graph from its seeds into `hitsByOrigin`. Pure reads; mutates `seenMemoryIds`
|
|
65
|
+
* and `seenContent` so a memory, or a share/promote copy of it, surfaces at most once across stores. */
|
|
66
|
+
function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, seenContent, hitsByOrigin, opts) {
|
|
70
67
|
const { hops, maxNeighbors, tenantId, includeSuperseded, asOfDate, recallScope } = opts;
|
|
71
68
|
// Seeds = graph entities (in THIS store) whose source memory is a base result.
|
|
72
69
|
const seedEntities = loadEntitiesByMemoryId(root, tenantId, baseResults.map((r) => r.entry.id));
|
|
@@ -147,6 +144,8 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
147
144
|
continue; // not found / wrong tenant / already in base
|
|
148
145
|
if (seenMemoryIds.has(mem.id))
|
|
149
146
|
continue; // another reached entity already added it
|
|
147
|
+
if (seenContent.has(mem.content))
|
|
148
|
+
continue; // share/promote copy: same text, another id
|
|
150
149
|
const via = reached.get(ent.id);
|
|
151
150
|
// A node reached as the `to` endpoint of a `supersedes` edge IS the superseded
|
|
152
151
|
// (older) version — the graph is the authoritative signal (the memory mirror's
|
|
@@ -175,6 +174,7 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
175
174
|
const origin = originMemByEntityId.get(ent.id) ?? baseResults[0].entry.id;
|
|
176
175
|
const originScore = baseScoreByMemId.get(origin) ?? baseResults[baseResults.length - 1].score;
|
|
177
176
|
seenMemoryIds.add(mem.id);
|
|
177
|
+
seenContent.add(mem.content);
|
|
178
178
|
const hit = {
|
|
179
179
|
entry: mem,
|
|
180
180
|
score: originScore * (1 - HOP_DISCOUNT * via.hops),
|
|
@@ -209,11 +209,12 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
209
209
|
const recallScope = opts.recallScope ?? {};
|
|
210
210
|
const baseScoreByMemId = new Map(baseResults.map((r) => [r.entry.id, r.score]));
|
|
211
211
|
const seenMemoryIds = new Set(baseResults.map((r) => r.entry.id));
|
|
212
|
+
const seenContent = new Set(baseResults.map((r) => r.entry.content));
|
|
212
213
|
const hitsByOrigin = new Map();
|
|
213
214
|
// Expand against each distinct store the seeds may live in (local + global).
|
|
214
215
|
const roots = globalRoot && globalRoot !== hippoRoot ? [hippoRoot, globalRoot] : [hippoRoot];
|
|
215
216
|
for (const root of roots) {
|
|
216
|
-
produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, hitsByOrigin, {
|
|
217
|
+
produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, seenContent, hitsByOrigin, {
|
|
217
218
|
hops, maxNeighbors, tenantId, includeSuperseded, asOfDate, recallScope,
|
|
218
219
|
});
|
|
219
220
|
}
|
|
@@ -246,15 +247,17 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
246
247
|
// baseResults is score-ordered, so slice(0, N) is the top N.
|
|
247
248
|
const protectedCount = Math.min(Math.max(minResults, 1), baseResults.length);
|
|
248
249
|
const keep = new Set(baseResults.slice(0, protectedCount));
|
|
249
|
-
|
|
250
|
+
const price = opts.cost ?? ((r) => r.tokens);
|
|
251
|
+
let usedTokens = [...keep].reduce((s, r) => s + price(r), 0);
|
|
250
252
|
// T2 note: PLAIN stable score sort on purpose -- both input lists are
|
|
251
253
|
// deterministically ordered by this point, stability inherits that, and a
|
|
252
254
|
// base-vs-graph-hit tie keeps the BASE result first (the concat order),
|
|
253
255
|
// preserving pre-T2 semantics.
|
|
254
256
|
for (const r of [...baseResults.slice(protectedCount), ...allHits].sort((a, b) => b.score - a.score)) {
|
|
255
|
-
|
|
257
|
+
const tokens = price(r);
|
|
258
|
+
if (usedTokens + tokens > budget)
|
|
256
259
|
continue;
|
|
257
|
-
usedTokens +=
|
|
260
|
+
usedTokens += tokens;
|
|
258
261
|
keep.add(r);
|
|
259
262
|
}
|
|
260
263
|
// DISPLAY order: base order preserved (it may be MMR-diversified); each kept new hit
|
package/dist/handoff.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HandoffEvidence {
|
|
|
12
12
|
gitRef?: string | null;
|
|
13
13
|
dirtyTree?: boolean | null;
|
|
14
14
|
testStatus?: 'pass' | 'fail' | 'unknown' | null;
|
|
15
|
+
/** 'transcript' when hippo read the handoff off the session's transcript at session end; a later exit may replace it. */
|
|
16
|
+
derivedFrom?: 'transcript';
|
|
15
17
|
}
|
|
16
18
|
/** Narrows an unvalidated value (e.g. CLI input or event content) to a HandoffOutcome. */
|
|
17
19
|
export declare function isHandoffOutcome(v: string | boolean | string[] | null | undefined): v is HandoffOutcome;
|
package/dist/hooks.d.ts
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* sequence, writing both outputs to the log file. The parent returns in
|
|
11
11
|
* <100ms so the TUI teardown can't kill the child before it finishes.
|
|
12
12
|
* - SessionStart: `hippo last-sleep --path <path>` - prints the log
|
|
13
|
-
* written by the previous session's detached worker
|
|
14
|
-
*
|
|
13
|
+
* written by the previous session's detached worker to stderr, which
|
|
14
|
+
* keeps it out of the model's context, and then clears it.
|
|
15
15
|
* Earlier Claude Code forms are detected and migrated automatically:
|
|
16
16
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
17
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
18
18
|
* - 0.22.x: separate sleep + capture SessionEnd entries.
|
|
19
|
+
* Codex's hooks.json gets only two groups (per-prompt memory and
|
|
20
|
+
* compact-resume); see installCodexHooks.
|
|
19
21
|
*
|
|
20
22
|
* 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
|
|
21
23
|
* JSON-hook schema — its config has `additionalProperties: false` and no
|
|
@@ -28,7 +30,11 @@
|
|
|
28
30
|
* the installer + the migration that removes any pre-existing broken
|
|
29
31
|
* `hooks` block from opencode.json.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
import type { JsonValue, JsonObject } from './working-memory.js';
|
|
34
|
+
/** JSON-value plain-object check (excludes arrays and null), typeof-free for the same
|
|
35
|
+
* reason as isJsonString above. */
|
|
36
|
+
export declare function isJsonObject(value: JsonValue | undefined): value is JsonObject;
|
|
37
|
+
export type JsonHookTarget = 'claude-code' | 'codex';
|
|
32
38
|
export interface CodexWrapperPaths {
|
|
33
39
|
wrapperDir: string;
|
|
34
40
|
metadataPath: string;
|
|
@@ -60,7 +66,7 @@ export interface CodexWrapperMetadata {
|
|
|
60
66
|
installedAt: string;
|
|
61
67
|
}
|
|
62
68
|
export interface EnsureCodexWrapperResult {
|
|
63
|
-
status: 'installed' | 'already-installed' | 'not-found';
|
|
69
|
+
status: 'installed' | 'already-installed' | 'not-found' | 'source-checkout';
|
|
64
70
|
metadataPath?: string;
|
|
65
71
|
realCodexPath?: string;
|
|
66
72
|
commandPath?: string;
|
|
@@ -93,6 +99,8 @@ export interface InstallResult {
|
|
|
93
99
|
migratedFromStop: boolean;
|
|
94
100
|
migratedLegacySessionEnd: boolean;
|
|
95
101
|
migratedSplitSessionEnd: boolean;
|
|
102
|
+
/** The file exists but is not JSON hippo can merge into, so it was left untouched. */
|
|
103
|
+
invalidJson: boolean;
|
|
96
104
|
}
|
|
97
105
|
export interface ToolDetection {
|
|
98
106
|
name: string;
|
|
@@ -141,6 +149,12 @@ declare const HIPPO_OPENCODE_PLUGIN_MARKER = "HIPPO_OPENCODE_PLUGIN_V1";
|
|
|
141
149
|
*/
|
|
142
150
|
export declare const OPENCODE_PLUGIN_SOURCE = "// HIPPO_OPENCODE_PLUGIN_V1\n// hippo-memory opencode plugin. DO NOT EDIT \u2014 regenerated on every\n// `hippo hook install opencode` from src/hooks.ts OPENCODE_PLUGIN_SOURCE\n// in https://github.com/kitfunso/hippo-memory. Local changes will be lost.\n\nexport const HippoPlugin = async ({ $ }) => {\n return {\n event: async ({ event }) => {\n // Defense in depth: opencode currently runs in Bun where $ is the shell\n // template helper. A non-Bun runtime would have $ as undefined; fail\n // closed instead of crashing the host session.\n if (typeof $ !== \"function\") return;\n try {\n if (event.type === \"session.idle\") {\n await $`hippo session-end`.quiet().nothrow();\n } else if (event.type === \"session.created\") {\n await $`hippo last-sleep`.quiet().nothrow();\n }\n } catch {\n // hippo CLI not on PATH or other failure \u2014 never crash the host session.\n }\n },\n };\n};\n";
|
|
143
151
|
export { HIPPO_OPENCODE_PLUGIN_MARKER };
|
|
152
|
+
/** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
|
|
153
|
+
export declare function codexHomeDir(home?: string): string;
|
|
154
|
+
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
155
|
+
export declare function isCodexPresent(home?: string): boolean;
|
|
156
|
+
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
|
157
|
+
export declare const CODEX_TRUST_LINE = "The per-prompt hook sends your pinned memories plus the five most recent ones. Codex runs hippo's hooks only after you trust them once in `/hooks`.";
|
|
144
158
|
/**
|
|
145
159
|
* Default log path consumed by `hippo last-sleep`. Shared fallback when
|
|
146
160
|
* a caller doesn't pass --path explicitly.
|
|
@@ -172,7 +186,7 @@ export declare function isCodexWrapperInstalled(): boolean;
|
|
|
172
186
|
* doing it from postinstall or routine commands is a consent violation and
|
|
173
187
|
* reads as binary hijacking to security scanners (issue #133).
|
|
174
188
|
*/
|
|
175
|
-
export declare function repairCodexWrapperIfInstalled(): EnsureCodexWrapperResult;
|
|
189
|
+
export declare function repairCodexWrapperIfInstalled(hippoCliPath?: string): EnsureCodexWrapperResult;
|
|
176
190
|
export declare function resolveCodexSessionTranscript(options: CodexSessionTranscriptOptions): string | null;
|
|
177
191
|
export declare function resolveJsonHookPaths(target: JsonHookTarget): JsonHookPaths;
|
|
178
192
|
export declare function installJsonHooks(target: JsonHookTarget): InstallResult;
|