@ran-sh/dsh-crew 1.2.0-rc.6 → 1.3.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/docs/installation.md +30 -0
- package/package.json +49 -49
- package/scripts/verify-official-bridge-e2e.mjs +2 -1
- package/src/dsh-cli-runtime.mjs +77 -1
- package/src/dsh-cohort.mjs +1 -1
- package/src/history/archive-store.mjs +14 -3
- package/src/history/runner.mjs +2 -1
- package/src/history/runtime.mjs +2 -1
- package/src/history/service.mjs +45 -16
- package/src/runtime-identity.mjs +49 -7
- package/windows/start-dsh-crew.ps1 +270 -36
- package/worker.cordis.yml +9 -5
package/docs/installation.md
CHANGED
|
@@ -90,6 +90,36 @@ pnpm run build:client
|
|
|
90
90
|
npm pack --dry-run
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
## Harness CLI selection
|
|
94
|
+
|
|
95
|
+
The Crew launcher and the 3210 Hub boot from one Crew-managed Harness entry,
|
|
96
|
+
chosen in this order:
|
|
97
|
+
|
|
98
|
+
1. `DSH_CREW_DSH_CLI`, when set to an explicit `@deepseek-ai/dsh` entry.
|
|
99
|
+
2. The Crew-managed npm runtime at `<crew home>/runtime` (the normal install
|
|
100
|
+
path, written by `dsh-crew update`). It carries whatever cohort the installed
|
|
101
|
+
Crew release pinned; the launcher never substitutes a different version.
|
|
102
|
+
3. A Crew-managed source cohort, discovered through a
|
|
103
|
+
`<crew home>/runtime-source-<label>.json` sidecar. The sidecar's recorded
|
|
104
|
+
`version` must equal the checkout's own `apps/cli/package.json`, so a stale
|
|
105
|
+
sidecar can never pin a half-updated tree.
|
|
106
|
+
|
|
107
|
+
For the npm runtime the launcher uses the Crew home as `DSH_HOME`; a source
|
|
108
|
+
cohort is its own home, because its profiles, sessions and settings live inside
|
|
109
|
+
that tree. The launcher then derives `dsh_version` from the verified
|
|
110
|
+
`@deepseek-ai/dsh` manifest and requires the Hub to report that same version, so
|
|
111
|
+
a process left over from a previous cohort is never treated as healthy.
|
|
112
|
+
|
|
113
|
+
Only override `DSH_CREW_DSH_CLI` deliberately — it wins over the managed runtime.
|
|
114
|
+
Never point it at the official `~/.dsh` tree, which the Crew launcher treats as a
|
|
115
|
+
read-only boundary.
|
|
116
|
+
|
|
117
|
+
To build a source cohort instead of the npm runtime (for example to test an
|
|
118
|
+
unpublished tag), clone the tag into `<crew home>/runtime-source-<label>`, build
|
|
119
|
+
its CLI, and write the matching sidecar. On Windows a source install needs the
|
|
120
|
+
native toolchain for its `fs-ext` dependency; a failed native build is reported
|
|
121
|
+
as a runtime failure rather than silently falling back to another cohort.
|
|
122
|
+
|
|
93
123
|
## Rollback vs uninstall
|
|
94
124
|
|
|
95
125
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ran-sh/dsh-crew",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/hub/entry.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -41,30 +41,30 @@
|
|
|
41
41
|
"./package.json": "./package.json"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh": "0.1.
|
|
45
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
46
|
-
"@deepseek-ai/dsh-agent-loop": "0.1.
|
|
47
|
-
"@deepseek-ai/dsh-agent-presets": "0.1.
|
|
48
|
-
"@deepseek-ai/dsh-bash-local": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
52
|
-
"@deepseek-ai/dsh-compaction-basic": "0.1.
|
|
53
|
-
"@deepseek-ai/dsh-fs-local": "0.1.
|
|
54
|
-
"@deepseek-ai/dsh-fs-observation-policy": "0.1.
|
|
55
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
56
|
-
"@deepseek-ai/dsh-llm-deepseek": "0.1.
|
|
57
|
-
"@deepseek-ai/dsh-sandbox-local": "0.1.
|
|
58
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.1.
|
|
59
|
-
"@deepseek-ai/dsh-sdk-client": "0.1.
|
|
60
|
-
"@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.
|
|
61
|
-
"@deepseek-ai/dsh-sdk-minimal": "0.1.
|
|
62
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
63
|
-
"@deepseek-ai/dsh-session-persistence-jsonl": "0.1.
|
|
64
|
-
"@deepseek-ai/dsh-subprocess-local": "0.1.
|
|
65
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
66
|
-
"@deepseek-ai/dsh-tool-fs": "0.1.
|
|
67
|
-
"@deepseek-ai/dsh-tool-todo": "0.1.
|
|
44
|
+
"@deepseek-ai/dsh": "0.1.5-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-agent-loop": "0.1.5-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-agent-presets": "0.1.5-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-bash-local": "0.1.5-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-compaction-basic": "0.1.5-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-fs-local": "0.1.5-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-fs-observation-policy": "0.1.5-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.1.5-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-sandbox-local": "0.1.5-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-sdk-client": "0.1.5-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.5-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-sdk-minimal": "0.1.5-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "0.1.5-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.5-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-token-meter": "0.1.5-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-tool-fs": "0.1.5-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-tool-todo": "0.1.5-rc.2",
|
|
68
68
|
"react": "^18.0.0",
|
|
69
69
|
"react-dom": "^18.0.0"
|
|
70
70
|
},
|
|
@@ -149,30 +149,30 @@
|
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
"devDependencies": {
|
|
152
|
-
"@deepseek-ai/dsh": "0.1.
|
|
153
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
154
|
-
"@deepseek-ai/dsh-agent-loop": "0.1.
|
|
155
|
-
"@deepseek-ai/dsh-agent-presets": "0.1.
|
|
156
|
-
"@deepseek-ai/dsh-bash-local": "0.1.
|
|
157
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
160
|
-
"@deepseek-ai/dsh-compaction-basic": "0.1.
|
|
161
|
-
"@deepseek-ai/dsh-fs-local": "0.1.
|
|
162
|
-
"@deepseek-ai/dsh-fs-observation-policy": "0.1.
|
|
163
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
164
|
-
"@deepseek-ai/dsh-llm-deepseek": "0.1.
|
|
165
|
-
"@deepseek-ai/dsh-sandbox-local": "0.1.
|
|
166
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.1.
|
|
167
|
-
"@deepseek-ai/dsh-sdk-client": "0.1.
|
|
168
|
-
"@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.
|
|
169
|
-
"@deepseek-ai/dsh-sdk-minimal": "0.1.
|
|
170
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
171
|
-
"@deepseek-ai/dsh-session-persistence-jsonl": "0.1.
|
|
172
|
-
"@deepseek-ai/dsh-subprocess-local": "0.1.
|
|
173
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
174
|
-
"@deepseek-ai/dsh-tool-fs": "0.1.
|
|
175
|
-
"@deepseek-ai/dsh-tool-todo": "0.1.
|
|
152
|
+
"@deepseek-ai/dsh": "0.1.5-rc.2",
|
|
153
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
154
|
+
"@deepseek-ai/dsh-agent-loop": "0.1.5-rc.2",
|
|
155
|
+
"@deepseek-ai/dsh-agent-presets": "0.1.5-rc.2",
|
|
156
|
+
"@deepseek-ai/dsh-bash-local": "0.1.5-rc.2",
|
|
157
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
158
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
160
|
+
"@deepseek-ai/dsh-compaction-basic": "0.1.5-rc.2",
|
|
161
|
+
"@deepseek-ai/dsh-fs-local": "0.1.5-rc.2",
|
|
162
|
+
"@deepseek-ai/dsh-fs-observation-policy": "0.1.5-rc.2",
|
|
163
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
164
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.1.5-rc.2",
|
|
165
|
+
"@deepseek-ai/dsh-sandbox-local": "0.1.5-rc.2",
|
|
166
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.2",
|
|
167
|
+
"@deepseek-ai/dsh-sdk-client": "0.1.5-rc.2",
|
|
168
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.5-rc.2",
|
|
169
|
+
"@deepseek-ai/dsh-sdk-minimal": "0.1.5-rc.2",
|
|
170
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|
|
171
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "0.1.5-rc.2",
|
|
172
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.5-rc.2",
|
|
173
|
+
"@deepseek-ai/dsh-token-meter": "0.1.5-rc.2",
|
|
174
|
+
"@deepseek-ai/dsh-tool-fs": "0.1.5-rc.2",
|
|
175
|
+
"@deepseek-ai/dsh-tool-todo": "0.1.5-rc.2",
|
|
176
176
|
"@types/react": "^18.3.0",
|
|
177
177
|
"react": "^18.3.1",
|
|
178
178
|
"react-dom": "^18.3.1",
|
|
@@ -17,6 +17,7 @@ import { createServer } from 'node:net';
|
|
|
17
17
|
import { dirname, join, resolve } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { npxInstall, npxIntegrate } from '../src/install/npx-lifecycle.mjs';
|
|
20
|
+
import { TARGET_DSH_VERSION } from '../src/dsh-cohort.mjs';
|
|
20
21
|
|
|
21
22
|
// RETIRED as a release gate: the legacy 3080 -> 3210 bridge is retired and
|
|
22
23
|
// npxIntegrate()/npxDetach() always return OFFICIAL_WEB_PROFILE_READ_ONLY, so
|
|
@@ -139,7 +140,7 @@ try {
|
|
|
139
140
|
const target = join(harnessHome, 'runtime');
|
|
140
141
|
mkdirSync(dirname(target), { recursive: true });
|
|
141
142
|
symlinkSync(realpathSync(runtimeRoot), target, process.platform === 'win32' ? 'junction' : 'dir');
|
|
142
|
-
return { ok: true, version:
|
|
143
|
+
return { ok: true, version: TARGET_DSH_VERSION };
|
|
143
144
|
},
|
|
144
145
|
});
|
|
145
146
|
assert(installed.ok, `disposable install failed: ${logs.join(' | ')}`);
|
package/src/dsh-cli-runtime.mjs
CHANGED
|
@@ -784,6 +784,75 @@ function ensureProfileScaffold(profileRoot) {
|
|
|
784
784
|
return changed;
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
+
function readPackageVersionAt(dir, read = readFileSync) {
|
|
788
|
+
try {
|
|
789
|
+
const parsed = JSON.parse(read(join(dir, 'package.json'), 'utf8'));
|
|
790
|
+
return typeof parsed.version === 'string' && parsed.version ? parsed.version : null;
|
|
791
|
+
} catch { return null; }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Nearest `<dir>/node_modules/<name>` walking up from `dir`, mirroring the
|
|
795
|
+
// resolution a require() from that directory would take.
|
|
796
|
+
export function resolveNearestBundleVersion(dir, name, { exists = defaultExists, read = readFileSync } = {}) {
|
|
797
|
+
let cursor = dir;
|
|
798
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
799
|
+
const candidate = join(cursor, 'node_modules', ...name.split('/'));
|
|
800
|
+
if (exists(candidate)) return readPackageVersionAt(candidate, read);
|
|
801
|
+
const parent = dirname(cursor);
|
|
802
|
+
if (parent === cursor) break;
|
|
803
|
+
cursor = parent;
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Stop a profile from resolving its bundle packages out of a foreign cohort.
|
|
810
|
+
*
|
|
811
|
+
* A profile first installed against a source checkout records `dsh-base` and
|
|
812
|
+
* `dsh-web-app` as links into that checkout. Switching the CLI to another
|
|
813
|
+
* cohort leaves those links behind, and because they sit in the profile's own
|
|
814
|
+
* node_modules they shadow the matching packages installed beside the profile.
|
|
815
|
+
* The host then composes the web app of one cohort while the plugin table comes
|
|
816
|
+
* from another; the client module table is built per cohort, so the mismatched
|
|
817
|
+
* plugin dies at require time with "missed the module table".
|
|
818
|
+
*
|
|
819
|
+
* Only an entry whose fallthrough provably resolves the target cohort is
|
|
820
|
+
* removed. A profile that deliberately tracks a different cohort keeps its
|
|
821
|
+
* links and is reported as skipped, never as an error.
|
|
822
|
+
*/
|
|
823
|
+
export function reconcileProfileBundleCohort({
|
|
824
|
+
home = homedir(),
|
|
825
|
+
profileRoot,
|
|
826
|
+
version = TARGET_DSH_VERSION,
|
|
827
|
+
bundles = CREW_PROFILE_DEFAULT_BUNDLES,
|
|
828
|
+
exists = defaultExists,
|
|
829
|
+
read = readFileSync,
|
|
830
|
+
remove = (path) => rmSync(path, { recursive: true, force: true }),
|
|
831
|
+
} = {}) {
|
|
832
|
+
const root = profileRoot ?? crewProfileDir({ home });
|
|
833
|
+
if (typeof root !== 'string' || !isAbsolute(root)) return { ok: false, code: 'INVALID_PROFILE_ROOT' };
|
|
834
|
+
const modulesDir = join(root, 'node_modules');
|
|
835
|
+
const parentDir = dirname(root);
|
|
836
|
+
const unshadowed = [];
|
|
837
|
+
const skipped = [];
|
|
838
|
+
const removeBundle = (path) => remove(path);
|
|
839
|
+
for (const name of bundles) {
|
|
840
|
+
const shadowPath = join(modulesDir, ...name.split('/'));
|
|
841
|
+
if (!exists(shadowPath)) continue;
|
|
842
|
+
const shadowed = readPackageVersionAt(shadowPath, read);
|
|
843
|
+
if (shadowed === version) continue;
|
|
844
|
+
const fallthrough = resolveNearestBundleVersion(parentDir, name, { exists, read });
|
|
845
|
+
if (fallthrough !== version) {
|
|
846
|
+
skipped.push({ bundle: name, shadowed: shadowed ?? null, fallthrough: fallthrough ?? null });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
try { removeBundle(shadowPath); } catch { return { ok: false, code: 'PROFILE_BUNDLE_UNSHADOW_FAILED', profileRoot: root, bundle: name }; }
|
|
850
|
+
if (exists(shadowPath)) return { ok: false, code: 'PROFILE_BUNDLE_UNSHADOW_FAILED', profileRoot: root, bundle: name };
|
|
851
|
+
unshadowed.push({ bundle: name, shadowed: shadowed ?? null });
|
|
852
|
+
}
|
|
853
|
+
return { ok: true, changed: unshadowed.length > 0, profileRoot: root, unshadowed, skipped };
|
|
854
|
+
}
|
|
855
|
+
|
|
787
856
|
function readCrewTombstones(home) {
|
|
788
857
|
const lifecycleFile = join(dirname(crewDshHome({ home })), 'provider-lifecycle.json');
|
|
789
858
|
if (!existsSync(lifecycleFile)) return { ok: true, tombstones: {} };
|
|
@@ -894,9 +963,16 @@ export function ensurePluginRegistration({
|
|
|
894
963
|
export function ensureCrewPluginRegistration({ home = homedir(), root, name } = {}) {
|
|
895
964
|
const result = ensurePluginRegistration({ profileRoot: crewProfileDir({ home }), root, name });
|
|
896
965
|
if (!result.ok) return result;
|
|
966
|
+
const reconciliation = reconcileProfileBundleCohort({ home, profileRoot: result.profileRoot });
|
|
967
|
+
if (!reconciliation.ok) return reconciliation;
|
|
897
968
|
const reconciled = reconcileCrewProfileProviders({ home, profileRoot: result.profileRoot });
|
|
898
969
|
if (!reconciled.ok) return reconciled;
|
|
899
|
-
return {
|
|
970
|
+
return {
|
|
971
|
+
...result,
|
|
972
|
+
changed: result.changed || reconciled.changed || reconciliation.changed,
|
|
973
|
+
provider_reconciliation: reconciled.removed ?? [],
|
|
974
|
+
bundle_reconciliation: reconciliation.unshadowed ?? [],
|
|
975
|
+
};
|
|
900
976
|
}
|
|
901
977
|
|
|
902
978
|
/**
|
package/src/dsh-cohort.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// resolve to this exact version. All other modules re-export from here so a
|
|
5
5
|
// cohort bump touches exactly one file (plus package.json's 48 DSH pins).
|
|
6
6
|
export const DSH_CLI_PACKAGE = '@deepseek-ai/dsh';
|
|
7
|
-
export const TARGET_DSH_VERSION = '0.1.
|
|
7
|
+
export const TARGET_DSH_VERSION = '0.1.5-rc.2';
|
|
8
8
|
export const TARGET_DSH_SPEC = `${DSH_CLI_PACKAGE}@${TARGET_DSH_VERSION}`;
|
|
9
9
|
|
|
10
10
|
// Retained-cohort support: when a release's manifest pins an older DSH
|
|
@@ -26,10 +26,21 @@ function pathInside(root, relativePath) {
|
|
|
26
26
|
return current;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// One canonical session artifact path. The jsonl backend keeps one immutable
|
|
30
|
+
// file per Session format generation: `session.jsonl` for version 0 and
|
|
31
|
+
// `session.vN.jsonl` (N >= 1) after that, each optionally zstd-compressed.
|
|
32
|
+
// Sharing the shape between the inventory and the archive guard keeps both in
|
|
33
|
+
// step when the backend's naming changes.
|
|
34
|
+
export const SESSION_ARTIFACT_PATTERN = /^harness\/sessions\/[^/]+\/[^/]+\/session(?:\.v[1-9][0-9]*)?\.jsonl(?:\.zstd)?$/;
|
|
35
|
+
|
|
36
|
+
export function isSessionArtifactPath(relativePath, sessionId) {
|
|
37
|
+
return typeof relativePath === 'string'
|
|
38
|
+
&& SESSION_ARTIFACT_PATTERN.test(relativePath)
|
|
39
|
+
&& relativePath.split('/')[3] === sessionId;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
function artifactPath(root, file) {
|
|
30
|
-
if (!validId(file?.sessionId) ||
|
|
31
|
-
|| !/^harness\/sessions\/[^/]+\/[^/]+\/session\.jsonl(?:\.zstd)?$/.test(file.relativePath)
|
|
32
|
-
|| file.relativePath.split('/')[3] !== file.sessionId) fail('INVALID_ARTIFACT');
|
|
43
|
+
if (!validId(file?.sessionId) || !isSessionArtifactPath(file.relativePath, file.sessionId)) fail('INVALID_ARTIFACT');
|
|
33
44
|
return pathInside(root, file.relativePath);
|
|
34
45
|
}
|
|
35
46
|
|
package/src/history/runner.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { acquireUpdateLock, releaseUpdateLock, createCrewSupervisor } from '../i
|
|
|
6
6
|
import { readMaintenanceSession } from '../supervisor/restart-request.mjs';
|
|
7
7
|
import { runHistoryOperation } from './operation.mjs';
|
|
8
8
|
import { readHistoryState } from './state.mjs';
|
|
9
|
+
import { TARGET_DSH_VERSION } from '../dsh-cohort.mjs';
|
|
9
10
|
|
|
10
11
|
async function portFree() {
|
|
11
12
|
return new Promise(resolve => {
|
|
@@ -40,7 +41,7 @@ export async function runProductionHistory({ id, recover = false } = {}) {
|
|
|
40
41
|
const runtime = await response.json();
|
|
41
42
|
return response.ok && runtime.ok === true && runtime.service === 'dsh-crew-hub' && runtime.profile === 'dsh-crew'
|
|
42
43
|
&& runtime.execution_plane === 'hub-3210' && runtime.listen_port === 3210 && runtime.protocol_version === 1
|
|
43
|
-
&& runtime.dsh_version ===
|
|
44
|
+
&& runtime.dsh_version === TARGET_DSH_VERSION && typeof runtime.runtime_id === 'string' && runtime.runtime_id !== s.runtimeId;
|
|
44
45
|
} catch { return false; }
|
|
45
46
|
},
|
|
46
47
|
});
|
package/src/history/runtime.mjs
CHANGED
|
@@ -3,13 +3,14 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
6
|
+
import { TARGET_DSH_VERSION } from '../dsh-cohort.mjs';
|
|
6
7
|
import { createHistoryService } from './service.mjs';
|
|
7
8
|
import { registerHistoryHttp } from './http.mjs';
|
|
8
9
|
|
|
9
10
|
export function registerRuntimeHistory(ctx) {
|
|
10
11
|
const crewRoot = join(homedir(), '.config', 'dsh-crew');
|
|
11
12
|
const runtime = getHubRuntimeIdentity();
|
|
12
|
-
const owned = process.platform === 'win32' && runtime.dsh_version ===
|
|
13
|
+
const owned = process.platform === 'win32' && runtime.dsh_version === TARGET_DSH_VERSION
|
|
13
14
|
&& resolve(process.env.DSH_HOME ?? '').toLowerCase() === resolve(join(crewRoot, 'harness')).toLowerCase();
|
|
14
15
|
if (!owned) return () => {};
|
|
15
16
|
return ctx.inject(['webServer', 'sessionPersistence', 'agents'], host => {
|
package/src/history/service.mjs
CHANGED
|
@@ -3,36 +3,65 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
3
3
|
import { relative } from 'node:path';
|
|
4
4
|
import { planHistoryCleanup } from './cleanup-plan.mjs';
|
|
5
5
|
import { installHistoryAdmissionGate } from './admission-gate.mjs';
|
|
6
|
-
import { historyHash, historyPath, readHistoryBytes, decodeWorkspaceStore, readHistoryManifest } from './archive-store.mjs';
|
|
6
|
+
import { historyHash, historyPath, readHistoryBytes, decodeWorkspaceStore, readHistoryManifest, isSessionArtifactPath } from './archive-store.mjs';
|
|
7
7
|
import { readHistoryState, writeHistoryState, historyPending, publicHistoryState } from './state.mjs';
|
|
8
8
|
|
|
9
9
|
export function createHistoryService({ crewRoot, agents, persistence, runtimeId, launch, now = Date.now }) {
|
|
10
10
|
const gate = installHistoryAdmissionGate(agents, () => historyPending(crewRoot));
|
|
11
11
|
const plans = new Map();
|
|
12
12
|
let entering = false;
|
|
13
|
+
// One row per materialized session: the header to archive and the artifact
|
|
14
|
+
// location the backend reported. The 0.1.5 jsonl backend resolves each
|
|
15
|
+
// session's current immutable generation itself and hands back a concrete
|
|
16
|
+
// path through `listArtifacts()`. The earlier backend listed snapshots and
|
|
17
|
+
// only hinted at a location, leaving the encoding to be probed below.
|
|
18
|
+
async function readInventory() {
|
|
19
|
+
if (typeof persistence?.listArtifacts === 'function') {
|
|
20
|
+
const artifacts = await persistence.listArtifacts();
|
|
21
|
+
if (!Array.isArray(artifacts)) throw Error('HISTORY_INVENTORY_INVALID');
|
|
22
|
+
return artifacts.map(({ header, path }) => {
|
|
23
|
+
if (typeof path !== 'string') throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
24
|
+
return { header, located: path, resolved: true };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (typeof persistence?.listSnapshots === 'function') {
|
|
28
|
+
const rows = await persistence.listSnapshots();
|
|
29
|
+
if (!Array.isArray(rows)) throw Error('HISTORY_INVENTORY_INVALID');
|
|
30
|
+
return rows.map(({ header }) => {
|
|
31
|
+
const location = persistence.locate(header);
|
|
32
|
+
if (location?.kind !== 'jsonl') throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
33
|
+
return { header, located: location.path, resolved: false };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
37
|
+
}
|
|
13
38
|
async function snapshot(options) {
|
|
14
|
-
if (typeof persistence?.
|
|
15
|
-
|
|
39
|
+
if (typeof persistence?.locate !== 'function') throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
40
|
+
// Refuse only a backend that explicitly declares it exposes no per-session
|
|
41
|
+
// raw artifact. The 0.1.5 backend dropped the flag rather than setting it
|
|
42
|
+
// false, and the artifact path itself is validated below either way.
|
|
43
|
+
if (persistence.supportsRawArtifacts === false) throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
16
44
|
const bytes = readHistoryBytes(historyPath(crewRoot, 'harness/storages/workspace.json'));
|
|
17
45
|
const store = decodeWorkspaceStore(bytes);
|
|
18
|
-
const listed = await
|
|
19
|
-
if (
|
|
46
|
+
const listed = await readInventory();
|
|
47
|
+
if (listed.length > 10000) throw Error('HISTORY_INVENTORY_TOO_LARGE');
|
|
20
48
|
let total = 0;
|
|
21
|
-
const sessions = listed.map(({ header,
|
|
49
|
+
const sessions = listed.map(({ header, located, resolved }) => {
|
|
22
50
|
if (!header || typeof header.id !== 'string') throw Error('HISTORY_INVENTORY_INVALID');
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
let path = relative(crewRoot, located).replaceAll('\\', '/');
|
|
52
|
+
if (!isSessionArtifactPath(path, header.id)) throw Error('HISTORY_STORAGE_UNSUPPORTED');
|
|
53
|
+
if (!resolved) {
|
|
54
|
+
// An unresolved location is a hint that may not name the encoding that
|
|
55
|
+
// actually exists, so exactly one candidate must be present on disk.
|
|
56
|
+
const plain = path.replace(/\.zstd$/, '');
|
|
57
|
+
const existing = [plain, `${plain}.zstd`].filter(candidate => existsSync(historyPath(crewRoot, candidate)));
|
|
58
|
+
if (existing.length !== 1) throw Error('HISTORY_ARTIFACT_AMBIGUOUS');
|
|
59
|
+
path = existing[0];
|
|
60
|
+
}
|
|
32
61
|
const raw = readHistoryBytes(historyPath(crewRoot, path)); total += raw.length;
|
|
33
62
|
if (total > 512 * 1024 * 1024) throw Error('HISTORY_INVENTORY_TOO_LARGE');
|
|
34
63
|
return { id: header.id, createdAt: header.createdAt, parentSession: header.parentSession,
|
|
35
|
-
revision: JSON.stringify([revision, historyHash(raw)]), artifact: { sessionId: header.id, relativePath: path, sha256: historyHash(raw) } };
|
|
64
|
+
revision: JSON.stringify([header.revision, historyHash(raw)]), artifact: { sessionId: header.id, relativePath: path, sha256: historyHash(raw) } };
|
|
36
65
|
});
|
|
37
66
|
// A retained child keeps its ancestor chain; do not leave a newer fork orphaned.
|
|
38
67
|
const workspaces = Object.entries(store.tables.workspaces).map(([id, row]) => ({ id, ...row }));
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// - HUB_PROTOCOL_VERSION changes only when Hub <-> MCP wire semantics become
|
|
6
6
|
// incompatible.
|
|
7
7
|
//
|
|
8
|
-
// The Harness/DSH cohort version (e.g. 0.1.
|
|
8
|
+
// The Harness/DSH cohort version (e.g. 0.1.5-rc.2) is a THIRD, independent
|
|
9
9
|
// domain: it identifies the installed @deepseek-ai/dsh package generation,
|
|
10
10
|
// never the Crew release. Verifiers must compare the cohort against
|
|
11
11
|
// dsh_version, never against runtime_version.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
|
-
import { join } from 'node:path';
|
|
19
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
20
20
|
import {
|
|
21
21
|
PRODUCTION_EXECUTION_PLANE,
|
|
22
22
|
PRODUCTION_PROFILE,
|
|
@@ -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 = '1.
|
|
39
|
+
export const RUNTIME_VERSION = '1.3.1';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -92,6 +92,43 @@ function normalizedCapabilities(value) {
|
|
|
92
92
|
return [...new Set(value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim()))];
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function readVersionManifest(file) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
98
|
+
if (parsed?.name !== '@deepseek-ai/dsh') return null;
|
|
99
|
+
return typeof parsed.version === 'string' && parsed.version.trim() ? parsed.version.trim() : null;
|
|
100
|
+
} catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the @deepseek-ai/dsh manifest belonging to an explicit CLI entry.
|
|
105
|
+
*
|
|
106
|
+
* This intentionally accepts only a package manifest whose name is the
|
|
107
|
+
* official Harness package. It lets a Crew-owned source checkout (for example
|
|
108
|
+
* the GitHub-only dsh-v0.1.3-alpha.1 tag) report its real cohort without ever
|
|
109
|
+
* reading the official ~/.dsh profile or trusting an arbitrary package.json.
|
|
110
|
+
*/
|
|
111
|
+
function readExplicitCliVersion(entry) {
|
|
112
|
+
if (typeof entry !== 'string' || !entry.trim()) return null;
|
|
113
|
+
let cursor;
|
|
114
|
+
try { cursor = resolve(entry); } catch { return null; }
|
|
115
|
+
if (extname(cursor).toLowerCase() !== '.js') {
|
|
116
|
+
// A Windows shim normally lives at <root>/node_modules/.bin/dsh.cmd.
|
|
117
|
+
// Starting at its parent still finds the sibling @deepseek-ai/dsh tree.
|
|
118
|
+
cursor = dirname(cursor);
|
|
119
|
+
}
|
|
120
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
121
|
+
const direct = readVersionManifest(join(cursor, 'package.json'));
|
|
122
|
+
if (direct) return direct;
|
|
123
|
+
const nested = readVersionManifest(join(cursor, 'node_modules', '@deepseek-ai', 'dsh', 'package.json'));
|
|
124
|
+
if (nested) return nested;
|
|
125
|
+
const parent = dirname(cursor);
|
|
126
|
+
if (parent === cursor) break;
|
|
127
|
+
cursor = parent;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
95
132
|
export function getHubRuntimeIdentity() {
|
|
96
133
|
return {
|
|
97
134
|
service: 'dsh-crew-hub',
|
|
@@ -109,10 +146,15 @@ export function getHubRuntimeIdentity() {
|
|
|
109
146
|
}
|
|
110
147
|
|
|
111
148
|
// Installed Harness cohort version (the @deepseek-ai/dsh package under the
|
|
112
|
-
// Crew-owned DSH home
|
|
113
|
-
// "unknown cohort", never as a match.
|
|
114
|
-
// verifiers may compare against TARGET_DSH_VERSION.
|
|
115
|
-
export function readCrewDshVersion({ home = homedir() } = {}) {
|
|
149
|
+
// Crew-owned DSH home, or an explicit Crew-owned CLI source checkout). Null
|
|
150
|
+
// when unreadable: callers treat null as "unknown cohort", never as a match.
|
|
151
|
+
// This is the ONLY field cohort verifiers may compare against TARGET_DSH_VERSION.
|
|
152
|
+
export function readCrewDshVersion({ home = homedir(), env = process.env } = {}) {
|
|
153
|
+
// Only the Crew-scoped override is trusted here. A generic DSH_CLI may
|
|
154
|
+
// point at the official web profile and must never become Crew provenance.
|
|
155
|
+
const explicit = env?.DSH_CREW_DSH_CLI;
|
|
156
|
+
const explicitVersion = readExplicitCliVersion(explicit);
|
|
157
|
+
if (explicitVersion) return explicitVersion;
|
|
116
158
|
try {
|
|
117
159
|
const file = join(home, '.config', 'dsh-crew', 'harness', 'runtime', 'node_modules', '@deepseek-ai', 'dsh', 'package.json');
|
|
118
160
|
if (!existsSync(file)) return null;
|
|
@@ -9,7 +9,95 @@ Set-StrictMode -Version Latest
|
|
|
9
9
|
$ErrorActionPreference = 'Stop'
|
|
10
10
|
|
|
11
11
|
$crewHome = Join-Path $env:USERPROFILE '.config\dsh-crew\harness'
|
|
12
|
-
$
|
|
12
|
+
$configuredDshCli = [string] $env:DSH_CREW_DSH_CLI
|
|
13
|
+
if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { $configuredDshCli = '' }
|
|
14
|
+
if (-not $configuredDshCli.Trim() -and $env:DSH_CREW_LAUNCHER_TEST_IMPORT -ne '1') {
|
|
15
|
+
# Crew-managed npm runtime: the normal install path, written by dsh-crew
|
|
16
|
+
# update and carrying whatever cohort that release pinned. An explicit
|
|
17
|
+
# DSH_CREW_DSH_CLI always wins so an operator can still pin another entry.
|
|
18
|
+
$runtimeEntry = Join-Path $crewHome 'runtime\node_modules\@deepseek-ai\dsh\lib\bin.js'
|
|
19
|
+
if (Test-Path -LiteralPath $runtimeEntry -PathType Leaf) {
|
|
20
|
+
$configuredDshCli = $runtimeEntry
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (-not $configuredDshCli.Trim() -and $env:DSH_CREW_LAUNCHER_TEST_IMPORT -ne '1') {
|
|
24
|
+
# Crew-managed source cohort: a runtime-source-<label>.json sidecar names the
|
|
25
|
+
# CLI entry. The sidecar's recorded version must equal the checkout's own
|
|
26
|
+
# manifest, so a stale sidecar can never pin a half-updated tree.
|
|
27
|
+
foreach ($sidecar in @(Get-ChildItem -LiteralPath $crewHome -Filter 'runtime-source-*.json' -File -ErrorAction SilentlyContinue | Sort-Object Name)) {
|
|
28
|
+
try {
|
|
29
|
+
$metadata = Get-Content -LiteralPath $sidecar.FullName -Raw | ConvertFrom-Json
|
|
30
|
+
if ($metadata.managed_by -ne 'dsh-crew') { continue }
|
|
31
|
+
$candidate = [string] $metadata.cli_entry
|
|
32
|
+
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
|
|
33
|
+
$sourceRoot = [IO.Path]::GetFullPath((Join-Path $crewHome ([IO.Path]::GetFileNameWithoutExtension($sidecar.Name))))
|
|
34
|
+
$candidateFull = [IO.Path]::GetFullPath($candidate)
|
|
35
|
+
$sourcePrefix = $sourceRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
|
36
|
+
$sourceManifest = Join-Path $sourceRoot 'apps\cli\package.json'
|
|
37
|
+
$sourceVersion = if (Test-Path -LiteralPath $sourceManifest -PathType Leaf) {
|
|
38
|
+
[string] (Get-Content -LiteralPath $sourceManifest -Raw | ConvertFrom-Json).version
|
|
39
|
+
} else { $null }
|
|
40
|
+
$candidateValid = $sourceVersion -and $sourceVersion -eq [string] $metadata.version `
|
|
41
|
+
-and $candidateFull.StartsWith($sourcePrefix, [StringComparison]::OrdinalIgnoreCase) `
|
|
42
|
+
-and (Test-Path -LiteralPath $candidateFull -PathType Leaf)
|
|
43
|
+
if ($candidateValid) {
|
|
44
|
+
$configuredDshCli = $candidateFull
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
} catch { }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
$dshCli = if ($configuredDshCli.Trim()) {
|
|
51
|
+
try { [IO.Path]::GetFullPath($configuredDshCli) } catch { $configuredDshCli }
|
|
52
|
+
} else {
|
|
53
|
+
Join-Path $crewHome 'runtime\node_modules\.bin\dsh.cmd'
|
|
54
|
+
}
|
|
55
|
+
if ($configuredDshCli.Trim()) { $env:DSH_CREW_DSH_CLI = $dshCli }
|
|
56
|
+
# DSH_HOME that owns the selected CLI. The managed npm runtime shares the Crew
|
|
57
|
+
# home; a Crew-managed source cohort is its own home (its profiles, sessions and
|
|
58
|
+
# settings live inside that tree). Derived from the selected entry so an explicit
|
|
59
|
+
# DSH_CREW_DSH_CLI override resolves correctly too.
|
|
60
|
+
$dshHome = $crewHome
|
|
61
|
+
$sourceRootMatch = [regex]::Match($dshCli, '^(?<root>.+?[\\/]runtime-source-[^\\/]+)[\\/]')
|
|
62
|
+
if ($sourceRootMatch.Success) {
|
|
63
|
+
$candidateRoot = $sourceRootMatch.Groups['root'].Value
|
|
64
|
+
if (Test-Path -LiteralPath (Join-Path $candidateRoot 'profiles') -PathType Container) { $dshHome = $candidateRoot }
|
|
65
|
+
}
|
|
66
|
+
$dshCliIsNodeEntry = [IO.Path]::GetExtension($dshCli).ToLowerInvariant() -eq '.js'
|
|
67
|
+
$dshCommand = if ($dshCliIsNodeEntry) {
|
|
68
|
+
(Get-Command node.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
|
|
69
|
+
} else { $dshCli }
|
|
70
|
+
|
|
71
|
+
function Get-CrewDshManifest {
|
|
72
|
+
param([string] $Entry)
|
|
73
|
+
if (-not $Entry) { return $null }
|
|
74
|
+
try { $cursor = [IO.Path]::GetFullPath($Entry) } catch { return $null }
|
|
75
|
+
if ([IO.Path]::GetExtension($cursor).ToLowerInvariant() -eq '.js') {
|
|
76
|
+
$cursor = Split-Path -Parent $cursor
|
|
77
|
+
} else {
|
|
78
|
+
$cursor = Split-Path -Parent $cursor
|
|
79
|
+
}
|
|
80
|
+
for ($depth = 0; $depth -lt 12 -and $cursor; $depth += 1) {
|
|
81
|
+
$candidates = @(
|
|
82
|
+
(Join-Path $cursor 'package.json'),
|
|
83
|
+
(Join-Path $cursor 'node_modules\@deepseek-ai\dsh\package.json')
|
|
84
|
+
)
|
|
85
|
+
foreach ($candidate in $candidates) {
|
|
86
|
+
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
|
|
87
|
+
try {
|
|
88
|
+
$manifest = Get-Content -LiteralPath $candidate -Raw | ConvertFrom-Json
|
|
89
|
+
$knownOfficial = -not $configuredDshCli.Trim() -or $manifest.name -eq '@deepseek-ai/dsh'
|
|
90
|
+
if ($knownOfficial -and [string] $manifest.version) { return $manifest }
|
|
91
|
+
} catch { }
|
|
92
|
+
}
|
|
93
|
+
$parent = Split-Path -Parent $cursor
|
|
94
|
+
if ($parent -eq $cursor) { break }
|
|
95
|
+
$cursor = $parent
|
|
96
|
+
}
|
|
97
|
+
return $null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$dshManifest = Get-CrewDshManifest -Entry $dshCli
|
|
13
101
|
$logRoot = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
|
|
14
102
|
$launcherLog = Join-Path $logRoot 'dsh-crew-launcher.log'
|
|
15
103
|
$startedAt = Get-Date
|
|
@@ -25,7 +113,7 @@ try {
|
|
|
25
113
|
} finally { $hashStream.Dispose() }
|
|
26
114
|
} catch { $launcherHelperHash = $null }
|
|
27
115
|
$services = @(
|
|
28
|
-
[pscustomobject]@{ Name = 'Crew backend'; Profile = 'dsh-crew'; Home = $
|
|
116
|
+
[pscustomobject]@{ Name = 'Crew backend'; Profile = 'dsh-crew'; Home = $dshHome; Port = 3210; Url = 'http://127.0.0.1:3210'; CrewOwned = $true; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null }
|
|
29
117
|
)
|
|
30
118
|
|
|
31
119
|
function Write-LaunchLog {
|
|
@@ -58,7 +146,7 @@ function Resolve-OfficialHarnessCommand {
|
|
|
58
146
|
}
|
|
59
147
|
|
|
60
148
|
function Test-OfficialHarnessListener {
|
|
61
|
-
param([int] $OwnerPid, [pscustomobject] $Official)
|
|
149
|
+
param([int] $OwnerPid, [pscustomobject] $Official, [string] $Profile = 'web')
|
|
62
150
|
if ($OwnerPid -le 0) { return $false }
|
|
63
151
|
try {
|
|
64
152
|
$owner = Get-CimInstance Win32_Process -Filter ('ProcessId={0}' -f $OwnerPid) -ErrorAction Stop
|
|
@@ -66,11 +154,154 @@ function Test-OfficialHarnessListener {
|
|
|
66
154
|
$line = ([string] $owner.CommandLine).Replace('/', '\')
|
|
67
155
|
$entry = [regex]::Escape($Official.Entry.Replace('/', '\'))
|
|
68
156
|
$node = [regex]::Escape($Official.NodePath.Replace('/', '\'))
|
|
69
|
-
$
|
|
157
|
+
$profileToken = [regex]::Escape($Profile)
|
|
158
|
+
$pattern = '^\s*(?:"?' + $node + '"?|"?node(?:\.exe)?"?)\s+"?' + $entry + '"?\s+(?:"?' + $profileToken + '"?|--profile\s+"?' + $profileToken + '"?)(?:\s|$)'
|
|
70
159
|
return $line -match $pattern
|
|
71
160
|
} catch { return $false }
|
|
72
161
|
}
|
|
73
162
|
|
|
163
|
+
function Test-CrewDshCliPreflight {
|
|
164
|
+
if ($null -eq $dshManifest -or [string]::IsNullOrWhiteSpace([string] $dshManifest.version)) {
|
|
165
|
+
throw "The DSH CLI at $dshCli is not a verified @deepseek-ai/dsh package entry."
|
|
166
|
+
}
|
|
167
|
+
if ($dshCliIsNodeEntry -and -not (Test-Path -LiteralPath (Join-Path (Join-Path $dshHome 'profiles') 'web\package.json') -PathType Leaf)) {
|
|
168
|
+
throw ('The Harness web profile is missing under {0}. Run: dsh-crew update' -f $dshHome)
|
|
169
|
+
}
|
|
170
|
+
$versionArgs = if ($dshCliIsNodeEntry) { @($dshCli, '--version') } else { @('--version') }
|
|
171
|
+
$reported = try { (& $dshCommand @versionArgs 2>&1 | Out-String).Trim() } catch { '' }
|
|
172
|
+
if ($LASTEXITCODE -ne 0 -or $reported -ne [string] $dshManifest.version) {
|
|
173
|
+
throw ('DSH CLI preflight failed: manifest={0}, reported={1}' -f $dshManifest.version, ($reported -replace '\s+', ' '))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function Get-CrewWebSessionUrl {
|
|
178
|
+
param([string] $OutputLog = $null)
|
|
179
|
+
$logs = if ($OutputLog) {
|
|
180
|
+
@(Get-Item -LiteralPath $OutputLog -ErrorAction SilentlyContinue)
|
|
181
|
+
} else {
|
|
182
|
+
@(Get-ChildItem -LiteralPath $logRoot -Filter 'dsh-crew-web-*.out.log' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending)
|
|
183
|
+
}
|
|
184
|
+
foreach ($log in $logs) {
|
|
185
|
+
try {
|
|
186
|
+
$text = Get-Content -LiteralPath $log.FullName -Raw -ErrorAction Stop
|
|
187
|
+
$match = [regex]::Match($text, 'https?://127\.0\.0\.1:3080/\?token=[^\s\r\n]+')
|
|
188
|
+
if ($match.Success) { return $match.Value }
|
|
189
|
+
} catch { }
|
|
190
|
+
}
|
|
191
|
+
return $null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function Open-CrewBrowserUrl {
|
|
195
|
+
param([Parameter(Mandatory = $true)] [string] $Url)
|
|
196
|
+
$edge = Get-Command msedge.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
197
|
+
if (-not $edge) {
|
|
198
|
+
$edgeCandidates = @(
|
|
199
|
+
@(
|
|
200
|
+
(Join-Path ${env:ProgramFiles(x86)} 'Microsoft\Edge\Application\msedge.exe'),
|
|
201
|
+
(Join-Path $env:ProgramFiles 'Microsoft\Edge\Application\msedge.exe'),
|
|
202
|
+
(Join-Path $env:LOCALAPPDATA 'Microsoft\Edge\Application\msedge.exe')
|
|
203
|
+
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) }
|
|
204
|
+
)
|
|
205
|
+
if ($edgeCandidates.Count -gt 0) { $edge = [pscustomobject]@{ Source = $edgeCandidates[0] } }
|
|
206
|
+
}
|
|
207
|
+
if ($edge) {
|
|
208
|
+
Start-Process -FilePath $edge.Source -ArgumentList @('--new-window', $Url) | Out-Null
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
Start-Process $Url | Out-Null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function Test-CrewWebSessionUrl {
|
|
215
|
+
param([string] $Url)
|
|
216
|
+
if (-not $Url) { return $false }
|
|
217
|
+
try {
|
|
218
|
+
# The token endpoint intentionally answers with a 303 + Set-Cookie and
|
|
219
|
+
# redirects to `/`; do not follow it here or PowerShell loses the cookie
|
|
220
|
+
# and falsely reports the authenticated page as 404.
|
|
221
|
+
$response = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -Uri $Url -TimeoutSec 3
|
|
222
|
+
return [int] $response.StatusCode -ge 200 -and [int] $response.StatusCode -lt 400
|
|
223
|
+
} catch {
|
|
224
|
+
$status = if ($_.Exception.Response) { [int] $_.Exception.Response.StatusCode } else { 0 }
|
|
225
|
+
return $status -in @(301, 302, 303, 307, 308)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function Open-CrewManagedFrontend {
|
|
230
|
+
param([int] $TimeoutSeconds = 90)
|
|
231
|
+
# The 3080 frontend boots from the same Crew-managed Harness entry as 3210,
|
|
232
|
+
# against that entry's own DSH_HOME (npm runtime -> Crew home, source cohort
|
|
233
|
+
# -> its own tree). No official ~/.dsh state is read or written here.
|
|
234
|
+
$managedHome = $dshHome
|
|
235
|
+
$managedProfile = 'web'
|
|
236
|
+
$profileManifest = Join-Path (Join-Path $managedHome 'profiles') ('{0}\package.json' -f $managedProfile)
|
|
237
|
+
if (-not $dshCliIsNodeEntry -or -not (Test-Path -LiteralPath $profileManifest -PathType Leaf)) {
|
|
238
|
+
return $false
|
|
239
|
+
}
|
|
240
|
+
$official = [pscustomobject]@{ NodePath = $dshCommand; Entry = $dshCli; Profile = $managedProfile }
|
|
241
|
+
$frontend = Get-OfficialFrontendOverlay
|
|
242
|
+
$mutex = New-Object System.Threading.Mutex($false, 'Local\DSHCrewOfficialFrontendLauncher')
|
|
243
|
+
$locked = $false
|
|
244
|
+
try {
|
|
245
|
+
try { $locked = $mutex.WaitOne(30000) } catch [System.Threading.AbandonedMutexException] { $locked = $true }
|
|
246
|
+
if (-not $locked) { throw 'Another desktop launch is still starting the Crew-managed Harness frontend.' }
|
|
247
|
+
$port = Get-PortState -Port 3080
|
|
248
|
+
if ($port.State -eq 'unknown') { throw $port.Error }
|
|
249
|
+
if ($port.State -eq 'occupied') {
|
|
250
|
+
if (-not (Test-OfficialHarnessListener -OwnerPid $port.Pid -Official $official -Profile $managedProfile)) {
|
|
251
|
+
throw 'Port 3080 is occupied by a non-Crew-managed process; it was left untouched.'
|
|
252
|
+
}
|
|
253
|
+
if (-not (Test-OfficialWebReady)) { throw 'The Crew-managed 3080 frontend is not ready; its process was left running.' }
|
|
254
|
+
$existingUrl = Get-CrewWebSessionUrl
|
|
255
|
+
if ($existingUrl -and (Test-CrewWebSessionUrl -Url $existingUrl)) {
|
|
256
|
+
Open-CrewBrowserUrl -Url $existingUrl
|
|
257
|
+
Write-LaunchLog 'Opened the existing Crew-managed Harness frontend on 3080 with its session URL.'
|
|
258
|
+
return $true
|
|
259
|
+
}
|
|
260
|
+
Write-LaunchLog 'The existing Crew-managed 3080 session URL is stale; restarting the verified listener.' 'WARN'
|
|
261
|
+
Stop-Process -Id $port.Pid -Force -ErrorAction Stop
|
|
262
|
+
$freeDeadline = (Get-Date).AddSeconds(5)
|
|
263
|
+
do { Start-Sleep -Milliseconds 250; $port = Get-PortState -Port 3080 } while ($port.State -ne 'free' -and (Get-Date) -lt $freeDeadline)
|
|
264
|
+
if ($port.State -ne 'free') { throw 'The stale Crew-managed 3080 listener did not release the port.' }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
$stamp = '{0}-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmssfff'), $PID
|
|
268
|
+
$stdout = Join-Path $logRoot ('dsh-crew-web-{0}.out.log' -f $stamp)
|
|
269
|
+
$stderr = Join-Path $logRoot ('dsh-crew-web-{0}.err.log' -f $stamp)
|
|
270
|
+
$previousHome = $env:DSH_HOME
|
|
271
|
+
$previousCli = $env:DSH_CREW_DSH_CLI
|
|
272
|
+
try {
|
|
273
|
+
$env:DSH_HOME = $managedHome
|
|
274
|
+
$env:DSH_CREW_DSH_CLI = $dshCli
|
|
275
|
+
$arguments = @($dshCli, '--profile', $managedProfile, '--patch', ('"{0}"' -f $frontend.Path), '--host', '127.0.0.1', '--port', '3080')
|
|
276
|
+
$process = Start-Process -FilePath $dshCommand -ArgumentList $arguments -WindowStyle Hidden -PassThru `
|
|
277
|
+
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
278
|
+
} finally {
|
|
279
|
+
$env:DSH_HOME = $previousHome
|
|
280
|
+
$env:DSH_CREW_DSH_CLI = $previousCli
|
|
281
|
+
}
|
|
282
|
+
Write-LaunchLog ('Started Crew-managed Harness on 3080; PID={0}. Logs: {1}, {2}' -f $process.Id, $stdout, $stderr)
|
|
283
|
+
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
284
|
+
do {
|
|
285
|
+
$port = Get-PortState -Port 3080
|
|
286
|
+
if ($port.State -eq 'occupied') {
|
|
287
|
+
if (-not (Test-OfficialHarnessListener -OwnerPid $port.Pid -Official $official -Profile $managedProfile)) {
|
|
288
|
+
throw 'Port 3080 became occupied by an unverified process; it was left untouched.'
|
|
289
|
+
}
|
|
290
|
+
if (Test-OfficialWebReady) {
|
|
291
|
+
$startedUrl = Get-CrewWebSessionUrl -OutputLog $stdout
|
|
292
|
+
if ($startedUrl) { Open-CrewBrowserUrl -Url $startedUrl; return $true }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if ($process.HasExited) { throw ('Crew-managed Harness exited before readiness. Diagnostic log: {0}' -f $stderr) }
|
|
296
|
+
Start-Sleep -Milliseconds 500
|
|
297
|
+
} while ((Get-Date) -lt $deadline)
|
|
298
|
+
throw ('The Crew-managed Harness did not become ready within {0}s; inspect {1}.' -f $TimeoutSeconds, $stderr)
|
|
299
|
+
} finally {
|
|
300
|
+
if ($locked) { $mutex.ReleaseMutex() }
|
|
301
|
+
$mutex.Dispose()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
74
305
|
function Test-OfficialWebReady {
|
|
75
306
|
try {
|
|
76
307
|
$response = Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:3080/' -TimeoutSec 2
|
|
@@ -82,28 +313,28 @@ function Test-OfficialWebReady {
|
|
|
82
313
|
}
|
|
83
314
|
}
|
|
84
315
|
|
|
85
|
-
function Get-OfficialFrontendOverlay {
|
|
86
|
-
$frontendRoot = Join-Path $env:USERPROFILE '.config\dsh-crew\frontend'
|
|
87
|
-
$path = Join-Path $frontendRoot 'official-web.patch.json'
|
|
88
|
-
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw 'Crew frontend overlay is missing. Run dsh-crew update with the current GitHub candidate.' }
|
|
89
|
-
# Windows PowerShell 5.1 keeps a JSON root array as one pipeline object;
|
|
90
|
-
# direct `$patch[0].insert[0]` therefore resolves `insert` as IList.Insert
|
|
91
|
-
# instead of indexing the JSON property. Flatten both PowerShell generations
|
|
92
|
-
# explicitly and read properties through PSObject so StrictMode reports a
|
|
93
|
-
# useful validation error instead of "property id cannot be found".
|
|
94
|
-
$parsed = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
|
|
95
|
-
$patch = @($parsed | ForEach-Object { $_ })
|
|
96
|
-
$root = if ($patch.Count -eq 1) { $patch[0] } else { $null }
|
|
97
|
-
$insertProperty = if ($null -ne $root) { $root.PSObject.Properties['insert'] } else { $null }
|
|
98
|
-
$insertValue = if ($null -ne $insertProperty) { $insertProperty.Value } else { $null }
|
|
99
|
-
$inserts = @($insertValue | ForEach-Object { $_ })
|
|
100
|
-
$bridge = if ($inserts.Count -eq 1) { $inserts[0] } else { $null }
|
|
101
|
-
$idProperty = if ($null -ne $bridge) { $bridge.PSObject.Properties['id'] } else { $null }
|
|
102
|
-
$nameProperty = if ($null -ne $bridge) { $bridge.PSObject.Properties['name'] } else { $null }
|
|
103
|
-
if ($patch.Count -ne 1 -or $inserts.Count -ne 1 -or $null -eq $idProperty -or [string] $idProperty.Value -ne 'dsh-crew-official-web-bridge' -or $null -eq $nameProperty) {
|
|
104
|
-
throw 'Crew frontend overlay has an unexpected structure.'
|
|
105
|
-
}
|
|
106
|
-
$uri = [Uri] ([string] $nameProperty.Value)
|
|
316
|
+
function Get-OfficialFrontendOverlay {
|
|
317
|
+
$frontendRoot = Join-Path $env:USERPROFILE '.config\dsh-crew\frontend'
|
|
318
|
+
$path = Join-Path $frontendRoot 'official-web.patch.json'
|
|
319
|
+
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw 'Crew frontend overlay is missing. Run dsh-crew update with the current GitHub candidate.' }
|
|
320
|
+
# Windows PowerShell 5.1 keeps a JSON root array as one pipeline object;
|
|
321
|
+
# direct `$patch[0].insert[0]` therefore resolves `insert` as IList.Insert
|
|
322
|
+
# instead of indexing the JSON property. Flatten both PowerShell generations
|
|
323
|
+
# explicitly and read properties through PSObject so StrictMode reports a
|
|
324
|
+
# useful validation error instead of "property id cannot be found".
|
|
325
|
+
$parsed = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
|
|
326
|
+
$patch = @($parsed | ForEach-Object { $_ })
|
|
327
|
+
$root = if ($patch.Count -eq 1) { $patch[0] } else { $null }
|
|
328
|
+
$insertProperty = if ($null -ne $root) { $root.PSObject.Properties['insert'] } else { $null }
|
|
329
|
+
$insertValue = if ($null -ne $insertProperty) { $insertProperty.Value } else { $null }
|
|
330
|
+
$inserts = @($insertValue | ForEach-Object { $_ })
|
|
331
|
+
$bridge = if ($inserts.Count -eq 1) { $inserts[0] } else { $null }
|
|
332
|
+
$idProperty = if ($null -ne $bridge) { $bridge.PSObject.Properties['id'] } else { $null }
|
|
333
|
+
$nameProperty = if ($null -ne $bridge) { $bridge.PSObject.Properties['name'] } else { $null }
|
|
334
|
+
if ($patch.Count -ne 1 -or $inserts.Count -ne 1 -or $null -eq $idProperty -or [string] $idProperty.Value -ne 'dsh-crew-official-web-bridge' -or $null -eq $nameProperty) {
|
|
335
|
+
throw 'Crew frontend overlay has an unexpected structure.'
|
|
336
|
+
}
|
|
337
|
+
$uri = [Uri] ([string] $nameProperty.Value)
|
|
107
338
|
if (-not $uri.IsFile) { throw 'Crew frontend must load from its local snapshot.' }
|
|
108
339
|
$entry = [IO.Path]::GetFullPath($uri.LocalPath)
|
|
109
340
|
$prefix = [IO.Path]::GetFullPath((Join-Path $frontendRoot 'revisions')) + [IO.Path]::DirectorySeparatorChar
|
|
@@ -126,6 +357,10 @@ function Test-OfficialFrontendAttached {
|
|
|
126
357
|
|
|
127
358
|
function Open-OfficialFrontend {
|
|
128
359
|
param([int] $TimeoutSeconds = 90)
|
|
360
|
+
# Preferred path: the Crew-managed Harness serves 3080 with the Crew panel
|
|
361
|
+
# already loaded. The legacy fallback below (separate official install in
|
|
362
|
+
# ~/.dsh) is only reached when no Crew-managed frontend is available.
|
|
363
|
+
if (Open-CrewManagedFrontend -TimeoutSeconds $TimeoutSeconds) { return }
|
|
129
364
|
$official = Resolve-OfficialHarnessCommand
|
|
130
365
|
$frontend = Get-OfficialFrontendOverlay
|
|
131
366
|
$mutex = New-Object System.Threading.Mutex($false, 'Local\DSHCrewOfficialFrontendLauncher')
|
|
@@ -196,12 +431,10 @@ function Get-HealthState {
|
|
|
196
431
|
# the process cannot be proven to match the tree it will next boot.
|
|
197
432
|
$expectedDshVersion = $null
|
|
198
433
|
$diskReadable = $false
|
|
199
|
-
$
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if ($expectedDshVersion) { $diskReadable = $true }
|
|
204
|
-
} catch { $expectedDshVersion = $null }
|
|
434
|
+
$activeDshManifest = Get-CrewDshManifest -Entry $dshCli
|
|
435
|
+
if ($null -ne $activeDshManifest -and [string] $activeDshManifest.version) {
|
|
436
|
+
$expectedDshVersion = [string] $activeDshManifest.version
|
|
437
|
+
$diskReadable = $true
|
|
205
438
|
}
|
|
206
439
|
$cohortMatches = $diskReadable -and $runtime.dsh_version -eq $expectedDshVersion
|
|
207
440
|
if ($response.ok -eq $true -and $version -and $cohortMatches) {
|
|
@@ -814,7 +1047,8 @@ function Start-CrewService {
|
|
|
814
1047
|
try {
|
|
815
1048
|
$env:DSH_HOME = $Service.Home
|
|
816
1049
|
$arguments = @('--profile', $Service.Profile, '--host', '127.0.0.1', '--port', [string] $Service.Port, '--no-open')
|
|
817
|
-
$
|
|
1050
|
+
$launchArguments = if ($dshCliIsNodeEntry) { @($dshCli) + $arguments } else { $arguments }
|
|
1051
|
+
$process = Start-Process -FilePath $dshCommand -ArgumentList $launchArguments -WindowStyle Hidden -PassThru `
|
|
818
1052
|
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
819
1053
|
$Service.Process = $process
|
|
820
1054
|
$Service.RootPid = $process.Id
|
|
@@ -1034,14 +1268,14 @@ if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { return }
|
|
|
1034
1268
|
try {
|
|
1035
1269
|
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
|
|
1036
1270
|
Write-LaunchLog ('Launcher started; mode={0}; user={1}' -f $Mode, $env:USERNAME)
|
|
1037
|
-
if ($Mode -eq 'open') { Open-OfficialFrontend }
|
|
1038
|
-
|
|
1039
1271
|
if (-not (Test-Path -LiteralPath $dshCli -PathType Leaf)) {
|
|
1040
|
-
throw "DSH CLI was not found at $dshCli.
|
|
1272
|
+
throw "DSH CLI was not found at $dshCli. Configure DSH_CREW_DSH_CLI to a Crew-owned official CLI entry, or run: npm install -g @ran-sh/dsh-crew@latest; dsh-crew update"
|
|
1041
1273
|
}
|
|
1274
|
+
Test-CrewDshCliPreflight
|
|
1042
1275
|
if (-not (Test-Path -LiteralPath (Join-Path $crewHome 'profiles\dsh-crew\package.json') -PathType Leaf)) {
|
|
1043
1276
|
throw "The isolated dsh-crew profile is missing under $crewHome. Run: dsh-crew update"
|
|
1044
1277
|
}
|
|
1278
|
+
if ($Mode -eq 'open') { Open-OfficialFrontend }
|
|
1045
1279
|
# Background/watch modes remain independent of the official frontend.
|
|
1046
1280
|
|
|
1047
1281
|
if ($Mode -eq 'watch') {
|
package/worker.cordis.yml
CHANGED
|
@@ -20,15 +20,11 @@
|
|
|
20
20
|
mode: !!js process.env.DSH_SANDBOX_MODE ?? 'workspace-write'
|
|
21
21
|
workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd()
|
|
22
22
|
|
|
23
|
-
- id: fs-local
|
|
24
|
-
config:
|
|
25
|
-
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
|
26
|
-
|
|
27
23
|
- id: system-prompt
|
|
28
24
|
config:
|
|
29
25
|
includeHarnessIdentity: false
|
|
30
26
|
includeRuntimeContext: false
|
|
31
|
-
|
|
27
|
+
personaPrefix: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a focused coding worker. Complete the assigned task, then summarize what you did in your final message.'
|
|
32
28
|
|
|
33
29
|
- id: agent-loop
|
|
34
30
|
config:
|
|
@@ -40,6 +36,14 @@
|
|
|
40
36
|
compression: zstd
|
|
41
37
|
|
|
42
38
|
- insert:
|
|
39
|
+
# sdk-minimal dropped its own fs-local row in the 0.1.5 cohort, but the
|
|
40
|
+
# Crew tool suite still injects the `fs` service, so Crew owns the row and
|
|
41
|
+
# keeps the worker's editor rooted at the task workspace.
|
|
42
|
+
- id: fs-local
|
|
43
|
+
name: '@deepseek-ai/dsh-fs-local'
|
|
44
|
+
config:
|
|
45
|
+
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
|
46
|
+
|
|
43
47
|
- id: fs-observation-policy
|
|
44
48
|
name: '@deepseek-ai/dsh-fs-observation-policy'
|
|
45
49
|
|