@foggy-projects/deepseek-harness-plugin 0.4.0-beta.10 → 0.4.0-beta.11
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/README.md +1 -1
- package/docs/PUBLIC-BETA-READINESS.md +7 -4
- package/docs/WINDOWS-BETA-ACCEPTANCE.md +8 -2
- package/experience/linux/prepare.sh +1 -1
- package/lib/client.js +32 -2
- package/lib/index.js +24 -12
- package/package.json +1 -1
- package/skills/foggy-deepseek-onboarding/assets/versions.json +1 -1
- package/skills/foggy-deepseek-onboarding/scripts/onboarding.py +208 -13
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ modify `PATH`, or register Python globally. Advanced users may explicitly set
|
|
|
14
14
|
## Local beta installation
|
|
15
15
|
|
|
16
16
|
```powershell
|
|
17
|
-
dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.
|
|
17
|
+
dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.11.tgz
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Restart `dsh web`, use the browser it opens (or the complete printed URL,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Public beta readiness: 0.4.0-beta.
|
|
1
|
+
# Public beta readiness: 0.4.0-beta.11
|
|
2
2
|
|
|
3
3
|
Assessment date: 2026-09-04
|
|
4
4
|
|
|
5
5
|
## Verdict
|
|
6
6
|
|
|
7
|
-
`0.4.0-beta.
|
|
7
|
+
`0.4.0-beta.11` is suitable for a scoped public beta on Windows 10/11 x64 and
|
|
8
8
|
Linux/WSL2 x64. It is not a general-availability release.
|
|
9
9
|
|
|
10
10
|
The plugin package stays small and downloads large components only during
|
|
@@ -21,13 +21,16 @@ system PATH, registry, or a user's existing Python installation.
|
|
|
21
21
|
- The component installer code is unchanged from beta.9, whose Windows x64 run
|
|
22
22
|
completed a cold Python download, checksum verification, extraction,
|
|
23
23
|
initialization, forced Python repair, and doctor run.
|
|
24
|
-
- DeepSeek Harness `0.1.2-rc.1` loaded the beta.
|
|
24
|
+
- DeepSeek Harness `0.1.2-rc.1` loaded the beta.11 candidate Bundle, web client,
|
|
25
25
|
remote status service, and native Skill provider on both Windows and WSL2.
|
|
26
26
|
- WSL2 reported the existing managed Python, CLI, Launcher, and analysis Skill
|
|
27
27
|
as ready; an isolated Windows profile correctly reported its fresh component
|
|
28
28
|
state and rejected the host's Java 12 as below the Java 17 prerequisite.
|
|
29
29
|
- The UI reported Python 3.12.13 as `Foggy private`, and CLI 0.1.23, Launcher
|
|
30
|
-
0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.
|
|
30
|
+
0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.11 as installed.
|
|
31
|
+
- Runtime startup now reports the current phase, elapsed/timeout seconds, and a
|
|
32
|
+
bounded progress value. A real WSL2 launch reached readiness in 13.4 seconds;
|
|
33
|
+
a forced timeout recorded failure evidence and cleaned up the Java process.
|
|
31
34
|
- Interrupted downloads resume with HTTP Range when the server supports it;
|
|
32
35
|
downloaded artifacts are not trusted until the complete SHA256 matches.
|
|
33
36
|
- Windows progress writes tolerate transient sharing violations without losing
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Native Windows Beta acceptance
|
|
2
2
|
|
|
3
3
|
This runbook validates DeepSeek Harness `0.1.2-rc.1` with Foggy plugin
|
|
4
|
-
`0.4.0-beta.
|
|
4
|
+
`0.4.0-beta.11` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
|
|
5
5
|
directory that is not synchronized by OneDrive.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
@@ -49,10 +49,16 @@ Expected component state:
|
|
|
49
49
|
- CLI 0.1.23;
|
|
50
50
|
- Launcher 0.1.18;
|
|
51
51
|
- analysis Skill 0.1.17;
|
|
52
|
-
- onboarding Skill 0.4.0-beta.
|
|
52
|
+
- onboarding Skill 0.4.0-beta.11;
|
|
53
53
|
- Java 17+ available;
|
|
54
54
|
- native DSH Skill registration available.
|
|
55
55
|
|
|
56
|
+
Runtime startup shows its current phase, elapsed time, and the 180-second
|
|
57
|
+
readiness deadline. Typical startup is 15–30 seconds on the validated WSL2 host
|
|
58
|
+
and may take up to 60 seconds on Windows with cold JVM or antivirus scanning.
|
|
59
|
+
On timeout, export diagnostics before retrying; the report includes the startup
|
|
60
|
+
phase, PID state, evidence directory, and Runtime log locations.
|
|
61
|
+
|
|
56
62
|
## Database and semantic-layer acceptance
|
|
57
63
|
|
|
58
64
|
Keep database credentials out of this public repository. Create a private CLI
|
package/lib/client.js
CHANGED
|
@@ -73,7 +73,16 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
progressSkills: '注册 DSH Skills',
|
|
74
74
|
progressState: '写入安装状态',
|
|
75
75
|
progressComplete: '初始化完成',
|
|
76
|
+
progressRuntimePreflight: '检查 Runtime 状态',
|
|
77
|
+
progressRuntimeLauncher: '启动 Java Launcher',
|
|
78
|
+
progressRuntimeReadiness: '等待 Runtime 就绪',
|
|
79
|
+
progressRuntimeCapabilities: '验证 Runtime 能力',
|
|
80
|
+
progressRuntimeState: '保存 Runtime 状态',
|
|
81
|
+
progressRuntimeComplete: 'Runtime 已就绪',
|
|
76
82
|
progressFiles: '文件',
|
|
83
|
+
progressElapsed: '已等待',
|
|
84
|
+
progressTimeout: '最长',
|
|
85
|
+
progressSeconds: '秒',
|
|
77
86
|
managedPrivate: 'Foggy 私有',
|
|
78
87
|
configuredOverride: '外部指定',
|
|
79
88
|
accepted: '操作已开始;页面会自动刷新状态。',
|
|
@@ -148,7 +157,16 @@ window.__ModuleLoader__.load({
|
|
|
148
157
|
progressSkills: 'Registering DSH Skills',
|
|
149
158
|
progressState: 'Writing install state',
|
|
150
159
|
progressComplete: 'Initialization complete',
|
|
160
|
+
progressRuntimePreflight: 'Checking Runtime state',
|
|
161
|
+
progressRuntimeLauncher: 'Starting Java Launcher',
|
|
162
|
+
progressRuntimeReadiness: 'Waiting for Runtime readiness',
|
|
163
|
+
progressRuntimeCapabilities: 'Verifying Runtime capabilities',
|
|
164
|
+
progressRuntimeState: 'Saving Runtime state',
|
|
165
|
+
progressRuntimeComplete: 'Runtime ready',
|
|
151
166
|
progressFiles: 'files',
|
|
167
|
+
progressElapsed: 'elapsed',
|
|
168
|
+
progressTimeout: 'timeout',
|
|
169
|
+
progressSeconds: 's',
|
|
152
170
|
managedPrivate: 'Foggy private',
|
|
153
171
|
configuredOverride: 'Configured override',
|
|
154
172
|
accepted: 'The operation started; this page will refresh automatically.',
|
|
@@ -247,21 +265,33 @@ window.__ModuleLoader__.load({
|
|
|
247
265
|
'workspace-skills': 'progressSkills',
|
|
248
266
|
state: 'progressState',
|
|
249
267
|
complete: 'progressComplete',
|
|
268
|
+
'runtime-preflight': 'progressRuntimePreflight',
|
|
269
|
+
'runtime-launcher': 'progressRuntimeLauncher',
|
|
270
|
+
'runtime-readiness': 'progressRuntimeReadiness',
|
|
271
|
+
'runtime-capabilities': 'progressRuntimeCapabilities',
|
|
272
|
+
'runtime-state': 'progressRuntimeState',
|
|
273
|
+
'runtime-complete': 'progressRuntimeComplete',
|
|
250
274
|
}
|
|
251
275
|
const phase = t(phaseKeys[progress.phase] || 'progressWorking')
|
|
276
|
+
const headline = String(progress.phase || '').startsWith('runtime-') ? phase : progress.message || phase
|
|
252
277
|
const step = progress.step?.index && progress.step?.total
|
|
253
278
|
? `${progress.step.index}/${progress.step.total}`
|
|
254
279
|
: ''
|
|
255
280
|
const files = progress.files?.total
|
|
256
281
|
? `${progress.files.completed || 0}/${progress.files.total} ${t('progressFiles')}`
|
|
257
282
|
: ''
|
|
283
|
+
const elapsed = Number(progress.timing?.elapsedSeconds)
|
|
284
|
+
const timeout = Number(progress.timing?.timeoutSeconds)
|
|
285
|
+
const timing = Number.isFinite(elapsed)
|
|
286
|
+
? `${t('progressElapsed')} ${elapsed}${t('progressSeconds')}${Number.isFinite(timeout) ? ` / ${t('progressTimeout')} ${timeout}${t('progressSeconds')}` : ''}`
|
|
287
|
+
: ''
|
|
258
288
|
return jsxs('div', {
|
|
259
289
|
className: 'foggy-progress',
|
|
260
290
|
role: 'status',
|
|
261
291
|
'aria-live': 'polite',
|
|
262
292
|
children: [
|
|
263
293
|
jsxs('div', { className: 'foggy-progress-head', children: [
|
|
264
|
-
jsx('strong', { children:
|
|
294
|
+
jsx('strong', { children: headline }),
|
|
265
295
|
jsx('span', { className: 'foggy-progress-percent', children: `${percent}%` }),
|
|
266
296
|
] }),
|
|
267
297
|
jsx('div', {
|
|
@@ -275,7 +305,7 @@ window.__ModuleLoader__.load({
|
|
|
275
305
|
}),
|
|
276
306
|
jsxs('div', { className: 'foggy-progress-meta', children: [
|
|
277
307
|
jsx('span', { children: phase }),
|
|
278
|
-
jsx('span', { children: [step, files].filter(Boolean).join(' · ') }),
|
|
308
|
+
jsx('span', { children: [step, files, timing].filter(Boolean).join(' · ') }),
|
|
279
309
|
] }),
|
|
280
310
|
progress.currentFile ? jsx('div', { className: 'foggy-progress-meta foggy-progress-file', title: progress.currentFile, children: progress.currentFile }) : null,
|
|
281
311
|
],
|
package/lib/index.js
CHANGED
|
@@ -131,6 +131,8 @@ async function assertSystemPrerequisites(kind) {
|
|
|
131
131
|
|
|
132
132
|
function createPythonProgressReporter(operation, path) {
|
|
133
133
|
let pending = Promise.resolve()
|
|
134
|
+
const runtimeStart = operation.kind === 'runtime-start'
|
|
135
|
+
const totalSteps = runtimeStart ? 6 : 7
|
|
134
136
|
return {
|
|
135
137
|
update(detail) {
|
|
136
138
|
const fraction = Math.max(0, Math.min(1, Number(detail.fraction) || 0))
|
|
@@ -139,11 +141,11 @@ function createPythonProgressReporter(operation, path) {
|
|
|
139
141
|
operationId: operation.id,
|
|
140
142
|
kind: operation.kind,
|
|
141
143
|
state: 'running',
|
|
142
|
-
phase: 'python',
|
|
143
|
-
message: detail.message || 'Preparing managed Python',
|
|
144
|
+
phase: runtimeStart ? 'runtime-preflight' : 'python',
|
|
145
|
+
message: runtimeStart ? 'Preparing the Foggy Runtime controller' : detail.message || 'Preparing managed Python',
|
|
144
146
|
currentFile: detail.currentFile || null,
|
|
145
|
-
percent: Math.round(((1 + fraction) /
|
|
146
|
-
step: { index: 2, total:
|
|
147
|
+
percent: runtimeStart ? Math.round(fraction * 3) : Math.round(((1 + fraction) / totalSteps) * 100),
|
|
148
|
+
step: { index: runtimeStart ? 1 : 2, total: totalSteps },
|
|
147
149
|
startedAt: operation.startedAt,
|
|
148
150
|
updatedAt: new Date().toISOString(),
|
|
149
151
|
}
|
|
@@ -348,6 +350,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
348
350
|
components: status.components,
|
|
349
351
|
onboarding: status.onboarding,
|
|
350
352
|
profileMigration: status.profileMigration,
|
|
353
|
+
lastRuntimeStartFailure: await readOptionalJson(join(roots.dataRoot, 'last-runtime-start-failure.json')),
|
|
351
354
|
doctor,
|
|
352
355
|
}
|
|
353
356
|
const directory = join(roots.dataRoot, 'diagnostics')
|
|
@@ -358,11 +361,17 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
async runtimeStart() {
|
|
361
|
-
|
|
364
|
+
const roots = defaultRoots()
|
|
365
|
+
return this.startOperation('runtime-start', false, [
|
|
366
|
+
'runtime-start', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
|
|
367
|
+
])
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
async runtimeStop() {
|
|
365
|
-
|
|
371
|
+
const roots = defaultRoots()
|
|
372
|
+
return this.startOperation('runtime-stop', false, [
|
|
373
|
+
'runtime-stop', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
|
|
374
|
+
])
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
startOperation(kind, replaceSkill, explicitArgs) {
|
|
@@ -371,7 +380,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
371
380
|
}
|
|
372
381
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
|
|
373
382
|
const args = explicitArgs ?? ['install']
|
|
374
|
-
const reportsProgress = args[0] === 'install'
|
|
383
|
+
const reportsProgress = args[0] === 'install' || args[0] === 'runtime-start'
|
|
375
384
|
if (reportsProgress) {
|
|
376
385
|
const roots = defaultRoots()
|
|
377
386
|
args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
|
|
@@ -380,6 +389,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
380
389
|
for (const cacheDir of (process.env.FOGGY_ASSET_CACHE_DIRS || '').split(delimiter).filter(Boolean)) {
|
|
381
390
|
args.push('--asset-cache-dir', cacheDir)
|
|
382
391
|
}
|
|
392
|
+
const runtimeStart = kind === 'runtime-start'
|
|
383
393
|
const operation = {
|
|
384
394
|
id,
|
|
385
395
|
kind,
|
|
@@ -394,10 +404,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
394
404
|
kind,
|
|
395
405
|
state: 'running',
|
|
396
406
|
phase: 'preflight',
|
|
397
|
-
message: 'Starting Foggy initialization',
|
|
407
|
+
message: runtimeStart ? 'Starting Foggy Runtime' : 'Starting Foggy initialization',
|
|
398
408
|
currentFile: null,
|
|
399
409
|
percent: 0,
|
|
400
|
-
step: { index: 1, total: 7 },
|
|
410
|
+
step: { index: 1, total: runtimeStart ? 6 : 7 },
|
|
401
411
|
} : null,
|
|
402
412
|
promise: null,
|
|
403
413
|
}
|
|
@@ -422,8 +432,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
422
432
|
operation.progress = {
|
|
423
433
|
...operation.progress,
|
|
424
434
|
state: operation.state,
|
|
425
|
-
phase: operation.state === 'succeeded' ? 'complete' : operation.progress.phase,
|
|
426
|
-
message: operation.state === 'succeeded'
|
|
435
|
+
phase: operation.state === 'succeeded' ? (runtimeStart ? 'runtime-complete' : 'complete') : operation.progress.phase,
|
|
436
|
+
message: operation.state === 'succeeded'
|
|
437
|
+
? (runtimeStart ? 'Foggy Runtime is ready' : 'Foggy initialization completed')
|
|
438
|
+
: (runtimeStart ? 'Foggy Runtime startup failed' : 'Foggy initialization failed'),
|
|
427
439
|
percent: operation.state === 'succeeded' ? 100 : operation.progress.percent,
|
|
428
440
|
}
|
|
429
441
|
}
|
|
@@ -437,7 +449,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
437
449
|
operation.progress = {
|
|
438
450
|
...operation.progress,
|
|
439
451
|
state: 'failed',
|
|
440
|
-
message: 'Foggy initialization failed',
|
|
452
|
+
message: runtimeStart ? 'Foggy Runtime startup failed' : 'Foggy initialization failed',
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
455
|
})
|
package/package.json
CHANGED
|
@@ -48,20 +48,35 @@ ACTIVE_PROGRESS: "ProgressReporter | None" = None
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class ProgressReporter:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
path: str | None,
|
|
54
|
+
operation_id: str | None,
|
|
55
|
+
kind: str,
|
|
56
|
+
*,
|
|
57
|
+
total_steps: int = 7,
|
|
58
|
+
completion_phase: str = "complete",
|
|
59
|
+
completion_message: str = "Foggy initialization completed",
|
|
60
|
+
failure_message: str = "Foggy initialization failed",
|
|
61
|
+
) -> None:
|
|
54
62
|
self.path = Path(path).expanduser() if path else None
|
|
55
63
|
self.operation_id = operation_id
|
|
56
64
|
self.kind = kind
|
|
65
|
+
self.total_steps = total_steps
|
|
66
|
+
self.completion_phase = completion_phase
|
|
67
|
+
self.completion_message = completion_message
|
|
68
|
+
self.failure_message = failure_message
|
|
57
69
|
self.started_at = now_utc()
|
|
58
70
|
self.phase = "preflight"
|
|
59
71
|
self.step_index = 0
|
|
60
72
|
self.fraction = 0.0
|
|
73
|
+
self.percent_override: int | None = None
|
|
61
74
|
self.message = ""
|
|
62
75
|
self.current_file: str | None = None
|
|
63
76
|
self.completed_files: int | None = None
|
|
64
77
|
self.total_files: int | None = None
|
|
78
|
+
self.elapsed_seconds: int | None = None
|
|
79
|
+
self.timeout_seconds: int | None = None
|
|
65
80
|
|
|
66
81
|
@property
|
|
67
82
|
def enabled(self) -> bool:
|
|
@@ -72,7 +87,11 @@ class ProgressReporter:
|
|
|
72
87
|
return
|
|
73
88
|
bounded_step = max(0, min(self.step_index, self.total_steps))
|
|
74
89
|
bounded_fraction = max(0.0, min(float(self.fraction), 1.0))
|
|
75
|
-
percent = 100 if state == "succeeded" else
|
|
90
|
+
percent = 100 if state == "succeeded" else (
|
|
91
|
+
max(0, min(self.percent_override, 99))
|
|
92
|
+
if self.percent_override is not None
|
|
93
|
+
else round(((bounded_step + bounded_fraction) / self.total_steps) * 100)
|
|
94
|
+
)
|
|
76
95
|
payload = {
|
|
77
96
|
"schemaVersion": PROGRESS_SCHEMA,
|
|
78
97
|
"operationId": self.operation_id,
|
|
@@ -88,6 +107,10 @@ class ProgressReporter:
|
|
|
88
107
|
}
|
|
89
108
|
if self.completed_files is not None and self.total_files is not None:
|
|
90
109
|
payload["files"] = {"completed": self.completed_files, "total": self.total_files}
|
|
110
|
+
if self.elapsed_seconds is not None:
|
|
111
|
+
payload["timing"] = {"elapsedSeconds": self.elapsed_seconds}
|
|
112
|
+
if self.timeout_seconds is not None:
|
|
113
|
+
payload["timing"]["timeoutSeconds"] = self.timeout_seconds
|
|
91
114
|
if finished_at:
|
|
92
115
|
payload["finishedAt"] = finished_at
|
|
93
116
|
if error:
|
|
@@ -105,28 +128,37 @@ class ProgressReporter:
|
|
|
105
128
|
current_file: str | None = None,
|
|
106
129
|
completed_files: int | None = None,
|
|
107
130
|
total_files: int | None = None,
|
|
131
|
+
percent: int | None = None,
|
|
132
|
+
elapsed_seconds: int | None = None,
|
|
133
|
+
timeout_seconds: int | None = None,
|
|
108
134
|
) -> None:
|
|
109
135
|
self.phase = phase
|
|
110
136
|
self.step_index = step_index
|
|
111
137
|
self.fraction = fraction
|
|
138
|
+
self.percent_override = percent
|
|
112
139
|
self.message = message
|
|
113
140
|
self.current_file = current_file
|
|
114
141
|
self.completed_files = completed_files
|
|
115
142
|
self.total_files = total_files
|
|
143
|
+
self.elapsed_seconds = elapsed_seconds
|
|
144
|
+
self.timeout_seconds = timeout_seconds
|
|
116
145
|
self._write("running")
|
|
117
146
|
|
|
118
147
|
def finish(self) -> None:
|
|
119
|
-
self.phase =
|
|
148
|
+
self.phase = self.completion_phase
|
|
120
149
|
self.step_index = self.total_steps
|
|
121
150
|
self.fraction = 0.0
|
|
122
|
-
self.
|
|
151
|
+
self.percent_override = 100
|
|
152
|
+
self.message = self.completion_message
|
|
123
153
|
self.current_file = None
|
|
124
154
|
self.completed_files = None
|
|
125
155
|
self.total_files = None
|
|
156
|
+
self.elapsed_seconds = None
|
|
157
|
+
self.timeout_seconds = None
|
|
126
158
|
self._write("succeeded", finished_at=now_utc())
|
|
127
159
|
|
|
128
160
|
def fail(self, error: object) -> None:
|
|
129
|
-
self.message =
|
|
161
|
+
self.message = self.failure_message
|
|
130
162
|
self._write("failed", finished_at=now_utc(), error=str(error))
|
|
131
163
|
|
|
132
164
|
|
|
@@ -245,6 +277,79 @@ def command_result(command: list[str], timeout: int = 30, check: bool = False) -
|
|
|
245
277
|
return payload
|
|
246
278
|
|
|
247
279
|
|
|
280
|
+
def command_result_with_progress(
|
|
281
|
+
command: list[str],
|
|
282
|
+
timeout: int,
|
|
283
|
+
progress: ProgressReporter,
|
|
284
|
+
*,
|
|
285
|
+
phase: str,
|
|
286
|
+
step_index: int,
|
|
287
|
+
message: str,
|
|
288
|
+
readiness_timeout: int,
|
|
289
|
+
start_percent: int,
|
|
290
|
+
end_percent: int,
|
|
291
|
+
poll_interval: float = 1.0,
|
|
292
|
+
) -> dict:
|
|
293
|
+
started = time.monotonic()
|
|
294
|
+
try:
|
|
295
|
+
process = subprocess.Popen(
|
|
296
|
+
command,
|
|
297
|
+
stdout=subprocess.PIPE,
|
|
298
|
+
stderr=subprocess.PIPE,
|
|
299
|
+
text=True,
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
errors="replace",
|
|
302
|
+
)
|
|
303
|
+
except FileNotFoundError:
|
|
304
|
+
return {
|
|
305
|
+
"command": command[0],
|
|
306
|
+
"available": False,
|
|
307
|
+
"exitCode": None,
|
|
308
|
+
"stdout": "",
|
|
309
|
+
"stderr": "command not found",
|
|
310
|
+
"durationMs": round((time.monotonic() - started) * 1000),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
stdout = ""
|
|
314
|
+
stderr = ""
|
|
315
|
+
while True:
|
|
316
|
+
try:
|
|
317
|
+
stdout, stderr = process.communicate(timeout=poll_interval)
|
|
318
|
+
break
|
|
319
|
+
except subprocess.TimeoutExpired:
|
|
320
|
+
elapsed = time.monotonic() - started
|
|
321
|
+
readiness_fraction = min(elapsed / max(readiness_timeout, 1), 1.0)
|
|
322
|
+
percent = round(start_percent + (end_percent - start_percent) * readiness_fraction)
|
|
323
|
+
progress.update(
|
|
324
|
+
phase,
|
|
325
|
+
step_index,
|
|
326
|
+
message,
|
|
327
|
+
percent=percent,
|
|
328
|
+
elapsed_seconds=max(1, round(elapsed)),
|
|
329
|
+
timeout_seconds=readiness_timeout,
|
|
330
|
+
)
|
|
331
|
+
if elapsed >= timeout:
|
|
332
|
+
process.kill()
|
|
333
|
+
stdout, stderr = process.communicate()
|
|
334
|
+
return {
|
|
335
|
+
"command": command[0],
|
|
336
|
+
"available": True,
|
|
337
|
+
"exitCode": None,
|
|
338
|
+
"stdout": stdout.strip(),
|
|
339
|
+
"stderr": f"timed out after {timeout} seconds",
|
|
340
|
+
"durationMs": round((time.monotonic() - started) * 1000),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
"command": command[0],
|
|
345
|
+
"available": True,
|
|
346
|
+
"exitCode": process.returncode,
|
|
347
|
+
"stdout": stdout.strip(),
|
|
348
|
+
"stderr": stderr.strip(),
|
|
349
|
+
"durationMs": round((time.monotonic() - started) * 1000),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
248
353
|
def sha256(path: Path) -> str:
|
|
249
354
|
digest = hashlib.sha256()
|
|
250
355
|
with path.open("rb") as stream:
|
|
@@ -1276,8 +1381,57 @@ def parse_json_output(result: dict, label: str) -> dict:
|
|
|
1276
1381
|
raise OnboardingError(f"{label} did not return JSON") from exc
|
|
1277
1382
|
|
|
1278
1383
|
|
|
1384
|
+
def write_runtime_start_failure(
|
|
1385
|
+
data_root: Path,
|
|
1386
|
+
evidence_dir: Path,
|
|
1387
|
+
launch: dict,
|
|
1388
|
+
phase: str,
|
|
1389
|
+
error: object,
|
|
1390
|
+
elapsed_ms: int,
|
|
1391
|
+
) -> dict:
|
|
1392
|
+
logs = []
|
|
1393
|
+
for name in ("stdoutLog", "stderrLog"):
|
|
1394
|
+
value = launch.get(name)
|
|
1395
|
+
path = normalized(value) if isinstance(value, str) and value else None
|
|
1396
|
+
item = {"name": name, "path": str(path) if path else None, "exists": bool(path and path.is_file())}
|
|
1397
|
+
if path and path.is_file():
|
|
1398
|
+
stat = path.stat()
|
|
1399
|
+
item.update({"sizeBytes": stat.st_size, "modifiedAt": dt.datetime.fromtimestamp(stat.st_mtime, dt.timezone.utc).isoformat()})
|
|
1400
|
+
logs.append(item)
|
|
1401
|
+
pid = int(launch.get("pid", 0))
|
|
1402
|
+
failure = {
|
|
1403
|
+
"schemaVersion": "foggy-deepseek-runtime-start-failure/v1",
|
|
1404
|
+
"failedAt": now_utc(),
|
|
1405
|
+
"phase": phase,
|
|
1406
|
+
"message": str(error),
|
|
1407
|
+
"elapsedMs": elapsed_ms,
|
|
1408
|
+
"pid": pid or None,
|
|
1409
|
+
"processRunningBeforeCleanup": process_info(pid)["running"] if pid else False,
|
|
1410
|
+
"runtimeUrl": launch.get("runtimeUrl"),
|
|
1411
|
+
"workDir": launch.get("workDir"),
|
|
1412
|
+
"evidenceDir": str(evidence_dir),
|
|
1413
|
+
"logs": logs,
|
|
1414
|
+
}
|
|
1415
|
+
atomic_json(evidence_dir / "failure.json", failure)
|
|
1416
|
+
atomic_json(data_root / "last-runtime-start-failure.json", failure)
|
|
1417
|
+
return failure
|
|
1418
|
+
|
|
1419
|
+
|
|
1279
1420
|
def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
1421
|
+
global ACTIVE_PROGRESS
|
|
1280
1422
|
versions = load_versions()
|
|
1423
|
+
readiness_timeout = args.timeout or int(versions["defaults"]["readinessTimeoutSeconds"])
|
|
1424
|
+
progress = ProgressReporter(
|
|
1425
|
+
getattr(args, "progress_file", None),
|
|
1426
|
+
getattr(args, "operation_id", None),
|
|
1427
|
+
getattr(args, "operation_kind", "runtime-start"),
|
|
1428
|
+
total_steps=6,
|
|
1429
|
+
completion_phase="runtime-complete",
|
|
1430
|
+
completion_message="Foggy Runtime is ready",
|
|
1431
|
+
failure_message="Foggy Runtime startup failed",
|
|
1432
|
+
)
|
|
1433
|
+
ACTIVE_PROGRESS = progress
|
|
1434
|
+
progress.update("runtime-preflight", 0, "Checking Runtime state and prerequisites", percent=3)
|
|
1281
1435
|
install_root = normalized(args.install_root or default_install_root())
|
|
1282
1436
|
state = read_install_state(install_root)
|
|
1283
1437
|
data_root = normalized(args.data_root or state["dataRoot"])
|
|
@@ -1296,15 +1450,24 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1296
1450
|
base_url = prior.get("runtimeUrl")
|
|
1297
1451
|
if not isinstance(base_url, str) or not base_url:
|
|
1298
1452
|
raise OnboardingError("Recorded Runtime does not contain runtimeUrl")
|
|
1299
|
-
|
|
1453
|
+
progress.update("runtime-readiness", 3, "Revalidating the recorded Runtime", percent=30, elapsed_seconds=0, timeout_seconds=readiness_timeout)
|
|
1454
|
+
wait_result = command_result_with_progress(
|
|
1300
1455
|
[cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready",
|
|
1301
|
-
"--timeout-seconds", str(
|
|
1456
|
+
"--timeout-seconds", str(readiness_timeout),
|
|
1302
1457
|
"--interval-seconds", "1"],
|
|
1303
|
-
|
|
1458
|
+
readiness_timeout + 30,
|
|
1459
|
+
progress,
|
|
1460
|
+
phase="runtime-readiness",
|
|
1461
|
+
step_index=3,
|
|
1462
|
+
message="Revalidating the recorded Runtime",
|
|
1463
|
+
readiness_timeout=readiness_timeout,
|
|
1464
|
+
start_percent=30,
|
|
1465
|
+
end_percent=85,
|
|
1304
1466
|
)
|
|
1305
1467
|
wait_payload = parse_json_output(wait_result, "wait-ready")
|
|
1306
1468
|
if wait_payload.get("success") is not True:
|
|
1307
1469
|
raise OnboardingError("wait-ready returned success=false for recorded Runtime")
|
|
1470
|
+
progress.update("runtime-capabilities", 4, "Verifying Runtime API capabilities", percent=90)
|
|
1308
1471
|
capabilities = parse_json_output(
|
|
1309
1472
|
command_result(
|
|
1310
1473
|
[cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "capabilities"],
|
|
@@ -1324,6 +1487,8 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1324
1487
|
"schemaVersion": capabilities.get("data", {}).get("schemaVersion"),
|
|
1325
1488
|
"securityMode": capabilities.get("data", {}).get("securityMode"),
|
|
1326
1489
|
}
|
|
1490
|
+
progress.finish()
|
|
1491
|
+
ACTIVE_PROGRESS = None
|
|
1327
1492
|
return {
|
|
1328
1493
|
"success": True,
|
|
1329
1494
|
**prior,
|
|
@@ -1340,6 +1505,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1340
1505
|
"productionReady": False,
|
|
1341
1506
|
}
|
|
1342
1507
|
existing.unlink()
|
|
1508
|
+
progress.update("runtime-preflight", 1, "Checking port and Runtime workspace", percent=10)
|
|
1343
1509
|
port = args.port or int(versions["defaults"]["port"])
|
|
1344
1510
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
1345
1511
|
try:
|
|
@@ -1350,6 +1516,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1350
1516
|
work_dir.mkdir(parents=True, exist_ok=True)
|
|
1351
1517
|
launcher_dir = normalized(state["launcher"]["path"])
|
|
1352
1518
|
java_exe = args.java or os.environ.get("JAVA_EXE", "java")
|
|
1519
|
+
progress.update("runtime-launcher", 2, "Starting the Java Runtime through Launcher", percent=20)
|
|
1353
1520
|
if os.name == "nt":
|
|
1354
1521
|
shell = shutil.which("pwsh") or shutil.which("powershell")
|
|
1355
1522
|
if not shell:
|
|
@@ -1397,12 +1564,25 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1397
1564
|
evidence_dir = data_root / "evidence" / f"runtime-start-{dt.datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
1398
1565
|
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
1399
1566
|
atomic_json(evidence_dir / "launch.json", launch)
|
|
1567
|
+
startup_started = time.monotonic()
|
|
1400
1568
|
try:
|
|
1401
|
-
|
|
1569
|
+
progress.update("runtime-readiness", 3, "Waiting for Java Runtime readiness", percent=30, elapsed_seconds=0, timeout_seconds=readiness_timeout)
|
|
1570
|
+
wait_result = command_result_with_progress(
|
|
1571
|
+
[cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready", "--timeout-seconds", str(readiness_timeout), "--interval-seconds", "1"],
|
|
1572
|
+
readiness_timeout + 30,
|
|
1573
|
+
progress,
|
|
1574
|
+
phase="runtime-readiness",
|
|
1575
|
+
step_index=3,
|
|
1576
|
+
message="Waiting for Java Runtime readiness",
|
|
1577
|
+
readiness_timeout=readiness_timeout,
|
|
1578
|
+
start_percent=30,
|
|
1579
|
+
end_percent=85,
|
|
1580
|
+
)
|
|
1402
1581
|
wait_payload = parse_json_output(wait_result, "wait-ready")
|
|
1403
1582
|
if wait_payload.get("success") is not True:
|
|
1404
1583
|
raise OnboardingError("wait-ready returned success=false")
|
|
1405
1584
|
atomic_json(evidence_dir / "wait-ready.json", wait_payload)
|
|
1585
|
+
progress.update("runtime-capabilities", 4, "Verifying Runtime API capabilities", percent=90)
|
|
1406
1586
|
capabilities_result = command_result([cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "capabilities"], timeout=30)
|
|
1407
1587
|
capabilities = parse_json_output(capabilities_result, "capabilities")
|
|
1408
1588
|
expected_contract = versions["components"]["launcher"]["runtimeApiContract"]
|
|
@@ -1411,12 +1591,21 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1411
1591
|
if capabilities.get("data", {}).get("securityMode") != versions["defaults"]["securityMode"]:
|
|
1412
1592
|
raise OnboardingError("Packaged Launcher did not report the expected dev/test security mode")
|
|
1413
1593
|
atomic_json(evidence_dir / "capabilities.json", capabilities)
|
|
1414
|
-
except Exception:
|
|
1594
|
+
except Exception as exc:
|
|
1595
|
+
failure = write_runtime_start_failure(
|
|
1596
|
+
data_root,
|
|
1597
|
+
evidence_dir,
|
|
1598
|
+
launch,
|
|
1599
|
+
progress.phase,
|
|
1600
|
+
exc,
|
|
1601
|
+
round((time.monotonic() - startup_started) * 1000),
|
|
1602
|
+
)
|
|
1415
1603
|
try:
|
|
1416
1604
|
os.kill(pid, signal.SIGTERM)
|
|
1417
1605
|
except OSError:
|
|
1418
1606
|
pass
|
|
1419
|
-
raise
|
|
1607
|
+
raise OnboardingError(f"{exc}; startup diagnostics: {failure['evidenceDir']}") from exc
|
|
1608
|
+
progress.update("runtime-state", 5, "Saving verified Runtime state", percent=96, current_file="runtime-state.json")
|
|
1420
1609
|
runtime_state = {
|
|
1421
1610
|
"schemaVersion": RUNTIME_STATE_SCHEMA,
|
|
1422
1611
|
"startedAt": now_utc(),
|
|
@@ -1435,6 +1624,9 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1435
1624
|
},
|
|
1436
1625
|
}
|
|
1437
1626
|
atomic_json(data_root / "runtime-state.json", runtime_state)
|
|
1627
|
+
(data_root / "last-runtime-start-failure.json").unlink(missing_ok=True)
|
|
1628
|
+
progress.finish()
|
|
1629
|
+
ACTIVE_PROGRESS = None
|
|
1438
1630
|
return {"success": True, **runtime_state, "productionReady": False}
|
|
1439
1631
|
|
|
1440
1632
|
|
|
@@ -2855,6 +3047,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2855
3047
|
start.add_argument("--namespace")
|
|
2856
3048
|
start.add_argument("--timeout", type=int)
|
|
2857
3049
|
start.add_argument("--java")
|
|
3050
|
+
start.add_argument("--progress-file")
|
|
3051
|
+
start.add_argument("--operation-id")
|
|
3052
|
+
start.add_argument("--operation-kind", default="runtime-start")
|
|
2858
3053
|
start.set_defaults(handler=runtime_start_command)
|
|
2859
3054
|
|
|
2860
3055
|
stop = sub.add_parser("runtime-stop")
|