From 2ce420091e136da4c83e65071c6caea68f3b1ac1 Mon Sep 17 00:00:00 2001 From: Sedrak Hovhannisyan <264150421+Sedrak-Hovhannisyan@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:19:17 +0400 Subject: [PATCH] fix: prevent agent hatch failure after gateway startup (#122210) * fix(system-agent): attach hatch TUI to running gateway * test(system-agent): isolate TUI gateway state --------- Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> --- src/system-agent/operations-execute.ts | 10 ++-- .../operations-execution-helpers.ts | 8 +-- src/system-agent/operations.tui.test.ts | 53 +++++++++++++++++-- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/system-agent/operations-execute.ts b/src/system-agent/operations-execute.ts index 0bd3fc15752b..c82ed6d192d8 100644 --- a/src/system-agent/operations-execute.ts +++ b/src/system-agent/operations-execute.ts @@ -502,15 +502,19 @@ export async function executeSystemAgentOperation( }, }); case "open-tui": { - const agentId = await resolveTuiAgentId({ + const overview = await loadOverviewForOperation(opts.deps); + const agentId = resolveTuiAgentId({ requestedAgentId: operation.agentId, requestedWorkspace: operation.workspace, - deps: opts.deps, + overview, }); const session = agentId ? buildAgentMainSessionKey({ agentId }) : undefined; const runTui = opts.deps?.runTui ?? (await import("../tui/tui.js")).runTui; + // A reachable Gateway owns the state lock, so embedded mode would fail during hatch. + // Keep embedded mode only as the no-Gateway fallback for standalone sessions. + const useEmbeddedTui = !overview.gateway.reachable; const result = await runTui({ - local: true, + local: useEmbeddedTui, session, deliver: false, historyLimit: 200, diff --git a/src/system-agent/operations-execution-helpers.ts b/src/system-agent/operations-execution-helpers.ts index dea1796f8101..49f23a871570 100644 --- a/src/system-agent/operations-execution-helpers.ts +++ b/src/system-agent/operations-execution-helpers.ts @@ -187,12 +187,12 @@ export function createNoExitRuntime(runtime: RuntimeEnv): RuntimeEnv { }; } -export async function resolveTuiAgentId(params: { +export function resolveTuiAgentId(params: { requestedAgentId: string | undefined; requestedWorkspace?: string; - deps?: SystemAgentCommandDeps; -}): Promise { - const overview = await loadOverviewForOperation(params.deps); + overview: SystemAgentOverview; +}): string | undefined { + const { overview } = params; const workspace = params.requestedWorkspace ? resolveUserPath(params.requestedWorkspace) : undefined; diff --git a/src/system-agent/operations.tui.test.ts b/src/system-agent/operations.tui.test.ts index 22d50b30ed32..3977024b2bfd 100644 --- a/src/system-agent/operations.tui.test.ts +++ b/src/system-agent/operations.tui.test.ts @@ -4,8 +4,35 @@ import path from "node:path"; import { withTempHome } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it, vi } from "vitest"; import { executeSystemAgentOperation, isPersistentSystemAgentOperation } from "./operations.js"; +import type { SystemAgentOverview } from "./overview.js"; import { createSystemAgentTestRuntime } from "./system-agent.runtime.test-support.js"; +function createOverview(gatewayReachable: boolean): SystemAgentOverview { + return { + config: { path: "/tmp/openclaw.json", exists: true, valid: true, issues: [], hash: null }, + agents: [ + { id: "main", isDefault: true }, + { id: "work", isDefault: false }, + ], + defaultAgentId: "main", + tools: { + codex: { command: "codex", found: false }, + claude: { command: "claude", found: false }, + gemini: { command: "gemini", found: false }, + apiKeys: { openai: false, anthropic: false }, + }, + gateway: { + url: "ws://127.0.0.1:18789", + source: "test", + reachable: gatewayReachable, + }, + references: { + docsUrl: "https://docs.openclaw.ai", + sourceUrl: "https://github.com/openclaw/openclaw", + }, + }; +} + describe("system-agent TUI operations", () => { it("refuses doctor repairs before any write or audit", async () => { await withTempHome(async (home) => { @@ -39,7 +66,7 @@ describe("system-agent TUI operations", () => { const result = await executeSystemAgentOperation( { kind: "open-tui", agentId: "work" }, runtime, - { deps: { runTui } }, + { deps: { runTui, loadOverview: async () => createOverview(false) } }, ); expect(runTui).toHaveBeenCalledWith({ @@ -58,22 +85,38 @@ describe("system-agent TUI operations", () => { ); }); - it("seeds a fresh hatch into the agent TUI", async () => { + it("connects a fresh hatch to the reachable Gateway", async () => { const { runtime } = createSystemAgentTestRuntime(); const runTui = vi.fn(async () => ({ exitReason: "exit" as const })); await executeSystemAgentOperation( { kind: "open-tui", agentId: "work", agentDraft: "hatch" }, runtime, - { deps: { runTui } }, + { deps: { runTui, loadOverview: async () => createOverview(true) } }, ); + expect(runTui).toHaveBeenCalledWith({ + local: false, + session: "agent:work:main", + deliver: false, + historyLimit: 200, + message: "Wake up, my friend!", + }); + }); + + it("keeps the embedded TUI fallback when the Gateway is unreachable", async () => { + const { runtime } = createSystemAgentTestRuntime(); + const runTui = vi.fn(async () => ({ exitReason: "exit" as const })); + + await executeSystemAgentOperation({ kind: "open-tui", agentId: "work" }, runtime, { + deps: { runTui, loadOverview: async () => createOverview(false) }, + }); + expect(runTui).toHaveBeenCalledWith({ local: true, session: "agent:work:main", deliver: false, historyLimit: 200, - message: "Wake up, my friend!", }); }); @@ -84,7 +127,7 @@ describe("system-agent TUI operations", () => { })); const result = await executeSystemAgentOperation({ kind: "open-tui" }, runtime, { - deps: { runTui }, + deps: { runTui, loadOverview: async () => createOverview(false) }, }); expect(result).toMatchObject({