@holon-run/agentinbox 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/store.js CHANGED
@@ -16,6 +16,17 @@ function parseJson(value) {
16
16
  }
17
17
  return JSON.parse(value);
18
18
  }
19
+ function earlierLifecycleRetirement(left, right) {
20
+ const leftAt = Date.parse(left.retireAt);
21
+ const rightAt = Date.parse(right.retireAt);
22
+ if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt <= rightAt) {
23
+ return {
24
+ ...left,
25
+ updatedAt: right.updatedAt,
26
+ };
27
+ }
28
+ return right;
29
+ }
19
30
  class AgentInboxStore {
20
31
  dbPath;
21
32
  db;
@@ -193,6 +204,38 @@ class AgentInboxStore {
193
204
  const rows = this.getAll("select * from sources order by created_at asc");
194
205
  return rows.map((row) => this.mapSource(row));
195
206
  }
207
+ getSourceIdleState(sourceId) {
208
+ const row = this.getOne("select * from source_idle_states where source_id = ?", [sourceId]);
209
+ return row ? this.mapSourceIdleState(row) : null;
210
+ }
211
+ listSourceIdleStatesDue(cutoffIso) {
212
+ const rows = this.getAll("select * from source_idle_states where auto_pause_at <= ? and auto_paused_at is null order by auto_pause_at asc", [cutoffIso]);
213
+ return rows.map((row) => this.mapSourceIdleState(row));
214
+ }
215
+ upsertSourceIdleState(idleState) {
216
+ this.db.run(`
217
+ insert into source_idle_states (
218
+ source_id, idle_since, auto_pause_at, auto_paused_at, updated_at
219
+ ) values (?, ?, ?, ?, ?)
220
+ on conflict(source_id) do update set
221
+ idle_since = excluded.idle_since,
222
+ auto_pause_at = excluded.auto_pause_at,
223
+ auto_paused_at = excluded.auto_paused_at,
224
+ updated_at = excluded.updated_at
225
+ `, [
226
+ idleState.sourceId,
227
+ idleState.idleSince,
228
+ idleState.autoPauseAt,
229
+ idleState.autoPausedAt ?? null,
230
+ idleState.updatedAt,
231
+ ]);
232
+ this.persist();
233
+ return this.getSourceIdleState(idleState.sourceId);
234
+ }
235
+ deleteSourceIdleState(sourceId) {
236
+ this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
237
+ this.persist();
238
+ }
196
239
  updateSourceDefinition(sourceId, input) {
197
240
  const current = this.getSource(sourceId);
198
241
  if (!current) {
@@ -245,6 +288,7 @@ class AgentInboxStore {
245
288
  this.db.run("delete from stream_events where stream_id = ?", [stream.streamId]);
246
289
  this.db.run("delete from streams where stream_id = ?", [stream.streamId]);
247
290
  }
291
+ this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
248
292
  this.db.run("delete from sources where source_id = ?", [sourceId]);
249
293
  });
250
294
  this.persist();
@@ -319,15 +363,15 @@ class AgentInboxStore {
319
363
  insertSubscription(subscription) {
320
364
  this.db.run(`
321
365
  insert into subscriptions (
322
- subscription_id, agent_id, source_id, filter_json, lifecycle_mode, expires_at, start_policy, start_offset, start_time, created_at
366
+ subscription_id, agent_id, source_id, filter_json, tracked_resource_ref, cleanup_policy_json, start_policy, start_offset, start_time, created_at
323
367
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
324
368
  `, [
325
369
  subscription.subscriptionId,
326
370
  subscription.agentId,
327
371
  subscription.sourceId,
328
372
  JSON.stringify(subscription.filter),
329
- subscription.lifecycleMode,
330
- subscription.expiresAt ?? null,
373
+ subscription.trackedResourceRef ?? null,
374
+ JSON.stringify(subscription.cleanupPolicy),
331
375
  subscription.startPolicy,
332
376
  subscription.startOffset ?? null,
333
377
  subscription.startTime ?? null,
@@ -352,10 +396,63 @@ class AgentInboxStore {
352
396
  if (!subscription) {
353
397
  return null;
354
398
  }
355
- this.db.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
399
+ this.inTransaction(() => {
400
+ this.db.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [subscriptionId]);
401
+ this.db.run("delete from consumers where subscription_id = ?", [subscriptionId]);
402
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
403
+ this.db.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
404
+ });
356
405
  this.persist();
357
406
  return subscription;
358
407
  }
408
+ getSubscriptionLifecycleRetirement(subscriptionId) {
409
+ const row = this.getOne("select * from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
410
+ return row ? this.mapSubscriptionLifecycleRetirement(row) : null;
411
+ }
412
+ listSubscriptionLifecycleRetirements() {
413
+ const rows = this.getAll("select * from subscription_lifecycle_retirements order by retire_at asc, created_at asc");
414
+ return rows.map((row) => this.mapSubscriptionLifecycleRetirement(row));
415
+ }
416
+ listSubscriptionLifecycleRetirementsDue(cutoffIso) {
417
+ const rows = this.getAll("select * from subscription_lifecycle_retirements where retire_at <= ? order by retire_at asc, created_at asc", [cutoffIso]);
418
+ return rows.map((row) => this.mapSubscriptionLifecycleRetirement(row));
419
+ }
420
+ upsertSubscriptionLifecycleRetirement(retirement) {
421
+ const existing = this.getSubscriptionLifecycleRetirement(retirement.subscriptionId);
422
+ const next = existing
423
+ ? earlierLifecycleRetirement(existing, retirement)
424
+ : retirement;
425
+ this.db.run(`
426
+ 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
428
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
429
+ on conflict(subscription_id) do update set
430
+ source_id = excluded.source_id,
431
+ tracked_resource_ref = excluded.tracked_resource_ref,
432
+ retire_at = excluded.retire_at,
433
+ terminal_state = excluded.terminal_state,
434
+ terminal_result = excluded.terminal_result,
435
+ terminal_occurred_at = excluded.terminal_occurred_at,
436
+ created_at = excluded.created_at,
437
+ updated_at = excluded.updated_at
438
+ `, [
439
+ next.subscriptionId,
440
+ next.sourceId,
441
+ next.trackedResourceRef,
442
+ next.retireAt,
443
+ next.terminalState ?? null,
444
+ next.terminalResult ?? null,
445
+ next.terminalOccurredAt ?? null,
446
+ next.createdAt,
447
+ next.updatedAt,
448
+ ]);
449
+ this.persist();
450
+ return this.getSubscriptionLifecycleRetirement(retirement.subscriptionId);
451
+ }
452
+ deleteSubscriptionLifecycleRetirement(subscriptionId) {
453
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
454
+ this.persist();
455
+ }
359
456
  getActivationTarget(targetId) {
360
457
  const row = this.getOne("select * from activation_targets where target_id = ?", [targetId]);
361
458
  return row ? this.mapActivationTarget(row) : null;
@@ -408,9 +505,9 @@ class AgentInboxStore {
408
505
  this.db.run(`
409
506
  insert into activation_targets (
410
507
  target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms,
411
- runtime_kind, runtime_session_id, backend, tmux_pane_id, tty, term_program, iterm_session_id,
508
+ runtime_kind, runtime_session_id, runtime_pid, backend, tmux_pane_id, tty, term_program, iterm_session_id,
412
509
  created_at, updated_at, last_seen_at
413
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
510
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
414
511
  `, [
415
512
  target.targetId,
416
513
  target.agentId,
@@ -424,6 +521,7 @@ class AgentInboxStore {
424
521
  target.notifyLeaseMs,
425
522
  target.runtimeKind,
426
523
  target.runtimeSessionId ?? null,
524
+ target.runtimePid ?? null,
427
525
  target.backend,
428
526
  target.tmuxPaneId ?? null,
429
527
  target.tty ?? null,
@@ -440,11 +538,12 @@ class AgentInboxStore {
440
538
  this.db.run(`
441
539
  update activation_targets
442
540
  set status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
443
- runtime_kind = ?, runtime_session_id = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, updated_at = ?, last_seen_at = ?
541
+ runtime_kind = ?, runtime_session_id = ?, runtime_pid = ?, tmux_pane_id = ?, tty = ?, term_program = ?, iterm_session_id = ?, updated_at = ?, last_seen_at = ?
444
542
  where target_id = ? and kind = 'terminal'
445
543
  `, [
446
544
  input.runtimeKind,
447
545
  input.runtimeSessionId ?? null,
546
+ input.runtimePid ?? null,
448
547
  input.tmuxPaneId ?? null,
449
548
  input.tty ?? null,
450
549
  input.termProgram ?? null,
@@ -507,6 +606,9 @@ class AgentInboxStore {
507
606
  ]);
508
607
  this.db.run("delete from consumers where subscription_id = ?", [subscription.subscriptionId]);
509
608
  }
609
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id in (select subscription_id from subscriptions where agent_id = ?)", [agentId]);
610
+ this.db.run("delete from source_idle_states where source_id in (select distinct source_id from subscriptions where agent_id = ?)", [agentId]);
611
+ this.db.run("delete from timers where agent_id = ?", [agentId]);
510
612
  this.db.run("delete from subscriptions where agent_id = ?", [agentId]);
511
613
  this.db.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
512
614
  this.db.run("delete from activation_targets where agent_id = ?", [agentId]);
@@ -532,6 +634,86 @@ class AgentInboxStore {
532
634
  const rows = this.getAll("select * from agents where status = 'offline' and offline_since is not null and offline_since <= ? order by offline_since asc", [cutoffIso]);
533
635
  return rows.map((row) => this.mapAgent(row));
534
636
  }
637
+ insertTimer(timer) {
638
+ this.db.run(`
639
+ insert into timers (
640
+ schedule_id, agent_id, status, mode, at, interval_ms, cron_expr, timezone,
641
+ message, sender, next_fire_at, last_fired_at, created_at, updated_at
642
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
643
+ `, [
644
+ timer.scheduleId,
645
+ timer.agentId,
646
+ timer.status,
647
+ timer.mode,
648
+ timer.at ?? null,
649
+ timer.intervalMs ?? null,
650
+ timer.cronExpr ?? null,
651
+ timer.timezone,
652
+ timer.message,
653
+ timer.sender ?? null,
654
+ timer.nextFireAt ?? null,
655
+ timer.lastFiredAt ?? null,
656
+ timer.createdAt,
657
+ timer.updatedAt,
658
+ ]);
659
+ this.persist();
660
+ }
661
+ getTimer(scheduleId) {
662
+ const row = this.getOne("select * from timers where schedule_id = ?", [scheduleId]);
663
+ return row ? this.mapTimer(row) : null;
664
+ }
665
+ listTimers() {
666
+ const rows = this.getAll("select * from timers order by created_at asc");
667
+ return rows.map((row) => this.mapTimer(row));
668
+ }
669
+ listTimersForAgent(agentId) {
670
+ const rows = this.getAll("select * from timers where agent_id = ? order by created_at asc", [agentId]);
671
+ return rows.map((row) => this.mapTimer(row));
672
+ }
673
+ listDueTimers(cutoffIso) {
674
+ const rows = this.getAll("select * from timers where status = 'active' and next_fire_at is not null and next_fire_at <= ? order by next_fire_at asc, schedule_id asc", [cutoffIso]);
675
+ return rows.map((row) => this.mapTimer(row));
676
+ }
677
+ getNearestActiveTimer() {
678
+ const row = this.getOne("select * from timers where status = 'active' and next_fire_at is not null order by next_fire_at asc, schedule_id asc limit 1");
679
+ return row ? this.mapTimer(row) : null;
680
+ }
681
+ updateTimer(scheduleId, input) {
682
+ const current = this.getTimer(scheduleId);
683
+ if (!current) {
684
+ throw new Error(`unknown timer: ${scheduleId}`);
685
+ }
686
+ this.db.run(`
687
+ update timers
688
+ set status = ?, mode = ?, at = ?, interval_ms = ?, cron_expr = ?, timezone = ?,
689
+ message = ?, sender = ?, next_fire_at = ?, last_fired_at = ?, updated_at = ?
690
+ where schedule_id = ?
691
+ `, [
692
+ input.status ?? current.status,
693
+ input.mode ?? current.mode,
694
+ input.at !== undefined ? input.at ?? null : current.at ?? null,
695
+ input.intervalMs !== undefined ? input.intervalMs ?? null : current.intervalMs ?? null,
696
+ input.cronExpr !== undefined ? input.cronExpr ?? null : current.cronExpr ?? null,
697
+ input.timezone ?? current.timezone,
698
+ input.message ?? current.message,
699
+ input.sender !== undefined ? input.sender ?? null : current.sender ?? null,
700
+ input.nextFireAt !== undefined ? input.nextFireAt ?? null : current.nextFireAt ?? null,
701
+ input.lastFiredAt !== undefined ? input.lastFiredAt ?? null : current.lastFiredAt ?? null,
702
+ input.updatedAt ?? current.updatedAt,
703
+ scheduleId,
704
+ ]);
705
+ this.persist();
706
+ return this.getTimer(scheduleId);
707
+ }
708
+ deleteTimer(scheduleId) {
709
+ const current = this.getTimer(scheduleId);
710
+ if (!current) {
711
+ return null;
712
+ }
713
+ this.db.run("delete from timers where schedule_id = ?", [scheduleId]);
714
+ this.persist();
715
+ return current;
716
+ }
535
717
  getActivationDispatchState(agentId, targetId) {
536
718
  const row = this.getOne("select * from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
537
719
  return row ? this.mapActivationDispatchState(row) : null;
@@ -895,6 +1077,8 @@ class AgentInboxStore {
895
1077
  agents: this.count("agents"),
896
1078
  offlineAgents: this.count("agents where status = 'offline'"),
897
1079
  subscriptions: this.count("subscriptions"),
1080
+ subscriptionLifecycleRetirements: this.count("subscription_lifecycle_retirements"),
1081
+ sourceIdleStates: this.count("source_idle_states"),
898
1082
  inboxes: this.count("inboxes"),
899
1083
  activationTargets: this.count("activation_targets"),
900
1084
  offlineActivationTargets: this.count("activation_targets where status = 'offline'"),
@@ -959,6 +1143,15 @@ class AgentInboxStore {
959
1143
  updatedAt: String(row.updated_at),
960
1144
  };
961
1145
  }
1146
+ mapSourceIdleState(row) {
1147
+ return {
1148
+ sourceId: String(row.source_id),
1149
+ idleSince: String(row.idle_since),
1150
+ autoPauseAt: String(row.auto_pause_at),
1151
+ autoPausedAt: row.auto_paused_at ? String(row.auto_paused_at) : null,
1152
+ updatedAt: String(row.updated_at),
1153
+ };
1154
+ }
962
1155
  mapAgent(row) {
963
1156
  return {
964
1157
  agentId: String(row.agent_id),
@@ -984,14 +1177,27 @@ class AgentInboxStore {
984
1177
  agentId: String(row.agent_id),
985
1178
  sourceId: String(row.source_id),
986
1179
  filter: parseJson(row.filter_json),
987
- lifecycleMode: row.lifecycle_mode,
988
- expiresAt: row.expires_at ? String(row.expires_at) : null,
1180
+ trackedResourceRef: row.tracked_resource_ref ? String(row.tracked_resource_ref) : null,
1181
+ cleanupPolicy: parseJson(row.cleanup_policy_json),
989
1182
  startPolicy: row.start_policy,
990
1183
  startOffset: row.start_offset != null ? Number(row.start_offset) : null,
991
1184
  startTime: row.start_time ? String(row.start_time) : null,
992
1185
  createdAt: String(row.created_at),
993
1186
  };
994
1187
  }
1188
+ mapSubscriptionLifecycleRetirement(row) {
1189
+ return {
1190
+ subscriptionId: String(row.subscription_id),
1191
+ sourceId: String(row.source_id),
1192
+ trackedResourceRef: String(row.tracked_resource_ref),
1193
+ retireAt: String(row.retire_at),
1194
+ terminalState: row.terminal_state ? String(row.terminal_state) : null,
1195
+ terminalResult: row.terminal_result ? String(row.terminal_result) : null,
1196
+ terminalOccurredAt: row.terminal_occurred_at ? String(row.terminal_occurred_at) : null,
1197
+ createdAt: String(row.created_at),
1198
+ updatedAt: String(row.updated_at),
1199
+ };
1200
+ }
995
1201
  mapActivationTarget(row) {
996
1202
  if (String(row.kind) === "webhook") {
997
1203
  const url = typeof row.url === "string" && row.url.trim().length > 0 ? row.url.trim() : null;
@@ -1028,6 +1234,7 @@ class AgentInboxStore {
1028
1234
  notifyLeaseMs: Number(row.notify_lease_ms),
1029
1235
  runtimeKind: (row.runtime_kind ? String(row.runtime_kind) : "unknown"),
1030
1236
  runtimeSessionId: row.runtime_session_id ? String(row.runtime_session_id) : null,
1237
+ runtimePid: row.runtime_pid == null ? null : Number(row.runtime_pid),
1031
1238
  backend: row.backend,
1032
1239
  tmuxPaneId: row.tmux_pane_id ? String(row.tmux_pane_id) : null,
1033
1240
  tty: row.tty ? String(row.tty) : null,
@@ -1051,6 +1258,24 @@ class AgentInboxStore {
1051
1258
  updatedAt: String(row.updated_at),
1052
1259
  };
1053
1260
  }
1261
+ mapTimer(row) {
1262
+ return {
1263
+ scheduleId: String(row.schedule_id),
1264
+ agentId: String(row.agent_id),
1265
+ status: String(row.status),
1266
+ mode: String(row.mode),
1267
+ at: row.at ? String(row.at) : null,
1268
+ intervalMs: row.interval_ms == null ? null : Number(row.interval_ms),
1269
+ cronExpr: row.cron_expr ? String(row.cron_expr) : null,
1270
+ timezone: String(row.timezone),
1271
+ message: String(row.message),
1272
+ sender: row.sender ? String(row.sender) : null,
1273
+ nextFireAt: row.next_fire_at ? String(row.next_fire_at) : null,
1274
+ lastFiredAt: row.last_fired_at ? String(row.last_fired_at) : null,
1275
+ createdAt: String(row.created_at),
1276
+ updatedAt: String(row.updated_at),
1277
+ };
1278
+ }
1054
1279
  mapInboxItem(row) {
1055
1280
  return {
1056
1281
  itemId: String(row.item_id),
@@ -24,6 +24,7 @@ function detectTerminalContext(env = process.env) {
24
24
  return {
25
25
  runtimeKind: runtime.runtimeKind,
26
26
  runtimeSessionId: runtime.runtimeSessionId,
27
+ runtimePid: runtime.runtimePid,
27
28
  backend: "tmux",
28
29
  tmuxPaneId,
29
30
  tty,
@@ -35,6 +36,7 @@ function detectTerminalContext(env = process.env) {
35
36
  return {
36
37
  runtimeKind: runtime.runtimeKind,
37
38
  runtimeSessionId: runtime.runtimeSessionId,
39
+ runtimePid: runtime.runtimePid,
38
40
  backend: "iterm2",
39
41
  tty,
40
42
  termProgram,
@@ -77,7 +79,8 @@ class TerminalDispatcher {
77
79
  if (!target.tmuxPaneId) {
78
80
  throw new Error(`tmux terminal target ${target.targetId} is missing tmuxPaneId`);
79
81
  }
80
- await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, prompt, "Enter"]);
82
+ await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, "-l", prompt]);
83
+ await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, "-l", "\r"]);
81
84
  return;
82
85
  }
83
86
  if (target.backend === "iterm2") {
@@ -164,15 +167,18 @@ function detectRuntimeContext(env) {
164
167
  return {
165
168
  runtimeKind: "codex",
166
169
  runtimeSessionId: codexThreadId,
170
+ runtimePid: findNamedProcessInAncestry((name) => name === "codex"),
167
171
  };
168
172
  }
169
173
  const claudeSessionId = normalizeOptionalString(env.CLAUDE_CODE_SESSION_ID
170
174
  ?? env.CLAUDECODE_SESSION_ID
171
175
  ?? env.CLAUDE_SESSION_ID);
172
176
  if (claudeSessionId) {
177
+ const claudePid = findNamedProcessInAncestry((name) => name.includes("claude"));
173
178
  return {
174
179
  runtimeKind: "claude_code",
175
180
  runtimeSessionId: claudeSessionId,
181
+ runtimePid: claudePid,
176
182
  };
177
183
  }
178
184
  // Fallback: try to detect Claude Code from filesystem
@@ -183,12 +189,13 @@ function detectRuntimeContext(env) {
183
189
  return {
184
190
  runtimeKind: "unknown",
185
191
  runtimeSessionId: null,
192
+ runtimePid: null,
186
193
  };
187
194
  }
188
195
  function detectClaudeCodeFromFileSystem(env) {
189
196
  try {
190
197
  // Walk up the process tree to find a claude process
191
- const claudePid = findClaudeProcessInAncestry();
198
+ const claudePid = findNamedProcessInAncestry((name) => name.includes("claude"));
192
199
  if (!claudePid) {
193
200
  return null;
194
201
  }
@@ -215,6 +222,7 @@ function detectClaudeCodeFromFileSystem(env) {
215
222
  return {
216
223
  runtimeKind: "claude_code",
217
224
  runtimeSessionId: sessionData.sessionId,
225
+ runtimePid: claudePid,
218
226
  };
219
227
  }
220
228
  catch {
@@ -222,13 +230,13 @@ function detectClaudeCodeFromFileSystem(env) {
222
230
  return null;
223
231
  }
224
232
  }
225
- function findClaudeProcessInAncestry() {
233
+ function findNamedProcessInAncestry(matcher) {
226
234
  let currentPid = process.ppid;
227
235
  const maxIterations = 10; // Prevent infinite loops
228
236
  let iterations = 0;
229
237
  while (currentPid && iterations < maxIterations) {
230
238
  const processName = tryGetProcessName(currentPid);
231
- if (processName?.includes("claude")) {
239
+ if (processName && matcher(processName)) {
232
240
  return currentPid;
233
241
  }
234
242
  // Get parent of current process
@@ -0,0 +1,45 @@
1
+ create table subscriptions__new (
2
+ subscription_id text primary key,
3
+ agent_id text not null,
4
+ source_id text not null,
5
+ filter_json text not null,
6
+ tracked_resource_ref text,
7
+ cleanup_policy_json text not null,
8
+ start_policy text not null,
9
+ start_offset integer,
10
+ start_time text,
11
+ created_at text not null
12
+ );
13
+
14
+ insert into subscriptions__new (
15
+ subscription_id,
16
+ agent_id,
17
+ source_id,
18
+ filter_json,
19
+ tracked_resource_ref,
20
+ cleanup_policy_json,
21
+ start_policy,
22
+ start_offset,
23
+ start_time,
24
+ created_at
25
+ )
26
+ select
27
+ subscription_id,
28
+ agent_id,
29
+ source_id,
30
+ filter_json,
31
+ null as tracked_resource_ref,
32
+ case
33
+ when expires_at is not null then json_object('mode', 'at', 'at', expires_at)
34
+ when lifecycle_mode = 'temporary' then json_object('mode', 'manual')
35
+ else json_object('mode', 'manual')
36
+ end as cleanup_policy_json,
37
+ start_policy,
38
+ start_offset,
39
+ start_time,
40
+ created_at
41
+ from subscriptions;
42
+
43
+ drop table subscriptions;
44
+
45
+ alter table subscriptions__new rename to subscriptions;
@@ -0,0 +1,14 @@
1
+ create table if not exists subscription_lifecycle_retirements (
2
+ subscription_id text primary key,
3
+ source_id text not null,
4
+ tracked_resource_ref text not null,
5
+ retire_at text not null,
6
+ terminal_state text,
7
+ terminal_result text,
8
+ terminal_occurred_at text,
9
+ created_at text not null,
10
+ updated_at text not null
11
+ );
12
+
13
+ create index if not exists idx_subscription_lifecycle_retirements_retire_at
14
+ on subscription_lifecycle_retirements(retire_at);
@@ -0,0 +1,10 @@
1
+ create table if not exists source_idle_states (
2
+ source_id text primary key not null,
3
+ idle_since text not null,
4
+ auto_pause_at text not null,
5
+ auto_paused_at text,
6
+ updated_at text not null
7
+ );
8
+
9
+ create index if not exists idx_source_idle_states_auto_pause_at
10
+ on source_idle_states(auto_pause_at);
@@ -0,0 +1,22 @@
1
+ create table if not exists timers (
2
+ schedule_id text primary key not null,
3
+ agent_id text not null,
4
+ status text not null,
5
+ mode text not null,
6
+ at text,
7
+ interval_ms integer,
8
+ cron_expr text,
9
+ timezone text not null,
10
+ message text not null,
11
+ sender text,
12
+ next_fire_at text,
13
+ last_fired_at text,
14
+ created_at text not null,
15
+ updated_at text not null
16
+ );
17
+
18
+ create index if not exists idx_timers_agent_next_fire_at
19
+ on timers(agent_id, next_fire_at);
20
+
21
+ create index if not exists idx_timers_next_fire_at
22
+ on timers(next_fire_at);
@@ -0,0 +1 @@
1
+ alter table activation_targets add column runtime_pid integer;
package/drizzle/schema.ts CHANGED
@@ -14,6 +14,36 @@ export const sources = sqliteTable("sources", {
14
14
  sourceTypeKey: uniqueIndex("idx_sources_type_key").on(table.sourceType, table.sourceKey),
15
15
  }));
16
16
 
17
+ export const sourceIdleStates = sqliteTable("source_idle_states", {
18
+ sourceId: text("source_id").primaryKey(),
19
+ idleSince: text("idle_since").notNull(),
20
+ autoPauseAt: text("auto_pause_at").notNull(),
21
+ autoPausedAt: text("auto_paused_at"),
22
+ updatedAt: text("updated_at").notNull(),
23
+ }, (table) => ({
24
+ autoPauseAtIdx: index("idx_source_idle_states_auto_pause_at").on(table.autoPauseAt),
25
+ }));
26
+
27
+ export const timers = sqliteTable("timers", {
28
+ scheduleId: text("schedule_id").primaryKey(),
29
+ agentId: text("agent_id").notNull(),
30
+ status: text("status").notNull(),
31
+ mode: text("mode").notNull(),
32
+ at: text("at"),
33
+ intervalMs: integer("interval_ms"),
34
+ cronExpr: text("cron_expr"),
35
+ timezone: text("timezone").notNull(),
36
+ message: text("message").notNull(),
37
+ sender: text("sender"),
38
+ nextFireAt: text("next_fire_at"),
39
+ lastFiredAt: text("last_fired_at"),
40
+ createdAt: text("created_at").notNull(),
41
+ updatedAt: text("updated_at").notNull(),
42
+ }, (table) => ({
43
+ agentNextFireIdx: index("idx_timers_agent_next_fire_at").on(table.agentId, table.nextFireAt),
44
+ nextFireIdx: index("idx_timers_next_fire_at").on(table.nextFireAt),
45
+ }));
46
+
17
47
  export const agents = sqliteTable("agents", {
18
48
  agentId: text("agent_id").primaryKey(),
19
49
  status: text("status").notNull(),
@@ -40,14 +70,26 @@ export const subscriptions = sqliteTable("subscriptions", {
40
70
  agentId: text("agent_id").notNull(),
41
71
  sourceId: text("source_id").notNull(),
42
72
  filterJson: text("filter_json").notNull(),
43
- lifecycleMode: text("lifecycle_mode").notNull(),
44
- expiresAt: text("expires_at"),
73
+ trackedResourceRef: text("tracked_resource_ref"),
74
+ cleanupPolicyJson: text("cleanup_policy_json").notNull(),
45
75
  startPolicy: text("start_policy").notNull(),
46
76
  startOffset: integer("start_offset"),
47
77
  startTime: text("start_time"),
48
78
  createdAt: text("created_at").notNull(),
49
79
  });
50
80
 
81
+ export const subscriptionLifecycleRetirements = sqliteTable("subscription_lifecycle_retirements", {
82
+ subscriptionId: text("subscription_id").primaryKey(),
83
+ sourceId: text("source_id").notNull(),
84
+ trackedResourceRef: text("tracked_resource_ref").notNull(),
85
+ retireAt: text("retire_at").notNull(),
86
+ terminalState: text("terminal_state"),
87
+ terminalResult: text("terminal_result"),
88
+ terminalOccurredAt: text("terminal_occurred_at"),
89
+ createdAt: text("created_at").notNull(),
90
+ updatedAt: text("updated_at").notNull(),
91
+ });
92
+
51
93
  export const activationTargets = sqliteTable("activation_targets", {
52
94
  targetId: text("target_id").primaryKey(),
53
95
  agentId: text("agent_id").notNull(),
@@ -62,6 +104,7 @@ export const activationTargets = sqliteTable("activation_targets", {
62
104
  url: text("url"),
63
105
  runtimeKind: text("runtime_kind"),
64
106
  runtimeSessionId: text("runtime_session_id"),
107
+ runtimePid: integer("runtime_pid"),
65
108
  backend: text("backend"),
66
109
  tmuxPaneId: text("tmux_pane_id"),
67
110
  tty: text("tty"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {