@holon-run/agentinbox 0.1.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/LICENSE +178 -0
- package/README.md +168 -0
- package/dist/src/adapters.js +111 -0
- package/dist/src/backend.js +175 -0
- package/dist/src/cli.js +620 -0
- package/dist/src/client.js +279 -0
- package/dist/src/control_server.js +93 -0
- package/dist/src/daemon.js +246 -0
- package/dist/src/filter.js +167 -0
- package/dist/src/http.js +408 -0
- package/dist/src/matcher.js +47 -0
- package/dist/src/model.js +2 -0
- package/dist/src/paths.js +141 -0
- package/dist/src/service.js +1338 -0
- package/dist/src/source_schema.js +150 -0
- package/dist/src/sources/feishu.js +567 -0
- package/dist/src/sources/github.js +485 -0
- package/dist/src/sources/github_ci.js +372 -0
- package/dist/src/store.js +1271 -0
- package/dist/src/terminal.js +301 -0
- package/dist/src/util.js +36 -0
- package/package.json +52 -0
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ActivationDispatcher = exports.AgentInboxService = void 0;
|
|
4
|
+
const backend_1 = require("./backend");
|
|
5
|
+
const util_1 = require("./util");
|
|
6
|
+
const source_schema_1 = require("./source_schema");
|
|
7
|
+
const filter_1 = require("./filter");
|
|
8
|
+
const terminal_1 = require("./terminal");
|
|
9
|
+
const DEFAULT_SUBSCRIPTION_POLL_LIMIT = 100;
|
|
10
|
+
const DEFAULT_ACTIVATION_WINDOW_MS = 3_000;
|
|
11
|
+
const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
|
|
12
|
+
const DEFAULT_NOTIFY_LEASE_MS = 10 * 60 * 1000;
|
|
13
|
+
const DEFAULT_NOTIFY_RETRY_MS = 5_000;
|
|
14
|
+
const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
+
const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
|
|
17
|
+
const WEBHOOK_ACTIVATION_MODES = new Set(["activation_only", "activation_with_items"]);
|
|
18
|
+
const SUBSCRIPTION_START_POLICIES = new Set(["latest", "earliest", "at_offset", "at_time"]);
|
|
19
|
+
const SUBSCRIPTION_LIFECYCLE_MODES = new Set(["standing", "temporary"]);
|
|
20
|
+
class AgentInboxService {
|
|
21
|
+
store;
|
|
22
|
+
adapters;
|
|
23
|
+
backend;
|
|
24
|
+
inFlightSubscriptions = new Set();
|
|
25
|
+
activationWindowMs;
|
|
26
|
+
activationMaxItems;
|
|
27
|
+
ackedRetentionMs;
|
|
28
|
+
notificationBuffers = new Map();
|
|
29
|
+
inboxWatchers = new Map();
|
|
30
|
+
syncInterval = null;
|
|
31
|
+
stopping = false;
|
|
32
|
+
lastAckedInboxGcAt = 0;
|
|
33
|
+
lastOfflineAgentGcAt = 0;
|
|
34
|
+
constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher()) {
|
|
35
|
+
this.store = store;
|
|
36
|
+
this.adapters = adapters;
|
|
37
|
+
this.activationDispatcher = activationDispatcher;
|
|
38
|
+
this.backend = backend ?? new backend_1.SqliteEventBusBackend(store);
|
|
39
|
+
this.activationWindowMs = activationPolicy?.windowMs ?? DEFAULT_ACTIVATION_WINDOW_MS;
|
|
40
|
+
this.activationMaxItems = activationPolicy?.maxItems ?? DEFAULT_ACTIVATION_MAX_ITEMS;
|
|
41
|
+
this.ackedRetentionMs = DEFAULT_ACKED_RETENTION_MS;
|
|
42
|
+
this.terminalDispatcher = terminalDispatcher;
|
|
43
|
+
}
|
|
44
|
+
activationDispatcher;
|
|
45
|
+
terminalDispatcher;
|
|
46
|
+
async start() {
|
|
47
|
+
if (this.syncInterval) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.stopping = false;
|
|
51
|
+
this.syncInterval = setInterval(() => {
|
|
52
|
+
void this.syncAllSubscriptions();
|
|
53
|
+
void this.syncActivationDispatchStates();
|
|
54
|
+
void this.runAckedInboxGcIfDue();
|
|
55
|
+
void this.syncLifecycleGc();
|
|
56
|
+
}, 2_000);
|
|
57
|
+
await this.syncAllSubscriptions();
|
|
58
|
+
await this.syncActivationDispatchStates();
|
|
59
|
+
await this.runAckedInboxGcIfDue(true);
|
|
60
|
+
await this.syncLifecycleGc();
|
|
61
|
+
}
|
|
62
|
+
async stop() {
|
|
63
|
+
this.stopping = true;
|
|
64
|
+
for (const buffer of this.notificationBuffers.values()) {
|
|
65
|
+
if (buffer.timer) {
|
|
66
|
+
clearTimeout(buffer.timer);
|
|
67
|
+
buffer.timer = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (this.syncInterval) {
|
|
71
|
+
clearInterval(this.syncInterval);
|
|
72
|
+
this.syncInterval = null;
|
|
73
|
+
}
|
|
74
|
+
await this.flushAllPendingNotifications();
|
|
75
|
+
}
|
|
76
|
+
async registerSource(input) {
|
|
77
|
+
if (input.sourceType === "remote_source") {
|
|
78
|
+
throw new Error("source type is reserved and not yet supported: remote_source");
|
|
79
|
+
}
|
|
80
|
+
const existing = this.store.getSourceByKey(input.sourceType, input.sourceKey);
|
|
81
|
+
if (existing) {
|
|
82
|
+
return existing;
|
|
83
|
+
}
|
|
84
|
+
const now = (0, util_1.nowIso)();
|
|
85
|
+
const source = {
|
|
86
|
+
sourceId: (0, util_1.generateId)("src"),
|
|
87
|
+
sourceType: input.sourceType,
|
|
88
|
+
sourceKey: input.sourceKey,
|
|
89
|
+
configRef: input.configRef ?? null,
|
|
90
|
+
config: input.config ?? {},
|
|
91
|
+
status: "active",
|
|
92
|
+
checkpoint: null,
|
|
93
|
+
createdAt: now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
};
|
|
96
|
+
this.store.insertSource(source);
|
|
97
|
+
await this.ensureStreamForSource(source);
|
|
98
|
+
await this.adapters.sourceAdapterFor(source.sourceType).ensureSource(source);
|
|
99
|
+
return source;
|
|
100
|
+
}
|
|
101
|
+
listSources() {
|
|
102
|
+
return this.store.listSources();
|
|
103
|
+
}
|
|
104
|
+
getSource(sourceId) {
|
|
105
|
+
const source = this.store.getSource(sourceId);
|
|
106
|
+
if (!source) {
|
|
107
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
108
|
+
}
|
|
109
|
+
return source;
|
|
110
|
+
}
|
|
111
|
+
getSourceDetails(sourceId) {
|
|
112
|
+
const source = this.getSource(sourceId);
|
|
113
|
+
return {
|
|
114
|
+
source,
|
|
115
|
+
schema: (0, source_schema_1.getSourceSchema)(source.sourceType),
|
|
116
|
+
stream: this.store.getStreamBySourceId(sourceId),
|
|
117
|
+
subscriptions: this.store.listSubscriptionsForSource(sourceId),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
getSourceSchema(sourceType) {
|
|
121
|
+
return (0, source_schema_1.getSourceSchema)(sourceType);
|
|
122
|
+
}
|
|
123
|
+
registerAgent(input) {
|
|
124
|
+
validateNotifyLeaseMs(input.notifyLeaseMs);
|
|
125
|
+
validateTerminalRegistration(input);
|
|
126
|
+
const runtimeKind = input.runtimeKind ?? "unknown";
|
|
127
|
+
const agentId = input.agentId ?? (0, terminal_1.assignedAgentIdFromContext)({
|
|
128
|
+
runtimeKind,
|
|
129
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
130
|
+
backend: input.backend,
|
|
131
|
+
tmuxPaneId: input.tmuxPaneId ?? null,
|
|
132
|
+
itermSessionId: input.itermSessionId ?? null,
|
|
133
|
+
tty: input.tty ?? null,
|
|
134
|
+
});
|
|
135
|
+
const now = (0, util_1.nowIso)();
|
|
136
|
+
this.handleAgentRegistrationConflicts(agentId, input);
|
|
137
|
+
const existingAgent = this.store.getAgent(agentId);
|
|
138
|
+
const agent = existingAgent
|
|
139
|
+
? this.store.updateAgent(agentId, {
|
|
140
|
+
status: "active",
|
|
141
|
+
offlineSince: null,
|
|
142
|
+
runtimeKind,
|
|
143
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
lastSeenAt: now,
|
|
146
|
+
})
|
|
147
|
+
: (() => {
|
|
148
|
+
const created = {
|
|
149
|
+
agentId,
|
|
150
|
+
status: "active",
|
|
151
|
+
offlineSince: null,
|
|
152
|
+
runtimeKind,
|
|
153
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
154
|
+
createdAt: now,
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
lastSeenAt: now,
|
|
157
|
+
};
|
|
158
|
+
this.store.insertAgent(created);
|
|
159
|
+
return created;
|
|
160
|
+
})();
|
|
161
|
+
const terminalTarget = this.upsertTerminalActivationTarget(agent.agentId, input, now);
|
|
162
|
+
const inbox = this.ensureInboxForAgent(agent.agentId);
|
|
163
|
+
return {
|
|
164
|
+
agent,
|
|
165
|
+
terminalTarget,
|
|
166
|
+
inbox,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
detectAndRegisterAgent(notifyLeaseMs, env = process.env) {
|
|
170
|
+
const detected = (0, terminal_1.detectTerminalContext)(env);
|
|
171
|
+
return this.registerAgent({
|
|
172
|
+
runtimeKind: detected.runtimeKind,
|
|
173
|
+
runtimeSessionId: detected.runtimeSessionId ?? null,
|
|
174
|
+
backend: detected.backend,
|
|
175
|
+
mode: "agent_prompt",
|
|
176
|
+
tmuxPaneId: detected.tmuxPaneId ?? null,
|
|
177
|
+
tty: detected.tty ?? null,
|
|
178
|
+
termProgram: detected.termProgram ?? null,
|
|
179
|
+
itermSessionId: detected.itermSessionId ?? null,
|
|
180
|
+
notifyLeaseMs: notifyLeaseMs ?? null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
listAgents() {
|
|
184
|
+
return this.store.listAgents();
|
|
185
|
+
}
|
|
186
|
+
getAgent(agentId) {
|
|
187
|
+
const agent = this.store.getAgent(agentId);
|
|
188
|
+
if (!agent) {
|
|
189
|
+
throw new Error(`unknown agent: ${agentId}`);
|
|
190
|
+
}
|
|
191
|
+
return agent;
|
|
192
|
+
}
|
|
193
|
+
getAgentDetails(agentId) {
|
|
194
|
+
const agent = this.getAgent(agentId);
|
|
195
|
+
return {
|
|
196
|
+
agent,
|
|
197
|
+
inbox: this.ensureInboxForAgent(agent.agentId),
|
|
198
|
+
subscriptions: this.store.listSubscriptionsForAgent(agent.agentId),
|
|
199
|
+
activationTargets: this.store.listActivationTargetsForAgent(agent.agentId),
|
|
200
|
+
activationDispatchStates: this.store.listActivationDispatchStatesForAgent(agent.agentId),
|
|
201
|
+
itemCounts: this.inboxCounts(agent.agentId),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
removeAgent(agentId) {
|
|
205
|
+
this.getAgent(agentId);
|
|
206
|
+
this.store.deleteAgent(agentId);
|
|
207
|
+
this.notificationBuffers.forEach((buffer, key) => {
|
|
208
|
+
if (key.startsWith(`${agentId}:`)) {
|
|
209
|
+
if (buffer.timer) {
|
|
210
|
+
clearTimeout(buffer.timer);
|
|
211
|
+
}
|
|
212
|
+
this.notificationBuffers.delete(key);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.inboxWatchers.delete(agentId);
|
|
216
|
+
return { removed: true };
|
|
217
|
+
}
|
|
218
|
+
addWebhookActivationTarget(agentId, input) {
|
|
219
|
+
this.getAgent(agentId);
|
|
220
|
+
validateNotifyLeaseMs(input.notifyLeaseMs);
|
|
221
|
+
const mode = normalizeWebhookActivationMode(input.activationMode);
|
|
222
|
+
const now = (0, util_1.nowIso)();
|
|
223
|
+
const target = {
|
|
224
|
+
targetId: (0, util_1.generateId)("tgt"),
|
|
225
|
+
agentId,
|
|
226
|
+
kind: "webhook",
|
|
227
|
+
status: "active",
|
|
228
|
+
offlineSince: null,
|
|
229
|
+
consecutiveFailures: 0,
|
|
230
|
+
lastDeliveredAt: null,
|
|
231
|
+
lastError: null,
|
|
232
|
+
mode,
|
|
233
|
+
url: input.url,
|
|
234
|
+
notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
|
|
235
|
+
createdAt: now,
|
|
236
|
+
updatedAt: now,
|
|
237
|
+
lastSeenAt: now,
|
|
238
|
+
};
|
|
239
|
+
this.store.insertActivationTarget(target);
|
|
240
|
+
this.markAgentActive(agentId);
|
|
241
|
+
return target;
|
|
242
|
+
}
|
|
243
|
+
listActivationTargets(agentId) {
|
|
244
|
+
if (agentId) {
|
|
245
|
+
this.getAgent(agentId);
|
|
246
|
+
return this.store.listActivationTargetsForAgent(agentId);
|
|
247
|
+
}
|
|
248
|
+
return this.store.listActivationTargets();
|
|
249
|
+
}
|
|
250
|
+
getActivationTarget(targetId) {
|
|
251
|
+
const target = this.store.getActivationTarget(targetId);
|
|
252
|
+
if (!target) {
|
|
253
|
+
throw new Error(`unknown activation target: ${targetId}`);
|
|
254
|
+
}
|
|
255
|
+
return target;
|
|
256
|
+
}
|
|
257
|
+
removeActivationTarget(agentId, targetId) {
|
|
258
|
+
const target = this.getActivationTarget(targetId);
|
|
259
|
+
if (target.agentId !== agentId) {
|
|
260
|
+
throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
|
|
261
|
+
}
|
|
262
|
+
this.store.deleteActivationTarget(agentId, targetId);
|
|
263
|
+
this.reconcileAgentStatus(agentId);
|
|
264
|
+
return { removed: true };
|
|
265
|
+
}
|
|
266
|
+
async registerSubscription(input) {
|
|
267
|
+
this.getAgent(input.agentId);
|
|
268
|
+
const source = this.store.getSource(input.sourceId);
|
|
269
|
+
if (!source) {
|
|
270
|
+
throw new Error(`unknown source: ${input.sourceId}`);
|
|
271
|
+
}
|
|
272
|
+
await (0, filter_1.validateSubscriptionFilter)(input.filter ?? {});
|
|
273
|
+
const lifecycleMode = input.lifecycleMode ?? "standing";
|
|
274
|
+
if (!SUBSCRIPTION_LIFECYCLE_MODES.has(lifecycleMode)) {
|
|
275
|
+
throw new Error(`unsupported lifecycle mode: ${lifecycleMode}`);
|
|
276
|
+
}
|
|
277
|
+
const subscription = {
|
|
278
|
+
subscriptionId: (0, util_1.generateId)("sub"),
|
|
279
|
+
agentId: input.agentId,
|
|
280
|
+
sourceId: input.sourceId,
|
|
281
|
+
filter: input.filter ?? {},
|
|
282
|
+
lifecycleMode,
|
|
283
|
+
expiresAt: input.expiresAt ?? null,
|
|
284
|
+
startPolicy: input.startPolicy ?? "latest",
|
|
285
|
+
startOffset: input.startOffset ?? null,
|
|
286
|
+
startTime: input.startTime ?? null,
|
|
287
|
+
createdAt: (0, util_1.nowIso)(),
|
|
288
|
+
};
|
|
289
|
+
this.ensureInboxForAgent(subscription.agentId);
|
|
290
|
+
this.store.insertSubscription(subscription);
|
|
291
|
+
const stream = await this.ensureStreamForSource(source);
|
|
292
|
+
await this.backend.ensureConsumer({
|
|
293
|
+
streamId: stream.streamId,
|
|
294
|
+
subscriptionId: subscription.subscriptionId,
|
|
295
|
+
consumerKey: `subscription:${subscription.subscriptionId}`,
|
|
296
|
+
startPolicy: subscription.startPolicy,
|
|
297
|
+
startOffset: subscription.startOffset ?? null,
|
|
298
|
+
startTime: subscription.startTime ?? null,
|
|
299
|
+
});
|
|
300
|
+
return subscription;
|
|
301
|
+
}
|
|
302
|
+
async removeSubscription(subscriptionId) {
|
|
303
|
+
const subscription = this.getSubscription(subscriptionId);
|
|
304
|
+
await this.backend.deleteConsumer({ subscriptionId });
|
|
305
|
+
this.store.deleteSubscription(subscriptionId);
|
|
306
|
+
this.clearSubscriptionRuntimeState(subscription);
|
|
307
|
+
return { removed: true, subscriptionId };
|
|
308
|
+
}
|
|
309
|
+
listSubscriptions(filters) {
|
|
310
|
+
return this.store.listSubscriptions().filter((subscription) => {
|
|
311
|
+
if (filters?.sourceId && subscription.sourceId !== filters.sourceId) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (filters?.agentId && subscription.agentId !== filters.agentId) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
getSubscription(subscriptionId) {
|
|
321
|
+
const subscription = this.store.getSubscription(subscriptionId);
|
|
322
|
+
if (!subscription) {
|
|
323
|
+
throw new Error(`unknown subscription: ${subscriptionId}`);
|
|
324
|
+
}
|
|
325
|
+
return subscription;
|
|
326
|
+
}
|
|
327
|
+
async getSubscriptionDetails(subscriptionId) {
|
|
328
|
+
const subscription = this.getSubscription(subscriptionId);
|
|
329
|
+
const consumer = await this.backend.getConsumer({ subscriptionId });
|
|
330
|
+
if (!consumer) {
|
|
331
|
+
throw new Error(`unknown consumer for subscription: ${subscriptionId}`);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
subscription,
|
|
335
|
+
source: this.getSource(subscription.sourceId),
|
|
336
|
+
inbox: this.ensureInboxForAgent(subscription.agentId),
|
|
337
|
+
activationTargets: this.store.listActivationTargetsForAgent(subscription.agentId),
|
|
338
|
+
consumer,
|
|
339
|
+
lag: await this.backend.getConsumerLag({ consumerId: consumer.consumerId }),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async getSubscriptionLag(subscriptionId) {
|
|
343
|
+
this.getSubscription(subscriptionId);
|
|
344
|
+
return this.backend.getConsumerLag({ subscriptionId });
|
|
345
|
+
}
|
|
346
|
+
async resetSubscription(input) {
|
|
347
|
+
this.getSubscription(input.subscriptionId);
|
|
348
|
+
if (!SUBSCRIPTION_START_POLICIES.has(input.startPolicy)) {
|
|
349
|
+
throw new Error(`unsupported start policy: ${input.startPolicy}`);
|
|
350
|
+
}
|
|
351
|
+
const consumer = await this.backend.getConsumer({ subscriptionId: input.subscriptionId });
|
|
352
|
+
if (!consumer) {
|
|
353
|
+
throw new Error(`unknown consumer for subscription: ${input.subscriptionId}`);
|
|
354
|
+
}
|
|
355
|
+
const reset = await this.backend.reset({
|
|
356
|
+
consumerId: consumer.consumerId,
|
|
357
|
+
startPolicy: input.startPolicy,
|
|
358
|
+
startOffset: input.startOffset ?? null,
|
|
359
|
+
startTime: input.startTime ?? null,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
subscription: this.getSubscription(input.subscriptionId),
|
|
363
|
+
consumer: reset,
|
|
364
|
+
lag: await this.backend.getConsumerLag({ consumerId: reset.consumerId }),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async appendSourceEvent(input) {
|
|
368
|
+
const source = this.store.getSource(input.sourceId);
|
|
369
|
+
if (!source) {
|
|
370
|
+
throw new Error(`unknown source: ${input.sourceId}`);
|
|
371
|
+
}
|
|
372
|
+
const stream = await this.ensureStreamForSource(source);
|
|
373
|
+
const result = await this.backend.append({
|
|
374
|
+
streamId: stream.streamId,
|
|
375
|
+
events: [input],
|
|
376
|
+
});
|
|
377
|
+
return (0, backend_1.toAppendResult)(result);
|
|
378
|
+
}
|
|
379
|
+
async appendSourceEventByCaller(sourceId, input) {
|
|
380
|
+
const source = this.getSource(sourceId);
|
|
381
|
+
if (source.sourceType !== "local_event") {
|
|
382
|
+
throw new Error(`manual append is not supported for source type: ${source.sourceType}`);
|
|
383
|
+
}
|
|
384
|
+
return this.appendSourceEvent({ ...input, sourceId });
|
|
385
|
+
}
|
|
386
|
+
async appendFixtureEvent(sourceId, input) {
|
|
387
|
+
const source = this.getSource(sourceId);
|
|
388
|
+
if (source.sourceType !== "fixture") {
|
|
389
|
+
throw new Error(`fixtures/emit requires fixture source, received: ${source.sourceType}`);
|
|
390
|
+
}
|
|
391
|
+
return this.appendSourceEvent({ ...input, sourceId });
|
|
392
|
+
}
|
|
393
|
+
listInboxAgentIds() {
|
|
394
|
+
return this.store.listInboxes().map((inbox) => inbox.ownerAgentId);
|
|
395
|
+
}
|
|
396
|
+
getInboxDetailsByAgent(agentId) {
|
|
397
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
398
|
+
return {
|
|
399
|
+
agent: this.getAgent(agentId),
|
|
400
|
+
inbox,
|
|
401
|
+
subscriptions: this.store.listSubscriptionsForAgent(agentId),
|
|
402
|
+
activationTargets: this.store.listActivationTargetsForAgent(agentId),
|
|
403
|
+
activationDispatchStates: this.store.listActivationDispatchStatesForAgent(agentId),
|
|
404
|
+
itemCounts: this.inboxCounts(agentId),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
listInboxItems(agentId, options) {
|
|
408
|
+
return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
409
|
+
}
|
|
410
|
+
watchInbox(agentId, options, onEvent) {
|
|
411
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
412
|
+
const pendingItems = [];
|
|
413
|
+
let started = false;
|
|
414
|
+
const emitItems = (items) => {
|
|
415
|
+
if (items.length === 0) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
onEvent({
|
|
419
|
+
event: "items",
|
|
420
|
+
agentId,
|
|
421
|
+
items,
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
const watcher = {
|
|
425
|
+
onItems: (items) => {
|
|
426
|
+
if (!started) {
|
|
427
|
+
pendingItems.push(...items);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
emitItems(items);
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
let watchers = this.inboxWatchers.get(agentId);
|
|
434
|
+
if (!watchers) {
|
|
435
|
+
watchers = new Set();
|
|
436
|
+
this.inboxWatchers.set(agentId, watchers);
|
|
437
|
+
}
|
|
438
|
+
watchers.add(watcher);
|
|
439
|
+
let initialItems;
|
|
440
|
+
try {
|
|
441
|
+
initialItems = this.store.listInboxItems(inbox.inboxId, {
|
|
442
|
+
afterItemId: options.afterItemId,
|
|
443
|
+
includeAcked: options.includeAcked ?? false,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
watchers.delete(watcher);
|
|
448
|
+
if (watchers.size === 0) {
|
|
449
|
+
this.inboxWatchers.delete(agentId);
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
const initialItemIds = new Set(initialItems.map((item) => item.itemId));
|
|
454
|
+
return {
|
|
455
|
+
initialItems,
|
|
456
|
+
start: () => {
|
|
457
|
+
if (started) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
started = true;
|
|
461
|
+
const replayItems = pendingItems.filter((item) => !initialItemIds.has(item.itemId));
|
|
462
|
+
pendingItems.length = 0;
|
|
463
|
+
emitItems(replayItems);
|
|
464
|
+
},
|
|
465
|
+
close: () => {
|
|
466
|
+
const activeWatchers = this.inboxWatchers.get(agentId);
|
|
467
|
+
if (!activeWatchers) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
activeWatchers.delete(watcher);
|
|
471
|
+
if (activeWatchers.size === 0) {
|
|
472
|
+
this.inboxWatchers.delete(agentId);
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
ackInboxItems(agentId, itemIds) {
|
|
478
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
479
|
+
const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
|
|
480
|
+
if (acked > 0) {
|
|
481
|
+
void this.handleInboxAckEffects(agentId);
|
|
482
|
+
}
|
|
483
|
+
return { acked };
|
|
484
|
+
}
|
|
485
|
+
ackInboxItemsThrough(agentId, itemId) {
|
|
486
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
487
|
+
const acked = this.store.ackItemsThrough(inbox.inboxId, itemId, (0, util_1.nowIso)());
|
|
488
|
+
if (acked > 0) {
|
|
489
|
+
void this.handleInboxAckEffects(agentId);
|
|
490
|
+
}
|
|
491
|
+
return { acked };
|
|
492
|
+
}
|
|
493
|
+
ackAllInboxItems(agentId) {
|
|
494
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
495
|
+
const itemIds = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => item.itemId);
|
|
496
|
+
const acked = this.store.ackItems(inbox.inboxId, itemIds, (0, util_1.nowIso)());
|
|
497
|
+
if (acked > 0) {
|
|
498
|
+
void this.handleInboxAckEffects(agentId);
|
|
499
|
+
}
|
|
500
|
+
return { acked };
|
|
501
|
+
}
|
|
502
|
+
compactInbox(agentId) {
|
|
503
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
504
|
+
return {
|
|
505
|
+
deleted: this.store.deleteAckedInboxItems(inbox.inboxId, retentionCutoffIso(this.ackedRetentionMs)),
|
|
506
|
+
retentionMs: this.ackedRetentionMs,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
gcAckedInboxItems() {
|
|
510
|
+
return {
|
|
511
|
+
deleted: this.store.deleteAckedInboxItemsGlobal(retentionCutoffIso(this.ackedRetentionMs)),
|
|
512
|
+
retentionMs: this.ackedRetentionMs,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
gc() {
|
|
516
|
+
const acked = this.gcAckedInboxItems();
|
|
517
|
+
const lifecycle = this.gcOfflineAgents();
|
|
518
|
+
return {
|
|
519
|
+
deleted: acked.deleted,
|
|
520
|
+
retentionMs: acked.retentionMs,
|
|
521
|
+
removedAgents: lifecycle.removedAgents,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async pollSource(sourceId) {
|
|
525
|
+
return this.adapters.pollSource(this.getSource(sourceId));
|
|
526
|
+
}
|
|
527
|
+
async pollSubscription(subscriptionId) {
|
|
528
|
+
if (this.inFlightSubscriptions.has(subscriptionId)) {
|
|
529
|
+
const subscription = this.getSubscription(subscriptionId);
|
|
530
|
+
return {
|
|
531
|
+
subscriptionId,
|
|
532
|
+
sourceId: subscription.sourceId,
|
|
533
|
+
eventsRead: 0,
|
|
534
|
+
matched: 0,
|
|
535
|
+
inboxItemsCreated: 0,
|
|
536
|
+
committedOffset: null,
|
|
537
|
+
note: "subscription poll already in flight",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
this.inFlightSubscriptions.add(subscriptionId);
|
|
541
|
+
try {
|
|
542
|
+
const subscription = this.getSubscription(subscriptionId);
|
|
543
|
+
const source = this.getSource(subscription.sourceId);
|
|
544
|
+
const stream = await this.ensureStreamForSource(source);
|
|
545
|
+
const consumer = await this.backend.ensureConsumer({
|
|
546
|
+
streamId: stream.streamId,
|
|
547
|
+
subscriptionId: subscription.subscriptionId,
|
|
548
|
+
consumerKey: `subscription:${subscription.subscriptionId}`,
|
|
549
|
+
startPolicy: subscription.startPolicy,
|
|
550
|
+
startOffset: subscription.startOffset ?? null,
|
|
551
|
+
startTime: subscription.startTime ?? null,
|
|
552
|
+
});
|
|
553
|
+
const batch = await this.backend.read({
|
|
554
|
+
streamId: stream.streamId,
|
|
555
|
+
consumerId: consumer.consumerId,
|
|
556
|
+
limit: DEFAULT_SUBSCRIPTION_POLL_LIMIT,
|
|
557
|
+
});
|
|
558
|
+
const inbox = this.ensureInboxForAgent(subscription.agentId);
|
|
559
|
+
const targets = this.store.listActivationTargetsForAgent(subscription.agentId).filter((target) => target.status === "active");
|
|
560
|
+
let matched = 0;
|
|
561
|
+
let inboxItemsCreated = 0;
|
|
562
|
+
let lastProcessedOffset = null;
|
|
563
|
+
const insertedItems = [];
|
|
564
|
+
try {
|
|
565
|
+
for (const event of batch.events) {
|
|
566
|
+
lastProcessedOffset = event.offset;
|
|
567
|
+
const match = await (0, filter_1.matchSubscriptionFilter)(subscription.filter, {
|
|
568
|
+
metadata: event.metadata,
|
|
569
|
+
payload: event.rawPayload,
|
|
570
|
+
eventVariant: event.eventVariant,
|
|
571
|
+
sourceType: source.sourceType,
|
|
572
|
+
sourceKey: source.sourceKey,
|
|
573
|
+
});
|
|
574
|
+
if (!match.matched) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
matched += 1;
|
|
578
|
+
const item = {
|
|
579
|
+
itemId: (0, util_1.generateId)("item"),
|
|
580
|
+
sourceId: event.sourceId,
|
|
581
|
+
sourceNativeId: event.sourceNativeId,
|
|
582
|
+
eventVariant: event.eventVariant,
|
|
583
|
+
inboxId: inbox.inboxId,
|
|
584
|
+
occurredAt: event.occurredAt,
|
|
585
|
+
metadata: { ...event.metadata, matchReason: match.reason, agentId: subscription.agentId },
|
|
586
|
+
rawPayload: event.rawPayload,
|
|
587
|
+
deliveryHandle: event.deliveryHandle,
|
|
588
|
+
ackedAt: null,
|
|
589
|
+
};
|
|
590
|
+
const inserted = this.store.insertInboxItem(item);
|
|
591
|
+
if (!inserted) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
inboxItemsCreated += 1;
|
|
595
|
+
insertedItems.push(item);
|
|
596
|
+
const activationItem = {
|
|
597
|
+
itemId: item.itemId,
|
|
598
|
+
sourceId: item.sourceId,
|
|
599
|
+
sourceNativeId: item.sourceNativeId,
|
|
600
|
+
eventVariant: item.eventVariant,
|
|
601
|
+
inboxId: item.inboxId,
|
|
602
|
+
occurredAt: item.occurredAt,
|
|
603
|
+
metadata: item.metadata,
|
|
604
|
+
rawPayload: item.rawPayload,
|
|
605
|
+
deliveryHandle: item.deliveryHandle,
|
|
606
|
+
};
|
|
607
|
+
for (const target of targets) {
|
|
608
|
+
this.enqueueActivationTarget(target, {
|
|
609
|
+
agentId: subscription.agentId,
|
|
610
|
+
subscriptionId: subscription.subscriptionId,
|
|
611
|
+
sourceId: source.sourceId,
|
|
612
|
+
summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
|
|
613
|
+
item: activationItem,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (insertedItems.length > 0) {
|
|
620
|
+
this.notifyInboxWatchers(subscription.agentId, insertedItems);
|
|
621
|
+
}
|
|
622
|
+
if (lastProcessedOffset != null) {
|
|
623
|
+
await this.backend.commit({
|
|
624
|
+
consumerId: consumer.consumerId,
|
|
625
|
+
committedOffset: lastProcessedOffset,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
if (insertedItems.length > 0) {
|
|
631
|
+
this.notifyInboxWatchers(subscription.agentId, insertedItems);
|
|
632
|
+
}
|
|
633
|
+
if (lastProcessedOffset != null) {
|
|
634
|
+
await this.backend.commit({
|
|
635
|
+
consumerId: consumer.consumerId,
|
|
636
|
+
committedOffset: lastProcessedOffset,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
subscriptionId: subscription.subscriptionId,
|
|
641
|
+
sourceId: subscription.sourceId,
|
|
642
|
+
eventsRead: batch.events.length,
|
|
643
|
+
matched,
|
|
644
|
+
inboxItemsCreated,
|
|
645
|
+
committedOffset: lastProcessedOffset,
|
|
646
|
+
note: batch.events.length === 0 ? "no new stream events" : "subscription batch processed",
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
this.inFlightSubscriptions.delete(subscriptionId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async sendDelivery(request) {
|
|
654
|
+
const handle = resolveDeliveryHandle(request);
|
|
655
|
+
const attempt = {
|
|
656
|
+
deliveryId: (0, util_1.generateId)("dlv"),
|
|
657
|
+
provider: handle.provider,
|
|
658
|
+
surface: handle.surface,
|
|
659
|
+
targetRef: handle.targetRef,
|
|
660
|
+
threadRef: handle.threadRef ?? null,
|
|
661
|
+
replyMode: handle.replyMode ?? null,
|
|
662
|
+
kind: request.kind,
|
|
663
|
+
payload: request.payload,
|
|
664
|
+
status: "accepted",
|
|
665
|
+
createdAt: (0, util_1.nowIso)(),
|
|
666
|
+
};
|
|
667
|
+
const adapter = this.adapters.deliveryAdapterFor(handle.provider);
|
|
668
|
+
const result = await adapter.send(request, attempt);
|
|
669
|
+
const storedAttempt = { ...attempt, status: result.status };
|
|
670
|
+
this.store.insertDelivery(storedAttempt);
|
|
671
|
+
return { ...storedAttempt, note: result.note };
|
|
672
|
+
}
|
|
673
|
+
status() {
|
|
674
|
+
return {
|
|
675
|
+
retention: {
|
|
676
|
+
ackedInboxItemsMs: this.ackedRetentionMs,
|
|
677
|
+
gcIntervalMs: DEFAULT_GC_INTERVAL_MS,
|
|
678
|
+
lastAckedInboxGcAt: this.lastAckedInboxGcAt > 0 ? new Date(this.lastAckedInboxGcAt).toISOString() : null,
|
|
679
|
+
},
|
|
680
|
+
counts: this.store.getCounts(),
|
|
681
|
+
agents: this.store.listAgents(),
|
|
682
|
+
sources: this.store.listSources(),
|
|
683
|
+
subscriptions: this.store.listSubscriptions(),
|
|
684
|
+
inboxes: this.store.listInboxes(),
|
|
685
|
+
activationTargets: this.store.listActivationTargets(),
|
|
686
|
+
activationDispatchStates: this.store.listActivationDispatchStates(),
|
|
687
|
+
streams: this.store.listStreams(),
|
|
688
|
+
consumers: this.store.listConsumers(),
|
|
689
|
+
adapters: this.adapters.status(),
|
|
690
|
+
recentActivations: this.store.listActivations().slice(0, 10),
|
|
691
|
+
recentDeliveries: this.store.listDeliveries().slice(0, 10),
|
|
692
|
+
lifecycle: {
|
|
693
|
+
offlineAgentTtlMs: DEFAULT_OFFLINE_AGENT_TTL_MS,
|
|
694
|
+
gcIntervalMs: DEFAULT_GC_INTERVAL_MS,
|
|
695
|
+
lastOfflineAgentGcAt: this.lastOfflineAgentGcAt > 0 ? new Date(this.lastOfflineAgentGcAt).toISOString() : null,
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
ensureInboxForAgent(agentId) {
|
|
700
|
+
const existing = this.store.getInboxByAgentId(agentId);
|
|
701
|
+
if (existing) {
|
|
702
|
+
return existing;
|
|
703
|
+
}
|
|
704
|
+
const inbox = {
|
|
705
|
+
inboxId: (0, backend_1.defaultInboxIdForAgent)(agentId),
|
|
706
|
+
ownerAgentId: agentId,
|
|
707
|
+
createdAt: (0, util_1.nowIso)(),
|
|
708
|
+
};
|
|
709
|
+
this.store.insertInbox(inbox);
|
|
710
|
+
return inbox;
|
|
711
|
+
}
|
|
712
|
+
inboxCounts(agentId) {
|
|
713
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
714
|
+
const total = this.store.countInboxItems(inbox.inboxId, true);
|
|
715
|
+
const unacked = this.store.countInboxItems(inbox.inboxId, false);
|
|
716
|
+
return {
|
|
717
|
+
total,
|
|
718
|
+
unacked,
|
|
719
|
+
acked: total - unacked,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
async runAckedInboxGcIfDue(force = false) {
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
if (!force && now - this.lastAckedInboxGcAt < DEFAULT_GC_INTERVAL_MS) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
this.lastAckedInboxGcAt = now;
|
|
728
|
+
this.gcAckedInboxItems();
|
|
729
|
+
}
|
|
730
|
+
async ensureStreamForSource(source) {
|
|
731
|
+
return this.backend.ensureStream({
|
|
732
|
+
sourceId: source.sourceId,
|
|
733
|
+
streamKey: (0, backend_1.streamKeyForSource)(source.sourceType, source.sourceKey),
|
|
734
|
+
backend: "sqlite",
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
notifyInboxWatchers(agentId, items) {
|
|
738
|
+
const watchers = this.inboxWatchers.get(agentId);
|
|
739
|
+
if (!watchers || watchers.size === 0) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
for (const watcher of watchers) {
|
|
743
|
+
watcher.onItems(items);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
handleAgentRegistrationConflicts(agentId, input) {
|
|
747
|
+
const currentTarget = findExistingTerminalActivationTarget(this.store, input);
|
|
748
|
+
if (currentTarget && currentTarget.agentId !== agentId) {
|
|
749
|
+
if (!input.forceRebind) {
|
|
750
|
+
throw new Error(`agent register conflict: current terminal target ${currentTarget.targetId} is already bound to agent ${currentTarget.agentId}; retry with forceRebind to rebind`);
|
|
751
|
+
}
|
|
752
|
+
this.store.deleteActivationTarget(currentTarget.agentId, currentTarget.targetId);
|
|
753
|
+
this.reconcileAgentStatus(currentTarget.agentId);
|
|
754
|
+
}
|
|
755
|
+
const conflictingTargets = this.store
|
|
756
|
+
.listActivationTargetsForAgent(agentId)
|
|
757
|
+
.filter((target) => target.kind === "terminal")
|
|
758
|
+
.filter((target) => !isSameTerminalIdentity(target, input));
|
|
759
|
+
if (conflictingTargets.length > 0 && !input.forceRebind) {
|
|
760
|
+
throw new Error(`agent register conflict: agent ${agentId} is already bound to terminal target ${conflictingTargets[0].targetId}; retry with forceRebind to rebind`);
|
|
761
|
+
}
|
|
762
|
+
if (input.forceRebind) {
|
|
763
|
+
for (const target of conflictingTargets) {
|
|
764
|
+
this.store.deleteActivationTarget(agentId, target.targetId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
upsertTerminalActivationTarget(agentId, input, now) {
|
|
769
|
+
const existing = findExistingTerminalActivationTarget(this.store, input);
|
|
770
|
+
if (existing) {
|
|
771
|
+
const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
|
|
772
|
+
runtimeKind: input.runtimeKind ?? "unknown",
|
|
773
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
774
|
+
tmuxPaneId: input.tmuxPaneId ?? null,
|
|
775
|
+
tty: input.tty ?? null,
|
|
776
|
+
termProgram: input.termProgram ?? null,
|
|
777
|
+
itermSessionId: input.itermSessionId ?? null,
|
|
778
|
+
updatedAt: now,
|
|
779
|
+
lastSeenAt: now,
|
|
780
|
+
});
|
|
781
|
+
if (target.agentId !== agentId) {
|
|
782
|
+
throw new Error(`terminal target ${target.targetId} is already bound to agent ${target.agentId}`);
|
|
783
|
+
}
|
|
784
|
+
this.markAgentActive(agentId);
|
|
785
|
+
return target;
|
|
786
|
+
}
|
|
787
|
+
const target = {
|
|
788
|
+
targetId: (0, util_1.generateId)("tgt"),
|
|
789
|
+
agentId,
|
|
790
|
+
kind: "terminal",
|
|
791
|
+
status: "active",
|
|
792
|
+
offlineSince: null,
|
|
793
|
+
consecutiveFailures: 0,
|
|
794
|
+
lastDeliveredAt: null,
|
|
795
|
+
lastError: null,
|
|
796
|
+
mode: "agent_prompt",
|
|
797
|
+
notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
|
|
798
|
+
runtimeKind: input.runtimeKind ?? "unknown",
|
|
799
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
800
|
+
backend: input.backend,
|
|
801
|
+
tmuxPaneId: input.tmuxPaneId ?? null,
|
|
802
|
+
tty: input.tty ?? null,
|
|
803
|
+
termProgram: input.termProgram ?? null,
|
|
804
|
+
itermSessionId: input.itermSessionId ?? null,
|
|
805
|
+
createdAt: now,
|
|
806
|
+
updatedAt: now,
|
|
807
|
+
lastSeenAt: now,
|
|
808
|
+
};
|
|
809
|
+
this.store.insertActivationTarget(target);
|
|
810
|
+
this.markAgentActive(agentId);
|
|
811
|
+
return target;
|
|
812
|
+
}
|
|
813
|
+
enqueueActivationTarget(target, input) {
|
|
814
|
+
const key = notificationBufferKey(target.agentId, target.targetId);
|
|
815
|
+
let buffer = this.notificationBuffers.get(key);
|
|
816
|
+
if (!buffer) {
|
|
817
|
+
buffer = {
|
|
818
|
+
agentId: target.agentId,
|
|
819
|
+
targetId: target.targetId,
|
|
820
|
+
pending: [],
|
|
821
|
+
timer: null,
|
|
822
|
+
inFlight: false,
|
|
823
|
+
};
|
|
824
|
+
this.notificationBuffers.set(key, buffer);
|
|
825
|
+
}
|
|
826
|
+
buffer.pending.push({
|
|
827
|
+
subscriptionId: input.subscriptionId,
|
|
828
|
+
sourceId: input.sourceId,
|
|
829
|
+
summary: input.summary,
|
|
830
|
+
item: input.item,
|
|
831
|
+
});
|
|
832
|
+
if (!buffer.timer && !buffer.inFlight) {
|
|
833
|
+
buffer.timer = setTimeout(() => {
|
|
834
|
+
void this.flushNotificationBuffer(key);
|
|
835
|
+
}, this.activationWindowMs);
|
|
836
|
+
}
|
|
837
|
+
if (buffer.pending.length >= this.activationMaxItems) {
|
|
838
|
+
if (buffer.timer) {
|
|
839
|
+
clearTimeout(buffer.timer);
|
|
840
|
+
buffer.timer = null;
|
|
841
|
+
}
|
|
842
|
+
void this.flushNotificationBuffer(key);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
clearSubscriptionRuntimeState(subscription) {
|
|
846
|
+
for (const [key, buffer] of this.notificationBuffers.entries()) {
|
|
847
|
+
const retained = buffer.pending.filter((entry) => entry.subscriptionId !== subscription.subscriptionId);
|
|
848
|
+
if (retained.length === buffer.pending.length) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
buffer.pending = retained;
|
|
852
|
+
if (buffer.pending.length === 0) {
|
|
853
|
+
if (buffer.timer) {
|
|
854
|
+
clearTimeout(buffer.timer);
|
|
855
|
+
buffer.timer = null;
|
|
856
|
+
}
|
|
857
|
+
this.notificationBuffers.delete(key);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const remainingSubscriptions = this.store.listSubscriptionsForAgent(subscription.agentId);
|
|
861
|
+
const states = this.store.listActivationDispatchStatesForAgent(subscription.agentId);
|
|
862
|
+
for (const state of states) {
|
|
863
|
+
const directlyReferencesRemovedSubscription = state.pendingSubscriptionIds.includes(subscription.subscriptionId);
|
|
864
|
+
if (!directlyReferencesRemovedSubscription && remainingSubscriptions.length > 0) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
// Dispatch state is stored per target rather than per subscription.
|
|
868
|
+
// Drop states that still directly reference the removed subscription.
|
|
869
|
+
// If the agent has no subscriptions left, also clear any residual lease
|
|
870
|
+
// state so future subscriptions do not inherit a stale notified window.
|
|
871
|
+
this.store.deleteActivationDispatchState(state.agentId, state.targetId);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async flushAllPendingNotifications() {
|
|
875
|
+
const keys = Array.from(this.notificationBuffers.keys());
|
|
876
|
+
for (const key of keys) {
|
|
877
|
+
await this.flushNotificationBuffer(key);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async flushNotificationBuffer(key) {
|
|
881
|
+
const buffer = this.notificationBuffers.get(key);
|
|
882
|
+
if (!buffer || buffer.inFlight || buffer.pending.length === 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (buffer.timer) {
|
|
886
|
+
clearTimeout(buffer.timer);
|
|
887
|
+
buffer.timer = null;
|
|
888
|
+
}
|
|
889
|
+
buffer.inFlight = true;
|
|
890
|
+
const entries = buffer.pending.splice(0, buffer.pending.length);
|
|
891
|
+
try {
|
|
892
|
+
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
893
|
+
if (!state) {
|
|
894
|
+
const dispatched = await this.dispatchActivationTarget({
|
|
895
|
+
agentId: buffer.agentId,
|
|
896
|
+
targetId: buffer.targetId,
|
|
897
|
+
newItemCount: entries.length,
|
|
898
|
+
summary: entries[0]?.summary ?? null,
|
|
899
|
+
subscriptionIds: uniqueSorted(entries.map((entry) => entry.subscriptionId)),
|
|
900
|
+
sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
901
|
+
items: entries.map((entry) => entry.item),
|
|
902
|
+
});
|
|
903
|
+
if (dispatched === "retryable_failure") {
|
|
904
|
+
this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
this.store.upsertActivationDispatchState({
|
|
909
|
+
agentId: buffer.agentId,
|
|
910
|
+
targetId: buffer.targetId,
|
|
911
|
+
status: "dirty",
|
|
912
|
+
leaseExpiresAt: state.leaseExpiresAt,
|
|
913
|
+
pendingNewItemCount: state.pendingNewItemCount + entries.length,
|
|
914
|
+
pendingSummary: state.pendingSummary ?? entries[0]?.summary ?? null,
|
|
915
|
+
pendingSubscriptionIds: uniqueSorted([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
916
|
+
pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
917
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const hasPendingDuringFlight = buffer.pending.length > 0;
|
|
921
|
+
buffer.inFlight = false;
|
|
922
|
+
if (hasPendingDuringFlight) {
|
|
923
|
+
buffer.timer = setTimeout(() => {
|
|
924
|
+
void this.flushNotificationBuffer(key);
|
|
925
|
+
}, this.activationWindowMs);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
this.notificationBuffers.delete(key);
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
buffer.pending.unshift(...entries);
|
|
932
|
+
buffer.inFlight = false;
|
|
933
|
+
if (!this.stopping && !buffer.timer) {
|
|
934
|
+
buffer.timer = setTimeout(() => {
|
|
935
|
+
void this.flushNotificationBuffer(key);
|
|
936
|
+
}, this.activationWindowMs);
|
|
937
|
+
}
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async handleInboxAckEffects(agentId) {
|
|
942
|
+
const states = this.store.listActivationDispatchStatesForAgent(agentId);
|
|
943
|
+
for (const state of states) {
|
|
944
|
+
await this.maybeDispatchActivationTarget(agentId, state.targetId, "ack");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async maybeDispatchActivationTarget(agentId, targetId, reason) {
|
|
948
|
+
const state = this.store.getActivationDispatchState(agentId, targetId);
|
|
949
|
+
if (!state) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
953
|
+
const unacked = this.store.countInboxItems(inbox.inboxId, false);
|
|
954
|
+
if (unacked === 0) {
|
|
955
|
+
this.store.deleteActivationDispatchState(agentId, targetId);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (reason === "ack" && state.status !== "dirty") {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const dispatched = await this.dispatchActivationTarget({
|
|
962
|
+
agentId,
|
|
963
|
+
targetId,
|
|
964
|
+
newItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
|
|
965
|
+
summary: state.pendingSummary,
|
|
966
|
+
subscriptionIds: state.pendingSubscriptionIds,
|
|
967
|
+
sourceIds: state.pendingSourceIds,
|
|
968
|
+
items: this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
|
|
969
|
+
itemId: item.itemId,
|
|
970
|
+
sourceId: item.sourceId,
|
|
971
|
+
sourceNativeId: item.sourceNativeId,
|
|
972
|
+
eventVariant: item.eventVariant,
|
|
973
|
+
inboxId: item.inboxId,
|
|
974
|
+
occurredAt: item.occurredAt,
|
|
975
|
+
metadata: item.metadata,
|
|
976
|
+
rawPayload: item.rawPayload,
|
|
977
|
+
deliveryHandle: item.deliveryHandle,
|
|
978
|
+
})),
|
|
979
|
+
});
|
|
980
|
+
if (dispatched === "offline") {
|
|
981
|
+
this.store.deleteActivationDispatchState(agentId, targetId);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (dispatched === "retryable_failure") {
|
|
985
|
+
this.store.upsertActivationDispatchState({
|
|
986
|
+
...state,
|
|
987
|
+
status: "dirty",
|
|
988
|
+
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
989
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async dispatchActivationTarget(input) {
|
|
994
|
+
const target = this.getActivationTarget(input.targetId);
|
|
995
|
+
if (target.status === "offline") {
|
|
996
|
+
return "offline";
|
|
997
|
+
}
|
|
998
|
+
const inbox = this.ensureInboxForAgent(input.agentId);
|
|
999
|
+
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1000
|
+
try {
|
|
1001
|
+
if (target.kind === "terminal") {
|
|
1002
|
+
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1003
|
+
inboxId: inbox.inboxId,
|
|
1004
|
+
newItemCount: input.newItemCount,
|
|
1005
|
+
summary: input.summary,
|
|
1006
|
+
});
|
|
1007
|
+
await this.terminalDispatcher.dispatch(target, prompt);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
const activation = {
|
|
1011
|
+
kind: "agentinbox.activation",
|
|
1012
|
+
activationId: (0, util_1.generateId)("act"),
|
|
1013
|
+
agentId: input.agentId,
|
|
1014
|
+
inboxId: inbox.inboxId,
|
|
1015
|
+
targetId: target.targetId,
|
|
1016
|
+
targetKind: target.kind,
|
|
1017
|
+
subscriptionIds: input.subscriptionIds,
|
|
1018
|
+
sourceIds: input.sourceIds,
|
|
1019
|
+
newItemCount: input.newItemCount,
|
|
1020
|
+
summary,
|
|
1021
|
+
items: target.mode === "activation_with_items" && input.items.length > 0 ? input.items : undefined,
|
|
1022
|
+
createdAt: (0, util_1.nowIso)(),
|
|
1023
|
+
deliveredAt: null,
|
|
1024
|
+
};
|
|
1025
|
+
await this.activationDispatcher.dispatch(target.url, activation);
|
|
1026
|
+
this.store.insertActivation(activation);
|
|
1027
|
+
}
|
|
1028
|
+
this.markActivationTargetDelivered(target.targetId);
|
|
1029
|
+
this.markAgentActive(input.agentId);
|
|
1030
|
+
this.store.upsertActivationDispatchState({
|
|
1031
|
+
agentId: input.agentId,
|
|
1032
|
+
targetId: input.targetId,
|
|
1033
|
+
status: "notified",
|
|
1034
|
+
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1035
|
+
pendingNewItemCount: 0,
|
|
1036
|
+
pendingSummary: null,
|
|
1037
|
+
pendingSubscriptionIds: [],
|
|
1038
|
+
pendingSourceIds: [],
|
|
1039
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1040
|
+
});
|
|
1041
|
+
return "dispatched";
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
console.warn(`activation target dispatch failed for ${target.targetId}:`, error);
|
|
1045
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1046
|
+
if (target.kind === "terminal") {
|
|
1047
|
+
const exists = await this.terminalDispatcher.probe(target);
|
|
1048
|
+
if (!exists) {
|
|
1049
|
+
this.markActivationTargetOffline(target.targetId, message);
|
|
1050
|
+
this.reconcileAgentStatus(target.agentId);
|
|
1051
|
+
return "offline";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
this.markActivationTargetDispatchFailure(target.targetId, message);
|
|
1055
|
+
return "retryable_failure";
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
upsertDirtyDispatchState(agentId, targetId, entries) {
|
|
1059
|
+
this.store.upsertActivationDispatchState({
|
|
1060
|
+
agentId,
|
|
1061
|
+
targetId,
|
|
1062
|
+
status: "dirty",
|
|
1063
|
+
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1064
|
+
pendingNewItemCount: entries.length,
|
|
1065
|
+
pendingSummary: entries[0]?.summary ?? null,
|
|
1066
|
+
pendingSubscriptionIds: uniqueSorted(entries.map((entry) => entry.subscriptionId)),
|
|
1067
|
+
pendingSourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1068
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
markActivationTargetDelivered(targetId) {
|
|
1072
|
+
const now = (0, util_1.nowIso)();
|
|
1073
|
+
this.store.updateActivationTargetRuntime(targetId, {
|
|
1074
|
+
status: "active",
|
|
1075
|
+
offlineSince: null,
|
|
1076
|
+
consecutiveFailures: 0,
|
|
1077
|
+
lastDeliveredAt: now,
|
|
1078
|
+
lastError: null,
|
|
1079
|
+
updatedAt: now,
|
|
1080
|
+
lastSeenAt: now,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
markActivationTargetDispatchFailure(targetId, message) {
|
|
1084
|
+
const target = this.getActivationTarget(targetId);
|
|
1085
|
+
this.store.updateActivationTargetRuntime(targetId, {
|
|
1086
|
+
status: "active",
|
|
1087
|
+
offlineSince: null,
|
|
1088
|
+
consecutiveFailures: target.consecutiveFailures + 1,
|
|
1089
|
+
lastError: message,
|
|
1090
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
markActivationTargetOffline(targetId, message) {
|
|
1094
|
+
const now = (0, util_1.nowIso)();
|
|
1095
|
+
const target = this.getActivationTarget(targetId);
|
|
1096
|
+
this.store.updateActivationTargetRuntime(targetId, {
|
|
1097
|
+
status: "offline",
|
|
1098
|
+
offlineSince: target.offlineSince ?? now,
|
|
1099
|
+
consecutiveFailures: target.consecutiveFailures + 1,
|
|
1100
|
+
lastError: message,
|
|
1101
|
+
updatedAt: now,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
markAgentActive(agentId) {
|
|
1105
|
+
const agent = this.getAgent(agentId);
|
|
1106
|
+
if (agent.status === "active" && !agent.offlineSince) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const now = (0, util_1.nowIso)();
|
|
1110
|
+
this.store.updateAgent(agentId, {
|
|
1111
|
+
status: "active",
|
|
1112
|
+
offlineSince: null,
|
|
1113
|
+
runtimeKind: agent.runtimeKind,
|
|
1114
|
+
runtimeSessionId: agent.runtimeSessionId ?? null,
|
|
1115
|
+
updatedAt: now,
|
|
1116
|
+
lastSeenAt: now,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
reconcileAgentStatus(agentId) {
|
|
1120
|
+
const agent = this.getAgent(agentId);
|
|
1121
|
+
if (this.store.countActiveActivationTargetsForAgent(agentId) > 0) {
|
|
1122
|
+
this.markAgentActive(agentId);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const now = (0, util_1.nowIso)();
|
|
1126
|
+
this.store.updateAgent(agentId, {
|
|
1127
|
+
status: "offline",
|
|
1128
|
+
offlineSince: agent.offlineSince ?? now,
|
|
1129
|
+
runtimeKind: agent.runtimeKind,
|
|
1130
|
+
runtimeSessionId: agent.runtimeSessionId ?? null,
|
|
1131
|
+
updatedAt: now,
|
|
1132
|
+
lastSeenAt: agent.lastSeenAt,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
gcOfflineAgents(now = Date.now()) {
|
|
1136
|
+
const cutoffIso = new Date(now - DEFAULT_OFFLINE_AGENT_TTL_MS).toISOString();
|
|
1137
|
+
const agents = this.store.listOfflineAgentsOlderThan(cutoffIso);
|
|
1138
|
+
for (const agent of agents) {
|
|
1139
|
+
this.store.deleteAgent(agent.agentId, { persist: false });
|
|
1140
|
+
this.notificationBuffers.forEach((buffer, key) => {
|
|
1141
|
+
if (key.startsWith(`${agent.agentId}:`)) {
|
|
1142
|
+
if (buffer.timer) {
|
|
1143
|
+
clearTimeout(buffer.timer);
|
|
1144
|
+
}
|
|
1145
|
+
this.notificationBuffers.delete(key);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
this.inboxWatchers.delete(agent.agentId);
|
|
1149
|
+
}
|
|
1150
|
+
if (agents.length > 0) {
|
|
1151
|
+
this.store.save();
|
|
1152
|
+
}
|
|
1153
|
+
return { removedAgents: agents.length };
|
|
1154
|
+
}
|
|
1155
|
+
async syncAllSubscriptions() {
|
|
1156
|
+
const subscriptions = this.store.listSubscriptions();
|
|
1157
|
+
for (const subscription of subscriptions) {
|
|
1158
|
+
try {
|
|
1159
|
+
await this.pollSubscription(subscription.subscriptionId);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
console.warn(`subscription poll failed for ${subscription.subscriptionId}:`, error);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async syncActivationDispatchStates() {
|
|
1167
|
+
const now = Date.now();
|
|
1168
|
+
const states = this.store.listActivationDispatchStates();
|
|
1169
|
+
for (const state of states) {
|
|
1170
|
+
if (!state.leaseExpiresAt) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const expiresAt = Date.parse(state.leaseExpiresAt);
|
|
1174
|
+
if (Number.isNaN(expiresAt) || expiresAt > now) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
await this.maybeDispatchActivationTarget(state.agentId, state.targetId, "lease");
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
console.warn(`activation target lease sync failed for ${state.targetId}/${state.agentId}:`, error);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async syncLifecycleGc() {
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
if (now - this.lastOfflineAgentGcAt < DEFAULT_GC_INTERVAL_MS) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this.lastOfflineAgentGcAt = now;
|
|
1191
|
+
try {
|
|
1192
|
+
this.gcOfflineAgents(now);
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
console.warn("offline agent gc failed:", error);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
exports.AgentInboxService = AgentInboxService;
|
|
1200
|
+
function retentionCutoffIso(retentionMs) {
|
|
1201
|
+
return new Date(Date.now() - retentionMs).toISOString();
|
|
1202
|
+
}
|
|
1203
|
+
function resolveDeliveryHandle(request) {
|
|
1204
|
+
if (request.deliveryHandle) {
|
|
1205
|
+
return request.deliveryHandle;
|
|
1206
|
+
}
|
|
1207
|
+
if (!request.provider || !request.surface || !request.targetRef) {
|
|
1208
|
+
throw new Error("delivery requires either deliveryHandle or provider/surface/targetRef");
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
provider: request.provider,
|
|
1212
|
+
surface: request.surface,
|
|
1213
|
+
targetRef: request.targetRef,
|
|
1214
|
+
threadRef: request.threadRef ?? null,
|
|
1215
|
+
replyMode: request.replyMode ?? null,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
class ActivationDispatcher {
|
|
1219
|
+
async dispatch(targetUrl, activation) {
|
|
1220
|
+
try {
|
|
1221
|
+
const response = await fetch(targetUrl, {
|
|
1222
|
+
method: "POST",
|
|
1223
|
+
headers: { "content-type": "application/json" },
|
|
1224
|
+
body: JSON.stringify(activation),
|
|
1225
|
+
});
|
|
1226
|
+
if (!response.ok) {
|
|
1227
|
+
throw new Error(`activation dispatch failed for ${targetUrl}: ${response.status}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
console.warn(`activation dispatch error for ${targetUrl}:`, error);
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
exports.ActivationDispatcher = ActivationDispatcher;
|
|
1237
|
+
function validateTerminalRegistration(input) {
|
|
1238
|
+
if (input.backend === "tmux") {
|
|
1239
|
+
if (!input.tmuxPaneId) {
|
|
1240
|
+
throw new Error("tmux agent registration requires tmuxPaneId");
|
|
1241
|
+
}
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (input.backend === "iterm2") {
|
|
1245
|
+
if (!input.itermSessionId && !input.tty) {
|
|
1246
|
+
throw new Error("iterm2 agent registration requires itermSessionId or tty");
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
throw new Error(`unsupported terminal backend: ${String(input.backend)}`);
|
|
1251
|
+
}
|
|
1252
|
+
function validateNotifyLeaseMs(value) {
|
|
1253
|
+
if (value == null) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1257
|
+
throw new Error("notifyLeaseMs must be a positive integer");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function normalizeWebhookActivationMode(mode) {
|
|
1261
|
+
const resolved = mode ?? "activation_only";
|
|
1262
|
+
if (!WEBHOOK_ACTIVATION_MODES.has(resolved)) {
|
|
1263
|
+
throw new Error(`unsupported activation mode: ${String(mode)}`);
|
|
1264
|
+
}
|
|
1265
|
+
return resolved;
|
|
1266
|
+
}
|
|
1267
|
+
function findExistingTerminalActivationTarget(store, input) {
|
|
1268
|
+
if (input.runtimeSessionId) {
|
|
1269
|
+
const target = store.getTerminalActivationTargetByRuntimeSession(input.runtimeKind ?? "unknown", input.runtimeSessionId);
|
|
1270
|
+
if (target) {
|
|
1271
|
+
return target;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (input.backend === "tmux" && input.tmuxPaneId) {
|
|
1275
|
+
return store.getTerminalActivationTargetByTmuxPaneId(input.tmuxPaneId);
|
|
1276
|
+
}
|
|
1277
|
+
if (input.backend === "iterm2") {
|
|
1278
|
+
if (input.itermSessionId) {
|
|
1279
|
+
return store.getTerminalActivationTargetByItermSessionId(input.itermSessionId);
|
|
1280
|
+
}
|
|
1281
|
+
if (input.tty) {
|
|
1282
|
+
return store.getTerminalActivationTargetByTty(input.tty);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
function isSameTerminalIdentity(target, input) {
|
|
1288
|
+
if (input.runtimeSessionId && target.runtimeKind === (input.runtimeKind ?? "unknown") && target.runtimeSessionId === input.runtimeSessionId) {
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
if (input.backend === "tmux" && input.tmuxPaneId && target.backend === "tmux" && target.tmuxPaneId === input.tmuxPaneId) {
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
if (input.backend === "iterm2" && target.backend === "iterm2") {
|
|
1295
|
+
if (input.itermSessionId && target.itermSessionId === input.itermSessionId) {
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
if (input.tty && target.tty === input.tty) {
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
function uniqueSorted(values) {
|
|
1305
|
+
return Array.from(new Set(values)).sort();
|
|
1306
|
+
}
|
|
1307
|
+
function notificationBufferKey(agentId, targetId) {
|
|
1308
|
+
return `${agentId}::${targetId}`;
|
|
1309
|
+
}
|
|
1310
|
+
function summarizeActivation(inboxId, newItemCount, firstSummary) {
|
|
1311
|
+
const itemWord = newItemCount === 1 ? "item" : "items";
|
|
1312
|
+
if (firstSummary) {
|
|
1313
|
+
return `${newItemCount} new ${itemWord} in ${inboxId} from ${firstSummary}`;
|
|
1314
|
+
}
|
|
1315
|
+
return `${newItemCount} new ${itemWord} in ${inboxId}`;
|
|
1316
|
+
}
|
|
1317
|
+
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
1318
|
+
if (sourceType === "github_repo_ci") {
|
|
1319
|
+
const parts = eventVariant.split(".");
|
|
1320
|
+
const [, second, third, fourth] = parts;
|
|
1321
|
+
const knownStatuses = new Set(["completed", "in_progress", "queued", "requested", "waiting", "pending", "observed"]);
|
|
1322
|
+
const workflowName = second && !knownStatuses.has(second) ? second : null;
|
|
1323
|
+
const status = workflowName ? third : second;
|
|
1324
|
+
const conclusion = workflowName ? fourth : third;
|
|
1325
|
+
const summaryParts = [`${sourceType}:${sourceKey}`];
|
|
1326
|
+
if (workflowName) {
|
|
1327
|
+
summaryParts.push(workflowName);
|
|
1328
|
+
}
|
|
1329
|
+
if (status) {
|
|
1330
|
+
summaryParts.push(status);
|
|
1331
|
+
}
|
|
1332
|
+
if (conclusion) {
|
|
1333
|
+
summaryParts.push(conclusion);
|
|
1334
|
+
}
|
|
1335
|
+
return summaryParts.join(":");
|
|
1336
|
+
}
|
|
1337
|
+
return `${sourceType}:${sourceKey}:${eventVariant}`;
|
|
1338
|
+
}
|