@dmsdc-ai/aigentry-telepty 0.1.96 → 0.1.98

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.
@@ -7,6 +7,7 @@ const path = require('path');
7
7
 
8
8
  const LOCK_POLL_MS = 10;
9
9
  const LOCK_TIMEOUT_MS = 500;
10
+ const DEFAULT_STALE_LOCK_AGE_MS = 60000; // 60s — lock hold time is ~5ms, so 60s is definitionally stale
10
11
 
11
12
  function isProcessAlive(pid) {
12
13
  try {
@@ -20,10 +21,15 @@ function isProcessAlive(pid) {
20
21
  /**
21
22
  * Acquire an advisory lock for a session directory.
22
23
  * Returns a release function. Throws on timeout.
24
+ *
25
+ * @param {string} sessionDir
26
+ * @param {Object} [options]
27
+ * @param {number} [options.staleLockAgeMs] — break locks older than this (default 60s)
23
28
  */
24
- function acquireLock(sessionDir) {
29
+ function acquireLock(sessionDir, options = {}) {
25
30
  const lockPath = path.join(sessionDir, '.lock');
26
31
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
32
+ const staleLockAgeMs = options.staleLockAgeMs || DEFAULT_STALE_LOCK_AGE_MS;
27
33
 
28
34
  while (Date.now() < deadline) {
29
35
  try {
@@ -36,12 +42,28 @@ function acquireLock(sessionDir) {
36
42
  } catch (err) {
37
43
  if (err.code !== 'EEXIST') throw err;
38
44
 
39
- // Lock file exists — check for stale PID
45
+ // Lock file exists — check age first, then PID
46
+
47
+ // Fix 2: Lock age threshold — if lock is older than staleLockAgeMs,
48
+ // break regardless of PID (handles PID recycling)
49
+ try {
50
+ const stat = fs.statSync(lockPath);
51
+ const ageMs = Date.now() - stat.mtimeMs;
52
+ if (ageMs > staleLockAgeMs) {
53
+ try { fs.unlinkSync(lockPath); } catch {}
54
+ continue;
55
+ }
56
+ } catch {
57
+ // stat failed — file may have been removed between EEXIST and stat
58
+ continue;
59
+ }
60
+
61
+ // Fix 1: Invalid PID handling — treat NaN, 0, negative, empty as stale
40
62
  try {
41
63
  const content = fs.readFileSync(lockPath, 'utf8').trim();
42
64
  const pid = Number(content);
43
- if (pid > 0 && !isProcessAlive(pid)) {
44
- // Stale lock remove and retry
65
+ if (!Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid)) {
66
+ // Invalid PID (empty, NaN, 0, negative) OR dead PID → stale lock
45
67
  try { fs.unlinkSync(lockPath); } catch {}
46
68
  continue;
47
69
  }
@@ -51,7 +73,7 @@ function acquireLock(sessionDir) {
51
73
  continue;
52
74
  }
53
75
 
54
- // Lock is held by a live process — wait
76
+ // Lock is held by a live process with a recent lock — wait
55
77
  const buffer = new SharedArrayBuffer(4);
56
78
  const view = new Int32Array(buffer);
57
79
  Atomics.wait(view, 0, 0, LOCK_POLL_MS);
@@ -61,6 +83,61 @@ function acquireLock(sessionDir) {
61
83
  throw new Error(`Mailbox lock timeout for ${sessionDir}`);
62
84
  }
63
85
 
86
+ /**
87
+ * Break stale lock files across all session directories (startup sweep).
88
+ * Returns count of broken locks.
89
+ *
90
+ * @param {string} root — mailbox root directory
91
+ * @param {Object} [options]
92
+ * @param {number} [options.staleLockAgeMs] — age threshold (default 60s)
93
+ */
94
+ function breakStaleLocks(root, options = {}) {
95
+ const staleLockAgeMs = options.staleLockAgeMs || DEFAULT_STALE_LOCK_AGE_MS;
96
+ const dirs = listSessionDirs(root);
97
+ let broken = 0;
98
+
99
+ for (const { sessionId, dir } of dirs) {
100
+ const lockPath = path.join(dir, '.lock');
101
+ if (!fs.existsSync(lockPath)) continue;
102
+
103
+ let shouldBreak = false;
104
+ let reason = '';
105
+
106
+ try {
107
+ const stat = fs.statSync(lockPath);
108
+ const ageMs = Date.now() - stat.mtimeMs;
109
+
110
+ if (ageMs > staleLockAgeMs) {
111
+ shouldBreak = true;
112
+ reason = `age ${Math.round(ageMs / 1000)}s > ${Math.round(staleLockAgeMs / 1000)}s threshold`;
113
+ } else {
114
+ // Check PID validity
115
+ const content = fs.readFileSync(lockPath, 'utf8').trim();
116
+ const pid = Number(content);
117
+ if (!Number.isFinite(pid) || pid <= 0) {
118
+ shouldBreak = true;
119
+ reason = `invalid PID: ${JSON.stringify(content)}`;
120
+ } else if (!isProcessAlive(pid)) {
121
+ shouldBreak = true;
122
+ reason = `dead PID ${pid}`;
123
+ }
124
+ }
125
+ } catch {
126
+ // Can't read/stat lock — treat as stale
127
+ shouldBreak = true;
128
+ reason = 'unreadable lock file';
129
+ }
130
+
131
+ if (shouldBreak) {
132
+ try { fs.unlinkSync(lockPath); } catch {}
133
+ console.log(`[MAILBOX] Broke stale lock for ${sessionId}: ${reason}`);
134
+ broken++;
135
+ }
136
+ }
137
+
138
+ return broken;
139
+ }
140
+
64
141
  // --- JSONL read/write ---
65
142
 
66
143
  function readJsonl(filePath) {
@@ -172,6 +249,7 @@ function compact(sessionDir, threshold) {
172
249
 
173
250
  module.exports = {
174
251
  acquireLock,
252
+ breakStaleLocks,
175
253
  readJsonl,
176
254
  appendJsonl,
177
255
  writeJsonl,
@@ -182,4 +260,5 @@ module.exports = {
182
260
  loadMessages,
183
261
  countPending,
184
262
  compact,
263
+ isProcessAlive,
185
264
  };