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