@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.
- package/README.md +36 -7
- package/dist/src/adapters.js +55 -0
- package/dist/src/backend.js +4 -16
- package/dist/src/cli.js +329 -52
- package/dist/src/client.js +2 -2
- package/dist/src/http.js +479 -55
- package/dist/src/runtime_gate.js +49 -41
- package/dist/src/service.js +922 -200
- package/dist/src/source_hosts.js +75 -0
- package/dist/src/source_resolution.js +2 -2
- package/dist/src/source_schema.js +2 -2
- package/dist/src/sources/feishu.js +66 -28
- package/dist/src/sources/github.js +222 -36
- package/dist/src/sources/remote.js +50 -3
- package/dist/src/sources/remote_modules.js +100 -15
- package/dist/src/store.js +875 -110
- package/dist/src/terminal.js +33 -7
- package/dist/src/util.js +46 -1
- package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
- package/drizzle/migrations/0002_host_scoped_lifecycle_retirements.sql +42 -0
- package/drizzle/migrations/0003_subscription_tracked_resource_indexes.sql +5 -0
- package/drizzle/schema.ts +98 -3
- package/package.json +2 -1
- package/scripts/release_smoke.sh +123 -0
- package/dist/src/sources/remote_profiles.js +0 -13
- package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
- package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
- package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
- package/drizzle/migrations/0004_source_idle_states.sql +0 -10
- package/drizzle/migrations/0005_timers.sql +0 -22
- package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
- package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
- package/drizzle/migrations/0008_inbox_item_sequence.sql +0 -21
package/dist/src/service.js
CHANGED
|
@@ -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.
|
|
108
|
-
|
|
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:
|
|
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 ??
|
|
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.
|
|
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.
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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.
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
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 = (
|
|
860
|
-
if (
|
|
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
|
-
|
|
1163
|
+
entries,
|
|
867
1164
|
});
|
|
868
1165
|
};
|
|
869
1166
|
const watcher = {
|
|
870
|
-
onItems: (
|
|
1167
|
+
onItems: (entries) => {
|
|
871
1168
|
if (!started) {
|
|
872
|
-
pendingItems.push(...
|
|
1169
|
+
pendingItems.push(...entries);
|
|
873
1170
|
return;
|
|
874
1171
|
}
|
|
875
|
-
emitItems(
|
|
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.
|
|
887
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
925
|
-
if (acked > 0) {
|
|
926
|
-
|
|
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.
|
|
933
|
-
if (acked > 0) {
|
|
934
|
-
|
|
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.
|
|
941
|
-
const acked = this.store.
|
|
942
|
-
if (acked > 0) {
|
|
943
|
-
|
|
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
|
-
|
|
952
|
-
|
|
1248
|
+
const throughEntryId = input.throughEntryId ?? null;
|
|
1249
|
+
if (throughEntryId) {
|
|
1250
|
+
return this.ackInboxItemsThrough(agentId, throughEntryId);
|
|
953
1251
|
}
|
|
954
|
-
return this.ackInboxItems(agentId, input.
|
|
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
|
|
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.
|
|
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: {
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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 (
|
|
1078
|
-
this.notifyInboxWatchers(subscription.agentId,
|
|
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 (
|
|
1089
|
-
this.notifyInboxWatchers(subscription.agentId,
|
|
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,
|
|
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.
|
|
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
|
|
1129
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
1561
|
+
scheduleLifecycleRetirements(source, signal) {
|
|
1219
1562
|
if (!signal.terminal) {
|
|
1220
1563
|
return;
|
|
1221
1564
|
}
|
|
1222
|
-
if (!
|
|
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
|
|
1228
|
-
|
|
1229
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
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
|
|
1855
|
+
leaseExpiresAt,
|
|
1445
1856
|
lastNotifiedFingerprint: state.lastNotifiedFingerprint,
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
1580
|
-
retryMs
|
|
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(
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
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
|
-
?
|
|
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.
|
|
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
|
+
}
|