diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 2b8c8901e..35a108a2b 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -88,18 +88,20 @@ export async function devCommand( spinner.succeed("Environment ready"); - const port = + const portRaw = options.port ?? mergedEnv.GATEWAY_PORT ?? mergedEnv.PORT ?? "8787"; - const portNum = Number(port); + const portNum = Number(portRaw); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { - console.error(chalk.red(`\n Invalid port "${port}" — must be 1-65535.\n`)); + console.error( + chalk.red(`\n Invalid port — must be an integer in 1-65535.\n`) + ); process.exit(1); } - const gatewayUrl = `http://localhost:${port}`; + const gatewayUrl = `http://localhost:${portNum}`; const portFree = await isPortFree(portNum); if (!portFree) { - console.error(chalk.red(`\n Port ${port} is already in use.`)); + console.error(chalk.red(`\n Port ${portNum} is already in use.`)); console.error( chalk.dim( " Stop the other process, or pass `--port ` / set `GATEWAY_PORT` to a free port.\n" @@ -108,8 +110,8 @@ export async function devCommand( console.error( chalk.dim( process.platform === "darwin" || process.platform === "linux" - ? ` Find what's holding it: lsof -iTCP:${port} -sTCP:LISTEN\n` - : ` Find what's holding it: netstat -ano | findstr :${port}\n` + ? ` Find what's holding it: lsof -iTCP:${portNum} -sTCP:LISTEN\n` + : ` Find what's holding it: netstat -ano | findstr :${portNum}\n` ) ); process.exit(1); @@ -136,8 +138,8 @@ export async function devCommand( ...mergedEnv, LOBU_DEV_PROJECT_PATH: process.env.LOBU_DEV_PROJECT_PATH || envVars.LOBU_DEV_PROJECT_PATH || cwd, - PORT: port, - GATEWAY_PORT: port, + PORT: String(portNum), + GATEWAY_PORT: String(portNum), ...(logLevel ? { LOG_LEVEL: logLevel } : {}), }; diff --git a/packages/server/src/gateway/auth/mcp/config-service.ts b/packages/server/src/gateway/auth/mcp/config-service.ts index 1e504edb1..99ccf3d87 100644 --- a/packages/server/src/gateway/auth/mcp/config-service.ts +++ b/packages/server/src/gateway/auth/mcp/config-service.ts @@ -322,9 +322,17 @@ export class McpConfigService { const servers = await this.getAgentMcpServers(agentId); const derived = await this.deriveLobuMemoryServer(agentId); if (!derived) { - const withoutLobuMemory = { ...servers }; - delete withoutLobuMemory["lobu-memory"]; - return withoutLobuMemory; + // Only drop the entry when it was derived (internal). A manually + // configured `lobu-memory` server in agent settings should survive a + // transient derive failure (DB blip, slug not yet resolvable, etc.) — + // otherwise a recoverable error silently disables memory tools. + const existing = servers["lobu-memory"]; + if (existing && typeof existing === "object" && existing.internal === true) { + const withoutLobuMemory = { ...servers }; + delete withoutLobuMemory["lobu-memory"]; + return withoutLobuMemory; + } + return servers; } const existing = servers["lobu-memory"]; diff --git a/packages/server/src/gateway/auth/mcp/proxy.ts b/packages/server/src/gateway/auth/mcp/proxy.ts index 94bd62db3..4b00df4a8 100644 --- a/packages/server/src/gateway/auth/mcp/proxy.ts +++ b/packages/server/src/gateway/auth/mcp/proxy.ts @@ -659,7 +659,8 @@ export class McpProxy { mcpId, toolName, agentId, - auth.tokenData + auth.tokenData, + auth.token ); if (found && requiresToolApproval(annotations)) { const pattern = `/mcp/${mcpId}/tools/${toolName}`; @@ -1074,7 +1075,8 @@ export class McpProxy { mcpId!, toolName, agentId, - tokenData + tokenData, + sessionToken ); if (found && requiresToolApproval(annotations)) { const pattern = `/mcp/${mcpId}/tools/${toolName}`; @@ -1191,7 +1193,8 @@ export class McpProxy { mcpId: string, toolName: string, agentId: string, - tokenData: any + tokenData: any, + workerToken?: string ): Promise<{ found: boolean; annotations?: McpTool["annotations"] }> { let tools: McpTool[] | null = null; if (this.toolCache) { @@ -1199,7 +1202,16 @@ export class McpProxy { } if (!tools) { - const result = await this.fetchToolsForMcp(mcpId, agentId, tokenData); + // Forward the worker JWT so internal MCPs (lobu-memory) can enumerate + // tools — without it the discovery call goes unauthenticated and + // returns an empty list, which would silently bypass the approval gate + // (`found=false` means "no approval needed" at call sites). + const result = await this.fetchToolsForMcp( + mcpId, + agentId, + tokenData, + workerToken + ); tools = result.tools; } diff --git a/packages/server/src/scheduled/task-scheduler.ts b/packages/server/src/scheduled/task-scheduler.ts index 98127de8b..0b7e2a332 100644 --- a/packages/server/src/scheduled/task-scheduler.ts +++ b/packages/server/src/scheduled/task-scheduler.ts @@ -157,8 +157,13 @@ export class TaskScheduler { } catch (err) { logger.error( { err, taskName: reg.name, cron: reg.cron }, - '[task-scheduler] Failed to seed cron row at boot', + '[task-scheduler] Failed to seed cron row at boot; will retry in background', ); + // Periodic tasks self-seed via line ~216 once they dispatch, but a + // task with no prior cron row stays idle until a successful seed. + // Retry in the background so a transient DB hiccup at boot doesn't + // disable the task until the next pod restart. + this.retrySeedInBackground(reg); } } @@ -171,6 +176,31 @@ export class TaskScheduler { ); } + private retrySeedInBackground(reg: TaskRegistration): void { + const delays = [5_000, 15_000, 60_000]; + const attempt = (i: number): void => { + if (!this.started || i >= delays.length) return; + setTimeout(() => { + if (!this.started) return; + this.seedNextCronTick(reg) + .then(() => + logger.info( + { taskName: reg.name, cron: reg.cron, attempt: i + 1 }, + '[task-scheduler] Recovered cron seed in background', + ), + ) + .catch((err) => { + logger.error( + { err, taskName: reg.name, cron: reg.cron, attempt: i + 1 }, + '[task-scheduler] Background cron seed retry failed', + ); + attempt(i + 1); + }); + }, delays[i]).unref(); + }; + attempt(0); + } + /** Stop dispatching. The underlying queue handles in-flight drain on its * own `stop()`; this just flips the local started flag. */ stop(): void {