@ran-sh/dsh-crew 0.4.0 → 0.4.2

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": "@ran-sh/dsh-crew",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
package/src/hub/index.mjs CHANGED
@@ -166,6 +166,10 @@ export class WorkerRegistry { constructor(ctx) {
166
166
  return v;
167
167
  }
168
168
 
169
+ list() {
170
+ return [...this.jobs.values()].map((job) => this.view(job));
171
+ }
172
+
169
173
  publish() {
170
174
  this.shard.publish([...this.jobs.values()].map((j) => this.view(j)));
171
175
  }
@@ -297,8 +301,14 @@ export class WorkerRegistry { constructor(ctx) {
297
301
  result: null, error: null, stopReason: null, handle: null, waiters: [],
298
302
  delivery_complete: false, delivery_missing: [], delivery_metadata: null,
299
303
  outcome: null,
300
- lastAssistantText: null,
301
- };
304
+ lastAssistantText: null, handle_dispose_promise: null, disposeHandle: null,
305
+ };
306
+ const disposeJobHandle = async () => {
307
+ if (!job.handle) return;
308
+ job.handle_dispose_promise ??= Promise.resolve().then(() => job.handle.dispose());
309
+ await job.handle_dispose_promise;
310
+ };
311
+ job.disposeHandle = disposeJobHandle;
302
312
  // Read-only pre-run snapshot (async, never blocks dispatch): the audit
303
313
  // only needs the before-state by the time the worker finishes. Non-repos
304
314
  // degrade to { kind:'no-git' } instead of failing the job.
@@ -386,7 +396,10 @@ export class WorkerRegistry { constructor(ctx) {
386
396
  job.endedAt = new Date().toISOString();
387
397
  this.publish();
388
398
  for (const waiter of job.waiters.splice(0)) waiter();
389
- try { await job.handle?.dispose(); } catch {}
399
+ try { await disposeJobHandle(); } catch (error) {
400
+ job.cleanup_warning = `agent cleanup failed: ${error?.message ?? String(error)}`;
401
+ this.publish();
402
+ }
390
403
  }, timeoutMs);
391
404
  job.timeoutHandle.unref?.();
392
405
  }
@@ -394,11 +407,15 @@ export class WorkerRegistry { constructor(ctx) {
394
407
  .catch((err) => {
395
408
  if (job.status === 'running') job.status = 'failed';
396
409
  job.error = job.error ?? (err?.message ?? String(err));
397
- })
410
+ })
398
411
  .finally(async () => {
399
412
  if (job.timeoutHandle) clearTimeout(job.timeoutHandle);
400
- job.endedAt = new Date().toISOString();
401
- job.currentTool = null;
413
+ job.endedAt = new Date().toISOString();
414
+ job.currentTool = null;
415
+ let handleCleanupWarning = job.cleanup_warning ?? null;
416
+ try { await disposeJobHandle(); } catch (error) {
417
+ handleCleanupWarning = `agent cleanup failed: ${error?.message ?? String(error)}`;
418
+ }
402
419
  // Delivery completeness is separate from execution status: a job can
403
420
  // be done yet fail to report Diff/Tests/Risks (or Review sections for
404
421
  // an automatic review). Parse whatever final message the worker
@@ -446,7 +463,10 @@ export class WorkerRegistry { constructor(ctx) {
446
463
  if (job.isolatedWorkspace) {
447
464
  const cleanup = await cleanupIsolatedWorkspace(job.isolatedWorkspace).catch((error) => ({ ok: false, error: error?.message ?? String(error) }));
448
465
  job.workspace_retained = cleanup.ok !== true;
449
- job.cleanup_warning = cleanup.ok === true ? null : cleanup.error ?? 'worktree cleanup failed';
466
+ const workspaceCleanupWarning = cleanup.ok === true ? null : cleanup.error ?? 'worktree cleanup failed';
467
+ job.cleanup_warning = [handleCleanupWarning, workspaceCleanupWarning].filter(Boolean).join('; ') || null;
468
+ } else {
469
+ job.cleanup_warning = handleCleanupWarning;
450
470
  }
451
471
  this.publish();
452
472
  for (const w of job.waiters.splice(0)) w();
@@ -467,24 +487,30 @@ export class WorkerRegistry { constructor(ctx) {
467
487
  return job;
468
488
  }
469
489
 
470
- async cancel(id) {
490
+ async cancel(id) {
471
491
  const job = this.jobs.get(id);
472
492
  if (!job) return undefined;
473
493
  if (job.status === 'running') {
474
494
  job.status = 'cancelled';
475
495
  job.phase = JOB_PHASES.CANCELLED;
476
496
  job.error = 'cancelled by request';
477
- try { await job.handle?.dispose(); } catch {}
497
+ try { await job.disposeHandle?.(); } catch (error) {
498
+ job.cleanup_warning = `agent cleanup failed: ${error?.message ?? String(error)}`;
499
+ }
478
500
  this.publish();
479
501
  }
480
502
  return job;
481
503
  }
482
504
 
483
- async dispose() {
484
- for (const job of this.jobs.values()) {
485
- if (job.status === 'running') { try { await job.handle?.dispose(); } catch {} }
486
- }
487
- }
505
+ async dispose() {
506
+ for (const job of this.jobs.values()) {
507
+ if (job.status === 'running') {
508
+ try { await job.disposeHandle?.(); } catch (error) {
509
+ job.cleanup_warning = `agent cleanup failed: ${error?.message ?? String(error)}`;
510
+ }
511
+ }
512
+ }
513
+ }
488
514
  }
489
515
 
490
516
  // ---------- loopback route helpers (pattern from dsh-noema) ----------
@@ -87,15 +87,20 @@ function compactSelectionTrace(attempts) {
87
87
  if (!Array.isArray(attempts)) return [];
88
88
  return attempts.slice(0, 16).map((attempt) => {
89
89
  const trace = attempt?.selection_trace ?? {};
90
- const selected = trace.selected ?? (
90
+ const rawSelected = trace.selected ?? (
91
91
  attempt?.provider || attempt?.model
92
92
  ? { provider: attempt?.provider ?? null, model: attempt?.model ?? null, source: attempt?.selection_source ?? null }
93
93
  : null
94
94
  );
95
+ const selected = rawSelected ? {
96
+ provider: boundedText(rawSelected.provider, 200),
97
+ model: boundedText(rawSelected.model, 300),
98
+ source: boundedText(rawSelected.source, 100),
99
+ } : null;
95
100
  const candidates = Array.isArray(trace.ordered_candidates)
96
101
  ? trace.ordered_candidates.slice(0, 32).map((candidate) => ({
97
- model: candidate?.model ?? null,
98
- provider: candidate?.provider ?? null,
102
+ model: boundedText(candidate?.model, 300),
103
+ provider: boundedText(candidate?.provider, 200),
99
104
  status: String(candidate?.status ?? 'CANDIDATE').toUpperCase(),
100
105
  ...(candidate?.reason ? { reason: boundedText(candidate.reason, 200) } : {}),
101
106
  }))
@@ -107,9 +112,9 @@ function compactSelectionTrace(attempts) {
107
112
  selected_model: selected?.model ?? null,
108
113
  candidates,
109
114
  fallback_chain: trace.fallback_reason ? [boundedText(trace.fallback_reason, 200)] : [],
110
- decision_reason: selected?.source ?? attempt?.selection_source ?? null,
111
- fallback_reason: trace.fallback_reason ?? null,
112
- escalation_reason: trace.escalation_reason ?? null,
115
+ decision_reason: boundedText(selected?.source ?? attempt?.selection_source, 200),
116
+ fallback_reason: boundedText(trace.fallback_reason, 200),
117
+ escalation_reason: boundedText(trace.escalation_reason, 200),
113
118
  };
114
119
  });
115
120
  }
@@ -140,6 +145,9 @@ export function buildEvidenceEnvelope(view = {}) {
140
145
  const errorMessage = boundedText(view.error, 1000);
141
146
  const changedFiles = boundedStrings(changedFilesFromView(view), { count: 120, length: 500 });
142
147
  const status = evidenceStatus(view);
148
+ const selectionAttempts = Array.isArray(view.child_attempts) && view.child_attempts.length > 0
149
+ ? view.child_attempts
150
+ : (view.selection_trace || view.provider || view.model ? [view] : []);
143
151
  return {
144
152
  schema_version: JOB_CONTRACT_SCHEMA_VERSION,
145
153
  job_id: view.id ?? null,
@@ -154,7 +162,7 @@ export function buildEvidenceEnvelope(view = {}) {
154
162
  delivery_complete: outcome.delivery?.complete === true,
155
163
  review_verdict: review?.verdict ?? null,
156
164
  },
157
- selection_trace: compactSelectionTrace(view.child_attempts),
165
+ selection_trace: compactSelectionTrace(selectionAttempts),
158
166
  changed_files: changedFiles,
159
167
  changes: boundedStrings(outcome.changes),
160
168
  tests: boundedTests(outcome.tests),
@@ -202,6 +210,8 @@ export function projectWorkflowView(view, { detail = 'compact', afterSequence =
202
210
  review: _review,
203
211
  events: _legacyEvents,
204
212
  child_attempts: _childAttempts,
213
+ selection_trace: _rawSelectionTrace,
214
+ selection_source: _rawSelectionSource,
205
215
  result: _rawResult,
206
216
  workspace_diff: _workspaceDiff,
207
217
  reasonDetail: _reasonDetail,
@@ -8,7 +8,7 @@
8
8
  // Keep this module pure and dependency-free so Hub, MCP and tests all use the
9
9
  // exact same compatibility rules.
10
10
 
11
- export const RUNTIME_VERSION = '0.4.0';
11
+ export const RUNTIME_VERSION = '0.4.2';
12
12
  export const HUB_PROTOCOL_VERSION = 1;
13
13
 
14
14
  export const HUB_CAPABILITIES = Object.freeze([