@holon-run/agentinbox 1.0.0-beta.1 → 1.0.0-beta.3

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/dist/src/cli.js CHANGED
@@ -871,7 +871,7 @@ async function printCurrentAgent(client) {
871
871
  const records = await listAgentsWithTargets(client);
872
872
  const current = (0, current_agent_1.resolveCurrentAgent)(records, context);
873
873
  if (!current) {
874
- throw new Error("no current agent is registered for this terminal/runtime context; run `agentinbox agent register`");
874
+ throw new Error(noCurrentAgentMessage());
875
875
  }
876
876
  console.log((0, util_1.jsonResponse)(current));
877
877
  }
@@ -907,7 +907,7 @@ async function selectAgentForCommand(client, options) {
907
907
  };
908
908
  }
909
909
  if (!options.autoRegister) {
910
- throw new Error("no current agent is registered for this terminal/runtime context; run `agentinbox agent register`");
910
+ throw new Error(noCurrentAgentMessage());
911
911
  }
912
912
  await requestRemote(client, "/agents", {
913
913
  backend: contextForCurrent.backend,
@@ -946,6 +946,9 @@ async function requestRemote(client, endpoint, body, method = "POST") {
946
946
  }
947
947
  return { data: response.data };
948
948
  }
949
+ function noCurrentAgentMessage() {
950
+ return "no current agent is registered for this terminal/runtime context; run `agentinbox agent list` to inspect offline agents, then `agentinbox agent register` or `agentinbox agent register --force-rebind --agent-id <agentId>` to rebind one";
951
+ }
949
952
  function takeFlagValue(args, flag) {
950
953
  const index = args.indexOf(flag);
951
954
  if (index === -1) {
@@ -39,6 +39,9 @@ function resolveCurrentAgent(agents, context) {
39
39
  matchesCurrentTerminal: matchesCurrentTerminal(agent, context),
40
40
  matchesCurrentRuntime: matchesCurrentRuntime(agent, context),
41
41
  terminalIdentity: terminalIdentityForAgent(agent),
42
+ notifyCapable: notifyCapableForAgent(agent),
43
+ activeTargetCount: activeActivationTargetCount(agent),
44
+ offlineTargetCount: offlineActivationTargetCount(agent),
42
45
  };
43
46
  }
44
47
  function annotateAgents(agents, context) {
@@ -52,24 +55,27 @@ function annotateAgents(agents, context) {
52
55
  matchesCurrentRuntime: context ? matchesCurrentRuntime(entry, context) : false,
53
56
  terminalIdentity: terminalIdentityForAgent(entry),
54
57
  isCurrent: current?.agentId === entry.agent.agentId,
58
+ notifyCapable: notifyCapableForAgent(entry),
59
+ activeTargetCount: activeActivationTargetCount(entry),
60
+ offlineTargetCount: offlineActivationTargetCount(entry),
55
61
  })),
56
62
  };
57
63
  }
58
64
  function resolveAgentRecord(agents, context) {
59
65
  if (context.tmuxPaneId) {
60
- const match = agents.find((agent) => activeTerminalTargets(agent).some((target) => target.backend === "tmux" && target.tmuxPaneId === context.tmuxPaneId));
66
+ const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.backend === "tmux" && target.tmuxPaneId === context.tmuxPaneId));
61
67
  if (match) {
62
68
  return match;
63
69
  }
64
70
  }
65
71
  if (context.itermSessionId) {
66
- const match = agents.find((agent) => activeTerminalTargets(agent).some((target) => target.backend === "iterm2" && target.itermSessionId === context.itermSessionId));
72
+ const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.backend === "iterm2" && target.itermSessionId === context.itermSessionId));
67
73
  if (match) {
68
74
  return match;
69
75
  }
70
76
  }
71
77
  if (context.tty) {
72
- const match = agents.find((agent) => activeTerminalTargets(agent).some((target) => target.tty === context.tty));
78
+ const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.tty === context.tty));
73
79
  if (match) {
74
80
  return match;
75
81
  }
@@ -94,6 +100,18 @@ function activeTerminalTargets(agent) {
94
100
  }
95
101
  return terminalTargets(agent).filter((target) => target.status === "active");
96
102
  }
103
+ function terminalTargetsForCurrentMatch(agent) {
104
+ return activeTerminalTargets(agent);
105
+ }
106
+ function activeActivationTargetCount(agent) {
107
+ return agent.activationTargets.filter((target) => target.status === "active").length;
108
+ }
109
+ function offlineActivationTargetCount(agent) {
110
+ return agent.activationTargets.filter((target) => target.status === "offline").length;
111
+ }
112
+ function notifyCapableForAgent(agent) {
113
+ return agent.agent.status === "active" && activeActivationTargetCount(agent) > 0;
114
+ }
97
115
  function terminalIdentityForAgent(agent) {
98
116
  const target = activeTerminalTargets(agent)[0];
99
117
  if (!target) {
@@ -111,7 +129,7 @@ function terminalIdentityForAgent(agent) {
111
129
  return null;
112
130
  }
113
131
  function matchesCurrentTerminal(agent, context) {
114
- return activeTerminalTargets(agent).some((target) => (context.tmuxPaneId != null && target.tmuxPaneId === context.tmuxPaneId)
132
+ return terminalTargetsForCurrentMatch(agent).some((target) => (context.tmuxPaneId != null && target.tmuxPaneId === context.tmuxPaneId)
115
133
  || (context.itermSessionId != null && target.itermSessionId === context.itermSessionId)
116
134
  || (context.tty != null && target.tty === context.tty));
117
135
  }
@@ -505,7 +505,7 @@ class AgentInboxService {
505
505
  return { removed: true };
506
506
  }
507
507
  registerTimer(input) {
508
- this.getAgent(input.agentId);
508
+ this.assertAgentNotifyCapable(input.agentId, "timer add");
509
509
  const normalized = normalizeTimerInput(input);
510
510
  const timezone = normalized.timezone ?? detectHostTimezone();
511
511
  assertValidTimeZone(timezone);
@@ -794,7 +794,7 @@ class AgentInboxService {
794
794
  return sibling;
795
795
  }
796
796
  async registerSingleSubscription(input) {
797
- this.getAgent(input.agentId);
797
+ this.assertAgentNotifyCapable(input.agentId, "subscription add");
798
798
  const source = this.store.getSource(input.sourceId);
799
799
  if (!source) {
800
800
  throw new Error(`unknown source: ${input.sourceId}`);
@@ -1595,6 +1595,19 @@ class AgentInboxService {
1595
1595
  watcher.onItems(items);
1596
1596
  }
1597
1597
  }
1598
+ assertAgentNotifyCapable(agentId, operation) {
1599
+ this.getAgent(agentId);
1600
+ const targets = this.store.listActivationTargetsForAgent(agentId);
1601
+ const activeTargetCount = targets.filter((target) => target.status === "active").length;
1602
+ if (activeTargetCount > 0) {
1603
+ return;
1604
+ }
1605
+ const offlineTargetCount = targets.filter((target) => target.status === "offline").length;
1606
+ const targetSummary = targets.length === 0
1607
+ ? "it has no activation targets"
1608
+ : `all ${offlineTargetCount} activation target(s) are offline`;
1609
+ throw new Error(`agent ${agentId} is not notify-capable: ${targetSummary}; refusing ${operation} to avoid a silent notification black hole. Rebind with \`agentinbox agent register --force-rebind --agent-id ${agentId}\` or resume/add a target first.`);
1610
+ }
1598
1611
  resolveAutoAgentId(input) {
1599
1612
  for (let attempt = 0; attempt < 32; attempt += 1) {
1600
1613
  const candidate = (0, terminal_1.assignedAgentIdFromContext)({
@@ -1784,6 +1797,11 @@ class AgentInboxService {
1784
1797
  buffer.inFlight = true;
1785
1798
  const entries = buffer.pending.splice(0, buffer.pending.length);
1786
1799
  try {
1800
+ if (!this.activationTargetExists(buffer.agentId, buffer.targetId)) {
1801
+ this.notificationBuffers.delete(key);
1802
+ buffer.inFlight = false;
1803
+ return;
1804
+ }
1787
1805
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1788
1806
  const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
1789
1807
  if (shouldAttemptDispatch) {
@@ -1921,7 +1939,11 @@ class AgentInboxService {
1921
1939
  if (!state) {
1922
1940
  return;
1923
1941
  }
1924
- const target = this.getActivationTarget(targetId);
1942
+ const target = this.getActivationTargetIfExists(agentId, targetId);
1943
+ if (!target) {
1944
+ this.store.deleteActivationDispatchState(agentId, targetId);
1945
+ return;
1946
+ }
1925
1947
  const inbox = this.ensureInboxForAgent(agentId);
1926
1948
  const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1927
1949
  if (unacked === 0) {
@@ -1988,7 +2010,10 @@ class AgentInboxService {
1988
2010
  }
1989
2011
  }
1990
2012
  async dispatchActivationTarget(input) {
1991
- const target = this.getActivationTarget(input.targetId);
2013
+ const target = this.getActivationTargetIfExists(input.agentId, input.targetId);
2014
+ if (!target) {
2015
+ return "offline";
2016
+ }
1992
2017
  if (target.status === "offline") {
1993
2018
  return "offline";
1994
2019
  }
@@ -2029,6 +2054,9 @@ class AgentInboxService {
2029
2054
  try {
2030
2055
  if (target.kind === "terminal") {
2031
2056
  const gate = await this.activationGate.evaluate(target);
2057
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2058
+ return "offline";
2059
+ }
2032
2060
  this.logger.debug("activation.gate_decision", {
2033
2061
  agentId: input.agentId,
2034
2062
  targetId: target.targetId,
@@ -2049,6 +2077,9 @@ class AgentInboxService {
2049
2077
  return "offline";
2050
2078
  }
2051
2079
  const reminder = await this.buildTerminalReminder(target, inbox.inboxId, totalUnackedCount, input.summary, input.entries);
2080
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2081
+ return "offline";
2082
+ }
2052
2083
  const prompt = reminder.prompt;
2053
2084
  const promptFingerprint = reminder.fingerprint;
2054
2085
  terminalPrompt = prompt;
@@ -2134,6 +2165,9 @@ class AgentInboxService {
2134
2165
  subscriptionIds: input.subscriptionIds,
2135
2166
  });
2136
2167
  await this.terminalDispatcher.dispatch(target, prompt);
2168
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2169
+ return "offline";
2170
+ }
2137
2171
  this.logger.debug("activation.terminal_dispatch_succeeded", {
2138
2172
  agentId: input.agentId,
2139
2173
  targetId: target.targetId,
@@ -2162,6 +2196,9 @@ class AgentInboxService {
2162
2196
  deliveredAt: null,
2163
2197
  };
2164
2198
  await this.activationDispatcher.dispatch(target.url, activation);
2199
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2200
+ return "offline";
2201
+ }
2165
2202
  this.store.insertActivation(activation);
2166
2203
  }
2167
2204
  this.markActivationTargetDelivered(target.targetId);
@@ -2196,8 +2233,14 @@ class AgentInboxService {
2196
2233
  error,
2197
2234
  message,
2198
2235
  });
2236
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2237
+ return "offline";
2238
+ }
2199
2239
  if (target.kind === "terminal") {
2200
2240
  const exists = await this.terminalDispatcher.probe(target);
2241
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2242
+ return "offline";
2243
+ }
2201
2244
  this.logger.debug("activation.terminal_probe_after_failure", {
2202
2245
  agentId: input.agentId,
2203
2246
  targetId: target.targetId,
@@ -2219,6 +2262,19 @@ class AgentInboxService {
2219
2262
  return "retryable_failure";
2220
2263
  }
2221
2264
  }
2265
+ getActivationTargetIfExists(agentId, targetId) {
2266
+ if (!this.store.getAgent(agentId)) {
2267
+ return null;
2268
+ }
2269
+ const target = this.store.getActivationTarget(targetId);
2270
+ if (!target || target.agentId !== agentId) {
2271
+ return null;
2272
+ }
2273
+ return target;
2274
+ }
2275
+ activationTargetExists(agentId, targetId) {
2276
+ return this.getActivationTargetIfExists(agentId, targetId) != null;
2277
+ }
2222
2278
  upsertDirtyDispatchState(agentId, targetId, entries) {
2223
2279
  this.store.upsertActivationDispatchState({
2224
2280
  agentId,
@@ -89,6 +89,7 @@ const SOURCE_SCHEMAS = {
89
89
  { name: "event", type: "string|null", description: "GitHub trigger event for the workflow run." },
90
90
  { name: "headBranch", type: "string|null", description: "Head branch for the workflow run." },
91
91
  { name: "headSha", type: "string|null", description: "Head commit SHA for the workflow run." },
92
+ { name: "pullRequestNumbers", type: "number[]", description: "Pull request numbers associated with the workflow run." },
92
93
  { name: "actor", type: "string|null", description: "Actor login for the workflow run." },
93
94
  { name: "commitMessage", type: "string|null", description: "Head commit message when present." },
94
95
  { name: "htmlUrl", type: "string|null", description: "GitHub Actions run URL." },
@@ -101,6 +102,7 @@ const SOURCE_SCHEMAS = {
101
102
  conclusion: "failure",
102
103
  event: "pull_request",
103
104
  head_branch: "main",
105
+ pull_requests: [{ number: 93 }],
104
106
  actor: "jolestar",
105
107
  head_commit: { message: "fix ci" },
106
108
  },
@@ -4,6 +4,7 @@ exports.GithubDeliveryAdapter = exports.GithubUxcClient = exports.DEFAULT_GITHUB
4
4
  exports.githubDeliveryOperationsForHandle = githubDeliveryOperationsForHandle;
5
5
  exports.invokeGithubDeliveryOperation = invokeGithubDeliveryOperation;
6
6
  exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
7
+ exports.hydrateGithubRepoEventIfNeeded = hydrateGithubRepoEventIfNeeded;
7
8
  exports.canonicalGithubPullRequestRef = canonicalGithubPullRequestRef;
8
9
  exports.deriveGithubTrackedResource = deriveGithubTrackedResource;
9
10
  exports.githubSubscriptionShortcutSpec = githubSubscriptionShortcutSpec;
@@ -12,6 +13,14 @@ exports.projectGithubLifecycleSignal = projectGithubLifecycleSignal;
12
13
  exports.parseGithubSourceConfig = parseGithubSourceConfig;
13
14
  const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
14
15
  exports.GITHUB_ENDPOINT = "https://api.github.com";
16
+ const DEFAULT_GITHUB_CALL_CLIENT = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env });
17
+ const GITHUB_EVENT_HYDRATION_MIN_PER_PAGE = 10;
18
+ const GITHUB_EVENT_HYDRATION_MAX_PER_PAGE = 30;
19
+ const GITHUB_EVENT_HYDRATION_MAX_PAGES = 3;
20
+ const HYDRATABLE_GITHUB_REPO_EVENT_TYPES = new Set([
21
+ "PullRequestReviewEvent",
22
+ "PullRequestReviewCommentEvent",
23
+ ]);
15
24
  exports.DEFAULT_GITHUB_EVENT_TYPES = [
16
25
  "IssuesEvent",
17
26
  "IssueCommentEvent",
@@ -21,7 +30,7 @@ exports.DEFAULT_GITHUB_EVENT_TYPES = [
21
30
  ];
22
31
  class GithubUxcClient {
23
32
  client;
24
- constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
33
+ constructor(client = DEFAULT_GITHUB_CALL_CLIENT) {
25
34
  this.client = client;
26
35
  }
27
36
  async createIssueComment(input) {
@@ -222,7 +231,7 @@ function normalizeGithubRepoEvent(source, config, raw) {
222
231
  if (!eventType || !(config.eventTypes ?? exports.DEFAULT_GITHUB_EVENT_TYPES).includes(eventType)) {
223
232
  return null;
224
233
  }
225
- const eventId = asString(event.id);
234
+ const eventId = asIdString(event.id);
226
235
  if (!eventId) {
227
236
  return null;
228
237
  }
@@ -234,18 +243,17 @@ function normalizeGithubRepoEvent(source, config, raw) {
234
243
  const review = asRecord(payload.review);
235
244
  const comment = asRecord(payload.comment);
236
245
  const action = asString(payload.action) ?? "observed";
237
- const number = asNumber(issue.number) ?? asNumber(pullRequest.number);
246
+ const url = extractGithubRepoEventUrl(event, issue, pullRequest, review, comment);
247
+ const number = asNumber(issue.number)
248
+ ?? asNumber(pullRequest.number)
249
+ ?? asNumber(payload.number)
250
+ ?? extractGithubNumberFromUrl(url);
238
251
  const isPullRequest = eventType.startsWith("PullRequest");
239
252
  const title = asString(issue.title) ?? asString(pullRequest.title) ?? null;
240
253
  const reviewState = asString(review.state) ?? null;
241
254
  const body = asString(comment.body) ?? asString(review.body) ?? asString(issue.body) ?? asString(pullRequest.body) ?? null;
242
255
  const labels = extractLabels(issue.labels);
243
256
  const mentions = extractMentions(title, body);
244
- const url = asString(comment.html_url) ??
245
- asString(review.html_url) ??
246
- asString(issue.html_url) ??
247
- asString(pullRequest.html_url) ??
248
- asString(event.url);
249
257
  const deliveryHandle = buildGithubDeliveryHandle(config, eventType, number, comment);
250
258
  return {
251
259
  sourceId: source.sourceId,
@@ -283,6 +291,47 @@ function normalizeGithubRepoEvent(source, config, raw) {
283
291
  deliveryHandle,
284
292
  };
285
293
  }
294
+ async function hydrateGithubRepoEventIfNeeded(config, raw, client = DEFAULT_GITHUB_CALL_CLIENT) {
295
+ if (!shouldHydrateGithubRepoEvent(raw)) {
296
+ return raw;
297
+ }
298
+ const eventId = asIdString(raw.id);
299
+ if (!eventId) {
300
+ return raw;
301
+ }
302
+ const perPage = computeGithubHydrationPerPage(config.perPage);
303
+ for (let page = 1; page <= GITHUB_EVENT_HYDRATION_MAX_PAGES; page += 1) {
304
+ let response;
305
+ try {
306
+ response = await client.call({
307
+ endpoint: exports.GITHUB_ENDPOINT,
308
+ operation: "get:/repos/{owner}/{repo}/events",
309
+ payload: {
310
+ owner: config.owner,
311
+ repo: config.repo,
312
+ per_page: perPage,
313
+ page,
314
+ },
315
+ options: { auth: config.uxcAuth },
316
+ });
317
+ }
318
+ catch {
319
+ return raw;
320
+ }
321
+ const candidates = Array.isArray(response.data) ? response.data : [];
322
+ if (candidates.length === 0) {
323
+ break;
324
+ }
325
+ for (const candidateValue of candidates) {
326
+ const candidate = asRecord(candidateValue);
327
+ if (asIdString(candidate.id) !== eventId) {
328
+ continue;
329
+ }
330
+ return hasGithubRepoEventMetadata(candidate) ? candidate : raw;
331
+ }
332
+ }
333
+ return raw;
334
+ }
286
335
  function canonicalGithubPullRequestRef(source, number) {
287
336
  const config = parseGithubSourceConfig(source);
288
337
  return `repo:${config.owner}/${config.repo}:pr:${number}`;
@@ -333,6 +382,11 @@ function expandGithubSubscriptionShortcut(input) {
333
382
  ...(withCi
334
383
  ? [{
335
384
  streamKind: "ci_runs",
385
+ filter: {
386
+ metadata: {
387
+ pullRequestNumbers: [number],
388
+ },
389
+ },
336
390
  trackedResourceRef,
337
391
  cleanupPolicy: { mode: "on_terminal" },
338
392
  }]
@@ -433,6 +487,64 @@ function extractMentions(...values) {
433
487
  }
434
488
  return Array.from(seen.values()).sort();
435
489
  }
490
+ function computeGithubHydrationPerPage(perPage) {
491
+ const configured = Number.isInteger(perPage) && perPage && perPage > 0
492
+ ? perPage
493
+ : GITHUB_EVENT_HYDRATION_MIN_PER_PAGE;
494
+ return Math.min(GITHUB_EVENT_HYDRATION_MAX_PER_PAGE, Math.max(GITHUB_EVENT_HYDRATION_MIN_PER_PAGE, configured));
495
+ }
496
+ function extractGithubRepoEventUrl(event, issue, pullRequest, review, comment) {
497
+ return (asString(comment.html_url)
498
+ ?? asString(review.html_url)
499
+ ?? asString(issue.html_url)
500
+ ?? asString(pullRequest.html_url)
501
+ ?? asString(comment.pull_request_url)
502
+ ?? asString(review.pull_request_url)
503
+ ?? asString(event.url));
504
+ }
505
+ function extractGithubNumberFromUrl(url) {
506
+ if (!url) {
507
+ return null;
508
+ }
509
+ const match = url.match(/\/(?:pull|pulls|issues)\/(\d+)(?:$|[/?#])/);
510
+ return match ? Number(match[1]) : null;
511
+ }
512
+ function shouldHydrateGithubRepoEvent(raw) {
513
+ const eventType = asString(raw.type);
514
+ if (!eventType || !HYDRATABLE_GITHUB_REPO_EVENT_TYPES.has(eventType)) {
515
+ return false;
516
+ }
517
+ const payload = asRecord(raw.payload);
518
+ const issue = asRecord(payload.issue);
519
+ const pullRequest = asRecord(payload.pull_request);
520
+ const review = asRecord(payload.review);
521
+ const comment = asRecord(payload.comment);
522
+ const url = extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment);
523
+ const number = asNumber(issue.number)
524
+ ?? asNumber(pullRequest.number)
525
+ ?? asNumber(payload.number)
526
+ ?? extractGithubNumberFromUrl(url);
527
+ if (number) {
528
+ return false;
529
+ }
530
+ return [pullRequest, review, comment].some(isUxcPreviewTruncatedObject);
531
+ }
532
+ function hasGithubRepoEventMetadata(raw) {
533
+ const payload = asRecord(raw.payload);
534
+ const issue = asRecord(payload.issue);
535
+ const pullRequest = asRecord(payload.pull_request);
536
+ const review = asRecord(payload.review);
537
+ const comment = asRecord(payload.comment);
538
+ return Boolean(asNumber(issue.number)
539
+ ?? asNumber(pullRequest.number)
540
+ ?? asNumber(payload.number)
541
+ ?? extractGithubNumberFromUrl(extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment))
542
+ ?? asString(review.state)
543
+ ?? extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment));
544
+ }
545
+ function isUxcPreviewTruncatedObject(value) {
546
+ return value._uxc_preview_truncated === true;
547
+ }
436
548
  function parseTargetRef(targetRef) {
437
549
  const match = targetRef.match(/^([^/]+)\/([^#]+)#(\d+)$/);
438
550
  if (!match) {
@@ -495,6 +607,15 @@ function asRecord(value) {
495
607
  function asString(value) {
496
608
  return typeof value === "string" ? value : null;
497
609
  }
610
+ function asIdString(value) {
611
+ if (typeof value === "string") {
612
+ return value;
613
+ }
614
+ if (typeof value === "number" && Number.isFinite(value)) {
615
+ return String(value);
616
+ }
617
+ return null;
618
+ }
498
619
  function asNumber(value) {
499
620
  return typeof value === "number" ? value : null;
500
621
  }
@@ -240,6 +240,7 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
240
240
  const variant = buildWorkflowRunVariant(workflowName, status, conclusion);
241
241
  const actor = asRecord(run.actor);
242
242
  const headCommit = asRecord(run.head_commit);
243
+ const pullRequestNumbers = extractPullRequestNumbers(run.pull_requests);
243
244
  return {
244
245
  sourceId: source.sourceId,
245
246
  sourceNativeId: `workflow_run:${runId}`,
@@ -259,6 +260,7 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
259
260
  event: asString(run.event),
260
261
  headSha: asString(run.head_sha),
261
262
  headBranch: asString(run.head_branch),
263
+ pullRequestNumbers,
262
264
  runNumber: asNumber(run.run_number),
263
265
  runAttempt: asNumber(run.run_attempt),
264
266
  actor: asString(actor.login),
@@ -277,6 +279,7 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
277
279
  event: asString(run.event),
278
280
  head_sha: asString(run.head_sha),
279
281
  head_branch: asString(run.head_branch),
282
+ pull_requests: pullRequestNumbers.map((number) => ({ number })),
280
283
  run_number: asNumber(run.run_number),
281
284
  run_attempt: asNumber(run.run_attempt),
282
285
  html_url: asString(run.html_url),
@@ -343,6 +346,19 @@ function asString(value) {
343
346
  function asNumber(value) {
344
347
  return typeof value === "number" && Number.isFinite(value) ? value : null;
345
348
  }
349
+ function extractPullRequestNumbers(value) {
350
+ if (!Array.isArray(value)) {
351
+ return [];
352
+ }
353
+ const numbers = new Set();
354
+ for (const entry of value) {
355
+ const number = asNumber(asRecord(entry).number);
356
+ if (number && Number.isInteger(number) && number > 0) {
357
+ numbers.add(number);
358
+ }
359
+ }
360
+ return Array.from(numbers).sort((left, right) => left - right);
361
+ }
346
362
  function inferWorkflowRunStatus(run, conclusion) {
347
363
  return asString(run.status)
348
364
  ?? (conclusion ? "completed" : null)
@@ -300,7 +300,7 @@ class RemoteSourceRuntime {
300
300
  if (Object.keys(rawPayload).length === 0) {
301
301
  continue;
302
302
  }
303
- const mapped = module.mapRawEvent(rawPayload, moduleSource);
303
+ const mapped = await module.mapRawEvent(rawPayload, moduleSource);
304
304
  if (!mapped) {
305
305
  continue;
306
306
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.RemoteSourceModuleRegistry = void 0;
7
7
  exports.builtInModuleIdForSourceType = builtInModuleIdForSourceType;
8
+ exports.createGithubRepoRemoteModule = createGithubRepoRemoteModule;
8
9
  exports.moduleConfigForSource = moduleConfigForSource;
9
10
  const node_fs_1 = __importDefault(require("node:fs"));
10
11
  const node_path_1 = __importDefault(require("node:path"));
@@ -133,169 +134,173 @@ function asNonEmptyString(value) {
133
134
  const trimmed = value.trim();
134
135
  return trimmed.length > 0 ? trimmed : null;
135
136
  }
136
- const GITHUB_REPO_MODULE = {
137
- id: "builtin.github_repo",
138
- listDeliveryOperations(input) {
139
- return (0, github_1.githubDeliveryOperationsForHandle)(input.handle);
140
- },
141
- async invokeDeliveryOperation(input) {
142
- return (0, github_1.invokeGithubDeliveryOperation)(input.handle, input.operation, input.input);
143
- },
144
- describeCapabilities(source) {
145
- const config = (0, github_1.parseGithubSourceConfig)(source);
146
- return {
147
- sourceKind: "github_repo",
148
- aliases: ["github_repo"],
149
- configSchema: [
150
- { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
151
- { name: "repo", type: "string", required: true, description: "GitHub repository name." },
152
- { name: "uxcAuth", type: "string", required: false, description: "Optional uxc auth profile." },
153
- { name: "eventTypes", type: "string[]", required: false, description: "Optional GitHub event type allowlist." },
154
- { name: "perPage", type: "number", required: false, description: `Repository events requested per poll. Default ${config.perPage ?? 10}.` },
155
- { name: "pollIntervalSecs", type: "number", required: false, description: `Polling interval in seconds. Default ${config.pollIntervalSecs ?? 30}.` },
156
- ],
157
- metadataFields: [
158
- { name: "eventType", type: "string", description: "GitHub event type such as IssueCommentEvent." },
159
- { name: "action", type: "string", description: "GitHub event action suffix such as created." },
160
- { name: "author", type: "string|null", description: "Actor login for the event." },
161
- { name: "isPullRequest", type: "boolean", description: "Whether the event targets a pull request surface." },
162
- { name: "reviewState", type: "string|null", description: "Review decision state for PullRequestReviewEvent such as approved or changes_requested." },
163
- { name: "labels", type: "string[]", description: "Labels extracted from the issue or pull request." },
164
- { name: "mentions", type: "string[]", description: "Mention handles extracted from title/body/comment text." },
165
- { name: "number", type: "number|null", description: "Issue or pull request number when present." },
166
- { name: "repoFullName", type: "string", description: "Repository full name in owner/repo form." },
167
- { name: "title", type: "string|null", description: "Issue, pull request, or comment title." },
168
- { name: "body", type: "string|null", description: "Issue, pull request, or comment body text." },
169
- { name: "url", type: "string|null", description: "Primary GitHub HTML URL for the event target." },
170
- ],
171
- payloadExamples: [
172
- {
173
- id: "1234567891",
174
- type: "PullRequestReviewEvent",
175
- action: "created",
176
- actor: "Copilot",
177
- pull_request: { number: 67, title: "feat: add remote module capability hooks" },
178
- review: { state: "commented", body: "review summary" },
179
- },
180
- {
181
- id: "1234567892",
182
- type: "PullRequestEvent",
183
- action: "closed",
184
- actor: "jolestar",
185
- pull_request: { number: 72, title: "feat: add cleanup policy lifecycle engine", merged: true },
186
- },
187
- ],
188
- eventVariantExamples: [
189
- "IssueCommentEvent.created",
190
- "PullRequestEvent.opened",
191
- "PullRequestEvent.closed",
192
- "PullRequestReviewEvent.created",
193
- "PullRequestReviewCommentEvent.created",
194
- ],
195
- };
196
- },
197
- listSubscriptionShortcuts() {
198
- return (0, github_1.githubSubscriptionShortcutSpec)();
199
- },
200
- expandSubscriptionShortcut(input) {
201
- return (0, github_1.expandGithubSubscriptionShortcut)(input);
202
- },
203
- deriveTrackedResource(filter, source) {
204
- return (0, github_1.deriveGithubTrackedResource)(filter, source);
205
- },
206
- deriveNotificationGrouping(item) {
207
- const number = typeof item.metadata.number === "number" ? item.metadata.number : null;
208
- if (!number) {
209
- return null;
210
- }
211
- const variant = item.eventVariant;
212
- if (variant.startsWith("PullRequestReviewCommentEvent.")) {
213
- return {
214
- groupable: true,
215
- resourceRef: `pr:${number}`,
216
- eventFamily: "review_comments",
217
- summaryHint: `review comments on PR #${number}`,
218
- };
219
- }
220
- if (variant.startsWith("PullRequestReviewEvent.")) {
137
+ function createGithubRepoRemoteModule(options) {
138
+ return {
139
+ id: "builtin.github_repo",
140
+ listDeliveryOperations(input) {
141
+ return (0, github_1.githubDeliveryOperationsForHandle)(input.handle);
142
+ },
143
+ async invokeDeliveryOperation(input) {
144
+ return (0, github_1.invokeGithubDeliveryOperation)(input.handle, input.operation, input.input);
145
+ },
146
+ describeCapabilities(source) {
147
+ const config = (0, github_1.parseGithubSourceConfig)(source);
221
148
  return {
222
- groupable: true,
223
- resourceRef: `pr:${number}`,
224
- eventFamily: "reviews",
225
- summaryHint: `reviews on PR #${number}`,
149
+ sourceKind: "github_repo",
150
+ aliases: ["github_repo"],
151
+ configSchema: [
152
+ { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
153
+ { name: "repo", type: "string", required: true, description: "GitHub repository name." },
154
+ { name: "uxcAuth", type: "string", required: false, description: "Optional uxc auth profile." },
155
+ { name: "eventTypes", type: "string[]", required: false, description: "Optional GitHub event type allowlist." },
156
+ { name: "perPage", type: "number", required: false, description: `Repository events requested per poll. Default ${config.perPage ?? 10}.` },
157
+ { name: "pollIntervalSecs", type: "number", required: false, description: `Polling interval in seconds. Default ${config.pollIntervalSecs ?? 30}.` },
158
+ ],
159
+ metadataFields: [
160
+ { name: "eventType", type: "string", description: "GitHub event type such as IssueCommentEvent." },
161
+ { name: "action", type: "string", description: "GitHub event action suffix such as created." },
162
+ { name: "author", type: "string|null", description: "Actor login for the event." },
163
+ { name: "isPullRequest", type: "boolean", description: "Whether the event targets a pull request surface." },
164
+ { name: "reviewState", type: "string|null", description: "Review decision state for PullRequestReviewEvent such as approved or changes_requested." },
165
+ { name: "labels", type: "string[]", description: "Labels extracted from the issue or pull request." },
166
+ { name: "mentions", type: "string[]", description: "Mention handles extracted from title/body/comment text." },
167
+ { name: "number", type: "number|null", description: "Issue or pull request number when present." },
168
+ { name: "repoFullName", type: "string", description: "Repository full name in owner/repo form." },
169
+ { name: "title", type: "string|null", description: "Issue, pull request, or comment title." },
170
+ { name: "body", type: "string|null", description: "Issue, pull request, or comment body text." },
171
+ { name: "url", type: "string|null", description: "Primary GitHub HTML URL for the event target." },
172
+ ],
173
+ payloadExamples: [
174
+ {
175
+ id: "1234567891",
176
+ type: "PullRequestReviewEvent",
177
+ action: "created",
178
+ actor: "Copilot",
179
+ pull_request: { number: 67, title: "feat: add remote module capability hooks" },
180
+ review: { state: "commented", body: "review summary" },
181
+ },
182
+ {
183
+ id: "1234567892",
184
+ type: "PullRequestEvent",
185
+ action: "closed",
186
+ actor: "jolestar",
187
+ pull_request: { number: 72, title: "feat: add cleanup policy lifecycle engine", merged: true },
188
+ },
189
+ ],
190
+ eventVariantExamples: [
191
+ "IssueCommentEvent.created",
192
+ "PullRequestEvent.opened",
193
+ "PullRequestEvent.closed",
194
+ "PullRequestReviewEvent.created",
195
+ "PullRequestReviewCommentEvent.created",
196
+ ],
226
197
  };
227
- }
228
- if (variant.startsWith("IssueCommentEvent.")) {
198
+ },
199
+ listSubscriptionShortcuts() {
200
+ return (0, github_1.githubSubscriptionShortcutSpec)();
201
+ },
202
+ expandSubscriptionShortcut(input) {
203
+ return (0, github_1.expandGithubSubscriptionShortcut)(input);
204
+ },
205
+ deriveTrackedResource(filter, source) {
206
+ return (0, github_1.deriveGithubTrackedResource)(filter, source);
207
+ },
208
+ deriveNotificationGrouping(item) {
209
+ const number = typeof item.metadata.number === "number" ? item.metadata.number : null;
210
+ if (!number) {
211
+ return null;
212
+ }
213
+ const variant = item.eventVariant;
214
+ if (variant.startsWith("PullRequestReviewCommentEvent.")) {
215
+ return {
216
+ groupable: true,
217
+ resourceRef: `pr:${number}`,
218
+ eventFamily: "review_comments",
219
+ summaryHint: `review comments on PR #${number}`,
220
+ };
221
+ }
222
+ if (variant.startsWith("PullRequestReviewEvent.")) {
223
+ return {
224
+ groupable: true,
225
+ resourceRef: `pr:${number}`,
226
+ eventFamily: "reviews",
227
+ summaryHint: `reviews on PR #${number}`,
228
+ };
229
+ }
230
+ if (variant.startsWith("IssueCommentEvent.")) {
231
+ return {
232
+ groupable: true,
233
+ resourceRef: item.metadata.isPullRequest === true ? `pr:${number}` : `issue:${number}`,
234
+ eventFamily: "comments",
235
+ summaryHint: item.metadata.isPullRequest === true ? `comments on PR #${number}` : `comments on issue #${number}`,
236
+ };
237
+ }
238
+ if (variant.startsWith("PullRequestEvent.closed")) {
239
+ return {
240
+ groupable: true,
241
+ resourceRef: `pr:${number}`,
242
+ eventFamily: "lifecycle",
243
+ summaryHint: `PR #${number} lifecycle updates`,
244
+ flushClass: "immediate",
245
+ };
246
+ }
247
+ return null;
248
+ },
249
+ summarizeDigestThread(items, _source, grouping) {
250
+ if (!grouping.summaryHint || items.length === 0) {
251
+ return null;
252
+ }
253
+ const count = items.length;
254
+ return `${count} ${grouping.summaryHint}`;
255
+ },
256
+ projectLifecycleSignal(rawPayload, source) {
257
+ return (0, github_1.projectGithubLifecycleSignal)(rawPayload, source);
258
+ },
259
+ validateConfig(source) {
260
+ (0, github_1.parseGithubSourceConfig)(source);
261
+ },
262
+ buildManagedSourceSpec(source) {
263
+ const config = (0, github_1.parseGithubSourceConfig)(source);
229
264
  return {
230
- groupable: true,
231
- resourceRef: item.metadata.isPullRequest === true ? `pr:${number}` : `issue:${number}`,
232
- eventFamily: "comments",
233
- summaryHint: item.metadata.isPullRequest === true ? `comments on PR #${number}` : `comments on issue #${number}`,
265
+ endpoint: github_1.GITHUB_ENDPOINT,
266
+ operation_id: "get:/repos/{owner}/{repo}/events",
267
+ args: {
268
+ owner: config.owner,
269
+ repo: config.repo,
270
+ per_page: config.perPage ?? 10,
271
+ },
272
+ mode: "poll",
273
+ poll_config: {
274
+ interval_secs: config.pollIntervalSecs ?? 30,
275
+ extract_items_pointer: "",
276
+ checkpoint_strategy: {
277
+ type: "item_key",
278
+ item_key_pointer: "/id",
279
+ seen_window: 1024,
280
+ },
281
+ },
282
+ options: { auth: config.uxcAuth },
234
283
  };
235
- }
236
- if (variant.startsWith("PullRequestEvent.closed")) {
284
+ },
285
+ async mapRawEvent(rawPayload, source) {
286
+ const config = (0, github_1.parseGithubSourceConfig)(source);
287
+ const hydratedPayload = await (0, github_1.hydrateGithubRepoEventIfNeeded)(config, rawPayload, options?.callClient);
288
+ const normalized = (0, github_1.normalizeGithubRepoEvent)(source, config, hydratedPayload);
289
+ if (!normalized) {
290
+ return null;
291
+ }
237
292
  return {
238
- groupable: true,
239
- resourceRef: `pr:${number}`,
240
- eventFamily: "lifecycle",
241
- summaryHint: `PR #${number} lifecycle updates`,
242
- flushClass: "immediate",
293
+ sourceNativeId: normalized.sourceNativeId,
294
+ eventVariant: normalized.eventVariant,
295
+ metadata: normalized.metadata ?? {},
296
+ rawPayload: normalized.rawPayload ?? hydratedPayload,
297
+ occurredAt: normalized.occurredAt,
298
+ deliveryHandle: normalized.deliveryHandle,
243
299
  };
244
- }
245
- return null;
246
- },
247
- summarizeDigestThread(items, _source, grouping) {
248
- if (!grouping.summaryHint || items.length === 0) {
249
- return null;
250
- }
251
- const count = items.length;
252
- return `${count} ${grouping.summaryHint}`;
253
- },
254
- projectLifecycleSignal(rawPayload, source) {
255
- return (0, github_1.projectGithubLifecycleSignal)(rawPayload, source);
256
- },
257
- validateConfig(source) {
258
- (0, github_1.parseGithubSourceConfig)(source);
259
- },
260
- buildManagedSourceSpec(source) {
261
- const config = (0, github_1.parseGithubSourceConfig)(source);
262
- return {
263
- endpoint: github_1.GITHUB_ENDPOINT,
264
- operation_id: "get:/repos/{owner}/{repo}/events",
265
- args: {
266
- owner: config.owner,
267
- repo: config.repo,
268
- per_page: config.perPage ?? 10,
269
- },
270
- mode: "poll",
271
- poll_config: {
272
- interval_secs: config.pollIntervalSecs ?? 30,
273
- extract_items_pointer: "",
274
- checkpoint_strategy: {
275
- type: "item_key",
276
- item_key_pointer: "/id",
277
- seen_window: 1024,
278
- },
279
- },
280
- options: { auth: config.uxcAuth },
281
- };
282
- },
283
- mapRawEvent(rawPayload, source) {
284
- const config = (0, github_1.parseGithubSourceConfig)(source);
285
- const normalized = (0, github_1.normalizeGithubRepoEvent)(source, config, rawPayload);
286
- if (!normalized) {
287
- return null;
288
- }
289
- return {
290
- sourceNativeId: normalized.sourceNativeId,
291
- eventVariant: normalized.eventVariant,
292
- metadata: normalized.metadata ?? {},
293
- rawPayload: normalized.rawPayload ?? rawPayload,
294
- occurredAt: normalized.occurredAt,
295
- deliveryHandle: normalized.deliveryHandle,
296
- };
297
- },
298
- };
300
+ },
301
+ };
302
+ }
303
+ const GITHUB_REPO_MODULE = createGithubRepoRemoteModule();
299
304
  const GITHUB_REPO_CI_MODULE = {
300
305
  id: "builtin.github_repo_ci",
301
306
  describeCapabilities() {
@@ -319,6 +324,7 @@ const GITHUB_REPO_CI_MODULE = {
319
324
  { name: "event", type: "string|null", description: "GitHub trigger event for the workflow run." },
320
325
  { name: "headBranch", type: "string|null", description: "Head branch for the workflow run." },
321
326
  { name: "headSha", type: "string|null", description: "Head commit SHA for the workflow run." },
327
+ { name: "pullRequestNumbers", type: "number[]", description: "Pull request numbers associated with the workflow run." },
322
328
  { name: "actor", type: "string|null", description: "Actor login for the workflow run." },
323
329
  { name: "commitMessage", type: "string|null", description: "Head commit message when present." },
324
330
  { name: "htmlUrl", type: "string|null", description: "GitHub Actions run URL." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {