kitty-hive 0.6.12 → 0.7.2
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/codex-channel.ts +463 -3
- package/dist/admin-http.js +266 -0
- package/dist/admin-http.js.map +1 -1
- package/dist/codex-channel-runtime.d.ts +126 -0
- package/dist/codex-channel-runtime.js +203 -0
- package/dist/codex-channel-runtime.js.map +1 -0
- package/dist/codex-supervisor.d.ts +84 -0
- package/dist/codex-supervisor.js +379 -0
- package/dist/codex-supervisor.js.map +1 -0
- package/dist/db.d.ts +26 -0
- package/dist/db.js +63 -2
- package/dist/db.js.map +1 -1
- package/dist/index.js +199 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-tools.js +62 -1
- package/dist/mcp/agent-tools.js.map +1 -1
- package/dist/mcp/dm-tools.js +6 -5
- package/dist/mcp/dm-tools.js.map +1 -1
- package/dist/mcp/task-tools.js +30 -10
- package/dist/mcp/task-tools.js.map +1 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.js.map +1 -1
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/start.d.ts +1 -0
- package/dist/tools/start.js +55 -12
- package/dist/tools/start.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex daemon supervisor.
|
|
3
|
+
*
|
|
4
|
+
* On hive serve boot, spawns one codex-channel daemon child per local agent
|
|
5
|
+
* registered with `tool='codex'`. Daemons get push delivery via WebSocket to
|
|
6
|
+
* a codex app-server they manage; the supervisor just keeps them alive.
|
|
7
|
+
*
|
|
8
|
+
* Design choices (settled with the user, 2026-05-20):
|
|
9
|
+
* - DB is the single source of truth: `WHERE tool='codex' AND origin_peer=''`
|
|
10
|
+
* decides what gets spawned. No separate config list, no opt-in flag —
|
|
11
|
+
* same UX as Claude plugin (install once, just works).
|
|
12
|
+
* - Restart with exponential backoff (1s → 2s → 4s → ... → cap 60s).
|
|
13
|
+
* - On serve SIGTERM, supervisor kills all children with SIGTERM and waits.
|
|
14
|
+
* - Daemon stderr/stdout are tee'd into serve's own stderr with an
|
|
15
|
+
* `[codex:<display_name>]` prefix.
|
|
16
|
+
*
|
|
17
|
+
* Thread persistence (v0.7.0):
|
|
18
|
+
* - daemon's codex thread_id is stored on agents.thread_id after first ready.
|
|
19
|
+
* - On respawn, supervisor injects HIVE_AGENT_THREAD_ID so daemon calls
|
|
20
|
+
* `thread/resume` (against the jsonl codex app-server already wrote to
|
|
21
|
+
* ~/.codex/sessions/) instead of `thread/start`. Conversation survives
|
|
22
|
+
* daemon kill / serve restart / machine reboot.
|
|
23
|
+
*
|
|
24
|
+
* Out of scope (deferred):
|
|
25
|
+
* - Hot reload on DB changes (poll / watch). Add/remove agents → must
|
|
26
|
+
* restart serve to pick up. Acceptable for now since codex agents are
|
|
27
|
+
* long-lived once configured.
|
|
28
|
+
*/
|
|
29
|
+
export declare function startCodexSupervisor(port?: number): void;
|
|
30
|
+
export declare function stopCodexSupervisor(): Promise<void>;
|
|
31
|
+
export interface DaemonSnapshot {
|
|
32
|
+
agent_id: string;
|
|
33
|
+
display_name: string;
|
|
34
|
+
pid: number;
|
|
35
|
+
uptime_ms: number;
|
|
36
|
+
restart_count: number;
|
|
37
|
+
ws_url: string | null;
|
|
38
|
+
thread_id: string | null;
|
|
39
|
+
ready: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function getDaemonSnapshots(): DaemonSnapshot[];
|
|
42
|
+
/** Look up the live daemon for an agent, by agent_id or external_key.
|
|
43
|
+
* Returns null when no daemon is running for that agent. */
|
|
44
|
+
export declare function getDaemonForAgent(lookup: {
|
|
45
|
+
agentId?: string;
|
|
46
|
+
agentKey?: string;
|
|
47
|
+
}): DaemonSnapshot | null;
|
|
48
|
+
/** Called by /admin/codex-daemon-ready when a daemon's codex app-server is up
|
|
49
|
+
* and its thread is created. Stores ws_url + thread_id on the DaemonInfo so
|
|
50
|
+
* outside callers can attach via `codex --remote <ws_url>`. */
|
|
51
|
+
export declare function markDaemonReady(agentId: string, wsUrl: string, threadId: string): boolean;
|
|
52
|
+
/** Called by /admin/notify-agent-created when a CLI-side `agent register`
|
|
53
|
+
* inserts a row. The CLI runs in its own process so the in-process
|
|
54
|
+
* onAgentCreated hook doesn't reach the supervisor; this is the bridge.
|
|
55
|
+
* Idempotent: returns false if already spawned, true if a new spawn started. */
|
|
56
|
+
export declare function notifyAgentCreated(agentId: string): boolean;
|
|
57
|
+
/** Kill the daemon (if any) for an agent that just got removed. Mirror of
|
|
58
|
+
* notifyAgentCreated for the delete path. Without this, `kitty-hive agent
|
|
59
|
+
* remove` deletes the row but leaves a ghost daemon (and its codex
|
|
60
|
+
* app-server + ws) running — see Bug 1 reported on 2026-05-20: a stale
|
|
61
|
+
* ws confused TUI routing into a schism. SIGTERM the child; the existing
|
|
62
|
+
* child.on('exit') handler in spawnDaemon checks `getAgentById(agentId)`
|
|
63
|
+
* before respawning, so a deleted row naturally short-circuits the
|
|
64
|
+
* respawn logic — no per-daemon "intentional kill" flag needed.
|
|
65
|
+
* Returns true if a daemon was killed. */
|
|
66
|
+
export declare function notifyAgentRemoved(agentId: string): boolean;
|
|
67
|
+
/** Force a daemon to (re)spawn for an agent, then wait up to `timeoutMs` for
|
|
68
|
+
* it to reach ready state. Used by /admin/codex-set-thread when the caller
|
|
69
|
+
* has just mutated agents.thread_id and needs the daemon to pick up the
|
|
70
|
+
* new value (either via thread/resume of a different thread, or thread/start
|
|
71
|
+
* for a fresh thread when thread_id was cleared).
|
|
72
|
+
*
|
|
73
|
+
* Two paths:
|
|
74
|
+
* - Daemon already running → SIGTERM it; supervisor's on-exit handler
|
|
75
|
+
* respawns automatically (with backoff, but immediate for the first
|
|
76
|
+
* restart). The new daemon will pick up the fresh agents.thread_id at
|
|
77
|
+
* its env-injection step.
|
|
78
|
+
* - No daemon → call notifyAgentCreated (skips if not eligible).
|
|
79
|
+
*
|
|
80
|
+
* After kicking, polls `daemons.get(agentId)` for readiness every 200ms.
|
|
81
|
+
* Returns the ready snapshot, or null on timeout. The caller is expected to
|
|
82
|
+
* have validated that the agent exists and is tool=codex; this function
|
|
83
|
+
* trusts that. */
|
|
84
|
+
export declare function requestDaemonRespawn(agentId: string, timeoutMs?: number): Promise<DaemonSnapshot | null>;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex daemon supervisor.
|
|
3
|
+
*
|
|
4
|
+
* On hive serve boot, spawns one codex-channel daemon child per local agent
|
|
5
|
+
* registered with `tool='codex'`. Daemons get push delivery via WebSocket to
|
|
6
|
+
* a codex app-server they manage; the supervisor just keeps them alive.
|
|
7
|
+
*
|
|
8
|
+
* Design choices (settled with the user, 2026-05-20):
|
|
9
|
+
* - DB is the single source of truth: `WHERE tool='codex' AND origin_peer=''`
|
|
10
|
+
* decides what gets spawned. No separate config list, no opt-in flag —
|
|
11
|
+
* same UX as Claude plugin (install once, just works).
|
|
12
|
+
* - Restart with exponential backoff (1s → 2s → 4s → ... → cap 60s).
|
|
13
|
+
* - On serve SIGTERM, supervisor kills all children with SIGTERM and waits.
|
|
14
|
+
* - Daemon stderr/stdout are tee'd into serve's own stderr with an
|
|
15
|
+
* `[codex:<display_name>]` prefix.
|
|
16
|
+
*
|
|
17
|
+
* Thread persistence (v0.7.0):
|
|
18
|
+
* - daemon's codex thread_id is stored on agents.thread_id after first ready.
|
|
19
|
+
* - On respawn, supervisor injects HIVE_AGENT_THREAD_ID so daemon calls
|
|
20
|
+
* `thread/resume` (against the jsonl codex app-server already wrote to
|
|
21
|
+
* ~/.codex/sessions/) instead of `thread/start`. Conversation survives
|
|
22
|
+
* daemon kill / serve restart / machine reboot.
|
|
23
|
+
*
|
|
24
|
+
* Out of scope (deferred):
|
|
25
|
+
* - Hot reload on DB changes (poll / watch). Add/remove agents → must
|
|
26
|
+
* restart serve to pick up. Acceptable for now since codex agents are
|
|
27
|
+
* long-lived once configured.
|
|
28
|
+
*/
|
|
29
|
+
import { spawn } from 'node:child_process';
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { execSync } from 'node:child_process';
|
|
34
|
+
import { log } from './log.js';
|
|
35
|
+
import { listLocalCodexAgents, getAgentById, onAgentCreated, setAgentThreadId } from './db.js';
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = dirname(__filename);
|
|
38
|
+
const daemons = new Map();
|
|
39
|
+
let shuttingDown = false;
|
|
40
|
+
let supervisorPort = 4123; // set by startCodexSupervisor
|
|
41
|
+
function findNpx() {
|
|
42
|
+
const probe = process.platform === 'win32' ? 'where npx' : 'command -v npx';
|
|
43
|
+
try {
|
|
44
|
+
const out = execSync(probe, { encoding: 'utf8' }).trim().split(/\r?\n/)[0];
|
|
45
|
+
return out || 'npx';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'npx';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function locateCodexChannelScript() {
|
|
52
|
+
// dist/codex-supervisor.js → ../codex-channel.ts (package root)
|
|
53
|
+
const p = join(__dirname, '..', 'codex-channel.ts');
|
|
54
|
+
if (existsSync(p))
|
|
55
|
+
return p;
|
|
56
|
+
// Fallback if running from src/ directly via tsx (dev)
|
|
57
|
+
const dev = join(__dirname, '..', '..', 'codex-channel.ts');
|
|
58
|
+
if (existsSync(dev))
|
|
59
|
+
return dev;
|
|
60
|
+
throw new Error(`cannot locate codex-channel.ts (tried ${p})`);
|
|
61
|
+
}
|
|
62
|
+
function spawnDaemon(agentId, displayName, restartCount = 0) {
|
|
63
|
+
if (shuttingDown)
|
|
64
|
+
return;
|
|
65
|
+
if (daemons.has(agentId))
|
|
66
|
+
return; // already running
|
|
67
|
+
let scriptPath;
|
|
68
|
+
try {
|
|
69
|
+
scriptPath = locateCodexChannelScript();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
log('warn', `[codex-supervisor] ${err}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
log('info', `[codex-supervisor] spawning daemon for "${displayName}" (${agentId.slice(-12)}) restart=${restartCount}`);
|
|
76
|
+
// CRITICAL: scrub inherited HIVE_* env vars from the operator's shell.
|
|
77
|
+
// Without this, if `kitty-hive serve` was launched from a shell with
|
|
78
|
+
// HIVE_AGENT_KEY set (e.g. for a Claude session bound to a different
|
|
79
|
+
// agent), the daemon would carry that key over, and codex-channel.ts's
|
|
80
|
+
// hive_start() would `key`-match to the operator's agent — silently
|
|
81
|
+
// taking it over (display_name + tool overwritten). Real incident on
|
|
82
|
+
// 2026-05-20: smoke test renamed `kitty-hive` to `test-codex-1` because
|
|
83
|
+
// the operator shell had HIVE_AGENT_KEY=<kitty-hive's external_key>.
|
|
84
|
+
//
|
|
85
|
+
// Daemon gets a minimal env: system basics (PATH, HOME, etc.) + EXACTLY
|
|
86
|
+
// the hive identity overrides we want.
|
|
87
|
+
const cleanEnv = {};
|
|
88
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
89
|
+
if (k.startsWith('HIVE_'))
|
|
90
|
+
continue; // drop any HIVE_* inherited from operator shell
|
|
91
|
+
cleanEnv[k] = v;
|
|
92
|
+
}
|
|
93
|
+
cleanEnv.HIVE_AGENT_ID = agentId;
|
|
94
|
+
cleanEnv.HIVE_AGENT_NAME = displayName;
|
|
95
|
+
// HIVE_URL: explicitly set so daemon talks to *this* serve (whose port the
|
|
96
|
+
// supervisor knows), not whatever was in the operator shell or the daemon's
|
|
97
|
+
// own fallback default.
|
|
98
|
+
cleanEnv.HIVE_URL = `http://127.0.0.1:${supervisorPort}/mcp`;
|
|
99
|
+
// CODEX_APPSERVER_CWD: codex-channel.ts reads this and passes to thread/start
|
|
100
|
+
// as cwd. Determines where the codex agent thinks it's running — must match
|
|
101
|
+
// the operator's intent (the project the kitty pane was launched for), not
|
|
102
|
+
// the cwd hive serve happens to be running from. agent.project_dir is
|
|
103
|
+
// operator-set via `agent register --project-dir`; empty falls back to
|
|
104
|
+
// serve's own cwd.
|
|
105
|
+
const fresh = getAgentById(agentId);
|
|
106
|
+
if (fresh?.project_dir)
|
|
107
|
+
cleanEnv.CODEX_APPSERVER_CWD = fresh.project_dir;
|
|
108
|
+
// HIVE_AGENT_THREAD_ID: presence tells codex-channel to `thread/resume` an
|
|
109
|
+
// existing codex thread (jsonl already on disk in ~/.codex/sessions/)
|
|
110
|
+
// instead of `thread/start`. Set on every spawn after the first ready
|
|
111
|
+
// (markDaemonReady persists it to agents.thread_id). On a respawn after
|
|
112
|
+
// resume failure, codex-channel announces a brand-new thread_id and
|
|
113
|
+
// markDaemonReady overwrites — old jsonl is orphaned but content survives
|
|
114
|
+
// on disk.
|
|
115
|
+
if (fresh?.thread_id)
|
|
116
|
+
cleanEnv.HIVE_AGENT_THREAD_ID = fresh.thread_id;
|
|
117
|
+
const child = spawn(findNpx(), ['-y', 'tsx', scriptPath], {
|
|
118
|
+
env: cleanEnv,
|
|
119
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
|
+
});
|
|
121
|
+
if (!child.pid) {
|
|
122
|
+
log('warn', `[codex-supervisor] failed to spawn daemon for ${displayName}: no pid`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const info = {
|
|
126
|
+
agentId, displayName, child, pid: child.pid,
|
|
127
|
+
startedAt: new Date(), restartCount,
|
|
128
|
+
wsUrl: null, threadId: null, readyAt: null,
|
|
129
|
+
};
|
|
130
|
+
daemons.set(agentId, info);
|
|
131
|
+
// Tee daemon output into serve's stderr with a label so user can see what
|
|
132
|
+
// each daemon is doing without separate log files. We forward both stdout
|
|
133
|
+
// and stderr to stderr (stdout would interleave with the http log lines).
|
|
134
|
+
const prefix = `[codex:${displayName}]`;
|
|
135
|
+
child.stdout?.on('data', (chunk) => process.stderr.write(`${prefix} ${chunk}`));
|
|
136
|
+
child.stderr?.on('data', (chunk) => process.stderr.write(`${prefix} ${chunk}`));
|
|
137
|
+
child.on('exit', (code, signal) => {
|
|
138
|
+
daemons.delete(agentId);
|
|
139
|
+
log('info', `[codex-supervisor] daemon "${displayName}" exited (code=${code}, signal=${signal})`);
|
|
140
|
+
if (shuttingDown)
|
|
141
|
+
return;
|
|
142
|
+
// Refresh agent record — it may have been removed or modified
|
|
143
|
+
const fresh = getAgentById(agentId);
|
|
144
|
+
if (!fresh || fresh.tool !== 'codex' || fresh.origin_peer !== '') {
|
|
145
|
+
log('info', `[codex-supervisor] daemon "${displayName}" no longer eligible for spawn (deleted / tool changed / now remote); not restarting`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, ... cap 60s
|
|
149
|
+
const delayMs = Math.min(60000, 1000 * Math.pow(2, restartCount));
|
|
150
|
+
log('warn', `[codex-supervisor] restarting "${fresh.display_name}" in ${delayMs}ms (attempt ${restartCount + 1})`);
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
spawnDaemon(fresh.id, fresh.display_name, restartCount + 1);
|
|
153
|
+
}, delayMs);
|
|
154
|
+
});
|
|
155
|
+
child.on('error', (err) => {
|
|
156
|
+
log('warn', `[codex-supervisor] daemon "${displayName}" process error: ${err}`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function startCodexSupervisor(port = 4123) {
|
|
160
|
+
supervisorPort = port;
|
|
161
|
+
const agents = listLocalCodexAgents();
|
|
162
|
+
if (agents.length === 0) {
|
|
163
|
+
log('info', '[codex-supervisor] no local tool=codex agents — nothing to spawn at boot');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
log('info', `[codex-supervisor] starting ${agents.length} codex daemon(s): ${agents.map(a => a.display_name).join(', ')}`);
|
|
167
|
+
for (const agent of agents) {
|
|
168
|
+
spawnDaemon(agent.id, agent.display_name, 0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Dynamic spawn: kitty / any other launcher may register a NEW codex agent
|
|
172
|
+
// while serve is running. Listen for agent-create events and spawn a daemon
|
|
173
|
+
// on the fly so the launcher doesn't have to restart serve. Subscribe AFTER
|
|
174
|
+
// initial boot scan so we don't double-spawn the already-spawned set above.
|
|
175
|
+
onAgentCreated((agent) => {
|
|
176
|
+
if (agent.tool !== 'codex' || agent.origin_peer !== '')
|
|
177
|
+
return;
|
|
178
|
+
if (daemons.has(agent.id))
|
|
179
|
+
return; // already spawning
|
|
180
|
+
log('info', `[codex-supervisor] new local tool=codex agent registered: "${agent.display_name}" (${agent.id.slice(-12)}) → spawning daemon`);
|
|
181
|
+
spawnDaemon(agent.id, agent.display_name, 0);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export function stopCodexSupervisor() {
|
|
185
|
+
shuttingDown = true;
|
|
186
|
+
if (daemons.size === 0)
|
|
187
|
+
return Promise.resolve();
|
|
188
|
+
log('info', `[codex-supervisor] stopping ${daemons.size} daemon(s)...`);
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const pending = new Set();
|
|
191
|
+
for (const info of daemons.values()) {
|
|
192
|
+
pending.add(info.agentId);
|
|
193
|
+
info.child.once('exit', () => {
|
|
194
|
+
pending.delete(info.agentId);
|
|
195
|
+
if (pending.size === 0)
|
|
196
|
+
resolve();
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
info.child.kill('SIGTERM');
|
|
200
|
+
}
|
|
201
|
+
catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
// Hard timeout: if children don't exit in 5s, SIGKILL and resolve
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
if (pending.size === 0)
|
|
206
|
+
return;
|
|
207
|
+
log('warn', `[codex-supervisor] ${pending.size} daemon(s) didn't exit cleanly; SIGKILL`);
|
|
208
|
+
for (const info of daemons.values()) {
|
|
209
|
+
if (pending.has(info.agentId)) {
|
|
210
|
+
try {
|
|
211
|
+
info.child.kill('SIGKILL');
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
resolve();
|
|
217
|
+
}, 5000);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
export function getDaemonSnapshots() {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
return [...daemons.values()].map(info => ({
|
|
223
|
+
agent_id: info.agentId,
|
|
224
|
+
display_name: info.displayName,
|
|
225
|
+
pid: info.pid,
|
|
226
|
+
uptime_ms: now - info.startedAt.getTime(),
|
|
227
|
+
restart_count: info.restartCount,
|
|
228
|
+
ws_url: info.wsUrl,
|
|
229
|
+
thread_id: info.threadId,
|
|
230
|
+
ready: !!info.readyAt,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
/** Look up the live daemon for an agent, by agent_id or external_key.
|
|
234
|
+
* Returns null when no daemon is running for that agent. */
|
|
235
|
+
export function getDaemonForAgent(lookup) {
|
|
236
|
+
let info;
|
|
237
|
+
if (lookup.agentId) {
|
|
238
|
+
info = daemons.get(lookup.agentId);
|
|
239
|
+
}
|
|
240
|
+
else if (lookup.agentKey) {
|
|
241
|
+
// No reverse index — small N, just scan. Need DB lookup to map key→id.
|
|
242
|
+
// To avoid pulling db in here, callers should resolve key→agent_id
|
|
243
|
+
// upstream (the MCP tool does that) and pass agent_id.
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (!info)
|
|
247
|
+
return null;
|
|
248
|
+
return {
|
|
249
|
+
agent_id: info.agentId,
|
|
250
|
+
display_name: info.displayName,
|
|
251
|
+
pid: info.pid,
|
|
252
|
+
uptime_ms: Date.now() - info.startedAt.getTime(),
|
|
253
|
+
restart_count: info.restartCount,
|
|
254
|
+
ws_url: info.wsUrl,
|
|
255
|
+
thread_id: info.threadId,
|
|
256
|
+
ready: !!info.readyAt,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** Called by /admin/codex-daemon-ready when a daemon's codex app-server is up
|
|
260
|
+
* and its thread is created. Stores ws_url + thread_id on the DaemonInfo so
|
|
261
|
+
* outside callers can attach via `codex --remote <ws_url>`. */
|
|
262
|
+
export function markDaemonReady(agentId, wsUrl, threadId) {
|
|
263
|
+
const info = daemons.get(agentId);
|
|
264
|
+
if (!info)
|
|
265
|
+
return false;
|
|
266
|
+
info.wsUrl = wsUrl;
|
|
267
|
+
info.threadId = threadId;
|
|
268
|
+
info.readyAt = new Date();
|
|
269
|
+
// Persist thread_id so next spawn (after daemon kill / serve restart) can
|
|
270
|
+
// resume the same codex thread instead of starting fresh. Idempotent —
|
|
271
|
+
// setting the same id is a cheap UPDATE.
|
|
272
|
+
try {
|
|
273
|
+
setAgentThreadId(agentId, threadId);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
log('warn', `[codex-supervisor] failed to persist thread_id for ${agentId}: ${err}`);
|
|
277
|
+
}
|
|
278
|
+
log('info', `[codex-supervisor] daemon "${info.displayName}" ready: ws=${wsUrl} thread=${threadId.slice(0, 8)}...`);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
/** Called by /admin/notify-agent-created when a CLI-side `agent register`
|
|
282
|
+
* inserts a row. The CLI runs in its own process so the in-process
|
|
283
|
+
* onAgentCreated hook doesn't reach the supervisor; this is the bridge.
|
|
284
|
+
* Idempotent: returns false if already spawned, true if a new spawn started. */
|
|
285
|
+
export function notifyAgentCreated(agentId) {
|
|
286
|
+
if (daemons.has(agentId))
|
|
287
|
+
return false;
|
|
288
|
+
const agent = getAgentById(agentId);
|
|
289
|
+
if (!agent)
|
|
290
|
+
return false;
|
|
291
|
+
if (agent.tool !== 'codex' || agent.origin_peer !== '')
|
|
292
|
+
return false;
|
|
293
|
+
log('info', `[codex-supervisor] notify-agent-created: "${agent.display_name}" (${agentId.slice(-12)}) → spawning daemon`);
|
|
294
|
+
spawnDaemon(agent.id, agent.display_name, 0);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
/** Kill the daemon (if any) for an agent that just got removed. Mirror of
|
|
298
|
+
* notifyAgentCreated for the delete path. Without this, `kitty-hive agent
|
|
299
|
+
* remove` deletes the row but leaves a ghost daemon (and its codex
|
|
300
|
+
* app-server + ws) running — see Bug 1 reported on 2026-05-20: a stale
|
|
301
|
+
* ws confused TUI routing into a schism. SIGTERM the child; the existing
|
|
302
|
+
* child.on('exit') handler in spawnDaemon checks `getAgentById(agentId)`
|
|
303
|
+
* before respawning, so a deleted row naturally short-circuits the
|
|
304
|
+
* respawn logic — no per-daemon "intentional kill" flag needed.
|
|
305
|
+
* Returns true if a daemon was killed. */
|
|
306
|
+
export function notifyAgentRemoved(agentId) {
|
|
307
|
+
const info = daemons.get(agentId);
|
|
308
|
+
if (!info)
|
|
309
|
+
return false;
|
|
310
|
+
log('info', `[codex-supervisor] notify-agent-removed: "${info.displayName}" (${agentId.slice(-12)}) → killing daemon`);
|
|
311
|
+
try {
|
|
312
|
+
info.child.kill('SIGTERM');
|
|
313
|
+
}
|
|
314
|
+
catch { /* ignore */ }
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
/** Force a daemon to (re)spawn for an agent, then wait up to `timeoutMs` for
|
|
318
|
+
* it to reach ready state. Used by /admin/codex-set-thread when the caller
|
|
319
|
+
* has just mutated agents.thread_id and needs the daemon to pick up the
|
|
320
|
+
* new value (either via thread/resume of a different thread, or thread/start
|
|
321
|
+
* for a fresh thread when thread_id was cleared).
|
|
322
|
+
*
|
|
323
|
+
* Two paths:
|
|
324
|
+
* - Daemon already running → SIGTERM it; supervisor's on-exit handler
|
|
325
|
+
* respawns automatically (with backoff, but immediate for the first
|
|
326
|
+
* restart). The new daemon will pick up the fresh agents.thread_id at
|
|
327
|
+
* its env-injection step.
|
|
328
|
+
* - No daemon → call notifyAgentCreated (skips if not eligible).
|
|
329
|
+
*
|
|
330
|
+
* After kicking, polls `daemons.get(agentId)` for readiness every 200ms.
|
|
331
|
+
* Returns the ready snapshot, or null on timeout. The caller is expected to
|
|
332
|
+
* have validated that the agent exists and is tool=codex; this function
|
|
333
|
+
* trusts that. */
|
|
334
|
+
export async function requestDaemonRespawn(agentId, timeoutMs = 30_000) {
|
|
335
|
+
const existing = daemons.get(agentId);
|
|
336
|
+
if (existing) {
|
|
337
|
+
log('info', `[codex-supervisor] requestDaemonRespawn: SIGTERM existing daemon for "${existing.displayName}" (${agentId.slice(-12)})`);
|
|
338
|
+
try {
|
|
339
|
+
existing.child.kill('SIGTERM');
|
|
340
|
+
}
|
|
341
|
+
catch { /* ignore */ }
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
log('info', `[codex-supervisor] requestDaemonRespawn: no live daemon for ${agentId.slice(-12)}, requesting spawn`);
|
|
345
|
+
notifyAgentCreated(agentId);
|
|
346
|
+
}
|
|
347
|
+
const startedAt = Date.now();
|
|
348
|
+
// Wait for the OLD daemon (if any) to exit and the supervisor's restart
|
|
349
|
+
// backoff to fire, plus the new daemon to call markDaemonReady (which
|
|
350
|
+
// happens after thread/resume or thread/start succeeds and the daemon
|
|
351
|
+
// POSTs /admin/codex-daemon-ready). Poll cheaply: 200ms is well below the
|
|
352
|
+
// typical daemon spawn-to-ready time (~5s) and short enough to bound
|
|
353
|
+
// perceived latency for the kitty UI caller.
|
|
354
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
355
|
+
await new Promise(r => setTimeout(r, 200));
|
|
356
|
+
const snap = daemons.get(agentId);
|
|
357
|
+
if (!snap)
|
|
358
|
+
continue; // still respawning; existing was killed, new not yet started
|
|
359
|
+
if (!snap.readyAt)
|
|
360
|
+
continue; // spawned but codex app-server / thread not ready yet
|
|
361
|
+
// Don't return the old daemon's snapshot if we just SIGTERM'd it — the
|
|
362
|
+
// pid will have changed when the new daemon spawns. Use startedAt as a
|
|
363
|
+
// sentinel: the new daemon's startedAt > our startedAt.
|
|
364
|
+
if (existing && snap.startedAt.getTime() <= startedAt)
|
|
365
|
+
continue;
|
|
366
|
+
return {
|
|
367
|
+
agent_id: snap.agentId,
|
|
368
|
+
display_name: snap.displayName,
|
|
369
|
+
pid: snap.pid,
|
|
370
|
+
uptime_ms: Date.now() - snap.startedAt.getTime(),
|
|
371
|
+
restart_count: snap.restartCount,
|
|
372
|
+
ws_url: snap.wsUrl,
|
|
373
|
+
thread_id: snap.threadId,
|
|
374
|
+
ready: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=codex-supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-supervisor.js","sourceRoot":"","sources":["../src/codex-supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts
CHANGED
|
@@ -19,9 +19,22 @@ export interface CreateAgentOpts {
|
|
|
19
19
|
externalKey?: string;
|
|
20
20
|
originPeer?: string;
|
|
21
21
|
remoteId?: string;
|
|
22
|
+
projectDir?: string;
|
|
22
23
|
}
|
|
24
|
+
type AgentCreateListener = (agent: Agent) => void;
|
|
25
|
+
export declare function onAgentCreated(fn: AgentCreateListener): void;
|
|
23
26
|
export declare function createAgent(displayName: string, tool: string, roles: string, expertise: string, opts?: CreateAgentOpts): Agent;
|
|
24
27
|
export declare function getAgentByExternalKey(key: string): Agent | undefined;
|
|
28
|
+
/** Update an agent's project_dir hint (used by codex-supervisor as cwd when
|
|
29
|
+
* spawning the daemon's codex app-server). Idempotent; empty string = clear.
|
|
30
|
+
* No-op for unknown ids. */
|
|
31
|
+
export declare function setAgentProjectDir(agentId: string, projectDir: string): void;
|
|
32
|
+
/** Persist the codex thread/session uuid for a codex agent. Supervisor reads
|
|
33
|
+
* this on daemon (re)spawn and injects via HIVE_AGENT_THREAD_ID env, so the
|
|
34
|
+
* daemon calls `thread/resume` instead of `thread/start` — preserving the
|
|
35
|
+
* jsonl-backed conversation history across daemon kill / serve restart.
|
|
36
|
+
* Empty string clears (used as fallback when resume fails on a stale id). */
|
|
37
|
+
export declare function setAgentThreadId(agentId: string, threadId: string): void;
|
|
25
38
|
/** Try to set agent.external_key. Silently no-ops on UNIQUE conflict (another
|
|
26
39
|
* agent already owns that key). Returns true on success, false on conflict. */
|
|
27
40
|
export declare function trySetAgentExternalKey(agentId: string, key: string): boolean;
|
|
@@ -33,6 +46,12 @@ export declare function getAgentByToken(token: string): Agent | undefined;
|
|
|
33
46
|
export declare function getAgentById(id: string): Agent | undefined;
|
|
34
47
|
export declare function getAgentsByName(name: string): Agent[];
|
|
35
48
|
export declare function listAllAgents(activeOnly?: boolean): Agent[];
|
|
49
|
+
/**
|
|
50
|
+
* Local agents registered with tool='codex'. Used by the codex-supervisor
|
|
51
|
+
* to decide which daemons to auto-spawn on hive serve boot. Excludes remote
|
|
52
|
+
* placeholders (origin_peer != '') since their codex lives on another node.
|
|
53
|
+
*/
|
|
54
|
+
export declare function listLocalCodexAgents(): Agent[];
|
|
36
55
|
/**
|
|
37
56
|
* Find an active local agent matching a role string.
|
|
38
57
|
*
|
|
@@ -113,6 +132,12 @@ export declare function getAgentTasks(agentId: string, status?: string): Task[];
|
|
|
113
132
|
export declare function getTeamTasks(teamId: string, status?: string): Task[];
|
|
114
133
|
export declare function appendTaskEvent(taskId: string, type: TaskEventType, actorAgentId: string | null, payload?: object): TaskEvent;
|
|
115
134
|
export declare function getTaskEvents(taskId: string, since?: number, limit?: number): TaskEvent[];
|
|
135
|
+
/** Latest seq number in task_events for a task (0 if no events). Used by the
|
|
136
|
+
* MCP layer to build stable event_id values for push notifications — calling
|
|
137
|
+
* this RIGHT AFTER a handler that appended events gives the seq of that
|
|
138
|
+
* handler's last event. Stable across re-delivery, so channel-side dedup
|
|
139
|
+
* can absorb duplicates from queue replay / daemon retry. */
|
|
140
|
+
export declare function getLatestTaskEventSeq(taskId: string): number;
|
|
116
141
|
export declare function getReadCursor(agentId: string, targetType: string, targetId: string): number;
|
|
117
142
|
export declare function setReadCursor(agentId: string, targetType: string, targetId: string, seq: number): void;
|
|
118
143
|
export interface UnreadSummary {
|
|
@@ -167,3 +192,4 @@ export declare function deletePendingInvite(tokenId: string): void;
|
|
|
167
192
|
export declare function cleanupExpiredInvites(): number;
|
|
168
193
|
export declare function isPeerExposed(peerName: string, agentName: string): boolean;
|
|
169
194
|
export declare function cleanupStaleTasks(maxAgeDays?: number): number;
|
|
195
|
+
export {};
|
package/dist/db.js
CHANGED
|
@@ -169,6 +169,17 @@ export function initDB(dbPath) {
|
|
|
169
169
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_external_key
|
|
170
170
|
ON agents(external_key) WHERE external_key != '';
|
|
171
171
|
`);
|
|
172
|
+
// project_dir: operator-supplied working directory hint. codex-supervisor
|
|
173
|
+
// uses it as cwd when spawning the daemon's codex app-server, so the TUI
|
|
174
|
+
// user attaches via `codex --remote` lands in the right project — without
|
|
175
|
+
// it, codex sees serve's cwd and the user sees the wrong files.
|
|
176
|
+
addColumnIfMissing('agents', 'project_dir', "TEXT NOT NULL DEFAULT ''");
|
|
177
|
+
// thread_id: persisted codex thread/session uuid for the agent's daemon.
|
|
178
|
+
// When the daemon respawns, supervisor injects this as HIVE_AGENT_THREAD_ID;
|
|
179
|
+
// daemon then calls `thread/resume {threadId}` instead of `thread/start`,
|
|
180
|
+
// so conversation history (stored as jsonl by codex app-server) survives
|
|
181
|
+
// daemon kill / serve restart / machine reboot.
|
|
182
|
+
addColumnIfMissing('agents', 'thread_id', "TEXT NOT NULL DEFAULT ''");
|
|
172
183
|
// Migrate tasks.status CHECK constraint when an older DB lacks
|
|
173
184
|
// 'awaiting_approval' (added in v0.5.6 but the schema migration was missed
|
|
174
185
|
// until v0.5.9). SQLite has no ALTER COLUMN, so we recreate the table.
|
|
@@ -314,6 +325,22 @@ function migrateTeamsAddRules(db) {
|
|
|
314
325
|
db.exec("ALTER TABLE teams ADD COLUMN rules TEXT NOT NULL DEFAULT ''");
|
|
315
326
|
console.log('[db] added teams.rules column');
|
|
316
327
|
}
|
|
328
|
+
const agentCreateListeners = [];
|
|
329
|
+
export function onAgentCreated(fn) {
|
|
330
|
+
agentCreateListeners.push(fn);
|
|
331
|
+
}
|
|
332
|
+
function emitAgentCreated(agent) {
|
|
333
|
+
for (const fn of agentCreateListeners) {
|
|
334
|
+
try {
|
|
335
|
+
fn(agent);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
// listener errors are isolated; don't break createAgent
|
|
339
|
+
// eslint-disable-next-line no-console
|
|
340
|
+
console.error('[db] onAgentCreated listener threw:', err);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
317
344
|
export function createAgent(displayName, tool, roles, expertise, opts = {}) {
|
|
318
345
|
const agent = {
|
|
319
346
|
id: opts.id || ulid(),
|
|
@@ -324,11 +351,14 @@ export function createAgent(displayName, tool, roles, expertise, opts = {}) {
|
|
|
324
351
|
origin_peer: opts.originPeer || '',
|
|
325
352
|
remote_id: opts.remoteId || '',
|
|
326
353
|
external_key: opts.externalKey || '',
|
|
354
|
+
project_dir: opts.projectDir || '',
|
|
355
|
+
thread_id: '',
|
|
327
356
|
};
|
|
328
357
|
getDB().prepare(`
|
|
329
|
-
INSERT INTO agents (id, display_name, token, tool, roles, expertise, status, created_at, last_seen, origin_peer, remote_id, external_key)
|
|
330
|
-
VALUES (@id, @display_name, @token, @tool, @roles, @expertise, @status, @created_at, @last_seen, @origin_peer, @remote_id, @external_key)
|
|
358
|
+
INSERT INTO agents (id, display_name, token, tool, roles, expertise, status, created_at, last_seen, origin_peer, remote_id, external_key, project_dir)
|
|
359
|
+
VALUES (@id, @display_name, @token, @tool, @roles, @expertise, @status, @created_at, @last_seen, @origin_peer, @remote_id, @external_key, @project_dir)
|
|
331
360
|
`).run(agent);
|
|
361
|
+
emitAgentCreated(agent);
|
|
332
362
|
return agent;
|
|
333
363
|
}
|
|
334
364
|
export function getAgentByExternalKey(key) {
|
|
@@ -336,6 +366,20 @@ export function getAgentByExternalKey(key) {
|
|
|
336
366
|
return undefined;
|
|
337
367
|
return getDB().prepare('SELECT * FROM agents WHERE external_key = ?').get(key);
|
|
338
368
|
}
|
|
369
|
+
/** Update an agent's project_dir hint (used by codex-supervisor as cwd when
|
|
370
|
+
* spawning the daemon's codex app-server). Idempotent; empty string = clear.
|
|
371
|
+
* No-op for unknown ids. */
|
|
372
|
+
export function setAgentProjectDir(agentId, projectDir) {
|
|
373
|
+
getDB().prepare('UPDATE agents SET project_dir = ? WHERE id = ?').run(projectDir || '', agentId);
|
|
374
|
+
}
|
|
375
|
+
/** Persist the codex thread/session uuid for a codex agent. Supervisor reads
|
|
376
|
+
* this on daemon (re)spawn and injects via HIVE_AGENT_THREAD_ID env, so the
|
|
377
|
+
* daemon calls `thread/resume` instead of `thread/start` — preserving the
|
|
378
|
+
* jsonl-backed conversation history across daemon kill / serve restart.
|
|
379
|
+
* Empty string clears (used as fallback when resume fails on a stale id). */
|
|
380
|
+
export function setAgentThreadId(agentId, threadId) {
|
|
381
|
+
getDB().prepare('UPDATE agents SET thread_id = ? WHERE id = ?').run(threadId || '', agentId);
|
|
382
|
+
}
|
|
339
383
|
/** Try to set agent.external_key. Silently no-ops on UNIQUE conflict (another
|
|
340
384
|
* agent already owns that key). Returns true on success, false on conflict. */
|
|
341
385
|
export function trySetAgentExternalKey(agentId, key) {
|
|
@@ -388,6 +432,14 @@ export function listAllAgents(activeOnly = false) {
|
|
|
388
432
|
: 'SELECT * FROM agents ORDER BY last_seen DESC';
|
|
389
433
|
return getDB().prepare(sql).all();
|
|
390
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Local agents registered with tool='codex'. Used by the codex-supervisor
|
|
437
|
+
* to decide which daemons to auto-spawn on hive serve boot. Excludes remote
|
|
438
|
+
* placeholders (origin_peer != '') since their codex lives on another node.
|
|
439
|
+
*/
|
|
440
|
+
export function listLocalCodexAgents() {
|
|
441
|
+
return getDB().prepare("SELECT * FROM agents WHERE tool = 'codex' AND origin_peer = '' ORDER BY display_name").all();
|
|
442
|
+
}
|
|
391
443
|
/**
|
|
392
444
|
* Find an active local agent matching a role string.
|
|
393
445
|
*
|
|
@@ -700,6 +752,15 @@ export function appendTaskEvent(taskId, type, actorAgentId, payload = {}) {
|
|
|
700
752
|
export function getTaskEvents(taskId, since = 0, limit = 100) {
|
|
701
753
|
return getDB().prepare('SELECT * FROM task_events WHERE task_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?').all(taskId, since, limit);
|
|
702
754
|
}
|
|
755
|
+
/** Latest seq number in task_events for a task (0 if no events). Used by the
|
|
756
|
+
* MCP layer to build stable event_id values for push notifications — calling
|
|
757
|
+
* this RIGHT AFTER a handler that appended events gives the seq of that
|
|
758
|
+
* handler's last event. Stable across re-delivery, so channel-side dedup
|
|
759
|
+
* can absorb duplicates from queue replay / daemon retry. */
|
|
760
|
+
export function getLatestTaskEventSeq(taskId) {
|
|
761
|
+
const row = getDB().prepare('SELECT COALESCE(MAX(seq), 0) AS max_seq FROM task_events WHERE task_id = ?').get(taskId);
|
|
762
|
+
return row.max_seq;
|
|
763
|
+
}
|
|
703
764
|
// --- Read cursors ---
|
|
704
765
|
export function getReadCursor(agentId, targetType, targetId) {
|
|
705
766
|
const row = getDB().prepare('SELECT last_seq FROM read_cursors WHERE agent_id = ? AND target_type = ? AND target_id = ?').get(agentId, targetType, targetId);
|