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
3b4843e
Queue updater installs across restarts
chrysb Apr 5, 2026
dd78cff
Prevent duplicate import apply during onboarding
chrysb Apr 6, 2026
353fb44
0.8.7-beta.0
chrysb Apr 6, 2026
ed7cfce
Isolate OpenClaw runtime updates from /app
chrysb Apr 6, 2026
bc5bd91
Seed managed OpenClaw runtime from bundled version
chrysb Apr 6, 2026
b9f6658
0.8.7-beta.1
chrysb Apr 6, 2026
48f6d9d
Bootstrap AlphaClaw from a managed runtime
chrysb Apr 6, 2026
3bff33b
0.8.7-beta.2
chrysb Apr 6, 2026
0957025
Canonicalize usage-tracker plugin paths
chrysb Apr 6, 2026
aa61f0a
Sync managed runtimes from bundled packages
chrysb Apr 6, 2026
7d33761
Install managed runtimes from tarballs
chrysb Apr 6, 2026
bbb8639
Repair symlinked managed runtimes on boot
chrysb Apr 6, 2026
77d804e
Fail fast before OpenClaw runtime sync
chrysb Apr 6, 2026
9d51694
0.8.7-beta.3
chrysb Apr 6, 2026
5e4dcb7
Seed fresh runtimes from bundled node_modules
chrysb Apr 6, 2026
859239f
Swap managed runtimes after pending updates
chrysb Apr 6, 2026
0bbf064
0.8.7-beta.4
chrysb Apr 6, 2026
8ce1cae
Isolate managed OpenClaw runtime usage
chrysb Apr 6, 2026
2970247
0.8.7-beta.5
chrysb Apr 6, 2026
86fea4c
Defer OpenClaw bundled plugin postinstall
chrysb Apr 6, 2026
44277be
0.8.7-beta.6
chrysb Apr 6, 2026
94d13da
Extend OpenClaw restart wait timeout
chrysb Apr 6, 2026
a05dccd
Avoid buffering OpenClaw postinstall output
chrysb Apr 6, 2026
b09c89d
0.8.7-beta.7
chrysb Apr 6, 2026
cd60ca1
Harden OpenClaw version probes
chrysb Apr 6, 2026
e82bbc5
0.8.7-beta.8
chrysb Apr 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
217 changes: 174 additions & 43 deletions bin/alphaclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@ const { buildSecretReplacements } = require("../lib/server/helpers");
const {
migrateManagedInternalFiles,
} = require("../lib/server/internal-files-migration");

const kUsageTrackerPluginPath = path.resolve(
__dirname,
"..",
"lib",
"plugin",
"usage-tracker",
);
const {
applyPendingAlphaclawUpdate,
} = require("../lib/server/pending-alphaclaw-update");
const {
getManagedAlphaclawCliPath,
getManagedAlphaclawRuntimeDir,
syncManagedAlphaclawRuntimeWithBundled,
} = require("../lib/server/alphaclaw-runtime");
const {
applyPendingOpenclawUpdate,
} = require("../lib/server/pending-openclaw-update");
const {
getManagedOpenclawRuntimeDir,
prependManagedOpenclawBinToPath,
syncManagedOpenclawRuntimeWithBundled,
} = require("../lib/server/openclaw-runtime");
const {
ensurePluginsShell,
ensureUsageTrackerPluginEntry,
} = require("../lib/server/usage-tracker-config");

// ---------------------------------------------------------------------------
// Parse CLI flags
Expand Down Expand Up @@ -125,6 +137,26 @@ const resolveGithubRepoPath = (value) =>
.replace(/^git@github\.com:/, "")
.replace(/^https:\/\/github\.com\//, "")
.replace(/\.git$/, "");
const isContainerRuntime = () =>
process.env.RAILWAY_ENVIRONMENT ||
process.env.RENDER ||
process.env.FLY_APP_NAME ||
fs.existsSync("/.dockerenv");
const restartAfterPendingUpdate = () => {
if (isContainerRuntime()) {
console.log("[alphaclaw] Restarting via container crash (exit 1)...");
process.exit(1);
}
console.log("[alphaclaw] Spawning new process and exiting...");
const { spawn } = require("child_process");
const child = spawn(process.argv[0], process.argv.slice(1), {
detached: true,
stdio: "inherit",
env: process.env,
});
child.unref();
process.exit(0);
};

// ---------------------------------------------------------------------------
// 1. Resolve root directory (before requiring any lib/ modules)
Expand All @@ -142,10 +174,89 @@ if (portFlag) {
process.env.PORT = portFlag;
}

const kManagedAlphaclawRuntimeEnvFlag = "ALPHACLAW_MANAGED_RUNTIME_ACTIVE";
const shouldBootstrapManagedAlphaclawRuntime =
command === "start" &&
process.env[kManagedAlphaclawRuntimeEnvFlag] !== "1";

// ---------------------------------------------------------------------------
// 2. Create directory structure
// ---------------------------------------------------------------------------

if (shouldBootstrapManagedAlphaclawRuntime) {
const { spawn } = require("child_process");
const managedAlphaclawRuntimeDir = getManagedAlphaclawRuntimeDir({ rootDir });
const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
if (fs.existsSync(pendingUpdateMarker)) {
applyPendingAlphaclawUpdate({
execSyncImpl: execSync,
fsModule: fs,
installDir: managedAlphaclawRuntimeDir,
logger: console,
markerPath: pendingUpdateMarker,
});
}
try {
syncManagedAlphaclawRuntimeWithBundled({
execSyncImpl: execSync,
fsModule: fs,
logger: console,
runtimeDir: managedAlphaclawRuntimeDir,
});
} catch (error) {
console.log(
`[alphaclaw] Could not sync managed AlphaClaw runtime from bundled install: ${error.message}`,
);
}

const managedAlphaclawCliPath = getManagedAlphaclawCliPath({
runtimeDir: managedAlphaclawRuntimeDir,
});
if (!fs.existsSync(managedAlphaclawCliPath)) {
console.error(
`[alphaclaw] Managed AlphaClaw runtime missing CLI at ${managedAlphaclawCliPath}`,
);
process.exit(1);
}

const runtimeChild = spawn(
process.argv[0],
[managedAlphaclawCliPath, ...process.argv.slice(2)],
{
stdio: "inherit",
env: {
...process.env,
[kManagedAlphaclawRuntimeEnvFlag]: "1",
ALPHACLAW_BOOTSTRAP_CLI_PATH: __filename,
},
},
);

const forwardSignal = (signal) => {
if (runtimeChild.exitCode === null && !runtimeChild.killed) {
runtimeChild.kill(signal);
}
};

process.on("SIGTERM", () => forwardSignal("SIGTERM"));
process.on("SIGINT", () => forwardSignal("SIGINT"));

runtimeChild.on("error", (error) => {
console.error(
`[alphaclaw] Managed AlphaClaw runtime launch failed: ${error.message}`,
);
process.exit(1);
});

runtimeChild.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(Number.isInteger(code) ? code : 0);
});
} else {

const openclawDir = path.join(rootDir, ".openclaw");
fs.mkdirSync(openclawDir, { recursive: true });
const { hourlyGitSyncPath } = migrateManagedInternalFiles({
Expand All @@ -155,34 +266,27 @@ const { hourlyGitSyncPath } = migrateManagedInternalFiles({
console.log(`[alphaclaw] Root directory: ${rootDir}`);

// Check for pending update marker (written by the update endpoint before restart).
// In environments where the container filesystem is ephemeral (Railway, etc.),
// the npm install from the update endpoint is lost on restart. This re-runs it
// from the fresh container using the persistent volume marker.
// We perform a real npm install during boot rather than copy-merging node_modules
// while the old process is still running. That keeps AlphaClaw and any newly
// pinned OpenClaw version in a coherent npm tree.
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,
},
);
fs.unlinkSync(pendingUpdateMarker);
console.log("[alphaclaw] Update applied successfully");
} catch (e) {
console.log(`[alphaclaw] Update install failed: ${e.message}`);
fs.unlinkSync(pendingUpdateMarker);
const pendingUpdate = applyPendingAlphaclawUpdate({
execSyncImpl: execSync,
fsModule: fs,
installDir,
logger: console,
markerPath: pendingUpdateMarker,
});
if (pendingUpdate.installed) {
console.log("[alphaclaw] Restarting to load updated code...");
restartAfterPendingUpdate();
}
}

Expand Down Expand Up @@ -492,7 +596,41 @@ if (!kSetupPassword) {
}

// ---------------------------------------------------------------------------
// 7. Set OPENCLAW_HOME globally so all child processes inherit it
// 7. Prepare managed OpenClaw runtime
// ---------------------------------------------------------------------------

const pendingOpenclawUpdateMarker = path.join(rootDir, ".openclaw-update-pending");
const managedOpenclawRuntimeDir = getManagedOpenclawRuntimeDir({ rootDir });
if (fs.existsSync(pendingOpenclawUpdateMarker)) {
applyPendingOpenclawUpdate({
execSyncImpl: execSync,
fsModule: fs,
installDir: managedOpenclawRuntimeDir,
logger: console,
markerPath: pendingOpenclawUpdateMarker,
});
}
try {
syncManagedOpenclawRuntimeWithBundled({
execSyncImpl: execSync,
fsModule: fs,
logger: console,
runtimeDir: managedOpenclawRuntimeDir,
});
} catch (error) {
console.log(
`[alphaclaw] Could not sync managed OpenClaw runtime from bundled install: ${error.message}`,
);
}
prependManagedOpenclawBinToPath({
env: process.env,
fsModule: fs,
logger: console,
runtimeDir: managedOpenclawRuntimeDir,
});

// ---------------------------------------------------------------------------
// 8. Set OPENCLAW_HOME globally so all child processes inherit it
// ---------------------------------------------------------------------------

process.env.OPENCLAW_HOME = rootDir;
Expand All @@ -501,7 +639,7 @@ process.env.GOG_KEYRING_PASSWORD =
process.env.GOG_KEYRING_PASSWORD || "alphaclaw";

// ---------------------------------------------------------------------------
// 8. Install gog (Google Workspace CLI) if not present
// 9. Install gog (Google Workspace CLI) if not present
// ---------------------------------------------------------------------------

process.env.XDG_CONFIG_HOME = openclawDir;
Expand Down Expand Up @@ -534,7 +672,7 @@ if (!gogInstalled) {
}

// ---------------------------------------------------------------------------
// 9. Install/reconcile system cron entry
// 10. Install/reconcile system cron entry
// ---------------------------------------------------------------------------

const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
Expand Down Expand Up @@ -600,7 +738,7 @@ if (fs.existsSync(hourlyGitSyncPath)) {
}

// ---------------------------------------------------------------------------
// 9. Start cron daemon if available
// 11. Start cron daemon if available
// ---------------------------------------------------------------------------

try {
Expand All @@ -614,7 +752,7 @@ try {
} catch {}

// ---------------------------------------------------------------------------
// 10. Reconcile channels if already onboarded
// 12. Reconcile channels if already onboarded
// ---------------------------------------------------------------------------

const configPath = path.join(openclawDir, "openclaw.json");
Expand Down Expand Up @@ -762,10 +900,7 @@ if (fs.existsSync(configPath)) {
try {
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (!cfg.channels) cfg.channels = {};
if (!cfg.plugins) cfg.plugins = {};
if (!cfg.plugins.load) cfg.plugins.load = {};
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
if (!cfg.plugins.entries) cfg.plugins.entries = {};
ensurePluginsShell(cfg);
let changed = false;

if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
Expand All @@ -791,12 +926,7 @@ if (fs.existsSync(configPath)) {
console.log("[alphaclaw] Discord added");
changed = true;
}
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
changed = true;
}
if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
cfg.plugins.entries["usage-tracker"] = { enabled: true };
if (ensureUsageTrackerPluginEntry(cfg)) {
changed = true;
}

Expand Down Expand Up @@ -898,3 +1028,4 @@ try {

console.log("[alphaclaw] Setup complete -- starting server");
require("../lib/server.js");
}
3 changes: 3 additions & 0 deletions lib/public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const App = () => {
onPreviewBrowseFile=${browseActions.handleBrowsePreviewFile}
acHasUpdate=${controllerState.acHasUpdate}
acLatest=${controllerState.acLatest}
acRestarting=${controllerState.acRestarting}
acUpdating=${controllerState.acUpdating}
onAcUpdate=${controllerActions.handleAcUpdate}
agents=${agentsState.agents}
Expand Down Expand Up @@ -384,6 +385,7 @@ const App = () => {
restartingGateway=${controllerState.restartingGateway}
onRestartGateway=${controllerActions.handleGatewayRestart}
restartSignal=${controllerState.gatewayRestartSignal}
openclawRestarting=${controllerState.openclawRestarting}
openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
Expand Down Expand Up @@ -419,6 +421,7 @@ const App = () => {
restartingGateway=${controllerState.restartingGateway}
onRestartGateway=${controllerActions.handleGatewayRestart}
restartSignal=${controllerState.gatewayRestartSignal}
openclawRestarting=${controllerState.openclawRestarting}
openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
Expand Down
9 changes: 6 additions & 3 deletions lib/public/js/components/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const VersionRow = ({
fetchVersion,
applyUpdate,
updateInProgress = false,
updateLoadingLabel = "Updating...",
onActionComplete = () => {},
}) => {
const [checking, setChecking] = useState(false);
Expand Down Expand Up @@ -236,7 +237,7 @@ const VersionRow = ({
? updateIdleLabel
: "Check updates"}
loadingLabel=${isUpdateActionActive
? "Updating..."
? updateLoadingLabel
: "Checking..."}
className="hidden md:inline-flex"
/>
Expand All @@ -250,7 +251,7 @@ const VersionRow = ({
? updateIdleLabel
: "Check updates"}
loadingLabel=${isUpdateActionActive
? "Updating..."
? updateLoadingLabel
: "Checking..."}
/>
`}
Expand All @@ -272,7 +273,7 @@ const VersionRow = ({
loading=${updateButtonLoading}
warning=${isUpdateActionActive}
idleLabel=${updateIdleLabel}
loadingLabel="Updating..."
loadingLabel=${updateLoadingLabel}
className="flex-1 h-9 px-3"
/>
</div>
Expand All @@ -299,6 +300,7 @@ export const Gateway = ({
onOpenWatchdog,
onRepair,
repairing = false,
openclawRestarting = false,
openclawUpdateInProgress = false,
onOpenclawVersionActionComplete = () => {},
onOpenclawUpdate = updateOpenclaw,
Expand Down Expand Up @@ -443,6 +445,7 @@ export const Gateway = ({
fetchVersion=${fetchOpenclawVersion}
applyUpdate=${onOpenclawUpdate}
updateInProgress=${openclawUpdateInProgress}
updateLoadingLabel=${openclawRestarting ? "Restarting..." : "Updating..."}
onActionComplete=${onOpenclawVersionActionComplete}
/>
</div>
Expand Down
Loading