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

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.
Files changed (34) hide show
  1. package/README.md +32 -7
  2. package/dist/src/adapters.js +61 -6
  3. package/dist/src/backend.js +4 -16
  4. package/dist/src/cli.js +307 -53
  5. package/dist/src/client.js +2 -2
  6. package/dist/src/daemon.js +36 -2
  7. package/dist/src/http.js +467 -45
  8. package/dist/src/logging.js +112 -0
  9. package/dist/src/paths.js +1 -0
  10. package/dist/src/runtime_gate.js +394 -63
  11. package/dist/src/service.js +856 -154
  12. package/dist/src/source_hosts.js +75 -0
  13. package/dist/src/source_resolution.js +20 -20
  14. package/dist/src/source_schema.js +2 -2
  15. package/dist/src/sources/feishu.js +66 -28
  16. package/dist/src/sources/github.js +188 -24
  17. package/dist/src/sources/remote.js +44 -24
  18. package/dist/src/sources/{remote_profiles.js → remote_modules.js} +143 -58
  19. package/dist/src/store.js +861 -106
  20. package/dist/src/terminal.js +33 -7
  21. package/dist/src/util.js +46 -1
  22. package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
  23. package/drizzle/schema.ts +93 -1
  24. package/package.json +2 -1
  25. package/scripts/iterm2_cursor_probe.py +8 -3
  26. package/scripts/release_smoke.sh +123 -0
  27. package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
  28. package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
  29. package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
  30. package/drizzle/migrations/0004_source_idle_states.sql +0 -10
  31. package/drizzle/migrations/0005_timers.sql +0 -22
  32. package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
  33. package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
  34. package/drizzle/migrations/0008_inbox_item_sequence.sql +0 -21
@@ -9,16 +9,22 @@ const source_resolution_1 = require("./source_resolution");
9
9
  const filter_1 = require("./filter");
10
10
  const terminal_1 = require("./terminal");
11
11
  const runtime_gate_1 = require("./runtime_gate");
12
+ const logging_1 = require("./logging");
13
+ const source_hosts_1 = require("./source_hosts");
12
14
  const DEFAULT_SUBSCRIPTION_POLL_LIMIT = 100;
13
15
  const DEFAULT_ACTIVATION_WINDOW_MS = 3_000;
14
16
  const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
15
17
  const DEFAULT_NOTIFY_LEASE_MS = 10 * 60 * 1000;
16
18
  const DEFAULT_NOTIFY_RETRY_MS = 5_000;
19
+ const MAX_TERMINAL_DEFER_RETRY_MS = 5 * 60 * 1000;
17
20
  const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
18
21
  const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
19
22
  const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
20
23
  const DEFAULT_SYNC_INTERVAL_MS = 2_000;
21
24
  const DEFAULT_IDLE_SOURCE_GRACE_MS = 5 * 60 * 1000;
25
+ const DEFAULT_INBOX_AGGREGATION_WINDOW_MS = 30 * 1000;
26
+ const DEFAULT_INBOX_AGGREGATION_MAX_ITEMS = 20;
27
+ const DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS = 24 * 60 * 60 * 1000;
22
28
  const DIRECT_INBOX_SOURCE_ID = "__agentinbox_direct_text__";
23
29
  const DIRECT_INBOX_EVENT_VARIANT = "agentinbox.direct_text_message";
24
30
  const TIMER_INBOX_SOURCE_ID = "__agentinbox_timer__";
@@ -43,7 +49,7 @@ class AgentInboxService {
43
49
  lastAckedInboxGcAt = 0;
44
50
  lastLifecycleCleanupAt = 0;
45
51
  lastOfflineAgentGcAt = 0;
46
- constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate = new runtime_gate_1.DefaultActivationGate()) {
52
+ constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate, logger = new logging_1.NoopLogger(), gatingPolicy) {
47
53
  this.store = store;
48
54
  this.adapters = adapters;
49
55
  this.activationDispatcher = activationDispatcher;
@@ -52,11 +58,13 @@ class AgentInboxService {
52
58
  this.activationMaxItems = activationPolicy?.maxItems ?? DEFAULT_ACTIVATION_MAX_ITEMS;
53
59
  this.ackedRetentionMs = DEFAULT_ACKED_RETENTION_MS;
54
60
  this.terminalDispatcher = terminalDispatcher;
55
- this.activationGate = activationGate;
61
+ this.logger = logger;
62
+ this.activationGate = activationGate ?? new runtime_gate_1.DefaultActivationGate(undefined, undefined, this.logger.child("gate"), gatingPolicy);
56
63
  }
57
64
  activationDispatcher;
58
65
  terminalDispatcher;
59
66
  activationGate;
67
+ logger;
60
68
  async start() {
61
69
  if (this.syncInterval) {
62
70
  return;
@@ -64,12 +72,14 @@ class AgentInboxService {
64
72
  this.stopping = false;
65
73
  this.syncInterval = setInterval(() => {
66
74
  void this.syncAllSubscriptions();
75
+ void this.syncPendingDigestThreads();
67
76
  void this.syncActivationDispatchStates();
68
77
  void this.runAckedInboxGcIfDue();
69
78
  void this.syncLifecycleGc();
70
79
  }, DEFAULT_SYNC_INTERVAL_MS);
71
80
  this.refreshTimersOnStart();
72
81
  await this.syncAllSubscriptions();
82
+ await this.syncPendingDigestThreads();
73
83
  await this.syncActivationDispatchStates();
74
84
  await this.runAckedInboxGcIfDue(true);
75
85
  await this.syncLifecycleGc();
@@ -99,13 +109,31 @@ class AgentInboxService {
99
109
  if (existing) {
100
110
  return existing;
101
111
  }
112
+ const resolved = (0, source_hosts_1.resolveSourceRegistration)(input);
102
113
  const now = (0, util_1.nowIso)();
114
+ let host = this.store.getSourceHostByKey(resolved.hostType, resolved.hostKey);
115
+ if (!host) {
116
+ host = {
117
+ hostId: (0, util_1.generateCanonicalId)("hst"),
118
+ hostType: resolved.hostType,
119
+ hostKey: resolved.hostKey,
120
+ configRef: input.configRef ?? null,
121
+ config: resolved.hostConfig,
122
+ status: "active",
123
+ createdAt: now,
124
+ updatedAt: now,
125
+ };
126
+ this.store.insertSourceHost(host);
127
+ }
103
128
  const source = {
104
- sourceId: (0, util_1.generateId)("src"),
105
- sourceType: input.sourceType,
129
+ sourceId: (0, util_1.generateCanonicalId)("src"),
130
+ hostId: host.hostId,
131
+ streamKind: resolved.streamKind,
132
+ streamKey: resolved.streamKey,
133
+ sourceType: resolved.sourceType,
106
134
  sourceKey: input.sourceKey,
107
135
  configRef: input.configRef ?? null,
108
- config: input.config ?? {},
136
+ config: resolved.streamConfig,
109
137
  status: "active",
110
138
  checkpoint: null,
111
139
  createdAt: now,
@@ -119,6 +147,25 @@ class AgentInboxService {
119
147
  listSources() {
120
148
  return this.store.listSources();
121
149
  }
150
+ listHosts() {
151
+ return this.store.listSourceHosts();
152
+ }
153
+ getHost(hostId) {
154
+ const host = this.store.getSourceHost(hostId);
155
+ if (!host) {
156
+ throw new Error(`unknown host: ${hostId}`);
157
+ }
158
+ return host;
159
+ }
160
+ getHostDetails(hostId) {
161
+ const host = this.getHost(hostId);
162
+ const streams = this.store.listSources().filter((stream) => stream.hostId === hostId);
163
+ return {
164
+ host,
165
+ schema: this.getHostSchema(host.hostType),
166
+ streams,
167
+ };
168
+ }
122
169
  getSource(sourceId) {
123
170
  const source = this.store.getSource(sourceId);
124
171
  if (!source) {
@@ -146,6 +193,7 @@ class AgentInboxService {
146
193
  : fallbackSchema;
147
194
  return {
148
195
  source,
196
+ host: source.hostId ? this.store.getSourceHost(source.hostId) : null,
149
197
  resolvedIdentity,
150
198
  ...(resolutionError ? { resolutionError } : {}),
151
199
  schema: resolvedSchema,
@@ -157,6 +205,74 @@ class AgentInboxService {
157
205
  getSourceSchema(sourceType) {
158
206
  return (0, source_schema_1.getSourceSchema)(sourceType);
159
207
  }
208
+ getHostSchema(hostType) {
209
+ return {
210
+ hostType,
211
+ configFields: getHostConfigFields(hostType),
212
+ streamKinds: listHostStreamKinds(hostType),
213
+ };
214
+ }
215
+ registerHost(input) {
216
+ const existing = this.store.getSourceHostByKey(input.hostType, input.hostKey);
217
+ if (existing) {
218
+ return existing;
219
+ }
220
+ const now = (0, util_1.nowIso)();
221
+ const host = {
222
+ hostId: (0, util_1.generateCanonicalId)("hst"),
223
+ hostType: input.hostType,
224
+ hostKey: input.hostKey,
225
+ configRef: input.configRef ?? null,
226
+ config: input.config ?? {},
227
+ status: "active",
228
+ createdAt: now,
229
+ updatedAt: now,
230
+ };
231
+ this.store.insertSourceHost(host);
232
+ return host;
233
+ }
234
+ registerStream(input) {
235
+ const host = this.getHost(input.hostId);
236
+ const existing = this.store.listSources().find((stream) => stream.hostId === input.hostId &&
237
+ stream.streamKind === input.streamKind &&
238
+ stream.streamKey === input.streamKey);
239
+ if (existing) {
240
+ return Promise.resolve(existing);
241
+ }
242
+ const sourceType = sourceTypeForStreamRegistration(host.hostType, input.streamKind);
243
+ const now = (0, util_1.nowIso)();
244
+ const stream = {
245
+ sourceId: (0, util_1.generateCanonicalId)("src"),
246
+ streamId: null,
247
+ hostId: input.hostId,
248
+ streamKind: input.streamKind,
249
+ streamKey: input.streamKey,
250
+ sourceType,
251
+ sourceKey: input.streamKey,
252
+ configRef: input.configRef ?? host.configRef ?? null,
253
+ config: input.config ?? {},
254
+ status: "active",
255
+ checkpoint: null,
256
+ createdAt: now,
257
+ updatedAt: now,
258
+ };
259
+ this.store.insertSource(stream);
260
+ return this.adapters.sourceAdapterFor(sourceType)
261
+ .ensureSource(stream)
262
+ .then(async () => {
263
+ await this.ensureStreamForSource(stream);
264
+ return this.getStream(stream.sourceId);
265
+ });
266
+ }
267
+ listStreams() {
268
+ return this.listSources();
269
+ }
270
+ getStream(streamId) {
271
+ return this.getSource(streamId);
272
+ }
273
+ getStreamDetails(streamId) {
274
+ return this.getSourceDetails(streamId);
275
+ }
160
276
  async getResolvedSourceSchema(sourceId) {
161
277
  const source = this.getSource(sourceId);
162
278
  return this.adapters.resolveSourceSchema(source);
@@ -288,9 +404,10 @@ class AgentInboxService {
288
404
  }
289
405
  registerAgent(input) {
290
406
  validateNotifyLeaseMs(input.notifyLeaseMs);
407
+ validateMinUnackedItems(input.minUnackedItems);
291
408
  validateTerminalRegistration(input);
292
409
  const runtimeKind = input.runtimeKind ?? "unknown";
293
- const agentId = input.agentId ?? (0, terminal_1.assignedAgentIdFromContext)({
410
+ const agentId = input.agentId ?? this.resolveAutoAgentId({
294
411
  runtimeKind,
295
412
  runtimeSessionId: input.runtimeSessionId ?? null,
296
413
  backend: input.backend,
@@ -404,7 +521,7 @@ class AgentInboxService {
404
521
  restartRecovery: false,
405
522
  });
406
523
  const timer = {
407
- scheduleId: (0, util_1.generateId)("sched"),
524
+ scheduleId: (0, util_1.generateCanonicalId)("sch"),
408
525
  agentId: input.agentId,
409
526
  status: nextFireAt ? "active" : "paused",
410
527
  mode: normalized.mode,
@@ -487,10 +604,11 @@ class AgentInboxService {
487
604
  addWebhookActivationTarget(agentId, input) {
488
605
  this.getAgent(agentId);
489
606
  validateNotifyLeaseMs(input.notifyLeaseMs);
607
+ validateMinUnackedItems(input.minUnackedItems);
490
608
  const mode = normalizeWebhookActivationMode(input.activationMode);
491
609
  const now = (0, util_1.nowIso)();
492
610
  const target = {
493
- targetId: (0, util_1.generateId)("tgt"),
611
+ targetId: (0, util_1.generateCanonicalId)("tgt"),
494
612
  agentId,
495
613
  kind: "webhook",
496
614
  status: "active",
@@ -501,6 +619,11 @@ class AgentInboxService {
501
619
  mode,
502
620
  url: input.url,
503
621
  notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
622
+ minUnackedItems: input.minUnackedItems ?? null,
623
+ notificationPolicy: {
624
+ notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
625
+ minUnackedItems: input.minUnackedItems ?? null,
626
+ },
504
627
  createdAt: now,
505
628
  updatedAt: now,
506
629
  lastSeenAt: now,
@@ -619,7 +742,7 @@ class AgentInboxService {
619
742
  this.store.deleteSourceIdleState(source.sourceId);
620
743
  }
621
744
  const subscription = {
622
- subscriptionId: (0, util_1.generateId)("sub"),
745
+ subscriptionId: (0, util_1.generateCanonicalId)("sub"),
623
746
  agentId: input.agentId,
624
747
  sourceId: input.sourceId,
625
748
  filter: input.filter ?? {},
@@ -744,15 +867,32 @@ class AgentInboxService {
744
867
  };
745
868
  }
746
869
  listInboxItems(agentId, options) {
870
+ return this.store.listInboxEntries(this.ensureInboxForAgent(agentId).inboxId, options);
871
+ }
872
+ listRawInboxItems(agentId, options) {
747
873
  return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
748
874
  }
875
+ getInboxAggregationPolicy(agentId) {
876
+ const inbox = this.ensureInboxForAgent(agentId);
877
+ return inboxAggregationPolicy(inbox);
878
+ }
879
+ updateInboxAggregationPolicy(agentId, input) {
880
+ const inbox = this.ensureInboxForAgent(agentId);
881
+ const updated = this.store.updateInboxAggregationPolicy(inbox.inboxId, {
882
+ enabled: input.enabled,
883
+ windowMs: input.windowMs,
884
+ maxItems: input.maxItems,
885
+ maxThreadAgeMs: input.maxThreadAgeMs,
886
+ });
887
+ return inboxAggregationPolicy(updated);
888
+ }
749
889
  async addDirectInboxTextMessage(agentId, input, env = process.env) {
750
890
  this.getAgent(agentId);
751
891
  const message = normalizeDirectInboxMessage(input.message);
752
892
  const sender = normalizeDirectInboxSender(input.sender) ?? this.detectDirectInboxSender(env);
753
893
  return this.materializeAgentInboxItem(agentId, {
754
894
  sourceId: DIRECT_INBOX_SOURCE_ID,
755
- sourceNativeId: (0, util_1.generateId)("direct"),
895
+ sourceNativeId: (0, util_1.generateCanonicalId)("direct"),
756
896
  eventVariant: DIRECT_INBOX_EVENT_VARIANT,
757
897
  summary: summarizeDirectInboxMessage(sender),
758
898
  rawPayload: {
@@ -799,7 +939,7 @@ class AgentInboxService {
799
939
  const inbox = this.ensureInboxForAgent(agentId);
800
940
  const occurredAt = input.occurredAt ?? (0, util_1.nowIso)();
801
941
  const item = {
802
- itemId: (0, util_1.generateId)("item"),
942
+ itemId: (0, util_1.generateCanonicalId)("itm"),
803
943
  sourceId: input.sourceId,
804
944
  sourceNativeId: input.sourceNativeId,
805
945
  eventVariant: input.eventVariant,
@@ -821,18 +961,11 @@ class AgentInboxService {
821
961
  activated: false,
822
962
  };
823
963
  }
824
- this.notifyInboxWatchers(agentId, [item]);
825
- const activationItem = {
826
- itemId: item.itemId,
827
- sourceId: item.sourceId,
828
- sourceNativeId: item.sourceNativeId,
829
- eventVariant: item.eventVariant,
830
- inboxId: item.inboxId,
831
- occurredAt: item.occurredAt,
832
- metadata: item.metadata,
833
- rawPayload: item.rawPayload,
834
- deliveryHandle: item.deliveryHandle,
835
- };
964
+ const entry = this.store.createInboxItemEntry(inbox.inboxId, item, {
965
+ summary: input.summary,
966
+ subscriptionIds: [],
967
+ });
968
+ this.notifyInboxWatchers(agentId, [entry]);
836
969
  const targets = this.store.listActivationTargetsForAgent(agentId).filter((target) => target.status === "active");
837
970
  for (const target of targets) {
838
971
  this.enqueueActivationTarget(target, {
@@ -840,7 +973,7 @@ class AgentInboxService {
840
973
  subscriptionId: null,
841
974
  sourceId: input.sourceId,
842
975
  summary: input.summary,
843
- item: activationItem,
976
+ entry,
844
977
  });
845
978
  }
846
979
  return {
@@ -849,27 +982,119 @@ class AgentInboxService {
849
982
  activated: targets.length > 0,
850
983
  };
851
984
  }
985
+ async materializeInboxEntryForItem(input) {
986
+ const activationItem = activationItemFromInboxItem(input.item);
987
+ const policy = inboxAggregationPolicy(input.inbox);
988
+ if (!policy.enabled || !input.source) {
989
+ return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
990
+ summary: input.summary,
991
+ subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
992
+ })];
993
+ }
994
+ const grouping = await this.adapters.deriveNotificationGrouping(input.source, activationItem);
995
+ if (!grouping?.groupable || !grouping.resourceRef || !grouping.eventFamily) {
996
+ return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
997
+ summary: input.summary,
998
+ subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
999
+ })];
1000
+ }
1001
+ const groupKey = digestGroupKey(input.source.sourceId, grouping.resourceRef, grouping.eventFamily);
1002
+ let thread = this.store.getOpenDigestThread(input.inbox.inboxId, groupKey);
1003
+ const now = (0, util_1.nowIso)();
1004
+ if (thread && Date.parse(thread.createdAt) + (policy.maxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS) <= Date.now()) {
1005
+ this.store.closeDigestThread(thread.threadId, now);
1006
+ thread = null;
1007
+ }
1008
+ const flushAfterAt = new Date(Date.now() + (policy.windowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS)).toISOString();
1009
+ if (!thread) {
1010
+ thread = this.store.createDigestThread({
1011
+ inboxId: input.inbox.inboxId,
1012
+ sourceId: input.source.sourceId,
1013
+ groupKey,
1014
+ resourceRef: grouping.resourceRef,
1015
+ eventFamily: grouping.eventFamily,
1016
+ summary: grouping.summaryHint ?? input.summary,
1017
+ firstItemAt: input.item.occurredAt,
1018
+ flushAfterAt,
1019
+ createdAt: now,
1020
+ });
1021
+ }
1022
+ else {
1023
+ thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
1024
+ }
1025
+ if (!thread || this.store.listUnackedInboxItemsForDigestThread(thread.threadId).find((entry) => entry.itemId === input.item.itemId) == null) {
1026
+ thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
1027
+ }
1028
+ const unacked = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
1029
+ const shouldFlush = grouping.flushClass === "immediate" || unacked.length >= (policy.maxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS);
1030
+ if (!shouldFlush) {
1031
+ return [];
1032
+ }
1033
+ const entry = await this.flushDigestThread(input.source, thread.threadId, grouping);
1034
+ return entry ? [entry] : [];
1035
+ }
1036
+ async flushDigestThread(source, threadId, groupingHint) {
1037
+ const thread = this.store.getDigestThread(threadId);
1038
+ if (!thread || thread.status !== "open") {
1039
+ return null;
1040
+ }
1041
+ const items = this.store.listUnackedInboxItemsForDigestThread(threadId);
1042
+ if (items.length === 0) {
1043
+ this.store.closeDigestThread(threadId, (0, util_1.nowIso)());
1044
+ return null;
1045
+ }
1046
+ const activationItems = items.map((item) => activationItemFromInboxItem(item));
1047
+ const grouping = groupingHint ?? {
1048
+ groupable: true,
1049
+ resourceRef: thread.resourceRef,
1050
+ eventFamily: thread.eventFamily,
1051
+ summaryHint: thread.summary,
1052
+ flushClass: "normal",
1053
+ };
1054
+ const moduleSummary = await this.adapters.summarizeDigestThread(source, activationItems, grouping);
1055
+ const summary = moduleSummary ?? `${items.length} ${thread.summary}`;
1056
+ const sourceIds = uniqueSorted(items.map((item) => item.sourceId));
1057
+ const subscriptionIds = uniqueSortedNullable(items.map((item) => typeof item.metadata.subscriptionId === "string" ? item.metadata.subscriptionId : null));
1058
+ const firstItemAt = items[0].occurredAt;
1059
+ const lastItemAt = items[items.length - 1].occurredAt;
1060
+ const deliveryHandle = sharedDeliveryHandle(items);
1061
+ return this.store.materializeDigestSnapshot({
1062
+ threadId,
1063
+ inboxId: thread.inboxId,
1064
+ groupKey: thread.groupKey,
1065
+ resourceRef: thread.resourceRef,
1066
+ eventFamily: thread.eventFamily,
1067
+ summary,
1068
+ sourceIds,
1069
+ subscriptionIds,
1070
+ itemIds: items.map((item) => item.itemId),
1071
+ firstItemAt,
1072
+ lastItemAt,
1073
+ deliveryHandleJson: deliveryHandle ? JSON.stringify(deliveryHandle) : null,
1074
+ createdAt: (0, util_1.nowIso)(),
1075
+ });
1076
+ }
852
1077
  watchInbox(agentId, options, onEvent) {
853
1078
  const inbox = this.ensureInboxForAgent(agentId);
854
1079
  const pendingItems = [];
855
1080
  let started = false;
856
- const emitItems = (items) => {
857
- if (items.length === 0) {
1081
+ const emitItems = (entries) => {
1082
+ if (entries.length === 0) {
858
1083
  return;
859
1084
  }
860
1085
  onEvent({
861
1086
  event: "items",
862
1087
  agentId,
863
- items,
1088
+ entries,
864
1089
  });
865
1090
  };
866
1091
  const watcher = {
867
- onItems: (items) => {
1092
+ onItems: (entries) => {
868
1093
  if (!started) {
869
- pendingItems.push(...items);
1094
+ pendingItems.push(...entries);
870
1095
  return;
871
1096
  }
872
- emitItems(items);
1097
+ emitItems(entries);
873
1098
  },
874
1099
  };
875
1100
  let watchers = this.inboxWatchers.get(agentId);
@@ -880,8 +1105,8 @@ class AgentInboxService {
880
1105
  watchers.add(watcher);
881
1106
  let initialItems;
882
1107
  try {
883
- initialItems = this.store.listInboxItems(inbox.inboxId, {
884
- afterItemId: options.afterItemId,
1108
+ initialItems = this.store.listInboxEntries(inbox.inboxId, {
1109
+ afterEntryId: options.afterEntryId,
885
1110
  includeAcked: options.includeAcked ?? false,
886
1111
  });
887
1112
  }
@@ -892,7 +1117,7 @@ class AgentInboxService {
892
1117
  }
893
1118
  throw error;
894
1119
  }
895
- const initialItemIds = new Set(initialItems.map((item) => item.itemId));
1120
+ const initialItemIds = new Set(initialItems.map((item) => item.entryId));
896
1121
  return {
897
1122
  initialItems,
898
1123
  start: () => {
@@ -900,7 +1125,7 @@ class AgentInboxService {
900
1125
  return;
901
1126
  }
902
1127
  started = true;
903
- const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.itemId));
1128
+ const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.entryId));
904
1129
  pendingItems.length = 0;
905
1130
  emitItems(replayItems);
906
1131
  },
@@ -916,39 +1141,40 @@ class AgentInboxService {
916
1141
  },
917
1142
  };
918
1143
  }
919
- ackInboxItems(agentId, itemIds) {
1144
+ async ackInboxItems(agentId, itemIds) {
920
1145
  const inbox = this.ensureInboxForAgent(agentId);
921
- const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
922
- if (acked > 0) {
923
- void this.handleInboxAckEffects(agentId);
1146
+ const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
1147
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1148
+ await this.handleInboxAckEffects(agentId);
924
1149
  }
925
- return { acked };
1150
+ return { acked: acked.ackedEntries };
926
1151
  }
927
- ackInboxItemsThrough(agentId, itemId) {
1152
+ async ackInboxItemsThrough(agentId, itemId) {
928
1153
  const inbox = this.ensureInboxForAgent(agentId);
929
- const acked = this.store.ackItemsThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
930
- if (acked > 0) {
931
- void this.handleInboxAckEffects(agentId);
1154
+ const acked = this.store.ackInboxEntriesThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
1155
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1156
+ await this.handleInboxAckEffects(agentId);
932
1157
  }
933
- return { acked };
1158
+ return { acked: acked.ackedEntries };
934
1159
  }
935
- ackAllInboxItems(agentId) {
1160
+ async ackAllInboxItems(agentId) {
936
1161
  const inbox = this.ensureInboxForAgent(agentId);
937
- const itemIds = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => item.itemId);
938
- const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
939
- if (acked > 0) {
940
- void this.handleInboxAckEffects(agentId);
1162
+ const itemIds = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).map((item) => item.entryId);
1163
+ const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
1164
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1165
+ await this.handleInboxAckEffects(agentId);
941
1166
  }
942
- return { acked };
1167
+ return { acked: acked.ackedEntries };
943
1168
  }
944
- ackInbox(agentId, input) {
1169
+ async ackInbox(agentId, input) {
945
1170
  if (input.all) {
946
1171
  return this.ackAllInboxItems(agentId);
947
1172
  }
948
- if (input.throughItemId) {
949
- return this.ackInboxItemsThrough(agentId, input.throughItemId);
1173
+ const throughEntryId = input.throughEntryId ?? null;
1174
+ if (throughEntryId) {
1175
+ return this.ackInboxItemsThrough(agentId, throughEntryId);
950
1176
  }
951
- return this.ackInboxItems(agentId, input.itemIds ?? []);
1177
+ return this.ackInboxItems(agentId, input.entryIds ?? []);
952
1178
  }
953
1179
  compactInbox(agentId) {
954
1180
  const inbox = this.ensureInboxForAgent(agentId);
@@ -1013,7 +1239,7 @@ class AgentInboxService {
1013
1239
  let matched = 0;
1014
1240
  let inboxItemsCreated = 0;
1015
1241
  let lastProcessedOffset = null;
1016
- const insertedItems = [];
1242
+ const insertedEntries = [];
1017
1243
  const lifecycleSignals = new Map();
1018
1244
  try {
1019
1245
  for (const event of batch.events) {
@@ -1031,13 +1257,18 @@ class AgentInboxService {
1031
1257
  }
1032
1258
  matched += 1;
1033
1259
  const item = {
1034
- itemId: (0, util_1.generateId)("item"),
1260
+ itemId: (0, util_1.generateCanonicalId)("itm"),
1035
1261
  sourceId: event.sourceId,
1036
1262
  sourceNativeId: event.sourceNativeId,
1037
1263
  eventVariant: event.eventVariant,
1038
1264
  inboxId: inbox.inboxId,
1039
1265
  occurredAt: event.occurredAt,
1040
- metadata: { ...event.metadata, matchReason: match.reason, agentId: subscription.agentId },
1266
+ metadata: {
1267
+ ...event.metadata,
1268
+ matchReason: match.reason,
1269
+ agentId: subscription.agentId,
1270
+ subscriptionId: subscription.subscriptionId,
1271
+ },
1041
1272
  rawPayload: event.rawPayload,
1042
1273
  deliveryHandle: event.deliveryHandle,
1043
1274
  ackedAt: null,
@@ -1047,32 +1278,31 @@ class AgentInboxService {
1047
1278
  continue;
1048
1279
  }
1049
1280
  inboxItemsCreated += 1;
1050
- insertedItems.push(item);
1051
- const activationItem = {
1052
- itemId: item.itemId,
1053
- sourceId: item.sourceId,
1054
- sourceNativeId: item.sourceNativeId,
1055
- eventVariant: item.eventVariant,
1056
- inboxId: item.inboxId,
1057
- occurredAt: item.occurredAt,
1058
- metadata: item.metadata,
1059
- rawPayload: item.rawPayload,
1060
- deliveryHandle: item.deliveryHandle,
1061
- };
1062
- for (const target of targets) {
1063
- this.enqueueActivationTarget(target, {
1064
- agentId: subscription.agentId,
1065
- subscriptionId: subscription.subscriptionId,
1066
- sourceId: source.sourceId,
1067
- summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
1068
- item: activationItem,
1069
- });
1281
+ const entries = await this.materializeInboxEntryForItem({
1282
+ agentId: subscription.agentId,
1283
+ inbox,
1284
+ source,
1285
+ subscriptionId: subscription.subscriptionId,
1286
+ item,
1287
+ summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
1288
+ });
1289
+ insertedEntries.push(...entries);
1290
+ for (const entry of entries) {
1291
+ for (const target of targets) {
1292
+ this.enqueueActivationTarget(target, {
1293
+ agentId: subscription.agentId,
1294
+ subscriptionId: subscription.subscriptionId,
1295
+ sourceId: source.sourceId,
1296
+ summary: entry.summary,
1297
+ entry,
1298
+ });
1299
+ }
1070
1300
  }
1071
1301
  }
1072
1302
  }
1073
1303
  catch (error) {
1074
- if (insertedItems.length > 0) {
1075
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1304
+ if (insertedEntries.length > 0) {
1305
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1076
1306
  }
1077
1307
  if (lastProcessedOffset != null) {
1078
1308
  await this.backend.commit({
@@ -1082,8 +1312,8 @@ class AgentInboxService {
1082
1312
  }
1083
1313
  throw error;
1084
1314
  }
1085
- if (insertedItems.length > 0) {
1086
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1315
+ if (insertedEntries.length > 0) {
1316
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1087
1317
  }
1088
1318
  if (lastProcessedOffset != null) {
1089
1319
  await this.backend.commit({
@@ -1110,8 +1340,9 @@ class AgentInboxService {
1110
1340
  }
1111
1341
  async sendDelivery(request) {
1112
1342
  const handle = resolveDeliveryHandle(request);
1343
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1113
1344
  const attempt = {
1114
- deliveryId: (0, util_1.generateId)("dlv"),
1345
+ deliveryId: (0, util_1.generateCanonicalId)("dlv"),
1115
1346
  provider: handle.provider,
1116
1347
  surface: handle.surface,
1117
1348
  targetRef: handle.targetRef,
@@ -1122,12 +1353,52 @@ class AgentInboxService {
1122
1353
  status: "accepted",
1123
1354
  createdAt: (0, util_1.nowIso)(),
1124
1355
  };
1125
- const adapter = this.adapters.deliveryAdapterFor(handle.provider);
1126
- const result = await adapter.send(request, attempt);
1356
+ const result = source
1357
+ ? await this.sendCanonicalDeliveryViaModule(source, handle, request.payload, attempt)
1358
+ : await this.adapters.deliveryAdapterFor(handle.provider).send(request, attempt);
1127
1359
  const storedAttempt = { ...attempt, status: result.status };
1128
1360
  this.store.insertDelivery(storedAttempt);
1129
1361
  return { ...storedAttempt, note: result.note };
1130
1362
  }
1363
+ async listDeliveryActions(request) {
1364
+ const handle = resolveDeliveryHandle(request);
1365
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1366
+ const operations = await this.adapters.listDeliveryOperations(source, handle);
1367
+ return {
1368
+ sourceId: source?.sourceId ?? null,
1369
+ handle,
1370
+ operations,
1371
+ };
1372
+ }
1373
+ async invokeDelivery(request) {
1374
+ const handle = resolveDeliveryHandle(request);
1375
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1376
+ const attempt = {
1377
+ deliveryId: (0, util_1.generateCanonicalId)("dlv"),
1378
+ provider: handle.provider,
1379
+ surface: handle.surface,
1380
+ targetRef: handle.targetRef,
1381
+ threadRef: handle.threadRef ?? null,
1382
+ replyMode: handle.replyMode ?? null,
1383
+ kind: request.operation,
1384
+ payload: request.input,
1385
+ status: "accepted",
1386
+ createdAt: (0, util_1.nowIso)(),
1387
+ };
1388
+ const result = await this.adapters.invokeDeliveryOperation(source, handle, request.operation, request.input, attempt);
1389
+ const storedAttempt = { ...attempt, status: result.status };
1390
+ this.store.insertDelivery(storedAttempt);
1391
+ return { ...storedAttempt, note: result.note, operation: request.operation };
1392
+ }
1393
+ async sendCanonicalDeliveryViaModule(source, handle, payload, attempt) {
1394
+ const operations = await this.adapters.listDeliveryOperations(source, handle);
1395
+ const canonicalOperation = operations.find((operation) => operation.canonicalTextAlias);
1396
+ if (!canonicalOperation) {
1397
+ throw new Error(`deliver send is not supported for ${handle.provider}/${handle.surface}; use deliver invoke`);
1398
+ }
1399
+ const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
1400
+ return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
1401
+ }
1131
1402
  status() {
1132
1403
  return {
1133
1404
  retention: {
@@ -1160,7 +1431,7 @@ class AgentInboxService {
1160
1431
  return existing;
1161
1432
  }
1162
1433
  const inbox = {
1163
- inboxId: (0, backend_1.defaultInboxIdForAgent)(agentId),
1434
+ inboxId: (0, util_1.generateCanonicalId)("inb"),
1164
1435
  ownerAgentId: agentId,
1165
1436
  createdAt: (0, util_1.nowIso)(),
1166
1437
  };
@@ -1188,7 +1459,7 @@ class AgentInboxService {
1188
1459
  async ensureStreamForSource(source) {
1189
1460
  return this.backend.ensureStream({
1190
1461
  sourceId: source.sourceId,
1191
- streamKey: (0, backend_1.streamKeyForSource)(source.sourceType, source.sourceKey),
1462
+ streamKey: (0, backend_1.streamKeyForSource)(source.sourceId),
1192
1463
  backend: "sqlite",
1193
1464
  });
1194
1465
  }
@@ -1247,6 +1518,29 @@ class AgentInboxService {
1247
1518
  watcher.onItems(items);
1248
1519
  }
1249
1520
  }
1521
+ resolveAutoAgentId(input) {
1522
+ for (let attempt = 0; attempt < 32; attempt += 1) {
1523
+ const candidate = (0, terminal_1.assignedAgentIdFromContext)({
1524
+ runtimeKind: input.runtimeKind,
1525
+ runtimeSessionId: input.runtimeSessionId,
1526
+ backend: input.backend,
1527
+ tmuxPaneId: input.tmuxPaneId,
1528
+ itermSessionId: input.itermSessionId,
1529
+ tty: input.tty,
1530
+ }, attempt);
1531
+ const existing = this.store.getAgent(candidate);
1532
+ if (!existing) {
1533
+ return candidate;
1534
+ }
1535
+ const targets = this.store
1536
+ .listActivationTargetsForAgent(candidate)
1537
+ .filter((target) => target.kind === "terminal");
1538
+ if (targets.length === 0 || targets.some((target) => isSameTerminalIdentity(target, input))) {
1539
+ return candidate;
1540
+ }
1541
+ }
1542
+ throw new Error("unable to derive a unique agentId from terminal context");
1543
+ }
1250
1544
  handleAgentRegistrationConflicts(agentId, input) {
1251
1545
  const currentTarget = findExistingTerminalActivationTarget(this.store, input);
1252
1546
  if (currentTarget && currentTarget.agentId !== agentId) {
@@ -1272,6 +1566,7 @@ class AgentInboxService {
1272
1566
  upsertTerminalActivationTarget(agentId, input, now) {
1273
1567
  const existing = findExistingTerminalActivationTarget(this.store, input);
1274
1568
  if (existing) {
1569
+ const existingPolicy = notificationPolicyForTarget(existing);
1275
1570
  const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
1276
1571
  runtimeKind: input.runtimeKind ?? "unknown",
1277
1572
  runtimeSessionId: input.runtimeSessionId ?? null,
@@ -1280,6 +1575,8 @@ class AgentInboxService {
1280
1575
  tty: input.tty ?? null,
1281
1576
  termProgram: input.termProgram ?? null,
1282
1577
  itermSessionId: input.itermSessionId ?? null,
1578
+ notifyLeaseMs: input.notifyLeaseMs ?? existingPolicy.notifyLeaseMs,
1579
+ minUnackedItems: input.minUnackedItems ?? existingPolicy.minUnackedItems ?? null,
1283
1580
  updatedAt: now,
1284
1581
  lastSeenAt: now,
1285
1582
  });
@@ -1290,7 +1587,7 @@ class AgentInboxService {
1290
1587
  return target;
1291
1588
  }
1292
1589
  const target = {
1293
- targetId: (0, util_1.generateId)("tgt"),
1590
+ targetId: (0, util_1.generateCanonicalId)("tgt"),
1294
1591
  agentId,
1295
1592
  kind: "terminal",
1296
1593
  status: "active",
@@ -1300,6 +1597,11 @@ class AgentInboxService {
1300
1597
  lastError: null,
1301
1598
  mode: "agent_prompt",
1302
1599
  notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
1600
+ minUnackedItems: input.minUnackedItems ?? null,
1601
+ notificationPolicy: {
1602
+ notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
1603
+ minUnackedItems: input.minUnackedItems ?? null,
1604
+ },
1303
1605
  runtimeKind: input.runtimeKind ?? "unknown",
1304
1606
  runtimeSessionId: input.runtimeSessionId ?? null,
1305
1607
  runtimePid: input.runtimePid ?? null,
@@ -1333,7 +1635,7 @@ class AgentInboxService {
1333
1635
  subscriptionId: input.subscriptionId,
1334
1636
  sourceId: input.sourceId,
1335
1637
  summary: input.summary,
1336
- item: input.item,
1638
+ entry: input.entry,
1337
1639
  });
1338
1640
  if (!buffer.timer && !buffer.inFlight) {
1339
1641
  buffer.timer = setTimeout(() => {
@@ -1406,44 +1708,84 @@ class AgentInboxService {
1406
1708
  const entries = buffer.pending.splice(0, buffer.pending.length);
1407
1709
  try {
1408
1710
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1409
- if (!state) {
1711
+ const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
1712
+ if (shouldAttemptDispatch) {
1410
1713
  const inbox = this.ensureInboxForAgent(buffer.agentId);
1411
- const unackedItems = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
1412
- itemId: item.itemId,
1413
- sourceId: item.sourceId,
1414
- sourceNativeId: item.sourceNativeId,
1415
- eventVariant: item.eventVariant,
1416
- inboxId: item.inboxId,
1417
- occurredAt: item.occurredAt,
1418
- metadata: item.metadata,
1419
- rawPayload: item.rawPayload,
1420
- deliveryHandle: item.deliveryHandle,
1421
- }));
1714
+ const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
1422
1715
  const dispatched = await this.dispatchActivationTarget({
1423
1716
  agentId: buffer.agentId,
1424
1717
  targetId: buffer.targetId,
1425
- newItemCount: entries.length,
1718
+ newItemCount: state ? state.pendingNewItemCount + entries.length : entries.length,
1426
1719
  totalUnackedCount: unackedItems.length,
1427
- summary: latestSummary(entries),
1428
- subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1429
- sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
1430
- items: unackedItems,
1720
+ summary: state ? latestSummary(entries) ?? state.pendingSummary : latestSummary(entries),
1721
+ subscriptionIds: state
1722
+ ? uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
1723
+ : uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1724
+ sourceIds: state
1725
+ ? uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
1726
+ : uniqueSorted(entries.map((entry) => entry.sourceId)),
1727
+ entries: unackedItems,
1431
1728
  });
1432
1729
  if (dispatched === "retryable_failure") {
1433
- this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
1730
+ if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
1731
+ this.store.upsertActivationDispatchState({
1732
+ agentId: buffer.agentId,
1733
+ targetId: buffer.targetId,
1734
+ status: "dirty",
1735
+ leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1736
+ lastNotifiedFingerprint: state.lastNotifiedFingerprint,
1737
+ deferReason: null,
1738
+ deferAttempts: 0,
1739
+ firstDeferredAt: null,
1740
+ lastDeferredAt: null,
1741
+ pendingFingerprint: null,
1742
+ pendingNewItemCount: state.pendingNewItemCount + entries.length,
1743
+ pendingSummary: latestSummary(entries) ?? state.pendingSummary,
1744
+ pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
1745
+ pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
1746
+ updatedAt: (0, util_1.nowIso)(),
1747
+ });
1748
+ }
1749
+ else {
1750
+ this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
1751
+ }
1434
1752
  }
1435
1753
  }
1436
1754
  else {
1755
+ const mergedNewItemCount = state.pendingNewItemCount + entries.length;
1756
+ const mergedSummary = latestSummary(entries) ?? state.pendingSummary;
1757
+ const mergedSubscriptionIds = uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]);
1758
+ const mergedSourceIds = uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]);
1759
+ let deferReason = state.deferReason;
1760
+ let deferAttempts = state.deferAttempts;
1761
+ let firstDeferredAt = state.firstDeferredAt;
1762
+ let lastDeferredAt = state.lastDeferredAt;
1763
+ let pendingFingerprint = state.pendingFingerprint;
1764
+ let leaseExpiresAt = state.leaseExpiresAt;
1765
+ if (state.status === "dirty" && state.leaseExpiresAt != null && state.deferReason) {
1766
+ pendingFingerprint = null;
1767
+ this.logger.debug("activation.defer_pending_marked_stale", {
1768
+ agentId: buffer.agentId,
1769
+ targetId: buffer.targetId,
1770
+ deferReason: state.deferReason,
1771
+ deferAttempts: state.deferAttempts,
1772
+ });
1773
+ }
1437
1774
  this.store.upsertActivationDispatchState({
1438
1775
  agentId: buffer.agentId,
1439
1776
  targetId: buffer.targetId,
1440
1777
  status: "dirty",
1441
- leaseExpiresAt: state.leaseExpiresAt,
1778
+ leaseExpiresAt,
1442
1779
  lastNotifiedFingerprint: state.lastNotifiedFingerprint,
1443
- pendingNewItemCount: state.pendingNewItemCount + entries.length,
1444
- pendingSummary: latestSummary(entries) ?? state.pendingSummary,
1445
- pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
1446
- pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
1780
+ deferReason,
1781
+ deferAttempts,
1782
+ firstDeferredAt,
1783
+ lastDeferredAt,
1784
+ pendingFingerprint,
1785
+ pendingNewItemCount: mergedNewItemCount,
1786
+ pendingSummary: mergedSummary,
1787
+ pendingSubscriptionIds: mergedSubscriptionIds,
1788
+ pendingSourceIds: mergedSourceIds,
1447
1789
  updatedAt: (0, util_1.nowIso)(),
1448
1790
  });
1449
1791
  }
@@ -1471,27 +1813,63 @@ class AgentInboxService {
1471
1813
  }
1472
1814
  }
1473
1815
  async handleInboxAckEffects(agentId) {
1816
+ await this.reconcileDigestThreadsForInbox(this.ensureInboxForAgent(agentId).inboxId);
1474
1817
  const states = this.store.listActivationDispatchStatesForAgent(agentId);
1475
1818
  for (const state of states) {
1476
1819
  await this.maybeDispatchActivationTarget(agentId, state.targetId, "ack");
1477
1820
  }
1478
1821
  }
1822
+ async reconcileDigestThreadsForInbox(inboxId) {
1823
+ for (const thread of this.store.listOpenDigestThreadsForInbox(inboxId)) {
1824
+ const source = this.store.getSource(thread.sourceId);
1825
+ if (!source) {
1826
+ this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
1827
+ continue;
1828
+ }
1829
+ const items = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
1830
+ if (items.length === 0) {
1831
+ this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
1832
+ continue;
1833
+ }
1834
+ const head = thread.latestEntryId != null ? this.store.getInboxEntry(thread.latestEntryId) : null;
1835
+ const currentIds = items.map((item) => item.itemId);
1836
+ const headIds = head?.itemIds ?? [];
1837
+ if (!sameStringArray(currentIds, headIds)) {
1838
+ await this.flushDigestThread(source, thread.threadId);
1839
+ }
1840
+ }
1841
+ }
1479
1842
  async maybeDispatchActivationTarget(agentId, targetId, reason) {
1480
1843
  const state = this.store.getActivationDispatchState(agentId, targetId);
1481
1844
  if (!state) {
1482
1845
  return;
1483
1846
  }
1847
+ const target = this.getActivationTarget(targetId);
1484
1848
  const inbox = this.ensureInboxForAgent(agentId);
1485
- const unacked = this.store.countInboxItems(inbox.inboxId, false);
1849
+ const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1486
1850
  if (unacked === 0) {
1487
1851
  this.store.deleteActivationDispatchState(agentId, targetId);
1488
1852
  return;
1489
1853
  }
1854
+ if (!meetsNotificationThreshold(notificationPolicyForTarget(target), unacked)) {
1855
+ this.store.upsertActivationDispatchState({
1856
+ ...state,
1857
+ status: "dirty",
1858
+ leaseExpiresAt: null,
1859
+ deferReason: null,
1860
+ deferAttempts: 0,
1861
+ firstDeferredAt: null,
1862
+ lastDeferredAt: null,
1863
+ pendingFingerprint: null,
1864
+ pendingNewItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
1865
+ updatedAt: (0, util_1.nowIso)(),
1866
+ });
1867
+ return;
1868
+ }
1490
1869
  if (reason === "ack" && state.status !== "dirty") {
1491
1870
  return;
1492
1871
  }
1493
1872
  if (reason === "lease" && state.status === "notified") {
1494
- const target = this.getActivationTarget(targetId);
1495
1873
  if (target.kind !== "terminal") {
1496
1874
  // Webhook targets keep the existing lease-based behavior.
1497
1875
  }
@@ -1512,17 +1890,7 @@ class AgentInboxService {
1512
1890
  summary: state.pendingSummary,
1513
1891
  subscriptionIds: state.pendingSubscriptionIds,
1514
1892
  sourceIds: state.pendingSourceIds,
1515
- items: this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
1516
- itemId: item.itemId,
1517
- sourceId: item.sourceId,
1518
- sourceNativeId: item.sourceNativeId,
1519
- eventVariant: item.eventVariant,
1520
- inboxId: item.inboxId,
1521
- occurredAt: item.occurredAt,
1522
- metadata: item.metadata,
1523
- rawPayload: item.rawPayload,
1524
- deliveryHandle: item.deliveryHandle,
1525
- })),
1893
+ entries: this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }),
1526
1894
  });
1527
1895
  if (dispatched === "offline") {
1528
1896
  this.store.deleteActivationDispatchState(agentId, targetId);
@@ -1533,6 +1901,11 @@ class AgentInboxService {
1533
1901
  ...state,
1534
1902
  status: "dirty",
1535
1903
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1904
+ deferReason: null,
1905
+ deferAttempts: 0,
1906
+ firstDeferredAt: null,
1907
+ lastDeferredAt: null,
1908
+ pendingFingerprint: null,
1536
1909
  updatedAt: (0, util_1.nowIso)(),
1537
1910
  });
1538
1911
  }
@@ -1545,23 +1918,94 @@ class AgentInboxService {
1545
1918
  const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
1546
1919
  const inbox = this.ensureInboxForAgent(input.agentId);
1547
1920
  const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
1548
- const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
1921
+ const totalUnackedCount = input.totalUnackedCount ?? this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1922
+ const notificationPolicy = notificationPolicyForTarget(target);
1923
+ if (!meetsNotificationThreshold(notificationPolicy, totalUnackedCount)) {
1924
+ this.logger.debug("activation.dispatch_suppressed", {
1925
+ agentId: input.agentId,
1926
+ targetId: target.targetId,
1927
+ reason: "min_unacked_items",
1928
+ totalUnackedCount,
1929
+ minUnackedItems: notificationPolicy.minUnackedItems ?? null,
1930
+ });
1931
+ this.store.upsertActivationDispatchState({
1932
+ agentId: input.agentId,
1933
+ targetId: input.targetId,
1934
+ status: "dirty",
1935
+ leaseExpiresAt: null,
1936
+ lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
1937
+ deferReason: null,
1938
+ deferAttempts: 0,
1939
+ firstDeferredAt: null,
1940
+ lastDeferredAt: null,
1941
+ pendingFingerprint: null,
1942
+ pendingNewItemCount: input.newItemCount,
1943
+ pendingSummary: input.summary,
1944
+ pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
1945
+ pendingSourceIds: uniqueSorted(input.sourceIds),
1946
+ updatedAt: (0, util_1.nowIso)(),
1947
+ });
1948
+ return "suppressed";
1949
+ }
1549
1950
  let terminalPrompt = null;
1951
+ let terminalPromptFingerprint = null;
1550
1952
  try {
1551
1953
  if (target.kind === "terminal") {
1552
1954
  const gate = await this.activationGate.evaluate(target);
1955
+ this.logger.debug("activation.gate_decision", {
1956
+ agentId: input.agentId,
1957
+ targetId: target.targetId,
1958
+ runtimeKind: target.runtimeKind,
1959
+ backend: target.backend,
1960
+ outcome: gate.outcome,
1961
+ reason: gate.reason,
1962
+ });
1553
1963
  if (gate.outcome === "offline") {
1964
+ this.logger.info("activation.target_offline", {
1965
+ agentId: input.agentId,
1966
+ targetId: target.targetId,
1967
+ reason: gate.reason,
1968
+ transition: "runtime_gate",
1969
+ });
1554
1970
  this.markActivationTargetOffline(target.targetId, `runtime gate: ${gate.reason}`);
1555
1971
  this.reconcileAgentStatus(target.agentId);
1556
1972
  return "offline";
1557
1973
  }
1974
+ const reminder = await this.buildTerminalReminder(target, inbox.inboxId, totalUnackedCount, input.summary, input.entries);
1975
+ const prompt = reminder.prompt;
1976
+ const promptFingerprint = reminder.fingerprint;
1977
+ terminalPrompt = prompt;
1978
+ terminalPromptFingerprint = promptFingerprint;
1558
1979
  if (gate.outcome === "defer") {
1980
+ const deferReason = toActivationDispatchDeferReason(gate.reason, this.logger);
1981
+ const hasExistingDeferred = state?.deferReason != null;
1982
+ const deferAttempts = hasExistingDeferred ? state.deferAttempts + 1 : 1;
1983
+ const deferredAt = (0, util_1.nowIso)();
1984
+ const retryMs = terminalDeferRetryMs(deferAttempts);
1985
+ const existingLeaseMs = state?.leaseExpiresAt == null ? Number.NaN : Date.parse(state.leaseExpiresAt);
1986
+ const candidateLeaseMs = Date.now() + retryMs;
1987
+ const nextLeaseMs = Number.isNaN(existingLeaseMs)
1988
+ ? candidateLeaseMs
1989
+ : Math.max(existingLeaseMs, candidateLeaseMs);
1990
+ this.logger.debug("activation.dispatch_deferred", {
1991
+ agentId: input.agentId,
1992
+ targetId: target.targetId,
1993
+ reason: deferReason,
1994
+ retryMs,
1995
+ deferAttempts,
1996
+ pendingFingerprint: promptFingerprint,
1997
+ });
1559
1998
  this.store.upsertActivationDispatchState({
1560
1999
  agentId: input.agentId,
1561
2000
  targetId: input.targetId,
1562
2001
  status: "dirty",
1563
- leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2002
+ leaseExpiresAt: new Date(nextLeaseMs).toISOString(),
1564
2003
  lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
2004
+ deferReason,
2005
+ deferAttempts,
2006
+ firstDeferredAt: state?.firstDeferredAt ?? deferredAt,
2007
+ lastDeferredAt: deferredAt,
2008
+ pendingFingerprint: promptFingerprint,
1565
2009
  pendingNewItemCount: input.newItemCount,
1566
2010
  pendingSummary: input.summary,
1567
2011
  pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
@@ -1570,36 +2014,32 @@ class AgentInboxService {
1570
2014
  });
1571
2015
  return "deferred";
1572
2016
  }
1573
- let preview = null;
1574
- if (totalUnackedCount === 1 && input.items.length === 1) {
1575
- const singleItem = input.items[0];
1576
- const source = this.store.getSource(singleItem.sourceId);
1577
- if (source) {
1578
- try {
1579
- const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
1580
- preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
1581
- }
1582
- catch {
1583
- preview = null;
1584
- }
1585
- }
1586
- preview ??= (0, terminal_1.deriveInlineItemPreview)(singleItem, input.summary);
2017
+ if (state?.pendingFingerprint && state.pendingFingerprint !== promptFingerprint) {
2018
+ this.logger.debug("activation.defer_pending_revalidated", {
2019
+ agentId: input.agentId,
2020
+ targetId: target.targetId,
2021
+ previousFingerprint: state.pendingFingerprint,
2022
+ currentFingerprint: promptFingerprint,
2023
+ });
1587
2024
  }
1588
- const prompt = (0, terminal_1.renderAgentPrompt)({
1589
- inboxId: inbox.inboxId,
1590
- totalUnackedCount,
1591
- summary: input.summary,
1592
- preview,
1593
- });
1594
- terminalPrompt = prompt;
1595
- const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
1596
- if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
2025
+ if (state?.lastNotifiedFingerprint === promptFingerprint) {
2026
+ this.logger.debug("activation.dispatch_suppressed", {
2027
+ agentId: input.agentId,
2028
+ targetId: target.targetId,
2029
+ reason: "unchanged_terminal_prompt",
2030
+ status: state.status,
2031
+ });
1597
2032
  this.store.upsertActivationDispatchState({
1598
2033
  agentId: input.agentId,
1599
2034
  targetId: input.targetId,
1600
2035
  status: "notified",
1601
2036
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1602
2037
  lastNotifiedFingerprint: promptFingerprint,
2038
+ deferReason: null,
2039
+ deferAttempts: 0,
2040
+ firstDeferredAt: null,
2041
+ lastDeferredAt: null,
2042
+ pendingFingerprint: null,
1603
2043
  pendingNewItemCount: 0,
1604
2044
  pendingSummary: null,
1605
2045
  pendingSubscriptionIds: [],
@@ -1608,21 +2048,39 @@ class AgentInboxService {
1608
2048
  });
1609
2049
  return "suppressed";
1610
2050
  }
2051
+ this.logger.debug("activation.terminal_dispatch_start", {
2052
+ agentId: input.agentId,
2053
+ targetId: target.targetId,
2054
+ totalUnackedCount,
2055
+ newItemCount: input.newItemCount,
2056
+ sourceIds: input.sourceIds,
2057
+ subscriptionIds: input.subscriptionIds,
2058
+ });
1611
2059
  await this.terminalDispatcher.dispatch(target, prompt);
2060
+ this.logger.debug("activation.terminal_dispatch_succeeded", {
2061
+ agentId: input.agentId,
2062
+ targetId: target.targetId,
2063
+ });
1612
2064
  }
1613
2065
  else {
1614
2066
  const activation = {
1615
2067
  kind: "agentinbox.activation",
1616
- activationId: (0, util_1.generateId)("act"),
2068
+ activationId: (0, util_1.generateCanonicalId)("act"),
1617
2069
  agentId: input.agentId,
1618
2070
  inboxId: inbox.inboxId,
1619
2071
  targetId: target.targetId,
1620
2072
  targetKind: target.kind,
1621
2073
  subscriptionIds: input.subscriptionIds,
1622
2074
  sourceIds: input.sourceIds,
2075
+ newEntryCount: input.entries.length,
1623
2076
  newItemCount: input.newItemCount,
1624
2077
  summary,
1625
- items: target.mode === "activation_with_items" && input.items.length > 0 ? input.items : undefined,
2078
+ items: target.mode === "activation_with_items"
2079
+ ? input.entries
2080
+ .filter((entry) => entry.kind === "item")
2081
+ .map((entry) => entry.item)
2082
+ : undefined,
2083
+ entries: target.mode === "activation_with_items" && input.entries.length > 0 ? input.entries : undefined,
1626
2084
  createdAt: (0, util_1.nowIso)(),
1627
2085
  deliveredAt: null,
1628
2086
  };
@@ -1637,8 +2095,13 @@ class AgentInboxService {
1637
2095
  status: "notified",
1638
2096
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1639
2097
  lastNotifiedFingerprint: target.kind === "terminal"
1640
- ? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
2098
+ ? terminalPromptFingerprint
1641
2099
  : state?.lastNotifiedFingerprint ?? null,
2100
+ deferReason: null,
2101
+ deferAttempts: 0,
2102
+ firstDeferredAt: null,
2103
+ lastDeferredAt: null,
2104
+ pendingFingerprint: null,
1642
2105
  pendingNewItemCount: 0,
1643
2106
  pendingSummary: null,
1644
2107
  pendingSubscriptionIds: [],
@@ -1648,11 +2111,28 @@ class AgentInboxService {
1648
2111
  return "dispatched";
1649
2112
  }
1650
2113
  catch (error) {
1651
- console.warn(`activation target dispatch failed for ${target.targetId}:`, error);
1652
2114
  const message = error instanceof Error ? error.message : String(error);
2115
+ this.logger.warn("activation.dispatch_failed", {
2116
+ agentId: input.agentId,
2117
+ targetId: target.targetId,
2118
+ targetKind: target.kind,
2119
+ error,
2120
+ message,
2121
+ });
1653
2122
  if (target.kind === "terminal") {
1654
2123
  const exists = await this.terminalDispatcher.probe(target);
2124
+ this.logger.debug("activation.terminal_probe_after_failure", {
2125
+ agentId: input.agentId,
2126
+ targetId: target.targetId,
2127
+ exists,
2128
+ });
1655
2129
  if (!exists) {
2130
+ this.logger.info("activation.target_offline", {
2131
+ agentId: input.agentId,
2132
+ targetId: target.targetId,
2133
+ reason: message,
2134
+ transition: "dispatch_probe",
2135
+ });
1656
2136
  this.markActivationTargetOffline(target.targetId, message);
1657
2137
  this.reconcileAgentStatus(target.agentId);
1658
2138
  return "offline";
@@ -1669,6 +2149,11 @@ class AgentInboxService {
1669
2149
  status: "dirty",
1670
2150
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1671
2151
  lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
2152
+ deferReason: null,
2153
+ deferAttempts: 0,
2154
+ firstDeferredAt: null,
2155
+ lastDeferredAt: null,
2156
+ pendingFingerprint: null,
1672
2157
  pendingNewItemCount: entries.length,
1673
2158
  pendingSummary: latestSummary(entries),
1674
2159
  pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
@@ -1676,6 +2161,33 @@ class AgentInboxService {
1676
2161
  updatedAt: (0, util_1.nowIso)(),
1677
2162
  });
1678
2163
  }
2164
+ async buildTerminalReminder(target, inboxId, totalUnackedCount, summary, entries) {
2165
+ let preview = null;
2166
+ if (totalUnackedCount === 1 && entries.length === 1 && entries[0]?.kind === "item") {
2167
+ const singleItem = entries[0].item;
2168
+ const source = this.store.getSource(singleItem.sourceId);
2169
+ if (source) {
2170
+ try {
2171
+ const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
2172
+ preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
2173
+ }
2174
+ catch {
2175
+ preview = null;
2176
+ }
2177
+ }
2178
+ preview ??= (0, terminal_1.deriveInlineItemPreview)(singleItem, summary);
2179
+ }
2180
+ const prompt = (0, terminal_1.renderAgentPrompt)({
2181
+ inboxId,
2182
+ totalUnackedCount,
2183
+ summary,
2184
+ preview,
2185
+ });
2186
+ return {
2187
+ prompt,
2188
+ fingerprint: terminalReminderFingerprint(prompt, entries),
2189
+ };
2190
+ }
1679
2191
  detectDirectInboxSender(env) {
1680
2192
  try {
1681
2193
  const detected = (0, terminal_1.detectTerminalContext)(env);
@@ -1949,6 +2461,43 @@ class AgentInboxService {
1949
2461
  }
1950
2462
  }
1951
2463
  }
2464
+ async syncPendingDigestThreads(force = false) {
2465
+ const now = (0, util_1.nowIso)();
2466
+ const threads = force
2467
+ ? this.store.listOpenDigestThreads()
2468
+ : this.store.listDueDigestThreads(now);
2469
+ for (const thread of threads) {
2470
+ try {
2471
+ const source = this.store.getSource(thread.sourceId);
2472
+ if (!source) {
2473
+ this.store.closeDigestThread(thread.threadId, now);
2474
+ continue;
2475
+ }
2476
+ const entry = await this.flushDigestThread(source, thread.threadId);
2477
+ if (!entry) {
2478
+ continue;
2479
+ }
2480
+ const inbox = this.store.getInbox(thread.inboxId);
2481
+ if (!inbox) {
2482
+ continue;
2483
+ }
2484
+ this.notifyInboxWatchers(inbox.ownerAgentId, [entry]);
2485
+ const targets = this.store.listActivationTargetsForAgent(inbox.ownerAgentId).filter((target) => target.status === "active");
2486
+ for (const target of targets) {
2487
+ this.enqueueActivationTarget(target, {
2488
+ agentId: inbox.ownerAgentId,
2489
+ subscriptionId: null,
2490
+ sourceId: thread.sourceId,
2491
+ summary: entry.summary,
2492
+ entry,
2493
+ });
2494
+ }
2495
+ }
2496
+ catch (error) {
2497
+ console.warn(`digest thread flush failed for ${thread.threadId}:`, error);
2498
+ }
2499
+ }
2500
+ }
1952
2501
  async syncLifecycleGc() {
1953
2502
  const now = Date.now();
1954
2503
  if (now - this.lastLifecycleCleanupAt >= DEFAULT_GC_INTERVAL_MS) {
@@ -2253,6 +2802,29 @@ function resolveDeliveryHandle(request) {
2253
2802
  replyMode: request.replyMode ?? null,
2254
2803
  };
2255
2804
  }
2805
+ function resolveDeliverySource(store, sourceId, handle) {
2806
+ if (!sourceId) {
2807
+ return null;
2808
+ }
2809
+ const source = store.getSource(sourceId);
2810
+ if (!source) {
2811
+ throw new Error(`unknown source: ${sourceId}`);
2812
+ }
2813
+ const expectedProvider = providerForSourceType(source.sourceType);
2814
+ if (expectedProvider && expectedProvider !== handle.provider) {
2815
+ throw new Error(`source ${sourceId} does not match delivery provider ${handle.provider}`);
2816
+ }
2817
+ return source;
2818
+ }
2819
+ function providerForSourceType(sourceType) {
2820
+ if (sourceType === "github_repo") {
2821
+ return "github";
2822
+ }
2823
+ if (sourceType === "feishu_bot") {
2824
+ return "feishu";
2825
+ }
2826
+ return null;
2827
+ }
2256
2828
  class ActivationDispatcher {
2257
2829
  async dispatch(targetUrl, activation) {
2258
2830
  try {
@@ -2295,6 +2867,76 @@ function validateNotifyLeaseMs(value) {
2295
2867
  throw new Error("notifyLeaseMs must be a positive integer");
2296
2868
  }
2297
2869
  }
2870
+ function validateMinUnackedItems(value) {
2871
+ if (value == null) {
2872
+ return;
2873
+ }
2874
+ if (!Number.isInteger(value) || value <= 0) {
2875
+ throw new Error("minUnackedItems must be a positive integer");
2876
+ }
2877
+ }
2878
+ function notificationPolicyForTarget(target) {
2879
+ return target.notificationPolicy ?? {
2880
+ notifyLeaseMs: target.notifyLeaseMs,
2881
+ minUnackedItems: target.minUnackedItems ?? null,
2882
+ };
2883
+ }
2884
+ function inboxAggregationPolicy(inbox) {
2885
+ return {
2886
+ enabled: inbox.aggregationEnabled ?? false,
2887
+ windowMs: inbox.aggregationWindowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS,
2888
+ maxItems: inbox.aggregationMaxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS,
2889
+ maxThreadAgeMs: inbox.aggregationMaxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS,
2890
+ };
2891
+ }
2892
+ function meetsNotificationThreshold(policy, totalUnackedCount) {
2893
+ if (policy.minUnackedItems == null) {
2894
+ return true;
2895
+ }
2896
+ return totalUnackedCount >= policy.minUnackedItems;
2897
+ }
2898
+ function activationItemFromInboxItem(item) {
2899
+ return {
2900
+ itemId: item.itemId,
2901
+ sourceId: item.sourceId,
2902
+ sourceNativeId: item.sourceNativeId,
2903
+ eventVariant: item.eventVariant,
2904
+ inboxId: item.inboxId,
2905
+ occurredAt: item.occurredAt,
2906
+ metadata: item.metadata,
2907
+ rawPayload: item.rawPayload,
2908
+ deliveryHandle: item.deliveryHandle,
2909
+ };
2910
+ }
2911
+ function digestGroupKey(sourceId, resourceRef, eventFamily) {
2912
+ return `${sourceId}::${resourceRef}::${eventFamily}`;
2913
+ }
2914
+ function sameStringArray(left, right) {
2915
+ if (left.length !== right.length) {
2916
+ return false;
2917
+ }
2918
+ for (let index = 0; index < left.length; index += 1) {
2919
+ if (left[index] !== right[index]) {
2920
+ return false;
2921
+ }
2922
+ }
2923
+ return true;
2924
+ }
2925
+ function sameDeliveryHandle(left, right) {
2926
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
2927
+ }
2928
+ function sharedDeliveryHandle(items) {
2929
+ const first = items[0]?.deliveryHandle ?? null;
2930
+ if (!first) {
2931
+ return null;
2932
+ }
2933
+ for (const item of items.slice(1)) {
2934
+ if (!sameDeliveryHandle(first, item.deliveryHandle ?? null)) {
2935
+ return null;
2936
+ }
2937
+ }
2938
+ return first;
2939
+ }
2298
2940
  function normalizeWebhookActivationMode(mode) {
2299
2941
  const resolved = mode ?? "activation_only";
2300
2942
  if (!WEBHOOK_ACTIVATION_MODES.has(resolved)) {
@@ -2368,11 +3010,26 @@ function terminalReminderFingerprint(prompt, items) {
2368
3010
  const payload = JSON.stringify({
2369
3011
  prompt,
2370
3012
  itemIds: items
2371
- .map((item) => item.itemId)
3013
+ .map((item) => item.entryId)
2372
3014
  .sort((left, right) => left.localeCompare(right)),
2373
3015
  });
2374
3016
  return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
2375
3017
  }
3018
+ function toActivationDispatchDeferReason(reason, logger) {
3019
+ if (reason === "terminal_recently_active" || reason === "terminal_busy") {
3020
+ return reason;
3021
+ }
3022
+ logger?.warn("activation.unexpected_defer_reason", {
3023
+ reason,
3024
+ fallback: reason,
3025
+ });
3026
+ return reason;
3027
+ }
3028
+ function terminalDeferRetryMs(attempts) {
3029
+ const normalizedAttempts = Math.max(1, attempts);
3030
+ const multiplier = 2 ** (normalizedAttempts - 1);
3031
+ return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
3032
+ }
2376
3033
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
2377
3034
  if (sourceType === "github_repo_ci") {
2378
3035
  const parts = eventVariant.split(".");
@@ -2691,3 +3348,48 @@ function weekdayToNumber(value) {
2691
3348
  throw new Error(`unsupported weekday: ${value}`);
2692
3349
  }
2693
3350
  }
3351
+ function listHostStreamKinds(hostType) {
3352
+ switch (hostType) {
3353
+ case "github":
3354
+ return ["repo_events", "ci_runs"];
3355
+ case "feishu":
3356
+ return ["message_events"];
3357
+ case "local_event":
3358
+ return ["events"];
3359
+ case "remote_source":
3360
+ return ["default"];
3361
+ }
3362
+ }
3363
+ function getHostConfigFields(hostType) {
3364
+ switch (hostType) {
3365
+ case "github":
3366
+ return [
3367
+ { name: "uxcAuth", type: "string", description: "Optional shared GitHub auth/runtime profile.", required: false },
3368
+ ];
3369
+ case "feishu":
3370
+ return [
3371
+ { name: "appId", type: "string", description: "Feishu app ID.", required: true },
3372
+ { name: "appSecret", type: "string", description: "Feishu app secret.", required: true },
3373
+ { name: "uxcAuth", type: "string", description: "Optional shared Feishu auth/runtime profile.", required: false },
3374
+ ];
3375
+ case "local_event":
3376
+ return [];
3377
+ case "remote_source":
3378
+ return [];
3379
+ }
3380
+ }
3381
+ function sourceTypeForStreamRegistration(hostType, streamKind) {
3382
+ if (hostType === "github") {
3383
+ if (streamKind === "ci_runs") {
3384
+ return "github_repo_ci";
3385
+ }
3386
+ return "github_repo";
3387
+ }
3388
+ if (hostType === "feishu") {
3389
+ return "feishu_bot";
3390
+ }
3391
+ if (hostType === "local_event") {
3392
+ return "local_event";
3393
+ }
3394
+ return "remote_source";
3395
+ }