@holon-run/agentinbox 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -794,12 +794,12 @@ async function main() {
794
794
  }
795
795
  if (command === "inbox" && normalized[1] === "ack") {
796
796
  const args = normalized.slice(2);
797
- const itemId = takeFlagValue(normalized, "--entry");
797
+ const itemId = takeFlagValue(normalized, "--entry-id") ?? takeFlagValue(normalized, "--entry");
798
798
  const throughEntryId = takeFlagValue(normalized, "--through");
799
799
  const ackAll = hasFlag(normalized, "--all");
800
800
  const modeCount = Number(Boolean(itemId)) + Number(Boolean(throughEntryId)) + Number(ackAll);
801
- if (positionalArgs(args, ["--agent-id", "--entry", "--through"]).length > 0 || modeCount !== 1) {
802
- throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --all)");
801
+ if (positionalArgs(args, ["--agent-id", "--entry", "--entry-id", "--through"]).length > 0 || modeCount !== 1) {
802
+ throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)");
803
803
  }
804
804
  const selection = await selectAgentForCommand(client, {
805
805
  explicitAgentId: takeFlagValue(normalized, "--agent-id"),
@@ -1571,7 +1571,7 @@ Usage:
1571
1571
  agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N]
1572
1572
  agentinbox inbox send --agent-id ID --message TEXT [--sender SENDER]
1573
1573
  agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N]
1574
- agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --all)
1574
+ agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)
1575
1575
  agentinbox inbox policy show [--agent-id ID]
1576
1576
  agentinbox inbox policy set [--agent-id ID] [--enabled|--disabled] [--window-ms N] [--max-items N] [--max-thread-age-ms N]
1577
1577
  agentinbox inbox compact <agentId>
@@ -2255,17 +2255,15 @@ class AgentInboxService {
2255
2255
  return;
2256
2256
  }
2257
2257
  if (reason === "lease" && state.status === "notified") {
2258
- if (target.kind !== "terminal") {
2259
- // Webhook targets keep the existing lease-based behavior.
2260
- }
2261
- else {
2262
- this.store.upsertActivationDispatchState({
2263
- ...state,
2264
- leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
2265
- updatedAt: (0, util_1.nowIso)(),
2266
- });
2267
- return;
2268
- }
2258
+ // No new items since last notification — just renew lease for both
2259
+ // terminal and webhook targets. Re-dispatching would cause an infinite
2260
+ // wake loop (the agent already knows about these items).
2261
+ this.store.upsertActivationDispatchState({
2262
+ ...state,
2263
+ leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
2264
+ updatedAt: (0, util_1.nowIso)(),
2265
+ });
2266
+ return;
2269
2267
  }
2270
2268
  const dispatched = await this.dispatchActivationTarget({
2271
2269
  agentId,
package/dist/src/store.js CHANGED
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AgentInboxStore = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const sql_js_1 = __importDefault(require("sql.js"));
9
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
10
10
  const util_1 = require("./util");
11
11
  const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
12
12
  const V1_BASELINE_TAG = "0000_v1_initial";
@@ -30,35 +30,99 @@ function earlierLifecycleRetirement(left, right) {
30
30
  class AgentInboxStore {
31
31
  dbPath;
32
32
  db;
33
- static sqlPromise = null;
34
33
  constructor(dbPath, db) {
35
34
  this.dbPath = dbPath;
36
35
  this.db = db;
37
36
  }
38
37
  static async open(dbPath) {
39
38
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(dbPath), { recursive: true });
40
- const SQL = await this.loadSqlJs();
41
- let db = node_fs_1.default.existsSync(dbPath)
42
- ? new SQL.Database(node_fs_1.default.readFileSync(dbPath))
43
- : new SQL.Database();
39
+ const existedBeforeOpen = node_fs_1.default.existsSync(dbPath);
40
+ let db = await this.openDatabaseWithRecovery(dbPath);
44
41
  let store = new AgentInboxStore(dbPath, db);
45
- if (node_fs_1.default.existsSync(dbPath) && store.shouldArchivePreV1Database()) {
42
+ if (existedBeforeOpen && store.shouldArchivePreV1Database()) {
46
43
  const archivedPath = store.archivePreV1Database();
47
44
  console.warn(`[agentinbox] archived pre-v1 local database to ${archivedPath}; starting with a fresh v1 database (no data imported).`);
48
- db = new SQL.Database();
45
+ db = this.openDatabase(dbPath);
49
46
  store = new AgentInboxStore(dbPath, db);
50
47
  }
48
+ else if (existedBeforeOpen) {
49
+ await store.backupHealthyDatabase();
50
+ }
51
51
  store.migrate();
52
52
  store.persist();
53
53
  return store;
54
54
  }
55
- static async loadSqlJs() {
56
- if (!this.sqlPromise) {
57
- this.sqlPromise = (0, sql_js_1.default)({
58
- locateFile: (file) => require.resolve(`sql.js/dist/${file}`),
59
- });
55
+ static async openDatabaseWithRecovery(dbPath) {
56
+ try {
57
+ return this.openDatabase(dbPath);
58
+ }
59
+ catch (error) {
60
+ if (!node_fs_1.default.existsSync(dbPath)) {
61
+ throw error;
62
+ }
63
+ const backupPath = this.findRecoverableBackup(dbPath);
64
+ if (!backupPath) {
65
+ throw error;
66
+ }
67
+ const corruptPath = this.nextPath(`${dbPath}.corrupt`);
68
+ node_fs_1.default.renameSync(dbPath, corruptPath);
69
+ node_fs_1.default.copyFileSync(backupPath, dbPath);
70
+ console.warn(`[agentinbox] recovered local database from ${backupPath}; archived corrupt database to ${corruptPath}.`);
71
+ return this.openDatabase(dbPath);
72
+ }
73
+ }
74
+ static openDatabase(dbPath) {
75
+ const db = new better_sqlite3_1.default(dbPath);
76
+ db.pragma("busy_timeout = 5000");
77
+ db.pragma("journal_mode = WAL");
78
+ db.pragma("synchronous = NORMAL");
79
+ db.pragma("foreign_keys = ON");
80
+ this.assertHealthy(db, dbPath);
81
+ return db;
82
+ }
83
+ static assertHealthy(db, dbPath) {
84
+ const result = db.pragma("integrity_check", { simple: true });
85
+ if (result !== "ok") {
86
+ db.close();
87
+ throw new Error(`sqlite integrity_check failed for ${dbPath}: ${String(result)}`);
88
+ }
89
+ }
90
+ static findRecoverableBackup(dbPath) {
91
+ const candidates = this.listBackupCandidates(dbPath);
92
+ for (const candidate of candidates) {
93
+ try {
94
+ const db = new better_sqlite3_1.default(candidate, { readonly: true, fileMustExist: true });
95
+ this.assertHealthy(db, candidate);
96
+ db.close();
97
+ return candidate;
98
+ }
99
+ catch {
100
+ // Try the next backup candidate.
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ static listBackupCandidates(dbPath) {
106
+ const dir = node_path_1.default.dirname(dbPath);
107
+ const baseName = node_path_1.default.basename(dbPath);
108
+ const startupBackups = node_fs_1.default.existsSync(dir)
109
+ ? node_fs_1.default.readdirSync(dir)
110
+ .filter((name) => name.startsWith(`${baseName}.startup.`) && name.endsWith(".bak"))
111
+ .sort()
112
+ .reverse()
113
+ .map((name) => node_path_1.default.join(dir, name))
114
+ : [];
115
+ return [`${dbPath}.bak`, ...startupBackups].filter((candidate, index, all) => node_fs_1.default.existsSync(candidate) && all.indexOf(candidate) === index);
116
+ }
117
+ static nextPath(base) {
118
+ if (!node_fs_1.default.existsSync(base)) {
119
+ return base;
120
+ }
121
+ let suffix = 1;
122
+ while (node_fs_1.default.existsSync(`${base}.${suffix}`)) {
123
+ suffix += 1;
60
124
  }
61
- return this.sqlPromise;
125
+ return `${base}.${suffix}`;
62
126
  }
63
127
  close() {
64
128
  this.db.close();
@@ -66,6 +130,18 @@ class AgentInboxStore {
66
130
  save() {
67
131
  this.persist();
68
132
  }
133
+ async backupHealthyDatabase() {
134
+ const backupPath = `${this.dbPath}.bak`;
135
+ const tmpPath = `${backupPath}.${process.pid}.tmp`;
136
+ try {
137
+ node_fs_1.default.rmSync(tmpPath, { force: true });
138
+ await this.db.backup(tmpPath);
139
+ node_fs_1.default.renameSync(tmpPath, backupPath);
140
+ }
141
+ finally {
142
+ node_fs_1.default.rmSync(tmpPath, { force: true });
143
+ }
144
+ }
69
145
  migrate() {
70
146
  const migrations = this.loadSqlMigrations();
71
147
  this.ensureDrizzleMigrationsTable();
@@ -124,7 +200,7 @@ class AgentInboxStore {
124
200
  });
125
201
  }
126
202
  recordAppliedMigration(tag) {
127
- this.db.run(`insert or ignore into ${DRIZZLE_MIGRATIONS_TABLE} (tag, applied_at) values (?, ?)`, [tag, (0, util_1.nowIso)()]);
203
+ this.run(`insert or ignore into ${DRIZZLE_MIGRATIONS_TABLE} (tag, applied_at) values (?, ?)`, [tag, (0, util_1.nowIso)()]);
128
204
  }
129
205
  shouldArchivePreV1Database() {
130
206
  if (this.isV1Database()) {
@@ -174,8 +250,7 @@ class AgentInboxStore {
174
250
  return `${base}.${suffix}`;
175
251
  }
176
252
  persist() {
177
- const data = this.db.export();
178
- node_fs_1.default.writeFileSync(this.dbPath, Buffer.from(data));
253
+ // better-sqlite3 persists changes through SQLite's journaling layer.
179
254
  }
180
255
  inTransaction(fn) {
181
256
  this.db.exec("begin");
@@ -189,7 +264,7 @@ class AgentInboxStore {
189
264
  }
190
265
  }
191
266
  setUserVersion(version) {
192
- this.db.exec(`pragma user_version = ${version};`);
267
+ this.db.pragma(`user_version = ${version}`);
193
268
  }
194
269
  tableExists(name) {
195
270
  const row = this.getOne("select name from sqlite_master where type = 'table' and name = ?", [name]);
@@ -259,7 +334,7 @@ class AgentInboxStore {
259
334
  supersededAt: null,
260
335
  createdAt: item.occurredAt,
261
336
  });
262
- this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
337
+ this.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
263
338
  }
264
339
  });
265
340
  }
@@ -272,7 +347,7 @@ class AgentInboxStore {
272
347
  return row ? this.mapSourceHost(row) : null;
273
348
  }
274
349
  insertSourceHost(host) {
275
- this.db.run(`
350
+ this.run(`
276
351
  insert into source_hosts (
277
352
  host_id, host_type, host_key, config_ref, config_json, status, created_at, updated_at
278
353
  ) values (?, ?, ?, ?, ?, ?, ?, ?)
@@ -299,7 +374,7 @@ class AgentInboxStore {
299
374
  if (!current) {
300
375
  throw new Error(`unknown source host: ${hostId}`);
301
376
  }
302
- this.db.run(`
377
+ this.run(`
303
378
  update source_hosts
304
379
  set config_ref = ?, config_json = ?, updated_at = ?
305
380
  where host_id = ?
@@ -317,7 +392,7 @@ class AgentInboxStore {
317
392
  if (!current) {
318
393
  throw new Error(`unknown source host: ${hostId}`);
319
394
  }
320
- this.db.run(`
395
+ this.run(`
321
396
  update source_hosts
322
397
  set status = ?, updated_at = ?
323
398
  where host_id = ?
@@ -341,7 +416,7 @@ class AgentInboxStore {
341
416
  if (!source.hostId || !source.streamKind || !source.streamKey) {
342
417
  throw new Error("source insert requires hostId, streamKind, and streamKey");
343
418
  }
344
- this.db.run(`
419
+ this.run(`
345
420
  insert into sources (
346
421
  source_id, host_id, stream_kind, stream_key,
347
422
  source_type, source_key, config_ref, config_json,
@@ -379,7 +454,7 @@ class AgentInboxStore {
379
454
  return rows.map((row) => this.mapSourceIdleState(row));
380
455
  }
381
456
  upsertSourceIdleState(idleState) {
382
- this.db.run(`
457
+ this.run(`
383
458
  insert into source_idle_states (
384
459
  source_id, idle_since, auto_pause_at, auto_paused_at, updated_at
385
460
  ) values (?, ?, ?, ?, ?)
@@ -399,7 +474,7 @@ class AgentInboxStore {
399
474
  return this.getSourceIdleState(idleState.sourceId);
400
475
  }
401
476
  deleteSourceIdleState(sourceId) {
402
- this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
477
+ this.run("delete from source_idle_states where source_id = ?", [sourceId]);
403
478
  this.persist();
404
479
  }
405
480
  updateSourceDefinition(sourceId, input) {
@@ -407,7 +482,7 @@ class AgentInboxStore {
407
482
  if (!current) {
408
483
  throw new Error(`unknown source: ${sourceId}`);
409
484
  }
410
- this.db.run(`
485
+ this.run(`
411
486
  update sources
412
487
  set config_ref = ?, config_json = ?, updated_at = ?
413
488
  where source_id = ?
@@ -425,7 +500,7 @@ class AgentInboxStore {
425
500
  if (!current) {
426
501
  throw new Error(`unknown source: ${sourceId}`);
427
502
  }
428
- this.db.run(`
503
+ this.run(`
429
504
  update sources
430
505
  set status = ?, checkpoint = ?, updated_at = ?
431
506
  where source_id = ?
@@ -448,20 +523,20 @@ class AgentInboxStore {
448
523
  const consumers = this.getAll("select consumer_id from consumers where stream_id = ?", [stream.streamId]);
449
524
  for (const row of consumers) {
450
525
  const consumerId = String(row.consumer_id);
451
- this.db.run("delete from consumer_commits where consumer_id = ?", [consumerId]);
526
+ this.run("delete from consumer_commits where consumer_id = ?", [consumerId]);
452
527
  }
453
- this.db.run("delete from consumers where stream_id = ?", [stream.streamId]);
454
- this.db.run("delete from stream_events where stream_id = ?", [stream.streamId]);
455
- this.db.run("delete from streams where stream_id = ?", [stream.streamId]);
528
+ this.run("delete from consumers where stream_id = ?", [stream.streamId]);
529
+ this.run("delete from stream_events where stream_id = ?", [stream.streamId]);
530
+ this.run("delete from streams where stream_id = ?", [stream.streamId]);
456
531
  }
457
- this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
458
- this.db.run("delete from sources where source_id = ?", [sourceId]);
532
+ this.run("delete from source_idle_states where source_id = ?", [sourceId]);
533
+ this.run("delete from sources where source_id = ?", [sourceId]);
459
534
  const hostId = source.hostId ?? null;
460
535
  const siblings = hostId
461
536
  ? this.getOne("select count(*) as count from sources where host_id = ?", [hostId])
462
537
  : null;
463
538
  if (hostId && Number(siblings?.count ?? 0) === 0) {
464
- this.db.run("delete from source_hosts where host_id = ?", [hostId]);
539
+ this.run("delete from source_hosts where host_id = ?", [hostId]);
465
540
  }
466
541
  });
467
542
  this.persist();
@@ -472,7 +547,7 @@ class AgentInboxStore {
472
547
  return row ? this.mapAgent(row) : null;
473
548
  }
474
549
  insertAgent(agent) {
475
- this.db.run(`
550
+ this.run(`
476
551
  insert into agents (
477
552
  agent_id, status, offline_since, runtime_kind, runtime_session_id, created_at, updated_at, last_seen_at
478
553
  ) values (?, ?, ?, ?, ?, ?, ?, ?)
@@ -493,7 +568,7 @@ class AgentInboxStore {
493
568
  if (!current) {
494
569
  throw new Error(`unknown agent: ${agentId}`);
495
570
  }
496
- this.db.run(`
571
+ this.run(`
497
572
  update agents
498
573
  set status = ?, offline_since = ?, runtime_kind = ?, runtime_session_id = ?, updated_at = ?, last_seen_at = ?
499
574
  where agent_id = ?
@@ -524,7 +599,7 @@ class AgentInboxStore {
524
599
  return row ? this.mapInbox(row) : null;
525
600
  }
526
601
  insertInbox(inbox) {
527
- this.db.run(`
602
+ this.run(`
528
603
  insert into inboxes (
529
604
  inbox_id, owner_agent_id, aggregation_enabled, aggregation_window_ms,
530
605
  aggregation_max_items, aggregation_max_thread_age_ms, created_at
@@ -545,7 +620,7 @@ class AgentInboxStore {
545
620
  if (!current) {
546
621
  throw new Error(`unknown inbox: ${inboxId}`);
547
622
  }
548
- this.db.run(`
623
+ this.run(`
549
624
  update inboxes
550
625
  set aggregation_enabled = ?,
551
626
  aggregation_window_ms = ?,
@@ -573,7 +648,7 @@ class AgentInboxStore {
573
648
  return row ? this.mapSubscription(row) : null;
574
649
  }
575
650
  insertSubscription(subscription) {
576
- this.db.run(`
651
+ this.run(`
577
652
  insert into subscriptions (
578
653
  subscription_id, agent_id, source_id, filter_json, tracked_resource_ref, cleanup_policy_json, start_policy, start_offset, start_time, created_at
579
654
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -644,10 +719,10 @@ class AgentInboxStore {
644
719
  return null;
645
720
  }
646
721
  this.inTransaction(() => {
647
- this.db.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [subscriptionId]);
648
- this.db.run("delete from consumers where subscription_id = ?", [subscriptionId]);
649
- this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
650
- this.db.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
722
+ this.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [subscriptionId]);
723
+ this.run("delete from consumers where subscription_id = ?", [subscriptionId]);
724
+ this.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
725
+ this.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
651
726
  });
652
727
  this.persist();
653
728
  return subscription;
@@ -669,7 +744,7 @@ class AgentInboxStore {
669
744
  const next = existing
670
745
  ? earlierLifecycleRetirement(existing, retirement)
671
746
  : retirement;
672
- this.db.run(`
747
+ this.run(`
673
748
  insert into subscription_lifecycle_retirements (
674
749
  subscription_id, host_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
675
750
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -697,7 +772,7 @@ class AgentInboxStore {
697
772
  return this.getSubscriptionLifecycleRetirement(retirement.subscriptionId);
698
773
  }
699
774
  deleteSubscriptionLifecycleRetirement(subscriptionId) {
700
- this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
775
+ this.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
701
776
  this.persist();
702
777
  }
703
778
  getActivationTarget(targetId) {
@@ -726,7 +801,7 @@ class AgentInboxStore {
726
801
  }
727
802
  insertActivationTarget(target) {
728
803
  if (target.kind === "webhook") {
729
- this.db.run(`
804
+ this.run(`
730
805
  insert into activation_targets (
731
806
  target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error,
732
807
  mode, notify_lease_ms, min_unacked_items, url, created_at, updated_at, last_seen_at
@@ -750,7 +825,7 @@ class AgentInboxStore {
750
825
  ]);
751
826
  }
752
827
  else {
753
- this.db.run(`
828
+ this.run(`
754
829
  insert into activation_targets (
755
830
  target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms, min_unacked_items,
756
831
  runtime_kind, runtime_session_id, runtime_pid, backend, tmux_pane_id, tty, term_program, iterm_session_id,
@@ -784,7 +859,7 @@ class AgentInboxStore {
784
859
  this.persist();
785
860
  }
786
861
  updateTerminalActivationTargetHeartbeat(targetId, input) {
787
- this.db.run(`
862
+ this.run(`
788
863
  update activation_targets
789
864
  set status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
790
865
  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 = ?
@@ -811,7 +886,7 @@ class AgentInboxStore {
811
886
  return target;
812
887
  }
813
888
  updateWebhookActivationTarget(targetId, input) {
814
- this.db.run(`
889
+ this.run(`
815
890
  update activation_targets
816
891
  set kind = 'webhook', status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
817
892
  mode = ?, notify_lease_ms = ?, min_unacked_items = ?, url = ?, updated_at = ?, last_seen_at = ?
@@ -837,7 +912,7 @@ class AgentInboxStore {
837
912
  if (!current) {
838
913
  throw new Error(`unknown activation target: ${targetId}`);
839
914
  }
840
- this.db.run(`
915
+ this.run(`
841
916
  update activation_targets
842
917
  set status = ?, offline_since = ?, consecutive_failures = ?, last_delivered_at = ?, last_error = ?, updated_at = ?, last_seen_at = ?
843
918
  where target_id = ?
@@ -868,8 +943,8 @@ class AgentInboxStore {
868
943
  }
869
944
  deleteActivationTarget(agentId, targetId) {
870
945
  this.inTransaction(() => {
871
- this.db.run("delete from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
872
- this.db.run("delete from activation_targets where agent_id = ? and target_id = ?", [agentId, targetId]);
946
+ this.run("delete from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
947
+ this.run("delete from activation_targets where agent_id = ? and target_id = ?", [agentId, targetId]);
873
948
  });
874
949
  this.persist();
875
950
  }
@@ -878,26 +953,26 @@ class AgentInboxStore {
878
953
  const subscriptions = this.listSubscriptionsForAgent(agentId);
879
954
  this.inTransaction(() => {
880
955
  for (const subscription of subscriptions) {
881
- this.db.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [
956
+ this.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [
882
957
  subscription.subscriptionId,
883
958
  ]);
884
- this.db.run("delete from consumers where subscription_id = ?", [subscription.subscriptionId]);
959
+ this.run("delete from consumers where subscription_id = ?", [subscription.subscriptionId]);
885
960
  }
886
- this.db.run("delete from subscription_lifecycle_retirements where subscription_id in (select subscription_id from subscriptions where agent_id = ?)", [agentId]);
887
- this.db.run("delete from source_idle_states where source_id in (select distinct source_id from subscriptions where agent_id = ?)", [agentId]);
888
- this.db.run("delete from timers where agent_id = ?", [agentId]);
889
- this.db.run("delete from subscriptions where agent_id = ?", [agentId]);
890
- this.db.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
891
- this.db.run("delete from activation_targets where agent_id = ?", [agentId]);
961
+ this.run("delete from subscription_lifecycle_retirements where subscription_id in (select subscription_id from subscriptions where agent_id = ?)", [agentId]);
962
+ this.run("delete from source_idle_states where source_id in (select distinct source_id from subscriptions where agent_id = ?)", [agentId]);
963
+ this.run("delete from timers where agent_id = ?", [agentId]);
964
+ this.run("delete from subscriptions where agent_id = ?", [agentId]);
965
+ this.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
966
+ this.run("delete from activation_targets where agent_id = ?", [agentId]);
892
967
  if (inbox) {
893
- this.db.run("delete from inbox_items where inbox_id = ?", [inbox.inboxId]);
894
- this.db.run("delete from activations where agent_id = ? or inbox_id = ?", [agentId, inbox.inboxId]);
895
- this.db.run("delete from inboxes where inbox_id = ?", [inbox.inboxId]);
968
+ this.run("delete from inbox_items where inbox_id = ?", [inbox.inboxId]);
969
+ this.run("delete from activations where agent_id = ? or inbox_id = ?", [agentId, inbox.inboxId]);
970
+ this.run("delete from inboxes where inbox_id = ?", [inbox.inboxId]);
896
971
  }
897
972
  else {
898
- this.db.run("delete from activations where agent_id = ?", [agentId]);
973
+ this.run("delete from activations where agent_id = ?", [agentId]);
899
974
  }
900
- this.db.run("delete from agents where agent_id = ?", [agentId]);
975
+ this.run("delete from agents where agent_id = ?", [agentId]);
901
976
  });
902
977
  if (options?.persist !== false) {
903
978
  this.persist();
@@ -912,7 +987,7 @@ class AgentInboxStore {
912
987
  return rows.map((row) => this.mapAgent(row));
913
988
  }
914
989
  insertTimer(timer) {
915
- this.db.run(`
990
+ this.run(`
916
991
  insert into timers (
917
992
  schedule_id, agent_id, status, mode, at, interval_ms, cron_expr, timezone,
918
993
  message, sender, next_fire_at, last_fired_at, created_at, updated_at
@@ -964,7 +1039,7 @@ class AgentInboxStore {
964
1039
  if (!current) {
965
1040
  throw new Error(`unknown timer: ${scheduleId}`);
966
1041
  }
967
- this.db.run(`
1042
+ this.run(`
968
1043
  update timers
969
1044
  set status = ?, mode = ?, at = ?, interval_ms = ?, cron_expr = ?, timezone = ?,
970
1045
  message = ?, sender = ?, next_fire_at = ?, last_fired_at = ?, updated_at = ?
@@ -991,7 +1066,7 @@ class AgentInboxStore {
991
1066
  if (!current) {
992
1067
  return null;
993
1068
  }
994
- this.db.run("delete from timers where schedule_id = ?", [scheduleId]);
1069
+ this.run("delete from timers where schedule_id = ?", [scheduleId]);
995
1070
  this.persist();
996
1071
  return current;
997
1072
  }
@@ -1008,7 +1083,7 @@ class AgentInboxStore {
1008
1083
  return rows.map((row) => this.mapActivationDispatchState(row));
1009
1084
  }
1010
1085
  upsertActivationDispatchState(state) {
1011
- this.db.run(`
1086
+ this.run(`
1012
1087
  insert into activation_dispatch_states (
1013
1088
  agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, defer_reason, defer_attempts,
1014
1089
  first_deferred_at, last_deferred_at, pending_fingerprint, pending_new_item_count, pending_summary,
@@ -1048,12 +1123,11 @@ class AgentInboxStore {
1048
1123
  this.persist();
1049
1124
  }
1050
1125
  deleteActivationDispatchState(agentId, targetId) {
1051
- this.db.run("delete from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
1126
+ this.run("delete from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
1052
1127
  this.persist();
1053
1128
  }
1054
1129
  insertInboxItem(item) {
1055
- const before = this.changes();
1056
- this.db.run(`
1130
+ const result = this.run(`
1057
1131
  insert or ignore into inbox_items (
1058
1132
  item_id, source_id, source_native_id, event_variant, inbox_id, occurred_at,
1059
1133
  metadata_json, raw_payload_json, provider_raw_payload_json, delivery_handle_json, acked_at
@@ -1071,10 +1145,10 @@ class AgentInboxStore {
1071
1145
  item.deliveryHandle ? JSON.stringify(item.deliveryHandle) : null,
1072
1146
  item.ackedAt ?? null,
1073
1147
  ]);
1074
- const inserted = this.changes() > before;
1148
+ const inserted = result.changes > 0;
1075
1149
  if (inserted) {
1076
- const rowId = this.lastInsertRowId();
1077
- this.db.run(`
1150
+ const rowId = Number(result.lastInsertRowid);
1151
+ this.run(`
1078
1152
  update inbox_items
1079
1153
  set inbox_sequence = ?
1080
1154
  where rowid = ? and inbox_sequence is null
@@ -1115,7 +1189,7 @@ class AgentInboxStore {
1115
1189
  supersededAt: null,
1116
1190
  createdAt: (0, util_1.nowIso)(),
1117
1191
  });
1118
- this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
1192
+ this.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, item.itemId]);
1119
1193
  this.persist();
1120
1194
  return this.getInboxEntry(entryId);
1121
1195
  }
@@ -1183,7 +1257,7 @@ class AgentInboxStore {
1183
1257
  }
1184
1258
  createDigestThread(input) {
1185
1259
  const threadId = (0, util_1.generateCanonicalId)("thr");
1186
- this.db.run(`
1260
+ this.run(`
1187
1261
  insert into digest_threads (
1188
1262
  thread_id, inbox_id, source_id, group_key, resource_ref, event_family, latest_revision,
1189
1263
  latest_entry_id, status, summary, first_item_at, last_item_at, flush_after_at,
@@ -1212,8 +1286,8 @@ class AgentInboxStore {
1212
1286
  }
1213
1287
  addItemToDigestThread(threadId, itemId, occurredAt, summary, flushAfterAt) {
1214
1288
  this.inTransaction(() => {
1215
- this.db.run("insert or ignore into digest_thread_items (thread_id, item_id) values (?, ?)", [threadId, itemId]);
1216
- this.db.run(`
1289
+ this.run("insert or ignore into digest_thread_items (thread_id, item_id) values (?, ?)", [threadId, itemId]);
1290
+ this.run(`
1217
1291
  update digest_threads
1218
1292
  set summary = ?, last_item_at = ?, flush_after_at = ?, updated_at = ?
1219
1293
  where thread_id = ?
@@ -1259,7 +1333,7 @@ class AgentInboxStore {
1259
1333
  }
1260
1334
  this.inTransaction(() => {
1261
1335
  if (thread.latestEntryId != null) {
1262
- this.db.run("update inbox_entries set superseded_at = ? where entry_id = ? and superseded_at is null", [input.createdAt, thread.latestEntryId]);
1336
+ this.run("update inbox_entries set superseded_at = ? where entry_id = ? and superseded_at is null", [input.createdAt, thread.latestEntryId]);
1263
1337
  }
1264
1338
  const nextRevision = thread.latestRevision + 1;
1265
1339
  const entryId = this.insertInboxEntryRecord({
@@ -1283,9 +1357,9 @@ class AgentInboxStore {
1283
1357
  createdAt: input.createdAt,
1284
1358
  });
1285
1359
  for (const itemId of input.itemIds) {
1286
- this.db.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, itemId]);
1360
+ this.run("insert or ignore into inbox_entry_items (entry_id, item_id) values (?, ?)", [entryId, itemId]);
1287
1361
  }
1288
- this.db.run(`
1362
+ this.run(`
1289
1363
  update digest_threads
1290
1364
  set latest_revision = ?, latest_entry_id = ?, summary = ?, first_item_at = ?, last_item_at = ?,
1291
1365
  flush_after_at = null, updated_at = ?
@@ -1296,7 +1370,7 @@ class AgentInboxStore {
1296
1370
  return this.getInboxEntry(this.getDigestThread(input.threadId).latestEntryId);
1297
1371
  }
1298
1372
  closeDigestThread(threadId, updatedAt) {
1299
- this.db.run("update digest_threads set status = 'closed', flush_after_at = null, updated_at = ? where thread_id = ?", [updatedAt, threadId]);
1373
+ this.run("update digest_threads set status = 'closed', flush_after_at = null, updated_at = ? where thread_id = ?", [updatedAt, threadId]);
1300
1374
  this.persist();
1301
1375
  }
1302
1376
  listDigestThreadsForRawItems(itemIds) {
@@ -1330,12 +1404,12 @@ class AgentInboxStore {
1330
1404
  ackItems(inboxId, itemIds, ackedAt) {
1331
1405
  let changes = 0;
1332
1406
  for (const itemId of itemIds) {
1333
- this.db.run(`
1407
+ const result = this.run(`
1334
1408
  update inbox_items
1335
1409
  set acked_at = ?
1336
1410
  where inbox_id = ? and item_id = ? and acked_at is null
1337
1411
  `, [ackedAt, inboxId, itemId]);
1338
- changes += this.changes();
1412
+ changes += result.changes;
1339
1413
  }
1340
1414
  if (changes > 0) {
1341
1415
  this.persist();
@@ -1347,14 +1421,14 @@ class AgentInboxStore {
1347
1421
  if (!anchor) {
1348
1422
  throw new Error(`unknown inbox item: ${itemId}`);
1349
1423
  }
1350
- this.db.run(`
1424
+ const result = this.run(`
1351
1425
  update inbox_items
1352
1426
  set acked_at = ?
1353
1427
  where inbox_id = ?
1354
1428
  and acked_at is null
1355
1429
  and coalesce(inbox_sequence, rowid) <= ?
1356
1430
  `, [ackedAt, inboxId, Number(anchor.inbox_sequence)]);
1357
- const changes = this.changes();
1431
+ const changes = result.changes;
1358
1432
  if (changes > 0) {
1359
1433
  this.persist();
1360
1434
  }
@@ -1397,7 +1471,7 @@ class AgentInboxStore {
1397
1471
  return Number(row?.count ?? 0);
1398
1472
  }
1399
1473
  insertActivation(activation) {
1400
- this.db.run(`
1474
+ this.run(`
1401
1475
  insert into activations (
1402
1476
  activation_id, kind, agent_id, inbox_id, target_id, target_kind,
1403
1477
  subscription_ids_json, source_ids_json, latest_entry_id, new_entry_count, new_item_count, summary, items_json, created_at, delivered_at
@@ -1426,7 +1500,7 @@ class AgentInboxStore {
1426
1500
  return rows.map((row) => this.mapActivation(row));
1427
1501
  }
1428
1502
  insertDelivery(delivery) {
1429
- this.db.run(`
1503
+ this.run(`
1430
1504
  insert into deliveries (
1431
1505
  delivery_id, provider, surface, target_ref, thread_ref, reply_mode,
1432
1506
  kind, payload_json, status, created_at
@@ -1450,7 +1524,7 @@ class AgentInboxStore {
1450
1524
  return rows.map((row) => this.mapDelivery(row));
1451
1525
  }
1452
1526
  insertStream(stream) {
1453
- this.db.run("insert into streams (stream_id, source_id, stream_key, backend, created_at) values (?, ?, ?, ?, ?)", [stream.streamId, stream.sourceId, stream.streamKey, stream.backend, stream.createdAt]);
1527
+ this.run("insert into streams (stream_id, source_id, stream_key, backend, created_at) values (?, ?, ?, ?, ?)", [stream.streamId, stream.sourceId, stream.streamKey, stream.backend, stream.createdAt]);
1454
1528
  this.persist();
1455
1529
  }
1456
1530
  getStream(streamId) {
@@ -1474,8 +1548,7 @@ class AgentInboxStore {
1474
1548
  const providerRawPayloadJson = event.providerRawPayload ? JSON.stringify(event.providerRawPayload) : null;
1475
1549
  const deliveryHandleJson = event.deliveryHandle ? JSON.stringify(event.deliveryHandle) : null;
1476
1550
  for (let attempt = 0; attempt < 5; attempt += 1) {
1477
- const before = this.changes();
1478
- this.db.run(`
1551
+ const result = this.run(`
1479
1552
  insert or ignore into stream_events (
1480
1553
  stream_event_id, stream_id, source_id, source_native_id, event_variant,
1481
1554
  occurred_at, metadata_json, raw_payload_json, provider_raw_payload_json, delivery_handle_json, created_at
@@ -1493,12 +1566,12 @@ class AgentInboxStore {
1493
1566
  deliveryHandleJson,
1494
1567
  (0, util_1.nowIso)(),
1495
1568
  ]);
1496
- if (this.changes() > before) {
1569
+ if (result.changes > 0) {
1497
1570
  this.persist();
1498
1571
  return {
1499
1572
  appended: 1,
1500
1573
  deduped: 0,
1501
- lastOffset: this.lastInsertRowId(),
1574
+ lastOffset: Number(result.lastInsertRowid),
1502
1575
  };
1503
1576
  }
1504
1577
  const existing = this.getOne(`
@@ -1551,7 +1624,7 @@ class AgentInboxStore {
1551
1624
  return Number(row?.count ?? 0);
1552
1625
  }
1553
1626
  insertConsumer(consumer) {
1554
- this.db.run(`
1627
+ this.run(`
1555
1628
  insert into consumers (
1556
1629
  consumer_id, stream_id, subscription_id, consumer_key, start_policy,
1557
1630
  start_offset, start_time, next_offset, created_at, updated_at
@@ -1584,8 +1657,8 @@ class AgentInboxStore {
1584
1657
  }
1585
1658
  deleteConsumer(consumerId) {
1586
1659
  this.inTransaction(() => {
1587
- this.db.run("delete from consumer_commits where consumer_id = ?", [consumerId]);
1588
- this.db.run("delete from consumers where consumer_id = ?", [consumerId]);
1660
+ this.run("delete from consumer_commits where consumer_id = ?", [consumerId]);
1661
+ this.run("delete from consumers where consumer_id = ?", [consumerId]);
1589
1662
  });
1590
1663
  this.persist();
1591
1664
  }
@@ -1596,8 +1669,8 @@ class AgentInboxStore {
1596
1669
  }
1597
1670
  const updatedAt = (0, util_1.nowIso)();
1598
1671
  this.inTransaction(() => {
1599
- this.db.run("update consumers set next_offset = ?, updated_at = ? where consumer_id = ?", [nextOffset, updatedAt, consumerId]);
1600
- this.db.run(`
1672
+ this.run("update consumers set next_offset = ?, updated_at = ? where consumer_id = ?", [nextOffset, updatedAt, consumerId]);
1673
+ this.run(`
1601
1674
  insert into consumer_commits (
1602
1675
  commit_id, consumer_id, stream_id, committed_offset, committed_at
1603
1676
  ) values (?, ?, ?, ?, ?)
@@ -1607,7 +1680,7 @@ class AgentInboxStore {
1607
1680
  return this.getConsumer(consumerId);
1608
1681
  }
1609
1682
  resetConsumer(consumerId, input) {
1610
- this.db.run(`
1683
+ this.run(`
1611
1684
  update consumers
1612
1685
  set start_policy = ?, start_offset = ?, start_time = ?, next_offset = ?, updated_at = ?
1613
1686
  where consumer_id = ?
@@ -1654,32 +1727,14 @@ class AgentInboxStore {
1654
1727
  const row = this.getOne("select last_insert_rowid() as rowid");
1655
1728
  return Number(row?.rowid ?? 0);
1656
1729
  }
1730
+ run(sql, params = []) {
1731
+ return this.db.prepare(sql).run(...params);
1732
+ }
1657
1733
  getOne(sql, params = []) {
1658
- const statement = this.db.prepare(sql);
1659
- try {
1660
- statement.bind(params);
1661
- if (!statement.step()) {
1662
- return undefined;
1663
- }
1664
- return statement.getAsObject();
1665
- }
1666
- finally {
1667
- statement.free();
1668
- }
1734
+ return this.db.prepare(sql).get(...params);
1669
1735
  }
1670
1736
  getAll(sql, params = []) {
1671
- const statement = this.db.prepare(sql);
1672
- try {
1673
- statement.bind(params);
1674
- const rows = [];
1675
- while (statement.step()) {
1676
- rows.push(statement.getAsObject());
1677
- }
1678
- return rows;
1679
- }
1680
- finally {
1681
- statement.free();
1682
- }
1737
+ return this.db.prepare(sql).all(...params);
1683
1738
  }
1684
1739
  mapSource(row) {
1685
1740
  const hostId = requiredText(row, "host_id");
@@ -2006,7 +2061,7 @@ class AgentInboxStore {
2006
2061
  }
2007
2062
  insertInboxEntryRecord(input) {
2008
2063
  const entryId = (0, util_1.generateCanonicalId)("ent");
2009
- this.db.run(`
2064
+ this.run(`
2010
2065
  insert into inbox_entries (
2011
2066
  entry_id, inbox_id, kind, sequence, thread_id, revision, group_key, resource_ref, event_family,
2012
2067
  item_json, count, summary, first_item_at, last_item_at, source_ids_json,
@@ -2085,7 +2140,7 @@ class AgentInboxStore {
2085
2140
  return rows.map((row) => this.mapInboxEntry(row, itemIdsByEntry.get(String(row.entry_id)) ?? []));
2086
2141
  }
2087
2142
  syncInboxEntryAckState(inboxId, ackedAt) {
2088
- this.db.run(`
2143
+ this.run(`
2089
2144
  update inbox_entries
2090
2145
  set acked_at = ?
2091
2146
  where inbox_id = ?
@@ -2111,9 +2166,9 @@ class AgentInboxStore {
2111
2166
  deleteInboxItemsAndEntryArtifacts(itemIds) {
2112
2167
  const placeholders = itemIds.map(() => "?").join(", ");
2113
2168
  this.inTransaction(() => {
2114
- this.db.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
2115
- this.db.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
2116
- this.db.run(`
2169
+ this.run(`delete from digest_thread_items where item_id in (${placeholders})`, itemIds);
2170
+ this.run(`delete from inbox_items where item_id in (${placeholders})`, itemIds);
2171
+ this.run(`
2117
2172
  delete from inbox_entries
2118
2173
  where entry_id in (
2119
2174
  select e.entry_id
@@ -2130,7 +2185,7 @@ class AgentInboxStore {
2130
2185
  )
2131
2186
  )
2132
2187
  `);
2133
- this.db.run(`
2188
+ this.run(`
2134
2189
  delete from inbox_entry_items
2135
2190
  where not exists (
2136
2191
  select 1
@@ -2138,7 +2193,7 @@ class AgentInboxStore {
2138
2193
  where ii.item_id = inbox_entry_items.item_id
2139
2194
  )
2140
2195
  `);
2141
- this.db.run(`
2196
+ this.run(`
2142
2197
  update digest_threads
2143
2198
  set latest_entry_id = (
2144
2199
  select e.entry_id
@@ -2153,7 +2208,7 @@ class AgentInboxStore {
2153
2208
  where e.thread_id = digest_threads.thread_id
2154
2209
  ), 0)
2155
2210
  `);
2156
- this.db.run(`
2211
+ this.run(`
2157
2212
  delete from digest_threads
2158
2213
  where not exists (
2159
2214
  select 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,14 +45,14 @@
45
45
  "dependencies": {
46
46
  "@fastify/swagger": "^9.5.2",
47
47
  "@holon-run/uxc-daemon-client": "^0.15.4",
48
+ "better-sqlite3": "^12.10.0",
48
49
  "commander": "^14.0.3",
49
50
  "fastify": "^5.6.1",
50
- "jexl": "^2.3.0",
51
- "sql.js": "^1.13.0"
51
+ "jexl": "^2.3.0"
52
52
  },
53
53
  "devDependencies": {
54
+ "@types/better-sqlite3": "^7.6.13",
54
55
  "@types/node": "^24.5.2",
55
- "@types/sql.js": "^1.4.9",
56
56
  "drizzle-kit": "1.0.0-beta.21",
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "mdorigin": "^0.5.0",