@holon-run/agentinbox 0.7.0 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -7
- package/dist/src/adapters.js +55 -0
- package/dist/src/backend.js +4 -16
- package/dist/src/cli.js +289 -48
- package/dist/src/client.js +2 -2
- package/dist/src/http.js +466 -45
- package/dist/src/runtime_gate.js +49 -41
- package/dist/src/service.js +800 -155
- 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 +188 -24
- package/dist/src/sources/remote.js +22 -2
- package/dist/src/sources/remote_modules.js +96 -11
- package/dist/src/store.js +861 -106
- 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/schema.ts +93 -1
- 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,
|
|
@@ -622,7 +742,7 @@ class AgentInboxService {
|
|
|
622
742
|
this.store.deleteSourceIdleState(source.sourceId);
|
|
623
743
|
}
|
|
624
744
|
const subscription = {
|
|
625
|
-
subscriptionId: (0, util_1.
|
|
745
|
+
subscriptionId: (0, util_1.generateCanonicalId)("sub"),
|
|
626
746
|
agentId: input.agentId,
|
|
627
747
|
sourceId: input.sourceId,
|
|
628
748
|
filter: input.filter ?? {},
|
|
@@ -747,15 +867,32 @@ class AgentInboxService {
|
|
|
747
867
|
};
|
|
748
868
|
}
|
|
749
869
|
listInboxItems(agentId, options) {
|
|
870
|
+
return this.store.listInboxEntries(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
871
|
+
}
|
|
872
|
+
listRawInboxItems(agentId, options) {
|
|
750
873
|
return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
751
874
|
}
|
|
875
|
+
getInboxAggregationPolicy(agentId) {
|
|
876
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
877
|
+
return inboxAggregationPolicy(inbox);
|
|
878
|
+
}
|
|
879
|
+
updateInboxAggregationPolicy(agentId, input) {
|
|
880
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
881
|
+
const updated = this.store.updateInboxAggregationPolicy(inbox.inboxId, {
|
|
882
|
+
enabled: input.enabled,
|
|
883
|
+
windowMs: input.windowMs,
|
|
884
|
+
maxItems: input.maxItems,
|
|
885
|
+
maxThreadAgeMs: input.maxThreadAgeMs,
|
|
886
|
+
});
|
|
887
|
+
return inboxAggregationPolicy(updated);
|
|
888
|
+
}
|
|
752
889
|
async addDirectInboxTextMessage(agentId, input, env = process.env) {
|
|
753
890
|
this.getAgent(agentId);
|
|
754
891
|
const message = normalizeDirectInboxMessage(input.message);
|
|
755
892
|
const sender = normalizeDirectInboxSender(input.sender) ?? this.detectDirectInboxSender(env);
|
|
756
893
|
return this.materializeAgentInboxItem(agentId, {
|
|
757
894
|
sourceId: DIRECT_INBOX_SOURCE_ID,
|
|
758
|
-
sourceNativeId: (0, util_1.
|
|
895
|
+
sourceNativeId: (0, util_1.generateCanonicalId)("direct"),
|
|
759
896
|
eventVariant: DIRECT_INBOX_EVENT_VARIANT,
|
|
760
897
|
summary: summarizeDirectInboxMessage(sender),
|
|
761
898
|
rawPayload: {
|
|
@@ -802,7 +939,7 @@ class AgentInboxService {
|
|
|
802
939
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
803
940
|
const occurredAt = input.occurredAt ?? (0, util_1.nowIso)();
|
|
804
941
|
const item = {
|
|
805
|
-
itemId: (0, util_1.
|
|
942
|
+
itemId: (0, util_1.generateCanonicalId)("itm"),
|
|
806
943
|
sourceId: input.sourceId,
|
|
807
944
|
sourceNativeId: input.sourceNativeId,
|
|
808
945
|
eventVariant: input.eventVariant,
|
|
@@ -824,18 +961,11 @@ class AgentInboxService {
|
|
|
824
961
|
activated: false,
|
|
825
962
|
};
|
|
826
963
|
}
|
|
827
|
-
this.
|
|
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
|
-
};
|
|
964
|
+
const entry = this.store.createInboxItemEntry(inbox.inboxId, item, {
|
|
965
|
+
summary: input.summary,
|
|
966
|
+
subscriptionIds: [],
|
|
967
|
+
});
|
|
968
|
+
this.notifyInboxWatchers(agentId, [entry]);
|
|
839
969
|
const targets = this.store.listActivationTargetsForAgent(agentId).filter((target) => target.status === "active");
|
|
840
970
|
for (const target of targets) {
|
|
841
971
|
this.enqueueActivationTarget(target, {
|
|
@@ -843,7 +973,7 @@ class AgentInboxService {
|
|
|
843
973
|
subscriptionId: null,
|
|
844
974
|
sourceId: input.sourceId,
|
|
845
975
|
summary: input.summary,
|
|
846
|
-
|
|
976
|
+
entry,
|
|
847
977
|
});
|
|
848
978
|
}
|
|
849
979
|
return {
|
|
@@ -852,27 +982,119 @@ class AgentInboxService {
|
|
|
852
982
|
activated: targets.length > 0,
|
|
853
983
|
};
|
|
854
984
|
}
|
|
985
|
+
async materializeInboxEntryForItem(input) {
|
|
986
|
+
const activationItem = activationItemFromInboxItem(input.item);
|
|
987
|
+
const policy = inboxAggregationPolicy(input.inbox);
|
|
988
|
+
if (!policy.enabled || !input.source) {
|
|
989
|
+
return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
|
|
990
|
+
summary: input.summary,
|
|
991
|
+
subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
|
|
992
|
+
})];
|
|
993
|
+
}
|
|
994
|
+
const grouping = await this.adapters.deriveNotificationGrouping(input.source, activationItem);
|
|
995
|
+
if (!grouping?.groupable || !grouping.resourceRef || !grouping.eventFamily) {
|
|
996
|
+
return [this.store.createInboxItemEntry(input.inbox.inboxId, input.item, {
|
|
997
|
+
summary: input.summary,
|
|
998
|
+
subscriptionIds: input.subscriptionId ? [input.subscriptionId] : [],
|
|
999
|
+
})];
|
|
1000
|
+
}
|
|
1001
|
+
const groupKey = digestGroupKey(input.source.sourceId, grouping.resourceRef, grouping.eventFamily);
|
|
1002
|
+
let thread = this.store.getOpenDigestThread(input.inbox.inboxId, groupKey);
|
|
1003
|
+
const now = (0, util_1.nowIso)();
|
|
1004
|
+
if (thread && Date.parse(thread.createdAt) + (policy.maxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS) <= Date.now()) {
|
|
1005
|
+
this.store.closeDigestThread(thread.threadId, now);
|
|
1006
|
+
thread = null;
|
|
1007
|
+
}
|
|
1008
|
+
const flushAfterAt = new Date(Date.now() + (policy.windowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS)).toISOString();
|
|
1009
|
+
if (!thread) {
|
|
1010
|
+
thread = this.store.createDigestThread({
|
|
1011
|
+
inboxId: input.inbox.inboxId,
|
|
1012
|
+
sourceId: input.source.sourceId,
|
|
1013
|
+
groupKey,
|
|
1014
|
+
resourceRef: grouping.resourceRef,
|
|
1015
|
+
eventFamily: grouping.eventFamily,
|
|
1016
|
+
summary: grouping.summaryHint ?? input.summary,
|
|
1017
|
+
firstItemAt: input.item.occurredAt,
|
|
1018
|
+
flushAfterAt,
|
|
1019
|
+
createdAt: now,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
|
|
1024
|
+
}
|
|
1025
|
+
if (!thread || this.store.listUnackedInboxItemsForDigestThread(thread.threadId).find((entry) => entry.itemId === input.item.itemId) == null) {
|
|
1026
|
+
thread = this.store.addItemToDigestThread(thread.threadId, input.item.itemId, input.item.occurredAt, grouping.summaryHint ?? thread.summary, flushAfterAt);
|
|
1027
|
+
}
|
|
1028
|
+
const unacked = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
|
|
1029
|
+
const shouldFlush = grouping.flushClass === "immediate" || unacked.length >= (policy.maxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS);
|
|
1030
|
+
if (!shouldFlush) {
|
|
1031
|
+
return [];
|
|
1032
|
+
}
|
|
1033
|
+
const entry = await this.flushDigestThread(input.source, thread.threadId, grouping);
|
|
1034
|
+
return entry ? [entry] : [];
|
|
1035
|
+
}
|
|
1036
|
+
async flushDigestThread(source, threadId, groupingHint) {
|
|
1037
|
+
const thread = this.store.getDigestThread(threadId);
|
|
1038
|
+
if (!thread || thread.status !== "open") {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
const items = this.store.listUnackedInboxItemsForDigestThread(threadId);
|
|
1042
|
+
if (items.length === 0) {
|
|
1043
|
+
this.store.closeDigestThread(threadId, (0, util_1.nowIso)());
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
const activationItems = items.map((item) => activationItemFromInboxItem(item));
|
|
1047
|
+
const grouping = groupingHint ?? {
|
|
1048
|
+
groupable: true,
|
|
1049
|
+
resourceRef: thread.resourceRef,
|
|
1050
|
+
eventFamily: thread.eventFamily,
|
|
1051
|
+
summaryHint: thread.summary,
|
|
1052
|
+
flushClass: "normal",
|
|
1053
|
+
};
|
|
1054
|
+
const moduleSummary = await this.adapters.summarizeDigestThread(source, activationItems, grouping);
|
|
1055
|
+
const summary = moduleSummary ?? `${items.length} ${thread.summary}`;
|
|
1056
|
+
const sourceIds = uniqueSorted(items.map((item) => item.sourceId));
|
|
1057
|
+
const subscriptionIds = uniqueSortedNullable(items.map((item) => typeof item.metadata.subscriptionId === "string" ? item.metadata.subscriptionId : null));
|
|
1058
|
+
const firstItemAt = items[0].occurredAt;
|
|
1059
|
+
const lastItemAt = items[items.length - 1].occurredAt;
|
|
1060
|
+
const deliveryHandle = sharedDeliveryHandle(items);
|
|
1061
|
+
return this.store.materializeDigestSnapshot({
|
|
1062
|
+
threadId,
|
|
1063
|
+
inboxId: thread.inboxId,
|
|
1064
|
+
groupKey: thread.groupKey,
|
|
1065
|
+
resourceRef: thread.resourceRef,
|
|
1066
|
+
eventFamily: thread.eventFamily,
|
|
1067
|
+
summary,
|
|
1068
|
+
sourceIds,
|
|
1069
|
+
subscriptionIds,
|
|
1070
|
+
itemIds: items.map((item) => item.itemId),
|
|
1071
|
+
firstItemAt,
|
|
1072
|
+
lastItemAt,
|
|
1073
|
+
deliveryHandleJson: deliveryHandle ? JSON.stringify(deliveryHandle) : null,
|
|
1074
|
+
createdAt: (0, util_1.nowIso)(),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
855
1077
|
watchInbox(agentId, options, onEvent) {
|
|
856
1078
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
857
1079
|
const pendingItems = [];
|
|
858
1080
|
let started = false;
|
|
859
|
-
const emitItems = (
|
|
860
|
-
if (
|
|
1081
|
+
const emitItems = (entries) => {
|
|
1082
|
+
if (entries.length === 0) {
|
|
861
1083
|
return;
|
|
862
1084
|
}
|
|
863
1085
|
onEvent({
|
|
864
1086
|
event: "items",
|
|
865
1087
|
agentId,
|
|
866
|
-
|
|
1088
|
+
entries,
|
|
867
1089
|
});
|
|
868
1090
|
};
|
|
869
1091
|
const watcher = {
|
|
870
|
-
onItems: (
|
|
1092
|
+
onItems: (entries) => {
|
|
871
1093
|
if (!started) {
|
|
872
|
-
pendingItems.push(...
|
|
1094
|
+
pendingItems.push(...entries);
|
|
873
1095
|
return;
|
|
874
1096
|
}
|
|
875
|
-
emitItems(
|
|
1097
|
+
emitItems(entries);
|
|
876
1098
|
},
|
|
877
1099
|
};
|
|
878
1100
|
let watchers = this.inboxWatchers.get(agentId);
|
|
@@ -883,8 +1105,8 @@ class AgentInboxService {
|
|
|
883
1105
|
watchers.add(watcher);
|
|
884
1106
|
let initialItems;
|
|
885
1107
|
try {
|
|
886
|
-
initialItems = this.store.
|
|
887
|
-
|
|
1108
|
+
initialItems = this.store.listInboxEntries(inbox.inboxId, {
|
|
1109
|
+
afterEntryId: options.afterEntryId,
|
|
888
1110
|
includeAcked: options.includeAcked ?? false,
|
|
889
1111
|
});
|
|
890
1112
|
}
|
|
@@ -895,7 +1117,7 @@ class AgentInboxService {
|
|
|
895
1117
|
}
|
|
896
1118
|
throw error;
|
|
897
1119
|
}
|
|
898
|
-
const initialItemIds = new Set(initialItems.map((item) => item.
|
|
1120
|
+
const initialItemIds = new Set(initialItems.map((item) => item.entryId));
|
|
899
1121
|
return {
|
|
900
1122
|
initialItems,
|
|
901
1123
|
start: () => {
|
|
@@ -903,7 +1125,7 @@ class AgentInboxService {
|
|
|
903
1125
|
return;
|
|
904
1126
|
}
|
|
905
1127
|
started = true;
|
|
906
|
-
const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.
|
|
1128
|
+
const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.entryId));
|
|
907
1129
|
pendingItems.length = 0;
|
|
908
1130
|
emitItems(replayItems);
|
|
909
1131
|
},
|
|
@@ -919,39 +1141,40 @@ class AgentInboxService {
|
|
|
919
1141
|
},
|
|
920
1142
|
};
|
|
921
1143
|
}
|
|
922
|
-
ackInboxItems(agentId, itemIds) {
|
|
1144
|
+
async ackInboxItems(agentId, itemIds) {
|
|
923
1145
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
924
|
-
const acked = this.store.
|
|
925
|
-
if (acked > 0) {
|
|
926
|
-
|
|
1146
|
+
const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
|
|
1147
|
+
if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
|
|
1148
|
+
await this.handleInboxAckEffects(agentId);
|
|
927
1149
|
}
|
|
928
|
-
return { acked };
|
|
1150
|
+
return { acked: acked.ackedEntries };
|
|
929
1151
|
}
|
|
930
|
-
ackInboxItemsThrough(agentId, itemId) {
|
|
1152
|
+
async ackInboxItemsThrough(agentId, itemId) {
|
|
931
1153
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
932
|
-
const acked = this.store.
|
|
933
|
-
if (acked > 0) {
|
|
934
|
-
|
|
1154
|
+
const acked = this.store.ackInboxEntriesThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
|
|
1155
|
+
if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
|
|
1156
|
+
await this.handleInboxAckEffects(agentId);
|
|
935
1157
|
}
|
|
936
|
-
return { acked };
|
|
1158
|
+
return { acked: acked.ackedEntries };
|
|
937
1159
|
}
|
|
938
|
-
ackAllInboxItems(agentId) {
|
|
1160
|
+
async ackAllInboxItems(agentId) {
|
|
939
1161
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
940
|
-
const itemIds = this.store.
|
|
941
|
-
const acked = this.store.
|
|
942
|
-
if (acked > 0) {
|
|
943
|
-
|
|
1162
|
+
const itemIds = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).map((item) => item.entryId);
|
|
1163
|
+
const acked = this.store.ackInboxEntries(inbox.inboxId, itemIds, (0, util_1.nowIso)());
|
|
1164
|
+
if (acked.ackedEntries > 0 || acked.ackedItems > 0) {
|
|
1165
|
+
await this.handleInboxAckEffects(agentId);
|
|
944
1166
|
}
|
|
945
|
-
return { acked };
|
|
1167
|
+
return { acked: acked.ackedEntries };
|
|
946
1168
|
}
|
|
947
|
-
ackInbox(agentId, input) {
|
|
1169
|
+
async ackInbox(agentId, input) {
|
|
948
1170
|
if (input.all) {
|
|
949
1171
|
return this.ackAllInboxItems(agentId);
|
|
950
1172
|
}
|
|
951
|
-
|
|
952
|
-
|
|
1173
|
+
const throughEntryId = input.throughEntryId ?? null;
|
|
1174
|
+
if (throughEntryId) {
|
|
1175
|
+
return this.ackInboxItemsThrough(agentId, throughEntryId);
|
|
953
1176
|
}
|
|
954
|
-
return this.ackInboxItems(agentId, input.
|
|
1177
|
+
return this.ackInboxItems(agentId, input.entryIds ?? []);
|
|
955
1178
|
}
|
|
956
1179
|
compactInbox(agentId) {
|
|
957
1180
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
@@ -1016,7 +1239,7 @@ class AgentInboxService {
|
|
|
1016
1239
|
let matched = 0;
|
|
1017
1240
|
let inboxItemsCreated = 0;
|
|
1018
1241
|
let lastProcessedOffset = null;
|
|
1019
|
-
const
|
|
1242
|
+
const insertedEntries = [];
|
|
1020
1243
|
const lifecycleSignals = new Map();
|
|
1021
1244
|
try {
|
|
1022
1245
|
for (const event of batch.events) {
|
|
@@ -1034,13 +1257,18 @@ class AgentInboxService {
|
|
|
1034
1257
|
}
|
|
1035
1258
|
matched += 1;
|
|
1036
1259
|
const item = {
|
|
1037
|
-
itemId: (0, util_1.
|
|
1260
|
+
itemId: (0, util_1.generateCanonicalId)("itm"),
|
|
1038
1261
|
sourceId: event.sourceId,
|
|
1039
1262
|
sourceNativeId: event.sourceNativeId,
|
|
1040
1263
|
eventVariant: event.eventVariant,
|
|
1041
1264
|
inboxId: inbox.inboxId,
|
|
1042
1265
|
occurredAt: event.occurredAt,
|
|
1043
|
-
metadata: {
|
|
1266
|
+
metadata: {
|
|
1267
|
+
...event.metadata,
|
|
1268
|
+
matchReason: match.reason,
|
|
1269
|
+
agentId: subscription.agentId,
|
|
1270
|
+
subscriptionId: subscription.subscriptionId,
|
|
1271
|
+
},
|
|
1044
1272
|
rawPayload: event.rawPayload,
|
|
1045
1273
|
deliveryHandle: event.deliveryHandle,
|
|
1046
1274
|
ackedAt: null,
|
|
@@ -1050,32 +1278,31 @@ class AgentInboxService {
|
|
|
1050
1278
|
continue;
|
|
1051
1279
|
}
|
|
1052
1280
|
inboxItemsCreated += 1;
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
});
|
|
1281
|
+
const entries = await this.materializeInboxEntryForItem({
|
|
1282
|
+
agentId: subscription.agentId,
|
|
1283
|
+
inbox,
|
|
1284
|
+
source,
|
|
1285
|
+
subscriptionId: subscription.subscriptionId,
|
|
1286
|
+
item,
|
|
1287
|
+
summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
|
|
1288
|
+
});
|
|
1289
|
+
insertedEntries.push(...entries);
|
|
1290
|
+
for (const entry of entries) {
|
|
1291
|
+
for (const target of targets) {
|
|
1292
|
+
this.enqueueActivationTarget(target, {
|
|
1293
|
+
agentId: subscription.agentId,
|
|
1294
|
+
subscriptionId: subscription.subscriptionId,
|
|
1295
|
+
sourceId: source.sourceId,
|
|
1296
|
+
summary: entry.summary,
|
|
1297
|
+
entry,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1073
1300
|
}
|
|
1074
1301
|
}
|
|
1075
1302
|
}
|
|
1076
1303
|
catch (error) {
|
|
1077
|
-
if (
|
|
1078
|
-
this.notifyInboxWatchers(subscription.agentId,
|
|
1304
|
+
if (insertedEntries.length > 0) {
|
|
1305
|
+
this.notifyInboxWatchers(subscription.agentId, insertedEntries);
|
|
1079
1306
|
}
|
|
1080
1307
|
if (lastProcessedOffset != null) {
|
|
1081
1308
|
await this.backend.commit({
|
|
@@ -1085,8 +1312,8 @@ class AgentInboxService {
|
|
|
1085
1312
|
}
|
|
1086
1313
|
throw error;
|
|
1087
1314
|
}
|
|
1088
|
-
if (
|
|
1089
|
-
this.notifyInboxWatchers(subscription.agentId,
|
|
1315
|
+
if (insertedEntries.length > 0) {
|
|
1316
|
+
this.notifyInboxWatchers(subscription.agentId, insertedEntries);
|
|
1090
1317
|
}
|
|
1091
1318
|
if (lastProcessedOffset != null) {
|
|
1092
1319
|
await this.backend.commit({
|
|
@@ -1113,8 +1340,9 @@ class AgentInboxService {
|
|
|
1113
1340
|
}
|
|
1114
1341
|
async sendDelivery(request) {
|
|
1115
1342
|
const handle = resolveDeliveryHandle(request);
|
|
1343
|
+
const source = resolveDeliverySource(this.store, request.sourceId, handle);
|
|
1116
1344
|
const attempt = {
|
|
1117
|
-
deliveryId: (0, util_1.
|
|
1345
|
+
deliveryId: (0, util_1.generateCanonicalId)("dlv"),
|
|
1118
1346
|
provider: handle.provider,
|
|
1119
1347
|
surface: handle.surface,
|
|
1120
1348
|
targetRef: handle.targetRef,
|
|
@@ -1125,12 +1353,52 @@ class AgentInboxService {
|
|
|
1125
1353
|
status: "accepted",
|
|
1126
1354
|
createdAt: (0, util_1.nowIso)(),
|
|
1127
1355
|
};
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1356
|
+
const result = source
|
|
1357
|
+
? await this.sendCanonicalDeliveryViaModule(source, handle, request.payload, attempt)
|
|
1358
|
+
: await this.adapters.deliveryAdapterFor(handle.provider).send(request, attempt);
|
|
1130
1359
|
const storedAttempt = { ...attempt, status: result.status };
|
|
1131
1360
|
this.store.insertDelivery(storedAttempt);
|
|
1132
1361
|
return { ...storedAttempt, note: result.note };
|
|
1133
1362
|
}
|
|
1363
|
+
async listDeliveryActions(request) {
|
|
1364
|
+
const handle = resolveDeliveryHandle(request);
|
|
1365
|
+
const source = resolveDeliverySource(this.store, request.sourceId, handle);
|
|
1366
|
+
const operations = await this.adapters.listDeliveryOperations(source, handle);
|
|
1367
|
+
return {
|
|
1368
|
+
sourceId: source?.sourceId ?? null,
|
|
1369
|
+
handle,
|
|
1370
|
+
operations,
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
async invokeDelivery(request) {
|
|
1374
|
+
const handle = resolveDeliveryHandle(request);
|
|
1375
|
+
const source = resolveDeliverySource(this.store, request.sourceId, handle);
|
|
1376
|
+
const attempt = {
|
|
1377
|
+
deliveryId: (0, util_1.generateCanonicalId)("dlv"),
|
|
1378
|
+
provider: handle.provider,
|
|
1379
|
+
surface: handle.surface,
|
|
1380
|
+
targetRef: handle.targetRef,
|
|
1381
|
+
threadRef: handle.threadRef ?? null,
|
|
1382
|
+
replyMode: handle.replyMode ?? null,
|
|
1383
|
+
kind: request.operation,
|
|
1384
|
+
payload: request.input,
|
|
1385
|
+
status: "accepted",
|
|
1386
|
+
createdAt: (0, util_1.nowIso)(),
|
|
1387
|
+
};
|
|
1388
|
+
const result = await this.adapters.invokeDeliveryOperation(source, handle, request.operation, request.input, attempt);
|
|
1389
|
+
const storedAttempt = { ...attempt, status: result.status };
|
|
1390
|
+
this.store.insertDelivery(storedAttempt);
|
|
1391
|
+
return { ...storedAttempt, note: result.note, operation: request.operation };
|
|
1392
|
+
}
|
|
1393
|
+
async sendCanonicalDeliveryViaModule(source, handle, payload, attempt) {
|
|
1394
|
+
const operations = await this.adapters.listDeliveryOperations(source, handle);
|
|
1395
|
+
const canonicalOperation = operations.find((operation) => operation.canonicalTextAlias);
|
|
1396
|
+
if (!canonicalOperation) {
|
|
1397
|
+
throw new Error(`deliver send is not supported for ${handle.provider}/${handle.surface}; use deliver invoke`);
|
|
1398
|
+
}
|
|
1399
|
+
const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
|
|
1400
|
+
return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
|
|
1401
|
+
}
|
|
1134
1402
|
status() {
|
|
1135
1403
|
return {
|
|
1136
1404
|
retention: {
|
|
@@ -1163,7 +1431,7 @@ class AgentInboxService {
|
|
|
1163
1431
|
return existing;
|
|
1164
1432
|
}
|
|
1165
1433
|
const inbox = {
|
|
1166
|
-
inboxId: (0,
|
|
1434
|
+
inboxId: (0, util_1.generateCanonicalId)("inb"),
|
|
1167
1435
|
ownerAgentId: agentId,
|
|
1168
1436
|
createdAt: (0, util_1.nowIso)(),
|
|
1169
1437
|
};
|
|
@@ -1191,7 +1459,7 @@ class AgentInboxService {
|
|
|
1191
1459
|
async ensureStreamForSource(source) {
|
|
1192
1460
|
return this.backend.ensureStream({
|
|
1193
1461
|
sourceId: source.sourceId,
|
|
1194
|
-
streamKey: (0, backend_1.streamKeyForSource)(source.
|
|
1462
|
+
streamKey: (0, backend_1.streamKeyForSource)(source.sourceId),
|
|
1195
1463
|
backend: "sqlite",
|
|
1196
1464
|
});
|
|
1197
1465
|
}
|
|
@@ -1250,6 +1518,29 @@ class AgentInboxService {
|
|
|
1250
1518
|
watcher.onItems(items);
|
|
1251
1519
|
}
|
|
1252
1520
|
}
|
|
1521
|
+
resolveAutoAgentId(input) {
|
|
1522
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
1523
|
+
const candidate = (0, terminal_1.assignedAgentIdFromContext)({
|
|
1524
|
+
runtimeKind: input.runtimeKind,
|
|
1525
|
+
runtimeSessionId: input.runtimeSessionId,
|
|
1526
|
+
backend: input.backend,
|
|
1527
|
+
tmuxPaneId: input.tmuxPaneId,
|
|
1528
|
+
itermSessionId: input.itermSessionId,
|
|
1529
|
+
tty: input.tty,
|
|
1530
|
+
}, attempt);
|
|
1531
|
+
const existing = this.store.getAgent(candidate);
|
|
1532
|
+
if (!existing) {
|
|
1533
|
+
return candidate;
|
|
1534
|
+
}
|
|
1535
|
+
const targets = this.store
|
|
1536
|
+
.listActivationTargetsForAgent(candidate)
|
|
1537
|
+
.filter((target) => target.kind === "terminal");
|
|
1538
|
+
if (targets.length === 0 || targets.some((target) => isSameTerminalIdentity(target, input))) {
|
|
1539
|
+
return candidate;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
throw new Error("unable to derive a unique agentId from terminal context");
|
|
1543
|
+
}
|
|
1253
1544
|
handleAgentRegistrationConflicts(agentId, input) {
|
|
1254
1545
|
const currentTarget = findExistingTerminalActivationTarget(this.store, input);
|
|
1255
1546
|
if (currentTarget && currentTarget.agentId !== agentId) {
|
|
@@ -1275,6 +1566,7 @@ class AgentInboxService {
|
|
|
1275
1566
|
upsertTerminalActivationTarget(agentId, input, now) {
|
|
1276
1567
|
const existing = findExistingTerminalActivationTarget(this.store, input);
|
|
1277
1568
|
if (existing) {
|
|
1569
|
+
const existingPolicy = notificationPolicyForTarget(existing);
|
|
1278
1570
|
const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
|
|
1279
1571
|
runtimeKind: input.runtimeKind ?? "unknown",
|
|
1280
1572
|
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
@@ -1283,6 +1575,8 @@ class AgentInboxService {
|
|
|
1283
1575
|
tty: input.tty ?? null,
|
|
1284
1576
|
termProgram: input.termProgram ?? null,
|
|
1285
1577
|
itermSessionId: input.itermSessionId ?? null,
|
|
1578
|
+
notifyLeaseMs: input.notifyLeaseMs ?? existingPolicy.notifyLeaseMs,
|
|
1579
|
+
minUnackedItems: input.minUnackedItems ?? existingPolicy.minUnackedItems ?? null,
|
|
1286
1580
|
updatedAt: now,
|
|
1287
1581
|
lastSeenAt: now,
|
|
1288
1582
|
});
|
|
@@ -1293,7 +1587,7 @@ class AgentInboxService {
|
|
|
1293
1587
|
return target;
|
|
1294
1588
|
}
|
|
1295
1589
|
const target = {
|
|
1296
|
-
targetId: (0, util_1.
|
|
1590
|
+
targetId: (0, util_1.generateCanonicalId)("tgt"),
|
|
1297
1591
|
agentId,
|
|
1298
1592
|
kind: "terminal",
|
|
1299
1593
|
status: "active",
|
|
@@ -1303,6 +1597,11 @@ class AgentInboxService {
|
|
|
1303
1597
|
lastError: null,
|
|
1304
1598
|
mode: "agent_prompt",
|
|
1305
1599
|
notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
|
|
1600
|
+
minUnackedItems: input.minUnackedItems ?? null,
|
|
1601
|
+
notificationPolicy: {
|
|
1602
|
+
notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
|
|
1603
|
+
minUnackedItems: input.minUnackedItems ?? null,
|
|
1604
|
+
},
|
|
1306
1605
|
runtimeKind: input.runtimeKind ?? "unknown",
|
|
1307
1606
|
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
1308
1607
|
runtimePid: input.runtimePid ?? null,
|
|
@@ -1336,7 +1635,7 @@ class AgentInboxService {
|
|
|
1336
1635
|
subscriptionId: input.subscriptionId,
|
|
1337
1636
|
sourceId: input.sourceId,
|
|
1338
1637
|
summary: input.summary,
|
|
1339
|
-
|
|
1638
|
+
entry: input.entry,
|
|
1340
1639
|
});
|
|
1341
1640
|
if (!buffer.timer && !buffer.inFlight) {
|
|
1342
1641
|
buffer.timer = setTimeout(() => {
|
|
@@ -1409,44 +1708,84 @@ class AgentInboxService {
|
|
|
1409
1708
|
const entries = buffer.pending.splice(0, buffer.pending.length);
|
|
1410
1709
|
try {
|
|
1411
1710
|
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
1412
|
-
|
|
1711
|
+
const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
|
|
1712
|
+
if (shouldAttemptDispatch) {
|
|
1413
1713
|
const inbox = this.ensureInboxForAgent(buffer.agentId);
|
|
1414
|
-
const unackedItems = this.store.
|
|
1415
|
-
itemId: item.itemId,
|
|
1416
|
-
sourceId: item.sourceId,
|
|
1417
|
-
sourceNativeId: item.sourceNativeId,
|
|
1418
|
-
eventVariant: item.eventVariant,
|
|
1419
|
-
inboxId: item.inboxId,
|
|
1420
|
-
occurredAt: item.occurredAt,
|
|
1421
|
-
metadata: item.metadata,
|
|
1422
|
-
rawPayload: item.rawPayload,
|
|
1423
|
-
deliveryHandle: item.deliveryHandle,
|
|
1424
|
-
}));
|
|
1714
|
+
const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
|
|
1425
1715
|
const dispatched = await this.dispatchActivationTarget({
|
|
1426
1716
|
agentId: buffer.agentId,
|
|
1427
1717
|
targetId: buffer.targetId,
|
|
1428
|
-
newItemCount: entries.length,
|
|
1718
|
+
newItemCount: state ? state.pendingNewItemCount + entries.length : entries.length,
|
|
1429
1719
|
totalUnackedCount: unackedItems.length,
|
|
1430
|
-
summary: latestSummary(entries),
|
|
1431
|
-
subscriptionIds:
|
|
1432
|
-
|
|
1433
|
-
|
|
1720
|
+
summary: state ? latestSummary(entries) ?? state.pendingSummary : latestSummary(entries),
|
|
1721
|
+
subscriptionIds: state
|
|
1722
|
+
? uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
|
|
1723
|
+
: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1724
|
+
sourceIds: state
|
|
1725
|
+
? uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
|
|
1726
|
+
: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1727
|
+
entries: unackedItems,
|
|
1434
1728
|
});
|
|
1435
1729
|
if (dispatched === "retryable_failure") {
|
|
1436
|
-
|
|
1730
|
+
if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
|
|
1731
|
+
this.store.upsertActivationDispatchState({
|
|
1732
|
+
agentId: buffer.agentId,
|
|
1733
|
+
targetId: buffer.targetId,
|
|
1734
|
+
status: "dirty",
|
|
1735
|
+
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1736
|
+
lastNotifiedFingerprint: state.lastNotifiedFingerprint,
|
|
1737
|
+
deferReason: null,
|
|
1738
|
+
deferAttempts: 0,
|
|
1739
|
+
firstDeferredAt: null,
|
|
1740
|
+
lastDeferredAt: null,
|
|
1741
|
+
pendingFingerprint: null,
|
|
1742
|
+
pendingNewItemCount: state.pendingNewItemCount + entries.length,
|
|
1743
|
+
pendingSummary: latestSummary(entries) ?? state.pendingSummary,
|
|
1744
|
+
pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
1745
|
+
pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
1746
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
|
|
1751
|
+
}
|
|
1437
1752
|
}
|
|
1438
1753
|
}
|
|
1439
1754
|
else {
|
|
1755
|
+
const mergedNewItemCount = state.pendingNewItemCount + entries.length;
|
|
1756
|
+
const mergedSummary = latestSummary(entries) ?? state.pendingSummary;
|
|
1757
|
+
const mergedSubscriptionIds = uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]);
|
|
1758
|
+
const mergedSourceIds = uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]);
|
|
1759
|
+
let deferReason = state.deferReason;
|
|
1760
|
+
let deferAttempts = state.deferAttempts;
|
|
1761
|
+
let firstDeferredAt = state.firstDeferredAt;
|
|
1762
|
+
let lastDeferredAt = state.lastDeferredAt;
|
|
1763
|
+
let pendingFingerprint = state.pendingFingerprint;
|
|
1764
|
+
let leaseExpiresAt = state.leaseExpiresAt;
|
|
1765
|
+
if (state.status === "dirty" && state.leaseExpiresAt != null && state.deferReason) {
|
|
1766
|
+
pendingFingerprint = null;
|
|
1767
|
+
this.logger.debug("activation.defer_pending_marked_stale", {
|
|
1768
|
+
agentId: buffer.agentId,
|
|
1769
|
+
targetId: buffer.targetId,
|
|
1770
|
+
deferReason: state.deferReason,
|
|
1771
|
+
deferAttempts: state.deferAttempts,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1440
1774
|
this.store.upsertActivationDispatchState({
|
|
1441
1775
|
agentId: buffer.agentId,
|
|
1442
1776
|
targetId: buffer.targetId,
|
|
1443
1777
|
status: "dirty",
|
|
1444
|
-
leaseExpiresAt
|
|
1778
|
+
leaseExpiresAt,
|
|
1445
1779
|
lastNotifiedFingerprint: state.lastNotifiedFingerprint,
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1780
|
+
deferReason,
|
|
1781
|
+
deferAttempts,
|
|
1782
|
+
firstDeferredAt,
|
|
1783
|
+
lastDeferredAt,
|
|
1784
|
+
pendingFingerprint,
|
|
1785
|
+
pendingNewItemCount: mergedNewItemCount,
|
|
1786
|
+
pendingSummary: mergedSummary,
|
|
1787
|
+
pendingSubscriptionIds: mergedSubscriptionIds,
|
|
1788
|
+
pendingSourceIds: mergedSourceIds,
|
|
1450
1789
|
updatedAt: (0, util_1.nowIso)(),
|
|
1451
1790
|
});
|
|
1452
1791
|
}
|
|
@@ -1474,27 +1813,63 @@ class AgentInboxService {
|
|
|
1474
1813
|
}
|
|
1475
1814
|
}
|
|
1476
1815
|
async handleInboxAckEffects(agentId) {
|
|
1816
|
+
await this.reconcileDigestThreadsForInbox(this.ensureInboxForAgent(agentId).inboxId);
|
|
1477
1817
|
const states = this.store.listActivationDispatchStatesForAgent(agentId);
|
|
1478
1818
|
for (const state of states) {
|
|
1479
1819
|
await this.maybeDispatchActivationTarget(agentId, state.targetId, "ack");
|
|
1480
1820
|
}
|
|
1481
1821
|
}
|
|
1822
|
+
async reconcileDigestThreadsForInbox(inboxId) {
|
|
1823
|
+
for (const thread of this.store.listOpenDigestThreadsForInbox(inboxId)) {
|
|
1824
|
+
const source = this.store.getSource(thread.sourceId);
|
|
1825
|
+
if (!source) {
|
|
1826
|
+
this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const items = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
|
|
1830
|
+
if (items.length === 0) {
|
|
1831
|
+
this.store.closeDigestThread(thread.threadId, (0, util_1.nowIso)());
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const head = thread.latestEntryId != null ? this.store.getInboxEntry(thread.latestEntryId) : null;
|
|
1835
|
+
const currentIds = items.map((item) => item.itemId);
|
|
1836
|
+
const headIds = head?.itemIds ?? [];
|
|
1837
|
+
if (!sameStringArray(currentIds, headIds)) {
|
|
1838
|
+
await this.flushDigestThread(source, thread.threadId);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1482
1842
|
async maybeDispatchActivationTarget(agentId, targetId, reason) {
|
|
1483
1843
|
const state = this.store.getActivationDispatchState(agentId, targetId);
|
|
1484
1844
|
if (!state) {
|
|
1485
1845
|
return;
|
|
1486
1846
|
}
|
|
1847
|
+
const target = this.getActivationTarget(targetId);
|
|
1487
1848
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
1488
|
-
const unacked = this.store.
|
|
1849
|
+
const unacked = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
|
|
1489
1850
|
if (unacked === 0) {
|
|
1490
1851
|
this.store.deleteActivationDispatchState(agentId, targetId);
|
|
1491
1852
|
return;
|
|
1492
1853
|
}
|
|
1854
|
+
if (!meetsNotificationThreshold(notificationPolicyForTarget(target), unacked)) {
|
|
1855
|
+
this.store.upsertActivationDispatchState({
|
|
1856
|
+
...state,
|
|
1857
|
+
status: "dirty",
|
|
1858
|
+
leaseExpiresAt: null,
|
|
1859
|
+
deferReason: null,
|
|
1860
|
+
deferAttempts: 0,
|
|
1861
|
+
firstDeferredAt: null,
|
|
1862
|
+
lastDeferredAt: null,
|
|
1863
|
+
pendingFingerprint: null,
|
|
1864
|
+
pendingNewItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
|
|
1865
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1866
|
+
});
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1493
1869
|
if (reason === "ack" && state.status !== "dirty") {
|
|
1494
1870
|
return;
|
|
1495
1871
|
}
|
|
1496
1872
|
if (reason === "lease" && state.status === "notified") {
|
|
1497
|
-
const target = this.getActivationTarget(targetId);
|
|
1498
1873
|
if (target.kind !== "terminal") {
|
|
1499
1874
|
// Webhook targets keep the existing lease-based behavior.
|
|
1500
1875
|
}
|
|
@@ -1515,17 +1890,7 @@ class AgentInboxService {
|
|
|
1515
1890
|
summary: state.pendingSummary,
|
|
1516
1891
|
subscriptionIds: state.pendingSubscriptionIds,
|
|
1517
1892
|
sourceIds: state.pendingSourceIds,
|
|
1518
|
-
|
|
1519
|
-
itemId: item.itemId,
|
|
1520
|
-
sourceId: item.sourceId,
|
|
1521
|
-
sourceNativeId: item.sourceNativeId,
|
|
1522
|
-
eventVariant: item.eventVariant,
|
|
1523
|
-
inboxId: item.inboxId,
|
|
1524
|
-
occurredAt: item.occurredAt,
|
|
1525
|
-
metadata: item.metadata,
|
|
1526
|
-
rawPayload: item.rawPayload,
|
|
1527
|
-
deliveryHandle: item.deliveryHandle,
|
|
1528
|
-
})),
|
|
1893
|
+
entries: this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }),
|
|
1529
1894
|
});
|
|
1530
1895
|
if (dispatched === "offline") {
|
|
1531
1896
|
this.store.deleteActivationDispatchState(agentId, targetId);
|
|
@@ -1536,6 +1901,11 @@ class AgentInboxService {
|
|
|
1536
1901
|
...state,
|
|
1537
1902
|
status: "dirty",
|
|
1538
1903
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1904
|
+
deferReason: null,
|
|
1905
|
+
deferAttempts: 0,
|
|
1906
|
+
firstDeferredAt: null,
|
|
1907
|
+
lastDeferredAt: null,
|
|
1908
|
+
pendingFingerprint: null,
|
|
1539
1909
|
updatedAt: (0, util_1.nowIso)(),
|
|
1540
1910
|
});
|
|
1541
1911
|
}
|
|
@@ -1548,8 +1918,37 @@ class AgentInboxService {
|
|
|
1548
1918
|
const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
|
|
1549
1919
|
const inbox = this.ensureInboxForAgent(input.agentId);
|
|
1550
1920
|
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1551
|
-
const totalUnackedCount = input.totalUnackedCount ?? this.store.
|
|
1921
|
+
const totalUnackedCount = input.totalUnackedCount ?? this.store.listInboxEntries(inbox.inboxId, { includeAcked: false }).length;
|
|
1922
|
+
const notificationPolicy = notificationPolicyForTarget(target);
|
|
1923
|
+
if (!meetsNotificationThreshold(notificationPolicy, totalUnackedCount)) {
|
|
1924
|
+
this.logger.debug("activation.dispatch_suppressed", {
|
|
1925
|
+
agentId: input.agentId,
|
|
1926
|
+
targetId: target.targetId,
|
|
1927
|
+
reason: "min_unacked_items",
|
|
1928
|
+
totalUnackedCount,
|
|
1929
|
+
minUnackedItems: notificationPolicy.minUnackedItems ?? null,
|
|
1930
|
+
});
|
|
1931
|
+
this.store.upsertActivationDispatchState({
|
|
1932
|
+
agentId: input.agentId,
|
|
1933
|
+
targetId: input.targetId,
|
|
1934
|
+
status: "dirty",
|
|
1935
|
+
leaseExpiresAt: null,
|
|
1936
|
+
lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
|
|
1937
|
+
deferReason: null,
|
|
1938
|
+
deferAttempts: 0,
|
|
1939
|
+
firstDeferredAt: null,
|
|
1940
|
+
lastDeferredAt: null,
|
|
1941
|
+
pendingFingerprint: null,
|
|
1942
|
+
pendingNewItemCount: input.newItemCount,
|
|
1943
|
+
pendingSummary: input.summary,
|
|
1944
|
+
pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
|
|
1945
|
+
pendingSourceIds: uniqueSorted(input.sourceIds),
|
|
1946
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1947
|
+
});
|
|
1948
|
+
return "suppressed";
|
|
1949
|
+
}
|
|
1552
1950
|
let terminalPrompt = null;
|
|
1951
|
+
let terminalPromptFingerprint = null;
|
|
1553
1952
|
try {
|
|
1554
1953
|
if (target.kind === "terminal") {
|
|
1555
1954
|
const gate = await this.activationGate.evaluate(target);
|
|
@@ -1572,19 +1971,41 @@ class AgentInboxService {
|
|
|
1572
1971
|
this.reconcileAgentStatus(target.agentId);
|
|
1573
1972
|
return "offline";
|
|
1574
1973
|
}
|
|
1974
|
+
const reminder = await this.buildTerminalReminder(target, inbox.inboxId, totalUnackedCount, input.summary, input.entries);
|
|
1975
|
+
const prompt = reminder.prompt;
|
|
1976
|
+
const promptFingerprint = reminder.fingerprint;
|
|
1977
|
+
terminalPrompt = prompt;
|
|
1978
|
+
terminalPromptFingerprint = promptFingerprint;
|
|
1575
1979
|
if (gate.outcome === "defer") {
|
|
1980
|
+
const deferReason = toActivationDispatchDeferReason(gate.reason, this.logger);
|
|
1981
|
+
const hasExistingDeferred = state?.deferReason != null;
|
|
1982
|
+
const deferAttempts = hasExistingDeferred ? state.deferAttempts + 1 : 1;
|
|
1983
|
+
const deferredAt = (0, util_1.nowIso)();
|
|
1984
|
+
const retryMs = terminalDeferRetryMs(deferAttempts);
|
|
1985
|
+
const existingLeaseMs = state?.leaseExpiresAt == null ? Number.NaN : Date.parse(state.leaseExpiresAt);
|
|
1986
|
+
const candidateLeaseMs = Date.now() + retryMs;
|
|
1987
|
+
const nextLeaseMs = Number.isNaN(existingLeaseMs)
|
|
1988
|
+
? candidateLeaseMs
|
|
1989
|
+
: Math.max(existingLeaseMs, candidateLeaseMs);
|
|
1576
1990
|
this.logger.debug("activation.dispatch_deferred", {
|
|
1577
1991
|
agentId: input.agentId,
|
|
1578
1992
|
targetId: target.targetId,
|
|
1579
|
-
reason:
|
|
1580
|
-
retryMs
|
|
1993
|
+
reason: deferReason,
|
|
1994
|
+
retryMs,
|
|
1995
|
+
deferAttempts,
|
|
1996
|
+
pendingFingerprint: promptFingerprint,
|
|
1581
1997
|
});
|
|
1582
1998
|
this.store.upsertActivationDispatchState({
|
|
1583
1999
|
agentId: input.agentId,
|
|
1584
2000
|
targetId: input.targetId,
|
|
1585
2001
|
status: "dirty",
|
|
1586
|
-
leaseExpiresAt: new Date(
|
|
2002
|
+
leaseExpiresAt: new Date(nextLeaseMs).toISOString(),
|
|
1587
2003
|
lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
|
|
2004
|
+
deferReason,
|
|
2005
|
+
deferAttempts,
|
|
2006
|
+
firstDeferredAt: state?.firstDeferredAt ?? deferredAt,
|
|
2007
|
+
lastDeferredAt: deferredAt,
|
|
2008
|
+
pendingFingerprint: promptFingerprint,
|
|
1588
2009
|
pendingNewItemCount: input.newItemCount,
|
|
1589
2010
|
pendingSummary: input.summary,
|
|
1590
2011
|
pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
|
|
@@ -1593,34 +2014,20 @@ class AgentInboxService {
|
|
|
1593
2014
|
});
|
|
1594
2015
|
return "deferred";
|
|
1595
2016
|
}
|
|
1596
|
-
|
|
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);
|
|
2017
|
+
if (state?.pendingFingerprint && state.pendingFingerprint !== promptFingerprint) {
|
|
2018
|
+
this.logger.debug("activation.defer_pending_revalidated", {
|
|
2019
|
+
agentId: input.agentId,
|
|
2020
|
+
targetId: target.targetId,
|
|
2021
|
+
previousFingerprint: state.pendingFingerprint,
|
|
2022
|
+
currentFingerprint: promptFingerprint,
|
|
2023
|
+
});
|
|
1610
2024
|
}
|
|
1611
|
-
|
|
1612
|
-
inboxId: inbox.inboxId,
|
|
1613
|
-
totalUnackedCount,
|
|
1614
|
-
summary: input.summary,
|
|
1615
|
-
preview,
|
|
1616
|
-
});
|
|
1617
|
-
terminalPrompt = prompt;
|
|
1618
|
-
const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
|
|
1619
|
-
if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
|
|
2025
|
+
if (state?.lastNotifiedFingerprint === promptFingerprint) {
|
|
1620
2026
|
this.logger.debug("activation.dispatch_suppressed", {
|
|
1621
2027
|
agentId: input.agentId,
|
|
1622
2028
|
targetId: target.targetId,
|
|
1623
2029
|
reason: "unchanged_terminal_prompt",
|
|
2030
|
+
status: state.status,
|
|
1624
2031
|
});
|
|
1625
2032
|
this.store.upsertActivationDispatchState({
|
|
1626
2033
|
agentId: input.agentId,
|
|
@@ -1628,6 +2035,11 @@ class AgentInboxService {
|
|
|
1628
2035
|
status: "notified",
|
|
1629
2036
|
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1630
2037
|
lastNotifiedFingerprint: promptFingerprint,
|
|
2038
|
+
deferReason: null,
|
|
2039
|
+
deferAttempts: 0,
|
|
2040
|
+
firstDeferredAt: null,
|
|
2041
|
+
lastDeferredAt: null,
|
|
2042
|
+
pendingFingerprint: null,
|
|
1631
2043
|
pendingNewItemCount: 0,
|
|
1632
2044
|
pendingSummary: null,
|
|
1633
2045
|
pendingSubscriptionIds: [],
|
|
@@ -1653,16 +2065,22 @@ class AgentInboxService {
|
|
|
1653
2065
|
else {
|
|
1654
2066
|
const activation = {
|
|
1655
2067
|
kind: "agentinbox.activation",
|
|
1656
|
-
activationId: (0, util_1.
|
|
2068
|
+
activationId: (0, util_1.generateCanonicalId)("act"),
|
|
1657
2069
|
agentId: input.agentId,
|
|
1658
2070
|
inboxId: inbox.inboxId,
|
|
1659
2071
|
targetId: target.targetId,
|
|
1660
2072
|
targetKind: target.kind,
|
|
1661
2073
|
subscriptionIds: input.subscriptionIds,
|
|
1662
2074
|
sourceIds: input.sourceIds,
|
|
2075
|
+
newEntryCount: input.entries.length,
|
|
1663
2076
|
newItemCount: input.newItemCount,
|
|
1664
2077
|
summary,
|
|
1665
|
-
items: target.mode === "activation_with_items"
|
|
2078
|
+
items: target.mode === "activation_with_items"
|
|
2079
|
+
? input.entries
|
|
2080
|
+
.filter((entry) => entry.kind === "item")
|
|
2081
|
+
.map((entry) => entry.item)
|
|
2082
|
+
: undefined,
|
|
2083
|
+
entries: target.mode === "activation_with_items" && input.entries.length > 0 ? input.entries : undefined,
|
|
1666
2084
|
createdAt: (0, util_1.nowIso)(),
|
|
1667
2085
|
deliveredAt: null,
|
|
1668
2086
|
};
|
|
@@ -1677,8 +2095,13 @@ class AgentInboxService {
|
|
|
1677
2095
|
status: "notified",
|
|
1678
2096
|
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1679
2097
|
lastNotifiedFingerprint: target.kind === "terminal"
|
|
1680
|
-
?
|
|
2098
|
+
? terminalPromptFingerprint
|
|
1681
2099
|
: state?.lastNotifiedFingerprint ?? null,
|
|
2100
|
+
deferReason: null,
|
|
2101
|
+
deferAttempts: 0,
|
|
2102
|
+
firstDeferredAt: null,
|
|
2103
|
+
lastDeferredAt: null,
|
|
2104
|
+
pendingFingerprint: null,
|
|
1682
2105
|
pendingNewItemCount: 0,
|
|
1683
2106
|
pendingSummary: null,
|
|
1684
2107
|
pendingSubscriptionIds: [],
|
|
@@ -1726,6 +2149,11 @@ class AgentInboxService {
|
|
|
1726
2149
|
status: "dirty",
|
|
1727
2150
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1728
2151
|
lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
|
|
2152
|
+
deferReason: null,
|
|
2153
|
+
deferAttempts: 0,
|
|
2154
|
+
firstDeferredAt: null,
|
|
2155
|
+
lastDeferredAt: null,
|
|
2156
|
+
pendingFingerprint: null,
|
|
1729
2157
|
pendingNewItemCount: entries.length,
|
|
1730
2158
|
pendingSummary: latestSummary(entries),
|
|
1731
2159
|
pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
@@ -1733,6 +2161,33 @@ class AgentInboxService {
|
|
|
1733
2161
|
updatedAt: (0, util_1.nowIso)(),
|
|
1734
2162
|
});
|
|
1735
2163
|
}
|
|
2164
|
+
async buildTerminalReminder(target, inboxId, totalUnackedCount, summary, entries) {
|
|
2165
|
+
let preview = null;
|
|
2166
|
+
if (totalUnackedCount === 1 && entries.length === 1 && entries[0]?.kind === "item") {
|
|
2167
|
+
const singleItem = entries[0].item;
|
|
2168
|
+
const source = this.store.getSource(singleItem.sourceId);
|
|
2169
|
+
if (source) {
|
|
2170
|
+
try {
|
|
2171
|
+
const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
|
|
2172
|
+
preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
|
|
2173
|
+
}
|
|
2174
|
+
catch {
|
|
2175
|
+
preview = null;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
preview ??= (0, terminal_1.deriveInlineItemPreview)(singleItem, summary);
|
|
2179
|
+
}
|
|
2180
|
+
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
2181
|
+
inboxId,
|
|
2182
|
+
totalUnackedCount,
|
|
2183
|
+
summary,
|
|
2184
|
+
preview,
|
|
2185
|
+
});
|
|
2186
|
+
return {
|
|
2187
|
+
prompt,
|
|
2188
|
+
fingerprint: terminalReminderFingerprint(prompt, entries),
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
1736
2191
|
detectDirectInboxSender(env) {
|
|
1737
2192
|
try {
|
|
1738
2193
|
const detected = (0, terminal_1.detectTerminalContext)(env);
|
|
@@ -2006,6 +2461,43 @@ class AgentInboxService {
|
|
|
2006
2461
|
}
|
|
2007
2462
|
}
|
|
2008
2463
|
}
|
|
2464
|
+
async syncPendingDigestThreads(force = false) {
|
|
2465
|
+
const now = (0, util_1.nowIso)();
|
|
2466
|
+
const threads = force
|
|
2467
|
+
? this.store.listOpenDigestThreads()
|
|
2468
|
+
: this.store.listDueDigestThreads(now);
|
|
2469
|
+
for (const thread of threads) {
|
|
2470
|
+
try {
|
|
2471
|
+
const source = this.store.getSource(thread.sourceId);
|
|
2472
|
+
if (!source) {
|
|
2473
|
+
this.store.closeDigestThread(thread.threadId, now);
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
const entry = await this.flushDigestThread(source, thread.threadId);
|
|
2477
|
+
if (!entry) {
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
const inbox = this.store.getInbox(thread.inboxId);
|
|
2481
|
+
if (!inbox) {
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
this.notifyInboxWatchers(inbox.ownerAgentId, [entry]);
|
|
2485
|
+
const targets = this.store.listActivationTargetsForAgent(inbox.ownerAgentId).filter((target) => target.status === "active");
|
|
2486
|
+
for (const target of targets) {
|
|
2487
|
+
this.enqueueActivationTarget(target, {
|
|
2488
|
+
agentId: inbox.ownerAgentId,
|
|
2489
|
+
subscriptionId: null,
|
|
2490
|
+
sourceId: thread.sourceId,
|
|
2491
|
+
summary: entry.summary,
|
|
2492
|
+
entry,
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
catch (error) {
|
|
2497
|
+
console.warn(`digest thread flush failed for ${thread.threadId}:`, error);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2009
2501
|
async syncLifecycleGc() {
|
|
2010
2502
|
const now = Date.now();
|
|
2011
2503
|
if (now - this.lastLifecycleCleanupAt >= DEFAULT_GC_INTERVAL_MS) {
|
|
@@ -2310,6 +2802,29 @@ function resolveDeliveryHandle(request) {
|
|
|
2310
2802
|
replyMode: request.replyMode ?? null,
|
|
2311
2803
|
};
|
|
2312
2804
|
}
|
|
2805
|
+
function resolveDeliverySource(store, sourceId, handle) {
|
|
2806
|
+
if (!sourceId) {
|
|
2807
|
+
return null;
|
|
2808
|
+
}
|
|
2809
|
+
const source = store.getSource(sourceId);
|
|
2810
|
+
if (!source) {
|
|
2811
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
2812
|
+
}
|
|
2813
|
+
const expectedProvider = providerForSourceType(source.sourceType);
|
|
2814
|
+
if (expectedProvider && expectedProvider !== handle.provider) {
|
|
2815
|
+
throw new Error(`source ${sourceId} does not match delivery provider ${handle.provider}`);
|
|
2816
|
+
}
|
|
2817
|
+
return source;
|
|
2818
|
+
}
|
|
2819
|
+
function providerForSourceType(sourceType) {
|
|
2820
|
+
if (sourceType === "github_repo") {
|
|
2821
|
+
return "github";
|
|
2822
|
+
}
|
|
2823
|
+
if (sourceType === "feishu_bot") {
|
|
2824
|
+
return "feishu";
|
|
2825
|
+
}
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2313
2828
|
class ActivationDispatcher {
|
|
2314
2829
|
async dispatch(targetUrl, activation) {
|
|
2315
2830
|
try {
|
|
@@ -2352,6 +2867,76 @@ function validateNotifyLeaseMs(value) {
|
|
|
2352
2867
|
throw new Error("notifyLeaseMs must be a positive integer");
|
|
2353
2868
|
}
|
|
2354
2869
|
}
|
|
2870
|
+
function validateMinUnackedItems(value) {
|
|
2871
|
+
if (value == null) {
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2875
|
+
throw new Error("minUnackedItems must be a positive integer");
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function notificationPolicyForTarget(target) {
|
|
2879
|
+
return target.notificationPolicy ?? {
|
|
2880
|
+
notifyLeaseMs: target.notifyLeaseMs,
|
|
2881
|
+
minUnackedItems: target.minUnackedItems ?? null,
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
function inboxAggregationPolicy(inbox) {
|
|
2885
|
+
return {
|
|
2886
|
+
enabled: inbox.aggregationEnabled ?? false,
|
|
2887
|
+
windowMs: inbox.aggregationWindowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS,
|
|
2888
|
+
maxItems: inbox.aggregationMaxItems ?? DEFAULT_INBOX_AGGREGATION_MAX_ITEMS,
|
|
2889
|
+
maxThreadAgeMs: inbox.aggregationMaxThreadAgeMs ?? DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS,
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function meetsNotificationThreshold(policy, totalUnackedCount) {
|
|
2893
|
+
if (policy.minUnackedItems == null) {
|
|
2894
|
+
return true;
|
|
2895
|
+
}
|
|
2896
|
+
return totalUnackedCount >= policy.minUnackedItems;
|
|
2897
|
+
}
|
|
2898
|
+
function activationItemFromInboxItem(item) {
|
|
2899
|
+
return {
|
|
2900
|
+
itemId: item.itemId,
|
|
2901
|
+
sourceId: item.sourceId,
|
|
2902
|
+
sourceNativeId: item.sourceNativeId,
|
|
2903
|
+
eventVariant: item.eventVariant,
|
|
2904
|
+
inboxId: item.inboxId,
|
|
2905
|
+
occurredAt: item.occurredAt,
|
|
2906
|
+
metadata: item.metadata,
|
|
2907
|
+
rawPayload: item.rawPayload,
|
|
2908
|
+
deliveryHandle: item.deliveryHandle,
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
function digestGroupKey(sourceId, resourceRef, eventFamily) {
|
|
2912
|
+
return `${sourceId}::${resourceRef}::${eventFamily}`;
|
|
2913
|
+
}
|
|
2914
|
+
function sameStringArray(left, right) {
|
|
2915
|
+
if (left.length !== right.length) {
|
|
2916
|
+
return false;
|
|
2917
|
+
}
|
|
2918
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
2919
|
+
if (left[index] !== right[index]) {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return true;
|
|
2924
|
+
}
|
|
2925
|
+
function sameDeliveryHandle(left, right) {
|
|
2926
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
2927
|
+
}
|
|
2928
|
+
function sharedDeliveryHandle(items) {
|
|
2929
|
+
const first = items[0]?.deliveryHandle ?? null;
|
|
2930
|
+
if (!first) {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
for (const item of items.slice(1)) {
|
|
2934
|
+
if (!sameDeliveryHandle(first, item.deliveryHandle ?? null)) {
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
return first;
|
|
2939
|
+
}
|
|
2355
2940
|
function normalizeWebhookActivationMode(mode) {
|
|
2356
2941
|
const resolved = mode ?? "activation_only";
|
|
2357
2942
|
if (!WEBHOOK_ACTIVATION_MODES.has(resolved)) {
|
|
@@ -2425,11 +3010,26 @@ function terminalReminderFingerprint(prompt, items) {
|
|
|
2425
3010
|
const payload = JSON.stringify({
|
|
2426
3011
|
prompt,
|
|
2427
3012
|
itemIds: items
|
|
2428
|
-
.map((item) => item.
|
|
3013
|
+
.map((item) => item.entryId)
|
|
2429
3014
|
.sort((left, right) => left.localeCompare(right)),
|
|
2430
3015
|
});
|
|
2431
3016
|
return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
|
|
2432
3017
|
}
|
|
3018
|
+
function toActivationDispatchDeferReason(reason, logger) {
|
|
3019
|
+
if (reason === "terminal_recently_active" || reason === "terminal_busy") {
|
|
3020
|
+
return reason;
|
|
3021
|
+
}
|
|
3022
|
+
logger?.warn("activation.unexpected_defer_reason", {
|
|
3023
|
+
reason,
|
|
3024
|
+
fallback: reason,
|
|
3025
|
+
});
|
|
3026
|
+
return reason;
|
|
3027
|
+
}
|
|
3028
|
+
function terminalDeferRetryMs(attempts) {
|
|
3029
|
+
const normalizedAttempts = Math.max(1, attempts);
|
|
3030
|
+
const multiplier = 2 ** (normalizedAttempts - 1);
|
|
3031
|
+
return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
|
|
3032
|
+
}
|
|
2433
3033
|
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
2434
3034
|
if (sourceType === "github_repo_ci") {
|
|
2435
3035
|
const parts = eventVariant.split(".");
|
|
@@ -2748,3 +3348,48 @@ function weekdayToNumber(value) {
|
|
|
2748
3348
|
throw new Error(`unsupported weekday: ${value}`);
|
|
2749
3349
|
}
|
|
2750
3350
|
}
|
|
3351
|
+
function listHostStreamKinds(hostType) {
|
|
3352
|
+
switch (hostType) {
|
|
3353
|
+
case "github":
|
|
3354
|
+
return ["repo_events", "ci_runs"];
|
|
3355
|
+
case "feishu":
|
|
3356
|
+
return ["message_events"];
|
|
3357
|
+
case "local_event":
|
|
3358
|
+
return ["events"];
|
|
3359
|
+
case "remote_source":
|
|
3360
|
+
return ["default"];
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
function getHostConfigFields(hostType) {
|
|
3364
|
+
switch (hostType) {
|
|
3365
|
+
case "github":
|
|
3366
|
+
return [
|
|
3367
|
+
{ name: "uxcAuth", type: "string", description: "Optional shared GitHub auth/runtime profile.", required: false },
|
|
3368
|
+
];
|
|
3369
|
+
case "feishu":
|
|
3370
|
+
return [
|
|
3371
|
+
{ name: "appId", type: "string", description: "Feishu app ID.", required: true },
|
|
3372
|
+
{ name: "appSecret", type: "string", description: "Feishu app secret.", required: true },
|
|
3373
|
+
{ name: "uxcAuth", type: "string", description: "Optional shared Feishu auth/runtime profile.", required: false },
|
|
3374
|
+
];
|
|
3375
|
+
case "local_event":
|
|
3376
|
+
return [];
|
|
3377
|
+
case "remote_source":
|
|
3378
|
+
return [];
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
function sourceTypeForStreamRegistration(hostType, streamKind) {
|
|
3382
|
+
if (hostType === "github") {
|
|
3383
|
+
if (streamKind === "ci_runs") {
|
|
3384
|
+
return "github_repo_ci";
|
|
3385
|
+
}
|
|
3386
|
+
return "github_repo";
|
|
3387
|
+
}
|
|
3388
|
+
if (hostType === "feishu") {
|
|
3389
|
+
return "feishu_bot";
|
|
3390
|
+
}
|
|
3391
|
+
if (hostType === "local_event") {
|
|
3392
|
+
return "local_event";
|
|
3393
|
+
}
|
|
3394
|
+
return "remote_source";
|
|
3395
|
+
}
|