@lifeaitools/rdc-skills 0.25.9 → 0.25.10
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/git-sha.json +1 -1
- package/hooks/check-rdc-environment.js +61 -1
- package/hooks/lib/box-lock.js +24 -3
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +1489 -1474
package/git-sha.json
CHANGED
|
@@ -137,6 +137,44 @@ function processesHoldingPackage() {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
const stoppedStampPath = path.join(os.tmpdir(), 'rdc-skills-stopped-holders.json');
|
|
141
|
+
|
|
142
|
+
function writeStoppedStamp(names) {
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(stoppedStampPath, JSON.stringify({ names, ts: new Date().toISOString() }), 'utf8');
|
|
145
|
+
} catch { /* best effort — the finally-restart still covers the normal path */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clearStoppedStamp() {
|
|
149
|
+
try { fs.unlinkSync(stoppedStampPath); } catch { /* nothing to clear */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Restart anything a PREVIOUS run stopped but never restarted.
|
|
154
|
+
*
|
|
155
|
+
* The only way a holder stays stopped is if that run died between `pm2 stop` and
|
|
156
|
+
* `pm2 restart` — a harness timeout kill, which neither `finally` nor
|
|
157
|
+
* process.on('exit') catches. Leaving the MCP down is strictly worse than the
|
|
158
|
+
* unhealthy state being repaired, so every run repairs the previous run's crash
|
|
159
|
+
* before doing anything else.
|
|
160
|
+
*/
|
|
161
|
+
function restartStrandedHolders() {
|
|
162
|
+
let names = [];
|
|
163
|
+
try {
|
|
164
|
+
names = JSON.parse(fs.readFileSync(stoppedStampPath, 'utf8')).names || [];
|
|
165
|
+
} catch {
|
|
166
|
+
return; // no stamp — nothing was left stopped
|
|
167
|
+
}
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
if (!SAFE_PM2_NAME.test(name)) continue;
|
|
170
|
+
try {
|
|
171
|
+
shell(`pm2 restart ${q(name)}`, { timeout: 30000 });
|
|
172
|
+
hookLog('check-rdc-environment', 'SessionStart', 'restarted-stranded-holder', { name });
|
|
173
|
+
} catch { /* reported by the health check below */ }
|
|
174
|
+
}
|
|
175
|
+
clearStoppedStamp();
|
|
176
|
+
}
|
|
177
|
+
|
|
140
178
|
function repair(reason) {
|
|
141
179
|
hookLog('check-rdc-environment', 'SessionStart', 'repair', { reason });
|
|
142
180
|
|
|
@@ -154,6 +192,13 @@ function repair(reason) {
|
|
|
154
192
|
hookLog('check-rdc-environment', 'SessionStart', 'skipped-unsafe-pm2-name', { name });
|
|
155
193
|
return false;
|
|
156
194
|
});
|
|
195
|
+
// Record what we are about to stop BEFORE stopping it. `finally` covers an
|
|
196
|
+
// exception but NOT the harness killing this hook at its timeout — and
|
|
197
|
+
// process.on('exit') does not fire for a SIGKILL either. Without this stamp a
|
|
198
|
+
// timeout kill between `pm2 stop` and `pm2 restart` leaves the MCP STOPPED with
|
|
199
|
+
// nothing that knows to bring it back. restartStrandedHolders() (called at hook
|
|
200
|
+
// entry) is the crash-safe half of this pair.
|
|
201
|
+
writeStoppedStamp(holders);
|
|
157
202
|
for (const name of holders) {
|
|
158
203
|
try {
|
|
159
204
|
shell(`pm2 stop ${q(name)}`, { timeout: 30000 });
|
|
@@ -181,6 +226,9 @@ function repair(reason) {
|
|
|
181
226
|
/* surfaced by the health re-check below */
|
|
182
227
|
}
|
|
183
228
|
}
|
|
229
|
+
// Restarted (or tried to) — the stamp has served its purpose. Clearing it here
|
|
230
|
+
// means a stamp that SURVIVES is unambiguous evidence of a killed run.
|
|
231
|
+
clearStoppedStamp();
|
|
184
232
|
}
|
|
185
233
|
}
|
|
186
234
|
|
|
@@ -244,6 +292,10 @@ function block(message, details = {}) {
|
|
|
244
292
|
}
|
|
245
293
|
|
|
246
294
|
async function main() {
|
|
295
|
+
// Repair a PREVIOUS run's crash first: if it was killed between `pm2 stop` and
|
|
296
|
+
// `pm2 restart`, the MCP is still down and no health check can fix that.
|
|
297
|
+
restartStrandedHolders();
|
|
298
|
+
|
|
247
299
|
const initialPkg = globalPackageJson();
|
|
248
300
|
const initialHealth = await health();
|
|
249
301
|
const reasons = [];
|
|
@@ -315,4 +367,12 @@ async function main() {
|
|
|
315
367
|
process.exit(0);
|
|
316
368
|
}
|
|
317
369
|
|
|
318
|
-
|
|
370
|
+
// Run ONLY when git/Claude invokes this file directly. Without the guard, merely
|
|
371
|
+
// require()-ing it executes the whole hook — which is what the installer's own
|
|
372
|
+
// load-check did, triggering a REAL `npm install -g` + pm2 stop/restart during the
|
|
373
|
+
// install, and on an unhealthy box a recursive install→repair→install loop.
|
|
374
|
+
if (require.main === module) {
|
|
375
|
+
main().catch((err) => block(`RDC skills environment check crashed: ${err.message}`));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = { main, repair, restartStrandedHolders };
|
package/hooks/lib/box-lock.js
CHANGED
|
@@ -96,8 +96,16 @@ function acquireBoxLock() {
|
|
|
96
96
|
// 0/12, 1/12, 2/12 double-leader rounds across runs, which is the worst kind
|
|
97
97
|
// of bug to ship: it passes CI and fails on a busy machine.
|
|
98
98
|
settle(SETTLE_MS);
|
|
99
|
+
// Distinguish "someone else took it" from "the read failed". Standing down on
|
|
100
|
+
// a transient read error can elect ZERO leaders — nobody repairs the box while
|
|
101
|
+
// every session waits and then hard-blocks. Only a genuine byte MISMATCH means
|
|
102
|
+
// another session leads.
|
|
99
103
|
let observed = null;
|
|
100
|
-
|
|
104
|
+
let readOk = false;
|
|
105
|
+
for (let r = 0; r < 2 && !readOk; r++) {
|
|
106
|
+
try { observed = fs.readFileSync(LOCK_PATH, 'utf8'); readOk = true; } catch { settle(20); }
|
|
107
|
+
}
|
|
108
|
+
if (!readOk) continue; // could not verify — retry the create
|
|
101
109
|
if (observed !== body) return false; // someone reclaimed it; they lead
|
|
102
110
|
|
|
103
111
|
lockHeld = true;
|
|
@@ -145,8 +153,21 @@ function acquireBoxLock() {
|
|
|
145
153
|
let claimRaw = null;
|
|
146
154
|
try { claimRaw = fs.readFileSync(claim, 'utf8'); } catch { claimRaw = null; }
|
|
147
155
|
if (claimRaw !== heldRaw) {
|
|
148
|
-
// Not ours to take —
|
|
149
|
-
|
|
156
|
+
// Not ours to take — DISCARD the claim and stand down.
|
|
157
|
+
//
|
|
158
|
+
// Do NOT rename it back. renameSync silently OVERWRITES an existing
|
|
159
|
+
// destination, and the bytes we hold are the STALE ones (that is why we
|
|
160
|
+
// judged them reclaimable). Restoring them clobbers the live leader's
|
|
161
|
+
// fresh lock with a stale body, so the next session judges it
|
|
162
|
+
// reclaimable and becomes a SECOND LEADER while the true leader is
|
|
163
|
+
// mid `npm install -g` — the exact race this module exists to prevent,
|
|
164
|
+
// arriving through the restore path. Measured, and invisible to the
|
|
165
|
+
// concurrency probe because it seeds a dead-PID lock, so claimRaw
|
|
166
|
+
// always equals heldRaw and this branch never runs there.
|
|
167
|
+
//
|
|
168
|
+
// Whoever owns the path already owns the box. Losing the stale bytes
|
|
169
|
+
// costs nothing; putting them back can only mislead.
|
|
170
|
+
try { fs.unlinkSync(claim); } catch { /* already gone */ }
|
|
150
171
|
return false;
|
|
151
172
|
}
|
|
152
173
|
}
|