Merge pull request #1076 from thedotmack/openclaw-installer
Add OpenClaw one-liner installer script with comprehensive test suite
This commit is contained in:
+48
-8
@@ -1,8 +1,46 @@
|
|||||||
# Claude-Mem OpenClaw Plugin — Setup Guide
|
# Claude-Mem OpenClaw Plugin — Setup Guide
|
||||||
|
|
||||||
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway from scratch. Follow every step in order. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
||||||
|
|
||||||
## Step 1: Clone the Claude-Mem Repo
|
## Quick Install (Recommended)
|
||||||
|
|
||||||
|
Run this one-liner to install everything automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer handles dependency checks (Bun, uv), plugin installation, memory slot configuration, AI provider setup, worker startup, and optional observation feed configuration — all interactively.
|
||||||
|
|
||||||
|
### Install with options
|
||||||
|
|
||||||
|
Pre-select your AI provider and API key to skip interactive prompts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
For fully unattended installation (defaults to Claude Max Plan, skips observation feed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
To upgrade an existing installation (preserves settings, updates plugin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, skip to [Step 4: Restart the Gateway and Verify](#step-4-restart-the-gateway-and-verify) to confirm everything is working.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
The steps below are for manual installation if you prefer not to use the automated installer, or need to troubleshoot individual steps.
|
||||||
|
|
||||||
|
### Step 1: Clone the Claude-Mem Repo
|
||||||
|
|
||||||
First, clone the claude-mem repository to a location accessible by your OpenClaw gateway. This gives you the worker service source and the plugin code.
|
First, clone the claude-mem repository to a location accessible by your OpenClaw gateway. This gives you the worker service source and the plugin code.
|
||||||
|
|
||||||
@@ -20,11 +58,11 @@ You'll need **bun** installed for the worker service. If you don't have it:
|
|||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://bun.sh/install | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Get the Worker Running
|
### Step 2: Get the Worker Running
|
||||||
|
|
||||||
The claude-mem worker is an HTTP service on port 37777. It stores observations, generates summaries, and serves the context timeline. The plugin talks to it over HTTP — it doesn't matter where the worker is running, just that it's reachable on localhost:37777.
|
The claude-mem worker is an HTTP service on port 37777. It stores observations, generates summaries, and serves the context timeline. The plugin talks to it over HTTP — it doesn't matter where the worker is running, just that it's reachable on localhost:37777.
|
||||||
|
|
||||||
### Check if it's already running
|
#### Check if it's already running
|
||||||
|
|
||||||
If this machine also runs Claude Code with claude-mem installed, the worker may already be running:
|
If this machine also runs Claude Code with claude-mem installed, the worker may already be running:
|
||||||
|
|
||||||
@@ -36,7 +74,7 @@ curl http://localhost:37777/api/health
|
|||||||
|
|
||||||
**Got connection refused or no response?** The worker isn't running. Continue below.
|
**Got connection refused or no response?** The worker isn't running. Continue below.
|
||||||
|
|
||||||
### If Claude Code has claude-mem installed
|
#### If Claude Code has claude-mem installed
|
||||||
|
|
||||||
If claude-mem is installed as a Claude Code plugin (at `~/.claude/plugins/marketplaces/thedotmack/`), start the worker from that installation:
|
If claude-mem is installed as a Claude Code plugin (at `~/.claude/plugins/marketplaces/thedotmack/`), start the worker from that installation:
|
||||||
|
|
||||||
@@ -54,7 +92,7 @@ curl http://localhost:37777/api/health
|
|||||||
|
|
||||||
**Still not working?** Check `npm run worker:status` for error details, or check that bun is installed and on your PATH.
|
**Still not working?** Check `npm run worker:status` for error details, or check that bun is installed and on your PATH.
|
||||||
|
|
||||||
### If there's no Claude Code installation
|
#### If there's no Claude Code installation
|
||||||
|
|
||||||
Run the worker from the cloned repo:
|
Run the worker from the cloned repo:
|
||||||
|
|
||||||
@@ -77,7 +115,7 @@ curl http://localhost:37777/api/health
|
|||||||
- Check logs: `npm run worker:logs` (if available)
|
- Check logs: `npm run worker:logs` (if available)
|
||||||
- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start`
|
- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start`
|
||||||
|
|
||||||
## Step 3: Add the Plugin to Your Gateway
|
### Step 3: Add the Plugin to Your Gateway
|
||||||
|
|
||||||
Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
||||||
|
|
||||||
@@ -96,7 +134,7 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config fields explained
|
#### Config fields explained
|
||||||
|
|
||||||
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
|
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
|
||||||
|
|
||||||
@@ -104,6 +142,8 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
|||||||
|
|
||||||
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
|
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 4: Restart the Gateway and Verify
|
## Step 4: Restart the Gateway and Verify
|
||||||
|
|
||||||
Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for:
|
Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for:
|
||||||
|
|||||||
Executable
+1732
File diff suppressed because it is too large
Load Diff
+41
-13
@@ -1,5 +1,5 @@
|
|||||||
import { writeFile } from "fs/promises";
|
import { writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { basename, join } from "path";
|
||||||
|
|
||||||
// Minimal type declarations for the OpenClaw Plugin SDK.
|
// Minimal type declarations for the OpenClaw Plugin SDK.
|
||||||
// These match the real OpenClawPluginApi provided by the gateway at runtime.
|
// These match the real OpenClawPluginApi provided by the gateway at runtime.
|
||||||
@@ -67,13 +67,27 @@ interface SessionEndEvent {
|
|||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MessageReceivedEvent {
|
||||||
|
from: string;
|
||||||
|
content: string;
|
||||||
|
timestamp?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
interface EventContext {
|
interface EventContext {
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MessageContext {
|
||||||
|
channelId: string;
|
||||||
|
accountId?: string;
|
||||||
|
conversationId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
|
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
|
||||||
|
type MessageEventCallback<T> = (event: T, ctx: MessageContext) => void | Promise<void>;
|
||||||
|
|
||||||
interface OpenClawPluginApi {
|
interface OpenClawPluginApi {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -100,6 +114,7 @@ interface OpenClawPluginApi {
|
|||||||
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
||||||
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
||||||
((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
|
((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
|
||||||
|
((event: "message_received", callback: MessageEventCallback<MessageReceivedEvent>) => void) &
|
||||||
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
||||||
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
||||||
runtime: {
|
runtime: {
|
||||||
@@ -158,7 +173,6 @@ interface ClaudeMemPluginConfig {
|
|||||||
|
|
||||||
const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
||||||
const DEFAULT_WORKER_PORT = 37777;
|
const DEFAULT_WORKER_PORT = 37777;
|
||||||
const TOOL_RESULT_MAX_LENGTH = 1000;
|
|
||||||
|
|
||||||
// Agent emoji map for observation feed messages.
|
// Agent emoji map for observation feed messages.
|
||||||
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
||||||
@@ -451,9 +465,11 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
||||||
|
// Derive project name from workspace directory (matches Claude Code's getProjectName logic)
|
||||||
|
const workspaceProject = basename(workspaceDir) || baseProjectName;
|
||||||
const contextText = await workerGetText(
|
const contextText = await workerGetText(
|
||||||
workerPort,
|
workerPort,
|
||||||
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
`/api/context/inject?projects=${encodeURIComponent(workspaceProject)}`,
|
||||||
api.logger
|
api.logger
|
||||||
);
|
);
|
||||||
if (contextText && contextText.trim().length > 0) {
|
if (contextText && contextText.trim().length > 0) {
|
||||||
@@ -482,6 +498,20 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
|
api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Event: message_received — capture inbound user prompts from channels
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
api.on("message_received", async (event, ctx) => {
|
||||||
|
const sessionKey = ctx.conversationId || ctx.channelId || "default";
|
||||||
|
const contentSessionId = getContentSessionId(sessionKey);
|
||||||
|
|
||||||
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
|
contentSessionId,
|
||||||
|
project: baseProjectName,
|
||||||
|
prompt: event.content || "[media prompt]",
|
||||||
|
}, api.logger);
|
||||||
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: after_compaction — re-init session after context compaction
|
// Event: after_compaction — re-init session after context compaction
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -500,7 +530,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("before_agent_start", async (_event, ctx) => {
|
api.on("before_agent_start", async (event, ctx) => {
|
||||||
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||||
if (ctx.workspaceDir) {
|
if (ctx.workspaceDir) {
|
||||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||||
@@ -512,7 +542,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: getProjectName(ctx),
|
project: getProjectName(ctx),
|
||||||
prompt: ctx.sessionKey || "agent run",
|
prompt: event.prompt || "agent run",
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|
||||||
// Sync MEMORY.md before agent runs (provides context to agent)
|
// Sync MEMORY.md before agent runs (provides context to agent)
|
||||||
@@ -527,20 +557,18 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
api.on("tool_result_persist", (event, ctx) => {
|
api.on("tool_result_persist", (event, ctx) => {
|
||||||
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
||||||
const toolName = event.toolName;
|
const toolName = event.toolName;
|
||||||
if (!toolName || toolName.startsWith("memory_")) return;
|
if (!toolName) return;
|
||||||
|
|
||||||
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||||
|
|
||||||
// Extract result text from message content
|
// Extract result text from all content blocks
|
||||||
let toolResponseText = "";
|
let toolResponseText = "";
|
||||||
const content = event.message?.content;
|
const content = event.message?.content;
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
const textBlock = content.find(
|
toolResponseText = content
|
||||||
(block) => block.type === "tool_result" || block.type === "text"
|
.filter((block) => (block.type === "tool_result" || block.type === "text") && "text" in block)
|
||||||
);
|
.map((block) => String(block.text))
|
||||||
if (textBlock && "text" in textBlock) {
|
.join("\n");
|
||||||
toolResponseText = String(textBlock.text).slice(0, TOOL_RESULT_MAX_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire-and-forget: send observation + sync MEMORY.md in parallel
|
// Fire-and-forget: send observation + sync MEMORY.md in parallel
|
||||||
|
|||||||
Executable
+2339
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+257
-262
File diff suppressed because one or more lines are too long
@@ -30,6 +30,19 @@ export interface RouteHandler {
|
|||||||
setupRoutes(app: Application): void;
|
setupRoutes(app: Application): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI provider status for health endpoint
|
||||||
|
*/
|
||||||
|
export interface AiStatus {
|
||||||
|
provider: string;
|
||||||
|
authMethod: string;
|
||||||
|
lastInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for initializing the server
|
* Options for initializing the server
|
||||||
*/
|
*/
|
||||||
@@ -42,6 +55,10 @@ export interface ServerOptions {
|
|||||||
onShutdown: () => Promise<void>;
|
onShutdown: () => Promise<void>;
|
||||||
/** Restart function for admin endpoints */
|
/** Restart function for admin endpoints */
|
||||||
onRestart: () => Promise<void>;
|
onRestart: () => Promise<void>;
|
||||||
|
/** Filesystem path to the worker entry point */
|
||||||
|
workerPath: string;
|
||||||
|
/** Callback to get current AI provider status */
|
||||||
|
getAiStatus: () => AiStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,20 +157,20 @@ export class Server {
|
|||||||
* Setup core system routes (health, readiness, version, admin)
|
* Setup core system routes (health, readiness, version, admin)
|
||||||
*/
|
*/
|
||||||
private setupCoreRoutes(): void {
|
private setupCoreRoutes(): void {
|
||||||
// Test build ID for debugging which build is running
|
|
||||||
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
|
|
||||||
|
|
||||||
// Health check endpoint - always responds, even during initialization
|
// Health check endpoint - always responds, even during initialization
|
||||||
this.app.get('/api/health', (_req: Request, res: Response) => {
|
this.app.get('/api/health', (_req: Request, res: Response) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
build: TEST_BUILD_ID,
|
version: BUILT_IN_VERSION,
|
||||||
|
workerPath: this.options.workerPath,
|
||||||
|
uptime: Date.now() - this.startTime,
|
||||||
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
initialized: this.options.getInitializationComplete(),
|
initialized: this.options.getInitializationComplete(),
|
||||||
mcpReady: this.options.getMcpReady(),
|
mcpReady: this.options.getMcpReady(),
|
||||||
|
ai: this.options.getAiStatus(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||||
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
||||||
|
import { getAuthMethodDescription } from '../shared/EnvManager.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
||||||
@@ -170,6 +171,14 @@ export class WorkerService {
|
|||||||
// Orphan reaper cleanup function (Issue #737)
|
// Orphan reaper cleanup function (Issue #737)
|
||||||
private stopOrphanReaper: (() => void) | null = null;
|
private stopOrphanReaper: (() => void) | null = null;
|
||||||
|
|
||||||
|
// AI interaction tracking for health endpoint
|
||||||
|
private lastAiInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
provider: string;
|
||||||
|
error?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize the promise that will resolve when background initialization completes
|
// Initialize the promise that will resolve when background initialization completes
|
||||||
this.initializationComplete = new Promise((resolve) => {
|
this.initializationComplete = new Promise((resolve) => {
|
||||||
@@ -206,7 +215,24 @@ export class WorkerService {
|
|||||||
getInitializationComplete: () => this.initializationCompleteFlag,
|
getInitializationComplete: () => this.initializationCompleteFlag,
|
||||||
getMcpReady: () => this.mcpReady,
|
getMcpReady: () => this.mcpReady,
|
||||||
onShutdown: () => this.shutdown(),
|
onShutdown: () => this.shutdown(),
|
||||||
onRestart: () => this.shutdown()
|
onRestart: () => this.shutdown(),
|
||||||
|
workerPath: __filename,
|
||||||
|
getAiStatus: () => {
|
||||||
|
let provider = 'claude';
|
||||||
|
if (isOpenRouterSelected() && isOpenRouterAvailable()) provider = 'openrouter';
|
||||||
|
else if (isGeminiSelected() && isGeminiAvailable()) provider = 'gemini';
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
authMethod: getAuthMethodDescription(),
|
||||||
|
lastInteraction: this.lastAiInteraction
|
||||||
|
? {
|
||||||
|
timestamp: this.lastAiInteraction.timestamp,
|
||||||
|
success: this.lastAiInteraction.success,
|
||||||
|
...(this.lastAiInteraction.error && { error: this.lastAiInteraction.error }),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register route handlers
|
// Register route handlers
|
||||||
@@ -459,6 +485,7 @@ export class WorkerService {
|
|||||||
|
|
||||||
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
||||||
let hadUnrecoverableError = false;
|
let hadUnrecoverableError = false;
|
||||||
|
let sessionFailed = false;
|
||||||
|
|
||||||
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
||||||
|
|
||||||
@@ -476,6 +503,12 @@ export class WorkerService {
|
|||||||
];
|
];
|
||||||
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
||||||
hadUnrecoverableError = true;
|
hadUnrecoverableError = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
||||||
sessionId: session.sessionDbId,
|
sessionId: session.sessionDbId,
|
||||||
project: session.project,
|
project: session.project,
|
||||||
@@ -512,11 +545,27 @@ export class WorkerService {
|
|||||||
project: session.project,
|
project: session.project,
|
||||||
provider: providerName
|
provider: providerName
|
||||||
}, error as Error);
|
}, error as Error);
|
||||||
|
sessionFailed = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
session.generatorPromise = null;
|
session.generatorPromise = null;
|
||||||
|
|
||||||
|
// Record successful AI interaction if no error occurred
|
||||||
|
if (!sessionFailed && !hadUnrecoverableError) {
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: true,
|
||||||
|
provider: providerName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
||||||
if (hadUnrecoverableError) {
|
if (hadUnrecoverableError) {
|
||||||
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
||||||
|
|||||||
Reference in New Issue
Block a user