@ckanner/dsh-plugin-flywheel 0.28.0 → 0.28.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/dist/control.js CHANGED
@@ -4,6 +4,15 @@ import { directory, nonempty, record } from './protocol.js';
4
4
  const actions = ['start', 'status', 'stop', 'config', 'doctor'];
5
5
  const checkStatuses = ['pass', 'fail', 'warn', 'unknown', 'skipped'];
6
6
 
7
+ // This marker is issued only where the host can prove that no control process
8
+ // started. An engine/transport error after spawn must never acquire it.
9
+ export class ControlNotDispatchedError extends Error {
10
+ constructor(cause) {
11
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
12
+ this.name = 'ControlNotDispatchedError';
13
+ }
14
+ }
15
+
7
16
  export async function validateControl(input, defaultCwd) {
8
17
  if (!record(input) || !actions.includes(input.action)) {
9
18
  throw new Error('action must be start, status, stop, config, or doctor');
@@ -46,13 +55,18 @@ function validDiagnostic(value, request) {
46
55
 
47
56
  /** Only the engine executable can mutate engine state. No shell interpolation. */
48
57
  export async function runControl(input, { binary = 'flywheel', defaultCwd, signal, timeoutMs = 60000 } = {}) {
49
- nonempty(binary, 'flywheelBinary', 4096);
50
- if (binary.startsWith('-')) throw new Error('flywheelBinary must name an executable');
51
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 120000) {
52
- throw new Error('controlTimeoutMs must be between 100 and 120000');
58
+ let request;
59
+ try {
60
+ nonempty(binary, 'flywheelBinary', 4096);
61
+ if (binary.startsWith('-')) throw new Error('flywheelBinary must name an executable');
62
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 120000) {
63
+ throw new Error('controlTimeoutMs must be between 100 and 120000');
64
+ }
65
+ request = await validateControl(input, defaultCwd);
66
+ if (signal?.aborted) throw new Error('Flywheel control request cancelled');
67
+ } catch (error) {
68
+ throw new ControlNotDispatchedError(error);
53
69
  }
54
- const request = await validateControl(input, defaultCwd);
55
- if (signal?.aborted) throw new Error('Flywheel control request cancelled');
56
70
  // Diagnostic commands own watchdog child groups. Its TERM grace, pipe
57
71
  // WaitDelay, and quiescence confirmation may each take 5s; keep the Go owner
58
72
  // alive long enough to finish all three instead of orphaning its workers.
@@ -61,9 +75,17 @@ export async function runControl(input, { binary = 'flywheel', defaultCwd, signa
61
75
  if (request.taskDir) args.push('--task-dir', request.taskDir);
62
76
  if (request.probe) args.push('--probe');
63
77
  return new Promise((resolve, reject) => {
64
- const child = spawn(binary, args, {
65
- cwd: request.cwd, detached: false, stdio: ['ignore', 'pipe', 'pipe'],
66
- });
78
+ let child;
79
+ try {
80
+ child = spawn(binary, args, {
81
+ cwd: request.cwd, detached: false, stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ } catch (error) {
84
+ reject(new ControlNotDispatchedError(error));
85
+ return;
86
+ }
87
+ let spawned = false;
88
+ child.once('spawn', () => { spawned = true; });
67
89
  let stdout = '';
68
90
  let stderr = '';
69
91
  let failure;
@@ -88,7 +110,11 @@ export async function runControl(input, { binary = 'flywheel', defaultCwd, signa
88
110
  if (Buffer.byteLength(stdout) > 1024 * 1024) stop(new Error('Flywheel control response exceeds 1 MiB'));
89
111
  });
90
112
  child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-8192); });
91
- child.on('error', error => { failure = error; });
113
+ child.on('error', error => {
114
+ // Node emits spawn before other events after successful process creation.
115
+ // Require both the missing spawn event and missing PID for positive proof.
116
+ failure = !spawned && child.pid === undefined ? new ControlNotDispatchedError(error) : error;
117
+ });
92
118
  child.on('close', (code, childSignal) => {
93
119
  clearTimeout(timer);
94
120
  clearTimeout(killTimer);
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { runControl } from './control.js';
1
+ import { ControlNotDispatchedError, runControl } from './control.js';
2
2
  import { resolveFlywheelBinary } from './installation.js';
3
3
 
4
4
  export const inject = ['commands', 'tools'];
@@ -33,11 +33,25 @@ export function apply(ctx, config = {}) {
33
33
  description: '通过 Go 引擎控制 Flywheel 任务、查看实际模型配置或执行无推理环境检查。',
34
34
  input: { hint: '{"action":"doctor"} 或 {"action":"status","taskDir":"/absolute/docs/task"}' },
35
35
  async handler({ rawInput, agent, signal }) {
36
+ let input;
36
37
  try {
37
- const value = await control(JSON.parse(rawInput), agent, signal);
38
+ try {
39
+ input = JSON.parse(rawInput);
40
+ } catch (error) {
41
+ throw new ControlNotDispatchedError(error);
42
+ }
43
+ const value = await control(input, agent, signal);
38
44
  return { kind: 'success', text: JSON.stringify(value, null, 2) };
39
45
  } catch (error) {
40
- return { kind: 'error', text: error.message };
46
+ // CommandResult preserves only kind/text. Wrap every failure, including
47
+ // unknown outcomes, so a child error's text cannot impersonate host proof.
48
+ return { kind: 'error', text: JSON.stringify({
49
+ protocol: 1,
50
+ type: 'flywheel-control-error',
51
+ action: typeof input?.action === 'string' ? input.action : null,
52
+ dispatch: error instanceof ControlNotDispatchedError ? 'not-dispatched' : 'unknown',
53
+ message: error instanceof Error ? error.message : String(error),
54
+ }, null, 2) };
41
55
  }
42
56
  },
43
57
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckanner/dsh-plugin-flywheel",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -25,7 +25,8 @@
25
25
  "cordis.patch.yml",
26
26
  "README.md",
27
27
  "README.en.md",
28
- "PROTOCOL.md"
28
+ "PROTOCOL.md",
29
+ "THIRD_PARTY_NOTICES.md"
29
30
  ],
30
31
  "dsh": {
31
32
  "bundle": {
@@ -33,9 +34,10 @@
33
34
  },
34
35
  "client": {
35
36
  "inject": [
36
- "@deepseek-ai/dsh-client-ui-chat",
37
+ "@deepseek-ai/dsh-client-ui-conversation",
37
38
  "@deepseek-ai/dsh-client-ui-commands",
38
- "@deepseek-ai/dsh-api-remotes"
39
+ "@deepseek-ai/dsh-api-remotes",
40
+ "@deepseek-ai/dsh-client-locale"
39
41
  ],
40
42
  "platform": "web"
41
43
  }
@@ -66,7 +68,11 @@
66
68
  "dsh-subagent-codex-pro": "0.8.0"
67
69
  },
68
70
  "devDependencies": {
71
+ "@deepseek-ai/cordis": "4.0.5",
72
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
73
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
69
74
  "jsdom": "26.1.0",
75
+ "lightningcss": "1.33.0",
70
76
  "react": "18.3.1",
71
77
  "react-dom": "18.3.1"
72
78
  },