@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 +5 -1
- package/dist/src/cli.js +40 -4
- package/dist/src/http.js +13 -10
- package/dist/src/service.js +167 -47
- package/dist/src/sources/github.js +158 -20
- package/dist/src/sources/remote.js +29 -2
- package/dist/src/sources/remote_modules.js +162 -157
- package/dist/src/store.js +14 -4
- package/drizzle/migrations/0002_host_scoped_lifecycle_retirements.sql +42 -0
- package/drizzle/migrations/0003_subscription_tracked_resource_indexes.sql +5 -0
- package/drizzle/schema.ts +5 -2
- package/package.json +1 -1
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`
|
|
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
|
|
23
|
-
const
|
|
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: ${
|
|
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:
|
|
1092
|
+
200: subscriptionCollectionSchema,
|
|
1091
1093
|
400: errorResponseSchema,
|
|
1092
1094
|
},
|
|
1093
1095
|
},
|
|
1094
1096
|
}, async (request) => {
|
|
1095
1097
|
const body = request.body;
|
|
1096
|
-
|
|
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: {
|
package/dist/src/service.js
CHANGED
|
@@ -707,26 +707,93 @@ class AgentInboxService {
|
|
|
707
707
|
return result;
|
|
708
708
|
}
|
|
709
709
|
async registerSubscription(input) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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,
|
|
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,
|
|
1561
|
+
scheduleLifecycleRetirements(source, signal) {
|
|
1487
1562
|
if (!signal.terminal) {
|
|
1488
1563
|
return;
|
|
1489
1564
|
}
|
|
1490
|
-
if (!
|
|
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
|
|
1496
|
-
|
|
1497
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|
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
|
-
|
|
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"),
|