claude-mem-lite 6.25.1 → 6.25.3
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/hook-shared.mjs +10 -7
- package/hook-update.mjs +128 -36
- package/hook.mjs +8 -5
- package/install.mjs +296 -63
- package/lib/binding-probe.mjs +9 -1
- package/lib/doctor-stale-temp.mjs +20 -10
- package/lib/native-binding-hint.mjs +3 -1
- package/lib/proc-lock.mjs +184 -27
- package/lib/wal-open-blocked.mjs +69 -0
- package/mem-cli.mjs +16 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/scripts/binding-probe-cli.mjs +5 -2
- package/scripts/hook-launcher.mjs +1 -3
- package/scripts/launch.mjs +13 -3
- package/source-files.mjs +3 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.25.
|
|
12
|
+
"version": "6.25.3",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.25.
|
|
3
|
+
"version": "6.25.3",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/hook-shared.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { isDbUnusableError, DB_UNUSABLE_MARKER_PREFIX } from './lib/db-unusable.mjs';
|
|
32
32
|
import { shouldRecordOnce } from './lib/record-once.mjs';
|
|
33
33
|
import { backgroundWorkerEnv, workerDirGone } from './lib/worker-data-dir.mjs';
|
|
34
|
+
import { LIVE_HOLDER_MAX_MS } from './lib/proc-lock.mjs';
|
|
34
35
|
import { hookSessionId } from './lib/provenance.mjs';
|
|
35
36
|
import { PROJECT_REKEY_MARKER_PREFIX } from './lib/project-rekey.mjs';
|
|
36
37
|
// Audit 2026-09-05 P1-2 (carried from 2026-09-02 P2-9): `callLLM`, the quiet/adoption
|
|
@@ -67,12 +68,13 @@ export const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24h
|
|
|
67
68
|
export const STALE_LOCK_MS = 30000; // 30s
|
|
68
69
|
|
|
69
70
|
// Backstop for cleanStaleLockFiles(): a lock whose recorded pid is ALIVE is kept until it
|
|
70
|
-
// reaches this age, not STALE_LOCK_MS.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
71
|
+
// reaches this age, not STALE_LOCK_MS. The same bound proc-lock.mjs steals a live holder's lock
|
|
72
|
+
// at, so the sweeper is never the more aggressive of the two — whatever it removes, the lock
|
|
73
|
+
// protocol itself would already have let the next caller steal. Its only job is to
|
|
74
|
+
// garbage-collect a leaked file whose pid was recycled onto an unrelated live process, which
|
|
75
|
+
// would otherwise pin the file forever. (A20260905-R5-P1-1) It was 10 min, shorter than an
|
|
76
|
+
// update holds install.lock while its smoke gate builds the binding from source (D#294).
|
|
77
|
+
export const ABANDONED_LOCK_MS = LIVE_HOLDER_MAX_MS;
|
|
76
78
|
|
|
77
79
|
// The background-maintenance mutex, defined HERE next to the sweeper policy it has to
|
|
78
80
|
// escape. cleanStaleLockFiles() sweeps every `*.lock` in RUNTIME_DIR; until
|
|
@@ -83,7 +85,8 @@ export const ABANDONED_LOCK_MS = 10 * 60 * 1000; // 10 min
|
|
|
83
85
|
// probe, and pid checks are meaningless across a shared homedir. `tests/auto-maintain-proc-lock.test.mjs`
|
|
84
86
|
// asserts that against THIS constant rather than a re-typed copy: the first version of that
|
|
85
87
|
// test built its own path from a literal, so renaming the lock left it green with the hazard
|
|
86
|
-
// back. proc-lock's own staleness policy (
|
|
88
|
+
// back. proc-lock's own staleness policy (a provably-dead pid at once, a live one only past
|
|
89
|
+
// LIVE_HOLDER_MAX_MS) is the correct one.
|
|
87
90
|
export const AUTO_MAINTAIN_LOCK = 'auto-maintain.proclock';
|
|
88
91
|
export const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 min (title dedup)
|
|
89
92
|
export const RELATED_OBS_WINDOW_MS = 7 * DAY_MS; // 7 days
|
package/hook-update.mjs
CHANGED
|
@@ -43,7 +43,11 @@ import {
|
|
|
43
43
|
// because checkForUpdate is silent on network failure the plugin then reports
|
|
44
44
|
// itself permanently up to date. Same tunnel the OpenRouter call site uses.
|
|
45
45
|
import { httpConnectProxyFor, getViaConnectProxy, redactProxyUrl } from './lib/proxy-fetch.mjs';
|
|
46
|
-
import
|
|
46
|
+
// A namespace, not a named import: a swap renames files one at a time, so for a moment this file can
|
|
47
|
+
// sit over the previous release's lib/proc-lock.mjs, and a named import of an export that release
|
|
48
|
+
// lacks is a SyntaxError that stops this module loading — and with it recoverInterruptedSwaps, which
|
|
49
|
+
// finishes that very swap (pre-ship delta review P2-1). takeLock is newer than v6.25.2: detected.
|
|
50
|
+
import * as procLock from './lib/proc-lock.mjs';
|
|
47
51
|
import { atomicWriteFileSync } from './lib/atomic-write.mjs';
|
|
48
52
|
import { verifyReleaseFiles, verifyManifestSignature } from './lib/release-digest.mjs';
|
|
49
53
|
import { detectInstallShape } from './lib/install-shape.mjs';
|
|
@@ -262,7 +266,7 @@ function pluginOnlyInstall() {
|
|
|
262
266
|
return Boolean(shape && !shape.managed && shape.activePluginVersion);
|
|
263
267
|
}
|
|
264
268
|
|
|
265
|
-
function isPluginMode() {
|
|
269
|
+
export function isPluginMode() {
|
|
266
270
|
return Boolean(process.env.CLAUDE_PLUGIN_ROOT) || pluginOnlyInstall();
|
|
267
271
|
}
|
|
268
272
|
|
|
@@ -954,6 +958,40 @@ function retireUnresolvedBackups(targetDir, names) {
|
|
|
954
958
|
}
|
|
955
959
|
}
|
|
956
960
|
|
|
961
|
+
// Backup dirs still unresolved are undone like a stack, newest first (D#293), and their order is
|
|
962
|
+
// carried in the dir NAME, `.update-backup-s<seq>-<ms>-<pid>`: a swap takes one more than the
|
|
963
|
+
// highest seq among the unresolved dirs it finds. Neither the wall clock in the name nor a field in
|
|
964
|
+
// the journal decides it: a clock can go back, and a journal can be unreadable at the moment the
|
|
965
|
+
// order is needed (a scanner holding it on Windows), which made a replay lose a file (review P2-1).
|
|
966
|
+
// A dir named before 6.25.2 has no seq and reads as 0, so it comes last.
|
|
967
|
+
const BACKUP_SEQ_RE = /^\.update-backup-s(\d+)-/;
|
|
968
|
+
const swapSeqOf = (name) => Number(BACKUP_SEQ_RE.exec(name)?.[1] ?? 0);
|
|
969
|
+
const nextSwapSeq = (names) => Math.max(0, ...names.map(swapSeqOf)) + 1;
|
|
970
|
+
|
|
971
|
+
/** Unresolved backup dir names in the order recovery undoes them: newest first, by seq, then name.
|
|
972
|
+
* cleanup --dry-run walks the same order to say what recovery would leave (D#307 F3). */
|
|
973
|
+
const unresolvedSwapOrder = (names) =>
|
|
974
|
+
[...names].sort((a, b) => swapSeqOf(b) - swapSeqOf(a) || b.localeCompare(a));
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Which unresolved backup dirs recovery would leave in place because a newer one's journal cannot be
|
|
978
|
+
* read: recovery walks them newest first and stops there (recoverInterruptedSwaps). cleanup --dry-run
|
|
979
|
+
* reports from this instead of judging each dir alone (D#307 F3). It cannot foresee a file that
|
|
980
|
+
* fails to go back, which also stops the walk.
|
|
981
|
+
* @param {{name: string, readable: boolean}[]} swaps
|
|
982
|
+
* @returns {Map<string, string>} name → the newer unreadable dir that blocks it
|
|
983
|
+
*/
|
|
984
|
+
export function swapsBlockedByUnreadable(swaps) {
|
|
985
|
+
const readable = new Map(swaps.map((s) => [s.name, s.readable]));
|
|
986
|
+
const blockedBy = new Map();
|
|
987
|
+
let newestUnreadable = null;
|
|
988
|
+
for (const name of unresolvedSwapOrder([...readable.keys()])) {
|
|
989
|
+
if (newestUnreadable) blockedBy.set(name, newestUnreadable);
|
|
990
|
+
else if (!readable.get(name)) newestUnreadable = name;
|
|
991
|
+
}
|
|
992
|
+
return blockedBy;
|
|
993
|
+
}
|
|
994
|
+
|
|
957
995
|
// The `.update-backup-*` dirs in targetDir that carry no resolved marker.
|
|
958
996
|
function unresolvedBackups(targetDir) {
|
|
959
997
|
try {
|
|
@@ -969,8 +1007,19 @@ function unresolvedBackups(targetDir) {
|
|
|
969
1007
|
|
|
970
1008
|
// The backup dir is resolved before staging goes: removing a whole staging tree takes long enough
|
|
971
1009
|
// for a kill to land in, and a kill there left a committed swap's journal to be replayed.
|
|
972
|
-
|
|
973
|
-
|
|
1010
|
+
//
|
|
1011
|
+
// After a rollback, `notRestored` lists what it could not put back. The backup dir then holds the
|
|
1012
|
+
// only copy of each, so it stays whole with its journal, unresolved, and the next entry replays it
|
|
1013
|
+
// (D#293). The caller used to delete it. Nothing gates a later update on it: one that commits
|
|
1014
|
+
// retires it, so a restore that keeps failing cannot block updates (a2cf96a1's gate did).
|
|
1015
|
+
function discardSwapDirs(stagingDir, backupDir, notRestored = []) {
|
|
1016
|
+
if (notRestored.length === 0) discardBackupDir(backupDir);
|
|
1017
|
+
else
|
|
1018
|
+
debugLog(
|
|
1019
|
+
'WARN',
|
|
1020
|
+
'hook-update',
|
|
1021
|
+
`Kept ${backupDir}: could not put back ${notRestored.join(', ')}; the next update, install or cleanup tries again`,
|
|
1022
|
+
);
|
|
974
1023
|
try {
|
|
975
1024
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
976
1025
|
} catch (e) {
|
|
@@ -978,6 +1027,18 @@ function discardSwapDirs(stagingDir, backupDir) {
|
|
|
978
1027
|
}
|
|
979
1028
|
}
|
|
980
1029
|
|
|
1030
|
+
// Whether a path is there, by lstat: a symlink counts whatever it points to, and an error other
|
|
1031
|
+
// than "no such path" counts as there. existsSync follows the link and answers false on EACCES, so
|
|
1032
|
+
// a backup that was a dangling symlink read as already put back, and was deleted (review r2 F11).
|
|
1033
|
+
function present(p) {
|
|
1034
|
+
try {
|
|
1035
|
+
lstatSync(p);
|
|
1036
|
+
return true;
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
return e.code !== 'ENOENT' && e.code !== 'ENOTDIR';
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
981
1042
|
/**
|
|
982
1043
|
* Finish any swap a previous process was killed in the middle of, then clear its
|
|
983
1044
|
* residue. Called on every install entry, under the install lock, BEFORE a new
|
|
@@ -995,6 +1056,7 @@ export function recoverInterruptedSwaps(targetDir = INSTALL_DIR) {
|
|
|
995
1056
|
}
|
|
996
1057
|
|
|
997
1058
|
let recovered = 0;
|
|
1059
|
+
const pending = [];
|
|
998
1060
|
for (const entry of entries) {
|
|
999
1061
|
if (!entry.isDirectory()) continue;
|
|
1000
1062
|
const dir = join(targetDir, entry.name);
|
|
@@ -1017,42 +1079,62 @@ export function recoverInterruptedSwaps(targetDir = INSTALL_DIR) {
|
|
|
1017
1079
|
recovered++;
|
|
1018
1080
|
continue;
|
|
1019
1081
|
}
|
|
1082
|
+
pending.push(entry.name);
|
|
1083
|
+
}
|
|
1020
1084
|
|
|
1085
|
+
// Several dirs are unresolved only when a rollback could not put every file back (D#293), and
|
|
1086
|
+
// they are undone like a stack: newest first by the seq in the name, and none older while a newer
|
|
1087
|
+
// one is unfinished or cannot be read. An older journal replayed first puts back a file that a
|
|
1088
|
+
// newer journal, which installed that path, then deletes; with nothing left to restore it from.
|
|
1089
|
+
for (const name of unresolvedSwapOrder(pending)) {
|
|
1090
|
+
const dir = join(targetDir, name);
|
|
1021
1091
|
// No journal: the swap is over and its journal is gone (or it was killed before its first
|
|
1022
1092
|
// rename), so there is nothing to replay and the dir is residue. A journal that is there but
|
|
1023
1093
|
// cannot be read, or not as one (an in-place write of 6.25.0 or older torn by a kill), says
|
|
1024
|
-
// nothing about what moved, and the dir may hold the only copy of it: leave it
|
|
1025
|
-
// read as "nothing moved" and deleted (v6.25.1 review). doctor and
|
|
1026
|
-
// swap that commits, or an install, retires it.
|
|
1027
|
-
let
|
|
1028
|
-
let raw = null;
|
|
1094
|
+
// nothing about what moved, and the dir may hold the only copy of it: leave it, and everything
|
|
1095
|
+
// older with it. It used to be read as "nothing moved" and deleted (v6.25.1 review). doctor and
|
|
1096
|
+
// cleanup name it; the next swap that commits, or an install, retires it.
|
|
1097
|
+
let raw;
|
|
1029
1098
|
try {
|
|
1030
1099
|
raw = readFileSync(join(dir, SWAP_JOURNAL), 'utf8');
|
|
1031
1100
|
} catch (e) {
|
|
1032
|
-
if (e.code
|
|
1033
|
-
|
|
1101
|
+
if (e.code === 'ENOENT') {
|
|
1102
|
+
discardBackupDir(dir);
|
|
1103
|
+
recovered++;
|
|
1034
1104
|
continue;
|
|
1035
1105
|
}
|
|
1106
|
+
debugCatch(e, 'recover-read-journal');
|
|
1107
|
+
break;
|
|
1036
1108
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1109
|
+
let journal;
|
|
1110
|
+
try {
|
|
1111
|
+
journal = JSON.parse(raw);
|
|
1112
|
+
} catch {
|
|
1113
|
+
journal = null;
|
|
1114
|
+
}
|
|
1115
|
+
if (!Array.isArray(journal?.backedUp) || !Array.isArray(journal?.installed)) {
|
|
1116
|
+
debugLog(
|
|
1117
|
+
'WARN',
|
|
1118
|
+
'hook-update',
|
|
1119
|
+
`Left ${name} in place, and any older one: its swap journal cannot be read`,
|
|
1120
|
+
);
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
const notRestored = rollbackInstall(journal.installed, journal.backedUp, dir, targetDir);
|
|
1124
|
+
if (notRestored.length > 0) {
|
|
1125
|
+
debugLog(
|
|
1126
|
+
'WARN',
|
|
1127
|
+
'hook-update',
|
|
1128
|
+
`Could not finish ${name}: ${notRestored.join(', ')} not put back; left, with any older one, for the next entry`,
|
|
1129
|
+
);
|
|
1130
|
+
break;
|
|
1047
1131
|
}
|
|
1048
|
-
const { backedUp, installed } = journal;
|
|
1049
|
-
rollbackInstall(installed, backedUp, dir, targetDir);
|
|
1050
1132
|
discardBackupDir(dir);
|
|
1051
1133
|
recovered++;
|
|
1052
1134
|
debugLog(
|
|
1053
1135
|
'WARN',
|
|
1054
1136
|
'hook-update',
|
|
1055
|
-
`Recovered an interrupted update swap: restored ${backedUp.length} path(s) from ${
|
|
1137
|
+
`Recovered an interrupted update swap: restored ${journal.backedUp.length} path(s) from ${name}`,
|
|
1056
1138
|
);
|
|
1057
1139
|
}
|
|
1058
1140
|
return recovered;
|
|
@@ -1069,10 +1151,12 @@ export function recoverInterruptedSwaps(targetDir = INSTALL_DIR) {
|
|
|
1069
1151
|
// when the journal cannot be removed, the resolved marker keeps it from being replayed. A backup
|
|
1070
1152
|
// dir 6.25.0 or older left half deleted beside its journal can still replay to a mix.
|
|
1071
1153
|
// The arrays are copied, not reversed in place, so a caller's journal is left as it was.
|
|
1154
|
+
//
|
|
1155
|
+
// Returns the backed-up paths it could not put back, still in the backup dir (D#293).
|
|
1072
1156
|
function rollbackInstall(installed, backedUp, backupDir, targetDir) {
|
|
1073
1157
|
markSwapStart();
|
|
1074
1158
|
try {
|
|
1075
|
-
const restored = new Set(backedUp.filter((relPath) => !
|
|
1159
|
+
const restored = new Set(backedUp.filter((relPath) => !present(join(backupDir, relPath))));
|
|
1076
1160
|
for (const relPath of [...installed].reverse()) {
|
|
1077
1161
|
if (restored.has(relPath)) continue;
|
|
1078
1162
|
try {
|
|
@@ -1085,7 +1169,7 @@ function rollbackInstall(installed, backedUp, backupDir, targetDir) {
|
|
|
1085
1169
|
const backupPath = join(backupDir, relPath);
|
|
1086
1170
|
const targetPath = join(targetDir, relPath);
|
|
1087
1171
|
try {
|
|
1088
|
-
if (
|
|
1172
|
+
if (present(backupPath)) {
|
|
1089
1173
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1090
1174
|
renameSync(backupPath, targetPath);
|
|
1091
1175
|
}
|
|
@@ -1096,6 +1180,7 @@ function rollbackInstall(installed, backedUp, backupDir, targetDir) {
|
|
|
1096
1180
|
} finally {
|
|
1097
1181
|
clearSwapMarker();
|
|
1098
1182
|
}
|
|
1183
|
+
return backedUp.filter((relPath) => present(join(backupDir, relPath)));
|
|
1099
1184
|
}
|
|
1100
1185
|
|
|
1101
1186
|
// MED-5 post-install health gate: load-test the freshly-switched code in a SEPARATE
|
|
@@ -1183,18 +1268,25 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
1183
1268
|
// holding the lock means an install is already in flight — skip rather than
|
|
1184
1269
|
// race. Shared path with install.mjs so direct install + repair + auto-update
|
|
1185
1270
|
// are mutually exclusive.
|
|
1186
|
-
const
|
|
1271
|
+
const lockPath = join(STATE_DIR, 'runtime', 'install.lock'); // runtime-dir:stays-put — install lock serialises real installers
|
|
1272
|
+
const { release, error: lockError } = procLock.takeLock
|
|
1273
|
+
? procLock.takeLock(lockPath)
|
|
1274
|
+
: { release: procLock.acquireLock(lockPath) };
|
|
1187
1275
|
if (!release) {
|
|
1276
|
+
// doctor is the face that names a lock that cannot be taken (D#307); this log is debug-only.
|
|
1188
1277
|
debugLog(
|
|
1189
1278
|
'DEBUG',
|
|
1190
1279
|
'hook-update',
|
|
1191
|
-
|
|
1280
|
+
lockError
|
|
1281
|
+
? `installExtractedRelease: install.lock cannot be taken — ${lockError.dir} (${lockError.code}) — skipping`
|
|
1282
|
+
: 'installExtractedRelease: another install/update is in progress — skipping',
|
|
1192
1283
|
);
|
|
1193
1284
|
return false;
|
|
1194
1285
|
}
|
|
1195
1286
|
const ts = `${Date.now()}-${process.pid}`;
|
|
1196
1287
|
const stagingDir = join(targetDir, `.update-staging-${ts}`);
|
|
1197
|
-
|
|
1288
|
+
// Named once the seq is known, below; until then nothing has been written into it.
|
|
1289
|
+
let backupDir = join(targetDir, `.update-backup-${ts}`);
|
|
1198
1290
|
const backedUp = [];
|
|
1199
1291
|
const installed = [];
|
|
1200
1292
|
let committed = false;
|
|
@@ -1207,9 +1299,11 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
1207
1299
|
// one — otherwise this install stacks on top of a mixed-version tree and its
|
|
1208
1300
|
// own backup can no longer restore a coherent state.
|
|
1209
1301
|
recoverInterruptedSwaps(targetDir);
|
|
1210
|
-
// What recovery left unfinished (a journal it cannot read right now
|
|
1211
|
-
// commits, and left for the next entry if it
|
|
1302
|
+
// What recovery left unfinished (a journal it cannot read right now, or a replay that could not
|
|
1303
|
+
// put every file back, D#293) is retired if this swap commits, and left for the next entry if it
|
|
1304
|
+
// rolls back. Nothing here waits on it: a gate on it blocked every later update (a2cf96a1).
|
|
1212
1305
|
const leftUnresolved = unresolvedBackups(targetDir);
|
|
1306
|
+
backupDir = join(targetDir, `.update-backup-s${nextSwapSeq(leftUnresolved)}-${ts}`);
|
|
1213
1307
|
|
|
1214
1308
|
mkdirSync(stagingDir, { recursive: true });
|
|
1215
1309
|
mkdirSync(backupDir, { recursive: true });
|
|
@@ -1238,7 +1332,7 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
1238
1332
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1239
1333
|
mkdirSync(dirname(backupPath), { recursive: true });
|
|
1240
1334
|
|
|
1241
|
-
if (
|
|
1335
|
+
if (present(targetPath)) {
|
|
1242
1336
|
backedUp.push(relPath);
|
|
1243
1337
|
journalSwap(backupDir, backedUp, installed);
|
|
1244
1338
|
renameSync(targetPath, backupPath);
|
|
@@ -1257,8 +1351,7 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
1257
1351
|
// If it can't, restore the backup and report failure — the running (old) version
|
|
1258
1352
|
// keeps working rather than leaving a broken install with no way back.
|
|
1259
1353
|
if (!opts.skipSmoke && !smokeInstalledRelease(targetDir)) {
|
|
1260
|
-
rollbackInstall(installed, backedUp, backupDir, targetDir);
|
|
1261
|
-
discardSwapDirs(stagingDir, backupDir);
|
|
1354
|
+
discardSwapDirs(stagingDir, backupDir, rollbackInstall(installed, backedUp, backupDir, targetDir));
|
|
1262
1355
|
return false;
|
|
1263
1356
|
}
|
|
1264
1357
|
|
|
@@ -1354,8 +1447,7 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
1354
1447
|
// Past the commit point the backups are gone and the new release IS the install: a rollback
|
|
1355
1448
|
// would delete the paths it added and restore nothing (D#283).
|
|
1356
1449
|
if (committed) return true;
|
|
1357
|
-
rollbackInstall(installed, backedUp, backupDir, targetDir);
|
|
1358
|
-
discardSwapDirs(stagingDir, backupDir);
|
|
1450
|
+
discardSwapDirs(stagingDir, backupDir, rollbackInstall(installed, backedUp, backupDir, targetDir));
|
|
1359
1451
|
return false;
|
|
1360
1452
|
} finally {
|
|
1361
1453
|
release();
|
package/hook.mjs
CHANGED
|
@@ -2287,10 +2287,12 @@ function scheduleSessionStartAutoMaintain(project) {
|
|
|
2287
2287
|
// to minutes (VACUUM INTO snapshot, purge, decay, dedup over the whole DB). It now spares a
|
|
2288
2288
|
// live holder (A20260905-R5-P1-1), but this escape stays: not being swept is a stronger
|
|
2289
2289
|
// guarantee than being spared by a pid probe, and pids are meaningless across a shared
|
|
2290
|
-
// homedir. proc-lock brings its own staleness policy (
|
|
2291
|
-
// the correct one here.
|
|
2292
|
-
//
|
|
2293
|
-
//
|
|
2290
|
+
// homedir. proc-lock brings its own staleness policy (a provably-dead pid at once, a live one
|
|
2291
|
+
// only past LIVE_HOLDER_MAX_MS), which is the correct one here.
|
|
2292
|
+
// Since D#294 this bound applies only to a lock that records no pid, which acquireLock never
|
|
2293
|
+
// writes: a crashed holder is reclaimed through its dead pid, and a pid that looks alive (a
|
|
2294
|
+
// recycled one, or one from another host on a shared homedir) holds the lock for
|
|
2295
|
+
// LIVE_HOLDER_MAX_MS, 1 h, not 10 min.
|
|
2294
2296
|
const AUTO_MAINTAIN_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
2295
2297
|
|
|
2296
2298
|
// Detached `auto-maintain` worker entry: opens its own DB and runs the maintenance
|
|
@@ -2448,7 +2450,8 @@ function saveHandoffAndFastSummary(
|
|
|
2448
2450
|
*
|
|
2449
2451
|
* Liveness of the two real lock families does not depend on this sweeper, so tightening it
|
|
2450
2452
|
* cannot wedge either: hook-episode.acquireLock() preempts a >30s episode lock itself at
|
|
2451
|
-
* acquire time, and proc-lock.acquireLock() steals on
|
|
2453
|
+
* acquire time, and proc-lock.acquireLock() steals on a dead pid at once and on age only past
|
|
2454
|
+
* LIVE_HOLDER_MAX_MS (D#294), the same bound as ABANDONED_LOCK_MS here.
|
|
2452
2455
|
*/
|
|
2453
2456
|
function cleanStaleLockFiles() {
|
|
2454
2457
|
try {
|