hippo-memory 1.56.0 → 1.58.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.
Files changed (129) hide show
  1. package/README.md +11 -0
  2. package/dist/agent-memories/claude-code.js +1 -1
  3. package/dist/agent-memories/gemini.js +1 -1
  4. package/dist/api-errors.d.ts +27 -0
  5. package/dist/api-errors.js +37 -0
  6. package/dist/api.d.ts +21 -14
  7. package/dist/api.js +97 -71
  8. package/dist/audit.d.ts +4 -0
  9. package/dist/audit.js +11 -0
  10. package/dist/autolearn.d.ts +1 -1
  11. package/dist/autolearn.js +7 -5
  12. package/dist/capture-contract.d.ts +47 -0
  13. package/dist/capture-contract.js +49 -0
  14. package/dist/capture-error.js +2 -1
  15. package/dist/capture.d.ts +0 -13
  16. package/dist/capture.js +5 -66
  17. package/dist/card-detail.d.ts +1 -1
  18. package/dist/card-detail.js +1 -1
  19. package/dist/cli/shared.d.ts +137 -0
  20. package/dist/cli/shared.js +834 -0
  21. package/dist/cli/sleep.d.ts +10 -0
  22. package/dist/cli/sleep.js +171 -0
  23. package/dist/cli.d.ts +0 -7
  24. package/dist/cli.js +322 -1827
  25. package/dist/client.js +9 -0
  26. package/dist/codex-patch.js +1 -1
  27. package/dist/compaction-record.d.ts +1 -1
  28. package/dist/compaction-record.js +3 -2
  29. package/dist/config.d.ts +5 -0
  30. package/dist/config.js +17 -0
  31. package/dist/connectors/github/dlq.js +5 -2
  32. package/dist/connectors/github/octokit-client.js +4 -2
  33. package/dist/connectors/github/webhook.d.ts +19 -0
  34. package/dist/connectors/github/webhook.js +313 -0
  35. package/dist/connectors/slack/dlq.js +6 -2
  36. package/dist/connectors/slack/web-client.js +7 -5
  37. package/dist/connectors/slack/webhook.d.ts +22 -0
  38. package/dist/connectors/slack/webhook.js +203 -0
  39. package/dist/consolidate.d.ts +10 -0
  40. package/dist/consolidate.js +38 -35
  41. package/dist/context-auto.d.ts +3 -0
  42. package/dist/context-auto.js +34 -0
  43. package/dist/customer-notes.js +16 -14
  44. package/dist/dag.js +3 -2
  45. package/dist/dashboard.js +3 -2
  46. package/dist/db.d.ts +12 -0
  47. package/dist/db.js +62 -1
  48. package/dist/decisions.js +11 -9
  49. package/dist/doctor.js +5 -0
  50. package/dist/embedding-provider.js +3 -3
  51. package/dist/embeddings.d.ts +4 -4
  52. package/dist/embeddings.js +72 -16
  53. package/dist/eval-stats.d.ts +58 -0
  54. package/dist/eval-stats.js +111 -0
  55. package/dist/extract.js +3 -2
  56. package/dist/goals.d.ts +49 -25
  57. package/dist/goals.js +39 -22
  58. package/dist/graph-extract.js +1 -1
  59. package/dist/graph-recall.d.ts +1 -1
  60. package/dist/graph-recall.js +1 -1
  61. package/dist/graph.js +1 -1
  62. package/dist/hooks.d.ts +1 -3
  63. package/dist/hooks.js +2 -4
  64. package/dist/http-retry.d.ts +21 -0
  65. package/dist/http-retry.js +50 -0
  66. package/dist/http-util.d.ts +39 -0
  67. package/dist/http-util.js +56 -0
  68. package/dist/importers.d.ts +2 -0
  69. package/dist/importers.js +16 -5
  70. package/dist/incidents.js +13 -11
  71. package/dist/index.d.ts +5 -2
  72. package/dist/index.js +5 -2
  73. package/dist/judgment.js +10 -17
  74. package/dist/log.d.ts +25 -0
  75. package/dist/log.js +48 -0
  76. package/dist/mcp/server.js +224 -308
  77. package/dist/mcp/tool-args.d.ts +21 -0
  78. package/dist/mcp/tool-args.js +80 -0
  79. package/dist/memory.d.ts +19 -0
  80. package/dist/memory.js +41 -2
  81. package/dist/overlap-index.d.ts +7 -0
  82. package/dist/overlap-index.js +38 -0
  83. package/dist/pilot-arm.d.ts +9 -0
  84. package/dist/pilot-arm.js +47 -0
  85. package/dist/policies.js +14 -12
  86. package/dist/predictions.js +11 -9
  87. package/dist/processes.js +16 -14
  88. package/dist/project-briefs.js +19 -16
  89. package/dist/project-identity.d.ts +1 -1
  90. package/dist/project-identity.js +25 -1
  91. package/dist/prompt-recall.js +1 -1
  92. package/dist/raw-archive.js +7 -6
  93. package/dist/recall-history.d.ts +5 -0
  94. package/dist/recall-history.js +9 -0
  95. package/dist/recall-pipeline.d.ts +101 -0
  96. package/dist/recall-pipeline.js +313 -0
  97. package/dist/recall-scope.d.ts +24 -1
  98. package/dist/recall-scope.js +29 -2
  99. package/dist/refine-llm.js +3 -2
  100. package/dist/reject-flow.js +6 -9
  101. package/dist/rejection.d.ts +2 -1
  102. package/dist/rejection.js +2 -1
  103. package/dist/search.d.ts +0 -20
  104. package/dist/search.js +16 -51
  105. package/dist/secret-detect.d.ts +13 -1
  106. package/dist/secret-detect.js +33 -1
  107. package/dist/server.d.ts +3 -1
  108. package/dist/server.js +1854 -2566
  109. package/dist/session-digest.js +2 -1
  110. package/dist/shared.js +7 -6
  111. package/dist/skills.js +17 -15
  112. package/dist/store-cards.d.ts +53 -0
  113. package/dist/store-cards.js +512 -0
  114. package/dist/store.d.ts +2 -89
  115. package/dist/store.js +10 -566
  116. package/dist/tenant.d.ts +22 -0
  117. package/dist/tenant.js +26 -0
  118. package/dist/token-ledger.d.ts +4 -2
  119. package/dist/token-ledger.js +2 -2
  120. package/dist/tokenize.d.ts +2 -0
  121. package/dist/tokenize.js +8 -0
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  125. package/extensions/openclaw-plugin/package.json +1 -1
  126. package/openclaw.plugin.json +1 -1
  127. package/package.json +1 -1
  128. package/dist/connectors/slack/ratelimit.d.ts +0 -9
  129. package/dist/connectors/slack/ratelimit.js +0 -18
package/dist/goals.d.ts CHANGED
@@ -79,12 +79,36 @@ export interface GetActiveGoalsOpts {
79
79
  }
80
80
  export declare function getActiveGoals(hippoRoot: string, opts: GetActiveGoalsOpts): Goal[];
81
81
  export declare function getActiveGoalsWithDb(db: DatabaseSyncLike, opts: GetActiveGoalsOpts): Goal[];
82
+ /** One `goal_recall_log` row: a boosted local memory recalled while its goal was active. */
83
+ export interface GoalRecallLogRow {
84
+ goalId: string;
85
+ memoryId: string;
86
+ tenantId: string;
87
+ sessionId: string;
88
+ recalledAt: string;
89
+ score: number;
90
+ }
91
+ /** Options shared by {@link computeGoalStackBoost} and {@link applyGoalStackBoost}. */
92
+ export interface GoalStackBoostOpts {
93
+ sessionId: string;
94
+ tenantId: string;
95
+ limit: number;
96
+ /**
97
+ * Optional side-channel: one goal-boost `RerankStep` per boosted row, keyed by
98
+ * `entry.id`. A map rather than a row field because the helper re-spreads rows.
99
+ * Only populated when passed, so the default path allocates nothing.
100
+ */
101
+ trace?: Map<string, RerankStep>;
102
+ }
103
+ /** The boosted, re-sorted rows and the `goal_recall_log` rows they earn. */
104
+ export interface GoalStackBoost<R> {
105
+ results: R[];
106
+ log: GoalRecallLogRow[];
107
+ }
82
108
  /**
83
- * v1.7.4 -- dlPFC goal-stack boost helper. Applies the multi-goal boost to a
84
- * list of entry-backed scored rows when (tenant, session) has active goals.
85
- * Pre-v1.7.4 this logic lived inline in cmdRecall (src/cli.ts:988-1140);
86
- * lifting here lets api.recall (primary band only) AND MCP physics/hybrid
87
- * call it.
109
+ * dlPFC goal-stack boost. Applies the multi-goal boost to entry-backed scored
110
+ * rows when (tenant, session) has active goals and returns the log rows to
111
+ * write, without writing them; {@link writeGoalRecallLog} persists them.
88
112
  *
89
113
  * Caller responsibilities:
90
114
  * - Do NOT call when an explicit `goalTag` is set (caller's gate)
@@ -94,31 +118,31 @@ export declare function getActiveGoalsWithDb(db: DatabaseSyncLike, opts: GetActi
94
118
  * - Recompute `tokens` after if returned rows are projected to a budgeted
95
119
  * shape
96
120
  *
97
- * Side effects:
98
- * - INSERT OR IGNORE into `goal_recall_log` for each (boosted, goal) pair
99
- * - Local memory id filter applied before INSERT (skips global-only ids
100
- * to preserve FK invariant on goal_recall_log.memory_id)
121
+ * Log rows cover the top `limit` boosted rows that live in this store's
122
+ * `memories` table (global-only ids are skipped to keep the FK on
123
+ * goal_recall_log.memory_id valid).
101
124
  *
102
- * @internal v1.7.4 -- internal recall ranking helper. Subject to change.
125
+ * @internal Recall ranking helper. Subject to change.
126
+ */
127
+ export declare function computeGoalStackBoost<R extends {
128
+ entry: MemoryEntry;
129
+ score: number;
130
+ }>(db: DatabaseSyncLike, results: R[], opts: GoalStackBoostOpts): GoalStackBoost<R>;
131
+ /**
132
+ * Writes goal-boost log rows. INSERT OR IGNORE because UNIQUE(memory_id, goal_id)
133
+ * makes a re-recall during the same goal life a no-op for outcome attribution.
134
+ */
135
+ export declare function writeGoalRecallLog(db: DatabaseSyncLike, rows: readonly GoalRecallLogRow[]): void;
136
+ /**
137
+ * {@link computeGoalStackBoost} plus {@link writeGoalRecallLog} in one call, for
138
+ * pipelines that boost and log on the same handle.
139
+ *
140
+ * @internal Recall ranking helper. Subject to change.
103
141
  */
104
142
  export declare function applyGoalStackBoost<R extends {
105
143
  entry: MemoryEntry;
106
144
  score: number;
107
- }>(db: DatabaseSyncLike, results: R[], opts: {
108
- sessionId: string;
109
- tenantId: string;
110
- limit: number;
111
- /**
112
- * A7 recall-trace (optional side-channel). When supplied, the helper
113
- * records one goal-boost `RerankStep` per ACTUALLY-boosted row, keyed by
114
- * `entry.id`. This is a SEPARATE accumulator, NOT a field on the result
115
- * row — the helper re-spreads rows and strips internal markers
116
- * (`_goalMatches` below), so a row field would be dropped. The score-mul
117
- * + re-sort math is untouched; the trace is only populated when this map
118
- * is passed (default path never allocates → byte-identical).
119
- */
120
- trace?: Map<string, RerankStep>;
121
- }): R[];
145
+ }>(db: DatabaseSyncLike, results: R[], opts: GoalStackBoostOpts): R[];
122
146
  export interface CompleteGoalOpts {
123
147
  outcomeScore?: number;
124
148
  /**
package/dist/goals.js CHANGED
@@ -142,11 +142,9 @@ export function getActiveGoalsWithDb(db, opts) {
142
142
  return rows.map(rowToGoal);
143
143
  }
144
144
  /**
145
- * v1.7.4 -- dlPFC goal-stack boost helper. Applies the multi-goal boost to a
146
- * list of entry-backed scored rows when (tenant, session) has active goals.
147
- * Pre-v1.7.4 this logic lived inline in cmdRecall (src/cli.ts:988-1140);
148
- * lifting here lets api.recall (primary band only) AND MCP physics/hybrid
149
- * call it.
145
+ * dlPFC goal-stack boost. Applies the multi-goal boost to entry-backed scored
146
+ * rows when (tenant, session) has active goals and returns the log rows to
147
+ * write, without writing them; {@link writeGoalRecallLog} persists them.
150
148
  *
151
149
  * Caller responsibilities:
152
150
  * - Do NOT call when an explicit `goalTag` is set (caller's gate)
@@ -156,18 +154,17 @@ export function getActiveGoalsWithDb(db, opts) {
156
154
  * - Recompute `tokens` after if returned rows are projected to a budgeted
157
155
  * shape
158
156
  *
159
- * Side effects:
160
- * - INSERT OR IGNORE into `goal_recall_log` for each (boosted, goal) pair
161
- * - Local memory id filter applied before INSERT (skips global-only ids
162
- * to preserve FK invariant on goal_recall_log.memory_id)
157
+ * Log rows cover the top `limit` boosted rows that live in this store's
158
+ * `memories` table (global-only ids are skipped to keep the FK on
159
+ * goal_recall_log.memory_id valid).
163
160
  *
164
- * @internal v1.7.4 -- internal recall ranking helper. Subject to change.
161
+ * @internal Recall ranking helper. Subject to change.
165
162
  */
166
- export function applyGoalStackBoost(db, results, opts) {
163
+ export function computeGoalStackBoost(db, results, opts) {
167
164
  const { sessionId, tenantId, limit, trace } = opts;
168
165
  const active = getActiveGoalsWithDb(db, { sessionId, tenantId });
169
166
  if (active.length === 0)
170
- return results;
167
+ return { results, log: [] };
171
168
  const goalsByTag = new Map(active.map((g) => [g.goalName, g]));
172
169
  // Load retrieval_policy rows for active goals so per-policy multipliers
173
170
  // can compose onto the base goal-tag boost. Composed result is hard-capped
@@ -271,15 +268,8 @@ export function applyGoalStackBoost(db, results, opts) {
271
268
  for (const row of localRows)
272
269
  localIds.add(row.id);
273
270
  }
274
- // Log top-K boosted recalls. INSERT OR IGNORE because
275
- // UNIQUE(memory_id, goal_id) means a re-recall during the same goal life
276
- // is a no-op for outcome attribution.
277
271
  const recalledAt = new Date().toISOString();
278
- const insertLog = db.prepare(`
279
- INSERT OR IGNORE INTO goal_recall_log
280
- (goal_id, memory_id, tenant_id, session_id, recalled_at, score)
281
- VALUES (?, ?, ?, ?, ?, ?)
282
- `);
272
+ const log = [];
283
273
  for (const r of boosted.slice(0, limit)) {
284
274
  if (!localIds.has(r.entry.id))
285
275
  continue; // global -> skip log insert
@@ -290,10 +280,37 @@ export function applyGoalStackBoost(db, results, opts) {
290
280
  const goal = goalsByTag.get(tag);
291
281
  if (!goal)
292
282
  continue;
293
- insertLog.run(goal.id, r.entry.id, tenantId, sessionId, recalledAt, r.score);
283
+ log.push({ goalId: goal.id, memoryId: r.entry.id, tenantId, sessionId, recalledAt, score: r.score });
294
284
  }
295
285
  }
296
- return boosted;
286
+ return { results: boosted, log };
287
+ }
288
+ /**
289
+ * Writes goal-boost log rows. INSERT OR IGNORE because UNIQUE(memory_id, goal_id)
290
+ * makes a re-recall during the same goal life a no-op for outcome attribution.
291
+ */
292
+ export function writeGoalRecallLog(db, rows) {
293
+ if (rows.length === 0)
294
+ return;
295
+ const insertLog = db.prepare(`
296
+ INSERT OR IGNORE INTO goal_recall_log
297
+ (goal_id, memory_id, tenant_id, session_id, recalled_at, score)
298
+ VALUES (?, ?, ?, ?, ?, ?)
299
+ `);
300
+ for (const row of rows) {
301
+ insertLog.run(row.goalId, row.memoryId, row.tenantId, row.sessionId, row.recalledAt, row.score);
302
+ }
303
+ }
304
+ /**
305
+ * {@link computeGoalStackBoost} plus {@link writeGoalRecallLog} in one call, for
306
+ * pipelines that boost and log on the same handle.
307
+ *
308
+ * @internal Recall ranking helper. Subject to change.
309
+ */
310
+ export function applyGoalStackBoost(db, results, opts) {
311
+ const boost = computeGoalStackBoost(db, results, opts);
312
+ writeGoalRecallLog(db, boost.log);
313
+ return boost.results;
297
314
  }
298
315
  const POSITIVE_OUTCOME_THRESHOLD = 0.7;
299
316
  const NEGATIVE_OUTCOME_THRESHOLD = 0.3;
@@ -25,7 +25,7 @@ import { loadDecisions } from './decisions.js';
25
25
  import { loadPolicies } from './policies.js';
26
26
  import { loadCustomerNotes } from './customer-notes.js';
27
27
  import { loadProjectBriefs } from './project-briefs.js';
28
- import { assertTenantId } from './store.js';
28
+ import { assertTenantId } from './tenant.js';
29
29
  /** Per-type load cap (the loaders default to 100). A type whose active or superseded
30
30
  * set exceeds this is truncated; `ExtractResult.truncated` records it so the
31
31
  * incompleteness is observable rather than silent. */
@@ -1,4 +1,4 @@
1
- import { type ResultCost, type SearchResult } from './search.js';
1
+ import type { ResultCost, 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;
@@ -37,7 +37,7 @@
37
37
  * check-graph-writes lint permits this module living outside graph.ts.
38
38
  */
39
39
  import { loadEntriesByIds } from './store.js';
40
- import { estimateTokens } from './search.js';
40
+ import { estimateTokens } from './token-ledger.js';
41
41
  import { compareEntryIdentity } from './compare.js';
42
42
  import { loadEntitiesByMemoryId, loadEntitiesByIds, loadNeighborRelations, } from './graph.js';
43
43
  import { passesCliRecallScopeFilter, passesScopeFilterForRecall } from './recall-scope.js';
package/dist/graph.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * until E3.2 multi-hop recall.
19
19
  */
20
20
  import { openHippoDb, closeHippoDb } from './db.js';
21
- import { assertTenantId } from './store.js';
21
+ import { assertTenantId } from './tenant.js';
22
22
  const SOURCE_OBJECT_TABLE = {
23
23
  decision: 'decisions',
24
24
  policy: 'policies',
package/dist/hooks.d.ts CHANGED
@@ -45,8 +45,8 @@ export interface CodexWrapperPaths {
45
45
  wrapperShPath: string;
46
46
  logFile: string;
47
47
  runsDir: string;
48
+ codexHome: string;
48
49
  historyPath: string;
49
- sessionsDir: string;
50
50
  }
51
51
  export interface CodexWrapperInstallResult {
52
52
  installed: boolean;
@@ -63,8 +63,6 @@ export interface CodexWrapperMetadata {
63
63
  backupPath: string;
64
64
  installMode: 'same-path' | 'cmd-shim';
65
65
  logFile: string;
66
- historyPath: string;
67
- sessionsDir: string;
68
66
  installedAt: string;
69
67
  }
70
68
  export interface EnsureCodexWrapperResult {
package/dist/hooks.js CHANGED
@@ -154,7 +154,7 @@ export function defaultPreCompactLogPath() {
154
154
  }
155
155
  export function resolveCodexWrapperPaths() {
156
156
  const home = homeDir();
157
- const codexHome = path.join(home, '.codex');
157
+ const codexHome = codexHomeDir(home);
158
158
  const wrapperDir = path.join(home, '.hippo', 'bin');
159
159
  return {
160
160
  wrapperDir,
@@ -164,8 +164,8 @@ export function resolveCodexWrapperPaths() {
164
164
  wrapperShPath: path.join(wrapperDir, 'codex'),
165
165
  logFile: path.join(home, '.hippo', 'logs', 'codex-sleep.log'),
166
166
  runsDir: path.join(home, '.hippo', 'runs', 'codex'),
167
+ codexHome,
167
168
  historyPath: path.join(codexHome, 'history.jsonl'),
168
- sessionsDir: path.join(codexHome, 'sessions'),
169
169
  };
170
170
  }
171
171
  function pathEquals(a, b) {
@@ -348,8 +348,6 @@ export function installCodexWrapper(realCodexPath) {
348
348
  backupPath: plan.backupPath,
349
349
  installMode: plan.installMode,
350
350
  logFile: paths.logFile,
351
- historyPath: paths.historyPath,
352
- sessionsDir: paths.sessionsDir,
353
351
  installedAt: new Date().toISOString(),
354
352
  };
355
353
  fs.writeFileSync(paths.metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf8');
@@ -0,0 +1,21 @@
1
+ /** One retry policy for outbound HTTP: a timeout on every attempt, and backoff on 429 and 5xx only. */
2
+ export interface RetryPolicy {
3
+ /** Per-attempt limit; a stalled peer ends as a thrown `TimeoutError`, never a hang. */
4
+ timeoutMs: number;
5
+ /** Total attempts including the first. */
6
+ attempts?: number;
7
+ baseDelayMs?: number;
8
+ /** A Retry-After longer than this hands the response back, so a caller with its own long pause keeps it. */
9
+ maxDelayMs?: number;
10
+ fetchFn?: typeof fetch;
11
+ sleep?: (ms: number) => Promise<void>;
12
+ random?: () => number;
13
+ }
14
+ /** LLM calls (consolidation refine, DAG summaries, fact extraction) share one budget; `HIPPO_LLM_TIMEOUT_MS` overrides it. */
15
+ export declare function llmTimeoutMs(): number;
16
+ export declare function isRetryableStatus(status: number): boolean;
17
+ /** Retry-After as milliseconds: delta-seconds or an HTTP date; null when absent or unreadable. */
18
+ export declare function parseRetryAfterMs(header: string | null, now?: number): number | null;
19
+ /** `fetch` with a per-attempt timeout and up to `attempts` tries on 429 and 5xx; the last response comes back as is and transport errors throw at once. */
20
+ export declare function fetchWithRetry(url: string | URL, init: RequestInit, policy: RetryPolicy): Promise<Response>;
21
+ //# sourceMappingURL=http-retry.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** One retry policy for outbound HTTP: a timeout on every attempt, and backoff on 429 and 5xx only. */
2
+ const DEFAULT_ATTEMPTS = 3;
3
+ const DEFAULT_BASE_DELAY_MS = 250;
4
+ const DEFAULT_MAX_DELAY_MS = 8_000;
5
+ const DEFAULT_LLM_TIMEOUT_MS = 60_000;
6
+ /** LLM calls (consolidation refine, DAG summaries, fact extraction) share one budget; `HIPPO_LLM_TIMEOUT_MS` overrides it. */
7
+ export function llmTimeoutMs() {
8
+ const parsed = Number.parseInt(process.env.HIPPO_LLM_TIMEOUT_MS ?? '', 10);
9
+ return parsed > 0 ? parsed : DEFAULT_LLM_TIMEOUT_MS;
10
+ }
11
+ export function isRetryableStatus(status) {
12
+ return status === 429 || (status >= 500 && status <= 599);
13
+ }
14
+ /** Retry-After as milliseconds: delta-seconds or an HTTP date; null when absent or unreadable. */
15
+ export function parseRetryAfterMs(header, now = Date.now()) {
16
+ if (header === null || header.trim() === '')
17
+ return null;
18
+ const seconds = Number(header);
19
+ if (Number.isFinite(seconds))
20
+ return Math.max(0, seconds * 1000);
21
+ const at = Date.parse(header);
22
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
23
+ }
24
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
25
+ /** `fetch` with a per-attempt timeout and up to `attempts` tries on 429 and 5xx; the last response comes back as is and transport errors throw at once. */
26
+ export async function fetchWithRetry(url, init, policy) {
27
+ const fetchFn = policy.fetchFn ?? fetch;
28
+ const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
29
+ const baseDelayMs = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
30
+ const maxDelayMs = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
31
+ const sleep = policy.sleep ?? realSleep;
32
+ const random = policy.random ?? Math.random;
33
+ for (let attempt = 1;; attempt++) {
34
+ const timeout = AbortSignal.timeout(policy.timeoutMs);
35
+ const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
36
+ const res = await fetchFn(url, { ...init, signal });
37
+ if (!isRetryableStatus(res.status) || attempt >= attempts)
38
+ return res;
39
+ const retryAfter = parseRetryAfterMs(res.headers.get('retry-after'));
40
+ if (retryAfter !== null && retryAfter > maxDelayMs)
41
+ return res;
42
+ // Jitter over the upper half keeps parallel callers from retrying in lockstep.
43
+ const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
44
+ const delay = retryAfter ?? ceiling / 2 + random() * (ceiling / 2);
45
+ // Frees the pooled socket before the next attempt.
46
+ await res.body?.cancel();
47
+ await sleep(delay);
48
+ }
49
+ }
50
+ //# sourceMappingURL=http-retry.js.map
@@ -0,0 +1,39 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export declare function isJsonObjectRecord(value: JsonValue | undefined): value is Record<string, JsonValue>;
6
+ export declare function isHeaderString(value: string | string[] | undefined): value is string;
7
+ export declare const JSON_HEADERS: {
8
+ readonly 'content-type': "application/json";
9
+ };
10
+ export declare class HttpError extends Error {
11
+ status: number;
12
+ constructor(status: number, message: string);
13
+ }
14
+ export declare class BodyTooLargeError extends Error {
15
+ }
16
+ /** The status and client-facing message for one failed request. */
17
+ export interface ApiErrorReply {
18
+ status: number;
19
+ message: string;
20
+ }
21
+ export declare const INTERNAL_ERROR_MESSAGE = "internal server error";
22
+ /** Maps by class so rewording a message never moves a status; an untyped error is a 500 whose text stays in the server log. */
23
+ export declare function mapApiError<E>(err: E): ApiErrorReply;
24
+ export declare function sendJson<T>(res: ServerResponse, status: number, body: T): void;
25
+ /**
26
+ * Read the entire request body into a Buffer. Caps at MAX_BODY_BYTES to keep
27
+ * a malicious or buggy client from exhausting memory. The cap is enforced
28
+ * mid-stream so we don't wait for an attacker to finish before erroring out.
29
+ */
30
+ export declare function readBody(req: IncomingMessage): Promise<string>;
31
+ /** Per-request values a connector webhook receiver reads; ServeOpts and the /v1 RouteRequest both satisfy it. */
32
+ export interface WebhookRequest {
33
+ req: IncomingMessage;
34
+ res: ServerResponse;
35
+ opts: {
36
+ hippoRoot: string;
37
+ };
38
+ }
39
+ //# sourceMappingURL=http-util.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { ApiError } from './api-errors.js';
2
+ export function isJsonObjectRecord(value) {
3
+ return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ // node:http header values are `string | string[] | undefined` (never a bare
6
+ // unknown), so this gets its own predicate rather than reusing isJsonString.
7
+ export function isHeaderString(value) {
8
+ return typeof value === 'string';
9
+ }
10
+ // 1 MB body cap. The CLI never sends payloads near this; anything bigger is
11
+ // almost certainly a misconfigured client or a deliberate memory-blowup attempt.
12
+ const MAX_BODY_BYTES = 1024 * 1024;
13
+ export const JSON_HEADERS = { 'content-type': 'application/json' };
14
+ export class HttpError extends Error {
15
+ status;
16
+ constructor(status, message) {
17
+ super(message);
18
+ this.status = status;
19
+ }
20
+ }
21
+ export class BodyTooLargeError extends Error {
22
+ }
23
+ export const INTERNAL_ERROR_MESSAGE = 'internal server error';
24
+ /** Maps by class so rewording a message never moves a status; an untyped error is a 500 whose text stays in the server log. */
25
+ export function mapApiError(err) {
26
+ if (err instanceof HttpError || err instanceof ApiError)
27
+ return { status: err.status, message: err.message };
28
+ if (err instanceof BodyTooLargeError)
29
+ return { status: 413, message: err.message };
30
+ return { status: 500, message: INTERNAL_ERROR_MESSAGE };
31
+ }
32
+ export function sendJson(res, status, body) {
33
+ res.writeHead(status, JSON_HEADERS);
34
+ res.end(JSON.stringify(body));
35
+ }
36
+ /**
37
+ * Read the entire request body into a Buffer. Caps at MAX_BODY_BYTES to keep
38
+ * a malicious or buggy client from exhausting memory. The cap is enforced
39
+ * mid-stream so we don't wait for an attacker to finish before erroring out.
40
+ */
41
+ export async function readBody(req) {
42
+ const chunks = [];
43
+ let total = 0;
44
+ for await (const chunk of req) {
45
+ // SAFETY: IncomingMessage never runs setEncoding() here, so every
46
+ // streamed chunk is a Buffer, not a decoded string.
47
+ const buf = chunk;
48
+ total += buf.length;
49
+ if (total > MAX_BODY_BYTES) {
50
+ throw new BodyTooLargeError('request body exceeds 1MB');
51
+ }
52
+ chunks.push(buf);
53
+ }
54
+ return Buffer.concat(chunks).toString('utf8');
55
+ }
56
+ //# sourceMappingURL=http-util.js.map
@@ -20,6 +20,8 @@ export interface ImportResult {
20
20
  /** K1 vault import: rows archived this run (changed + source-deleted). In a
21
21
  * dryRun this is the would-be count (a true deletion-sync preview). */
22
22
  archived?: number;
23
+ /** Entries stored with secret-shaped text redacted; optional for the same published-surface reason as `rejected`. */
24
+ redacted?: number;
23
25
  entries: MemoryEntry[];
24
26
  }
25
27
  export interface ImportOptions {
package/dist/importers.js CHANGED
@@ -13,6 +13,7 @@ import { remember, archiveRaw, isPrivateScope } from './api.js';
13
13
  import { openHippoDb, closeHippoDb } from './db.js';
14
14
  import { RejectedValueError, checkRejectionGuard } from './rejection.js';
15
15
  import { loadConfig } from './config.js';
16
+ import { vetSecrets } from './secret-detect.js';
16
17
  // ---------------------------------------------------------------------------
17
18
  // Shared core: dedup + write
18
19
  // ---------------------------------------------------------------------------
@@ -33,6 +34,7 @@ export function importEntries(chunks, source, tags, options) {
33
34
  let imported = 0;
34
35
  let skipped = 0;
35
36
  let rejected = 0;
37
+ let redacted = 0;
36
38
  const entries = [];
37
39
  // AT1 P2 fix: a dry-run preview never called writeEntry, so it never
38
40
  // checked tombstones either — every non-duplicate chunk counted as
@@ -42,7 +44,9 @@ export function importEntries(chunks, source, tags, options) {
42
44
  const dryRunDb = options.dryRun ? openHippoDb(targetRoot) : null;
43
45
  try {
44
46
  for (const raw of chunks) {
45
- const trimmed = raw.trim();
47
+ const original = raw.trim();
48
+ const trimmed = vetSecrets(original, allTags, true).content;
49
+ const wasRedacted = trimmed !== original;
46
50
  if (trimmed.length > 1000) {
47
51
  console.error(`Warning: imported memory truncated from ${trimmed.length} to 1000 chars`);
48
52
  }
@@ -109,8 +113,10 @@ export function importEntries(chunks, source, tags, options) {
109
113
  }
110
114
  entries.push(entry);
111
115
  imported++;
116
+ if (wasRedacted)
117
+ redacted++;
112
118
  }
113
- return { total, imported, skipped, rejected, entries };
119
+ return { total, imported, skipped, rejected, redacted, entries };
114
120
  }
115
121
  finally {
116
122
  if (dryRunDb)
@@ -430,6 +436,7 @@ export function importMarkdown(filePath, options) {
430
436
  // AT1 P2 fix: `rejected` is now optional on ImportResult (compat) — tolerate
431
437
  // undefined on either side of the accumulation.
432
438
  rejected: (totalResult.rejected ?? 0) + (result.rejected ?? 0),
439
+ redacted: (totalResult.redacted ?? 0) + (result.redacted ?? 0),
433
440
  entries: [...totalResult.entries, ...result.entries],
434
441
  };
435
442
  }
@@ -695,6 +702,7 @@ export function importVault(folderPath, options) {
695
702
  let skipped = 0;
696
703
  let rejected = 0;
697
704
  let archived = 0;
705
+ let redacted = 0;
698
706
  const entries = [];
699
707
  const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
700
708
  // AT1 P2 fix: dry-run never called remember(), so it never probed
@@ -796,7 +804,8 @@ export function importVault(folderPath, options) {
796
804
  // remember() owns the actual write. We build an `echo` of the SAME content +
797
805
  // tags via createMemory purely for the ImportResult, then reconcile its id to
798
806
  // remember()'s real row id so entries[] reflects the row that landed.
799
- const echo = createMemory(body, {
807
+ const content = vetSecrets(body, tags, true).content;
808
+ const echo = createMemory(content, {
800
809
  kind: 'raw',
801
810
  tags,
802
811
  scope,
@@ -815,7 +824,7 @@ export function importVault(folderPath, options) {
815
824
  // loud each time via the rejected count.
816
825
  try {
817
826
  const result = remember(ctx, {
818
- content: body,
827
+ content,
819
828
  kind: 'raw',
820
829
  artifactRef,
821
830
  owner: 'agent:vault-import',
@@ -848,6 +857,8 @@ export function importVault(folderPath, options) {
848
857
  }
849
858
  entries.push(echo);
850
859
  imported++;
860
+ if (content !== body)
861
+ redacted++;
851
862
  }
852
863
  }
853
864
  finally {
@@ -867,7 +878,7 @@ export function importVault(folderPath, options) {
867
878
  archiveRaw(ctx, row.id, `source_deleted:${artifactRef}`);
868
879
  }
869
880
  }
870
- return { total, imported, skipped, rejected, archived, entries };
881
+ return { total, imported, skipped, rejected, archived, redacted, entries };
871
882
  }
872
883
  /** Local tolerant JSON-array parse for the loader's `tags_json` column. The
873
884
  * store's own `parseJsonArray` is not exported; this matches its contract
package/dist/incidents.js CHANGED
@@ -25,8 +25,10 @@
25
25
  * the row, default `[]`. On save, every id must exist in the SAME tenant; a
26
26
  * cross-tenant or nonexistent id is rejected (throw) before the insert.
27
27
  */
28
+ import { BadRequestError, ConflictError, NotFoundError } from './api-errors.js';
28
29
  import { openHippoDb, closeHippoDb } from './db.js';
29
- import { writeEntry, assertTenantId } from './store.js';
30
+ import { writeEntry } from './store.js';
31
+ import { assertTenantId } from './tenant.js';
30
32
  import { createMemory, Layer } from './memory.js';
31
33
  import { appendAuditEvent } from './audit.js';
32
34
  import { objectHalfLifeDays } from './half-life-migration.js';
@@ -88,7 +90,7 @@ const INCIDENT_COLS = `
88
90
  export function saveIncident(hippoRoot, tenantId, opts, actor = 'cli') {
89
91
  assertTenantId('saveIncident', tenantId);
90
92
  if (!opts.incidentText)
91
- throw new Error('saveIncident: incidentText is required');
93
+ throw new BadRequestError('saveIncident: incidentText is required');
92
94
  const now = new Date().toISOString();
93
95
  const content = opts.context
94
96
  ? `${opts.incidentText}\n\nContext: ${opts.context}`
@@ -117,7 +119,7 @@ export function saveIncident(hippoRoot, tenantId, opts, actor = 'cli') {
117
119
  // SAFETY: row shape matches the single `id` column named in the SELECT above.
118
120
  const exists = db.prepare(`SELECT id FROM memories WHERE id = ? AND tenant_id = ?`).get(linkId, tenantId);
119
121
  if (!exists) {
120
- throw new Error(`saveIncident: linked memory ${linkId} not found for tenant ${tenantId}`);
122
+ throw new NotFoundError(`saveIncident: linked memory ${linkId} not found for tenant ${tenantId}`);
121
123
  }
122
124
  validated.push(linkId);
123
125
  }
@@ -163,7 +165,7 @@ export function saveIncident(hippoRoot, tenantId, opts, actor = 'cli') {
163
165
  export function resolveIncident(hippoRoot, tenantId, id, resolutionText, actor = 'cli') {
164
166
  assertTenantId('resolveIncident', tenantId);
165
167
  if (!resolutionText || !resolutionText.trim()) {
166
- throw new Error('resolveIncident: resolutionText is required (non-empty)');
168
+ throw new BadRequestError('resolveIncident: resolutionText is required (non-empty)');
167
169
  }
168
170
  const now = new Date().toISOString();
169
171
  const db = openHippoDb(hippoRoot);
@@ -179,15 +181,15 @@ export function resolveIncident(hippoRoot, tenantId, id, resolutionText, actor =
179
181
  // SAFETY: row shape matches the single `status` column named in the SELECT above.
180
182
  const existing = db.prepare(`SELECT status FROM incidents WHERE id = ? AND tenant_id = ?`).get(id, tenantId);
181
183
  if (!existing) {
182
- throw new Error(`resolveIncident: incident ${id} not found for tenant ${tenantId}`);
184
+ throw new NotFoundError(`resolveIncident: incident ${id} not found for tenant ${tenantId}`);
183
185
  }
184
- throw new Error(`resolveIncident: incident ${id} is not open (status='${existing.status}'); only open incidents can be resolved.`);
186
+ throw new ConflictError(`resolveIncident: incident ${id} is not open (status='${existing.status}'); only open incidents can be resolved.`);
185
187
  }
186
188
  // SAFETY: row's shape matches the columns named in INCIDENT_COLS above.
187
189
  const row = db.prepare(`SELECT ${INCIDENT_COLS} FROM incidents WHERE id = ? AND tenant_id = ?`)
188
190
  .get(id, tenantId);
189
191
  if (!row)
190
- throw new Error(`resolveIncident: incident ${id} not found after UPDATE`);
192
+ throw new NotFoundError(`resolveIncident: incident ${id} not found after UPDATE`);
191
193
  appendAuditEvent(db, {
192
194
  tenantId,
193
195
  actor,
@@ -234,15 +236,15 @@ export function closeIncident(hippoRoot, tenantId, id, actor = 'cli') {
234
236
  // SAFETY: row shape matches the single `status` column named in the SELECT above.
235
237
  const existing = db.prepare(`SELECT status FROM incidents WHERE id = ? AND tenant_id = ?`).get(id, tenantId);
236
238
  if (!existing) {
237
- throw new Error(`closeIncident: incident ${id} not found for tenant ${tenantId}`);
239
+ throw new NotFoundError(`closeIncident: incident ${id} not found for tenant ${tenantId}`);
238
240
  }
239
- throw new Error(`closeIncident: incident ${id} is already closed (status='${existing.status}'); only open or resolved incidents can be closed.`);
241
+ throw new ConflictError(`closeIncident: incident ${id} is already closed (status='${existing.status}'); only open or resolved incidents can be closed.`);
240
242
  }
241
243
  // SAFETY: row's shape matches the columns named in INCIDENT_COLS above.
242
244
  const row = db.prepare(`SELECT ${INCIDENT_COLS} FROM incidents WHERE id = ? AND tenant_id = ?`)
243
245
  .get(id, tenantId);
244
246
  if (!row)
245
- throw new Error(`closeIncident: incident ${id} not found after UPDATE`);
247
+ throw new NotFoundError(`closeIncident: incident ${id} not found after UPDATE`);
246
248
  appendAuditEvent(db, {
247
249
  tenantId,
248
250
  actor,
@@ -288,7 +290,7 @@ export function loadIncidents(hippoRoot, tenantId, opts = {}) {
288
290
  let rows;
289
291
  if (opts.status) {
290
292
  if (!VALID_INCIDENT_STATES.has(opts.status)) {
291
- throw new Error(`loadIncidents: status must be one of ${Array.from(VALID_INCIDENT_STATES).join('|')}; got ${opts.status}`);
293
+ throw new BadRequestError(`loadIncidents: status must be one of ${Array.from(VALID_INCIDENT_STATES).join('|')}; got ${opts.status}`);
292
294
  }
293
295
  // SAFETY: rows' shape matches the columns named in INCIDENT_COLS above.
294
296
  rows = db.prepare(`
package/dist/index.d.ts CHANGED
@@ -5,10 +5,13 @@ import { type CreateMemoryOptions, type MemoryEntry } from './memory.js';
5
5
  export { MemoryEntry, Layer, EmotionalValence, ConfidenceLevel, DecayOptions, calculateStrength, resolveConfidence, confidenceFacets, type ConfidenceFacets, applyOutcome, generateId, computeSchemaFit } from './memory.js';
6
6
  /** Published signature, so `baseHalfLifeDays` stays optional here; hippo's own writers use the strict one in memory.ts. */
7
7
  export declare function createMemory(content: string, options?: Partial<CreateMemoryOptions>): MemoryEntry;
8
- export { search, hybridSearch, physicsSearch, markRetrieved, estimateTokens, textOverlap, tokenize, explainMatch, detectTemporalDirection, temporalBoost, computeTemporalRange, SearchResult, MatchExplanation } from './search.js';
8
+ export { search, hybridSearch, physicsSearch, estimateTokens, textOverlap, explainMatch, detectTemporalDirection, temporalBoost, computeTemporalRange, SearchResult, MatchExplanation } from './search.js';
9
+ export { tokenize } from './tokenize.js';
10
+ export { markRetrieved } from './memory.js';
9
11
  export { multihopSearch } from './multihop.js';
10
12
  export { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS, type GraphExpandOpts } from './graph-recall.js';
11
- export { initStore, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, writeEntry, readEntry, deleteEntry, loadIndex, rebuildIndex, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, replaceDetectedConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, loadSessionDecayContext, SessionDecayContext, createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, loadLatestHandoffForCard, } from './store.js';
13
+ export { initStore, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, writeEntry, readEntry, deleteEntry, loadIndex, rebuildIndex, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, replaceDetectedConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, loadSessionDecayContext, SessionDecayContext, } from './store.js';
14
+ export { createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, loadLatestHandoffForCard, } from './store-cards.js';
12
15
  export { SessionHandoff, HandoffOutcome, HandoffEvidence, isHandoffOutcome } from './handoff.js';
13
16
  export { Card, CardStatus, CardRun, CardComment, CardTransitions, isCardStatus, CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
14
17
  export { consolidate, ConsolidationResult } from './consolidate.js';