hippo-memory 1.2.1 → 1.3.1
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 +17 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/github/backfill.d.ts +48 -0
- package/dist/connectors/github/backfill.d.ts.map +1 -0
- package/dist/connectors/github/backfill.js +272 -0
- package/dist/connectors/github/backfill.js.map +1 -0
- package/dist/connectors/github/cli-impl.d.ts +24 -0
- package/dist/connectors/github/cli-impl.d.ts.map +1 -0
- package/dist/connectors/github/cli-impl.js +192 -0
- package/dist/connectors/github/cli-impl.js.map +1 -0
- package/dist/connectors/github/deletion.d.ts +43 -0
- package/dist/connectors/github/deletion.d.ts.map +1 -0
- package/dist/connectors/github/deletion.js +83 -0
- package/dist/connectors/github/deletion.js.map +1 -0
- package/dist/connectors/github/dlq.d.ts +108 -0
- package/dist/connectors/github/dlq.d.ts.map +1 -0
- package/dist/connectors/github/dlq.js +182 -0
- package/dist/connectors/github/dlq.js.map +1 -0
- package/dist/connectors/github/idempotency.d.ts +19 -0
- package/dist/connectors/github/idempotency.d.ts.map +1 -0
- package/dist/connectors/github/idempotency.js +25 -0
- package/dist/connectors/github/idempotency.js.map +1 -0
- package/dist/connectors/github/ingest.d.ts +67 -0
- package/dist/connectors/github/ingest.d.ts.map +1 -0
- package/dist/connectors/github/ingest.js +138 -0
- package/dist/connectors/github/ingest.js.map +1 -0
- package/dist/connectors/github/octokit-client.d.ts +36 -0
- package/dist/connectors/github/octokit-client.d.ts.map +1 -0
- package/dist/connectors/github/octokit-client.js +65 -0
- package/dist/connectors/github/octokit-client.js.map +1 -0
- package/dist/connectors/github/ratelimit.d.ts +20 -0
- package/dist/connectors/github/ratelimit.d.ts.map +1 -0
- package/dist/connectors/github/ratelimit.js +31 -0
- package/dist/connectors/github/ratelimit.js.map +1 -0
- package/dist/connectors/github/scope.d.ts +8 -0
- package/dist/connectors/github/scope.d.ts.map +1 -0
- package/dist/connectors/github/scope.js +13 -0
- package/dist/connectors/github/scope.js.map +1 -0
- package/dist/connectors/github/signature.d.ts +47 -0
- package/dist/connectors/github/signature.d.ts.map +1 -0
- package/dist/connectors/github/signature.js +58 -0
- package/dist/connectors/github/signature.js.map +1 -0
- package/dist/connectors/github/tenant-routing.d.ts +33 -0
- package/dist/connectors/github/tenant-routing.d.ts.map +1 -0
- package/dist/connectors/github/tenant-routing.js +61 -0
- package/dist/connectors/github/tenant-routing.js.map +1 -0
- package/dist/connectors/github/transform.d.ts +7 -0
- package/dist/connectors/github/transform.d.ts.map +1 -0
- package/dist/connectors/github/transform.js +103 -0
- package/dist/connectors/github/transform.js.map +1 -0
- package/dist/connectors/github/types.d.ts +87 -0
- package/dist/connectors/github/types.d.ts.map +1 -0
- package/dist/connectors/github/types.js +94 -0
- package/dist/connectors/github/types.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +94 -1
- package/dist/db.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +313 -2
- package/dist/server.js.map +1 -1
- package/dist/src/cli.js +10 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/connectors/github/backfill.js +272 -0
- package/dist/src/connectors/github/backfill.js.map +1 -0
- package/dist/src/connectors/github/cli-impl.js +192 -0
- package/dist/src/connectors/github/cli-impl.js.map +1 -0
- package/dist/src/connectors/github/deletion.js +83 -0
- package/dist/src/connectors/github/deletion.js.map +1 -0
- package/dist/src/connectors/github/dlq.js +182 -0
- package/dist/src/connectors/github/dlq.js.map +1 -0
- package/dist/src/connectors/github/idempotency.js +25 -0
- package/dist/src/connectors/github/idempotency.js.map +1 -0
- package/dist/src/connectors/github/ingest.js +138 -0
- package/dist/src/connectors/github/ingest.js.map +1 -0
- package/dist/src/connectors/github/octokit-client.js +65 -0
- package/dist/src/connectors/github/octokit-client.js.map +1 -0
- package/dist/src/connectors/github/ratelimit.js +31 -0
- package/dist/src/connectors/github/ratelimit.js.map +1 -0
- package/dist/src/connectors/github/scope.js +13 -0
- package/dist/src/connectors/github/scope.js.map +1 -0
- package/dist/src/connectors/github/signature.js +58 -0
- package/dist/src/connectors/github/signature.js.map +1 -0
- package/dist/src/connectors/github/tenant-routing.js +61 -0
- package/dist/src/connectors/github/tenant-routing.js.map +1 -0
- package/dist/src/connectors/github/transform.js +103 -0
- package/dist/src/connectors/github/transform.js.map +1 -0
- package/dist/src/connectors/github/types.js +94 -0
- package/dist/src/connectors/github/types.js.map +1 -0
- package/dist/src/db.js +94 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/mcp/server.js +5 -4
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/server.js +313 -2
- package/dist/src/server.js.map +1 -1
- package/dist/src/version.js +35 -0
- package/dist/src/version.js.map +1 -0
- package/dist/version.d.ts +25 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +35 -0
- package/dist/version.js.map +1 -0
- 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 +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
2
|
+
import { archiveRawMemory } from '../../raw-archive.js';
|
|
3
|
+
import { hasSeenKey, markKeySeen } from './idempotency.js';
|
|
4
|
+
/**
|
|
5
|
+
* Handle GitHub `issue_comment.deleted` and `pull_request_review_comment.deleted`.
|
|
6
|
+
*
|
|
7
|
+
* Codex round 1 P0 #5: filter by tenant_id + kind='raw'. Multi-row archive:
|
|
8
|
+
* GitHub edits keep the same artifact_ref, so multiple active raw rows can
|
|
9
|
+
* match a single deletion event. Archive ALL of them.
|
|
10
|
+
*
|
|
11
|
+
* Claude round 2 P0 #2 (v1.3.1 hotfix): the v1.3.0 implementation called
|
|
12
|
+
* archiveRaw N times in a loop, each opening its own DB handle and SAVEPOINT.
|
|
13
|
+
* The first archive's afterArchive committed the idempotency mark. If archive
|
|
14
|
+
* 2..N threw, idempotency was already committed and retry returned 'duplicate'
|
|
15
|
+
* with archivedCount=0 — survivors stayed searchable, leaking private bodies.
|
|
16
|
+
*
|
|
17
|
+
* v1.3.1 fix: ONE shared DB handle wrapping ALL archives + the idempotency
|
|
18
|
+
* mark in a single outer SAVEPOINT. Any per-row failure rolls back the entire
|
|
19
|
+
* batch (including idempotency), so retry re-attempts cleanly. archiveRawMemory
|
|
20
|
+
* (the lower-level function from raw-archive.js) runs its own inner SAVEPOINT
|
|
21
|
+
* which nests safely inside the outer one.
|
|
22
|
+
*
|
|
23
|
+
* Tenant scope and kind='raw' filtering are load-bearing: without them a
|
|
24
|
+
* deletion event from tenant A could archive tenant B's row sharing the same
|
|
25
|
+
* artifact_ref, or accidentally target a distilled row.
|
|
26
|
+
*/
|
|
27
|
+
export function handleCommentDeleted(ctx, input) {
|
|
28
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
29
|
+
try {
|
|
30
|
+
if (hasSeenKey(db, input.idempotencyKey)) {
|
|
31
|
+
return { status: 'duplicate', archivedCount: 0 };
|
|
32
|
+
}
|
|
33
|
+
const rows = db
|
|
34
|
+
.prepare(`SELECT id FROM memories WHERE artifact_ref = ? AND tenant_id = ? AND kind = 'raw'`)
|
|
35
|
+
.all(input.artifactRef, ctx.tenantId);
|
|
36
|
+
const memoryIds = rows.map((r) => r.id);
|
|
37
|
+
if (memoryIds.length === 0) {
|
|
38
|
+
// Nothing to archive — still mark idempotency so a retry returns 'duplicate'.
|
|
39
|
+
// Independent INSERT, no rollback needed.
|
|
40
|
+
markKeySeen(db, {
|
|
41
|
+
idempotencyKey: input.idempotencyKey,
|
|
42
|
+
deliveryId: input.deliveryId,
|
|
43
|
+
eventName: input.eventName,
|
|
44
|
+
memoryId: null,
|
|
45
|
+
});
|
|
46
|
+
return { status: 'archive_skipped_not_found', archivedCount: 0 };
|
|
47
|
+
}
|
|
48
|
+
// Outer SAVEPOINT wrapping all archives + the idempotency mark. Any throw
|
|
49
|
+
// rolls back the whole batch so retry sees neither archive nor mark — and
|
|
50
|
+
// re-attempts the full set.
|
|
51
|
+
db.exec('SAVEPOINT github_delete_all');
|
|
52
|
+
try {
|
|
53
|
+
for (const id of memoryIds) {
|
|
54
|
+
archiveRawMemory(db, id, {
|
|
55
|
+
reason: `source_deleted:github:${input.eventName}:${input.deliveryId}`,
|
|
56
|
+
who: ctx.actor || 'connector:github',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
markKeySeen(db, {
|
|
60
|
+
idempotencyKey: input.idempotencyKey,
|
|
61
|
+
deliveryId: input.deliveryId,
|
|
62
|
+
eventName: input.eventName,
|
|
63
|
+
memoryId: memoryIds[0],
|
|
64
|
+
});
|
|
65
|
+
db.exec('RELEASE SAVEPOINT github_delete_all');
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
try {
|
|
69
|
+
db.exec('ROLLBACK TO SAVEPOINT github_delete_all');
|
|
70
|
+
db.exec('RELEASE SAVEPOINT github_delete_all');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Best effort. Surface the original error.
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
return { status: 'archived', archivedCount: memoryIds.length };
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
closeHippoDb(db);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=deletion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deletion.js","sourceRoot":"","sources":["../../../src/connectors/github/deletion.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAsB3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY,EAAE,KAAoB;IACrE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;YACzC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;QACnD,CAAC;QAED,MAAM,IAAI,GAAG,EAAE;aACZ,OAAO,CACN,mFAAmF,CACpF;aACA,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,QAAQ,CAA0B,CAAC;QACjE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAExC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,8EAA8E;YAC9E,0CAA0C;YAC1C,WAAW,CAAC,EAAE,EAAE;gBACd,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;QAED,0EAA0E;QAC1E,0EAA0E;QAC1E,4BAA4B;QAC5B,EAAE,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACvC,IAAI,CAAC;YACH,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;gBAC3B,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE;oBACvB,MAAM,EAAE,yBAAyB,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE;oBACtE,GAAG,EAAE,GAAG,CAAC,KAAK,IAAI,kBAAkB;iBACrC,CAAC,CAAC;YACL,CAAC;YACD,WAAW,CAAC,EAAE,EAAE;gBACd,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAE;aACxB,CAAC,CAAC;YACH,EAAE,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,EAAE,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;gBACnD,EAAE,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,2CAA2C;YAC7C,CAAC;YACD,MAAM,CAAC,CAAC;QACV,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;IACjE,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from '../../db.js';
|
|
2
|
+
import type { Context } from '../../api.js';
|
|
3
|
+
/**
|
|
4
|
+
* GitHub webhook DLQ. Mirrors the Slack DLQ shape (src/connectors/slack/dlq.ts)
|
|
5
|
+
* but carries GitHub-specific metadata: event_name, delivery_id, signature,
|
|
6
|
+
* installation_id, repo_full_name. Codex P1 #5 mandates this rich context
|
|
7
|
+
* so a `hippo gh dlq replay` operator can triage without re-deriving anything
|
|
8
|
+
* from the raw payload.
|
|
9
|
+
*
|
|
10
|
+
* Buckets:
|
|
11
|
+
* - parse_error — raw_payload was not valid JSON
|
|
12
|
+
* - unroutable — no tenant resolved for installation_id / repo_full_name
|
|
13
|
+
* - signature_failed — HMAC did not verify against the active webhook secret
|
|
14
|
+
* - unhandled — parsed but no handler matched the event
|
|
15
|
+
*/
|
|
16
|
+
export type DlqBucket = 'parse_error' | 'unroutable' | 'signature_failed' | 'unhandled';
|
|
17
|
+
export interface DlqItem {
|
|
18
|
+
id: number;
|
|
19
|
+
tenantId: string;
|
|
20
|
+
rawPayload: string;
|
|
21
|
+
error: string;
|
|
22
|
+
eventName: string | null;
|
|
23
|
+
deliveryId: string | null;
|
|
24
|
+
signature: string | null;
|
|
25
|
+
installationId: string | null;
|
|
26
|
+
repoFullName: string | null;
|
|
27
|
+
retryCount: number;
|
|
28
|
+
receivedAt: string;
|
|
29
|
+
retriedAt: string | null;
|
|
30
|
+
bucket: DlqBucket | string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `tenantId: null` means the connector could not resolve a tenant for the
|
|
34
|
+
* envelope (unroutable installation/repo). Stored as the sentinel
|
|
35
|
+
* `'__unroutable__'` so the NOT NULL column is honored — same convention as
|
|
36
|
+
* Slack DLQ.
|
|
37
|
+
*/
|
|
38
|
+
export interface WriteDlqOpts {
|
|
39
|
+
tenantId: string | null;
|
|
40
|
+
rawPayload: string;
|
|
41
|
+
error: string;
|
|
42
|
+
bucket?: DlqBucket;
|
|
43
|
+
eventName?: string | null;
|
|
44
|
+
deliveryId?: string | null;
|
|
45
|
+
signature?: string | null;
|
|
46
|
+
installationId?: string | null;
|
|
47
|
+
repoFullName?: string | null;
|
|
48
|
+
}
|
|
49
|
+
export declare function writeToDlq(db: DatabaseSyncLike, opts: WriteDlqOpts): number;
|
|
50
|
+
export declare function listDlq(db: DatabaseSyncLike, opts: {
|
|
51
|
+
tenantId: string;
|
|
52
|
+
limit?: number;
|
|
53
|
+
}): DlqItem[];
|
|
54
|
+
export declare function getDlqEntry(db: DatabaseSyncLike, id: number): DlqItem | null;
|
|
55
|
+
export interface ReplayDlqOpts {
|
|
56
|
+
/** Current webhook secret. If omitted, signature check is skipped (force-only path). */
|
|
57
|
+
webhookSecret?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Previous webhook secret during rotation (v1.3.1 hotfix — claude P1).
|
|
60
|
+
* Operators rotating GITHUB_WEBHOOK_SECRET would otherwise be forced into
|
|
61
|
+
* --force on DLQ rows written under the old secret. Plumbed through to
|
|
62
|
+
* verifyGitHubSignature.previousSecret.
|
|
63
|
+
*/
|
|
64
|
+
previousSecret?: string;
|
|
65
|
+
/** When true, skip signature verification (used for legacy entries after secret rotation). */
|
|
66
|
+
force?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type ReplayStatus = 'replayed' | 'parse_error' | 'sig_fail' | 'sig_missing' | 'unhandled' | 'not_found';
|
|
69
|
+
export interface ReplayResult {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
status: ReplayStatus;
|
|
72
|
+
memoryId: string | null;
|
|
73
|
+
retryCount: number;
|
|
74
|
+
reason?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Hook the webhook route injects to actually re-ingest a row. Decoupling
|
|
78
|
+
* the dispatch keeps this module free of every event-type handler — the
|
|
79
|
+
* route already knows how to route an envelope, so it passes that capability
|
|
80
|
+
* back in.
|
|
81
|
+
*/
|
|
82
|
+
export type IngestHook = (ctx: Context, args: {
|
|
83
|
+
rawPayload: string;
|
|
84
|
+
eventName: string;
|
|
85
|
+
idempotencyKey: string;
|
|
86
|
+
deliveryId: string;
|
|
87
|
+
}) => Promise<{
|
|
88
|
+
memoryId: string | null;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Replay a DLQ row through the normal ingest path. Behavior:
|
|
92
|
+
* 1. Fetch row by id. Not found → `not_found`.
|
|
93
|
+
* 2. If !force and webhookSecret provided, verify signature with the
|
|
94
|
+
* current secret. Fail → bump retry_count, `sig_fail`.
|
|
95
|
+
* Missing signature on the row → `sig_missing` (no bump; --force required).
|
|
96
|
+
* 3. JSON.parse the raw payload. Fail → bump, `parse_error`.
|
|
97
|
+
* 4. Type-guard the envelope. Fail → bump, `unhandled`.
|
|
98
|
+
* 5. If an `ingestHook` is supplied, call it and return its memoryId.
|
|
99
|
+
* If not (dry-run path), bump retry_count and return status `replayed`
|
|
100
|
+
* with memoryId=null. The webhook route wires the real hook in Task 14.
|
|
101
|
+
*
|
|
102
|
+
* Mirrors Slack's "always use current routing" policy: replays use the
|
|
103
|
+
* deployment state NOW, not at the time of original DLQing.
|
|
104
|
+
*/
|
|
105
|
+
export declare function replayDlqEntry(ctx: Context, id: number, opts?: ReplayDlqOpts & {
|
|
106
|
+
ingestHook?: IngestHook;
|
|
107
|
+
}): Promise<ReplayResult>;
|
|
108
|
+
//# sourceMappingURL=dlq.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dlq.d.ts","sourceRoot":"","sources":["../../../src/connectors/github/dlq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
2
|
+
import { verifyGitHubSignature } from './signature.js';
|
|
3
|
+
import { isGitHubWebhookEnvelope } from './types.js';
|
|
4
|
+
export function writeToDlq(db, opts) {
|
|
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)
|
|
9
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
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
|
+
return Number(result.lastInsertRowid);
|
|
12
|
+
}
|
|
13
|
+
const SELECT_COLUMNS = `id, tenant_id, raw_payload, error, event_name, delivery_id,
|
|
14
|
+
signature, installation_id, repo_full_name, retry_count,
|
|
15
|
+
received_at, retried_at, bucket`;
|
|
16
|
+
export function listDlq(db, opts) {
|
|
17
|
+
const rows = db
|
|
18
|
+
.prepare(`SELECT ${SELECT_COLUMNS}
|
|
19
|
+
FROM github_dlq
|
|
20
|
+
WHERE tenant_id = ?
|
|
21
|
+
ORDER BY received_at ASC
|
|
22
|
+
LIMIT ?`)
|
|
23
|
+
.all(opts.tenantId, opts.limit ?? 100);
|
|
24
|
+
return rows.map(rowToItem);
|
|
25
|
+
}
|
|
26
|
+
export function getDlqEntry(db, id) {
|
|
27
|
+
const row = db
|
|
28
|
+
.prepare(`SELECT ${SELECT_COLUMNS}
|
|
29
|
+
FROM github_dlq
|
|
30
|
+
WHERE id = ?`)
|
|
31
|
+
.get(id);
|
|
32
|
+
if (!row)
|
|
33
|
+
return null;
|
|
34
|
+
return rowToItem(row);
|
|
35
|
+
}
|
|
36
|
+
function rowToItem(r) {
|
|
37
|
+
return {
|
|
38
|
+
id: Number(r.id),
|
|
39
|
+
tenantId: String(r.tenant_id),
|
|
40
|
+
rawPayload: String(r.raw_payload),
|
|
41
|
+
error: String(r.error),
|
|
42
|
+
eventName: r.event_name == null ? null : String(r.event_name),
|
|
43
|
+
deliveryId: r.delivery_id == null ? null : String(r.delivery_id),
|
|
44
|
+
signature: r.signature == null ? null : String(r.signature),
|
|
45
|
+
installationId: r.installation_id == null ? null : String(r.installation_id),
|
|
46
|
+
repoFullName: r.repo_full_name == null ? null : String(r.repo_full_name),
|
|
47
|
+
retryCount: Number(r.retry_count ?? 0),
|
|
48
|
+
receivedAt: String(r.received_at),
|
|
49
|
+
retriedAt: r.retried_at == null ? null : String(r.retried_at),
|
|
50
|
+
bucket: r.bucket == null ? 'parse_error' : String(r.bucket),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function bumpRetryCount(hippoRoot, id) {
|
|
54
|
+
const db = openHippoDb(hippoRoot);
|
|
55
|
+
try {
|
|
56
|
+
db.prepare(`UPDATE github_dlq
|
|
57
|
+
SET retry_count = retry_count + 1,
|
|
58
|
+
retried_at = ?
|
|
59
|
+
WHERE id = ?`).run(new Date().toISOString(), id);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
closeHippoDb(db);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Replay a DLQ row through the normal ingest path. Behavior:
|
|
67
|
+
* 1. Fetch row by id. Not found → `not_found`.
|
|
68
|
+
* 2. If !force and webhookSecret provided, verify signature with the
|
|
69
|
+
* current secret. Fail → bump retry_count, `sig_fail`.
|
|
70
|
+
* Missing signature on the row → `sig_missing` (no bump; --force required).
|
|
71
|
+
* 3. JSON.parse the raw payload. Fail → bump, `parse_error`.
|
|
72
|
+
* 4. Type-guard the envelope. Fail → bump, `unhandled`.
|
|
73
|
+
* 5. If an `ingestHook` is supplied, call it and return its memoryId.
|
|
74
|
+
* If not (dry-run path), bump retry_count and return status `replayed`
|
|
75
|
+
* with memoryId=null. The webhook route wires the real hook in Task 14.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors Slack's "always use current routing" policy: replays use the
|
|
78
|
+
* deployment state NOW, not at the time of original DLQing.
|
|
79
|
+
*/
|
|
80
|
+
export async function replayDlqEntry(ctx, id, opts = {}) {
|
|
81
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
82
|
+
let row;
|
|
83
|
+
try {
|
|
84
|
+
row = getDlqEntry(db, id);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeHippoDb(db);
|
|
88
|
+
}
|
|
89
|
+
if (!row) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
status: 'not_found',
|
|
93
|
+
memoryId: null,
|
|
94
|
+
retryCount: 0,
|
|
95
|
+
reason: `dlq id ${id} not found`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Signature verification (current secret, not the one in effect when DLQed).
|
|
99
|
+
if (!opts.force && opts.webhookSecret) {
|
|
100
|
+
if (!row.signature) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 'sig_missing',
|
|
104
|
+
memoryId: null,
|
|
105
|
+
retryCount: row.retryCount,
|
|
106
|
+
reason: 'legacy row without signature; pass --force to replay',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const sigOk = verifyGitHubSignature({
|
|
110
|
+
rawBody: row.rawPayload,
|
|
111
|
+
signature: row.signature,
|
|
112
|
+
webhookSecret: opts.webhookSecret,
|
|
113
|
+
previousSecret: opts.previousSecret,
|
|
114
|
+
});
|
|
115
|
+
if (!sigOk) {
|
|
116
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
status: 'sig_fail',
|
|
120
|
+
memoryId: null,
|
|
121
|
+
retryCount: row.retryCount + 1,
|
|
122
|
+
reason: 'signature did not verify against current GITHUB_WEBHOOK_SECRET; pass --force to replay anyway',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Parse + envelope guard.
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(row.rawPayload);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
status: 'parse_error',
|
|
136
|
+
memoryId: null,
|
|
137
|
+
retryCount: row.retryCount + 1,
|
|
138
|
+
reason: `still unparseable: ${e.message}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (!isGitHubWebhookEnvelope(parsed)) {
|
|
142
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
status: 'unhandled',
|
|
146
|
+
memoryId: null,
|
|
147
|
+
retryCount: row.retryCount + 1,
|
|
148
|
+
reason: 'not a GitHub webhook envelope',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Without an ingest hook this is a dry-run validation. Bump and report.
|
|
152
|
+
if (!opts.ingestHook) {
|
|
153
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
status: 'replayed',
|
|
157
|
+
memoryId: null,
|
|
158
|
+
retryCount: row.retryCount + 1,
|
|
159
|
+
reason: 'dry-run: no ingest hook supplied',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Real replay path. The route's IngestHook is responsible for routing,
|
|
163
|
+
// idempotency, and writing the memory. The DLQ module only validates the
|
|
164
|
+
// surface and bumps the retry counter.
|
|
165
|
+
const eventName = row.eventName ?? '';
|
|
166
|
+
const deliveryId = row.deliveryId ?? '';
|
|
167
|
+
const idempotencyKey = (await import('./signature.js')).computeIdempotencyKey(eventName, row.rawPayload);
|
|
168
|
+
const { memoryId } = await opts.ingestHook(ctx, {
|
|
169
|
+
rawPayload: row.rawPayload,
|
|
170
|
+
eventName,
|
|
171
|
+
idempotencyKey,
|
|
172
|
+
deliveryId,
|
|
173
|
+
});
|
|
174
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
status: 'replayed',
|
|
178
|
+
memoryId,
|
|
179
|
+
retryCount: row.retryCount + 1,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=dlq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dlq.js","sourceRoot":"","sources":["../../../src/connectors/github/dlq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from '../../db.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by ingest's afterWrite hook when a concurrent worker has already
|
|
4
|
+
* inserted github_event_log for the same idempotency_key. Roll back the
|
|
5
|
+
* SAVEPOINT so exactly one memory row exists per key.
|
|
6
|
+
*/
|
|
7
|
+
export declare class DuplicateIdempotencyError extends Error {
|
|
8
|
+
readonly idempotencyKey: string;
|
|
9
|
+
constructor(idempotencyKey: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function hasSeenKey(db: DatabaseSyncLike, idempotencyKey: string): boolean;
|
|
12
|
+
export declare function markKeySeen(db: DatabaseSyncLike, args: {
|
|
13
|
+
idempotencyKey: string;
|
|
14
|
+
deliveryId: string;
|
|
15
|
+
eventName: string;
|
|
16
|
+
memoryId: string | null;
|
|
17
|
+
}): void;
|
|
18
|
+
export declare function lookupMemoryByKey(db: DatabaseSyncLike, idempotencyKey: string): string | null;
|
|
19
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/connectors/github/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBACpB,cAAc,EAAE,MAAM;CAKnC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAGhF;AAED,wBAAgB,WAAW,CACzB,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC/F,IAAI,CAIN;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7F"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by ingest's afterWrite hook when a concurrent worker has already
|
|
3
|
+
* inserted github_event_log for the same idempotency_key. Roll back the
|
|
4
|
+
* SAVEPOINT so exactly one memory row exists per key.
|
|
5
|
+
*/
|
|
6
|
+
export class DuplicateIdempotencyError extends Error {
|
|
7
|
+
idempotencyKey;
|
|
8
|
+
constructor(idempotencyKey) {
|
|
9
|
+
super(`duplicate github idempotency_key: ${idempotencyKey}`);
|
|
10
|
+
this.name = 'DuplicateIdempotencyError';
|
|
11
|
+
this.idempotencyKey = idempotencyKey;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function hasSeenKey(db, idempotencyKey) {
|
|
15
|
+
const row = db.prepare(`SELECT 1 FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
|
|
16
|
+
return !!row;
|
|
17
|
+
}
|
|
18
|
+
export function markKeySeen(db, args) {
|
|
19
|
+
db.prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`).run(args.idempotencyKey, args.deliveryId, args.eventName, new Date().toISOString(), args.memoryId);
|
|
20
|
+
}
|
|
21
|
+
export function lookupMemoryByKey(db, idempotencyKey) {
|
|
22
|
+
const row = db.prepare(`SELECT memory_id FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
|
|
23
|
+
return row?.memory_id ?? null;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../../src/connectors/github/idempotency.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACzC,cAAc,CAAS;IAChC,YAAY,cAAsB;QAChC,KAAK,CAAC,qCAAqC,cAAc,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;CACF;AAED,MAAM,UAAU,UAAU,CAAC,EAAoB,EAAE,cAAsB;IACrE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACvG,OAAO,CAAC,CAAC,GAAG,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,EAAoB,EACpB,IAAgG;IAEhG,EAAE,CAAC,OAAO,CACR,kIAAkI,CACnI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;AACvG,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAoB,EAAE,cAAsB;IAC5E,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,kEAAkE,CAAC,CAAC,GAAG,CAAC,cAAc,CAEhG,CAAC;IACd,OAAO,GAAG,EAAE,SAAS,IAAI,IAAI,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub event ingest with afterWrite race-safe idempotency.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName +
|
|
5
|
+
* ':' + rawBody) (codex P0 #3) — derived from the signed body, not from the
|
|
6
|
+
* unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass
|
|
7
|
+
* idempotency by rotating the delivery UUID.
|
|
8
|
+
*
|
|
9
|
+
* Race semantics (codex P1 #6):
|
|
10
|
+
* - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case.
|
|
11
|
+
* - Slow path: hasSeenKey passes (no row yet). Two workers may race into
|
|
12
|
+
* remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR
|
|
13
|
+
* IGNORE on github_event_log either inserts (changes=1, commit) or
|
|
14
|
+
* collides (changes=0, throw DuplicateIdempotencyError -> SAVEPOINT
|
|
15
|
+
* rolls back this worker's memory row). Exactly one memory exists per
|
|
16
|
+
* idempotency_key.
|
|
17
|
+
*
|
|
18
|
+
* The Slack precedent's race test was insufficient — it tested the fast path,
|
|
19
|
+
* not the SAVEPOINT collision. The `__testInjectBeforeLog` hook below lets
|
|
20
|
+
* tests pre-populate github_event_log inside the SAVEPOINT to actually
|
|
21
|
+
* exercise the changes=0 -> rollback path.
|
|
22
|
+
*/
|
|
23
|
+
import { type Context } from '../../api.js';
|
|
24
|
+
import { type DatabaseSyncLike } from '../../db.js';
|
|
25
|
+
import type { GitHubIssueEvent, GitHubIssueCommentEvent, GitHubPullRequestEvent, GitHubPullRequestReviewCommentEvent } from './types.js';
|
|
26
|
+
export type IngestStatus = 'ingested' | 'duplicate' | 'skipped' | 'skipped_duplicate';
|
|
27
|
+
export interface IngestResult {
|
|
28
|
+
status: IngestStatus;
|
|
29
|
+
memoryId: string | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Discriminated union of the four event shapes V1 ingests. The eventName
|
|
33
|
+
* field MUST equal the X-GitHub-Event header value; computeIdempotencyKey
|
|
34
|
+
* folds it into the dedupe key so the same body posted under two different
|
|
35
|
+
* X-GitHub-Event headers produces two distinct keys (test 7).
|
|
36
|
+
*/
|
|
37
|
+
export type IngestEvent = {
|
|
38
|
+
eventName: 'issues';
|
|
39
|
+
payload: GitHubIssueEvent;
|
|
40
|
+
} | {
|
|
41
|
+
eventName: 'issue_comment';
|
|
42
|
+
payload: GitHubIssueCommentEvent;
|
|
43
|
+
} | {
|
|
44
|
+
eventName: 'pull_request';
|
|
45
|
+
payload: GitHubPullRequestEvent;
|
|
46
|
+
} | {
|
|
47
|
+
eventName: 'pull_request_review_comment';
|
|
48
|
+
payload: GitHubPullRequestReviewCommentEvent;
|
|
49
|
+
};
|
|
50
|
+
export interface IngestInput {
|
|
51
|
+
/** The X-GitHub-Event header value + parsed body, discriminated. */
|
|
52
|
+
event: IngestEvent;
|
|
53
|
+
/** The raw HTTP body — used for the idempotency key (replay-safe per codex P0 #3). */
|
|
54
|
+
rawBody: string;
|
|
55
|
+
/** X-GitHub-Delivery header value, audit metadata only. */
|
|
56
|
+
deliveryId: string;
|
|
57
|
+
/**
|
|
58
|
+
* Test-only hook fired inside the SAVEPOINT, AFTER the memory row is
|
|
59
|
+
* inserted but BEFORE the github_event_log INSERT OR IGNORE. Tests pass
|
|
60
|
+
* a function that pre-inserts the github_event_log row using the
|
|
61
|
+
* provided db handle to simulate a concurrent worker winning the race.
|
|
62
|
+
* Production callers leave this undefined.
|
|
63
|
+
*/
|
|
64
|
+
__testInjectBeforeLog?: (db: DatabaseSyncLike, idempotencyKey: string) => void;
|
|
65
|
+
}
|
|
66
|
+
export declare function ingestEvent(ctx: Context, input: IngestInput): IngestResult;
|
|
67
|
+
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../../src/connectors/github/ingest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAY,KAAK,OAAO,EAAqB,MAAM,cAAc,CAAC;AACzE,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAS/E,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,EACpC,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAEtF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,SAAS,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAClD;IAAE,SAAS,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,uBAAuB,CAAA;CAAE,GAChE;IAAE,SAAS,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAA;CAAE,GAC9D;IAAE,SAAS,EAAE,6BAA6B,CAAC;IAAC,OAAO,EAAE,mCAAmC,CAAA;CAAE,CAAC;AAE/F,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,KAAK,EAAE,WAAW,CAAC;IACnB,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,CAAC,EAAE,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF;AAgDD,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,YAAY,CAkF1E"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub event ingest with afterWrite race-safe idempotency.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName +
|
|
5
|
+
* ':' + rawBody) (codex P0 #3) — derived from the signed body, not from the
|
|
6
|
+
* unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass
|
|
7
|
+
* idempotency by rotating the delivery UUID.
|
|
8
|
+
*
|
|
9
|
+
* Race semantics (codex P1 #6):
|
|
10
|
+
* - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case.
|
|
11
|
+
* - Slow path: hasSeenKey passes (no row yet). Two workers may race into
|
|
12
|
+
* remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR
|
|
13
|
+
* IGNORE on github_event_log either inserts (changes=1, commit) or
|
|
14
|
+
* collides (changes=0, throw DuplicateIdempotencyError -> SAVEPOINT
|
|
15
|
+
* rolls back this worker's memory row). Exactly one memory exists per
|
|
16
|
+
* idempotency_key.
|
|
17
|
+
*
|
|
18
|
+
* The Slack precedent's race test was insufficient — it tested the fast path,
|
|
19
|
+
* not the SAVEPOINT collision. The `__testInjectBeforeLog` hook below lets
|
|
20
|
+
* tests pre-populate github_event_log inside the SAVEPOINT to actually
|
|
21
|
+
* exercise the changes=0 -> rollback path.
|
|
22
|
+
*/
|
|
23
|
+
import { remember } from '../../api.js';
|
|
24
|
+
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
25
|
+
import { hasSeenKey, lookupMemoryByKey, DuplicateIdempotencyError } from './idempotency.js';
|
|
26
|
+
import { computeIdempotencyKey } from './signature.js';
|
|
27
|
+
import { issueEventToRememberOpts, issueCommentEventToRememberOpts, pullRequestEventToRememberOpts, prReviewCommentEventToRememberOpts, } from './transform.js';
|
|
28
|
+
function transformEvent(event) {
|
|
29
|
+
switch (event.eventName) {
|
|
30
|
+
case 'issues':
|
|
31
|
+
return issueEventToRememberOpts(event.payload);
|
|
32
|
+
case 'issue_comment':
|
|
33
|
+
return issueCommentEventToRememberOpts(event.payload);
|
|
34
|
+
case 'pull_request':
|
|
35
|
+
return pullRequestEventToRememberOpts(event.payload);
|
|
36
|
+
case 'pull_request_review_comment':
|
|
37
|
+
return prReviewCommentEventToRememberOpts(event.payload);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* v1.3.1: extract the source-normalized identifier the idempotency key needs.
|
|
42
|
+
* Backfill and webhook both produce IngestEvent objects describing the same
|
|
43
|
+
* source revision, so deriving the key from these fields collapses both paths
|
|
44
|
+
* onto the same dedupe row. Mirrors the artifactRef strings in transform.ts.
|
|
45
|
+
*/
|
|
46
|
+
function eventArtifactRef(event) {
|
|
47
|
+
const repo = event.payload.repository?.full_name ?? 'unknown/unknown';
|
|
48
|
+
switch (event.eventName) {
|
|
49
|
+
case 'issues':
|
|
50
|
+
return `github://${repo}/issue/${event.payload.issue.number}`;
|
|
51
|
+
case 'issue_comment':
|
|
52
|
+
return `github://${repo}/issue/${event.payload.issue.number}/comment/${event.payload.comment.id}`;
|
|
53
|
+
case 'pull_request':
|
|
54
|
+
return `github://${repo}/pull/${event.payload.pull_request.number}`;
|
|
55
|
+
case 'pull_request_review_comment':
|
|
56
|
+
return `github://${repo}/pull/${event.payload.pull_request.number}/review_comment/${event.payload.comment.id}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function eventUpdatedAt(event) {
|
|
60
|
+
switch (event.eventName) {
|
|
61
|
+
case 'issues':
|
|
62
|
+
return event.payload.issue.updated_at ?? null;
|
|
63
|
+
case 'issue_comment':
|
|
64
|
+
return event.payload.comment.updated_at ?? null;
|
|
65
|
+
case 'pull_request':
|
|
66
|
+
return event.payload.pull_request.updated_at ?? null;
|
|
67
|
+
case 'pull_request_review_comment':
|
|
68
|
+
return event.payload.comment.updated_at ?? null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function ingestEvent(ctx, input) {
|
|
72
|
+
const idempotencyKey = computeIdempotencyKey(eventArtifactRef(input.event), eventUpdatedAt(input.event));
|
|
73
|
+
// Fast path: pre-check. Avoids running the transform / opening a write tx
|
|
74
|
+
// for the common already-seen case (GitHub auto-retries with the same body).
|
|
75
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
76
|
+
try {
|
|
77
|
+
if (hasSeenKey(db, idempotencyKey)) {
|
|
78
|
+
return { status: 'duplicate', memoryId: lookupMemoryByKey(db, idempotencyKey) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
closeHippoDb(db);
|
|
83
|
+
}
|
|
84
|
+
const opts = transformEvent(input.event);
|
|
85
|
+
if (!opts) {
|
|
86
|
+
// Empty body: no memory to write, but mark seen so a retry of the same
|
|
87
|
+
// empty event returns 'duplicate' (not 'skipped' again — that would
|
|
88
|
+
// re-run the transform on every retry).
|
|
89
|
+
const db2 = openHippoDb(ctx.hippoRoot);
|
|
90
|
+
try {
|
|
91
|
+
db2
|
|
92
|
+
.prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`)
|
|
93
|
+
.run(idempotencyKey, input.deliveryId, input.event.eventName, new Date().toISOString(), null);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
closeHippoDb(db2);
|
|
97
|
+
}
|
|
98
|
+
return { status: 'skipped', memoryId: null };
|
|
99
|
+
}
|
|
100
|
+
// Atomic write via afterWrite. Inside writeEntry's SAVEPOINT:
|
|
101
|
+
// 1. memories row INSERT lands.
|
|
102
|
+
// 2. (test-only) __testInjectBeforeLog can race-inject a colliding key.
|
|
103
|
+
// 3. INSERT OR IGNORE on github_event_log; if a concurrent worker (or
|
|
104
|
+
// the test injection) beat us, changes=0 -> throw -> SAVEPOINT rolls
|
|
105
|
+
// back our memory row. Other worker's commit stands.
|
|
106
|
+
try {
|
|
107
|
+
const result = remember({ ...ctx, actor: ctx.actor || 'connector:github' }, {
|
|
108
|
+
...opts,
|
|
109
|
+
afterWrite: (innerDb, memoryId) => {
|
|
110
|
+
if (input.__testInjectBeforeLog) {
|
|
111
|
+
input.__testInjectBeforeLog(innerDb, idempotencyKey);
|
|
112
|
+
}
|
|
113
|
+
const inserted = innerDb
|
|
114
|
+
.prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`)
|
|
115
|
+
.run(idempotencyKey, input.deliveryId, input.event.eventName, new Date().toISOString(), memoryId);
|
|
116
|
+
if (Number(inserted.changes ?? 0) === 0) {
|
|
117
|
+
throw new DuplicateIdempotencyError(idempotencyKey);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return { status: 'ingested', memoryId: result.id };
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof DuplicateIdempotencyError) {
|
|
125
|
+
// Other worker's row is committed. Return its memory_id so callers
|
|
126
|
+
// behave identically to the fast-path 'duplicate' branch.
|
|
127
|
+
const db3 = openHippoDb(ctx.hippoRoot);
|
|
128
|
+
try {
|
|
129
|
+
return { status: 'skipped_duplicate', memoryId: lookupMemoryByKey(db3, idempotencyKey) };
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
closeHippoDb(db3);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=ingest.js.map
|