hippo-memory 1.24.0 → 1.25.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 (71) hide show
  1. package/bin/hippo.js +2 -2
  2. package/dist/api.d.ts +12 -17
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +25 -55
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli.d.ts +5 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +88 -10
  9. package/dist/cli.js.map +1 -1
  10. package/dist/connectors/github/backfill.js +4 -4
  11. package/dist/connectors/github/cli-impl.js +6 -6
  12. package/dist/connectors/github/dlq.js +14 -14
  13. package/dist/connectors/slack/backfill.js +1 -1
  14. package/dist/connectors/slack/dlq.js +10 -10
  15. package/dist/connectors/slack/workspaces.js +4 -4
  16. package/dist/dag.js +6 -6
  17. package/dist/dashboard.js +7 -7
  18. package/dist/hooks.js +24 -24
  19. package/dist/predictions.js +67 -67
  20. package/dist/recall-scope.d.ts +60 -0
  21. package/dist/recall-scope.d.ts.map +1 -0
  22. package/dist/recall-scope.js +84 -0
  23. package/dist/recall-scope.js.map +1 -0
  24. package/dist/refine-llm.js +13 -13
  25. package/dist/shared.d.ts +30 -0
  26. package/dist/shared.d.ts.map +1 -1
  27. package/dist/shared.js +41 -9
  28. package/dist/shared.js.map +1 -1
  29. package/dist/sleep-redact.d.ts +3 -0
  30. package/dist/sleep-redact.d.ts.map +1 -1
  31. package/dist/sleep-redact.js +3 -0
  32. package/dist/sleep-redact.js.map +1 -1
  33. package/dist/src/api.js +25 -55
  34. package/dist/src/api.js.map +1 -1
  35. package/dist/src/cli.js +88 -10
  36. package/dist/src/cli.js.map +1 -1
  37. package/dist/src/connectors/github/backfill.js +4 -4
  38. package/dist/src/connectors/github/cli-impl.js +6 -6
  39. package/dist/src/connectors/github/dlq.js +14 -14
  40. package/dist/src/connectors/slack/backfill.js +1 -1
  41. package/dist/src/connectors/slack/dlq.js +10 -10
  42. package/dist/src/connectors/slack/workspaces.js +4 -4
  43. package/dist/src/dag.js +6 -6
  44. package/dist/src/dashboard.js +7 -7
  45. package/dist/src/hooks.js +24 -24
  46. package/dist/src/predictions.js +67 -67
  47. package/dist/src/recall-scope.js +84 -0
  48. package/dist/src/recall-scope.js.map +1 -0
  49. package/dist/src/refine-llm.js +13 -13
  50. package/dist/src/shared.js +41 -9
  51. package/dist/src/shared.js.map +1 -1
  52. package/dist/src/sleep-redact.js +3 -0
  53. package/dist/src/sleep-redact.js.map +1 -1
  54. package/dist/src/store.js +57 -17
  55. package/dist/src/store.js.map +1 -1
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/working-memory.js +19 -19
  58. package/dist/store.d.ts +30 -9
  59. package/dist/store.d.ts.map +1 -1
  60. package/dist/store.js +57 -17
  61. package/dist/store.js.map +1 -1
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/working-memory.js +19 -19
  65. package/dist-ui/index.html +12 -12
  66. package/extensions/openclaw-plugin/index.ts +650 -650
  67. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  68. package/extensions/openclaw-plugin/package.json +1 -1
  69. package/openclaw.plugin.json +1 -1
  70. package/package.json +1 -1
  71. package/dist/benchmarks/e1.3/scenarios.json +0 -2587
@@ -30,7 +30,7 @@ function readCursor(root, tenantId, repo) {
30
30
  const db = openHippoDb(root);
31
31
  try {
32
32
  const row = db
33
- .prepare(`SELECT issues_hwm, issue_comments_hwm, pr_review_comments_hwm
33
+ .prepare(`SELECT issues_hwm, issue_comments_hwm, pr_review_comments_hwm
34
34
  FROM github_cursors WHERE tenant_id = ? AND repo_full_name = ?`)
35
35
  .get(tenantId, repo);
36
36
  return {
@@ -46,9 +46,9 @@ function readCursor(root, tenantId, repo) {
46
46
  function writeOneHwm(root, tenantId, repo, column, value) {
47
47
  const db = openHippoDb(root);
48
48
  try {
49
- db.prepare(`INSERT INTO github_cursors (tenant_id, repo_full_name, ${column}, updated_at)
50
- VALUES (?, ?, ?, ?)
51
- ON CONFLICT(tenant_id, repo_full_name)
49
+ db.prepare(`INSERT INTO github_cursors (tenant_id, repo_full_name, ${column}, updated_at)
50
+ VALUES (?, ?, ?, ?)
51
+ ON CONFLICT(tenant_id, repo_full_name)
52
52
  DO UPDATE SET ${column} = excluded.${column}, updated_at = excluded.updated_at`).run(tenantId, repo, value, new Date().toISOString());
53
53
  }
54
54
  finally {
@@ -83,12 +83,12 @@ export async function cmdGithubBackfill(hippoRoot, flags, fetcher = realGitHubFe
83
83
  if (sinceIso) {
84
84
  const db = openHippoDb(hippoRoot);
85
85
  try {
86
- db.prepare(`INSERT INTO github_cursors (tenant_id, repo_full_name, issues_hwm, issue_comments_hwm, pr_review_comments_hwm, updated_at)
87
- VALUES (?, ?, ?, ?, ?, ?)
88
- ON CONFLICT(tenant_id, repo_full_name) DO UPDATE SET
89
- issues_hwm = COALESCE(github_cursors.issues_hwm, excluded.issues_hwm),
90
- issue_comments_hwm = COALESCE(github_cursors.issue_comments_hwm, excluded.issue_comments_hwm),
91
- pr_review_comments_hwm = COALESCE(github_cursors.pr_review_comments_hwm, excluded.pr_review_comments_hwm),
86
+ db.prepare(`INSERT INTO github_cursors (tenant_id, repo_full_name, issues_hwm, issue_comments_hwm, pr_review_comments_hwm, updated_at)
87
+ VALUES (?, ?, ?, ?, ?, ?)
88
+ ON CONFLICT(tenant_id, repo_full_name) DO UPDATE SET
89
+ issues_hwm = COALESCE(github_cursors.issues_hwm, excluded.issues_hwm),
90
+ issue_comments_hwm = COALESCE(github_cursors.issue_comments_hwm, excluded.issue_comments_hwm),
91
+ pr_review_comments_hwm = COALESCE(github_cursors.pr_review_comments_hwm, excluded.pr_review_comments_hwm),
92
92
  updated_at = excluded.updated_at`).run(tenantId, repo, sinceIso, sinceIso, sinceIso, new Date().toISOString());
93
93
  }
94
94
  finally {
@@ -3,30 +3,30 @@ import { verifyGitHubSignature } from './signature.js';
3
3
  import { isGitHubWebhookEnvelope } from './types.js';
4
4
  export function writeToDlq(db, opts) {
5
5
  const result = db
6
- .prepare(`INSERT INTO github_dlq
7
- (tenant_id, raw_payload, error, event_name, delivery_id, signature,
8
- installation_id, repo_full_name, received_at, bucket)
6
+ .prepare(`INSERT INTO github_dlq
7
+ (tenant_id, raw_payload, error, event_name, delivery_id, signature,
8
+ installation_id, repo_full_name, received_at, bucket)
9
9
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
10
10
  .run(opts.tenantId ?? '__unroutable__', opts.rawPayload, opts.error, opts.eventName ?? null, opts.deliveryId ?? null, opts.signature ?? null, opts.installationId ?? null, opts.repoFullName ?? null, new Date().toISOString(), opts.bucket ?? 'parse_error');
11
11
  return Number(result.lastInsertRowid);
12
12
  }
13
- const SELECT_COLUMNS = `id, tenant_id, raw_payload, error, event_name, delivery_id,
14
- signature, installation_id, repo_full_name, retry_count,
13
+ const SELECT_COLUMNS = `id, tenant_id, raw_payload, error, event_name, delivery_id,
14
+ signature, installation_id, repo_full_name, retry_count,
15
15
  received_at, retried_at, bucket`;
16
16
  export function listDlq(db, opts) {
17
17
  const rows = db
18
- .prepare(`SELECT ${SELECT_COLUMNS}
19
- FROM github_dlq
20
- WHERE tenant_id = ?
21
- ORDER BY received_at ASC
18
+ .prepare(`SELECT ${SELECT_COLUMNS}
19
+ FROM github_dlq
20
+ WHERE tenant_id = ?
21
+ ORDER BY received_at ASC
22
22
  LIMIT ?`)
23
23
  .all(opts.tenantId, opts.limit ?? 100);
24
24
  return rows.map(rowToItem);
25
25
  }
26
26
  export function getDlqEntry(db, id) {
27
27
  const row = db
28
- .prepare(`SELECT ${SELECT_COLUMNS}
29
- FROM github_dlq
28
+ .prepare(`SELECT ${SELECT_COLUMNS}
29
+ FROM github_dlq
30
30
  WHERE id = ?`)
31
31
  .get(id);
32
32
  if (!row)
@@ -53,9 +53,9 @@ function rowToItem(r) {
53
53
  function bumpRetryCount(hippoRoot, id) {
54
54
  const db = openHippoDb(hippoRoot);
55
55
  try {
56
- db.prepare(`UPDATE github_dlq
57
- SET retry_count = retry_count + 1,
58
- retried_at = ?
56
+ db.prepare(`UPDATE github_dlq
57
+ SET retry_count = retry_count + 1,
58
+ retried_at = ?
59
59
  WHERE id = ?`).run(new Date().toISOString(), id);
60
60
  }
61
61
  finally {
@@ -15,7 +15,7 @@ function readCursor(root, tenantId, channelId) {
15
15
  function writeCursor(root, tenantId, channelId, latestTs) {
16
16
  const db = openHippoDb(root);
17
17
  try {
18
- db.prepare(`INSERT INTO slack_cursors (tenant_id, channel_id, latest_ts, updated_at) VALUES (?,?,?,?)
18
+ db.prepare(`INSERT INTO slack_cursors (tenant_id, channel_id, latest_ts, updated_at) VALUES (?,?,?,?)
19
19
  ON CONFLICT(tenant_id, channel_id) DO UPDATE SET latest_ts = excluded.latest_ts, updated_at = excluded.updated_at`).run(tenantId, channelId, latestTs, new Date().toISOString());
20
20
  }
21
21
  finally {
@@ -7,28 +7,28 @@ import { isSlackEventEnvelope, isSlackMessageEvent } from './types.js';
7
7
  import { handleMessageDeleted } from './deletion.js';
8
8
  export function writeToDlq(db, opts) {
9
9
  const result = db
10
- .prepare(`INSERT INTO slack_dlq
11
- (tenant_id, team_id, raw_payload, error, received_at, bucket, signature, slack_timestamp)
10
+ .prepare(`INSERT INTO slack_dlq
11
+ (tenant_id, team_id, raw_payload, error, received_at, bucket, signature, slack_timestamp)
12
12
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
13
13
  .run(opts.tenantId ?? '__unroutable__', opts.teamId ?? null, opts.rawPayload, opts.error, new Date().toISOString(), opts.bucket ?? 'parse_error', opts.signature ?? null, opts.slackTimestamp ?? null);
14
14
  return Number(result.lastInsertRowid);
15
15
  }
16
16
  export function listDlq(db, opts) {
17
17
  const rows = db
18
- .prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
19
- bucket, retry_count, signature, slack_timestamp
20
- FROM slack_dlq
21
- WHERE tenant_id = ?
22
- ORDER BY received_at ASC
18
+ .prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
19
+ bucket, retry_count, signature, slack_timestamp
20
+ FROM slack_dlq
21
+ WHERE tenant_id = ?
22
+ ORDER BY received_at ASC
23
23
  LIMIT ?`)
24
24
  .all(opts.tenantId, opts.limit ?? 100);
25
25
  return rows.map(rowToItem);
26
26
  }
27
27
  export function getDlqEntry(db, id) {
28
28
  const row = db
29
- .prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
30
- bucket, retry_count, signature, slack_timestamp
31
- FROM slack_dlq
29
+ .prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
30
+ bucket, retry_count, signature, slack_timestamp
31
+ FROM slack_dlq
32
32
  WHERE id = ?`)
33
33
  .get(id);
34
34
  if (!row)
@@ -26,10 +26,10 @@
26
26
  */
27
27
  export function addWorkspace(db, opts) {
28
28
  const addedAt = new Date().toISOString();
29
- db.prepare(`INSERT INTO slack_workspaces (team_id, tenant_id, added_at)
30
- VALUES (?, ?, ?)
31
- ON CONFLICT(team_id) DO UPDATE SET
32
- tenant_id = excluded.tenant_id,
29
+ db.prepare(`INSERT INTO slack_workspaces (team_id, tenant_id, added_at)
30
+ VALUES (?, ?, ?)
31
+ ON CONFLICT(team_id) DO UPDATE SET
32
+ tenant_id = excluded.tenant_id,
33
33
  added_at = excluded.added_at`).run(opts.teamId, opts.tenantId, addedAt);
34
34
  return { teamId: opts.teamId, tenantId: opts.tenantId, addedAt };
35
35
  }
package/dist/dag.js CHANGED
@@ -31,12 +31,12 @@ export function clusterFacts(facts) {
31
31
  }
32
32
  return clusters;
33
33
  }
34
- const DAG_SUMMARY_PROMPT = `You are summarizing a cluster of facts about a specific topic/entity for a memory system.
35
-
36
- Topic: {label}
37
- Facts:
38
- {facts}
39
-
34
+ const DAG_SUMMARY_PROMPT = `You are summarizing a cluster of facts about a specific topic/entity for a memory system.
35
+
36
+ Topic: {label}
37
+ Facts:
38
+ {facts}
39
+
40
40
  Write a single concise paragraph (2-4 sentences) that captures all the key information from these facts. This summary will be used to quickly determine if this cluster is relevant to a future query, so include specific names, dates, numbers, and key details. Output ONLY the summary paragraph, no preamble.`;
41
41
  export async function generateDagSummary(label, factContents, opts) {
42
42
  const model = opts.model ?? 'claude-sonnet-4-6';
package/dist/dashboard.js CHANGED
@@ -202,13 +202,13 @@ export function serveDashboard(hippoRoot, port = 3333) {
202
202
  // was removed in the hybrid-v4 revamp E1 since dist-ui/ is now the
203
203
  // sole UI surface). ---
204
204
  res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
205
- res.end(`<!doctype html>
206
- <html lang="en"><head><title>Hippo Dashboard</title><meta charset="utf-8"></head>
207
- <body style="font-family:Georgia,'Palatino Linotype',serif;max-width:640px;margin:60px auto;padding:24px;line-height:1.6;background:#f4efe6;color:#3a3228">
208
- <h1 style="color:#c45c3c">Hippo Dashboard</h1>
209
- <p>The React UI bundle is not built yet. Run:</p>
210
- <pre style="background:#faf7f2;padding:16px;border:1px solid #c4b9a8;border-radius:3px;font-family:Consolas,monospace">cd ui && npm install && npm run build</pre>
211
- <p>Then refresh this page. The dashboard server will serve <code>dist-ui/index.html</code> automatically once present.</p>
205
+ res.end(`<!doctype html>
206
+ <html lang="en"><head><title>Hippo Dashboard</title><meta charset="utf-8"></head>
207
+ <body style="font-family:Georgia,'Palatino Linotype',serif;max-width:640px;margin:60px auto;padding:24px;line-height:1.6;background:#f4efe6;color:#3a3228">
208
+ <h1 style="color:#c45c3c">Hippo Dashboard</h1>
209
+ <p>The React UI bundle is not built yet. Run:</p>
210
+ <pre style="background:#faf7f2;padding:16px;border:1px solid #c4b9a8;border-radius:3px;font-family:Consolas,monospace">cd ui && npm install && npm run build</pre>
211
+ <p>Then refresh this page. The dashboard server will serve <code>dist-ui/index.html</code> automatically once present.</p>
212
212
  </body></html>`);
213
213
  });
214
214
  server.listen(port, '127.0.0.1', () => {
package/dist/hooks.js CHANGED
@@ -77,30 +77,30 @@ const HIPPO_OPENCODE_PLUGIN_MARKER = 'HIPPO_OPENCODE_PLUGIN_V1';
77
77
  * marker match AND content equality, so a plugin-source revision under
78
78
  * the same V1 marker re-writes the file on next install.
79
79
  */
80
- export const OPENCODE_PLUGIN_SOURCE = `// ${HIPPO_OPENCODE_PLUGIN_MARKER}
81
- // hippo-memory opencode plugin. DO NOT EDIT — regenerated on every
82
- // \`hippo hook install opencode\` from src/hooks.ts OPENCODE_PLUGIN_SOURCE
83
- // in https://github.com/kitfunso/hippo-memory. Local changes will be lost.
84
-
85
- export const HippoPlugin = async ({ $ }) => {
86
- return {
87
- event: async ({ event }) => {
88
- // Defense in depth: opencode currently runs in Bun where $ is the shell
89
- // template helper. A non-Bun runtime would have $ as undefined; fail
90
- // closed instead of crashing the host session.
91
- if (typeof $ !== "function") return;
92
- try {
93
- if (event.type === "session.idle") {
94
- await $\`hippo session-end\`.quiet().nothrow();
95
- } else if (event.type === "session.created") {
96
- await $\`hippo last-sleep\`.quiet().nothrow();
97
- }
98
- } catch {
99
- // hippo CLI not on PATH or other failure — never crash the host session.
100
- }
101
- },
102
- };
103
- };
80
+ export const OPENCODE_PLUGIN_SOURCE = `// ${HIPPO_OPENCODE_PLUGIN_MARKER}
81
+ // hippo-memory opencode plugin. DO NOT EDIT — regenerated on every
82
+ // \`hippo hook install opencode\` from src/hooks.ts OPENCODE_PLUGIN_SOURCE
83
+ // in https://github.com/kitfunso/hippo-memory. Local changes will be lost.
84
+
85
+ export const HippoPlugin = async ({ $ }) => {
86
+ return {
87
+ event: async ({ event }) => {
88
+ // Defense in depth: opencode currently runs in Bun where $ is the shell
89
+ // template helper. A non-Bun runtime would have $ as undefined; fail
90
+ // closed instead of crashing the host session.
91
+ if (typeof $ !== "function") return;
92
+ try {
93
+ if (event.type === "session.idle") {
94
+ await $\`hippo session-end\`.quiet().nothrow();
95
+ } else if (event.type === "session.created") {
96
+ await $\`hippo last-sleep\`.quiet().nothrow();
97
+ }
98
+ } catch {
99
+ // hippo CLI not on PATH or other failure — never crash the host session.
100
+ }
101
+ },
102
+ };
103
+ };
104
104
  `;
105
105
  export { HIPPO_OPENCODE_PLUGIN_MARKER };
106
106
  function homeDir() {
@@ -86,20 +86,20 @@ export function savePrediction(hippoRoot, tenantId, opts, actor = 'cli') {
86
86
  writeEntry(hippoRoot, mem, {
87
87
  actor,
88
88
  afterWrite: (db, memoryId) => {
89
- const result = db.prepare(`
90
- INSERT INTO predictions(
91
- memory_id, tenant_id, class_tag, claim_text,
92
- estimate_value, estimate_unit, target_date,
93
- actual_value, closure_state, closed_at, closure_note, created_at
94
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open', NULL, NULL, ?)
89
+ const result = db.prepare(`
90
+ INSERT INTO predictions(
91
+ memory_id, tenant_id, class_tag, claim_text,
92
+ estimate_value, estimate_unit, target_date,
93
+ actual_value, closure_state, closed_at, closure_note, created_at
94
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open', NULL, NULL, ?)
95
95
  `).run(memoryId, tenantId, opts.classTag, opts.claimText, opts.estimateValue ?? null, opts.estimateUnit ?? null, opts.targetDate ?? null, null, // actual_value — null until close
96
96
  now);
97
97
  const predictionId = Number(result.lastInsertRowid ?? 0);
98
- const row = db.prepare(`
99
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
100
- estimate_value, estimate_unit, target_date,
101
- actual_value, closure_state, closed_at, closure_note, created_at
102
- FROM predictions WHERE id = ?
98
+ const row = db.prepare(`
99
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
100
+ estimate_value, estimate_unit, target_date,
101
+ actual_value, closure_state, closed_at, closure_note, created_at
102
+ FROM predictions WHERE id = ?
103
103
  `).get(predictionId);
104
104
  if (!row) {
105
105
  throw new Error('Failed to reload saved prediction row');
@@ -150,16 +150,16 @@ export function closePrediction(hippoRoot, tenantId, id, opts, actor = 'cli') {
150
150
  // predict_close audit row. Zero changed rows → caller decides
151
151
  // whether it's a "not found" or "already closed" case based on the
152
152
  // load-then-close pattern.
153
- const updateResult = db.prepare(`
154
- UPDATE predictions
155
- SET actual_value = ?, closure_state = ?, closed_at = ?, closure_note = ?
156
- WHERE id = ? AND tenant_id = ? AND closure_state = 'open'
153
+ const updateResult = db.prepare(`
154
+ UPDATE predictions
155
+ SET actual_value = ?, closure_state = ?, closed_at = ?, closure_note = ?
156
+ WHERE id = ? AND tenant_id = ? AND closure_state = 'open'
157
157
  `).run(opts.actualValue ?? null, opts.closureState, now, opts.closureNote ?? null, id, tenantId);
158
158
  if (updateResult.changes === 0) {
159
159
  // Distinguish "not found" from "already closed" so callers (CLI, HTTP)
160
160
  // can surface the right error to the user.
161
- const existing = db.prepare(`
162
- SELECT closure_state FROM predictions WHERE id = ? AND tenant_id = ?
161
+ const existing = db.prepare(`
162
+ SELECT closure_state FROM predictions WHERE id = ? AND tenant_id = ?
163
163
  `).get(id, tenantId);
164
164
  if (!existing) {
165
165
  throw new Error(`closePrediction: prediction ${id} not found for tenant ${tenantId}`);
@@ -167,11 +167,11 @@ export function closePrediction(hippoRoot, tenantId, id, opts, actor = 'cli') {
167
167
  throw new Error(`closePrediction: prediction ${id} is already closed (state='${existing.closure_state}'); ` +
168
168
  `cannot re-close. Open predictions only.`);
169
169
  }
170
- const row = db.prepare(`
171
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
172
- estimate_value, estimate_unit, target_date,
173
- actual_value, closure_state, closed_at, closure_note, created_at
174
- FROM predictions WHERE id = ? AND tenant_id = ?
170
+ const row = db.prepare(`
171
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
172
+ estimate_value, estimate_unit, target_date,
173
+ actual_value, closure_state, closed_at, closure_note, created_at
174
+ FROM predictions WHERE id = ? AND tenant_id = ?
175
175
  `).get(id, tenantId);
176
176
  if (!row) {
177
177
  throw new Error(`closePrediction: prediction ${id} not found after UPDATE`);
@@ -208,11 +208,11 @@ export function loadPredictionById(hippoRoot, tenantId, id) {
208
208
  assertTenantId('loadPredictionById', tenantId);
209
209
  const db = openHippoDb(hippoRoot);
210
210
  try {
211
- const row = db.prepare(`
212
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
213
- estimate_value, estimate_unit, target_date,
214
- actual_value, closure_state, closed_at, closure_note, created_at
215
- FROM predictions WHERE id = ? AND tenant_id = ?
211
+ const row = db.prepare(`
212
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
213
+ estimate_value, estimate_unit, target_date,
214
+ actual_value, closure_state, closed_at, closure_note, created_at
215
+ FROM predictions WHERE id = ? AND tenant_id = ?
216
216
  `).get(id, tenantId);
217
217
  return row ? rowToPrediction(row) : null;
218
218
  }
@@ -230,25 +230,25 @@ export function loadPredictionsByClass(hippoRoot, tenantId, classTag, opts = {})
230
230
  if (!VALID_CLOSURE_STATES.has(opts.closureState)) {
231
231
  throw new Error(`loadPredictionsByClass: closureState must be one of ${Array.from(VALID_CLOSURE_STATES).join('|')}; got ${opts.closureState}`);
232
232
  }
233
- rows = db.prepare(`
234
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
235
- estimate_value, estimate_unit, target_date,
236
- actual_value, closure_state, closed_at, closure_note, created_at
237
- FROM predictions
238
- WHERE tenant_id = ? AND class_tag = ? AND closure_state = ?
239
- ORDER BY created_at DESC, id DESC
240
- LIMIT ?
233
+ rows = db.prepare(`
234
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
235
+ estimate_value, estimate_unit, target_date,
236
+ actual_value, closure_state, closed_at, closure_note, created_at
237
+ FROM predictions
238
+ WHERE tenant_id = ? AND class_tag = ? AND closure_state = ?
239
+ ORDER BY created_at DESC, id DESC
240
+ LIMIT ?
241
241
  `).all(tenantId, classTag, opts.closureState, limit);
242
242
  }
243
243
  else {
244
- rows = db.prepare(`
245
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
246
- estimate_value, estimate_unit, target_date,
247
- actual_value, closure_state, closed_at, closure_note, created_at
248
- FROM predictions
249
- WHERE tenant_id = ? AND class_tag = ?
250
- ORDER BY created_at DESC, id DESC
251
- LIMIT ?
244
+ rows = db.prepare(`
245
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
246
+ estimate_value, estimate_unit, target_date,
247
+ actual_value, closure_state, closed_at, closure_note, created_at
248
+ FROM predictions
249
+ WHERE tenant_id = ? AND class_tag = ?
250
+ ORDER BY created_at DESC, id DESC
251
+ LIMIT ?
252
252
  `).all(tenantId, classTag, limit);
253
253
  }
254
254
  return rows.map(rowToPrediction);
@@ -286,14 +286,14 @@ emitAudit = true) {
286
286
  throw new Error('computePredictionBaserate: classTag is required');
287
287
  const db = openHippoDb(hippoRoot);
288
288
  try {
289
- const rows = db.prepare(`
290
- SELECT estimate_value, actual_value
291
- FROM predictions
292
- WHERE tenant_id = ?
293
- AND class_tag = ?
294
- AND closure_state = 'closed'
295
- AND estimate_value IS NOT NULL
296
- AND actual_value IS NOT NULL
289
+ const rows = db.prepare(`
290
+ SELECT estimate_value, actual_value
291
+ FROM predictions
292
+ WHERE tenant_id = ?
293
+ AND class_tag = ?
294
+ AND closure_state = 'closed'
295
+ AND estimate_value IS NOT NULL
296
+ AND actual_value IS NOT NULL
297
297
  `).all(tenantId, classTag);
298
298
  const nClosed = rows.length;
299
299
  if (nClosed === 0) {
@@ -373,25 +373,25 @@ export function loadOpenPredictions(hippoRoot, tenantId, opts = {}) {
373
373
  try {
374
374
  let rows;
375
375
  if (opts.classTag) {
376
- rows = db.prepare(`
377
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
378
- estimate_value, estimate_unit, target_date,
379
- actual_value, closure_state, closed_at, closure_note, created_at
380
- FROM predictions
381
- WHERE tenant_id = ? AND class_tag = ? AND closure_state = 'open'
382
- ORDER BY created_at DESC, id DESC
383
- LIMIT ?
376
+ rows = db.prepare(`
377
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
378
+ estimate_value, estimate_unit, target_date,
379
+ actual_value, closure_state, closed_at, closure_note, created_at
380
+ FROM predictions
381
+ WHERE tenant_id = ? AND class_tag = ? AND closure_state = 'open'
382
+ ORDER BY created_at DESC, id DESC
383
+ LIMIT ?
384
384
  `).all(tenantId, opts.classTag, limit);
385
385
  }
386
386
  else {
387
- rows = db.prepare(`
388
- SELECT id, memory_id, tenant_id, class_tag, claim_text,
389
- estimate_value, estimate_unit, target_date,
390
- actual_value, closure_state, closed_at, closure_note, created_at
391
- FROM predictions
392
- WHERE tenant_id = ? AND closure_state = 'open'
393
- ORDER BY created_at DESC, id DESC
394
- LIMIT ?
387
+ rows = db.prepare(`
388
+ SELECT id, memory_id, tenant_id, class_tag, claim_text,
389
+ estimate_value, estimate_unit, target_date,
390
+ actual_value, closure_state, closed_at, closure_note, created_at
391
+ FROM predictions
392
+ WHERE tenant_id = ? AND closure_state = 'open'
393
+ ORDER BY created_at DESC, id DESC
394
+ LIMIT ?
395
395
  `).all(tenantId, limit);
396
396
  }
397
397
  return rows.map(rowToPrediction);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * v1.25.0 — recall-side scope predicates, extracted from api.ts into a leaf
3
+ * module so shared.ts (which api.ts imports) can apply the same default-deny
4
+ * rule to searchBothHybrid's internal candidate loads without an import
5
+ * cycle. Mirrors the v39 `project-identity.ts` precedent. api.ts imports
6
+ * these for its own call sites AND re-exports them for back-compat
7
+ * (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
8
+ */
9
+ /**
10
+ * v1.2.1: source-agnostic private-scope detector. A scope string is treated
11
+ * as private when it has the shape `<lowercase-source>:private:<rest>`.
12
+ *
13
+ * Examples that match:
14
+ * slack:private:Cabc, github:private:owner/repo, jira:private:PROJ-1
15
+ * Examples that DO NOT match:
16
+ * slack:public:Cgeneral, acme:public:my-private-channel, null, '',
17
+ * 'unknown:legacy', 'private' (alone), 'private:foo' (no source prefix).
18
+ *
19
+ * Used by api.recall, mcp/server.ts (hippo_recall + hippo_context),
20
+ * cli.ts (cmdRecall + cmdExplain + continuity), shared.ts (searchBothHybrid
21
+ * recall mode). Keep these in sync — the export is the single source of
22
+ * truth so connector work cannot drift.
23
+ */
24
+ export declare const PRIVATE_SCOPE_RE: RegExp;
25
+ /** True when `scope` matches the `<source>:private:*` shape. */
26
+ export declare function isPrivateScope(scope: string | null | undefined): boolean;
27
+ /**
28
+ * Recall-side scope filter — the canonical JS half of the recall default-deny
29
+ * rule (the SQL half lives in `loadSearchRows` via `loadRecallSearchEntries`).
30
+ *
31
+ * - When `requested` is set and non-empty: exact match required.
32
+ * - When `requested` is undefined/empty: default-deny on any
33
+ * `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
34
+ * quarantine buckets. `null` and public scopes pass.
35
+ *
36
+ * @internal v1.7.2 — exported for test parity with
37
+ * `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
38
+ * of the public API surface; not re-exported from `src/index.ts`. Subject to
39
+ * change without semver bump.
40
+ */
41
+ export declare function passesScopeFilterForRecall(scope: string | null, requested: string | undefined): boolean;
42
+ /**
43
+ * v1.25.0 — the CLI `--scope` variant of the recall filter (JS half of the
44
+ * SQL 'default-deny-or-exact' mode in loadSearchRows).
45
+ *
46
+ * The CLI flag predates the envelope column as a TAG-boost ranking hint
47
+ * (`scope:<v>` tags, HIPPO_SCOPE, detectScope()), so an explicit `--scope X`
48
+ * UNLOCKS envelope scope X in addition to the default-admitted set — it does
49
+ * NOT narrow the result to X (that would return zero rows for every
50
+ * tag-scoped workflow, whose envelope scope is NULL). api.recall keeps the
51
+ * narrowing 'exact' semantics via `passesScopeFilterForRecall`.
52
+ *
53
+ * Note the unlock applies to whatever scope was explicitly named — including
54
+ * a private scope or a quarantine bucket (`--scope unknown:legacy`). That is
55
+ * deliberate owner access, identical in reach to api.recall's exact-match
56
+ * for the same input; only NON-requested private/quarantine scopes stay
57
+ * denied.
58
+ */
59
+ export declare function passesCliRecallScopeFilter(scope: string | null, requested: string | undefined): boolean;
60
+ //# sourceMappingURL=recall-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-scope.d.ts","sourceRoot":"","sources":["../src/recall-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,QAA+B,CAAC;AAE7D,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAExE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAaT;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAKT"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * v1.25.0 — recall-side scope predicates, extracted from api.ts into a leaf
3
+ * module so shared.ts (which api.ts imports) can apply the same default-deny
4
+ * rule to searchBothHybrid's internal candidate loads without an import
5
+ * cycle. Mirrors the v39 `project-identity.ts` precedent. api.ts imports
6
+ * these for its own call sites AND re-exports them for back-compat
7
+ * (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
8
+ */
9
+ import { RECALL_DEFAULT_DENY_SCOPES } from './store.js';
10
+ /**
11
+ * v1.2.1: source-agnostic private-scope detector. A scope string is treated
12
+ * as private when it has the shape `<lowercase-source>:private:<rest>`.
13
+ *
14
+ * Examples that match:
15
+ * slack:private:Cabc, github:private:owner/repo, jira:private:PROJ-1
16
+ * Examples that DO NOT match:
17
+ * slack:public:Cgeneral, acme:public:my-private-channel, null, '',
18
+ * 'unknown:legacy', 'private' (alone), 'private:foo' (no source prefix).
19
+ *
20
+ * Used by api.recall, mcp/server.ts (hippo_recall + hippo_context),
21
+ * cli.ts (cmdRecall + cmdExplain + continuity), shared.ts (searchBothHybrid
22
+ * recall mode). Keep these in sync — the export is the single source of
23
+ * truth so connector work cannot drift.
24
+ */
25
+ export const PRIVATE_SCOPE_RE = /^[a-z][a-z0-9_-]*:private:/;
26
+ /** True when `scope` matches the `<source>:private:*` shape. */
27
+ export function isPrivateScope(scope) {
28
+ return typeof scope === 'string' && PRIVATE_SCOPE_RE.test(scope);
29
+ }
30
+ /**
31
+ * Recall-side scope filter — the canonical JS half of the recall default-deny
32
+ * rule (the SQL half lives in `loadSearchRows` via `loadRecallSearchEntries`).
33
+ *
34
+ * - When `requested` is set and non-empty: exact match required.
35
+ * - When `requested` is undefined/empty: default-deny on any
36
+ * `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
37
+ * quarantine buckets. `null` and public scopes pass.
38
+ *
39
+ * @internal v1.7.2 — exported for test parity with
40
+ * `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
41
+ * of the public API surface; not re-exported from `src/index.ts`. Subject to
42
+ * change without semver bump.
43
+ */
44
+ export function passesScopeFilterForRecall(scope, requested) {
45
+ if (requested !== undefined && requested !== '') {
46
+ return scope === requested;
47
+ }
48
+ if (scope === null)
49
+ return true;
50
+ if (isPrivateScope(scope))
51
+ return false;
52
+ // v1.7.2 — read from RECALL_DEFAULT_DENY_SCOPES (single source of truth
53
+ // shared with the SQL clause in loadSearchRows). Cast the array to
54
+ // readonly string[] so .includes() accepts arbitrary string scopes
55
+ // without a cast on the input (codex P0-2: casting `scope` would defeat
56
+ // the constant's safety).
57
+ if (RECALL_DEFAULT_DENY_SCOPES.includes(scope))
58
+ return false;
59
+ return true;
60
+ }
61
+ /**
62
+ * v1.25.0 — the CLI `--scope` variant of the recall filter (JS half of the
63
+ * SQL 'default-deny-or-exact' mode in loadSearchRows).
64
+ *
65
+ * The CLI flag predates the envelope column as a TAG-boost ranking hint
66
+ * (`scope:<v>` tags, HIPPO_SCOPE, detectScope()), so an explicit `--scope X`
67
+ * UNLOCKS envelope scope X in addition to the default-admitted set — it does
68
+ * NOT narrow the result to X (that would return zero rows for every
69
+ * tag-scoped workflow, whose envelope scope is NULL). api.recall keeps the
70
+ * narrowing 'exact' semantics via `passesScopeFilterForRecall`.
71
+ *
72
+ * Note the unlock applies to whatever scope was explicitly named — including
73
+ * a private scope or a quarantine bucket (`--scope unknown:legacy`). That is
74
+ * deliberate owner access, identical in reach to api.recall's exact-match
75
+ * for the same input; only NON-requested private/quarantine scopes stay
76
+ * denied.
77
+ */
78
+ export function passesCliRecallScopeFilter(scope, requested) {
79
+ if (requested !== undefined && requested !== '' && scope === requested) {
80
+ return true;
81
+ }
82
+ return passesScopeFilterForRecall(scope, undefined);
83
+ }
84
+ //# sourceMappingURL=recall-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-scope.js","sourceRoot":"","sources":["../src/recall-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAE7D,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,KAAgC;IAC7D,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,0BAA0B,CACxC,KAAoB,EACpB,SAA6B;IAE7B,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE,EAAE,CAAC;QAChD,OAAO,KAAK,KAAK,SAAS,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,cAAc,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,wEAAwE;IACxE,mEAAmE;IACnE,mEAAmE;IACnE,wEAAwE;IACxE,0BAA0B;IAC1B,IAAK,0BAAgD,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpF,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,0BAA0B,CACxC,KAAoB,EACpB,SAA6B;IAE7B,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AACtD,CAAC"}
@@ -31,19 +31,19 @@ export async function refineSemanticMemory(merged, sources, opts) {
31
31
  .slice(0, 8)
32
32
  .map((s, i) => `[source ${i + 1}] ${s.content.slice(0, 400)}`)
33
33
  .join('\n\n');
34
- const prompt = `You are refining a semantic memory in an agent's memory store. The rule-based consolidator merged several related episodic memories into one, but the output is clumsy. Produce a single coherent semantic memory that captures the underlying principle.
35
-
36
- Rules:
37
- - Output ONLY the refined content — no preamble, no quote marks, no "Here is...".
38
- - Keep it concise: one paragraph, no headers, no bullet lists unless the sources are inherently a list.
39
- - Preserve specific facts (names, numbers, paths, IDs) from the sources.
40
- - Generalize: state the pattern, not each instance.
41
- - Do NOT include the "[Consolidated from N ...]" marker.
42
-
43
- Current merged content:
44
- ${merged}
45
-
46
- Source memories (up to 8 shown):
34
+ const prompt = `You are refining a semantic memory in an agent's memory store. The rule-based consolidator merged several related episodic memories into one, but the output is clumsy. Produce a single coherent semantic memory that captures the underlying principle.
35
+
36
+ Rules:
37
+ - Output ONLY the refined content — no preamble, no quote marks, no "Here is...".
38
+ - Keep it concise: one paragraph, no headers, no bullet lists unless the sources are inherently a list.
39
+ - Preserve specific facts (names, numbers, paths, IDs) from the sources.
40
+ - Generalize: state the pattern, not each instance.
41
+ - Do NOT include the "[Consolidated from N ...]" marker.
42
+
43
+ Current merged content:
44
+ ${merged}
45
+
46
+ Source memories (up to 8 shown):
47
47
  ${sourceBlock}`;
48
48
  let res;
49
49
  try {