@ran-sh/dsh-crew 1.4.0 → 1.5.1
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/.claude-plugin/plugin.json +1 -1
- package/lib/client.js +5926 -5451
- package/package.json +1 -1
- package/src/client/index.tsx +159 -7
- package/src/hub/entry.mjs +6 -0
- package/src/hub/index.mjs +50 -0
- package/src/install/install-legacy.mjs +739 -717
- package/src/install/install.mjs +13 -5
- package/src/install/npx-lifecycle.mjs +16 -2
- package/src/jobs.mjs +17 -0
- package/src/model-routing.mjs +250 -147
- package/src/model-schedule.mjs +207 -0
- package/src/multimodal.mjs +0 -0
- package/src/policy.mjs +69 -62
- package/src/release-in-use.mjs +110 -0
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +6 -1
package/src/install/install.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import * as legacy from './install-legacy.mjs';
|
|
15
15
|
import { normalizeModelPriority } from '../model-routing.mjs';
|
|
16
|
+
import { normalizeModelSchedule } from '../model-schedule.mjs';
|
|
16
17
|
import { runtimeActivationMetadata } from '../runtime-controls.mjs';
|
|
17
18
|
import {
|
|
18
19
|
CONFIG_SCHEMA_VERSION,
|
|
@@ -93,12 +94,12 @@ function legacySnapshotFrom(config = {}) {
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
function attachReadMetadata(config, { version, canonical }) {
|
|
97
|
-
return {
|
|
97
|
+
function attachReadMetadata(config, { version, canonical }) {
|
|
98
|
+
return {
|
|
98
99
|
...config,
|
|
99
100
|
config_schema_version: version,
|
|
100
|
-
config_authority: canonical ? 'canonical' : 'legacy-import',
|
|
101
|
-
config_migration_required: !canonical || version < CONFIG_SCHEMA_VERSION,
|
|
101
|
+
config_authority: canonical ? 'canonical' : 'legacy-import',
|
|
102
|
+
config_migration_required: !canonical || version < CONFIG_SCHEMA_VERSION,
|
|
102
103
|
config_activation: runtimeActivationMetadata(),
|
|
103
104
|
};
|
|
104
105
|
}
|
|
@@ -210,6 +211,10 @@ function mergeCanonicalPatch(current, patch) {
|
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
213
|
if (validObject(patch.legacy)) next.legacy = { ...next.legacy, ...patch.legacy };
|
|
214
|
+
// The peak/off-peak schedule replaces as a whole: its parts (windows, weekdays,
|
|
215
|
+
// offset, per-model modes) only make sense together, so a partial merge could
|
|
216
|
+
// pair one operator's windows with another's model list.
|
|
217
|
+
if (validObject(patch.model_schedule)) next.model_schedule = normalizeModelSchedule(patch.model_schedule);
|
|
213
218
|
|
|
214
219
|
// v0.3 still has one provider-mode control. A direct canonical patch is
|
|
215
220
|
// allowed, but it cannot manufacture separate worker/reviewer authorities.
|
|
@@ -397,10 +402,13 @@ export function writeGlobalConfig(patch, { configFile = GLOBAL_CONFIG_FILE } = {
|
|
|
397
402
|
? mergeLegacyPatchIntoCanonical(current, compatibilityView, candidate, patch)
|
|
398
403
|
: { ...candidate, legacy: legacySnapshotFrom(candidate) };
|
|
399
404
|
|
|
405
|
+
// `model_schedule` is canonical-only (it has no flat mirror), so it must open
|
|
406
|
+
// the canonical merge path on its own rather than ride along with worker/review.
|
|
400
407
|
const hasCanonicalPatch = validObject(patch?.worker)
|
|
401
408
|
|| validObject(patch?.review)
|
|
402
409
|
|| validObject(patch?.execution)
|
|
403
|
-
|| validObject(patch?.legacy)
|
|
410
|
+
|| validObject(patch?.legacy)
|
|
411
|
+
|| validObject(patch?.model_schedule);
|
|
404
412
|
if (hasCanonicalPatch) normalized = mergeCanonicalPatch(normalized, patch);
|
|
405
413
|
|
|
406
414
|
if (!validObject(normalized.legacy)) normalized.legacy = legacySnapshotFrom(candidate);
|
|
@@ -42,6 +42,7 @@ import { homedir } from 'node:os';
|
|
|
42
42
|
import * as realInstaller from './install.mjs';
|
|
43
43
|
import { samePayloadContent, capturePayloadContent } from './payload-content.mjs';
|
|
44
44
|
import { crewDshHome, crewProfileDir } from './install.mjs';
|
|
45
|
+
import { liveReleaseClaims } from '../release-in-use.mjs';
|
|
45
46
|
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
46
47
|
import {
|
|
47
48
|
ensureOfficialWebIntegration,
|
|
@@ -1085,9 +1086,15 @@ function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
|
1085
1086
|
const releasesDir = crewReleasesDir({ home });
|
|
1086
1087
|
if (!existsSync(releasesDir)) return;
|
|
1087
1088
|
const removed = [];
|
|
1089
|
+
// A running Hub keeps executing the release it started from and re-reads
|
|
1090
|
+
// several modules from disk on every request, so removing that release breaks
|
|
1091
|
+
// those routes with no recovery but a restart. Protect what live processes
|
|
1092
|
+
// claim, not just what the pointer names.
|
|
1093
|
+
const claimed = liveReleaseClaims({ home }).map((dir) => resolve(dir));
|
|
1094
|
+
const live = new Set([...(protect ? [resolve(protect)] : []), ...claimed]);
|
|
1088
1095
|
const dirs = readdirSync(releasesDir)
|
|
1089
1096
|
.map((name) => join(releasesDir, name))
|
|
1090
|
-
.filter((dir) => (!pointer || dir !== pointer.path) && (
|
|
1097
|
+
.filter((dir) => (!pointer || dir !== pointer.path) && !live.has(resolve(dir)));
|
|
1091
1098
|
const incomplete = [];
|
|
1092
1099
|
const complete = [];
|
|
1093
1100
|
for (const dir of dirs) {
|
|
@@ -3434,7 +3441,14 @@ export async function runNpxCli({
|
|
|
3434
3441
|
else if (command === 'releases') result = await actions.releases({ args, log });
|
|
3435
3442
|
else if (command === 'rollback') result = await actions.rollback({ version: args?.[0], args, log });
|
|
3436
3443
|
else result = await actions[command]({ log });
|
|
3437
|
-
|
|
3444
|
+
// Surface why the command failed. Actions return a structured failure rather
|
|
3445
|
+
// than throwing, so without this the CLI exits 1 having printed nothing —
|
|
3446
|
+
// `dsh-crew rollback` with no version was a silent failure.
|
|
3447
|
+
if (result?.ok === false) {
|
|
3448
|
+
error(`dsh-crew ${command} failed: ${result.error ?? result.code ?? 'unknown error'}`);
|
|
3449
|
+
return 1;
|
|
3450
|
+
}
|
|
3451
|
+
return 0;
|
|
3438
3452
|
} catch (err) {
|
|
3439
3453
|
error(`dsh-crew ${command} failed: ${err?.message ?? err}`);
|
|
3440
3454
|
return 1;
|
package/src/jobs.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY }
|
|
|
19
19
|
import { buildOutcome, JOB_PHASES } from './workflow.mjs';
|
|
20
20
|
import { raceWaiters } from './removable-waiter.mjs';
|
|
21
21
|
import { buildDirectSelectionTrace } from './model-routing.mjs';
|
|
22
|
+
import { normalizeModelSchedule, scheduleAdmission } from './model-schedule.mjs';
|
|
22
23
|
import { appendSessionOrigin } from './session-origins.mjs';
|
|
24
|
+
import { readGlobalConfig } from './install/install.mjs';
|
|
23
25
|
|
|
24
26
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
27
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
@@ -27,6 +29,13 @@ const CREW_DSH_HOME = join(homedir(), '.config', 'dsh-crew', 'harness');
|
|
|
27
29
|
const STATUS_FILE = join(CONFIG_DIR, 'status.json');
|
|
28
30
|
const CORDIS = join(ROOT, 'worker.cordis.yml');
|
|
29
31
|
|
|
32
|
+
// Read the operator's schedule once per dispatch. An unreadable config must not
|
|
33
|
+
// block a job: a missing or damaged file means no restriction, matching how the
|
|
34
|
+
// router treats an absent schedule.
|
|
35
|
+
function readModelSchedule() {
|
|
36
|
+
try { return normalizeModelSchedule(readGlobalConfig().model_schedule); } catch { return normalizeModelSchedule(undefined); }
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export const TIERS = {
|
|
31
40
|
flash: { model: 'deepseek-v4-flash', label: 'V4 Flash' },
|
|
32
41
|
pro: { model: 'deepseek-v4-pro', label: 'V4 Pro' },
|
|
@@ -110,6 +119,14 @@ export function startJob({
|
|
|
110
119
|
}) {
|
|
111
120
|
const tierInfo = TIERS[tier];
|
|
112
121
|
if (!tierInfo) throw new Error(`unknown tier "${tier}" (expected: ${Object.keys(TIERS).join(', ')})`);
|
|
122
|
+
// Standalone dispatch names its provider/model directly rather than walking a
|
|
123
|
+
// catalog, but a peak `block` still applies: without this the schedule would be
|
|
124
|
+
// silently ignored on every standalone job.
|
|
125
|
+
const standaloneRef = { provider: 'deepseek-official', model: tierInfo.model };
|
|
126
|
+
const standaloneAt = new Date();
|
|
127
|
+
if (scheduleAdmission(readModelSchedule(), standaloneRef, standaloneAt)?.mode === 'block') {
|
|
128
|
+
throw Object.assign(new Error('MODEL_BLOCKED_PEAK'), { policyCode: 'MODEL_BLOCKED_PEAK' });
|
|
129
|
+
}
|
|
113
130
|
if (!ROLES[role]) throw new Error(`unknown role "${role}" (expected: ${Object.keys(ROLES).join(', ')})`);
|
|
114
131
|
if (!['off', 'high', 'max'].includes(effort)) throw new Error(`unknown effort "${effort}" (expected: off, high, max)`);
|
|
115
132
|
if (!existsSync(CORDIS)) throw new Error(`worker overlay not found at ${CORDIS}`);
|