@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.5
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/marketplace.json +16 -17
- package/.claude-plugin/plugin.json +7 -1
- package/README.md +44 -15
- package/README.zh.md +37 -12
- package/agents/ds-flash.md +16 -18
- package/agents/ds-pro.md +16 -24
- package/agents/ds-reviewer.md +15 -15
- package/agents/ds-worker.md +16 -16
- package/bin/dsh-crew.mjs +16 -12
- package/codex/AGENTS.md +37 -85
- package/codex/agents/ds-flash.toml +18 -14
- package/codex/agents/ds-pro.toml +18 -15
- package/codex/agents/ds-reviewer.toml +17 -13
- package/codex/agents/ds-worker.toml +17 -13
- package/codex/prompts/dsh-config.md +17 -1
- package/codex/prompts/dsh-status.md +5 -1
- package/commands/config.md +16 -4
- package/commands/status.md +6 -2
- package/docs/installation.md +54 -9
- package/docs/readiness-matrix.md +14 -6
- package/docs/ui-surfaces.md +32 -17
- package/lib/client.js +769 -177
- package/official-web-bridge/lib/client.js +297 -209
- package/official-web-bridge/overlay-entry.mjs +59 -0
- package/package.json +6 -3
- package/scripts/build-client.mjs +2 -0
- package/scripts/setup.mjs +8 -7
- package/scripts/verify-crew-ui-polish.mjs +145 -0
- package/scripts/verify-history-ui.mjs +97 -0
- package/scripts/verify-npm-install.mjs +1 -1
- package/src/client/history-panel.tsx +108 -0
- package/src/client/host-readiness.mjs +15 -6
- package/src/client/index.tsx +91 -69
- package/src/client/model-callability-view.mjs +17 -0
- package/src/client/panel-chrome.tsx +40 -0
- package/src/client/quick-panel.tsx +88 -73
- package/src/client/readiness-envelope.mjs +34 -0
- package/src/client/task-telemetry.mjs +11 -4
- package/src/config-readiness.mjs +181 -115
- package/src/extension-contract.mjs +44 -26
- package/src/history/admission-gate.mjs +51 -0
- package/src/history/archive-store.mjs +261 -0
- package/src/history/cleanup-plan.mjs +64 -0
- package/src/history/http.mjs +32 -0
- package/src/history/operation.mjs +86 -0
- package/src/history/runner.mjs +51 -0
- package/src/history/runtime.mjs +28 -0
- package/src/history/service.mjs +137 -0
- package/src/history/state.mjs +27 -0
- package/src/hub/index.mjs +141 -24
- package/src/hub-client.mjs +17 -15
- package/src/image-output.mjs +71 -0
- package/src/install/install-legacy.mjs +126 -23
- package/src/install/npx-lifecycle.mjs +373 -64
- package/src/install/official-frontend-assets.mjs +78 -0
- package/src/install/payload-content.mjs +88 -0
- package/src/install/windows-startup.mjs +107 -19
- package/src/install/windows-supervisor-adapter.mjs +443 -0
- package/src/install/windows-supervisor-lifecycle.mjs +782 -0
- package/src/install/zcode.mjs +53 -14
- package/src/job-contracts.mjs +37 -31
- package/src/jobs.mjs +2 -2
- package/src/mcp-runtime.mjs +74 -9
- package/src/model-callability-contract.mjs +79 -0
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +0 -2
- package/src/provider-health.mjs +130 -124
- package/src/readiness-matrix.mjs +20 -15
- package/src/runtime-identity-contract.mjs +34 -0
- package/src/runtime-identity.mjs +14 -4
- package/src/runtime-readiness-snapshot.mjs +285 -87
- package/src/server.mjs +86 -76
- package/src/standalone-sdk.mjs +23 -0
- package/src/supervisor/restart-request.mjs +64 -10
- package/src/workflow-runtime.mjs +28 -13
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +327 -51
- package/windows/supervisor-control.ps1 +467 -0
- package/zcode/AGENTS.md +4 -1
- package/zcode/agents/ds-reviewer.md +31 -36
- package/zcode/agents/ds-worker.md +31 -39
- package/zcode/commands/dsh-config.md +17 -6
- package/zcode/commands/dsh-status.md +5 -4
- package/.mcp.json +0 -8
|
@@ -9,13 +9,22 @@ Set-StrictMode -Version Latest
|
|
|
9
9
|
$ErrorActionPreference = 'Stop'
|
|
10
10
|
|
|
11
11
|
$crewHome = Join-Path $env:USERPROFILE '.config\dsh-crew\harness'
|
|
12
|
-
$officialHome = Join-Path $env:USERPROFILE '.dsh'
|
|
13
12
|
$dshCli = Join-Path $crewHome 'runtime\node_modules\.bin\dsh.cmd'
|
|
14
13
|
$logRoot = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
|
|
15
14
|
$launcherLog = Join-Path $logRoot 'dsh-crew-launcher.log'
|
|
16
15
|
$startedAt = Get-Date
|
|
16
|
+
$launcherProcessStartedAtUtcTicks = try { (Get-Process -Id $PID -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture) } catch { $null }
|
|
17
|
+
$supervisorInstanceId = [guid]::NewGuid().ToString()
|
|
18
|
+
$launcherHelperHash = $null
|
|
19
|
+
try {
|
|
20
|
+
$hashStream = [System.IO.File]::OpenRead($PSCommandPath)
|
|
21
|
+
try {
|
|
22
|
+
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
|
23
|
+
try { $launcherHelperHash = (($sha256.ComputeHash($hashStream) | ForEach-Object { $_.ToString('x2') }) -join '') }
|
|
24
|
+
finally { $sha256.Dispose() }
|
|
25
|
+
} finally { $hashStream.Dispose() }
|
|
26
|
+
} catch { $launcherHelperHash = $null }
|
|
17
27
|
$services = @(
|
|
18
|
-
[pscustomobject]@{ Name = 'Official UI'; Profile = 'web'; Home = $officialHome; Port = 3080; Url = 'http://127.0.0.1:3080'; CrewOwned = $false; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null },
|
|
19
28
|
[pscustomobject]@{ Name = 'Crew backend'; Profile = 'dsh-crew'; Home = $crewHome; 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 }
|
|
20
29
|
)
|
|
21
30
|
|
|
@@ -30,11 +39,149 @@ function Write-LaunchLog {
|
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function Resolve-OfficialHarnessCommand {
|
|
43
|
+
$shim = Get-Command dsh.cmd -CommandType Application -ErrorAction Stop | Select-Object -First 1
|
|
44
|
+
$root = Join-Path (Split-Path -Parent $shim.Source) 'node_modules\@deepseek-ai\dsh'
|
|
45
|
+
$manifest = Get-Content -LiteralPath (Join-Path $root 'package.json') -Raw | ConvertFrom-Json
|
|
46
|
+
if ($manifest.name -ne '@deepseek-ai/dsh') { throw 'The dsh command is not a verified official Harness installation.' }
|
|
47
|
+
$entrySpec = [string] $manifest.bin.dsh
|
|
48
|
+
if (-not $entrySpec -or [IO.Path]::IsPathRooted($entrySpec) -or $entrySpec.Contains(':') -or ($entrySpec.Replace('\', '/').Split('/') -contains '..')) {
|
|
49
|
+
throw 'The official Harness CLI entry is invalid.'
|
|
50
|
+
}
|
|
51
|
+
$entry = [IO.Path]::GetFullPath((Join-Path $root $entrySpec))
|
|
52
|
+
$rootPrefix = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
|
53
|
+
if (-not $entry.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $entry -PathType Leaf)) {
|
|
54
|
+
throw 'The official Harness CLI entry was not found.'
|
|
55
|
+
}
|
|
56
|
+
$node = Get-Command node.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1
|
|
57
|
+
return [pscustomobject]@{ NodePath = $node.Source; Entry = $entry }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Test-OfficialHarnessListener {
|
|
61
|
+
param([int] $OwnerPid, [pscustomobject] $Official)
|
|
62
|
+
if ($OwnerPid -le 0) { return $false }
|
|
63
|
+
try {
|
|
64
|
+
$owner = Get-CimInstance Win32_Process -Filter ('ProcessId={0}' -f $OwnerPid) -ErrorAction Stop
|
|
65
|
+
if (-not $owner -or [IO.Path]::GetFileName([string] $owner.ExecutablePath) -ine 'node.exe') { return $false }
|
|
66
|
+
$line = ([string] $owner.CommandLine).Replace('/', '\')
|
|
67
|
+
$entry = [regex]::Escape($Official.Entry.Replace('/', '\'))
|
|
68
|
+
$node = [regex]::Escape($Official.NodePath.Replace('/', '\'))
|
|
69
|
+
$pattern = '^\s*(?:"?' + $node + '"?|"?node(?:\.exe)?"?)\s+"?' + $entry + '"?\s+(?:"?web"?|--profile\s+"?web"?)(?:\s|$)'
|
|
70
|
+
return $line -match $pattern
|
|
71
|
+
} catch { return $false }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Test-OfficialWebReady {
|
|
75
|
+
try {
|
|
76
|
+
$response = Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:3080/' -TimeoutSec 2
|
|
77
|
+
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
|
|
78
|
+
} catch {
|
|
79
|
+
# An authenticated official UI may reject this cookie-free health request.
|
|
80
|
+
if ($_.Exception.Response) { return [int] $_.Exception.Response.StatusCode -in @(401, 403) }
|
|
81
|
+
return $false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
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)
|
|
107
|
+
if (-not $uri.IsFile) { throw 'Crew frontend must load from its local snapshot.' }
|
|
108
|
+
$entry = [IO.Path]::GetFullPath($uri.LocalPath)
|
|
109
|
+
$prefix = [IO.Path]::GetFullPath((Join-Path $frontendRoot 'revisions')) + [IO.Path]::DirectorySeparatorChar
|
|
110
|
+
if (-not $entry.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'Crew frontend snapshot is outside its owned directory.' }
|
|
111
|
+
$metadata = Get-Content -LiteralPath (Join-Path (Split-Path -Parent $entry) 'package.json') -Raw | ConvertFrom-Json
|
|
112
|
+
$revision = [string] $metadata.dshCrewFrontendRevision
|
|
113
|
+
if ($metadata.dshCrewManagedFrontend -ne $true -or $revision -notmatch '^[a-f0-9]{64}$' -or -not (Test-Path -LiteralPath $entry -PathType Leaf)) {
|
|
114
|
+
throw 'Crew frontend snapshot is incomplete.'
|
|
115
|
+
}
|
|
116
|
+
return [pscustomobject]@{ Path = $path; Revision = $revision }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Test-OfficialFrontendAttached {
|
|
120
|
+
param([string] $Revision)
|
|
121
|
+
try {
|
|
122
|
+
$response = Invoke-RestMethod -Uri 'http://127.0.0.1:3080/_dsh/dsh-crew/bridge-status' -TimeoutSec 2
|
|
123
|
+
return $response.ok -eq $true -and $response.surface -eq 'official-bridge' -and $response.frontend_revision -eq $Revision
|
|
124
|
+
} catch { return $false }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Open-OfficialFrontend {
|
|
128
|
+
param([int] $TimeoutSeconds = 90)
|
|
129
|
+
$official = Resolve-OfficialHarnessCommand
|
|
130
|
+
$frontend = Get-OfficialFrontendOverlay
|
|
131
|
+
$mutex = New-Object System.Threading.Mutex($false, 'Local\DSHCrewOfficialFrontendLauncher')
|
|
132
|
+
$locked = $false
|
|
133
|
+
try {
|
|
134
|
+
try { $locked = $mutex.WaitOne(30000) } catch [System.Threading.AbandonedMutexException] { $locked = $true }
|
|
135
|
+
if (-not $locked) { throw 'Another desktop launch is still starting the official frontend.' }
|
|
136
|
+
$port = Get-PortState -Port 3080
|
|
137
|
+
if ($port.State -eq 'unknown') { throw $port.Error }
|
|
138
|
+
if ($port.State -eq 'occupied') {
|
|
139
|
+
if (-not (Test-OfficialHarnessListener -OwnerPid $port.Pid -Official $official)) { throw 'Port 3080 is occupied by an unverified process; it was left untouched.' }
|
|
140
|
+
if (-not (Test-OfficialWebReady)) { throw 'The official 3080 frontend is not ready; its process was left running.' }
|
|
141
|
+
if (-not (Test-OfficialFrontendAttached -Revision $frontend.Revision)) { throw 'The running official frontend has not loaded the current Crew panel. Stop that official instance when idle, then launch from the desktop again. It was left untouched.' }
|
|
142
|
+
Start-Process 'http://127.0.0.1:3080/' | Out-Null
|
|
143
|
+
Write-LaunchLog 'Opened the existing official Harness frontend on 3080.'
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$stamp = '{0}-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmssfff'), $PID
|
|
148
|
+
$stdout = Join-Path $logRoot ('dsh-official-web-{0}.out.log' -f $stamp)
|
|
149
|
+
$stderr = Join-Path $logRoot ('dsh-official-web-{0}.err.log' -f $stamp)
|
|
150
|
+
$previousHome = $env:DSH_HOME
|
|
151
|
+
try {
|
|
152
|
+
# The official application owns its normal state; no Crew plugin/profile
|
|
153
|
+
# registration or repair is performed in this home by the launcher.
|
|
154
|
+
$env:DSH_HOME = Join-Path $env:USERPROFILE '.dsh'
|
|
155
|
+
$arguments = @(('"{0}"' -f $official.Entry), 'web', '--patch', ('"{0}"' -f $frontend.Path), '--host', '127.0.0.1', '--port', '3080')
|
|
156
|
+
$process = Start-Process -FilePath $official.NodePath -ArgumentList $arguments -WindowStyle Hidden -PassThru `
|
|
157
|
+
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
158
|
+
} finally { $env:DSH_HOME = $previousHome }
|
|
159
|
+
Write-LaunchLog ('Started official Harness on 3080; PID={0}. The official CLI opens the browser. Logs: {1}, {2}' -f $process.Id, $stdout, $stderr)
|
|
160
|
+
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
161
|
+
$nextProgress = (Get-Date).AddSeconds(5)
|
|
162
|
+
do {
|
|
163
|
+
$port = Get-PortState -Port 3080
|
|
164
|
+
if ($port.State -eq 'occupied') {
|
|
165
|
+
if (-not (Test-OfficialHarnessListener -OwnerPid $port.Pid -Official $official)) { throw 'Port 3080 became occupied by an unverified process; it was left untouched.' }
|
|
166
|
+
if ((Test-OfficialWebReady) -and (Test-OfficialFrontendAttached -Revision $frontend.Revision)) { return }
|
|
167
|
+
}
|
|
168
|
+
if ($process.HasExited) { throw ('Official Harness exited before readiness. Diagnostic log: {0}' -f $stderr) }
|
|
169
|
+
if ((Get-Date) -ge $nextProgress) {
|
|
170
|
+
Write-LaunchLog 'Waiting for the official Harness frontend on 3080...'
|
|
171
|
+
$nextProgress = (Get-Date).AddSeconds(5)
|
|
172
|
+
}
|
|
173
|
+
Start-Sleep -Milliseconds 500
|
|
174
|
+
} while ((Get-Date) -lt $deadline)
|
|
175
|
+
throw ('Official Harness did not become ready within {0}s; inspect {1}. No official process was stopped.' -f $TimeoutSeconds, $stderr)
|
|
176
|
+
} finally {
|
|
177
|
+
if ($locked) { $mutex.ReleaseMutex() }
|
|
178
|
+
$mutex.Dispose()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
33
182
|
function Get-HealthState {
|
|
34
183
|
param([pscustomObject] $Service)
|
|
35
|
-
#
|
|
36
|
-
# is an external dependency: probe only its native web root, never the
|
|
37
|
-
# Crew bridge endpoint, so a missing legacy bridge cannot fail 3080 health.
|
|
184
|
+
# Preserve the installed extension health contract used by maintenance.
|
|
38
185
|
if ($Service.CrewOwned) {
|
|
39
186
|
try {
|
|
40
187
|
$response = Invoke-RestMethod -Uri ($Service.Url + '/_dsh/dsh-crew/extension') -TimeoutSec 2
|
|
@@ -71,28 +218,10 @@ function Get-HealthState {
|
|
|
71
218
|
return [pscustomobject]@{ Ready = $false; Version = $null; Error = $_.Exception.Message }
|
|
72
219
|
}
|
|
73
220
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# is NOT healthy.
|
|
79
|
-
$response = Invoke-WebRequest -UseBasicParsing -Uri $Service.Url -TimeoutSec 2
|
|
80
|
-
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) {
|
|
81
|
-
return [pscustomobject]@{ Ready = $true; Version = $null; Error = $null }
|
|
82
|
-
}
|
|
83
|
-
return [pscustomobject]@{ Ready = $false; Version = $null; Error = ('Official UI returned HTTP {0}.' -f $response.StatusCode) }
|
|
84
|
-
} catch {
|
|
85
|
-
# Invoke-WebRequest surfaces 401/403 as exceptions; those prove the web
|
|
86
|
-
# service is alive and answering (token gate present).
|
|
87
|
-
$status = $null
|
|
88
|
-
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
|
|
89
|
-
$status = [int] $_.Exception.Response.StatusCode
|
|
90
|
-
}
|
|
91
|
-
if ($status -eq 401 -or $status -eq 403) {
|
|
92
|
-
return [pscustomobject]@{ Ready = $true; Version = $null; Error = $null }
|
|
93
|
-
}
|
|
94
|
-
return [pscustomobject]@{ Ready = $false; Version = $null; Error = $_.Exception.Message }
|
|
95
|
-
}
|
|
221
|
+
# The launcher supervises only the Crew-owned 3210 service. Any non-Crew
|
|
222
|
+
# service is never started or health-gated here; the legacy 3080 bridge
|
|
223
|
+
# is probed only diagnostically after 3210 readiness (see Write-LegacyBridgeDiagnostic).
|
|
224
|
+
return [pscustomobject]@{ Ready = $false; Version = $null; Error = 'Unknown service: only the Crew-owned 3210 service is supervised.' }
|
|
96
225
|
}
|
|
97
226
|
|
|
98
227
|
# Optional legacy compatibility probe: returns true only when a 3080 that
|
|
@@ -127,8 +256,9 @@ function Write-Utf8NoBom {
|
|
|
127
256
|
}
|
|
128
257
|
|
|
129
258
|
function Write-SupervisorHeartbeat {
|
|
259
|
+
param([bool] $OwnershipReady = $false)
|
|
130
260
|
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
131
|
-
$record = @{ schema_version = 1; pid = $PID; last_seen = $now; protocol_version = 1 } | ConvertTo-Json -Compress
|
|
261
|
+
$record = @{ schema_version = 1; supervisor_instance_id = $supervisorInstanceId; pid = $PID; process_started_at_utc_ticks = $launcherProcessStartedAtUtcTicks; helper_hash = $launcherHelperHash; ownership_ready = $OwnershipReady; last_seen = $now; protocol_version = 1 } | ConvertTo-Json -Compress
|
|
132
262
|
try {
|
|
133
263
|
if (-not (Test-Path -LiteralPath $crewSupervisorRoot -PathType Container)) { New-Item -ItemType Directory -Path $crewSupervisorRoot -Force | Out-Null }
|
|
134
264
|
$temp = Join-Path $crewSupervisorRoot ("heartbeat.{0}.tmp" -f $PID)
|
|
@@ -137,6 +267,93 @@ function Write-SupervisorHeartbeat {
|
|
|
137
267
|
} catch { /* heartbeat is best-effort */ }
|
|
138
268
|
}
|
|
139
269
|
|
|
270
|
+
function Get-SupervisorLaunchArguments {
|
|
271
|
+
param([string] $ScriptPath = $PSCommandPath)
|
|
272
|
+
if ([string]::IsNullOrWhiteSpace($ScriptPath) -or $ScriptPath.Contains('"')) {
|
|
273
|
+
throw 'A valid launcher script path is required.'
|
|
274
|
+
}
|
|
275
|
+
return @(
|
|
276
|
+
'-NoLogo',
|
|
277
|
+
'-NoProfile',
|
|
278
|
+
'-NonInteractive',
|
|
279
|
+
'-ExecutionPolicy', 'Bypass',
|
|
280
|
+
'-File', ('"{0}"' -f $ScriptPath),
|
|
281
|
+
'-Mode', 'watch'
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function Get-SupervisorHeartbeatRecord {
|
|
286
|
+
param([int] $MaxAgeSeconds = 30)
|
|
287
|
+
try {
|
|
288
|
+
if (-not (Test-Path -LiteralPath $crewHeartbeatFile -PathType Leaf)) { return $null }
|
|
289
|
+
$record = Get-Content -LiteralPath $crewHeartbeatFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
290
|
+
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
291
|
+
$lastSeen = [long] $record.last_seen
|
|
292
|
+
$age = $now - $lastSeen
|
|
293
|
+
if ($record.schema_version -ne 1 -or $record.protocol_version -ne 1 -or [int] $record.pid -le 0) { return $null }
|
|
294
|
+
if ($age -lt -5000 -or $age -gt ([long] $MaxAgeSeconds * 1000)) { return $null }
|
|
295
|
+
$process = Get-Process -Id ([int] $record.pid) -ErrorAction Stop
|
|
296
|
+
$processStartProperty = $record.PSObject.Properties['process_started_at_utc_ticks']
|
|
297
|
+
if ($null -ne $processStartProperty -and $processStartProperty.Value) {
|
|
298
|
+
$expectedTicks = [long] $processStartProperty.Value
|
|
299
|
+
if ($process.StartTime.ToUniversalTime().Ticks -ne $expectedTicks) { return $null }
|
|
300
|
+
}
|
|
301
|
+
$ownershipProperty = $record.PSObject.Properties['ownership_ready']
|
|
302
|
+
$helperHashProperty = $record.PSObject.Properties['helper_hash']
|
|
303
|
+
$instanceProperty = $record.PSObject.Properties['supervisor_instance_id']
|
|
304
|
+
$identityReady = $null -ne $processStartProperty -and $processStartProperty.Value `
|
|
305
|
+
-and $null -ne $helperHashProperty -and $helperHashProperty.Value -match '^[a-f0-9]{64}$' `
|
|
306
|
+
-and $null -ne $instanceProperty -and -not [string]::IsNullOrWhiteSpace([string] $instanceProperty.Value)
|
|
307
|
+
$state = if ($null -eq $ownershipProperty) { 'legacy-v1' } elseif ($ownershipProperty.Value -eq $true -and $identityReady) { 'ready' } else { 'starting' }
|
|
308
|
+
return [pscustomobject]@{ State = $state; Record = $record }
|
|
309
|
+
} catch {
|
|
310
|
+
return $null
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function Get-FreshSupervisorHeartbeat {
|
|
315
|
+
param([int] $MaxAgeSeconds = 30)
|
|
316
|
+
$observed = Get-SupervisorHeartbeatRecord -MaxAgeSeconds $MaxAgeSeconds
|
|
317
|
+
if ($observed -and $observed.State -eq 'ready') { return $observed.Record }
|
|
318
|
+
return $null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function Ensure-CrewSupervisorRunning {
|
|
322
|
+
param([int] $TimeoutSeconds = 90)
|
|
323
|
+
$watcher = $null
|
|
324
|
+
$observed = Get-SupervisorHeartbeatRecord
|
|
325
|
+
if ($observed -and $observed.State -eq 'legacy-v1') {
|
|
326
|
+
throw 'CREW_SUPERVISOR_UPGRADE_REQUIRED: a legacy watcher is active and must be handed off before interactive launch.'
|
|
327
|
+
}
|
|
328
|
+
if (-not $observed) {
|
|
329
|
+
$arguments = @(Get-SupervisorLaunchArguments -ScriptPath $PSCommandPath)
|
|
330
|
+
$watcher = Start-Process -FilePath 'powershell.exe' -ArgumentList $arguments -WindowStyle Hidden -PassThru
|
|
331
|
+
Write-LaunchLog ('Started persistent Crew supervisor; PID={0}.' -f $watcher.Id)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
$crew = $services | Where-Object { $_.CrewOwned } | Select-Object -First 1
|
|
335
|
+
if (-not $crew) { throw 'Crew-owned 3210 service definition is missing.' }
|
|
336
|
+
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
337
|
+
$nextProgress = (Get-Date).AddSeconds(5)
|
|
338
|
+
$lastHealthError = 'not checked'
|
|
339
|
+
do {
|
|
340
|
+
$heartbeat = Get-FreshSupervisorHeartbeat
|
|
341
|
+
$health = Get-HealthState $crew
|
|
342
|
+
$lastHealthError = $health.Error
|
|
343
|
+
if ($heartbeat -and $health.Ready) {
|
|
344
|
+
Write-LaunchLog ('Persistent supervisor ready; PID={0}; 3210 runtime={1}.' -f $heartbeat.pid, $health.Version)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
if ((Get-Date) -ge $nextProgress) {
|
|
348
|
+
Write-LaunchLog ('Waiting for Crew on 3210; {0:n0}s remaining. Health: {1}' -f [Math]::Max(0, ($deadline - (Get-Date)).TotalSeconds), $lastHealthError)
|
|
349
|
+
$nextProgress = (Get-Date).AddSeconds(5)
|
|
350
|
+
}
|
|
351
|
+
Start-Sleep -Milliseconds 500
|
|
352
|
+
} while ((Get-Date) -lt $deadline)
|
|
353
|
+
|
|
354
|
+
throw ('Persistent supervisor did not make 3210 ready within {0}s. Last health error: {1}' -f $TimeoutSeconds, $lastHealthError)
|
|
355
|
+
}
|
|
356
|
+
|
|
140
357
|
function Read-RestartRequests {
|
|
141
358
|
if (-not (Test-Path -LiteralPath $crewRequestsDir -PathType Container)) { return @() }
|
|
142
359
|
$requests = @()
|
|
@@ -351,7 +568,14 @@ function Invoke-CrewMaintenanceRequests {
|
|
|
351
568
|
$liveRuntimeId = $null
|
|
352
569
|
try {
|
|
353
570
|
$resp = Invoke-RestMethod -Uri 'http://127.0.0.1:3210/_dsh/dsh-crew/extension' -TimeoutSec 3
|
|
354
|
-
|
|
571
|
+
$runtime = $resp.extension.runtime
|
|
572
|
+
$canonical = $resp.ok -eq $true `
|
|
573
|
+
-and $runtime.service -eq 'dsh-crew-hub' `
|
|
574
|
+
-and $runtime.execution_plane -eq 'hub-3210' `
|
|
575
|
+
-and $runtime.profile -eq 'dsh-crew' `
|
|
576
|
+
-and ($runtime.listen_port -as [int]) -eq 3210 `
|
|
577
|
+
-and ($runtime.protocol_version -as [int]) -eq 1
|
|
578
|
+
if ($canonical) { $liveRuntimeId = $runtime.runtime_id }
|
|
355
579
|
} catch { $liveRuntimeId = $null }
|
|
356
580
|
if (-not $liveRuntimeId -or $liveRuntimeId -ne $request.runtime_id) {
|
|
357
581
|
Write-MaintenanceResult $request 'SUPERVISOR_OWNERSHIP_CONFLICT'
|
|
@@ -417,7 +641,14 @@ function Invoke-CrewMaintenanceRequests {
|
|
|
417
641
|
$resp = Invoke-RestMethod -Uri 'http://127.0.0.1:3210/_dsh/dsh-crew/extension' -TimeoutSec 5
|
|
418
642
|
if ($resp.ok -eq $true) { $newRuntime = $resp.extension.runtime }
|
|
419
643
|
} catch { $newRuntime = $null }
|
|
420
|
-
$ok = $newRuntime -and $newRuntime.runtime_id
|
|
644
|
+
$ok = $newRuntime -and $newRuntime.runtime_id `
|
|
645
|
+
-and $newRuntime.service -eq 'dsh-crew-hub' `
|
|
646
|
+
-and $newRuntime.execution_plane -eq 'hub-3210' `
|
|
647
|
+
-and $newRuntime.profile -eq 'dsh-crew' `
|
|
648
|
+
-and ($newRuntime.listen_port -as [int]) -eq 3210 `
|
|
649
|
+
-and ($newRuntime.protocol_version -as [int]) -eq 1 `
|
|
650
|
+
-and ($expectedCrew -eq $null -or $newRuntime.runtime_version -eq $expectedCrew) `
|
|
651
|
+
-and ($expectedDsh -eq $null -or $newRuntime.dsh_version -eq $expectedDsh)
|
|
421
652
|
if ($ok) {
|
|
422
653
|
# Verifiable one-shot consumption: only a provably-cleared session
|
|
423
654
|
# completes the transaction; otherwise the lease stays live and any
|
|
@@ -518,6 +749,27 @@ function Set-TrackedListenerIdentity {
|
|
|
518
749
|
}
|
|
519
750
|
}
|
|
520
751
|
|
|
752
|
+
function Test-CrewServiceOwnership {
|
|
753
|
+
param(
|
|
754
|
+
[pscustomObject] $Service,
|
|
755
|
+
[pscustomObject] $PortState = $null,
|
|
756
|
+
[object[]] $ProcessTable = $null
|
|
757
|
+
)
|
|
758
|
+
if (-not $Service.ListenerPid -or -not $Service.ListenerStartedAtUtcTicks) { return $false }
|
|
759
|
+
$port = if ($null -ne $PortState) { $PortState } else { Get-PortState $Service.Port }
|
|
760
|
+
if ($port.State -ne 'occupied' -or -not $port.Pid -or [int] $port.Pid -ne [int] $Service.ListenerPid) { return $false }
|
|
761
|
+
try {
|
|
762
|
+
$tree = if ($null -ne $ProcessTable) {
|
|
763
|
+
@(Get-TrackedProcessTree -Service $Service -ProcessTable $ProcessTable)
|
|
764
|
+
} else {
|
|
765
|
+
@(Get-TrackedProcessTree -Service $Service)
|
|
766
|
+
}
|
|
767
|
+
return ([int] $port.Pid -in $tree)
|
|
768
|
+
} catch {
|
|
769
|
+
return $false
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
521
773
|
function Stop-OwnedListener {
|
|
522
774
|
param([pscustomobject] $Service, [int] $ListenerPid)
|
|
523
775
|
if ($Mode -ne 'watch' -or -not $Service.ListenerPid -or $ListenerPid -ne $Service.ListenerPid) {
|
|
@@ -542,8 +794,19 @@ function Stop-OwnedListener {
|
|
|
542
794
|
return $false
|
|
543
795
|
}
|
|
544
796
|
|
|
797
|
+
function Assert-HistoryStartAllowed {
|
|
798
|
+
param([string] $StatePath = (Join-Path $crewHome '..\history\active.json'))
|
|
799
|
+
if (-not (Test-Path -LiteralPath $StatePath)) { return }
|
|
800
|
+
try {
|
|
801
|
+
$state = Get-Content -LiteralPath $StatePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
802
|
+
if ($state.schemaVersion -eq 1 -and $state.phase -in @('STARTING', 'VERIFYING', 'DONE', 'FAILED', 'ROLLED_BACK')) { return }
|
|
803
|
+
} catch { }
|
|
804
|
+
throw 'HISTORY_RECOVERY_REQUIRED: Crew history maintenance is unfinished. Run dsh-crew history recover. Official 3080 is unchanged.'
|
|
805
|
+
}
|
|
806
|
+
|
|
545
807
|
function Start-CrewService {
|
|
546
808
|
param([pscustomobject] $Service)
|
|
809
|
+
Assert-HistoryStartAllowed
|
|
547
810
|
$serviceRunStamp = (Get-Date).ToString('yyyyMMdd-HHmmssfff')
|
|
548
811
|
$stdout = Join-Path $logRoot ('dsh-crew-{0}-{1}-{2}.out.log' -f $Service.Profile, $Service.Port, $serviceRunStamp)
|
|
549
812
|
$stderr = Join-Path $logRoot ('dsh-crew-{0}-{1}-{2}.err.log' -f $Service.Profile, $Service.Port, $serviceRunStamp)
|
|
@@ -573,9 +836,14 @@ function Wait-CrewServices {
|
|
|
573
836
|
$health = Get-HealthState $service
|
|
574
837
|
$service.LastError = $health.Error
|
|
575
838
|
if ($health.Ready) {
|
|
839
|
+
if ($service.CrewOwned -and -not $service.ListenerPid -and -not (Set-TrackedListenerIdentity -Service $service)) {
|
|
840
|
+
throw ('{0} is healthy on {1}, but this supervisor cannot prove process ownership.' -f $service.Name, $service.Port)
|
|
841
|
+
}
|
|
842
|
+
if ($service.CrewOwned -and -not (Test-CrewServiceOwnership -Service $service)) {
|
|
843
|
+
throw ('{0} is healthy on {1}, but its tracked listener identity is not owned.' -f $service.Name, $service.Port)
|
|
844
|
+
}
|
|
576
845
|
$service.State = 'ready'
|
|
577
846
|
$service.ConsecutiveFailures = 0
|
|
578
|
-
if ($service.CrewOwned) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
579
847
|
Write-LaunchLog ('{0} ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
580
848
|
} elseif ($service.Process -and $service.Process.HasExited) {
|
|
581
849
|
throw ('{0} exited before becoming ready; PID={1}; exit={2}; last health error: {3}' -f $service.Name, $service.Process.Id, $service.Process.ExitCode, $health.Error)
|
|
@@ -593,9 +861,7 @@ function Wait-CrewServices {
|
|
|
593
861
|
}
|
|
594
862
|
}
|
|
595
863
|
|
|
596
|
-
#
|
|
597
|
-
# logs legacy bridge presence for operator awareness. Never gates, delays,
|
|
598
|
-
# or claims 3210; never runs on the boot path.
|
|
864
|
+
# Explicit diagnostic helper; not called by normal startup or supervision.
|
|
599
865
|
function Write-LegacyBridgeDiagnostic {
|
|
600
866
|
if (Test-LegacyBridgeAvailable) {
|
|
601
867
|
Write-LaunchLog 'Diagnostic: legacy 3080 bridge still answers ping; Crew launcher owns 3210 directly, bridge ignored.'
|
|
@@ -609,8 +875,7 @@ function Ensure-CrewServices {
|
|
|
609
875
|
# Maintenance fence: while a STOPPED maintenance session is active for
|
|
610
876
|
# the Crew backend, ordinary supervision must NOT auto-start 3210 —
|
|
611
877
|
# the stopped window belongs to the npx lifecycle's tree swap. Only a
|
|
612
|
-
# matching maintenance-start owns the launch right.
|
|
613
|
-
# continues normally during the window.
|
|
878
|
+
# matching maintenance-start owns the launch right.
|
|
614
879
|
if ($service.CrewOwned -and (Test-MaintenanceSessionActive)) {
|
|
615
880
|
$service.State = 'maintenance'
|
|
616
881
|
$service.LastError = $null
|
|
@@ -619,10 +884,15 @@ function Ensure-CrewServices {
|
|
|
619
884
|
$health = Get-HealthState $service
|
|
620
885
|
if ($health.Ready) {
|
|
621
886
|
$wasReady = $service.State -eq 'ready'
|
|
887
|
+
if ($service.CrewOwned -and -not $service.ListenerPid -and -not (Set-TrackedListenerIdentity -Service $service)) {
|
|
888
|
+
throw ('{0} is healthy on {1}, but this supervisor cannot prove process ownership.' -f $service.Name, $service.Port)
|
|
889
|
+
}
|
|
890
|
+
if ($service.CrewOwned -and -not (Test-CrewServiceOwnership -Service $service)) {
|
|
891
|
+
throw ('{0} is healthy on {1}, but its tracked listener identity is not owned.' -f $service.Name, $service.Port)
|
|
892
|
+
}
|
|
622
893
|
$service.State = 'ready'
|
|
623
894
|
$service.ConsecutiveFailures = 0
|
|
624
895
|
$service.LastError = $null
|
|
625
|
-
if ($service.CrewOwned -and -not $service.ListenerPid) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
626
896
|
if (-not $QuietHealthy -or -not $wasReady) {
|
|
627
897
|
Write-LaunchLog ('{0} already ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
628
898
|
}
|
|
@@ -686,7 +956,11 @@ function Start-ServiceSupervisor {
|
|
|
686
956
|
return
|
|
687
957
|
}
|
|
688
958
|
|
|
689
|
-
|
|
959
|
+
# Publish process identity before any 3210 startup/health wait. Handoff
|
|
960
|
+
# callers can now distinguish a legitimate cold-starting target watcher
|
|
961
|
+
# from the stale heartbeat left by the watcher it replaced.
|
|
962
|
+
Write-SupervisorHeartbeat -OwnershipReady $false
|
|
963
|
+
Write-LaunchLog 'Supervisor active; monitoring Crew-owned 3210 every 10 seconds.'
|
|
690
964
|
$lastRecoveryError = $null
|
|
691
965
|
$updateLockFile = Join-Path $crewHome '..\app\update-in-progress.lock'
|
|
692
966
|
$staleLockNotified = $false
|
|
@@ -718,25 +992,30 @@ function Start-ServiceSupervisor {
|
|
|
718
992
|
} else {
|
|
719
993
|
$staleLockNotified = $false
|
|
720
994
|
}
|
|
995
|
+
# Maintenance stop/start is the updater's owned process handoff and
|
|
996
|
+
# must remain live while the update lock fences ordinary recovery.
|
|
997
|
+
Invoke-CrewMaintenanceRequests
|
|
721
998
|
if ($updateHeld) {
|
|
722
|
-
Write-LaunchLog 'Update in progress;
|
|
999
|
+
Write-LaunchLog 'Update in progress; maintenance only, ordinary recovery suspended.'
|
|
723
1000
|
$lastRecoveryError = $null
|
|
724
1001
|
} else {
|
|
725
1002
|
# Control protocol: publish this watcher's heartbeat and consume
|
|
726
1003
|
# any durable restart requests the hub wrote (3210 never spawns
|
|
727
|
-
# itself).
|
|
728
|
-
#
|
|
729
|
-
Write-SupervisorHeartbeat
|
|
1004
|
+
# itself). Maintenance was consumed above; now run the ordinary
|
|
1005
|
+
# health pass.
|
|
730
1006
|
Invoke-CrewRestartRequests
|
|
731
|
-
Invoke-CrewMaintenanceRequests
|
|
732
1007
|
Ensure-CrewServices -QuietHealthy
|
|
733
1008
|
if ($lastRecoveryError) {
|
|
734
|
-
Write-LaunchLog 'Supervisor recovery succeeded;
|
|
1009
|
+
Write-LaunchLog 'Supervisor recovery succeeded; Crew-owned 3210 is healthy.'
|
|
735
1010
|
$lastRecoveryError = $null
|
|
736
1011
|
}
|
|
737
1012
|
}
|
|
1013
|
+
$ownedServices = @($services | Where-Object { $_.CrewOwned -and (Test-CrewServiceOwnership -Service $_) })
|
|
1014
|
+
$expectedServices = @($services | Where-Object { $_.CrewOwned })
|
|
1015
|
+
Write-SupervisorHeartbeat -OwnershipReady ($expectedServices.Count -gt 0 -and $ownedServices.Count -eq $expectedServices.Count)
|
|
738
1016
|
} catch {
|
|
739
1017
|
$recoveryError = $_.Exception.Message
|
|
1018
|
+
Write-SupervisorHeartbeat -OwnershipReady $false
|
|
740
1019
|
if ($recoveryError -ne $lastRecoveryError) {
|
|
741
1020
|
Write-LaunchLog ('Supervisor recovery failed; will retry: {0}' -f $recoveryError) 'WARN'
|
|
742
1021
|
$lastRecoveryError = $recoveryError
|
|
@@ -755,6 +1034,7 @@ if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { return }
|
|
|
755
1034
|
try {
|
|
756
1035
|
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
|
|
757
1036
|
Write-LaunchLog ('Launcher started; mode={0}; user={1}' -f $Mode, $env:USERNAME)
|
|
1037
|
+
if ($Mode -eq 'open') { Open-OfficialFrontend }
|
|
758
1038
|
|
|
759
1039
|
if (-not (Test-Path -LiteralPath $dshCli -PathType Leaf)) {
|
|
760
1040
|
throw "DSH CLI was not found at $dshCli. Run: npm install -g @ran-sh/dsh-crew@latest; dsh-crew update"
|
|
@@ -762,9 +1042,7 @@ try {
|
|
|
762
1042
|
if (-not (Test-Path -LiteralPath (Join-Path $crewHome 'profiles\dsh-crew\package.json') -PathType Leaf)) {
|
|
763
1043
|
throw "The isolated dsh-crew profile is missing under $crewHome. Run: dsh-crew update"
|
|
764
1044
|
}
|
|
765
|
-
|
|
766
|
-
throw "The official web profile is missing under $officialHome. Repair or install the official DeepSeek Harness web profile outside dsh-crew; dsh-crew never mutates the official profile."
|
|
767
|
-
}
|
|
1045
|
+
# Background/watch modes remain independent of the official frontend.
|
|
768
1046
|
|
|
769
1047
|
if ($Mode -eq 'watch') {
|
|
770
1048
|
Start-ServiceSupervisor
|
|
@@ -772,12 +1050,10 @@ try {
|
|
|
772
1050
|
exit 0
|
|
773
1051
|
}
|
|
774
1052
|
|
|
775
|
-
Ensure-
|
|
776
|
-
Write-LegacyBridgeDiagnostic
|
|
1053
|
+
Ensure-CrewSupervisorRunning
|
|
777
1054
|
|
|
778
1055
|
if ($Mode -eq 'open') {
|
|
779
|
-
|
|
780
|
-
Write-LaunchLog 'Opened daily console at http://127.0.0.1:3080/'
|
|
1056
|
+
Write-LaunchLog 'Official frontend is on 3080; Crew is running silently on 3210.'
|
|
781
1057
|
}
|
|
782
1058
|
Write-LaunchLog ('Launcher completed successfully in {0:n1}s.' -f ((Get-Date) - $startedAt).TotalSeconds)
|
|
783
1059
|
exit 0
|