modelstat 0.0.33 → 0.0.34

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/dist/cli.mjs CHANGED
@@ -45129,7 +45129,7 @@ var init_scan = __esm({
45129
45129
  init_pipeline2();
45130
45130
  init_config2();
45131
45131
  init_api();
45132
- AGENT_VERSION = "agent-0.0.33";
45132
+ AGENT_VERSION = "agent-0.0.34";
45133
45133
  BATCH_MAX_EVENTS = 2e3;
45134
45134
  }
45135
45135
  });
@@ -47221,7 +47221,7 @@ var init_daemon = __esm({
47221
47221
  init_config2();
47222
47222
  init_lock();
47223
47223
  init_scan();
47224
- AGENT_VERSION2 = "agent-0.0.33";
47224
+ AGENT_VERSION2 = "agent-0.0.34";
47225
47225
  HEARTBEAT_INTERVAL_MS = 1e4;
47226
47226
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
47227
47227
  status = {
@@ -47646,7 +47646,7 @@ function tryOpenBrowser(url) {
47646
47646
  return false;
47647
47647
  }
47648
47648
  }
47649
- var AGENT_VERSION3 = "agent-0.0.33";
47649
+ var AGENT_VERSION3 = "agent-0.0.34";
47650
47650
  function osFamily() {
47651
47651
  const p = platform4();
47652
47652
  if (p === "darwin") return "macos";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelstat",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "description": "modelstat companion — reads local AI-tool usage and ships tokenised events to modelstat.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -317,13 +317,71 @@ async function setupNativeNodeModules(installedBundle) {
317
317
  }
318
318
 
319
319
  /**
320
- * Kill any daemon process the service supervisor didn't reap.
321
- * Reads ~/.modelstat/daemon.lock for the PID, sends SIGTERM, then
322
- * SIGKILL after a short grace period. Tolerates a missing/stale
323
- * lock file we just want to make sure the new bundle is the only
324
- * thing holding the lock when we restart.
320
+ * Kill any modelstat-related process the service supervisor didn't
321
+ * reap. Two passes:
322
+ * (1) PID from ~/.modelstat/daemon.lock the most reliable hit
323
+ * when the lock is fresh and our own daemon wrote it.
324
+ * (2) Process scan via `pgrep -f` on macOS/Linux — catches stray
325
+ * daemons launched by an OLDER bundle (different command
326
+ * line, different lock format), zombies that survived a
327
+ * launchd KeepAlive bounce, and the case where the user has
328
+ * BOTH the launchd-managed daemon AND a hand-spawned one
329
+ * running. The pattern is intentionally narrow
330
+ * (`modelstat\\.mjs`) so we don't kill `modelstatd` or any
331
+ * other process that happens to share a substring.
332
+ *
333
+ * SIGTERM first, escalate to SIGKILL after 2 s. All failures
334
+ * tolerated — we just want the new bundle to be the only modelstat
335
+ * process holding the lock when we restart.
325
336
  */
326
337
  async function killStaleDaemon(stateDir) {
338
+ await killByLockfile(stateDir);
339
+ await killByProcessScan();
340
+ }
341
+
342
+ async function killByProcessScan() {
343
+ const { spawnSync } = await import("node:child_process");
344
+ // pgrep is on macOS by default, Linux via procps.
345
+ const r = spawnSync("pgrep", ["-f", "modelstat\\.mjs"], {
346
+ encoding: "utf8",
347
+ });
348
+ if (r.status !== 0 || !r.stdout) return;
349
+ const pids = r.stdout
350
+ .split(/\s+/)
351
+ .map((s) => Number(s))
352
+ .filter((n) => Number.isInteger(n) && n > 0 && n !== process.pid);
353
+ if (pids.length === 0) return;
354
+ for (const pid of pids) {
355
+ try {
356
+ process.kill(pid, "SIGTERM");
357
+ } catch {
358
+ /* gone or not ours */
359
+ }
360
+ }
361
+ // Wait up to 2 s for graceful exits.
362
+ for (let i = 0; i < 20; i++) {
363
+ let alive = 0;
364
+ for (const pid of pids) {
365
+ try {
366
+ process.kill(pid, 0);
367
+ alive += 1;
368
+ } catch {
369
+ /* exited */
370
+ }
371
+ }
372
+ if (alive === 0) return;
373
+ await new Promise((r) => setTimeout(r, 100));
374
+ }
375
+ for (const pid of pids) {
376
+ try {
377
+ process.kill(pid, "SIGKILL");
378
+ } catch {
379
+ /* gone or denied */
380
+ }
381
+ }
382
+ }
383
+
384
+ async function killByLockfile(stateDir) {
327
385
  const { readFileSync } = await import("node:fs");
328
386
  const lockPath = join(stateDir, "daemon.lock");
329
387
  let payload;