@open-everywhere/provider-github 0.0.0-stage → 0.1.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.
Files changed (111) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +50 -2
  4. package/agent/channels/github-catch-up.ts +134 -0
  5. package/agent/channels/github-engagement.ts +381 -0
  6. package/agent/channels/github-identity.ts +376 -0
  7. package/agent/channels/github-installations.ts +152 -0
  8. package/agent/channels/github-message-ops.ts +62 -0
  9. package/agent/channels/github-origin-codec.ts +42 -0
  10. package/agent/channels/github-post-arm.ts +422 -0
  11. package/agent/channels/github-reaction-feedback.ts +489 -0
  12. package/agent/channels/github-reaction-scope.ts +312 -0
  13. package/agent/channels/github-read.ts +471 -0
  14. package/agent/channels/github-stop.ts +175 -0
  15. package/agent/channels/github-supervisor.ts +645 -0
  16. package/agent/channels/github-webhook-verifier.ts +56 -0
  17. package/agent/channels/github.ts +1458 -0
  18. package/agent/contributions.ts +31 -0
  19. package/agent/docs-generated.ts +15 -0
  20. package/agent/memory/memory.ts +265 -0
  21. package/agent/ops.ts +17 -0
  22. package/agent/provider-events.ts +114 -0
  23. package/agent/register.ts +130 -0
  24. package/agent/surface-parity.ts +116 -0
  25. package/dist/agent/channels/github-catch-up.d.ts +29 -0
  26. package/dist/agent/channels/github-catch-up.d.ts.map +1 -0
  27. package/dist/agent/channels/github-catch-up.js +105 -0
  28. package/dist/agent/channels/github-catch-up.js.map +1 -0
  29. package/dist/agent/channels/github-engagement.d.ts +223 -0
  30. package/dist/agent/channels/github-engagement.d.ts.map +1 -0
  31. package/dist/agent/channels/github-engagement.js +317 -0
  32. package/dist/agent/channels/github-engagement.js.map +1 -0
  33. package/dist/agent/channels/github-identity.d.ts +164 -0
  34. package/dist/agent/channels/github-identity.d.ts.map +1 -0
  35. package/dist/agent/channels/github-identity.js +295 -0
  36. package/dist/agent/channels/github-identity.js.map +1 -0
  37. package/dist/agent/channels/github-installations.d.ts +49 -0
  38. package/dist/agent/channels/github-installations.d.ts.map +1 -0
  39. package/dist/agent/channels/github-installations.js +114 -0
  40. package/dist/agent/channels/github-installations.js.map +1 -0
  41. package/dist/agent/channels/github-message-ops.d.ts +3 -0
  42. package/dist/agent/channels/github-message-ops.d.ts.map +1 -0
  43. package/dist/agent/channels/github-message-ops.js +58 -0
  44. package/dist/agent/channels/github-message-ops.js.map +1 -0
  45. package/dist/agent/channels/github-origin-codec.d.ts +7 -0
  46. package/dist/agent/channels/github-origin-codec.d.ts.map +1 -0
  47. package/dist/agent/channels/github-origin-codec.js +36 -0
  48. package/dist/agent/channels/github-origin-codec.js.map +1 -0
  49. package/dist/agent/channels/github-post-arm.d.ts +54 -0
  50. package/dist/agent/channels/github-post-arm.d.ts.map +1 -0
  51. package/dist/agent/channels/github-post-arm.js +339 -0
  52. package/dist/agent/channels/github-post-arm.js.map +1 -0
  53. package/dist/agent/channels/github-reaction-feedback.d.ts +97 -0
  54. package/dist/agent/channels/github-reaction-feedback.d.ts.map +1 -0
  55. package/dist/agent/channels/github-reaction-feedback.js +380 -0
  56. package/dist/agent/channels/github-reaction-feedback.js.map +1 -0
  57. package/dist/agent/channels/github-reaction-scope.d.ts +102 -0
  58. package/dist/agent/channels/github-reaction-scope.d.ts.map +1 -0
  59. package/dist/agent/channels/github-reaction-scope.js +296 -0
  60. package/dist/agent/channels/github-reaction-scope.js.map +1 -0
  61. package/dist/agent/channels/github-read.d.ts +135 -0
  62. package/dist/agent/channels/github-read.d.ts.map +1 -0
  63. package/dist/agent/channels/github-read.js +371 -0
  64. package/dist/agent/channels/github-read.js.map +1 -0
  65. package/dist/agent/channels/github-stop.d.ts +52 -0
  66. package/dist/agent/channels/github-stop.d.ts.map +1 -0
  67. package/dist/agent/channels/github-stop.js +145 -0
  68. package/dist/agent/channels/github-stop.js.map +1 -0
  69. package/dist/agent/channels/github-supervisor.d.ts +308 -0
  70. package/dist/agent/channels/github-supervisor.d.ts.map +1 -0
  71. package/dist/agent/channels/github-supervisor.js +339 -0
  72. package/dist/agent/channels/github-supervisor.js.map +1 -0
  73. package/dist/agent/channels/github-webhook-verifier.d.ts +5 -0
  74. package/dist/agent/channels/github-webhook-verifier.d.ts.map +1 -0
  75. package/dist/agent/channels/github-webhook-verifier.js +55 -0
  76. package/dist/agent/channels/github-webhook-verifier.js.map +1 -0
  77. package/dist/agent/channels/github.d.ts +139 -0
  78. package/dist/agent/channels/github.d.ts.map +1 -0
  79. package/dist/agent/channels/github.js +1232 -0
  80. package/dist/agent/channels/github.js.map +1 -0
  81. package/dist/agent/contributions.d.ts +12 -0
  82. package/dist/agent/contributions.d.ts.map +1 -0
  83. package/dist/agent/contributions.js +31 -0
  84. package/dist/agent/contributions.js.map +1 -0
  85. package/dist/agent/docs-generated.d.ts +3 -0
  86. package/dist/agent/docs-generated.d.ts.map +1 -0
  87. package/dist/agent/docs-generated.js +13 -0
  88. package/dist/agent/docs-generated.js.map +1 -0
  89. package/dist/agent/memory/memory.d.ts +54 -0
  90. package/dist/agent/memory/memory.d.ts.map +1 -0
  91. package/dist/agent/memory/memory.js +224 -0
  92. package/dist/agent/memory/memory.js.map +1 -0
  93. package/dist/agent/ops.d.ts +3 -0
  94. package/dist/agent/ops.d.ts.map +1 -0
  95. package/dist/agent/ops.js +16 -0
  96. package/dist/agent/ops.js.map +1 -0
  97. package/dist/agent/provider-events.d.ts +3 -0
  98. package/dist/agent/provider-events.d.ts.map +1 -0
  99. package/dist/agent/provider-events.js +112 -0
  100. package/dist/agent/provider-events.js.map +1 -0
  101. package/dist/agent/register.d.ts +4 -0
  102. package/dist/agent/register.d.ts.map +1 -0
  103. package/dist/agent/register.js +116 -0
  104. package/dist/agent/register.js.map +1 -0
  105. package/dist/agent/surface-parity.d.ts +3 -0
  106. package/dist/agent/surface-parity.d.ts.map +1 -0
  107. package/dist/agent/surface-parity.js +110 -0
  108. package/dist/agent/surface-parity.js.map +1 -0
  109. package/package.json +156 -4
  110. package/tsconfig.build.json +22 -0
  111. package/tsconfig.json +15 -0
@@ -0,0 +1,380 @@
1
+ // THE GITHUB REACTION-FEEDBACK ARM (OPE-1502) — a reaction on the agent's own comment becomes a
2
+ // durable feedback signal, the same way it already does on Slack and Discord.
3
+ //
4
+ // 🔴 GITHUB PUSHES NO REACTION WEBHOOK. Slack, Discord, Teams and Linear all DELIVER a reaction
5
+ // event; GitHub delivers none for an issue, a pull request or a comment. That is the same vendor
6
+ // fact the stop arm rests on (channels/github-stop.ts, which is a `stop` COMMENT for exactly this
7
+ // reason). So the inbound half here is a READ, not a delivery — and every design choice below falls
8
+ // out of that one difference.
9
+ //
10
+ // ── THE TRIGGER, AND WHAT IT COSTS
11
+ //
12
+ // The poll rides an inbound delivery this deployment already receives for the same conversation: an
13
+ // `issue_comment` or `issues` webhook names an issue, and the arm then reads the reactions on the
14
+ // agent's own comments in it. A reaction therefore reaches the seat on the NEXT delivery for that
15
+ // conversation, not the instant somebody clicks. That is a real latency, it is the honest ceiling of
16
+ // what GitHub offers, and it is what the bundle's parity declaration says out loud rather than
17
+ // claiming a parity this surface does not have.
18
+ //
19
+ // 🔴 TWO GATES RUN BEFORE THE FIRST REST CALL (OPE-1540), and channels/github-reaction-scope.ts owns
20
+ // both. The agent must be IN this conversation — a conversation it is not in can hold no reaction it
21
+ // could record — and the repository must have hourly budget left. Only then does the arm read
22
+ // anything. Before those gates every delivery paid the issue read and the comment page just to
23
+ // discover there was nothing to look at.
24
+ //
25
+ // 🔴 READING THE NEWEST COMMENTS TAKES AN EXTRA REQUEST, and getting this wrong is silent.
26
+ // `/issues/{n}/comments` returns OLDEST first and has no `direction` parameter — the measured vendor
27
+ // fact this bundle's read adapter is built on (channels/github-read.ts, its header). Asking page 1
28
+ // of a 250-comment thread would hand the poll the conversation's START, so the agent's recent
29
+ // replies would never be examined and the arm would report nothing while looking perfectly healthy.
30
+ // So the poll does what the read adapter does: it takes the issue's own `comments` COUNT, which
31
+ // names the last page directly, and reads that page.
32
+ //
33
+ // THE COST, per ADMITTED delivery — one the two gates above let through: one issue read for the
34
+ // count, one comment page (a second when that page is short and there is a page before it), plus one
35
+ // reactions read per agent comment the page already says carries a reaction — capped by
36
+ // {@link GITHUB_REACTION_POLL_COMMENT_BUDGET}. A conversation with no comments costs the issue read
37
+ // and nothing more. A REFUSED delivery costs no read at all.
38
+ //
39
+ // ── WHY ONLY `added`
40
+ //
41
+ // The reactions route returns the reactions that EXIST. It cannot report one that was taken away, so
42
+ // a poll can only ever see an add. The arm records `added` and never invents a `removed`: a
43
+ // retraction this surface cannot observe is better absent than guessed at.
44
+ //
45
+ // ── THE DEDUPE IS A KEY (OPE-1535)
46
+ //
47
+ // A poll re-reads reactions it has already recorded, so it must not write a second row for each. The
48
+ // arm therefore keeps a durable CLAIM per recorded reaction, keyed by the three fields that identify
49
+ // one: the comment, the reactor and the glyph ({@link githubReactionKey}). A poll asks the store
50
+ // about the exact reaction in front of it and skips the ones it already holds.
51
+ //
52
+ // 🔴 IT IS A `notice_claim`, AND THE LIFETIME IS THE WHOLE POINT. "This reaction was recorded" never
53
+ // stops being true, and a reaction stays visible to the poll for as long as its comment stays in the
54
+ // poll's view — which on a quiet conversation is forever. So the claim must never expire.
55
+ // `claimNotice`/`noticeClaimed` (operational/store.ts) is the tree's at-most-once claim with no TTL
56
+ // and no prune, which is exactly that shape. Growth is one row per recorded reaction — the same
57
+ // number of rows the audit log already holds for them, so the marginal cost is a copy of a thing the
58
+ // deployment already keeps.
59
+ //
60
+ // 🔴 WHY THE AUDIT LOG STOPPED BEING THE ANSWER. It was: the arm read back the newest reaction rows
61
+ // for the surface and skipped what it found. That read is a WINDOW, not a key — `readAudit` takes a
62
+ // LIMIT rather than a cursor, and the surface grain the rows carry is the REPOSITORY, so a repository
63
+ // with more recorded reactions than the window held would re-record one whose row had aged out as
64
+ // soon as a delivery brought the poll back to that conversation. The window's size decided
65
+ // correctness, which is the property a key does not have.
66
+ //
67
+ // 🔴 THE DEDUPE FAILS CLOSED, AND SAYS SO. An unreadable claim reads as ALREADY RECORDED, which
68
+ // bounds what the audit log claims happened — but a store that stays down then disables the feedback
69
+ // signal entirely, with zero rows and no error to look at. So a poll that could not read the claim
70
+ // store writes ONE `github.reaction_claim_unreadable` audit row naming how many reactions it skipped,
71
+ // the same way the budget gate audits its trip (channels/github-reaction-scope.ts).
72
+ //
73
+ // 🔴 THAT ROW ANSWERS A PARTIAL FAULT, NOT EVERY ONE. It is written through `appendAudit`, into the
74
+ // SAME operational store whose claim read just threw — so a whole-store outage loses the row as well,
75
+ // and shows up as the audit DROP COUNTER and its warn instead. The row is what makes the partial case
76
+ // legible: a missing `notice_claim` table, or a grant that covers `audit_log` and not it.
77
+ //
78
+ // 🔴 THE ORDER IS READ, RECORD, CLAIM — and it is chosen so nothing is ever LOST. The claim is
79
+ // written only after `ingestReaction` reports the row recorded, so a sink that fails leaves no claim
80
+ // and the next poll retries the reaction. Claiming first would make the dedupe a mutex across
81
+ // concurrent deliveries, but a failed write would then swallow the reaction permanently — the claim
82
+ // table has no release. Two cases stay open in the same benign direction, both of them a repeated
83
+ // audit row that over-reports the count and corrupts nothing: two deliveries polling the same
84
+ // conversation at the same instant, where both read "not recorded" before either writes; and a claim
85
+ // WRITE that fails after the row landed, which the next poll answers by recording the reaction again.
86
+ // The first was there before the key and is unchanged by it; the second is the price of the order.
87
+ //
88
+ // ── IDENTITY
89
+ //
90
+ // "The agent's own comment" is `<GITHUB_APP_SLUG>[bot]` — the login GitHub gives an App's comments,
91
+ // and the same self-identity channels/github-supervisor.ts drops its own replies by. 🔴 IT FAILS
92
+ // CLOSED: with no slug configured the arm polls nothing at all, because it cannot tell the agent's
93
+ // comments from anybody else's and reading every comment's reactions would record strangers' reactions
94
+ // to strangers' posts as feedback about the agent.
95
+ import { buildIngestedReaction, deploymentOrg, getOperationalStore, ingestReaction, liveReactionSink, resolvePartition, } from "@open-everywhere/core/agent/adapter-api.js";
96
+ import { buildGitHubAuth } from "./github-identity.js";
97
+ import { GITHUB_API_BASE, installationTokenFor } from "./github-post-arm.js";
98
+ import { getJson, liveGithubReadFetch } from "./github-read.js";
99
+ import { auditGitHubReactionClaimUnreadable, githubConversationInScope, spendGitHubPollBudget } from "./github-reaction-scope.js";
100
+ /** The webhook families that name a conversation this arm can poll. Both carry `repository` and an
101
+ * issue number; a PR arrives as an `issue_comment` too, because GitHub models a PR as an issue for
102
+ * timeline comments. */
103
+ const POLL_EVENT_TYPES = new Set(["issue_comment", "issues"]);
104
+ /** One poll examines at most this many of the agent's own comments, newest first. A conversation
105
+ * with more agent comments than this is one where the older ones stopped attracting reactions. */
106
+ export const GITHUB_REACTION_POLL_COMMENT_BUDGET = 10;
107
+ function isObject(v) {
108
+ return v !== null && typeof v === "object" && !Array.isArray(v);
109
+ }
110
+ function str(v) {
111
+ return typeof v === "string" && v.length > 0 ? v : undefined;
112
+ }
113
+ function num(v) {
114
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
115
+ }
116
+ function pick(o, k) {
117
+ return isObject(o) ? o[k] : undefined;
118
+ }
119
+ /**
120
+ * The conversation one delivery names, or null when the delivery names none. NEVER THROWS.
121
+ *
122
+ * It reads the SAME body the stop leg and the ambient leg read, and it accepts a delivery those two
123
+ * refuse — an `issues` event, or an `issue_comment` the agent itself wrote. The poll is about the
124
+ * CONVERSATION, so any accepted delivery naming one is a valid occasion to look.
125
+ */
126
+ export function parseGitHubReactionPollTarget(body, eventType) {
127
+ if (!POLL_EVENT_TYPES.has(eventType))
128
+ return null;
129
+ let ev;
130
+ try {
131
+ ev = JSON.parse(body);
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ if (!isObject(ev))
137
+ return null;
138
+ const repository = ev["repository"];
139
+ const owner = str(pick(pick(repository, "owner"), "login"));
140
+ const repo = str(pick(repository, "name"));
141
+ const number = num(pick(ev["issue"], "number")) ?? num(pick(ev["pull_request"], "number"));
142
+ if (!owner || !repo || number === undefined)
143
+ return null;
144
+ const installationId = num(pick(ev["installation"], "id"));
145
+ const repoPrivate = pick(repository, "private");
146
+ return {
147
+ owner,
148
+ repo,
149
+ number,
150
+ ...(installationId === undefined ? {} : { installationId }),
151
+ ...(typeof repoPrivate === "boolean" ? { repoPrivate } : {}),
152
+ };
153
+ }
154
+ /** Is this comment the agent's own? `user.type === "Bot"` AND the configured `<slug>[bot]` login —
155
+ * both, because either alone is wrong: every App's comment is type `Bot`, and a human account may
156
+ * legally be named to look like one. */
157
+ export function githubCommentIsOwn(comment, botLogin) {
158
+ const login = botLogin?.trim().toLowerCase();
159
+ if (!login)
160
+ return false;
161
+ if (comment.authorType?.toLowerCase() !== "bot")
162
+ return false;
163
+ return comment.authorLogin?.toLowerCase() === login;
164
+ }
165
+ /** The identity of one recorded reaction — a comment, a reactor and a glyph. The dedupe key, and the
166
+ * same three fields `liveReactionSink` writes into the audit row. */
167
+ export function githubReactionKey(messageId, reactorId, emoji) {
168
+ return [messageId, reactorId, emoji].join("\u001f");
169
+ }
170
+ /**
171
+ * Poll one conversation and record every reaction on the agent's own comments that is not recorded
172
+ * yet. Returns one outcome per NEW reaction — an empty array when there is nothing new, which is the
173
+ * ordinary case on a busy conversation.
174
+ *
175
+ * NEVER THROWS: a failed read or a failed sink must never cost the ingress its ack. A comment whose
176
+ * reactions cannot be read is skipped, and the rest of the poll continues.
177
+ */
178
+ export async function routeGitHubReactionFeedback(target, deps) {
179
+ // FAIL CLOSED on self-identity — see the header. Without it every reaction in the thread would
180
+ // read as feedback on the agent.
181
+ const botLogin = deps.botLogin?.trim().toLowerCase();
182
+ if (!botLogin)
183
+ return [];
184
+ // THE PARTITION IS RESOLVED FIRST, because the gates below are keyed by the same surface the rows
185
+ // are. It is PURE — no store, no network — so computing it ahead of a gate that may refuse costs
186
+ // nothing.
187
+ const partition = resolvePartition({
188
+ channel: { kind: "github" },
189
+ session: {
190
+ auth: {
191
+ current: buildGitHubAuth({
192
+ owner: target.owner,
193
+ repo: target.repo,
194
+ issueNumber: target.number,
195
+ // The POLL has no actor — it is the deployment looking, not a person acting. The reactor's
196
+ // own login rides each ingested reaction instead, which is where it belongs.
197
+ actorLogin: botLogin,
198
+ ...(target.repoPrivate === undefined ? {} : { isPrivate: target.repoPrivate }),
199
+ }),
200
+ },
201
+ },
202
+ });
203
+ const coords = { org: partition.org, workspace: partition.workspace, surfaceKey: partition.surfaceKey };
204
+ // ── THE TWO SCOPE GATES (OPE-1540) ─────────────────────────────────────────────────────────────
205
+ //
206
+ // 🔴 BOTH RUN BEFORE THE FIRST REST CALL, which is the whole of the fix: the cost this bundle used
207
+ // to pay was paid to LEARN there was nothing to do. Gate 1 is the cheap one and the one that cuts
208
+ // the most, so it runs first — and a conversation it refuses spends no budget either, because a
209
+ // poll that never happens is not a poll this repository ran.
210
+ //
211
+ // Both FAIL OPEN on a throw, matching their live implementations: a gate that cannot answer costs
212
+ // REST calls, never a lost feedback signal. The `.catch` is also what keeps this function's
213
+ // never-throws contract true for an injected dep.
214
+ const atIso = deps.now?.() ?? new Date().toISOString();
215
+ if (!(await deps.inScope(target, coords).catch(() => true)))
216
+ return [];
217
+ if (!(await deps.spendBudget(target, coords, atIso).catch(() => true)))
218
+ return [];
219
+ const comments = await deps.listComments(target).catch(() => []);
220
+ const own = comments
221
+ .filter((c) => githubCommentIsOwn(c, botLogin) && (c.reactionCount ?? 0) > 0)
222
+ .slice(0, deps.commentBudget ?? GITHUB_REACTION_POLL_COMMENT_BUDGET);
223
+ if (own.length === 0)
224
+ return [];
225
+ // The keys this poll has already answered for. It is a memo over `deps.recorded`, not the dedupe
226
+ // itself: the durable claim is what decides, and this only stops one poll asking the store twice
227
+ // about a reaction the vendor happened to list twice.
228
+ const asked = new Set();
229
+ // How many reactions this poll skipped because the claim store could not answer. It is a COUNT,
230
+ // not a flag, because the row written at the end reports it — see below.
231
+ let claimUnreadable = 0;
232
+ const outcomes = [];
233
+ for (const comment of own) {
234
+ const reactions = await deps.listReactions(target, comment.id).catch(() => []);
235
+ for (const reaction of reactions) {
236
+ const reactorId = reaction.userLogin;
237
+ // The agent's OWN reactions ride the same route — its `react` verb places them. They are never
238
+ // feedback, and unlike the comment filter this one needs no type check: the login is exact.
239
+ if (!reactorId || reactorId.toLowerCase() === botLogin)
240
+ continue;
241
+ const messageId = String(comment.id);
242
+ const key = githubReactionKey(messageId, reactorId, reaction.content);
243
+ if (asked.has(key))
244
+ continue;
245
+ asked.add(key);
246
+ // FAIL-CLOSED on an unreadable claim: an unknown answer is treated as ALREADY RECORDED, so a
247
+ // store blip costs a delayed feedback row rather than a duplicate one. The opposite posture
248
+ // from the two scope gates above, and deliberately: those bound a COST, this one bounds what
249
+ // the audit log claims happened.
250
+ //
251
+ // 🔴 BUT IT IS NEVER SILENT. Failing closed on a store that stays down disables the whole
252
+ // feedback signal with zero rows and no error, which reads exactly like a week nobody reacted
253
+ // in. The throw is counted here and audited ONCE at the end of the poll.
254
+ const claimed = await deps.recorded(coords, key).catch(() => {
255
+ claimUnreadable++;
256
+ return true;
257
+ });
258
+ if (claimed)
259
+ continue;
260
+ const ingested = buildIngestedReaction({
261
+ surface: "github",
262
+ // Only ever an ADD — see the header. A poll cannot observe a retraction.
263
+ verb: "added",
264
+ reactorId,
265
+ emoji: reaction.content,
266
+ // The CONVERSATION is the channel grain a GitHub turn is keyed by; the COMMENT is the
267
+ // message the reaction landed on.
268
+ target: { channelId: `${target.owner}/${target.repo}#${target.number}`, messageId },
269
+ // Structurally true: the loop only ever reaches the agent's own comments.
270
+ onBotReply: true,
271
+ partition,
272
+ });
273
+ const outcome = await ingestReaction(ingested, deps.sink).catch(() => ({ recorded: false, onBotReply: true, reason: "sink failed" }));
274
+ outcomes.push(outcome);
275
+ // CLAIM ONLY WHAT LANDED — see the header's read/record/claim order. A sink that failed leaves
276
+ // no claim, so the next poll for this conversation tries the reaction again.
277
+ if (outcome.recorded)
278
+ await deps.markRecorded(coords, key, atIso).catch(() => undefined);
279
+ }
280
+ }
281
+ // ONE ROW PER POLL for an unreadable claim store, and only when one read actually threw. The count
282
+ // is what makes a single row as informative as the flood it replaces. Best-effort and swallowed,
283
+ // like every other write on this path — the never-throws contract holds through it.
284
+ if (claimUnreadable > 0)
285
+ await deps.auditClaimUnreadable?.(target, coords, atIso, claimUnreadable).catch(() => undefined);
286
+ return outcomes;
287
+ }
288
+ /** The durable claim key one recorded reaction occupies, namespaced by the SURFACE — `notice_claim`
289
+ * is keyed by org alone, so two repositories in one deployment would otherwise share a key space and
290
+ * a comment id collision across repos would read as a duplicate. */
291
+ function githubReactionClaimKey(p, key) {
292
+ return `github-reaction:${p.surfaceKey}\u001f${key}`;
293
+ }
294
+ /** The live dedupe's read half: has this exact reaction already been recorded? */
295
+ async function liveGitHubReactionRecorded(p, key) {
296
+ return getOperationalStore().noticeClaimed(deploymentOrg(), githubReactionClaimKey(p, key));
297
+ }
298
+ /** The live dedupe's write half. Idempotent — a re-claim of a held key answers false, which nothing
299
+ * here reads: the question this store answers is "is it claimed", never "who claimed it". */
300
+ async function liveGitHubReactionMarkRecorded(p, key, atIso) {
301
+ await getOperationalStore().claimNotice(deploymentOrg(), githubReactionClaimKey(p, key), atIso);
302
+ }
303
+ // --- THE LIVE READS ------------------------------------------------------------------------------
304
+ //
305
+ // Every GET below runs under the SAME short-lived installation token every other GitHub call in this
306
+ // bundle mints, through the SAME `getJson` and the SAME transport the read adapter uses — so they
307
+ // inherit its 429 backoff and add no second transport (channels/github-read.ts, and
308
+ // `test:github-write-seam` §4). None of them reads a comment BODY: the poll needs an author, a count
309
+ // and a glyph, and nothing else.
310
+ /** GitHub's own page ceiling for the comment list. The read adapter pages by the same number. */
311
+ const GITHUB_COMMENT_PAGE = 100;
312
+ function tokenFor(target) {
313
+ return installationTokenFor({
314
+ platform: "github",
315
+ owner: target.owner,
316
+ repo: target.repo,
317
+ issueNumber: target.number,
318
+ ...(target.installationId === undefined ? {} : { installationId: target.installationId }),
319
+ });
320
+ }
321
+ /** Production deps for the poll. `botLogin` is `<GITHUB_APP_SLUG>[bot]` — absent when the slug is
322
+ * unset, which is exactly the fail-closed case {@link routeGitHubReactionFeedback} refuses on. */
323
+ export function liveGitHubReactionFeedbackDeps(appSlug, fetchFn = liveGithubReadFetch) {
324
+ const slug = appSlug?.trim();
325
+ return {
326
+ sink: liveReactionSink(),
327
+ ...(slug ? { botLogin: `${slug}[bot]` } : {}),
328
+ listComments: async (target) => {
329
+ const token = await tokenFor(target);
330
+ // 🔴 THE LAST PAGE, NOT THE FIRST. `/issues/{n}/comments` returns OLDEST first and honours no
331
+ // `direction` — see the header. The issue object's own `comments` count names the last page
332
+ // directly, which is the same two-request shape channels/github-read.ts uses and for the same
333
+ // reason. Reading page 1 here would examine the conversation's START and find nothing.
334
+ const issue = await getJson(fetchFn, `${GITHUB_API_BASE}/repos/${target.owner}/${target.repo}/issues/${target.number}`, token, "issues.get");
335
+ const total = typeof issue.comments === "number" && issue.comments > 0 ? issue.comments : 0;
336
+ if (total === 0)
337
+ return [];
338
+ const page = (nth) => `${GITHUB_API_BASE}/repos/${target.owner}/${target.repo}/issues/${target.number}/comments?per_page=${GITHUB_COMMENT_PAGE}&page=${nth}`;
339
+ const lastPage = Math.max(1, Math.ceil(total / GITHUB_COMMENT_PAGE));
340
+ let raw = await getJson(fetchFn, page(lastPage), token, "issues.comments.list");
341
+ if (!Array.isArray(raw))
342
+ return [];
343
+ // A short last page can hold fewer of the agent's comments than the budget wants, so the page
344
+ // before it is prepended — the same fallback the read adapter makes, and enough for any budget
345
+ // up to the page size.
346
+ if (raw.length < GITHUB_REACTION_POLL_COMMENT_BUDGET && lastPage > 1) {
347
+ const previous = await getJson(fetchFn, page(lastPage - 1), token, "issues.comments.list");
348
+ if (Array.isArray(previous))
349
+ raw = [...previous, ...raw];
350
+ }
351
+ return raw
352
+ .filter((c) => typeof c.id === "number")
353
+ // NEWEST FIRST, so the budget's slice takes the agent's most recent comments — the ones a
354
+ // person is plausibly still reacting to.
355
+ .reverse()
356
+ .map((c) => ({
357
+ id: c.id,
358
+ ...(c.user?.login ? { authorLogin: c.user.login } : {}),
359
+ ...(c.user?.type ? { authorType: c.user.type } : {}),
360
+ ...(typeof c.reactions?.total_count === "number" ? { reactionCount: c.reactions.total_count } : {}),
361
+ }));
362
+ },
363
+ listReactions: async (target, commentId) => {
364
+ const token = await tokenFor(target);
365
+ const raw = await getJson(fetchFn, `${GITHUB_API_BASE}/repos/${target.owner}/${target.repo}/issues/comments/${commentId}/reactions?per_page=100`, token, "reactions.listForIssueComment");
366
+ return raw
367
+ .filter((r) => typeof r.content === "string" && r.content.length > 0)
368
+ .map((r) => ({
369
+ content: r.content,
370
+ ...(r.user?.login ? { userLogin: r.user.login } : {}),
371
+ }));
372
+ },
373
+ recorded: (partition, key) => liveGitHubReactionRecorded(partition, key),
374
+ markRecorded: (partition, key, atIso) => liveGitHubReactionMarkRecorded(partition, key, atIso),
375
+ inScope: (target, partition) => githubConversationInScope(partition.surfaceKey, target.owner, target.repo, target.number),
376
+ spendBudget: async (target, _partition, atIso) => (await spendGitHubPollBudget(target.owner, target.repo, atIso)).admitted,
377
+ auditClaimUnreadable: (target, _partition, atIso, skipped) => auditGitHubReactionClaimUnreadable(target.owner, target.repo, atIso, skipped),
378
+ };
379
+ }
380
+ //# sourceMappingURL=github-reaction-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-reaction-feedback.js","sourceRoot":"","sources":["../../../agent/channels/github-reaction-feedback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /** How many reaction polls one repository may run in one clock hour. Sized so an ordinarily busy
2
+ * repository never reaches it — the gate is a ceiling on a runaway conversation, not a throttle on
3
+ * normal traffic. One ADMITTED poll costs up to 13 GETs (the issue read, one or two comment pages,
4
+ * and up to `GITHUB_REACTION_POLL_COMMENT_BUDGET` reaction reads), so this bounds a repository's
5
+ * worst hour to ~780 GETs on the installation token the reply path shares. The everyday cost is far
6
+ * below that: most polls read three. */
7
+ export declare const GITHUB_REACTION_POLL_HOURLY_BUDGET = 60;
8
+ /** The audit action a tripped budget writes. Queryable over the control plane's audit route, the
9
+ * same way `github.ambient_skipped` answers "why did the ambient leg stay quiet". */
10
+ export declare const GITHUB_REACTION_POLL_BUDGET_ACTION = "github.reaction_poll_budget";
11
+ /** The audit action an UNREADABLE CLAIM STORE writes, once per poll. The dedupe next door fails
12
+ * CLOSED, so a store that cannot answer "is this recorded" stops every feedback row for as long as
13
+ * it stays down — and a silent zero reads exactly like a quiet week. This row is the signal. It
14
+ * carries the same audit ACTION shape as the budget trip above, so an operator reads both off the
15
+ * control plane's audit route; the GRAIN differs, and {@link auditGitHubReactionClaimUnreadable}
16
+ * says how. */
17
+ export declare const GITHUB_REACTION_CLAIM_UNREADABLE_ACTION = "github.reaction_claim_unreadable";
18
+ /** How long a reply may wait on the best-effort post claim below.
19
+ *
20
+ * 🔴 THE CEILING IS THE POINT, NOT THE NUMBER. `liveGitHubPost` marks the conversation AFTER the
21
+ * comment has already landed, so this write can only ever delay the reply tool's RETURN — and with
22
+ * no ceiling a slow store held that return for as long as it liked, on the turn path. Two seconds is
23
+ * far more than a keyed claim needs and far less than a turn can notice. */
24
+ export declare const GITHUB_POSTED_MARK_TIMEOUT_MS = 2000;
25
+ /** The hourly budget's key for one repository. The clock hour rides IN the key, so the counter needs
26
+ * no reset and a stale bucket simply stops being read. */
27
+ export declare function githubPollBudgetKey(owner: string, repo: string, atIso: string): string;
28
+ /**
29
+ * Record that the agent has commented in this conversation. Best-effort and idempotent — a second
30
+ * post in the same conversation re-claims a key that is already claimed, which the store answers
31
+ * false to and this function ignores. NEVER THROWS: a lost mark costs a later poll, never a reply.
32
+ *
33
+ * 🔴 IT IS ALSO BOUNDED, WHICH IS THE OTHER HALF OF THAT CONTRACT. The one caller is
34
+ * `liveGitHubPost`, on the reply tool's own path, and it calls this AFTER the comment has landed. An
35
+ * unbounded await therefore could not lose a reply but could hold the tool's return for as long as
36
+ * the store took — a real hazard on a tree that already stalls in its post-turn closeout. So the
37
+ * write RACES {@link GITHUB_POSTED_MARK_TIMEOUT_MS}: a store slower than that is treated exactly
38
+ * like the store that throws, and costs a later poll rather than the turn.
39
+ *
40
+ * The abandoned write is not cancelled — nothing here can cancel a store call — so it carries its
41
+ * own settled handler and may still land after this returns. That is the harmless direction: the
42
+ * claim is idempotent and its only reader is the next poll.
43
+ */
44
+ export declare function markGitHubConversationPosted(owner: string | undefined, repo: string | undefined, number: number | undefined, atIso: string): Promise<void>;
45
+ /**
46
+ * Is the agent IN this conversation — has this bundle's reply path posted here, or did an @mention
47
+ * engage the container? Either answers yes; see the header for why both reads are needed.
48
+ *
49
+ * FAIL-OPEN on a store error, and on a delivery that names no container: an unanswerable question
50
+ * admits the poll, whose own parse then refuses a delivery that names nothing.
51
+ */
52
+ export declare function githubConversationInScope(surfaceKey: string, owner: string | undefined, repo: string | undefined, number: number | undefined): Promise<boolean>;
53
+ /** What one budget check decided, and what it counted. `polls` is the count INCLUDING this one when
54
+ * the check admitted it, so a caller can log the pressure without a second read. */
55
+ export interface GitHubPollBudgetVerdict {
56
+ readonly admitted: boolean;
57
+ readonly polls: number;
58
+ }
59
+ /**
60
+ * Spend one poll from this repository's hourly budget.
61
+ *
62
+ * Admits and counts while the hour has room; refuses once the budget is spent, and writes ONE
63
+ * {@link GITHUB_REACTION_POLL_BUDGET_ACTION} row for that hour rather than one per refused delivery —
64
+ * the refusals are the loud part of a busy hour, and an unthrottled row per delivery would be the
65
+ * same flood in the audit log that the budget exists to stop in GitHub's API.
66
+ *
67
+ * FAIL-OPEN on a store error: an unreadable counter admits the poll. A budget is a cost bound, so a
68
+ * store blip must never silence the feedback signal.
69
+ */
70
+ export declare function spendGitHubPollBudget(owner: string, repo: string, atIso: string, budget?: number): Promise<GitHubPollBudgetVerdict>;
71
+ /**
72
+ * Write the one row that says this poll's claim store could not be read.
73
+ *
74
+ * 🔴 WHY IT EXISTS. The reaction dedupe (channels/github-reaction-feedback.ts) fails CLOSED on an
75
+ * unreadable claim — an unknown answer counts as ALREADY RECORDED — which is the right posture for
76
+ * what the audit log claims happened, and a silent one. A store that stays unreadable therefore
77
+ * records nothing at all, forever, and the operator sees a feature that looks healthy and reports
78
+ * zero. This row is the difference between "nobody reacted" and "the store is down".
79
+ *
80
+ * ONE ROW PER POLL, never one per reaction: a busy comment page holds dozens of reactions and one
81
+ * broken store would write a row for each. `skipped` carries the count instead, so the row is as
82
+ * loud as the flood would have been. That is a DIFFERENT GRAIN from the budget trip below, which
83
+ * `markEventSeen` throttles to one row per repository per hour — this one has no throttle, because a
84
+ * poll that could not read the store is a distinct event and the count is the thing being reported.
85
+ *
86
+ * 🔴 WHAT BOUNDS THE VOLUME, HONESTLY. The poll reaches here only past gate 2, so an ordinary hour
87
+ * holds at most {@link GITHUB_REACTION_POLL_HOURLY_BUDGET} of these per repository — but only WHILE
88
+ * THE BUDGET COUNTER IS WRITABLE. Gate 2 fails open on a store error, so a fault that breaks the
89
+ * `task_result` write while `audit_log` inserts still land gives one row per delivery instead. That
90
+ * is the same trade the gate already made, and both directions of it are a cost, never a lost row.
91
+ *
92
+ * 🔴 AND IT IS NOT A SIGNAL FOR EVERY OUTAGE. `appendAudit` writes through the SAME operational
93
+ * store whose claim read just threw, so a whole-store outage loses this row too — best-effort, one
94
+ * retry, then a drop-counter tick and a `[open-everywhere:audit] dropped a …` warn (core
95
+ * agent/audit/audit.ts). This row is what makes a PARTIAL fault legible — a missing `notice_claim`
96
+ * table, a table-level grant — and the audit drop counter is what makes the total one legible. The
97
+ * operator doc states the same split.
98
+ *
99
+ * Best-effort, like every other write in this module: nothing here may cost the ingress its ack.
100
+ */
101
+ export declare function auditGitHubReactionClaimUnreadable(owner: string, repo: string, atIso: string, skipped: number): Promise<void>;
102
+ //# sourceMappingURL=github-reaction-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-reaction-scope.d.ts","sourceRoot":"","sources":["../../../agent/channels/github-reaction-scope.ts"],"names":[],"mappings":"AAmFA;;;;;yCAKyC;AACzC,eAAO,MAAM,kCAAkC,KAAK,CAAC;AAMrD;sFACsF;AACtF,eAAO,MAAM,kCAAkC,gCAAgC,CAAC;AAEhF;;;;;gBAKgB;AAChB,eAAO,MAAM,uCAAuC,qCAAqC,CAAC;AAE1F;;;;;4EAK4E;AAC5E,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAclD;2DAC2D;AAC3D,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BhK;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBrK;AAED;qFACqF;AACrF,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,SAAqC,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAsBrK;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,kCAAkC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnI"}