@holon-run/agentinbox 0.7.0 → 1.0.0-beta.1

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 (33) hide show
  1. package/README.md +36 -7
  2. package/dist/src/adapters.js +55 -0
  3. package/dist/src/backend.js +4 -16
  4. package/dist/src/cli.js +329 -52
  5. package/dist/src/client.js +2 -2
  6. package/dist/src/http.js +479 -55
  7. package/dist/src/runtime_gate.js +49 -41
  8. package/dist/src/service.js +922 -200
  9. package/dist/src/source_hosts.js +75 -0
  10. package/dist/src/source_resolution.js +2 -2
  11. package/dist/src/source_schema.js +2 -2
  12. package/dist/src/sources/feishu.js +66 -28
  13. package/dist/src/sources/github.js +222 -36
  14. package/dist/src/sources/remote.js +50 -3
  15. package/dist/src/sources/remote_modules.js +100 -15
  16. package/dist/src/store.js +875 -110
  17. package/dist/src/terminal.js +33 -7
  18. package/dist/src/util.js +46 -1
  19. package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
  20. package/drizzle/migrations/0002_host_scoped_lifecycle_retirements.sql +42 -0
  21. package/drizzle/migrations/0003_subscription_tracked_resource_indexes.sql +5 -0
  22. package/drizzle/schema.ts +98 -3
  23. package/package.json +2 -1
  24. package/scripts/release_smoke.sh +123 -0
  25. package/dist/src/sources/remote_profiles.js +0 -13
  26. package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
  27. package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
  28. package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
  29. package/drizzle/migrations/0004_source_idle_states.sql +0 -10
  30. package/drizzle/migrations/0005_timers.sql +0 -22
  31. package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
  32. package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
  33. package/drizzle/migrations/0008_inbox_item_sequence.sql +0 -21
@@ -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,
@@ -587,26 +707,93 @@ class AgentInboxService {
587
707
  return result;
588
708
  }
589
709
  async registerSubscription(input) {
590
- if (input.shortcut) {
591
- const source = this.store.getSource(input.sourceId);
592
- if (!source) {
593
- throw new Error(`unknown source: ${input.sourceId}`);
710
+ const expanded = await this.expandShortcutRegistration(input);
711
+ if (!expanded) {
712
+ return this.registerSingleSubscription(input);
713
+ }
714
+ if (expanded.plan.members.length !== 1) {
715
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} for source ${input.sourceId} expands to ${expanded.plan.members.length} subscriptions; use registerSubscriptions() instead`);
716
+ }
717
+ const subscriptions = await this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
718
+ return subscriptions[0];
719
+ }
720
+ async registerSubscriptions(input) {
721
+ const expanded = await this.expandShortcutRegistration(input);
722
+ if (expanded) {
723
+ return this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
724
+ }
725
+ return [await this.registerSingleSubscription(input)];
726
+ }
727
+ async expandShortcutRegistration(input) {
728
+ if (!input.shortcut) {
729
+ return null;
730
+ }
731
+ const source = this.store.getSource(input.sourceId);
732
+ if (!source) {
733
+ throw new Error(`unknown source: ${input.sourceId}`);
734
+ }
735
+ if (hasSubscriptionFieldOverride(input)) {
736
+ throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
737
+ }
738
+ const plan = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
739
+ if (!plan) {
740
+ throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
741
+ }
742
+ return { source, plan };
743
+ }
744
+ async registerExpandedShortcutSubscriptions(input, source, plan) {
745
+ if (plan.members.length === 0) {
746
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} did not produce any subscriptions`);
747
+ }
748
+ const resolvedMembers = plan.members.map((member) => ({
749
+ source: this.resolveShortcutMemberSource(source, member),
750
+ member,
751
+ }));
752
+ const created = [];
753
+ try {
754
+ for (const { source: memberSource, member } of resolvedMembers) {
755
+ created.push(await this.registerSingleSubscription({
756
+ ...input,
757
+ sourceId: memberSource.sourceId,
758
+ shortcut: undefined,
759
+ filter: member.filter,
760
+ trackedResourceRef: member.trackedResourceRef ?? null,
761
+ cleanupPolicy: member.cleanupPolicy ?? null,
762
+ }));
594
763
  }
595
- if (hasSubscriptionFieldOverride(input)) {
596
- throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
764
+ }
765
+ catch (error) {
766
+ await this.rollbackRegisteredSubscriptions(created);
767
+ throw error;
768
+ }
769
+ return created;
770
+ }
771
+ async rollbackRegisteredSubscriptions(subscriptions) {
772
+ for (const subscription of [...subscriptions].reverse()) {
773
+ try {
774
+ await this.removeSubscription(subscription.subscriptionId);
597
775
  }
598
- const expanded = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
599
- if (!expanded) {
600
- throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
776
+ catch (error) {
777
+ console.warn(`failed to roll back subscription ${subscription.subscriptionId}:`, error);
601
778
  }
602
- return this.registerSubscription({
603
- ...input,
604
- shortcut: undefined,
605
- filter: expanded.filter,
606
- trackedResourceRef: expanded.trackedResourceRef ?? null,
607
- cleanupPolicy: expanded.cleanupPolicy,
608
- });
609
779
  }
780
+ }
781
+ resolveShortcutMemberSource(source, member) {
782
+ if (!member.streamKind || member.streamKind === source.streamKind) {
783
+ return source;
784
+ }
785
+ if (!source.hostId || !source.streamKey) {
786
+ throw new Error(`subscription shortcut ${source.sourceId} requires a host-scoped source to resolve ${member.streamKind}`);
787
+ }
788
+ const sibling = this.store.listSources().find((candidate) => candidate.hostId === source.hostId &&
789
+ candidate.streamKind === member.streamKind &&
790
+ candidate.streamKey === source.streamKey);
791
+ if (!sibling) {
792
+ throw new Error(`subscription shortcut ${source.sourceId} requires an existing sibling ${member.streamKind} stream under host ${source.hostId} with streamKey ${source.streamKey}`);
793
+ }
794
+ return sibling;
795
+ }
796
+ async registerSingleSubscription(input) {
610
797
  this.getAgent(input.agentId);
611
798
  const source = this.store.getSource(input.sourceId);
612
799
  if (!source) {
@@ -622,7 +809,7 @@ class AgentInboxService {
622
809
  this.store.deleteSourceIdleState(source.sourceId);
623
810
  }
624
811
  const subscription = {
625
- subscriptionId: (0, util_1.generateId)("sub"),
812
+ subscriptionId: (0, util_1.generateCanonicalId)("sub"),
626
813
  agentId: input.agentId,
627
814
  sourceId: input.sourceId,
628
815
  filter: input.filter ?? {},
@@ -635,17 +822,25 @@ class AgentInboxService {
635
822
  };
636
823
  this.ensureInboxForAgent(subscription.agentId);
637
824
  this.store.insertSubscription(subscription);
638
- const stream = await this.ensureStreamForSource(source);
639
- await this.backend.ensureConsumer({
640
- streamId: stream.streamId,
641
- subscriptionId: subscription.subscriptionId,
642
- consumerKey: `subscription:${subscription.subscriptionId}`,
643
- startPolicy: subscription.startPolicy,
644
- startOffset: subscription.startOffset ?? null,
645
- startTime: subscription.startTime ?? null,
646
- });
647
- this.store.deleteSourceIdleState(source.sourceId);
648
- return subscription;
825
+ try {
826
+ const stream = await this.ensureStreamForSource(source);
827
+ await this.backend.ensureConsumer({
828
+ streamId: stream.streamId,
829
+ subscriptionId: subscription.subscriptionId,
830
+ consumerKey: `subscription:${subscription.subscriptionId}`,
831
+ startPolicy: subscription.startPolicy,
832
+ startOffset: subscription.startOffset ?? null,
833
+ startTime: subscription.startTime ?? null,
834
+ });
835
+ this.store.deleteSourceIdleState(source.sourceId);
836
+ return subscription;
837
+ }
838
+ catch (error) {
839
+ this.store.deleteSubscription(subscription.subscriptionId);
840
+ this.clearSubscriptionRuntimeState(subscription);
841
+ this.refreshSourceIdleState(source.sourceId);
842
+ throw error;
843
+ }
649
844
  }
650
845
  async removeSubscription(subscriptionId) {
651
846
  const subscription = this.getSubscription(subscriptionId);
@@ -747,15 +942,32 @@ class AgentInboxService {
747
942
  };
748
943
  }
749
944
  listInboxItems(agentId, options) {
945
+ return this.store.listInboxEntries(this.ensureInboxForAgent(agentId).inboxId, options);
946
+ }
947
+ listRawInboxItems(agentId, options) {
750
948
  return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
751
949
  }
950
+ getInboxAggregationPolicy(agentId) {
951
+ const inbox = this.ensureInboxForAgent(agentId);
952
+ return inboxAggregationPolicy(inbox);
953
+ }
954
+ updateInboxAggregationPolicy(agentId, input) {
955
+ const inbox = this.ensureInboxForAgent(agentId);
956
+ const updated = this.store.updateInboxAggregationPolicy(inbox.inboxId, {
957
+ enabled: input.enabled,
958
+ windowMs: input.windowMs,
959
+ maxItems: input.maxItems,
960
+ maxThreadAgeMs: input.maxThreadAgeMs,
961
+ });
962
+ return inboxAggregationPolicy(updated);
963
+ }
752
964
  async addDirectInboxTextMessage(agentId, input, env = process.env) {
753
965
  this.getAgent(agentId);
754
966
  const message = normalizeDirectInboxMessage(input.message);
755
967
  const sender = normalizeDirectInboxSender(input.sender) ?? this.detectDirectInboxSender(env);
756
968
  return this.materializeAgentInboxItem(agentId, {
757
969
  sourceId: DIRECT_INBOX_SOURCE_ID,
758
- sourceNativeId: (0, util_1.generateId)("direct"),
970
+ sourceNativeId: (0, util_1.generateCanonicalId)("direct"),
759
971
  eventVariant: DIRECT_INBOX_EVENT_VARIANT,
760
972
  summary: summarizeDirectInboxMessage(sender),
761
973
  rawPayload: {
@@ -802,7 +1014,7 @@ class AgentInboxService {
802
1014
  const inbox = this.ensureInboxForAgent(agentId);
803
1015
  const occurredAt = input.occurredAt ?? (0, util_1.nowIso)();
804
1016
  const item = {
805
- itemId: (0, util_1.generateId)("item"),
1017
+ itemId: (0, util_1.generateCanonicalId)("itm"),
806
1018
  sourceId: input.sourceId,
807
1019
  sourceNativeId: input.sourceNativeId,
808
1020
  eventVariant: input.eventVariant,
@@ -824,18 +1036,11 @@ class AgentInboxService {
824
1036
  activated: false,
825
1037
  };
826
1038
  }
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
- };
1039
+ const entry = this.store.createInboxItemEntry(inbox.inboxId, item, {
1040
+ summary: input.summary,
1041
+ subscriptionIds: [],
1042
+ });
1043
+ this.notifyInboxWatchers(agentId, [entry]);
839
1044
  const targets = this.store.listActivationTargetsForAgent(agentId).filter((target) => target.status === "active");
840
1045
  for (const target of targets) {
841
1046
  this.enqueueActivationTarget(target, {
@@ -843,7 +1048,7 @@ class AgentInboxService {
843
1048
  subscriptionId: null,
844
1049
  sourceId: input.sourceId,
845
1050
  summary: input.summary,
846
- item: activationItem,
1051
+ entry,
847
1052
  });
848
1053
  }
849
1054
  return {
@@ -852,27 +1057,119 @@ class AgentInboxService {
852
1057
  activated: targets.length > 0,
853
1058
  };
854
1059
  }
1060
+ async materializeInboxEntryForItem(input) {
1061
+ const activationItem = activationItemFromInboxItem(input.item);
1062
+ const policy = inboxAggregationPolicy(input.inbox);
1063
+ if (!policy.enabled || !input.source) {
1064
+ return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
1065
+ summary: input.summary,
1066
+ subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
1067
+ })];
1068
+ }
1069
+ const grouping = await this.adapters.deriveNotificationGrouping(input.source, activationItem);
1070
+ if (!grouping?.groupable || !grouping.resourceRef || !grouping.eventFamily) {
1071
+ return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
1072
+ summary: input.summary,
1073
+ subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
1074
+ })];
1075
+ }
1076
+ const groupKey = digestGroupKey(input.source.sourceId, grouping.resourceRef, grouping.eventFamily);
1077
+ let thread = this.store.getOpenDigestThread(input.inbox.inboxId, groupKey);
1078
+ const now = (0, util_1.nowIso)();
1079
+ if (thread && Date.parse(thread.createdAt) + (policy.maxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS) <= Date.now()) {
1080
+ this.store.closeDigestThread(thread.threadId, now);
1081
+ thread = null;
1082
+ }
1083
+ const flushAfterAt = new Date(Date.now() + (policy.windowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS)).toISOString();
1084
+ if (!thread) {
1085
+ thread = this.store.createDigestThread({
1086
+ inboxId: input.inbox.inboxId,
1087
+ sourceId: input.source.sourceId,
1088
+ groupKey,
1089
+ resourceRef: grouping.resourceRef,
1090
+ eventFamily: grouping.eventFamily,
1091
+ summary: grouping.summaryHint ?? input.summary,
1092
+ firstItemAt: input.item.occurredAt,
1093
+ flushAfterAt,
1094
+ createdAt: now,
1095
+ });
1096
+ }
1097
+ else {
1098
+ thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
1099
+ }
1100
+ if (!thread || this.store.listUnackedInboxItemsForDigestThread(thread.threadId).find((entry) => entry.itemId === input.item.itemId) == null) {
1101
+ thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
1102
+ }
1103
+ const unacked = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
1104
+ const shouldFlush = grouping.flushClass === "immediate" || unacked.length >= (policy.maxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS);
1105
+ if (!shouldFlush) {
1106
+ return [];
1107
+ }
1108
+ const entry = await this.flushDigestThread(input.source, thread.threadId, grouping);
1109
+ return entry ? [entry] : [];
1110
+ }
1111
+ async flushDigestThread(source, threadId, groupingHint) {
1112
+ const thread = this.store.getDigestThread(threadId);
1113
+ if (!thread || thread.status !== "open") {
1114
+ return null;
1115
+ }
1116
+ const items = this.store.listUnackedInboxItemsForDigestThread(threadId);
1117
+ if (items.length === 0) {
1118
+ this.store.closeDigestThread(threadId, (0, util_1.nowIso)());
1119
+ return null;
1120
+ }
1121
+ const activationItems = items.map((item) => activationItemFromInboxItem(item));
1122
+ const grouping = groupingHint ?? {
1123
+ groupable: true,
1124
+ resourceRef: thread.resourceRef,
1125
+ eventFamily: thread.eventFamily,
1126
+ summaryHint: thread.summary,
1127
+ flushClass: "normal",
1128
+ };
1129
+ const moduleSummary = await this.adapters.summarizeDigestThread(source, activationItems, grouping);
1130
+ const summary = moduleSummary ?? `${items.length} ${thread.summary}`;
1131
+ const sourceIds = uniqueSorted(items.map((item) => item.sourceId));
1132
+ const subscriptionIds = uniqueSortedNullable(items.map((item) => typeof item.metadata.subscriptionId === "string" ? item.metadata.subscriptionId : null));
1133
+ const firstItemAt = items[0].occurredAt;
1134
+ const lastItemAt = items[items.length - 1].occurredAt;
1135
+ const deliveryHandle = sharedDeliveryHandle(items);
1136
+ return this.store.materializeDigestSnapshot({
1137
+ threadId,
1138
+ inboxId: thread.inboxId,
1139
+ groupKey: thread.groupKey,
1140
+ resourceRef: thread.resourceRef,
1141
+ eventFamily: thread.eventFamily,
1142
+ summary,
1143
+ sourceIds,
1144
+ subscriptionIds,
1145
+ itemIds: items.map((item) => item.itemId),
1146
+ firstItemAt,
1147
+ lastItemAt,
1148
+ deliveryHandleJson: deliveryHandle ? JSON.stringify(deliveryHandle) : null,
1149
+ createdAt: (0, util_1.nowIso)(),
1150
+ });
1151
+ }
855
1152
  watchInbox(agentId, options, onEvent) {
856
1153
  const inbox = this.ensureInboxForAgent(agentId);
857
1154
  const pendingItems = [];
858
1155
  let started = false;
859
- const emitItems = (items) => {
860
- if (items.length === 0) {
1156
+ const emitItems = (entries) => {
1157
+ if (entries.length === 0) {
861
1158
  return;
862
1159
  }
863
1160
  onEvent({
864
1161
  event: "items",
865
1162
  agentId,
866
- items,
1163
+ entries,
867
1164
  });
868
1165
  };
869
1166
  const watcher = {
870
- onItems: (items) => {
1167
+ onItems: (entries) => {
871
1168
  if (!started) {
872
- pendingItems.push(...items);
1169
+ pendingItems.push(...entries);
873
1170
  return;
874
1171
  }
875
- emitItems(items);
1172
+ emitItems(entries);
876
1173
  },
877
1174
  };
878
1175
  let watchers = this.inboxWatchers.get(agentId);
@@ -883,8 +1180,8 @@ class AgentInboxService {
883
1180
  watchers.add(watcher);
884
1181
  let initialItems;
885
1182
  try {
886
- initialItems = this.store.listInboxItems(inbox.inboxId, {
887
- afterItemId: options.afterItemId,
1183
+ initialItems = this.store.listInboxEntries(inbox.inboxId, {
1184
+ afterEntryId: options.afterEntryId,
888
1185
  includeAcked: options.includeAcked ?? false,
889
1186
  });
890
1187
  }
@@ -895,7 +1192,7 @@ class AgentInboxService {
895
1192
  }
896
1193
  throw error;
897
1194
  }
898
- const initialItemIds = new Set(initialItems.map((item) => item.itemId));
1195
+ const initialItemIds = new Set(initialItems.map((item) => item.entryId));
899
1196
  return {
900
1197
  initialItems,
901
1198
  start: () => {
@@ -903,7 +1200,7 @@ class AgentInboxService {
903
1200
  return;
904
1201
  }
905
1202
  started = true;
906
- const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.itemId));
1203
+ const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.entryId));
907
1204
  pendingItems.length = 0;
908
1205
  emitItems(replayItems);
909
1206
  },
@@ -919,39 +1216,40 @@ class AgentInboxService {
919
1216
  },
920
1217
  };
921
1218
  }
922
- ackInboxItems(agentId, itemIds) {
1219
+ async ackInboxItems(agentId, itemIds) {
923
1220
  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);
1221
+ const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
1222
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1223
+ await this.handleInboxAckEffects(agentId);
927
1224
  }
928
- return { acked };
1225
+ return { acked: acked.ackedEntries };
929
1226
  }
930
- ackInboxItemsThrough(agentId, itemId) {
1227
+ async ackInboxItemsThrough(agentId, itemId) {
931
1228
  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);
1229
+ const acked = this.store.ackInboxEntriesThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
1230
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1231
+ await this.handleInboxAckEffects(agentId);
935
1232
  }
936
- return { acked };
1233
+ return { acked: acked.ackedEntries };
937
1234
  }
938
- ackAllInboxItems(agentId) {
1235
+ async ackAllInboxItems(agentId) {
939
1236
  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);
1237
+ const itemIds = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).map((item) => item.entryId);
1238
+ const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
1239
+ if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
1240
+ await this.handleInboxAckEffects(agentId);
944
1241
  }
945
- return { acked };
1242
+ return { acked: acked.ackedEntries };
946
1243
  }
947
- ackInbox(agentId, input) {
1244
+ async ackInbox(agentId, input) {
948
1245
  if (input.all) {
949
1246
  return this.ackAllInboxItems(agentId);
950
1247
  }
951
- if (input.throughItemId) {
952
- return this.ackInboxItemsThrough(agentId, input.throughItemId);
1248
+ const throughEntryId = input.throughEntryId ?? null;
1249
+ if (throughEntryId) {
1250
+ return this.ackInboxItemsThrough(agentId, throughEntryId);
953
1251
  }
954
- return this.ackInboxItems(agentId, input.itemIds ?? []);
1252
+ return this.ackInboxItems(agentId, input.entryIds ?? []);
955
1253
  }
956
1254
  compactInbox(agentId) {
957
1255
  const inbox = this.ensureInboxForAgent(agentId);
@@ -1016,7 +1314,7 @@ class AgentInboxService {
1016
1314
  let matched = 0;
1017
1315
  let inboxItemsCreated = 0;
1018
1316
  let lastProcessedOffset = null;
1019
- const insertedItems = [];
1317
+ const insertedEntries = [];
1020
1318
  const lifecycleSignals = new Map();
1021
1319
  try {
1022
1320
  for (const event of batch.events) {
@@ -1034,13 +1332,18 @@ class AgentInboxService {
1034
1332
  }
1035
1333
  matched += 1;
1036
1334
  const item = {
1037
- itemId: (0, util_1.generateId)("item"),
1335
+ itemId: (0, util_1.generateCanonicalId)("itm"),
1038
1336
  sourceId: event.sourceId,
1039
1337
  sourceNativeId: event.sourceNativeId,
1040
1338
  eventVariant: event.eventVariant,
1041
1339
  inboxId: inbox.inboxId,
1042
1340
  occurredAt: event.occurredAt,
1043
- metadata: { ...event.metadata, matchReason: match.reason, agentId: subscription.agentId },
1341
+ metadata: {
1342
+ ...event.metadata,
1343
+ matchReason: match.reason,
1344
+ agentId: subscription.agentId,
1345
+ subscriptionId: subscription.subscriptionId,
1346
+ },
1044
1347
  rawPayload: event.rawPayload,
1045
1348
  deliveryHandle: event.deliveryHandle,
1046
1349
  ackedAt: null,
@@ -1050,32 +1353,31 @@ class AgentInboxService {
1050
1353
  continue;
1051
1354
  }
1052
1355
  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
- });
1356
+ const entries = await this.materializeInboxEntryForItem({
1357
+ agentId: subscription.agentId,
1358
+ inbox,
1359
+ source,
1360
+ subscriptionId: subscription.subscriptionId,
1361
+ item,
1362
+ summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
1363
+ });
1364
+ insertedEntries.push(...entries);
1365
+ for (const entry of entries) {
1366
+ for (const target of targets) {
1367
+ this.enqueueActivationTarget(target, {
1368
+ agentId: subscription.agentId,
1369
+ subscriptionId: subscription.subscriptionId,
1370
+ sourceId: source.sourceId,
1371
+ summary: entry.summary,
1372
+ entry,
1373
+ });
1374
+ }
1073
1375
  }
1074
1376
  }
1075
1377
  }
1076
1378
  catch (error) {
1077
- if (insertedItems.length > 0) {
1078
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1379
+ if (insertedEntries.length > 0) {
1380
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1079
1381
  }
1080
1382
  if (lastProcessedOffset != null) {
1081
1383
  await this.backend.commit({
@@ -1085,8 +1387,8 @@ class AgentInboxService {
1085
1387
  }
1086
1388
  throw error;
1087
1389
  }
1088
- if (insertedItems.length > 0) {
1089
- this.notifyInboxWatchers(subscription.agentId, insertedItems);
1390
+ if (insertedEntries.length > 0) {
1391
+ this.notifyInboxWatchers(subscription.agentId, insertedEntries);
1090
1392
  }
1091
1393
  if (lastProcessedOffset != null) {
1092
1394
  await this.backend.commit({
@@ -1095,7 +1397,7 @@ class AgentInboxService {
1095
1397
  });
1096
1398
  }
1097
1399
  for (const signal of lifecycleSignals.values()) {
1098
- this.scheduleLifecycleRetirements(source, subscription, signal);
1400
+ this.scheduleLifecycleRetirements(source, signal);
1099
1401
  }
1100
1402
  return {
1101
1403
  subscriptionId: subscription.subscriptionId,
@@ -1113,8 +1415,9 @@ class AgentInboxService {
1113
1415
  }
1114
1416
  async sendDelivery(request) {
1115
1417
  const handle = resolveDeliveryHandle(request);
1418
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1116
1419
  const attempt = {
1117
- deliveryId: (0, util_1.generateId)("dlv"),
1420
+ deliveryId: (0, util_1.generateCanonicalId)("dlv"),
1118
1421
  provider: handle.provider,
1119
1422
  surface: handle.surface,
1120
1423
  targetRef: handle.targetRef,
@@ -1125,12 +1428,52 @@ class AgentInboxService {
1125
1428
  status: "accepted",
1126
1429
  createdAt: (0, util_1.nowIso)(),
1127
1430
  };
1128
- const adapter = this.adapters.deliveryAdapterFor(handle.provider);
1129
- const result = await adapter.send(request, attempt);
1431
+ const result = source
1432
+ ? await this.sendCanonicalDeliveryViaModule(source, handle, request.payload, attempt)
1433
+ : await this.adapters.deliveryAdapterFor(handle.provider).send(request, attempt);
1130
1434
  const storedAttempt = { ...attempt, status: result.status };
1131
1435
  this.store.insertDelivery(storedAttempt);
1132
1436
  return { ...storedAttempt, note: result.note };
1133
1437
  }
1438
+ async listDeliveryActions(request) {
1439
+ const handle = resolveDeliveryHandle(request);
1440
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1441
+ const operations = await this.adapters.listDeliveryOperations(source, handle);
1442
+ return {
1443
+ sourceId: source?.sourceId ?? null,
1444
+ handle,
1445
+ operations,
1446
+ };
1447
+ }
1448
+ async invokeDelivery(request) {
1449
+ const handle = resolveDeliveryHandle(request);
1450
+ const source = resolveDeliverySource(this.store, request.sourceId, handle);
1451
+ const attempt = {
1452
+ deliveryId: (0, util_1.generateCanonicalId)("dlv"),
1453
+ provider: handle.provider,
1454
+ surface: handle.surface,
1455
+ targetRef: handle.targetRef,
1456
+ threadRef: handle.threadRef ?? null,
1457
+ replyMode: handle.replyMode ?? null,
1458
+ kind: request.operation,
1459
+ payload: request.input,
1460
+ status: "accepted",
1461
+ createdAt: (0, util_1.nowIso)(),
1462
+ };
1463
+ const result = await this.adapters.invokeDeliveryOperation(source, handle, request.operation, request.input, attempt);
1464
+ const storedAttempt = { ...attempt, status: result.status };
1465
+ this.store.insertDelivery(storedAttempt);
1466
+ return { ...storedAttempt, note: result.note, operation: request.operation };
1467
+ }
1468
+ async sendCanonicalDeliveryViaModule(source, handle, payload, attempt) {
1469
+ const operations = await this.adapters.listDeliveryOperations(source, handle);
1470
+ const canonicalOperation = operations.find((operation) => operation.canonicalTextAlias);
1471
+ if (!canonicalOperation) {
1472
+ throw new Error(`deliver send is not supported for ${handle.provider}/${handle.surface}; use deliver invoke`);
1473
+ }
1474
+ const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
1475
+ return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
1476
+ }
1134
1477
  status() {
1135
1478
  return {
1136
1479
  retention: {
@@ -1163,7 +1506,7 @@ class AgentInboxService {
1163
1506
  return existing;
1164
1507
  }
1165
1508
  const inbox = {
1166
- inboxId: (0, backend_1.defaultInboxIdForAgent)(agentId),
1509
+ inboxId: (0, util_1.generateCanonicalId)("inb"),
1167
1510
  ownerAgentId: agentId,
1168
1511
  createdAt: (0, util_1.nowIso)(),
1169
1512
  };
@@ -1191,7 +1534,7 @@ class AgentInboxService {
1191
1534
  async ensureStreamForSource(source) {
1192
1535
  return this.backend.ensureStream({
1193
1536
  sourceId: source.sourceId,
1194
- streamKey: (0, backend_1.streamKeyForSource)(source.sourceType, source.sourceKey),
1537
+ streamKey: (0, backend_1.streamKeyForSource)(source.sourceId),
1195
1538
  backend: "sqlite",
1196
1539
  });
1197
1540
  }
@@ -1215,31 +1558,33 @@ class AgentInboxService {
1215
1558
  signals.set(normalized.ref, normalized);
1216
1559
  }
1217
1560
  }
1218
- scheduleLifecycleRetirements(source, subscription, signal) {
1561
+ scheduleLifecycleRetirements(source, signal) {
1219
1562
  if (!signal.terminal) {
1220
1563
  return;
1221
1564
  }
1222
- if (!subscription.trackedResourceRef || subscription.trackedResourceRef !== signal.ref) {
1565
+ if (!source.hostId) {
1223
1566
  return;
1224
1567
  }
1225
1568
  const signalOccurredAt = signal.occurredAt ?? (0, util_1.nowIso)();
1226
1569
  const signalOccurredAtMs = Date.parse(signalOccurredAt);
1227
- const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1228
- if (!retireAt) {
1229
- return;
1570
+ for (const subscription of this.store.listSubscriptionsForHostTrackedResourceRef(source.hostId, signal.ref)) {
1571
+ const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1572
+ if (!retireAt) {
1573
+ continue;
1574
+ }
1575
+ const now = (0, util_1.nowIso)();
1576
+ this.store.upsertSubscriptionLifecycleRetirement({
1577
+ subscriptionId: subscription.subscriptionId,
1578
+ hostId: source.hostId,
1579
+ trackedResourceRef: signal.ref,
1580
+ retireAt,
1581
+ terminalState: signal.state ?? null,
1582
+ terminalResult: signal.result ?? null,
1583
+ terminalOccurredAt: signalOccurredAt,
1584
+ createdAt: now,
1585
+ updatedAt: now,
1586
+ });
1230
1587
  }
1231
- const now = (0, util_1.nowIso)();
1232
- this.store.upsertSubscriptionLifecycleRetirement({
1233
- subscriptionId: subscription.subscriptionId,
1234
- sourceId: source.sourceId,
1235
- trackedResourceRef: signal.ref,
1236
- retireAt,
1237
- terminalState: signal.state ?? null,
1238
- terminalResult: signal.result ?? null,
1239
- terminalOccurredAt: signalOccurredAt,
1240
- createdAt: now,
1241
- updatedAt: now,
1242
- });
1243
1588
  }
1244
1589
  notifyInboxWatchers(agentId, items) {
1245
1590
  const watchers = this.inboxWatchers.get(agentId);
@@ -1250,6 +1595,29 @@ class AgentInboxService {
1250
1595
  watcher.onItems(items);
1251
1596
  }
1252
1597
  }
1598
+ resolveAutoAgentId(input) {
1599
+ for (let attempt = 0; attempt < 32; attempt += 1) {
1600
+ const candidate = (0, terminal_1.assignedAgentIdFromContext)({
1601
+ runtimeKind: input.runtimeKind,
1602
+ runtimeSessionId: input.runtimeSessionId,
1603
+ backend: input.backend,
1604
+ tmuxPaneId: input.tmuxPaneId,
1605
+ itermSessionId: input.itermSessionId,
1606
+ tty: input.tty,
1607
+ }, attempt);
1608
+ const existing = this.store.getAgent(candidate);
1609
+ if (!existing) {
1610
+ return candidate;
1611
+ }
1612
+ const targets = this.store
1613
+ .listActivationTargetsForAgent(candidate)
1614
+ .filter((target) => target.kind === "terminal");
1615
+ if (targets.length === 0 || targets.some((target) => isSameTerminalIdentity(target, input))) {
1616
+ return candidate;
1617
+ }
1618
+ }
1619
+ throw new Error("unable to derive a unique agentId from terminal context");
1620
+ }
1253
1621
  handleAgentRegistrationConflicts(agentId, input) {
1254
1622
  const currentTarget = findExistingTerminalActivationTarget(this.store, input);
1255
1623
  if (currentTarget && currentTarget.agentId !== agentId) {
@@ -1275,6 +1643,7 @@ class AgentInboxService {
1275
1643
  upsertTerminalActivationTarget(agentId, input, now) {
1276
1644
  const existing = findExistingTerminalActivationTarget(this.store, input);
1277
1645
  if (existing) {
1646
+ const existingPolicy = notificationPolicyForTarget(existing);
1278
1647
  const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
1279
1648
  runtimeKind: input.runtimeKind ?? "unknown",
1280
1649
  runtimeSessionId: input.runtimeSessionId ?? null,
@@ -1283,6 +1652,8 @@ class AgentInboxService {
1283
1652
  tty: input.tty ?? null,
1284
1653
  termProgram: input.termProgram ?? null,
1285
1654
  itermSessionId: input.itermSessionId ?? null,
1655
+ notifyLeaseMs: input.notifyLeaseMs ?? existingPolicy.notifyLeaseMs,
1656
+ minUnackedItems: input.minUnackedItems ?? existingPolicy.minUnackedItems ?? null,
1286
1657
  updatedAt: now,
1287
1658
  lastSeenAt: now,
1288
1659
  });
@@ -1293,7 +1664,7 @@ class AgentInboxService {
1293
1664
  return target;
1294
1665
  }
1295
1666
  const target = {
1296
- targetId: (0, util_1.generateId)("tgt"),
1667
+ targetId: (0, util_1.generateCanonicalId)("tgt"),
1297
1668
  agentId,
1298
1669
  kind: "terminal",
1299
1670
  status: "active",
@@ -1303,6 +1674,11 @@ class AgentInboxService {
1303
1674
  lastError: null,
1304
1675
  mode: "agent_prompt",
1305
1676
  notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
1677
+ minUnackedItems: input.minUnackedItems ?? null,
1678
+ notificationPolicy: {
1679
+ notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
1680
+ minUnackedItems: input.minUnackedItems ?? null,
1681
+ },
1306
1682
  runtimeKind: input.runtimeKind ?? "unknown",
1307
1683
  runtimeSessionId: input.runtimeSessionId ?? null,
1308
1684
  runtimePid: input.runtimePid ?? null,
@@ -1336,7 +1712,7 @@ class AgentInboxService {
1336
1712
  subscriptionId: input.subscriptionId,
1337
1713
  sourceId: input.sourceId,
1338
1714
  summary: input.summary,
1339
- item: input.item,
1715
+ entry: input.entry,
1340
1716
  });
1341
1717
  if (!buffer.timer && !buffer.inFlight) {
1342
1718
  buffer.timer = setTimeout(() => {
@@ -1409,44 +1785,84 @@ class AgentInboxService {
1409
1785
  const entries = buffer.pending.splice(0, buffer.pending.length);
1410
1786
  try {
1411
1787
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1412
- if (!state) {
1788
+ const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
1789
+ if (shouldAttemptDispatch) {
1413
1790
  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
- }));
1791
+ const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
1425
1792
  const dispatched = await this.dispatchActivationTarget({
1426
1793
  agentId: buffer.agentId,
1427
1794
  targetId: buffer.targetId,
1428
- newItemCount: entries.length,
1795
+ newItemCount: state ? state.pendingNewItemCount + entries.length : entries.length,
1429
1796
  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,
1797
+ summary: state ? latestSummary(entries) ?? state.pendingSummary : latestSummary(entries),
1798
+ subscriptionIds: state
1799
+ ? uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
1800
+ : uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1801
+ sourceIds: state
1802
+ ? uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
1803
+ : uniqueSorted(entries.map((entry) => entry.sourceId)),
1804
+ entries: unackedItems,
1434
1805
  });
1435
1806
  if (dispatched === "retryable_failure") {
1436
- this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
1807
+ if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
1808
+ this.store.upsertActivationDispatchState({
1809
+ agentId: buffer.agentId,
1810
+ targetId: buffer.targetId,
1811
+ status: "dirty",
1812
+ leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1813
+ lastNotifiedFingerprint: state.lastNotifiedFingerprint,
1814
+ deferReason: null,
1815
+ deferAttempts: 0,
1816
+ firstDeferredAt: null,
1817
+ lastDeferredAt: null,
1818
+ pendingFingerprint: null,
1819
+ pendingNewItemCount: state.pendingNewItemCount + entries.length,
1820
+ pendingSummary: latestSummary(entries) ?? state.pendingSummary,
1821
+ pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
1822
+ pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
1823
+ updatedAt: (0, util_1.nowIso)(),
1824
+ });
1825
+ }
1826
+ else {
1827
+ this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
1828
+ }
1437
1829
  }
1438
1830
  }
1439
1831
  else {
1832
+ const mergedNewItemCount = state.pendingNewItemCount + entries.length;
1833
+ const mergedSummary = latestSummary(entries) ?? state.pendingSummary;
1834
+ const mergedSubscriptionIds = uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]);
1835
+ const mergedSourceIds = uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]);
1836
+ let deferReason = state.deferReason;
1837
+ let deferAttempts = state.deferAttempts;
1838
+ let firstDeferredAt = state.firstDeferredAt;
1839
+ let lastDeferredAt = state.lastDeferredAt;
1840
+ let pendingFingerprint = state.pendingFingerprint;
1841
+ let leaseExpiresAt = state.leaseExpiresAt;
1842
+ if (state.status === "dirty" && state.leaseExpiresAt != null && state.deferReason) {
1843
+ pendingFingerprint = null;
1844
+ this.logger.debug("activation.defer_pending_marked_stale", {
1845
+ agentId: buffer.agentId,
1846
+ targetId: buffer.targetId,
1847
+ deferReason: state.deferReason,
1848
+ deferAttempts: state.deferAttempts,
1849
+ });
1850
+ }
1440
1851
  this.store.upsertActivationDispatchState({
1441
1852
  agentId: buffer.agentId,
1442
1853
  targetId: buffer.targetId,
1443
1854
  status: "dirty",
1444
- leaseExpiresAt: state.leaseExpiresAt,
1855
+ leaseExpiresAt,
1445
1856
  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)]),
1857
+ deferReason,
1858
+ deferAttempts,
1859
+ firstDeferredAt,
1860
+ lastDeferredAt,
1861
+ pendingFingerprint,
1862
+ pendingNewItemCount: mergedNewItemCount,
1863
+ pendingSummary: mergedSummary,
1864
+ pendingSubscriptionIds: mergedSubscriptionIds,
1865
+ pendingSourceIds: mergedSourceIds,
1450
1866
  updatedAt: (0, util_1.nowIso)(),
1451
1867
  });
1452
1868
  }
@@ -1474,27 +1890,63 @@ class AgentInboxService {
1474
1890
  }
1475
1891
  }
1476
1892
  async handleInboxAckEffects(agentId) {
1893
+ await this.reconcileDigestThreadsForInbox(this.ensureInboxForAgent(agentId).inboxId);
1477
1894
  const states = this.store.listActivationDispatchStatesForAgent(agentId);
1478
1895
  for (const state of states) {
1479
1896
  await this.maybeDispatchActivationTarget(agentId, state.targetId, "ack");
1480
1897
  }
1481
1898
  }
1899
+ async reconcileDigestThreadsForInbox(inboxId) {
1900
+ for (const thread of this.store.listOpenDigestThreadsForInbox(inboxId)) {
1901
+ const source = this.store.getSource(thread.sourceId);
1902
+ if (!source) {
1903
+ this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
1904
+ continue;
1905
+ }
1906
+ const items = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
1907
+ if (items.length === 0) {
1908
+ this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
1909
+ continue;
1910
+ }
1911
+ const head = thread.latestEntryId != null ? this.store.getInboxEntry(thread.latestEntryId) : null;
1912
+ const currentIds = items.map((item) => item.itemId);
1913
+ const headIds = head?.itemIds ?? [];
1914
+ if (!sameStringArray(currentIds, headIds)) {
1915
+ await this.flushDigestThread(source, thread.threadId);
1916
+ }
1917
+ }
1918
+ }
1482
1919
  async maybeDispatchActivationTarget(agentId, targetId, reason) {
1483
1920
  const state = this.store.getActivationDispatchState(agentId, targetId);
1484
1921
  if (!state) {
1485
1922
  return;
1486
1923
  }
1924
+ const target = this.getActivationTarget(targetId);
1487
1925
  const inbox = this.ensureInboxForAgent(agentId);
1488
- const unacked = this.store.countInboxItems(inbox.inboxId, false);
1926
+ const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1489
1927
  if (unacked === 0) {
1490
1928
  this.store.deleteActivationDispatchState(agentId, targetId);
1491
1929
  return;
1492
1930
  }
1931
+ if (!meetsNotificationThreshold(notificationPolicyForTarget(target), unacked)) {
1932
+ this.store.upsertActivationDispatchState({
1933
+ ...state,
1934
+ status: "dirty",
1935
+ leaseExpiresAt: null,
1936
+ deferReason: null,
1937
+ deferAttempts: 0,
1938
+ firstDeferredAt: null,
1939
+ lastDeferredAt: null,
1940
+ pendingFingerprint: null,
1941
+ pendingNewItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
1942
+ updatedAt: (0, util_1.nowIso)(),
1943
+ });
1944
+ return;
1945
+ }
1493
1946
  if (reason === "ack" && state.status !== "dirty") {
1494
1947
  return;
1495
1948
  }
1496
1949
  if (reason === "lease" && state.status === "notified") {
1497
- const target = this.getActivationTarget(targetId);
1498
1950
  if (target.kind !== "terminal") {
1499
1951
  // Webhook targets keep the existing lease-based behavior.
1500
1952
  }
@@ -1515,17 +1967,7 @@ class AgentInboxService {
1515
1967
  summary: state.pendingSummary,
1516
1968
  subscriptionIds: state.pendingSubscriptionIds,
1517
1969
  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
- })),
1970
+ entries: this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }),
1529
1971
  });
1530
1972
  if (dispatched === "offline") {
1531
1973
  this.store.deleteActivationDispatchState(agentId, targetId);
@@ -1536,6 +1978,11 @@ class AgentInboxService {
1536
1978
  ...state,
1537
1979
  status: "dirty",
1538
1980
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1981
+ deferReason: null,
1982
+ deferAttempts: 0,
1983
+ firstDeferredAt: null,
1984
+ lastDeferredAt: null,
1985
+ pendingFingerprint: null,
1539
1986
  updatedAt: (0, util_1.nowIso)(),
1540
1987
  });
1541
1988
  }
@@ -1548,8 +1995,37 @@ class AgentInboxService {
1548
1995
  const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
1549
1996
  const inbox = this.ensureInboxForAgent(input.agentId);
1550
1997
  const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
1551
- const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
1998
+ const totalUnackedCount = input.totalUnackedCount ?? this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
1999
+ const notificationPolicy = notificationPolicyForTarget(target);
2000
+ if (!meetsNotificationThreshold(notificationPolicy, totalUnackedCount)) {
2001
+ this.logger.debug("activation.dispatch_suppressed", {
2002
+ agentId: input.agentId,
2003
+ targetId: target.targetId,
2004
+ reason: "min_unacked_items",
2005
+ totalUnackedCount,
2006
+ minUnackedItems: notificationPolicy.minUnackedItems ?? null,
2007
+ });
2008
+ this.store.upsertActivationDispatchState({
2009
+ agentId: input.agentId,
2010
+ targetId: input.targetId,
2011
+ status: "dirty",
2012
+ leaseExpiresAt: null,
2013
+ lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
2014
+ deferReason: null,
2015
+ deferAttempts: 0,
2016
+ firstDeferredAt: null,
2017
+ lastDeferredAt: null,
2018
+ pendingFingerprint: null,
2019
+ pendingNewItemCount: input.newItemCount,
2020
+ pendingSummary: input.summary,
2021
+ pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
2022
+ pendingSourceIds: uniqueSorted(input.sourceIds),
2023
+ updatedAt: (0, util_1.nowIso)(),
2024
+ });
2025
+ return "suppressed";
2026
+ }
1552
2027
  let terminalPrompt = null;
2028
+ let terminalPromptFingerprint = null;
1553
2029
  try {
1554
2030
  if (target.kind === "terminal") {
1555
2031
  const gate = await this.activationGate.evaluate(target);
@@ -1572,19 +2048,41 @@ class AgentInboxService {
1572
2048
  this.reconcileAgentStatus(target.agentId);
1573
2049
  return "offline";
1574
2050
  }
2051
+ const reminder = await this.buildTerminalReminder(target, inbox.inboxId, totalUnackedCount, input.summary, input.entries);
2052
+ const prompt = reminder.prompt;
2053
+ const promptFingerprint = reminder.fingerprint;
2054
+ terminalPrompt = prompt;
2055
+ terminalPromptFingerprint = promptFingerprint;
1575
2056
  if (gate.outcome === "defer") {
2057
+ const deferReason = toActivationDispatchDeferReason(gate.reason, this.logger);
2058
+ const hasExistingDeferred = state?.deferReason != null;
2059
+ const deferAttempts = hasExistingDeferred ? state.deferAttempts + 1 : 1;
2060
+ const deferredAt = (0, util_1.nowIso)();
2061
+ const retryMs = terminalDeferRetryMs(deferAttempts);
2062
+ const existingLeaseMs = state?.leaseExpiresAt == null ? Number.NaN : Date.parse(state.leaseExpiresAt);
2063
+ const candidateLeaseMs = Date.now() + retryMs;
2064
+ const nextLeaseMs = Number.isNaN(existingLeaseMs)
2065
+ ? candidateLeaseMs
2066
+ : Math.max(existingLeaseMs, candidateLeaseMs);
1576
2067
  this.logger.debug("activation.dispatch_deferred", {
1577
2068
  agentId: input.agentId,
1578
2069
  targetId: target.targetId,
1579
- reason: gate.reason,
1580
- retryMs: DEFAULT_NOTIFY_RETRY_MS,
2070
+ reason: deferReason,
2071
+ retryMs,
2072
+ deferAttempts,
2073
+ pendingFingerprint: promptFingerprint,
1581
2074
  });
1582
2075
  this.store.upsertActivationDispatchState({
1583
2076
  agentId: input.agentId,
1584
2077
  targetId: input.targetId,
1585
2078
  status: "dirty",
1586
- leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2079
+ leaseExpiresAt: new Date(nextLeaseMs).toISOString(),
1587
2080
  lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
2081
+ deferReason,
2082
+ deferAttempts,
2083
+ firstDeferredAt: state?.firstDeferredAt ?? deferredAt,
2084
+ lastDeferredAt: deferredAt,
2085
+ pendingFingerprint: promptFingerprint,
1588
2086
  pendingNewItemCount: input.newItemCount,
1589
2087
  pendingSummary: input.summary,
1590
2088
  pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
@@ -1593,34 +2091,20 @@ class AgentInboxService {
1593
2091
  });
1594
2092
  return "deferred";
1595
2093
  }
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);
2094
+ if (state?.pendingFingerprint && state.pendingFingerprint !== promptFingerprint) {
2095
+ this.logger.debug("activation.defer_pending_revalidated", {
2096
+ agentId: input.agentId,
2097
+ targetId: target.targetId,
2098
+ previousFingerprint: state.pendingFingerprint,
2099
+ currentFingerprint: promptFingerprint,
2100
+ });
1610
2101
  }
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) {
2102
+ if (state?.lastNotifiedFingerprint === promptFingerprint) {
1620
2103
  this.logger.debug("activation.dispatch_suppressed", {
1621
2104
  agentId: input.agentId,
1622
2105
  targetId: target.targetId,
1623
2106
  reason: "unchanged_terminal_prompt",
2107
+ status: state.status,
1624
2108
  });
1625
2109
  this.store.upsertActivationDispatchState({
1626
2110
  agentId: input.agentId,
@@ -1628,6 +2112,11 @@ class AgentInboxService {
1628
2112
  status: "notified",
1629
2113
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1630
2114
  lastNotifiedFingerprint: promptFingerprint,
2115
+ deferReason: null,
2116
+ deferAttempts: 0,
2117
+ firstDeferredAt: null,
2118
+ lastDeferredAt: null,
2119
+ pendingFingerprint: null,
1631
2120
  pendingNewItemCount: 0,
1632
2121
  pendingSummary: null,
1633
2122
  pendingSubscriptionIds: [],
@@ -1653,16 +2142,22 @@ class AgentInboxService {
1653
2142
  else {
1654
2143
  const activation = {
1655
2144
  kind: "agentinbox.activation",
1656
- activationId: (0, util_1.generateId)("act"),
2145
+ activationId: (0, util_1.generateCanonicalId)("act"),
1657
2146
  agentId: input.agentId,
1658
2147
  inboxId: inbox.inboxId,
1659
2148
  targetId: target.targetId,
1660
2149
  targetKind: target.kind,
1661
2150
  subscriptionIds: input.subscriptionIds,
1662
2151
  sourceIds: input.sourceIds,
2152
+ newEntryCount: input.entries.length,
1663
2153
  newItemCount: input.newItemCount,
1664
2154
  summary,
1665
- items: target.mode === "activation_with_items" && input.items.length > 0 ? input.items : undefined,
2155
+ items: target.mode === "activation_with_items"
2156
+ ? input.entries
2157
+ .filter((entry) => entry.kind === "item")
2158
+ .map((entry) => entry.item)
2159
+ : undefined,
2160
+ entries: target.mode === "activation_with_items" && input.entries.length > 0 ? input.entries : undefined,
1666
2161
  createdAt: (0, util_1.nowIso)(),
1667
2162
  deliveredAt: null,
1668
2163
  };
@@ -1677,8 +2172,13 @@ class AgentInboxService {
1677
2172
  status: "notified",
1678
2173
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1679
2174
  lastNotifiedFingerprint: target.kind === "terminal"
1680
- ? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
2175
+ ? terminalPromptFingerprint
1681
2176
  : state?.lastNotifiedFingerprint ?? null,
2177
+ deferReason: null,
2178
+ deferAttempts: 0,
2179
+ firstDeferredAt: null,
2180
+ lastDeferredAt: null,
2181
+ pendingFingerprint: null,
1682
2182
  pendingNewItemCount: 0,
1683
2183
  pendingSummary: null,
1684
2184
  pendingSubscriptionIds: [],
@@ -1726,6 +2226,11 @@ class AgentInboxService {
1726
2226
  status: "dirty",
1727
2227
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1728
2228
  lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
2229
+ deferReason: null,
2230
+ deferAttempts: 0,
2231
+ firstDeferredAt: null,
2232
+ lastDeferredAt: null,
2233
+ pendingFingerprint: null,
1729
2234
  pendingNewItemCount: entries.length,
1730
2235
  pendingSummary: latestSummary(entries),
1731
2236
  pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
@@ -1733,6 +2238,33 @@ class AgentInboxService {
1733
2238
  updatedAt: (0, util_1.nowIso)(),
1734
2239
  });
1735
2240
  }
2241
+ async buildTerminalReminder(target, inboxId, totalUnackedCount, summary, entries) {
2242
+ let preview = null;
2243
+ if (totalUnackedCount === 1 && entries.length === 1 && entries[0]?.kind === "item") {
2244
+ const singleItem = entries[0].item;
2245
+ const source = this.store.getSource(singleItem.sourceId);
2246
+ if (source) {
2247
+ try {
2248
+ const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
2249
+ preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
2250
+ }
2251
+ catch {
2252
+ preview = null;
2253
+ }
2254
+ }
2255
+ preview ??= (0, terminal_1.deriveInlineItemPreview)(singleItem, summary);
2256
+ }
2257
+ const prompt = (0, terminal_1.renderAgentPrompt)({
2258
+ inboxId,
2259
+ totalUnackedCount,
2260
+ summary,
2261
+ preview,
2262
+ });
2263
+ return {
2264
+ prompt,
2265
+ fingerprint: terminalReminderFingerprint(prompt, entries),
2266
+ };
2267
+ }
1736
2268
  detectDirectInboxSender(env) {
1737
2269
  try {
1738
2270
  const detected = (0, terminal_1.detectTerminalContext)(env);
@@ -2006,6 +2538,43 @@ class AgentInboxService {
2006
2538
  }
2007
2539
  }
2008
2540
  }
2541
+ async syncPendingDigestThreads(force = false) {
2542
+ const now = (0, util_1.nowIso)();
2543
+ const threads = force
2544
+ ? this.store.listOpenDigestThreads()
2545
+ : this.store.listDueDigestThreads(now);
2546
+ for (const thread of threads) {
2547
+ try {
2548
+ const source = this.store.getSource(thread.sourceId);
2549
+ if (!source) {
2550
+ this.store.closeDigestThread(thread.threadId, now);
2551
+ continue;
2552
+ }
2553
+ const entry = await this.flushDigestThread(source, thread.threadId);
2554
+ if (!entry) {
2555
+ continue;
2556
+ }
2557
+ const inbox = this.store.getInbox(thread.inboxId);
2558
+ if (!inbox) {
2559
+ continue;
2560
+ }
2561
+ this.notifyInboxWatchers(inbox.ownerAgentId, [entry]);
2562
+ const targets = this.store.listActivationTargetsForAgent(inbox.ownerAgentId).filter((target) => target.status === "active");
2563
+ for (const target of targets) {
2564
+ this.enqueueActivationTarget(target, {
2565
+ agentId: inbox.ownerAgentId,
2566
+ subscriptionId: null,
2567
+ sourceId: thread.sourceId,
2568
+ summary: entry.summary,
2569
+ entry,
2570
+ });
2571
+ }
2572
+ }
2573
+ catch (error) {
2574
+ console.warn(`digest thread flush failed for ${thread.threadId}:`, error);
2575
+ }
2576
+ }
2577
+ }
2009
2578
  async syncLifecycleGc() {
2010
2579
  const now = Date.now();
2011
2580
  if (now - this.lastLifecycleCleanupAt >= DEFAULT_GC_INTERVAL_MS) {
@@ -2310,6 +2879,29 @@ function resolveDeliveryHandle(request) {
2310
2879
  replyMode: request.replyMode ?? null,
2311
2880
  };
2312
2881
  }
2882
+ function resolveDeliverySource(store, sourceId, handle) {
2883
+ if (!sourceId) {
2884
+ return null;
2885
+ }
2886
+ const source = store.getSource(sourceId);
2887
+ if (!source) {
2888
+ throw new Error(`unknown source: ${sourceId}`);
2889
+ }
2890
+ const expectedProvider = providerForSourceType(source.sourceType);
2891
+ if (expectedProvider && expectedProvider !== handle.provider) {
2892
+ throw new Error(`source ${sourceId} does not match delivery provider ${handle.provider}`);
2893
+ }
2894
+ return source;
2895
+ }
2896
+ function providerForSourceType(sourceType) {
2897
+ if (sourceType === "github_repo") {
2898
+ return "github";
2899
+ }
2900
+ if (sourceType === "feishu_bot") {
2901
+ return "feishu";
2902
+ }
2903
+ return null;
2904
+ }
2313
2905
  class ActivationDispatcher {
2314
2906
  async dispatch(targetUrl, activation) {
2315
2907
  try {
@@ -2352,6 +2944,76 @@ function validateNotifyLeaseMs(value) {
2352
2944
  throw new Error("notifyLeaseMs must be a positive integer");
2353
2945
  }
2354
2946
  }
2947
+ function validateMinUnackedItems(value) {
2948
+ if (value == null) {
2949
+ return;
2950
+ }
2951
+ if (!Number.isInteger(value) || value <= 0) {
2952
+ throw new Error("minUnackedItems must be a positive integer");
2953
+ }
2954
+ }
2955
+ function notificationPolicyForTarget(target) {
2956
+ return target.notificationPolicy ?? {
2957
+ notifyLeaseMs: target.notifyLeaseMs,
2958
+ minUnackedItems: target.minUnackedItems ?? null,
2959
+ };
2960
+ }
2961
+ function inboxAggregationPolicy(inbox) {
2962
+ return {
2963
+ enabled: inbox.aggregationEnabled ?? false,
2964
+ windowMs: inbox.aggregationWindowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS,
2965
+ maxItems: inbox.aggregationMaxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS,
2966
+ maxThreadAgeMs: inbox.aggregationMaxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS,
2967
+ };
2968
+ }
2969
+ function meetsNotificationThreshold(policy, totalUnackedCount) {
2970
+ if (policy.minUnackedItems == null) {
2971
+ return true;
2972
+ }
2973
+ return totalUnackedCount >= policy.minUnackedItems;
2974
+ }
2975
+ function activationItemFromInboxItem(item) {
2976
+ return {
2977
+ itemId: item.itemId,
2978
+ sourceId: item.sourceId,
2979
+ sourceNativeId: item.sourceNativeId,
2980
+ eventVariant: item.eventVariant,
2981
+ inboxId: item.inboxId,
2982
+ occurredAt: item.occurredAt,
2983
+ metadata: item.metadata,
2984
+ rawPayload: item.rawPayload,
2985
+ deliveryHandle: item.deliveryHandle,
2986
+ };
2987
+ }
2988
+ function digestGroupKey(sourceId, resourceRef, eventFamily) {
2989
+ return `${sourceId}::${resourceRef}::${eventFamily}`;
2990
+ }
2991
+ function sameStringArray(left, right) {
2992
+ if (left.length !== right.length) {
2993
+ return false;
2994
+ }
2995
+ for (let index = 0; index < left.length; index += 1) {
2996
+ if (left[index] !== right[index]) {
2997
+ return false;
2998
+ }
2999
+ }
3000
+ return true;
3001
+ }
3002
+ function sameDeliveryHandle(left, right) {
3003
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
3004
+ }
3005
+ function sharedDeliveryHandle(items) {
3006
+ const first = items[0]?.deliveryHandle ?? null;
3007
+ if (!first) {
3008
+ return null;
3009
+ }
3010
+ for (const item of items.slice(1)) {
3011
+ if (!sameDeliveryHandle(first, item.deliveryHandle ?? null)) {
3012
+ return null;
3013
+ }
3014
+ }
3015
+ return first;
3016
+ }
2355
3017
  function normalizeWebhookActivationMode(mode) {
2356
3018
  const resolved = mode ?? "activation_only";
2357
3019
  if (!WEBHOOK_ACTIVATION_MODES.has(resolved)) {
@@ -2425,11 +3087,26 @@ function terminalReminderFingerprint(prompt, items) {
2425
3087
  const payload = JSON.stringify({
2426
3088
  prompt,
2427
3089
  itemIds: items
2428
- .map((item) => item.itemId)
3090
+ .map((item) => item.entryId)
2429
3091
  .sort((left, right) => left.localeCompare(right)),
2430
3092
  });
2431
3093
  return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
2432
3094
  }
3095
+ function toActivationDispatchDeferReason(reason, logger) {
3096
+ if (reason === "terminal_recently_active" || reason === "terminal_busy") {
3097
+ return reason;
3098
+ }
3099
+ logger?.warn("activation.unexpected_defer_reason", {
3100
+ reason,
3101
+ fallback: reason,
3102
+ });
3103
+ return reason;
3104
+ }
3105
+ function terminalDeferRetryMs(attempts) {
3106
+ const normalizedAttempts = Math.max(1, attempts);
3107
+ const multiplier = 2 ** (normalizedAttempts - 1);
3108
+ return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
3109
+ }
2433
3110
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
2434
3111
  if (sourceType === "github_repo_ci") {
2435
3112
  const parts = eventVariant.split(".");
@@ -2748,3 +3425,48 @@ function weekdayToNumber(value) {
2748
3425
  throw new Error(`unsupported weekday: ${value}`);
2749
3426
  }
2750
3427
  }
3428
+ function listHostStreamKinds(hostType) {
3429
+ switch (hostType) {
3430
+ case "github":
3431
+ return ["repo_events", "ci_runs"];
3432
+ case "feishu":
3433
+ return ["message_events"];
3434
+ case "local_event":
3435
+ return ["events"];
3436
+ case "remote_source":
3437
+ return ["default"];
3438
+ }
3439
+ }
3440
+ function getHostConfigFields(hostType) {
3441
+ switch (hostType) {
3442
+ case "github":
3443
+ return [
3444
+ { name: "uxcAuth", type: "string", description: "Optional shared GitHub auth/runtime profile.", required: false },
3445
+ ];
3446
+ case "feishu":
3447
+ return [
3448
+ { name: "appId", type: "string", description: "Feishu app ID.", required: true },
3449
+ { name: "appSecret", type: "string", description: "Feishu app secret.", required: true },
3450
+ { name: "uxcAuth", type: "string", description: "Optional shared Feishu auth/runtime profile.", required: false },
3451
+ ];
3452
+ case "local_event":
3453
+ return [];
3454
+ case "remote_source":
3455
+ return [];
3456
+ }
3457
+ }
3458
+ function sourceTypeForStreamRegistration(hostType, streamKind) {
3459
+ if (hostType === "github") {
3460
+ if (streamKind === "ci_runs") {
3461
+ return "github_repo_ci";
3462
+ }
3463
+ return "github_repo";
3464
+ }
3465
+ if (hostType === "feishu") {
3466
+ return "feishu_bot";
3467
+ }
3468
+ if (hostType === "local_event") {
3469
+ return "local_event";
3470
+ }
3471
+ return "remote_source";
3472
+ }