@holon-run/agentinbox 1.0.0-beta.2 → 1.0.0-beta.4
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 +1 -1
- package/dist/src/adapters.js +49 -1
- package/dist/src/cli.js +96 -2
- package/dist/src/current_agent.js +22 -4
- package/dist/src/http.js +53 -3
- package/dist/src/service.js +316 -18
- package/dist/src/source_resolution.js +10 -0
- package/dist/src/source_schema.js +4 -0
- package/dist/src/sources/github.js +218 -1
- package/dist/src/sources/github_ci.js +64 -23
- package/dist/src/sources/remote.js +183 -1
- package/dist/src/sources/remote_modules.js +23 -3
- package/dist/src/store.js +8 -4
- package/dist/src/terminal.js +4 -3
- package/drizzle/migrations/0001_activation_entry_boundary.sql +11 -0
- package/drizzle/schema.ts +2 -0
- package/package.json +2 -2
package/dist/src/service.js
CHANGED
|
@@ -147,6 +147,9 @@ class AgentInboxService {
|
|
|
147
147
|
listSources() {
|
|
148
148
|
return this.store.listSources();
|
|
149
149
|
}
|
|
150
|
+
async listSourceViews() {
|
|
151
|
+
return this.attachSourceRuntimes(this.store.listSources());
|
|
152
|
+
}
|
|
150
153
|
listHosts() {
|
|
151
154
|
return this.store.listSourceHosts();
|
|
152
155
|
}
|
|
@@ -191,8 +194,10 @@ class AgentInboxService {
|
|
|
191
194
|
? schema
|
|
192
195
|
: (0, source_resolution_1.withResolvedIdentity)(source.sourceId, fallbackSchema, resolvedIdentity))
|
|
193
196
|
: fallbackSchema;
|
|
197
|
+
const runtime = await this.adapters.getSourceRuntime(source);
|
|
194
198
|
return {
|
|
195
199
|
source,
|
|
200
|
+
runtime,
|
|
196
201
|
host: source.hostId ? this.store.getSourceHost(source.hostId) : null,
|
|
197
202
|
resolvedIdentity,
|
|
198
203
|
...(resolutionError ? { resolutionError } : {}),
|
|
@@ -299,6 +304,112 @@ class AgentInboxService {
|
|
|
299
304
|
throw new Error(`preview failed: ${message}`);
|
|
300
305
|
}
|
|
301
306
|
}
|
|
307
|
+
async follow(input) {
|
|
308
|
+
const templateArgs = input.templateArgs ?? {};
|
|
309
|
+
const resolved = await this.resolveFollowTemplate(input, templateArgs);
|
|
310
|
+
validateFollowTemplateArgs(resolved.templateSpec.argsSchema ?? [], templateArgs, resolved.templateSpec.templateId);
|
|
311
|
+
const plan = await this.adapters.expandFollowTemplate(resolved.source, {
|
|
312
|
+
template: input.template,
|
|
313
|
+
args: templateArgs,
|
|
314
|
+
source: resolved.source,
|
|
315
|
+
});
|
|
316
|
+
if (!plan) {
|
|
317
|
+
throw new Error(`unknown follow template ${input.providerOrKind}.${input.template}`);
|
|
318
|
+
}
|
|
319
|
+
validateExpandedFollowPlan(plan, resolved.templateSpec.templateId);
|
|
320
|
+
const sourceRefs = new Map();
|
|
321
|
+
const sourceResults = [];
|
|
322
|
+
for (const ensuredSource of plan.sources) {
|
|
323
|
+
const result = await this.ensureFollowSource(ensuredSource);
|
|
324
|
+
sourceRefs.set(ensuredSource.logicalName, result.source);
|
|
325
|
+
sourceResults.push({
|
|
326
|
+
sourceId: result.source.sourceId,
|
|
327
|
+
streamKind: result.source.streamKind,
|
|
328
|
+
created: result.created,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const createdSubscriptions = [];
|
|
332
|
+
const subscriptionResults = [];
|
|
333
|
+
try {
|
|
334
|
+
for (const ensuredSubscription of plan.subscriptions) {
|
|
335
|
+
const source = sourceRefs.get(ensuredSubscription.sourceLogicalName);
|
|
336
|
+
if (!source) {
|
|
337
|
+
throw new Error(`follow template ${plan.templateId} references unknown ensured source ${ensuredSubscription.sourceLogicalName}`);
|
|
338
|
+
}
|
|
339
|
+
const result = await this.ensureSubscription({
|
|
340
|
+
agentId: input.agentId,
|
|
341
|
+
sourceId: source.sourceId,
|
|
342
|
+
filter: ensuredSubscription.filter,
|
|
343
|
+
trackedResourceRef: ensuredSubscription.trackedResourceRef ?? null,
|
|
344
|
+
cleanupPolicy: ensuredSubscription.cleanupPolicy ?? null,
|
|
345
|
+
startPolicy: ensuredSubscription.startPolicy ?? input.startPolicy,
|
|
346
|
+
startOffset: ensuredSubscription.startOffset ?? input.startOffset ?? null,
|
|
347
|
+
startTime: ensuredSubscription.startTime ?? input.startTime ?? null,
|
|
348
|
+
});
|
|
349
|
+
if (result.created) {
|
|
350
|
+
createdSubscriptions.push(result.subscription);
|
|
351
|
+
}
|
|
352
|
+
subscriptionResults.push({
|
|
353
|
+
subscriptionId: result.subscription.subscriptionId,
|
|
354
|
+
sourceId: result.subscription.sourceId,
|
|
355
|
+
created: result.created,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
await this.rollbackRegisteredSubscriptions(createdSubscriptions);
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
templateId: plan.templateId,
|
|
365
|
+
agentId: input.agentId,
|
|
366
|
+
sources: sourceResults,
|
|
367
|
+
subscriptions: subscriptionResults,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async resolveFollowTemplate(input, templateArgs) {
|
|
371
|
+
const previewInput = {
|
|
372
|
+
configRef: input.configRef ?? null,
|
|
373
|
+
config: mergeFollowPreviewConfig(input.config, templateArgs),
|
|
374
|
+
};
|
|
375
|
+
const candidates = this.adapters.followPreviewRefsForProviderOrKind(input.providerOrKind, previewInput.config);
|
|
376
|
+
let previewFailure = null;
|
|
377
|
+
for (const sourceRef of candidates) {
|
|
378
|
+
try {
|
|
379
|
+
const source = buildPreviewSource({
|
|
380
|
+
sourceRef,
|
|
381
|
+
configRef: previewInput.configRef,
|
|
382
|
+
config: previewInput.config,
|
|
383
|
+
});
|
|
384
|
+
const templateSpec = (await this.adapters.listFollowTemplates(source)).find((candidate) => (candidate.providerOrKind === input.providerOrKind || sourceRef === input.providerOrKind) &&
|
|
385
|
+
followTemplateName(candidate.templateId) === input.template);
|
|
386
|
+
if (!templateSpec) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
await this.adapters.sourceAdapterFor(source.sourceType).validateSource?.(source);
|
|
390
|
+
return { source, templateSpec };
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (!previewFailure || sourceRef === input.providerOrKind || candidates.length === 1) {
|
|
394
|
+
previewFailure = error instanceof Error ? error : new Error(String(error));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (previewFailure) {
|
|
399
|
+
throw new Error(`follow preview failed: ${previewFailure.message}`);
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`unknown follow template ${input.providerOrKind}.${input.template}`);
|
|
402
|
+
}
|
|
403
|
+
async ensureFollowSource(input) {
|
|
404
|
+
const existing = this.store.getSourceByKey(input.sourceType, input.sourceKey);
|
|
405
|
+
if (existing) {
|
|
406
|
+
return { source: existing, created: false };
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
source: await this.registerSource(input),
|
|
410
|
+
created: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
302
413
|
async removeSource(sourceId, options = {}) {
|
|
303
414
|
const source = this.store.getSource(sourceId);
|
|
304
415
|
if (!source) {
|
|
@@ -505,7 +616,7 @@ class AgentInboxService {
|
|
|
505
616
|
return { removed: true };
|
|
506
617
|
}
|
|
507
618
|
registerTimer(input) {
|
|
508
|
-
this.
|
|
619
|
+
this.assertAgentNotifyCapable(input.agentId, "timer add");
|
|
509
620
|
const normalized = normalizeTimerInput(input);
|
|
510
621
|
const timezone = normalized.timezone ?? detectHostTimezone();
|
|
511
622
|
assertValidTimeZone(timezone);
|
|
@@ -724,6 +835,38 @@ class AgentInboxService {
|
|
|
724
835
|
}
|
|
725
836
|
return [await this.registerSingleSubscription(input)];
|
|
726
837
|
}
|
|
838
|
+
async ensureSubscription(input) {
|
|
839
|
+
if (input.shortcut) {
|
|
840
|
+
throw new Error("follow ensureSubscription does not support shortcuts");
|
|
841
|
+
}
|
|
842
|
+
const source = this.store.getSource(input.sourceId);
|
|
843
|
+
if (!source) {
|
|
844
|
+
throw new Error(`unknown source: ${input.sourceId}`);
|
|
845
|
+
}
|
|
846
|
+
const normalized = await this.normalizeSubscriptionRegistrationInput(input);
|
|
847
|
+
const existing = this.store.listSubscriptionsForAgent(input.agentId).find((subscription) => subscription.sourceId === source.sourceId &&
|
|
848
|
+
subscription.startPolicy === normalized.startPolicy &&
|
|
849
|
+
(subscription.startOffset ?? null) === normalized.startOffset &&
|
|
850
|
+
(subscription.startTime ?? null) === normalized.startTime &&
|
|
851
|
+
stableJson(subscription.filter) === stableJson(normalized.filter) &&
|
|
852
|
+
normalizeTrackedResourceRef(subscription.trackedResourceRef) === normalized.trackedResourceRef &&
|
|
853
|
+
stableJson(subscription.cleanupPolicy) === stableJson(normalized.cleanupPolicy));
|
|
854
|
+
if (existing) {
|
|
855
|
+
return { subscription: existing, created: false };
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
subscription: await this.registerSingleSubscription({
|
|
859
|
+
...input,
|
|
860
|
+
filter: normalized.filter,
|
|
861
|
+
trackedResourceRef: normalized.trackedResourceRef,
|
|
862
|
+
cleanupPolicy: normalized.cleanupPolicy,
|
|
863
|
+
startPolicy: normalized.startPolicy,
|
|
864
|
+
startOffset: normalized.startOffset,
|
|
865
|
+
startTime: normalized.startTime,
|
|
866
|
+
}),
|
|
867
|
+
created: true,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
727
870
|
async expandShortcutRegistration(input) {
|
|
728
871
|
if (!input.shortcut) {
|
|
729
872
|
return null;
|
|
@@ -794,13 +937,12 @@ class AgentInboxService {
|
|
|
794
937
|
return sibling;
|
|
795
938
|
}
|
|
796
939
|
async registerSingleSubscription(input) {
|
|
797
|
-
this.
|
|
940
|
+
this.assertAgentNotifyCapable(input.agentId, "subscription add");
|
|
798
941
|
const source = this.store.getSource(input.sourceId);
|
|
799
942
|
if (!source) {
|
|
800
943
|
throw new Error(`unknown source: ${input.sourceId}`);
|
|
801
944
|
}
|
|
802
|
-
await
|
|
803
|
-
const cleanupPolicy = normalizeCleanupPolicy(input.cleanupPolicy ?? null);
|
|
945
|
+
const normalized = await this.normalizeSubscriptionRegistrationInput(input);
|
|
804
946
|
const idleState = this.store.getSourceIdleState(source.sourceId);
|
|
805
947
|
if (idleState?.autoPausedAt) {
|
|
806
948
|
await this.resumeSource(source.sourceId);
|
|
@@ -812,12 +954,12 @@ class AgentInboxService {
|
|
|
812
954
|
subscriptionId: (0, util_1.generateCanonicalId)("sub"),
|
|
813
955
|
agentId: input.agentId,
|
|
814
956
|
sourceId: input.sourceId,
|
|
815
|
-
filter:
|
|
816
|
-
trackedResourceRef:
|
|
817
|
-
cleanupPolicy,
|
|
818
|
-
startPolicy:
|
|
819
|
-
startOffset:
|
|
820
|
-
startTime:
|
|
957
|
+
filter: normalized.filter,
|
|
958
|
+
trackedResourceRef: normalized.trackedResourceRef,
|
|
959
|
+
cleanupPolicy: normalized.cleanupPolicy,
|
|
960
|
+
startPolicy: normalized.startPolicy,
|
|
961
|
+
startOffset: normalized.startOffset,
|
|
962
|
+
startTime: normalized.startTime,
|
|
821
963
|
createdAt: (0, util_1.nowIso)(),
|
|
822
964
|
};
|
|
823
965
|
this.ensureInboxForAgent(subscription.agentId);
|
|
@@ -842,6 +984,22 @@ class AgentInboxService {
|
|
|
842
984
|
throw error;
|
|
843
985
|
}
|
|
844
986
|
}
|
|
987
|
+
async normalizeSubscriptionRegistrationInput(input) {
|
|
988
|
+
await (0, filter_1.validateSubscriptionFilter)(input.filter ?? {});
|
|
989
|
+
const start = normalizeSubscriptionStartPosition({
|
|
990
|
+
startPolicy: input.startPolicy ?? "latest",
|
|
991
|
+
startOffset: input.startOffset ?? null,
|
|
992
|
+
startTime: input.startTime ?? null,
|
|
993
|
+
});
|
|
994
|
+
return {
|
|
995
|
+
filter: input.filter ?? {},
|
|
996
|
+
trackedResourceRef: normalizeTrackedResourceRef(input.trackedResourceRef),
|
|
997
|
+
cleanupPolicy: normalizeCleanupPolicy(input.cleanupPolicy ?? null),
|
|
998
|
+
startPolicy: start.startPolicy,
|
|
999
|
+
startOffset: start.startOffset,
|
|
1000
|
+
startTime: start.startTime,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
845
1003
|
async removeSubscription(subscriptionId) {
|
|
846
1004
|
const subscription = this.getSubscription(subscriptionId);
|
|
847
1005
|
await this.backend.deleteConsumer({ subscriptionId });
|
|
@@ -889,18 +1047,16 @@ class AgentInboxService {
|
|
|
889
1047
|
}
|
|
890
1048
|
async resetSubscription(input) {
|
|
891
1049
|
this.getSubscription(input.subscriptionId);
|
|
892
|
-
|
|
893
|
-
throw new Error(`unsupported start policy: ${input.startPolicy}`);
|
|
894
|
-
}
|
|
1050
|
+
const normalizedStart = normalizeSubscriptionStartPosition(input);
|
|
895
1051
|
const consumer = await this.backend.getConsumer({ subscriptionId: input.subscriptionId });
|
|
896
1052
|
if (!consumer) {
|
|
897
1053
|
throw new Error(`unknown consumer for subscription: ${input.subscriptionId}`);
|
|
898
1054
|
}
|
|
899
1055
|
const reset = await this.backend.reset({
|
|
900
1056
|
consumerId: consumer.consumerId,
|
|
901
|
-
startPolicy:
|
|
902
|
-
startOffset:
|
|
903
|
-
startTime:
|
|
1057
|
+
startPolicy: normalizedStart.startPolicy,
|
|
1058
|
+
startOffset: normalizedStart.startOffset,
|
|
1059
|
+
startTime: normalizedStart.startTime,
|
|
904
1060
|
});
|
|
905
1061
|
return {
|
|
906
1062
|
subscription: this.getSubscription(input.subscriptionId),
|
|
@@ -1474,7 +1630,8 @@ class AgentInboxService {
|
|
|
1474
1630
|
const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
|
|
1475
1631
|
return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
|
|
1476
1632
|
}
|
|
1477
|
-
status() {
|
|
1633
|
+
async status() {
|
|
1634
|
+
const sources = await this.listSourceViews();
|
|
1478
1635
|
return {
|
|
1479
1636
|
retention: {
|
|
1480
1637
|
ackedInboxItemsMs: this.ackedRetentionMs,
|
|
@@ -1483,7 +1640,7 @@ class AgentInboxService {
|
|
|
1483
1640
|
},
|
|
1484
1641
|
counts: this.store.getCounts(),
|
|
1485
1642
|
agents: this.store.listAgents(),
|
|
1486
|
-
sources
|
|
1643
|
+
sources,
|
|
1487
1644
|
subscriptions: this.store.listSubscriptions(),
|
|
1488
1645
|
inboxes: this.store.listInboxes(),
|
|
1489
1646
|
activationTargets: this.store.listActivationTargets(),
|
|
@@ -1500,6 +1657,13 @@ class AgentInboxService {
|
|
|
1500
1657
|
},
|
|
1501
1658
|
};
|
|
1502
1659
|
}
|
|
1660
|
+
async attachSourceRuntimes(sources) {
|
|
1661
|
+
const runtimes = await this.adapters.listSourceRuntimes(sources);
|
|
1662
|
+
return sources.map((source) => ({
|
|
1663
|
+
...source,
|
|
1664
|
+
runtime: runtimes.get(source.sourceId) ?? null,
|
|
1665
|
+
}));
|
|
1666
|
+
}
|
|
1503
1667
|
ensureInboxForAgent(agentId) {
|
|
1504
1668
|
const existing = this.store.getInboxByAgentId(agentId);
|
|
1505
1669
|
if (existing) {
|
|
@@ -1595,6 +1759,19 @@ class AgentInboxService {
|
|
|
1595
1759
|
watcher.onItems(items);
|
|
1596
1760
|
}
|
|
1597
1761
|
}
|
|
1762
|
+
assertAgentNotifyCapable(agentId, operation) {
|
|
1763
|
+
this.getAgent(agentId);
|
|
1764
|
+
const targets = this.store.listActivationTargetsForAgent(agentId);
|
|
1765
|
+
const activeTargetCount = targets.filter((target) => target.status === "active").length;
|
|
1766
|
+
if (activeTargetCount > 0) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const offlineTargetCount = targets.filter((target) => target.status === "offline").length;
|
|
1770
|
+
const targetSummary = targets.length === 0
|
|
1771
|
+
? "it has no activation targets"
|
|
1772
|
+
: `all ${offlineTargetCount} activation target(s) are offline`;
|
|
1773
|
+
throw new Error(`agent ${agentId} is not notify-capable: ${targetSummary}; refusing ${operation} to avoid a silent notification black hole. Rebind with \`agentinbox agent register --force-rebind --agent-id ${agentId}\` or resume/add a target first.`);
|
|
1774
|
+
}
|
|
1598
1775
|
resolveAutoAgentId(input) {
|
|
1599
1776
|
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
1600
1777
|
const candidate = (0, terminal_1.assignedAgentIdFromContext)({
|
|
@@ -2170,6 +2347,7 @@ class AgentInboxService {
|
|
|
2170
2347
|
targetKind: target.kind,
|
|
2171
2348
|
subscriptionIds: input.subscriptionIds,
|
|
2172
2349
|
sourceIds: input.sourceIds,
|
|
2350
|
+
latestEntryId: latestEntryIdForNotification(input.entries),
|
|
2173
2351
|
newEntryCount: input.entries.length,
|
|
2174
2352
|
newItemCount: input.newItemCount,
|
|
2175
2353
|
summary,
|
|
@@ -2302,6 +2480,7 @@ class AgentInboxService {
|
|
|
2302
2480
|
totalUnackedCount,
|
|
2303
2481
|
summary,
|
|
2304
2482
|
preview,
|
|
2483
|
+
latestEntryId: latestEntryIdForNotification(entries),
|
|
2305
2484
|
});
|
|
2306
2485
|
return {
|
|
2307
2486
|
prompt,
|
|
@@ -2753,6 +2932,12 @@ function buildPreviewSource(input) {
|
|
|
2753
2932
|
updatedAt: now,
|
|
2754
2933
|
};
|
|
2755
2934
|
}
|
|
2935
|
+
function mergeFollowPreviewConfig(config, templateArgs) {
|
|
2936
|
+
return {
|
|
2937
|
+
...(config ?? {}),
|
|
2938
|
+
...templateArgs,
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2756
2941
|
function sourceTypeForPreviewRef(sourceRef) {
|
|
2757
2942
|
if (sourceRef === "local_event" || sourceRef === "remote_source" || sourceRef === "github_repo" || sourceRef === "github_repo_ci" || sourceRef === "feishu_bot") {
|
|
2758
2943
|
return sourceRef;
|
|
@@ -2762,6 +2947,67 @@ function sourceTypeForPreviewRef(sourceRef) {
|
|
|
2762
2947
|
}
|
|
2763
2948
|
throw new Error(`unknown source kind or type for preview: ${sourceRef}`);
|
|
2764
2949
|
}
|
|
2950
|
+
function followTemplateName(templateId) {
|
|
2951
|
+
const trimmed = templateId.trim();
|
|
2952
|
+
const separator = trimmed.lastIndexOf(".");
|
|
2953
|
+
return separator === -1 ? trimmed : trimmed.slice(separator + 1);
|
|
2954
|
+
}
|
|
2955
|
+
function validateFollowTemplateArgs(argsSchema, args, templateId) {
|
|
2956
|
+
for (const field of argsSchema) {
|
|
2957
|
+
const value = args[field.name];
|
|
2958
|
+
if (field.required && value === undefined) {
|
|
2959
|
+
throw new Error(`follow template ${templateId} requires argument ${field.name}`);
|
|
2960
|
+
}
|
|
2961
|
+
if (value === undefined) {
|
|
2962
|
+
continue;
|
|
2963
|
+
}
|
|
2964
|
+
if (!matchesFollowArgType(value, field.type)) {
|
|
2965
|
+
throw new Error(`follow template ${templateId} expected ${field.name} to be ${field.type}`);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function matchesFollowArgType(value, type) {
|
|
2970
|
+
if (type === "string") {
|
|
2971
|
+
return typeof value === "string";
|
|
2972
|
+
}
|
|
2973
|
+
if (type === "number") {
|
|
2974
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
2975
|
+
}
|
|
2976
|
+
if (type === "boolean") {
|
|
2977
|
+
return typeof value === "boolean";
|
|
2978
|
+
}
|
|
2979
|
+
if (type === "string[]") {
|
|
2980
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
2981
|
+
}
|
|
2982
|
+
if (type === "number[]") {
|
|
2983
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
|
|
2984
|
+
}
|
|
2985
|
+
return true;
|
|
2986
|
+
}
|
|
2987
|
+
function validateExpandedFollowPlan(plan, expectedTemplateId) {
|
|
2988
|
+
if (plan.templateId !== expectedTemplateId) {
|
|
2989
|
+
throw new Error(`follow template ${expectedTemplateId} expanded to unexpected templateId ${plan.templateId}`);
|
|
2990
|
+
}
|
|
2991
|
+
if (plan.sources.length === 0) {
|
|
2992
|
+
throw new Error(`follow template ${plan.templateId} did not produce any sources`);
|
|
2993
|
+
}
|
|
2994
|
+
const logicalNames = new Set();
|
|
2995
|
+
for (const source of plan.sources) {
|
|
2996
|
+
const logicalName = source.logicalName.trim();
|
|
2997
|
+
if (logicalName.length === 0) {
|
|
2998
|
+
throw new Error(`follow template ${plan.templateId} returned an empty source logicalName`);
|
|
2999
|
+
}
|
|
3000
|
+
if (logicalNames.has(logicalName)) {
|
|
3001
|
+
throw new Error(`follow template ${plan.templateId} returned duplicate source logicalName ${logicalName}`);
|
|
3002
|
+
}
|
|
3003
|
+
logicalNames.add(logicalName);
|
|
3004
|
+
}
|
|
3005
|
+
for (const subscription of plan.subscriptions) {
|
|
3006
|
+
if (!logicalNames.has(subscription.sourceLogicalName)) {
|
|
3007
|
+
throw new Error(`follow template ${plan.templateId} references unknown ensured source ${subscription.sourceLogicalName}`);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
2765
3011
|
function normalizeTrackedResourceRef(value) {
|
|
2766
3012
|
if (value == null) {
|
|
2767
3013
|
return null;
|
|
@@ -2769,6 +3015,38 @@ function normalizeTrackedResourceRef(value) {
|
|
|
2769
3015
|
const trimmed = value.trim();
|
|
2770
3016
|
return trimmed.length > 0 ? trimmed : null;
|
|
2771
3017
|
}
|
|
3018
|
+
function normalizeSubscriptionStartPosition(input) {
|
|
3019
|
+
if (!SUBSCRIPTION_START_POLICIES.has(input.startPolicy)) {
|
|
3020
|
+
throw new Error(`unsupported start policy: ${input.startPolicy}`);
|
|
3021
|
+
}
|
|
3022
|
+
const startOffset = input.startOffset ?? null;
|
|
3023
|
+
const startTime = input.startTime ?? null;
|
|
3024
|
+
if (input.startPolicy === "at_offset") {
|
|
3025
|
+
if (startOffset == null || !Number.isInteger(startOffset) || startOffset < 1) {
|
|
3026
|
+
throw new Error("subscriptions requires positive integer startOffset when startPolicy=at_offset");
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
startPolicy: input.startPolicy,
|
|
3030
|
+
startOffset,
|
|
3031
|
+
startTime: null,
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
if (input.startPolicy === "at_time") {
|
|
3035
|
+
if (!startTime || !isValidIsoTimestamp(startTime)) {
|
|
3036
|
+
throw new Error("subscriptions requires valid ISO8601 startTime when startPolicy=at_time");
|
|
3037
|
+
}
|
|
3038
|
+
return {
|
|
3039
|
+
startPolicy: input.startPolicy,
|
|
3040
|
+
startOffset: null,
|
|
3041
|
+
startTime: canonicalIsoTimestamp(startTime),
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
return {
|
|
3045
|
+
startPolicy: input.startPolicy,
|
|
3046
|
+
startOffset: null,
|
|
3047
|
+
startTime: null,
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
2772
3050
|
function normalizeCleanupPolicy(input) {
|
|
2773
3051
|
if (!input) {
|
|
2774
3052
|
return { mode: "manual" };
|
|
@@ -2815,6 +3093,23 @@ function normalizeGracePeriodSecs(value) {
|
|
|
2815
3093
|
}
|
|
2816
3094
|
return value;
|
|
2817
3095
|
}
|
|
3096
|
+
function stableJson(value) {
|
|
3097
|
+
return JSON.stringify(sortJsonValue(value));
|
|
3098
|
+
}
|
|
3099
|
+
function sortJsonValue(value) {
|
|
3100
|
+
if (Array.isArray(value)) {
|
|
3101
|
+
return value.map((entry) => sortJsonValue(entry));
|
|
3102
|
+
}
|
|
3103
|
+
if (!value || typeof value !== "object") {
|
|
3104
|
+
return value;
|
|
3105
|
+
}
|
|
3106
|
+
return Object.keys(value)
|
|
3107
|
+
.sort()
|
|
3108
|
+
.reduce((acc, key) => {
|
|
3109
|
+
acc[key] = sortJsonValue(value[key]);
|
|
3110
|
+
return acc;
|
|
3111
|
+
}, {});
|
|
3112
|
+
}
|
|
2818
3113
|
function normalizeLifecycleSignal(signal, fallbackOccurredAt) {
|
|
2819
3114
|
const ref = typeof signal.ref === "string" ? signal.ref.trim() : "";
|
|
2820
3115
|
if (ref.length === 0) {
|
|
@@ -3117,6 +3412,9 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
|
|
|
3117
3412
|
}
|
|
3118
3413
|
return `${newItemCount} new ${itemWord} in ${inboxId}`;
|
|
3119
3414
|
}
|
|
3415
|
+
function latestEntryIdForNotification(entries) {
|
|
3416
|
+
return entries.length > 0 ? entries[entries.length - 1]?.entryId ?? null : null;
|
|
3417
|
+
}
|
|
3120
3418
|
function latestSummary(entries) {
|
|
3121
3419
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
3122
3420
|
const summary = entries[index]?.summary;
|
|
@@ -65,6 +65,9 @@ async function resolveSourceSchema(source, context = {}) {
|
|
|
65
65
|
shortcuts: typeof module?.listSubscriptionShortcuts === "function"
|
|
66
66
|
? module.listSubscriptionShortcuts(moduleInputSource(source))
|
|
67
67
|
: [],
|
|
68
|
+
followTemplates: typeof module?.listFollowTemplates === "function"
|
|
69
|
+
? module.listFollowTemplates(moduleInputSource(source))
|
|
70
|
+
: undefined,
|
|
68
71
|
}
|
|
69
72
|
: undefined);
|
|
70
73
|
}
|
|
@@ -87,6 +90,13 @@ function withResolvedIdentity(sourceId, schema, identity, capabilityMetadata) {
|
|
|
87
90
|
shortcuts: capabilityMetadata.shortcuts ?? [],
|
|
88
91
|
},
|
|
89
92
|
} : {}),
|
|
93
|
+
...(capabilityMetadata?.followTemplates && capabilityMetadata.followTemplates.length > 0
|
|
94
|
+
? {
|
|
95
|
+
followSchema: {
|
|
96
|
+
templates: capabilityMetadata.followTemplates,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
: {}),
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
function moduleInputSource(source) {
|
|
@@ -88,7 +88,9 @@ const SOURCE_SCHEMAS = {
|
|
|
88
88
|
{ name: "conclusion", type: "string|null", description: "Workflow run conclusion when completed." },
|
|
89
89
|
{ name: "event", type: "string|null", description: "GitHub trigger event for the workflow run." },
|
|
90
90
|
{ name: "headBranch", type: "string|null", description: "Head branch for the workflow run." },
|
|
91
|
+
{ name: "headRepositoryFullName", type: "string|null", description: "Head repository full name for the workflow run when available." },
|
|
91
92
|
{ name: "headSha", type: "string|null", description: "Head commit SHA for the workflow run." },
|
|
93
|
+
{ name: "pullRequestNumbers", type: "number[]", description: "Pull request numbers associated with the workflow run." },
|
|
92
94
|
{ name: "actor", type: "string|null", description: "Actor login for the workflow run." },
|
|
93
95
|
{ name: "commitMessage", type: "string|null", description: "Head commit message when present." },
|
|
94
96
|
{ name: "htmlUrl", type: "string|null", description: "GitHub Actions run URL." },
|
|
@@ -101,6 +103,8 @@ const SOURCE_SCHEMAS = {
|
|
|
101
103
|
conclusion: "failure",
|
|
102
104
|
event: "pull_request",
|
|
103
105
|
head_branch: "main",
|
|
106
|
+
head_repository: { full_name: "holon-run/agentinbox" },
|
|
107
|
+
pull_requests: [{ number: 93 }],
|
|
104
108
|
actor: "jolestar",
|
|
105
109
|
head_commit: { message: "fix ci" },
|
|
106
110
|
},
|