@ran-sh/dsh-crew 2.2.3 → 2.2.4
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/package.json +1 -1
- package/src/config-readiness.mjs +1 -0
- package/src/hub/index.mjs +8 -0
- package/src/install/install-legacy.mjs +5 -0
- package/src/install/npx-lifecycle.mjs +20 -1
- package/src/install/windows-startup.mjs +16 -7
- package/src/log-prune.mjs +70 -0
- package/src/provider-health.mjs +10 -2
- package/src/readiness-matrix.mjs +17 -0
- package/src/runtime-identity.mjs +1 -1
- package/src/runtime-readiness-snapshot.mjs +10 -7
- package/windows/start-dsh-crew.ps1 +30 -1
package/package.json
CHANGED
package/src/config-readiness.mjs
CHANGED
package/src/hub/index.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { inspectProviderProfile, readProviderDeclarations, readProviderMateriali
|
|
|
43
43
|
import { hasHarnessDefaultBlock, inspectProviderSettings, readHarnessDefault, readProviderSettingsDeclarations, readProviderSettingsMaterialization } from '../provider-settings-store.mjs';
|
|
44
44
|
import { normalizeProviderLifecycleState } from '../provider-lifecycle-state.mjs';
|
|
45
45
|
import { createProviderHealthStore } from '../provider-health.mjs';
|
|
46
|
+
import { pruneCrewTempLogs } from '../log-prune.mjs';
|
|
46
47
|
import { planProviderDelete } from '../provider-lifecycle.mjs';
|
|
47
48
|
import { createProviderDeleteFileHooks, isRecoverableProviderDeleteBackup, readProviderDeleteManifestFile } from '../provider-delete-adapters.mjs';
|
|
48
49
|
import { buildCredentialReferenceInventory } from '../credential-reference-inventory.mjs';
|
|
@@ -2809,6 +2810,13 @@ export async function apply(ctx) {
|
|
|
2809
2810
|
return () => { for (const d of disposers.reverse()) d(); };
|
|
2810
2811
|
});
|
|
2811
2812
|
|
|
2813
|
+
// Each hub start writes another diagnostics pair into %TEMP%; bound them here,
|
|
2814
|
+
// on the machine that owns them, keeping the newest runs (see log-prune.mjs).
|
|
2815
|
+
try {
|
|
2816
|
+
const pruned = pruneCrewTempLogs();
|
|
2817
|
+
if (pruned.removed.length > 0) ctx.logger?.info?.(`dsh-crew diagnostics pruned: ${pruned.removed.length} file(s)`);
|
|
2818
|
+
} catch { /* pruning is best-effort and must never block a boot */ }
|
|
2819
|
+
|
|
2812
2820
|
ctx.logger?.info?.('dsh-crew hub mounted (jobs API + installer endpoints)');
|
|
2813
2821
|
return async () => {
|
|
2814
2822
|
for (const d of disposers.reverse()) { try { d(); } catch {} }
|
|
@@ -383,6 +383,11 @@ export const GLOBAL_CONFIG_DEFAULTS = {
|
|
|
383
383
|
// Automatic Pro review after a successful Flash run (review-pipeline forces
|
|
384
384
|
// this on; balanced/custom only when this flag is set).
|
|
385
385
|
pro_reviews_flash: false,
|
|
386
|
+
// Opt-in: the login/background launcher also starts the Crew-managed 3080
|
|
387
|
+
// frontend — without opening a browser, logging the session URL instead. Off by
|
|
388
|
+
// default: the interactive desktop entry is what opens a session, and an extra
|
|
389
|
+
// always-on harness process is the operator's choice to make.
|
|
390
|
+
frontend_autostart: false,
|
|
386
391
|
// Worker provider routing for HUB workers: which DSH provider backs each
|
|
387
392
|
// worker session. Fresh installs stay on the built-in DeepSeek provider;
|
|
388
393
|
// follow-dsh is an explicit opt-in that uses the provider selected in DSH
|
|
@@ -51,6 +51,7 @@ import { checkRuntimeAdvance, normalizeRuntimeState, runtimeStateMayHaveStarted
|
|
|
51
51
|
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, ensureCrewWebProfile, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
52
52
|
import { desktopAttach, desktopDetach, desktopStatus } from './desktop-profile.mjs';
|
|
53
53
|
import { mirrorOfficialHarnessConfig, officialConfigMirrorStatus } from './harness-config-import.mjs';
|
|
54
|
+
import { pruneCrewTempLogs } from '../log-prune.mjs';
|
|
54
55
|
import {
|
|
55
56
|
ensureOfficialWebIntegration,
|
|
56
57
|
officialWebIntegrationStatus,
|
|
@@ -2772,6 +2773,21 @@ export async function npxDesktop({ home = homedir(), args = [], dryRun = false,
|
|
|
2772
2773
|
return result;
|
|
2773
2774
|
}
|
|
2774
2775
|
|
|
2776
|
+
// Bound the diagnostics that accumulate in the temp directory. The hub does this
|
|
2777
|
+
// on every boot too; the command exists so an operator can reclaim space without
|
|
2778
|
+
// waiting for a restart or guessing which files are safe to delete.
|
|
2779
|
+
export async function npxLogs({ args = [], log = console.log } = {}) {
|
|
2780
|
+
const action = args[0] ?? 'prune';
|
|
2781
|
+
if (action !== 'prune') {
|
|
2782
|
+
log(`✗ unknown logs action: ${action}`);
|
|
2783
|
+
return { ok: false, error: 'usage: dsh-crew logs prune [keep]' };
|
|
2784
|
+
}
|
|
2785
|
+
const keep = Number(args[1]);
|
|
2786
|
+
const result = pruneCrewTempLogs(Number.isInteger(keep) && keep > 0 ? { keepRuns: keep } : {});
|
|
2787
|
+
log(`✓ Crew diagnostics pruned: removed ${result.removed.length} file(s); kept ${result.kept.length} run(s)`);
|
|
2788
|
+
return { ok: true, ...result };
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2775
2791
|
// Bring the operator's user-level DSH configuration across from the official home
|
|
2776
2792
|
// into Crew's home: the Crew hub then runs the same models, providers, policy and
|
|
2777
2793
|
// MCP servers as the desktop app. Read-only on ~/.dsh, no secrets copied (provider
|
|
@@ -3426,6 +3442,7 @@ Commands:
|
|
|
3426
3442
|
home, so the Crew hub matches the desktop app. Read-only on ~/.dsh; no
|
|
3427
3443
|
secrets are copied (provider entries keep their apiKeyEnv references);
|
|
3428
3444
|
entries naming packages absent from Crew's runtime cohort are skipped
|
|
3445
|
+
logs prune the Crew diagnostics in the temp directory: prune [keep]
|
|
3429
3446
|
status read-only report of launcher/installed versions and integrations
|
|
3430
3447
|
inspect print the machine-readable extension capability/readiness contract
|
|
3431
3448
|
jobs machine-first job API: list|get|watch|cancel|submit
|
|
@@ -3830,7 +3847,7 @@ export async function runNpxCli({
|
|
|
3830
3847
|
error(USAGE);
|
|
3831
3848
|
return 1;
|
|
3832
3849
|
}
|
|
3833
|
-
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'desktop', 'config', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
3850
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'desktop', 'config', 'logs', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
3834
3851
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
3835
3852
|
return 1;
|
|
3836
3853
|
}
|
|
@@ -3841,6 +3858,7 @@ export async function runNpxCli({
|
|
|
3841
3858
|
detach: commands.detach ?? npxDetach,
|
|
3842
3859
|
desktop: commands.desktop ?? npxDesktop,
|
|
3843
3860
|
config: commands.config ?? npxConfig,
|
|
3861
|
+
logs: commands.logs ?? npxLogs,
|
|
3844
3862
|
status: commands.status ?? npxStatus,
|
|
3845
3863
|
inspect: commands.inspect ?? npxInspect,
|
|
3846
3864
|
jobs: commands.jobs ?? npxJobs,
|
|
@@ -3856,6 +3874,7 @@ export async function runNpxCli({
|
|
|
3856
3874
|
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
3857
3875
|
else if (command === 'desktop') result = await actions.desktop({ args, dryRun, log });
|
|
3858
3876
|
else if (command === 'config') result = await actions.config({ args, dryRun, log });
|
|
3877
|
+
else if (command === 'logs') result = await actions.logs({ args, log });
|
|
3859
3878
|
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
3860
3879
|
else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
|
|
3861
3880
|
else if (command === 'credentials') result = await actions.credentials({ args, planId, expectedRevision, confirm, log });
|
|
@@ -107,14 +107,15 @@ export function windowsStartupStatus({
|
|
|
107
107
|
const sourceControl = readFileSync(join(root, 'windows', WINDOWS_CONTROL_FILENAME), 'utf8');
|
|
108
108
|
const sourceVbs = readFileSync(join(root, 'windows', 'start-dsh-crew.vbs'), 'utf8');
|
|
109
109
|
const startup = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
110
|
-
components.startup_entry_content = startup === renderVbs(sourceVbs, resolved.launcherFile);
|
|
111
|
-
components.launcher_content = readFileSync(resolved.launcherFile, 'utf8') === sourceLauncher;
|
|
112
|
-
components.helper_content = readFileSync(resolved.helperFile, 'utf8') === sourceHelper;
|
|
113
|
-
components.control_content = readFileSync(resolved.controlFile, 'utf8') === sourceControl;
|
|
110
|
+
components.startup_entry_content = normalizeEol(startup) === normalizeEol(renderVbs(sourceVbs, resolved.launcherFile));
|
|
111
|
+
components.launcher_content = normalizeEol(readFileSync(resolved.launcherFile, 'utf8')) === normalizeEol(sourceLauncher);
|
|
112
|
+
components.helper_content = normalizeEol(readFileSync(resolved.helperFile, 'utf8')) === normalizeEol(sourceHelper);
|
|
113
|
+
components.control_content = normalizeEol(readFileSync(resolved.controlFile, 'utf8')) === normalizeEol(sourceControl);
|
|
114
|
+
// The manifest is written from the INSTALLED bytes, so it is checked against
|
|
115
|
+
// them (inside readWindowsSupervisorAssets); what ties the installation to the
|
|
116
|
+
// shipped payload is the *_content* comparison above, which is EOL-tolerant.
|
|
114
117
|
const assets = readWindowsSupervisorAssets({ home });
|
|
115
|
-
components.supervisor_manifest = assets.ok
|
|
116
|
-
&& assets.helper_hash === sha256File(join(root, 'windows', WINDOWS_HELPER_FILENAME))
|
|
117
|
-
&& assets.control_hash === sha256File(join(root, 'windows', WINDOWS_CONTROL_FILENAME));
|
|
118
|
+
components.supervisor_manifest = assets.ok;
|
|
118
119
|
} catch { /* missing or unreadable content remains false */ }
|
|
119
120
|
}
|
|
120
121
|
const missing = Object.entries(components).filter(([, ready]) => !ready).map(([key]) => key);
|
|
@@ -125,6 +126,14 @@ function sha256File(file) {
|
|
|
125
126
|
try { return createHash('sha256').update(readFileSync(file)).digest('hex'); } catch { return null; }
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// Git checks these scripts out as CRLF on a `core.autocrlf=true` machine and
|
|
130
|
+
// PowerShell accepts either, so a line-ending difference is not a difference this
|
|
131
|
+
// check may report: comparing raw bytes made every Windows dev checkout read as
|
|
132
|
+
// "needs repair" while the npm payload (LF) was installed correctly.
|
|
133
|
+
function normalizeEol(text) {
|
|
134
|
+
return String(text).replace(/\r\n/g, '\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
function samePath(left, right) {
|
|
129
138
|
try { return resolve(left).toLowerCase() === resolve(right).toLowerCase(); } catch { return false; }
|
|
130
139
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Bound the Crew diagnostics that accumulate in %TEMP%: every hub start writes a
|
|
2
|
+
// new dsh-crew-dsh-crew-3210-<stamp>.out/.err.log pair, every desktop launch a
|
|
3
|
+
// dsh-crew-web-*, every official launch a dsh-official-web-*, and the launcher
|
|
4
|
+
// appends to dsh-crew-launcher.log forever. They are the only post-mortem evidence
|
|
5
|
+
// for a crash-before-readiness, so the rule keeps the most recent runs instead of
|
|
6
|
+
// deleting everything: per family, the newest `keep` runs survive, plus anything
|
|
7
|
+
// written within the recency guard (a running process owns its own log files).
|
|
8
|
+
|
|
9
|
+
import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_KEEP_RUNS = 20;
|
|
14
|
+
export const DEFAULT_MAX_AGE_DAYS = 14;
|
|
15
|
+
const RECENT_GUARD_MS = 5 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
const LOG_FAMILIES = [
|
|
18
|
+
{ prefix: 'dsh-crew-dsh-crew-3210-', suffix: '.out.log' },
|
|
19
|
+
{ prefix: 'dsh-crew-web-', suffix: '.out.log' },
|
|
20
|
+
{ prefix: 'dsh-official-web-', suffix: '.out.log' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// `dsh-crew-<profile>-<port>-<stamp>.out.log` -> the stamp identifies one run, and
|
|
24
|
+
// its .err.log sibling must live or die with it.
|
|
25
|
+
function runStamp(fileName, family) {
|
|
26
|
+
if (!fileName.startsWith(family.prefix) || !fileName.endsWith(family.suffix)) return null;
|
|
27
|
+
const stamp = fileName.slice(family.prefix.length, fileName.length - family.suffix.length);
|
|
28
|
+
return stamp || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns = DEFAULT_KEEP_RUNS, maxAgeDays = DEFAULT_MAX_AGE_DAYS, now = Date.now() } = {}) {
|
|
32
|
+
const keep = Number.isInteger(keepRuns) && keepRuns > 0 ? keepRuns : DEFAULT_KEEP_RUNS;
|
|
33
|
+
const maxAgeMs = Number.isFinite(maxAgeDays) && maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : DEFAULT_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
34
|
+
let names;
|
|
35
|
+
try { names = readdirSync(tempDir); } catch { return { ok: true, removed: [], kept: [], skipped: 'unreadable' }; }
|
|
36
|
+
|
|
37
|
+
const removed = [];
|
|
38
|
+
const kept = [];
|
|
39
|
+
for (const family of LOG_FAMILIES) {
|
|
40
|
+
const runs = new Map();
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const stamp = runStamp(name, family);
|
|
43
|
+
if (!stamp) continue;
|
|
44
|
+
const file = join(tempDir, name);
|
|
45
|
+
let mtimeMs = 0;
|
|
46
|
+
try { mtimeMs = statSync(file).mtimeMs; } catch { continue; }
|
|
47
|
+
const run = runs.get(stamp) ?? { stamp, files: [], newest: 0 };
|
|
48
|
+
run.files.push(name);
|
|
49
|
+
run.newest = Math.max(run.newest, mtimeMs);
|
|
50
|
+
runs.set(stamp, run);
|
|
51
|
+
}
|
|
52
|
+
const ordered = [...runs.values()].sort((left, right) => right.newest - left.newest);
|
|
53
|
+
ordered.forEach((run, index) => {
|
|
54
|
+
const tooOld = now - run.newest > maxAgeMs;
|
|
55
|
+
const tooMany = index >= keep;
|
|
56
|
+
const tooRecentToTouch = now - run.newest < RECENT_GUARD_MS;
|
|
57
|
+
if ((tooOld || tooMany) && !tooRecentToTouch) {
|
|
58
|
+
for (const name of run.files) removed.push(name);
|
|
59
|
+
const errName = run.files[0].replace(/\.out\.log$/, '.err.log');
|
|
60
|
+
if (names.includes(errName)) removed.push(errName);
|
|
61
|
+
} else {
|
|
62
|
+
kept.push(run.stamp);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const name of removed) {
|
|
67
|
+
try { rmSync(join(tempDir, name), { force: true }); } catch { /* a locked file stays until the next pass */ }
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, removed: [...new Set(removed)].sort(), kept: kept.sort() };
|
|
70
|
+
}
|
package/src/provider-health.mjs
CHANGED
|
@@ -73,7 +73,10 @@ function boundedTimestamp(value, fallback) {
|
|
|
73
73
|
function view(entry, now) {
|
|
74
74
|
if (!entry) return { state: 'unprobed', reason_code: null, observed_at: null, expires_at: null, fresh: false };
|
|
75
75
|
const fresh = now < entry.expires_at;
|
|
76
|
-
|
|
76
|
+
// An aged-out record keeps its identity (provider/model) and timestamps: the
|
|
77
|
+
// readiness projection matches it against the selected route to report STALE
|
|
78
|
+
// instead of "never probed". Only the verdict is reset to `unprobed`.
|
|
79
|
+
if (!fresh) return { ...entry, state: 'unprobed', reason_code: null, fresh: false };
|
|
77
80
|
return { ...entry, fresh: true };
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -124,7 +127,12 @@ export function createProviderHealthStore({ clock = () => Date.now(), ttls = {},
|
|
|
124
127
|
return get(provider, model);
|
|
125
128
|
};
|
|
126
129
|
|
|
127
|
-
|
|
130
|
+
// Expired records are included, as `fresh: false` with their timestamps: "this
|
|
131
|
+
// route was probed and the evidence has aged out" is knowledge the readiness
|
|
132
|
+
// projection needs (it renders STALE), and dropping it made an aged-out probe
|
|
133
|
+
// read as never-probed. Consumers that need live evidence filter on `fresh`
|
|
134
|
+
// themselves (currentRouteHealthEvidence does exactly that).
|
|
135
|
+
const list = () => [...records.values()].map((entry) => view(entry, clock())).slice(0, boundedMax);
|
|
128
136
|
|
|
129
137
|
return { get, record, list, ttls: { ...limits }, maxEntries: boundedMax };
|
|
130
138
|
}
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export const READINESS_REASON_CODES = Object.freeze({
|
|
|
22
22
|
NO_CI_EVIDENCE: 'NO_CI_EVIDENCE',
|
|
23
23
|
NO_EXECUTION_EVIDENCE: 'NO_EXECUTION_EVIDENCE',
|
|
24
24
|
CREDENTIAL_STATUS_NOT_PROBED: 'CREDENTIAL_STATUS_NOT_PROBED',
|
|
25
|
+
ROUTE_NOT_SELECTED: 'ROUTE_NOT_SELECTED',
|
|
25
26
|
EVIDENCE_REPORTED: 'EVIDENCE_REPORTED',
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -101,6 +102,7 @@ export function buildReadinessMatrix({
|
|
|
101
102
|
platform = process.platform,
|
|
102
103
|
hubCompatibility = null,
|
|
103
104
|
workerProviderMode = null,
|
|
105
|
+
workerSelection = null,
|
|
104
106
|
providerCatalogChecked = false,
|
|
105
107
|
providerCatalogOk = false,
|
|
106
108
|
evidence = {},
|
|
@@ -161,6 +163,21 @@ export function buildReadinessMatrix({
|
|
|
161
163
|
);
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
// The two built-in DeepSeek rows describe a route only when the operator runs
|
|
167
|
+
// it. Under follow-dsh with a KNOWN, other selected provider they can never
|
|
168
|
+
// gather evidence, and NOT_RUN reads like a check that should have run rather
|
|
169
|
+
// than a route that was never chosen. An unknown selection stays NOT_RUN: this
|
|
170
|
+
// matrix is conservative, and "we could not tell" must not read as "not
|
|
171
|
+
// applicable". Reported evidence still wins, because it is applied below.
|
|
172
|
+
const selectionKnown = typeof workerSelection?.provider === 'string' && workerSelection.provider.length > 0;
|
|
173
|
+
const deepseekRouteSelected = workerProviderMode === 'deepseek-official'
|
|
174
|
+
|| workerSelection?.provider === 'deepseek-official';
|
|
175
|
+
if (workerProviderMode !== null && selectionKnown && !deepseekRouteSelected) {
|
|
176
|
+
for (const id of ['deepseek_flash', 'deepseek_pro']) {
|
|
177
|
+
rows[id] = baseRow(id, 'real-execution', 'SKIP', READINESS_REASON_CODES.ROUTE_NOT_SELECTED, 'runtime-policy');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
164
181
|
for (const [id] of TARGET_ROWS) {
|
|
165
182
|
rows[id] = normalizeEvidence(rows[id], evidence?.[id]);
|
|
166
183
|
}
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -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.
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.4';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -95,13 +95,13 @@ function projectModelRole({ role, selected, health, jobs, runtime, now, executio
|
|
|
95
95
|
})[0];
|
|
96
96
|
const expiresAt = Number.isFinite(matchingHealth?.expires_at) ? matchingHealth.expires_at : null;
|
|
97
97
|
const observedAt = Number.isFinite(matchingHealth?.observed_at) ? matchingHealth.observed_at : null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const healthCurrent = matchingHealth && matchingHealth.fresh === true;
|
|
98
|
+
// An aged-out probe is remembered, not forgotten: it is reported as STALE below,
|
|
99
|
+
// but only after the job evidence is consulted — a real successful execution is
|
|
100
|
+
// stronger than "a probe aged out", and letting the stale record decide first
|
|
101
|
+
// would downgrade a route that just ran.
|
|
102
|
+
const healthExpired = Boolean(matchingHealth) && (observedAt === null || observedAt > now || expiresAt === null
|
|
103
|
+
|| expiresAt <= now || expiresAt - observedAt > MAX_EXECUTION_EVIDENCE_TTL_MS);
|
|
104
|
+
const healthCurrent = matchingHealth && matchingHealth.fresh === true && !healthExpired;
|
|
105
105
|
if (healthCurrent && matchingHealth.state !== 'callable') {
|
|
106
106
|
return { state: 'NOT_CALLABLE', reason_code: matchingHealth.reason_code ?? 'PROVIDER_ROUTE_UNCALLABLE', selected, observed_at: matchingHealth.observed_at ?? null, expires_at: expiresAt, source: 'provider_health', last_success: null };
|
|
107
107
|
}
|
|
@@ -122,6 +122,9 @@ function projectModelRole({ role, selected, health, jobs, runtime, now, executio
|
|
|
122
122
|
if (expires > now) return { state: 'CALLABLE', reason_code: 'RECENT_EXECUTION_PASSED', selected, observed_at: completed.endedAt, expires_at: expires, source: 'execution', last_success: lastSuccess };
|
|
123
123
|
return { state: 'STALE', reason_code: 'EXECUTION_EVIDENCE_EXPIRED', selected, observed_at: completed.endedAt, expires_at: expires, source: 'execution', last_success: lastSuccess };
|
|
124
124
|
}
|
|
125
|
+
if (healthExpired) {
|
|
126
|
+
return { state: 'STALE', reason_code: 'PROVIDER_HEALTH_STALE_OR_INVALID', selected, observed_at: observedAt, expires_at: expiresAt, source: 'provider_health', last_success: null };
|
|
127
|
+
}
|
|
125
128
|
return { state: 'UNKNOWN', reason_code: 'NO_CURRENT_MODEL_EVIDENCE', selected, last_success: null };
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -372,6 +372,33 @@ function Start-CrewManagedFrontendQuietly {
|
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
function Test-CrewFrontendAutostart {
|
|
376
|
+
# Opt-in only. The login/background launch has never started 3080 — the desktop
|
|
377
|
+
# entry does that, with a browser — which left operators who expect the browser
|
|
378
|
+
# surface after a reboot with nothing on 3080. Any unreadable config reads as off.
|
|
379
|
+
try {
|
|
380
|
+
$configFile = Join-Path $env:USERPROFILE '.config\dsh-crew\config.json'
|
|
381
|
+
if (-not (Test-Path -LiteralPath $configFile -PathType Leaf)) { return $false }
|
|
382
|
+
$config = Get-Content -LiteralPath $configFile -Raw | ConvertFrom-Json
|
|
383
|
+
return $config.frontend_autostart -eq $true
|
|
384
|
+
} catch { return $false }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function Start-CrewFrontendFromLogin {
|
|
388
|
+
# Never fatal: this runs before the supervisor starts, so a failure must not stop
|
|
389
|
+
# the backend. The session URL goes to the log instead of a browser window.
|
|
390
|
+
try {
|
|
391
|
+
if (Open-CrewManagedFrontend -Quiet) {
|
|
392
|
+
$url = Get-CrewWebSessionUrl
|
|
393
|
+
Write-LaunchLog ('Crew-managed frontend on 3080 is serving{0}' -f $(if ($url) { '; session: ' + $url } else { '' }))
|
|
394
|
+
} else {
|
|
395
|
+
Write-LaunchLog 'Crew-managed frontend on 3080 was not started (no Crew-managed CLI entry).' 'WARN'
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
Write-LaunchLog ('Crew-managed frontend on 3080 could not be started: {0}' -f $_.Exception.Message) 'WARN'
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
375
402
|
function Get-OfficialFrontendOverlay {
|
|
376
403
|
$frontendRoot = Join-Path $env:USERPROFILE '.config\dsh-crew\frontend'
|
|
377
404
|
$path = Join-Path $frontendRoot 'official-web.patch.json'
|
|
@@ -1510,7 +1537,9 @@ try {
|
|
|
1510
1537
|
throw "The isolated dsh-crew profile is missing under $crewHome. Run: dsh-crew update"
|
|
1511
1538
|
}
|
|
1512
1539
|
if ($Mode -eq 'open') { Open-OfficialFrontend }
|
|
1513
|
-
# Background/watch modes remain independent of the official frontend
|
|
1540
|
+
# Background/watch modes remain independent of the official frontend, unless the
|
|
1541
|
+
# operator opted in: then the login launch also serves 3080, without a browser.
|
|
1542
|
+
if ($Mode -ne 'open' -and (Test-CrewFrontendAutostart)) { Start-CrewFrontendFromLogin }
|
|
1514
1543
|
|
|
1515
1544
|
if ($Mode -eq 'watch') {
|
|
1516
1545
|
Start-ServiceSupervisor
|