hippo-memory 1.52.8 → 1.52.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +158 -98
  2. package/dist/api.d.ts +51 -18
  3. package/dist/api.js +121 -76
  4. package/dist/audit.d.ts +2 -1
  5. package/dist/audit.js +63 -0
  6. package/dist/capture.d.ts +37 -0
  7. package/dist/capture.js +111 -81
  8. package/dist/cli.js +627 -654
  9. package/dist/codex-patch.d.ts +12 -0
  10. package/dist/codex-patch.js +71 -0
  11. package/dist/config.d.ts +0 -1
  12. package/dist/config.js +0 -4
  13. package/dist/connectors/slack/types.d.ts +0 -1
  14. package/dist/consolidate.js +85 -32
  15. package/dist/context-render.d.ts +36 -0
  16. package/dist/context-render.js +154 -0
  17. package/dist/dag.js +3 -2
  18. package/dist/db.js +6 -6
  19. package/dist/dedupe.d.ts +6 -6
  20. package/dist/dedupe.js +10 -9
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/doctor.js +35 -2
  23. package/dist/dormant.d.ts +4 -0
  24. package/dist/dormant.js +17 -2
  25. package/dist/embedding-provider.d.ts +2 -1
  26. package/dist/embedding-provider.js +2 -1
  27. package/dist/embeddings.js +23 -3
  28. package/dist/extract.js +5 -1
  29. package/dist/forward-claim-detector.d.ts +1 -1
  30. package/dist/forward-claim-detector.js +1 -1
  31. package/dist/graph-recall.d.ts +3 -1
  32. package/dist/graph-recall.js +5 -3
  33. package/dist/hooks.d.ts +15 -1
  34. package/dist/hooks.js +122 -27
  35. package/dist/importers.js +5 -12
  36. package/dist/judgment.d.ts +30 -0
  37. package/dist/judgment.js +122 -0
  38. package/dist/mcp/server.js +171 -210
  39. package/dist/merged-row.d.ts +6 -0
  40. package/dist/merged-row.js +35 -0
  41. package/dist/multihop.d.ts +2 -1
  42. package/dist/multihop.js +7 -4
  43. package/dist/physics-state.d.ts +0 -4
  44. package/dist/physics-state.js +0 -6
  45. package/dist/predictions.d.ts +2 -17
  46. package/dist/predictions.js +2 -15
  47. package/dist/reject-flow.d.ts +7 -5
  48. package/dist/reject-flow.js +41 -12
  49. package/dist/salience.js +12 -5
  50. package/dist/same-text.d.ts +17 -0
  51. package/dist/same-text.js +38 -0
  52. package/dist/scheduler.d.ts +4 -0
  53. package/dist/scheduler.js +8 -0
  54. package/dist/search.d.ts +7 -0
  55. package/dist/search.js +16 -32
  56. package/dist/secret-detect.d.ts +2 -0
  57. package/dist/secret-detect.js +6 -0
  58. package/dist/server-detect.js +9 -33
  59. package/dist/server.js +6 -62
  60. package/dist/session-digest.d.ts +79 -0
  61. package/dist/session-digest.js +528 -0
  62. package/dist/shared.d.ts +10 -2
  63. package/dist/shared.js +35 -30
  64. package/dist/store.d.ts +1 -0
  65. package/dist/store.js +4 -0
  66. package/dist/token-ledger.d.ts +46 -8
  67. package/dist/token-ledger.js +140 -21
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/extensions/openclaw-plugin/README.md +4 -4
  71. package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
  72. package/extensions/openclaw-plugin/package.json +1 -1
  73. package/openclaw.plugin.json +2 -2
  74. package/package.json +2 -2
@@ -47,10 +47,7 @@ export async function detectServer(hippoRoot) {
47
47
  info = JSON.parse(readFileSync(path, 'utf8'));
48
48
  }
49
49
  catch {
50
- try {
51
- unlinkSync(path);
52
- }
53
- catch { }
50
+ removePidfile(hippoRoot);
54
51
  return null;
55
52
  }
56
53
  // Probe the process. Sending signal 0 throws if the pid is dead or owned
@@ -61,10 +58,7 @@ export async function detectServer(hippoRoot) {
61
58
  process.kill(info.pid, 0);
62
59
  }
63
60
  catch {
64
- try {
65
- unlinkSync(path);
66
- }
67
- catch { }
61
+ removePidfile(hippoRoot);
68
62
  return null;
69
63
  }
70
64
  // The pid is live, but it may have been reused by an unrelated process, and
@@ -77,19 +71,13 @@ export async function detectServer(hippoRoot) {
77
71
  probeUrl = new URL(info.url);
78
72
  }
79
73
  catch {
80
- try {
81
- unlinkSync(path);
82
- }
83
- catch { }
74
+ removePidfile(hippoRoot);
84
75
  return null;
85
76
  }
86
77
  if (probeUrl.protocol !== 'http:' ||
87
78
  !PIDFILE_LOOPBACK_HOSTS.has(probeUrl.hostname) ||
88
79
  probeUrl.port !== String(info.port)) {
89
- try {
90
- unlinkSync(path);
91
- }
92
- catch { }
80
+ removePidfile(hippoRoot);
93
81
  return null;
94
82
  }
95
83
  // Confirm the process answering on info.url is this hippo server by matching
@@ -103,10 +91,7 @@ export async function detectServer(hippoRoot) {
103
91
  signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),
104
92
  });
105
93
  if (!res.ok || !res.body) {
106
- try {
107
- unlinkSync(path);
108
- }
109
- catch { }
94
+ removePidfile(hippoRoot);
110
95
  return null;
111
96
  }
112
97
  // Read the body under a hard byte cap. The process answering on info.url
@@ -123,10 +108,7 @@ export async function detectServer(hippoRoot) {
123
108
  received += value.byteLength;
124
109
  if (received > HEALTH_BODY_MAX_BYTES) {
125
110
  await reader.cancel();
126
- try {
127
- unlinkSync(path);
128
- }
129
- catch { }
111
+ removePidfile(hippoRoot);
130
112
  return null;
131
113
  }
132
114
  raw += decoder.decode(value, { stream: true });
@@ -134,10 +116,7 @@ export async function detectServer(hippoRoot) {
134
116
  raw += decoder.decode();
135
117
  const body = JSON.parse(raw);
136
118
  if (body.started_at !== info.started_at) {
137
- try {
138
- unlinkSync(path);
139
- }
140
- catch { }
119
+ removePidfile(hippoRoot);
141
120
  return null;
142
121
  }
143
122
  }
@@ -148,10 +127,7 @@ export async function detectServer(hippoRoot) {
148
127
  // SAFETY: err's shape is unknown (catch clause); reading an optional
149
128
  // .name property structurally is safe regardless of the object's actual type.
150
129
  if (err?.name !== 'TimeoutError') {
151
- try {
152
- unlinkSync(path);
153
- }
154
- catch { }
130
+ removePidfile(hippoRoot);
155
131
  }
156
132
  return null;
157
133
  }
@@ -190,7 +166,7 @@ export function removePidfile(hippoRoot) {
190
166
  try {
191
167
  unlinkSync(path);
192
168
  }
193
- catch { }
169
+ catch { /* already gone or undeletable; the next detectServer probe re-checks */ }
194
170
  }
195
171
  /**
196
172
  * Remove the pidfile only if it still describes the caller's own server.
package/dist/server.js CHANGED
@@ -2,12 +2,13 @@ import { createServer } from 'node:http';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { resolveProjectIdentity } from './project-identity.js';
5
+ import { assembleCost, contextCost, drillCost } from './context-render.js';
5
6
  import { detectServer, writePidfile, removePidfileIfOwned } from './server-detect.js';
6
7
  import { resolveTenantId } from './tenant.js';
7
8
  import { openHippoDb, closeHippoDb } from './db.js';
8
9
  import { updateStats } from './store.js';
9
10
  import { buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, hashQueryText, } from './recall-history.js';
10
- import { appendAuditEvent } from './audit.js';
11
+ import { appendAuditEvent, AUDIT_OPS } from './audit.js';
11
12
  // v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
12
13
  // for the HTTP pipeline. Separate from CLI/MCP rings per plan v3 (per-
13
14
  // pipeline rings; no IPC). HTTP is the only caller that threads its
@@ -63,65 +64,7 @@ const PUBLIC_ROUTES = new Set([
63
64
  function isPublicRoute(method, path) {
64
65
  return PUBLIC_ROUTES.has(`${method} ${path}`);
65
66
  }
66
- const VALID_AUDIT_OPS = new Set([
67
- 'remember',
68
- 'recall',
69
- 'promote',
70
- 'supersede',
71
- 'forget',
72
- 'archive_raw',
73
- 'auth_revoke',
74
- 'auth_create', // v1.12.4: emitted by api.authCreate
75
- 'outcome', // v1.11.5: pre-existing drift — emitted today but rejected by old Set
76
- 'consolidate', // v1.11.5: emitted by api.sleep / POST /v1/sleep
77
- 'audit_prune', // v1.12.9: emitted by pruneAuditLog
78
- 'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
79
- 'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
80
- 'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
81
- 'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
82
- 'predict_close', // v0.31 / E2 — emitted by closePrediction
83
- 'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
84
- 'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
85
- 'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
86
- 'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
87
- 'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
88
- 'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
89
- 'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
90
- 'recall_availability_detected', // v1.13.x / J2 - emitted when availability/recency-bias hint fires
91
- 'decision_create', // E2 decision first-class object — emitted by saveDecision
92
- 'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
93
- 'decision_close', // E2 — emitted by closeDecision
94
- 'incident_open', // E2 incident first-class object — emitted by saveIncident
95
- 'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
96
- 'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
97
- 'process_create', // E2 process first-class object — emitted by saveProcess
98
- 'process_supersede', // E2 — emitted by saveProcess on a supersession
99
- 'process_close', // E2 — emitted by closeProcess
100
- 'policy_create', // E2 policy first-class object — emitted by savePolicy
101
- 'policy_supersede', // E2 — emitted by savePolicy on a supersession
102
- 'policy_close', // E2 — emitted by closePolicy
103
- 'skill_create', // E2 skill first-class object — emitted by saveSkill
104
- 'skill_supersede', // E2 — emitted by saveSkill on a supersession
105
- 'skill_close', // E2 — emitted by closeSkill
106
- 'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
107
- 'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
108
- 'project_brief_close', // E2 — emitted by closeProjectBrief
109
- 'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
110
- 'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
111
- 'customer_note_close', // E2 — emitted by closeCustomerNote
112
- 'mv_rescue', // LC2-E3 — emitted by consolidate() per rescue; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
113
- 'reject_value', // AT1 — emitted by `hippo reject`; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
114
- 'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
115
- 'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
116
- 'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
117
- 'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
118
- 'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
119
- 'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
120
- 'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
121
- 'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
122
- 'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
123
- 'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
124
- ]);
67
+ const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
125
68
  // Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
126
69
  // enough to dump a small deployment's full audit log without paginating, but
127
70
  // small enough that a malicious client can't ask for the world.
@@ -853,7 +796,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
853
796
  assembleExtra.summarizeOlder = summarizeOlder;
854
797
  if (scope !== undefined)
855
798
  assembleExtra.scope = scope;
856
- const result = assemble(ctx, assembleMatch.id, assembleExtra);
799
+ const result = assemble(ctx, assembleMatch.id, { ...assembleExtra, cost: assembleCost(assembleMatch.id) });
857
800
  recordTokens(ctx, 'http_assemble', { items: result.items.length, tokens: result.tokens, sessionId: assembleMatch.id });
858
801
  sendJson(res, 200, result);
859
802
  return;
@@ -896,7 +839,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
896
839
  drillExtra.budget = budget;
897
840
  if (depth !== undefined)
898
841
  drillExtra.depth = depth;
899
- const result = drillDown(ctx, drillMatch.id, drillExtra);
842
+ const result = drillDown(ctx, drillMatch.id, { ...drillExtra, cost: drillCost });
900
843
  if ('failure' in result) {
901
844
  // v1.6.4: leaf id maps to 422 (caller-actionable). Other cases stay
902
845
  // as 404 to avoid leaking cross-tenant existence or scope grants.
@@ -1054,6 +997,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
1054
997
  includeRecent,
1055
998
  crossProject,
1056
999
  currentProject: resolveProjectIdentity(dirname(resolve(opts.hippoRoot))).name,
1000
+ cost: contextCost('markdown', 'observe'), // clients render; the budget prices the block `hippo context` would print
1057
1001
  });
1058
1002
  recordTokens(ctx, 'http_context', { items: result.entries.length, tokens: result.tokens });
1059
1003
  sendJson(res, 200, result);
@@ -0,0 +1,79 @@
1
+ import { type SessionTurn } from './capture.js';
2
+ import { type MemoryEntry } from './memory.js';
3
+ export declare const SESSION_DIGEST_TAG = "session-digest";
4
+ /** Five-word runs are stock phrases; six in a row is a copied clause. */
5
+ export declare const ECHO_WINDOW = 6;
6
+ /** Five sentences of about 150 chars leave room for the Changed line inside the total cap. */
7
+ export declare const MAX_SENTENCES = 5;
8
+ /** A longer "sentence" in agent text is an unsplit list or pasted output. */
9
+ export declare const MAX_SENTENCE_CHARS = 300;
10
+ /** At about 20 chars per repo path this keeps the Changed line near 50 tokens. */
11
+ export declare const MAX_FILES = 10;
12
+ /** 300 tokens at 4 chars each: 20% of the 1500-token default budget of both context surfaces. */
13
+ export declare const MAX_DIGEST_CHARS = 1200;
14
+ /** Bounds a detached worker's memory; the final message sits at the end, so a tail read loses nothing. */
15
+ export declare const READ_CAP_BYTES: number;
16
+ export interface DigestEdit {
17
+ filePath: string;
18
+ /** Working directory in effect at the call, for relative paths. */
19
+ base: string | null;
20
+ }
21
+ export interface SessionScan {
22
+ turns: SessionTurn[];
23
+ finalText: string;
24
+ /** The first working directory the transcript names: where the session ran. */
25
+ cwd: string | null;
26
+ /** Edits that landed, in call order. */
27
+ edits: DigestEdit[];
28
+ }
29
+ /** One pass over a Claude Code or Codex transcript: the turns capture reads, the closing message, and the edits that landed. */
30
+ export declare function scanSessionTranscript(jsonl: string): SessionScan;
31
+ export declare function readSessionTranscript(transcriptPath: string, log: (message: string) => void): string;
32
+ /** The transcript, read once and scanned, or null with the reason logged. */
33
+ export declare function readSessionScan(transcriptPath: string, log: (message: string) => void): SessionScan | null;
34
+ /** Test-only seam, the scheduler's pattern: lets a test prove a network path never reaches the probe. Null restores it. */
35
+ export declare function __setDigestExistsProbe(probe: ((p: string) => boolean) | null): void;
36
+ /** The store root is realpath'd; transcript paths are not, and a deleted file has no realpath of its own. Null for a network path. */
37
+ export declare function realFsPath(p: string): string | null;
38
+ /** Repo-relative with forward slashes, '' for the root itself, null outside the repo or when the path cannot be resolved. */
39
+ export declare function repoRelative(p: string, repoRoot: string): string | null;
40
+ export declare const USER_SEGMENT: readonly RegExp[];
41
+ /** Sentences of the reply; code spans and "e.g." are held out of the split so their dots never end a sentence. */
42
+ export declare function digestSentences(text: string): string[];
43
+ export declare function sentenceScore(sentence: string): number;
44
+ export interface DigestSources {
45
+ finalText: string;
46
+ /** Text the session already holds: its own prompts, and what hippo injected into it. */
47
+ echoTexts: readonly string[];
48
+ edits: readonly DigestEdit[];
49
+ repoRoot: string;
50
+ }
51
+ export interface DigestDraft {
52
+ content: string;
53
+ sentences: number;
54
+ files: number;
55
+ }
56
+ /** The digest text, or null when nothing survives the filters. Sentences print in reply order. */
57
+ export declare function buildSessionDigest(sources: DigestSources): DigestDraft | null;
58
+ /** Facts extracted from a digest and DAG summaries over one inherit its tag, but they are not the digest itself. */
59
+ export declare function isSessionDigestRow(entry: Pick<MemoryEntry, 'source' | 'tags' | 'extracted_from'>): boolean;
60
+ /** Same session, same row: a second worker for one session updates the digest instead of adding one. */
61
+ export declare function sessionDigestId(tenantId: string, key: string): string;
62
+ export interface SessionDigestOptions {
63
+ /** Session id, or the transcript's basename when the host gives none. */
64
+ key: string;
65
+ tenantId: string;
66
+ }
67
+ export interface DigestOutcome {
68
+ written: boolean;
69
+ reason: string;
70
+ sentences: number;
71
+ files: number;
72
+ }
73
+ export declare function writeSessionDigest(hippoRoot: string, scan: SessionScan, opts: SessionDigestOptions): DigestOutcome;
74
+ export interface DigestRunOptions extends SessionDigestOptions {
75
+ log: (message: string) => void;
76
+ }
77
+ /** Never throws: a digest failure must not block the handoff or the snapshot close. Logs carry no digest text. */
78
+ export declare function recordSessionDigest(hippoRoot: string, scan: SessionScan | null, opts: DigestRunOptions): void;
79
+ //# sourceMappingURL=session-digest.d.ts.map