@holon-run/agentinbox 0.7.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +36 -7
  2. package/dist/src/adapters.js +55 -0
  3. package/dist/src/backend.js +4 -16
  4. package/dist/src/cli.js +329 -52
  5. package/dist/src/client.js +2 -2
  6. package/dist/src/http.js +479 -55
  7. package/dist/src/runtime_gate.js +49 -41
  8. package/dist/src/service.js +922 -200
  9. package/dist/src/source_hosts.js +75 -0
  10. package/dist/src/source_resolution.js +2 -2
  11. package/dist/src/source_schema.js +2 -2
  12. package/dist/src/sources/feishu.js +66 -28
  13. package/dist/src/sources/github.js +222 -36
  14. package/dist/src/sources/remote.js +50 -3
  15. package/dist/src/sources/remote_modules.js +100 -15
  16. package/dist/src/store.js +875 -110
  17. package/dist/src/terminal.js +33 -7
  18. package/dist/src/util.js +46 -1
  19. package/drizzle/migrations/{0000_initial.sql → 0000_v1_initial.sql} +182 -6
  20. package/drizzle/migrations/0002_host_scoped_lifecycle_retirements.sql +42 -0
  21. package/drizzle/migrations/0003_subscription_tracked_resource_indexes.sql +5 -0
  22. package/drizzle/schema.ts +98 -3
  23. package/package.json +2 -1
  24. package/scripts/release_smoke.sh +123 -0
  25. package/dist/src/sources/remote_profiles.js +0 -13
  26. package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +0 -3
  27. package/drizzle/migrations/0002_subscription_cleanup_policy.sql +0 -45
  28. package/drizzle/migrations/0003_subscription_lifecycle_retirements.sql +0 -14
  29. package/drizzle/migrations/0004_source_idle_states.sql +0 -10
  30. package/drizzle/migrations/0005_timers.sql +0 -22
  31. package/drizzle/migrations/0006_terminal_runtime_pid.sql +0 -1
  32. package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +0 -2
  33. 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,9 +518,44 @@ 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
+ ]);
353
535
  this.persist();
354
536
  }
537
+ updateInboxAggregationPolicy(inboxId, policy) {
538
+ const current = this.getInbox(inboxId);
539
+ if (!current) {
540
+ throw new Error(`unknown inbox: ${inboxId}`);
541
+ }
542
+ this.db.run(`
543
+ update inboxes
544
+ set aggregation_enabled = ?,
545
+ aggregation_window_ms = ?,
546
+ aggregation_max_items = ?,
547
+ aggregation_max_thread_age_ms = ?
548
+ where inbox_id = ?
549
+ `, [
550
+ policy.enabled === undefined ? (current.aggregationEnabled ? 1 : 0) : (policy.enabled ? 1 : 0),
551
+ Object.prototype.hasOwnProperty.call(policy, "windowMs") ? policy.windowMs ?? null : current.aggregationWindowMs ?? null,
552
+ Object.prototype.hasOwnProperty.call(policy, "maxItems") ? policy.maxItems ?? null : current.aggregationMaxItems ?? null,
553
+ Object.prototype.hasOwnProperty.call(policy, "maxThreadAgeMs") ? policy.maxThreadAgeMs ?? null : current.aggregationMaxThreadAgeMs ?? null,
554
+ inboxId,
555
+ ]);
556
+ this.persist();
557
+ return this.getInbox(inboxId);
558
+ }
355
559
  listInboxes() {
356
560
  const rows = this.getAll("select * from inboxes order by created_at asc");
357
561
  return rows.map((row) => this.mapInbox(row));
@@ -391,6 +595,16 @@ class AgentInboxStore {
391
595
  const rows = this.getAll("select * from subscriptions where agent_id = ? order by created_at asc", [agentId]);
392
596
  return rows.map((row) => this.mapSubscription(row));
393
597
  }
598
+ listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
599
+ const rows = this.getAll(`
600
+ select subscriptions.*
601
+ from subscriptions
602
+ inner join sources on sources.source_id = subscriptions.source_id
603
+ where sources.host_id = ? and subscriptions.tracked_resource_ref = ?
604
+ order by subscriptions.created_at asc
605
+ `, [hostId, trackedResourceRef]);
606
+ return rows.map((row) => this.mapSubscription(row));
607
+ }
394
608
  deleteSubscription(subscriptionId) {
395
609
  const subscription = this.getSubscription(subscriptionId);
396
610
  if (!subscription) {
@@ -424,10 +638,10 @@ class AgentInboxStore {
424
638
  : retirement;
425
639
  this.db.run(`
426
640
  insert into subscription_lifecycle_retirements (
427
- subscription_id, source_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
641
+ subscription_id, host_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
428
642
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
429
643
  on conflict(subscription_id) do update set
430
- source_id = excluded.source_id,
644
+ host_id = excluded.host_id,
431
645
  tracked_resource_ref = excluded.tracked_resource_ref,
432
646
  retire_at = excluded.retire_at,
433
647
  terminal_state = excluded.terminal_state,
@@ -437,7 +651,7 @@ class AgentInboxStore {
437
651
  updated_at = excluded.updated_at
438
652
  `, [
439
653
  next.subscriptionId,
440
- next.sourceId,
654
+ next.hostId,
441
655
  next.trackedResourceRef,
442
656
  next.retireAt,
443
657
  next.terminalState ?? null,
@@ -482,8 +696,8 @@ class AgentInboxStore {
482
696
  this.db.run(`
483
697
  insert into activation_targets (
484
698
  target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error,
485
- mode, notify_lease_ms, url, created_at, updated_at, last_seen_at
486
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
699
+ mode, notify_lease_ms, min_unacked_items, url, created_at, updated_at, last_seen_at
700
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
487
701
  `, [
488
702
  target.targetId,
489
703
  target.agentId,
@@ -495,6 +709,7 @@ class AgentInboxStore {
495
709
  target.lastError ?? null,
496
710
  target.mode,
497
711
  target.notifyLeaseMs,
712
+ target.minUnackedItems ?? null,
498
713
  target.url,
499
714
  target.createdAt,
500
715
  target.updatedAt,
@@ -504,10 +719,10 @@ class AgentInboxStore {
504
719
  else {
505
720
  this.db.run(`
506
721
  insert into activation_targets (
507
- target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms,
722
+ target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms, min_unacked_items,
508
723
  runtime_kind, runtime_session_id, runtime_pid, backend, tmux_pane_id, tty, term_program, iterm_session_id,
509
724
  created_at, updated_at, last_seen_at
510
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
725
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
511
726
  `, [
512
727
  target.targetId,
513
728
  target.agentId,
@@ -519,6 +734,7 @@ class AgentInboxStore {
519
734
  target.lastError ?? null,
520
735
  target.mode,
521
736
  target.notifyLeaseMs,
737
+ target.minUnackedItems ?? null,
522
738
  target.runtimeKind,
523
739
  target.runtimeSessionId ?? null,
524
740
  target.runtimePid ?? null,
@@ -538,7 +754,7 @@ class AgentInboxStore {
538
754
  this.db.run(`
539
755
  update activation_targets
540
756
  set status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
541
- runtime_kind = ?, runtime_session_id = ?, runtime_pid = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, updated_at = ?, last_seen_at = ?
757
+ runtime_kind = ?, runtime_session_id = ?, runtime_pid = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, notify_lease_ms = ?, min_unacked_items = ?, updated_at = ?, last_seen_at = ?
542
758
  where target_id = ? and kind = 'terminal'
543
759
  `, [
544
760
  input.runtimeKind,
@@ -548,6 +764,8 @@ class AgentInboxStore {
548
764
  input.tty ?? null,
549
765
  input.termProgram ?? null,
550
766
  input.itermSessionId ?? null,
767
+ input.notifyLeaseMs,
768
+ input.minUnackedItems,
551
769
  input.updatedAt,
552
770
  input.lastSeenAt,
553
771
  targetId,
@@ -729,13 +947,19 @@ class AgentInboxStore {
729
947
  upsertActivationDispatchState(state) {
730
948
  this.db.run(`
731
949
  insert into activation_dispatch_states (
732
- agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, pending_new_item_count, pending_summary,
950
+ agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, defer_reason, defer_attempts,
951
+ first_deferred_at, last_deferred_at, pending_fingerprint, pending_new_item_count, pending_summary,
733
952
  pending_subscription_ids_json, pending_source_ids_json, updated_at
734
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
953
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
735
954
  on conflict(agent_id, target_id) do update set
736
955
  status = excluded.status,
737
956
  lease_expires_at = excluded.lease_expires_at,
738
957
  last_notified_fingerprint = excluded.last_notified_fingerprint,
958
+ defer_reason = excluded.defer_reason,
959
+ defer_attempts = excluded.defer_attempts,
960
+ first_deferred_at = excluded.first_deferred_at,
961
+ last_deferred_at = excluded.last_deferred_at,
962
+ pending_fingerprint = excluded.pending_fingerprint,
739
963
  pending_new_item_count = excluded.pending_new_item_count,
740
964
  pending_summary = excluded.pending_summary,
741
965
  pending_subscription_ids_json = excluded.pending_subscription_ids_json,
@@ -747,6 +971,11 @@ class AgentInboxStore {
747
971
  state.status,
748
972
  state.leaseExpiresAt ?? null,
749
973
  state.lastNotifiedFingerprint ?? null,
974
+ state.deferReason ?? null,
975
+ state.deferAttempts,
976
+ state.firstDeferredAt ?? null,
977
+ state.lastDeferredAt ?? null,
978
+ state.pendingFingerprint ?? null,
750
979
  state.pendingNewItemCount,
751
980
  state.pendingSummary ?? null,
752
981
  JSON.stringify(state.pendingSubscriptionIds),
@@ -790,20 +1019,237 @@ class AgentInboxStore {
790
1019
  }
791
1020
  return inserted;
792
1021
  }
793
- listInboxItems(inboxId, options) {
1022
+ createInboxItemEntry(inboxId, item, input) {
1023
+ const entryId = this.insertInboxEntryRecord({
1024
+ inboxId,
1025
+ kind: "item",
1026
+ threadId: null,
1027
+ revision: null,
1028
+ groupKey: null,
1029
+ resourceRef: null,
1030
+ eventFamily: null,
1031
+ itemJson: JSON.stringify({
1032
+ itemId: item.itemId,
1033
+ sourceId: item.sourceId,
1034
+ sourceNativeId: item.sourceNativeId,
1035
+ eventVariant: item.eventVariant,
1036
+ inboxId: item.inboxId,
1037
+ occurredAt: item.occurredAt,
1038
+ metadata: item.metadata,
1039
+ rawPayload: item.rawPayload,
1040
+ deliveryHandle: item.deliveryHandle ?? null,
1041
+ }),
1042
+ count: 1,
1043
+ summary: input.summary,
1044
+ firstItemAt: item.occurredAt,
1045
+ lastItemAt: item.occurredAt,
1046
+ sourceIdsJson: JSON.stringify([item.sourceId]),
1047
+ subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
1048
+ deliveryHandleJson: item.deliveryHandle ? JSON.stringify(item.deliveryHandle) : null,
1049
+ ackedAt: item.ackedAt ?? null,
1050
+ supersededAt: null,
1051
+ createdAt: (0, util_1.nowIso)(),
1052
+ });
1053
+ this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
1054
+ this.persist();
1055
+ return this.getInboxEntry(entryId);
1056
+ }
1057
+ listInboxEntries(inboxId, options) {
794
1058
  const includeAcked = options?.includeAcked ?? false;
795
1059
  const filters = ["inbox_id = ?"];
796
1060
  const params = [inboxId];
797
1061
  if (!includeAcked) {
798
1062
  filters.push("acked_at is null");
1063
+ filters.push("superseded_at is null");
799
1064
  }
800
- if (options?.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]);
1065
+ if (options?.afterEntryId) {
1066
+ const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, (0, util_1.parseEntryRef)(options.afterEntryId)]);
802
1067
  if (!anchor) {
803
- throw new Error(`unknown inbox item: ${options.afterItemId}`);
1068
+ throw new Error(`unknown inbox entry: ${options.afterEntryId}`);
804
1069
  }
805
- filters.push("coalesce(inbox_sequence, rowid) > ?");
806
- params.push(Number(anchor.inbox_sequence));
1070
+ filters.push("sequence > ?");
1071
+ params.push(Number(anchor.sequence));
1072
+ }
1073
+ const rows = this.getAll(`select * from inbox_entries where ${filters.join(" and ")} order by sequence asc`, params);
1074
+ return this.mapInboxEntries(rows);
1075
+ }
1076
+ getInboxEntry(entryId) {
1077
+ const rows = this.getAll("select * from inbox_entries where entry_id = ?", [(0, util_1.parseEntryRef)(entryId)]);
1078
+ return this.mapInboxEntries(rows)[0] ?? null;
1079
+ }
1080
+ ackInboxEntries(inboxId, entryIds, ackedAt) {
1081
+ const resolvedIds = uniqueSorted(entryIds).map((entryId) => this.resolveInboxEntryId(inboxId, entryId));
1082
+ const itemIds = this.getRawItemIdsForEntryIds(resolvedIds);
1083
+ const ackedItems = itemIds.length > 0 ? this.ackItems(inboxId, itemIds, ackedAt) : 0;
1084
+ if (ackedItems > 0) {
1085
+ this.syncInboxEntryAckState(inboxId, ackedAt);
1086
+ }
1087
+ const ackedEntries = this.countAckedEntries(resolvedIds, ackedAt);
1088
+ return { ackedEntries, ackedItems };
1089
+ }
1090
+ ackInboxEntriesThrough(inboxId, entryId, ackedAt) {
1091
+ const anchor = this.getOne("select sequence from inbox_entries where inbox_id = ? and entry_id = ?", [inboxId, this.resolveInboxEntryId(inboxId, entryId)]);
1092
+ if (!anchor) {
1093
+ throw new Error(`unknown inbox entry: ${entryId}`);
1094
+ }
1095
+ const rows = this.getAll(`
1096
+ select entry_id
1097
+ from inbox_entries
1098
+ where inbox_id = ?
1099
+ and acked_at is null
1100
+ and superseded_at is null
1101
+ and sequence <= ?
1102
+ order by sequence asc
1103
+ `, [inboxId, Number(anchor.sequence)]);
1104
+ return this.ackInboxEntries(inboxId, rows.map((row) => (0, util_1.formatEntryRef)(String(row.entry_id))), ackedAt);
1105
+ }
1106
+ getOpenDigestThread(inboxId, groupKey) {
1107
+ const row = this.getOne(`
1108
+ select * from digest_threads
1109
+ where inbox_id = ? and group_key = ? and status = 'open'
1110
+ order by created_at desc, rowid desc
1111
+ limit 1
1112
+ `, [inboxId, groupKey]);
1113
+ return row ? this.mapDigestThread(row) : null;
1114
+ }
1115
+ createDigestThread(input) {
1116
+ const threadId = (0, util_1.generateCanonicalId)("thr");
1117
+ this.db.run(`
1118
+ insert into digest_threads (
1119
+ thread_id, inbox_id, source_id, group_key, resource_ref, event_family, latest_revision,
1120
+ latest_entry_id, status, summary, first_item_at, last_item_at, flush_after_at,
1121
+ created_at, updated_at
1122
+ ) values (?, ?, ?, ?, ?, ?, 0, null, 'open', ?, ?, ?, ?, ?, ?)
1123
+ `, [
1124
+ threadId,
1125
+ input.inboxId,
1126
+ input.sourceId,
1127
+ input.groupKey,
1128
+ input.resourceRef ?? null,
1129
+ input.eventFamily ?? null,
1130
+ input.summary,
1131
+ input.firstItemAt,
1132
+ input.firstItemAt,
1133
+ input.flushAfterAt ?? null,
1134
+ input.createdAt,
1135
+ input.createdAt,
1136
+ ]);
1137
+ this.persist();
1138
+ return this.getDigestThread(threadId);
1139
+ }
1140
+ getDigestThread(threadId) {
1141
+ const row = this.getOne("select * from digest_threads where thread_id = ?", [threadId]);
1142
+ return row ? this.mapDigestThread(row) : null;
1143
+ }
1144
+ addItemToDigestThread(threadId, itemId, occurredAt, summary, flushAfterAt) {
1145
+ this.inTransaction(() => {
1146
+ this.db.run("insert or ignore into digest_thread_items (thread_id, item_id) values (?, ?)", [threadId, itemId]);
1147
+ this.db.run(`
1148
+ update digest_threads
1149
+ set summary = ?, last_item_at = ?, flush_after_at = ?, updated_at = ?
1150
+ where thread_id = ?
1151
+ `, [summary, occurredAt, flushAfterAt, (0, util_1.nowIso)(), threadId]);
1152
+ });
1153
+ this.persist();
1154
+ return this.getDigestThread(threadId);
1155
+ }
1156
+ listDueDigestThreads(now) {
1157
+ const rows = this.getAll(`
1158
+ select *
1159
+ from digest_threads
1160
+ where status = 'open'
1161
+ and flush_after_at is not null
1162
+ and flush_after_at <= ?
1163
+ order by flush_after_at asc, created_at asc, thread_id asc
1164
+ `, [now]);
1165
+ return rows.map((row) => this.mapDigestThread(row));
1166
+ }
1167
+ listUnackedInboxItemsForDigestThread(threadId) {
1168
+ const rows = this.getAll(`
1169
+ select i.*
1170
+ from digest_thread_items t
1171
+ join inbox_items i on i.item_id = t.item_id
1172
+ where t.thread_id = ?
1173
+ and i.acked_at is null
1174
+ order by coalesce(i.inbox_sequence, i.rowid) asc
1175
+ `, [threadId]);
1176
+ return rows.map((row) => this.mapInboxItem(row));
1177
+ }
1178
+ listOpenDigestThreadsForInbox(inboxId) {
1179
+ const rows = this.getAll("select * from digest_threads where inbox_id = ? and status = 'open' order by created_at asc, thread_id asc", [inboxId]);
1180
+ return rows.map((row) => this.mapDigestThread(row));
1181
+ }
1182
+ listOpenDigestThreads() {
1183
+ const rows = this.getAll("select * from digest_threads where status = 'open' order by created_at asc, thread_id asc");
1184
+ return rows.map((row) => this.mapDigestThread(row));
1185
+ }
1186
+ materializeDigestSnapshot(input) {
1187
+ const thread = this.getDigestThread(input.threadId);
1188
+ if (!thread) {
1189
+ throw new Error(`unknown digest thread: ${input.threadId}`);
1190
+ }
1191
+ this.inTransaction(() => {
1192
+ if (thread.latestEntryId != null) {
1193
+ this.db.run("update inbox_entries set superseded_at = ? where entry_id = ? and superseded_at is null", [input.createdAt, thread.latestEntryId]);
1194
+ }
1195
+ const nextRevision = thread.latestRevision + 1;
1196
+ const entryId = this.insertInboxEntryRecord({
1197
+ inboxId: input.inboxId,
1198
+ kind: "digest_snapshot",
1199
+ threadId: input.threadId,
1200
+ revision: nextRevision,
1201
+ groupKey: input.groupKey,
1202
+ resourceRef: input.resourceRef ?? null,
1203
+ eventFamily: input.eventFamily ?? null,
1204
+ itemJson: null,
1205
+ count: input.itemIds.length,
1206
+ summary: input.summary,
1207
+ firstItemAt: input.firstItemAt,
1208
+ lastItemAt: input.lastItemAt,
1209
+ sourceIdsJson: JSON.stringify(uniqueSorted(input.sourceIds)),
1210
+ subscriptionIdsJson: JSON.stringify(uniqueSorted(input.subscriptionIds)),
1211
+ deliveryHandleJson: input.deliveryHandleJson ?? null,
1212
+ ackedAt: null,
1213
+ supersededAt: null,
1214
+ createdAt: input.createdAt,
1215
+ });
1216
+ for (const itemId of input.itemIds) {
1217
+ this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, itemId]);
1218
+ }
1219
+ this.db.run(`
1220
+ update digest_threads
1221
+ set latest_revision = ?, latest_entry_id = ?, summary = ?, first_item_at = ?, last_item_at = ?,
1222
+ flush_after_at = null, updated_at = ?
1223
+ where thread_id = ?
1224
+ `, [nextRevision, entryId, input.summary, input.firstItemAt, input.lastItemAt, input.createdAt, input.threadId]);
1225
+ });
1226
+ this.persist();
1227
+ return this.getInboxEntry(this.getDigestThread(input.threadId).latestEntryId);
1228
+ }
1229
+ closeDigestThread(threadId, updatedAt) {
1230
+ this.db.run("update digest_threads set status = 'closed', flush_after_at = null, updated_at = ? where thread_id = ?", [updatedAt, threadId]);
1231
+ this.persist();
1232
+ }
1233
+ listDigestThreadsForRawItems(itemIds) {
1234
+ if (itemIds.length === 0) {
1235
+ return [];
1236
+ }
1237
+ const placeholders = itemIds.map(() => "?").join(", ");
1238
+ const rows = this.getAll(`
1239
+ select distinct t.*
1240
+ from digest_threads t
1241
+ join digest_thread_items i on i.thread_id = t.thread_id
1242
+ where i.item_id in (${placeholders})
1243
+ order by t.created_at asc, t.thread_id asc
1244
+ `, itemIds);
1245
+ return rows.map((row) => this.mapDigestThread(row));
1246
+ }
1247
+ listInboxItems(inboxId, options) {
1248
+ const includeAcked = options?.includeAcked ?? false;
1249
+ const filters = ["inbox_id = ?"];
1250
+ const params = [inboxId];
1251
+ if (!includeAcked) {
1252
+ filters.push("acked_at is null");
807
1253
  }
808
1254
  const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by coalesce(inbox_sequence, rowid) asc`, params);
809
1255
  return rows.map((row) => this.mapInboxItem(row));
@@ -842,29 +1288,31 @@ class AgentInboxStore {
842
1288
  return changes;
843
1289
  }
844
1290
  deleteAckedInboxItems(inboxId, olderThan) {
845
- this.db.run(`
846
- delete from inbox_items
1291
+ const itemIds = this.getAll(`
1292
+ select item_id
1293
+ from inbox_items
847
1294
  where inbox_id = ?
848
1295
  and acked_at is not null
849
1296
  and acked_at < ?
850
- `, [inboxId, olderThan]);
851
- const changes = this.changes();
852
- if (changes > 0) {
853
- this.persist();
1297
+ `, [inboxId, olderThan]).map((row) => String(row.item_id));
1298
+ if (itemIds.length === 0) {
1299
+ return 0;
854
1300
  }
855
- return changes;
1301
+ this.deleteInboxItemsAndEntryArtifacts(itemIds);
1302
+ return itemIds.length;
856
1303
  }
857
1304
  deleteAckedInboxItemsGlobal(olderThan) {
858
- this.db.run(`
859
- delete from inbox_items
1305
+ const itemIds = this.getAll(`
1306
+ select item_id
1307
+ from inbox_items
860
1308
  where acked_at is not null
861
1309
  and acked_at < ?
862
- `, [olderThan]);
863
- const changes = this.changes();
864
- if (changes > 0) {
865
- this.persist();
1310
+ `, [olderThan]).map((row) => String(row.item_id));
1311
+ if (itemIds.length === 0) {
1312
+ return 0;
866
1313
  }
867
- return changes;
1314
+ this.deleteInboxItemsAndEntryArtifacts(itemIds);
1315
+ return itemIds.length;
868
1316
  }
869
1317
  countInboxItems(inboxId, includeAcked) {
870
1318
  const row = this.getOne(`
@@ -892,7 +1340,7 @@ class AgentInboxStore {
892
1340
  JSON.stringify(activation.sourceIds),
893
1341
  activation.newItemCount,
894
1342
  activation.summary,
895
- activation.items ? JSON.stringify(activation.items) : null,
1343
+ activation.entries ? JSON.stringify(activation.entries) : (activation.items ? JSON.stringify(activation.items) : null),
896
1344
  activation.createdAt,
897
1345
  activation.deliveredAt ?? null,
898
1346
  ]);
@@ -943,34 +1391,50 @@ class AgentInboxStore {
943
1391
  return row ? this.mapStream(row) : null;
944
1392
  }
945
1393
  insertStreamEvent(streamId, event) {
946
- const 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
- };
1394
+ const occurredAt = event.occurredAt ?? (0, util_1.nowIso)();
1395
+ const metadataJson = JSON.stringify(event.metadata ?? {});
1396
+ const rawPayloadJson = JSON.stringify(event.rawPayload ?? {});
1397
+ const deliveryHandleJson = event.deliveryHandle ? JSON.stringify(event.deliveryHandle) : null;
1398
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1399
+ const before = this.changes();
1400
+ this.db.run(`
1401
+ insert or ignore into stream_events (
1402
+ stream_event_id, stream_id, source_id, source_native_id, event_variant,
1403
+ occurred_at, metadata_json, raw_payload_json, delivery_handle_json, created_at
1404
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1405
+ `, [
1406
+ (0, util_1.generateCanonicalId)("evt"),
1407
+ streamId,
1408
+ event.sourceId,
1409
+ event.sourceNativeId,
1410
+ event.eventVariant,
1411
+ occurredAt,
1412
+ metadataJson,
1413
+ rawPayloadJson,
1414
+ deliveryHandleJson,
1415
+ (0, util_1.nowIso)(),
1416
+ ]);
1417
+ if (this.changes() > before) {
1418
+ this.persist();
1419
+ return {
1420
+ appended: 1,
1421
+ deduped: 0,
1422
+ lastOffset: this.lastInsertRowId(),
1423
+ };
1424
+ }
1425
+ const existing = this.getOne(`
1426
+ select offset
1427
+ from stream_events
1428
+ where stream_id = ?
1429
+ and source_native_id = ?
1430
+ and event_variant = ?
1431
+ limit 1
1432
+ `, [streamId, event.sourceNativeId, event.eventVariant]);
1433
+ if (existing) {
1434
+ return { appended: 0, deduped: 1, lastOffset: null };
1435
+ }
972
1436
  }
973
- return { appended: 0, deduped: 1, lastOffset: null };
1437
+ throw new Error(`unable to allocate a unique stream_event_id for ${event.sourceNativeId}`);
974
1438
  }
975
1439
  readStreamEvents(streamId, nextOffset, limit) {
976
1440
  const rows = this.getAll(`
@@ -1058,7 +1522,7 @@ class AgentInboxStore {
1058
1522
  insert into consumer_commits (
1059
1523
  commit_id, consumer_id, stream_id, committed_offset, committed_at
1060
1524
  ) values (?, ?, ?, ?, ?)
1061
- `, [(0, util_1.generateId)("commit"), consumerId, consumer.streamId, committedOffset, updatedAt]);
1525
+ `, [(0, util_1.generateCanonicalId)("cmt"), consumerId, consumer.streamId, committedOffset, updatedAt]);
1062
1526
  });
1063
1527
  this.persist();
1064
1528
  return this.getConsumer(consumerId);
@@ -1139,8 +1603,15 @@ class AgentInboxStore {
1139
1603
  }
1140
1604
  }
1141
1605
  mapSource(row) {
1606
+ const hostId = requiredText(row, "host_id");
1607
+ const streamKind = requiredText(row, "stream_kind");
1608
+ const streamKey = requiredText(row, "stream_key");
1142
1609
  return {
1143
1610
  sourceId: String(row.source_id),
1611
+ streamId: String(row.source_id),
1612
+ hostId,
1613
+ streamKind,
1614
+ streamKey,
1144
1615
  sourceType: row.source_type,
1145
1616
  sourceKey: String(row.source_key),
1146
1617
  configRef: row.config_ref ? String(row.config_ref) : null,
@@ -1151,6 +1622,18 @@ class AgentInboxStore {
1151
1622
  updatedAt: String(row.updated_at),
1152
1623
  };
1153
1624
  }
1625
+ mapSourceHost(row) {
1626
+ return {
1627
+ hostId: String(row.host_id),
1628
+ hostType: String(row.host_type),
1629
+ hostKey: String(row.host_key),
1630
+ configRef: row.config_ref ? String(row.config_ref) : null,
1631
+ config: parseJson(row.config_json),
1632
+ status: String(row.status),
1633
+ createdAt: String(row.created_at),
1634
+ updatedAt: String(row.updated_at),
1635
+ };
1636
+ }
1154
1637
  mapSourceIdleState(row) {
1155
1638
  return {
1156
1639
  sourceId: String(row.source_id),
@@ -1176,6 +1659,10 @@ class AgentInboxStore {
1176
1659
  return {
1177
1660
  inboxId: String(row.inbox_id),
1178
1661
  ownerAgentId: String(row.owner_agent_id),
1662
+ aggregationEnabled: Number(row.aggregation_enabled ?? 0) > 0,
1663
+ aggregationWindowMs: row.aggregation_window_ms == null ? null : Number(row.aggregation_window_ms),
1664
+ aggregationMaxItems: row.aggregation_max_items == null ? null : Number(row.aggregation_max_items),
1665
+ aggregationMaxThreadAgeMs: row.aggregation_max_thread_age_ms == null ? null : Number(row.aggregation_max_thread_age_ms),
1179
1666
  createdAt: String(row.created_at),
1180
1667
  };
1181
1668
  }
@@ -1196,7 +1683,7 @@ class AgentInboxStore {
1196
1683
  mapSubscriptionLifecycleRetirement(row) {
1197
1684
  return {
1198
1685
  subscriptionId: String(row.subscription_id),
1199
- sourceId: String(row.source_id),
1686
+ hostId: String(row.host_id),
1200
1687
  trackedResourceRef: String(row.tracked_resource_ref),
1201
1688
  retireAt: String(row.retire_at),
1202
1689
  terminalState: row.terminal_state ? String(row.terminal_state) : null,
@@ -1224,6 +1711,11 @@ class AgentInboxStore {
1224
1711
  mode: row.mode,
1225
1712
  url,
1226
1713
  notifyLeaseMs: Number(row.notify_lease_ms),
1714
+ minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
1715
+ notificationPolicy: {
1716
+ notifyLeaseMs: Number(row.notify_lease_ms),
1717
+ minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
1718
+ },
1227
1719
  createdAt: String(row.created_at),
1228
1720
  updatedAt: String(row.updated_at),
1229
1721
  lastSeenAt: String(row.last_seen_at),
@@ -1240,6 +1732,11 @@ class AgentInboxStore {
1240
1732
  lastError: row.last_error ? String(row.last_error) : null,
1241
1733
  mode: row.mode,
1242
1734
  notifyLeaseMs: Number(row.notify_lease_ms),
1735
+ minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
1736
+ notificationPolicy: {
1737
+ notifyLeaseMs: Number(row.notify_lease_ms),
1738
+ minUnackedItems: row.min_unacked_items == null ? null : Number(row.min_unacked_items),
1739
+ },
1243
1740
  runtimeKind: (row.runtime_kind ? String(row.runtime_kind) : "unknown"),
1244
1741
  runtimeSessionId: row.runtime_session_id ? String(row.runtime_session_id) : null,
1245
1742
  runtimePid: row.runtime_pid == null ? null : Number(row.runtime_pid),
@@ -1260,6 +1757,11 @@ class AgentInboxStore {
1260
1757
  status: row.status,
1261
1758
  leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
1262
1759
  lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
1760
+ deferReason: row.defer_reason ? row.defer_reason : null,
1761
+ deferAttempts: Number(row.defer_attempts ?? 0),
1762
+ firstDeferredAt: row.first_deferred_at ? String(row.first_deferred_at) : null,
1763
+ lastDeferredAt: row.last_deferred_at ? String(row.last_deferred_at) : null,
1764
+ pendingFingerprint: row.pending_fingerprint ? String(row.pending_fingerprint) : null,
1263
1765
  pendingNewItemCount: Number(row.pending_new_item_count),
1264
1766
  pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
1265
1767
  pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
@@ -1301,6 +1803,70 @@ class AgentInboxStore {
1301
1803
  ackedAt: row.acked_at ? String(row.acked_at) : null,
1302
1804
  };
1303
1805
  }
1806
+ mapInboxEntry(row, itemIds) {
1807
+ const base = {
1808
+ entryId: (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
1809
+ inboxId: String(row.inbox_id),
1810
+ sequence: Number(row.sequence),
1811
+ itemId: "",
1812
+ summary: String(row.summary),
1813
+ count: Number(row.count),
1814
+ itemIds,
1815
+ sourceIds: parseJson(row.source_ids_json),
1816
+ subscriptionIds: parseJson(row.subscription_ids_json),
1817
+ firstItemAt: String(row.first_item_at),
1818
+ lastItemAt: String(row.last_item_at),
1819
+ deliveryHandle: row.delivery_handle_json
1820
+ ? parseJson(row.delivery_handle_json)
1821
+ : null,
1822
+ ackedAt: row.acked_at ? String(row.acked_at) : null,
1823
+ supersededAt: row.superseded_at ? String(row.superseded_at) : null,
1824
+ };
1825
+ if (String(row.kind) === "item") {
1826
+ const item = parseJson(row.item_json);
1827
+ return {
1828
+ ...base,
1829
+ kind: "item",
1830
+ itemId: item.itemId,
1831
+ sourceId: item.sourceId,
1832
+ sourceNativeId: item.sourceNativeId,
1833
+ eventVariant: item.eventVariant,
1834
+ occurredAt: item.occurredAt,
1835
+ metadata: item.metadata,
1836
+ rawPayload: item.rawPayload,
1837
+ item,
1838
+ };
1839
+ }
1840
+ return {
1841
+ ...base,
1842
+ kind: "digest_snapshot",
1843
+ itemId: base.itemIds[0] ?? (0, util_1.formatEntryRef)(requiredText(row, "entry_id")),
1844
+ threadId: (0, util_1.formatThreadRef)(requiredText(row, "thread_id")),
1845
+ revision: Number(row.revision),
1846
+ groupKey: String(row.group_key),
1847
+ resourceRef: row.resource_ref ? String(row.resource_ref) : null,
1848
+ eventFamily: row.event_family ? String(row.event_family) : null,
1849
+ };
1850
+ }
1851
+ mapDigestThread(row) {
1852
+ return {
1853
+ threadId: requiredText(row, "thread_id"),
1854
+ inboxId: String(row.inbox_id),
1855
+ sourceId: String(row.source_id),
1856
+ groupKey: String(row.group_key),
1857
+ resourceRef: row.resource_ref ? String(row.resource_ref) : null,
1858
+ eventFamily: row.event_family ? String(row.event_family) : null,
1859
+ latestRevision: Number(row.latest_revision),
1860
+ latestEntryId: row.latest_entry_id == null ? null : String(row.latest_entry_id),
1861
+ status: String(row.status),
1862
+ summary: String(row.summary),
1863
+ firstItemAt: String(row.first_item_at),
1864
+ lastItemAt: String(row.last_item_at),
1865
+ flushAfterAt: row.flush_after_at ? String(row.flush_after_at) : null,
1866
+ createdAt: String(row.created_at),
1867
+ updatedAt: String(row.updated_at),
1868
+ };
1869
+ }
1304
1870
  mapActivation(row) {
1305
1871
  return {
1306
1872
  kind: "agentinbox.activation",
@@ -1311,9 +1877,10 @@ class AgentInboxStore {
1311
1877
  targetKind: row.target_kind,
1312
1878
  subscriptionIds: parseJson(row.subscription_ids_json),
1313
1879
  sourceIds: parseJson(row.source_ids_json),
1880
+ newEntryCount: row.items_json ? parseJson(row.items_json)?.length ?? 0 : Number(row.new_item_count),
1314
1881
  newItemCount: Number(row.new_item_count),
1315
1882
  summary: String(row.summary),
1316
- items: row.items_json ? parseJson(row.items_json) : undefined,
1883
+ entries: row.items_json ? parseJson(row.items_json) : undefined,
1317
1884
  createdAt: String(row.created_at),
1318
1885
  deliveredAt: row.delivered_at ? String(row.delivered_at) : null,
1319
1886
  };
@@ -1332,6 +1899,191 @@ class AgentInboxStore {
1332
1899
  createdAt: String(row.created_at),
1333
1900
  };
1334
1901
  }
1902
+ resolveInboxEntryId(inboxId, refOrItemId) {
1903
+ try {
1904
+ return (0, util_1.parseEntryRef)(refOrItemId);
1905
+ }
1906
+ catch {
1907
+ const row = this.getOne(`
1908
+ select e.entry_id
1909
+ from inbox_entries e
1910
+ join inbox_entry_items i on i.entry_id = e.entry_id
1911
+ where e.inbox_id = ?
1912
+ and i.item_id = ?
1913
+ order by e.sequence desc
1914
+ limit 1
1915
+ `, [inboxId, refOrItemId]);
1916
+ if (!row) {
1917
+ throw new Error(`unknown inbox entry: ${refOrItemId}`);
1918
+ }
1919
+ return String(row.entry_id);
1920
+ }
1921
+ }
1922
+ insertInboxEntryRecord(input) {
1923
+ const entryId = (0, util_1.generateCanonicalId)("ent");
1924
+ this.db.run(`
1925
+ insert into inbox_entries (
1926
+ entry_id, inbox_id, kind, sequence, thread_id, revision, group_key, resource_ref, event_family,
1927
+ item_json, count, summary, first_item_at, last_item_at, source_ids_json,
1928
+ subscription_ids_json, delivery_handle_json, acked_at, superseded_at, created_at
1929
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1930
+ `, [
1931
+ entryId,
1932
+ input.inboxId,
1933
+ input.kind,
1934
+ this.nextInboxEntrySequence(),
1935
+ input.threadId,
1936
+ input.revision,
1937
+ input.groupKey,
1938
+ input.resourceRef,
1939
+ input.eventFamily,
1940
+ input.itemJson,
1941
+ input.count,
1942
+ input.summary,
1943
+ input.firstItemAt,
1944
+ input.lastItemAt,
1945
+ input.sourceIdsJson,
1946
+ input.subscriptionIdsJson,
1947
+ input.deliveryHandleJson,
1948
+ input.ackedAt,
1949
+ input.supersededAt,
1950
+ input.createdAt,
1951
+ ]);
1952
+ return entryId;
1953
+ }
1954
+ nextInboxEntrySequence() {
1955
+ const row = this.getOne("select coalesce(max(sequence), 0) + 1 as next_sequence from inbox_entries");
1956
+ return Number(row?.next_sequence ?? 1);
1957
+ }
1958
+ listEntryItemIds(entryId) {
1959
+ const rows = this.getAll(`
1960
+ select iei.item_id
1961
+ from inbox_entry_items iei
1962
+ left join inbox_items ii on ii.item_id = iei.item_id
1963
+ where iei.entry_id = ?
1964
+ order by coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
1965
+ `, [entryId]);
1966
+ return rows.map((row) => String(row.item_id));
1967
+ }
1968
+ listEntryItemIdsBulk(entryIds) {
1969
+ const result = new Map();
1970
+ if (entryIds.length === 0) {
1971
+ return result;
1972
+ }
1973
+ const placeholders = entryIds.map(() => "?").join(", ");
1974
+ const rows = this.getAll(`
1975
+ select iei.entry_id, iei.item_id
1976
+ from inbox_entry_items iei
1977
+ left join inbox_items ii on ii.item_id = iei.item_id
1978
+ where iei.entry_id in (${placeholders})
1979
+ order by iei.entry_id asc, coalesce(ii.inbox_sequence, ii.rowid, iei.rowid) asc
1980
+ `, entryIds);
1981
+ for (const row of rows) {
1982
+ const entryId = String(row.entry_id);
1983
+ const itemIds = result.get(entryId) ?? [];
1984
+ itemIds.push(String(row.item_id));
1985
+ result.set(entryId, itemIds);
1986
+ }
1987
+ return result;
1988
+ }
1989
+ getRawItemIdsForEntryIds(entryIds) {
1990
+ const itemIds = new Set();
1991
+ for (const entryId of entryIds) {
1992
+ for (const itemId of this.listEntryItemIds(entryId)) {
1993
+ itemIds.add(itemId);
1994
+ }
1995
+ }
1996
+ return [...itemIds];
1997
+ }
1998
+ mapInboxEntries(rows) {
1999
+ const itemIdsByEntry = this.listEntryItemIdsBulk(rows.map((row) => String(row.entry_id)));
2000
+ return rows.map((row) => this.mapInboxEntry(row, itemIdsByEntry.get(String(row.entry_id)) ?? []));
2001
+ }
2002
+ syncInboxEntryAckState(inboxId, ackedAt) {
2003
+ this.db.run(`
2004
+ update inbox_entries
2005
+ set acked_at = ?
2006
+ where inbox_id = ?
2007
+ and acked_at is null
2008
+ and not exists (
2009
+ select 1
2010
+ from inbox_entry_items e
2011
+ join inbox_items i on i.item_id = e.item_id
2012
+ where e.entry_id = inbox_entries.entry_id
2013
+ and i.acked_at is null
2014
+ )
2015
+ `, [ackedAt, inboxId]);
2016
+ this.persist();
2017
+ }
2018
+ countAckedEntries(entryIds, ackedAt) {
2019
+ if (entryIds.length === 0) {
2020
+ return 0;
2021
+ }
2022
+ const placeholders = entryIds.map(() => "?").join(", ");
2023
+ const row = this.getOne(`select count(*) as count from inbox_entries where entry_id in (${placeholders}) and acked_at = ?`, [...entryIds, ackedAt]);
2024
+ return Number(row?.count ?? 0);
2025
+ }
2026
+ deleteInboxItemsAndEntryArtifacts(itemIds) {
2027
+ const placeholders = itemIds.map(() => "?").join(", ");
2028
+ this.inTransaction(() => {
2029
+ this.db.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
2030
+ this.db.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
2031
+ this.db.run(`
2032
+ delete from inbox_entries
2033
+ where entry_id in (
2034
+ select e.entry_id
2035
+ from inbox_entries e
2036
+ where not exists (
2037
+ select 1
2038
+ from inbox_entry_items ee
2039
+ where ee.entry_id = e.entry_id
2040
+ and exists (
2041
+ select 1
2042
+ from inbox_items ii
2043
+ where ii.item_id = ee.item_id
2044
+ )
2045
+ )
2046
+ )
2047
+ `);
2048
+ this.db.run(`
2049
+ delete from inbox_entry_items
2050
+ where not exists (
2051
+ select 1
2052
+ from inbox_items ii
2053
+ where ii.item_id = inbox_entry_items.item_id
2054
+ )
2055
+ `);
2056
+ this.db.run(`
2057
+ update digest_threads
2058
+ set latest_entry_id = (
2059
+ select e.entry_id
2060
+ from inbox_entries e
2061
+ where e.thread_id = digest_threads.thread_id
2062
+ order by coalesce(e.revision, 0) desc, e.entry_id desc
2063
+ limit 1
2064
+ ),
2065
+ latest_revision = coalesce((
2066
+ select max(coalesce(e.revision, 0))
2067
+ from inbox_entries e
2068
+ where e.thread_id = digest_threads.thread_id
2069
+ ), 0)
2070
+ `);
2071
+ this.db.run(`
2072
+ delete from digest_threads
2073
+ where not exists (
2074
+ select 1
2075
+ from digest_thread_items dti
2076
+ where dti.thread_id = digest_threads.thread_id
2077
+ )
2078
+ and not exists (
2079
+ select 1
2080
+ from inbox_entries e
2081
+ where e.thread_id = digest_threads.thread_id
2082
+ )
2083
+ `);
2084
+ });
2085
+ this.persist();
2086
+ }
1335
2087
  mapStream(row) {
1336
2088
  return {
1337
2089
  streamId: String(row.stream_id),
@@ -1374,3 +2126,16 @@ class AgentInboxStore {
1374
2126
  }
1375
2127
  }
1376
2128
  exports.AgentInboxStore = AgentInboxStore;
2129
+ function requiredText(row, key) {
2130
+ const value = row[key];
2131
+ if (value == null) {
2132
+ throw new Error(`missing required column: ${key}`);
2133
+ }
2134
+ return String(value);
2135
+ }
2136
+ function uniqueSorted(values) {
2137
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))].sort();
2138
+ }
2139
+ function summarizeBackfilledItemEntry(item) {
2140
+ return `${item.eventVariant} from ${item.sourceId}`;
2141
+ }