@holon-run/agentinbox 0.6.0 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -7
- package/dist/src/adapters.js +61 -6
- package/dist/src/backend.js +4 -16
- package/dist/src/cli.js +307 -53
- package/dist/src/client.js +2 -2
- package/dist/src/daemon.js +36 -2
- package/dist/src/http.js +467 -45
- package/dist/src/logging.js +112 -0
- package/dist/src/paths.js +1 -0
- package/dist/src/runtime_gate.js +394 -63
- package/dist/src/service.js +856 -154
- package/dist/src/source_hosts.js +75 -0
- package/dist/src/source_resolution.js +20 -20
- package/dist/src/source_schema.js +2 -2
- package/dist/src/sources/feishu.js +66 -28
- package/dist/src/sources/github.js +188 -24
- package/dist/src/sources/remote.js +44 -24
- package/dist/src/sources/{remote_profiles.js → remote_modules.js} +143 -58
- package/dist/src/store.js +861 -106
- package/dist/src/terminal.js +33 -7
- package/dist/src/util.js +46 -1
- package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
- package/drizzle/schema.ts +93 -1
- package/package.json +2 -1
- package/scripts/iterm2_cursor_probe.py +8 -3
- package/scripts/release_smoke.sh +123 -0
- package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
- package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
- package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
- package/drizzle/migrations/0004_source_idle_states.sql +0 -10
- package/drizzle/migrations/0005_timers.sql +0 -22
- package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
- package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
- package/drizzle/migrations/0008_inbox_item_sequence.sql +0 -21
package/dist/src/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,8 +518,43 @@ 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
|
+
]);
|
|
535
|
+
this.persist();
|
|
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
|
+
]);
|
|
353
556
|
this.persist();
|
|
557
|
+
return this.getInbox(inboxId);
|
|
354
558
|
}
|
|
355
559
|
listInboxes() {
|
|
356
560
|
const rows = this.getAll("select * from inboxes order by created_at asc");
|
|
@@ -482,8 +686,8 @@ class AgentInboxStore {
|
|
|
482
686
|
this.db.run(`
|
|
483
687
|
insert into activation_targets (
|
|
484
688
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
689
|
+
mode, notify_lease_ms, min_unacked_items, url, created_at, updated_at, last_seen_at
|
|
690
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
487
691
|
`, [
|
|
488
692
|
target.targetId,
|
|
489
693
|
target.agentId,
|
|
@@ -495,6 +699,7 @@ class AgentInboxStore {
|
|
|
495
699
|
target.lastError ?? null,
|
|
496
700
|
target.mode,
|
|
497
701
|
target.notifyLeaseMs,
|
|
702
|
+
target.minUnackedItems ?? null,
|
|
498
703
|
target.url,
|
|
499
704
|
target.createdAt,
|
|
500
705
|
target.updatedAt,
|
|
@@ -504,10 +709,10 @@ class AgentInboxStore {
|
|
|
504
709
|
else {
|
|
505
710
|
this.db.run(`
|
|
506
711
|
insert into activation_targets (
|
|
507
|
-
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms,
|
|
712
|
+
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms, min_unacked_items,
|
|
508
713
|
runtime_kind, runtime_session_id, runtime_pid, backend, tmux_pane_id, tty, term_program, iterm_session_id,
|
|
509
714
|
created_at, updated_at, last_seen_at
|
|
510
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
715
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
716
|
`, [
|
|
512
717
|
target.targetId,
|
|
513
718
|
target.agentId,
|
|
@@ -519,6 +724,7 @@ class AgentInboxStore {
|
|
|
519
724
|
target.lastError ?? null,
|
|
520
725
|
target.mode,
|
|
521
726
|
target.notifyLeaseMs,
|
|
727
|
+
target.minUnackedItems ?? null,
|
|
522
728
|
target.runtimeKind,
|
|
523
729
|
target.runtimeSessionId ?? null,
|
|
524
730
|
target.runtimePid ?? null,
|
|
@@ -538,7 +744,7 @@ class AgentInboxStore {
|
|
|
538
744
|
this.db.run(`
|
|
539
745
|
update activation_targets
|
|
540
746
|
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 = ?
|
|
747
|
+
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
748
|
where target_id = ? and kind = 'terminal'
|
|
543
749
|
`, [
|
|
544
750
|
input.runtimeKind,
|
|
@@ -548,6 +754,8 @@ class AgentInboxStore {
|
|
|
548
754
|
input.tty ?? null,
|
|
549
755
|
input.termProgram ?? null,
|
|
550
756
|
input.itermSessionId ?? null,
|
|
757
|
+
input.notifyLeaseMs,
|
|
758
|
+
input.minUnackedItems,
|
|
551
759
|
input.updatedAt,
|
|
552
760
|
input.lastSeenAt,
|
|
553
761
|
targetId,
|
|
@@ -729,13 +937,19 @@ class AgentInboxStore {
|
|
|
729
937
|
upsertActivationDispatchState(state) {
|
|
730
938
|
this.db.run(`
|
|
731
939
|
insert into activation_dispatch_states (
|
|
732
|
-
agent_id, target_id, status, lease_expires_at, last_notified_fingerprint,
|
|
940
|
+
agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, defer_reason, defer_attempts,
|
|
941
|
+
first_deferred_at, last_deferred_at, pending_fingerprint, pending_new_item_count, pending_summary,
|
|
733
942
|
pending_subscription_ids_json, pending_source_ids_json, updated_at
|
|
734
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
943
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
735
944
|
on conflict(agent_id, target_id) do update set
|
|
736
945
|
status = excluded.status,
|
|
737
946
|
lease_expires_at = excluded.lease_expires_at,
|
|
738
947
|
last_notified_fingerprint = excluded.last_notified_fingerprint,
|
|
948
|
+
defer_reason = excluded.defer_reason,
|
|
949
|
+
defer_attempts = excluded.defer_attempts,
|
|
950
|
+
first_deferred_at = excluded.first_deferred_at,
|
|
951
|
+
last_deferred_at = excluded.last_deferred_at,
|
|
952
|
+
pending_fingerprint = excluded.pending_fingerprint,
|
|
739
953
|
pending_new_item_count = excluded.pending_new_item_count,
|
|
740
954
|
pending_summary = excluded.pending_summary,
|
|
741
955
|
pending_subscription_ids_json = excluded.pending_subscription_ids_json,
|
|
@@ -747,6 +961,11 @@ class AgentInboxStore {
|
|
|
747
961
|
state.status,
|
|
748
962
|
state.leaseExpiresAt ?? null,
|
|
749
963
|
state.lastNotifiedFingerprint ?? null,
|
|
964
|
+
state.deferReason ?? null,
|
|
965
|
+
state.deferAttempts,
|
|
966
|
+
state.firstDeferredAt ?? null,
|
|
967
|
+
state.lastDeferredAt ?? null,
|
|
968
|
+
state.pendingFingerprint ?? null,
|
|
750
969
|
state.pendingNewItemCount,
|
|
751
970
|
state.pendingSummary ?? null,
|
|
752
971
|
JSON.stringify(state.pendingSubscriptionIds),
|
|
@@ -790,20 +1009,237 @@ class AgentInboxStore {
|
|
|
790
1009
|
}
|
|
791
1010
|
return inserted;
|
|
792
1011
|
}
|
|
793
|
-
|
|
1012
|
+
createInboxItemEntry(inboxId, item, input) {
|
|
1013
|
+
const entryId = this.insertInboxEntryRecord({
|
|
1014
|
+
inboxId,
|
|
1015
|
+
kind: "item",
|
|
1016
|
+
threadId: null,
|
|
1017
|
+
revision: null,
|
|
1018
|
+
groupKey: null,
|
|
1019
|
+
resourceRef: null,
|
|
1020
|
+
eventFamily: null,
|
|
1021
|
+
itemJson: JSON.stringify({
|
|
1022
|
+
itemId: item.itemId,
|
|
1023
|
+
sourceId: item.sourceId,
|
|
1024
|
+
sourceNativeId: item.sourceNativeId,
|
|
1025
|
+
eventVariant: item.eventVariant,
|
|
1026
|
+
inboxId: item.inboxId,
|
|
1027
|
+
occurredAt: item.occurredAt,
|
|
1028
|
+
metadata: item.metadata,
|
|
1029
|
+
rawPayload: item.rawPayload,
|
|
1030
|
+
deliveryHandle: item.deliveryHandle ?? null,
|
|
1031
|
+
}),
|
|
1032
|
+
count: 1,
|
|
1033
|
+
summary: input.summary,
|
|
1034
|
+
firstItemAt: item.occurredAt,
|
|
1035
|
+
lastItemAt: item.occurredAt,
|
|
1036
|
+
sourceIdsJson: JSON.stringify([item.sourceId]),
|
|
1037
|
+
subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
|
|
1038
|
+
deliveryHandleJson: item.deliveryHandle ? JSON.stringify(item.deliveryHandle) : null,
|
|
1039
|
+
ackedAt: item.ackedAt ?? null,
|
|
1040
|
+
supersededAt: null,
|
|
1041
|
+
createdAt: (0, util_1.nowIso)(),
|
|
1042
|
+
});
|
|
1043
|
+
this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
|
|
1044
|
+
this.persist();
|
|
1045
|
+
return this.getInboxEntry(entryId);
|
|
1046
|
+
}
|
|
1047
|
+
listInboxEntries(inboxId, options) {
|
|
794
1048
|
const includeAcked = options?.includeAcked ?? false;
|
|
795
1049
|
const filters = ["inbox_id = ?"];
|
|
796
1050
|
const params = [inboxId];
|
|
797
1051
|
if (!includeAcked) {
|
|
798
1052
|
filters.push("acked_at is null");
|
|
1053
|
+
filters.push("superseded_at is null");
|
|
799
1054
|
}
|
|
800
|
-
if (options?.
|
|
801
|
-
const anchor = this.getOne("select
|
|
1055
|
+
if (options?.afterEntryId) {
|
|
1056
|
+
const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, (0, util_1.parseEntryRef)(options.afterEntryId)]);
|
|
802
1057
|
if (!anchor) {
|
|
803
|
-
throw new Error(`unknown inbox
|
|
1058
|
+
throw new Error(`unknown inbox entry: ${options.afterEntryId}`);
|
|
804
1059
|
}
|
|
805
|
-
filters.push("
|
|
806
|
-
params.push(Number(anchor.
|
|
1060
|
+
filters.push("sequence > ?");
|
|
1061
|
+
params.push(Number(anchor.sequence));
|
|
1062
|
+
}
|
|
1063
|
+
const rows = this.getAll(`select * from inbox_entries where ${filters.join(" and ")} order by sequence asc`, params);
|
|
1064
|
+
return this.mapInboxEntries(rows);
|
|
1065
|
+
}
|
|
1066
|
+
getInboxEntry(entryId) {
|
|
1067
|
+
const rows = this.getAll("select * from inbox_entries where entry_id = ?", [(0, util_1.parseEntryRef)(entryId)]);
|
|
1068
|
+
return this.mapInboxEntries(rows)[0] ?? null;
|
|
1069
|
+
}
|
|
1070
|
+
ackInboxEntries(inboxId, entryIds, ackedAt) {
|
|
1071
|
+
const resolvedIds = uniqueSorted(entryIds).map((entryId) => this.resolveInboxEntryId(inboxId, entryId));
|
|
1072
|
+
const itemIds = this.getRawItemIdsForEntryIds(resolvedIds);
|
|
1073
|
+
const ackedItems = itemIds.length > 0 ? this.ackItems(inboxId, itemIds, ackedAt) : 0;
|
|
1074
|
+
if (ackedItems > 0) {
|
|
1075
|
+
this.syncInboxEntryAckState(inboxId, ackedAt);
|
|
1076
|
+
}
|
|
1077
|
+
const ackedEntries = this.countAckedEntries(resolvedIds, ackedAt);
|
|
1078
|
+
return { ackedEntries, ackedItems };
|
|
1079
|
+
}
|
|
1080
|
+
ackInboxEntriesThrough(inboxId, entryId, ackedAt) {
|
|
1081
|
+
const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, this.resolveInboxEntryId(inboxId, entryId)]);
|
|
1082
|
+
if (!anchor) {
|
|
1083
|
+
throw new Error(`unknown inbox entry: ${entryId}`);
|
|
1084
|
+
}
|
|
1085
|
+
const rows = this.getAll(`
|
|
1086
|
+
select entry_id
|
|
1087
|
+
from inbox_entries
|
|
1088
|
+
where inbox_id = ?
|
|
1089
|
+
and acked_at is null
|
|
1090
|
+
and superseded_at is null
|
|
1091
|
+
and sequence <= ?
|
|
1092
|
+
order by sequence asc
|
|
1093
|
+
`, [inboxId, Number(anchor.sequence)]);
|
|
1094
|
+
return this.ackInboxEntries(inboxId, rows.map((row) => (0, util_1.formatEntryRef)(String(row.entry_id))), ackedAt);
|
|
1095
|
+
}
|
|
1096
|
+
getOpenDigestThread(inboxId, groupKey) {
|
|
1097
|
+
const row = this.getOne(`
|
|
1098
|
+
select * from digest_threads
|
|
1099
|
+
where inbox_id = ? and group_key = ? and status = 'open'
|
|
1100
|
+
order by created_at desc, rowid desc
|
|
1101
|
+
limit 1
|
|
1102
|
+
`, [inboxId, groupKey]);
|
|
1103
|
+
return row ? this.mapDigestThread(row) : null;
|
|
1104
|
+
}
|
|
1105
|
+
createDigestThread(input) {
|
|
1106
|
+
const threadId = (0, util_1.generateCanonicalId)("thr");
|
|
1107
|
+
this.db.run(`
|
|
1108
|
+
insert into digest_threads (
|
|
1109
|
+
thread_id, inbox_id, source_id, group_key, resource_ref, event_family, latest_revision,
|
|
1110
|
+
latest_entry_id, status, summary, first_item_at, last_item_at, flush_after_at,
|
|
1111
|
+
created_at, updated_at
|
|
1112
|
+
) values (?, ?, ?, ?, ?, ?, 0, null, 'open', ?, ?, ?, ?, ?, ?)
|
|
1113
|
+
`, [
|
|
1114
|
+
threadId,
|
|
1115
|
+
input.inboxId,
|
|
1116
|
+
input.sourceId,
|
|
1117
|
+
input.groupKey,
|
|
1118
|
+
input.resourceRef ?? null,
|
|
1119
|
+
input.eventFamily ?? null,
|
|
1120
|
+
input.summary,
|
|
1121
|
+
input.firstItemAt,
|
|
1122
|
+
input.firstItemAt,
|
|
1123
|
+
input.flushAfterAt ?? null,
|
|
1124
|
+
input.createdAt,
|
|
1125
|
+
input.createdAt,
|
|
1126
|
+
]);
|
|
1127
|
+
this.persist();
|
|
1128
|
+
return this.getDigestThread(threadId);
|
|
1129
|
+
}
|
|
1130
|
+
getDigestThread(threadId) {
|
|
1131
|
+
const row = this.getOne("select * from digest_threads where thread_id = ?", [threadId]);
|
|
1132
|
+
return row ? this.mapDigestThread(row) : null;
|
|
1133
|
+
}
|
|
1134
|
+
addItemToDigestThread(threadId, itemId, occurredAt, summary, flushAfterAt) {
|
|
1135
|
+
this.inTransaction(() => {
|
|
1136
|
+
this.db.run("insert or ignore into digest_thread_items (thread_id, item_id) values (?, ?)", [threadId, itemId]);
|
|
1137
|
+
this.db.run(`
|
|
1138
|
+
update digest_threads
|
|
1139
|
+
set summary = ?, last_item_at = ?, flush_after_at = ?, updated_at = ?
|
|
1140
|
+
where thread_id = ?
|
|
1141
|
+
`, [summary, occurredAt, flushAfterAt, (0, util_1.nowIso)(), threadId]);
|
|
1142
|
+
});
|
|
1143
|
+
this.persist();
|
|
1144
|
+
return this.getDigestThread(threadId);
|
|
1145
|
+
}
|
|
1146
|
+
listDueDigestThreads(now) {
|
|
1147
|
+
const rows = this.getAll(`
|
|
1148
|
+
select *
|
|
1149
|
+
from digest_threads
|
|
1150
|
+
where status = 'open'
|
|
1151
|
+
and flush_after_at is not null
|
|
1152
|
+
and flush_after_at <= ?
|
|
1153
|
+
order by flush_after_at asc, created_at asc, thread_id asc
|
|
1154
|
+
`, [now]);
|
|
1155
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1156
|
+
}
|
|
1157
|
+
listUnackedInboxItemsForDigestThread(threadId) {
|
|
1158
|
+
const rows = this.getAll(`
|
|
1159
|
+
select i.*
|
|
1160
|
+
from digest_thread_items t
|
|
1161
|
+
join inbox_items i on i.item_id = t.item_id
|
|
1162
|
+
where t.thread_id = ?
|
|
1163
|
+
and i.acked_at is null
|
|
1164
|
+
order by coalesce(i.inbox_sequence, i.rowid) asc
|
|
1165
|
+
`, [threadId]);
|
|
1166
|
+
return rows.map((row) => this.mapInboxItem(row));
|
|
1167
|
+
}
|
|
1168
|
+
listOpenDigestThreadsForInbox(inboxId) {
|
|
1169
|
+
const rows = this.getAll("select * from digest_threads where inbox_id = ? and status = 'open' order by created_at asc, thread_id asc", [inboxId]);
|
|
1170
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1171
|
+
}
|
|
1172
|
+
listOpenDigestThreads() {
|
|
1173
|
+
const rows = this.getAll("select * from digest_threads where status = 'open' order by created_at asc, thread_id asc");
|
|
1174
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1175
|
+
}
|
|
1176
|
+
materializeDigestSnapshot(input) {
|
|
1177
|
+
const thread = this.getDigestThread(input.threadId);
|
|
1178
|
+
if (!thread) {
|
|
1179
|
+
throw new Error(`unknown digest thread: ${input.threadId}`);
|
|
1180
|
+
}
|
|
1181
|
+
this.inTransaction(() => {
|
|
1182
|
+
if (thread.latestEntryId != null) {
|
|
1183
|
+
this.db.run("update inbox_entries set superseded_at = ? where entry_id = ? and superseded_at is null", [input.createdAt, thread.latestEntryId]);
|
|
1184
|
+
}
|
|
1185
|
+
const nextRevision = thread.latestRevision + 1;
|
|
1186
|
+
const entryId = this.insertInboxEntryRecord({
|
|
1187
|
+
inboxId: input.inboxId,
|
|
1188
|
+
kind: "digest_snapshot",
|
|
1189
|
+
threadId: input.threadId,
|
|
1190
|
+
revision: nextRevision,
|
|
1191
|
+
groupKey: input.groupKey,
|
|
1192
|
+
resourceRef: input.resourceRef ?? null,
|
|
1193
|
+
eventFamily: input.eventFamily ?? null,
|
|
1194
|
+
itemJson: null,
|
|
1195
|
+
count: input.itemIds.length,
|
|
1196
|
+
summary: input.summary,
|
|
1197
|
+
firstItemAt: input.firstItemAt,
|
|
1198
|
+
lastItemAt: input.lastItemAt,
|
|
1199
|
+
sourceIdsJson: JSON.stringify(uniqueSorted(input.sourceIds)),
|
|
1200
|
+
subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
|
|
1201
|
+
deliveryHandleJson: input.deliveryHandleJson ?? null,
|
|
1202
|
+
ackedAt: null,
|
|
1203
|
+
supersededAt: null,
|
|
1204
|
+
createdAt: input.createdAt,
|
|
1205
|
+
});
|
|
1206
|
+
for (const itemId of input.itemIds) {
|
|
1207
|
+
this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, itemId]);
|
|
1208
|
+
}
|
|
1209
|
+
this.db.run(`
|
|
1210
|
+
update digest_threads
|
|
1211
|
+
set latest_revision = ?, latest_entry_id = ?, summary = ?, first_item_at = ?, last_item_at = ?,
|
|
1212
|
+
flush_after_at = null, updated_at = ?
|
|
1213
|
+
where thread_id = ?
|
|
1214
|
+
`, [nextRevision, entryId, input.summary, input.firstItemAt, input.lastItemAt, input.createdAt, input.threadId]);
|
|
1215
|
+
});
|
|
1216
|
+
this.persist();
|
|
1217
|
+
return this.getInboxEntry(this.getDigestThread(input.threadId).latestEntryId);
|
|
1218
|
+
}
|
|
1219
|
+
closeDigestThread(threadId, updatedAt) {
|
|
1220
|
+
this.db.run("update digest_threads set status = 'closed', flush_after_at = null, updated_at = ? where thread_id = ?", [updatedAt, threadId]);
|
|
1221
|
+
this.persist();
|
|
1222
|
+
}
|
|
1223
|
+
listDigestThreadsForRawItems(itemIds) {
|
|
1224
|
+
if (itemIds.length === 0) {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
const placeholders = itemIds.map(() => "?").join(", ");
|
|
1228
|
+
const rows = this.getAll(`
|
|
1229
|
+
select distinct t.*
|
|
1230
|
+
from digest_threads t
|
|
1231
|
+
join digest_thread_items i on i.thread_id = t.thread_id
|
|
1232
|
+
where i.item_id in (${placeholders})
|
|
1233
|
+
order by t.created_at asc, t.thread_id asc
|
|
1234
|
+
`, itemIds);
|
|
1235
|
+
return rows.map((row) => this.mapDigestThread(row));
|
|
1236
|
+
}
|
|
1237
|
+
listInboxItems(inboxId, options) {
|
|
1238
|
+
const includeAcked = options?.includeAcked ?? false;
|
|
1239
|
+
const filters = ["inbox_id = ?"];
|
|
1240
|
+
const params = [inboxId];
|
|
1241
|
+
if (!includeAcked) {
|
|
1242
|
+
filters.push("acked_at is null");
|
|
807
1243
|
}
|
|
808
1244
|
const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by coalesce(inbox_sequence, rowid) asc`, params);
|
|
809
1245
|
return rows.map((row) => this.mapInboxItem(row));
|
|
@@ -842,29 +1278,31 @@ class AgentInboxStore {
|
|
|
842
1278
|
return changes;
|
|
843
1279
|
}
|
|
844
1280
|
deleteAckedInboxItems(inboxId, olderThan) {
|
|
845
|
-
this.
|
|
846
|
-
|
|
1281
|
+
const itemIds = this.getAll(`
|
|
1282
|
+
select item_id
|
|
1283
|
+
from inbox_items
|
|
847
1284
|
where inbox_id = ?
|
|
848
1285
|
and acked_at is not null
|
|
849
1286
|
and acked_at < ?
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
this.persist();
|
|
1287
|
+
`, [inboxId, olderThan]).map((row) => String(row.item_id));
|
|
1288
|
+
if (itemIds.length === 0) {
|
|
1289
|
+
return 0;
|
|
854
1290
|
}
|
|
855
|
-
|
|
1291
|
+
this.deleteInboxItemsAndEntryArtifacts(itemIds);
|
|
1292
|
+
return itemIds.length;
|
|
856
1293
|
}
|
|
857
1294
|
deleteAckedInboxItemsGlobal(olderThan) {
|
|
858
|
-
this.
|
|
859
|
-
|
|
1295
|
+
const itemIds = this.getAll(`
|
|
1296
|
+
select item_id
|
|
1297
|
+
from inbox_items
|
|
860
1298
|
where acked_at is not null
|
|
861
1299
|
and acked_at < ?
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.persist();
|
|
1300
|
+
`, [olderThan]).map((row) => String(row.item_id));
|
|
1301
|
+
if (itemIds.length === 0) {
|
|
1302
|
+
return 0;
|
|
866
1303
|
}
|
|
867
|
-
|
|
1304
|
+
this.deleteInboxItemsAndEntryArtifacts(itemIds);
|
|
1305
|
+
return itemIds.length;
|
|
868
1306
|
}
|
|
869
1307
|
countInboxItems(inboxId, includeAcked) {
|
|
870
1308
|
const row = this.getOne(`
|
|
@@ -892,7 +1330,7 @@ class AgentInboxStore {
|
|
|
892
1330
|
JSON.stringify(activation.sourceIds),
|
|
893
1331
|
activation.newItemCount,
|
|
894
1332
|
activation.summary,
|
|
895
|
-
activation.items ? JSON.stringify(activation.items) : null,
|
|
1333
|
+
activation.entries ? JSON.stringify(activation.entries) : (activation.items ? JSON.stringify(activation.items) : null),
|
|
896
1334
|
activation.createdAt,
|
|
897
1335
|
activation.deliveredAt ?? null,
|
|
898
1336
|
]);
|
|
@@ -943,34 +1381,50 @@ class AgentInboxStore {
|
|
|
943
1381
|
return row ? this.mapStream(row) : null;
|
|
944
1382
|
}
|
|
945
1383
|
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
|
-
|
|
1384
|
+
const occurredAt = event.occurredAt ?? (0, util_1.nowIso)();
|
|
1385
|
+
const metadataJson = JSON.stringify(event.metadata ?? {});
|
|
1386
|
+
const rawPayloadJson = JSON.stringify(event.rawPayload ?? {});
|
|
1387
|
+
const deliveryHandleJson = event.deliveryHandle ? JSON.stringify(event.deliveryHandle) : null;
|
|
1388
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
1389
|
+
const before = this.changes();
|
|
1390
|
+
this.db.run(`
|
|
1391
|
+
insert or ignore into stream_events (
|
|
1392
|
+
stream_event_id, stream_id, source_id, source_native_id, event_variant,
|
|
1393
|
+
occurred_at, metadata_json, raw_payload_json, delivery_handle_json, created_at
|
|
1394
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1395
|
+
`, [
|
|
1396
|
+
(0, util_1.generateCanonicalId)("evt"),
|
|
1397
|
+
streamId,
|
|
1398
|
+
event.sourceId,
|
|
1399
|
+
event.sourceNativeId,
|
|
1400
|
+
event.eventVariant,
|
|
1401
|
+
occurredAt,
|
|
1402
|
+
metadataJson,
|
|
1403
|
+
rawPayloadJson,
|
|
1404
|
+
deliveryHandleJson,
|
|
1405
|
+
(0, util_1.nowIso)(),
|
|
1406
|
+
]);
|
|
1407
|
+
if (this.changes() > before) {
|
|
1408
|
+
this.persist();
|
|
1409
|
+
return {
|
|
1410
|
+
appended: 1,
|
|
1411
|
+
deduped: 0,
|
|
1412
|
+
lastOffset: this.lastInsertRowId(),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
const existing = this.getOne(`
|
|
1416
|
+
select offset
|
|
1417
|
+
from stream_events
|
|
1418
|
+
where stream_id = ?
|
|
1419
|
+
and source_native_id = ?
|
|
1420
|
+
and event_variant = ?
|
|
1421
|
+
limit 1
|
|
1422
|
+
`, [streamId, event.sourceNativeId, event.eventVariant]);
|
|
1423
|
+
if (existing) {
|
|
1424
|
+
return { appended: 0, deduped: 1, lastOffset: null };
|
|
1425
|
+
}
|
|
972
1426
|
}
|
|
973
|
-
|
|
1427
|
+
throw new Error(`unable to allocate a unique stream_event_id for ${event.sourceNativeId}`);
|
|
974
1428
|
}
|
|
975
1429
|
readStreamEvents(streamId, nextOffset, limit) {
|
|
976
1430
|
const rows = this.getAll(`
|
|
@@ -1058,7 +1512,7 @@ class AgentInboxStore {
|
|
|
1058
1512
|
insert into consumer_commits (
|
|
1059
1513
|
commit_id, consumer_id, stream_id, committed_offset, committed_at
|
|
1060
1514
|
) values (?, ?, ?, ?, ?)
|
|
1061
|
-
`, [(0, util_1.
|
|
1515
|
+
`, [(0, util_1.generateCanonicalId)("cmt"), consumerId, consumer.streamId, committedOffset, updatedAt]);
|
|
1062
1516
|
});
|
|
1063
1517
|
this.persist();
|
|
1064
1518
|
return this.getConsumer(consumerId);
|
|
@@ -1139,8 +1593,15 @@ class AgentInboxStore {
|
|
|
1139
1593
|
}
|
|
1140
1594
|
}
|
|
1141
1595
|
mapSource(row) {
|
|
1596
|
+
const hostId = requiredText(row, "host_id");
|
|
1597
|
+
const streamKind = requiredText(row, "stream_kind");
|
|
1598
|
+
const streamKey = requiredText(row, "stream_key");
|
|
1142
1599
|
return {
|
|
1143
1600
|
sourceId: String(row.source_id),
|
|
1601
|
+
streamId: String(row.source_id),
|
|
1602
|
+
hostId,
|
|
1603
|
+
streamKind,
|
|
1604
|
+
streamKey,
|
|
1144
1605
|
sourceType: row.source_type,
|
|
1145
1606
|
sourceKey: String(row.source_key),
|
|
1146
1607
|
configRef: row.config_ref ? String(row.config_ref) : null,
|
|
@@ -1151,6 +1612,18 @@ class AgentInboxStore {
|
|
|
1151
1612
|
updatedAt: String(row.updated_at),
|
|
1152
1613
|
};
|
|
1153
1614
|
}
|
|
1615
|
+
mapSourceHost(row) {
|
|
1616
|
+
return {
|
|
1617
|
+
hostId: String(row.host_id),
|
|
1618
|
+
hostType: String(row.host_type),
|
|
1619
|
+
hostKey: String(row.host_key),
|
|
1620
|
+
configRef: row.config_ref ? String(row.config_ref) : null,
|
|
1621
|
+
config: parseJson(row.config_json),
|
|
1622
|
+
status: String(row.status),
|
|
1623
|
+
createdAt: String(row.created_at),
|
|
1624
|
+
updatedAt: String(row.updated_at),
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1154
1627
|
mapSourceIdleState(row) {
|
|
1155
1628
|
return {
|
|
1156
1629
|
sourceId: String(row.source_id),
|
|
@@ -1176,6 +1649,10 @@ class AgentInboxStore {
|
|
|
1176
1649
|
return {
|
|
1177
1650
|
inboxId: String(row.inbox_id),
|
|
1178
1651
|
ownerAgentId: String(row.owner_agent_id),
|
|
1652
|
+
aggregationEnabled: Number(row.aggregation_enabled ?? 0) > 0,
|
|
1653
|
+
aggregationWindowMs: row.aggregation_window_ms == null ? null : Number(row.aggregation_window_ms),
|
|
1654
|
+
aggregationMaxItems: row.aggregation_max_items == null ? null : Number(row.aggregation_max_items),
|
|
1655
|
+
aggregationMaxThreadAgeMs: row.aggregation_max_thread_age_ms == null ? null : Number(row.aggregation_max_thread_age_ms),
|
|
1179
1656
|
createdAt: String(row.created_at),
|
|
1180
1657
|
};
|
|
1181
1658
|
}
|
|
@@ -1224,6 +1701,11 @@ class AgentInboxStore {
|
|
|
1224
1701
|
mode: row.mode,
|
|
1225
1702
|
url,
|
|
1226
1703
|
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1704
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1705
|
+
notificationPolicy: {
|
|
1706
|
+
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1707
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1708
|
+
},
|
|
1227
1709
|
createdAt: String(row.created_at),
|
|
1228
1710
|
updatedAt: String(row.updated_at),
|
|
1229
1711
|
lastSeenAt: String(row.last_seen_at),
|
|
@@ -1240,6 +1722,11 @@ class AgentInboxStore {
|
|
|
1240
1722
|
lastError: row.last_error ? String(row.last_error) : null,
|
|
1241
1723
|
mode: row.mode,
|
|
1242
1724
|
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1725
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1726
|
+
notificationPolicy: {
|
|
1727
|
+
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1728
|
+
minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
|
|
1729
|
+
},
|
|
1243
1730
|
runtimeKind: (row.runtime_kind ? String(row.runtime_kind) : "unknown"),
|
|
1244
1731
|
runtimeSessionId: row.runtime_session_id ? String(row.runtime_session_id) : null,
|
|
1245
1732
|
runtimePid: row.runtime_pid == null ? null : Number(row.runtime_pid),
|
|
@@ -1260,6 +1747,11 @@ class AgentInboxStore {
|
|
|
1260
1747
|
status: row.status,
|
|
1261
1748
|
leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
|
|
1262
1749
|
lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
|
|
1750
|
+
deferReason: row.defer_reason ? row.defer_reason : null,
|
|
1751
|
+
deferAttempts: Number(row.defer_attempts ?? 0),
|
|
1752
|
+
firstDeferredAt: row.first_deferred_at ? String(row.first_deferred_at) : null,
|
|
1753
|
+
lastDeferredAt: row.last_deferred_at ? String(row.last_deferred_at) : null,
|
|
1754
|
+
pendingFingerprint: row.pending_fingerprint ? String(row.pending_fingerprint) : null,
|
|
1263
1755
|
pendingNewItemCount: Number(row.pending_new_item_count),
|
|
1264
1756
|
pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
|
|
1265
1757
|
pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
|
|
@@ -1301,6 +1793,70 @@ class AgentInboxStore {
|
|
|
1301
1793
|
ackedAt: row.acked_at ? String(row.acked_at) : null,
|
|
1302
1794
|
};
|
|
1303
1795
|
}
|
|
1796
|
+
mapInboxEntry(row, itemIds) {
|
|
1797
|
+
const base = {
|
|
1798
|
+
entryId: (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
1799
|
+
inboxId: String(row.inbox_id),
|
|
1800
|
+
sequence: Number(row.sequence),
|
|
1801
|
+
itemId: "",
|
|
1802
|
+
summary: String(row.summary),
|
|
1803
|
+
count: Number(row.count),
|
|
1804
|
+
itemIds,
|
|
1805
|
+
sourceIds: parseJson(row.source_ids_json),
|
|
1806
|
+
subscriptionIds: parseJson(row.subscription_ids_json),
|
|
1807
|
+
firstItemAt: String(row.first_item_at),
|
|
1808
|
+
lastItemAt: String(row.last_item_at),
|
|
1809
|
+
deliveryHandle: row.delivery_handle_json
|
|
1810
|
+
? parseJson(row.delivery_handle_json)
|
|
1811
|
+
: null,
|
|
1812
|
+
ackedAt: row.acked_at ? String(row.acked_at) : null,
|
|
1813
|
+
supersededAt: row.superseded_at ? String(row.superseded_at) : null,
|
|
1814
|
+
};
|
|
1815
|
+
if (String(row.kind) === "item") {
|
|
1816
|
+
const item = parseJson(row.item_json);
|
|
1817
|
+
return {
|
|
1818
|
+
...base,
|
|
1819
|
+
kind: "item",
|
|
1820
|
+
itemId: item.itemId,
|
|
1821
|
+
sourceId: item.sourceId,
|
|
1822
|
+
sourceNativeId: item.sourceNativeId,
|
|
1823
|
+
eventVariant: item.eventVariant,
|
|
1824
|
+
occurredAt: item.occurredAt,
|
|
1825
|
+
metadata: item.metadata,
|
|
1826
|
+
rawPayload: item.rawPayload,
|
|
1827
|
+
item,
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
...base,
|
|
1832
|
+
kind: "digest_snapshot",
|
|
1833
|
+
itemId: base.itemIds[0] ?? (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
1834
|
+
threadId: (0, util_1.formatThreadRef)(requiredText(row, "thread_id")),
|
|
1835
|
+
revision: Number(row.revision),
|
|
1836
|
+
groupKey: String(row.group_key),
|
|
1837
|
+
resourceRef: row.resource_ref ? String(row.resource_ref) : null,
|
|
1838
|
+
eventFamily: row.event_family ? String(row.event_family) : null,
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
mapDigestThread(row) {
|
|
1842
|
+
return {
|
|
1843
|
+
threadId: requiredText(row, "thread_id"),
|
|
1844
|
+
inboxId: String(row.inbox_id),
|
|
1845
|
+
sourceId: String(row.source_id),
|
|
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
|
+
latestRevision: Number(row.latest_revision),
|
|
1850
|
+
latestEntryId: row.latest_entry_id == null ? null : String(row.latest_entry_id),
|
|
1851
|
+
status: String(row.status),
|
|
1852
|
+
summary: String(row.summary),
|
|
1853
|
+
firstItemAt: String(row.first_item_at),
|
|
1854
|
+
lastItemAt: String(row.last_item_at),
|
|
1855
|
+
flushAfterAt: row.flush_after_at ? String(row.flush_after_at) : null,
|
|
1856
|
+
createdAt: String(row.created_at),
|
|
1857
|
+
updatedAt: String(row.updated_at),
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1304
1860
|
mapActivation(row) {
|
|
1305
1861
|
return {
|
|
1306
1862
|
kind: "agentinbox.activation",
|
|
@@ -1311,9 +1867,10 @@ class AgentInboxStore {
|
|
|
1311
1867
|
targetKind: row.target_kind,
|
|
1312
1868
|
subscriptionIds: parseJson(row.subscription_ids_json),
|
|
1313
1869
|
sourceIds: parseJson(row.source_ids_json),
|
|
1870
|
+
newEntryCount: row.items_json ? parseJson(row.items_json)?.length ?? 0 : Number(row.new_item_count),
|
|
1314
1871
|
newItemCount: Number(row.new_item_count),
|
|
1315
1872
|
summary: String(row.summary),
|
|
1316
|
-
|
|
1873
|
+
entries: row.items_json ? parseJson(row.items_json) : undefined,
|
|
1317
1874
|
createdAt: String(row.created_at),
|
|
1318
1875
|
deliveredAt: row.delivered_at ? String(row.delivered_at) : null,
|
|
1319
1876
|
};
|
|
@@ -1332,6 +1889,191 @@ class AgentInboxStore {
|
|
|
1332
1889
|
createdAt: String(row.created_at),
|
|
1333
1890
|
};
|
|
1334
1891
|
}
|
|
1892
|
+
resolveInboxEntryId(inboxId, refOrItemId) {
|
|
1893
|
+
try {
|
|
1894
|
+
return (0, util_1.parseEntryRef)(refOrItemId);
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
const row = this.getOne(`
|
|
1898
|
+
select e.entry_id
|
|
1899
|
+
from inbox_entries e
|
|
1900
|
+
join inbox_entry_items i on i.entry_id = e.entry_id
|
|
1901
|
+
where e.inbox_id = ?
|
|
1902
|
+
and i.item_id = ?
|
|
1903
|
+
order by e.sequence desc
|
|
1904
|
+
limit 1
|
|
1905
|
+
`, [inboxId, refOrItemId]);
|
|
1906
|
+
if (!row) {
|
|
1907
|
+
throw new Error(`unknown inbox entry: ${refOrItemId}`);
|
|
1908
|
+
}
|
|
1909
|
+
return String(row.entry_id);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
insertInboxEntryRecord(input) {
|
|
1913
|
+
const entryId = (0, util_1.generateCanonicalId)("ent");
|
|
1914
|
+
this.db.run(`
|
|
1915
|
+
insert into inbox_entries (
|
|
1916
|
+
entry_id, inbox_id, kind, sequence, thread_id, revision, group_key, resource_ref, event_family,
|
|
1917
|
+
item_json, count, summary, first_item_at, last_item_at, source_ids_json,
|
|
1918
|
+
subscription_ids_json, delivery_handle_json, acked_at, superseded_at, created_at
|
|
1919
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1920
|
+
`, [
|
|
1921
|
+
entryId,
|
|
1922
|
+
input.inboxId,
|
|
1923
|
+
input.kind,
|
|
1924
|
+
this.nextInboxEntrySequence(),
|
|
1925
|
+
input.threadId,
|
|
1926
|
+
input.revision,
|
|
1927
|
+
input.groupKey,
|
|
1928
|
+
input.resourceRef,
|
|
1929
|
+
input.eventFamily,
|
|
1930
|
+
input.itemJson,
|
|
1931
|
+
input.count,
|
|
1932
|
+
input.summary,
|
|
1933
|
+
input.firstItemAt,
|
|
1934
|
+
input.lastItemAt,
|
|
1935
|
+
input.sourceIdsJson,
|
|
1936
|
+
input.subscriptionIdsJson,
|
|
1937
|
+
input.deliveryHandleJson,
|
|
1938
|
+
input.ackedAt,
|
|
1939
|
+
input.supersededAt,
|
|
1940
|
+
input.createdAt,
|
|
1941
|
+
]);
|
|
1942
|
+
return entryId;
|
|
1943
|
+
}
|
|
1944
|
+
nextInboxEntrySequence() {
|
|
1945
|
+
const row = this.getOne("select coalesce(max(sequence), 0) + 1 as next_sequence from inbox_entries");
|
|
1946
|
+
return Number(row?.next_sequence ?? 1);
|
|
1947
|
+
}
|
|
1948
|
+
listEntryItemIds(entryId) {
|
|
1949
|
+
const rows = this.getAll(`
|
|
1950
|
+
select iei.item_id
|
|
1951
|
+
from inbox_entry_items iei
|
|
1952
|
+
left join inbox_items ii on ii.item_id = iei.item_id
|
|
1953
|
+
where iei.entry_id = ?
|
|
1954
|
+
order by coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
|
|
1955
|
+
`, [entryId]);
|
|
1956
|
+
return rows.map((row) => String(row.item_id));
|
|
1957
|
+
}
|
|
1958
|
+
listEntryItemIdsBulk(entryIds) {
|
|
1959
|
+
const result = new Map();
|
|
1960
|
+
if (entryIds.length === 0) {
|
|
1961
|
+
return result;
|
|
1962
|
+
}
|
|
1963
|
+
const placeholders = entryIds.map(() => "?").join(", ");
|
|
1964
|
+
const rows = this.getAll(`
|
|
1965
|
+
select iei.entry_id, iei.item_id
|
|
1966
|
+
from inbox_entry_items iei
|
|
1967
|
+
left join inbox_items ii on ii.item_id = iei.item_id
|
|
1968
|
+
where iei.entry_id in (${placeholders})
|
|
1969
|
+
order by iei.entry_id asc, coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
|
|
1970
|
+
`, entryIds);
|
|
1971
|
+
for (const row of rows) {
|
|
1972
|
+
const entryId = String(row.entry_id);
|
|
1973
|
+
const itemIds = result.get(entryId) ?? [];
|
|
1974
|
+
itemIds.push(String(row.item_id));
|
|
1975
|
+
result.set(entryId, itemIds);
|
|
1976
|
+
}
|
|
1977
|
+
return result;
|
|
1978
|
+
}
|
|
1979
|
+
getRawItemIdsForEntryIds(entryIds) {
|
|
1980
|
+
const itemIds = new Set();
|
|
1981
|
+
for (const entryId of entryIds) {
|
|
1982
|
+
for (const itemId of this.listEntryItemIds(entryId)) {
|
|
1983
|
+
itemIds.add(itemId);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return [...itemIds];
|
|
1987
|
+
}
|
|
1988
|
+
mapInboxEntries(rows) {
|
|
1989
|
+
const itemIdsByEntry = this.listEntryItemIdsBulk(rows.map((row) => String(row.entry_id)));
|
|
1990
|
+
return rows.map((row) => this.mapInboxEntry(row, itemIdsByEntry.get(String(row.entry_id)) ?? []));
|
|
1991
|
+
}
|
|
1992
|
+
syncInboxEntryAckState(inboxId, ackedAt) {
|
|
1993
|
+
this.db.run(`
|
|
1994
|
+
update inbox_entries
|
|
1995
|
+
set acked_at = ?
|
|
1996
|
+
where inbox_id = ?
|
|
1997
|
+
and acked_at is null
|
|
1998
|
+
and not exists (
|
|
1999
|
+
select 1
|
|
2000
|
+
from inbox_entry_items e
|
|
2001
|
+
join inbox_items i on i.item_id = e.item_id
|
|
2002
|
+
where e.entry_id = inbox_entries.entry_id
|
|
2003
|
+
and i.acked_at is null
|
|
2004
|
+
)
|
|
2005
|
+
`, [ackedAt, inboxId]);
|
|
2006
|
+
this.persist();
|
|
2007
|
+
}
|
|
2008
|
+
countAckedEntries(entryIds, ackedAt) {
|
|
2009
|
+
if (entryIds.length === 0) {
|
|
2010
|
+
return 0;
|
|
2011
|
+
}
|
|
2012
|
+
const placeholders = entryIds.map(() => "?").join(", ");
|
|
2013
|
+
const row = this.getOne(`select count(*) as count from inbox_entries where entry_id in (${placeholders}) and acked_at = ?`, [...entryIds, ackedAt]);
|
|
2014
|
+
return Number(row?.count ?? 0);
|
|
2015
|
+
}
|
|
2016
|
+
deleteInboxItemsAndEntryArtifacts(itemIds) {
|
|
2017
|
+
const placeholders = itemIds.map(() => "?").join(", ");
|
|
2018
|
+
this.inTransaction(() => {
|
|
2019
|
+
this.db.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
|
|
2020
|
+
this.db.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
|
|
2021
|
+
this.db.run(`
|
|
2022
|
+
delete from inbox_entries
|
|
2023
|
+
where entry_id in (
|
|
2024
|
+
select e.entry_id
|
|
2025
|
+
from inbox_entries e
|
|
2026
|
+
where not exists (
|
|
2027
|
+
select 1
|
|
2028
|
+
from inbox_entry_items ee
|
|
2029
|
+
where ee.entry_id = e.entry_id
|
|
2030
|
+
and exists (
|
|
2031
|
+
select 1
|
|
2032
|
+
from inbox_items ii
|
|
2033
|
+
where ii.item_id = ee.item_id
|
|
2034
|
+
)
|
|
2035
|
+
)
|
|
2036
|
+
)
|
|
2037
|
+
`);
|
|
2038
|
+
this.db.run(`
|
|
2039
|
+
delete from inbox_entry_items
|
|
2040
|
+
where not exists (
|
|
2041
|
+
select 1
|
|
2042
|
+
from inbox_items ii
|
|
2043
|
+
where ii.item_id = inbox_entry_items.item_id
|
|
2044
|
+
)
|
|
2045
|
+
`);
|
|
2046
|
+
this.db.run(`
|
|
2047
|
+
update digest_threads
|
|
2048
|
+
set latest_entry_id = (
|
|
2049
|
+
select e.entry_id
|
|
2050
|
+
from inbox_entries e
|
|
2051
|
+
where e.thread_id = digest_threads.thread_id
|
|
2052
|
+
order by coalesce(e.revision, 0) desc, e.entry_id desc
|
|
2053
|
+
limit 1
|
|
2054
|
+
),
|
|
2055
|
+
latest_revision = coalesce((
|
|
2056
|
+
select max(coalesce(e.revision, 0))
|
|
2057
|
+
from inbox_entries e
|
|
2058
|
+
where e.thread_id = digest_threads.thread_id
|
|
2059
|
+
), 0)
|
|
2060
|
+
`);
|
|
2061
|
+
this.db.run(`
|
|
2062
|
+
delete from digest_threads
|
|
2063
|
+
where not exists (
|
|
2064
|
+
select 1
|
|
2065
|
+
from digest_thread_items dti
|
|
2066
|
+
where dti.thread_id = digest_threads.thread_id
|
|
2067
|
+
)
|
|
2068
|
+
and not exists (
|
|
2069
|
+
select 1
|
|
2070
|
+
from inbox_entries e
|
|
2071
|
+
where e.thread_id = digest_threads.thread_id
|
|
2072
|
+
)
|
|
2073
|
+
`);
|
|
2074
|
+
});
|
|
2075
|
+
this.persist();
|
|
2076
|
+
}
|
|
1335
2077
|
mapStream(row) {
|
|
1336
2078
|
return {
|
|
1337
2079
|
streamId: String(row.stream_id),
|
|
@@ -1374,3 +2116,16 @@ class AgentInboxStore {
|
|
|
1374
2116
|
}
|
|
1375
2117
|
}
|
|
1376
2118
|
exports.AgentInboxStore = AgentInboxStore;
|
|
2119
|
+
function requiredText(row, key) {
|
|
2120
|
+
const value = row[key];
|
|
2121
|
+
if (value == null) {
|
|
2122
|
+
throw new Error(`missing required column: ${key}`);
|
|
2123
|
+
}
|
|
2124
|
+
return String(value);
|
|
2125
|
+
}
|
|
2126
|
+
function uniqueSorted(values) {
|
|
2127
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))].sort();
|
|
2128
|
+
}
|
|
2129
|
+
function summarizeBackfilledItemEntry(item) {
|
|
2130
|
+
return `${item.eventVariant} from ${item.sourceId}`;
|
|
2131
|
+
}
|