@gonzih/cc-discord 0.2.34 → 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 -174
- 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,146 +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
|
-
const proc = spawn(claudeBin, [
|
|
302
|
-
"--continue",
|
|
303
|
-
"-p",
|
|
304
|
-
"--output-format", "stream-json",
|
|
305
|
-
"--input-format", "stream-json",
|
|
306
|
-
"--verbose",
|
|
307
|
-
"--dangerously-skip-permissions",
|
|
308
|
-
], { cwd: wsPath, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
309
|
-
proc.stdin.setDefaultEncoding("utf8");
|
|
310
|
-
wireStdoutToRedis(proc, ns, wire);
|
|
311
|
-
proc.on("exit", (code) => {
|
|
312
|
-
console.log(`[meta-agent-manager] persistent session exited (ns=${ns}, code=${code})`);
|
|
313
|
-
onExit();
|
|
314
|
-
});
|
|
315
|
-
proc.on("error", (err) => {
|
|
316
|
-
console.error(`[meta-agent-manager] persistent session spawn error (ns=${ns}):`, err.message);
|
|
317
|
-
onExit();
|
|
318
|
-
});
|
|
319
|
-
return proc;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Create a MetaAgentManager that maintains one persistent Claude process per namespace.
|
|
291
|
+
* Create a MetaAgentManager that processes Discord messages via spawn-per-message.
|
|
323
292
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* 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.
|
|
328
296
|
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* On process exit: remove from sessions map. Next message triggers a respawn.
|
|
332
|
-
*
|
|
333
|
-
* The 3-second poll loop drains any messages that arrived while a session was
|
|
334
|
-
* starting up or temporarily unavailable.
|
|
297
|
+
* Messages queue per namespace and process sequentially — no concurrent spawns
|
|
298
|
+
* for the same namespace.
|
|
335
299
|
*/
|
|
336
300
|
export function createMetaAgentManager() {
|
|
337
301
|
let pollInterval = null;
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
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();
|
|
342
308
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
309
|
+
* Drain the local queue for a namespace, spawning one session per message.
|
|
310
|
+
* No-ops if already processing or queue is empty.
|
|
345
311
|
*/
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
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)
|
|
349
320
|
return;
|
|
321
|
+
processing.add(ns);
|
|
322
|
+
console.log(`[meta-agent-manager] processQueue started (ns=${ns})`);
|
|
350
323
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for (;;) {
|
|
365
|
-
let raw;
|
|
366
|
-
try {
|
|
367
|
-
raw = await wire._redis.lpop(inputKey);
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
if (!raw)
|
|
373
|
-
break;
|
|
374
|
-
let content;
|
|
375
|
-
try {
|
|
376
|
-
content = JSON.parse(raw).content ?? raw;
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
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
|
+
}
|
|
380
337
|
}
|
|
381
|
-
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
processing.delete(ns);
|
|
341
|
+
console.log(`[meta-agent-manager] processQueue done (ns=${ns})`);
|
|
382
342
|
}
|
|
383
343
|
}
|
|
384
344
|
/**
|
|
385
|
-
* Ensure
|
|
386
|
-
* If one already exists, returns immediately.
|
|
387
|
-
* 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.
|
|
388
346
|
*/
|
|
389
347
|
async function ensureSession(ns, repoUrl, token, wire, firstMessage) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
startingUp.add(ns);
|
|
400
|
-
try {
|
|
401
|
-
// Ensure workspace exists
|
|
402
|
-
const wsPath = workspacePath(ns);
|
|
403
|
-
await ensureWorkspace(ns, repoUrl);
|
|
404
|
-
injectMcp(ns, wsPath, token);
|
|
405
|
-
const proc = spawnPersistentSession(ns, token, wire, () => {
|
|
406
|
-
// On exit: remove from map so next message triggers a respawn
|
|
407
|
-
sessions.delete(ns);
|
|
408
|
-
console.log(`[meta-agent-manager] session removed from map (ns=${ns})`);
|
|
409
|
-
});
|
|
410
|
-
sessions.set(ns, { proc, ns });
|
|
411
|
-
// Drain any messages that arrived before this session started
|
|
412
|
-
await drainQueue(ns, wire);
|
|
413
|
-
// Write the triggering message last (after queue drain, in order)
|
|
414
|
-
if (firstMessage !== undefined)
|
|
415
|
-
writeToStdin(ns, firstMessage);
|
|
416
|
-
await wire.discord.setStatus(ns, {
|
|
417
|
-
namespace: ns,
|
|
418
|
-
status: "running",
|
|
419
|
-
isTyping: true,
|
|
420
|
-
turnCount: 0,
|
|
421
|
-
updatedAt: new Date().toISOString(),
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
console.error(`[meta-agent-manager] ensureSession failed (ns=${ns}):`, err.message);
|
|
426
|
-
sessions.delete(ns);
|
|
427
|
-
}
|
|
428
|
-
finally {
|
|
429
|
-
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);
|
|
430
356
|
}
|
|
357
|
+
processQueue(ns).catch((err) => {
|
|
358
|
+
console.error(`[meta-agent-manager] processQueue error (ns=${ns}):`, err.message);
|
|
359
|
+
});
|
|
431
360
|
}
|
|
432
361
|
return {
|
|
433
362
|
async ensureWorkspace(ns, repoUrl) {
|
|
@@ -439,7 +368,7 @@ export function createMetaAgentManager() {
|
|
|
439
368
|
},
|
|
440
369
|
startPolling(wire, getNamespaces, instanceId) {
|
|
441
370
|
if (pollInterval)
|
|
442
|
-
return;
|
|
371
|
+
return;
|
|
443
372
|
pollInterval = setInterval(() => {
|
|
444
373
|
const namespaces = getNamespaces();
|
|
445
374
|
if (namespaces.length === 0)
|
|
@@ -454,25 +383,21 @@ export function createMetaAgentManager() {
|
|
|
454
383
|
}
|
|
455
384
|
}).catch(() => { });
|
|
456
385
|
}
|
|
457
|
-
//
|
|
458
|
-
if (
|
|
459
|
-
drainQueue(ns, wire).catch((err) => {
|
|
460
|
-
console.warn(`[meta-agent-manager] drainQueue error (ns=${ns}):`, err.message);
|
|
461
|
-
});
|
|
386
|
+
// Skip if already processing
|
|
387
|
+
if (processing.has(ns))
|
|
462
388
|
continue;
|
|
463
|
-
|
|
464
|
-
// Check if anything is queued for this namespace
|
|
389
|
+
// Check Redis queue for pending messages
|
|
465
390
|
const inputKey = discordMetaInputKey(ns);
|
|
466
391
|
wire._redis.llen(inputKey).then(async (queueLen) => {
|
|
467
392
|
if (queueLen === 0)
|
|
468
393
|
return;
|
|
469
|
-
// Resolve token
|
|
470
394
|
let token;
|
|
471
395
|
try {
|
|
472
396
|
token = await wire.token.getMaster();
|
|
473
397
|
}
|
|
474
398
|
catch {
|
|
475
|
-
token =
|
|
399
|
+
token = sessionMeta.get(ns)?.token
|
|
400
|
+
?? process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
476
401
|
?? process.env.CLAUDE_CODE_TOKEN
|
|
477
402
|
?? process.env.ANTHROPIC_API_KEY
|
|
478
403
|
?? "";
|
|
@@ -481,8 +406,31 @@ export function createMetaAgentManager() {
|
|
|
481
406
|
console.warn(`[meta-agent-manager] no token available, skipping session for ns=${ns}`);
|
|
482
407
|
return;
|
|
483
408
|
}
|
|
484
|
-
//
|
|
485
|
-
|
|
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
|
+
});
|
|
486
434
|
}).catch((err) => {
|
|
487
435
|
console.warn(`[meta-agent-manager] llen error (ns=${ns}):`, err.message);
|
|
488
436
|
});
|
|
@@ -496,35 +444,20 @@ export function createMetaAgentManager() {
|
|
|
496
444
|
pollInterval = null;
|
|
497
445
|
console.log("[meta-agent-manager] polling stopped");
|
|
498
446
|
}
|
|
499
|
-
|
|
500
|
-
for (const [ns, session] of sessions) {
|
|
501
|
-
try {
|
|
502
|
-
session.proc.stdin.end();
|
|
503
|
-
session.proc.kill();
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
// ignore errors during shutdown
|
|
507
|
-
}
|
|
508
|
-
sessions.delete(ns);
|
|
509
|
-
console.log(`[meta-agent-manager] killed session on stop (ns=${ns})`);
|
|
510
|
-
}
|
|
447
|
+
queues.clear();
|
|
511
448
|
},
|
|
512
449
|
killSession(ns) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
session.proc.stdin.end();
|
|
518
|
-
session.proc.kill();
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
// ignore
|
|
522
|
-
}
|
|
523
|
-
sessions.delete(ns);
|
|
524
|
-
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})`);
|
|
525
453
|
},
|
|
526
454
|
sendToSession(ns, line) {
|
|
527
|
-
|
|
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
|
+
});
|
|
528
461
|
},
|
|
529
462
|
};
|
|
530
463
|
}
|