@holon-run/agentinbox 0.2.0 → 0.4.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/README.md +38 -0
- package/dist/src/cli.js +128 -0
- package/dist/src/current_agent.js +1 -0
- package/dist/src/daemon.js +69 -0
- package/dist/src/http.js +194 -1
- package/dist/src/runtime_gate.js +645 -0
- package/dist/src/service.js +741 -8
- package/dist/src/store.js +105 -3
- package/dist/src/terminal.js +35 -12
- package/drizzle/migrations/0005_timers.sql +22 -0
- package/drizzle/migrations/0006_terminal_runtime_pid.sql +1 -0
- package/drizzle/schema.ts +21 -0
- package/package.json +1 -1
package/dist/src/store.js
CHANGED
|
@@ -505,9 +505,9 @@ class AgentInboxStore {
|
|
|
505
505
|
this.db.run(`
|
|
506
506
|
insert into activation_targets (
|
|
507
507
|
target_id, agent_id, kind, status, offline_since, consecutive_failures, last_delivered_at, last_error, mode, notify_lease_ms,
|
|
508
|
-
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,
|
|
509
509
|
created_at, updated_at, last_seen_at
|
|
510
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
510
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
511
|
`, [
|
|
512
512
|
target.targetId,
|
|
513
513
|
target.agentId,
|
|
@@ -521,6 +521,7 @@ class AgentInboxStore {
|
|
|
521
521
|
target.notifyLeaseMs,
|
|
522
522
|
target.runtimeKind,
|
|
523
523
|
target.runtimeSessionId ?? null,
|
|
524
|
+
target.runtimePid ?? null,
|
|
524
525
|
target.backend,
|
|
525
526
|
target.tmuxPaneId ?? null,
|
|
526
527
|
target.tty ?? null,
|
|
@@ -537,11 +538,12 @@ class AgentInboxStore {
|
|
|
537
538
|
this.db.run(`
|
|
538
539
|
update activation_targets
|
|
539
540
|
set status = 'active', offline_since = null, last_error = null, consecutive_failures = 0,
|
|
540
|
-
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 = ?
|
|
541
542
|
where target_id = ? and kind = 'terminal'
|
|
542
543
|
`, [
|
|
543
544
|
input.runtimeKind,
|
|
544
545
|
input.runtimeSessionId ?? null,
|
|
546
|
+
input.runtimePid ?? null,
|
|
545
547
|
input.tmuxPaneId ?? null,
|
|
546
548
|
input.tty ?? null,
|
|
547
549
|
input.termProgram ?? null,
|
|
@@ -606,6 +608,7 @@ class AgentInboxStore {
|
|
|
606
608
|
}
|
|
607
609
|
this.db.run("delete from subscription_lifecycle_retirements where subscription_id in (select subscription_id from subscriptions where agent_id = ?)", [agentId]);
|
|
608
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]);
|
|
609
612
|
this.db.run("delete from subscriptions where agent_id = ?", [agentId]);
|
|
610
613
|
this.db.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
|
|
611
614
|
this.db.run("delete from activation_targets where agent_id = ?", [agentId]);
|
|
@@ -631,6 +634,86 @@ class AgentInboxStore {
|
|
|
631
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]);
|
|
632
635
|
return rows.map((row) => this.mapAgent(row));
|
|
633
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
|
+
}
|
|
634
717
|
getActivationDispatchState(agentId, targetId) {
|
|
635
718
|
const row = this.getOne("select * from activation_dispatch_states where agent_id = ? and target_id = ?", [agentId, targetId]);
|
|
636
719
|
return row ? this.mapActivationDispatchState(row) : null;
|
|
@@ -1151,6 +1234,7 @@ class AgentInboxStore {
|
|
|
1151
1234
|
notifyLeaseMs: Number(row.notify_lease_ms),
|
|
1152
1235
|
runtimeKind: (row.runtime_kind ? String(row.runtime_kind) : "unknown"),
|
|
1153
1236
|
runtimeSessionId: row.runtime_session_id ? String(row.runtime_session_id) : null,
|
|
1237
|
+
runtimePid: row.runtime_pid == null ? null : Number(row.runtime_pid),
|
|
1154
1238
|
backend: row.backend,
|
|
1155
1239
|
tmuxPaneId: row.tmux_pane_id ? String(row.tmux_pane_id) : null,
|
|
1156
1240
|
tty: row.tty ? String(row.tty) : null,
|
|
@@ -1174,6 +1258,24 @@ class AgentInboxStore {
|
|
|
1174
1258
|
updatedAt: String(row.updated_at),
|
|
1175
1259
|
};
|
|
1176
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
|
+
}
|
|
1177
1279
|
mapInboxItem(row) {
|
|
1178
1280
|
return {
|
|
1179
1281
|
itemId: String(row.item_id),
|
package/dist/src/terminal.js
CHANGED
|
@@ -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,
|
|
@@ -44,7 +46,9 @@ function detectTerminalContext(env = process.env) {
|
|
|
44
46
|
throw new Error("unable to detect a supported terminal context");
|
|
45
47
|
}
|
|
46
48
|
function renderAgentPrompt(input) {
|
|
47
|
-
const
|
|
49
|
+
const totalUnackedCount = input.totalUnackedCount ?? input.newItemCount ?? 0;
|
|
50
|
+
const itemWord = totalUnackedCount === 1 ? "item" : "items";
|
|
51
|
+
const base = `AgentInbox: ${totalUnackedCount} unacked ${itemWord} in inbox ${input.inboxId}.`;
|
|
48
52
|
if (input.summary) {
|
|
49
53
|
return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
|
|
50
54
|
}
|
|
@@ -77,7 +81,8 @@ class TerminalDispatcher {
|
|
|
77
81
|
if (!target.tmuxPaneId) {
|
|
78
82
|
throw new Error(`tmux terminal target ${target.targetId} is missing tmuxPaneId`);
|
|
79
83
|
}
|
|
80
|
-
await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId,
|
|
84
|
+
await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, "-l", prompt]);
|
|
85
|
+
await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, "-l", "\r"]);
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
83
88
|
if (target.backend === "iterm2") {
|
|
@@ -87,31 +92,44 @@ class TerminalDispatcher {
|
|
|
87
92
|
throw new Error(`unsupported terminal backend: ${String(target.backend)}`);
|
|
88
93
|
}
|
|
89
94
|
async probe(target) {
|
|
95
|
+
return (await this.probeStatus(target)) === "available";
|
|
96
|
+
}
|
|
97
|
+
async probeStatus(target) {
|
|
90
98
|
try {
|
|
91
99
|
if (target.backend === "tmux") {
|
|
92
100
|
if (!target.tmuxPaneId) {
|
|
93
|
-
return
|
|
101
|
+
return "unknown";
|
|
94
102
|
}
|
|
95
103
|
const result = await this.execAsync("tmux", ["display-message", "-p", "-t", target.tmuxPaneId, "#{pane_id}"]);
|
|
96
|
-
return result.stdout.trim() === target.tmuxPaneId;
|
|
104
|
+
return result.stdout.trim() === target.tmuxPaneId ? "available" : "gone";
|
|
97
105
|
}
|
|
98
106
|
if (target.backend === "iterm2") {
|
|
99
107
|
const sessionId = normalizeOptionalString(target.itermSessionId);
|
|
100
108
|
if (!sessionId) {
|
|
101
|
-
return
|
|
109
|
+
return "unknown";
|
|
102
110
|
}
|
|
103
111
|
const it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
|
|
104
112
|
const result = await this.execAsync(it2api, ["list-sessions"]);
|
|
105
|
-
return result.stdout.includes(sessionId);
|
|
113
|
+
return result.stdout.includes(sessionId) ? "available" : "gone";
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
|
-
catch {
|
|
109
|
-
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (target.backend === "tmux" && isTmuxMissingPaneError(error)) {
|
|
118
|
+
return "gone";
|
|
119
|
+
}
|
|
120
|
+
return "unknown";
|
|
110
121
|
}
|
|
111
|
-
return
|
|
122
|
+
return "unknown";
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
exports.TerminalDispatcher = TerminalDispatcher;
|
|
126
|
+
function isTmuxMissingPaneError(error) {
|
|
127
|
+
if (!(error instanceof Error)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const candidate = `${error.message} ${"stderr" in error ? String(error.stderr ?? "") : ""}`;
|
|
131
|
+
return candidate.includes("can't find pane");
|
|
132
|
+
}
|
|
115
133
|
function detectTty(env) {
|
|
116
134
|
const direct = tryReadCurrentTty();
|
|
117
135
|
if (direct) {
|
|
@@ -164,15 +182,18 @@ function detectRuntimeContext(env) {
|
|
|
164
182
|
return {
|
|
165
183
|
runtimeKind: "codex",
|
|
166
184
|
runtimeSessionId: codexThreadId,
|
|
185
|
+
runtimePid: findNamedProcessInAncestry((name) => name === "codex"),
|
|
167
186
|
};
|
|
168
187
|
}
|
|
169
188
|
const claudeSessionId = normalizeOptionalString(env.CLAUDE_CODE_SESSION_ID
|
|
170
189
|
?? env.CLAUDECODE_SESSION_ID
|
|
171
190
|
?? env.CLAUDE_SESSION_ID);
|
|
172
191
|
if (claudeSessionId) {
|
|
192
|
+
const claudePid = findNamedProcessInAncestry((name) => name.includes("claude"));
|
|
173
193
|
return {
|
|
174
194
|
runtimeKind: "claude_code",
|
|
175
195
|
runtimeSessionId: claudeSessionId,
|
|
196
|
+
runtimePid: claudePid,
|
|
176
197
|
};
|
|
177
198
|
}
|
|
178
199
|
// Fallback: try to detect Claude Code from filesystem
|
|
@@ -183,12 +204,13 @@ function detectRuntimeContext(env) {
|
|
|
183
204
|
return {
|
|
184
205
|
runtimeKind: "unknown",
|
|
185
206
|
runtimeSessionId: null,
|
|
207
|
+
runtimePid: null,
|
|
186
208
|
};
|
|
187
209
|
}
|
|
188
210
|
function detectClaudeCodeFromFileSystem(env) {
|
|
189
211
|
try {
|
|
190
212
|
// Walk up the process tree to find a claude process
|
|
191
|
-
const claudePid =
|
|
213
|
+
const claudePid = findNamedProcessInAncestry((name) => name.includes("claude"));
|
|
192
214
|
if (!claudePid) {
|
|
193
215
|
return null;
|
|
194
216
|
}
|
|
@@ -215,6 +237,7 @@ function detectClaudeCodeFromFileSystem(env) {
|
|
|
215
237
|
return {
|
|
216
238
|
runtimeKind: "claude_code",
|
|
217
239
|
runtimeSessionId: sessionData.sessionId,
|
|
240
|
+
runtimePid: claudePid,
|
|
218
241
|
};
|
|
219
242
|
}
|
|
220
243
|
catch {
|
|
@@ -222,13 +245,13 @@ function detectClaudeCodeFromFileSystem(env) {
|
|
|
222
245
|
return null;
|
|
223
246
|
}
|
|
224
247
|
}
|
|
225
|
-
function
|
|
248
|
+
function findNamedProcessInAncestry(matcher) {
|
|
226
249
|
let currentPid = process.ppid;
|
|
227
250
|
const maxIterations = 10; // Prevent infinite loops
|
|
228
251
|
let iterations = 0;
|
|
229
252
|
while (currentPid && iterations < maxIterations) {
|
|
230
253
|
const processName = tryGetProcessName(currentPid);
|
|
231
|
-
if (processName
|
|
254
|
+
if (processName && matcher(processName)) {
|
|
232
255
|
return currentPid;
|
|
233
256
|
}
|
|
234
257
|
// Get parent of current process
|
|
@@ -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
|
@@ -24,6 +24,26 @@ export const sourceIdleStates = sqliteTable("source_idle_states", {
|
|
|
24
24
|
autoPauseAtIdx: index("idx_source_idle_states_auto_pause_at").on(table.autoPauseAt),
|
|
25
25
|
}));
|
|
26
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
|
+
|
|
27
47
|
export const agents = sqliteTable("agents", {
|
|
28
48
|
agentId: text("agent_id").primaryKey(),
|
|
29
49
|
status: text("status").notNull(),
|
|
@@ -84,6 +104,7 @@ export const activationTargets = sqliteTable("activation_targets", {
|
|
|
84
104
|
url: text("url"),
|
|
85
105
|
runtimeKind: text("runtime_kind"),
|
|
86
106
|
runtimeSessionId: text("runtime_session_id"),
|
|
107
|
+
runtimePid: integer("runtime_pid"),
|
|
87
108
|
backend: text("backend"),
|
|
88
109
|
tmuxPaneId: text("tmux_pane_id"),
|
|
89
110
|
tty: text("tty"),
|