hippo-memory 1.61.0 → 1.63.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 +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +62 -66
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/promote.js +55 -66
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +2 -2
- package/dist/capture/compact.js +11 -15
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +80 -76
- package/dist/cli/transfer.js +88 -107
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -2
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +103 -91
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +46 -39
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +96 -81
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +34 -56
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +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 +1 -1
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* - /repos/{repo}/issues/comments -> issue_comments_hwm
|
|
6
6
|
* - /repos/{repo}/pulls/comments -> pr_review_comments_hwm
|
|
7
7
|
*
|
|
8
|
-
* Crash safety
|
|
8
|
+
* Crash safety: each stream's HWM is persisted ONLY after
|
|
9
9
|
* the stream fully drains. If stream 2 throws mid-flight, stream 1's HWM
|
|
10
10
|
* is committed and stream 2's stays NULL (or its prior value). Rerun
|
|
11
11
|
* picks up from stream 1's saved HWM and re-fetches stream 2 from its
|
|
12
12
|
* last committed point.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* The /issues endpoint returns BOTH issues and PRs (a PR is
|
|
15
15
|
* an issue with a `pull_request` field). We skip PRs here so they don't
|
|
16
16
|
* get ingested under the issues schema. PRs are handled via webhook in
|
|
17
17
|
* V1 (no /pulls backfill stream — review comments cover the discussion
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* - /repos/{repo}/issues/comments -> issue_comments_hwm
|
|
6
6
|
* - /repos/{repo}/pulls/comments -> pr_review_comments_hwm
|
|
7
7
|
*
|
|
8
|
-
* Crash safety
|
|
8
|
+
* Crash safety: each stream's HWM is persisted ONLY after
|
|
9
9
|
* the stream fully drains. If stream 2 throws mid-flight, stream 1's HWM
|
|
10
10
|
* is committed and stream 2's stays NULL (or its prior value). Rerun
|
|
11
11
|
* picks up from stream 1's saved HWM and re-fetches stream 2 from its
|
|
12
12
|
* last committed point.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* The /issues endpoint returns BOTH issues and PRs (a PR is
|
|
15
15
|
* an issue with a `pull_request` field). We skip PRs here so they don't
|
|
16
16
|
* get ingested under the issues schema. PRs are handled via webhook in
|
|
17
17
|
* V1 (no /pulls backfill stream — review comments cover the discussion
|
|
@@ -110,18 +110,15 @@ function isCommentItem(x) {
|
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Drain one stream end-to-end. Pauses and retries on rate-limit. Throws on
|
|
113
|
-
* any other fetch error so the caller leaves the HWM unchanged
|
|
114
|
-
* codex P1 #3 crash safety).
|
|
113
|
+
* any other fetch error so the caller leaves the HWM unchanged.
|
|
115
114
|
*
|
|
116
|
-
* v1.3.1 (round 2 codex P1s + claude P1):
|
|
117
115
|
* - Tracks max(updated_at) across ALL fetched items, including ones
|
|
118
116
|
* `toIngestEvent` rejects (e.g., PRs returned via `/issues`). Otherwise
|
|
119
117
|
* a page of pure PRs would never advance the HWM and the next run would
|
|
120
118
|
* re-fetch the same window forever.
|
|
121
119
|
* - Returns `drained: boolean` — true only when the stream actually ran
|
|
122
|
-
* to next=null. Callers MUST NOT advance the HWM when drained=false
|
|
123
|
-
*
|
|
124
|
-
* maxUpdatedAt and the caller persisted it, skipping the unfetched tail.
|
|
120
|
+
* to next=null. Callers MUST NOT advance the HWM when drained=false,
|
|
121
|
+
* or a capped run would persist a partial HWM and skip the unfetched tail.
|
|
125
122
|
*/
|
|
126
123
|
async function drainStream(ctx, url0, toIngestEvent, fetcher, token, sleep, maxItems) {
|
|
127
124
|
let url = url0;
|
|
@@ -141,9 +138,7 @@ async function drainStream(ctx, url0, toIngestEvent, fetcher, token, sleep, maxI
|
|
|
141
138
|
}
|
|
142
139
|
pages++;
|
|
143
140
|
for (const item of page.items) {
|
|
144
|
-
//
|
|
145
|
-
// filter. Skipped PRs from /issues still contribute to the HWM so
|
|
146
|
-
// PR-only pages don't loop forever.
|
|
141
|
+
// Track updated_at on EVERY item, before the toIngestEvent filter, so PR-only pages don't loop forever.
|
|
147
142
|
// SAFETY: updated_at is GitHub's ISO-timestamp field on every item
|
|
148
143
|
// shape this stream returns; a non-string value would only fail the
|
|
149
144
|
// string comparisons below, matching pre-migration passthrough.
|
|
@@ -175,7 +170,7 @@ async function drainStream(ctx, url0, toIngestEvent, fetcher, token, sleep, maxI
|
|
|
175
170
|
function issueItemToEvent(item, repository) {
|
|
176
171
|
if (!isIssuesItem(item))
|
|
177
172
|
return null;
|
|
178
|
-
//
|
|
173
|
+
// /issues returns PRs too; skip them.
|
|
179
174
|
if (item.pull_request)
|
|
180
175
|
return null;
|
|
181
176
|
const payload = {
|
|
@@ -240,9 +235,7 @@ function prReviewCommentItemToEvent(item, repository) {
|
|
|
240
235
|
/** Drains one stream, then persists its HWM only when the stream ran to the end. */
|
|
241
236
|
async function backfillStream(ctx, opts, sleep, stream) {
|
|
242
237
|
const res = await drainStream(ctx, stream.url, stream.toIngestEvent, opts.fetcher, opts.token, sleep, opts.maxPerStream);
|
|
243
|
-
//
|
|
244
|
-
// (--max) must leave the HWM at its previous value so the next invocation
|
|
245
|
-
// re-fetches the unprocessed tail.
|
|
238
|
+
// A capped run (--max) must leave the HWM at its previous value so the next run re-fetches the unprocessed tail.
|
|
246
239
|
if (res.drained && res.maxUpdatedAt) {
|
|
247
240
|
writeOneHwm(ctx.hippoRoot, ctx.tenantId, opts.repoFullName, stream.column, res.maxUpdatedAt);
|
|
248
241
|
}
|
|
@@ -162,9 +162,7 @@ export async function cmdGithubDlqReplay(hippoRoot, args, flags) {
|
|
|
162
162
|
tenantId: resolveTenantId({}),
|
|
163
163
|
actor: adminActor('cli:github-dlq-replay'),
|
|
164
164
|
};
|
|
165
|
-
//
|
|
166
|
-
// dry-run that printed "replay ok" while only bumping retry_count. Wire the
|
|
167
|
-
// real hook so `replay` actually re-runs the ingest path.
|
|
165
|
+
// Without an ingestHook replay only bumps retry_count while printing "replay ok"; the real hook re-runs ingest.
|
|
168
166
|
const result = await replayDlqEntry(ctx, id, {
|
|
169
167
|
force,
|
|
170
168
|
webhookSecret: envGithubWebhookSecret(),
|
|
@@ -175,11 +173,8 @@ export async function cmdGithubDlqReplay(hippoRoot, args, flags) {
|
|
|
175
173
|
if (!event) {
|
|
176
174
|
return { memoryId: null };
|
|
177
175
|
}
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// handler, NOT to ingestEvent. The v1.3.1 hook unconditionally called
|
|
181
|
-
// ingestEvent and would have written the deleted comment as a NEW raw
|
|
182
|
-
// memory instead of archiving the matching ones.
|
|
176
|
+
// A replayed comment `.deleted` row must route to the deletion handler, NOT to ingestEvent,
|
|
177
|
+
// which would write the deleted comment as a NEW raw memory instead of archiving the matching ones.
|
|
183
178
|
if ((event.eventName === 'issue_comment' || event.eventName === 'pull_request_review_comment') &&
|
|
184
179
|
event.payload.action === 'deleted') {
|
|
185
180
|
const repo = event.payload.repository?.full_name ?? '';
|
|
@@ -19,21 +19,14 @@ export interface DeletionResult {
|
|
|
19
19
|
/**
|
|
20
20
|
* Handle GitHub `issue_comment.deleted` and `pull_request_review_comment.deleted`.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* Filter by tenant_id + kind='raw'. Multi-row archive:
|
|
23
23
|
* GitHub edits keep the same artifact_ref, so multiple active raw rows can
|
|
24
24
|
* match a single deletion event. Archive ALL of them.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* with archivedCount=0 — survivors stayed searchable, leaking private bodies.
|
|
31
|
-
*
|
|
32
|
-
* v1.3.1 fix: ONE shared DB handle wrapping ALL archives + the idempotency
|
|
33
|
-
* mark in a single outer SAVEPOINT. Any per-row failure rolls back the entire
|
|
34
|
-
* batch (including idempotency), so retry re-attempts cleanly. archiveRawMemory
|
|
35
|
-
* (the lower-level function from raw-archive.js) runs its own inner SAVEPOINT
|
|
36
|
-
* which nests safely inside the outer one.
|
|
26
|
+
* ONE shared DB handle wraps ALL archives + the idempotency mark in a single
|
|
27
|
+
* outer SAVEPOINT: a per-row failure rolls back the whole batch, idempotency
|
|
28
|
+
* included, so a retry re-attempts cleanly instead of leaving searchable
|
|
29
|
+
* survivors. archiveRawMemory's own inner SAVEPOINT nests safely inside it.
|
|
37
30
|
*
|
|
38
31
|
* Tenant scope and kind='raw' filtering are load-bearing: without them a
|
|
39
32
|
* deletion event from tenant A could archive tenant B's row sharing the same
|
|
@@ -4,21 +4,14 @@ import { hasSeenKey, markKeySeen } from './idempotency.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Handle GitHub `issue_comment.deleted` and `pull_request_review_comment.deleted`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Filter by tenant_id + kind='raw'. Multi-row archive:
|
|
8
8
|
* GitHub edits keep the same artifact_ref, so multiple active raw rows can
|
|
9
9
|
* match a single deletion event. Archive ALL of them.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
11
|
+
* ONE shared DB handle wraps ALL archives + the idempotency mark in a single
|
|
12
|
+
* outer SAVEPOINT: a per-row failure rolls back the whole batch, idempotency
|
|
13
|
+
* included, so a retry re-attempts cleanly instead of leaving searchable
|
|
14
|
+
* survivors. archiveRawMemory's own inner SAVEPOINT nests safely inside it.
|
|
22
15
|
*
|
|
23
16
|
* Tenant scope and kind='raw' filtering are load-bearing: without them a
|
|
24
17
|
* deletion event from tenant A could archive tenant B's row sharing the same
|
|
@@ -3,9 +3,8 @@ import type { Context } from '../../api.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* GitHub webhook DLQ. Mirrors the Slack DLQ shape (src/connectors/slack/dlq.ts)
|
|
5
5
|
* but carries GitHub-specific metadata: event_name, delivery_id, signature,
|
|
6
|
-
* installation_id, repo_full_name
|
|
7
|
-
*
|
|
8
|
-
* from the raw payload.
|
|
6
|
+
* installation_id, repo_full_name, so a `hippo gh dlq replay` operator can
|
|
7
|
+
* triage without re-deriving anything from the raw payload.
|
|
9
8
|
*
|
|
10
9
|
* Buckets:
|
|
11
10
|
* - parse_error — raw_payload was not valid JSON
|
|
@@ -56,7 +55,7 @@ export interface ReplayDlqOpts {
|
|
|
56
55
|
/** Current webhook secret. If omitted, signature check is skipped (force-only path). */
|
|
57
56
|
webhookSecret?: string;
|
|
58
57
|
/**
|
|
59
|
-
* Previous webhook secret during rotation
|
|
58
|
+
* Previous webhook secret during rotation.
|
|
60
59
|
* Operators rotating GITHUB_WEBHOOK_SECRET would otherwise be forced into
|
|
61
60
|
* --force on DLQ rows written under the old secret. Plumbed through to
|
|
62
61
|
* verifyGitHubSignature.previousSecret.
|
|
@@ -79,10 +78,8 @@ export interface ReplayResult {
|
|
|
79
78
|
* route already knows how to route an envelope, so it passes that capability
|
|
80
79
|
* back in.
|
|
81
80
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* itself, so the field was a phantom — any future hook that trusted the
|
|
85
|
-
* passed-in value would dedupe against a stale key. Field removed.
|
|
81
|
+
* No `idempotencyKey` field: ingest re-derives the key from the parsed event,
|
|
82
|
+
* so a hook trusting a passed-in key would dedupe against a stale one.
|
|
86
83
|
*/
|
|
87
84
|
export type IngestHook = (ctx: Context, args: {
|
|
88
85
|
rawPayload: string;
|
|
@@ -101,7 +98,7 @@ export type IngestHook = (ctx: Context, args: {
|
|
|
101
98
|
* 4. Type-guard the envelope. Fail → bump, `unhandled`.
|
|
102
99
|
* 5. If an `ingestHook` is supplied, call it and return its memoryId.
|
|
103
100
|
* If not (dry-run path), bump retry_count and return status `replayed`
|
|
104
|
-
* with memoryId=null. The webhook route wires the real hook
|
|
101
|
+
* with memoryId=null. The webhook route wires the real hook.
|
|
105
102
|
*
|
|
106
103
|
* Mirrors Slack's "always use current routing" policy: replays use the
|
|
107
104
|
* deployment state NOW, not at the time of original DLQing.
|
|
@@ -79,7 +79,7 @@ function bumpRetryCount(hippoRoot, id) {
|
|
|
79
79
|
* 4. Type-guard the envelope. Fail → bump, `unhandled`.
|
|
80
80
|
* 5. If an `ingestHook` is supplied, call it and return its memoryId.
|
|
81
81
|
* If not (dry-run path), bump retry_count and return status `replayed`
|
|
82
|
-
* with memoryId=null. The webhook route wires the real hook
|
|
82
|
+
* with memoryId=null. The webhook route wires the real hook.
|
|
83
83
|
*
|
|
84
84
|
* Mirrors Slack's "always use current routing" policy: replays use the
|
|
85
85
|
* deployment state NOW, not at the time of original DLQing.
|
|
@@ -125,8 +125,7 @@ export async function replayDlqEntry(ctx, id, opts = {}) {
|
|
|
125
125
|
// Real replay path. The route's IngestHook is responsible for routing,
|
|
126
126
|
// idempotency, and writing the memory. The DLQ module only validates the
|
|
127
127
|
// surface and bumps the retry counter.
|
|
128
|
-
//
|
|
129
|
-
// from the parsed event (artifact_ref + updated_at) since v1.3.1.
|
|
128
|
+
// No idempotencyKey arg: the hook re-derives it from the parsed event (artifact_ref + updated_at).
|
|
130
129
|
const eventName = row.eventName ?? '';
|
|
131
130
|
const deliveryId = row.deliveryId ?? '';
|
|
132
131
|
const { memoryId } = await opts.ingestHook(ctx, {
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* GitHub event ingest with afterWrite race-safe idempotency.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName +
|
|
5
|
-
* ':' + rawBody)
|
|
5
|
+
* ':' + rawBody), derived from the signed body, not from the
|
|
6
6
|
* unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass
|
|
7
7
|
* idempotency by rotating the delivery UUID.
|
|
8
8
|
*
|
|
9
|
-
* Race semantics
|
|
9
|
+
* Race semantics:
|
|
10
10
|
* - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case.
|
|
11
11
|
* - Slow path: hasSeenKey passes (no row yet). Two workers may race into
|
|
12
12
|
* remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
* rolls back this worker's memory row). Exactly one memory exists per
|
|
16
16
|
* idempotency_key.
|
|
17
17
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
* tests pre-populate github_event_log inside the SAVEPOINT to actually
|
|
21
|
-
* exercise the changes=0 -> rollback path.
|
|
18
|
+
* The `__testInjectBeforeLog` hook below lets tests pre-populate github_event_log
|
|
19
|
+
* inside the SAVEPOINT to exercise the changes=0 -> rollback path, not just the fast path.
|
|
22
20
|
*/
|
|
23
21
|
import { type Context } from '../../api.js';
|
|
24
22
|
import { type DatabaseSyncLike } from '../../db.js';
|
|
@@ -50,7 +48,7 @@ export type IngestEvent = {
|
|
|
50
48
|
export interface IngestInput {
|
|
51
49
|
/** The X-GitHub-Event header value + parsed body, discriminated. */
|
|
52
50
|
event: IngestEvent;
|
|
53
|
-
/** The raw HTTP body
|
|
51
|
+
/** The raw HTTP body, used for the idempotency key (replay-safe). */
|
|
54
52
|
rawBody: string;
|
|
55
53
|
/** X-GitHub-Delivery header value, audit metadata only. */
|
|
56
54
|
deliveryId: string;
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* GitHub event ingest with afterWrite race-safe idempotency.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName +
|
|
5
|
-
* ':' + rawBody)
|
|
5
|
+
* ':' + rawBody), derived from the signed body, not from the
|
|
6
6
|
* unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass
|
|
7
7
|
* idempotency by rotating the delivery UUID.
|
|
8
8
|
*
|
|
9
|
-
* Race semantics
|
|
9
|
+
* Race semantics:
|
|
10
10
|
* - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case.
|
|
11
11
|
* - Slow path: hasSeenKey passes (no row yet). Two workers may race into
|
|
12
12
|
* remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
* rolls back this worker's memory row). Exactly one memory exists per
|
|
16
16
|
* idempotency_key.
|
|
17
17
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
* tests pre-populate github_event_log inside the SAVEPOINT to actually
|
|
21
|
-
* exercise the changes=0 -> rollback path.
|
|
18
|
+
* The `__testInjectBeforeLog` hook below lets tests pre-populate github_event_log
|
|
19
|
+
* inside the SAVEPOINT to exercise the changes=0 -> rollback path, not just the fast path.
|
|
22
20
|
*/
|
|
23
21
|
import { remember } from '../../api.js';
|
|
24
22
|
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
@@ -39,7 +37,7 @@ function transformEvent(event) {
|
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
40
|
+
* Extract the source-normalized identifier the idempotency key needs.
|
|
43
41
|
* Backfill and webhook both produce IngestEvent objects describing the same
|
|
44
42
|
* source revision, so deriving the key from these fields collapses both paths
|
|
45
43
|
* onto the same dedupe row. Mirrors the artifactRef strings in transform.ts.
|
|
@@ -113,10 +111,8 @@ export function ingestEvent(ctx, input) {
|
|
|
113
111
|
}
|
|
114
112
|
}
|
|
115
113
|
if (e instanceof RejectedValueError) {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// seen exactly like the empty-body branch above so a GitHub retry of
|
|
119
|
-
// the same delivery acks as done, not error.
|
|
114
|
+
// A tombstone hit is a PERMANENT skip, never DLQ-retried: mark the key seen like the empty-body
|
|
115
|
+
// branch above so a GitHub retry of the same delivery acks as done, not error.
|
|
120
116
|
markKeySeenWithoutMemory(ctx.hippoRoot, idempotencyKey, input);
|
|
121
117
|
return { status: 'skipped', memoryId: null };
|
|
122
118
|
}
|
|
@@ -135,7 +131,7 @@ function markKeySeenWithoutMemory(hippoRoot, idempotencyKey, input) {
|
|
|
135
131
|
}
|
|
136
132
|
}
|
|
137
133
|
function rememberWithEventLog(ctx, input, idempotencyKey, opts) {
|
|
138
|
-
//
|
|
134
|
+
// No `|| 'connector:github'` fallback (see rememberWithEventLog in slack/ingest.ts for rationale).
|
|
139
135
|
const result = remember(ctx, {
|
|
140
136
|
...opts,
|
|
141
137
|
untrusted: true,
|
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
* Production uses `realGitHubFetcher` against `https://api.github.com`.
|
|
5
5
|
* Tests inject a fake `GitHubFetcher` so they never hit the network.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 401/403/404/500 into empty pages produced empty backfills with no
|
|
10
|
-
* operator signal, so this code path is now load-bearing.
|
|
7
|
+
* Any non-200 response that is NOT a recognized rate-limit pause MUST throw
|
|
8
|
+
* `GitHubFetchError`: an empty page would be an empty backfill with no operator signal.
|
|
11
9
|
*/
|
|
12
|
-
import type { JsonValue } from './types.js';
|
|
13
10
|
import { type RateLimitInfo } from './ratelimit.js';
|
|
11
|
+
import type { JsonValue } from '../../json.js';
|
|
14
12
|
export declare class GitHubFetchError extends Error {
|
|
15
13
|
readonly status: number;
|
|
16
14
|
readonly bodyExcerpt: string;
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* Production uses `realGitHubFetcher` against `https://api.github.com`.
|
|
5
5
|
* Tests inject a fake `GitHubFetcher` so they never hit the network.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 401/403/404/500 into empty pages produced empty backfills with no
|
|
10
|
-
* operator signal, so this code path is now load-bearing.
|
|
7
|
+
* Any non-200 response that is NOT a recognized rate-limit pause MUST throw
|
|
8
|
+
* `GitHubFetchError`: an empty page would be an empty backfill with no operator signal.
|
|
11
9
|
*/
|
|
12
10
|
import { parseRateLimit } from './ratelimit.js';
|
|
13
11
|
import { fetchWithRetry } from '../../http-retry.js';
|
|
@@ -44,6 +42,7 @@ function headersToRecord(h) {
|
|
|
44
42
|
return Object.fromEntries(entries);
|
|
45
43
|
}
|
|
46
44
|
const GITHUB_TIMEOUT_MS = 30_000;
|
|
45
|
+
const ERROR_BODY_SNIPPET_CHARS = 256;
|
|
47
46
|
export const realGitHubFetcher = async ({ url, token }) => {
|
|
48
47
|
const res = await fetchWithRetry(url, {
|
|
49
48
|
headers: {
|
|
@@ -54,10 +53,10 @@ export const realGitHubFetcher = async ({ url, token }) => {
|
|
|
54
53
|
}, { timeoutMs: GITHUB_TIMEOUT_MS });
|
|
55
54
|
const headers = headersToRecord(res.headers);
|
|
56
55
|
const rateLimit = parseRateLimit(headers, res.status);
|
|
57
|
-
//
|
|
56
|
+
// Don't silently turn 401/403/404/500 into empty pages.
|
|
58
57
|
if (res.status !== 200 && rateLimit.reason === 'none') {
|
|
59
58
|
const body = await res.text().catch(() => '');
|
|
60
|
-
throw new GitHubFetchError(res.status, body.slice(0,
|
|
59
|
+
throw new GitHubFetchError(res.status, body.slice(0, ERROR_BODY_SNIPPET_CHARS), url);
|
|
61
60
|
}
|
|
62
61
|
// SAFETY: GitHub's list/paginated endpoints (issues, comments, etc. — the
|
|
63
62
|
// only endpoints this backfill fetcher targets) return a JSON array body
|
|
@@ -8,24 +8,11 @@ export interface VerifyOpts {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function verifyGitHubSignature(opts: VerifyOpts): boolean;
|
|
10
10
|
/**
|
|
11
|
-
* Source-aware idempotency key
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Round 2 found that key produced different hashes for the SAME source event
|
|
18
|
-
* delivered via webhook vs via REST backfill, because backfill rawBody is the
|
|
19
|
-
* REST list-item shape while webhook rawBody is the envelope. Result:
|
|
20
|
-
* backfill + later webhook of the same issue created two `kind='raw'` rows
|
|
21
|
-
* with the same artifact_ref. Combined with the deletion bug, deletion could
|
|
22
|
-
* not archive both.
|
|
23
|
-
*
|
|
24
|
-
* v1.3.1 fix: key from the SOURCE-NORMALIZED identifier — artifact_ref plus
|
|
25
|
-
* the source-side updated_at timestamp. Same artifact + same revision = same
|
|
26
|
-
* key, regardless of which path delivered it. Different revisions of the same
|
|
27
|
-
* issue (an edit) get different keys, which is correct: each edit IS a new
|
|
28
|
-
* memory revision.
|
|
11
|
+
* Source-aware idempotency key: artifact_ref plus the source-side updated_at.
|
|
12
|
+
* A raw-body key would hash a webhook envelope and a REST backfill item of the
|
|
13
|
+
* SAME source event differently and ingest it twice. Same artifact + same
|
|
14
|
+
* revision = same key, whichever path delivered it; an edit gets a new key,
|
|
15
|
+
* which is correct because each edit IS a new memory revision.
|
|
29
16
|
*
|
|
30
17
|
* Both inputs are upstream-derived from the parsed event, not from the
|
|
31
18
|
* unsigned delivery header — replay attacks still cannot bypass dedupe.
|
|
@@ -36,30 +23,13 @@ export declare function verifyGitHubSignature(opts: VerifyOpts): boolean;
|
|
|
36
23
|
* - updatedAt: source-side ISO timestamp (issue.updated_at,
|
|
37
24
|
* comment.updated_at, pull_request.updated_at). Empty string when the
|
|
38
25
|
* payload omits it (rare; older REST shapes).
|
|
39
|
-
*
|
|
40
|
-
* Migration note for v1.3.0 → v1.3.1: existing github_event_log rows from
|
|
41
|
-
* v1.3.0 used the round-1 key shape and will not collide with v1.3.1 keys.
|
|
42
|
-
* The first webhook delivery after upgrading creates a new log row with the
|
|
43
|
-
* new key. This is acceptable for a hotfix (no production users on v1.3.0)
|
|
44
|
-
* and correct semantics going forward.
|
|
45
26
|
*/
|
|
46
27
|
export declare function computeIdempotencyKey(artifactRef: string, updatedAt: string | null | undefined): string;
|
|
47
28
|
/**
|
|
48
|
-
*
|
|
49
|
-
* computeIdempotencyKey so an ingest's row
|
|
50
|
-
* a deletion
|
|
51
|
-
*
|
|
52
|
-
* The codex round 3 P0 fix on server.ts was to call computeIdempotencyKey
|
|
53
|
-
* with the right (artifactRef, updatedAt) shape for deletions. That made
|
|
54
|
-
* the deletion key MATCH the ingest key — which collapsed to a "deletion
|
|
55
|
-
* always returns duplicate" bug because both shared github_event_log.
|
|
56
|
-
*
|
|
57
|
-
* v1.3.2 splits the namespace: deletion key = sha256('deleted:' +
|
|
58
|
-
* artifactRef + ':' + updatedAt). Two retries of the SAME deletion event
|
|
59
|
-
* still dedupe (same artifact + same updatedAt + same prefix → same key).
|
|
60
|
-
* Ingest of the same artifact + same updatedAt produces a DIFFERENT key,
|
|
61
|
-
* so a deletion event does not get short-circuited by the ingest's prior
|
|
62
|
-
* log row.
|
|
29
|
+
* Deletion-specific idempotency key: sha256('deleted:' + artifactRef + ':' +
|
|
30
|
+
* updatedAt). Distinct namespace from computeIdempotencyKey so an ingest's row
|
|
31
|
+
* in github_event_log doesn't make a deletion return 'duplicate' before it
|
|
32
|
+
* gets a chance to archive; retries of the SAME deletion still dedupe.
|
|
63
33
|
*
|
|
64
34
|
* Kept as a separate exported function so the namespace prefix is explicit
|
|
65
35
|
* at every call site (server.ts deletion branches, deletion.ts, DLQ replay).
|
|
@@ -17,24 +17,11 @@ export function verifyGitHubSignature(opts) {
|
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Source-aware idempotency key
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Round 2 found that key produced different hashes for the SAME source event
|
|
27
|
-
* delivered via webhook vs via REST backfill, because backfill rawBody is the
|
|
28
|
-
* REST list-item shape while webhook rawBody is the envelope. Result:
|
|
29
|
-
* backfill + later webhook of the same issue created two `kind='raw'` rows
|
|
30
|
-
* with the same artifact_ref. Combined with the deletion bug, deletion could
|
|
31
|
-
* not archive both.
|
|
32
|
-
*
|
|
33
|
-
* v1.3.1 fix: key from the SOURCE-NORMALIZED identifier — artifact_ref plus
|
|
34
|
-
* the source-side updated_at timestamp. Same artifact + same revision = same
|
|
35
|
-
* key, regardless of which path delivered it. Different revisions of the same
|
|
36
|
-
* issue (an edit) get different keys, which is correct: each edit IS a new
|
|
37
|
-
* memory revision.
|
|
20
|
+
* Source-aware idempotency key: artifact_ref plus the source-side updated_at.
|
|
21
|
+
* A raw-body key would hash a webhook envelope and a REST backfill item of the
|
|
22
|
+
* SAME source event differently and ingest it twice. Same artifact + same
|
|
23
|
+
* revision = same key, whichever path delivered it; an edit gets a new key,
|
|
24
|
+
* which is correct because each edit IS a new memory revision.
|
|
38
25
|
*
|
|
39
26
|
* Both inputs are upstream-derived from the parsed event, not from the
|
|
40
27
|
* unsigned delivery header — replay attacks still cannot bypass dedupe.
|
|
@@ -45,32 +32,15 @@ export function verifyGitHubSignature(opts) {
|
|
|
45
32
|
* - updatedAt: source-side ISO timestamp (issue.updated_at,
|
|
46
33
|
* comment.updated_at, pull_request.updated_at). Empty string when the
|
|
47
34
|
* payload omits it (rare; older REST shapes).
|
|
48
|
-
*
|
|
49
|
-
* Migration note for v1.3.0 → v1.3.1: existing github_event_log rows from
|
|
50
|
-
* v1.3.0 used the round-1 key shape and will not collide with v1.3.1 keys.
|
|
51
|
-
* The first webhook delivery after upgrading creates a new log row with the
|
|
52
|
-
* new key. This is acceptable for a hotfix (no production users on v1.3.0)
|
|
53
|
-
* and correct semantics going forward.
|
|
54
35
|
*/
|
|
55
36
|
export function computeIdempotencyKey(artifactRef, updatedAt) {
|
|
56
37
|
return createHash('sha256').update(`${artifactRef}:${updatedAt ?? ''}`).digest('hex');
|
|
57
38
|
}
|
|
58
39
|
/**
|
|
59
|
-
*
|
|
60
|
-
* computeIdempotencyKey so an ingest's row
|
|
61
|
-
* a deletion
|
|
62
|
-
*
|
|
63
|
-
* The codex round 3 P0 fix on server.ts was to call computeIdempotencyKey
|
|
64
|
-
* with the right (artifactRef, updatedAt) shape for deletions. That made
|
|
65
|
-
* the deletion key MATCH the ingest key — which collapsed to a "deletion
|
|
66
|
-
* always returns duplicate" bug because both shared github_event_log.
|
|
67
|
-
*
|
|
68
|
-
* v1.3.2 splits the namespace: deletion key = sha256('deleted:' +
|
|
69
|
-
* artifactRef + ':' + updatedAt). Two retries of the SAME deletion event
|
|
70
|
-
* still dedupe (same artifact + same updatedAt + same prefix → same key).
|
|
71
|
-
* Ingest of the same artifact + same updatedAt produces a DIFFERENT key,
|
|
72
|
-
* so a deletion event does not get short-circuited by the ingest's prior
|
|
73
|
-
* log row.
|
|
40
|
+
* Deletion-specific idempotency key: sha256('deleted:' + artifactRef + ':' +
|
|
41
|
+
* updatedAt). Distinct namespace from computeIdempotencyKey so an ingest's row
|
|
42
|
+
* in github_event_log doesn't make a deletion return 'duplicate' before it
|
|
43
|
+
* gets a chance to archive; retries of the SAME deletion still dedupe.
|
|
74
44
|
*
|
|
75
45
|
* Kept as a separate exported function so the namespace prefix is explicit
|
|
76
46
|
* at every call site (server.ts deletion branches, deletion.ts, DLQ replay).
|
|
@@ -20,7 +20,7 @@ export interface ResolveArgs {
|
|
|
20
20
|
* is non-empty (multi-tenant install with foreign installation)
|
|
21
21
|
* - `installationId` is missing AND either routing table is non-empty
|
|
22
22
|
* AND no `repository.full_name` match (PAT-mode webhook from a foreign
|
|
23
|
-
* account
|
|
23
|
+
* account)
|
|
24
24
|
*
|
|
25
25
|
* Escape hatch: `GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK=1` restores the
|
|
26
26
|
* env fallback for emergency rollback only. Mirrors the Slack equivalent
|
|
@@ -14,7 +14,7 @@ import { envGithubAllowUnknownInstallationFallback, envTenant } from '../../env.
|
|
|
14
14
|
* is non-empty (multi-tenant install with foreign installation)
|
|
15
15
|
* - `installationId` is missing AND either routing table is non-empty
|
|
16
16
|
* AND no `repository.full_name` match (PAT-mode webhook from a foreign
|
|
17
|
-
* account
|
|
17
|
+
* account)
|
|
18
18
|
*
|
|
19
19
|
* Escape hatch: `GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK=1` restores the
|
|
20
20
|
* env fallback for emergency rollback only. Mirrors the Slack equivalent
|
|
@@ -5,13 +5,13 @@ import { scopeFromRepository } from './scope.js';
|
|
|
5
5
|
* caller can mark the delivery seen for idempotency and skip the insert.
|
|
6
6
|
*
|
|
7
7
|
* Contract (mirrors src/connectors/slack/transform.ts):
|
|
8
|
-
* - kind is the literal 'raw' (
|
|
8
|
+
* - kind is the literal 'raw' (connector boundary, see src/importers.ts).
|
|
9
9
|
* - artifact_ref formats are stable; deletion paths look up by these strings.
|
|
10
10
|
* - issue: github://<owner/repo>/issue/<number>
|
|
11
11
|
* - issue_comment: github://<owner/repo>/issue/<number>/comment/<id>
|
|
12
12
|
* - pull_request: github://<owner/repo>/pull/<number>
|
|
13
13
|
* - pr_review_comment: github://<owner/repo>/pull/<number>/review_comment/<id>
|
|
14
|
-
* - owner is `user:github:<login>`. Required by the
|
|
14
|
+
* - owner is `user:github:<login>`. Required by the provenance gate.
|
|
15
15
|
* - scope is derived from repository.private via scopeFromRepository (default
|
|
16
16
|
* private when undetermined).
|
|
17
17
|
*/
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* header value so a malicious or misrouted payload cannot satisfy a guard
|
|
8
8
|
* for the wrong event type.
|
|
9
9
|
*/
|
|
10
|
+
import { type JsonValue } from '../../json.js';
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* `private` MUST be optional, not required. The Slack-style
|
|
12
13
|
* fail-safe in scope.ts requires an envelope with `private: undefined` to
|
|
13
14
|
* map to private; a strict boolean type would reject the payload before
|
|
14
15
|
* scope can fail closed.
|
|
@@ -79,15 +80,6 @@ export interface GitHubPullRequestReviewCommentEvent extends GitHubWebhookEnvelo
|
|
|
79
80
|
updated_at?: string;
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
|
-
/**
|
|
83
|
-
* The full value space `JSON.parse` can produce. Boundary-guard functions in
|
|
84
|
-
* this file accept `JsonValue` (never `unknown`) so a value's origin as
|
|
85
|
-
* unparsed external JSON stays visible in its type, then narrow it via the
|
|
86
|
-
* `isJson*` predicates below.
|
|
87
|
-
*/
|
|
88
|
-
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
89
|
-
[key: string]: JsonValue;
|
|
90
|
-
};
|
|
91
83
|
export declare function isGitHubWebhookEnvelope(x: JsonValue): x is JsonValue & GitHubWebhookEnvelope;
|
|
92
84
|
export declare function isGitHubIssueEvent(x: JsonValue, evtHeader: string): x is JsonValue & GitHubIssueEvent;
|
|
93
85
|
export declare function isGitHubIssueCommentEvent(x: JsonValue, evtHeader: string): x is JsonValue & GitHubIssueCommentEvent;
|
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
* header value so a malicious or misrouted payload cannot satisfy a guard
|
|
8
8
|
* for the wrong event type.
|
|
9
9
|
*/
|
|
10
|
+
import { isJsonString } from '../../json.js';
|
|
10
11
|
function isJsonObject(x) {
|
|
11
12
|
return x !== null && typeof x === 'object';
|
|
12
13
|
}
|
|
13
|
-
function isJsonString(x) {
|
|
14
|
-
return typeof x === 'string';
|
|
15
|
-
}
|
|
16
14
|
function isJsonNumber(x) {
|
|
17
15
|
return typeof x === 'number';
|
|
18
16
|
}
|
|
@@ -11,14 +11,9 @@ export interface DeletionResult {
|
|
|
11
11
|
memoryId: string | null;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Handle Slack `message_deleted`.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* hit a now-archived row and returned `not_found` instead of `duplicate`.
|
|
18
|
-
*
|
|
19
|
-
* Fix: pass `afterArchive` to `archiveRaw`, which runs inside the same
|
|
20
|
-
* SAVEPOINT as the archive itself. The slack_event_log row commits with the
|
|
21
|
-
* archive or not at all.
|
|
14
|
+
* Handle Slack `message_deleted`. `afterArchive` runs inside the archive's own
|
|
15
|
+
* SAVEPOINT, so the slack_event_log row commits with the archive or not at all
|
|
16
|
+
* and a crash cannot leave a retry hitting `not_found` instead of `duplicate`.
|
|
22
17
|
*/
|
|
23
18
|
export declare function handleMessageDeleted(ctx: Context, input: DeletionInput): DeletionResult;
|
|
24
19
|
//# sourceMappingURL=deletion.d.ts.map
|