Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .crew/amos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ scope:
- lib/**
- .crew/**
- test/**
- deploy/**
- scripts/**
- package.json

branch_conventions:
feat: "feat/lr-{task_id}-{slug}"
Expand Down
28 changes: 28 additions & 0 deletions deploy/clagentic-console.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[Unit]
Description=Clagentic Console daemon
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/clagentic-daemon.sh
Restart=always
RestartSec=5
KillMode=mixed
TimeoutStopSec=20
SendSIGKILL=yes
# Soft ceiling — kernel begins reclaiming memory early (requires systemd 240+; host runs systemd 255)
MemoryHigh=70%
# Hard ceiling — triggers per-service OOM so OOMPolicy fires instead of host-level OOM killer
MemoryMax=85%
# On OOM, kill all remaining cgroup members so no orphaned child processes survive
OOMPolicy=kill
StandardOutput=journal
StandardError=journal
SyslogIdentifier=clagentic-console
Environment=SHELL=/bin/bash
Environment=PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# TODO(lr-9452): rename to CLAGENTIC_CONSOLE_MAX_CONCURRENT_SESSIONS once sdk-bridge.js is updated
Environment=CLAGENTIC_MAX_CONCURRENT_SESSIONS=50

[Install]
WantedBy=multi-user.target
3 changes: 3 additions & 0 deletions lib/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,9 @@ var ipc = createIPCServer(socketPath(), function (msg) {
stdio: ["ignore", "pipe", "pipe"],
timeout: 120000,
encoding: "utf8",
// CLAGENTIC_SELF_UPDATE=1 tells postinstall.js not to restart the
// service — gracefulShutdown() below hands off to systemd Restart=always.
env: Object.assign({}, process.env, { CLAGENTIC_SELF_UPDATE: "1" }),
});
console.log("[daemon] Global install complete. Shutting down for supervised restart.");
updOk = true;
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
},
"scripts": {
"dev": "node bin/cli.js --dev",
"postinstall": "node scripts/postinstall.js",
"test": "node --test --test-force-exit test/*.test.js",
"semantic-release": "semantic-release"
},
"files": [
"bin/",
"lib/",
"scripts/",
"deploy/",
"CREDITS.md"
],
"keywords": [
Expand Down
169 changes: 169 additions & 0 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env node
'use strict';

// postinstall.js — installs/updates the systemd service unit on Linux global npm installs.
// Runs automatically after `npm install -g @clagentic/console`.
// Silently exits on non-Linux platforms and non-root invocations.

const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const PREFIX = '[clagentic-console postinstall]';

function log(msg) {
console.log(`${PREFIX} ${msg}`);
}

function runCmd(cmd, args) {
// Returns stdout string on success, throws on failure.
// stderr is captured in err.stderr on failure.
return execFileSync(cmd, args, { stdio: 'pipe' }).toString().trim();
}

function errMsg(err) {
// execFileSync puts detail in err.stderr; fall back to err.message.
const stderr = err.stderr && err.stderr.toString().trim();
return stderr || err.message;
}

// Skip silently on non-Linux platforms.
if (process.platform !== 'linux') {
process.exit(0);
}

// Skip silently when not running as root — cannot write to /etc/systemd/system/.
if (process.getuid() !== 0) {
log('skipping (not root)');
process.exit(0);
}

// When the daemon triggers its own in-app update it passes CLAGENTIC_SELF_UPDATE=1.
// In that case, skip restarting the service — the daemon calls gracefulShutdown()
// immediately after npm install completes, and systemd Restart=always handles the
// supervised restart. Issuing systemctl restart here would race with gracefulShutdown
// and tear down sessions before state is flushed.
const selfUpdate = process.env.CLAGENTIC_SELF_UPDATE === '1';
if (selfUpdate) {
log('self-update detected (CLAGENTIC_SELF_UPDATE=1) — skipping service restart');
}

const SYSTEMD_DIR = '/etc/systemd/system';
const NEW_UNIT = 'clagentic-console.service';
const OLD_UNIT = 'clagentic.service';
const NEW_UNIT_DEST = path.join(SYSTEMD_DIR, NEW_UNIT);
const OLD_UNIT_PATH = path.join(SYSTEMD_DIR, OLD_UNIT);
const UNIT_SRC = path.join(__dirname, '..', 'deploy', 'clagentic-console.service');

// Step 1: Copy the new unit file.
log(`installing unit file -> ${NEW_UNIT_DEST}`);
try {
fs.copyFileSync(UNIT_SRC, NEW_UNIT_DEST);
} catch (err) {
log(`ERROR copying unit file: ${errMsg(err)}`);
// Cannot continue without the unit file in place.
process.exit(0);
}

// Step 2: daemon-reload to pick up the new unit file.
log('running systemctl daemon-reload');
try {
runCmd('systemctl', ['daemon-reload']);
} catch (err) {
log(`WARNING: daemon-reload failed: ${errMsg(err)}`);
}

// Step 3: Enable the new unit if not already enabled.
log(`enabling ${NEW_UNIT}`);
try {
runCmd('systemctl', ['enable', NEW_UNIT]);
} catch (err) {
log(`WARNING: enable ${NEW_UNIT} failed: ${errMsg(err)}`);
}

// Step 4: Handle rename — migrate from old clagentic.service if present.
let wasRunningUnderOldUnit = false;
if (fs.existsSync(OLD_UNIT_PATH)) {
log(`old unit file detected: ${OLD_UNIT_PATH} — performing rename cutover`);

// Detect whether the old unit is currently active before stopping it.
try {
const activeState = runCmd('systemctl', ['is-active', OLD_UNIT]);
if (activeState === 'active') {
wasRunningUnderOldUnit = true;
}
} catch (_) {
// is-active exits non-zero when not active — not an error.
}

// Disable the old unit.
log(`disabling ${OLD_UNIT}`);
try {
runCmd('systemctl', ['disable', OLD_UNIT]);
} catch (err) {
log(`WARNING: disable ${OLD_UNIT} failed (ignored): ${errMsg(err)}`);
}

// Stop the old unit.
log(`stopping ${OLD_UNIT}`);
try {
runCmd('systemctl', ['stop', OLD_UNIT]);
} catch (err) {
log(`WARNING: stop ${OLD_UNIT} failed (ignored): ${errMsg(err)}`);
}

// Remove the old unit file.
log(`removing ${OLD_UNIT_PATH}`);
try {
fs.unlinkSync(OLD_UNIT_PATH);
} catch (err) {
log(`WARNING: remove ${OLD_UNIT_PATH} failed: ${errMsg(err)}`);
}

// daemon-reload again to clear the old unit from systemd's view.
log('running systemctl daemon-reload (post-rename)');
try {
runCmd('systemctl', ['daemon-reload']);
} catch (err) {
log(`WARNING: daemon-reload (post-rename) failed: ${errMsg(err)}`);
}
}

// Step 5: Start or restart clagentic-console.service.
// Never restart during a self-update — gracefulShutdown() + Restart=always handles it.
if (selfUpdate) {
log('skipping start/restart (self-update — gracefulShutdown will hand off to systemd)');
} else {
let newUnitActive = false;
try {
const activeState = runCmd('systemctl', ['is-active', NEW_UNIT]);
newUnitActive = activeState === 'active';
} catch (_) {
// is-active exits non-zero when not active.
}

if (newUnitActive) {
// Already running (manual operator update) — restart so new unit file changes
// (memory limits, env vars, etc.) take effect immediately.
log(`restarting ${NEW_UNIT} (already active — applying unit file changes)`);
try {
runCmd('systemctl', ['restart', NEW_UNIT]);
} catch (err) {
log(`WARNING: restart ${NEW_UNIT} failed: ${errMsg(err)}`);
}
} else if (wasRunningUnderOldUnit) {
// Was running under the old unit — start it under the new one now.
log(`starting ${NEW_UNIT} (was running under ${OLD_UNIT})`);
try {
runCmd('systemctl', ['start', NEW_UNIT]);
} catch (err) {
log(`WARNING: start ${NEW_UNIT} failed: ${errMsg(err)}`);
}
} else {
// Not previously running — do not auto-start. The daemon requires configuration
// before it can run; starting without config would loop under Restart=always.
log(`${NEW_UNIT} was not running — skipping auto-start (run: systemctl start ${NEW_UNIT})`);
}
}

log('done');
Loading