Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5b342f8
feat(api): align OpenMem v1 SDK with cloud API
Zyntrael Jul 8, 2026
57f0919
Merge branch 'MemTensor:main' into dev-v2.0.23-sdk
Zyntrael Jul 8, 2026
36f6849
Merge branch 'MemTensor:main' into dev-v2.0.23-sdk
Zyntrael Jul 14, 2026
f46c02e
feat(api): align OpenMem v1 SDK with cloud API
Zyntrael Jul 14, 2026
39643a2
Merge remote-tracking branch '我的/dev-v2.0.23-sdk' into dev-v2.0.23-sdk
Zyntrael Jul 14, 2026
c9745f6
align OpenMem v1 SDK with cloud API
bittergreen Jul 14, 2026
9d2c9b7
fix(memos-local): include json hint in user messages (#1756)
de1tydev Jul 14, 2026
a000a1c
docs: fix scheduler API examples to match server endpoints (#2113)
shinetata Jul 16, 2026
2c60267
fix(api): show structured add example in /docs for /product/add (#2112)
shinetata Jul 16, 2026
c0f6c2c
fix(memos-local-plugin): revert half-merged MemosHttpClient usages (#…
Memtensor-AI Jul 16, 2026
698c30f
fix: configure logging once per process
bittergreen Jul 16, 2026
2017cd5
fix: close scheduler resources on API shutdown
bittergreen Jul 16, 2026
8aefda7
fix: configure logging once per process
bittergreen Jul 16, 2026
9c7dd2f
fix: close scheduler resources via API lifespan
bittergreen Jul 16, 2026
61c1322
fix: stabilize logging and RabbitMQ shutdown lifecycle (#2117)
bittergreen Jul 16, 2026
e072676
feat(api):update SDK version number
Zyntrael Jul 17, 2026
e8b6690
feat(api):update SDK version number (#2118)
bittergreen Jul 17, 2026
d0802b6
chore: update version to v2.0.24
Jul 19, 2026
d9130a7
feat: chunk batch reflection scoring (#1957)
chiefmojo Jul 23, 2026
df41fe7
Revert "feat: chunk batch reflection scoring (#1957)" (#2145)
syzsunshine219 Jul 23, 2026
90c53de
fix: ensure gateway restarts on install failure via ERR trap
Zhe-SH-CN Jul 23, 2026
2e07638
Merge main and harden installer gateway recovery
Aug 6, 2026
8543757
fix: report gateway recovery failures
Aug 6, 2026
2d302f6
fix: clarify final gateway start state
Aug 6, 2026
2b37f10
test: cover gateway foreground fallback recovery
Aug 6, 2026
b3fc216
fix: make gateway recovery state explicit
Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions apps/memos-local-openclaw/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ success() {
}

warn() {
echo -e "${YELLOW}$1${NC}"
echo -e "${YELLOW}$1${NC}" >&2
}

error() {
echo -e "${RED}$1${NC}"
echo -e "${RED}$1${NC}" >&2
}

node_major_version() {
Expand Down Expand Up @@ -225,6 +225,25 @@ success "Using global OpenClaw CLI, 使用全局 OpenClaw CLI: ${OPENCLAW_BIN}"
PACKAGE_SPEC="${PLUGIN_PACKAGE}@${PLUGIN_VERSION}"
EXTENSION_DIR="${OPENCLAW_HOME}/extensions/${PLUGIN_ID}"
OPENCLAW_CONFIG_PATH="${OPENCLAW_HOME}/openclaw.json"
TMP_PACK_DIR=""
GATEWAY_RECOVERY_STATE="inactive"

cleanup_on_exit() {
if [[ "${GATEWAY_RECOVERY_STATE:-inactive}" == "needs_recovery" ]]; then
local recovery_out=""
if ! recovery_out="$("${OPENCLAW_BIN}" gateway start 2>&1)"; then
warn "Gateway recovery start failed; OpenClaw Gateway may still be stopped. Gateway 恢复启动失败,服务可能仍处于停止状态。"
if [[ -n "${recovery_out}" ]]; then
printf '%s\n' "${recovery_out}" | sed 's/^/ /' >&2
fi
fi
fi
if [[ -n "${TMP_PACK_DIR:-}" ]]; then
rm -rf "${TMP_PACK_DIR}"
fi
}

trap cleanup_on_exit EXIT

update_openclaw_config() {
info "Update OpenClaw config, 更新 OpenClaw 配置..."
Expand Down Expand Up @@ -317,6 +336,7 @@ NODE

info "Stop OpenClaw Gateway, 停止 OpenClaw Gateway..."
"${OPENCLAW_BIN}" gateway stop >/dev/null 2>&1 || true
GATEWAY_RECOVERY_STATE="needs_recovery"

if command -v lsof >/dev/null 2>&1; then
PIDS="$(lsof -i :"${PORT}" -t 2>/dev/null || true)"
Expand All @@ -334,7 +354,6 @@ fi

info "Install plugin ${PACKAGE_SPEC}, 安装插件 ${PACKAGE_SPEC}..."
TMP_PACK_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_PACK_DIR}"' EXIT

if [[ -f "${PLUGIN_VERSION}" ]]; then
info "Using local tarball, 使用本地包: ${PLUGIN_VERSION}"
Expand Down Expand Up @@ -404,8 +423,15 @@ update_openclaw_config
info "Install OpenClaw Gateway service, 安装 OpenClaw Gateway 服务..."
"${OPENCLAW_BIN}" gateway install --port "${PORT}" --force 2>&1 || true

success "Start OpenClaw Gateway service, 启动 OpenClaw Gateway 服务..."
"${OPENCLAW_BIN}" gateway start 2>&1
info "Starting OpenClaw Gateway service, 正在启动 OpenClaw Gateway 服务..."
if ! "${OPENCLAW_BIN}" gateway start 2>&1; then
# Do not repeat the same failed final start from the EXIT cleanup.
GATEWAY_RECOVERY_STATE="final_failed"
error "Failed to start OpenClaw Gateway, OpenClaw Gateway 启动失败"
exit 1
fi
GATEWAY_RECOVERY_STATE="inactive"
success "OpenClaw Gateway started, OpenClaw Gateway 已启动"

info "Starting Memory Viewer, 正在启动记忆面板..."
VIEWER_URL="http://127.0.0.1:18799"
Expand Down
32 changes: 31 additions & 1 deletion apps/memos-local-plugin/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,28 @@ BUILT_TARBALL=""
STAGE_DIR=""
SOURCE_KIND="" # "path" for a local file, "npm" otherwise
SOURCE_SPEC=""
GATEWAY_RECOVERY_BIN=""
GATEWAY_RECOVERY_STATE="inactive"

cleanup_on_exit() {
if [[ "${GATEWAY_RECOVERY_STATE:-inactive}" == "needs_recovery" && -n "${GATEWAY_RECOVERY_BIN:-}" ]]; then
local recovery_out=""
if ! recovery_out="$("${GATEWAY_RECOVERY_BIN}" gateway start 2>&1)"; then
warn "OpenClaw gateway recovery failed; the gateway may still be stopped."
if [[ -n "${recovery_out}" ]]; then
printf '%s\n' "${recovery_out}" | sed 's/^/ /' >&2
fi
fi
fi
if [[ -n "${STAGE_DIR:-}" ]]; then
rm -rf "${STAGE_DIR}"
fi
}

trap cleanup_on_exit EXIT

resolve_tarball() {
STAGE_DIR="$(mktemp -d)"
trap 'rm -rf "${STAGE_DIR}"' EXIT

if [[ -n "${VERSION_ARG}" && -f "${VERSION_ARG}" ]]; then
BUILT_TARBALL="$(cd "$(dirname "${VERSION_ARG}")" && pwd)/$(basename "${VERSION_ARG}")"
Expand Down Expand Up @@ -429,11 +447,17 @@ install_openclaw() {
mkdir -p "${HOME}/.openclaw"

local oc_bin=""
# These remain global because the EXIT trap reads them after this function returns.
# The top-level agent dispatch invokes this installer at most once.
GATEWAY_RECOVERY_BIN=""
GATEWAY_RECOVERY_STATE="inactive"
if oc_bin="$(find_openclaw_cli)"; then
step "Stopping OpenClaw gateway"
"${oc_bin}" gateway stop >/dev/null 2>&1 || true
sleep 1
success "Gateway stopped"
GATEWAY_RECOVERY_BIN="${oc_bin}"
GATEWAY_RECOVERY_STATE="needs_recovery"
fi

deploy_tarball_to_prefix "${prefix}"
Expand Down Expand Up @@ -608,6 +632,8 @@ NODE
|| (command -v lsof >/dev/null 2>&1 && lsof -i ":18789" -t >/dev/null 2>&1); then
success "OpenClaw gateway already running"
else
# Do not repeat the same failed final start from the EXIT cleanup.
GATEWAY_RECOVERY_STATE="final_failed"
error "openclaw gateway start failed:"
echo "${start_out}" | sed 's/^/ /' >&2
warn "Inspect ~/.openclaw/logs/gateway.err.log for the full reason."
Expand All @@ -616,6 +642,10 @@ NODE
else
success "OpenClaw gateway started"
fi
# The service started (or was already running), so the viewer fallback must not
# trigger another service start from the EXIT trap.
GATEWAY_RECOVERY_STATE="inactive"
GATEWAY_RECOVERY_BIN=""

step "Waiting for Memory Viewer"
if wait_for_viewer "${OPENCLAW_PORT}"; then
Expand Down
211 changes: 211 additions & 0 deletions apps/memos-local-plugin/tests/unit/install-gateway-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

import { describe, expect, it } from "vitest";

const pluginInstaller = fileURLToPath(new URL("../../install.sh", import.meta.url));
const legacyInstaller = fileURLToPath(
new URL("../../../memos-local-openclaw/install.sh", import.meta.url),
);

interface InstallerHarness {
root: string;
home: string;
bin: string;
temp: string;
gatewayLog: string;
}

function writeExecutable(path: string, body: string): void {
writeFileSync(path, `#!/usr/bin/env bash\nset -u\n${body}\n`, "utf8");
chmodSync(path, 0o755);
}

function createHarness(): InstallerHarness {
const root = mkdtempSync(join(tmpdir(), "memos-installer-recovery-"));
const home = join(root, "home");
const bin = join(root, "bin");
const temp = join(root, "tmp");
const gatewayLog = join(root, "gateway.log");
mkdirSync(join(home, ".openclaw"), { recursive: true });
mkdirSync(bin);
mkdirSync(temp);

writeExecutable(
join(bin, "node"),
'if [[ "${1:-}" == "-v" ]]; then printf "v22.0.0\\n"; fi\nexit 0',
);
writeExecutable(join(bin, "npx"), "exit 0");
writeExecutable(
join(bin, "npm"),
`case "\${1:-}" in
pack) exit "\${FAKE_NPM_PACK_EXIT:-0}" ;;
install) mkdir -p node_modules/better-sqlite3; exit 0 ;;
rebuild) exit 0 ;;
*) exit 0 ;;
esac`,
);
writeExecutable(
join(bin, "openclaw"),
`printf '%s\\n' "$*" >> "\${FAKE_GATEWAY_LOG:?}"
if [[ "$*" == "gateway start" ]]; then
if [[ "\${FAKE_GATEWAY_START_EXIT:-0}" != "0" ]]; then
printf 'fake gateway start failure\\n' >&2
fi
exit "\${FAKE_GATEWAY_START_EXIT:-0}"
fi
exit 0`,
);
writeExecutable(join(bin, "sleep"), "exit 0");
writeExecutable(join(bin, "lsof"), "exit 1");
writeExecutable(join(bin, "curl"), 'exit "${FAKE_CURL_EXIT:-0}"');

return { root, home, bin, temp, gatewayLog };
}

function runInstaller(
harness: InstallerHarness,
script: string,
args: string[],
extraEnv: NodeJS.ProcessEnv = {},
) {
return spawnSync("bash", [script, ...args], {
cwd: harness.root,
encoding: "utf8",
timeout: 30_000,
env: {
...process.env,
...extraEnv,
HOME: harness.home,
TMPDIR: harness.temp,
PATH: `${harness.bin}:${process.env.PATH ?? ""}`,
FAKE_GATEWAY_LOG: harness.gatewayLog,
},
});
}

function gatewayCalls(harness: InstallerHarness): string[] {
if (!existsSync(harness.gatewayLog)) return [];
return readFileSync(harness.gatewayLog, "utf8").trim().split("\n").filter(Boolean);
}

function expectTempCleaned(harness: InstallerHarness): void {
expect(readdirSync(harness.temp)).toEqual([]);
}

describe.skipIf(process.platform === "win32")("installer gateway recovery", () => {
it("restarts the gateway when the unified installer cannot extract the package", () => {
const harness = createHarness();
try {
const brokenTarball = join(harness.root, "broken.tgz");
writeFileSync(brokenTarball, "not a tarball", "utf8");

const result = runInstaller(harness, pluginInstaller, [
"--agent",
"openclaw",
"--version",
brokenTarball,
]);

expect(result.status).not.toBe(0);
expect(gatewayCalls(harness)).toEqual(["gateway stop", "gateway start"]);
expectTempCleaned(harness);
} finally {
rmSync(harness.root, { recursive: true, force: true });
}
});

it("warns when legacy-installer gateway recovery also fails", () => {
const harness = createHarness();
try {
const result = runInstaller(
harness,
legacyInstaller,
["--version", "missing-test-version", "--openclaw-home", join(harness.home, ".openclaw")],
{ FAKE_GATEWAY_START_EXIT: "17", FAKE_NPM_PACK_EXIT: "23" },
);

expect(result.status).not.toBe(0);
expect(gatewayCalls(harness)).toEqual(["gateway stop", "gateway start"]);
expect(result.stderr).toContain("Gateway recovery start failed");
expect(result.stderr).toContain("fake gateway start failure");
expectTempCleaned(harness);
} finally {
rmSync(harness.root, { recursive: true, force: true });
}
});

it("does not restart the gateway after the unified installer enters foreground fallback", () => {
const harness = createHarness();
try {
const packageRoot = join(harness.root, "package");
const runtimeDir = join(packageRoot, "dist", "adapters", "openclaw");
const tarball = join(harness.root, "plugin.tgz");
mkdirSync(runtimeDir, { recursive: true });
writeFileSync(join(packageRoot, "package.json"), '{"name":"test-plugin"}\n', "utf8");
writeFileSync(join(runtimeDir, "index.js"), "export {};\n", "utf8");
const tar = spawnSync("tar", ["-czf", tarball, "-C", harness.root, "package"], {
encoding: "utf8",
});
expect(tar.status, tar.stderr).toBe(0);

const result = runInstaller(
harness,
pluginInstaller,
["--agent", "openclaw", "--version", tarball],
{ FAKE_CURL_EXIT: "1" },
);

expect(result.status).not.toBe(0);
expect(gatewayCalls(harness)).toEqual(["gateway stop", "gateway start", "gateway"]);
expectTempCleaned(harness);
} finally {
rmSync(harness.root, { recursive: true, force: true });
}
});

it("does not retry a failed final gateway start from the exit trap", () => {
const harness = createHarness();
try {
const packageRoot = join(harness.root, "package");
const tarball = join(harness.root, "plugin.tgz");
mkdirSync(packageRoot);
writeFileSync(join(packageRoot, "package.json"), '{"name":"test-plugin"}\n', "utf8");
const tar = spawnSync("tar", ["-czf", tarball, "-C", harness.root, "package"], {
encoding: "utf8",
});
expect(tar.status, tar.stderr).toBe(0);

const result = runInstaller(
harness,
legacyInstaller,
["--version", tarball, "--openclaw-home", join(harness.home, ".openclaw")],
{ FAKE_GATEWAY_START_EXIT: "17" },
);

expect(result.status).not.toBe(0);
const calls = gatewayCalls(harness);
expect(calls[0]).toBe("gateway stop");
expect(calls.filter((call) => call === "gateway start")).toHaveLength(1);
expect(result.stdout).toContain("Starting OpenClaw Gateway service");
expect(result.stdout).not.toContain("OpenClaw Gateway started");
expect(result.stdout).not.toContain("Start OpenClaw Gateway service");
expect(result.stderr).toContain("Failed to start OpenClaw Gateway");
expectTempCleaned(harness);
} finally {
rmSync(harness.root, { recursive: true, force: true });
}
});
});
Loading