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/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 shadowFuse = false;
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 buffer = Buffer.alloc(49);
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 token = buffer.subarray(0, count).toString("utf8");
97
- return /^[0-9a-f]{48}$/.test(token) ? token : "";
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
- if (optedIn && !existsSync(owner) && !existsSync(shared)) {
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
- touchExclusive(owner, rotationOwner);
459
+ if (!claimRotationOwner(owner)) touchExclusive(shared);
396
460
  }
397
461
  if (existsSync(owner)) {
398
- try { token = readRotationOwner(owner); } catch {}
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
- shadowFuse = false;
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 shadowFuse;
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
- shadowFuse = true;
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 (shadowFuse) {
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 && shadowFuse) {
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 ((access.platform ?? process.platform) === "win32" && !access.runner && !shadowFuse) {
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
- const target = exclusive
965
- ? file
966
- : join(
967
- dir,
968
- `.${basename(file)}-${randomBytes(8).toString("hex")}.tmp`,
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, { readdir = readdirSync, stat = statSync, onError } = {}) {
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, { readdir = readdirSync, stat = statSync, onError } = {}) {
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
- if (!current || current === previous || current.fiber !== ctx.fiber) return false;
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 (ctx.fiber?.store?.compaction === current) delete ctx.fiber.store.compaction;
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;