@gonzih/cc-discord 0.2.36 → 0.2.38
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/meta-agent-manager.d.ts +24 -15
- package/dist/meta-agent-manager.js +177 -107
- package/package.json +1 -1
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Flow per namespace:
|
|
5
5
|
* 1. ensureWorkspace: git clone repo to ~/cc-discord-workspace/{ns}
|
|
6
6
|
* 2. injectMcp: write .mcp.json so the claude subprocess has MCP tool access
|
|
7
|
-
* 3. ensureSession:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 3. ensureSession: spawn one persistent `claude --continue` process per namespace
|
|
8
|
+
* (no -p flag — messages go via stdin)
|
|
9
|
+
* 4. On new message: write "${message}\n" to stdin of the running process
|
|
10
|
+
* 5. On process exit: remove from sessions map; next message respawns
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The polling loop now just drains any queued Redis messages into stdin.
|
|
13
|
+
* No more per-message process spawn — Claude receives messages in sequence.
|
|
13
14
|
*/
|
|
14
15
|
import type { createCcWire } from "@gonzih/cc-wire";
|
|
15
16
|
type Wire = ReturnType<typeof createCcWire>;
|
|
@@ -43,9 +44,12 @@ export declare function injectMcp(ns: string, wsPath: string, token: string): vo
|
|
|
43
44
|
export declare const metaStreamChannel: (ns: string) => string;
|
|
44
45
|
export declare const metaLogKey: (ns: string) => string;
|
|
45
46
|
/**
|
|
46
|
-
* Spawn `claude --continue -p
|
|
47
|
-
*
|
|
47
|
+
* Spawn `claude --continue -p "{message}" --dangerously-skip-permissions` in the
|
|
48
|
+
* namespace workspace. Pipes stdout line-by-line → wire.discord.publishOutgoing.
|
|
49
|
+
* Also streams each chunk to Redis: PUBLISH cca:meta:{ns}:stream and LPUSH cca:meta:{ns}:log.
|
|
48
50
|
* Returns a Promise that resolves when the process exits.
|
|
51
|
+
*
|
|
52
|
+
* Kept for backwards-compat and direct integration-test usage.
|
|
49
53
|
*/
|
|
50
54
|
export declare function spawnSession(ns: string, message: string, token: string, wire: Wire): Promise<void>;
|
|
51
55
|
export interface MetaAgentManager {
|
|
@@ -56,20 +60,25 @@ export interface MetaAgentManager {
|
|
|
56
60
|
repoUrl: string;
|
|
57
61
|
}>, instanceId?: string) => void;
|
|
58
62
|
stop: () => void;
|
|
59
|
-
/**
|
|
63
|
+
/** Kill and remove the persistent session for a namespace (e.g. on /clear). */
|
|
60
64
|
killSession: (ns: string) => void;
|
|
61
|
-
/**
|
|
65
|
+
/** Write a raw line to the stdin of the running session, if any. */
|
|
62
66
|
sendToSession: (ns: string, line: string) => void;
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
65
|
-
* Create a MetaAgentManager that
|
|
69
|
+
* Create a MetaAgentManager that maintains one persistent Claude process per namespace.
|
|
70
|
+
*
|
|
71
|
+
* On first message for a namespace:
|
|
72
|
+
* 1. ensureWorkspace + injectMcp
|
|
73
|
+
* 2. Drain any pending Redis queue entries to stdin
|
|
74
|
+
* 3. Write the new message to stdin
|
|
75
|
+
*
|
|
76
|
+
* On subsequent messages: write directly to stdin of the running process.
|
|
66
77
|
*
|
|
67
|
-
*
|
|
68
|
-
* then stdin is closed (EOF) so Claude processes and exits. --continue maintains
|
|
69
|
-
* conversation history via the JSONL session file in the workspace.
|
|
78
|
+
* On process exit: remove from sessions map. Next message triggers a respawn.
|
|
70
79
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
80
|
+
* The 3-second poll loop drains any messages that arrived while a session was
|
|
81
|
+
* starting up or temporarily unavailable.
|
|
73
82
|
*/
|
|
74
83
|
export declare function createMetaAgentManager(): MetaAgentManager;
|
|
75
84
|
/**
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Flow per namespace:
|
|
5
5
|
* 1. ensureWorkspace: git clone repo to ~/cc-discord-workspace/{ns}
|
|
6
6
|
* 2. injectMcp: write .mcp.json so the claude subprocess has MCP tool access
|
|
7
|
-
* 3. ensureSession:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 3. ensureSession: spawn one persistent `claude --continue` process per namespace
|
|
8
|
+
* (no -p flag — messages go via stdin)
|
|
9
|
+
* 4. On new message: write "${message}\n" to stdin of the running process
|
|
10
|
+
* 5. On process exit: remove from sessions map; next message respawns
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The polling loop now just drains any queued Redis messages into stdin.
|
|
13
|
+
* No more per-message process spawn — Claude receives messages in sequence.
|
|
13
14
|
*/
|
|
14
15
|
import { spawn, execSync } from "child_process";
|
|
15
16
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
@@ -256,9 +257,12 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
256
257
|
});
|
|
257
258
|
}
|
|
258
259
|
/**
|
|
259
|
-
* Spawn `claude --continue -p
|
|
260
|
-
*
|
|
260
|
+
* Spawn `claude --continue -p "{message}" --dangerously-skip-permissions` in the
|
|
261
|
+
* namespace workspace. Pipes stdout line-by-line → wire.discord.publishOutgoing.
|
|
262
|
+
* Also streams each chunk to Redis: PUBLISH cca:meta:{ns}:stream and LPUSH cca:meta:{ns}:log.
|
|
261
263
|
* Returns a Promise that resolves when the process exits.
|
|
264
|
+
*
|
|
265
|
+
* Kept for backwards-compat and direct integration-test usage.
|
|
262
266
|
*/
|
|
263
267
|
export function spawnSession(ns, message, token, wire) {
|
|
264
268
|
return new Promise((resolve, reject) => {
|
|
@@ -267,15 +271,11 @@ export function spawnSession(ns, message, token, wire) {
|
|
|
267
271
|
const env = buildEnv(token);
|
|
268
272
|
const proc = spawn(claudeBin, [
|
|
269
273
|
"--continue",
|
|
270
|
-
"-p",
|
|
274
|
+
"-p", message,
|
|
271
275
|
"--output-format", "stream-json",
|
|
272
276
|
"--verbose",
|
|
273
277
|
"--dangerously-skip-permissions",
|
|
274
|
-
], { cwd: wsPath, env, stdio: ["
|
|
275
|
-
// Write message to stdin, then close (EOF triggers Claude to process and respond)
|
|
276
|
-
proc.stdin.write(message + "\n", () => {
|
|
277
|
-
proc.stdin.end();
|
|
278
|
-
});
|
|
278
|
+
], { cwd: wsPath, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
279
279
|
wireStdoutToRedis(proc, ns, wire);
|
|
280
280
|
proc.on("exit", (code) => {
|
|
281
281
|
console.log(`[meta-agent-manager] session exited (ns=${ns}, code=${code})`);
|
|
@@ -288,75 +288,149 @@ export function spawnSession(ns, message, token, wire) {
|
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
/**
|
|
291
|
-
*
|
|
291
|
+
* Spawn a persistent `claude --continue` process for a namespace.
|
|
292
|
+
* No -p flag — messages are delivered via stdin.
|
|
293
|
+
* Stdout is wired to Redis via wireStdoutToRedis.
|
|
294
|
+
* Returns the ChildProcess.
|
|
295
|
+
*/
|
|
296
|
+
function spawnPersistentSession(ns, token, wire, onExit) {
|
|
297
|
+
const wsPath = workspacePath(ns);
|
|
298
|
+
const claudeBin = resolveClaude();
|
|
299
|
+
const env = buildEnv(token);
|
|
300
|
+
console.log(`[meta-agent-manager] spawning persistent session (ns=${ns})`);
|
|
301
|
+
// DO NOT ADD -p OR --input-format stream-json HERE.
|
|
302
|
+
// With -p + --input-format stream-json, Claude buffers all stdin until EOF before responding.
|
|
303
|
+
// Since the stdin pipe is never closed (it stays open for subsequent messages), Claude
|
|
304
|
+
// waits forever and never produces output. Interactive mode (no -p) reads one line at a
|
|
305
|
+
// time and responds immediately. Verified by direct test: `echo msg | claude --continue
|
|
306
|
+
// --output-format stream-json ...` responds correctly; the -p variant does not.
|
|
307
|
+
const proc = spawn(claudeBin, [
|
|
308
|
+
"--continue",
|
|
309
|
+
"--output-format", "stream-json",
|
|
310
|
+
"--verbose",
|
|
311
|
+
"--dangerously-skip-permissions",
|
|
312
|
+
], { cwd: wsPath, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
313
|
+
proc.stdin.setDefaultEncoding("utf8");
|
|
314
|
+
wireStdoutToRedis(proc, ns, wire);
|
|
315
|
+
proc.on("exit", (code) => {
|
|
316
|
+
console.log(`[meta-agent-manager] persistent session exited (ns=${ns}, code=${code})`);
|
|
317
|
+
onExit();
|
|
318
|
+
});
|
|
319
|
+
proc.on("error", (err) => {
|
|
320
|
+
console.error(`[meta-agent-manager] persistent session spawn error (ns=${ns}):`, err.message);
|
|
321
|
+
onExit();
|
|
322
|
+
});
|
|
323
|
+
return proc;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Create a MetaAgentManager that maintains one persistent Claude process per namespace.
|
|
292
327
|
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
328
|
+
* On first message for a namespace:
|
|
329
|
+
* 1. ensureWorkspace + injectMcp
|
|
330
|
+
* 2. Drain any pending Redis queue entries to stdin
|
|
331
|
+
* 3. Write the new message to stdin
|
|
296
332
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
333
|
+
* On subsequent messages: write directly to stdin of the running process.
|
|
334
|
+
*
|
|
335
|
+
* On process exit: remove from sessions map. Next message triggers a respawn.
|
|
336
|
+
*
|
|
337
|
+
* The 3-second poll loop drains any messages that arrived while a session was
|
|
338
|
+
* starting up or temporarily unavailable.
|
|
299
339
|
*/
|
|
300
340
|
export function createMetaAgentManager() {
|
|
301
341
|
let pollInterval = null;
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
// Cached session metadata (token, wire, repoUrl) per namespace
|
|
307
|
-
const sessionMeta = new Map();
|
|
342
|
+
/** One persistent ChildProcess per namespace. */
|
|
343
|
+
const sessions = new Map();
|
|
344
|
+
/** Namespaces currently being set up (workspace clone / first spawn). */
|
|
345
|
+
const startingUp = new Set();
|
|
308
346
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
347
|
+
* Write a line to the stdin of the persistent session for ns.
|
|
348
|
+
* Silently no-ops if no session exists.
|
|
311
349
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const queue = queues.get(ns);
|
|
316
|
-
if (!queue || queue.length === 0)
|
|
317
|
-
return;
|
|
318
|
-
const meta = sessionMeta.get(ns);
|
|
319
|
-
if (!meta)
|
|
350
|
+
function writeToStdin(ns, line) {
|
|
351
|
+
const session = sessions.get(ns);
|
|
352
|
+
if (!session)
|
|
320
353
|
return;
|
|
321
|
-
processing.add(ns);
|
|
322
|
-
console.log(`[meta-agent-manager] processQueue started (ns=${ns})`);
|
|
323
354
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (!q || q.length === 0)
|
|
327
|
-
break;
|
|
328
|
-
const message = q.shift();
|
|
329
|
-
const { token, wire } = sessionMeta.get(ns) ?? meta;
|
|
330
|
-
console.log(`[meta-agent-manager] spawning session for message (ns=${ns})`);
|
|
331
|
-
try {
|
|
332
|
-
await spawnSession(ns, message, token, wire);
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
console.error(`[meta-agent-manager] spawnSession error (ns=${ns}):`, err.message);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
355
|
+
// Interactive mode: plain text line, Claude reads it as user input
|
|
356
|
+
session.proc.stdin.write(`${line}\n`);
|
|
338
357
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
358
|
+
catch (err) {
|
|
359
|
+
console.warn(`[meta-agent-manager] stdin write failed (ns=${ns}):`, err.message);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Drain all pending messages from the Redis input queue into the session's stdin.
|
|
364
|
+
*/
|
|
365
|
+
async function drainQueue(ns, wire) {
|
|
366
|
+
const inputKey = discordMetaInputKey(ns);
|
|
367
|
+
for (;;) {
|
|
368
|
+
let raw;
|
|
369
|
+
try {
|
|
370
|
+
raw = await wire._redis.lpop(inputKey);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
if (!raw)
|
|
376
|
+
break;
|
|
377
|
+
let content;
|
|
378
|
+
try {
|
|
379
|
+
content = JSON.parse(raw).content ?? raw;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
content = raw;
|
|
383
|
+
}
|
|
384
|
+
writeToStdin(ns, content);
|
|
342
385
|
}
|
|
343
386
|
}
|
|
344
387
|
/**
|
|
345
|
-
* Ensure
|
|
388
|
+
* Ensure a persistent session exists for ns.
|
|
389
|
+
* If one already exists, returns immediately.
|
|
390
|
+
* If not, spawns one, drains any queued messages, then writes `firstMessage` if provided.
|
|
346
391
|
*/
|
|
347
392
|
async function ensureSession(ns, repoUrl, token, wire, firstMessage) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
if (sessions.has(ns)) {
|
|
394
|
+
if (firstMessage !== undefined)
|
|
395
|
+
writeToStdin(ns, firstMessage);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (startingUp.has(ns)) {
|
|
399
|
+
// Already spinning up — queue the message so drainQueue picks it up
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
startingUp.add(ns);
|
|
403
|
+
try {
|
|
404
|
+
// Ensure workspace exists
|
|
405
|
+
const wsPath = workspacePath(ns);
|
|
406
|
+
await ensureWorkspace(ns, repoUrl);
|
|
407
|
+
injectMcp(ns, wsPath, token);
|
|
408
|
+
const proc = spawnPersistentSession(ns, token, wire, () => {
|
|
409
|
+
// On exit: remove from map so next message triggers a respawn
|
|
410
|
+
sessions.delete(ns);
|
|
411
|
+
console.log(`[meta-agent-manager] session removed from map (ns=${ns})`);
|
|
412
|
+
});
|
|
413
|
+
sessions.set(ns, { proc, ns });
|
|
414
|
+
// Drain any messages that arrived before this session started
|
|
415
|
+
await drainQueue(ns, wire);
|
|
416
|
+
// Write the triggering message last (after queue drain, in order)
|
|
417
|
+
if (firstMessage !== undefined)
|
|
418
|
+
writeToStdin(ns, firstMessage);
|
|
419
|
+
await wire.discord.setStatus(ns, {
|
|
420
|
+
namespace: ns,
|
|
421
|
+
status: "running",
|
|
422
|
+
isTyping: true,
|
|
423
|
+
turnCount: 0,
|
|
424
|
+
updatedAt: new Date().toISOString(),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
console.error(`[meta-agent-manager] ensureSession failed (ns=${ns}):`, err.message);
|
|
429
|
+
sessions.delete(ns);
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
startingUp.delete(ns);
|
|
356
433
|
}
|
|
357
|
-
processQueue(ns).catch((err) => {
|
|
358
|
-
console.error(`[meta-agent-manager] processQueue error (ns=${ns}):`, err.message);
|
|
359
|
-
});
|
|
360
434
|
}
|
|
361
435
|
return {
|
|
362
436
|
async ensureWorkspace(ns, repoUrl) {
|
|
@@ -368,7 +442,7 @@ export function createMetaAgentManager() {
|
|
|
368
442
|
},
|
|
369
443
|
startPolling(wire, getNamespaces, instanceId) {
|
|
370
444
|
if (pollInterval)
|
|
371
|
-
return;
|
|
445
|
+
return; // already running
|
|
372
446
|
pollInterval = setInterval(() => {
|
|
373
447
|
const namespaces = getNamespaces();
|
|
374
448
|
if (namespaces.length === 0)
|
|
@@ -383,21 +457,25 @@ export function createMetaAgentManager() {
|
|
|
383
457
|
}
|
|
384
458
|
}).catch(() => { });
|
|
385
459
|
}
|
|
386
|
-
//
|
|
387
|
-
if (
|
|
460
|
+
// If session exists, drain any queued messages
|
|
461
|
+
if (sessions.has(ns)) {
|
|
462
|
+
drainQueue(ns, wire).catch((err) => {
|
|
463
|
+
console.warn(`[meta-agent-manager] drainQueue error (ns=${ns}):`, err.message);
|
|
464
|
+
});
|
|
388
465
|
continue;
|
|
389
|
-
|
|
466
|
+
}
|
|
467
|
+
// Check if anything is queued for this namespace
|
|
390
468
|
const inputKey = discordMetaInputKey(ns);
|
|
391
469
|
wire._redis.llen(inputKey).then(async (queueLen) => {
|
|
392
470
|
if (queueLen === 0)
|
|
393
471
|
return;
|
|
472
|
+
// Resolve token
|
|
394
473
|
let token;
|
|
395
474
|
try {
|
|
396
475
|
token = await wire.token.getMaster();
|
|
397
476
|
}
|
|
398
477
|
catch {
|
|
399
|
-
token =
|
|
400
|
-
?? process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
478
|
+
token = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
401
479
|
?? process.env.CLAUDE_CODE_TOKEN
|
|
402
480
|
?? process.env.ANTHROPIC_API_KEY
|
|
403
481
|
?? "";
|
|
@@ -406,31 +484,8 @@ export function createMetaAgentManager() {
|
|
|
406
484
|
console.warn(`[meta-agent-manager] no token available, skipping session for ns=${ns}`);
|
|
407
485
|
return;
|
|
408
486
|
}
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
queues.set(ns, []);
|
|
412
|
-
for (;;) {
|
|
413
|
-
const raw = await wire._redis.lpop(inputKey).catch(() => null);
|
|
414
|
-
if (!raw)
|
|
415
|
-
break;
|
|
416
|
-
let content;
|
|
417
|
-
try {
|
|
418
|
-
content = JSON.parse(raw).content ?? raw;
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
content = raw;
|
|
422
|
-
}
|
|
423
|
-
queues.get(ns).push(content);
|
|
424
|
-
}
|
|
425
|
-
if (!sessionMeta.has(ns)) {
|
|
426
|
-
sessionMeta.set(ns, { token, wire, repoUrl });
|
|
427
|
-
}
|
|
428
|
-
const wsPath = workspacePath(ns);
|
|
429
|
-
await ensureWorkspace(ns, repoUrl);
|
|
430
|
-
injectMcp(ns, wsPath, token);
|
|
431
|
-
processQueue(ns).catch((err) => {
|
|
432
|
-
console.error(`[meta-agent-manager] processQueue error (ns=${ns}):`, err.message);
|
|
433
|
-
});
|
|
487
|
+
// ensureSession will drain the queue itself
|
|
488
|
+
await ensureSession(ns, repoUrl, token, wire);
|
|
434
489
|
}).catch((err) => {
|
|
435
490
|
console.warn(`[meta-agent-manager] llen error (ns=${ns}):`, err.message);
|
|
436
491
|
});
|
|
@@ -444,20 +499,35 @@ export function createMetaAgentManager() {
|
|
|
444
499
|
pollInterval = null;
|
|
445
500
|
console.log("[meta-agent-manager] polling stopped");
|
|
446
501
|
}
|
|
447
|
-
|
|
502
|
+
// Kill all active sessions
|
|
503
|
+
for (const [ns, session] of sessions) {
|
|
504
|
+
try {
|
|
505
|
+
session.proc.stdin.end();
|
|
506
|
+
session.proc.kill();
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// ignore errors during shutdown
|
|
510
|
+
}
|
|
511
|
+
sessions.delete(ns);
|
|
512
|
+
console.log(`[meta-agent-manager] killed session on stop (ns=${ns})`);
|
|
513
|
+
}
|
|
448
514
|
},
|
|
449
515
|
killSession(ns) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
516
|
+
const session = sessions.get(ns);
|
|
517
|
+
if (!session)
|
|
518
|
+
return;
|
|
519
|
+
try {
|
|
520
|
+
session.proc.stdin.end();
|
|
521
|
+
session.proc.kill();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// ignore
|
|
525
|
+
}
|
|
526
|
+
sessions.delete(ns);
|
|
527
|
+
console.log(`[meta-agent-manager] killed session (ns=${ns})`);
|
|
453
528
|
},
|
|
454
529
|
sendToSession(ns, line) {
|
|
455
|
-
|
|
456
|
-
queues.set(ns, []);
|
|
457
|
-
queues.get(ns).push(line);
|
|
458
|
-
processQueue(ns).catch((err) => {
|
|
459
|
-
console.error(`[meta-agent-manager] processQueue error (ns=${ns}):`, err.message);
|
|
460
|
-
});
|
|
530
|
+
writeToStdin(ns, line);
|
|
461
531
|
},
|
|
462
532
|
};
|
|
463
533
|
}
|