@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.
@@ -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.getAgent(input.agentId);
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.getAgent(input.agentId);
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 (0, filter_1.validateSubscriptionFilter)(input.filter ?? {});
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: input.filter ?? {},
816
- trackedResourceRef: normalizeTrackedResourceRef(input.trackedResourceRef),
817
- cleanupPolicy,
818
- startPolicy: input.startPolicy ?? "latest",
819
- startOffset: input.startOffset ?? null,
820
- startTime: input.startTime ?? null,
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
- if (!SUBSCRIPTION_START_POLICIES.has(input.startPolicy)) {
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: input.startPolicy,
902
- startOffset: input.startOffset ?? null,
903
- startTime: input.startTime ?? null,
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: this.store.listSources(),
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
  },