@holon-run/agentinbox 0.7.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/dist/src/adapters.js +55 -0
- package/dist/src/backend.js +4 -16
- package/dist/src/cli.js +329 -52
- package/dist/src/client.js +2 -2
- package/dist/src/http.js +479 -55
- package/dist/src/runtime_gate.js +49 -41
- package/dist/src/service.js +922 -200
- package/dist/src/source_hosts.js +75 -0
- package/dist/src/source_resolution.js +2 -2
- package/dist/src/source_schema.js +2 -2
- package/dist/src/sources/feishu.js +66 -28
- package/dist/src/sources/github.js +222 -36
- package/dist/src/sources/remote.js +50 -3
- package/dist/src/sources/remote_modules.js +100 -15
- package/dist/src/store.js +875 -110
- package/dist/src/terminal.js +33 -7
- package/dist/src/util.js +46 -1
- package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
- package/drizzle/migrations/0002_host_scoped_lifecycle_retirements.sql +42 -0
- package/drizzle/migrations/0003_subscription_tracked_resource_indexes.sql +5 -0
- package/drizzle/schema.ts +98 -3
- package/package.json +2 -1
- package/scripts/release_smoke.sh +123 -0
- package/dist/src/sources/remote_profiles.js +0 -13
- package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
- package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
- package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
- package/drizzle/migrations/0004_source_idle_states.sql +0 -10
- package/drizzle/migrations/0005_timers.sql +0 -22
- package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
- package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
- package/drizzle/migrations/0008_inbox_item_sequence.sql +0 -21
package/dist/src/store.js
CHANGED
|
@@ -8,8 +8,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const sql_js_1 = __importDefault(require("sql.js"));
|
|
10
10
|
const util_1 = require("./util");
|
|
11
|
-
const LEGACY_SCHEMA_VERSION = 12;
|
|
12
11
|
const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
12
|
+
const V1_BASELINE_TAG = "0000_v1_initial";
|
|
13
13
|
function parseJson(value) {
|
|
14
14
|
if (!value) {
|
|
15
15
|
return {};
|
|
@@ -38,10 +38,16 @@ class AgentInboxStore {
|
|
|
38
38
|
static async open(dbPath) {
|
|
39
39
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dbPath), { recursive: true });
|
|
40
40
|
const SQL = await this.loadSqlJs();
|
|
41
|
-
|
|
41
|
+
let db = node_fs_1.default.existsSync(dbPath)
|
|
42
42
|
? new SQL.Database(node_fs_1.default.readFileSync(dbPath))
|
|
43
43
|
: new SQL.Database();
|
|
44
|
-
|
|
44
|
+
let store = new AgentInboxStore(dbPath, db);
|
|
45
|
+
if (node_fs_1.default.existsSync(dbPath) && store.shouldArchivePreV1Database()) {
|
|
46
|
+
const archivedPath = store.archivePreV1Database();
|
|
47
|
+
console.warn(`[agentinbox] archived pre-v1 local database to ${archivedPath}; starting with a fresh v1 database (no data imported).`);
|
|
48
|
+
db = new SQL.Database();
|
|
49
|
+
store = new AgentInboxStore(dbPath, db);
|
|
50
|
+
}
|
|
45
51
|
store.migrate();
|
|
46
52
|
store.persist();
|
|
47
53
|
return store;
|
|
@@ -62,26 +68,13 @@ class AgentInboxStore {
|
|
|
62
68
|
}
|
|
63
69
|
migrate() {
|
|
64
70
|
const migrations = this.loadSqlMigrations();
|
|
65
|
-
const hasMigrationTable = this.tableExists(DRIZZLE_MIGRATIONS_TABLE);
|
|
66
71
|
this.ensureDrizzleMigrationsTable();
|
|
67
72
|
const applied = this.listAppliedMigrationTags();
|
|
68
|
-
const userVersion = this.userVersion();
|
|
69
|
-
if (applied.size === 0 && !hasMigrationTable && this.tableExists("sources")) {
|
|
70
|
-
if (userVersion !== 0 && userVersion !== LEGACY_SCHEMA_VERSION) {
|
|
71
|
-
throw new Error(`unsupported legacy database schema version ${userVersion} at ${this.dbPath}`);
|
|
72
|
-
}
|
|
73
|
-
this.recordAppliedMigration(migrations[0].tag);
|
|
74
|
-
applied.add(migrations[0].tag);
|
|
75
|
-
}
|
|
76
73
|
const pending = migrations.filter((migration) => !applied.has(migration.tag));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.backupDatabase();
|
|
80
|
-
}
|
|
81
|
-
for (const migration of pending) {
|
|
82
|
-
this.applyMigration(migration);
|
|
83
|
-
}
|
|
74
|
+
for (const migration of pending) {
|
|
75
|
+
this.applyMigration(migration);
|
|
84
76
|
}
|
|
77
|
+
this.ensureInboxEntryBackfill();
|
|
85
78
|
this.setUserVersion(migrations.length);
|
|
86
79
|
}
|
|
87
80
|
loadSqlMigrations() {
|
|
@@ -133,16 +126,52 @@ class AgentInboxStore {
|
|
|
133
126
|
recordAppliedMigration(tag) {
|
|
134
127
|
this.db.run(`insert or ignore into ${DRIZZLE_MIGRATIONS_TABLE} (tag, applied_at) values (?, ?)`, [tag, (0, util_1.nowIso)()]);
|
|
135
128
|
}
|
|
136
|
-
|
|
137
|
-
if (
|
|
129
|
+
shouldArchivePreV1Database() {
|
|
130
|
+
if (this.isV1Database()) {
|
|
138
131
|
return false;
|
|
139
132
|
}
|
|
140
|
-
return this.
|
|
133
|
+
return this.hasRecognizablePreV1Schema();
|
|
141
134
|
}
|
|
142
|
-
|
|
135
|
+
isV1Database() {
|
|
136
|
+
if (!this.tableExists(DRIZZLE_MIGRATIONS_TABLE)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const applied = this.listAppliedMigrationTags();
|
|
140
|
+
if (!applied.has(V1_BASELINE_TAG)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return this.tableExists("source_hosts")
|
|
144
|
+
&& this.tableExists("inbox_entries")
|
|
145
|
+
&& this.tableExists("digest_threads")
|
|
146
|
+
&& this.columnExists("activation_dispatch_states", "pending_fingerprint")
|
|
147
|
+
&& this.columnType("inbox_entries", "entry_id") === "text"
|
|
148
|
+
&& this.columnType("digest_threads", "thread_id") === "text";
|
|
149
|
+
}
|
|
150
|
+
hasRecognizablePreV1Schema() {
|
|
151
|
+
return this.tableExists("sources")
|
|
152
|
+
|| this.tableExists("agents")
|
|
153
|
+
|| this.tableExists("subscriptions")
|
|
154
|
+
|| this.tableExists("activation_targets")
|
|
155
|
+
|| this.tableExists("inbox_items")
|
|
156
|
+
|| this.tableExists(DRIZZLE_MIGRATIONS_TABLE);
|
|
157
|
+
}
|
|
158
|
+
archivePreV1Database() {
|
|
159
|
+
this.close();
|
|
160
|
+
const archivedPath = this.nextArchivedDatabasePath();
|
|
161
|
+
node_fs_1.default.renameSync(this.dbPath, archivedPath);
|
|
162
|
+
return archivedPath;
|
|
163
|
+
}
|
|
164
|
+
nextArchivedDatabasePath() {
|
|
143
165
|
const safeStamp = (0, util_1.nowIso)().replace(/[:.]/g, "-");
|
|
144
|
-
const
|
|
145
|
-
node_fs_1.default.
|
|
166
|
+
const base = `${this.dbPath}.pre-v1.${safeStamp}.bak`;
|
|
167
|
+
if (!node_fs_1.default.existsSync(base)) {
|
|
168
|
+
return base;
|
|
169
|
+
}
|
|
170
|
+
let suffix = 1;
|
|
171
|
+
while (node_fs_1.default.existsSync(`${base}.${suffix}`)) {
|
|
172
|
+
suffix += 1;
|
|
173
|
+
}
|
|
174
|
+
return `${base}.${suffix}`;
|
|
146
175
|
}
|
|
147
176
|
persist() {
|
|
148
177
|
const data = this.db.export();
|
|
@@ -159,13 +188,6 @@ class AgentInboxStore {
|
|
|
159
188
|
throw error;
|
|
160
189
|
}
|
|
161
190
|
}
|
|
162
|
-
userVersion() {
|
|
163
|
-
const row = this.getOne("pragma user_version;");
|
|
164
|
-
if (!row) {
|
|
165
|
-
return 0;
|
|
166
|
-
}
|
|
167
|
-
return Number(row.user_version ?? 0);
|
|
168
|
-
}
|
|
169
191
|
setUserVersion(version) {
|
|
170
192
|
this.db.exec(`pragma user_version = ${version};`);
|
|
171
193
|
}
|
|
@@ -173,6 +195,137 @@ class AgentInboxStore {
|
|
|
173
195
|
const row = this.getOne("select name from sqlite_master where type = 'table' and name = ?", [name]);
|
|
174
196
|
return Boolean(row);
|
|
175
197
|
}
|
|
198
|
+
columnExists(tableName, columnName) {
|
|
199
|
+
if (!this.tableExists(tableName)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const rows = this.getAll(`pragma table_info(${tableName});`);
|
|
203
|
+
return rows.some((row) => String(row.name) === columnName);
|
|
204
|
+
}
|
|
205
|
+
columnType(tableName, columnName) {
|
|
206
|
+
if (!this.tableExists(tableName)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const rows = this.getAll(`pragma table_info(${tableName});`);
|
|
210
|
+
const row = rows.find((entry) => String(entry.name) === columnName);
|
|
211
|
+
return row ? String(row.type).toLowerCase() : null;
|
|
212
|
+
}
|
|
213
|
+
ensureInboxEntryBackfill() {
|
|
214
|
+
if (!this.tableExists("inbox_entries") || !this.tableExists("inbox_items")) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const existing = this.getOne("select count(*) as count from inbox_entries");
|
|
218
|
+
if (existing && Number(existing.count ?? 0) > 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const rows = this.getAll(`
|
|
222
|
+
select *
|
|
223
|
+
from inbox_items
|
|
224
|
+
order by inbox_id asc, coalesce(inbox_sequence, rowid) asc
|
|
225
|
+
`);
|
|
226
|
+
if (rows.length === 0) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.inTransaction(() => {
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
const item = this.mapInboxItem(row);
|
|
232
|
+
const entryId = this.insertInboxEntryRecord({
|
|
233
|
+
inboxId: item.inboxId,
|
|
234
|
+
kind: "item",
|
|
235
|
+
threadId: null,
|
|
236
|
+
revision: null,
|
|
237
|
+
groupKey: null,
|
|
238
|
+
resourceRef: null,
|
|
239
|
+
eventFamily: null,
|
|
240
|
+
itemJson: JSON.stringify({
|
|
241
|
+
itemId: item.itemId,
|
|
242
|
+
sourceId: item.sourceId,
|
|
243
|
+
sourceNativeId: item.sourceNativeId,
|
|
244
|
+
eventVariant: item.eventVariant,
|
|
245
|
+
inboxId: item.inboxId,
|
|
246
|
+
occurredAt: item.occurredAt,
|
|
247
|
+
metadata: item.metadata,
|
|
248
|
+
rawPayload: item.rawPayload,
|
|
249
|
+
deliveryHandle: item.deliveryHandle ?? null,
|
|
250
|
+
}),
|
|
251
|
+
count: 1,
|
|
252
|
+
summary: summarizeBackfilledItemEntry(item),
|
|
253
|
+
firstItemAt: item.occurredAt,
|
|
254
|
+
lastItemAt: item.occurredAt,
|
|
255
|
+
sourceIdsJson: JSON.stringify([item.sourceId]),
|
|
256
|
+
subscriptionIdsJson: JSON.stringify([]),
|
|
257
|
+
deliveryHandleJson: item.deliveryHandle ? JSON.stringify(item.deliveryHandle) : null,
|
|
258
|
+
ackedAt: item.ackedAt ?? null,
|
|
259
|
+
supersededAt: null,
|
|
260
|
+
createdAt: item.occurredAt,
|
|
261
|
+
});
|
|
262
|
+
this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
getSourceHostByKey(hostType, hostKey) {
|
|
267
|
+
const row = this.getOne("select * from source_hosts where host_type = ? and host_key = ?", [hostType, hostKey]);
|
|
268
|
+
return row ? this.mapSourceHost(row) : null;
|
|
269
|
+
}
|
|
270
|
+
getSourceHost(hostId) {
|
|
271
|
+
const row = this.getOne("select * from source_hosts where host_id = ?", [hostId]);
|
|
272
|
+
return row ? this.mapSourceHost(row) : null;
|
|
273
|
+
}
|
|
274
|
+
insertSourceHost(host) {
|
|
275
|
+
this.db.run(`
|
|
276
|
+
insert into source_hosts (
|
|
277
|
+
host_id, host_type, host_key, config_ref, config_json, status, created_at, updated_at
|
|
278
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
279
|
+
`, [
|
|
280
|
+
host.hostId,
|
|
281
|
+
host.hostType,
|
|
282
|
+
host.hostKey,
|
|
283
|
+
host.configRef ?? null,
|
|
284
|
+
JSON.stringify(host.config ?? {}),
|
|
285
|
+
host.status,
|
|
286
|
+
host.createdAt,
|
|
287
|
+
host.updatedAt,
|
|
288
|
+
]);
|
|
289
|
+
this.persist();
|
|
290
|
+
}
|
|
291
|
+
listSourceHosts() {
|
|
292
|
+
const rows = this.getAll("select * from source_hosts order by created_at asc");
|
|
293
|
+
return rows.map((row) => this.mapSourceHost(row));
|
|
294
|
+
}
|
|
295
|
+
updateSourceHostDefinition(hostId, input) {
|
|
296
|
+
const current = this.getSourceHost(hostId);
|
|
297
|
+
if (!current) {
|
|
298
|
+
throw new Error(`unknown source host: ${hostId}`);
|
|
299
|
+
}
|
|
300
|
+
this.db.run(`
|
|
301
|
+
update source_hosts
|
|
302
|
+
set config_ref = ?, config_json = ?, updated_at = ?
|
|
303
|
+
where host_id = ?
|
|
304
|
+
`, [
|
|
305
|
+
Object.prototype.hasOwnProperty.call(input, "configRef") ? input.configRef ?? null : current.configRef ?? null,
|
|
306
|
+
JSON.stringify(Object.prototype.hasOwnProperty.call(input, "config") ? input.config ?? {} : current.config ?? {}),
|
|
307
|
+
(0, util_1.nowIso)(),
|
|
308
|
+
hostId,
|
|
309
|
+
]);
|
|
310
|
+
this.persist();
|
|
311
|
+
return this.getSourceHost(hostId);
|
|
312
|
+
}
|
|
313
|
+
updateSourceHostRuntime(hostId, input) {
|
|
314
|
+
const current = this.getSourceHost(hostId);
|
|
315
|
+
if (!current) {
|
|
316
|
+
throw new Error(`unknown source host: ${hostId}`);
|
|
317
|
+
}
|
|
318
|
+
this.db.run(`
|
|
319
|
+
update source_hosts
|
|
320
|
+
set status = ?, updated_at = ?
|
|
321
|
+
where host_id = ?
|
|
322
|
+
`, [
|
|
323
|
+
Object.prototype.hasOwnProperty.call(input, "status") ? input.status ?? current.status : current.status,
|
|
324
|
+
(0, util_1.nowIso)(),
|
|
325
|
+
hostId,
|
|
326
|
+
]);
|
|
327
|
+
this.persist();
|
|
328
|
+
}
|
|
176
329
|
getSourceByKey(sourceType, sourceKey) {
|
|
177
330
|
const row = this.getOne("select * from sources where source_type = ? and source_key = ?", [sourceType, sourceKey]);
|
|
178
331
|
return row ? this.mapSource(row) : null;
|
|
@@ -182,22 +335,31 @@ class AgentInboxStore {
|
|
|
182
335
|
return row ? this.mapSource(row) : null;
|
|
183
336
|
}
|
|
184
337
|
insertSource(source) {
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
338
|
+
this.inTransaction(() => {
|
|
339
|
+
if (!source.hostId || !source.streamKind || !source.streamKey) {
|
|
340
|
+
throw new Error("source insert requires hostId, streamKind, and streamKey");
|
|
341
|
+
}
|
|
342
|
+
this.db.run(`
|
|
343
|
+
insert into sources (
|
|
344
|
+
source_id, host_id, stream_kind, stream_key,
|
|
345
|
+
source_type, source_key, config_ref, config_json,
|
|
346
|
+
status, checkpoint, created_at, updated_at
|
|
347
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
348
|
+
`, [
|
|
349
|
+
source.sourceId,
|
|
350
|
+
source.hostId,
|
|
351
|
+
source.streamKind,
|
|
352
|
+
source.streamKey,
|
|
353
|
+
source.sourceType,
|
|
354
|
+
source.sourceKey,
|
|
355
|
+
source.configRef ?? null,
|
|
356
|
+
JSON.stringify(source.config ?? {}),
|
|
357
|
+
source.status,
|
|
358
|
+
source.checkpoint ?? null,
|
|
359
|
+
source.createdAt,
|
|
360
|
+
source.updatedAt,
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
201
363
|
this.persist();
|
|
202
364
|
}
|
|
203
365
|
listSources() {
|
|
@@ -290,6 +452,13 @@ class AgentInboxStore {
|
|
|
290
452
|
}
|
|
291
453
|
this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
|
|
292
454
|
this.db.run("delete from sources where source_id = ?", [sourceId]);
|
|
455
|
+
const hostId = source.hostId ?? null;
|
|
456
|
+
const siblings = hostId
|
|
457
|
+
? this.getOne("select count(*) as count from sources where host_id = ?", [hostId])
|
|
458
|
+
: null;
|
|
459
|
+
if (hostId && Number(siblings?.count ?? 0) === 0) {
|
|
460
|
+
this.db.run("delete from source_hosts where host_id = ?", [hostId]);
|
|
461
|
+
}
|
|
293
462
|
});
|
|
294
463
|
this.persist();
|
|
295
464
|
return source;
|
|
@@ -349,9 +518,44 @@ class AgentInboxStore {
|
|
|
349
518
|
return row ? this.mapInbox(row) : null;
|
|
350
519
|
}
|
|
351
520
|
insertInbox(inbox) {
|
|
352
|
-
this.db.run(
|
|
521
|
+
this.db.run(`
|
|
522
|
+
insert into inboxes (
|
|
523
|
+
inbox_id, owner_agent_id, aggregation_enabled, aggregation_window_ms,
|
|
524
|
+
aggregation_max_items, aggregation_max_thread_age_ms, created_at
|
|
525
|
+
) values (?, ?, ?, ?, ?, ?, ?)
|
|
526
|
+
`, [
|
|
527
|
+
inbox.inboxId,
|
|
528
|
+
inbox.ownerAgentId,
|
|
529
|
+
inbox.aggregationEnabled ? 1 : 0,
|
|
530
|
+
inbox.aggregationWindowMs ?? null,
|
|
531
|
+
inbox.aggregationMaxItems ?? null,
|
|
532
|
+
inbox.aggregationMaxThreadAgeMs ?? null,
|
|
533
|
+
inbox.createdAt,
|
|
534
|
+
]);
|
|
353
535
|
this.persist();
|
|
354
536
|
}
|
|
537
|
+
updateInboxAggregationPolicy(inboxId, policy) {
|
|
538
|
+
const current = this.getInbox(inboxId);
|
|
539
|
+
if (!current) {
|
|
540
|
+
throw new Error(`unknown inbox: ${inboxId}`);
|
|
541
|
+
}
|
|
542
|
+
this.db.run(`
|
|
543
|
+
update inboxes
|
|
544
|
+
set aggregation_enabled = ?,
|
|
545
|
+
aggregation_window_ms = ?,
|
|
546
|
+
aggregation_max_items = ?,
|
|
547
|
+
aggregation_max_thread_age_ms = ?
|
|
548
|
+
where inbox_id = ?
|
|
549
|
+
`, [
|
|
550
|
+
policy.enabled === undefined ? (current.aggregationEnabled ? 1 : 0) : (policy.enabled ? 1 : 0),
|
|
551
|
+
Object.prototype.hasOwnProperty.call(policy, "windowMs") ? policy.windowMs ?? null : current.aggregationWindowMs ?? null,
|
|
552
|
+
Object.prototype.hasOwnProperty.call(policy, "maxItems") ? policy.maxItems ?? null : current.aggregationMaxItems ?? null,
|
|
553
|
+
Object.prototype.hasOwnProperty.call(policy, "maxThreadAgeMs") ? policy.maxThreadAgeMs ?? null : current.aggregationMaxThreadAgeMs ?? null,
|
|
554
|
+
inboxId,
|
|
555
|
+
]);
|
|
556
|
+
this.persist();
|
|
557
|
+
return this.getInbox(inboxId);
|
|
558
|
+
}
|
|
355
559
|
listInboxes() {
|
|
356
560
|
const rows = this.getAll("select * from inboxes order by created_at asc");
|
|
357
561
|
return rows.map((row) => this.mapInbox(row));
|
|
@@ -391,6 +595,16 @@ class AgentInboxStore {
|
|
|
391
595
|
const rows = this.getAll("select * from subscriptions where agent_id = ? order by created_at asc", [agentId]);
|
|
392
596
|
return rows.map((row) => this.mapSubscription(row));
|
|
393
597
|
}
|
|
598
|
+
listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
|
|
599
|
+
const rows = this.getAll(`
|
|
600
|
+
select subscriptions.*
|
|
601
|
+
from subscriptions
|
|
602
|
+
inner join sources on sources.source_id = subscriptions.source_id
|
|
603
|
+
where sources.host_id = ? and subscriptions.tracked_resource_ref = ?
|
|
604
|
+
order by subscriptions.created_at asc
|
|
605
|
+
`, [hostId, trackedResourceRef]);
|
|
606
|
+
return rows.map((row) => this.mapSubscription(row));
|
|
607
|
+
}
|
|
394
608
|
deleteSubscription(subscriptionId) {
|
|
395
609
|
const subscription = this.getSubscription(subscriptionId);
|
|
396
610
|
if (!subscription) {
|
|
@@ -424,10 +638,10 @@ class AgentInboxStore {
|
|
|
424
638
|
: retirement;
|
|
425
639
|
this.db.run(`
|
|
426
640
|
insert into subscription_lifecycle_retirements (
|
|
427
|
-
subscription_id,
|
|
641
|
+
subscription_id, host_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
|
|
428
642
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
429
643
|
on conflict(subscription_id) do update set
|
|
430
|
-
|
|
644
|
+
host_id = excluded.host_id,
|
|
431
645
|
tracked_resource_ref = excluded.tracked_resource_ref,
|
|
432
646
|
retire_at = excluded.retire_at,
|
|
433
647
|
terminal_state = excluded.terminal_state,
|
|
@@ -437,7 +651,7 @@ class AgentInboxStore {
|
|
|
437
651
|
updated_at = excluded.updated_at
|
|
438
652
|
`, [
|
|
439
653
|
next.subscriptionId,
|
|
440
|
-
next.
|
|
654
|
+
next.hostId,
|
|
441
655
|
next.trackedResourceRef,
|
|
442
656
|
next.retireAt,
|
|
443
657
|
next.terminalState ?? null,
|
|
@@ -482,8 +696,8 @@ class AgentInboxStore {
|
|
|
482
696
|
this.db.run(`
|
|
483
697
|
insert into activation_targets (
|
|
484
698
|
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error,
|
|
485
|
-
mode, notify_lease_ms, url, created_at, updated_at, last_seen_at
|
|
486
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
699
|
+
mode, notify_lease_ms, min_unacked_items, url, created_at, updated_at, last_seen_at
|
|
700
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
487
701
|
`, [
|
|
488
702
|
target.targetId,
|
|
489
703
|
target.agentId,
|
|
@@ -495,6 +709,7 @@ class AgentInboxStore {
|
|
|
495
709
|
target.lastError ?? null,
|
|
496
710
|
target.mode,
|
|
497
711
|
target.notifyLeaseMs,
|
|
712
|
+
target.minUnackedItems ?? null,
|
|
498
713
|
target.url,
|
|
499
714
|
target.createdAt,
|
|
500
715
|
target.updatedAt,
|
|
@@ -504,10 +719,10 @@ class AgentInboxStore {
|
|
|
504
719
|
else {
|
|
505
720
|
this.db.run(`
|
|
506
721
|
insert into activation_targets (
|
|
507
|
-
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms,
|
|
722
|
+
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms, min_unacked_items,
|
|
508
723
|
runtime_kind, runtime_session_id, runtime_pid, backend, tmux_pane_id, tty, term_program, iterm_session_id,
|
|
509
724
|
created_at, updated_at, last_seen_at
|
|
510
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
725
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
726
|
`, [
|
|
512
727
|
target.targetId,
|
|
513
728
|
target.agentId,
|
|
@@ -519,6 +734,7 @@ class AgentInboxStore {
|
|
|
519
734
|
target.lastError ?? null,
|
|
520
735
|
target.mode,
|
|
521
736
|
target.notifyLeaseMs,
|
|
737
|
+
target.minUnackedItems ?? null,
|
|
522
738
|
target.runtimeKind,
|
|
523
739
|
target.runtimeSessionId ?? null,
|
|
524
740
|
target.runtimePid ?? null,
|
|
@@ -538,7 +754,7 @@ class AgentInboxStore {
|
|
|
538
754
|
this.db.run(`
|
|
539
755
|
update activation_targets
|
|
540
756
|
set status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
|
|
541
|
-
runtime_kind = ?, runtime_session_id = ?, runtime_pid = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, updated_at = ?, last_seen_at = ?
|
|
757
|
+
runtime_kind = ?, runtime_session_id = ?, runtime_pid = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, notify_lease_ms = ?, min_unacked_items = ?, updated_at = ?, last_seen_at = ?
|
|
542
758
|
where target_id = ? and kind = 'terminal'
|
|
543
759
|
`, [
|
|
544
760
|
input.runtimeKind,
|
|
@@ -548,6 +764,8 @@ class AgentInboxStore {
|
|
|
548
764
|
input.tty ?? null,
|
|
549
765
|
input.termProgram ?? null,
|
|
550
766
|
input.itermSessionId ?? null,
|
|
767
|
+
input.notifyLeaseMs,
|
|
768
|
+
input.minUnackedItems,
|
|
551
769
|
input.updatedAt,
|
|
552
770
|
input.lastSeenAt,
|
|
553
771
|
targetId,
|
|
@@ -729,13 +947,19 @@ class AgentInboxStore {
|
|
|
729
947
|
upsertActivationDispatchState(state) {
|
|
730
948
|
this.db.run(`
|
|
731
949
|
insert into activation_dispatch_states (
|
|
732
|
-
agent_id, target_id, status, lease_expires_at, last_notified_fingerprint,
|
|
950
|
+
agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, defer_reason, defer_attempts,
|
|
951
|
+
first_deferred_at, last_deferred_at, pending_fingerprint, pending_new_item_count, pending_summary,
|
|
733
952
|
pending_subscription_ids_json, pending_source_ids_json, updated_at
|
|
734
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
953
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
735
954
|
on conflict(agent_id, target_id) do update set
|
|
736
955
|
status = excluded.status,
|
|
737
956
|
lease_expires_at = excluded.lease_expires_at,
|
|
738
957
|
last_notified_fingerprint = excluded.last_notified_fingerprint,
|
|
958
|
+
defer_reason = excluded.defer_reason,
|
|
959
|
+
defer_attempts = excluded.defer_attempts,
|
|
960
|
+
first_deferred_at = excluded.first_deferred_at,
|
|
961
|
+
last_deferred_at = excluded.last_deferred_at,
|
|
962
|
+
pending_fingerprint = excluded.pending_fingerprint,
|
|
739
963
|
pending_new_item_count = excluded.pending_new_item_count,
|
|
740
964
|
pending_summary = excluded.pending_summary,
|
|
741
965
|
pending_subscription_ids_json = excluded.pending_subscription_ids_json,
|
|
@@ -747,6 +971,11 @@ class AgentInboxStore {
|
|
|
747
971
|
state.status,
|
|
748
972
|
state.leaseExpiresAt ?? null,
|
|
749
973
|
state.lastNotifiedFingerprint ?? null,
|
|
974
|
+
state.deferReason ?? null,
|
|
975
|
+
state.deferAttempts,
|
|
976
|
+
state.firstDeferredAt ?? null,
|
|
977
|
+
state.lastDeferredAt ?? null,
|
|
978
|
+
state.pendingFingerprint ?? null,
|
|
750
979
|
state.pendingNewItemCount,
|
|
751
980
|
state.pendingSummary ?? null,
|
|
752
981
|
JSON.stringify(state.pendingSubscriptionIds),
|
|
@@ -790,20 +1019,237 @@ class AgentInboxStore {
|
|
|
790
1019
|
}
|
|
791
1020
|
return inserted;
|
|
792
1021
|
}
|
|
793
|
-
|
|
1022
|
+
createInboxItemEntry(inboxId, item, input) {
|
|
1023
|
+
const entryId = this.insertInboxEntryRecord({
|
|
1024
|
+
inboxId,
|
|
1025
|
+
kind: "item",
|
|
1026
|
+
threadId: null,
|
|
1027
|
+
revision: null,
|
|
1028
|
+
groupKey: null,
|
|
1029
|
+
resourceRef: null,
|
|
1030
|
+
eventFamily: null,
|
|
1031
|
+
itemJson: JSON.stringify({
|
|
1032
|
+
itemId: item.itemId,
|
|
1033
|
+
sourceId: item.sourceId,
|
|
1034
|
+
sourceNativeId: item.sourceNativeId,
|
|
1035
|
+
eventVariant: item.eventVariant,
|
|
1036
|
+
inboxId: item.inboxId,
|
|
1037
|
+
occurredAt: item.occurredAt,
|
|
1038
|
+
metadata: item.metadata,
|
|
1039
|
+
rawPayload: item.rawPayload,
|
|
1040
|
+
deliveryHandle: item.deliveryHandle ?? null,
|
|
1041
|
+
}),
|
|
1042
|
+
count: 1,
|
|
1043
|
+
summary: input.summary,
|
|
1044
|
+
firstItemAt: item.occurredAt,
|
|
1045
|
+
lastItemAt: item.occurredAt,
|
|
1046
|
+
sourceIdsJson: JSON.stringify([item.sourceId]),
|
|
1047
|
+
subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
|
|
1048
|
+
deliveryHandleJson: item.deliveryHandle ? JSON.stringify(item.deliveryHandle) : null,
|
|
1049
|
+
ackedAt: item.ackedAt ?? null,
|
|
1050
|
+
supersededAt: null,
|
|
1051
|
+
createdAt: (0, util_1.nowIso)(),
|
|
1052
|
+
});
|
|
1053
|
+
this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
|
|
1054
|
+
this.persist();
|
|
1055
|
+
return this.getInboxEntry(entryId);
|
|
1056
|
+
}
|
|
1057
|
+
listInboxEntries(inboxId, options) {
|
|
794
1058
|
const includeAcked = options?.includeAcked ?? false;
|
|
795
1059
|
const filters = ["inbox_id = ?"];
|
|
796
1060
|
const params = [inboxId];
|
|
797
1061
|
if (!includeAcked) {
|
|
798
1062
|
filters.push("acked_at is null");
|
|
1063
|
+
filters.push("superseded_at is null");
|
|
799
1064
|
}
|
|
800
|
-
if (options?.
|
|
801
|
-
const anchor = this.getOne("select
|
|
1065
|
+
if (options?.afterEntryId) {
|
|
1066
|
+
const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, (0, util_1.parseEntryRef)(options.afterEntryId)]);
|
|
802
1067
|
if (!anchor) {
|
|
803
|
-
throw new Error(`unknown inbox
|
|
1068
|
+
throw new Error(`unknown inbox entry: ${options.afterEntryId}`);
|
|
804
1069
|
}
|
|
805
|
-
filters.push("
|
|
806
|
-
params.push(Number(anchor.
|
|
1070
|
+
filters.push("sequence > ?");
|
|
1071
|
+
params.push(Number(anchor.sequence));
|
|
1072
|
+
}
|
|
1073
|
+
const rows = this.getAll(`select * from inbox_entries where ${filters.join(" and ")} order by sequence asc`, params);
|
|
1074
|
+
return this.mapInboxEntries(rows);
|
|
1075
|
+
}
|
|
1076
|
+
getInboxEntry(entryId) {
|
|
1077
|
+
const rows = this.getAll("select * from inbox_entries where entry_id = ?", [(0, util_1.parseEntryRef)(entryId)]);
|
|
1078
|
+
return this.mapInboxEntries(rows)[0] ?? null;
|
|
1079
|
+
}
|
|
1080
|
+
ackInboxEntries(inboxId, entryIds, ackedAt) {
|
|
1081
|
+
const resolvedIds = uniqueSorted(entryIds).map((entryId) => this.resolveInboxEntryId(inboxId, entryId));
|
|
1082
|
+
const itemIds = this.getRawItemIdsForEntryIds(resolvedIds);
|
|
1083
|
+
const ackedItems = itemIds.length > 0 ? this.ackItems(inboxId, itemIds, ackedAt) : 0;
|
|
1084
|
+
if (ackedItems > 0) {
|
|
1085
|
+
this.syncInboxEntryAckState(inboxId, ackedAt);
|
|
1086
|
+
}
|
|
1087
|
+
const ackedEntries = this.countAckedEntries(resolvedIds, ackedAt);
|
|
1088
|
+
return { ackedEntries, ackedItems };
|
|
1089
|
+
}
|
|
1090
|
+
ackInboxEntriesThrough(inboxId, entryId, ackedAt) {
|
|
1091
|
+
const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, this.resolveInboxEntryId(inboxId, entryId)]);
|
|
1092
|
+
if (!anchor) {
|
|
1093
|
+
throw new Error(`unknown inbox entry: ${entryId}`);
|
|
1094
|
+
}
|
|
1095
|
+
const rows = this.getAll(`
|
|
1096
|
+
select entry_id
|
|
1097
|
+
from inbox_entries
|
|
1098
|
+
where inbox_id = ?
|
|
1099
|
+
and acked_at is null
|
|
1100
|
+
and superseded_at is null
|
|
1101
|
+
and sequence <= ?
|
|
1102
|
+
order by sequence asc
|
|
1103
|
+
`, [inboxId, Number(anchor.sequence)]);
|
|
1104
|
+
return this.ackInboxEntries(inboxId, rows.map((row) => (0, util_1.formatEntryRef)(String(row.entry_id))), ackedAt);
|
|
1105
|
+
}
|
|
1106
|
+
getOpenDigestThread(inboxId, groupKey) {
|
|
1107
|
+
const row = this.getOne(`
|
|
1108
|
+
select * from digest_threads
|
|
1109
|
+
where inbox_id = ? and group_key = ? and status = 'open'
|
|
1110
|
+
order by created_at desc, rowid desc
|
|
1111
|
+
limit 1
|
|
1112
|
+
`, [inboxId, groupKey]);
|
|
1113
|
+
return row ? this.mapDigestThread(row) : null;
|
|
1114
|
+
}
|
|
1115
|
+
createDigestThread(input) {
|
|
1116
|
+
const threadId = (0, util_1.generateCanonicalId)("thr");
|
|
1117
|
+
this.db.run(`
|
|
1118
|
+
insert into digest_threads (
|
|
1119
|
+
thread_id, inbox_id, source_id, group_key, resource_ref, event_family, latest_revision,
|
|
1120
|
+
latest_entry_id, status, summary, first_item_at, last_item_at, flush_after_at,
|
|
1121
|
+
created_at, updated_at
|
|
1122
|
+
) values (?, ?, ?, ?, ?, ?, 0, null, 'open', ?, ?, ?, ?, ?, ?)
|
|
1123
|
+
`, [
|
|
1124
|
+
threadId,
|
|
1125
|
+
input.inboxId,
|
|
1126
|
+
input.sourceId,
|
|
1127
|
+
input.groupKey,
|
|
1128
|
+
input.resourceRef ?? null,
|
|
1129
|
+
input.eventFamily ?? null,
|
|
1130
|
+
input.summary,
|
|
1131
|
+
input.firstItemAt,
|
|
1132
|
+
input.firstItemAt,
|
|
1133
|
+
input.flushAfterAt ?? null,
|
|
1134
|
+
input.createdAt,
|
|
1135
|
+
input.createdAt,
|
|
1136
|
+
]);
|
|
1137
|
+
this.persist();
|
|
1138
|
+
return this.getDigestThread(threadId);
|
|
1139
|
+
}
|
|
1140
|
+
getDigestThread(threadId) {
|
|
1141
|
+
const row = this.getOne("select * from digest_threads where thread_id = ?", [threadId]);
|
|
1142
|
+
return row ? this.mapDigestThread(row) : null;
|
|
1143
|
+
}
|
|
1144
|
+
addItemToDigestThread(threadId, itemId, occurredAt, summary, flushAfterAt) {
|
|
1145
|
+
this.inTransaction(() => {
|
|
1146
|
+
this.db.run("insert or ignore into digest_thread_items (thread_id, item_id) values (?, ?)", [threadId, itemId]);
|
|
1147
|
+
this.db.run(`
|
|
1148
|
+
update digest_threads
|
|
1149
|
+
set summary = ?, last_item_at = ?, flush_after_at = ?, updated_at = ?
|
|
1150
|
+
where thread_id = ?
|
|
1151
|
+
`, [summary, occurredAt, flushAfterAt, (0, util_1.nowIso)(), threadId]);
|
|
1152
|
+
});
|
|
1153
|
+
this.persist();
|
|
1154
|
+
return this.getDigestThread(threadId);
|
|
1155
|
+
}
|
|
1156
|
+
listDueDigestThreads(now) {
|
|
1157
|
+
const rows = this.getAll(`
|
|
1158
|
+
select *
|
|
1159
|
+
from digest_threads
|
|
1160
|
+
where status = 'open'
|
|
1161
|
+
and flush_after_at is not null
|
|
1162
|
+
and flush_after_at <= ?
|
|
1163
|
+
order by flush_after_at asc, created_at asc, thread_id asc
|
|
1164
|
+
`, [now]);
|
|
1165
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1166
|
+
}
|
|
1167
|
+
listUnackedInboxItemsForDigestThread(threadId) {
|
|
1168
|
+
const rows = this.getAll(`
|
|
1169
|
+
select i.*
|
|
1170
|
+
from digest_thread_items t
|
|
1171
|
+
join inbox_items i on i.item_id = t.item_id
|
|
1172
|
+
where t.thread_id = ?
|
|
1173
|
+
and i.acked_at is null
|
|
1174
|
+
order by coalesce(i.inbox_sequence, i.rowid) asc
|
|
1175
|
+
`, [threadId]);
|
|
1176
|
+
return rows.map((row) => this.mapInboxItem(row));
|
|
1177
|
+
}
|
|
1178
|
+
listOpenDigestThreadsForInbox(inboxId) {
|
|
1179
|
+
const rows = this.getAll("select * from digest_threads where inbox_id = ? and status = 'open' order by created_at asc, thread_id asc", [inboxId]);
|
|
1180
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1181
|
+
}
|
|
1182
|
+
listOpenDigestThreads() {
|
|
1183
|
+
const rows = this.getAll("select * from digest_threads where status = 'open' order by created_at asc, thread_id asc");
|
|
1184
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1185
|
+
}
|
|
1186
|
+
materializeDigestSnapshot(input) {
|
|
1187
|
+
const thread = this.getDigestThread(input.threadId);
|
|
1188
|
+
if (!thread) {
|
|
1189
|
+
throw new Error(`unknown digest thread: ${input.threadId}`);
|
|
1190
|
+
}
|
|
1191
|
+
this.inTransaction(() => {
|
|
1192
|
+
if (thread.latestEntryId != null) {
|
|
1193
|
+
this.db.run("update inbox_entries set superseded_at = ? where entry_id = ? and superseded_at is null", [input.createdAt, thread.latestEntryId]);
|
|
1194
|
+
}
|
|
1195
|
+
const nextRevision = thread.latestRevision + 1;
|
|
1196
|
+
const entryId = this.insertInboxEntryRecord({
|
|
1197
|
+
inboxId: input.inboxId,
|
|
1198
|
+
kind: "digest_snapshot",
|
|
1199
|
+
threadId: input.threadId,
|
|
1200
|
+
revision: nextRevision,
|
|
1201
|
+
groupKey: input.groupKey,
|
|
1202
|
+
resourceRef: input.resourceRef ?? null,
|
|
1203
|
+
eventFamily: input.eventFamily ?? null,
|
|
1204
|
+
itemJson: null,
|
|
1205
|
+
count: input.itemIds.length,
|
|
1206
|
+
summary: input.summary,
|
|
1207
|
+
firstItemAt: input.firstItemAt,
|
|
1208
|
+
lastItemAt: input.lastItemAt,
|
|
1209
|
+
sourceIdsJson: JSON.stringify(uniqueSorted(input.sourceIds)),
|
|
1210
|
+
subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
|
|
1211
|
+
deliveryHandleJson: input.deliveryHandleJson ?? null,
|
|
1212
|
+
ackedAt: null,
|
|
1213
|
+
supersededAt: null,
|
|
1214
|
+
createdAt: input.createdAt,
|
|
1215
|
+
});
|
|
1216
|
+
for (const itemId of input.itemIds) {
|
|
1217
|
+
this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, itemId]);
|
|
1218
|
+
}
|
|
1219
|
+
this.db.run(`
|
|
1220
|
+
update digest_threads
|
|
1221
|
+
set latest_revision = ?, latest_entry_id = ?, summary = ?, first_item_at = ?, last_item_at = ?,
|
|
1222
|
+
flush_after_at = null, updated_at = ?
|
|
1223
|
+
where thread_id = ?
|
|
1224
|
+
`, [nextRevision, entryId, input.summary, input.firstItemAt, input.lastItemAt, input.createdAt, input.threadId]);
|
|
1225
|
+
});
|
|
1226
|
+
this.persist();
|
|
1227
|
+
return this.getInboxEntry(this.getDigestThread(input.threadId).latestEntryId);
|
|
1228
|
+
}
|
|
1229
|
+
closeDigestThread(threadId, updatedAt) {
|
|
1230
|
+
this.db.run("update digest_threads set status = 'closed', flush_after_at = null, updated_at = ? where thread_id = ?", [updatedAt, threadId]);
|
|
1231
|
+
this.persist();
|
|
1232
|
+
}
|
|
1233
|
+
listDigestThreadsForRawItems(itemIds) {
|
|
1234
|
+
if (itemIds.length === 0) {
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
const placeholders = itemIds.map(() => "?").join(", ");
|
|
1238
|
+
const rows = this.getAll(`
|
|
1239
|
+
select distinct t.*
|
|
1240
|
+
from digest_threads t
|
|
1241
|
+
join digest_thread_items i on i.thread_id = t.thread_id
|
|
1242
|
+
where i.item_id in (${placeholders})
|
|
1243
|
+
order by t.created_at asc, t.thread_id asc
|
|
1244
|
+
`, itemIds);
|
|
1245
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1246
|
+
}
|
|
1247
|
+
listInboxItems(inboxId, options) {
|
|
1248
|
+
const includeAcked = options?.includeAcked ?? false;
|
|
1249
|
+
const filters = ["inbox_id = ?"];
|
|
1250
|
+
const params = [inboxId];
|
|
1251
|
+
if (!includeAcked) {
|
|
1252
|
+
filters.push("acked_at is null");
|
|
807
1253
|
}
|
|
808
1254
|
const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by coalesce(inbox_sequence, rowid) asc`, params);
|
|
809
1255
|
return rows.map((row) => this.mapInboxItem(row));
|
|
@@ -842,29 +1288,31 @@ class AgentInboxStore {
|
|
|
842
1288
|
return changes;
|
|
843
1289
|
}
|
|
844
1290
|
deleteAckedInboxItems(inboxId, olderThan) {
|
|
845
|
-
this.
|
|
846
|
-
|
|
1291
|
+
const itemIds = this.getAll(`
|
|
1292
|
+
select item_id
|
|
1293
|
+
from inbox_items
|
|
847
1294
|
where inbox_id = ?
|
|
848
1295
|
and acked_at is not null
|
|
849
1296
|
and acked_at < ?
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
this.persist();
|
|
1297
|
+
`, [inboxId, olderThan]).map((row) => String(row.item_id));
|
|
1298
|
+
if (itemIds.length === 0) {
|
|
1299
|
+
return 0;
|
|
854
1300
|
}
|
|
855
|
-
|
|
1301
|
+
this.deleteInboxItemsAndEntryArtifacts(itemIds);
|
|
1302
|
+
return itemIds.length;
|
|
856
1303
|
}
|
|
857
1304
|
deleteAckedInboxItemsGlobal(olderThan) {
|
|
858
|
-
this.
|
|
859
|
-
|
|
1305
|
+
const itemIds = this.getAll(`
|
|
1306
|
+
select item_id
|
|
1307
|
+
from inbox_items
|
|
860
1308
|
where acked_at is not null
|
|
861
1309
|
and acked_at < ?
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.persist();
|
|
1310
|
+
`, [olderThan]).map((row) => String(row.item_id));
|
|
1311
|
+
if (itemIds.length === 0) {
|
|
1312
|
+
return 0;
|
|
866
1313
|
}
|
|
867
|
-
|
|
1314
|
+
this.deleteInboxItemsAndEntryArtifacts(itemIds);
|
|
1315
|
+
return itemIds.length;
|
|
868
1316
|
}
|
|
869
1317
|
countInboxItems(inboxId, includeAcked) {
|
|
870
1318
|
const row = this.getOne(`
|
|
@@ -892,7 +1340,7 @@ class AgentInboxStore {
|
|
|
892
1340
|
JSON.stringify(activation.sourceIds),
|
|
893
1341
|
activation.newItemCount,
|
|
894
1342
|
activation.summary,
|
|
895
|
-
activation.items ? JSON.stringify(activation.items) : null,
|
|
1343
|
+
activation.entries ? JSON.stringify(activation.entries) : (activation.items ? JSON.stringify(activation.items) : null),
|
|
896
1344
|
activation.createdAt,
|
|
897
1345
|
activation.deliveredAt ?? null,
|
|
898
1346
|
]);
|
|
@@ -943,34 +1391,50 @@ class AgentInboxStore {
|
|
|
943
1391
|
return row ? this.mapStream(row) : null;
|
|
944
1392
|
}
|
|
945
1393
|
insertStreamEvent(streamId, event) {
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1394
|
+
const occurredAt = event.occurredAt ?? (0, util_1.nowIso)();
|
|
1395
|
+
const metadataJson = JSON.stringify(event.metadata ?? {});
|
|
1396
|
+
const rawPayloadJson = JSON.stringify(event.rawPayload ?? {});
|
|
1397
|
+
const deliveryHandleJson = event.deliveryHandle ? JSON.stringify(event.deliveryHandle) : null;
|
|
1398
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
1399
|
+
const before = this.changes();
|
|
1400
|
+
this.db.run(`
|
|
1401
|
+
insert or ignore into stream_events (
|
|
1402
|
+
stream_event_id, stream_id, source_id, source_native_id, event_variant,
|
|
1403
|
+
occurred_at, metadata_json, raw_payload_json, delivery_handle_json, created_at
|
|
1404
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1405
|
+
`, [
|
|
1406
|
+
(0, util_1.generateCanonicalId)("evt"),
|
|
1407
|
+
streamId,
|
|
1408
|
+
event.sourceId,
|
|
1409
|
+
event.sourceNativeId,
|
|
1410
|
+
event.eventVariant,
|
|
1411
|
+
occurredAt,
|
|
1412
|
+
metadataJson,
|
|
1413
|
+
rawPayloadJson,
|
|
1414
|
+
deliveryHandleJson,
|
|
1415
|
+
(0, util_1.nowIso)(),
|
|
1416
|
+
]);
|
|
1417
|
+
if (this.changes() > before) {
|
|
1418
|
+
this.persist();
|
|
1419
|
+
return {
|
|
1420
|
+
appended: 1,
|
|
1421
|
+
deduped: 0,
|
|
1422
|
+
lastOffset: this.lastInsertRowId(),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const existing = this.getOne(`
|
|
1426
|
+
select offset
|
|
1427
|
+
from stream_events
|
|
1428
|
+
where stream_id = ?
|
|
1429
|
+
and source_native_id = ?
|
|
1430
|
+
and event_variant = ?
|
|
1431
|
+
limit 1
|
|
1432
|
+
`, [streamId, event.sourceNativeId, event.eventVariant]);
|
|
1433
|
+
if (existing) {
|
|
1434
|
+
return { appended: 0, deduped: 1, lastOffset: null };
|
|
1435
|
+
}
|
|
972
1436
|
}
|
|
973
|
-
|
|
1437
|
+
throw new Error(`unable to allocate a unique stream_event_id for ${event.sourceNativeId}`);
|
|
974
1438
|
}
|
|
975
1439
|
readStreamEvents(streamId, nextOffset, limit) {
|
|
976
1440
|
const rows = this.getAll(`
|
|
@@ -1058,7 +1522,7 @@ class AgentInboxStore {
|
|
|
1058
1522
|
insert into consumer_commits (
|
|
1059
1523
|
commit_id, consumer_id, stream_id, committed_offset, committed_at
|
|
1060
1524
|
) values (?, ?, ?, ?, ?)
|
|
1061
|
-
`, [(0, util_1.
|
|
1525
|
+
`, [(0, util_1.generateCanonicalId)("cmt"), consumerId, consumer.streamId, committedOffset, updatedAt]);
|
|
1062
1526
|
});
|
|
1063
1527
|
this.persist();
|
|
1064
1528
|
return this.getConsumer(consumerId);
|
|
@@ -1139,8 +1603,15 @@ class AgentInboxStore {
|
|
|
1139
1603
|
}
|
|
1140
1604
|
}
|
|
1141
1605
|
mapSource(row) {
|
|
1606
|
+
const hostId = requiredText(row, "host_id");
|
|
1607
|
+
const streamKind = requiredText(row, "stream_kind");
|
|
1608
|
+
const streamKey = requiredText(row, "stream_key");
|
|
1142
1609
|
return {
|
|
1143
1610
|
sourceId: String(row.source_id),
|
|
1611
|
+
streamId: String(row.source_id),
|
|
1612
|
+
hostId,
|
|
1613
|
+
streamKind,
|
|
1614
|
+
streamKey,
|
|
1144
1615
|
sourceType: row.source_type,
|
|
1145
1616
|
sourceKey: String(row.source_key),
|
|
1146
1617
|
configRef: row.config_ref ? String(row.config_ref) : null,
|
|
@@ -1151,6 +1622,18 @@ class AgentInboxStore {
|
|
|
1151
1622
|
updatedAt: String(row.updated_at),
|
|
1152
1623
|
};
|
|
1153
1624
|
}
|
|
1625
|
+
mapSourceHost(row) {
|
|
1626
|
+
return {
|
|
1627
|
+
hostId: String(row.host_id),
|
|
1628
|
+
hostType: String(row.host_type),
|
|
1629
|
+
hostKey: String(row.host_key),
|
|
1630
|
+
configRef: row.config_ref ? String(row.config_ref) : null,
|
|
1631
|
+
config: parseJson(row.config_json),
|
|
1632
|
+
status: String(row.status),
|
|
1633
|
+
createdAt: String(row.created_at),
|
|
1634
|
+
updatedAt: String(row.updated_at),
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1154
1637
|
mapSourceIdleState(row) {
|
|
1155
1638
|
return {
|
|
1156
1639
|
sourceId: String(row.source_id),
|
|
@@ -1176,6 +1659,10 @@ class AgentInboxStore {
|
|
|
1176
1659
|
return {
|
|
1177
1660
|
inboxId: String(row.inbox_id),
|
|
1178
1661
|
ownerAgentId: String(row.owner_agent_id),
|
|
1662
|
+
aggregationEnabled: Number(row.aggregation_enabled ?? 0) > 0,
|
|
1663
|
+
aggregationWindowMs: row.aggregation_window_ms == null ? null : Number(row.aggregation_window_ms),
|
|
1664
|
+
aggregationMaxItems: row.aggregation_max_items == null ? null : Number(row.aggregation_max_items),
|
|
1665
|
+
aggregationMaxThreadAgeMs: row.aggregation_max_thread_age_ms == null ? null : Number(row.aggregation_max_thread_age_ms),
|
|
1179
1666
|
createdAt: String(row.created_at),
|
|
1180
1667
|
};
|
|
1181
1668
|
}
|
|
@@ -1196,7 +1683,7 @@ class AgentInboxStore {
|
|
|
1196
1683
|
mapSubscriptionLifecycleRetirement(row) {
|
|
1197
1684
|
return {
|
|
1198
1685
|
subscriptionId: String(row.subscription_id),
|
|
1199
|
-
|
|
1686
|
+
hostId: String(row.host_id),
|
|
1200
1687
|
trackedResourceRef: String(row.tracked_resource_ref),
|
|
1201
1688
|
retireAt: String(row.retire_at),
|
|
1202
1689
|
terminalState: row.terminal_state ? String(row.terminal_state) : null,
|
|
@@ -1224,6 +1711,11 @@ class AgentInboxStore {
|
|
|
1224
1711
|
mode: row.mode,
|
|
1225
1712
|
url,
|
|
1226
1713
|
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1714
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1715
|
+
notificationPolicy: {
|
|
1716
|
+
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1717
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1718
|
+
},
|
|
1227
1719
|
createdAt: String(row.created_at),
|
|
1228
1720
|
updatedAt: String(row.updated_at),
|
|
1229
1721
|
lastSeenAt: String(row.last_seen_at),
|
|
@@ -1240,6 +1732,11 @@ class AgentInboxStore {
|
|
|
1240
1732
|
lastError: row.last_error ? String(row.last_error) : null,
|
|
1241
1733
|
mode: row.mode,
|
|
1242
1734
|
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1735
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1736
|
+
notificationPolicy: {
|
|
1737
|
+
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1738
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1739
|
+
},
|
|
1243
1740
|
runtimeKind: (row.runtime_kind ? String(row.runtime_kind) : "unknown"),
|
|
1244
1741
|
runtimeSessionId: row.runtime_session_id ? String(row.runtime_session_id) : null,
|
|
1245
1742
|
runtimePid: row.runtime_pid == null ? null : Number(row.runtime_pid),
|
|
@@ -1260,6 +1757,11 @@ class AgentInboxStore {
|
|
|
1260
1757
|
status: row.status,
|
|
1261
1758
|
leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
|
|
1262
1759
|
lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
|
|
1760
|
+
deferReason: row.defer_reason ? row.defer_reason : null,
|
|
1761
|
+
deferAttempts: Number(row.defer_attempts ?? 0),
|
|
1762
|
+
firstDeferredAt: row.first_deferred_at ? String(row.first_deferred_at) : null,
|
|
1763
|
+
lastDeferredAt: row.last_deferred_at ? String(row.last_deferred_at) : null,
|
|
1764
|
+
pendingFingerprint: row.pending_fingerprint ? String(row.pending_fingerprint) : null,
|
|
1263
1765
|
pendingNewItemCount: Number(row.pending_new_item_count),
|
|
1264
1766
|
pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
|
|
1265
1767
|
pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
|
|
@@ -1301,6 +1803,70 @@ class AgentInboxStore {
|
|
|
1301
1803
|
ackedAt: row.acked_at ? String(row.acked_at) : null,
|
|
1302
1804
|
};
|
|
1303
1805
|
}
|
|
1806
|
+
mapInboxEntry(row, itemIds) {
|
|
1807
|
+
const base = {
|
|
1808
|
+
entryId: (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
1809
|
+
inboxId: String(row.inbox_id),
|
|
1810
|
+
sequence: Number(row.sequence),
|
|
1811
|
+
itemId: "",
|
|
1812
|
+
summary: String(row.summary),
|
|
1813
|
+
count: Number(row.count),
|
|
1814
|
+
itemIds,
|
|
1815
|
+
sourceIds: parseJson(row.source_ids_json),
|
|
1816
|
+
subscriptionIds: parseJson(row.subscription_ids_json),
|
|
1817
|
+
firstItemAt: String(row.first_item_at),
|
|
1818
|
+
lastItemAt: String(row.last_item_at),
|
|
1819
|
+
deliveryHandle: row.delivery_handle_json
|
|
1820
|
+
? parseJson(row.delivery_handle_json)
|
|
1821
|
+
: null,
|
|
1822
|
+
ackedAt: row.acked_at ? String(row.acked_at) : null,
|
|
1823
|
+
supersededAt: row.superseded_at ? String(row.superseded_at) : null,
|
|
1824
|
+
};
|
|
1825
|
+
if (String(row.kind) === "item") {
|
|
1826
|
+
const item = parseJson(row.item_json);
|
|
1827
|
+
return {
|
|
1828
|
+
...base,
|
|
1829
|
+
kind: "item",
|
|
1830
|
+
itemId: item.itemId,
|
|
1831
|
+
sourceId: item.sourceId,
|
|
1832
|
+
sourceNativeId: item.sourceNativeId,
|
|
1833
|
+
eventVariant: item.eventVariant,
|
|
1834
|
+
occurredAt: item.occurredAt,
|
|
1835
|
+
metadata: item.metadata,
|
|
1836
|
+
rawPayload: item.rawPayload,
|
|
1837
|
+
item,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
...base,
|
|
1842
|
+
kind: "digest_snapshot",
|
|
1843
|
+
itemId: base.itemIds[0] ?? (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
1844
|
+
threadId: (0, util_1.formatThreadRef)(requiredText(row, "thread_id")),
|
|
1845
|
+
revision: Number(row.revision),
|
|
1846
|
+
groupKey: String(row.group_key),
|
|
1847
|
+
resourceRef: row.resource_ref ? String(row.resource_ref) : null,
|
|
1848
|
+
eventFamily: row.event_family ? String(row.event_family) : null,
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
mapDigestThread(row) {
|
|
1852
|
+
return {
|
|
1853
|
+
threadId: requiredText(row, "thread_id"),
|
|
1854
|
+
inboxId: String(row.inbox_id),
|
|
1855
|
+
sourceId: String(row.source_id),
|
|
1856
|
+
groupKey: String(row.group_key),
|
|
1857
|
+
resourceRef: row.resource_ref ? String(row.resource_ref) : null,
|
|
1858
|
+
eventFamily: row.event_family ? String(row.event_family) : null,
|
|
1859
|
+
latestRevision: Number(row.latest_revision),
|
|
1860
|
+
latestEntryId: row.latest_entry_id == null ? null : String(row.latest_entry_id),
|
|
1861
|
+
status: String(row.status),
|
|
1862
|
+
summary: String(row.summary),
|
|
1863
|
+
firstItemAt: String(row.first_item_at),
|
|
1864
|
+
lastItemAt: String(row.last_item_at),
|
|
1865
|
+
flushAfterAt: row.flush_after_at ? String(row.flush_after_at) : null,
|
|
1866
|
+
createdAt: String(row.created_at),
|
|
1867
|
+
updatedAt: String(row.updated_at),
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1304
1870
|
mapActivation(row) {
|
|
1305
1871
|
return {
|
|
1306
1872
|
kind: "agentinbox.activation",
|
|
@@ -1311,9 +1877,10 @@ class AgentInboxStore {
|
|
|
1311
1877
|
targetKind: row.target_kind,
|
|
1312
1878
|
subscriptionIds: parseJson(row.subscription_ids_json),
|
|
1313
1879
|
sourceIds: parseJson(row.source_ids_json),
|
|
1880
|
+
newEntryCount: row.items_json ? parseJson(row.items_json)?.length ?? 0 : Number(row.new_item_count),
|
|
1314
1881
|
newItemCount: Number(row.new_item_count),
|
|
1315
1882
|
summary: String(row.summary),
|
|
1316
|
-
|
|
1883
|
+
entries: row.items_json ? parseJson(row.items_json) : undefined,
|
|
1317
1884
|
createdAt: String(row.created_at),
|
|
1318
1885
|
deliveredAt: row.delivered_at ? String(row.delivered_at) : null,
|
|
1319
1886
|
};
|
|
@@ -1332,6 +1899,191 @@ class AgentInboxStore {
|
|
|
1332
1899
|
createdAt: String(row.created_at),
|
|
1333
1900
|
};
|
|
1334
1901
|
}
|
|
1902
|
+
resolveInboxEntryId(inboxId, refOrItemId) {
|
|
1903
|
+
try {
|
|
1904
|
+
return (0, util_1.parseEntryRef)(refOrItemId);
|
|
1905
|
+
}
|
|
1906
|
+
catch {
|
|
1907
|
+
const row = this.getOne(`
|
|
1908
|
+
select e.entry_id
|
|
1909
|
+
from inbox_entries e
|
|
1910
|
+
join inbox_entry_items i on i.entry_id = e.entry_id
|
|
1911
|
+
where e.inbox_id = ?
|
|
1912
|
+
and i.item_id = ?
|
|
1913
|
+
order by e.sequence desc
|
|
1914
|
+
limit 1
|
|
1915
|
+
`, [inboxId, refOrItemId]);
|
|
1916
|
+
if (!row) {
|
|
1917
|
+
throw new Error(`unknown inbox entry: ${refOrItemId}`);
|
|
1918
|
+
}
|
|
1919
|
+
return String(row.entry_id);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
insertInboxEntryRecord(input) {
|
|
1923
|
+
const entryId = (0, util_1.generateCanonicalId)("ent");
|
|
1924
|
+
this.db.run(`
|
|
1925
|
+
insert into inbox_entries (
|
|
1926
|
+
entry_id, inbox_id, kind, sequence, thread_id, revision, group_key, resource_ref, event_family,
|
|
1927
|
+
item_json, count, summary, first_item_at, last_item_at, source_ids_json,
|
|
1928
|
+
subscription_ids_json, delivery_handle_json, acked_at, superseded_at, created_at
|
|
1929
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1930
|
+
`, [
|
|
1931
|
+
entryId,
|
|
1932
|
+
input.inboxId,
|
|
1933
|
+
input.kind,
|
|
1934
|
+
this.nextInboxEntrySequence(),
|
|
1935
|
+
input.threadId,
|
|
1936
|
+
input.revision,
|
|
1937
|
+
input.groupKey,
|
|
1938
|
+
input.resourceRef,
|
|
1939
|
+
input.eventFamily,
|
|
1940
|
+
input.itemJson,
|
|
1941
|
+
input.count,
|
|
1942
|
+
input.summary,
|
|
1943
|
+
input.firstItemAt,
|
|
1944
|
+
input.lastItemAt,
|
|
1945
|
+
input.sourceIdsJson,
|
|
1946
|
+
input.subscriptionIdsJson,
|
|
1947
|
+
input.deliveryHandleJson,
|
|
1948
|
+
input.ackedAt,
|
|
1949
|
+
input.supersededAt,
|
|
1950
|
+
input.createdAt,
|
|
1951
|
+
]);
|
|
1952
|
+
return entryId;
|
|
1953
|
+
}
|
|
1954
|
+
nextInboxEntrySequence() {
|
|
1955
|
+
const row = this.getOne("select coalesce(max(sequence), 0) + 1 as next_sequence from inbox_entries");
|
|
1956
|
+
return Number(row?.next_sequence ?? 1);
|
|
1957
|
+
}
|
|
1958
|
+
listEntryItemIds(entryId) {
|
|
1959
|
+
const rows = this.getAll(`
|
|
1960
|
+
select iei.item_id
|
|
1961
|
+
from inbox_entry_items iei
|
|
1962
|
+
left join inbox_items ii on ii.item_id = iei.item_id
|
|
1963
|
+
where iei.entry_id = ?
|
|
1964
|
+
order by coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
|
|
1965
|
+
`, [entryId]);
|
|
1966
|
+
return rows.map((row) => String(row.item_id));
|
|
1967
|
+
}
|
|
1968
|
+
listEntryItemIdsBulk(entryIds) {
|
|
1969
|
+
const result = new Map();
|
|
1970
|
+
if (entryIds.length === 0) {
|
|
1971
|
+
return result;
|
|
1972
|
+
}
|
|
1973
|
+
const placeholders = entryIds.map(() => "?").join(", ");
|
|
1974
|
+
const rows = this.getAll(`
|
|
1975
|
+
select iei.entry_id, iei.item_id
|
|
1976
|
+
from inbox_entry_items iei
|
|
1977
|
+
left join inbox_items ii on ii.item_id = iei.item_id
|
|
1978
|
+
where iei.entry_id in (${placeholders})
|
|
1979
|
+
order by iei.entry_id asc, coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
|
|
1980
|
+
`, entryIds);
|
|
1981
|
+
for (const row of rows) {
|
|
1982
|
+
const entryId = String(row.entry_id);
|
|
1983
|
+
const itemIds = result.get(entryId) ?? [];
|
|
1984
|
+
itemIds.push(String(row.item_id));
|
|
1985
|
+
result.set(entryId, itemIds);
|
|
1986
|
+
}
|
|
1987
|
+
return result;
|
|
1988
|
+
}
|
|
1989
|
+
getRawItemIdsForEntryIds(entryIds) {
|
|
1990
|
+
const itemIds = new Set();
|
|
1991
|
+
for (const entryId of entryIds) {
|
|
1992
|
+
for (const itemId of this.listEntryItemIds(entryId)) {
|
|
1993
|
+
itemIds.add(itemId);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return [...itemIds];
|
|
1997
|
+
}
|
|
1998
|
+
mapInboxEntries(rows) {
|
|
1999
|
+
const itemIdsByEntry = this.listEntryItemIdsBulk(rows.map((row) => String(row.entry_id)));
|
|
2000
|
+
return rows.map((row) => this.mapInboxEntry(row, itemIdsByEntry.get(String(row.entry_id)) ?? []));
|
|
2001
|
+
}
|
|
2002
|
+
syncInboxEntryAckState(inboxId, ackedAt) {
|
|
2003
|
+
this.db.run(`
|
|
2004
|
+
update inbox_entries
|
|
2005
|
+
set acked_at = ?
|
|
2006
|
+
where inbox_id = ?
|
|
2007
|
+
and acked_at is null
|
|
2008
|
+
and not exists (
|
|
2009
|
+
select 1
|
|
2010
|
+
from inbox_entry_items e
|
|
2011
|
+
join inbox_items i on i.item_id = e.item_id
|
|
2012
|
+
where e.entry_id = inbox_entries.entry_id
|
|
2013
|
+
and i.acked_at is null
|
|
2014
|
+
)
|
|
2015
|
+
`, [ackedAt, inboxId]);
|
|
2016
|
+
this.persist();
|
|
2017
|
+
}
|
|
2018
|
+
countAckedEntries(entryIds, ackedAt) {
|
|
2019
|
+
if (entryIds.length === 0) {
|
|
2020
|
+
return 0;
|
|
2021
|
+
}
|
|
2022
|
+
const placeholders = entryIds.map(() => "?").join(", ");
|
|
2023
|
+
const row = this.getOne(`select count(*) as count from inbox_entries where entry_id in (${placeholders}) and acked_at = ?`, [...entryIds, ackedAt]);
|
|
2024
|
+
return Number(row?.count ?? 0);
|
|
2025
|
+
}
|
|
2026
|
+
deleteInboxItemsAndEntryArtifacts(itemIds) {
|
|
2027
|
+
const placeholders = itemIds.map(() => "?").join(", ");
|
|
2028
|
+
this.inTransaction(() => {
|
|
2029
|
+
this.db.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
|
|
2030
|
+
this.db.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
|
|
2031
|
+
this.db.run(`
|
|
2032
|
+
delete from inbox_entries
|
|
2033
|
+
where entry_id in (
|
|
2034
|
+
select e.entry_id
|
|
2035
|
+
from inbox_entries e
|
|
2036
|
+
where not exists (
|
|
2037
|
+
select 1
|
|
2038
|
+
from inbox_entry_items ee
|
|
2039
|
+
where ee.entry_id = e.entry_id
|
|
2040
|
+
and exists (
|
|
2041
|
+
select 1
|
|
2042
|
+
from inbox_items ii
|
|
2043
|
+
where ii.item_id = ee.item_id
|
|
2044
|
+
)
|
|
2045
|
+
)
|
|
2046
|
+
)
|
|
2047
|
+
`);
|
|
2048
|
+
this.db.run(`
|
|
2049
|
+
delete from inbox_entry_items
|
|
2050
|
+
where not exists (
|
|
2051
|
+
select 1
|
|
2052
|
+
from inbox_items ii
|
|
2053
|
+
where ii.item_id = inbox_entry_items.item_id
|
|
2054
|
+
)
|
|
2055
|
+
`);
|
|
2056
|
+
this.db.run(`
|
|
2057
|
+
update digest_threads
|
|
2058
|
+
set latest_entry_id = (
|
|
2059
|
+
select e.entry_id
|
|
2060
|
+
from inbox_entries e
|
|
2061
|
+
where e.thread_id = digest_threads.thread_id
|
|
2062
|
+
order by coalesce(e.revision, 0) desc, e.entry_id desc
|
|
2063
|
+
limit 1
|
|
2064
|
+
),
|
|
2065
|
+
latest_revision = coalesce((
|
|
2066
|
+
select max(coalesce(e.revision, 0))
|
|
2067
|
+
from inbox_entries e
|
|
2068
|
+
where e.thread_id = digest_threads.thread_id
|
|
2069
|
+
), 0)
|
|
2070
|
+
`);
|
|
2071
|
+
this.db.run(`
|
|
2072
|
+
delete from digest_threads
|
|
2073
|
+
where not exists (
|
|
2074
|
+
select 1
|
|
2075
|
+
from digest_thread_items dti
|
|
2076
|
+
where dti.thread_id = digest_threads.thread_id
|
|
2077
|
+
)
|
|
2078
|
+
and not exists (
|
|
2079
|
+
select 1
|
|
2080
|
+
from inbox_entries e
|
|
2081
|
+
where e.thread_id = digest_threads.thread_id
|
|
2082
|
+
)
|
|
2083
|
+
`);
|
|
2084
|
+
});
|
|
2085
|
+
this.persist();
|
|
2086
|
+
}
|
|
1335
2087
|
mapStream(row) {
|
|
1336
2088
|
return {
|
|
1337
2089
|
streamId: String(row.stream_id),
|
|
@@ -1374,3 +2126,16 @@ class AgentInboxStore {
|
|
|
1374
2126
|
}
|
|
1375
2127
|
}
|
|
1376
2128
|
exports.AgentInboxStore = AgentInboxStore;
|
|
2129
|
+
function requiredText(row, key) {
|
|
2130
|
+
const value = row[key];
|
|
2131
|
+
if (value == null) {
|
|
2132
|
+
throw new Error(`missing required column: ${key}`);
|
|
2133
|
+
}
|
|
2134
|
+
return String(value);
|
|
2135
|
+
}
|
|
2136
|
+
function uniqueSorted(values) {
|
|
2137
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))].sort();
|
|
2138
|
+
}
|
|
2139
|
+
function summarizeBackfilledItemEntry(item) {
|
|
2140
|
+
return `${item.eventVariant} from ${item.sourceId}`;
|
|
2141
|
+
}
|