@ran-sh/dsh-crew 2.2.9 → 2.2.10

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -1249,10 +1249,11 @@ export function installHudSegment({ home = homedir() } = {}) {
1249
1249
 
1250
1250
  // The paths Crew has recorded as its own in this Claude host: the marketplace records
1251
1251
  // it wrote (under the current and pre-rename names, in settings and in the CLI's own
1252
- // cache), the constant an older installer used, and the checkout this module runs
1253
- // from. Only these are grounds for removing anything: a directory that merely happens
1254
- // to be named `dsh-crew` belongs to whoever made it, and a path that is not recorded
1255
- // here cannot be identified as Crew's, so it is left alone.
1252
+ // cache) and the constant an older installer used. Only these are grounds for removing
1253
+ // anything: a directory that merely happens to be named `dsh-crew` belongs to whoever
1254
+ // made it, the module's own checkout is not evidence (an uninstall may run from a
1255
+ // checkout this host never recorded), and a path that is not recorded here is left
1256
+ // alone.
1256
1257
  function crewOwnedRoots({ home, settings, markets, installed }) {
1257
1258
  const roots = [];
1258
1259
  const push = (value) => { if (typeof value === 'string' && value.trim() && !roots.includes(value.trim())) roots.push(value.trim()); };
@@ -1266,7 +1267,6 @@ function crewOwnedRoots({ home, settings, markets, installed }) {
1266
1267
  for (const entry of entries) push(entry?.installPath);
1267
1268
  }
1268
1269
  push(join(home, '.config', 'dsh-crew', 'marketplace'));
1269
- push(ROOT);
1270
1270
  return roots;
1271
1271
  }
1272
1272
 
@@ -2785,6 +2785,9 @@ export async function npxLogs({ args = [], log = console.log } = {}) {
2785
2785
  const keep = Number(args[1]);
2786
2786
  const result = pruneCrewTempLogs(Number.isInteger(keep) && keep > 0 ? { keepRuns: keep } : {});
2787
2787
  log(`✓ Crew diagnostics pruned: removed ${result.removed.length} file(s); kept ${result.kept.length} run(s)`);
2788
+ if (result.failed?.length) {
2789
+ log(`- ${result.failed.length} file(s) could not be deleted (a running process may hold them); they are counted in neither number and the next prune retries`);
2790
+ }
2788
2791
  return { ok: true, ...result };
2789
2792
  }
2790
2793
 
package/src/log-prune.mjs CHANGED
@@ -26,7 +26,14 @@ const LOG_FAMILIES = [
26
26
  { prefix: 'dsh-crew-dsh-crew-3210-', suffix: '.out.log', keep: 10 },
27
27
  { prefix: 'dsh-crew-web-', suffix: '.out.log', keep: 20 },
28
28
  { prefix: 'dsh-official-web-', suffix: '.out.log', keep: 20 },
29
- ];
29
+ ].map((family) => ({
30
+ ...family,
31
+ // An `.err.log` whose `.out.log` sibling is gone (or was never written) is still a
32
+ // Crew diagnostic, so each family recognizes its error files as prunable runs too.
33
+ errStamp: (name) => name.startsWith(family.prefix) && name.endsWith('.err.log')
34
+ ? name.slice(family.prefix.length, name.length - '.err.log'.length) || null
35
+ : null,
36
+ }));
30
37
 
31
38
  // `dsh-crew-<profile>-<port>-<stamp>.out.log` -> the stamp identifies one run, and
32
39
  // its .err.log sibling must live or die with it.
@@ -51,14 +58,28 @@ export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns = null, maxAgeD
51
58
  const runs = new Map();
52
59
  for (const name of names) {
53
60
  const stamp = runStamp(name, family);
54
- if (!stamp) continue;
61
+ if (stamp) {
62
+ const file = join(tempDir, name);
63
+ let mtimeMs = 0;
64
+ try { mtimeMs = statSync(file).mtimeMs; } catch { continue; }
65
+ const run = runs.get(stamp) ?? { stamp, files: [], newest: 0 };
66
+ run.files.push(name);
67
+ run.newest = Math.max(run.newest, mtimeMs);
68
+ runs.set(stamp, run);
69
+ continue;
70
+ }
71
+ // An orphan `.err.log` (its `.out.log` sibling already pruned or never written)
72
+ // is still a Crew diagnostic: prune it on its own stamp so a half-written run
73
+ // cannot outlive every bound.
74
+ const errStamp = family.errStamp(name);
75
+ if (!errStamp) continue;
55
76
  const file = join(tempDir, name);
56
77
  let mtimeMs = 0;
57
78
  try { mtimeMs = statSync(file).mtimeMs; } catch { continue; }
58
- const run = runs.get(stamp) ?? { stamp, files: [], newest: 0 };
79
+ const run = runs.get(errStamp) ?? { stamp: errStamp, files: [], newest: 0 };
59
80
  run.files.push(name);
60
81
  run.newest = Math.max(run.newest, mtimeMs);
61
- runs.set(stamp, run);
82
+ runs.set(errStamp, run);
62
83
  }
63
84
  const ordered = [...runs.values()].sort((left, right) => right.newest - left.newest);
64
85
  const keep = keepOverride ?? family.keep;
@@ -73,15 +94,23 @@ export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns = null, maxAgeD
73
94
  // bound exists to stop. A burst of eleven young runs still keeps ten.
74
95
  if ((tooOld || tooMany) && !tooRecentToTouch) {
75
96
  for (const name of run.files) removed.push(name);
76
- const errName = run.files[0].replace(/\.out\.log$/, '.err.log');
77
- if (names.includes(errName)) removed.push(errName);
78
97
  } else {
79
98
  kept.push(run.stamp);
80
99
  }
81
100
  });
82
101
  }
83
- for (const name of removed) {
84
- try { rmSync(join(tempDir, name), { force: true }); } catch { /* a locked file stays until the next pass */ }
102
+ // `removed` lists what is actually gone. A locked file (Windows holds a log open
103
+ // while its process runs) is reported in `failed` instead of pretending a deletion
104
+ // happened: the next pass tries again, and the operator is not told a number that
105
+ // is not true.
106
+ const failed = [];
107
+ for (const name of [...new Set(removed)]) {
108
+ try { rmSync(join(tempDir, name), { force: true }); } catch { failed.push(name); }
85
109
  }
86
- return { ok: true, removed: [...new Set(removed)].sort(), kept: kept.sort() };
110
+ return {
111
+ ok: failed.length === 0 ? true : 'partial',
112
+ removed: removed.filter((name) => !failed.includes(name)).sort(),
113
+ kept: kept.sort(),
114
+ ...(failed.length ? { failed: failed.sort() } : {}),
115
+ };
87
116
  }
@@ -36,7 +36,7 @@ export {
36
36
  // included in the identity contract.
37
37
  const RUNTIME_ID = randomUUID();
38
38
 
39
- export const RUNTIME_VERSION = '2.2.9';
39
+ export const RUNTIME_VERSION = '2.2.10';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -251,7 +251,12 @@ export function reprojectRuntimeModelCallability(snapshot, { enabled_roles = {},
251
251
  const roles = {};
252
252
  for (const role of ['worker', 'reviewer']) {
253
253
  if (enabled_roles?.[role] === false) {
254
- roles[role] = { state: 'NOT_APPLICABLE', reason_code: 'ROLE_DISABLED', selected: null, last_success: null };
254
+ // The hub may still record a selected route for a role this session disables;
255
+ // keeping that route here (rather than nulling it) is what lets the canonical
256
+ // validation below accept the reprojection. Nulling it manufactured a
257
+ // selection mismatch against the unchanged outer snapshot and read the whole
258
+ // contract as DEGRADED for an intentionally disabled role.
259
+ roles[role] = { state: 'NOT_APPLICABLE', reason_code: 'ROLE_DISABLED', selected: snapshot[role]?.selected ?? null, last_success: null };
255
260
  } else if (sourceProjection.enabled_roles?.[role] === true) {
256
261
  roles[role] = structuredClone(sourceProjection.roles[role]);
257
262
  } else {
@@ -275,9 +280,14 @@ export function reprojectRuntimeModelCallability(snapshot, { enabled_roles = {},
275
280
  projection: projected,
276
281
  runtime: snapshot.runtime,
277
282
  expectedEnabledRoles: projected.enabled_roles,
283
+ // Expected selections are the routes the snapshot RECORDED, for disabled roles
284
+ // too: the canonical rule is "a disabled role keeps its route", and expecting
285
+ // null here would contradict the very projection this function just built — the
286
+ // mismatch then discarded a healthy worker's evidence and read the session as
287
+ // degraded for a role that was intentionally switched off.
278
288
  expectedSelections: {
279
- worker: enabled_roles.worker === true ? snapshot.worker?.selected ?? null : null,
280
- reviewer: enabled_roles.reviewer === true ? snapshot.reviewer?.selected ?? null : null,
289
+ worker: snapshot.worker?.selected ?? null,
290
+ reviewer: snapshot.reviewer?.selected ?? null,
281
291
  },
282
292
  now,
283
293
  });