@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/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.7.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.15.8",
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",