@holon-run/agentinbox 1.7.1 → 1.8.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 +22 -0
- package/dist/src/cli.js +150 -8
- package/dist/src/client.js +52 -0
- package/dist/src/email_attachment.js +199 -0
- package/dist/src/email_attachment_content.js +759 -0
- package/dist/src/email_body.js +392 -0
- package/dist/src/http.js +147 -3
- package/dist/src/service.js +61 -3
- package/dist/src/source_schema.js +2 -1
- package/dist/src/sources/email.js +49 -3
- package/dist/src/sources/remote_modules.js +3 -1
- package/dist/src/sources/telegram.js +2 -1
- package/dist/src/store.js +451 -1
- package/drizzle/migrations/0006_email_body_cache.sql +19 -0
- package/drizzle/migrations/0007_email_attachment_materializations.sql +20 -0
- package/drizzle/migrations/0008_email_attachment_audit.sql +17 -0
- package/drizzle/schema.ts +62 -0
- package/package.json +2 -2
- package/skills/agentinbox/SKILL.md +20 -0
package/dist/src/store.js
CHANGED
|
@@ -166,6 +166,9 @@ class AgentInboxStore {
|
|
|
166
166
|
save() {
|
|
167
167
|
this.persist();
|
|
168
168
|
}
|
|
169
|
+
getDataDirectory() {
|
|
170
|
+
return node_path_1.default.dirname(this.dbPath);
|
|
171
|
+
}
|
|
169
172
|
getDatabaseHealth() {
|
|
170
173
|
return {
|
|
171
174
|
quickCheck: String(this.db.pragma("quick_check", { simple: true })),
|
|
@@ -347,8 +350,9 @@ class AgentInboxStore {
|
|
|
347
350
|
inTransaction(fn) {
|
|
348
351
|
this.db.exec("begin");
|
|
349
352
|
try {
|
|
350
|
-
fn();
|
|
353
|
+
const result = fn();
|
|
351
354
|
this.db.exec("commit");
|
|
355
|
+
return result;
|
|
352
356
|
}
|
|
353
357
|
catch (error) {
|
|
354
358
|
this.db.exec("rollback");
|
|
@@ -1040,6 +1044,9 @@ class AgentInboxStore {
|
|
|
1040
1044
|
this.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
|
|
1041
1045
|
this.run("delete from activation_targets where agent_id = ?", [agentId]);
|
|
1042
1046
|
if (inbox) {
|
|
1047
|
+
this.run("delete from email_body_cache where inbox_id = ?", [inbox.inboxId]);
|
|
1048
|
+
const itemIds = this.getAll("select item_id from inbox_items where inbox_id = ?", [inbox.inboxId]).map((row) => requiredText(row, "item_id"));
|
|
1049
|
+
this.auditAndDeleteEmailAttachmentMaterializations(itemIds, "attachment_parent_deleted");
|
|
1043
1050
|
this.run("delete from inbox_items where inbox_id = ?", [inbox.inboxId]);
|
|
1044
1051
|
this.run("delete from activations where agent_id = ? or inbox_id = ?", [agentId, inbox.inboxId]);
|
|
1045
1052
|
this.run("delete from inboxes where inbox_id = ?", [inbox.inboxId]);
|
|
@@ -1295,6 +1302,308 @@ class AgentInboxStore {
|
|
|
1295
1302
|
const rows = this.getAll("select * from inbox_entries where entry_id = ?", [(0, util_1.parseEntryRef)(entryId)]);
|
|
1296
1303
|
return this.mapInboxEntries(rows)[0] ?? null;
|
|
1297
1304
|
}
|
|
1305
|
+
getInboxEntryForInbox(inboxId, entryId) {
|
|
1306
|
+
const rows = this.getAll("select * from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, (0, util_1.parseEntryRef)(entryId)]);
|
|
1307
|
+
return this.mapInboxEntries(rows)[0] ?? null;
|
|
1308
|
+
}
|
|
1309
|
+
getInboxItemForInbox(inboxId, itemId) {
|
|
1310
|
+
const row = this.getOne("select * from inbox_items where inbox_id = ? and item_id = ?", [inboxId, itemId]);
|
|
1311
|
+
return row ? this.mapInboxItem(row) : null;
|
|
1312
|
+
}
|
|
1313
|
+
getEmailBodyCache(inboxId, entryId) {
|
|
1314
|
+
const row = this.getOne("select * from email_body_cache where inbox_id = ? and entry_id = ?", [inboxId, (0, util_1.parseEntryRef)(entryId)]);
|
|
1315
|
+
return row ? this.mapEmailBodyCache(row) : null;
|
|
1316
|
+
}
|
|
1317
|
+
getEmailAttachmentMaterialization(itemId, attachmentSelector) {
|
|
1318
|
+
const row = this.getOne(`
|
|
1319
|
+
select *
|
|
1320
|
+
from email_attachment_materializations
|
|
1321
|
+
where item_id = ? and attachment_selector = ?
|
|
1322
|
+
`, [itemId, attachmentSelector]);
|
|
1323
|
+
return row ? this.mapEmailAttachmentMaterialization(row) : null;
|
|
1324
|
+
}
|
|
1325
|
+
appendEmailAttachmentAudit(event) {
|
|
1326
|
+
this.insertEmailAttachmentAudit(event);
|
|
1327
|
+
this.persist();
|
|
1328
|
+
}
|
|
1329
|
+
listEmailAttachmentAuditEvents(attachmentRef) {
|
|
1330
|
+
const rows = attachmentRef
|
|
1331
|
+
? this.getAll("select * from email_attachment_audit_events where attachment_ref = ? order by created_at asc, audit_id asc", [attachmentRef])
|
|
1332
|
+
: this.getAll("select * from email_attachment_audit_events order by created_at asc, audit_id asc");
|
|
1333
|
+
return rows.map((row) => this.mapEmailAttachmentAuditEvent(row));
|
|
1334
|
+
}
|
|
1335
|
+
beginEmailAttachmentMaterialization(inboxId, record) {
|
|
1336
|
+
const written = this.inTransaction(() => {
|
|
1337
|
+
const itemStillExists = this.getOne("select 1 from inbox_items where inbox_id = ? and item_id = ?", [inboxId, record.itemId]);
|
|
1338
|
+
if (!itemStillExists) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
this.run(`
|
|
1342
|
+
insert into email_attachment_materializations (
|
|
1343
|
+
item_id, attachment_selector, status, last_error_code, object_key,
|
|
1344
|
+
sha256, declared_content_type, detected_content_type, declared_size,
|
|
1345
|
+
stored_size, created_at, updated_at, expires_at, deleted_at
|
|
1346
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1347
|
+
on conflict(item_id, attachment_selector) do update set
|
|
1348
|
+
status = excluded.status,
|
|
1349
|
+
last_error_code = excluded.last_error_code,
|
|
1350
|
+
object_key = excluded.object_key,
|
|
1351
|
+
sha256 = excluded.sha256,
|
|
1352
|
+
declared_content_type = excluded.declared_content_type,
|
|
1353
|
+
detected_content_type = excluded.detected_content_type,
|
|
1354
|
+
declared_size = excluded.declared_size,
|
|
1355
|
+
stored_size = excluded.stored_size,
|
|
1356
|
+
updated_at = excluded.updated_at,
|
|
1357
|
+
expires_at = excluded.expires_at,
|
|
1358
|
+
deleted_at = excluded.deleted_at
|
|
1359
|
+
`, materializationParams(record));
|
|
1360
|
+
return true;
|
|
1361
|
+
});
|
|
1362
|
+
if (written) {
|
|
1363
|
+
this.persist();
|
|
1364
|
+
}
|
|
1365
|
+
return written;
|
|
1366
|
+
}
|
|
1367
|
+
finishEmailAttachmentMaterialization(inboxId, record) {
|
|
1368
|
+
const written = this.inTransaction(() => {
|
|
1369
|
+
const itemStillExists = this.getOne("select 1 from inbox_items where inbox_id = ? and item_id = ?", [inboxId, record.itemId]);
|
|
1370
|
+
if (!itemStillExists) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
const result = this.run(`
|
|
1374
|
+
update email_attachment_materializations
|
|
1375
|
+
set status = ?, last_error_code = ?, object_key = ?, sha256 = ?,
|
|
1376
|
+
declared_content_type = ?, detected_content_type = ?,
|
|
1377
|
+
declared_size = ?, stored_size = ?, updated_at = ?, expires_at = ?,
|
|
1378
|
+
deleted_at = ?
|
|
1379
|
+
where item_id = ? and attachment_selector = ?
|
|
1380
|
+
`, [
|
|
1381
|
+
record.status,
|
|
1382
|
+
record.lastErrorCode,
|
|
1383
|
+
record.objectKey,
|
|
1384
|
+
record.sha256,
|
|
1385
|
+
record.declaredContentType,
|
|
1386
|
+
record.detectedContentType,
|
|
1387
|
+
record.declaredSize,
|
|
1388
|
+
record.storedSize,
|
|
1389
|
+
record.updatedAt,
|
|
1390
|
+
record.expiresAt,
|
|
1391
|
+
record.deletedAt,
|
|
1392
|
+
record.itemId,
|
|
1393
|
+
record.attachmentSelector,
|
|
1394
|
+
]);
|
|
1395
|
+
return result.changes > 0;
|
|
1396
|
+
});
|
|
1397
|
+
if (written) {
|
|
1398
|
+
this.persist();
|
|
1399
|
+
}
|
|
1400
|
+
return written;
|
|
1401
|
+
}
|
|
1402
|
+
tombstoneEmailAttachmentMaterialization(inboxId, itemId, attachmentSelector, deletedAt, audit, fallback) {
|
|
1403
|
+
const tombstone = this.inTransaction(() => {
|
|
1404
|
+
const itemStillExists = this.getOne("select 1 from inbox_items where inbox_id = ? and item_id = ?", [inboxId, itemId]);
|
|
1405
|
+
if (!itemStillExists) {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
const row = this.getOne(`
|
|
1409
|
+
select *
|
|
1410
|
+
from email_attachment_materializations
|
|
1411
|
+
where item_id = ? and attachment_selector = ?
|
|
1412
|
+
`, [itemId, attachmentSelector]);
|
|
1413
|
+
if (row?.status === "deleted") {
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
const current = row ? this.mapEmailAttachmentMaterialization(row) : fallback ?? null;
|
|
1417
|
+
if (!current) {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
if (row) {
|
|
1421
|
+
this.run(`
|
|
1422
|
+
update email_attachment_materializations
|
|
1423
|
+
set status = 'deleted', last_error_code = ?, object_key = null,
|
|
1424
|
+
updated_at = ?, expires_at = null, deleted_at = ?
|
|
1425
|
+
where item_id = ? and attachment_selector = ?
|
|
1426
|
+
`, [audit.errorCode, deletedAt, deletedAt, itemId, attachmentSelector]);
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
this.run(`
|
|
1430
|
+
insert into email_attachment_materializations (
|
|
1431
|
+
item_id, attachment_selector, status, last_error_code, object_key,
|
|
1432
|
+
sha256, declared_content_type, detected_content_type, declared_size,
|
|
1433
|
+
stored_size, created_at, updated_at, expires_at, deleted_at
|
|
1434
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1435
|
+
`, materializationParams({
|
|
1436
|
+
...current,
|
|
1437
|
+
status: "deleted",
|
|
1438
|
+
lastErrorCode: audit.errorCode,
|
|
1439
|
+
objectKey: null,
|
|
1440
|
+
updatedAt: deletedAt,
|
|
1441
|
+
expiresAt: null,
|
|
1442
|
+
deletedAt,
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
this.insertEmailAttachmentAudit(audit);
|
|
1446
|
+
return current;
|
|
1447
|
+
});
|
|
1448
|
+
if (tombstone) {
|
|
1449
|
+
this.persist();
|
|
1450
|
+
}
|
|
1451
|
+
return tombstone;
|
|
1452
|
+
}
|
|
1453
|
+
listExpiredEmailAttachmentMaterializations(cutoffIso, limit) {
|
|
1454
|
+
return this.getAll(`
|
|
1455
|
+
select m.*, i.inbox_id, i.source_id, b.owner_agent_id
|
|
1456
|
+
from email_attachment_materializations m
|
|
1457
|
+
join inbox_items i on i.item_id = m.item_id
|
|
1458
|
+
join inboxes b on b.inbox_id = i.inbox_id
|
|
1459
|
+
where m.status in ('available', 'quarantined')
|
|
1460
|
+
and m.expires_at is not null
|
|
1461
|
+
and m.expires_at <= ?
|
|
1462
|
+
order by m.expires_at asc, m.updated_at asc
|
|
1463
|
+
limit ?
|
|
1464
|
+
`, [cutoffIso, limit]).map((row) => ({
|
|
1465
|
+
materialization: this.mapEmailAttachmentMaterialization(row),
|
|
1466
|
+
inboxId: requiredText(row, "inbox_id"),
|
|
1467
|
+
agentId: requiredText(row, "owner_agent_id"),
|
|
1468
|
+
sourceId: requiredText(row, "source_id"),
|
|
1469
|
+
}));
|
|
1470
|
+
}
|
|
1471
|
+
listEmailAttachmentMaterializationsForObject(objectKey) {
|
|
1472
|
+
return this.getAll(`
|
|
1473
|
+
select m.*, i.inbox_id, i.source_id, b.owner_agent_id
|
|
1474
|
+
from email_attachment_materializations m
|
|
1475
|
+
join inbox_items i on i.item_id = m.item_id
|
|
1476
|
+
join inboxes b on b.inbox_id = i.inbox_id
|
|
1477
|
+
where m.object_key = ? and m.status in ('available', 'quarantined')
|
|
1478
|
+
order by m.updated_at asc
|
|
1479
|
+
`, [objectKey]).map((row) => ({
|
|
1480
|
+
materialization: this.mapEmailAttachmentMaterialization(row),
|
|
1481
|
+
inboxId: requiredText(row, "inbox_id"),
|
|
1482
|
+
agentId: requiredText(row, "owner_agent_id"),
|
|
1483
|
+
sourceId: requiredText(row, "source_id"),
|
|
1484
|
+
}));
|
|
1485
|
+
}
|
|
1486
|
+
listOldestEmailAttachmentObjects(limit) {
|
|
1487
|
+
return this.getAll(`
|
|
1488
|
+
select object_key, max(stored_size) as stored_size
|
|
1489
|
+
from email_attachment_materializations
|
|
1490
|
+
where status in ('available', 'quarantined')
|
|
1491
|
+
and object_key is not null
|
|
1492
|
+
and stored_size is not null
|
|
1493
|
+
group by object_key
|
|
1494
|
+
order by min(updated_at) asc, object_key asc
|
|
1495
|
+
limit ?
|
|
1496
|
+
`, [limit]).map((row) => ({
|
|
1497
|
+
objectKey: requiredText(row, "object_key"),
|
|
1498
|
+
storedSize: Number(row.stored_size),
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1501
|
+
sumManagedEmailAttachmentBytes() {
|
|
1502
|
+
const row = this.getOne(`
|
|
1503
|
+
select coalesce(sum(stored_size), 0) as total
|
|
1504
|
+
from (
|
|
1505
|
+
select max(stored_size) as stored_size
|
|
1506
|
+
from email_attachment_materializations
|
|
1507
|
+
where status in ('available', 'quarantined')
|
|
1508
|
+
and object_key is not null
|
|
1509
|
+
and stored_size is not null
|
|
1510
|
+
group by object_key
|
|
1511
|
+
)
|
|
1512
|
+
`);
|
|
1513
|
+
return Number(row?.total ?? 0);
|
|
1514
|
+
}
|
|
1515
|
+
countEmailAttachmentObjectReferences(objectKey) {
|
|
1516
|
+
const row = this.getOne(`
|
|
1517
|
+
select count(*) as count
|
|
1518
|
+
from email_attachment_materializations
|
|
1519
|
+
where object_key = ? and status in ('available', 'quarantined')
|
|
1520
|
+
`, [objectKey]);
|
|
1521
|
+
return Number(row?.count ?? 0);
|
|
1522
|
+
}
|
|
1523
|
+
listReferencedEmailAttachmentObjectKeys() {
|
|
1524
|
+
return this.getAll(`
|
|
1525
|
+
select distinct object_key
|
|
1526
|
+
from email_attachment_materializations
|
|
1527
|
+
where object_key is not null and status in ('available', 'quarantined')
|
|
1528
|
+
`).map((row) => requiredText(row, "object_key"));
|
|
1529
|
+
}
|
|
1530
|
+
sumAvailableEmailAttachmentBytes(itemId, excludeAttachmentSelector) {
|
|
1531
|
+
const row = this.getOne(`
|
|
1532
|
+
select coalesce(sum(stored_size), 0) as total
|
|
1533
|
+
from email_attachment_materializations
|
|
1534
|
+
where item_id = ? and status = 'available'
|
|
1535
|
+
${excludeAttachmentSelector ? "and attachment_selector <> ?" : ""}
|
|
1536
|
+
`, excludeAttachmentSelector ? [itemId, excludeAttachmentSelector] : [itemId]);
|
|
1537
|
+
return Number(row?.total ?? 0);
|
|
1538
|
+
}
|
|
1539
|
+
putEmailBodyCache(record, maxTotalBytes) {
|
|
1540
|
+
this.inTransaction(() => {
|
|
1541
|
+
const entryStillExists = this.getOne(`
|
|
1542
|
+
select 1
|
|
1543
|
+
from inbox_entries e
|
|
1544
|
+
join inbox_entry_items ei on ei.entry_id = e.entry_id
|
|
1545
|
+
join inbox_items i on i.item_id = ei.item_id
|
|
1546
|
+
where e.entry_id = ?
|
|
1547
|
+
and e.inbox_id = ?
|
|
1548
|
+
and ei.item_id = ?
|
|
1549
|
+
and i.source_id = ?
|
|
1550
|
+
limit 1
|
|
1551
|
+
`, [(0, util_1.parseEntryRef)(record.entryId), record.inboxId, record.itemId, record.sourceId]);
|
|
1552
|
+
if (!entryStillExists) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
this.run(`
|
|
1556
|
+
insert into email_body_cache (
|
|
1557
|
+
entry_id, inbox_id, item_id, source_id, content_version, schema_version,
|
|
1558
|
+
parser_version, text, bytes, completeness, reasons_json, created_at,
|
|
1559
|
+
expires_at, last_accessed_at
|
|
1560
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1561
|
+
on conflict(entry_id) do update set
|
|
1562
|
+
inbox_id = excluded.inbox_id,
|
|
1563
|
+
item_id = excluded.item_id,
|
|
1564
|
+
source_id = excluded.source_id,
|
|
1565
|
+
content_version = excluded.content_version,
|
|
1566
|
+
schema_version = excluded.schema_version,
|
|
1567
|
+
parser_version = excluded.parser_version,
|
|
1568
|
+
text = excluded.text,
|
|
1569
|
+
bytes = excluded.bytes,
|
|
1570
|
+
completeness = excluded.completeness,
|
|
1571
|
+
reasons_json = excluded.reasons_json,
|
|
1572
|
+
created_at = excluded.created_at,
|
|
1573
|
+
expires_at = excluded.expires_at,
|
|
1574
|
+
last_accessed_at = excluded.last_accessed_at
|
|
1575
|
+
`, [
|
|
1576
|
+
(0, util_1.parseEntryRef)(record.entryId),
|
|
1577
|
+
record.inboxId,
|
|
1578
|
+
record.itemId,
|
|
1579
|
+
record.sourceId,
|
|
1580
|
+
record.contentVersion,
|
|
1581
|
+
record.schemaVersion,
|
|
1582
|
+
record.parserVersion,
|
|
1583
|
+
record.text,
|
|
1584
|
+
record.bytes,
|
|
1585
|
+
record.completeness,
|
|
1586
|
+
JSON.stringify(record.reasons),
|
|
1587
|
+
record.createdAt,
|
|
1588
|
+
record.expiresAt,
|
|
1589
|
+
record.lastAccessedAt,
|
|
1590
|
+
]);
|
|
1591
|
+
this.deleteExpiredEmailBodyCache(record.lastAccessedAt, false);
|
|
1592
|
+
this.evictEmailBodyCacheToLimit(maxTotalBytes);
|
|
1593
|
+
});
|
|
1594
|
+
this.persist();
|
|
1595
|
+
}
|
|
1596
|
+
touchEmailBodyCache(entryId, accessedAt) {
|
|
1597
|
+
this.run("update email_body_cache set last_accessed_at = ? where entry_id = ?", [accessedAt, (0, util_1.parseEntryRef)(entryId)]);
|
|
1598
|
+
this.persist();
|
|
1599
|
+
}
|
|
1600
|
+
deleteExpiredEmailBodyCache(now, persist = true) {
|
|
1601
|
+
const result = this.run("delete from email_body_cache where expires_at <= ?", [now]);
|
|
1602
|
+
if (persist && result.changes > 0) {
|
|
1603
|
+
this.persist();
|
|
1604
|
+
}
|
|
1605
|
+
return result.changes;
|
|
1606
|
+
}
|
|
1298
1607
|
ackInboxEntries(inboxId, entryIds, ackedAt) {
|
|
1299
1608
|
const resolvedIds = uniqueSorted(entryIds).map((entryId) => this.resolveInboxEntryId(inboxId, entryId));
|
|
1300
1609
|
const itemIds = this.getRawItemIdsForEntryIds(resolvedIds);
|
|
@@ -2094,6 +2403,79 @@ class AgentInboxStore {
|
|
|
2094
2403
|
ackedAt: row.acked_at ? String(row.acked_at) : null,
|
|
2095
2404
|
};
|
|
2096
2405
|
}
|
|
2406
|
+
mapEmailBodyCache(row) {
|
|
2407
|
+
return {
|
|
2408
|
+
entryId: (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
2409
|
+
inboxId: requiredText(row, "inbox_id"),
|
|
2410
|
+
itemId: requiredText(row, "item_id"),
|
|
2411
|
+
sourceId: requiredText(row, "source_id"),
|
|
2412
|
+
contentVersion: requiredText(row, "content_version"),
|
|
2413
|
+
schemaVersion: Number(row.schema_version),
|
|
2414
|
+
parserVersion: requiredText(row, "parser_version"),
|
|
2415
|
+
text: requiredText(row, "text"),
|
|
2416
|
+
bytes: Number(row.bytes),
|
|
2417
|
+
completeness: requiredText(row, "completeness"),
|
|
2418
|
+
reasons: parseJson(row.reasons_json),
|
|
2419
|
+
createdAt: requiredText(row, "created_at"),
|
|
2420
|
+
expiresAt: requiredText(row, "expires_at"),
|
|
2421
|
+
lastAccessedAt: requiredText(row, "last_accessed_at"),
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
mapEmailAttachmentMaterialization(row) {
|
|
2425
|
+
return {
|
|
2426
|
+
itemId: requiredText(row, "item_id"),
|
|
2427
|
+
attachmentSelector: requiredText(row, "attachment_selector"),
|
|
2428
|
+
status: requiredText(row, "status"),
|
|
2429
|
+
lastErrorCode: row.last_error_code ? String(row.last_error_code) : null,
|
|
2430
|
+
objectKey: row.object_key ? String(row.object_key) : null,
|
|
2431
|
+
sha256: row.sha256 ? String(row.sha256) : null,
|
|
2432
|
+
declaredContentType: row.declared_content_type ? String(row.declared_content_type) : null,
|
|
2433
|
+
detectedContentType: row.detected_content_type ? String(row.detected_content_type) : null,
|
|
2434
|
+
declaredSize: row.declared_size == null ? null : Number(row.declared_size),
|
|
2435
|
+
storedSize: row.stored_size == null ? null : Number(row.stored_size),
|
|
2436
|
+
createdAt: requiredText(row, "created_at"),
|
|
2437
|
+
updatedAt: requiredText(row, "updated_at"),
|
|
2438
|
+
expiresAt: row.expires_at ? String(row.expires_at) : null,
|
|
2439
|
+
deletedAt: row.deleted_at ? String(row.deleted_at) : null,
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
mapEmailAttachmentAuditEvent(row) {
|
|
2443
|
+
return {
|
|
2444
|
+
auditId: requiredText(row, "audit_id"),
|
|
2445
|
+
attachmentRef: requiredText(row, "attachment_ref"),
|
|
2446
|
+
claimedAgentId: requiredText(row, "claimed_agent_id"),
|
|
2447
|
+
inboxId: row.inbox_id ? String(row.inbox_id) : null,
|
|
2448
|
+
itemId: row.item_id ? String(row.item_id) : null,
|
|
2449
|
+
sourceId: row.source_id ? String(row.source_id) : null,
|
|
2450
|
+
action: requiredText(row, "action"),
|
|
2451
|
+
result: requiredText(row, "result"),
|
|
2452
|
+
errorCode: row.error_code ? String(row.error_code) : null,
|
|
2453
|
+
bytes: row.bytes == null ? null : Number(row.bytes),
|
|
2454
|
+
sha256: row.sha256 ? String(row.sha256) : null,
|
|
2455
|
+
createdAt: requiredText(row, "created_at"),
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
insertEmailAttachmentAudit(event) {
|
|
2459
|
+
this.run(`
|
|
2460
|
+
insert into email_attachment_audit_events (
|
|
2461
|
+
audit_id, attachment_ref, claimed_agent_id, inbox_id, item_id, source_id,
|
|
2462
|
+
action, result, error_code, bytes, sha256, created_at
|
|
2463
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2464
|
+
`, [
|
|
2465
|
+
event.auditId,
|
|
2466
|
+
event.attachmentRef,
|
|
2467
|
+
event.claimedAgentId,
|
|
2468
|
+
event.inboxId,
|
|
2469
|
+
event.itemId,
|
|
2470
|
+
event.sourceId,
|
|
2471
|
+
event.action,
|
|
2472
|
+
event.result,
|
|
2473
|
+
event.errorCode,
|
|
2474
|
+
event.bytes,
|
|
2475
|
+
event.sha256,
|
|
2476
|
+
event.createdAt,
|
|
2477
|
+
]);
|
|
2478
|
+
}
|
|
2097
2479
|
mapInboxEntry(row, itemIds) {
|
|
2098
2480
|
const base = {
|
|
2099
2481
|
entryId: (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
|
|
@@ -2323,6 +2705,20 @@ class AgentInboxStore {
|
|
|
2323
2705
|
const itemIdsByEntry = this.listEntryItemIdsBulk(rows.map((row) => String(row.entry_id)));
|
|
2324
2706
|
return rows.map((row) => this.mapInboxEntry(row, itemIdsByEntry.get(String(row.entry_id)) ?? []));
|
|
2325
2707
|
}
|
|
2708
|
+
evictEmailBodyCacheToLimit(maxTotalBytes) {
|
|
2709
|
+
let total = Number(this.getOne("select coalesce(sum(bytes), 0) as total from email_body_cache")?.total ?? 0);
|
|
2710
|
+
if (total <= maxTotalBytes) {
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
const rows = this.getAll("select entry_id, bytes from email_body_cache order by last_accessed_at asc, created_at asc, entry_id asc");
|
|
2714
|
+
for (const row of rows) {
|
|
2715
|
+
if (total <= maxTotalBytes) {
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
this.run("delete from email_body_cache where entry_id = ?", [String(row.entry_id)]);
|
|
2719
|
+
total -= Number(row.bytes);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2326
2722
|
syncInboxEntryAckState(inboxId, ackedAt) {
|
|
2327
2723
|
this.run(`
|
|
2328
2724
|
update inbox_entries
|
|
@@ -2350,6 +2746,8 @@ class AgentInboxStore {
|
|
|
2350
2746
|
deleteInboxItemsAndEntryArtifacts(itemIds) {
|
|
2351
2747
|
const placeholders = itemIds.map(() => "?").join(", ");
|
|
2352
2748
|
this.inTransaction(() => {
|
|
2749
|
+
this.run(`delete from email_body_cache where item_id in (${placeholders})`, itemIds);
|
|
2750
|
+
this.auditAndDeleteEmailAttachmentMaterializations(itemIds, "attachment_parent_deleted");
|
|
2353
2751
|
this.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
|
|
2354
2752
|
this.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
|
|
2355
2753
|
this.run(`
|
|
@@ -2408,6 +2806,40 @@ class AgentInboxStore {
|
|
|
2408
2806
|
});
|
|
2409
2807
|
this.persist();
|
|
2410
2808
|
}
|
|
2809
|
+
auditAndDeleteEmailAttachmentMaterializations(itemIds, errorCode) {
|
|
2810
|
+
if (itemIds.length === 0) {
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
const placeholders = itemIds.map(() => "?").join(", ");
|
|
2814
|
+
const rows = this.getAll(`
|
|
2815
|
+
select m.item_id, m.attachment_selector, m.stored_size, m.sha256,
|
|
2816
|
+
i.inbox_id, i.source_id, b.owner_agent_id
|
|
2817
|
+
from email_attachment_materializations m
|
|
2818
|
+
join inbox_items i on i.item_id = m.item_id
|
|
2819
|
+
join inboxes b on b.inbox_id = i.inbox_id
|
|
2820
|
+
where m.item_id in (${placeholders})
|
|
2821
|
+
`, itemIds);
|
|
2822
|
+
const createdAt = (0, util_1.nowIso)();
|
|
2823
|
+
for (const row of rows) {
|
|
2824
|
+
const itemId = requiredText(row, "item_id");
|
|
2825
|
+
const attachmentSelector = requiredText(row, "attachment_selector");
|
|
2826
|
+
this.insertEmailAttachmentAudit({
|
|
2827
|
+
auditId: (0, util_1.generateCanonicalId)("aat"),
|
|
2828
|
+
attachmentRef: `att_v1.${itemId}.${attachmentSelector}`,
|
|
2829
|
+
claimedAgentId: requiredText(row, "owner_agent_id"),
|
|
2830
|
+
inboxId: requiredText(row, "inbox_id"),
|
|
2831
|
+
itemId,
|
|
2832
|
+
sourceId: requiredText(row, "source_id"),
|
|
2833
|
+
action: "gc",
|
|
2834
|
+
result: "deleted",
|
|
2835
|
+
errorCode,
|
|
2836
|
+
bytes: row.stored_size == null ? null : Number(row.stored_size),
|
|
2837
|
+
sha256: row.sha256 ? String(row.sha256) : null,
|
|
2838
|
+
createdAt,
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
this.run(`delete from email_attachment_materializations where item_id in (${placeholders})`, itemIds);
|
|
2842
|
+
}
|
|
2411
2843
|
mapStream(row) {
|
|
2412
2844
|
return {
|
|
2413
2845
|
streamId: String(row.stream_id),
|
|
@@ -2453,6 +2885,24 @@ class AgentInboxStore {
|
|
|
2453
2885
|
}
|
|
2454
2886
|
}
|
|
2455
2887
|
exports.AgentInboxStore = AgentInboxStore;
|
|
2888
|
+
function materializationParams(record) {
|
|
2889
|
+
return [
|
|
2890
|
+
record.itemId,
|
|
2891
|
+
record.attachmentSelector,
|
|
2892
|
+
record.status,
|
|
2893
|
+
record.lastErrorCode,
|
|
2894
|
+
record.objectKey,
|
|
2895
|
+
record.sha256,
|
|
2896
|
+
record.declaredContentType,
|
|
2897
|
+
record.detectedContentType,
|
|
2898
|
+
record.declaredSize,
|
|
2899
|
+
record.storedSize,
|
|
2900
|
+
record.createdAt,
|
|
2901
|
+
record.updatedAt,
|
|
2902
|
+
record.expiresAt,
|
|
2903
|
+
record.deletedAt,
|
|
2904
|
+
];
|
|
2905
|
+
}
|
|
2456
2906
|
function requiredText(row, key) {
|
|
2457
2907
|
const value = row[key];
|
|
2458
2908
|
if (value == null) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
CREATE TABLE `email_body_cache` (
|
|
2
|
+
`entry_id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`inbox_id` text NOT NULL,
|
|
4
|
+
`item_id` text NOT NULL,
|
|
5
|
+
`source_id` text NOT NULL,
|
|
6
|
+
`content_version` text NOT NULL,
|
|
7
|
+
`schema_version` integer NOT NULL,
|
|
8
|
+
`parser_version` text NOT NULL,
|
|
9
|
+
`text` text NOT NULL,
|
|
10
|
+
`bytes` integer NOT NULL,
|
|
11
|
+
`completeness` text NOT NULL,
|
|
12
|
+
`reasons_json` text NOT NULL,
|
|
13
|
+
`created_at` text NOT NULL,
|
|
14
|
+
`expires_at` text NOT NULL,
|
|
15
|
+
`last_accessed_at` text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX `idx_email_body_cache_inbox_entry` ON `email_body_cache` (`inbox_id`,`entry_id`);
|
|
18
|
+
CREATE INDEX `idx_email_body_cache_expiry` ON `email_body_cache` (`expires_at`);
|
|
19
|
+
CREATE INDEX `idx_email_body_cache_access` ON `email_body_cache` (`last_accessed_at`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE `email_attachment_materializations` (
|
|
2
|
+
`item_id` text NOT NULL,
|
|
3
|
+
`attachment_selector` text NOT NULL,
|
|
4
|
+
`status` text NOT NULL,
|
|
5
|
+
`last_error_code` text,
|
|
6
|
+
`object_key` text,
|
|
7
|
+
`sha256` text,
|
|
8
|
+
`declared_content_type` text,
|
|
9
|
+
`detected_content_type` text,
|
|
10
|
+
`declared_size` integer,
|
|
11
|
+
`stored_size` integer,
|
|
12
|
+
`created_at` text NOT NULL,
|
|
13
|
+
`updated_at` text NOT NULL,
|
|
14
|
+
`expires_at` text,
|
|
15
|
+
`deleted_at` text,
|
|
16
|
+
PRIMARY KEY (`item_id`, `attachment_selector`)
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX `idx_email_attachment_materializations_object` ON `email_attachment_materializations` (`object_key`);
|
|
19
|
+
CREATE INDEX `idx_email_attachment_materializations_status` ON `email_attachment_materializations` (`status`,`updated_at`);
|
|
20
|
+
CREATE INDEX `idx_email_attachment_materializations_expiry` ON `email_attachment_materializations` (`expires_at`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE `email_attachment_audit_events` (
|
|
2
|
+
`audit_id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`attachment_ref` text NOT NULL,
|
|
4
|
+
`claimed_agent_id` text NOT NULL,
|
|
5
|
+
`inbox_id` text,
|
|
6
|
+
`item_id` text,
|
|
7
|
+
`source_id` text,
|
|
8
|
+
`action` text NOT NULL,
|
|
9
|
+
`result` text NOT NULL,
|
|
10
|
+
`error_code` text,
|
|
11
|
+
`bytes` integer,
|
|
12
|
+
`sha256` text,
|
|
13
|
+
`created_at` text NOT NULL
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX `idx_email_attachment_audit_attachment` ON `email_attachment_audit_events` (`attachment_ref`,`created_at`);
|
|
16
|
+
CREATE INDEX `idx_email_attachment_audit_item` ON `email_attachment_audit_events` (`item_id`,`created_at`);
|
|
17
|
+
CREATE INDEX `idx_email_attachment_audit_created` ON `email_attachment_audit_events` (`created_at`);
|
package/drizzle/schema.ts
CHANGED
|
@@ -243,6 +243,68 @@ export const inboxEntryItems = sqliteTable("inbox_entry_items", {
|
|
|
243
243
|
itemIdx: index("idx_inbox_entry_items_item").on(table.itemId),
|
|
244
244
|
}));
|
|
245
245
|
|
|
246
|
+
export const emailBodyCache = sqliteTable("email_body_cache", {
|
|
247
|
+
entryId: text("entry_id").primaryKey(),
|
|
248
|
+
inboxId: text("inbox_id").notNull(),
|
|
249
|
+
itemId: text("item_id").notNull(),
|
|
250
|
+
sourceId: text("source_id").notNull(),
|
|
251
|
+
contentVersion: text("content_version").notNull(),
|
|
252
|
+
schemaVersion: integer("schema_version").notNull(),
|
|
253
|
+
parserVersion: text("parser_version").notNull(),
|
|
254
|
+
text: text("text").notNull(),
|
|
255
|
+
bytes: integer("bytes").notNull(),
|
|
256
|
+
completeness: text("completeness").notNull(),
|
|
257
|
+
reasonsJson: text("reasons_json").notNull(),
|
|
258
|
+
createdAt: text("created_at").notNull(),
|
|
259
|
+
expiresAt: text("expires_at").notNull(),
|
|
260
|
+
lastAccessedAt: text("last_accessed_at").notNull(),
|
|
261
|
+
}, (table) => ({
|
|
262
|
+
inboxEntryIdx: index("idx_email_body_cache_inbox_entry").on(table.inboxId, table.entryId),
|
|
263
|
+
expiryIdx: index("idx_email_body_cache_expiry").on(table.expiresAt),
|
|
264
|
+
accessIdx: index("idx_email_body_cache_access").on(table.lastAccessedAt),
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
export const emailAttachmentMaterializations = sqliteTable("email_attachment_materializations", {
|
|
268
|
+
itemId: text("item_id").notNull(),
|
|
269
|
+
attachmentSelector: text("attachment_selector").notNull(),
|
|
270
|
+
status: text("status").notNull(),
|
|
271
|
+
lastErrorCode: text("last_error_code"),
|
|
272
|
+
objectKey: text("object_key"),
|
|
273
|
+
sha256: text("sha256"),
|
|
274
|
+
declaredContentType: text("declared_content_type"),
|
|
275
|
+
detectedContentType: text("detected_content_type"),
|
|
276
|
+
declaredSize: integer("declared_size"),
|
|
277
|
+
storedSize: integer("stored_size"),
|
|
278
|
+
createdAt: text("created_at").notNull(),
|
|
279
|
+
updatedAt: text("updated_at").notNull(),
|
|
280
|
+
expiresAt: text("expires_at"),
|
|
281
|
+
deletedAt: text("deleted_at"),
|
|
282
|
+
}, (table) => ({
|
|
283
|
+
pk: primaryKey({ columns: [table.itemId, table.attachmentSelector] }),
|
|
284
|
+
objectIdx: index("idx_email_attachment_materializations_object").on(table.objectKey),
|
|
285
|
+
statusIdx: index("idx_email_attachment_materializations_status").on(table.status, table.updatedAt),
|
|
286
|
+
expiryIdx: index("idx_email_attachment_materializations_expiry").on(table.expiresAt),
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
export const emailAttachmentAuditEvents = sqliteTable("email_attachment_audit_events", {
|
|
290
|
+
auditId: text("audit_id").primaryKey(),
|
|
291
|
+
attachmentRef: text("attachment_ref").notNull(),
|
|
292
|
+
claimedAgentId: text("claimed_agent_id").notNull(),
|
|
293
|
+
inboxId: text("inbox_id"),
|
|
294
|
+
itemId: text("item_id"),
|
|
295
|
+
sourceId: text("source_id"),
|
|
296
|
+
action: text("action").notNull(),
|
|
297
|
+
result: text("result").notNull(),
|
|
298
|
+
errorCode: text("error_code"),
|
|
299
|
+
bytes: integer("bytes"),
|
|
300
|
+
sha256: text("sha256"),
|
|
301
|
+
createdAt: text("created_at").notNull(),
|
|
302
|
+
}, (table) => ({
|
|
303
|
+
attachmentIdx: index("idx_email_attachment_audit_attachment").on(table.attachmentRef, table.createdAt),
|
|
304
|
+
itemIdx: index("idx_email_attachment_audit_item").on(table.itemId, table.createdAt),
|
|
305
|
+
createdIdx: index("idx_email_attachment_audit_created").on(table.createdAt),
|
|
306
|
+
}));
|
|
307
|
+
|
|
246
308
|
export const digestThreadItems = sqliteTable("digest_thread_items", {
|
|
247
309
|
threadId: text("thread_id").notNull(),
|
|
248
310
|
itemId: text("item_id").notNull(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holon-run/agentinbox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Local event subscription and delivery service for agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@fastify/swagger": "^9.5.2",
|
|
47
|
-
"@holon-run/uxc-daemon-client": "^0.
|
|
47
|
+
"@holon-run/uxc-daemon-client": "^0.21.1",
|
|
48
48
|
"better-sqlite3": "^12.10.0",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"fastify": "^5.6.1",
|