quilltap 4.10.0-dev.22 → 4.10.0-dev.8

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.
Files changed (2) hide show
  1. package/bin/quilltap.js +48 -55
  2. package/package.json +1 -1
package/bin/quilltap.js CHANGED
@@ -484,39 +484,6 @@ function verifyPidIsNode(pid, expectedArgv0) {
484
484
  }
485
485
  }
486
486
 
487
- const HEARTBEAT_FRESH_MS = 5 * 60 * 1000;
488
-
489
- /**
490
- * Decide whether a lock record belongs to a live process.
491
- *
492
- * A hostname that differs from ours does NOT mean a different machine: macOS
493
- * derives gethostname() dynamically when `scutil --get HostName` is unset, so
494
- * one Mac reports e.g. "MacBook-Pro.local" and "Mac" at different times. So
495
- * check PID liveness regardless of the recorded name, and fall back to
496
- * heartbeat freshness for any environment rather than only for containers.
497
- * See bug 126.
498
- */
499
- function assessLock(lock, hostname) {
500
- const sameHost = lock.hostname === hostname;
501
- const alive = isPidAlive(lock.pid);
502
- const isNode = alive ? verifyPidIsNode(lock.pid, lock.processArgv0 || '') : false;
503
- const heartbeatAgeMs = lock.lastHeartbeat
504
- ? Date.now() - new Date(lock.lastHeartbeat).getTime()
505
- : Infinity;
506
- const heartbeatFresh = heartbeatAgeMs < HEARTBEAT_FRESH_MS;
507
-
508
- return {
509
- sameHost,
510
- alive,
511
- isNode,
512
- heartbeatAgeMs,
513
- heartbeatFresh,
514
- // Held by something running: a confirmed local process, or any holder
515
- // still refreshing its heartbeat.
516
- live: (alive && isNode) || heartbeatFresh,
517
- };
518
- }
519
-
520
487
  /**
521
488
  * Handle --lock-status, --lock-clean, --lock-override commands.
522
489
  */
@@ -551,20 +518,32 @@ function handleLockCommand(dataDir, opts) {
551
518
  return;
552
519
  }
553
520
 
554
- const { sameHost, alive, isNode, heartbeatAgeMs, heartbeatFresh } =
555
- assessLock(lock, hostname);
521
+ const sameHost = lock.hostname === hostname;
522
+ const alive = sameHost && isPidAlive(lock.pid);
523
+ const isNode = alive ? verifyPidIsNode(lock.pid, lock.processArgv0 || '') : false;
556
524
 
557
525
  // Status line
558
526
  let status;
559
527
  if (alive && isNode) {
560
528
  status = '\x1b[32mACTIVE\x1b[0m (process confirmed running)';
561
- } else if (heartbeatFresh) {
562
- const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
563
- status = `\x1b[32mACTIVE\x1b[0m (${lock.environment || 'unknown'}, heartbeat ${ageStr} ago)`;
564
529
  } else if (alive && !isNode) {
565
530
  status = '\x1b[33mSUSPECT\x1b[0m (PID alive but does not look like Quilltap — possible PID reuse)';
566
531
  } else if (!sameHost) {
567
- status = `\x1b[33mSTALE (${lock.environment || 'unknown'} on ${lock.hostname}, no recent heartbeat)\x1b[0m — will be auto-claimed on next startup`;
532
+ // Different hostname — could be a container on this machine
533
+ const isVMOrContainer = lock.environment === 'docker';
534
+ const heartbeatAgeMs = lock.lastHeartbeat
535
+ ? Date.now() - new Date(lock.lastHeartbeat).getTime()
536
+ : Infinity;
537
+ const heartbeatFreshMs = 5 * 60 * 1000;
538
+
539
+ if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
540
+ const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
541
+ status = `\x1b[32mACTIVE (${lock.environment}, heartbeat ${ageStr} ago)\x1b[0m`;
542
+ } else if (isVMOrContainer) {
543
+ status = `\x1b[33mSTALE (${lock.environment}, no recent heartbeat)\x1b[0m — will be auto-claimed on next startup`;
544
+ } else {
545
+ status = '\x1b[33mSTALE (different host)\x1b[0m — will be auto-claimed on next startup';
546
+ }
568
547
  } else {
569
548
  status = '\x1b[31mSTALE (process dead)\x1b[0m — will be auto-claimed on next startup';
570
549
  }
@@ -572,7 +551,7 @@ function handleLockCommand(dataDir, opts) {
572
551
  console.log(`Instance Lock Status: ${status}`);
573
552
  console.log();
574
553
  console.log(` PID: ${lock.pid}`);
575
- console.log(` Hostname: ${lock.hostname}${sameHost ? ' (this host)' : ` (recorded name differs from ours: ${hostname})`}`);
554
+ console.log(` Hostname: ${lock.hostname}${sameHost ? ' (this host)' : ' (different host)'}`);
576
555
  console.log(` Environment: ${lock.environment || 'unknown'}`);
577
556
  console.log(` Process: ${lock.processTitle || 'unknown'}`);
578
557
  console.log(` Started: ${lock.startedAt || 'unknown'}`);
@@ -620,23 +599,37 @@ function handleLockCommand(dataDir, opts) {
620
599
  return;
621
600
  }
622
601
 
623
- const { sameHost, alive, isNode, heartbeatAgeMs, heartbeatFresh } =
624
- assessLock(lock, hostname);
602
+ const sameHost = lock.hostname === hostname;
603
+ const alive = sameHost && isPidAlive(lock.pid);
625
604
 
626
- if (alive && isNode) {
627
- console.log(`Lock is held by a live Quilltap process (PID ${lock.pid}). Cannot clean.`);
628
- console.log('Stop the running instance first, or use --lock-override to force.');
629
- process.exit(1);
630
- } else if (heartbeatFresh) {
631
- const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
632
- console.log(`Lock is still being refreshed (heartbeat ${ageStr} ago) — its holder is alive. Cannot clean.`);
633
- console.log('Stop the running instance first, or use --lock-override to force.');
634
- process.exit(1);
635
- } else if (alive && !isNode) {
636
- console.log(`Lock references PID ${lock.pid} which is alive but does NOT look like a Quilltap process.`);
637
- console.log('This is likely a stale lock with a reused PID. Removing.');
605
+ if (alive) {
606
+ const isNode = verifyPidIsNode(lock.pid, lock.processArgv0 || '');
607
+ if (isNode) {
608
+ console.log(`Lock is held by a live Quilltap process (PID ${lock.pid}). Cannot clean.`);
609
+ console.log('Stop the running instance first, or use --lock-override to force.');
610
+ process.exit(1);
611
+ } else {
612
+ console.log(`Lock references PID ${lock.pid} which is alive but does NOT look like a Quilltap process.`);
613
+ console.log('This is likely a stale lock with a reused PID. Removing.');
614
+ }
638
615
  } else if (!sameHost) {
639
- console.log(`Lock was held by ${lock.environment || 'unknown'} on ${lock.hostname} with no recent heartbeat. Removing stale lock.`);
616
+ // Different hostname — check if it's a container with a recent heartbeat
617
+ const isVMOrContainer = lock.environment === 'docker';
618
+ const heartbeatAgeMs = lock.lastHeartbeat
619
+ ? Date.now() - new Date(lock.lastHeartbeat).getTime()
620
+ : Infinity;
621
+ const heartbeatFreshMs = 5 * 60 * 1000;
622
+
623
+ if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
624
+ const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
625
+ console.log(`Lock is held by a live ${lock.environment} instance (heartbeat ${ageStr} ago). Cannot clean.`);
626
+ console.log('Stop the other instance first, or use --lock-override to force.');
627
+ process.exit(1);
628
+ } else if (isVMOrContainer) {
629
+ console.log(`Lock was held by ${lock.environment} (${lock.hostname}) with no recent heartbeat. Removing stale lock.`);
630
+ } else {
631
+ console.log(`Lock was held by a different host (${lock.hostname}). Removing stale lock.`);
632
+ }
640
633
  } else {
641
634
  console.log(`Lock was held by PID ${lock.pid} which is no longer running. Removing stale lock.`);
642
635
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.10.0-dev.22",
3
+ "version": "4.10.0-dev.8",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",