dsh-live-compact 1.0.8 → 1.1.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/README.md +89 -10
- package/cooldown.js +106 -0
- package/gate.js +3 -3
- package/index.js +336 -111
- package/package.json +6 -1
- package/plan.js +375 -79
- package/raw.js +91 -18
- package/shadow.js +265 -3
package/raw.js
CHANGED
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* An existing file must match the first meta hash. A mismatch is not a backup.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { existsSync,
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
discardShadowFiles,
|
|
13
|
+
ensureShadowDir,
|
|
14
|
+
tightenShadow,
|
|
15
|
+
writeShadowFile,
|
|
16
|
+
writeShadowOwner,
|
|
17
|
+
} from "./shadow.js";
|
|
11
18
|
|
|
12
19
|
export const PRUNE_MARKER = "[... tool result middle pruned ...]";
|
|
13
20
|
|
|
@@ -90,40 +97,106 @@ export function rawExcerptText(surface, record) {
|
|
|
90
97
|
// 正文已经带修剪标记、又没有旧文件时,不把修剪后的文本当成原文。
|
|
91
98
|
// Write once per toolCallId. An existing file is verified, never overwritten.
|
|
92
99
|
// Pruned text with no earlier file is not stored as the original.
|
|
93
|
-
export function captureToolResult({
|
|
94
|
-
|
|
100
|
+
export function captureToolResult({
|
|
101
|
+
dir,
|
|
102
|
+
toolCallId,
|
|
103
|
+
text,
|
|
104
|
+
turn,
|
|
105
|
+
name,
|
|
106
|
+
pathLabel,
|
|
107
|
+
access = {},
|
|
108
|
+
}) {
|
|
109
|
+
if (!toolCallId) {
|
|
110
|
+
return { ok: false, reason: "missing-id", toolCallId: "" };
|
|
111
|
+
}
|
|
112
|
+
|
|
95
113
|
const body = String(text ?? "");
|
|
96
114
|
const file = join(dir, rawName(toolCallId));
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
const metaPath = `${file}.meta.json`;
|
|
116
|
+
|
|
117
|
+
if (!existsSync(file) && body.includes(PRUNE_MARKER)) {
|
|
118
|
+
return { ok: false, reason: "already-pruned", toolCallId };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const readProtectedExisting = () => {
|
|
122
|
+
tightenShadow(file, "file", access);
|
|
123
|
+
if (existsSync(metaPath)) {
|
|
124
|
+
tightenShadow(metaPath, "file", access);
|
|
125
|
+
}
|
|
126
|
+
return readExisting(file, toolCallId);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let createdRaw = false;
|
|
130
|
+
|
|
99
131
|
try {
|
|
100
|
-
|
|
132
|
+
ensureShadowDir(dir, access);
|
|
133
|
+
|
|
134
|
+
if (existsSync(file)) return readProtectedExisting();
|
|
135
|
+
|
|
101
136
|
const bytes = Buffer.from(body, "utf8");
|
|
102
137
|
const hash = contentHash(bytes);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
|
|
139
|
+
writeShadowFile(file, bytes, access, { exclusive: true });
|
|
140
|
+
createdRaw = true;
|
|
141
|
+
|
|
142
|
+
writeShadowFile(
|
|
143
|
+
metaPath,
|
|
144
|
+
JSON.stringify({
|
|
107
145
|
toolCallId,
|
|
108
146
|
bytes: bytes.length,
|
|
109
147
|
sha256: hash,
|
|
110
148
|
turn: turn ?? null,
|
|
111
149
|
name: name || "",
|
|
112
150
|
label: pathLabel || "",
|
|
113
|
-
}),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
}),
|
|
152
|
+
access,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
existed: false,
|
|
158
|
+
path: file,
|
|
159
|
+
bytes: bytes.length,
|
|
160
|
+
sha256: hash,
|
|
161
|
+
toolCallId,
|
|
162
|
+
label: pathLabel || "",
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (createdRaw) discardShadowFiles([file], error);
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
error?.code === "ESHADOWACCESS" ||
|
|
169
|
+
error?.code === "ESHADOWCLEANUP"
|
|
170
|
+
) {
|
|
118
171
|
throw error;
|
|
119
172
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
173
|
+
|
|
174
|
+
if (error?.code === "EEXIST" && existsSync(file)) {
|
|
175
|
+
return readProtectedExisting();
|
|
176
|
+
}
|
|
177
|
+
|
|
123
178
|
return { ok: false, reason: "unwritable", toolCallId };
|
|
124
179
|
}
|
|
125
180
|
}
|
|
126
181
|
|
|
182
|
+
export function captureOwnedToolResult(options) {
|
|
183
|
+
const { dir, access = {} } = options;
|
|
184
|
+
const owner = join(dir, "owner");
|
|
185
|
+
const ownerExisted = existsSync(owner);
|
|
186
|
+
let ownerWritten = false;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
writeShadowOwner(dir, access);
|
|
190
|
+
ownerWritten = true;
|
|
191
|
+
return captureToolResult(options);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (ownerWritten && !ownerExisted) {
|
|
194
|
+
discardShadowFiles([owner], error);
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
127
200
|
export function pointerFor(record, readFile = readFileSync) {
|
|
128
201
|
const id = record?.toolCallId || "";
|
|
129
202
|
if (!id) return "";
|
package/shadow.js
CHANGED
|
@@ -7,9 +7,27 @@
|
|
|
7
7
|
* Delete legacy root files, then dead sessions, then old files of the current session.
|
|
8
8
|
* Never delete the current card's file or another live session. ENOENT counts as already freed.
|
|
9
9
|
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
10
11
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import {
|
|
13
|
+
chmodSync,
|
|
14
|
+
appendFileSync,
|
|
15
|
+
closeSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
openSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
readSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
statSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
31
|
|
|
14
32
|
const PLUGIN_NAME = /^(?:(?:cut|official|shadow)-\d+-[0-9a-f]{8}|raw-[0-9a-f]{16}|\d+-[0-9a-f]{8}-[0-9a-f]{8}|\d+-[0-9a-f]{8,16})\.txt$/;
|
|
15
33
|
const SESSION_MARK = /^# spine-session (\S+)/;
|
|
@@ -52,6 +70,7 @@ export function shadowFileBody(sessionKey, body) {
|
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
export const STATS_NAME = "spine-stats.json";
|
|
73
|
+
export const CUTS_NAME = "spine-cuts.jsonl";
|
|
55
74
|
|
|
56
75
|
export function readCutStats(dir) {
|
|
57
76
|
try {
|
|
@@ -60,9 +79,79 @@ export function readCutStats(dir) {
|
|
|
60
79
|
cuts: Number(parsed.cuts) || 0,
|
|
61
80
|
savedTokens: Number(parsed.savedTokens) || 0,
|
|
62
81
|
rewriteTokens: Number(parsed.rewriteTokens) || 0,
|
|
82
|
+
drains: Number(parsed.drains) || 0,
|
|
63
83
|
};
|
|
64
84
|
} catch {
|
|
65
|
-
return { cuts: 0, savedTokens: 0, rewriteTokens: 0 };
|
|
85
|
+
return { cuts: 0, savedTokens: 0, rewriteTokens: 0, drains: 0 };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function drainRewrite(notes) {
|
|
90
|
+
const cuts = (notes || []).filter((note) => note?.kind === "cut" && note.saved > 0);
|
|
91
|
+
if (!cuts.length) return 0;
|
|
92
|
+
const later = cuts.slice(1).reduce((sum, note) => sum + (note.saved || 0), 0);
|
|
93
|
+
return Math.max(0, (cuts[0].afterPrefix || 0) - later);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function flushDrainStats(dir, notes, meta = {}) {
|
|
97
|
+
const cuts = (notes || []).filter((note) => note?.kind === "cut" && note.saved > 0);
|
|
98
|
+
const charged = drainRewrite(notes);
|
|
99
|
+
const saved = cuts.reduce((sum, note) => sum + note.saved, 0);
|
|
100
|
+
if (cuts.length) recordDrainStats(dir, { cuts: cuts.length, saved, rewrite: charged });
|
|
101
|
+
const lines = (notes || []).map((note, index) => JSON.stringify({
|
|
102
|
+
v: 1,
|
|
103
|
+
ts: new Date().toISOString(),
|
|
104
|
+
sid: meta.sid || "",
|
|
105
|
+
kind: note.kind || "cut",
|
|
106
|
+
pass: index,
|
|
107
|
+
drain: meta.drain === true,
|
|
108
|
+
saved: note.saved || 0,
|
|
109
|
+
rewriteCharged: note.kind === "cut" && index === (notes || []).findIndex((item) => item?.kind === "cut") ? charged : 0,
|
|
110
|
+
tailBeyondKept: note.tailBeyondKept || 0,
|
|
111
|
+
prefix: note.prefix || 0,
|
|
112
|
+
afterPrefix: note.afterPrefix || 0,
|
|
113
|
+
tokens: note.tokens || 0,
|
|
114
|
+
total: note.total || 0,
|
|
115
|
+
gate: note.gate || "cut",
|
|
116
|
+
}));
|
|
117
|
+
if (!lines.length) return charged;
|
|
118
|
+
try {
|
|
119
|
+
appendFileSync(join(dir, CUTS_NAME), `${lines.join("\n")}\n`);
|
|
120
|
+
} catch {}
|
|
121
|
+
return charged;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function recordDrainStats(dir, { cuts = 0, saved = 0, rewrite = 0 } = {}) {
|
|
125
|
+
if (!(cuts > 0) || !(saved > 0)) return null;
|
|
126
|
+
try {
|
|
127
|
+
const file = join(dir, STATS_NAME);
|
|
128
|
+
const lock = join(dir, "spine-stats.lock");
|
|
129
|
+
const tmp = `${file}.${process.pid}-${randomBytes(8).toString("hex")}.tmp`;
|
|
130
|
+
let fd;
|
|
131
|
+
try {
|
|
132
|
+
fd = openSync(lock, "wx");
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const prev = readCutStats(dir);
|
|
138
|
+
const locked = {
|
|
139
|
+
cuts: prev.cuts + cuts,
|
|
140
|
+
savedTokens: prev.savedTokens + saved,
|
|
141
|
+
rewriteTokens: prev.rewriteTokens + rewrite,
|
|
142
|
+
drains: (Number(prev.drains) || 0) + 1,
|
|
143
|
+
updatedAt: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
writeFileSync(tmp, JSON.stringify(locked));
|
|
146
|
+
renameSync(tmp, file);
|
|
147
|
+
return locked;
|
|
148
|
+
} finally {
|
|
149
|
+
try { unlinkSync(tmp); } catch {}
|
|
150
|
+
closeSync(fd);
|
|
151
|
+
try { unlinkSync(lock); } catch {}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
66
155
|
}
|
|
67
156
|
}
|
|
68
157
|
|
|
@@ -99,6 +188,179 @@ export function recordCutStats(dir, { saved = 0, rewrite = 0 } = {}) {
|
|
|
99
188
|
}
|
|
100
189
|
}
|
|
101
190
|
|
|
191
|
+
export function shadowAccessPlan(platform = process.platform, userSid = "") {
|
|
192
|
+
if (platform === "win32") {
|
|
193
|
+
if (!/^S-\d+(?:-\d+)+$/.test(userSid)) {
|
|
194
|
+
throw new Error("spine: current Windows user SID is required");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
kind: "acl",
|
|
199
|
+
usesChmodOnly: false,
|
|
200
|
+
inheritance: "remove",
|
|
201
|
+
grant: [
|
|
202
|
+
`*${userSid}:(OI)(CI)F`,
|
|
203
|
+
"*S-1-5-18:(OI)(CI)F",
|
|
204
|
+
"*S-1-5-32-544:(OI)(CI)F",
|
|
205
|
+
],
|
|
206
|
+
sddl:
|
|
207
|
+
`D:P(A;OICI;FA;;;${userSid})` +
|
|
208
|
+
"(A;OICI;FA;;;SY)" +
|
|
209
|
+
"(A;OICI;FA;;;BA)",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
kind: "mode",
|
|
215
|
+
usesChmodOnly: true,
|
|
216
|
+
dirMode: 0o700,
|
|
217
|
+
fileMode: 0o600,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function defaultShadowAccessRunner({ path, kind, platform, execFile = execFileSync }) {
|
|
222
|
+
if (platform !== "win32") {
|
|
223
|
+
const plan = shadowAccessPlan(platform);
|
|
224
|
+
chmodSync(path, kind === "dir" ? plan.dirMode : plan.fileMode);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const identity = execFile(
|
|
229
|
+
"whoami.exe",
|
|
230
|
+
["/user", "/fo", "csv", "/nh"],
|
|
231
|
+
{ encoding: "utf8", windowsHide: true },
|
|
232
|
+
);
|
|
233
|
+
const match = identity.match(/,"(S-\d+(?:-\d+)+)"\s*$/i);
|
|
234
|
+
if (!match) {
|
|
235
|
+
throw new Error("spine: cannot determine current Windows user SID");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const plan = shadowAccessPlan("win32", match[1]);
|
|
239
|
+
const target = resolve(path);
|
|
240
|
+
const temp = mkdtempSync(join(tmpdir(), "spine-acl-"));
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const aclFile = join(temp, "access.acl");
|
|
244
|
+
const aclBody = `\ufeff${basename(target)}\r\n${plan.sddl}\r\n`;
|
|
245
|
+
|
|
246
|
+
writeFileSync(aclFile, Buffer.from(aclBody, "utf16le"), {
|
|
247
|
+
flag: "wx",
|
|
248
|
+
mode: 0o600,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
execFile(
|
|
252
|
+
"icacls.exe",
|
|
253
|
+
[dirname(target), "/restore", aclFile, "/q"],
|
|
254
|
+
{ stdio: "pipe", windowsHide: true },
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return true;
|
|
258
|
+
} finally {
|
|
259
|
+
rmSync(temp, { recursive: true, force: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function tightenShadow(path, kind = "dir", access = {}) {
|
|
264
|
+
const platform = access.platform ?? process.platform;
|
|
265
|
+
const runner = access.runner ?? defaultShadowAccessRunner;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
if (runner({ path, kind, platform }) !== true) {
|
|
269
|
+
throw new Error("permission runner did not confirm success");
|
|
270
|
+
}
|
|
271
|
+
} catch (cause) {
|
|
272
|
+
const error = new Error(
|
|
273
|
+
"spine: shadow permission action failed; backup refused",
|
|
274
|
+
{ cause },
|
|
275
|
+
);
|
|
276
|
+
error.code = "ESHADOWACCESS";
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function ensureShadowDir(dir, access = {}) {
|
|
282
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
283
|
+
tightenShadow(dir, "dir", access);
|
|
284
|
+
return dir;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function discardShadowFiles(files, cause) {
|
|
288
|
+
const errors = [];
|
|
289
|
+
|
|
290
|
+
for (const file of files) {
|
|
291
|
+
try {
|
|
292
|
+
unlinkSync(file);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error?.code !== "ENOENT") errors.push(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (errors.length > 0) {
|
|
299
|
+
const error = new AggregateError(
|
|
300
|
+
[cause, ...errors],
|
|
301
|
+
"spine: shadow write failed and cleanup failed",
|
|
302
|
+
);
|
|
303
|
+
error.code = cause?.code || "ESHADOWCLEANUP";
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function writeShadowFile(
|
|
309
|
+
file,
|
|
310
|
+
body,
|
|
311
|
+
access = {},
|
|
312
|
+
{ exclusive = false } = {},
|
|
313
|
+
) {
|
|
314
|
+
const dir = dirname(file);
|
|
315
|
+
ensureShadowDir(dir, access);
|
|
316
|
+
|
|
317
|
+
const target = exclusive
|
|
318
|
+
? file
|
|
319
|
+
: join(
|
|
320
|
+
dir,
|
|
321
|
+
`.${basename(file)}-${randomBytes(8).toString("hex")}.tmp`,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
let fd = null;
|
|
325
|
+
let created = false;
|
|
326
|
+
let protectedPath = target;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
fd = openSync(target, "wx", 0o600);
|
|
330
|
+
created = true;
|
|
331
|
+
|
|
332
|
+
if (!exclusive) {
|
|
333
|
+
renameSync(target, file);
|
|
334
|
+
protectedPath = file;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
tightenShadow(protectedPath, "file", access);
|
|
338
|
+
writeFileSync(fd, body);
|
|
339
|
+
|
|
340
|
+
const opened = fd;
|
|
341
|
+
fd = null;
|
|
342
|
+
closeSync(opened);
|
|
343
|
+
return file;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (fd !== null) {
|
|
346
|
+
try {
|
|
347
|
+
closeSync(fd);
|
|
348
|
+
} catch {}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (created) discardShadowFiles([protectedPath], error);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function writeShadowOwner(dir, access = {}) {
|
|
357
|
+
return writeShadowFile(
|
|
358
|
+
join(dir, "owner"),
|
|
359
|
+
String(process.pid),
|
|
360
|
+
access,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
102
364
|
export function shadowMaxBytes(env = process.env) {
|
|
103
365
|
const bytes = Number(env.SPINE_SHADOW_MAX_BYTES);
|
|
104
366
|
if (Number.isFinite(bytes) && bytes > 0) return Math.floor(bytes);
|