@holon-run/agentinbox 0.7.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.
@@ -10,16 +10,21 @@ const filter_1 = require("./filter");
10
10
  const terminal_1 = require("./terminal");
11
11
  const runtime_gate_1 = require("./runtime_gate");
12
12
  const logging_1 = require("./logging");
13
+ const source_hosts_1 = require("./source_hosts");
13
14
  const DEFAULT_SUBSCRIPTION_POLL_LIMIT = 100;
14
15
  const DEFAULT_ACTIVATION_WINDOW_MS = 3_000;
15
16
  const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
16
17
  const DEFAULT_NOTIFY_LEASE_MS = 10 * 60 * 1000;
17
18
  const DEFAULT_NOTIFY_RETRY_MS = 5_000;
19
+ const MAX_TERMINAL_DEFER_RETRY_MS = 5 * 60 * 1000;
18
20
  const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
19
21
  const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
20
22
  const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
21
23
  const DEFAULT_SYNC_INTERVAL_MS = 2_000;
22
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;
23
28
  const DIRECT_INBOX_SOURCE_ID = "__agentinbox_direct_text__";
24
29
  const DIRECT_INBOX_EVENT_VARIANT = "agentinbox.direct_text_message";
25
30
  const TIMER_INBOX_SOURCE_ID = "__agentinbox_timer__";
@@ -44,7 +49,7 @@ class AgentInboxService {
44
49
  lastAckedInboxGcAt = 0;
45
50
  lastLifecycleCleanupAt = 0;
46
51
  lastOfflineAgentGcAt = 0;
47
- constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate, logger = new logging_1.NoopLogger()) {
52
+ constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate, logger = new logging_1.NoopLogger(), gatingPolicy) {
48
53
  this.store = store;
49
54
  this.adapters = adapters;
50
55
  this.activationDispatcher = activationDispatcher;
@@ -54,7 +59,7 @@ class AgentInboxService {
54
59
  this.ackedRetentionMs = DEFAULT_ACKED_RETENTION_MS;
55
60
  this.terminalDispatcher = terminalDispatcher;
56
61
  this.logger = logger;
57
- this.activationGate = activationGate ?? new runtime_gate_1.DefaultActivationGate(undefined, undefined, this.logger.child("gate"));
62
+ this.activationGate = activationGate ?? new runtime_gate_1.DefaultActivationGate(undefined, undefined, this.logger.child("gate"), gatingPolicy);
58
63
  }
59
64
  activationDispatcher;
60
65
  terminalDispatcher;
@@ -67,12 +72,14 @@ class AgentInboxService {
67
72
  this.stopping = false;
68
73
  this.syncInterval = setInterval(() => {
69
74
  void this.syncAllSubscriptions();
75
+ void this.syncPendingDigestThreads();
70
76
  void this.syncActivationDispatchStates();
71
77
  void this.runAckedInboxGcIfDue();
72
78
  void this.syncLifecycleGc();
73
79
  }, DEFAULT_SYNC_INTERVAL_MS);
74
80
  this.refreshTimersOnStart();
75
81
  await this.syncAllSubscriptions();
82
+ await this.syncPendingDigestThreads();
76
83
  await this.syncActivationDispatchStates();
77
84
  await this.runAckedInboxGcIfDue(true);
78
85
  await this.syncLifecycleGc();
@@ -102,13 +109,31 @@ class AgentInboxService {
102
109
  if (existing) {
103
110
  return existing;
104
111
  }
112
+ const resolved = (0, source_hosts_1.resolveSourceRegistration)(input);
105
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
+ }
106
128
  const source = {
107
- sourceId: (0, util_1.generateId)("src"),
108
- 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,
109
134
  sourceKey: input.sourceKey,
110
135
  configRef: input.configRef ?? null,
111
- config: input.config ?? {},
136
+ config: resolved.streamConfig,
112
137
  status: "active",
113
138
  checkpoint: null,
114
139
  createdAt: now,
@@ -122,6 +147,25 @@ class AgentInboxService {
122
147
  listSources() {
123
148
  return this.store.listSources();
124
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
+ }
125
169
  getSource(sourceId) {
126
170
  const source = this.store.getSource(sourceId);
127
171
  if (!source) {
@@ -149,6 +193,7 @@ class AgentInboxService {
149
193
  : fallbackSchema;
150
194
  return {
151
195
  source,
196
+ host: source.hostId ? this.store.getSourceHost(source.hostId) : null,
152
197
  resolvedIdentity,
153
198
  ...(resolutionError ? { resolutionError } : {}),
154
199
  schema: resolvedSchema,
@@ -160,6 +205,74 @@ class AgentInboxService {
160
205
  getSourceSchema(sourceType) {
161
206
  return (0, source_schema_1.getSourceSchema)(sourceType);
162
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
+ }
163
276
  async getResolvedSourceSchema(sourceId) {
164
277
  const source = this.getSource(sourceId);
165
278
  return this.adapters.resolveSourceSchema(source);
@@ -291,9 +404,10 @@ class AgentInboxService {
291
404
  }
292
405
  registerAgent(input) {
293
406
  validateNotifyLeaseMs(input.notifyLeaseMs);
407
+ validateMinUnackedItems(input.minUnackedItems);
294
408
  validateTerminalRegistration(input);
295
409
  const runtimeKind = input.runtimeKind ?? "unknown";
296
- const agentId = input.agentId ?? (0, terminal_1.assignedAgentIdFromContext)({
410
+ const agentId = input.agentId ?? this.resolveAutoAgentId({
297
411
  runtimeKind,
298
412
  runtimeSessionId: input.runtimeSessionId ?? null,
299
413
  backend: input.backend,
@@ -407,7 +521,7 @@ class AgentInboxService {
407
521
  restartRecovery: false,
408
522
  });
409
523
  const timer = {
410
- scheduleId: (0, util_1.generateId)("sched"),
524
+ scheduleId: (0, util_1.generateCanonicalId)("sch"),
411
525
  agentId: input.agentId,
412
526
  status: nextFireAt ? "active" : "paused",
413
527
  mode: normalized.mode,
@@ -490,10 +604,11 @@ class AgentInboxService {
490
604
  addWebhookActivationTarget(agentId, input) {
491
605
  this.getAgent(agentId);
492
606
  validateNotifyLeaseMs(input.notifyLeaseMs);
607
+ validateMinUnackedItems(input.minUnackedItems);
493
608
  const mode = normalizeWebhookActivationMode(input.activationMode);
494
609
  const now = (0, util_1.nowIso)();
495
610
  const target = {
496
- targetId: (0, util_1.generateId)("tgt"),
611
+ targetId: (0, util_1.generateCanonicalId)("tgt"),
497
612
  agentId,
498
613
  kind: "webhook",
499
614
  status: "active",
@@ -504,6 +619,11 @@ class AgentInboxService {
504
619
  mode,
505
620
  url: input.url,
506
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
+ },
507
627
  createdAt: now,
508
628
  updatedAt: now,
509
629
  lastSeenAt: now,
@@ -622,7 +742,7 @@ class AgentInboxService {
622
742
  this.store.deleteSourceIdleState(source.sourceId);
623
743
  }
624
744
  const subscription = {
625
- subscriptionId: (0, util_1.generateId)("sub"),
745
+ subscriptionId: (0, util_1.generateCanonicalId)("sub"),
626
746
  agentId: input.agentId,
627
747
  sourceId: input.sourceId,
628
748
  filter: input.filter ?? {},
@@ -747,15 +867,32 @@ class AgentInboxService {
747
867
  };
748
868
  }
749
869
  listInboxItems(agentId, options) {
870
+ return this.store.listInboxEntries(this.ensureInboxForAgent(agentId).inboxId, options);
871
+ }
872
+ listRawInboxItems(agentId, options) {
750
873
  return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
751
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
+ }
752
889
  async addDirectInboxTextMessage(agentId, input, env = process.env) {
753
890
  this.getAgent(agentId);
754
891
  const message = normalizeDirectInboxMessage(input.message);
755
892
  const sender = normalizeDirectInboxSender(input.sender) ?? this.detectDirectInboxSender(env);
756
893
  return this.materializeAgentInboxItem(agentId, {
757
894
  sourceId: DIRECT_INBOX_SOURCE_ID,
758
- sourceNativeId: (0, util_1.generateId)("direct"),
895
+ sourceNativeId: (0, util_1.generateCanonicalId)("direct"),
759
896
  eventVariant: DIRECT_INBOX_EVENT_VARIANT,
760
897
  summary: summarizeDirectInboxMessage(sender),
761
898
  rawPayload: {
@@ -802,7 +939,7 @@ class AgentInboxService {
802
939
  const inbox = this.ensureInboxForAgent(agentId);
803
940
  const occurredAt = input.occurredAt ?? (0, util_1.nowIso)();
804
941
  const item = {
805
- itemId: (0, util_1.generateId)("item"),
942
+ itemId: (0, util_1.generateCanonicalId)("itm"),
806
943
  sourceId: input.sourceId,
807
944
  sourceNativeId: input.sourceNativeId,
808
945
  eventVariant: input.eventVariant,
@@ -824,18 +961,11 @@ class AgentInboxService {
824
961
  activated: false,
825
962
  };
826
963
  }
827
- this.notifyInboxWatchers(agentId, [item]);
828
- const activationItem = {
829
- itemId: item.itemId,
830
- sourceId: item.sourceId,
831
- sourceNativeId: item.sourceNativeId,
832
- eventVariant: item.eventVariant,
833
- inboxId: item.inboxId,
834
- occurredAt: item.occurredAt,
835
- metadata: item.metadata,
836
- rawPayload: item.rawPayload,
837
- deliveryHandle: item.deliveryHandle,
838
- };
964
+ const entry = this.store.createInboxItemEntry(inbox.inboxId, item, {
965
+ summary: input.summary,
966
+ subscriptionIds: [],
967
+ });
968
+ this.notifyInboxWatchers(agentId, [entry]);
839
969
  const targets = this.store.listActivationTargetsForAgent(agentId).filter((target) => target.status === "active");
840
970
  for (const target of targets) {
841
971
  this.enqueueActivationTarget(target, {
@@ -843,7 +973,7 @@ class AgentInboxService {
843
973
  subscriptionId: null,
844
974
  sourceId: input.sourceId,
845
975
  summary: input.summary,
846
- item: activationItem,
976
+ entry,
847
977
  });
848
978
  }
849
979
  return {
@@ -852,27 +982,119 @@ class AgentInboxService {
852
982
  activated: targets.length > 0,
853
983
  };
854
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
+ }
855
1077
  watchInbox(agentId, options, onEvent) {
856
1078
  const inbox = this.ensureInboxForAgent(agentId);
857
1079
  const pendingItems = [];
858
1080
  let started = false;
859
- const emitItems = (items) => {
860
- if (items.length === 0) {
1081
+ const emitItems = (entries) => {
1082
+ if (entries.length === 0) {
861
1083
  return;
862
1084
  }
863
1085
  onEvent({
864
1086
  event: "items",
865
1087
  agentId,
866
- items,
1088
+ entries,
867
1089
  });
868
1090
  };
869
1091
  const watcher = {
870
- onItems: (items) => {
1092
+ onItems: (entries) => {
871
1093
  if (!started) {
872
- pendingItems.push(...items);
1094
+ pendingItems.push(...entries);
873
1095
  return;
874
1096
  }
875
- emitItems(items);
1097
+ emitItems(entries);
876
1098
  },
877
1099
  };
878
1100
  let watchers = this.inboxWatchers.get(agentId);
@@ -883,8 +1105,8 @@ class AgentInboxService {
883
1105
  watchers.add(watcher);
884
1106
  let initialItems;
885
1107
  try {
886
- initialItems = this.store.listInboxItems(inbox.inboxId, {
887
- afterItemId: options.afterItemId,
1108
+ initialItems = this.store.listInboxEntries(inbox.inboxId, {
1109
+ afterEntryId: options.afterEntryId,
888
1110
  includeAcked: options.includeAcked ?? false,
889
1111
  });
890
1112
  }
@@ -895,7 +1117,7 @@ class AgentInboxService {
895
1117
  }
896
1118
  throw error;
897
1119
  }
898
- const initialItemIds = new Set(initialItems.map((item) => item.itemId));
1120
+ const initialItemIds = new Set(initialItems.map((item) => item.entryId));
899
1121
  return {
900
1122
  initialItems,
901
1123
  start: () => {
@@ -903,7 +1125,7 @@ class AgentInboxService {
903
1125
  return;
904
1126
  }
905
1127
  started = true;
906
- const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.itemId));
1128
+ const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.entryId));
907
1129
  pendingItems.length = 0;
908
1130
  emitItems(replayItems);
909
1131
  },
@@ -919,39 +1141,40 @@ class AgentInboxService {
919
1141
  },
920
1142
  };
921
1143
  }
922
- ackInboxItems(agentId, itemIds) {
1144
+ async ackInboxItems(agentId, itemIds) {
923
1145
  const inbox = this.ensureInboxForAgent(agentId);
924
- const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
925
- if (acked > 0) {
926
- 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);
927
1149
  }
928
- return { acked };
1150
+ return { acked: acked.ackedEntries };
929
1151
  }
930
- ackInboxItemsThrough(agentId, itemId) {
1152
+ async ackInboxItemsThrough(agentId, itemId) {
931
1153
  const inbox = this.ensureInboxForAgent(agentId);
932
- const acked = this.store.ackItemsThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
933
- if (acked > 0) {
934
- 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);
935
1157
  }
936
- return { acked };
1158
+ return { acked: acked.ackedEntries };
937
1159
  }
938
- ackAllInboxItems(agentId) {
1160
+ async ackAllInboxItems(agentId) {
939
1161
  const inbox = this.ensureInboxForAgent(agentId);
940
- const itemIds = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => item.itemId);
941
- const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
942
- if (acked > 0) {
943
- 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);
944
1166
  }
945
- return { acked };
1167
+ return { acked: acked.ackedEntries };
946
1168
  }
947
- ackInbox(agentId, input) {
1169
+ async ackInbox(agentId, input) {
948
1170
  if (input.all) {
949
1171
  return this.ackAllInboxItems(agentId);
950
1172
  }
951
- if (input.throughItemId) {
952
- return this.ackInboxItemsThrough(agentId, input.throughItemId);
1173
+ const throughEntryId = input.throughEntryId ?? null;
1174
+ if (throughEntryId) {
1175
+ return this.ackInboxItemsThrough(agentId, throughEntryId);
953
1176
  }
954
- return this.ackInboxItems(agentId, input.itemIds ?? []);
1177
+ return this.ackInboxItems(agentId, input.entryIds ?? []);
955
1178
  }
956
1179
  compactInbox(agentId) {
957
1180
  const inbox = this.ensureInboxForAgent(agentId);
@@ -1016,7 +1239,7 @@ class AgentInboxService {
1016
1239
  let matched = 0;
1017
1240
  let inboxItemsCreated = 0;
1018
1241
  let lastProcessedOffset = null;
1019
- const insertedItems = [];
1242
+ const insertedEntries = [];
1020
1243
  const lifecycleSignals = new Map();
1021
1244
  try {
1022
1245
  for (const event of batch.events) {
@@ -1034,13 +1257,18 @@ class AgentInboxService {
1034
1257
  }
1035
1258
  matched += 1;
1036
1259
  const item = {
1037
- itemId: (0, util_1.generateId)("item"),
1260
+ itemId: (0, util_1.generateCanonicalId)("itm"),
1038
1261
  sourceId: event.sourceId,
1039
1262
  sourceNativeId: event.sourceNativeId,
1040
1263
  eventVariant: event.eventVariant,
1041
1264
  inboxId: inbox.inboxId,
1042
1265
  occurredAt: event.occurredAt,
1043
- 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
+ },
1044
1272
  rawPayload: event.rawPayload,
1045
1273
  deliveryHandle: event.deliveryHandle,
1046
1274
  ackedAt: null,
@@ -1050,32 +1278,31 @@ class AgentInboxService {
1050
1278
  continue;
1051
1279
  }
1052
1280
  inboxItemsCreated += 1;
1053
- insertedItems.push(item);
1054
- const activationItem = {
1055
- itemId: item.itemId,
1056
- sourceId: item.sourceId,
1057
- sourceNativeId: item.sourceNativeId,
1058
- eventVariant: item.eventVariant,
1059
- inboxId: item.inboxId,
1060
- occurredAt: item.occurredAt,
1061
- metadata: item.metadata,
1062
- rawPayload: item.rawPayload,
1063
- deliveryHandle: item.deliveryHandle,
1064
- };
1065
- for (const target of targets) {
1066
- this.enqueueActivationTarget(target, {
1067
- agentId: subscription.agentId,
1068
- subscriptionId: subscription.subscriptionId,
1069
- sourceId: source.sourceId,
1070
- summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
1071
- item: activationItem,
1072
- });
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
+ }
1073
1300
  }
1074
1301
  }
1075
1302
  }
1076
1303
  catch (error) {
1077
- if (insertedItems.length > 0) {
1078
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1304
+ if (insertedEntries.length > 0) {
1305
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1079
1306
  }
1080
1307
  if (lastProcessedOffset != null) {
1081
1308
  await this.backend.commit({
@@ -1085,8 +1312,8 @@ class AgentInboxService {
1085
1312
  }
1086
1313
  throw error;
1087
1314
  }
1088
- if (insertedItems.length > 0) {
1089
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1315
+ if (insertedEntries.length > 0) {
1316
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1090
1317
  }
1091
1318
  if (lastProcessedOffset != null) {
1092
1319
  await this.backend.commit({
@@ -1113,8 +1340,9 @@ class AgentInboxService {
1113
1340
  }
1114
1341
  async sendDelivery(request) {
1115
1342
  const handle = resolveDeliveryHandle(request);
1343
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1116
1344
  const attempt = {
1117
- deliveryId: (0, util_1.generateId)("dlv"),
1345
+ deliveryId: (0, util_1.generateCanonicalId)("dlv"),
1118
1346
  provider: handle.provider,
1119
1347
  surface: handle.surface,
1120
1348
  targetRef: handle.targetRef,
@@ -1125,12 +1353,52 @@ class AgentInboxService {
1125
1353
  status: "accepted",
1126
1354
  createdAt: (0, util_1.nowIso)(),
1127
1355
  };
1128
- const adapter = this.adapters.deliveryAdapterFor(handle.provider);
1129
- 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);
1130
1359
  const storedAttempt = { ...attempt, status: result.status };
1131
1360
  this.store.insertDelivery(storedAttempt);
1132
1361
  return { ...storedAttempt, note: result.note };
1133
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
+ }
1134
1402
  status() {
1135
1403
  return {
1136
1404
  retention: {
@@ -1163,7 +1431,7 @@ class AgentInboxService {
1163
1431
  return existing;
1164
1432
  }
1165
1433
  const inbox = {
1166
- inboxId: (0, backend_1.defaultInboxIdForAgent)(agentId),
1434
+ inboxId: (0, util_1.generateCanonicalId)("inb"),
1167
1435
  ownerAgentId: agentId,
1168
1436
  createdAt: (0, util_1.nowIso)(),
1169
1437
  };
@@ -1191,7 +1459,7 @@ class AgentInboxService {
1191
1459
  async ensureStreamForSource(source) {
1192
1460
  return this.backend.ensureStream({
1193
1461
  sourceId: source.sourceId,
1194
- streamKey: (0, backend_1.streamKeyForSource)(source.sourceType, source.sourceKey),
1462
+ streamKey: (0, backend_1.streamKeyForSource)(source.sourceId),
1195
1463
  backend: "sqlite",
1196
1464
  });
1197
1465
  }
@@ -1250,6 +1518,29 @@ class AgentInboxService {
1250
1518
  watcher.onItems(items);
1251
1519
  }
1252
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
+ }
1253
1544
  handleAgentRegistrationConflicts(agentId, input) {
1254
1545
  const currentTarget = findExistingTerminalActivationTarget(this.store, input);
1255
1546
  if (currentTarget && currentTarget.agentId !== agentId) {
@@ -1275,6 +1566,7 @@ class AgentInboxService {
1275
1566
  upsertTerminalActivationTarget(agentId, input, now) {
1276
1567
  const existing = findExistingTerminalActivationTarget(this.store, input);
1277
1568
  if (existing) {
1569
+ const existingPolicy = notificationPolicyForTarget(existing);
1278
1570
  const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
1279
1571
  runtimeKind: input.runtimeKind ?? "unknown",
1280
1572
  runtimeSessionId: input.runtimeSessionId ?? null,
@@ -1283,6 +1575,8 @@ class AgentInboxService {
1283
1575
  tty: input.tty ?? null,
1284
1576
  termProgram: input.termProgram ?? null,
1285
1577
  itermSessionId: input.itermSessionId ?? null,
1578
+ notifyLeaseMs: input.notifyLeaseMs ?? existingPolicy.notifyLeaseMs,
1579
+ minUnackedItems: input.minUnackedItems ?? existingPolicy.minUnackedItems ?? null,
1286
1580
  updatedAt: now,
1287
1581
  lastSeenAt: now,
1288
1582
  });
@@ -1293,7 +1587,7 @@ class AgentInboxService {
1293
1587
  return target;
1294
1588
  }
1295
1589
  const target = {
1296
- targetId: (0, util_1.generateId)("tgt"),
1590
+ targetId: (0, util_1.generateCanonicalId)("tgt"),
1297
1591
  agentId,
1298
1592
  kind: "terminal",
1299
1593
  status: "active",
@@ -1303,6 +1597,11 @@ class AgentInboxService {
1303
1597
  lastError: null,
1304
1598
  mode: "agent_prompt",
1305
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
+ },
1306
1605
  runtimeKind: input.runtimeKind ?? "unknown",
1307
1606
  runtimeSessionId: input.runtimeSessionId ?? null,
1308
1607
  runtimePid: input.runtimePid ?? null,
@@ -1336,7 +1635,7 @@ class AgentInboxService {
1336
1635
  subscriptionId: input.subscriptionId,
1337
1636
  sourceId: input.sourceId,
1338
1637
  summary: input.summary,
1339
- item: input.item,
1638
+ entry: input.entry,
1340
1639
  });
1341
1640
  if (!buffer.timer && !buffer.inFlight) {
1342
1641
  buffer.timer = setTimeout(() => {
@@ -1409,44 +1708,84 @@ class AgentInboxService {
1409
1708
  const entries = buffer.pending.splice(0, buffer.pending.length);
1410
1709
  try {
1411
1710
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1412
- if (!state) {
1711
+ const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
1712
+ if (shouldAttemptDispatch) {
1413
1713
  const inbox = this.ensureInboxForAgent(buffer.agentId);
1414
- const unackedItems = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
1415
- itemId: item.itemId,
1416
- sourceId: item.sourceId,
1417
- sourceNativeId: item.sourceNativeId,
1418
- eventVariant: item.eventVariant,
1419
- inboxId: item.inboxId,
1420
- occurredAt: item.occurredAt,
1421
- metadata: item.metadata,
1422
- rawPayload: item.rawPayload,
1423
- deliveryHandle: item.deliveryHandle,
1424
- }));
1714
+ const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
1425
1715
  const dispatched = await this.dispatchActivationTarget({
1426
1716
  agentId: buffer.agentId,
1427
1717
  targetId: buffer.targetId,
1428
- newItemCount: entries.length,
1718
+ newItemCount: state ? state.pendingNewItemCount + entries.length : entries.length,
1429
1719
  totalUnackedCount: unackedItems.length,
1430
- summary: latestSummary(entries),
1431
- subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1432
- sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
1433
- 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,
1434
1728
  });
1435
1729
  if (dispatched === "retryable_failure") {
1436
- 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
+ }
1437
1752
  }
1438
1753
  }
1439
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
+ }
1440
1774
  this.store.upsertActivationDispatchState({
1441
1775
  agentId: buffer.agentId,
1442
1776
  targetId: buffer.targetId,
1443
1777
  status: "dirty",
1444
- leaseExpiresAt: state.leaseExpiresAt,
1778
+ leaseExpiresAt,
1445
1779
  lastNotifiedFingerprint: state.lastNotifiedFingerprint,
1446
- pendingNewItemCount: state.pendingNewItemCount + entries.length,
1447
- pendingSummary: latestSummary(entries) ?? state.pendingSummary,
1448
- pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
1449
- 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,
1450
1789
  updatedAt: (0, util_1.nowIso)(),
1451
1790
  });
1452
1791
  }
@@ -1474,27 +1813,63 @@ class AgentInboxService {
1474
1813
  }
1475
1814
  }
1476
1815
  async handleInboxAckEffects(agentId) {
1816
+ await this.reconcileDigestThreadsForInbox(this.ensureInboxForAgent(agentId).inboxId);
1477
1817
  const states = this.store.listActivationDispatchStatesForAgent(agentId);
1478
1818
  for (const state of states) {
1479
1819
  await this.maybeDispatchActivationTarget(agentId, state.targetId, "ack");
1480
1820
  }
1481
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
+ }
1482
1842
  async maybeDispatchActivationTarget(agentId, targetId, reason) {
1483
1843
  const state = this.store.getActivationDispatchState(agentId, targetId);
1484
1844
  if (!state) {
1485
1845
  return;
1486
1846
  }
1847
+ const target = this.getActivationTarget(targetId);
1487
1848
  const inbox = this.ensureInboxForAgent(agentId);
1488
- const unacked = this.store.countInboxItems(inbox.inboxId, false);
1849
+ const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1489
1850
  if (unacked === 0) {
1490
1851
  this.store.deleteActivationDispatchState(agentId, targetId);
1491
1852
  return;
1492
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
+ }
1493
1869
  if (reason === "ack" && state.status !== "dirty") {
1494
1870
  return;
1495
1871
  }
1496
1872
  if (reason === "lease" && state.status === "notified") {
1497
- const target = this.getActivationTarget(targetId);
1498
1873
  if (target.kind !== "terminal") {
1499
1874
  // Webhook targets keep the existing lease-based behavior.
1500
1875
  }
@@ -1515,17 +1890,7 @@ class AgentInboxService {
1515
1890
  summary: state.pendingSummary,
1516
1891
  subscriptionIds: state.pendingSubscriptionIds,
1517
1892
  sourceIds: state.pendingSourceIds,
1518
- items: this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
1519
- itemId: item.itemId,
1520
- sourceId: item.sourceId,
1521
- sourceNativeId: item.sourceNativeId,
1522
- eventVariant: item.eventVariant,
1523
- inboxId: item.inboxId,
1524
- occurredAt: item.occurredAt,
1525
- metadata: item.metadata,
1526
- rawPayload: item.rawPayload,
1527
- deliveryHandle: item.deliveryHandle,
1528
- })),
1893
+ entries: this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }),
1529
1894
  });
1530
1895
  if (dispatched === "offline") {
1531
1896
  this.store.deleteActivationDispatchState(agentId, targetId);
@@ -1536,6 +1901,11 @@ class AgentInboxService {
1536
1901
  ...state,
1537
1902
  status: "dirty",
1538
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,
1539
1909
  updatedAt: (0, util_1.nowIso)(),
1540
1910
  });
1541
1911
  }
@@ -1548,8 +1918,37 @@ class AgentInboxService {
1548
1918
  const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
1549
1919
  const inbox = this.ensureInboxForAgent(input.agentId);
1550
1920
  const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
1551
- 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
+ }
1552
1950
  let terminalPrompt = null;
1951
+ let terminalPromptFingerprint = null;
1553
1952
  try {
1554
1953
  if (target.kind === "terminal") {
1555
1954
  const gate = await this.activationGate.evaluate(target);
@@ -1572,19 +1971,41 @@ class AgentInboxService {
1572
1971
  this.reconcileAgentStatus(target.agentId);
1573
1972
  return "offline";
1574
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;
1575
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);
1576
1990
  this.logger.debug("activation.dispatch_deferred", {
1577
1991
  agentId: input.agentId,
1578
1992
  targetId: target.targetId,
1579
- reason: gate.reason,
1580
- retryMs: DEFAULT_NOTIFY_RETRY_MS,
1993
+ reason: deferReason,
1994
+ retryMs,
1995
+ deferAttempts,
1996
+ pendingFingerprint: promptFingerprint,
1581
1997
  });
1582
1998
  this.store.upsertActivationDispatchState({
1583
1999
  agentId: input.agentId,
1584
2000
  targetId: input.targetId,
1585
2001
  status: "dirty",
1586
- leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2002
+ leaseExpiresAt: new Date(nextLeaseMs).toISOString(),
1587
2003
  lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
2004
+ deferReason,
2005
+ deferAttempts,
2006
+ firstDeferredAt: state?.firstDeferredAt ?? deferredAt,
2007
+ lastDeferredAt: deferredAt,
2008
+ pendingFingerprint: promptFingerprint,
1588
2009
  pendingNewItemCount: input.newItemCount,
1589
2010
  pendingSummary: input.summary,
1590
2011
  pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
@@ -1593,34 +2014,20 @@ class AgentInboxService {
1593
2014
  });
1594
2015
  return "deferred";
1595
2016
  }
1596
- let preview = null;
1597
- if (totalUnackedCount === 1 && input.items.length === 1) {
1598
- const singleItem = input.items[0];
1599
- const source = this.store.getSource(singleItem.sourceId);
1600
- if (source) {
1601
- try {
1602
- const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
1603
- preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
1604
- }
1605
- catch {
1606
- preview = null;
1607
- }
1608
- }
1609
- 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
+ });
1610
2024
  }
1611
- const prompt = (0, terminal_1.renderAgentPrompt)({
1612
- inboxId: inbox.inboxId,
1613
- totalUnackedCount,
1614
- summary: input.summary,
1615
- preview,
1616
- });
1617
- terminalPrompt = prompt;
1618
- const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
1619
- if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
2025
+ if (state?.lastNotifiedFingerprint === promptFingerprint) {
1620
2026
  this.logger.debug("activation.dispatch_suppressed", {
1621
2027
  agentId: input.agentId,
1622
2028
  targetId: target.targetId,
1623
2029
  reason: "unchanged_terminal_prompt",
2030
+ status: state.status,
1624
2031
  });
1625
2032
  this.store.upsertActivationDispatchState({
1626
2033
  agentId: input.agentId,
@@ -1628,6 +2035,11 @@ class AgentInboxService {
1628
2035
  status: "notified",
1629
2036
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1630
2037
  lastNotifiedFingerprint: promptFingerprint,
2038
+ deferReason: null,
2039
+ deferAttempts: 0,
2040
+ firstDeferredAt: null,
2041
+ lastDeferredAt: null,
2042
+ pendingFingerprint: null,
1631
2043
  pendingNewItemCount: 0,
1632
2044
  pendingSummary: null,
1633
2045
  pendingSubscriptionIds: [],
@@ -1653,16 +2065,22 @@ class AgentInboxService {
1653
2065
  else {
1654
2066
  const activation = {
1655
2067
  kind: "agentinbox.activation",
1656
- activationId: (0, util_1.generateId)("act"),
2068
+ activationId: (0, util_1.generateCanonicalId)("act"),
1657
2069
  agentId: input.agentId,
1658
2070
  inboxId: inbox.inboxId,
1659
2071
  targetId: target.targetId,
1660
2072
  targetKind: target.kind,
1661
2073
  subscriptionIds: input.subscriptionIds,
1662
2074
  sourceIds: input.sourceIds,
2075
+ newEntryCount: input.entries.length,
1663
2076
  newItemCount: input.newItemCount,
1664
2077
  summary,
1665
- 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,
1666
2084
  createdAt: (0, util_1.nowIso)(),
1667
2085
  deliveredAt: null,
1668
2086
  };
@@ -1677,8 +2095,13 @@ class AgentInboxService {
1677
2095
  status: "notified",
1678
2096
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1679
2097
  lastNotifiedFingerprint: target.kind === "terminal"
1680
- ? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
2098
+ ? terminalPromptFingerprint
1681
2099
  : state?.lastNotifiedFingerprint ?? null,
2100
+ deferReason: null,
2101
+ deferAttempts: 0,
2102
+ firstDeferredAt: null,
2103
+ lastDeferredAt: null,
2104
+ pendingFingerprint: null,
1682
2105
  pendingNewItemCount: 0,
1683
2106
  pendingSummary: null,
1684
2107
  pendingSubscriptionIds: [],
@@ -1726,6 +2149,11 @@ class AgentInboxService {
1726
2149
  status: "dirty",
1727
2150
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1728
2151
  lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
2152
+ deferReason: null,
2153
+ deferAttempts: 0,
2154
+ firstDeferredAt: null,
2155
+ lastDeferredAt: null,
2156
+ pendingFingerprint: null,
1729
2157
  pendingNewItemCount: entries.length,
1730
2158
  pendingSummary: latestSummary(entries),
1731
2159
  pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
@@ -1733,6 +2161,33 @@ class AgentInboxService {
1733
2161
  updatedAt: (0, util_1.nowIso)(),
1734
2162
  });
1735
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
+ }
1736
2191
  detectDirectInboxSender(env) {
1737
2192
  try {
1738
2193
  const detected = (0, terminal_1.detectTerminalContext)(env);
@@ -2006,6 +2461,43 @@ class AgentInboxService {
2006
2461
  }
2007
2462
  }
2008
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
+ }
2009
2501
  async syncLifecycleGc() {
2010
2502
  const now = Date.now();
2011
2503
  if (now - this.lastLifecycleCleanupAt >= DEFAULT_GC_INTERVAL_MS) {
@@ -2310,6 +2802,29 @@ function resolveDeliveryHandle(request) {
2310
2802
  replyMode: request.replyMode ?? null,
2311
2803
  };
2312
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
+ }
2313
2828
  class ActivationDispatcher {
2314
2829
  async dispatch(targetUrl, activation) {
2315
2830
  try {
@@ -2352,6 +2867,76 @@ function validateNotifyLeaseMs(value) {
2352
2867
  throw new Error("notifyLeaseMs must be a positive integer");
2353
2868
  }
2354
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
+ }
2355
2940
  function normalizeWebhookActivationMode(mode) {
2356
2941
  const resolved = mode ?? "activation_only";
2357
2942
  if (!WEBHOOK_ACTIVATION_MODES.has(resolved)) {
@@ -2425,11 +3010,26 @@ function terminalReminderFingerprint(prompt, items) {
2425
3010
  const payload = JSON.stringify({
2426
3011
  prompt,
2427
3012
  itemIds: items
2428
- .map((item) => item.itemId)
3013
+ .map((item) => item.entryId)
2429
3014
  .sort((left, right) => left.localeCompare(right)),
2430
3015
  });
2431
3016
  return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
2432
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
+ }
2433
3033
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
2434
3034
  if (sourceType === "github_repo_ci") {
2435
3035
  const parts = eventVariant.split(".");
@@ -2748,3 +3348,48 @@ function weekdayToNumber(value) {
2748
3348
  throw new Error(`unsupported weekday: ${value}`);
2749
3349
  }
2750
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
+ }