@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.
Files changed (34) hide show
  1. package/README.md +32 -7
  2. package/dist/src/adapters.js +61 -6
  3. package/dist/src/backend.js +4 -16
  4. package/dist/src/cli.js +307 -53
  5. package/dist/src/client.js +2 -2
  6. package/dist/src/daemon.js +36 -2
  7. package/dist/src/http.js +467 -45
  8. package/dist/src/logging.js +112 -0
  9. package/dist/src/paths.js +1 -0
  10. package/dist/src/runtime_gate.js +394 -63
  11. package/dist/src/service.js +856 -154
  12. package/dist/src/source_hosts.js +75 -0
  13. package/dist/src/source_resolution.js +20 -20
  14. package/dist/src/source_schema.js +2 -2
  15. package/dist/src/sources/feishu.js +66 -28
  16. package/dist/src/sources/github.js +188 -24
  17. package/dist/src/sources/remote.js +44 -24
  18. package/dist/src/sources/{remote_profiles.js → remote_modules.js} +143 -58
  19. package/dist/src/store.js +861 -106
  20. package/dist/src/terminal.js +33 -7
  21. package/dist/src/util.js +46 -1
  22. package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
  23. package/drizzle/schema.ts +93 -1
  24. package/package.json +2 -1
  25. package/scripts/iterm2_cursor_probe.py +8 -3
  26. package/scripts/release_smoke.sh +123 -0
  27. package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
  28. package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
  29. package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
  30. package/drizzle/migrations/0004_source_idle_states.sql +0 -10
  31. package/drizzle/migrations/0005_timers.sql +0 -22
  32. package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
  33. package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
  34. 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
- const db = node_fs_1.default.existsSync(dbPath)
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
- const store = new AgentInboxStore(dbPath, db);
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
- if (pending.length > 0) {
78
- if (this.shouldBackupBeforeMigration()) {
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
- shouldBackupBeforeMigration() {
137
- if (!node_fs_1.default.existsSync(this.dbPath)) {
129
+ shouldArchivePreV1Database() {
130
+ if (this.isV1Database()) {
138
131
  return false;
139
132
  }
140
- return this.tableExists("sources");
133
+ return this.hasRecognizablePreV1Schema();
141
134
  }
142
- backupDatabase() {
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 backupPath = `${this.dbPath}.backup-${safeStamp}`;
145
- node_fs_1.default.copyFileSync(this.dbPath, backupPath);
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.db.run(`
186
- insert into sources (
187
- source_id, source_type, source_key, config_ref, config_json,
188
- status, checkpoint, created_at, updated_at
189
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
190
- `, [
191
- source.sourceId,
192
- source.sourceType,
193
- source.sourceKey,
194
- source.configRef ?? null,
195
- JSON.stringify(source.config ?? {}),
196
- source.status,
197
- source.checkpoint ?? null,
198
- source.createdAt,
199
- source.updatedAt,
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("insert into inboxes (inbox_id, owner_agent_id, created_at) values (?, ?, ?)", [inbox.inboxId, inbox.ownerAgentId, inbox.createdAt]);
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, pending_new_item_count, pending_summary,
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
- listInboxItems(inboxId, options) {
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?.afterItemId) {
801
- const anchor = this.getOne("select coalesce(inbox_sequence, rowid) as inbox_sequence from inbox_items where inbox_id = ? and item_id = ?", [inboxId, options.afterItemId]);
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 item: ${options.afterItemId}`);
1058
+ throw new Error(`unknown inbox entry: ${options.afterEntryId}`);
804
1059
  }
805
- filters.push("coalesce(inbox_sequence, rowid) > ?");
806
- params.push(Number(anchor.inbox_sequence));
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.db.run(`
846
- delete from inbox_items
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
- `, [inboxId, olderThan]);
851
- const changes = this.changes();
852
- if (changes > 0) {
853
- this.persist();
1287
+ `, [inboxId, olderThan]).map((row) => String(row.item_id));
1288
+ if (itemIds.length === 0) {
1289
+ return 0;
854
1290
  }
855
- return changes;
1291
+ this.deleteInboxItemsAndEntryArtifacts(itemIds);
1292
+ return itemIds.length;
856
1293
  }
857
1294
  deleteAckedInboxItemsGlobal(olderThan) {
858
- this.db.run(`
859
- delete from inbox_items
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
- `, [olderThan]);
863
- const changes = this.changes();
864
- if (changes > 0) {
865
- this.persist();
1300
+ `, [olderThan]).map((row) => String(row.item_id));
1301
+ if (itemIds.length === 0) {
1302
+ return 0;
866
1303
  }
867
- return changes;
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 before = this.changes();
947
- this.db.run(`
948
- insert or ignore into stream_events (
949
- stream_event_id, stream_id, source_id, source_native_id, event_variant,
950
- occurred_at, metadata_json, raw_payload_json, delivery_handle_json, created_at
951
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
952
- `, [
953
- (0, util_1.generateId)("strevt"),
954
- streamId,
955
- event.sourceId,
956
- event.sourceNativeId,
957
- event.eventVariant,
958
- event.occurredAt ?? (0, util_1.nowIso)(),
959
- JSON.stringify(event.metadata ?? {}),
960
- JSON.stringify(event.rawPayload ?? {}),
961
- event.deliveryHandle ? JSON.stringify(event.deliveryHandle) : null,
962
- (0, util_1.nowIso)(),
963
- ]);
964
- const inserted = this.changes() > before;
965
- if (inserted) {
966
- this.persist();
967
- return {
968
- appended: 1,
969
- deduped: 0,
970
- lastOffset: this.lastInsertRowId(),
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
- return { appended: 0, deduped: 1, lastOffset: null };
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.generateId)("commit"), consumerId, consumer.streamId, committedOffset, updatedAt]);
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
- items: row.items_json ? parseJson(row.items_json) : undefined,
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
+ }