@holon-run/agentinbox 1.0.0-beta.0 → 1.0.0-beta.2

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 CHANGED
@@ -66,7 +66,7 @@ manually or relying on the agent runtime to expose its own notification API.
66
66
 
67
67
  ## v1.0.0 Beta Upgrade Notes
68
68
 
69
- `v1.0.0-beta.0` is the first release on the final v1 storage and API boundary.
69
+ `v1.0.0-beta.0` was the first release on the final v1 storage and API boundary.
70
70
 
71
71
  - pre-v1 local databases are archived and replaced with a fresh v1 database
72
72
  - old local state is not imported into the new v1 database
@@ -171,6 +171,10 @@ agentinbox inbox read --agent-id <agent_id>
171
171
  agentinbox inbox ack --agent-id <agent_id> --through <last_entry_id>
172
172
  ```
173
173
 
174
+ The HTTP `POST /subscriptions` endpoint always returns
175
+ `{ "subscriptions": [...] }`, even when only one subscription is created. That
176
+ keeps shortcut expansion consistent for external callers.
177
+
174
178
  When acking a reviewed batch, prefer `--through <last_entry_id>` over `--all`.
175
179
  That preserves the boundary of the items you actually inspected. Use
176
180
  `ack --all` only when you have intentionally verified that every current
package/dist/src/cli.js CHANGED
@@ -19,8 +19,11 @@ const store_1 = require("./store");
19
19
  const terminal_1 = require("./terminal");
20
20
  const util_1 = require("./util");
21
21
  async function main() {
22
- const args = process.argv.slice(2);
23
- const normalized = normalizeHelpArgs(args);
22
+ const rawArgs = process.argv.slice(2);
23
+ const detectedArgs = normalizeHelpArgs(rawArgs);
24
+ const normalized = shouldTreatJsonFlagAsNoop(detectedArgs)
25
+ ? stripNoopJsonFlag(detectedArgs)
26
+ : detectedArgs;
24
27
  const command = normalized[0];
25
28
  if (!command || command === "help" || command === "--help" || command === "-h") {
26
29
  printHelp(normalized.slice(1));
@@ -445,7 +448,7 @@ async function main() {
445
448
  startOffset: parseOptionalNumber(takeFlagValue(normalized, "--start-offset")),
446
449
  startTime: takeFlagValue(normalized, "--start-time") ?? undefined,
447
450
  });
448
- console.log((0, util_1.jsonResponse)(withCommandMetadata(response.data, selection)));
451
+ console.log((0, util_1.jsonResponse)(withCommandMetadata(normalizeSubscriptionAddOutput(response.data, Boolean(shortcutName)), selection)));
449
452
  return;
450
453
  }
451
454
  if (command === "subscription" && normalized[1] === "list") {
@@ -767,7 +770,7 @@ async function main() {
767
770
  printHelp([command]);
768
771
  return;
769
772
  }
770
- throw new Error(`unknown command: ${args.join(" ")}`);
773
+ throw new Error(`unknown command: ${normalized.join(" ")}`);
771
774
  }
772
775
  async function runServe(args) {
773
776
  const port = parseOptionalNumber(takeFlagValue(args, "--port"));
@@ -1000,6 +1003,26 @@ function normalizeHelpArgs(args) {
1000
1003
  }
1001
1004
  return [args[1], "--help"];
1002
1005
  }
1006
+ function stripNoopJsonFlag(args) {
1007
+ return args.filter((arg) => arg !== "--json");
1008
+ }
1009
+ function shouldTreatJsonFlagAsNoop(args) {
1010
+ if (!args.includes("--json")) {
1011
+ return false;
1012
+ }
1013
+ const command = args[0];
1014
+ if (!command) {
1015
+ return false;
1016
+ }
1017
+ if (args.includes("--help") || args.includes("-h")) {
1018
+ return false;
1019
+ }
1020
+ return command !== "help" &&
1021
+ command !== "version" &&
1022
+ command !== "--version" &&
1023
+ command !== "-v" &&
1024
+ command !== "serve";
1025
+ }
1003
1026
  function hasHelpFlag(args) {
1004
1027
  return args.includes("--help") || args.includes("-h");
1005
1028
  }
@@ -1047,6 +1070,19 @@ function withCommandMetadata(data, selection) {
1047
1070
  ...(selection.warnings.length > 0 ? { warnings: selection.warnings } : {}),
1048
1071
  };
1049
1072
  }
1073
+ function normalizeSubscriptionAddOutput(data, shortcutUsed) {
1074
+ if (shortcutUsed) {
1075
+ return data;
1076
+ }
1077
+ const subscriptions = data.subscriptions;
1078
+ if (Array.isArray(subscriptions) &&
1079
+ subscriptions.length === 1 &&
1080
+ subscriptions[0] &&
1081
+ typeof subscriptions[0] === "object") {
1082
+ return subscriptions[0];
1083
+ }
1084
+ return data;
1085
+ }
1050
1086
  function getRequiredTerminalContext() {
1051
1087
  try {
1052
1088
  return (0, terminal_1.detectTerminalContext)(process.env);
package/dist/src/http.js CHANGED
@@ -29,6 +29,14 @@ const errorResponseSchema = {
29
29
  error: { type: "string" },
30
30
  },
31
31
  };
32
+ const subscriptionCollectionSchema = {
33
+ type: "object",
34
+ required: ["subscriptions"],
35
+ additionalProperties: false,
36
+ properties: {
37
+ subscriptions: { type: "array", items: jsonObjectSchema },
38
+ },
39
+ };
32
40
  const deliveryHandleSchema = {
33
41
  type: "object",
34
42
  additionalProperties: false,
@@ -1041,13 +1049,7 @@ function buildFastifyServer(service) {
1041
1049
  },
1042
1050
  },
1043
1051
  response: {
1044
- 200: {
1045
- type: "object",
1046
- required: ["subscriptions"],
1047
- properties: {
1048
- subscriptions: { type: "array", items: jsonObjectSchema },
1049
- },
1050
- },
1052
+ 200: subscriptionCollectionSchema,
1051
1053
  },
1052
1054
  },
1053
1055
  }, async (request) => {
@@ -1087,13 +1089,13 @@ function buildFastifyServer(service) {
1087
1089
  },
1088
1090
  },
1089
1091
  response: {
1090
- 200: jsonObjectSchema,
1092
+ 200: subscriptionCollectionSchema,
1091
1093
  400: errorResponseSchema,
1092
1094
  },
1093
1095
  },
1094
1096
  }, async (request) => {
1095
1097
  const body = request.body;
1096
- return service.registerSubscription({
1098
+ const input = {
1097
1099
  agentId: String(body.agentId),
1098
1100
  sourceId: String(body.sourceId),
1099
1101
  shortcut: body.shortcut && typeof body.shortcut === "object"
@@ -1110,7 +1112,8 @@ function buildFastifyServer(service) {
1110
1112
  startPolicy: optionalString(body.startPolicy),
1111
1113
  startOffset: typeof body.startOffset === "number" ? body.startOffset : undefined,
1112
1114
  startTime: optionalString(body.startTime) ?? undefined,
1113
- });
1115
+ };
1116
+ return { subscriptions: await service.registerSubscriptions(input) };
1114
1117
  });
1115
1118
  app.get("/subscriptions/:subscriptionId", {
1116
1119
  schema: {
@@ -707,26 +707,93 @@ class AgentInboxService {
707
707
  return result;
708
708
  }
709
709
  async registerSubscription(input) {
710
- if (input.shortcut) {
711
- const source = this.store.getSource(input.sourceId);
712
- if (!source) {
713
- throw new Error(`unknown source: ${input.sourceId}`);
710
+ const expanded = await this.expandShortcutRegistration(input);
711
+ if (!expanded) {
712
+ return this.registerSingleSubscription(input);
713
+ }
714
+ if (expanded.plan.members.length !== 1) {
715
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} for source ${input.sourceId} expands to ${expanded.plan.members.length} subscriptions; use registerSubscriptions() instead`);
716
+ }
717
+ const subscriptions = await this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
718
+ return subscriptions[0];
719
+ }
720
+ async registerSubscriptions(input) {
721
+ const expanded = await this.expandShortcutRegistration(input);
722
+ if (expanded) {
723
+ return this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
724
+ }
725
+ return [await this.registerSingleSubscription(input)];
726
+ }
727
+ async expandShortcutRegistration(input) {
728
+ if (!input.shortcut) {
729
+ return null;
730
+ }
731
+ const source = this.store.getSource(input.sourceId);
732
+ if (!source) {
733
+ throw new Error(`unknown source: ${input.sourceId}`);
734
+ }
735
+ if (hasSubscriptionFieldOverride(input)) {
736
+ throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
737
+ }
738
+ const plan = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
739
+ if (!plan) {
740
+ throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
741
+ }
742
+ return { source, plan };
743
+ }
744
+ async registerExpandedShortcutSubscriptions(input, source, plan) {
745
+ if (plan.members.length === 0) {
746
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} did not produce any subscriptions`);
747
+ }
748
+ const resolvedMembers = plan.members.map((member) => ({
749
+ source: this.resolveShortcutMemberSource(source, member),
750
+ member,
751
+ }));
752
+ const created = [];
753
+ try {
754
+ for (const { source: memberSource, member } of resolvedMembers) {
755
+ created.push(await this.registerSingleSubscription({
756
+ ...input,
757
+ sourceId: memberSource.sourceId,
758
+ shortcut: undefined,
759
+ filter: member.filter,
760
+ trackedResourceRef: member.trackedResourceRef ?? null,
761
+ cleanupPolicy: member.cleanupPolicy ?? null,
762
+ }));
714
763
  }
715
- if (hasSubscriptionFieldOverride(input)) {
716
- throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
764
+ }
765
+ catch (error) {
766
+ await this.rollbackRegisteredSubscriptions(created);
767
+ throw error;
768
+ }
769
+ return created;
770
+ }
771
+ async rollbackRegisteredSubscriptions(subscriptions) {
772
+ for (const subscription of [...subscriptions].reverse()) {
773
+ try {
774
+ await this.removeSubscription(subscription.subscriptionId);
717
775
  }
718
- const expanded = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
719
- if (!expanded) {
720
- throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
776
+ catch (error) {
777
+ console.warn(`failed to roll back subscription ${subscription.subscriptionId}:`, error);
721
778
  }
722
- return this.registerSubscription({
723
- ...input,
724
- shortcut: undefined,
725
- filter: expanded.filter,
726
- trackedResourceRef: expanded.trackedResourceRef ?? null,
727
- cleanupPolicy: expanded.cleanupPolicy,
728
- });
729
779
  }
780
+ }
781
+ resolveShortcutMemberSource(source, member) {
782
+ if (!member.streamKind || member.streamKind === source.streamKind) {
783
+ return source;
784
+ }
785
+ if (!source.hostId || !source.streamKey) {
786
+ throw new Error(`subscription shortcut ${source.sourceId} requires a host-scoped source to resolve ${member.streamKind}`);
787
+ }
788
+ const sibling = this.store.listSources().find((candidate) => candidate.hostId === source.hostId &&
789
+ candidate.streamKind === member.streamKind &&
790
+ candidate.streamKey === source.streamKey);
791
+ if (!sibling) {
792
+ throw new Error(`subscription shortcut ${source.sourceId} requires an existing sibling ${member.streamKind} stream under host ${source.hostId} with streamKey ${source.streamKey}`);
793
+ }
794
+ return sibling;
795
+ }
796
+ async registerSingleSubscription(input) {
730
797
  this.getAgent(input.agentId);
731
798
  const source = this.store.getSource(input.sourceId);
732
799
  if (!source) {
@@ -755,17 +822,25 @@ class AgentInboxService {
755
822
  };
756
823
  this.ensureInboxForAgent(subscription.agentId);
757
824
  this.store.insertSubscription(subscription);
758
- const stream = await this.ensureStreamForSource(source);
759
- await this.backend.ensureConsumer({
760
- streamId: stream.streamId,
761
- subscriptionId: subscription.subscriptionId,
762
- consumerKey: `subscription:${subscription.subscriptionId}`,
763
- startPolicy: subscription.startPolicy,
764
- startOffset: subscription.startOffset ?? null,
765
- startTime: subscription.startTime ?? null,
766
- });
767
- this.store.deleteSourceIdleState(source.sourceId);
768
- return subscription;
825
+ try {
826
+ const stream = await this.ensureStreamForSource(source);
827
+ await this.backend.ensureConsumer({
828
+ streamId: stream.streamId,
829
+ subscriptionId: subscription.subscriptionId,
830
+ consumerKey: `subscription:${subscription.subscriptionId}`,
831
+ startPolicy: subscription.startPolicy,
832
+ startOffset: subscription.startOffset ?? null,
833
+ startTime: subscription.startTime ?? null,
834
+ });
835
+ this.store.deleteSourceIdleState(source.sourceId);
836
+ return subscription;
837
+ }
838
+ catch (error) {
839
+ this.store.deleteSubscription(subscription.subscriptionId);
840
+ this.clearSubscriptionRuntimeState(subscription);
841
+ this.refreshSourceIdleState(source.sourceId);
842
+ throw error;
843
+ }
769
844
  }
770
845
  async removeSubscription(subscriptionId) {
771
846
  const subscription = this.getSubscription(subscriptionId);
@@ -1322,7 +1397,7 @@ class AgentInboxService {
1322
1397
  });
1323
1398
  }
1324
1399
  for (const signal of lifecycleSignals.values()) {
1325
- this.scheduleLifecycleRetirements(source, subscription, signal);
1400
+ this.scheduleLifecycleRetirements(source, signal);
1326
1401
  }
1327
1402
  return {
1328
1403
  subscriptionId: subscription.subscriptionId,
@@ -1483,31 +1558,33 @@ class AgentInboxService {
1483
1558
  signals.set(normalized.ref, normalized);
1484
1559
  }
1485
1560
  }
1486
- scheduleLifecycleRetirements(source, subscription, signal) {
1561
+ scheduleLifecycleRetirements(source, signal) {
1487
1562
  if (!signal.terminal) {
1488
1563
  return;
1489
1564
  }
1490
- if (!subscription.trackedResourceRef || subscription.trackedResourceRef !== signal.ref) {
1565
+ if (!source.hostId) {
1491
1566
  return;
1492
1567
  }
1493
1568
  const signalOccurredAt = signal.occurredAt ?? (0, util_1.nowIso)();
1494
1569
  const signalOccurredAtMs = Date.parse(signalOccurredAt);
1495
- const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1496
- if (!retireAt) {
1497
- return;
1570
+ for (const subscription of this.store.listSubscriptionsForHostTrackedResourceRef(source.hostId, signal.ref)) {
1571
+ const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1572
+ if (!retireAt) {
1573
+ continue;
1574
+ }
1575
+ const now = (0, util_1.nowIso)();
1576
+ this.store.upsertSubscriptionLifecycleRetirement({
1577
+ subscriptionId: subscription.subscriptionId,
1578
+ hostId: source.hostId,
1579
+ trackedResourceRef: signal.ref,
1580
+ retireAt,
1581
+ terminalState: signal.state ?? null,
1582
+ terminalResult: signal.result ?? null,
1583
+ terminalOccurredAt: signalOccurredAt,
1584
+ createdAt: now,
1585
+ updatedAt: now,
1586
+ });
1498
1587
  }
1499
- const now = (0, util_1.nowIso)();
1500
- this.store.upsertSubscriptionLifecycleRetirement({
1501
- subscriptionId: subscription.subscriptionId,
1502
- sourceId: source.sourceId,
1503
- trackedResourceRef: signal.ref,
1504
- retireAt,
1505
- terminalState: signal.state ?? null,
1506
- terminalResult: signal.result ?? null,
1507
- terminalOccurredAt: signalOccurredAt,
1508
- createdAt: now,
1509
- updatedAt: now,
1510
- });
1511
1588
  }
1512
1589
  notifyInboxWatchers(agentId, items) {
1513
1590
  const watchers = this.inboxWatchers.get(agentId);
@@ -1707,6 +1784,11 @@ class AgentInboxService {
1707
1784
  buffer.inFlight = true;
1708
1785
  const entries = buffer.pending.splice(0, buffer.pending.length);
1709
1786
  try {
1787
+ if (!this.activationTargetExists(buffer.agentId, buffer.targetId)) {
1788
+ this.notificationBuffers.delete(key);
1789
+ buffer.inFlight = false;
1790
+ return;
1791
+ }
1710
1792
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1711
1793
  const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
1712
1794
  if (shouldAttemptDispatch) {
@@ -1844,7 +1926,11 @@ class AgentInboxService {
1844
1926
  if (!state) {
1845
1927
  return;
1846
1928
  }
1847
- const target = this.getActivationTarget(targetId);
1929
+ const target = this.getActivationTargetIfExists(agentId, targetId);
1930
+ if (!target) {
1931
+ this.store.deleteActivationDispatchState(agentId, targetId);
1932
+ return;
1933
+ }
1848
1934
  const inbox = this.ensureInboxForAgent(agentId);
1849
1935
  const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1850
1936
  if (unacked === 0) {
@@ -1911,7 +1997,10 @@ class AgentInboxService {
1911
1997
  }
1912
1998
  }
1913
1999
  async dispatchActivationTarget(input) {
1914
- const target = this.getActivationTarget(input.targetId);
2000
+ const target = this.getActivationTargetIfExists(input.agentId, input.targetId);
2001
+ if (!target) {
2002
+ return "offline";
2003
+ }
1915
2004
  if (target.status === "offline") {
1916
2005
  return "offline";
1917
2006
  }
@@ -1952,6 +2041,9 @@ class AgentInboxService {
1952
2041
  try {
1953
2042
  if (target.kind === "terminal") {
1954
2043
  const gate = await this.activationGate.evaluate(target);
2044
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2045
+ return "offline";
2046
+ }
1955
2047
  this.logger.debug("activation.gate_decision", {
1956
2048
  agentId: input.agentId,
1957
2049
  targetId: target.targetId,
@@ -1972,6 +2064,9 @@ class AgentInboxService {
1972
2064
  return "offline";
1973
2065
  }
1974
2066
  const reminder = await this.buildTerminalReminder(target, inbox.inboxId, totalUnackedCount, input.summary, input.entries);
2067
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2068
+ return "offline";
2069
+ }
1975
2070
  const prompt = reminder.prompt;
1976
2071
  const promptFingerprint = reminder.fingerprint;
1977
2072
  terminalPrompt = prompt;
@@ -2057,6 +2152,9 @@ class AgentInboxService {
2057
2152
  subscriptionIds: input.subscriptionIds,
2058
2153
  });
2059
2154
  await this.terminalDispatcher.dispatch(target, prompt);
2155
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2156
+ return "offline";
2157
+ }
2060
2158
  this.logger.debug("activation.terminal_dispatch_succeeded", {
2061
2159
  agentId: input.agentId,
2062
2160
  targetId: target.targetId,
@@ -2085,6 +2183,9 @@ class AgentInboxService {
2085
2183
  deliveredAt: null,
2086
2184
  };
2087
2185
  await this.activationDispatcher.dispatch(target.url, activation);
2186
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2187
+ return "offline";
2188
+ }
2088
2189
  this.store.insertActivation(activation);
2089
2190
  }
2090
2191
  this.markActivationTargetDelivered(target.targetId);
@@ -2119,8 +2220,14 @@ class AgentInboxService {
2119
2220
  error,
2120
2221
  message,
2121
2222
  });
2223
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2224
+ return "offline";
2225
+ }
2122
2226
  if (target.kind === "terminal") {
2123
2227
  const exists = await this.terminalDispatcher.probe(target);
2228
+ if (!this.activationTargetExists(input.agentId, input.targetId)) {
2229
+ return "offline";
2230
+ }
2124
2231
  this.logger.debug("activation.terminal_probe_after_failure", {
2125
2232
  agentId: input.agentId,
2126
2233
  targetId: target.targetId,
@@ -2142,6 +2249,19 @@ class AgentInboxService {
2142
2249
  return "retryable_failure";
2143
2250
  }
2144
2251
  }
2252
+ getActivationTargetIfExists(agentId, targetId) {
2253
+ if (!this.store.getAgent(agentId)) {
2254
+ return null;
2255
+ }
2256
+ const target = this.store.getActivationTarget(targetId);
2257
+ if (!target || target.agentId !== agentId) {
2258
+ return null;
2259
+ }
2260
+ return target;
2261
+ }
2262
+ activationTargetExists(agentId, targetId) {
2263
+ return this.getActivationTargetIfExists(agentId, targetId) != null;
2264
+ }
2145
2265
  upsertDirtyDispatchState(agentId, targetId, entries) {
2146
2266
  this.store.upsertActivationDispatchState({
2147
2267
  agentId,
@@ -4,6 +4,8 @@ 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;
8
+ exports.canonicalGithubPullRequestRef = canonicalGithubPullRequestRef;
7
9
  exports.deriveGithubTrackedResource = deriveGithubTrackedResource;
8
10
  exports.githubSubscriptionShortcutSpec = githubSubscriptionShortcutSpec;
9
11
  exports.expandGithubSubscriptionShortcut = expandGithubSubscriptionShortcut;
@@ -11,6 +13,14 @@ exports.projectGithubLifecycleSignal = projectGithubLifecycleSignal;
11
13
  exports.parseGithubSourceConfig = parseGithubSourceConfig;
12
14
  const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
13
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
+ ]);
14
24
  exports.DEFAULT_GITHUB_EVENT_TYPES = [
15
25
  "IssuesEvent",
16
26
  "IssueCommentEvent",
@@ -20,7 +30,7 @@ exports.DEFAULT_GITHUB_EVENT_TYPES = [
20
30
  ];
21
31
  class GithubUxcClient {
22
32
  client;
23
- constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
33
+ constructor(client = DEFAULT_GITHUB_CALL_CLIENT) {
24
34
  this.client = client;
25
35
  }
26
36
  async createIssueComment(input) {
@@ -221,7 +231,7 @@ function normalizeGithubRepoEvent(source, config, raw) {
221
231
  if (!eventType || !(config.eventTypes ?? exports.DEFAULT_GITHUB_EVENT_TYPES).includes(eventType)) {
222
232
  return null;
223
233
  }
224
- const eventId = asString(event.id);
234
+ const eventId = asIdString(event.id);
225
235
  if (!eventId) {
226
236
  return null;
227
237
  }
@@ -233,18 +243,17 @@ function normalizeGithubRepoEvent(source, config, raw) {
233
243
  const review = asRecord(payload.review);
234
244
  const comment = asRecord(payload.comment);
235
245
  const action = asString(payload.action) ?? "observed";
236
- 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);
237
251
  const isPullRequest = eventType.startsWith("PullRequest");
238
252
  const title = asString(issue.title) ?? asString(pullRequest.title) ?? null;
239
253
  const reviewState = asString(review.state) ?? null;
240
254
  const body = asString(comment.body) ?? asString(review.body) ?? asString(issue.body) ?? asString(pullRequest.body) ?? null;
241
255
  const labels = extractLabels(issue.labels);
242
256
  const mentions = extractMentions(title, body);
243
- const url = asString(comment.html_url) ??
244
- asString(review.html_url) ??
245
- asString(issue.html_url) ??
246
- asString(pullRequest.html_url) ??
247
- asString(event.url);
248
257
  const deliveryHandle = buildGithubDeliveryHandle(config, eventType, number, comment);
249
258
  return {
250
259
  sourceId: source.sourceId,
@@ -282,7 +291,52 @@ function normalizeGithubRepoEvent(source, config, raw) {
282
291
  deliveryHandle,
283
292
  };
284
293
  }
285
- function deriveGithubTrackedResource(filter) {
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
+ }
335
+ function canonicalGithubPullRequestRef(source, number) {
336
+ const config = parseGithubSourceConfig(source);
337
+ return `repo:${config.owner}/${config.repo}:pr:${number}`;
338
+ }
339
+ function deriveGithubTrackedResource(filter, source) {
286
340
  const metadata = asRecord(filter.metadata);
287
341
  const payload = asRecord(filter.payload);
288
342
  const number = asNumber(metadata.number) ?? asNumber(payload.number) ?? asNumber(payload.ref);
@@ -290,13 +344,16 @@ function deriveGithubTrackedResource(filter) {
290
344
  if (!number || isPullRequest !== true) {
291
345
  return null;
292
346
  }
293
- return { ref: `pr:${number}` };
347
+ return { ref: canonicalGithubPullRequestRef(source, number) };
294
348
  }
295
349
  function githubSubscriptionShortcutSpec() {
296
350
  return [{
297
351
  name: "pr",
298
352
  description: "Follow one pull request and auto-retire when it closes.",
299
- argsSchema: [{ name: "number", type: "number", required: true, description: "Pull request number." }],
353
+ argsSchema: [
354
+ { name: "number", type: "number", required: true, description: "Pull request number." },
355
+ { name: "withCi", type: "boolean", required: false, description: "Also create a sibling ci_runs subscription under the same host and stream key." },
356
+ ],
300
357
  }];
301
358
  }
302
359
  function expandGithubSubscriptionShortcut(input) {
@@ -307,18 +364,32 @@ function expandGithubSubscriptionShortcut(input) {
307
364
  if (!number || !Number.isInteger(number) || number <= 0) {
308
365
  throw new Error("subscription shortcut pr requires a positive integer number");
309
366
  }
367
+ const trackedResourceRef = canonicalGithubPullRequestRef(input.source, number);
368
+ const withCi = input.args?.withCi === true;
310
369
  return {
311
- filter: {
312
- metadata: {
313
- number,
314
- isPullRequest: true,
370
+ members: [
371
+ {
372
+ streamKind: "repo_events",
373
+ filter: {
374
+ metadata: {
375
+ number,
376
+ isPullRequest: true,
377
+ },
378
+ },
379
+ trackedResourceRef,
380
+ cleanupPolicy: { mode: "on_terminal" },
315
381
  },
316
- },
317
- trackedResourceRef: `pr:${number}`,
318
- cleanupPolicy: { mode: "on_terminal" },
382
+ ...(withCi
383
+ ? [{
384
+ streamKind: "ci_runs",
385
+ trackedResourceRef,
386
+ cleanupPolicy: { mode: "on_terminal" },
387
+ }]
388
+ : []),
389
+ ],
319
390
  };
320
391
  }
321
- function projectGithubLifecycleSignal(rawPayload) {
392
+ function projectGithubLifecycleSignal(rawPayload, source) {
322
393
  const eventType = asString(rawPayload.type);
323
394
  if (eventType !== "PullRequestEvent") {
324
395
  return null;
@@ -334,7 +405,7 @@ function projectGithubLifecycleSignal(rawPayload) {
334
405
  }
335
406
  const merged = action === "merged" || asBoolean(rawPayload.merged) === true || asBoolean(pullRequest.merged) === true;
336
407
  return {
337
- ref: `pr:${number}`,
408
+ ref: canonicalGithubPullRequestRef(source, number),
338
409
  terminal: true,
339
410
  state: "closed",
340
411
  result: merged ? "merged" : "closed",
@@ -411,6 +482,64 @@ function extractMentions(...values) {
411
482
  }
412
483
  return Array.from(seen.values()).sort();
413
484
  }
485
+ function computeGithubHydrationPerPage(perPage) {
486
+ const configured = Number.isInteger(perPage) && perPage && perPage > 0
487
+ ? perPage
488
+ : GITHUB_EVENT_HYDRATION_MIN_PER_PAGE;
489
+ return Math.min(GITHUB_EVENT_HYDRATION_MAX_PER_PAGE, Math.max(GITHUB_EVENT_HYDRATION_MIN_PER_PAGE, configured));
490
+ }
491
+ function extractGithubRepoEventUrl(event, issue, pullRequest, review, comment) {
492
+ return (asString(comment.html_url)
493
+ ?? asString(review.html_url)
494
+ ?? asString(issue.html_url)
495
+ ?? asString(pullRequest.html_url)
496
+ ?? asString(comment.pull_request_url)
497
+ ?? asString(review.pull_request_url)
498
+ ?? asString(event.url));
499
+ }
500
+ function extractGithubNumberFromUrl(url) {
501
+ if (!url) {
502
+ return null;
503
+ }
504
+ const match = url.match(/\/(?:pull|pulls|issues)\/(\d+)(?:$|[/?#])/);
505
+ return match ? Number(match[1]) : null;
506
+ }
507
+ function shouldHydrateGithubRepoEvent(raw) {
508
+ const eventType = asString(raw.type);
509
+ if (!eventType || !HYDRATABLE_GITHUB_REPO_EVENT_TYPES.has(eventType)) {
510
+ return false;
511
+ }
512
+ const payload = asRecord(raw.payload);
513
+ const issue = asRecord(payload.issue);
514
+ const pullRequest = asRecord(payload.pull_request);
515
+ const review = asRecord(payload.review);
516
+ const comment = asRecord(payload.comment);
517
+ const url = extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment);
518
+ const number = asNumber(issue.number)
519
+ ?? asNumber(pullRequest.number)
520
+ ?? asNumber(payload.number)
521
+ ?? extractGithubNumberFromUrl(url);
522
+ if (number) {
523
+ return false;
524
+ }
525
+ return [pullRequest, review, comment].some(isUxcPreviewTruncatedObject);
526
+ }
527
+ function hasGithubRepoEventMetadata(raw) {
528
+ const payload = asRecord(raw.payload);
529
+ const issue = asRecord(payload.issue);
530
+ const pullRequest = asRecord(payload.pull_request);
531
+ const review = asRecord(payload.review);
532
+ const comment = asRecord(payload.comment);
533
+ return Boolean(asNumber(issue.number)
534
+ ?? asNumber(pullRequest.number)
535
+ ?? asNumber(payload.number)
536
+ ?? extractGithubNumberFromUrl(extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment))
537
+ ?? asString(review.state)
538
+ ?? extractGithubRepoEventUrl(raw, issue, pullRequest, review, comment));
539
+ }
540
+ function isUxcPreviewTruncatedObject(value) {
541
+ return value._uxc_preview_truncated === true;
542
+ }
414
543
  function parseTargetRef(targetRef) {
415
544
  const match = targetRef.match(/^([^/]+)\/([^#]+)#(\d+)$/);
416
545
  if (!match) {
@@ -473,6 +602,15 @@ function asRecord(value) {
473
602
  function asString(value) {
474
603
  return typeof value === "string" ? value : null;
475
604
  }
605
+ function asIdString(value) {
606
+ if (typeof value === "string") {
607
+ return value;
608
+ }
609
+ if (typeof value === "number" && Number.isFinite(value)) {
610
+ return String(value);
611
+ }
612
+ return null;
613
+ }
476
614
  function asNumber(value) {
477
615
  return typeof value === "number" ? value : null;
478
616
  }
@@ -185,11 +185,12 @@ class RemoteSourceRuntime {
185
185
  if (typeof module.expandSubscriptionShortcut !== "function") {
186
186
  return null;
187
187
  }
188
- return module.expandSubscriptionShortcut({
188
+ const expanded = module.expandSubscriptionShortcut({
189
189
  name: input.name,
190
190
  args: input.args,
191
191
  source: moduleInputSource(source),
192
192
  });
193
+ return normalizeExpandedSubscriptionPlan(expanded);
193
194
  }
194
195
  async deriveInlinePreview(source, item) {
195
196
  if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
@@ -299,7 +300,7 @@ class RemoteSourceRuntime {
299
300
  if (Object.keys(rawPayload).length === 0) {
300
301
  continue;
301
302
  }
302
- const mapped = module.mapRawEvent(rawPayload, moduleSource);
303
+ const mapped = await module.mapRawEvent(rawPayload, moduleSource);
303
304
  if (!mapped) {
304
305
  continue;
305
306
  }
@@ -368,6 +369,32 @@ class RemoteSourceRuntime {
368
369
  }
369
370
  }
370
371
  exports.RemoteSourceRuntime = RemoteSourceRuntime;
372
+ function normalizeExpandedSubscriptionPlan(value) {
373
+ if (!value) {
374
+ return null;
375
+ }
376
+ if (isExpandedSubscriptionPlan(value)) {
377
+ return {
378
+ members: value.members.map((member) => ({
379
+ streamKind: typeof member.streamKind === "string" ? member.streamKind : null,
380
+ filter: member.filter,
381
+ trackedResourceRef: member.trackedResourceRef ?? null,
382
+ cleanupPolicy: member.cleanupPolicy ?? null,
383
+ })),
384
+ };
385
+ }
386
+ return {
387
+ members: [{
388
+ streamKind: null,
389
+ filter: value.filter,
390
+ trackedResourceRef: value.trackedResourceRef ?? null,
391
+ cleanupPolicy: value.cleanupPolicy ?? null,
392
+ }],
393
+ };
394
+ }
395
+ function isExpandedSubscriptionPlan(value) {
396
+ return "members" in value && Array.isArray(value.members);
397
+ }
371
398
  function moduleInputSource(source) {
372
399
  if (source.sourceType !== "remote_source") {
373
400
  return source;
@@ -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) {
204
- return (0, github_1.deriveGithubTrackedResource)(filter);
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) {
255
- return (0, github_1.projectGithubLifecycleSignal)(rawPayload);
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() {
package/dist/src/store.js CHANGED
@@ -595,6 +595,16 @@ class AgentInboxStore {
595
595
  const rows = this.getAll("select * from subscriptions where agent_id = ? order by created_at asc", [agentId]);
596
596
  return rows.map((row) => this.mapSubscription(row));
597
597
  }
598
+ listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
599
+ const rows = this.getAll(`
600
+ select subscriptions.*
601
+ from subscriptions
602
+ inner join sources on sources.source_id = subscriptions.source_id
603
+ where sources.host_id = ? and subscriptions.tracked_resource_ref = ?
604
+ order by subscriptions.created_at asc
605
+ `, [hostId, trackedResourceRef]);
606
+ return rows.map((row) => this.mapSubscription(row));
607
+ }
598
608
  deleteSubscription(subscriptionId) {
599
609
  const subscription = this.getSubscription(subscriptionId);
600
610
  if (!subscription) {
@@ -628,10 +638,10 @@ class AgentInboxStore {
628
638
  : retirement;
629
639
  this.db.run(`
630
640
  insert into subscription_lifecycle_retirements (
631
- subscription_id, source_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
641
+ subscription_id, host_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
632
642
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
633
643
  on conflict(subscription_id) do update set
634
- source_id = excluded.source_id,
644
+ host_id = excluded.host_id,
635
645
  tracked_resource_ref = excluded.tracked_resource_ref,
636
646
  retire_at = excluded.retire_at,
637
647
  terminal_state = excluded.terminal_state,
@@ -641,7 +651,7 @@ class AgentInboxStore {
641
651
  updated_at = excluded.updated_at
642
652
  `, [
643
653
  next.subscriptionId,
644
- next.sourceId,
654
+ next.hostId,
645
655
  next.trackedResourceRef,
646
656
  next.retireAt,
647
657
  next.terminalState ?? null,
@@ -1673,7 +1683,7 @@ class AgentInboxStore {
1673
1683
  mapSubscriptionLifecycleRetirement(row) {
1674
1684
  return {
1675
1685
  subscriptionId: String(row.subscription_id),
1676
- sourceId: String(row.source_id),
1686
+ hostId: String(row.host_id),
1677
1687
  trackedResourceRef: String(row.tracked_resource_ref),
1678
1688
  retireAt: String(row.retire_at),
1679
1689
  terminalState: row.terminal_state ? String(row.terminal_state) : null,
@@ -0,0 +1,42 @@
1
+ pragma foreign_keys=off;
2
+
3
+ create table if not exists subscription_lifecycle_retirements__new (
4
+ subscription_id text primary key,
5
+ host_id text not null,
6
+ tracked_resource_ref text not null,
7
+ retire_at text not null,
8
+ terminal_state text,
9
+ terminal_result text,
10
+ terminal_occurred_at text not null,
11
+ created_at text not null,
12
+ updated_at text not null
13
+ );
14
+
15
+ insert into subscription_lifecycle_retirements__new (
16
+ subscription_id,
17
+ host_id,
18
+ tracked_resource_ref,
19
+ retire_at,
20
+ terminal_state,
21
+ terminal_result,
22
+ terminal_occurred_at,
23
+ created_at,
24
+ updated_at
25
+ )
26
+ select
27
+ retirement.subscription_id,
28
+ coalesce(source.host_id, retirement.source_id),
29
+ retirement.tracked_resource_ref,
30
+ retirement.retire_at,
31
+ retirement.terminal_state,
32
+ retirement.terminal_result,
33
+ retirement.terminal_occurred_at,
34
+ retirement.created_at,
35
+ retirement.updated_at
36
+ from subscription_lifecycle_retirements as retirement
37
+ left join sources as source on source.source_id = retirement.source_id;
38
+
39
+ drop table subscription_lifecycle_retirements;
40
+ alter table subscription_lifecycle_retirements__new rename to subscription_lifecycle_retirements;
41
+
42
+ pragma foreign_keys=on;
@@ -0,0 +1,5 @@
1
+ create index if not exists idx_subscriptions_tracked_resource_ref
2
+ on subscriptions(tracked_resource_ref);
3
+
4
+ create index if not exists idx_subscriptions_tracked_resource_source
5
+ on subscriptions(tracked_resource_ref, source_id);
package/drizzle/schema.ts CHANGED
@@ -97,11 +97,14 @@ export const subscriptions = sqliteTable("subscriptions", {
97
97
  startOffset: integer("start_offset"),
98
98
  startTime: text("start_time"),
99
99
  createdAt: text("created_at").notNull(),
100
- });
100
+ }, (table) => ({
101
+ trackedResourceRefIdx: index("idx_subscriptions_tracked_resource_ref").on(table.trackedResourceRef),
102
+ trackedResourceRefSourceIdx: index("idx_subscriptions_tracked_resource_source").on(table.trackedResourceRef, table.sourceId),
103
+ }));
101
104
 
102
105
  export const subscriptionLifecycleRetirements = sqliteTable("subscription_lifecycle_retirements", {
103
106
  subscriptionId: text("subscription_id").primaryKey(),
104
- sourceId: text("source_id").notNull(),
107
+ hostId: text("host_id").notNull(),
105
108
  trackedResourceRef: text("tracked_resource_ref").notNull(),
106
109
  retireAt: text("retire_at").notNull(),
107
110
  terminalState: text("terminal_state"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {