@holon-run/agentinbox 1.0.0-beta.3 → 1.0.0-beta.5
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 +91 -0
- package/dist/src/http.js +53 -3
- package/dist/src/service.js +301 -16
- package/dist/src/source_resolution.js +10 -0
- package/dist/src/source_schema.js +2 -0
- package/dist/src/sources/github.js +218 -6
- package/dist/src/sources/github_ci.js +48 -23
- package/dist/src/sources/remote.js +183 -1
- package/dist/src/sources/remote_modules.js +22 -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) {
|
|
@@ -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;
|
|
@@ -799,8 +942,7 @@ class AgentInboxService {
|
|
|
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) {
|
|
@@ -2183,6 +2347,7 @@ class AgentInboxService {
|
|
|
2183
2347
|
targetKind: target.kind,
|
|
2184
2348
|
subscriptionIds: input.subscriptionIds,
|
|
2185
2349
|
sourceIds: input.sourceIds,
|
|
2350
|
+
latestEntryId: latestEntryIdForNotification(input.entries),
|
|
2186
2351
|
newEntryCount: input.entries.length,
|
|
2187
2352
|
newItemCount: input.newItemCount,
|
|
2188
2353
|
summary,
|
|
@@ -2315,6 +2480,7 @@ class AgentInboxService {
|
|
|
2315
2480
|
totalUnackedCount,
|
|
2316
2481
|
summary,
|
|
2317
2482
|
preview,
|
|
2483
|
+
latestEntryId: latestEntryIdForNotification(entries),
|
|
2318
2484
|
});
|
|
2319
2485
|
return {
|
|
2320
2486
|
prompt,
|
|
@@ -2766,6 +2932,12 @@ function buildPreviewSource(input) {
|
|
|
2766
2932
|
updatedAt: now,
|
|
2767
2933
|
};
|
|
2768
2934
|
}
|
|
2935
|
+
function mergeFollowPreviewConfig(config, templateArgs) {
|
|
2936
|
+
return {
|
|
2937
|
+
...(config ?? {}),
|
|
2938
|
+
...templateArgs,
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2769
2941
|
function sourceTypeForPreviewRef(sourceRef) {
|
|
2770
2942
|
if (sourceRef === "local_event" || sourceRef === "remote_source" || sourceRef === "github_repo" || sourceRef === "github_repo_ci" || sourceRef === "feishu_bot") {
|
|
2771
2943
|
return sourceRef;
|
|
@@ -2775,6 +2947,67 @@ function sourceTypeForPreviewRef(sourceRef) {
|
|
|
2775
2947
|
}
|
|
2776
2948
|
throw new Error(`unknown source kind or type for preview: ${sourceRef}`);
|
|
2777
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
|
+
}
|
|
2778
3011
|
function normalizeTrackedResourceRef(value) {
|
|
2779
3012
|
if (value == null) {
|
|
2780
3013
|
return null;
|
|
@@ -2782,6 +3015,38 @@ function normalizeTrackedResourceRef(value) {
|
|
|
2782
3015
|
const trimmed = value.trim();
|
|
2783
3016
|
return trimmed.length > 0 ? trimmed : null;
|
|
2784
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
|
+
}
|
|
2785
3050
|
function normalizeCleanupPolicy(input) {
|
|
2786
3051
|
if (!input) {
|
|
2787
3052
|
return { mode: "manual" };
|
|
@@ -2828,6 +3093,23 @@ function normalizeGracePeriodSecs(value) {
|
|
|
2828
3093
|
}
|
|
2829
3094
|
return value;
|
|
2830
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
|
+
}
|
|
2831
3113
|
function normalizeLifecycleSignal(signal, fallbackOccurredAt) {
|
|
2832
3114
|
const ref = typeof signal.ref === "string" ? signal.ref.trim() : "";
|
|
2833
3115
|
if (ref.length === 0) {
|
|
@@ -3130,6 +3412,9 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
|
|
|
3130
3412
|
}
|
|
3131
3413
|
return `${newItemCount} new ${itemWord} in ${inboxId}`;
|
|
3132
3414
|
}
|
|
3415
|
+
function latestEntryIdForNotification(entries) {
|
|
3416
|
+
return entries.length > 0 ? entries[entries.length - 1]?.entryId ?? null : null;
|
|
3417
|
+
}
|
|
3133
3418
|
function latestSummary(entries) {
|
|
3134
3419
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
3135
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,6 +88,7 @@ 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." },
|
|
92
93
|
{ name: "pullRequestNumbers", type: "number[]", description: "Pull request numbers associated with the workflow run." },
|
|
93
94
|
{ name: "actor", type: "string|null", description: "Actor login for the workflow run." },
|
|
@@ -102,6 +103,7 @@ const SOURCE_SCHEMAS = {
|
|
|
102
103
|
conclusion: "failure",
|
|
103
104
|
event: "pull_request",
|
|
104
105
|
head_branch: "main",
|
|
106
|
+
head_repository: { full_name: "holon-run/agentinbox" },
|
|
105
107
|
pull_requests: [{ number: 93 }],
|
|
106
108
|
actor: "jolestar",
|
|
107
109
|
head_commit: { message: "fix ci" },
|