feat(openclaw): improve dispatcher and integration

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim
2026-03-31 17:33:45 -07:00
parent 94a2b8ec2c
commit 366ebb33c4
3 changed files with 66 additions and 3 deletions
+2
View File
@@ -14,6 +14,7 @@ import { createModelCacheState } from "./plugin-state"
import { createFirstMessageVariantGate } from "./shared/first-message-variant"
import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared"
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
import { lspManager } from "./tools/lsp/client"
import { startTmuxCheck } from "./tools"
let activePluginDispose: PluginDispose | null = null
@@ -83,6 +84,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const dispose = createPluginDispose({
backgroundManager: managers.backgroundManager,
skillMcpManager: managers.skillMcpManager,
lspManager,
disposeHooks: hooks.disposeHooks,
})
+41
View File
@@ -3,6 +3,7 @@ import {
interpolateInstruction,
resolveCommandTimeoutMs,
shellEscapeArg,
terminateCommandProcess,
wakeGateway,
wakeCommandGateway,
} from "../dispatcher"
@@ -41,6 +42,10 @@ describe("OpenClaw Dispatcher", () => {
expect(result.success).toBe(true)
expect(fetchSpy).toHaveBeenCalled()
const call = fetchSpy.mock.calls.find(c => c[0] === "https://example.com")
expect(call).toBeDefined()
if (!call) {
throw new Error("Expected fetch call for https://example.com")
}
expect(call[0]).toBe("https://example.com")
expect(call[1]?.method).toBe("POST")
expect(call[1]?.body).toBe('{"foo":"bar"}')
@@ -67,4 +72,40 @@ describe("OpenClaw Dispatcher", () => {
else process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS = original
}
})
test("terminateCommandProcess kills process group on unix when pid exists", () => {
const killSpy = spyOn(process, "kill").mockImplementation(() => true)
const proc = {
pid: 4321,
kill: mock(() => {}),
}
try {
terminateCommandProcess(proc, "SIGKILL")
expect(killSpy).toHaveBeenCalledWith(-4321, "SIGKILL")
expect(proc.kill).not.toHaveBeenCalled()
} finally {
killSpy.mockRestore()
}
})
test("terminateCommandProcess falls back to direct kill when process group kill fails", () => {
const killSpy = spyOn(process, "kill").mockImplementation(() => {
throw new Error("group kill failed")
})
const proc = {
pid: 9876,
kill: mock(() => {}),
}
try {
terminateCommandProcess(proc, "SIGKILL")
expect(killSpy).toHaveBeenCalledWith(-9876, "SIGKILL")
expect(proc.kill).toHaveBeenCalledWith("SIGKILL")
} finally {
killSpy.mockRestore()
}
})
})
+23 -3
View File
@@ -141,18 +141,17 @@ export async function wakeCommandGateway(
return shellEscapeArg(value)
})
// Always use sh -c to handle the shell command string correctly
const proc = spawn(["sh", "-c", interpolated], {
env: { ...process.env },
stdout: "ignore",
stderr: "ignore",
detached: process.platform !== "win32",
})
// Handle timeout manually
let timeoutId: ReturnType<typeof setTimeout> | undefined
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
proc.kill()
terminateCommandProcess(proc, "SIGKILL")
reject(new Error("Command timed out"))
}, timeout)
})
@@ -178,3 +177,24 @@ export async function wakeCommandGateway(
}
}
}
type KillableProcess = {
pid?: number
kill: (signal?: NodeJS.Signals) => void
}
export function terminateCommandProcess(proc: KillableProcess, signal: NodeJS.Signals): void {
try {
if (process.platform !== "win32" && proc.pid) {
try {
process.kill(-proc.pid, signal)
return
} catch {
proc.kill(signal)
return
}
}
proc.kill(signal)
} catch {}
}