@gonzih/cc-discord 0.2.35 → 0.2.36
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 +15 -24
- package/dist/meta-agent-manager.js +107 -177
- package/package.json +1 -1
|
@@ -4,13 +4,12 @@
|
|
|
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
|
-
*
|
|
10
|
-
* 5. On process exit: remove from sessions map; next message respawns
|
|
7
|
+
* 3. ensureSession: add message to per-namespace queue, start processQueue
|
|
8
|
+
* 4. processQueue: spawn `claude --continue -p` (stdin+EOF) per message, one at a time
|
|
9
|
+
* 5. Context is maintained via --continue (JSONL session file in workspace)
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Spawn-per-message with --continue maintains full conversation history.
|
|
12
|
+
* Messages queue up and process sequentially per namespace.
|
|
14
13
|
*/
|
|
15
14
|
import type { createCcWire } from "@gonzih/cc-wire";
|
|
16
15
|
type Wire = ReturnType<typeof createCcWire>;
|
|
@@ -44,12 +43,9 @@ export declare function injectMcp(ns: string, wsPath: string, token: string): vo
|
|
|
44
43
|
export declare const metaStreamChannel: (ns: string) => string;
|
|
45
44
|
export declare const metaLogKey: (ns: string) => string;
|
|
46
45
|
/**
|
|
47
|
-
* Spawn `claude --continue -p
|
|
48
|
-
*
|
|
49
|
-
* Also streams each chunk to Redis: PUBLISH cca:meta:{ns}:stream and LPUSH cca:meta:{ns}:log.
|
|
46
|
+
* Spawn `claude --continue -p` with message written to stdin, then close stdin (EOF).
|
|
47
|
+
* Claude responds and exits. Context is maintained via --continue (JSONL session file).
|
|
50
48
|
* Returns a Promise that resolves when the process exits.
|
|
51
|
-
*
|
|
52
|
-
* Kept for backwards-compat and direct integration-test usage.
|
|
53
49
|
*/
|
|
54
50
|
export declare function spawnSession(ns: string, message: string, token: string, wire: Wire): Promise<void>;
|
|
55
51
|
export interface MetaAgentManager {
|
|
@@ -60,25 +56,20 @@ export interface MetaAgentManager {
|
|
|
60
56
|
repoUrl: string;
|
|
61
57
|
}>, instanceId?: string) => void;
|
|
62
58
|
stop: () => void;
|
|
63
|
-
/**
|
|
59
|
+
/** Clear the message queue for a namespace (e.g. on /clear). */
|
|
64
60
|
killSession: (ns: string) => void;
|
|
65
|
-
/**
|
|
61
|
+
/** Queue a message for a namespace. */
|
|
66
62
|
sendToSession: (ns: string, line: string) => void;
|
|
67
63
|
}
|
|
68
64
|
/**
|
|
69
|
-
* Create a MetaAgentManager that
|
|
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.
|
|
65
|
+
* Create a MetaAgentManager that processes Discord messages via spawn-per-message.
|
|
77
66
|
*
|
|
78
|
-
*
|
|
67
|
+
* Each message spawns `claude --continue -p` with the message written to stdin,
|
|
68
|
+
* then stdin is closed (EOF) so Claude processes and exits. --continue maintains
|
|
69
|
+
* conversation history via the JSONL session file in the workspace.
|
|
79
70
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
71
|
+
* Messages queue per namespace and process sequentially — no concurrent spawns
|
|
72
|
+
* for the same namespace.
|
|
82
73
|
*/
|
|
83
74
|
export declare function createMetaAgentManager(): MetaAgentManager;
|
|
84
75
|
/**
|
|
@@ -4,13 +4,12 @@
|
|
|
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
|
-
*
|
|
10
|
-
* 5. On process exit: remove from sessions map; next message respawns
|
|
7
|
+
* 3. ensureSession: add message to per-namespace queue, start processQueue
|
|
8
|
+
* 4. processQueue: spawn `claude --continue -p` (stdin+EOF) per message, one at a time
|
|
9
|
+
* 5. Context is maintained via --continue (JSONL session file in workspace)
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Spawn-per-message with --continue maintains full conversation history.
|
|
12
|
+
* Messages queue up and process sequentially per namespace.
|
|
14
13
|
*/
|
|
15
14
|
import { spawn, execSync } from "child_process";
|
|
16
15
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
@@ -257,12 +256,9 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
257
256
|
});
|
|
258
257
|
}
|
|
259
258
|
/**
|
|
260
|
-
* Spawn `claude --continue -p
|
|
261
|
-
*
|
|
262
|
-
* Also streams each chunk to Redis: PUBLISH cca:meta:{ns}:stream and LPUSH cca:meta:{ns}:log.
|
|
259
|
+
* Spawn `claude --continue -p` with message written to stdin, then close stdin (EOF).
|
|
260
|
+
* Claude responds and exits. Context is maintained via --continue (JSONL session file).
|
|
263
261
|
* Returns a Promise that resolves when the process exits.
|
|
264
|
-
*
|
|
265
|
-
* Kept for backwards-compat and direct integration-test usage.
|
|
266
262
|
*/
|
|
267
263
|
export function spawnSession(ns, message, token, wire) {
|
|
268
264
|
return new Promise((resolve, reject) => {
|
|
@@ -271,11 +267,15 @@ export function spawnSession(ns, message, token, wire) {
|
|
|
271
267
|
const env = buildEnv(token);
|
|
272
268
|
const proc = spawn(claudeBin, [
|
|
273
269
|
"--continue",
|
|
274
|
-
"-p",
|
|
270
|
+
"-p",
|
|
275
271
|
"--output-format", "stream-json",
|
|
276
272
|
"--verbose",
|
|
277
273
|
"--dangerously-skip-permissions",
|
|
278
|
-
], { cwd: wsPath, env, stdio: ["
|
|
274
|
+
], { cwd: wsPath, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
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
|
+
});
|
|
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,149 +288,75 @@ export function spawnSession(ns, message, token, wire) {
|
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
/**
|
|
291
|
-
*
|
|
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.
|
|
291
|
+
* Create a MetaAgentManager that processes Discord messages via spawn-per-message.
|
|
327
292
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* 3. Write the new message to stdin
|
|
293
|
+
* Each message spawns `claude --continue -p` with the message written to stdin,
|
|
294
|
+
* then stdin is closed (EOF) so Claude processes and exits. --continue maintains
|
|
295
|
+
* conversation history via the JSONL session file in the workspace.
|
|
332
296
|
*
|
|
333
|
-
*
|
|
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.
|
|
297
|
+
* Messages queue per namespace and process sequentially — no concurrent spawns
|
|
298
|
+
* for the same namespace.
|
|
339
299
|
*/
|
|
340
300
|
export function createMetaAgentManager() {
|
|
341
301
|
let pollInterval = null;
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const
|
|
302
|
+
// Per-namespace message queue (local, drained by processQueue)
|
|
303
|
+
const queues = new Map();
|
|
304
|
+
// Namespaces currently running a spawnSession (blocks concurrent spawns)
|
|
305
|
+
const processing = new Set();
|
|
306
|
+
// Cached session metadata (token, wire, repoUrl) per namespace
|
|
307
|
+
const sessionMeta = new Map();
|
|
346
308
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
309
|
+
* Drain the local queue for a namespace, spawning one session per message.
|
|
310
|
+
* No-ops if already processing or queue is empty.
|
|
349
311
|
*/
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
312
|
+
async function processQueue(ns) {
|
|
313
|
+
if (processing.has(ns))
|
|
314
|
+
return;
|
|
315
|
+
const queue = queues.get(ns);
|
|
316
|
+
if (!queue || queue.length === 0)
|
|
317
|
+
return;
|
|
318
|
+
const meta = sessionMeta.get(ns);
|
|
319
|
+
if (!meta)
|
|
353
320
|
return;
|
|
321
|
+
processing.add(ns);
|
|
322
|
+
console.log(`[meta-agent-manager] processQueue started (ns=${ns})`);
|
|
354
323
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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;
|
|
324
|
+
while (true) {
|
|
325
|
+
const q = queues.get(ns);
|
|
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
|
+
}
|
|
383
337
|
}
|
|
384
|
-
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
processing.delete(ns);
|
|
341
|
+
console.log(`[meta-agent-manager] processQueue done (ns=${ns})`);
|
|
385
342
|
}
|
|
386
343
|
}
|
|
387
344
|
/**
|
|
388
|
-
* Ensure
|
|
389
|
-
* If one already exists, returns immediately.
|
|
390
|
-
* If not, spawns one, drains any queued messages, then writes `firstMessage` if provided.
|
|
345
|
+
* Ensure workspace + MCP are ready, add message to queue, start processing.
|
|
391
346
|
*/
|
|
392
347
|
async function ensureSession(ns, repoUrl, token, wire, firstMessage) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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);
|
|
348
|
+
sessionMeta.set(ns, { token, wire, repoUrl });
|
|
349
|
+
const wsPath = workspacePath(ns);
|
|
350
|
+
await ensureWorkspace(ns, repoUrl);
|
|
351
|
+
injectMcp(ns, wsPath, token);
|
|
352
|
+
if (!queues.has(ns))
|
|
353
|
+
queues.set(ns, []);
|
|
354
|
+
if (firstMessage !== undefined) {
|
|
355
|
+
queues.get(ns).push(firstMessage);
|
|
433
356
|
}
|
|
357
|
+
processQueue(ns).catch((err) => {
|
|
358
|
+
console.error(`[meta-agent-manager] processQueue error (ns=${ns}):`, err.message);
|
|
359
|
+
});
|
|
434
360
|
}
|
|
435
361
|
return {
|
|
436
362
|
async ensureWorkspace(ns, repoUrl) {
|
|
@@ -442,7 +368,7 @@ export function createMetaAgentManager() {
|
|
|
442
368
|
},
|
|
443
369
|
startPolling(wire, getNamespaces, instanceId) {
|
|
444
370
|
if (pollInterval)
|
|
445
|
-
return;
|
|
371
|
+
return;
|
|
446
372
|
pollInterval = setInterval(() => {
|
|
447
373
|
const namespaces = getNamespaces();
|
|
448
374
|
if (namespaces.length === 0)
|
|
@@ -457,25 +383,21 @@ export function createMetaAgentManager() {
|
|
|
457
383
|
}
|
|
458
384
|
}).catch(() => { });
|
|
459
385
|
}
|
|
460
|
-
//
|
|
461
|
-
if (
|
|
462
|
-
drainQueue(ns, wire).catch((err) => {
|
|
463
|
-
console.warn(`[meta-agent-manager] drainQueue error (ns=${ns}):`, err.message);
|
|
464
|
-
});
|
|
386
|
+
// Skip if already processing
|
|
387
|
+
if (processing.has(ns))
|
|
465
388
|
continue;
|
|
466
|
-
|
|
467
|
-
// Check if anything is queued for this namespace
|
|
389
|
+
// Check Redis queue for pending messages
|
|
468
390
|
const inputKey = discordMetaInputKey(ns);
|
|
469
391
|
wire._redis.llen(inputKey).then(async (queueLen) => {
|
|
470
392
|
if (queueLen === 0)
|
|
471
393
|
return;
|
|
472
|
-
// Resolve token
|
|
473
394
|
let token;
|
|
474
395
|
try {
|
|
475
396
|
token = await wire.token.getMaster();
|
|
476
397
|
}
|
|
477
398
|
catch {
|
|
478
|
-
token =
|
|
399
|
+
token = sessionMeta.get(ns)?.token
|
|
400
|
+
?? process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
479
401
|
?? process.env.CLAUDE_CODE_TOKEN
|
|
480
402
|
?? process.env.ANTHROPIC_API_KEY
|
|
481
403
|
?? "";
|
|
@@ -484,8 +406,31 @@ export function createMetaAgentManager() {
|
|
|
484
406
|
console.warn(`[meta-agent-manager] no token available, skipping session for ns=${ns}`);
|
|
485
407
|
return;
|
|
486
408
|
}
|
|
487
|
-
//
|
|
488
|
-
|
|
409
|
+
// Drain Redis queue into local queue
|
|
410
|
+
if (!queues.has(ns))
|
|
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
|
+
});
|
|
489
434
|
}).catch((err) => {
|
|
490
435
|
console.warn(`[meta-agent-manager] llen error (ns=${ns}):`, err.message);
|
|
491
436
|
});
|
|
@@ -499,35 +444,20 @@ export function createMetaAgentManager() {
|
|
|
499
444
|
pollInterval = null;
|
|
500
445
|
console.log("[meta-agent-manager] polling stopped");
|
|
501
446
|
}
|
|
502
|
-
|
|
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
|
-
}
|
|
447
|
+
queues.clear();
|
|
514
448
|
},
|
|
515
449
|
killSession(ns) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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})`);
|
|
450
|
+
// Clear local queue — any in-flight spawn will complete naturally
|
|
451
|
+
queues.delete(ns);
|
|
452
|
+
console.log(`[meta-agent-manager] killed session queue (ns=${ns})`);
|
|
528
453
|
},
|
|
529
454
|
sendToSession(ns, line) {
|
|
530
|
-
|
|
455
|
+
if (!queues.has(ns))
|
|
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
|
+
});
|
|
531
461
|
},
|
|
532
462
|
};
|
|
533
463
|
}
|