mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix: show doctor runtime dependency install progress
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory-core: run one-shot memory CLI commands through transient builtin and QMD managers so `memory index`, `memory status --index`, and `memory search` no longer start long-lived file watchers that can hit macOS `EMFILE` limits. Fixes #59101; carries forward #49851. Thanks @mbear469210-coder and @maoyuanxue.
|
||||
- Agents/ACP: ship the Claude ACP adapter with OpenClaw and require Claude result messages before idle can complete a prompt, preventing parent agents from waking early on long-running `sessions_spawn(runtime: "acp", agentId: "claude")` children. Fixes #72080. Thanks @siavash-saki and @iannwu.
|
||||
- CLI/tasks: route `tasks --json`, `tasks list --json`, and `tasks audit --json` through a lean JSON path so read-only task inspection no longer loads unrelated plugin/runtime command graphs. Fixes #66238. Thanks @ChuckChambers.
|
||||
- CLI/doctor: stream bundled plugin runtime dependency repair progress before, during, and after npm installs, so long `doctor --fix` runs no longer look hung in TTY or captured logs. Fixes #72775. Thanks @dfpalhano.
|
||||
- Memory-core: re-resolve the active runtime config whenever `memory_search` or `memory_get` executes, so provider changes made by `config.patch` stop leaving stale embedding backends behind in existing tool instances. Fixes #61098. Thanks @BradGroux and @Linux2010.
|
||||
- WebChat: keep bare `/new` and `/reset` startup instructions out of visible chat history while preserving `/reset <note>` as user-visible transcript text. Fixes #72369. Thanks @collynes and @haishmg.
|
||||
- CLI/doctor: remove dangling channel config, heartbeat targets, and channel model overrides when stale plugin repair removes a missing channel plugin, preventing Gateway boot loops after failed plugin reinstalls. Fixes #65293. Thanks @yidecode.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
resolveBundledRuntimeDependencyPackageInstallRoot,
|
||||
scanBundledPluginRuntimeDeps,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
} from "../plugins/bundled-runtime-deps.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { maybeRepairBundledPluginRuntimeDeps } from "./doctor-bundled-plugin-runtime-deps.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
@@ -84,7 +85,25 @@ function createNonInteractivePrompter(
|
||||
} as DoctorPrompter;
|
||||
}
|
||||
|
||||
function createRuntime(options: { logs?: string[]; errors?: string[] } = {}): RuntimeEnv {
|
||||
return {
|
||||
log: (message: unknown) => {
|
||||
options.logs?.push(String(message));
|
||||
},
|
||||
error: (message: unknown) => {
|
||||
options.errors?.push(String(message));
|
||||
},
|
||||
exit: (code: number) => {
|
||||
throw new Error(`Unexpected runtime exit ${code}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("doctor bundled plugin runtime deps", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("skips source checkouts", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
fs.mkdirSync(path.join(root, ".git"));
|
||||
@@ -410,7 +429,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
@@ -441,7 +460,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
@@ -472,7 +491,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
@@ -496,6 +515,64 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
expect(readRetainedRuntimeDepsManifest(installRoot)).toEqual(["grammy@1.37.0"]);
|
||||
});
|
||||
|
||||
it("logs runtime dependency repair progress before and after install", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
writeJson(path.join(root, "package.json"), { name: "openclaw" });
|
||||
writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" });
|
||||
const logs: string[] = [];
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: createRuntime({ logs }),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
plugins: { enabled: true },
|
||||
channels: { telegram: { enabled: true } },
|
||||
},
|
||||
installDeps: async () => {},
|
||||
});
|
||||
|
||||
expect(logs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"Installing bundled plugin runtime deps (1 missing, 1 install specs): grammy@1.37.0",
|
||||
),
|
||||
expect.stringContaining("Installed bundled plugin runtime deps in"),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("logs runtime dependency repair heartbeats while install is pending", async () => {
|
||||
vi.useFakeTimers();
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
writeJson(path.join(root, "package.json"), { name: "openclaw" });
|
||||
writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" });
|
||||
const logs: string[] = [];
|
||||
let finishInstall!: () => void;
|
||||
|
||||
const repair = maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: createRuntime({ logs }),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
plugins: { enabled: true },
|
||||
channels: { telegram: { enabled: true } },
|
||||
},
|
||||
installDeps: async () =>
|
||||
await new Promise<void>((resolve) => {
|
||||
finishInstall = resolve;
|
||||
}),
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
expect(logs).toEqual([expect.stringContaining("Installing bundled plugin runtime deps")]);
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
expect(logs).toContain("Still installing bundled plugin runtime deps after 15s...");
|
||||
|
||||
finishInstall();
|
||||
await repair;
|
||||
});
|
||||
|
||||
it("repairs deps for configured channel owner plugins", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
writeJson(path.join(root, "package.json"), { name: "openclaw" });
|
||||
@@ -503,7 +580,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
@@ -533,7 +610,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
|
||||
await expect(
|
||||
maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: (message: string) => errors.push(message) } as never,
|
||||
runtime: createRuntime({ errors }),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
config: {
|
||||
@@ -558,7 +635,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter({ updateInProgress: true }),
|
||||
packageRoot: root,
|
||||
includeConfiguredChannels: true,
|
||||
@@ -591,7 +668,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
env,
|
||||
packageRoot: root,
|
||||
@@ -641,7 +718,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
env,
|
||||
packageRoot: root,
|
||||
@@ -677,7 +754,7 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
const installed = createInstalledRuntimeDeps();
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
runtime: createRuntime(),
|
||||
prompter: createNonInteractivePrompter(),
|
||||
packageRoot: root,
|
||||
includeConfiguredChannels: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import {
|
||||
createBundledRuntimeDepsWritableInstallSpecs,
|
||||
repairBundledRuntimeDepsInstallRoot,
|
||||
repairBundledRuntimeDepsInstallRootAsync,
|
||||
resolveBundledRuntimeDependencyPackageInstallRootPlan,
|
||||
scanBundledPluginRuntimeDeps,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
@@ -14,6 +14,25 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
const RUNTIME_DEPS_INSTALL_HEARTBEAT_MS = 15_000;
|
||||
|
||||
function formatElapsedMs(elapsedMs: number): string {
|
||||
if (elapsedMs < 1000) {
|
||||
return `${elapsedMs}ms`;
|
||||
}
|
||||
const seconds = Math.round(elapsedMs / 1000);
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
function logRuntimeDepsInstallProgress(runtime: RuntimeEnv, message: string): void {
|
||||
runtime.log(message);
|
||||
}
|
||||
|
||||
export async function maybeRepairBundledPluginRuntimeDeps(params: {
|
||||
runtime: RuntimeEnv;
|
||||
prompter: DoctorPrompter;
|
||||
@@ -21,7 +40,7 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
packageRoot?: string | null;
|
||||
includeConfiguredChannels?: boolean;
|
||||
installDeps?: (params: BundledRuntimeDepsInstallParams) => void;
|
||||
installDeps?: (params: BundledRuntimeDepsInstallParams) => void | Promise<void>;
|
||||
}): Promise<void> {
|
||||
const packageRoot =
|
||||
params.packageRoot ??
|
||||
@@ -104,18 +123,43 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
let heartbeat: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
const result = repairBundledRuntimeDepsInstallRoot({
|
||||
const installStartedAt = Date.now();
|
||||
logRuntimeDepsInstallProgress(
|
||||
params.runtime,
|
||||
`Installing bundled plugin runtime deps (${missingSpecs.length} missing, ${installSpecs.length} install specs): ${missingSpecs.join(", ")}`,
|
||||
);
|
||||
heartbeat = setInterval(() => {
|
||||
logRuntimeDepsInstallProgress(
|
||||
params.runtime,
|
||||
`Still installing bundled plugin runtime deps after ${formatElapsedMs(Date.now() - installStartedAt)}...`,
|
||||
);
|
||||
}, RUNTIME_DEPS_INSTALL_HEARTBEAT_MS);
|
||||
heartbeat.unref?.();
|
||||
const result = await repairBundledRuntimeDepsInstallRootAsync({
|
||||
installRoot: installRootPlan.installRoot,
|
||||
missingSpecs,
|
||||
installSpecs,
|
||||
env: params.env ?? process.env,
|
||||
installDeps: params.installDeps,
|
||||
warn: (message) => params.runtime.log(message),
|
||||
installDeps: params.installDeps
|
||||
? async (installParams) => {
|
||||
await params.installDeps?.(installParams);
|
||||
}
|
||||
: undefined,
|
||||
warn: (message) => logRuntimeDepsInstallProgress(params.runtime, message),
|
||||
});
|
||||
logRuntimeDepsInstallProgress(
|
||||
params.runtime,
|
||||
`Installed bundled plugin runtime deps in ${formatElapsedMs(Date.now() - installStartedAt)}: ${result.installSpecs.join(", ")}`,
|
||||
);
|
||||
note(`Installed bundled plugin deps: ${result.installSpecs.join(", ")}`, "Bundled plugins");
|
||||
} catch (error) {
|
||||
params.runtime.error(`Failed to install bundled plugin runtime deps: ${String(error)}`);
|
||||
throw error instanceof Error ? error : new Error(String(error));
|
||||
} finally {
|
||||
if (heartbeat) {
|
||||
clearInterval(heartbeat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user