claude-mem-lite 6.25.2 → 6.26.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/deep-search.mjs +18 -1
- package/haiku-client.mjs +96 -27
- package/hook-llm.mjs +59 -44
- package/hook-update.mjs +39 -6
- package/install.mjs +211 -33
- package/lib/binding-probe.mjs +9 -1
- package/lib/native-binding-hint.mjs +3 -1
- package/lib/proc-lock.mjs +136 -13
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/binding-probe-cli.mjs +5 -2
- package/scripts/hook-launcher.mjs +1 -3
- package/scripts/launch.mjs +13 -3
package/install.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
lstatSync,
|
|
19
19
|
constants as fsConstants,
|
|
20
20
|
} from 'fs';
|
|
21
|
-
import { join, resolve, dirname, basename, sep } from 'path';
|
|
21
|
+
import { join, resolve, dirname, basename, sep, relative, isAbsolute } from 'path';
|
|
22
22
|
import { homedir, tmpdir } from 'os';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
@@ -93,11 +93,25 @@ import {
|
|
|
93
93
|
ensureBetterSqlite3Working,
|
|
94
94
|
nativeBindingRepairHint,
|
|
95
95
|
isNativeBindingError,
|
|
96
|
+
probeBindingInFreshProcess,
|
|
97
|
+
flattenBindingError,
|
|
96
98
|
} from './lib/binding-probe.mjs';
|
|
97
99
|
import { detectInstallShape, probeRuntimeRoots, hasAnyManagedCode } from './lib/install-shape.mjs';
|
|
98
100
|
import { probeSchemaCompat, schemaSkewRemedy } from './lib/schema-skew.mjs';
|
|
99
101
|
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
100
|
-
|
|
102
|
+
// A namespace, not named imports: a swap renames files one at a time, so for a moment this file can
|
|
103
|
+
// sit over the previous release's lib/proc-lock.mjs, and a named import of an export that release
|
|
104
|
+
// lacks is a SyntaxError that stops install.mjs loading — and repair and cleanup are what finish that
|
|
105
|
+
// state (pre-ship delta review P2-1). Exports newer than v6.25.2 are detected.
|
|
106
|
+
import * as procLock from './lib/proc-lock.mjs';
|
|
107
|
+
const { lockHeld } = procLock;
|
|
108
|
+
const lockDirBlocked = procLock.lockDirBlocked ?? (() => null);
|
|
109
|
+
const takeLock =
|
|
110
|
+
procLock.takeLock ??
|
|
111
|
+
((p) => {
|
|
112
|
+
const release = procLock.acquireLock(p);
|
|
113
|
+
return release ? { release } : {};
|
|
114
|
+
});
|
|
101
115
|
import { atomicWriteFileSync, atomicCopyFileSync } from './lib/atomic-write.mjs';
|
|
102
116
|
import { shellWord } from './cli-path.mjs';
|
|
103
117
|
import { dirAccessError, walOpenBlocked, dataDirRemedy, WAL_BLOCKED_WHY } from './lib/wal-open-blocked.mjs';
|
|
@@ -1873,6 +1887,7 @@ async function status() {
|
|
|
1873
1887
|
const unwritable = dataDirDenied
|
|
1874
1888
|
? ` — ${MEM_DATA_DIR} can be read but not written (${dataDirDenied}). Fix: ${dataDirAccessRemedy(dataDirDenied)}`
|
|
1875
1889
|
: '';
|
|
1890
|
+
let statusProbe;
|
|
1876
1891
|
if (dataDirUnreadable) {
|
|
1877
1892
|
push(
|
|
1878
1893
|
'fail',
|
|
@@ -1880,6 +1895,17 @@ async function status() {
|
|
|
1880
1895
|
`Database: ${MEM_DATA_DIR} is not accessible (${dataDirUnreadable}) — Fix: ${dataDirAccessRemedy(dataDirUnreadable)}`,
|
|
1881
1896
|
{ exists: null, error: dataDirUnreadable },
|
|
1882
1897
|
);
|
|
1898
|
+
} else if (existsSync(DB_PATH) && !(statusProbe = probeBindingInFreshProcess(PROJECT_DIR)).ok) {
|
|
1899
|
+
// Out of process first, as doctor does: the open below loads the addon IN this process, and a
|
|
1900
|
+
// truncated prebuild raises SIGBUS in dlopen rather than throwing, so status died with no
|
|
1901
|
+
// output (D#306). The human line says why it did not look; the machine face keeps the loader's
|
|
1902
|
+
// own error (review P3-10).
|
|
1903
|
+
push(
|
|
1904
|
+
dataDirDenied ? 'fail' : 'warn',
|
|
1905
|
+
'database',
|
|
1906
|
+
`Database: exists, but not checked — ${BINDING_MISSING_WHY}${unwritable}`,
|
|
1907
|
+
{ exists: true, error: flattenBindingError(statusProbe.error) },
|
|
1908
|
+
);
|
|
1883
1909
|
} else if (existsSync(DB_PATH)) {
|
|
1884
1910
|
try {
|
|
1885
1911
|
const Database = (await import('better-sqlite3')).default;
|
|
@@ -2173,6 +2199,11 @@ async function doctor() {
|
|
|
2173
2199
|
// process.
|
|
2174
2200
|
const rootProbes = probeRuntimeRoots(shape.runtimeRoots);
|
|
2175
2201
|
const brokenRoots = rootProbes.filter((r) => !r.ok);
|
|
2202
|
+
// Database and DB stats below open the store IN this process, through the better-sqlite3 the
|
|
2203
|
+
// running CLI's tree resolves. When the out-of-process probe of that tree has already failed,
|
|
2204
|
+
// loading it here is not just pointless: a truncated prebuild raises SIGBUS in dlopen, which
|
|
2205
|
+
// killed doctor (exit 135) before it printed the rest (D#306). They say "not checked" instead.
|
|
2206
|
+
const bindingBrokenHere = rootProbes.some((r) => !r.ok && sameDir(r.root, PROJECT_DIR));
|
|
2176
2207
|
if (rootProbes.length === 0 && codeDirDenied) {
|
|
2177
2208
|
// The code home is locked, so it may own a binding nobody could look at (D#284 review P3-4).
|
|
2178
2209
|
notCheckedDenied('better-sqlite3', INSTALL_DIR);
|
|
@@ -2604,6 +2635,8 @@ async function doctor() {
|
|
|
2604
2635
|
// Database
|
|
2605
2636
|
if (dataDirUnreadable) {
|
|
2606
2637
|
notCheckedDenied('Database');
|
|
2638
|
+
} else if (existsSync(DB_PATH) && bindingBrokenHere) {
|
|
2639
|
+
dwarn(`Database: not checked — ${BINDING_MISSING_WHY}`);
|
|
2607
2640
|
} else if (existsSync(DB_PATH)) {
|
|
2608
2641
|
try {
|
|
2609
2642
|
const Database = (await import('better-sqlite3')).default;
|
|
@@ -3028,8 +3061,34 @@ async function doctor() {
|
|
|
3028
3061
|
// install.lock is held, cleanup skips update residue (a running update's own staging and backup
|
|
3029
3062
|
// dirs among it), so doctor does not count it either, and says it did not look (D#297). The holder
|
|
3030
3063
|
// may be any installer: an update, install, repair, a binding rebuild. It asks without taking it.
|
|
3064
|
+
// A runtime dir install.lock cannot be created in (a sudo run left it root-owned, a read-only data
|
|
3065
|
+
// dir): every update entry skips on it as if a peer held the lock, silently, so this is the only
|
|
3066
|
+
// face that can say why updates stopped (D#307). Counted here unless the Data directory line above
|
|
3067
|
+
// already counts the directory it sits in.
|
|
3068
|
+
const installLockBlocked = lockDirBlocked(INSTALL_LOCK_PATH);
|
|
3069
|
+
if (installLockBlocked) {
|
|
3070
|
+
const line = `Install lock: ${lockBlockedWhy(installLockBlocked)} — every update, install, repair and binding rebuild skips until it can be taken`;
|
|
3071
|
+
// The same directory the Data directory line counts (or one above it), or the same read-only
|
|
3072
|
+
// mount: one fault, one ✗. A runtime dir inside a data dir that both need fixing is two.
|
|
3073
|
+
const relToData = relative(MEM_DATA_DIR, installLockBlocked.dir);
|
|
3074
|
+
const sameFault =
|
|
3075
|
+
dataDirDenied &&
|
|
3076
|
+
(relToData === '' ||
|
|
3077
|
+
relToData.startsWith('..') ||
|
|
3078
|
+
isAbsolute(relToData) ||
|
|
3079
|
+
(dataDirDenied === 'EROFS' && installLockBlocked.code === 'EROFS'));
|
|
3080
|
+
if (sameFault) dwarn(line);
|
|
3081
|
+
else {
|
|
3082
|
+
fail(line);
|
|
3083
|
+
issues++;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3031
3086
|
try {
|
|
3032
|
-
|
|
3087
|
+
// Held and blocked are separate answers. A lock can be both: a sudo-run installer still running
|
|
3088
|
+
// in the root-owned runtime dir it made holds it there (pre-ship review). Held, the residue may
|
|
3089
|
+
// be that installer's own and is not counted; blocked and not held, an unfinished swap is a real
|
|
3090
|
+
// one and is counted (D#289), and every repair it names waits on the lock line's fix.
|
|
3091
|
+
const lockHeldNow = lockHeld(INSTALL_LOCK_PATH);
|
|
3033
3092
|
const { stale, inFlight, unfinishedSwaps, unreadableJournals, notChecked } = scanStaleTempFiles({
|
|
3034
3093
|
codeDir: INSTALL_DIR,
|
|
3035
3094
|
runtimeDir: MEM_RUNTIME_DIR,
|
|
@@ -3037,14 +3096,16 @@ async function doctor() {
|
|
|
3037
3096
|
installLockHeld: lockHeldNow,
|
|
3038
3097
|
});
|
|
3039
3098
|
const installer = `node ${shellWord(join(PROJECT_DIR, 'install.mjs'))}`;
|
|
3099
|
+
const onceLock =
|
|
3100
|
+
installLockBlocked && !lockHeldNow ? ' once install.lock can be taken (see Install lock)' : '';
|
|
3040
3101
|
if (unfinishedSwaps > 0) {
|
|
3041
3102
|
dwarn(
|
|
3042
|
-
`Unfinished update: ${unfinishedSwaps} backup dir(s) in ${INSTALL_DIR} still hold a swap journal — an update was interrupted, or could not put back every file it moved aside (run: ${installer} cleanup — it finishes them newest first, and stops at one it cannot read or put back)`,
|
|
3103
|
+
`Unfinished update: ${unfinishedSwaps} backup dir(s) in ${INSTALL_DIR} still hold a swap journal — an update was interrupted, or could not put back every file it moved aside (run: ${installer} cleanup${onceLock} — it finishes them newest first, and stops at one it cannot read or put back)`,
|
|
3043
3104
|
);
|
|
3044
3105
|
}
|
|
3045
3106
|
if (unreadableJournals > 0) {
|
|
3046
3107
|
dwarn(
|
|
3047
|
-
`Unfinished update: ${unreadableJournals} backup dir(s) in ${INSTALL_DIR} whose journal cannot be read, so nothing can finish that update; they hold the files it moved aside (run: ${installer} repair — it reinstalls over them; cleanup then removes them)`,
|
|
3108
|
+
`Unfinished update: ${unreadableJournals} backup dir(s) in ${INSTALL_DIR} whose journal cannot be read, so nothing can finish that update; they hold the files it moved aside (run: ${installer} repair${onceLock} — it reinstalls over them; cleanup then removes them)`,
|
|
3048
3109
|
);
|
|
3049
3110
|
}
|
|
3050
3111
|
const skipped = lockHeldNow
|
|
@@ -3054,11 +3115,13 @@ async function doctor() {
|
|
|
3054
3115
|
const where = notChecked.map((n) => `${n.dir} is not accessible (${n.code})`).join('; ');
|
|
3055
3116
|
dwarn(
|
|
3056
3117
|
`Stale temp files: not checked — ${where}` +
|
|
3057
|
-
(stale > 0 ? `; ${stale} found elsewhere (run: ${installer} cleanup)` : '') +
|
|
3118
|
+
(stale > 0 ? `; ${stale} found elsewhere (run: ${installer} cleanup${onceLock})` : '') +
|
|
3058
3119
|
(skipped ? `; ${skipped}` : ''),
|
|
3059
3120
|
);
|
|
3060
3121
|
} else if (stale > 0) {
|
|
3061
|
-
dwarn(
|
|
3122
|
+
dwarn(
|
|
3123
|
+
`Stale temp files: ${stale} found (run: ${installer} cleanup${onceLock})${skipped ? `; ${skipped}` : ''}`,
|
|
3124
|
+
);
|
|
3062
3125
|
} else if (skipped) {
|
|
3063
3126
|
dwarn(`Stale temp files: ${skipped}`);
|
|
3064
3127
|
} else {
|
|
@@ -3081,7 +3144,9 @@ async function doctor() {
|
|
|
3081
3144
|
}
|
|
3082
3145
|
|
|
3083
3146
|
// DB stats
|
|
3084
|
-
if (existsSync(DB_PATH)) {
|
|
3147
|
+
if (existsSync(DB_PATH) && bindingBrokenHere) {
|
|
3148
|
+
dwarn(`DB stats: not checked — ${BINDING_MISSING_WHY}`);
|
|
3149
|
+
} else if (existsSync(DB_PATH)) {
|
|
3085
3150
|
try {
|
|
3086
3151
|
const dbSize = statSync(DB_PATH).size;
|
|
3087
3152
|
const sizeMB = (dbSize / 1024 / 1024).toFixed(1);
|
|
@@ -3504,10 +3569,23 @@ async function cleanup() {
|
|
|
3504
3569
|
let finished = 0;
|
|
3505
3570
|
// --dry-run writes nothing, so it asks whether the lock is held instead of taking it: taking it
|
|
3506
3571
|
// created the data dir, its runtime dir and the lock on a machine with nothing installed (D#297).
|
|
3507
|
-
|
|
3508
|
-
|
|
3572
|
+
// A lock that cannot be taken at all is not one a peer holds (D#304): both runs name its directory.
|
|
3573
|
+
// The real run takes the answer from its own failed create (any errno); the dry run, which must not
|
|
3574
|
+
// create anything, predicts it from the directory (lockDirBlocked).
|
|
3575
|
+
let updateLock = null;
|
|
3576
|
+
let lockBlocked;
|
|
3577
|
+
if (dryRun) {
|
|
3578
|
+
lockBlocked = lockDirBlocked(INSTALL_LOCK_PATH);
|
|
3579
|
+
if (!lockBlocked && !lockHeld(INSTALL_LOCK_PATH)) updateLock = () => {};
|
|
3580
|
+
} else {
|
|
3581
|
+
const taken = takeLock(INSTALL_LOCK_PATH);
|
|
3582
|
+
updateLock = taken.release || null;
|
|
3583
|
+
lockBlocked = taken.error || null;
|
|
3584
|
+
}
|
|
3509
3585
|
if (!updateLock) {
|
|
3510
|
-
warn(
|
|
3586
|
+
warn(
|
|
3587
|
+
`Update residue skipped: ${lockBlocked ? lockBlockedWhy(lockBlocked) : 'install in progress (install.lock held)'}`,
|
|
3588
|
+
);
|
|
3511
3589
|
} else {
|
|
3512
3590
|
try {
|
|
3513
3591
|
let found = [];
|
|
@@ -3517,8 +3595,29 @@ async function cleanup() {
|
|
|
3517
3595
|
warn(`Update residue: could not list ${INSTALL_DIR} (${e.code || e.message})`);
|
|
3518
3596
|
}
|
|
3519
3597
|
if (dryRun) {
|
|
3598
|
+
// Recovery undoes unfinished swaps like a stack, newest first, and stops at the first whose
|
|
3599
|
+
// journal cannot be read, leaving it and every older one (D#293). Judged one dir at a time,
|
|
3600
|
+
// the dry run promised to finish an older swap the real run leaves (D#307 F3), so it walks
|
|
3601
|
+
// them in recovery's own order, taken from hook-update rather than restated here.
|
|
3602
|
+
const swaps = found
|
|
3603
|
+
.filter((r) => r.kind === 'unfinished-swap' || r.kind === 'unreadable-journal')
|
|
3604
|
+
.map((r) => ({ name: r.f, readable: r.kind === 'unfinished-swap' }));
|
|
3605
|
+
let blockedBy = new Map();
|
|
3606
|
+
let cannotLoad = null;
|
|
3607
|
+
if (swaps.some((s) => s.readable)) {
|
|
3608
|
+
try {
|
|
3609
|
+
const { swapsBlockedByUnreadable } = await import('./hook-update.mjs');
|
|
3610
|
+
blockedBy = swapsBlockedByUnreadable(swaps);
|
|
3611
|
+
} catch (e) {
|
|
3612
|
+
cannotLoad = e.message;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3520
3615
|
for (const { f, kind } of found) {
|
|
3521
|
-
if (kind === 'unfinished-swap') {
|
|
3616
|
+
if (kind === 'unfinished-swap' && cannotLoad) {
|
|
3617
|
+
warn(`Would leave in place: ${f} (the updater that finishes it cannot load: ${cannotLoad})`);
|
|
3618
|
+
} else if (kind === 'unfinished-swap' && blockedBy.has(f)) {
|
|
3619
|
+
warn(`Would leave in place: ${f} (a newer one, ${blockedBy.get(f)}, cannot be finished first)`);
|
|
3620
|
+
} else if (kind === 'unfinished-swap') {
|
|
3522
3621
|
ok(`Would finish an interrupted update: ${f} (puts back the files its journal lists)`);
|
|
3523
3622
|
finished++;
|
|
3524
3623
|
} else if (kind === 'unreadable-journal') {
|
|
@@ -3547,21 +3646,29 @@ async function cleanup() {
|
|
|
3547
3646
|
}
|
|
3548
3647
|
// A relocated data dir that an updater older than v2.90.0 swapped into: its residue is stale,
|
|
3549
3648
|
// whatever it holds, and is never replayed (lib/doctor-stale-temp.mjs, legacyDir).
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3649
|
+
let legacyEntries = [];
|
|
3650
|
+
if (!sameDir(INSTALL_DIR, MEM_DATA_DIR)) {
|
|
3651
|
+
try {
|
|
3652
|
+
legacyEntries = readdirSync(MEM_DATA_DIR);
|
|
3653
|
+
} catch (e) {
|
|
3654
|
+
// Absent is nothing to clean; one it can enter but not list threw out of cleanup (review P3-6).
|
|
3655
|
+
if (e.code !== 'ENOENT')
|
|
3656
|
+
warn(`Update residue: could not list ${MEM_DATA_DIR} (${e.code || e.message})`);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
for (const f of legacyEntries) {
|
|
3660
|
+
if (!isUpdateResidue(f)) continue;
|
|
3661
|
+
if (dryRun) {
|
|
3662
|
+
ok(`Would remove: ${join(MEM_DATA_DIR, f)}`);
|
|
3663
|
+
removed++;
|
|
3664
|
+
continue;
|
|
3665
|
+
}
|
|
3666
|
+
try {
|
|
3667
|
+
rmSync(join(MEM_DATA_DIR, f), { recursive: true, force: true });
|
|
3668
|
+
ok(`Removed: ${join(MEM_DATA_DIR, f)}`);
|
|
3669
|
+
removed++;
|
|
3670
|
+
} catch (e) {
|
|
3671
|
+
warn(`Failed to remove ${join(MEM_DATA_DIR, f)}: ${e.message}`);
|
|
3565
3672
|
}
|
|
3566
3673
|
}
|
|
3567
3674
|
} finally {
|
|
@@ -3582,10 +3689,18 @@ async function cleanup() {
|
|
|
3582
3689
|
// fixture sweep below, whose comment already states the principle: a MANUAL cleanup is
|
|
3583
3690
|
// the conservative one.
|
|
3584
3691
|
const runtimeDir = MEM_RUNTIME_DIR;
|
|
3585
|
-
|
|
3692
|
+
let runtimeEntries = [];
|
|
3693
|
+
try {
|
|
3694
|
+
runtimeEntries = readdirSync(runtimeDir);
|
|
3695
|
+
} catch (e) {
|
|
3696
|
+
// Absent is nothing to clean. A runtime dir that cannot be listed (a sudo run left it 0700 and
|
|
3697
|
+
// root-owned) threw out of cleanup uncaught, dry run included (D#304).
|
|
3698
|
+
if (e.code !== 'ENOENT') warn(`Runtime scratch: could not list ${runtimeDir} (${e.code || e.message})`);
|
|
3699
|
+
}
|
|
3700
|
+
if (runtimeEntries.length > 0) {
|
|
3586
3701
|
const now = Date.now();
|
|
3587
3702
|
let inFlight = 0;
|
|
3588
|
-
for (const f of
|
|
3703
|
+
for (const f of runtimeEntries) {
|
|
3589
3704
|
if (isEpisodeResidue(f)) {
|
|
3590
3705
|
// The gate itself lives in lib/doctor-stale-temp.mjs, so doctor's count and this
|
|
3591
3706
|
// deletion cannot disagree about which files are in flight (D#53).
|
|
@@ -3625,6 +3740,10 @@ async function cleanup() {
|
|
|
3625
3740
|
if (removed > 0) done.push(`${removed} stale file(s) ${verb}`);
|
|
3626
3741
|
if (finished > 0)
|
|
3627
3742
|
done.push(`${finished} interrupted update(s) ${dryRun ? 'would be finished' : 'finished'}`);
|
|
3743
|
+
// Skipped update residue is not "nothing found" (review P3-4). Held, a peer is working on it; a
|
|
3744
|
+
// lock that cannot be taken is a failure no wait clears, so the exit says so, as install's does.
|
|
3745
|
+
if (!updateLock) done.push('update residue not checked (see above)');
|
|
3746
|
+
if (lockBlocked) process.exitCode = 1;
|
|
3628
3747
|
console.log(`\n ${done.length === 0 ? 'No stale files found.' : `${done.join('; ')}.`}\n`);
|
|
3629
3748
|
}
|
|
3630
3749
|
|
|
@@ -3635,7 +3754,8 @@ async function manualUpdate() {
|
|
|
3635
3754
|
console.log('\nclaude-mem-lite self-update\n');
|
|
3636
3755
|
|
|
3637
3756
|
// Force check by importing hook-update (bypasses throttle for manual use)
|
|
3638
|
-
const { checkForUpdate, getCurrentVersion, updateCheckDisabledReason } =
|
|
3757
|
+
const { checkForUpdate, getCurrentVersion, updateCheckDisabledReason, isPluginMode } =
|
|
3758
|
+
await import('./hook-update.mjs');
|
|
3639
3759
|
// checkForUpdate returns null without looking in these two cases, and the branch at the
|
|
3640
3760
|
// bottom would then report "Already up to date" for a check that never ran. (D#251)
|
|
3641
3761
|
const disabled = updateCheckDisabledReason();
|
|
@@ -3651,6 +3771,16 @@ async function manualUpdate() {
|
|
|
3651
3771
|
console.log('');
|
|
3652
3772
|
return;
|
|
3653
3773
|
}
|
|
3774
|
+
// An update installs under install.lock, and hook-update reads a lock it cannot take as "an install
|
|
3775
|
+
// is in progress", which this printed as "install failed" (review P3-5).
|
|
3776
|
+
// Plugin mode only checks, it never installs, so the lock is not its business (delta review P3-3).
|
|
3777
|
+
const lockBlocked = isPluginMode?.() ? null : lockDirBlocked(INSTALL_LOCK_PATH);
|
|
3778
|
+
if (lockBlocked) {
|
|
3779
|
+
warn(`Not checked: ${lockBlockedWhy(lockBlocked)}`);
|
|
3780
|
+
process.exitCode = 1;
|
|
3781
|
+
console.log('');
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3654
3784
|
log('Checking for updates...');
|
|
3655
3785
|
const result = await checkForUpdate({ force: true, allowInstall: true, reportFailure: true });
|
|
3656
3786
|
|
|
@@ -3687,6 +3817,15 @@ async function manualUpdate() {
|
|
|
3687
3817
|
// buggy on disk.
|
|
3688
3818
|
async function repair() {
|
|
3689
3819
|
console.log('\nclaude-mem-lite repair — re-syncing from the latest SIGNED GitHub release\n');
|
|
3820
|
+
// The install this ends in takes install.lock. One that cannot be taken here fails that install
|
|
3821
|
+
// after a whole release was downloaded, and the manual fallback below fails the same way, so say
|
|
3822
|
+
// it first and stop (review P3-5). A peer holding it is left to the install's own message.
|
|
3823
|
+
const lockBlocked = lockDirBlocked(INSTALL_LOCK_PATH);
|
|
3824
|
+
if (lockBlocked) {
|
|
3825
|
+
fail(`Repair not started: ${lockBlockedWhy(lockBlocked)}`);
|
|
3826
|
+
process.exitCode = 1;
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3690
3829
|
const stagingDir = mkdtempSync(join(tmpdir(), 'claude-mem-lite-repair-'));
|
|
3691
3830
|
try {
|
|
3692
3831
|
// Resolve the latest RELEASE (tag) and cryptographically VERIFY it before running any
|
|
@@ -3929,12 +4068,16 @@ function bindingHostDir() {
|
|
|
3929
4068
|
// two concurrent rebuilds can clobber the .node mid-compile. A live peer → report
|
|
3930
4069
|
// and exit 0 (it is doing this very work), never race it.
|
|
3931
4070
|
async function rebuildBinding() {
|
|
3932
|
-
const release
|
|
4071
|
+
const { release, error: blocked } = takeLock(INSTALL_LOCK_PATH);
|
|
3933
4072
|
if (!release) {
|
|
3934
4073
|
// NOT exit 0: skipping is not healing. Callers key their state on the exit
|
|
3935
4074
|
// code — a false success would let the launcher drop its cooldown and the
|
|
3936
4075
|
// CLI re-exec into the same broken binding.
|
|
3937
|
-
console.error(
|
|
4076
|
+
console.error(
|
|
4077
|
+
blocked
|
|
4078
|
+
? `[install] ${lockBlockedWhy(blocked)} — the binding was not rebuilt.`
|
|
4079
|
+
: '[install] Another install/repair is in progress — it owns the rebuild; skipping.',
|
|
4080
|
+
);
|
|
3938
4081
|
process.exitCode = 1;
|
|
3939
4082
|
return;
|
|
3940
4083
|
}
|
|
@@ -4011,6 +4154,35 @@ function sameDir(a, b) {
|
|
|
4011
4154
|
// The fix for a data dir this user cannot use, chosen by the error code (lib/wal-open-blocked.mjs).
|
|
4012
4155
|
export const dataDirAccessRemedy = (code) => dataDirRemedy(code, MEM_DATA_DIR);
|
|
4013
4156
|
|
|
4157
|
+
// install.lock serialises every installer (install, repair, update, binding rebuild, cleanup's
|
|
4158
|
+
// update residue). One that cannot be created at all is not one a peer holds, and each face says
|
|
4159
|
+
// which (D#304, D#307): waiting does not clear it, the fix below does.
|
|
4160
|
+
const INSTALL_LOCK_PATH = join(MEM_DATA_DIR, 'runtime', 'install.lock'); // runtime-dir:stays-put — install lock serialises real installers
|
|
4161
|
+
const lockBlockedWhy = ({ dir, code }) =>
|
|
4162
|
+
`install.lock cannot be taken — ${dir} cannot be written (${code}). Fix: ${lockDirRemedy(code, dir)}`;
|
|
4163
|
+
|
|
4164
|
+
// The fix for the directory lockDirBlocked / takeLock name. One ABOVE the data dir (it does not exist
|
|
4165
|
+
// yet and cannot be created there, or cannot be reached) is not this program's to chmod or chown: on
|
|
4166
|
+
// a CLAUDE_MEM_DIR under /opt that advice would hand /opt to the user (review P3-3).
|
|
4167
|
+
function lockDirRemedy(code, dir) {
|
|
4168
|
+
const rel = relative(MEM_DATA_DIR, dir);
|
|
4169
|
+
// Creating the data dir fixes only a parent that can be entered but not written; one that cannot be
|
|
4170
|
+
// entered (the data dir may already be the user's), or a link to nowhere, leaves CLAUDE_MEM_DIR
|
|
4171
|
+
// (delta review P3-1).
|
|
4172
|
+
const above = rel.startsWith('..') || isAbsolute(rel);
|
|
4173
|
+
if (above && (code === 'ENOENT' || dirAccessError(dir, fsConstants.X_OK)))
|
|
4174
|
+
return `${shellWord(dir)} cannot be entered or does not exist — set CLAUDE_MEM_DIR to a directory your user can write`;
|
|
4175
|
+
if (above)
|
|
4176
|
+
return `make ${shellWord(MEM_DATA_DIR)} a directory your user owns (e.g. sudo mkdir -p ${shellWord(MEM_DATA_DIR)} && sudo chown "$USER" ${shellWord(MEM_DATA_DIR)}), or set CLAUDE_MEM_DIR to one`;
|
|
4177
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') return dataDirRemedy(code, dir);
|
|
4178
|
+
if (code === 'ENOSPC' || code === 'EDQUOT')
|
|
4179
|
+
return `free space on the file system holding ${shellWord(dir)}`;
|
|
4180
|
+
if (code === 'ENOTDIR') return `${shellWord(dir)} is a file, not a directory — move it aside`;
|
|
4181
|
+
if (code === 'ENOENT')
|
|
4182
|
+
return `${shellWord(dir)} is a link to a directory that does not exist — create its target, or remove the link`;
|
|
4183
|
+
return `check that your user can create files in ${shellWord(dir)}`;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4014
4186
|
// This process cannot load better-sqlite3: none installed where it runs (a checkout, or a plugin
|
|
4015
4187
|
// cache before its first launch installed node_modules), or one that will not load (a stale ABI, a
|
|
4016
4188
|
// damaged prebuild). The better-sqlite3 line names and counts that; a line that would open the DB
|
|
@@ -4028,8 +4200,14 @@ async function runLockedInstall() {
|
|
|
4028
4200
|
process.exitCode = 1;
|
|
4029
4201
|
return;
|
|
4030
4202
|
}
|
|
4031
|
-
const release
|
|
4203
|
+
const { release, error: blocked } = takeLock(INSTALL_LOCK_PATH);
|
|
4032
4204
|
if (!release) {
|
|
4205
|
+
if (blocked) {
|
|
4206
|
+
// Not a peer: nothing will ever release it, and exit 0 told the caller it installed.
|
|
4207
|
+
console.error(`[install] ${lockBlockedWhy(blocked)} — nothing was done.`);
|
|
4208
|
+
process.exitCode = 1;
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4033
4211
|
console.log('[install] Another install/repair is in progress — skipping to avoid a torn write.');
|
|
4034
4212
|
return;
|
|
4035
4213
|
}
|
package/lib/binding-probe.mjs
CHANGED
|
@@ -76,10 +76,16 @@ export function nativeBindingRepairHint(root) {
|
|
|
76
76
|
// Set on a re-exec'd child so one failed heal cannot fork-bomb the CLI.
|
|
77
77
|
export const BINDING_HEAL_GUARD_ENV = 'CLAUDE_MEM_BINDING_HEALED';
|
|
78
78
|
|
|
79
|
-
// The native-binding fault family, in the
|
|
79
|
+
// The native-binding fault family, in the shapes it actually reaches callers:
|
|
80
80
|
// • ERR_DLOPEN_FAILED — Node's code for a failed dlopen (ABI mismatch)
|
|
81
81
|
// • NODE_MODULE_VERSION N vs M — the ABI text itself (some throws carry no code)
|
|
82
82
|
// • Could not locate the bindings file — build/Release missing or never compiled
|
|
83
|
+
// (better-sqlite3 ≤12, through the `bindings` package)
|
|
84
|
+
// • Cannot find module '…better_sqlite3.node' — the same fault on 13, which dropped
|
|
85
|
+
// `bindings`: with no prebuild for the platform and no source build, lib/binding.js
|
|
86
|
+
// requires build/Release/better_sqlite3.node and Node throws a plain MODULE_NOT_FOUND
|
|
87
|
+
// (D#306). Every first load on a platform 13 ships no prebuild for. Anchored on the
|
|
88
|
+
// addon's file name: a missing PACKAGE needs an install, not a rebuild.
|
|
83
89
|
// • Module did not self-register — the .node was REPLACED under a process that
|
|
84
90
|
// already dlopen'd the old one; only a fresh process recovers (hence the
|
|
85
91
|
// re-exec in healAndReexec, not an in-process retry)
|
|
@@ -88,6 +94,8 @@ export const BINDING_HEAL_GUARD_ENV = 'CLAUDE_MEM_BINDING_HEALED';
|
|
|
88
94
|
const NATIVE_BINDING_PATTERNS = [
|
|
89
95
|
/NODE_MODULE_VERSION/,
|
|
90
96
|
/Could not locate the bindings file/i,
|
|
97
|
+
// A quote may sit inside the path; the quoted path must end the line (Node's own shape).
|
|
98
|
+
/Cannot find module '[^\n]*[\\/]better_sqlite3\.node'(?:\r?\n|$)/,
|
|
91
99
|
/did not self-register/i,
|
|
92
100
|
/invalid ELF header/i,
|
|
93
101
|
];
|
|
@@ -135,9 +135,11 @@ export function formatHookError(err, event, { now = Date.now(), runtimeDir } = {
|
|
|
135
135
|
!nativeBindingHintDue(runtimeDir, now, NATIVE_BINDING_HINT_COOLDOWN_MS, errKey(err.message))
|
|
136
136
|
)
|
|
137
137
|
return null;
|
|
138
|
+
// No guessed cause: better-sqlite3 13's addons are N-API and survive a Node upgrade, and on 13 the
|
|
139
|
+
// addon can also never have been there — no prebuild for the platform (D#306).
|
|
138
140
|
return (
|
|
139
141
|
`[claude-mem-lite] [${ts}] [WARN] ${event}: native DB binding can't load ` +
|
|
140
|
-
|
|
142
|
+
`— auto-heals at the next session start, or run now: ${CLI_REBUILD_BINDING}`
|
|
141
143
|
);
|
|
142
144
|
}
|
|
143
145
|
return `[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err && err.message}`;
|