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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live-compact",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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/stand-down.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 test/p1-fable-review.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
|
|
|
@@ -730,11 +734,18 @@ export function providerURLFromPatch(text, provider) {
|
|
|
730
734
|
|
|
731
735
|
// 内置目录只在这家没有自己的地址时使用。调用方传入 {id, baseURL},这里不读包。
|
|
732
736
|
// Use a built-in catalog URL only when this provider has no URL of its own.
|
|
733
|
-
export function catalogProviderURL(provider, catalog) {
|
|
737
|
+
export function catalogProviderURL(provider, catalog, model = "") {
|
|
734
738
|
const name = String(provider || "");
|
|
735
739
|
if (!name || !catalog) return "";
|
|
736
740
|
const list = Array.isArray(catalog) ? catalog : [];
|
|
737
741
|
const match = list.find((entry) => entry?.id === name || entry?.provider === name);
|
|
742
|
+
const modelName = String(model || "");
|
|
743
|
+
if (match && modelName) {
|
|
744
|
+
const models = Array.isArray(match.models) ? match.models : [];
|
|
745
|
+
const modelMatch = models.find((entry) => entry?.id === modelName || entry?.model === modelName);
|
|
746
|
+
const modelURL = modelMatch?.baseURL || modelMatch?.baseUrl || "";
|
|
747
|
+
if (modelURL) return String(modelURL);
|
|
748
|
+
}
|
|
738
749
|
return String(match?.baseURL || match?.baseUrl || "");
|
|
739
750
|
}
|
|
740
751
|
|
|
@@ -744,6 +755,7 @@ export function catalogProviderURL(provider, catalog) {
|
|
|
744
755
|
// Official DeepSeek and the built-in catalog apply only when the profile has no URL. Empty stays unresolved.
|
|
745
756
|
export function resolveProviderBaseURL({
|
|
746
757
|
provider = "",
|
|
758
|
+
model = "",
|
|
747
759
|
headerURL = "",
|
|
748
760
|
listed = [],
|
|
749
761
|
layers = [],
|
|
@@ -767,10 +779,11 @@ export function resolveProviderBaseURL({
|
|
|
767
779
|
if (fromConfig) return fromConfig;
|
|
768
780
|
const fromPatch = providerURLFromPatch(patchText, provider);
|
|
769
781
|
if (fromPatch) return fromPatch;
|
|
770
|
-
|
|
782
|
+
const launch = String(launchURL || "").trim();
|
|
783
|
+
if (provider === "deepseek-account" && launch) return launch;
|
|
771
784
|
const fallback = fallbackProviderURL(provider);
|
|
772
785
|
if (fallback) return fallback;
|
|
773
|
-
return catalogProviderURL(provider, catalog);
|
|
786
|
+
return catalogProviderURL(provider, catalog, model);
|
|
774
787
|
}
|
|
775
788
|
|
|
776
789
|
export function resolveEngine({ provider = "", baseURL = "", env = process.env } = {}) {
|
|
@@ -947,7 +960,7 @@ export function pointerOnlyText(pieces) {
|
|
|
947
960
|
const lines = tools.map((piece) => {
|
|
948
961
|
const cmd = piece.command || piece.name || "tool";
|
|
949
962
|
const call = Number.isInteger(piece.callSeq) ? piece.callSeq : "?";
|
|
950
|
-
const flag = piece.isError ? " error" : "";
|
|
963
|
+
const flag = isToolFailure(piece.text, piece.isError) ? " error" : "";
|
|
951
964
|
return `* ${piece.name || "tool"} "${cmd}" (seq ${call} -> result ${piece.seq}${flag})`;
|
|
952
965
|
});
|
|
953
966
|
const userBlock = users.length
|
|
@@ -1033,9 +1046,15 @@ function toolBlock(span) {
|
|
|
1033
1046
|
if (command) prefix.push(command);
|
|
1034
1047
|
const exitCode = failed ? exitCodeOf(text) : null;
|
|
1035
1048
|
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
|
-
|
|
1049
|
+
if (small) return { block: [...prefix, ...lines.map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete: true, needsBackup: false, hardLines, ordinaryLines };
|
|
1050
|
+
if (failed) {
|
|
1051
|
+
const original = text.trim();
|
|
1052
|
+
const diagnostic = usefulLines(original.startsWith("{") ? original : failureBody(text));
|
|
1053
|
+
const kept = failureKeptLines(text);
|
|
1054
|
+
const failureDiagnosticsComplete = diagnostic.length === kept.length && diagnostic.every((line, index) => line === kept[index]);
|
|
1055
|
+
return { block: [...prefix, ...kept.map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete, needsBackup: true, hardLines, ordinaryLines };
|
|
1056
|
+
}
|
|
1057
|
+
return { block: [...prefix, ...unknownKeptLines(text).map((line) => `│ ${line}`)].join("\n"), failed, failureDiagnosticsComplete: true, needsBackup: true, hardLines, ordinaryLines };
|
|
1039
1058
|
}
|
|
1040
1059
|
|
|
1041
1060
|
export function fitFailure(block, budget) {
|
|
@@ -1051,7 +1070,7 @@ export function fitFailure(block, budget) {
|
|
|
1051
1070
|
|
|
1052
1071
|
export function toolExcerpt(spans, backupId = "") {
|
|
1053
1072
|
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: [] };
|
|
1073
|
+
if (tools.length === 0) return { text: "", omitted: 0, needsBackup: false, failureDiagnosticsComplete: true, incompleteFailures: [], excerpts: [], stats: [] };
|
|
1055
1074
|
let lastFailure = -1;
|
|
1056
1075
|
tools.forEach((span, index) => {
|
|
1057
1076
|
if (isToolFailure(span.text, span.isError)) lastFailure = index;
|
|
@@ -1067,15 +1086,22 @@ export function toolExcerpt(spans, backupId = "") {
|
|
|
1067
1086
|
});
|
|
1068
1087
|
const chosen = [];
|
|
1069
1088
|
const stats = [];
|
|
1089
|
+
const incompleteFailures = new Map();
|
|
1070
1090
|
let used = 0;
|
|
1071
1091
|
let needsBackup = false;
|
|
1072
1092
|
for (const item of ranked) {
|
|
1073
1093
|
const built = toolBlock(item.span);
|
|
1094
|
+
const incomplete = (reason) => {
|
|
1095
|
+
if (built.failed && !incompleteFailures.has(item.index)) incompleteFailures.set(item.index, { sourceId: item.span.id, reason });
|
|
1096
|
+
};
|
|
1097
|
+
if (!built.failureDiagnosticsComplete) incomplete("failed-tool-lines-truncated");
|
|
1074
1098
|
let block = built.block;
|
|
1075
1099
|
if (item.index === lastFailure && used + block.length > TOOL_BUDGET) {
|
|
1100
|
+
incomplete("failed-tool-budget-truncated");
|
|
1076
1101
|
block = fitFailure(block, Math.max(0, TOOL_BUDGET - used));
|
|
1077
1102
|
}
|
|
1078
1103
|
if (used + block.length > TOOL_BUDGET) {
|
|
1104
|
+
incomplete("failed-tool-budget-omitted");
|
|
1079
1105
|
needsBackup = true;
|
|
1080
1106
|
stats.push({ sourceId: item.span.id, hardLines: built.hardLines, omittedOrdinaryToolLines: built.ordinaryLines.length });
|
|
1081
1107
|
continue;
|
|
@@ -1107,6 +1133,8 @@ export function toolExcerpt(spans, backupId = "") {
|
|
|
1107
1133
|
text: parts.join("\n"),
|
|
1108
1134
|
omitted: skipped,
|
|
1109
1135
|
needsBackup,
|
|
1136
|
+
failureDiagnosticsComplete: incompleteFailures.size === 0,
|
|
1137
|
+
incompleteFailures: [...incompleteFailures.values()],
|
|
1110
1138
|
excerpts: chosen.map(({ sourceId, text }) => ({ sourceId, text })),
|
|
1111
1139
|
stats,
|
|
1112
1140
|
};
|
|
@@ -1151,7 +1179,7 @@ function pointerLines(spans) {
|
|
|
1151
1179
|
if (Number.isInteger(span.seq)) {
|
|
1152
1180
|
const cmd = span.command || span.name || "tool";
|
|
1153
1181
|
const call = Number.isInteger(span.callSeq) ? span.callSeq : "?";
|
|
1154
|
-
const flag = span.isError ? " error" : "";
|
|
1182
|
+
const flag = isToolFailure(span.text, span.isError) ? " error" : "";
|
|
1155
1183
|
lines.push(`* ${span.name || "tool"} "${cmd}" (seq ${call} -> result ${span.seq}${flag})`);
|
|
1156
1184
|
continue;
|
|
1157
1185
|
}
|
|
@@ -1165,6 +1193,17 @@ function hasProtectedToolFact(line) {
|
|
|
1165
1193
|
return /必须|不要|禁止|口令|方案|[a-z]{2,}-[a-z0-9]+-\d+|\bpinned\s+to\b/i.test(line);
|
|
1166
1194
|
}
|
|
1167
1195
|
|
|
1196
|
+
// A model/version/region token by itself is ordinary prose. Only retain an
|
|
1197
|
+
// assistant line as an unresolved decision when the line contains a decision
|
|
1198
|
+
// verb or the explicit Chinese plan form; this avoids treating `us-east-1`
|
|
1199
|
+
// and `claude-3-5` as commands that block the cut.
|
|
1200
|
+
function unresolvedAssistantDecision(line) {
|
|
1201
|
+
const text = String(line || "");
|
|
1202
|
+
if (/方案\s*[A-Za-z0-9]/.test(text)) return true;
|
|
1203
|
+
if (/(?:决定|采用|选用|选择|固定为|改用|切换到|定为|使用|确定)/.test(text)) return true;
|
|
1204
|
+
return /\b(?:pinned\s+to|decid(?:e|ed|es)|adopt(?:ed|s)?|selected|choose|chosen|will\s+use|must\s+use)\b/i.test(text);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1168
1207
|
function normalizeSourceLine(role, text) {
|
|
1169
1208
|
return (role === "tool" ? text.replace(/^[ \t]*\d+:[ \t]*/, "") : text).trim();
|
|
1170
1209
|
}
|
|
@@ -1281,13 +1320,22 @@ export function buildCard(
|
|
|
1281
1320
|
requiredLines: hard,
|
|
1282
1321
|
keptLines: kept.map((item) => ({ sourceId: item.sourceId, text: item.line })),
|
|
1283
1322
|
});
|
|
1323
|
+
const incompleteFailures = tools.incompleteFailures.filter((item) => sourceById.has(item.sourceId));
|
|
1324
|
+
for (const item of incompleteFailures) {
|
|
1325
|
+
coverage.ok = false;
|
|
1326
|
+
coverage.missing.push({
|
|
1327
|
+
sourceId: item.sourceId,
|
|
1328
|
+
text: `failed-tool-diagnostics-not-retained (seq ${sourceById.get(item.sourceId).seq ?? "?"})`,
|
|
1329
|
+
reason: "failed-tool-diagnostics-not-retained",
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1284
1332
|
|
|
1285
1333
|
// 保留既有助手决策否决;助手正文既不进 Kept,也不能为其他来源提供证据。
|
|
1286
1334
|
for (const source of sources) {
|
|
1287
1335
|
if (source.role !== "assistant") continue;
|
|
1288
1336
|
source.text.split(/\r?\n/).forEach((line) => {
|
|
1289
1337
|
if (liveLines.has(line.trim())) return;
|
|
1290
|
-
if (
|
|
1338
|
+
if (unresolvedAssistantDecision(line)) {
|
|
1291
1339
|
coverage.ok = false;
|
|
1292
1340
|
coverage.missing.push({ sourceId: source.id, text: line, reason: "assistant-decision-not-retained" });
|
|
1293
1341
|
}
|
|
@@ -1341,12 +1389,18 @@ export function buildCard(
|
|
|
1341
1389
|
|
|
1342
1390
|
return {
|
|
1343
1391
|
ok: coverage.ok,
|
|
1392
|
+
failureDiagnosticsComplete: incompleteFailures.length === 0,
|
|
1344
1393
|
scope: TRUNK_SCOPE,
|
|
1345
1394
|
hardLines: hard.map((item) => item.line),
|
|
1395
|
+
hardFacts: hard.map((item) => ({ role: item.role, line: item.line })),
|
|
1346
1396
|
hardCount: hard.length,
|
|
1347
1397
|
omittedHard,
|
|
1348
1398
|
deferred,
|
|
1349
1399
|
missing,
|
|
1400
|
+
missingFacts: coverage.missing.map((item) => ({
|
|
1401
|
+
role: sourceById.get(item.sourceId)?.role || "unknown",
|
|
1402
|
+
line: item.text,
|
|
1403
|
+
})),
|
|
1350
1404
|
omittedOrdinaryToolLines: omittedOrdinaryToolLines,
|
|
1351
1405
|
uncovered: split.uncovered,
|
|
1352
1406
|
keptChars: used,
|
package/raw.js
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
|
|
21
21
|
export const PRUNE_MARKER = "[... tool result middle pruned ...]";
|
|
22
22
|
|
|
23
|
+
function transientCaptureError(error) {
|
|
24
|
+
return ["ESHADOWBUSY", "EAGAIN", "EBUSY", "EINTR", "EIO"].includes(error?.code);
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
export function contentHash(text) {
|
|
24
28
|
const hash = createHash("sha256");
|
|
25
29
|
hash.update(typeof text === "string" ? Buffer.from(text, "utf8") : text);
|
|
@@ -62,12 +66,14 @@ function readExisting(file, toolCallId) {
|
|
|
62
66
|
|
|
63
67
|
// Restart recovery is read-only: never manufacture an original from the surface.
|
|
64
68
|
// Resolve only the requested ID, with the same permissions/meta checks as capture.
|
|
65
|
-
export function readRawToolResult({ dir, toolCallId, access = {} }) {
|
|
69
|
+
export function readRawToolResult({ dir, toolCallId, access = {}, tighten = true }) {
|
|
66
70
|
const file = join(dir, rawName(toolCallId));
|
|
67
71
|
if (!existsSync(file)) return { ok: false, reason: "missing-file", toolCallId };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
if (tighten) {
|
|
73
|
+
ensureShadowDir(dir, access);
|
|
74
|
+
tightenShadow(file, "file", access);
|
|
75
|
+
if (existsSync(`${file}.meta.json`)) tightenShadow(`${file}.meta.json`, "file", access);
|
|
76
|
+
}
|
|
71
77
|
return readExisting(file, toolCallId);
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -120,7 +126,7 @@ export function captureToolResult(options) {
|
|
|
120
126
|
ensureShadowDir(dir, access);
|
|
121
127
|
} catch (error) {
|
|
122
128
|
if (error?.code === "ESHADOWACCESS" || error?.code === "ESHADOWCLEANUP") throw error;
|
|
123
|
-
return { ok: false, reason: "unwritable", toolCallId };
|
|
129
|
+
return { ok: false, reason: "unwritable", toolCallId, retryable: transientCaptureError(error) };
|
|
124
130
|
}
|
|
125
131
|
let entered = false;
|
|
126
132
|
try {
|
|
@@ -129,7 +135,7 @@ export function captureToolResult(options) {
|
|
|
129
135
|
return captureToolResultLocked({ ...options, root });
|
|
130
136
|
});
|
|
131
137
|
} catch (error) {
|
|
132
|
-
if (!entered || error
|
|
138
|
+
if (!entered || transientCaptureError(error)) return { ok: false, reason: "unwritable", toolCallId, retryable: transientCaptureError(error) };
|
|
133
139
|
throw error; // In particular, preserve exceptions from failed raw cleanup.
|
|
134
140
|
}
|
|
135
141
|
}
|
|
@@ -151,6 +157,8 @@ function captureToolResultLocked({
|
|
|
151
157
|
const body = String(text ?? "");
|
|
152
158
|
const file = join(dir, rawName(toolCallId));
|
|
153
159
|
const metaPath = `${file}.meta.json`;
|
|
160
|
+
const bytes = Buffer.from(body, "utf8");
|
|
161
|
+
const hash = contentHash(bytes);
|
|
154
162
|
|
|
155
163
|
if (!existsSync(file) && body.includes(PRUNE_MARKER)) {
|
|
156
164
|
return { ok: false, reason: "already-pruned", toolCallId };
|
|
@@ -161,10 +169,24 @@ function captureToolResultLocked({
|
|
|
161
169
|
let createdRaw = false;
|
|
162
170
|
|
|
163
171
|
try {
|
|
164
|
-
if (existsSync(file))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
if (existsSync(file)) {
|
|
173
|
+
const existing = readProtectedExisting();
|
|
174
|
+
if (!existing.ok) {
|
|
175
|
+
// A raw without its meta marker is an incomplete publication. When
|
|
176
|
+
// the current event still has the original body, discard the orphan
|
|
177
|
+
// and rebuild the pair; a malformed/valid meta file remains fail-closed.
|
|
178
|
+
if (existing.reason === "missing-meta" && !body.includes(PRUNE_MARKER)) {
|
|
179
|
+
discardShadowFiles([file, metaPath], new Error("incomplete raw/meta pair"));
|
|
180
|
+
} else {
|
|
181
|
+
return existing;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (existing.ok && body.includes(PRUNE_MARKER)) return existing;
|
|
185
|
+
if (existing.ok && (existing.sha256 !== hash || existing.bytes !== bytes.length)) {
|
|
186
|
+
return { ok: false, reason: "duplicate-tool-call-id", toolCallId, retryable: false };
|
|
187
|
+
}
|
|
188
|
+
if (existing.ok) return existing;
|
|
189
|
+
}
|
|
168
190
|
|
|
169
191
|
writeShadowFile(file, bytes, access, { exclusive: true, root });
|
|
170
192
|
createdRaw = true;
|
|
@@ -206,7 +228,7 @@ function captureToolResultLocked({
|
|
|
206
228
|
return readProtectedExisting();
|
|
207
229
|
}
|
|
208
230
|
|
|
209
|
-
return { ok: false, reason: "unwritable", toolCallId, retryable: error
|
|
231
|
+
return { ok: false, reason: "unwritable", toolCallId, retryable: transientCaptureError(error) };
|
|
210
232
|
}
|
|
211
233
|
}
|
|
212
234
|
|
|
@@ -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
|
+
});
|