hippo-memory 1.55.0 → 1.57.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 (77) hide show
  1. package/README.md +11 -0
  2. package/dist/api.d.ts +19 -9
  3. package/dist/api.js +112 -35
  4. package/dist/card-detail.d.ts +1 -1
  5. package/dist/card-detail.js +1 -1
  6. package/dist/cli/shared.d.ts +137 -0
  7. package/dist/cli/shared.js +830 -0
  8. package/dist/cli/sleep.d.ts +10 -0
  9. package/dist/cli/sleep.js +171 -0
  10. package/dist/cli.d.ts +0 -7
  11. package/dist/cli.js +313 -1806
  12. package/dist/config.d.ts +5 -0
  13. package/dist/config.js +21 -0
  14. package/dist/connectors/github/webhook.d.ts +19 -0
  15. package/dist/connectors/github/webhook.js +313 -0
  16. package/dist/connectors/slack/webhook.d.ts +22 -0
  17. package/dist/connectors/slack/webhook.js +203 -0
  18. package/dist/consolidate.js +3 -2
  19. package/dist/context-auto.d.ts +3 -0
  20. package/dist/context-auto.js +34 -0
  21. package/dist/customer-notes.js +2 -1
  22. package/dist/dashboard.js +2 -1
  23. package/dist/db.js +67 -1
  24. package/dist/decisions.js +2 -1
  25. package/dist/delivery-recorder.d.ts +127 -0
  26. package/dist/delivery-recorder.js +218 -0
  27. package/dist/eval-stats.d.ts +58 -0
  28. package/dist/eval-stats.js +111 -0
  29. package/dist/goals.d.ts +49 -25
  30. package/dist/goals.js +39 -22
  31. package/dist/graph-extract.js +1 -1
  32. package/dist/graph-recall.d.ts +1 -1
  33. package/dist/graph-recall.js +1 -1
  34. package/dist/graph.js +1 -1
  35. package/dist/hooks.d.ts +1 -3
  36. package/dist/hooks.js +2 -4
  37. package/dist/http-util.d.ts +31 -0
  38. package/dist/http-util.js +46 -0
  39. package/dist/incidents.js +2 -1
  40. package/dist/index.d.ts +5 -2
  41. package/dist/index.js +5 -2
  42. package/dist/mcp/server.js +173 -285
  43. package/dist/memory.d.ts +19 -0
  44. package/dist/memory.js +38 -0
  45. package/dist/policies.js +2 -1
  46. package/dist/predictions.js +2 -1
  47. package/dist/processes.js +2 -1
  48. package/dist/project-briefs.js +3 -1
  49. package/dist/prompt-recall.js +1 -1
  50. package/dist/recall-history.d.ts +5 -0
  51. package/dist/recall-history.js +9 -0
  52. package/dist/recall-pipeline.d.ts +101 -0
  53. package/dist/recall-pipeline.js +313 -0
  54. package/dist/recall-scope.d.ts +22 -0
  55. package/dist/recall-scope.js +27 -1
  56. package/dist/recall-trace.d.ts +69 -0
  57. package/dist/recall-trace.js +136 -0
  58. package/dist/search.d.ts +0 -20
  59. package/dist/search.js +2 -49
  60. package/dist/server.js +1901 -2384
  61. package/dist/skills.js +2 -1
  62. package/dist/store-cards.d.ts +53 -0
  63. package/dist/store-cards.js +512 -0
  64. package/dist/store.d.ts +2 -89
  65. package/dist/store.js +6 -562
  66. package/dist/tenant.d.ts +22 -0
  67. package/dist/tenant.js +26 -0
  68. package/dist/token-ledger.d.ts +2 -0
  69. package/dist/token-ledger.js +5 -0
  70. package/dist/tokenize.d.ts +2 -0
  71. package/dist/tokenize.js +8 -0
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  75. package/extensions/openclaw-plugin/package.json +1 -1
  76. package/openclaw.plugin.json +1 -1
  77. package/package.json +2 -1
package/dist/config.d.ts CHANGED
@@ -140,6 +140,11 @@ export interface HippoConfig {
140
140
  agentMemories: {
141
141
  tools: string[] | null;
142
142
  };
143
+ /** Per-turn delivery ledger (src/recall-trace.ts): hashes, ids, counts and rejection reasons for each
144
+ * pinned-only context call. Default off; read from the store the token ledger writes to. */
145
+ deliveryLedger: {
146
+ enabled: boolean;
147
+ };
143
148
  }
144
149
  export declare function loadConfig(hippoRoot: string): HippoConfig;
145
150
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -85,6 +85,9 @@ const DEFAULT_CONFIG = {
85
85
  agentMemories: {
86
86
  tools: null,
87
87
  },
88
+ deliveryLedger: {
89
+ enabled: false,
90
+ },
88
91
  };
89
92
  function isMemoryValueConfig(value) {
90
93
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -95,6 +98,23 @@ function isDormantConfig(value) {
95
98
  function isChurnStalenessConfig(value) {
96
99
  return typeof value === 'object' && value !== null && !Array.isArray(value);
97
100
  }
101
+ function isDeliveryLedgerConfig(value) {
102
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
103
+ }
104
+ // Only `{"enabled": true}` turns it on; anything malformed warns and stays off.
105
+ function deliveryLedgerEnabled(value) {
106
+ if (value === undefined)
107
+ return false;
108
+ const isObject = isDeliveryLedgerConfig(value);
109
+ const enabled = isObject ? value.enabled : undefined;
110
+ if (enabled === true || enabled === false)
111
+ return enabled;
112
+ if (isObject && enabled === undefined)
113
+ return false;
114
+ console.error(`Warning: config.json's "deliveryLedger" must be an object like {"enabled": true} ` +
115
+ `(got ${JSON.stringify(value)}) - using false.`);
116
+ return false;
117
+ }
98
118
  function agentMemoryTools(value) {
99
119
  if (value === undefined || value === null)
100
120
  return null;
@@ -205,6 +225,7 @@ export function loadConfig(hippoRoot) {
205
225
  enabled: churnStalenessEnabled,
206
226
  },
207
227
  agentMemories: { tools: agentMemoryTools(raw.agentMemories?.tools) },
228
+ deliveryLedger: { enabled: deliveryLedgerEnabled(raw.deliveryLedger) },
208
229
  };
209
230
  }
210
231
  catch (err) {
@@ -0,0 +1,19 @@
1
+ import { type WebhookRequest } from '../../http-util.js';
2
+ /**
3
+ * GitHub webhook receiver. Mirrors the Slack route shape but with
4
+ * GitHub-specific idioms:
5
+ * 1. HMAC SHA-256 over the raw body (X-Hub-Signature-256), no timestamp.
6
+ * 2. Event type discriminated by the X-GitHub-Event header (not body.type).
7
+ * 3. X-GitHub-Delivery is required audit metadata (NOT the dedupe seam - see
8
+ * computeIdempotencyKey, which folds the signed body into the key so a
9
+ * replayed body with a fresh delivery UUID still dedupes).
10
+ * 4. Tenant resolved by installation.id → github_installations, then by
11
+ * repository.full_name → github_repositories (PAT-mode multi-tenant).
12
+ * 5. ALWAYS ACK 200 on signed envelopes (DLQ included). 401 only on bad
13
+ * signature; 404 only when GITHUB_WEBHOOK_SECRET is unset (don't expose
14
+ * the route's existence on builds where it's gated off).
15
+ *
16
+ * Bearer auth is skipped on purpose: server.ts checks isPublicRoute before calling this.
17
+ */
18
+ export declare function handleGitHubEventsWebhook({ req, res, opts }: WebhookRequest): Promise<void>;
19
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1,313 @@
1
+ import { verifyGitHubSignature } from './signature.js';
2
+ import { isGitHubWebhookEnvelope, isGitHubIssueEvent, isGitHubIssueCommentEvent, isGitHubPullRequestEvent, isGitHubPullRequestReviewCommentEvent, } from './types.js';
3
+ import { ingestEvent as ingestGitHubEvent } from './ingest.js';
4
+ import { handleCommentDeleted as handleGitHubCommentDeleted } from './deletion.js';
5
+ import { writeToDlq as writeToGitHubDlq } from './dlq.js';
6
+ import { resolveTenantForGitHub } from './tenant-routing.js';
7
+ import { computeDeletionKey as computeGitHubDeletionKey } from './signature.js';
8
+ import { resolveTenantId } from '../../tenant.js';
9
+ import { openHippoDb, closeHippoDb } from '../../db.js';
10
+ import { adminActor } from '../../api.js';
11
+ import { HttpError, JSON_HEADERS, isHeaderString, readBody, sendJson, } from '../../http-util.js';
12
+ /**
13
+ * GitHub webhook receiver. Mirrors the Slack route shape but with
14
+ * GitHub-specific idioms:
15
+ * 1. HMAC SHA-256 over the raw body (X-Hub-Signature-256), no timestamp.
16
+ * 2. Event type discriminated by the X-GitHub-Event header (not body.type).
17
+ * 3. X-GitHub-Delivery is required audit metadata (NOT the dedupe seam - see
18
+ * computeIdempotencyKey, which folds the signed body into the key so a
19
+ * replayed body with a fresh delivery UUID still dedupes).
20
+ * 4. Tenant resolved by installation.id → github_installations, then by
21
+ * repository.full_name → github_repositories (PAT-mode multi-tenant).
22
+ * 5. ALWAYS ACK 200 on signed envelopes (DLQ included). 401 only on bad
23
+ * signature; 404 only when GITHUB_WEBHOOK_SECRET is unset (don't expose
24
+ * the route's existence on builds where it's gated off).
25
+ *
26
+ * Bearer auth is skipped on purpose: server.ts checks isPublicRoute before calling this.
27
+ */
28
+ export async function handleGitHubEventsWebhook({ req, res, opts }) {
29
+ const rawBody = await readBody(req);
30
+ const secret = process.env.GITHUB_WEBHOOK_SECRET;
31
+ if (!secret) {
32
+ res.writeHead(404, JSON_HEADERS);
33
+ res.end(JSON.stringify({ error: 'not found' }));
34
+ return;
35
+ }
36
+ const previousSecret = process.env.GITHUB_WEBHOOK_SECRET_PREVIOUS;
37
+ const sigHdr = req.headers['x-hub-signature-256'];
38
+ const eventHdr = req.headers['x-github-event'];
39
+ const deliveryHdr = req.headers['x-github-delivery'];
40
+ const sigStr = isHeaderString(sigHdr) ? sigHdr : null;
41
+ const eventName = isHeaderString(eventHdr) ? eventHdr : null;
42
+ const deliveryId = isHeaderString(deliveryHdr) ? deliveryHdr : null;
43
+ if (sigStr === null ||
44
+ !verifyGitHubSignature({
45
+ rawBody,
46
+ signature: sigStr,
47
+ webhookSecret: secret,
48
+ previousSecret,
49
+ })) {
50
+ throw new HttpError(401, 'invalid GitHub signature');
51
+ }
52
+ // Signature OK from here on. Everything else is ACK-200; bad envelopes go
53
+ // to the DLQ and a human can replay later.
54
+ // Cheap regex extraction of installation_id / repo for DLQ rows that fail
55
+ // to JSON.parse - gives operators something to triage.
56
+ const installationFromRaw = (() => {
57
+ const m = rawBody.match(/"installation"\s*:\s*\{[^}]*"id"\s*:\s*(\d+)/);
58
+ return m ? m[1] : null;
59
+ })();
60
+ const repoFromRaw = (() => {
61
+ const m = rawBody.match(/"full_name"\s*:\s*"([^"]+)"/);
62
+ return m ? m[1] : null;
63
+ })();
64
+ if (deliveryId === null) {
65
+ // Body was signed but caller omitted the audit header. Park.
66
+ const db = openHippoDb(opts.hippoRoot);
67
+ try {
68
+ writeToGitHubDlq(db, {
69
+ tenantId: resolveTenantId({}),
70
+ rawPayload: rawBody,
71
+ error: 'missing X-GitHub-Delivery header',
72
+ bucket: 'parse_error',
73
+ eventName,
74
+ deliveryId: null,
75
+ signature: sigStr,
76
+ installationId: installationFromRaw,
77
+ repoFullName: repoFromRaw,
78
+ });
79
+ }
80
+ finally {
81
+ closeHippoDb(db);
82
+ }
83
+ sendJson(res, 200, { ok: true, status: 'dlq' });
84
+ return;
85
+ }
86
+ // Ping fires once at hook creation. Don't ingest, don't DLQ - just pong.
87
+ if (eventName === 'ping') {
88
+ sendJson(res, 200, { pong: true });
89
+ return;
90
+ }
91
+ const ALLOWED_EVENTS = new Set([
92
+ 'issues',
93
+ 'issue_comment',
94
+ 'pull_request',
95
+ 'pull_request_review_comment',
96
+ ]);
97
+ if (eventName === null || !ALLOWED_EVENTS.has(eventName)) {
98
+ const db = openHippoDb(opts.hippoRoot);
99
+ try {
100
+ writeToGitHubDlq(db, {
101
+ tenantId: resolveTenantId({}),
102
+ rawPayload: rawBody,
103
+ error: `unhandled event: ${eventName ?? '(missing X-GitHub-Event)'}`,
104
+ bucket: 'unhandled',
105
+ eventName,
106
+ deliveryId,
107
+ signature: sigStr,
108
+ installationId: installationFromRaw,
109
+ repoFullName: repoFromRaw,
110
+ });
111
+ }
112
+ finally {
113
+ closeHippoDb(db);
114
+ }
115
+ sendJson(res, 200, { ok: true, status: 'dlq' });
116
+ return;
117
+ }
118
+ let body;
119
+ try {
120
+ body = JSON.parse(rawBody);
121
+ }
122
+ catch {
123
+ const db = openHippoDb(opts.hippoRoot);
124
+ try {
125
+ writeToGitHubDlq(db, {
126
+ tenantId: resolveTenantId({}),
127
+ rawPayload: rawBody,
128
+ error: 'invalid JSON',
129
+ bucket: 'parse_error',
130
+ eventName,
131
+ deliveryId,
132
+ signature: sigStr,
133
+ installationId: installationFromRaw,
134
+ repoFullName: repoFromRaw,
135
+ });
136
+ }
137
+ finally {
138
+ closeHippoDb(db);
139
+ }
140
+ sendJson(res, 200, { ok: true, status: 'dlq' });
141
+ return;
142
+ }
143
+ if (body === undefined || !isGitHubWebhookEnvelope(body)) {
144
+ const db = openHippoDb(opts.hippoRoot);
145
+ try {
146
+ writeToGitHubDlq(db, {
147
+ tenantId: resolveTenantId({}),
148
+ rawPayload: rawBody,
149
+ error: 'not a GitHub webhook envelope',
150
+ bucket: 'parse_error',
151
+ eventName,
152
+ deliveryId,
153
+ signature: sigStr,
154
+ installationId: installationFromRaw,
155
+ repoFullName: repoFromRaw,
156
+ });
157
+ }
158
+ finally {
159
+ closeHippoDb(db);
160
+ }
161
+ sendJson(res, 200, { ok: true, status: 'dlq' });
162
+ return;
163
+ }
164
+ const installationId = body.installation?.id != null ? String(body.installation.id) : null;
165
+ const repoFullName = body.repository?.full_name ?? null;
166
+ // Tenant resolution. Fail closed on multi-tenant installs with unknown
167
+ // routing - same policy as Slack.
168
+ let resolvedTenant;
169
+ {
170
+ const db = openHippoDb(opts.hippoRoot);
171
+ try {
172
+ resolvedTenant = resolveTenantForGitHub(db, {
173
+ installationId,
174
+ repoFullName,
175
+ });
176
+ }
177
+ finally {
178
+ closeHippoDb(db);
179
+ }
180
+ }
181
+ if (resolvedTenant === null) {
182
+ const db = openHippoDb(opts.hippoRoot);
183
+ try {
184
+ writeToGitHubDlq(db, {
185
+ tenantId: null,
186
+ rawPayload: rawBody,
187
+ error: `unroutable: installation_id=${installationId ?? '(none)'} repo=${repoFullName ?? '(none)'}`,
188
+ bucket: 'unroutable',
189
+ eventName,
190
+ deliveryId,
191
+ signature: sigStr,
192
+ installationId,
193
+ repoFullName,
194
+ });
195
+ }
196
+ finally {
197
+ closeHippoDb(db);
198
+ }
199
+ sendJson(res, 200, { ok: true, status: 'dlq' });
200
+ return;
201
+ }
202
+ const ctx = {
203
+ hippoRoot: opts.hippoRoot,
204
+ tenantId: resolvedTenant,
205
+ actor: adminActor('connector:github'),
206
+ };
207
+ // Dispatch by event header. Type guards cross-check the body shape against
208
+ // the header so a payload of one event type cannot satisfy another's guard.
209
+ if (eventName === 'issues' && isGitHubIssueEvent(body, 'issues')) {
210
+ if (body.action === 'deleted') {
211
+ // GitHub does fire issues.deleted (admin-initiated). Don't archive - V1
212
+ // policy is to log and let an operator decide. Archive could lose the
213
+ // memory if the issue is being moved between accounts.
214
+ const db = openHippoDb(opts.hippoRoot);
215
+ try {
216
+ writeToGitHubDlq(db, {
217
+ tenantId: resolvedTenant,
218
+ rawPayload: rawBody,
219
+ error: 'issues.deleted requires manual review',
220
+ bucket: 'unhandled',
221
+ eventName,
222
+ deliveryId,
223
+ signature: sigStr,
224
+ installationId,
225
+ repoFullName,
226
+ });
227
+ }
228
+ finally {
229
+ closeHippoDb(db);
230
+ }
231
+ sendJson(res, 200, { ok: true, status: 'dlq' });
232
+ return;
233
+ }
234
+ const ingestInput = { eventName: 'issues', payload: body };
235
+ const r = ingestGitHubEvent(ctx, { event: ingestInput, rawBody, deliveryId });
236
+ sendJson(res, 200, { ok: true, status: r.status, memoryId: r.memoryId });
237
+ return;
238
+ }
239
+ if (eventName === 'issue_comment' && isGitHubIssueCommentEvent(body, 'issue_comment')) {
240
+ if (body.action === 'deleted') {
241
+ const repo = body.repository?.full_name ?? '';
242
+ const artifactRef = `github://${repo}/issue/${body.issue.number}/comment/${body.comment.id}`;
243
+ // The "deleted:" key namespace keeps this from colliding with the ingest key for the
244
+ // same artifact; a shared key would make hasSeenKey skip the archive.
245
+ const idempotencyKey = computeGitHubDeletionKey(artifactRef, body.comment.updated_at ?? null);
246
+ const r = handleGitHubCommentDeleted(ctx, {
247
+ artifactRef,
248
+ idempotencyKey,
249
+ deliveryId,
250
+ eventName,
251
+ });
252
+ sendJson(res, 200, { ok: true, status: r.status, archivedCount: r.archivedCount });
253
+ return;
254
+ }
255
+ const ingestInput = { eventName: 'issue_comment', payload: body };
256
+ const r = ingestGitHubEvent(ctx, { event: ingestInput, rawBody, deliveryId });
257
+ sendJson(res, 200, { ok: true, status: r.status, memoryId: r.memoryId });
258
+ return;
259
+ }
260
+ if (eventName === 'pull_request' && isGitHubPullRequestEvent(body, 'pull_request')) {
261
+ const ingestInput = { eventName: 'pull_request', payload: body };
262
+ const r = ingestGitHubEvent(ctx, { event: ingestInput, rawBody, deliveryId });
263
+ sendJson(res, 200, { ok: true, status: r.status, memoryId: r.memoryId });
264
+ return;
265
+ }
266
+ if (eventName === 'pull_request_review_comment' &&
267
+ isGitHubPullRequestReviewCommentEvent(body, 'pull_request_review_comment')) {
268
+ if (body.action === 'deleted') {
269
+ const repo = body.repository?.full_name ?? '';
270
+ const artifactRef = `github://${repo}/pull/${body.pull_request.number}/review_comment/${body.comment.id}`;
271
+ // Same "deleted:" key namespace as the issue_comment branch above.
272
+ const idempotencyKey = computeGitHubDeletionKey(artifactRef, body.comment.updated_at ?? null);
273
+ const r = handleGitHubCommentDeleted(ctx, {
274
+ artifactRef,
275
+ idempotencyKey,
276
+ deliveryId,
277
+ eventName,
278
+ });
279
+ sendJson(res, 200, { ok: true, status: r.status, archivedCount: r.archivedCount });
280
+ return;
281
+ }
282
+ const ingestInput = {
283
+ eventName: 'pull_request_review_comment',
284
+ payload: body,
285
+ };
286
+ const r = ingestGitHubEvent(ctx, { event: ingestInput, rawBody, deliveryId });
287
+ sendJson(res, 200, { ok: true, status: r.status, memoryId: r.memoryId });
288
+ return;
289
+ }
290
+ // Header allow-listed but body shape didn't satisfy the matching guard.
291
+ {
292
+ const db = openHippoDb(opts.hippoRoot);
293
+ try {
294
+ writeToGitHubDlq(db, {
295
+ tenantId: resolvedTenant,
296
+ rawPayload: rawBody,
297
+ error: `body shape did not match X-GitHub-Event=${eventName}`,
298
+ bucket: 'parse_error',
299
+ eventName,
300
+ deliveryId,
301
+ signature: sigStr,
302
+ installationId,
303
+ repoFullName,
304
+ });
305
+ }
306
+ finally {
307
+ closeHippoDb(db);
308
+ }
309
+ sendJson(res, 200, { ok: true, status: 'dlq' });
310
+ return;
311
+ }
312
+ }
313
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1,22 @@
1
+ import { type WebhookRequest } from '../../http-util.js';
2
+ /**
3
+ * Slack Events API webhook. Auth is signature-based (HMAC over the raw
4
+ * body with SLACK_SIGNING_SECRET); Bearer is NOT required, which is why
5
+ * this route is in PUBLIC_ROUTES. The route is responsible for:
6
+ * 1. Echoing the one-time url_verification challenge.
7
+ * 2. Verifying the HMAC on every other inbound payload.
8
+ * 3. Resolving body.team_id → tenantId via slack_workspaces, falling
9
+ * back to HIPPO_TENANT then 'default'.
10
+ * 4. Dispatching event_callback envelopes to ingestMessage /
11
+ * handleMessageDeleted.
12
+ * 5. Parking malformed or unhandled payloads in slack_dlq and STILL
13
+ * ACKing 200 - Slack retries forever otherwise.
14
+ *
15
+ * When SLACK_SIGNING_SECRET is unset we return 404, not
16
+ * 503, so an external probe cannot distinguish "route gated off by config"
17
+ * from "route does not exist on this build".
18
+ *
19
+ * Bearer auth is skipped on purpose: server.ts checks isPublicRoute before calling this.
20
+ */
21
+ export declare function handleSlackEventsWebhook({ req, res, opts }: WebhookRequest): Promise<void>;
22
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1,203 @@
1
+ import { verifySlackSignature } from './signature.js';
2
+ import { isSlackEventEnvelope, isSlackMessageEvent } from './types.js';
3
+ import { ingestMessage } from './ingest.js';
4
+ import { handleMessageDeleted } from './deletion.js';
5
+ import { writeToDlq } from './dlq.js';
6
+ import { resolveTenantForTeam } from './tenant-routing.js';
7
+ import { resolveTenantId } from '../../tenant.js';
8
+ import { openHippoDb, closeHippoDb } from '../../db.js';
9
+ import { adminActor } from '../../api.js';
10
+ import { HttpError, JSON_HEADERS, isHeaderString, isJsonObjectRecord, readBody, sendJson, } from '../../http-util.js';
11
+ /**
12
+ * Slack Events API webhook. Auth is signature-based (HMAC over the raw
13
+ * body with SLACK_SIGNING_SECRET); Bearer is NOT required, which is why
14
+ * this route is in PUBLIC_ROUTES. The route is responsible for:
15
+ * 1. Echoing the one-time url_verification challenge.
16
+ * 2. Verifying the HMAC on every other inbound payload.
17
+ * 3. Resolving body.team_id → tenantId via slack_workspaces, falling
18
+ * back to HIPPO_TENANT then 'default'.
19
+ * 4. Dispatching event_callback envelopes to ingestMessage /
20
+ * handleMessageDeleted.
21
+ * 5. Parking malformed or unhandled payloads in slack_dlq and STILL
22
+ * ACKing 200 - Slack retries forever otherwise.
23
+ *
24
+ * When SLACK_SIGNING_SECRET is unset we return 404, not
25
+ * 503, so an external probe cannot distinguish "route gated off by config"
26
+ * from "route does not exist on this build".
27
+ *
28
+ * Bearer auth is skipped on purpose: server.ts checks isPublicRoute before calling this.
29
+ */
30
+ export async function handleSlackEventsWebhook({ req, res, opts }) {
31
+ const rawBody = await readBody(req);
32
+ const secret = process.env.SLACK_SIGNING_SECRET;
33
+ if (!secret) {
34
+ res.writeHead(404, JSON_HEADERS);
35
+ res.end(JSON.stringify({ error: 'not found' }));
36
+ return;
37
+ }
38
+ const previousSecret = process.env.SLACK_SIGNING_SECRET_PREVIOUS;
39
+ const sig = req.headers['x-slack-signature'];
40
+ const tsHdr = req.headers['x-slack-request-timestamp'];
41
+ const sigStr = isHeaderString(sig) ? sig : null;
42
+ const tsStr = isHeaderString(tsHdr) ? tsHdr : null;
43
+ if (sigStr === null ||
44
+ tsStr === null ||
45
+ !verifySlackSignature({
46
+ rawBody,
47
+ timestamp: tsStr,
48
+ signature: sigStr,
49
+ signingSecret: secret,
50
+ previousSecret,
51
+ })) {
52
+ throw new HttpError(401, 'invalid Slack signature');
53
+ }
54
+ // Cheap regex extracts team_id from a (possibly malformed) raw body so the
55
+ // DLQ row carries it for triage even when JSON.parse fails.
56
+ const teamIdFromRaw = (() => {
57
+ const m = rawBody.match(/"team_id"\s*:\s*"([^"]+)"/);
58
+ return m ? m[1] : null;
59
+ })();
60
+ let body;
61
+ try {
62
+ body = JSON.parse(rawBody);
63
+ }
64
+ catch {
65
+ // Attribute the parse failure to the originating workspace via the regex-extracted
66
+ // team_id; a null or unknown team writes tenantId=null, which lands as '__unroutable__'.
67
+ const db = openHippoDb(opts.hippoRoot);
68
+ try {
69
+ const parseFailTenant = teamIdFromRaw !== null ? resolveTenantForTeam(db, teamIdFromRaw) : null;
70
+ writeToDlq(db, {
71
+ tenantId: parseFailTenant, // null → '__unroutable__' sentinel
72
+ teamId: teamIdFromRaw,
73
+ rawPayload: rawBody,
74
+ error: 'invalid JSON',
75
+ bucket: 'parse_error',
76
+ signature: sigStr,
77
+ slackTimestamp: tsStr,
78
+ });
79
+ }
80
+ finally {
81
+ closeHippoDb(db);
82
+ }
83
+ sendJson(res, 200, { ok: true, status: 'dlq' });
84
+ return;
85
+ }
86
+ if (isJsonObjectRecord(body)) {
87
+ const bodyRecord = body;
88
+ if (bodyRecord.type === 'url_verification') {
89
+ sendJson(res, 200, {
90
+ challenge: String(bodyRecord.challenge ?? ''),
91
+ });
92
+ return;
93
+ }
94
+ }
95
+ // Resolve tenant, failing closed: when slack_workspaces is non-empty
96
+ // and the team_id is unknown, resolveTenantForTeam returns null and we
97
+ // park the envelope in slack_dlq with bucket='unroutable'. Mandatory ACK
98
+ // 200 so Slack stops retrying; do NOT call ingest.
99
+ let resolvedTenant = null;
100
+ if (body !== undefined && isSlackEventEnvelope(body)) {
101
+ const db = openHippoDb(opts.hippoRoot);
102
+ try {
103
+ resolvedTenant = resolveTenantForTeam(db, body.team_id);
104
+ }
105
+ finally {
106
+ closeHippoDb(db);
107
+ }
108
+ if (resolvedTenant === null) {
109
+ const db2 = openHippoDb(opts.hippoRoot);
110
+ try {
111
+ writeToDlq(db2, {
112
+ tenantId: null, // unroutable - stored as '__unroutable__'
113
+ teamId: body.team_id,
114
+ rawPayload: rawBody,
115
+ error: `unroutable team_id: ${body.team_id}`,
116
+ bucket: 'unroutable',
117
+ signature: sigStr,
118
+ slackTimestamp: tsStr,
119
+ });
120
+ }
121
+ finally {
122
+ closeHippoDb(db2);
123
+ }
124
+ sendJson(res, 200, { ok: true, status: 'dlq' });
125
+ return;
126
+ }
127
+ }
128
+ else {
129
+ // Non-envelope payload: use env tenant for the DLQ row's bookkeeping.
130
+ resolvedTenant = resolveTenantId({});
131
+ }
132
+ const ctx = {
133
+ hippoRoot: opts.hippoRoot,
134
+ tenantId: resolvedTenant,
135
+ actor: adminActor('connector:slack'),
136
+ };
137
+ if (body === undefined || !isSlackEventEnvelope(body)) {
138
+ const db = openHippoDb(ctx.hippoRoot);
139
+ try {
140
+ writeToDlq(db, {
141
+ tenantId: ctx.tenantId,
142
+ teamId: teamIdFromRaw,
143
+ rawPayload: rawBody,
144
+ error: 'not an event_callback envelope',
145
+ bucket: 'parse_error',
146
+ signature: sigStr,
147
+ slackTimestamp: tsStr,
148
+ });
149
+ }
150
+ finally {
151
+ closeHippoDb(db);
152
+ }
153
+ sendJson(res, 200, { ok: true, status: 'dlq' });
154
+ return;
155
+ }
156
+ const inner = body.event;
157
+ if (isSlackMessageEvent(inner)) {
158
+ if (inner.subtype === 'message_deleted' && inner.deleted_ts) {
159
+ const r = handleMessageDeleted(ctx, {
160
+ teamId: body.team_id,
161
+ channelId: inner.channel,
162
+ deletedTs: inner.deleted_ts,
163
+ eventId: body.event_id,
164
+ });
165
+ sendJson(res, 200, { ok: true, status: r.status });
166
+ return;
167
+ }
168
+ const r = ingestMessage(ctx, {
169
+ teamId: body.team_id,
170
+ // channel privacy isn't on the inner event; use channel_type as a
171
+ // proxy. 'group'|'im'|'mpim' → private. 'channel' → public. Unknown
172
+ // → private (fail closed).
173
+ channel: {
174
+ id: inner.channel,
175
+ is_private: inner.channel_type !== 'channel',
176
+ is_im: inner.channel_type === 'im',
177
+ is_mpim: inner.channel_type === 'mpim',
178
+ },
179
+ message: inner,
180
+ eventId: body.event_id,
181
+ });
182
+ sendJson(res, 200, { ok: true, status: r.status, memoryId: r.memoryId });
183
+ return;
184
+ }
185
+ const db = openHippoDb(ctx.hippoRoot);
186
+ try {
187
+ writeToDlq(db, {
188
+ tenantId: ctx.tenantId,
189
+ teamId: body.team_id,
190
+ rawPayload: rawBody,
191
+ error: `unhandled event type: ${inner.type ?? 'unknown'}`,
192
+ bucket: 'parse_error',
193
+ signature: sigStr,
194
+ slackTimestamp: tsStr,
195
+ });
196
+ }
197
+ finally {
198
+ closeHippoDb(db);
199
+ }
200
+ sendJson(res, 200, { ok: true, status: 'dlq' });
201
+ return;
202
+ }
203
+ //# sourceMappingURL=webhook.js.map
@@ -7,9 +7,10 @@
7
7
  * 3. Stats tracking
8
8
  */
9
9
  import { evalNow, isRecallBoostAblated } from './ablation.js';
10
- import { Layer, calculateStrength, canAutoDelete, createMemory } from './memory.js';
10
+ import { Layer, calculateStrength, canAutoDelete, createMemory, markRetrieved } from './memory.js';
11
11
  import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, memoriesBackingObjects, } from './store.js';
12
- import { textOverlap, markRetrieved, tokenize } from './search.js';
12
+ import { textOverlap } from './search.js';
13
+ import { tokenize } from './tokenize.js';
13
14
  import { compareEntryIdentity } from './compare.js';
14
15
  import { duplicateKey, mergedText } from './same-text.js';
15
16
  import { successorAfterRetirement } from './merged-row.js';
@@ -0,0 +1,3 @@
1
+ /** A context query from the cwd's git state ('' means `getContext` lists by strength); kept out of the api layer, which never shells out. */
2
+ export declare function autoDetectContext(): string;
3
+ //# sourceMappingURL=context-auto.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { execSync } from 'child_process';
2
+ const SKIPPED_PATH_TERMS = ['src', 'dist', 'test', 'tests', 'node_modules', 'index'];
3
+ /** A context query from the cwd's git state ('' means `getContext` lists by strength); kept out of the api layer, which never shells out. */
4
+ export function autoDetectContext() {
5
+ try {
6
+ const diff = execSync('git diff --name-only HEAD 2>&1', {
7
+ encoding: 'utf8',
8
+ timeout: 3000,
9
+ windowsHide: true,
10
+ }).trim();
11
+ if (diff) {
12
+ const terms = diff
13
+ .split('\n')
14
+ .flatMap((f) => f.replace(/[/\\.]/g, ' ').split(/\s+/))
15
+ .filter((t) => t.length > 2 && !SKIPPED_PATH_TERMS.includes(t))
16
+ .slice(0, 10);
17
+ if (terms.length > 0)
18
+ return terms.join(' ');
19
+ }
20
+ const branch = execSync('git branch --show-current 2>&1', {
21
+ encoding: 'utf8',
22
+ timeout: 3000,
23
+ windowsHide: true,
24
+ }).trim();
25
+ if (branch && branch !== 'main' && branch !== 'master') {
26
+ return branch.replace(/[-_/]/g, ' ');
27
+ }
28
+ }
29
+ catch {
30
+ // Not a git repo or git missing: no query, so the caller lists by strength.
31
+ }
32
+ return '';
33
+ }
34
+ //# sourceMappingURL=context-auto.js.map
@@ -20,7 +20,8 @@
20
20
  * Lifecycle: active -> superseded (a corrected version) or active -> closed (retired).
21
21
  */
22
22
  import { openHippoDb, closeHippoDb } from './db.js';
23
- import { writeEntry, assertTenantId } from './store.js';
23
+ import { writeEntry } from './store.js';
24
+ import { assertTenantId } from './tenant.js';
24
25
  import { markGraphDirty, removeGraphEntitiesForObject } from './graph.js';
25
26
  import { createMemory, Layer } from './memory.js';
26
27
  import { appendAuditEvent } from './audit.js';