dsh-live-compact 1.1.4 → 1.1.6
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/CHANGELOG.md +268 -0
- package/README.md +86 -213
- package/gate.js +59 -6
- package/index.js +547 -141
- package/package.json +5 -2
- package/plan.js +68 -14
- package/raw.js +33 -11
- package/shadow-save-worker.mjs +24 -0
- package/shadow.js +210 -32
- package/shell.js +4 -2
- package/thunder.js +8 -1
package/shadow.js
CHANGED
|
@@ -38,7 +38,7 @@ const PLUGIN_NAME = /^(?:(?:cut|official|shadow)-\d+-[0-9a-f]{8}|raw-[0-9a-f]{16
|
|
|
38
38
|
let cachedSid = "";
|
|
39
39
|
const lockedDirs = new Map();
|
|
40
40
|
const lockedFiles = new Map();
|
|
41
|
-
let
|
|
41
|
+
let shadowFuseState = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
42
42
|
const shadowWarnings = new Set();
|
|
43
43
|
|
|
44
44
|
// No shared byte ledger. A positively dead holder leaves an immutable fence:
|
|
@@ -89,17 +89,78 @@ function touchExclusive(file, body = "") {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function readRotationOwner(file) {
|
|
92
|
+
if (!lstatSync(file).isFile()) throw new Error("shadow owner is not a regular file");
|
|
92
93
|
const fd = openSync(file, "r");
|
|
93
94
|
try {
|
|
94
|
-
const
|
|
95
|
+
const info = fstatSync(fd, { bigint: true });
|
|
96
|
+
if (!info.isFile() || info.size <= 0n || info.size > 256n) throw new Error("invalid shadow owner size");
|
|
97
|
+
const buffer = Buffer.alloc(257);
|
|
95
98
|
const count = readSync(fd, buffer, 0, buffer.length, 0);
|
|
96
|
-
const
|
|
97
|
-
|
|
99
|
+
const text = buffer.subarray(0, count).toString("utf8");
|
|
100
|
+
const record = JSON.parse(text);
|
|
101
|
+
if (count !== Number(info.size) || record?.v !== 1 ||
|
|
102
|
+
!Number.isSafeInteger(record.pid) || record.pid <= 0 || record.pid > 0x7fffffff ||
|
|
103
|
+
!/^[0-9a-f]{48}$/.test(record.id) || `${JSON.stringify({ v: 1, pid: record.pid, id: record.id })}\n` !== text) {
|
|
104
|
+
throw new Error("invalid shadow owner record");
|
|
105
|
+
}
|
|
106
|
+
return record;
|
|
98
107
|
} finally {
|
|
99
108
|
closeSync(fd);
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
function rotationOwnerText(record) {
|
|
113
|
+
return `${JSON.stringify({ v: 1, pid: record.pid, id: record.id })}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function publishRotationOwner(file, record) {
|
|
117
|
+
const temp = `${file}.${randomBytes(24).toString("hex")}.tmp`;
|
|
118
|
+
let fd;
|
|
119
|
+
try {
|
|
120
|
+
fd = openSync(temp, "wx", 0o600);
|
|
121
|
+
writeFileSync(fd, rotationOwnerText(record));
|
|
122
|
+
fsyncSync(fd);
|
|
123
|
+
closeSync(fd);
|
|
124
|
+
fd = undefined;
|
|
125
|
+
// The rotation lock serializes owner claims. Remove only the stale owner
|
|
126
|
+
// after its PID was proven dead, then publish the complete replacement.
|
|
127
|
+
try { unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
128
|
+
renameSync(temp, file);
|
|
129
|
+
} finally {
|
|
130
|
+
if (fd !== undefined) closeSync(fd);
|
|
131
|
+
try { unlinkSync(temp); } catch {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ownerProcessAlive(record) {
|
|
136
|
+
try {
|
|
137
|
+
process.kill(record.pid, 0);
|
|
138
|
+
return true;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return error?.code !== "ESRCH";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function claimRotationOwner(file) {
|
|
145
|
+
const next = { pid: process.pid, id: rotationOwner };
|
|
146
|
+
let current;
|
|
147
|
+
try {
|
|
148
|
+
current = readRotationOwner(file);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error?.code !== "ENOENT") return false;
|
|
151
|
+
}
|
|
152
|
+
if (current) {
|
|
153
|
+
if (current.pid === next.pid && current.id === next.id) return true;
|
|
154
|
+
if (ownerProcessAlive(current)) return false;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
publishRotationOwner(file, next);
|
|
158
|
+
return readRotationOwner(file).pid === next.pid && readRotationOwner(file).id === next.id;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
103
164
|
function rotationRecordText(record) {
|
|
104
165
|
return `${JSON.stringify({ v: 1, pid: record.pid, id: record.id })}\n`;
|
|
105
166
|
}
|
|
@@ -390,12 +451,18 @@ export function withShadowRoot(dir, action) {
|
|
|
390
451
|
const optedIn = guard.owned && dedicatedRoot(root, identity);
|
|
391
452
|
const owner = join(root, ROTATION_OWNER);
|
|
392
453
|
let token = "";
|
|
393
|
-
|
|
454
|
+
let currentOwner;
|
|
455
|
+
try { currentOwner = readRotationOwner(owner); } catch {}
|
|
456
|
+
const ownsCurrent = currentOwner?.pid === process.pid && currentOwner?.id === rotationOwner;
|
|
457
|
+
if (optedIn && !existsSync(shared) && !ownsCurrent) {
|
|
394
458
|
shadowUsage = null;
|
|
395
|
-
|
|
459
|
+
if (!claimRotationOwner(owner)) touchExclusive(shared);
|
|
396
460
|
}
|
|
397
461
|
if (existsSync(owner)) {
|
|
398
|
-
try {
|
|
462
|
+
try {
|
|
463
|
+
const record = readRotationOwner(owner);
|
|
464
|
+
token = record.pid === process.pid && record.id === rotationOwner ? record.id : "";
|
|
465
|
+
} catch {}
|
|
399
466
|
if (token !== rotationOwner || !optedIn) touchExclusive(shared);
|
|
400
467
|
}
|
|
401
468
|
const exclusive = optedIn && token === rotationOwner && !existsSync(shared);
|
|
@@ -420,16 +487,31 @@ export function resetShadowAccessState() {
|
|
|
420
487
|
cachedSid = "";
|
|
421
488
|
lockedDirs.clear();
|
|
422
489
|
lockedFiles.clear();
|
|
423
|
-
|
|
490
|
+
// Test/restart helper only: callers must have no in-flight workers. Keep the
|
|
491
|
+
// same shared buffer so idle worker retirement never creates a new fuse domain.
|
|
492
|
+
Atomics.store(shadowFuseState, 0, 0);
|
|
424
493
|
shadowWarnings.clear();
|
|
425
494
|
}
|
|
426
495
|
|
|
427
496
|
export function shadowAccessFused() {
|
|
428
|
-
return
|
|
497
|
+
return Atomics.load(shadowFuseState, 0) !== 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function shadowAccessFuseBuffer() {
|
|
501
|
+
return shadowFuseState.buffer;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function attachShadowAccessFuse(buffer) {
|
|
505
|
+
if (!(buffer instanceof SharedArrayBuffer) || buffer.byteLength !== Int32Array.BYTES_PER_ELEMENT) {
|
|
506
|
+
throw new TypeError("spine: invalid shared shadow access fuse");
|
|
507
|
+
}
|
|
508
|
+
const alreadyFused = shadowAccessFused();
|
|
509
|
+
shadowFuseState = new Int32Array(buffer);
|
|
510
|
+
if (alreadyFused) Atomics.store(shadowFuseState, 0, 1);
|
|
429
511
|
}
|
|
430
512
|
|
|
431
513
|
function tripFuse() {
|
|
432
|
-
|
|
514
|
+
Atomics.store(shadowFuseState, 0, 1);
|
|
433
515
|
}
|
|
434
516
|
|
|
435
517
|
function envValue(env, name) {
|
|
@@ -766,7 +848,7 @@ export function defaultShadowAccessRunner({
|
|
|
766
848
|
return true;
|
|
767
849
|
}
|
|
768
850
|
|
|
769
|
-
if (
|
|
851
|
+
if (shadowAccessFused()) {
|
|
770
852
|
const error = new Error("spine: shadow permission already failed");
|
|
771
853
|
error.code = "ESHADOWACCESS";
|
|
772
854
|
throw error;
|
|
@@ -846,7 +928,7 @@ export function defaultShadowAccessRunner({
|
|
|
846
928
|
export function tightenShadow(path, kind = "dir", access = {}) {
|
|
847
929
|
const platform = access.platform ?? process.platform;
|
|
848
930
|
const windows = platform === "win32" && !access.runner;
|
|
849
|
-
if (windows &&
|
|
931
|
+
if (windows && shadowAccessFused()) {
|
|
850
932
|
const error = new Error("spine: shadow permission action failed; backup refused");
|
|
851
933
|
error.code = "ESHADOWACCESS";
|
|
852
934
|
throw error;
|
|
@@ -890,8 +972,25 @@ export function ensureShadowDir(dir, access = {}) {
|
|
|
890
972
|
return dir;
|
|
891
973
|
}
|
|
892
974
|
|
|
893
|
-
export function tightenCreatedShadow(file, fd, access, { remember = true } = {}) {
|
|
894
|
-
if (
|
|
975
|
+
export function tightenCreatedShadow(file, fd, access, { remember = true, skipRunner = false } = {}) {
|
|
976
|
+
if (skipRunner) {
|
|
977
|
+
try {
|
|
978
|
+
const entry = accessEntry(file);
|
|
979
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
980
|
+
if (!entry.identity || entry.identity !== accessIdentity(opened) || opened.nlink !== 1n) {
|
|
981
|
+
throw new Error("spine: shadow published file identity changed");
|
|
982
|
+
}
|
|
983
|
+
if (remember) lockedFiles.set(entry.canonical, entry);
|
|
984
|
+
return;
|
|
985
|
+
} catch (cause) {
|
|
986
|
+
tripFuse();
|
|
987
|
+
const error = new Error("spine: shadow permission action failed; backup refused", { cause });
|
|
988
|
+
error.code = "ESHADOWACCESS";
|
|
989
|
+
error.step = "file";
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if ((access.platform ?? process.platform) === "win32" && !access.runner && !shadowAccessFused()) {
|
|
895
994
|
try {
|
|
896
995
|
if (shadowDirLocked(dirname(file))) {
|
|
897
996
|
const entry = accessEntry(file, "file");
|
|
@@ -961,12 +1060,13 @@ function writeShadowFileLocked(file, body, access, { exclusive, write, unlink, a
|
|
|
961
1060
|
const dir = dirname(file);
|
|
962
1061
|
const parentIdentity = rememberShadowParent(dir);
|
|
963
1062
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1063
|
+
// Always stage the body in a private temporary file. Publishing the final
|
|
1064
|
+
// name only after the bytes are flushed prevents readers from observing a
|
|
1065
|
+
// half-written raw or meta file after a process interruption.
|
|
1066
|
+
const target = join(
|
|
1067
|
+
dir,
|
|
1068
|
+
`.${basename(file)}-${randomBytes(8).toString("hex")}.tmp`,
|
|
1069
|
+
);
|
|
970
1070
|
|
|
971
1071
|
let fd = null;
|
|
972
1072
|
let created = false;
|
|
@@ -976,25 +1076,34 @@ function writeShadowFileLocked(file, body, access, { exclusive, write, unlink, a
|
|
|
976
1076
|
fd = openSync(target, "wx", 0o600);
|
|
977
1077
|
created = true;
|
|
978
1078
|
|
|
979
|
-
if (!exclusive) {
|
|
980
|
-
renameSync(target, file);
|
|
981
|
-
protectedPath = file;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Keep the caller's spelling for the permission cache/runner.
|
|
985
|
-
// The opened descriptor and cleanup path remain in the canonical root.
|
|
986
|
-
tightenCreatedShadow(accessPath, fd, access);
|
|
987
1079
|
verifyRotationScope();
|
|
988
1080
|
if (parentIdentity && directoryIdentity(dir) !== parentIdentity) {
|
|
989
1081
|
retireRotationScope();
|
|
990
1082
|
throw coordinationError(new Error("shadow session directory identity changed"));
|
|
991
1083
|
}
|
|
1084
|
+
// Confirm ACL/mode before any secret bytes are written to the staged file.
|
|
1085
|
+
tightenCreatedShadow(target, fd, access);
|
|
992
1086
|
// Full replacement bytes are a conservative charge, even for overwrites,
|
|
993
1087
|
// partial writes and failed cleanup. Metadata/owner/stats are not capacity.
|
|
994
1088
|
if (shadowUsage && isPluginShadowName(basename(file))) {
|
|
995
1089
|
shadowUsage.bytes += typeof body === "string" ? Buffer.byteLength(body, "utf8") : body.byteLength;
|
|
996
1090
|
}
|
|
997
1091
|
write(fd, body);
|
|
1092
|
+
fsyncSync(fd);
|
|
1093
|
+
|
|
1094
|
+
// The rotation/root lock provides the no-overwrite boundary for exclusive
|
|
1095
|
+
// raw files. Refuse a file that appeared after admission; never replace it.
|
|
1096
|
+
if (exclusive && existsSync(file)) {
|
|
1097
|
+
const error = new Error("shadow file already exists");
|
|
1098
|
+
error.code = "EEXIST";
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
1101
|
+
renameSync(target, file);
|
|
1102
|
+
protectedPath = file;
|
|
1103
|
+
|
|
1104
|
+
// Keep the caller's spelling for the permission cache/runner. The opened
|
|
1105
|
+
// descriptor and the final path now refer to the same published object.
|
|
1106
|
+
tightenCreatedShadow(file, fd, access, { skipRunner: true });
|
|
998
1107
|
|
|
999
1108
|
const opened = fd;
|
|
1000
1109
|
fd = null;
|
|
@@ -1049,7 +1158,12 @@ function readSessionKey(file) {
|
|
|
1049
1158
|
}
|
|
1050
1159
|
}
|
|
1051
1160
|
|
|
1052
|
-
export function listPluginShadows(dir, {
|
|
1161
|
+
export function listPluginShadows(dir, {
|
|
1162
|
+
readdir = readdirSync,
|
|
1163
|
+
stat = statSync,
|
|
1164
|
+
onError,
|
|
1165
|
+
includeSessionKey = true,
|
|
1166
|
+
} = {}) {
|
|
1053
1167
|
let names = [];
|
|
1054
1168
|
try {
|
|
1055
1169
|
names = readdir(dir);
|
|
@@ -1069,7 +1183,7 @@ export function listPluginShadows(dir, { readdir = readdirSync, stat = statSync,
|
|
|
1069
1183
|
path,
|
|
1070
1184
|
size: info.size,
|
|
1071
1185
|
mtimeMs: info.mtimeMs,
|
|
1072
|
-
sessionKey: readSessionKey(path),
|
|
1186
|
+
sessionKey: includeSessionKey ? readSessionKey(path) : "",
|
|
1073
1187
|
});
|
|
1074
1188
|
} catch { onError?.(); }
|
|
1075
1189
|
}
|
|
@@ -1080,7 +1194,12 @@ function byAge(left, right) {
|
|
|
1080
1194
|
return left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name);
|
|
1081
1195
|
}
|
|
1082
1196
|
|
|
1083
|
-
export function listShadowEntries(dir, alive = processAlive, {
|
|
1197
|
+
export function listShadowEntries(dir, alive = processAlive, {
|
|
1198
|
+
readdir = readdirSync,
|
|
1199
|
+
stat = statSync,
|
|
1200
|
+
onError,
|
|
1201
|
+
includeSessionKey = false,
|
|
1202
|
+
} = {}) {
|
|
1084
1203
|
const entries = [];
|
|
1085
1204
|
let names = [];
|
|
1086
1205
|
try {
|
|
@@ -1113,7 +1232,7 @@ export function listShadowEntries(dir, alive = processAlive, { readdir = readdir
|
|
|
1113
1232
|
if (owner > 0) {
|
|
1114
1233
|
try { live = alive(owner) !== false; } catch { live = true; }
|
|
1115
1234
|
}
|
|
1116
|
-
for (const file of listPluginShadows(path, { readdir, stat, onError })) {
|
|
1235
|
+
for (const file of listPluginShadows(path, { readdir, stat, onError, includeSessionKey })) {
|
|
1117
1236
|
// The directory and its owner define deletion authority. Raw tool output
|
|
1118
1237
|
// is untrusted data and may itself start with a spine-session marker.
|
|
1119
1238
|
entries.push({ ...file, legacy: false, live, sessionKey: entry.name });
|
|
@@ -1130,6 +1249,65 @@ export function rotateShadow(options) {
|
|
|
1130
1249
|
return withShadowRoot(options.dir, () => rotateShadowLocked(options));
|
|
1131
1250
|
}
|
|
1132
1251
|
|
|
1252
|
+
// Complete backup transaction used by both the synchronous compatibility API
|
|
1253
|
+
// and the optional root-writer worker. The rotation lock stays held from the
|
|
1254
|
+
// full reconcile through ACL-protected body publication; callers must not move
|
|
1255
|
+
// only the scan to a worker and release the lock before the write.
|
|
1256
|
+
export function saveShadowTransaction({
|
|
1257
|
+
shadow,
|
|
1258
|
+
file,
|
|
1259
|
+
body,
|
|
1260
|
+
required = false,
|
|
1261
|
+
protectPaths = [],
|
|
1262
|
+
sessionKey = "default",
|
|
1263
|
+
cap,
|
|
1264
|
+
access = {},
|
|
1265
|
+
rotationIO = {},
|
|
1266
|
+
} = {}) {
|
|
1267
|
+
ensureShadowDir(shadow, access);
|
|
1268
|
+
const dir = sessionDir(shadow, sessionKey);
|
|
1269
|
+
ensureShadowDir(dir, access);
|
|
1270
|
+
return withShadowRoot(shadow, () => {
|
|
1271
|
+
const owner = join(dir, "owner");
|
|
1272
|
+
const ownerExisted = existsSync(owner);
|
|
1273
|
+
let ownerWritten = false;
|
|
1274
|
+
try {
|
|
1275
|
+
writeShadowOwner(dir, access, shadow);
|
|
1276
|
+
ownerWritten = true;
|
|
1277
|
+
const incoming = Buffer.byteLength(body);
|
|
1278
|
+
const rotation = rotateShadow({
|
|
1279
|
+
...rotationIO,
|
|
1280
|
+
dir: shadow,
|
|
1281
|
+
incomingBytes: incoming,
|
|
1282
|
+
protectPath: file,
|
|
1283
|
+
protectPaths,
|
|
1284
|
+
sessionKey,
|
|
1285
|
+
cap,
|
|
1286
|
+
});
|
|
1287
|
+
if (!rotation.ok) {
|
|
1288
|
+
if (rotation.reason) {
|
|
1289
|
+
const incomplete = rotation.reason === "scan-incomplete";
|
|
1290
|
+
const error = Object.assign(new Error(incomplete
|
|
1291
|
+
? "spine: shadow capacity scan is incomplete; backup refused; leaving the span"
|
|
1292
|
+
: "spine: shadow capacity measurement is invalid; backup refused; leaving the span"), {
|
|
1293
|
+
code: incomplete ? "ESHADOWSCAN" : "ESHADOWMETER",
|
|
1294
|
+
});
|
|
1295
|
+
if (required) throw error;
|
|
1296
|
+
return { ok: false, skipped: true, reason: rotation.reason, rotation };
|
|
1297
|
+
}
|
|
1298
|
+
if (required) throw new Error("spine: shadow backup is full; leaving the span");
|
|
1299
|
+
return { ok: false, skipped: true, reason: "full", rotation };
|
|
1300
|
+
}
|
|
1301
|
+
writeShadowFile(file, body, access, { root: shadow });
|
|
1302
|
+
if (!existsSync(file)) throw new Error("spine: shadow backup was not written");
|
|
1303
|
+
return { ok: true, file, rotation };
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (ownerWritten && !ownerExisted) discardShadowFiles([owner], error);
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1133
1311
|
function rotationTime(now) {
|
|
1134
1312
|
try {
|
|
1135
1313
|
const value = now();
|
package/shell.js
CHANGED
|
@@ -68,9 +68,11 @@ function releaseCompaction(ctx, previous) {
|
|
|
68
68
|
const store = ctx?.reflect?.store;
|
|
69
69
|
const key = ctx?.[Symbol.for("cordis.isolate")]?.compaction;
|
|
70
70
|
const current = compactionRecord(ctx);
|
|
71
|
-
|
|
71
|
+
const currentFiber = current?.fiber ?? current?.ctx?.fiber;
|
|
72
|
+
const contextFiber = ctx?.fiber;
|
|
73
|
+
if (!current || current === previous || currentFiber !== contextFiber) return false;
|
|
72
74
|
delete store[key];
|
|
73
|
-
if (
|
|
75
|
+
if (contextFiber?.store?.compaction === current) delete contextFiber.store.compaction;
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
76
78
|
|
package/thunder.js
CHANGED
|
@@ -34,7 +34,14 @@ function estimateTokens(text) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function buildSendLess({ taskLine, uncovered, facts, proseTokens = PROSE_TOKEN_CAP }) {
|
|
37
|
-
const kept = (facts || []).filter(Boolean)
|
|
37
|
+
const kept = (facts || []).filter(Boolean).map((fact) => {
|
|
38
|
+
if (typeof fact === "object" && fact !== null) {
|
|
39
|
+
const role = String(fact.role || "unknown");
|
|
40
|
+
const line = String(fact.line || "");
|
|
41
|
+
return line ? `[${role}] ${line}` : "";
|
|
42
|
+
}
|
|
43
|
+
return `[unknown] ${String(fact)}`;
|
|
44
|
+
}).filter(Boolean);
|
|
38
45
|
let prose = uncovered || "";
|
|
39
46
|
if (estimateTokens(prose) > proseTokens) {
|
|
40
47
|
let low = 0;
|