Initial commit: Discord-Claude Gateway with event-driven agent runtime
This commit is contained in:
@@ -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}`));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user