Initial commit: Discord-Claude Gateway with event-driven agent runtime

This commit is contained in:
2026-02-22 13:59:57 -05:00
parent b4f340b610
commit f2247ea3ac
28 changed files with 2056 additions and 205 deletions

View File

@@ -11,6 +11,8 @@ import type { SessionManager } from "./session-manager.js";
import { formatErrorForUser } from "./error-formatter.js";
import type { HookManager } from "./hook-manager.js";
import type { GatewayConfig } from "./config.js";
import { loadSkills } from "./skills-loader.js";
import { logger } from "./logger.js";
export interface EventResult {
responseText?: string;
@@ -21,6 +23,40 @@ export interface EventResult {
export type OnStreamResult = (text: string, channelId: string) => Promise<void>;
function isTransientError(error: unknown): boolean {
if (error instanceof Error) {
const msg = error.message.toLowerCase();
if (msg.includes("session") && (msg.includes("invalid") || msg.includes("corrupt") || msg.includes("not found") || msg.includes("expired"))) {
return false;
}
return msg.includes("timed out") || msg.includes("timeout") || msg.includes("exit") || msg.includes("spawn") || msg.includes("crash");
}
return true;
}
export async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number,
baseDelayMs: number,
shouldRetry: (error: unknown) => boolean,
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt >= maxRetries || !shouldRetry(error)) {
throw error;
}
const delay = baseDelayMs * Math.pow(2, attempt);
logger.info({ attempt: attempt + 1, maxRetries, delayMs: delay }, "Retrying after transient error");
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
interface ClaudeJsonResponse {
type: string;
subtype?: string;
@@ -77,7 +113,8 @@ export class AgentRuntime {
private async processEventCore(event: Event, onStreamResult?: OnStreamResult): Promise<EventResult> {
const configs = await this.markdownConfigLoader.loadAll(this.config.configDir);
const systemPrompt = this.systemPromptAssembler.assemble(configs);
const skills = await loadSkills(this.config.configDir);
const systemPrompt = this.systemPromptAssembler.assemble(configs, skills);
switch (event.type) {
case "message":
@@ -104,7 +141,12 @@ export class AgentRuntime {
: undefined;
try {
const response = await this.executeClaude(promptText, systemPrompt, existingSessionId, streamCallback);
const response = await withRetry(
() => this.executeClaude(promptText, systemPrompt, existingSessionId, streamCallback),
3,
5000,
isTransientError,
);
if (response.session_id && channelId) {
this.sessionManager.setSessionId(channelId, response.session_id);
@@ -203,7 +245,7 @@ export class AgentRuntime {
args.push("--max-turns", "25");
const configDir = path.resolve(this.config.configDir);
console.log(`[DEBUG] Spawning: ${this.config.claudeCliPath} cwd=${configDir} args=${JSON.stringify(args.slice(0, 8))}... (${args.length} total)`);
logger.debug({ cliPath: this.config.claudeCliPath, cwd: configDir, argCount: args.length }, "Spawning Claude CLI");
const child = spawn(this.config.claudeCliPath, args, {
stdio: ["ignore", "pipe", "pipe"],
@@ -242,7 +284,7 @@ export class AgentRuntime {
if (onResult) {
streamedResults = true;
onResult(obj.result).catch((err) =>
console.error("[DEBUG] Stream callback error:", err)
logger.error({ err }, "Stream callback error")
);
}
}
@@ -257,14 +299,14 @@ export class AgentRuntime {
});
const timer = setTimeout(() => {
console.log(`[DEBUG] Timeout reached, killing Claude CLI process`);
logger.debug("Timeout reached, killing Claude CLI process");
child.kill("SIGTERM");
reject(new Error("Query timed out"));
}, this.config.queryTimeoutMs);
child.on("close", (code) => {
clearTimeout(timer);
console.log(`[DEBUG] Claude CLI exited: code=${code}, stdout=${stdout.length} chars, streamed=${streamedResults}`);
logger.debug({ code, stdoutLength: stdout.length, streamed: streamedResults }, "Claude CLI exited");
if (code !== 0 && code !== null) {
reject(new Error(`Claude CLI error (exit ${code}): ${stderr.slice(0, 500) || "unknown error"}`));
@@ -302,7 +344,7 @@ export class AgentRuntime {
} catch { /* ignore */ }
}
console.log(`[DEBUG] Parsed: result=${lastResultText.length} chars, session=${parsedSessionId ?? "none"}`);
logger.debug({ resultLength: lastResultText.length, session: parsedSessionId ?? "none" }, "Parsed Claude response");
resolve({
type: "result",
@@ -314,7 +356,7 @@ export class AgentRuntime {
child.on("error", (err) => {
clearTimeout(timer);
console.error(`[DEBUG] Failed to spawn Claude CLI: ${err.message}`);
logger.error({ err }, "Failed to spawn Claude CLI");
reject(new Error(`Failed to spawn Claude CLI: ${err.message}`));
});
});