robot-resources 1.9.5 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/service.js CHANGED
@@ -216,6 +216,28 @@ WantedBy=default.target
216
216
  `;
217
217
  }
218
218
 
219
+ /**
220
+ * Check whether linger is enabled for the current user.
221
+ *
222
+ * Without linger, systemd-user services are torn down when the user logs
223
+ * out (SSH disconnect, login manager logout). On the Finland signup
224
+ * (2026-04-23) this was the root cause: 3 heartbeats, then session ended,
225
+ * then the router died with the session.
226
+ */
227
+ function isLingerEnabled() {
228
+ try {
229
+ const user = process.env.USER || process.env.LOGNAME;
230
+ if (!user) return false;
231
+ const res = spawnSync('loginctl', ['show-user', user, '--property=Linger'], {
232
+ stdio: 'pipe', encoding: 'utf-8',
233
+ });
234
+ if (res.status !== 0) return false;
235
+ return /^Linger=yes\s*$/m.test(res.stdout || '');
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+
219
241
  function installSystemdUser(venvPythonPath) {
220
242
  const unitPath = getUserUnitPath();
221
243
  const logsDir = join(homedir(), '.robot-resources', 'logs');
@@ -232,12 +254,17 @@ function installSystemdUser(venvPythonPath) {
232
254
  execSync('systemctl --user enable robot-resources-router.service', { stdio: 'pipe' });
233
255
  execSync('systemctl --user start robot-resources-router.service', { stdio: 'pipe' });
234
256
 
235
- // Enable linger so the service survives SSH disconnects (critical for VMs)
257
+ // Enable linger so the service survives SSH disconnects (critical for VMs).
258
+ // On many distros this needs polkit auth and silently no-ops from a
259
+ // non-interactive shell — we attempt it then VERIFY the result.
260
+ let lingerEnabled = false;
236
261
  try {
237
262
  execSync('loginctl enable-linger', { stdio: 'pipe' });
238
263
  } catch {
239
- // Non-fatal linger may not be available (e.g. no loginctl)
264
+ // fall through to verification
240
265
  }
266
+ lingerEnabled = isLingerEnabled();
267
+ return { lingerEnabled };
241
268
  }
242
269
 
243
270
  function uninstallSystemdUser() {
@@ -603,8 +630,29 @@ export function installService(venvPythonPath) {
603
630
  }
604
631
 
605
632
  // mode === 'user'
606
- installSystemdUser(venvPythonPath);
607
- return { type: 'systemd-user', path: getUserUnitPath() };
633
+ const { lingerEnabled } = installSystemdUser(venvPythonPath);
634
+
635
+ // Belt-and-suspenders: ALSO install crontab @reboot so the router comes
636
+ // back on reboot even if linger isn't taking effect (polkit denied,
637
+ // container restrictions, etc.). Idempotent — removes any existing
638
+ // RR crontab entry before adding the fresh one. Safe to call even
639
+ // when crontab is absent (we skip silently).
640
+ let crontabFallback = false;
641
+ if (hasCrontab()) {
642
+ try {
643
+ installCrontab(venvPythonPath);
644
+ crontabFallback = true;
645
+ } catch {
646
+ // Non-fatal — systemd-user still works while user is logged in.
647
+ }
648
+ }
649
+
650
+ return {
651
+ type: 'systemd-user',
652
+ path: getUserUnitPath(),
653
+ lingerEnabled,
654
+ crontabFallback,
655
+ };
608
656
  }
609
657
 
610
658
  if (process.platform === 'win32') {
@@ -624,10 +672,12 @@ export function installService(venvPythonPath) {
624
672
  export function uninstallService() {
625
673
  if (process.platform === 'darwin') return uninstallLaunchd();
626
674
  if (process.platform === 'linux') {
627
- // Clean up whichever variant is installed.
628
- if (existsSync(SYSTEM_UNIT_PATH)) return uninstallSystemdSystem();
629
- if (existsSync(getUserUnitPath())) return uninstallSystemdUser();
630
- if (isCrontabInstalled()) return uninstallCrontab();
675
+ // Clean up whatever variants are installed. systemd-user users may
676
+ // also have a crontab belt installed alongside — remove both.
677
+ if (existsSync(SYSTEM_UNIT_PATH)) uninstallSystemdSystem();
678
+ if (existsSync(getUserUnitPath())) uninstallSystemdUser();
679
+ if (isCrontabInstalled()) uninstallCrontab();
680
+ return;
631
681
  }
632
682
  if (process.platform === 'win32') return uninstallTaskScheduler();
633
683
  }
package/lib/wizard.js CHANGED
@@ -83,6 +83,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
83
83
  // "pip installed but router never served a request" from a real
84
84
  // working setup in post-hoc telemetry.
85
85
  serviceType: null,
86
+ lingerEnabled: null,
87
+ crontabFallback: null,
86
88
  pluginInstalled: false,
87
89
  openclawDetected: false,
88
90
  openclawConfigPatched: false,
@@ -235,12 +237,24 @@ export async function runWizard({ nonInteractive = false } = {}) {
235
237
  try {
236
238
  const svc = installService(getVenvPythonPath());
237
239
  results.serviceType = svc.type || null;
240
+ // systemd-user only survives user sessions with linger enabled; the
241
+ // installer now verifies the bit actually flipped and installs a
242
+ // crontab @reboot belt when it didn't. Capture both signals so we
243
+ // can tell which users land on a live-forever setup vs one that
244
+ // dies on logout.
245
+ results.lingerEnabled = svc.lingerEnabled ?? null;
246
+ results.crontabFallback = svc.crontabFallback ?? null;
238
247
  if (svc.type === 'skipped') {
239
248
  warn(svc.reason);
240
249
  results.service = false;
241
250
  } else {
242
251
  success(`Router registered as ${svc.type} service`);
243
252
  info(`Config: ${svc.path}`);
253
+ if (svc.type === 'systemd-user') {
254
+ if (svc.lingerEnabled) info('Linger enabled — router survives logout');
255
+ else warn('Linger not enabled — router may stop when you log out');
256
+ if (svc.crontabFallback) info('Crontab @reboot installed as fallback');
257
+ }
244
258
  info('Router will start automatically and restart on crash');
245
259
  results.service = true;
246
260
  }
@@ -419,6 +433,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
419
433
  install_duration_ms: Date.now() - wizardStartMs,
420
434
  python_source: results.pythonSource ?? null,
421
435
  service_type: results.serviceType ?? null,
436
+ linger_enabled: results.lingerEnabled,
437
+ crontab_fallback: results.crontabFallback,
422
438
  health_check: results.healthCheck,
423
439
  plugin_installed: results.pluginInstalled,
424
440
  openclaw_detected: results.openclawDetected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
4
4
  "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {