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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live-compact",
3
- "version": "1.1.4",
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.map(shorten);
261
- const marked = useful.filter((line) => FAILURE_LINE.test(line)).slice(-8).map(shorten);
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 rounds = finishedToolRounds(events).filter((round) => !roundHasLongRead(events, round));
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
- if (provider === "deepseek-account" && launchURL) return String(launchURL);
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) return { block: [...prefix, ...failureKeptLines(text).map((line) => `│ ${line}`)].join("\n"), failed, needsBackup: true, hardLines, ordinaryLines };
1038
- return { block: [...prefix, ...unknownKeptLines(text).map((line) => `│ ${line}`)].join("\n"), failed, needsBackup: true, hardLines, ordinaryLines };
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 (/[a-z]{2,}-[a-z0-9]+-\d+/i.test(line) || /方案\s*[A-Za-z0-9]/.test(line)) {
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
- ensureShadowDir(dir, access);
69
- tightenShadow(file, "file", access);
70
- if (existsSync(`${file}.meta.json`)) tightenShadow(`${file}.meta.json`, "file", access);
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?.code === "ESHADOWBUSY") return { ok: false, reason: "unwritable", toolCallId, retryable: error?.code === "ESHADOWBUSY" };
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)) return readProtectedExisting();
165
-
166
- const bytes = Buffer.from(body, "utf8");
167
- const hash = contentHash(bytes);
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?.code === "ESHADOWBUSY" };
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
+ });