dsh-live-compact 1.1.4 → 1.1.5
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 +260 -0
- package/README.md +72 -211
- package/index.js +365 -119
- package/package.json +5 -2
- package/plan.js +41 -11
- package/raw.js +6 -4
- package/shadow-save-worker.mjs +24 -0
- package/shadow.js +168 -17
- package/shell.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live-compact",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "In-place newest-turn compaction for DeepSeek Harness. Compress while the task runs and keep the prefix.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "MerkyorLynn <268568828+MerkyorLynn@users.noreply.github.com>",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./shadow-search": "./shadow-search.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
+
"CHANGELOG.md",
|
|
20
21
|
"shell.js",
|
|
21
22
|
"adapter.js",
|
|
22
23
|
"settings.js",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"plan.js",
|
|
27
28
|
"raw.js",
|
|
28
29
|
"shadow.js",
|
|
30
|
+
"shadow-save-worker.mjs",
|
|
29
31
|
"shadow-search.js",
|
|
30
32
|
"cooldown.js",
|
|
31
33
|
"thunder.js",
|
|
@@ -41,7 +43,8 @@
|
|
|
41
43
|
"compaction"
|
|
42
44
|
],
|
|
43
45
|
"scripts": {
|
|
44
|
-
"test:gate": "node test/p0-gate.mjs && node test/p0-blockers.mjs && node test/excerpt.mjs && node --test test/public-exports.test.mjs test/cooldown.test.mjs test/gate-config.test.mjs test/drain-stopline.mjs test/scan-once.mjs test/issue-19.mjs test/issue-20.mjs test/issue-34.mjs test/windows-acl.test.mjs test/issue-27.mjs test/issue-29.mjs test/issue-29-notice.mjs test/client-settings.test.mjs test/settings-integration.test.mjs test/runtime-lifecycle.mjs test/shadow-lifecycle.mjs test/finalize-over-32000.mjs",
|
|
46
|
+
"test:gate": "node test/p0-gate.mjs && node test/p0-blockers.mjs && node test/excerpt.mjs && node test/shadow-worker.mjs && node test/shadow-worker-pool.mjs && node test/shadow-worker-fuse.mjs && node --test test/failed-diagnostics.test.mjs test/public-exports.test.mjs test/cooldown.test.mjs test/gate-config.test.mjs test/drain-stopline.mjs test/scan-once.mjs test/issue-19.mjs test/issue-20.mjs test/issue-34.mjs test/windows-acl.test.mjs test/issue-27.mjs test/issue-29.mjs test/issue-29-notice.mjs test/client-settings.test.mjs test/settings-integration.test.mjs test/runtime-lifecycle.mjs test/shadow-lifecycle.mjs test/finalize-over-32000.mjs",
|
|
47
|
+
"test:compat41": "node test/compat-cordis41.mjs",
|
|
45
48
|
"test:live-gate": "python3 -B -m unittest discover -s test/live-fill -p test_*.py"
|
|
46
49
|
},
|
|
47
50
|
"engines": {
|
package/plan.js
CHANGED
|
@@ -241,6 +241,8 @@ function failureBody(text) {
|
|
|
241
241
|
if (raw.startsWith("{")) {
|
|
242
242
|
try {
|
|
243
243
|
const parsed = JSON.parse(raw);
|
|
244
|
+
// A bounded failed result keeps every field, not only the two streams.
|
|
245
|
+
if (raw.length <= TOOL_BUDGET) return raw;
|
|
244
246
|
const streams = [];
|
|
245
247
|
for (const [label, value] of [["stderr", parsed?.stderr?.text], ["stdout", parsed?.stdout?.text]]) {
|
|
246
248
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -257,8 +259,9 @@ function failureBody(text) {
|
|
|
257
259
|
|
|
258
260
|
export function failureKeptLines(text) {
|
|
259
261
|
const useful = usefulLines(failureBody(text));
|
|
260
|
-
if (useful.join("\n").length <= TOOL_BUDGET) return useful
|
|
261
|
-
const
|
|
262
|
+
if (useful.join("\n").length <= TOOL_BUDGET) return useful;
|
|
263
|
+
const markedLines = useful.filter((line) => FAILURE_LINE.test(line));
|
|
264
|
+
const marked = (markedLines.length > 8 ? [markedLines[0], ...markedLines.slice(-7)] : markedLines).map(shorten);
|
|
262
265
|
const causes = useful.filter((line) => /(?:root|stderr|stdout)[ _-]*cause\b|CAUSE\b/i.test(line)).slice(-4).map(shorten);
|
|
263
266
|
const edges = [...useful.slice(0, 1), ...useful.slice(-2)].map(shorten);
|
|
264
267
|
const picked = [];
|
|
@@ -538,9 +541,10 @@ export function roundHasLongRead(events, round) {
|
|
|
538
541
|
return false;
|
|
539
542
|
}
|
|
540
543
|
|
|
541
|
-
export function keptToolRounds(events, limit = 3) {
|
|
544
|
+
export function keptToolRounds(events, limit = 3, afterIndex = -1) {
|
|
542
545
|
const count = Number.isInteger(limit) && limit > 0 ? limit : 3;
|
|
543
|
-
const
|
|
546
|
+
const floor = Number.isInteger(afterIndex) ? afterIndex : -1;
|
|
547
|
+
const rounds = finishedToolRounds(events).filter((round) => round.start > floor && !roundHasLongRead(events, round));
|
|
544
548
|
return rounds.slice(-count);
|
|
545
549
|
}
|
|
546
550
|
|
|
@@ -767,7 +771,8 @@ export function resolveProviderBaseURL({
|
|
|
767
771
|
if (fromConfig) return fromConfig;
|
|
768
772
|
const fromPatch = providerURLFromPatch(patchText, provider);
|
|
769
773
|
if (fromPatch) return fromPatch;
|
|
770
|
-
|
|
774
|
+
const launch = String(launchURL || "").trim();
|
|
775
|
+
if (provider === "deepseek-account" && launch) return launch;
|
|
771
776
|
const fallback = fallbackProviderURL(provider);
|
|
772
777
|
if (fallback) return fallback;
|
|
773
778
|
return catalogProviderURL(provider, catalog);
|
|
@@ -947,7 +952,7 @@ export function pointerOnlyText(pieces) {
|
|
|
947
952
|
const lines = tools.map((piece) => {
|
|
948
953
|
const cmd = piece.command || piece.name || "tool";
|
|
949
954
|
const call = Number.isInteger(piece.callSeq) ? piece.callSeq : "?";
|
|
950
|
-
const flag = piece.isError ? " error" : "";
|
|
955
|
+
const flag = isToolFailure(piece.text, piece.isError) ? " error" : "";
|
|
951
956
|
return `* ${piece.name || "tool"} "${cmd}" (seq ${call} -> result ${piece.seq}${flag})`;
|
|
952
957
|
});
|
|
953
958
|
const userBlock = users.length
|
|
@@ -1033,9 +1038,15 @@ function toolBlock(span) {
|
|
|
1033
1038
|
if (command) prefix.push(command);
|
|
1034
1039
|
const exitCode = failed ? exitCodeOf(text) : null;
|
|
1035
1040
|
if (exitCode !== null) prefix.push(`[exit code: ${exitCode}]`);
|
|
1036
|
-
if (small) return { block: [...prefix, ...lines.map((line) => `│ ${line}`)].join("\n"), failed, needsBackup: false, hardLines, ordinaryLines };
|
|
1037
|
-
if (failed)
|
|
1038
|
-
|
|
1041
|
+
if (small) return { block: [...prefix, ...lines.map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete: true, needsBackup: false, hardLines, ordinaryLines };
|
|
1042
|
+
if (failed) {
|
|
1043
|
+
const original = text.trim();
|
|
1044
|
+
const diagnostic = usefulLines(original.startsWith("{") ? original : failureBody(text));
|
|
1045
|
+
const kept = failureKeptLines(text);
|
|
1046
|
+
const failureDiagnosticsComplete = diagnostic.length === kept.length && diagnostic.every((line, index) => line === kept[index]);
|
|
1047
|
+
return { block: [...prefix, ...kept.map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete, needsBackup: true, hardLines, ordinaryLines };
|
|
1048
|
+
}
|
|
1049
|
+
return { block: [...prefix, ...unknownKeptLines(text).map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete: true, needsBackup: true, hardLines, ordinaryLines };
|
|
1039
1050
|
}
|
|
1040
1051
|
|
|
1041
1052
|
export function fitFailure(block, budget) {
|
|
@@ -1051,7 +1062,7 @@ export function fitFailure(block, budget) {
|
|
|
1051
1062
|
|
|
1052
1063
|
export function toolExcerpt(spans, backupId = "") {
|
|
1053
1064
|
const tools = (spans || []).filter((span) => sourceRole(span.role) === "tool" && String(span.text || "").trim().length > 0);
|
|
1054
|
-
if (tools.length === 0) return { text: "", omitted: 0, needsBackup: false, excerpts: [], stats: [] };
|
|
1065
|
+
if (tools.length === 0) return { text: "", omitted: 0, needsBackup: false, failureDiagnosticsComplete: true, incompleteFailures: [], excerpts: [], stats: [] };
|
|
1055
1066
|
let lastFailure = -1;
|
|
1056
1067
|
tools.forEach((span, index) => {
|
|
1057
1068
|
if (isToolFailure(span.text, span.isError)) lastFailure = index;
|
|
@@ -1067,15 +1078,22 @@ export function toolExcerpt(spans, backupId = "") {
|
|
|
1067
1078
|
});
|
|
1068
1079
|
const chosen = [];
|
|
1069
1080
|
const stats = [];
|
|
1081
|
+
const incompleteFailures = new Map();
|
|
1070
1082
|
let used = 0;
|
|
1071
1083
|
let needsBackup = false;
|
|
1072
1084
|
for (const item of ranked) {
|
|
1073
1085
|
const built = toolBlock(item.span);
|
|
1086
|
+
const incomplete = (reason) => {
|
|
1087
|
+
if (built.failed && !incompleteFailures.has(item.index)) incompleteFailures.set(item.index, { sourceId: item.span.id, reason });
|
|
1088
|
+
};
|
|
1089
|
+
if (!built.failureDiagnosticsComplete) incomplete("failed-tool-lines-truncated");
|
|
1074
1090
|
let block = built.block;
|
|
1075
1091
|
if (item.index === lastFailure && used + block.length > TOOL_BUDGET) {
|
|
1092
|
+
incomplete("failed-tool-budget-truncated");
|
|
1076
1093
|
block = fitFailure(block, Math.max(0, TOOL_BUDGET - used));
|
|
1077
1094
|
}
|
|
1078
1095
|
if (used + block.length > TOOL_BUDGET) {
|
|
1096
|
+
incomplete("failed-tool-budget-omitted");
|
|
1079
1097
|
needsBackup = true;
|
|
1080
1098
|
stats.push({ sourceId: item.span.id, hardLines: built.hardLines, omittedOrdinaryToolLines: built.ordinaryLines.length });
|
|
1081
1099
|
continue;
|
|
@@ -1107,6 +1125,8 @@ export function toolExcerpt(spans, backupId = "") {
|
|
|
1107
1125
|
text: parts.join("\n"),
|
|
1108
1126
|
omitted: skipped,
|
|
1109
1127
|
needsBackup,
|
|
1128
|
+
failureDiagnosticsComplete: incompleteFailures.size === 0,
|
|
1129
|
+
incompleteFailures: [...incompleteFailures.values()],
|
|
1110
1130
|
excerpts: chosen.map(({ sourceId, text }) => ({ sourceId, text })),
|
|
1111
1131
|
stats,
|
|
1112
1132
|
};
|
|
@@ -1151,7 +1171,7 @@ function pointerLines(spans) {
|
|
|
1151
1171
|
if (Number.isInteger(span.seq)) {
|
|
1152
1172
|
const cmd = span.command || span.name || "tool";
|
|
1153
1173
|
const call = Number.isInteger(span.callSeq) ? span.callSeq : "?";
|
|
1154
|
-
const flag = span.isError ? " error" : "";
|
|
1174
|
+
const flag = isToolFailure(span.text, span.isError) ? " error" : "";
|
|
1155
1175
|
lines.push(`* ${span.name || "tool"} "${cmd}" (seq ${call} -> result ${span.seq}${flag})`);
|
|
1156
1176
|
continue;
|
|
1157
1177
|
}
|
|
@@ -1281,6 +1301,15 @@ export function buildCard(
|
|
|
1281
1301
|
requiredLines: hard,
|
|
1282
1302
|
keptLines: kept.map((item) => ({ sourceId: item.sourceId, text: item.line })),
|
|
1283
1303
|
});
|
|
1304
|
+
const incompleteFailures = tools.incompleteFailures.filter((item) => sourceById.has(item.sourceId));
|
|
1305
|
+
for (const item of incompleteFailures) {
|
|
1306
|
+
coverage.ok = false;
|
|
1307
|
+
coverage.missing.push({
|
|
1308
|
+
sourceId: item.sourceId,
|
|
1309
|
+
text: `failed-tool-diagnostics-not-retained (seq ${sourceById.get(item.sourceId).seq ?? "?"})`,
|
|
1310
|
+
reason: "failed-tool-diagnostics-not-retained",
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1284
1313
|
|
|
1285
1314
|
// 保留既有助手决策否决;助手正文既不进 Kept,也不能为其他来源提供证据。
|
|
1286
1315
|
for (const source of sources) {
|
|
@@ -1341,6 +1370,7 @@ export function buildCard(
|
|
|
1341
1370
|
|
|
1342
1371
|
return {
|
|
1343
1372
|
ok: coverage.ok,
|
|
1373
|
+
failureDiagnosticsComplete: incompleteFailures.length === 0,
|
|
1344
1374
|
scope: TRUNK_SCOPE,
|
|
1345
1375
|
hardLines: hard.map((item) => item.line),
|
|
1346
1376
|
hardCount: hard.length,
|
package/raw.js
CHANGED
|
@@ -62,12 +62,14 @@ function readExisting(file, toolCallId) {
|
|
|
62
62
|
|
|
63
63
|
// Restart recovery is read-only: never manufacture an original from the surface.
|
|
64
64
|
// Resolve only the requested ID, with the same permissions/meta checks as capture.
|
|
65
|
-
export function readRawToolResult({ dir, toolCallId, access = {} }) {
|
|
65
|
+
export function readRawToolResult({ dir, toolCallId, access = {}, tighten = true }) {
|
|
66
66
|
const file = join(dir, rawName(toolCallId));
|
|
67
67
|
if (!existsSync(file)) return { ok: false, reason: "missing-file", toolCallId };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
if (tighten) {
|
|
69
|
+
ensureShadowDir(dir, access);
|
|
70
|
+
tightenShadow(file, "file", access);
|
|
71
|
+
if (existsSync(`${file}.meta.json`)) tightenShadow(`${file}.meta.json`, "file", access);
|
|
72
|
+
}
|
|
71
73
|
return readExisting(file, toolCallId);
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { attachShadowAccessFuse, saveShadowTransaction } from "./shadow.js";
|
|
3
|
+
|
|
4
|
+
const managed = workerData?.spineWorker === 1;
|
|
5
|
+
if (managed) attachShadowAccessFuse(workerData.accessFuse);
|
|
6
|
+
|
|
7
|
+
async function handle(id, payload) {
|
|
8
|
+
try {
|
|
9
|
+
const result = saveShadowTransaction(payload);
|
|
10
|
+
parentPort.postMessage({ id, ok: true, result });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
parentPort.postMessage({ id, ok: false, error: {
|
|
13
|
+
name: error?.name || "Error",
|
|
14
|
+
message: error?.message || String(error),
|
|
15
|
+
code: error?.code,
|
|
16
|
+
} });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let queue = Promise.resolve();
|
|
21
|
+
if (workerData && !managed) handle(0, workerData);
|
|
22
|
+
parentPort.on("message", ({ id, payload }) => {
|
|
23
|
+
queue = queue.then(() => handle(id, payload));
|
|
24
|
+
});
|
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;
|
|
@@ -891,7 +973,7 @@ export function ensureShadowDir(dir, access = {}) {
|
|
|
891
973
|
}
|
|
892
974
|
|
|
893
975
|
export function tightenCreatedShadow(file, fd, access, { remember = true } = {}) {
|
|
894
|
-
if ((access.platform ?? process.platform) === "win32" && !access.runner && !
|
|
976
|
+
if ((access.platform ?? process.platform) === "win32" && !access.runner && !shadowAccessFused()) {
|
|
895
977
|
try {
|
|
896
978
|
if (shadowDirLocked(dirname(file))) {
|
|
897
979
|
const entry = accessEntry(file, "file");
|
|
@@ -1049,7 +1131,12 @@ function readSessionKey(file) {
|
|
|
1049
1131
|
}
|
|
1050
1132
|
}
|
|
1051
1133
|
|
|
1052
|
-
export function listPluginShadows(dir, {
|
|
1134
|
+
export function listPluginShadows(dir, {
|
|
1135
|
+
readdir = readdirSync,
|
|
1136
|
+
stat = statSync,
|
|
1137
|
+
onError,
|
|
1138
|
+
includeSessionKey = true,
|
|
1139
|
+
} = {}) {
|
|
1053
1140
|
let names = [];
|
|
1054
1141
|
try {
|
|
1055
1142
|
names = readdir(dir);
|
|
@@ -1069,7 +1156,7 @@ export function listPluginShadows(dir, { readdir = readdirSync, stat = statSync,
|
|
|
1069
1156
|
path,
|
|
1070
1157
|
size: info.size,
|
|
1071
1158
|
mtimeMs: info.mtimeMs,
|
|
1072
|
-
sessionKey: readSessionKey(path),
|
|
1159
|
+
sessionKey: includeSessionKey ? readSessionKey(path) : "",
|
|
1073
1160
|
});
|
|
1074
1161
|
} catch { onError?.(); }
|
|
1075
1162
|
}
|
|
@@ -1080,7 +1167,12 @@ function byAge(left, right) {
|
|
|
1080
1167
|
return left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name);
|
|
1081
1168
|
}
|
|
1082
1169
|
|
|
1083
|
-
export function listShadowEntries(dir, alive = processAlive, {
|
|
1170
|
+
export function listShadowEntries(dir, alive = processAlive, {
|
|
1171
|
+
readdir = readdirSync,
|
|
1172
|
+
stat = statSync,
|
|
1173
|
+
onError,
|
|
1174
|
+
includeSessionKey = false,
|
|
1175
|
+
} = {}) {
|
|
1084
1176
|
const entries = [];
|
|
1085
1177
|
let names = [];
|
|
1086
1178
|
try {
|
|
@@ -1113,7 +1205,7 @@ export function listShadowEntries(dir, alive = processAlive, { readdir = readdir
|
|
|
1113
1205
|
if (owner > 0) {
|
|
1114
1206
|
try { live = alive(owner) !== false; } catch { live = true; }
|
|
1115
1207
|
}
|
|
1116
|
-
for (const file of listPluginShadows(path, { readdir, stat, onError })) {
|
|
1208
|
+
for (const file of listPluginShadows(path, { readdir, stat, onError, includeSessionKey })) {
|
|
1117
1209
|
// The directory and its owner define deletion authority. Raw tool output
|
|
1118
1210
|
// is untrusted data and may itself start with a spine-session marker.
|
|
1119
1211
|
entries.push({ ...file, legacy: false, live, sessionKey: entry.name });
|
|
@@ -1130,6 +1222,65 @@ export function rotateShadow(options) {
|
|
|
1130
1222
|
return withShadowRoot(options.dir, () => rotateShadowLocked(options));
|
|
1131
1223
|
}
|
|
1132
1224
|
|
|
1225
|
+
// Complete backup transaction used by both the synchronous compatibility API
|
|
1226
|
+
// and the optional root-writer worker. The rotation lock stays held from the
|
|
1227
|
+
// full reconcile through ACL-protected body publication; callers must not move
|
|
1228
|
+
// only the scan to a worker and release the lock before the write.
|
|
1229
|
+
export function saveShadowTransaction({
|
|
1230
|
+
shadow,
|
|
1231
|
+
file,
|
|
1232
|
+
body,
|
|
1233
|
+
required = false,
|
|
1234
|
+
protectPaths = [],
|
|
1235
|
+
sessionKey = "default",
|
|
1236
|
+
cap,
|
|
1237
|
+
access = {},
|
|
1238
|
+
rotationIO = {},
|
|
1239
|
+
} = {}) {
|
|
1240
|
+
ensureShadowDir(shadow, access);
|
|
1241
|
+
const dir = sessionDir(shadow, sessionKey);
|
|
1242
|
+
ensureShadowDir(dir, access);
|
|
1243
|
+
return withShadowRoot(shadow, () => {
|
|
1244
|
+
const owner = join(dir, "owner");
|
|
1245
|
+
const ownerExisted = existsSync(owner);
|
|
1246
|
+
let ownerWritten = false;
|
|
1247
|
+
try {
|
|
1248
|
+
writeShadowOwner(dir, access, shadow);
|
|
1249
|
+
ownerWritten = true;
|
|
1250
|
+
const incoming = Buffer.byteLength(body);
|
|
1251
|
+
const rotation = rotateShadow({
|
|
1252
|
+
...rotationIO,
|
|
1253
|
+
dir: shadow,
|
|
1254
|
+
incomingBytes: incoming,
|
|
1255
|
+
protectPath: file,
|
|
1256
|
+
protectPaths,
|
|
1257
|
+
sessionKey,
|
|
1258
|
+
cap,
|
|
1259
|
+
});
|
|
1260
|
+
if (!rotation.ok) {
|
|
1261
|
+
if (rotation.reason) {
|
|
1262
|
+
const incomplete = rotation.reason === "scan-incomplete";
|
|
1263
|
+
const error = Object.assign(new Error(incomplete
|
|
1264
|
+
? "spine: shadow capacity scan is incomplete; backup refused; leaving the span"
|
|
1265
|
+
: "spine: shadow capacity measurement is invalid; backup refused; leaving the span"), {
|
|
1266
|
+
code: incomplete ? "ESHADOWSCAN" : "ESHADOWMETER",
|
|
1267
|
+
});
|
|
1268
|
+
if (required) throw error;
|
|
1269
|
+
return { ok: false, skipped: true, reason: rotation.reason, rotation };
|
|
1270
|
+
}
|
|
1271
|
+
if (required) throw new Error("spine: shadow backup is full; leaving the span");
|
|
1272
|
+
return { ok: false, skipped: true, reason: "full", rotation };
|
|
1273
|
+
}
|
|
1274
|
+
writeShadowFile(file, body, access, { root: shadow });
|
|
1275
|
+
if (!existsSync(file)) throw new Error("spine: shadow backup was not written");
|
|
1276
|
+
return { ok: true, file, rotation };
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
if (ownerWritten && !ownerExisted) discardShadowFiles([owner], error);
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1133
1284
|
function rotationTime(now) {
|
|
1134
1285
|
try {
|
|
1135
1286
|
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
|
|