@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 +1 -1
- package/src/hub/index.mjs +40 -14
- package/src/job-contracts.mjs +17 -7
- package/src/runtime-identity.mjs +1 -1
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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.
|
|
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') {
|
|
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) ----------
|
package/src/job-contracts.mjs
CHANGED
|
@@ -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
|
|
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
|
|
98
|
-
provider: candidate?.provider
|
|
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
|
|
111
|
-
fallback_reason: trace.fallback_reason
|
|
112
|
-
escalation_reason: trace.escalation_reason
|
|
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(
|
|
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,
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -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.
|
|
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([
|