agent-afk 5.249.0 → 5.249.2

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.
@@ -30,6 +30,47 @@ export function detectPathGap(prefix, pathEnv) {
30
30
  return { onPath, binDir };
31
31
  }
32
32
 
33
+ /**
34
+ * Pure function. Determines whether this postinstall run is part of a global
35
+ * package install (`npm install -g` / `pnpm add -g`) as opposed to a local
36
+ * dev-checkout `pnpm install` or a worktree install.
37
+ *
38
+ * Detection uses two independent signals and fails CLOSED toward NOT-global
39
+ * (i.e. no restart) whenever either signal is uncertain:
40
+ *
41
+ * 1. npm_config_global env var — npm and pnpm both set this to the string
42
+ * "true" for every global install. A local `pnpm install` inside a repo
43
+ * never sets it. This is the primary, cheapest signal.
44
+ *
45
+ * 2. git-worktree presence check — if the package root contains a .git
46
+ * entry (file or directory) it is a source checkout or managed worktree,
47
+ * never a globally-installed package. A global install lands under
48
+ * node_modules inside the global npm prefix, never inside a git tree.
49
+ * This is a belt-and-suspenders guard: even if npm_config_global were
50
+ * wrong or missing, a .git marker unambiguously rules out a global install.
51
+ *
52
+ * // History: Daemon was unconditionally restarted on every postinstall.
53
+ * // A cron job that ran `pnpm install` inside a repo worktree triggered this
54
+ * // path and restarted the running daemon mid-session, orphaning 438 subagents
55
+ * // and leaving the daemon stopped. The guard introduced here ensures restarts
56
+ * // occur ONLY for genuine global upgrades. See PR #2194.
57
+ *
58
+ * @param {string} pkgRoot - Absolute path to the package root.
59
+ * @param {object} [env] - Environment variable bag; defaults to process.env.
60
+ * @param {function} [existsFn] - Injectable fs.existsSync for testing.
61
+ * @returns {boolean} true only when both signals confirm a global install.
62
+ */
63
+ export function isGlobalInstall(pkgRoot, env = process.env, existsFn = existsSync) {
64
+ // Signal 1: npm/pnpm lifecycle env var. Absent or non-"true" → not global.
65
+ if (env['npm_config_global'] !== 'true') return false;
66
+
67
+ // Signal 2: .git marker. Present → source checkout / worktree → not global.
68
+ const gitMarker = join(pkgRoot, '.git');
69
+ if (existsFn(gitMarker)) return false;
70
+
71
+ return true;
72
+ }
73
+
33
74
  /**
34
75
  * Pure function. Attempts to SIGTERM a daemon whose PID is written in the
35
76
  * given file. All errors are silently discarded — this is best-effort cleanup
@@ -102,6 +143,13 @@ export function isManualBotRunning(pidFilePath, probeFn = process.kill) {
102
143
  * is swallowed so the install never fails. macOS only — the caller gates on
103
144
  * platform; elsewhere ~/Library/LaunchAgents won't exist so nothing restarts.
104
145
  *
146
+ * // Invariant: the daemon is NOT stateless. It may be mid-run on one or more
147
+ * // scheduled agent sessions. This function MUST only be called from a global
148
+ * // package upgrade path (verified by isGlobalInstall()) — never from a local
149
+ * // `pnpm install` inside a source checkout or worktree. The caller in the main
150
+ * // block enforces this via isGlobalInstall(); callers in tests must pass stubs
151
+ * // for execFn/restartFn so real launchctl is never invoked.
152
+ *
105
153
  * Label / path / domain conventions mirror src/service/launchd/paths.ts
106
154
  * (labelFor → `com.afk.<name>`, plist under ~/Library/LaunchAgents, guiDomain →
107
155
  * `gui/<uid>`). Kept in sync by hand — this plain .mjs cannot import the
@@ -260,20 +308,30 @@ if (isMain) {
260
308
  // here bypasses that session-safety logic and is the root cause of "sessions
261
309
  // that work but never respond" — see telegram-stuck-diagnosis.md.
262
310
  //
263
- // The daemon is stateless (no active user sessions) and safe to force-restart.
311
+ // Invariant: the daemon may be mid-run on scheduled agent sessions and is
312
+ // NOT safe to restart unconditionally. Service restart is only correct for a
313
+ // global package upgrade (`npm install -g`). A local `pnpm install` inside a
314
+ // source checkout or managed worktree must NEVER trigger a restart — doing so
315
+ // kills any in-flight sessions owned by that daemon. The isGlobalInstall()
316
+ // guard below enforces this; it fails closed toward NOT restarting whenever
317
+ // either detection signal is absent or ambiguous.
264
318
  if (process.platform === 'darwin') {
265
- try {
266
- const restarted = restartLaunchdServices({
267
- labels: ['com.afk.daemon'],
268
- });
269
- if (restarted.length > 0) {
270
- const names = restarted.map((l) => l.replace(/^com\.afk\./, '')).join(', ');
271
- process.stdout.write(
272
- `\n↻ Restarted AFK service(s) onto the new version: ${names}\n`,
273
- );
319
+ const scriptDir = new URL('.', import.meta.url).pathname;
320
+ const pkgRoot = join(scriptDir, '..');
321
+ if (isGlobalInstall(pkgRoot, process.env, existsSync)) {
322
+ try {
323
+ const restarted = restartLaunchdServices({
324
+ labels: ['com.afk.daemon'],
325
+ });
326
+ if (restarted.length > 0) {
327
+ const names = restarted.map((l) => l.replace(/^com\.afk\./, '')).join(', ');
328
+ process.stdout.write(
329
+ `\n↻ Restarted AFK service(s) onto the new version: ${names}\n`,
330
+ );
331
+ }
332
+ } catch {
333
+ // restartLaunchdServices is already fail-open; belt-and-suspenders.
274
334
  }
275
- } catch {
276
- // restartLaunchdServices is already fail-open; belt-and-suspenders.
277
335
  }
278
336
  }
279
337