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.
- package/README.md +11 -0
- package/dist/api.d.ts +19 -9
- package/dist/api.js +112 -35
- package/dist/card-detail.d.ts +1 -1
- package/dist/card-detail.js +1 -1
- package/dist/cli/shared.d.ts +137 -0
- package/dist/cli/shared.js +830 -0
- package/dist/cli/sleep.d.ts +10 -0
- package/dist/cli/sleep.js +171 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +313 -1806
- package/dist/config.d.ts +5 -0
- package/dist/config.js +21 -0
- package/dist/connectors/github/webhook.d.ts +19 -0
- package/dist/connectors/github/webhook.js +313 -0
- package/dist/connectors/slack/webhook.d.ts +22 -0
- package/dist/connectors/slack/webhook.js +203 -0
- package/dist/consolidate.js +3 -2
- package/dist/context-auto.d.ts +3 -0
- package/dist/context-auto.js +34 -0
- package/dist/customer-notes.js +2 -1
- package/dist/dashboard.js +2 -1
- package/dist/db.js +67 -1
- package/dist/decisions.js +2 -1
- package/dist/delivery-recorder.d.ts +127 -0
- package/dist/delivery-recorder.js +218 -0
- package/dist/eval-stats.d.ts +58 -0
- package/dist/eval-stats.js +111 -0
- package/dist/goals.d.ts +49 -25
- package/dist/goals.js +39 -22
- package/dist/graph-extract.js +1 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +1 -1
- package/dist/graph.js +1 -1
- package/dist/hooks.d.ts +1 -3
- package/dist/hooks.js +2 -4
- package/dist/http-util.d.ts +31 -0
- package/dist/http-util.js +46 -0
- package/dist/incidents.js +2 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/mcp/server.js +173 -285
- package/dist/memory.d.ts +19 -0
- package/dist/memory.js +38 -0
- package/dist/policies.js +2 -1
- package/dist/predictions.js +2 -1
- package/dist/processes.js +2 -1
- package/dist/project-briefs.js +3 -1
- package/dist/prompt-recall.js +1 -1
- package/dist/recall-history.d.ts +5 -0
- package/dist/recall-history.js +9 -0
- package/dist/recall-pipeline.d.ts +101 -0
- package/dist/recall-pipeline.js +313 -0
- package/dist/recall-scope.d.ts +22 -0
- package/dist/recall-scope.js +27 -1
- package/dist/recall-trace.d.ts +69 -0
- package/dist/recall-trace.js +136 -0
- package/dist/search.d.ts +0 -20
- package/dist/search.js +2 -49
- package/dist/server.js +1901 -2384
- package/dist/skills.js +2 -1
- package/dist/store-cards.d.ts +53 -0
- package/dist/store-cards.js +512 -0
- package/dist/store.d.ts +2 -89
- package/dist/store.js +6 -562
- package/dist/tenant.d.ts +22 -0
- package/dist/tenant.js +26 -0
- package/dist/token-ledger.d.ts +2 -0
- package/dist/token-ledger.js +5 -0
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- 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
|
package/dist/consolidate.js
CHANGED
|
@@ -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
|
|
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,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
|
package/dist/customer-notes.js
CHANGED
|
@@ -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
|
|
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';
|