Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
02c5b35
Bump openclaw to 2026.6.5.
garrytan Jun 12, 2026
92f25f8
Add prepare script so git-dependency installs build the UI
garrytan Jun 12, 2026
5042e2f
Merge pull request #1 from garrytan/prepare-script-for-git-installs
garrytan Jun 12, 2026
20f818c
Prune stale usage-tracker plugin paths on boot
garrytan Jun 12, 2026
096a13d
Support git-based deployment of AlphaClaw
garrytan Jun 12, 2026
eaccebb
Merge pull request #2 from garrytan/git-deployment-support
garrytan Jun 12, 2026
dbe63d7
Prune stale usage-tracker plugin paths on every boot, not just onboarded
garrytan Jun 12, 2026
41a90a9
Merge pull request #3 from garrytan/prune-stale-paths-unconditionally
garrytan Jun 12, 2026
3fc02c1
Bump openclaw to 2026.6.8.
garrytan Jun 17, 2026
5f1eb98
Bump openclaw to 2026.6.11.
garrytan Jul 11, 2026
ccb0f95
Merge remote-tracking branch 'upstream/main'
garrytan Jul 11, 2026
32849d2
Bump openclaw to 2026.7.1-2.
garrytan Jul 19, 2026
6aef485
Harden watchdog for OpenClaw 2026.7.1 gateway lifecycle contract.
garrytan Jul 19, 2026
95f9de7
Merge upstream/main (0.9.31): OpenClaw 7.1 alignment, Codex reliabili…
garrytan Jul 19, 2026
accfe42
Raise test coverage from 71% to 99.63% lines (2,077 tests).
garrytan Jul 19, 2026
3ef995f
Merge upstream/main (0.9.33): SQLite cron store restore, cron warning…
garrytan Jul 22, 2026
c85a108
Merge remote-tracking branch 'upstream/main'
garrytan Jul 28, 2026
f947a45
Merge upstream/main (cron root-dir fix); harden two real-socket test …
garrytan Jul 28, 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
64 changes: 43 additions & 21 deletions bin/alphaclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
restoreMissingOpenclawConfigFromRemote,
} = require("../lib/cli/openclaw-config-restore");
const { buildSecretReplacements } = require("../lib/server/helpers");
const { resolveSelfDependency } = require("../lib/server/self-dependency");
const {
migrateLegacyTelegramStreamingConfig,
} = require("../lib/server/openclaw-config-migrations");
Expand Down Expand Up @@ -189,29 +190,34 @@ console.log(`[alphaclaw] Root directory: ${rootDir}`);
// from the fresh container using the persistent volume marker.
const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
if (fs.existsSync(pendingUpdateMarker)) {
console.log(
"[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...",
);
const alphaPkgRoot = path.resolve(__dirname, "..");
const nmIndex = alphaPkgRoot.lastIndexOf(
`${path.sep}node_modules${path.sep}`,
);
const installDir =
nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot;
try {
execSync(
"npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online",
{
cwd: installDir,
stdio: "inherit",
timeout: 180000,
},
const selfDep = resolveSelfDependency({ fsImpl: fs });
if (selfDep.isGit) {
// Git-based installs update by redeploying (which reinstalls from the pinned
// ref), not by `npm install <pkg>@latest`. Clear the marker and move on.
console.log(
"[alphaclaw] Pending update marker found, but this install is git-based; updates apply on redeploy. Skipping npm install.",
);
fs.unlinkSync(pendingUpdateMarker);
console.log("[alphaclaw] Update applied successfully");
} catch (e) {
console.log(`[alphaclaw] Update install failed: ${e.message}`);
fs.unlinkSync(pendingUpdateMarker);
} else {
const selfUpdatePackageName = selfDep.key || "alphaclaw";
console.log(
`[alphaclaw] Pending update detected, installing ${selfUpdatePackageName}@latest...`,
);
try {
execSync(
`npm install ${selfUpdatePackageName}@latest --omit=dev --prefer-online`,
{
cwd: selfDep.installDir,
stdio: "inherit",
timeout: 180000,
},
);
fs.unlinkSync(pendingUpdateMarker);
console.log("[alphaclaw] Update applied successfully");
} catch (e) {
console.log(`[alphaclaw] Update install failed: ${e.message}`);
fs.unlinkSync(pendingUpdateMarker);
}
}
}

Expand Down Expand Up @@ -894,6 +900,22 @@ if (fs.existsSync(configPath)) {
console.log("[alphaclaw] Discord added");
changed = true;
}
// Drop usage-tracker plugin paths left by a previous install location (e.g. a
// prior @chrysb/alphaclaw npm install at /app/node_modules/@chrysb/alphaclaw/...
// after switching to a git dependency at /app/node_modules/alphaclaw/...). The
// dead path makes OpenClaw reject the whole config. This block runs on every
// boot whenever a config exists — onboarded or not — so it is the migration's
// load-bearing prune; the onboarded reconcile prune is a backstop.
const usageTrackerPathPattern = /[\\/]plugin[\\/]usage-tracker[\\/]?$/;
const prunedPaths = cfg.plugins.load.paths.filter(
(entry) =>
entry === kUsageTrackerPluginPath ||
!usageTrackerPathPattern.test(String(entry || "")),
);
if (prunedPaths.length !== cfg.plugins.load.paths.length) {
cfg.plugins.load.paths = prunedPaths;
changed = true;
}
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
changed = true;
Expand Down
1 change: 1 addition & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ const watchdog = createWatchdog({
reloadEnv,
resolveSetupUrl,
resolveGatewayHealthUrl: () => `${getGatewayUrl()}/health`,
resolveGatewayReadyzUrl: () => `${getGatewayUrl()}/readyz`,
});
const watchdogTerminal = createWatchdogTerminalService({
cwd: constants.OPENCLAW_DIR,
Expand Down
92 changes: 52 additions & 40 deletions lib/server/alphaclaw-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ const {
normalizeOpenclawVersion,
resolveGithubRepoUrl,
} = require("./helpers");
const {
resolveSelfDependency,
looksLikeGitDependency,
} = require("./self-dependency");

const kGithubApiBaseUrl = "https://api.github.com/repos";
const kGithubRawBaseUrl = "https://raw.githubusercontent.com";
const kDefaultTemplateBranch = "main";
const kRailwayTemplateRepoUrl =
"https://github.com/chrysb/openclaw-railway-template.git";
const kRenderTemplateRepoUrl =
"https://github.com/chrysb/openclaw-render-template.git";
"https://github.com/garrytan/openclaw-render-template.git";
const kApexTemplateRepoUrl =
"https://github.com/chrysb/openclaw-apex-template.git";

Expand Down Expand Up @@ -91,10 +95,18 @@ const buildGithubHeaders = ({ env = process.env, accept = "application/json" } =
return headers;
};

const extractTemplateVersions = (pkg) => ({
latestVersion: normalizeVersion(pkg?.dependencies?.["@chrysb/alphaclaw"]),
latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw),
});
const extractTemplateVersions = (pkg) => {
const alphaclawSpec =
pkg?.dependencies?.["alphaclaw"] || pkg?.dependencies?.["@chrysb/alphaclaw"];
return {
// A git-pinned template has no semver to compare against — report null so we
// never surface a bogus "update available" from a git URL string.
latestVersion: looksLikeGitDependency(alphaclawSpec)
? null
: normalizeVersion(alphaclawSpec),
latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw),
};
};

const fetchLatestVersionFromRegistry = async ({ fetchImpl, version = null }) => {
if (typeof fetchImpl !== "function") {
Expand Down Expand Up @@ -343,12 +355,33 @@ const detectUpdateStrategy = ({
});
}

// Git-based installs (e.g. `"alphaclaw": "git+https://github.com/<owner>/alphaclaw.git#main"`)
// can't be updated by `npm install <pkg>@latest` from the registry — updates come
// from pulling the source and reinstalling/redeploying. Keep the in-place npm
// self-update only when AlphaClaw is pinned to an npm version.
const selfDep = resolveSelfDependency({ fsImpl });
if (selfDep.isGit) {
return createUpdateStrategy({
action: "instructions",
provider: "git",
label: "Git source",
description:
"This AlphaClaw is installed from a git repository. Update by pulling the latest commit of your source repo and reinstalling (or redeploying), then restart AlphaClaw.",
steps: [
"Pull the latest commit of your AlphaClaw source repository",
"Reinstall dependencies so the new version is built (npm install)",
"Restart AlphaClaw to load the update",
],
primaryActionLabel: "Done",
});
}

const selfUpdatePackageName = selfDep.key || "alphaclaw";
return createUpdateStrategy({
action: "self-update",
provider: "self-hosted",
label: "This install",
description:
"This will install the latest @chrysb/alphaclaw package in place and restart AlphaClaw.",
description: `This will install the latest ${selfUpdatePackageName} package in place and restart AlphaClaw.`,
steps: [
"AlphaClaw will install the latest published package in place",
"The process will restart after the new files are copied into node_modules",
Expand Down Expand Up @@ -451,7 +484,9 @@ const createAlphaclawVersionService = ({

const installLatestAlphaclaw = () =>
new Promise((resolve, reject) => {
const installDir = findInstallDir(fsImpl);
const selfDep = resolveSelfDependency({ fsImpl });
const installDir = selfDep.installDir;
const selfUpdatePackageName = selfDep.key || "alphaclaw";
const tmpDir = fsImpl.mkdtempSync(path.join(os.tmpdir(), "alphaclaw-update-"));

const cleanup = () => {
Expand All @@ -464,7 +499,7 @@ const createAlphaclawVersionService = ({
path.join(tmpDir, "package.json"),
JSON.stringify({
private: true,
dependencies: { "@chrysb/alphaclaw": "latest" },
dependencies: { [selfUpdatePackageName]: "latest" },
}),
);

Expand All @@ -476,7 +511,7 @@ const createAlphaclawVersionService = ({
};

console.log(
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
`[alphaclaw] Running: npm install ${selfUpdatePackageName}@latest in temp dir (target: ${installDir})`,
);
childProcess.exec(
"npm install --omit=dev --prefer-online --package-lock=false",
Expand All @@ -494,7 +529,7 @@ const createAlphaclawVersionService = ({
cleanup();
return reject(
new Error(
message || "Failed to install @chrysb/alphaclaw@latest",
message || `Failed to install ${selfUpdatePackageName}@latest`,
),
);
}
Expand Down Expand Up @@ -616,6 +651,12 @@ const createAlphaclawVersionService = ({
const getVersionStatus = async (refresh) => {
const strategy = detectUpdateStrategy({ env, fsImpl });
try {
if (strategy.provider === "git") {
// Git-sourced installs have no npm registry or template version to compare
// against — updates come from redeploying the pinned ref. Report current
// state without a remote version check.
return buildVersionStatus({ strategy });
}
if (strategy.templateRepoUrl) {
const status = await readTemplateStatus({
repoUrl: strategy.templateRepoUrl,
Expand Down Expand Up @@ -719,35 +760,6 @@ const createAlphaclawVersionService = ({
};
};

const findInstallDir = (fsImpl) => {
let dir = kNpmPackageRoot;
while (dir !== path.dirname(dir)) {
const parent = path.dirname(dir);
if (
path.basename(parent) === "node_modules" ||
parent.includes(`${path.sep}node_modules${path.sep}`)
) {
dir = parent;
continue;
}
const pkgPath = path.join(parent, "package.json");
if (fsImpl.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fsImpl.readFileSync(pkgPath, "utf8"));
if (
pkg.dependencies?.["@chrysb/alphaclaw"] ||
pkg.devDependencies?.["@chrysb/alphaclaw"] ||
pkg.optionalDependencies?.["@chrysb/alphaclaw"]
) {
return parent;
}
} catch {}
}
dir = parent;
}
return kNpmPackageRoot;
};

module.exports = {
createAlphaclawVersionService,
detectUpdateStrategy,
Expand Down
2 changes: 1 addition & 1 deletion lib/server/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const kOpenclawUpdateCopyTimeoutMs = 5 * 60 * 1000;
const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
const kAlphaclawGithubReleasesBaseUrl =
"https://api.github.com/repos/chrysb/alphaclaw/releases";
"https://api.github.com/repos/garrytan/alphaclaw/releases";
const kAppDir = kNpmPackageRoot;
const kMaxPayloadBytes = parsePositiveInt(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
const kWebhookPruneDays = parsePositiveInt(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
Expand Down
34 changes: 5 additions & 29 deletions lib/server/openclaw-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
} = require("./constants");
const { normalizeOpenclawVersion } = require("./helpers");
const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
const { resolveSelfDependency } = require("./self-dependency");
const { assertSupportedNodeVersion } = require("../node-runtime");

const createOpenclawVersionService = ({
Expand Down Expand Up @@ -89,35 +90,10 @@ const createOpenclawVersionService = ({
}
};

const findInstallDir = () => {
// Resolve the consumer app root (for example /app in Docker), not this package directory.
let dir = kNpmPackageRoot;
while (dir !== path.dirname(dir)) {
const parent = path.dirname(dir);
if (
path.basename(parent) === "node_modules" ||
parent.includes(`${path.sep}node_modules${path.sep}`)
) {
dir = parent;
continue;
}
const pkgPath = path.join(parent, "package.json");
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
if (
pkg.dependencies?.["@chrysb/alphaclaw"] ||
pkg.devDependencies?.["@chrysb/alphaclaw"] ||
pkg.optionalDependencies?.["@chrysb/alphaclaw"]
) {
return parent;
}
} catch {}
}
dir = parent;
}
return kNpmPackageRoot;
};
// Resolve the consumer app root (for example /app in Docker), not this package
// directory. Matches AlphaClaw under either the `alphaclaw` alias (git installs)
// or the `@chrysb/alphaclaw` npm scope.
const findInstallDir = () => resolveSelfDependency({ fsImpl: fs }).installDir;

// Install to a temp directory, then copy into the real node_modules.
// Running `npm install` directly in the app dir causes EBUSY on Docker
Expand Down
13 changes: 13 additions & 0 deletions lib/server/routes/watchdog.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ const registerWatchdogRoutes = ({
}
});

app.post("/api/watchdog/resume-channels", requireAuth, async (req, res) => {
try {
const result = await watchdog.resumeChannels();
if (result?.skipped) {
res.status(409).json({ ok: false, error: result.reason, result });
return;
}
res.json({ ok: !!result?.ok, result });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});

app.get("/api/watchdog/settings", requireAuth, (req, res) => {
try {
res.json({ ok: true, settings: watchdog.getSettings() });
Expand Down
Loading