@winmatrix/supervisor 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@winmatrix/supervisor",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "main": "src/run-agent-engine.mjs",
6
6
  "bin": {
@@ -0,0 +1,208 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+ import { createHash, createPublicKey, generateKeyPairSync } from 'node:crypto';
15
+
16
+ const REQUIRED_SCOPES = ['operator.read', 'operator.write'];
17
+
18
+ function parseRecord(raw, gatewayOrigin) {
19
+ const value = JSON.parse(raw);
20
+ if (!value || value.version !== 1 || value.gatewayOrigin !== gatewayOrigin
21
+ || !['unpaired', 'pairing_pending', 'paired', 'token_stale'].includes(value.status)
22
+ || typeof value.deviceId !== 'string'
23
+ || typeof value.privateKeyPem !== 'string' || typeof value.publicKeyPem !== 'string'
24
+ || typeof value.role !== 'string' || !Array.isArray(value.scopes)) {
25
+ throw new Error('OpenClaw workstation device binding is invalid');
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function publicKeyRaw(publicKeyPem) {
31
+ const der = createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' });
32
+ if (der.length < 32) throw new Error('OpenClaw workstation public key is invalid');
33
+ return der.subarray(der.length - 32);
34
+ }
35
+
36
+ export function fingerprintOpenClawPublicKey(publicKeyPem) {
37
+ return createHash('sha256').update(publicKeyRaw(publicKeyPem)).digest('hex');
38
+ }
39
+
40
+ export function encodeOpenClawPublicKey(publicKeyPem) {
41
+ return publicKeyRaw(publicKeyPem).toString('base64url');
42
+ }
43
+
44
+ function assertPrivatePermissions(path) {
45
+ if (process.platform === 'win32') return;
46
+ if ((statSync(path).mode & 0o077) !== 0) {
47
+ throw new Error('OpenClaw workstation device binding must have mode 0600');
48
+ }
49
+ }
50
+
51
+ function acquireLock(lockPath) {
52
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
53
+ for (let attempt = 0; attempt < 80; attempt += 1) {
54
+ try {
55
+ const fd = openSync(lockPath, 'wx', 0o600);
56
+ writeFileSync(fd, `${process.pid}\n`, 'utf8');
57
+ closeSync(fd);
58
+ return;
59
+ } catch (error) {
60
+ if (error?.code !== 'EEXIST') throw error;
61
+ const ageMs = Date.now() - statSync(lockPath).mtimeMs;
62
+ if (ageMs > 30_000) {
63
+ rmSync(lockPath, { force: true });
64
+ continue;
65
+ }
66
+ const wait = new Int32Array(new SharedArrayBuffer(4));
67
+ Atomics.wait(wait, 0, 0, 25);
68
+ }
69
+ }
70
+ throw new Error('OpenClaw workstation device binding is locked');
71
+ }
72
+
73
+ function atomicWrite(path, value, lockPath) {
74
+ acquireLock(lockPath);
75
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
76
+ try {
77
+ writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600, flag: 'wx' });
78
+ chmodSync(tempPath, 0o600);
79
+ renameSync(tempPath, path);
80
+ chmodSync(path, 0o600);
81
+ } finally {
82
+ rmSync(tempPath, { force: true });
83
+ rmSync(lockPath, { force: true });
84
+ }
85
+ }
86
+
87
+ export class WorkstationOpenClawDeviceStore {
88
+ #filePath;
89
+ #lockPath;
90
+ #gatewayOrigin;
91
+
92
+ constructor({ stateDir, gatewayOrigin }) {
93
+ this.#filePath = join(stateDir, 'winmatrix-gateway-client', 'device-auth.json');
94
+ this.#lockPath = `${this.#filePath}.lock`;
95
+ this.#gatewayOrigin = gatewayOrigin;
96
+ }
97
+
98
+ #load() {
99
+ if (!existsSync(this.#filePath)) {
100
+ throw new Error('OpenClaw workstation DeviceToken is not paired');
101
+ }
102
+ assertPrivatePermissions(this.#filePath);
103
+ return parseRecord(readFileSync(this.#filePath, 'utf8'), this.#gatewayOrigin);
104
+ }
105
+
106
+ #loadOptional() {
107
+ if (!existsSync(this.#filePath)) return null;
108
+ assertPrivatePermissions(this.#filePath);
109
+ return parseRecord(readFileSync(this.#filePath, 'utf8'), this.#gatewayOrigin);
110
+ }
111
+
112
+ loadOrCreateIdentity() {
113
+ let record = this.#loadOptional();
114
+ if (!record) {
115
+ const pair = generateKeyPairSync('ed25519');
116
+ const privateKeyPem = pair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
117
+ const publicKeyPem = pair.publicKey.export({ type: 'spki', format: 'pem' }).toString();
118
+ record = {
119
+ version: 1,
120
+ gatewayOrigin: this.#gatewayOrigin,
121
+ deviceId: fingerprintOpenClawPublicKey(publicKeyPem),
122
+ privateKeyPem,
123
+ publicKeyPem,
124
+ role: 'operator',
125
+ scopes: [],
126
+ status: 'unpaired',
127
+ };
128
+ mkdirSync(dirname(this.#filePath), { recursive: true, mode: 0o700 });
129
+ chmodSync(dirname(this.#filePath), 0o700);
130
+ atomicWrite(this.#filePath, record, this.#lockPath);
131
+ }
132
+ return {
133
+ deviceId: record.deviceId,
134
+ privateKeyPem: record.privateKeyPem,
135
+ publicKeyPem: record.publicKeyPem,
136
+ };
137
+ }
138
+
139
+ loadToken({ gatewayOrigin, deviceId, role }) {
140
+ const record = this.#load();
141
+ if (gatewayOrigin !== this.#gatewayOrigin || record.deviceId !== deviceId || record.role !== role) {
142
+ return null;
143
+ }
144
+ if (record.status !== 'paired' || typeof record.deviceToken !== 'string') return null;
145
+ for (const scope of REQUIRED_SCOPES) {
146
+ if (!record.scopes.includes(scope)) {
147
+ throw new Error(`OpenClaw workstation DeviceToken is missing scope '${scope}'`);
148
+ }
149
+ }
150
+ return { token: record.deviceToken, scopes: [...record.scopes] };
151
+ }
152
+
153
+ storeToken({ gatewayOrigin, deviceId, role, token, scopes }) {
154
+ const record = this.#loadOptional();
155
+ if (!record) throw new Error('OpenClaw workstation device identity is unavailable');
156
+ if (gatewayOrigin !== this.#gatewayOrigin || record.deviceId !== deviceId) {
157
+ throw new Error('OpenClaw workstation DeviceToken rotation binding mismatch');
158
+ }
159
+ atomicWrite(this.#filePath, {
160
+ ...record,
161
+ role,
162
+ scopes: [...scopes],
163
+ status: 'paired',
164
+ deviceToken: token,
165
+ }, this.#lockPath);
166
+ }
167
+
168
+ clearToken({ gatewayOrigin, deviceId, role }) {
169
+ const record = this.#loadOptional();
170
+ if (!record) return;
171
+ if (gatewayOrigin !== this.#gatewayOrigin || record.deviceId !== deviceId || record.role !== role) return;
172
+ const { deviceToken: _removed, ...withoutToken } = record;
173
+ atomicWrite(this.#filePath, {
174
+ ...withoutToken,
175
+ scopes: [],
176
+ status: 'token_stale',
177
+ }, this.#lockPath);
178
+ }
179
+
180
+
181
+ markPairingPending(requestId) {
182
+ const record = this.#load();
183
+ atomicWrite(this.#filePath, {
184
+ ...record,
185
+ status: 'pairing_pending',
186
+ requestId,
187
+ }, this.#lockPath);
188
+ }
189
+ }
190
+
191
+ export function createWorkstationOpenClawAdapterConfig(env = process.env) {
192
+ const stateDir = env.OPENCLAW_STATE_DIR || '/home/node/.openclaw';
193
+ const port = Number(env.OPENCLAW_GATEWAY_PORT || 18789);
194
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
195
+ throw new Error('OPENCLAW_GATEWAY_PORT must be a valid TCP port');
196
+ }
197
+ const gatewayUrl = `ws://127.0.0.1:${port}`;
198
+ const gatewayOrigin = `http://127.0.0.1:${port}`;
199
+ const deviceAuthStore = new WorkstationOpenClawDeviceStore({ stateDir, gatewayOrigin });
200
+ deviceAuthStore.loadOrCreateIdentity();
201
+ return {
202
+ mode: 'gateway',
203
+ hostKind: 'workstation',
204
+ gatewayUrl,
205
+ gatewayOrigin,
206
+ deviceAuthStore,
207
+ };
208
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { join } from 'node:path';
5
+ import {
6
+ WorkstationOpenClawDeviceStore,
7
+ encodeOpenClawPublicKey,
8
+ fingerprintOpenClawPublicKey,
9
+ } from './openclaw-device-store.mjs';
10
+
11
+ const SDK_PATH = process.env.WINMATRIX_AGENT_SDK_PATH
12
+ || '/home/node/.local/lib/node_modules/@winmatrix/agent-sdk';
13
+
14
+ function readArg(argv, name) {
15
+ const index = argv.indexOf(name);
16
+ return index >= 0 ? argv[index + 1]?.trim() : undefined;
17
+ }
18
+
19
+ export function decodeSetupCode(setupCode) {
20
+ if (typeof setupCode !== 'string' || !setupCode) throw new Error('OpenClaw setup code is missing');
21
+ const payload = JSON.parse(Buffer.from(setupCode, 'base64url').toString('utf8'));
22
+ if (!payload || typeof payload.bootstrapToken !== 'string' || !payload.bootstrapToken.trim()) {
23
+ throw new Error('OpenClaw setup code does not contain a bootstrap credential');
24
+ }
25
+ return payload;
26
+ }
27
+
28
+ function resolvePairingContext(input) {
29
+ const expectedWorkstationId = input.workstationId?.trim();
30
+ const env = input.env ?? process.env;
31
+ const actualWorkstationId = env.WINMATRIX_WORKSTATION_ID?.trim();
32
+ if (!expectedWorkstationId || actualWorkstationId !== expectedWorkstationId) {
33
+ throw new Error('OpenClaw pairing workstation identity mismatch');
34
+ }
35
+ const port = Number(env.OPENCLAW_GATEWAY_PORT || 18789);
36
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
37
+ throw new Error('OPENCLAW_GATEWAY_PORT must be a valid TCP port');
38
+ }
39
+ return {
40
+ expectedWorkstationId,
41
+ env,
42
+ gatewayUrl: `ws://127.0.0.1:${port}`,
43
+ };
44
+ }
45
+
46
+ export async function issueOpenClawBootstrap(input, deps = {}) {
47
+ const context = resolvePairingContext(input);
48
+ const cliJson = deps.runJson ?? ((args) => runJson('openclaw', args, { env: context.env }));
49
+ const qr = await cliJson(['qr', '--json', '--url', context.gatewayUrl]);
50
+ const setup = decodeSetupCode(qr.setupCode);
51
+ return { bootstrapToken: setup.bootstrapToken.trim() };
52
+ }
53
+
54
+ export async function approveOpenClawPairing(input, deps = {}) {
55
+ const context = resolvePairingContext(input);
56
+ const requestId = input.requestId?.trim();
57
+ const deviceId = input.deviceId?.trim();
58
+ const publicKey = input.publicKey?.trim();
59
+ if (!requestId || !deviceId || !publicKey) {
60
+ throw new Error('OpenClaw pairing approval identity is incomplete');
61
+ }
62
+ const cliJson = deps.runJson ?? ((args) => runJson('openclaw', args, { env: context.env }));
63
+ const listed = await cliJson(['devices', 'list', '--json']);
64
+ const pending = Array.isArray(listed?.pending) ? listed.pending : [];
65
+ const exact = pending.find((item) => item?.requestId === requestId);
66
+ if (!exact || exact.deviceId !== deviceId || exact.publicKey !== publicKey) {
67
+ throw new Error('OpenClaw pending pairing request identity mismatch');
68
+ }
69
+ await cliJson(['devices', 'approve', requestId, '--json']);
70
+ return { approved: true, requestId, deviceId };
71
+ }
72
+
73
+ function runJson(command, args, options = {}) {
74
+ return new Promise((resolve, reject) => {
75
+ const child = spawn(command, args, {
76
+ env: options.env ?? process.env,
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ });
79
+ const stdout = [];
80
+ const stderr = [];
81
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
82
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
83
+ child.once('error', () => reject(new Error(`OpenClaw command unavailable: ${args[0] ?? command}`)));
84
+ child.once('close', (code) => {
85
+ if (code !== 0) {
86
+ reject(new Error(`OpenClaw command failed: ${args.slice(0, 2).join(' ')}`));
87
+ return;
88
+ }
89
+ try {
90
+ resolve(JSON.parse(Buffer.concat(stdout).toString('utf8')));
91
+ } catch {
92
+ reject(new Error('OpenClaw command returned invalid JSON'));
93
+ }
94
+ void stderr;
95
+ });
96
+ });
97
+ }
98
+
99
+ function createBootstrapProvider(value) {
100
+ let credential = value;
101
+ return {
102
+ readOnce: async () => credential,
103
+ clear: async () => { credential = ''; },
104
+ };
105
+ }
106
+
107
+ export async function pairOpenClawWorkstation(input, deps = {}) {
108
+ const context = resolvePairingContext(input);
109
+ const { expectedWorkstationId, env, gatewayUrl } = context;
110
+ const stateDir = env.OPENCLAW_STATE_DIR || '/home/node/.openclaw';
111
+ const port = Number(env.OPENCLAW_GATEWAY_PORT || 18789);
112
+ const gatewayOrigin = `http://127.0.0.1:${port}`;
113
+ const store = deps.store ?? new WorkstationOpenClawDeviceStore({ stateDir, gatewayOrigin });
114
+ const identity = store.loadOrCreateIdentity();
115
+ const expectedPublicKey = encodeOpenClawPublicKey(identity.publicKeyPem);
116
+ const publicKeyFingerprint = fingerprintOpenClawPublicKey(identity.publicKeyPem);
117
+ const cliJson = deps.runJson ?? ((args) => runJson('openclaw', args, { env }));
118
+ const provision = deps.provisionGatewayDevice ?? (await import(
119
+ pathToFileURL(join(SDK_PATH, 'dist', 'index.js')).href
120
+ )).provisionGatewayDevice;
121
+
122
+ let bootstrapToken = '';
123
+ try {
124
+ const qr = await cliJson(['qr', '--json', '--url', gatewayUrl]);
125
+ const setup = decodeSetupCode(qr.setupCode);
126
+ bootstrapToken = setup.bootstrapToken.trim();
127
+ let result = await provision({
128
+ gatewayUrl,
129
+ gatewayOrigin,
130
+ deviceAuthStore: store,
131
+ bootstrapProvider: createBootstrapProvider(bootstrapToken),
132
+ timeoutMs: 30_000,
133
+ });
134
+ if (result.status === 'pairing_pending') {
135
+ const listed = await cliJson(['devices', 'list', '--json']);
136
+ const pending = Array.isArray(listed?.pending) ? listed.pending : [];
137
+ const exact = pending.find((item) => item?.requestId === result.requestId);
138
+ if (!exact || exact.deviceId !== identity.deviceId || exact.publicKey !== expectedPublicKey) {
139
+ throw new Error('OpenClaw pending pairing request identity mismatch');
140
+ }
141
+ store.markPairingPending?.(result.requestId);
142
+ await cliJson(['devices', 'approve', result.requestId, '--json']);
143
+ result = await provision({
144
+ gatewayUrl,
145
+ gatewayOrigin,
146
+ deviceAuthStore: store,
147
+ bootstrapProvider: createBootstrapProvider(bootstrapToken),
148
+ timeoutMs: 30_000,
149
+ });
150
+ }
151
+ if (result.status !== 'paired' || result.deviceId !== identity.deviceId) {
152
+ throw new Error(`OpenClaw pairing did not complete: ${result.reasonCode ?? result.status}`);
153
+ }
154
+ return {
155
+ paired: true,
156
+ workstationId: expectedWorkstationId,
157
+ deviceId: result.deviceId,
158
+ role: result.role,
159
+ scopes: [...result.scopes],
160
+ publicKeyFingerprint,
161
+ };
162
+ } finally {
163
+ bootstrapToken = '';
164
+ }
165
+ }
166
+
167
+ async function main() {
168
+ const argv = process.argv.slice(2);
169
+ const command = argv[0]?.startsWith('--') ? 'pair' : argv[0] ?? 'pair';
170
+ const workstationId = readArg(argv, '--workstation-id');
171
+ let result;
172
+ if (command === 'bootstrap') {
173
+ result = await issueOpenClawBootstrap({ workstationId });
174
+ } else if (command === 'approve') {
175
+ const chunks = [];
176
+ for await (const chunk of process.stdin) chunks.push(chunk);
177
+ const approval = JSON.parse(Buffer.concat(chunks).toString('utf8'));
178
+ result = await approveOpenClawPairing({ workstationId, ...approval });
179
+ } else {
180
+ result = await pairOpenClawWorkstation({ workstationId });
181
+ }
182
+ process.stdout.write(`${JSON.stringify(result)}\n`);
183
+ }
184
+
185
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
186
+ main().catch((error) => {
187
+ process.stderr.write(`${error instanceof Error ? error.message : 'OpenClaw pairing failed'}\n`);
188
+ process.exitCode = 1;
189
+ });
190
+ }
@@ -72,6 +72,7 @@ if (!SUPERVISOR_ROOT) {
72
72
  }
73
73
  const CALLBACK_TIMEOUT_MS = Number(process.env.WINMATRIX_CALLBACK_TIMEOUT_MS) || 5000;
74
74
  const MAX_ACTIVE_RUNS = Number(process.env.WINMATRIX_MAX_ACTIVE_RUNS) || 100;
75
+ const LAUNCH_ACK_TIMEOUT_MS = Number(process.env.WINMATRIX_LAUNCH_ACK_TIMEOUT_MS) || 10_000;
75
76
  /** 单次 events 请求返回的最大事件数,避免大量事件堆积导致响应超限。 */
76
77
  const MAX_EVENTS_PAGE_SIZE = 500;
77
78
 
@@ -92,7 +93,27 @@ function saveActiveRuns() {
92
93
  runKey,
93
94
  metadata,
94
95
  }));
95
- fs.writeFileSync(activeRunsFile, JSON.stringify(data, null, 2), 'utf8');
96
+ atomicWriteFile(activeRunsFile, JSON.stringify(data, null, 2));
97
+ }
98
+
99
+ function atomicWriteFile(filePath, content) {
100
+ const tempPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
101
+ fs.writeFileSync(tempPath, content, 'utf8');
102
+ try {
103
+ fs.renameSync(tempPath, filePath);
104
+ } finally {
105
+ try { fs.rmSync(tempPath, { force: true }); } catch { /* best effort */ }
106
+ }
107
+ }
108
+
109
+ function atomicCreateExclusive(filePath, content) {
110
+ const tempPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
111
+ fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600 });
112
+ try {
113
+ fs.linkSync(tempPath, filePath);
114
+ } finally {
115
+ try { fs.rmSync(tempPath, { force: true }); } catch { /* best effort */ }
116
+ }
96
117
  }
97
118
 
98
119
  /**
@@ -199,7 +220,7 @@ function saveRunMetadata(runKey, metadata) {
199
220
  fs.mkdirSync(runDir, { recursive: true });
200
221
  }
201
222
  const statusFile = path.join(runDir, 'status.json');
202
- fs.writeFileSync(statusFile, JSON.stringify(metadata, null, 2), 'utf8');
223
+ atomicWriteFile(statusFile, JSON.stringify(metadata, null, 2));
203
224
  }
204
225
 
205
226
  /**
@@ -232,7 +253,7 @@ function saveResult(runKey, result) {
232
253
  fs.mkdirSync(runDir, { recursive: true });
233
254
  }
234
255
  const resultFile = path.join(runDir, 'result.json');
235
- fs.writeFileSync(resultFile, JSON.stringify(result, null, 2), 'utf8');
256
+ atomicWriteFile(resultFile, JSON.stringify(result, null, 2));
236
257
  }
237
258
 
238
259
  /**
@@ -291,7 +312,7 @@ function hasCancelMarker(runKey) {
291
312
  * @param {RunMetadata} startMetadata - 启动元数据(startTime)
292
313
  * @returns {object} Callback payload
293
314
  */
294
- function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata) {
315
+ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata) {
295
316
  const sep = runKey.lastIndexOf(':');
296
317
  const recordId = sep > 0 ? runKey.slice(0, sep) : runKey;
297
318
  const parsedAttempt = sep > 0 ? Number(runKey.slice(sep + 1)) : NaN;
@@ -304,8 +325,10 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
304
325
  ? Math.round((endMs - startMs) / 1000)
305
326
  : undefined;
306
327
 
307
- const text = typeof result?.text === 'string' ? result.text : undefined;
308
- return {
328
+ const text = typeof result?.text === 'string' ? result.text : undefined;
329
+ const engineId = typeof startMetadata?.engineId === 'string' ? startMetadata.engineId : undefined;
330
+ const engineSessionId = typeof result?.sessionId === 'string' ? result.sessionId : undefined;
331
+ return {
309
332
  recordId,
310
333
  attemptNo,
311
334
  runKey,
@@ -314,7 +337,7 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
314
337
  result: text,
315
338
  error: typeof result?.error === 'string' ? result.error : undefined,
316
339
  durationSeconds,
317
- claudeSessionId: typeof result?.sessionId === 'string' ? result.sessionId : undefined,
340
+ claudeSessionId: engineId === 'claude-code' ? engineSessionId : undefined,
318
341
  statusFilePath: path.join(runDir, 'status.json'),
319
342
  resultFilePath: path.join(runDir, 'result.json'),
320
343
  completedAt: endMetadata.endTime,
@@ -322,10 +345,13 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
322
345
  exitCode: endMetadata.exitCode,
323
346
  ...(endMetadata.signal ? { signal: endMetadata.signal } : {}),
324
347
  ...(endMetadata.resumeDegraded ? { resumeDegraded: true } : {}),
325
- ...(result?.usage ? { usage: result.usage } : {}),
326
- },
327
- };
328
- }
348
+ ...(result?.usage ? { usage: result.usage } : {}),
349
+ ...(engineId && engineId !== 'claude-code' && engineSessionId
350
+ ? { agentEngine: { engineId, sessionId: engineSessionId } }
351
+ : {}),
352
+ },
353
+ };
354
+ }
329
355
 
330
356
  /** 重定向最大跳数,防重定向环。 */
331
357
  const MAX_CALLBACK_REDIRECTS = 5;
@@ -533,6 +559,10 @@ async function runOwned(payload) {
533
559
  instanceId: payload.instanceId,
534
560
  lastSequence: 0,
535
561
  };
562
+ const testStartDelayMs = Number(process.env.WINMATRIX_AGENT_ENGINE_TEST_START_DELAY_MS) || 0;
563
+ if (testStartDelayMs > 0) {
564
+ await new Promise((resolve) => setTimeout(resolve, testStartDelayMs));
565
+ }
536
566
  saveRunMetadata(runKey, metadata);
537
567
  activeRuns.set(runKey, { child, metadata });
538
568
  saveActiveRuns();
@@ -798,7 +828,7 @@ function buildRunAck(payload, disposition) {
798
828
  };
799
829
  }
800
830
 
801
- async function launch(payload) {
831
+ async function launch(payload, retryCount = 0) {
802
832
  const { runKey, invocationFingerprint, launchSpec, instanceId } = payload;
803
833
  if (!runKey || !invocationFingerprint || !launchSpec || !instanceId) {
804
834
  throw new Error('Missing required fields: runKey, invocationFingerprint, launchSpec, instanceId');
@@ -817,8 +847,32 @@ async function launch(payload) {
817
847
  }
818
848
 
819
849
  const claimFile = path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.launch`);
850
+ const readClaim = () => {
851
+ let raw;
852
+ try {
853
+ raw = fs.readFileSync(claimFile, 'utf8');
854
+ } catch (error) {
855
+ if (error?.code === 'ENOENT') return null;
856
+ throw error;
857
+ }
858
+ try {
859
+ const parsed = JSON.parse(raw);
860
+ return { fingerprint: parsed.fingerprint, ownerPid: parsed.ownerPid };
861
+ } catch {
862
+ return { fingerprint: raw.trim(), ownerPid: undefined };
863
+ }
864
+ };
865
+ const isPidAlive = (pid) => {
866
+ if (!Number.isInteger(pid) || pid <= 0) return false;
867
+ try {
868
+ process.kill(pid, 0);
869
+ return true;
870
+ } catch {
871
+ return false;
872
+ }
873
+ };
820
874
  const waitForClaimedRun = async () => {
821
- const deadline = Date.now() + 2_000;
875
+ const deadline = Date.now() + LAUNCH_ACK_TIMEOUT_MS;
822
876
  while (Date.now() < deadline) {
823
877
  const metadata = loadRunMetadata(runKey);
824
878
  if (metadata) {
@@ -828,24 +882,44 @@ async function launch(payload) {
828
882
  return metadata;
829
883
  }
830
884
  if (!fs.existsSync(claimFile)) break;
885
+ const claim = readClaim();
886
+ if (!claim) continue;
887
+ const ownerPid = claim.ownerPid;
888
+ if (ownerPid && !isPidAlive(ownerPid)) {
889
+ fs.rmSync(claimFile, { force: true });
890
+ break;
891
+ }
831
892
  await new Promise((resolve) => setTimeout(resolve, 20));
832
893
  }
833
- throw new Error(`Claimed run did not start: ${runKey}`);
894
+ throw new Error(`Launch ack timeout: runKey=${runKey} code=launch_ack_timeout`);
834
895
  };
835
- let claim;
836
896
  try {
837
- claim = fs.openSync(claimFile, 'wx', 0o600);
838
- fs.writeFileSync(claim, invocationFingerprint, 'utf8');
897
+ atomicCreateExclusive(claimFile, JSON.stringify({
898
+ fingerprint: invocationFingerprint,
899
+ ownerPid: process.pid,
900
+ createdAt: new Date().toISOString(),
901
+ }));
839
902
  } catch (error) {
840
903
  if (error?.code !== 'EEXIST') throw error;
841
- const claimedFingerprint = fs.readFileSync(claimFile, 'utf8');
904
+ const claimed = readClaim();
905
+ if (!claimed) {
906
+ if (retryCount < 1) return launch(payload, retryCount + 1);
907
+ throw new Error(`Launch claim disappeared: runKey=${runKey}`);
908
+ }
909
+ const claimedFingerprint = claimed.fingerprint;
842
910
  if (claimedFingerprint !== invocationFingerprint) {
843
911
  throw new Error(`Fingerprint mismatch for runKey=${runKey}`);
844
912
  }
845
- await waitForClaimedRun();
846
- return buildRunAck(payload, 'existing');
847
- } finally {
848
- if (claim !== undefined) fs.closeSync(claim);
913
+ try {
914
+ await waitForClaimedRun();
915
+ return buildRunAck(payload, 'existing');
916
+ } catch (waitError) {
917
+ if (String(waitError?.message ?? '').includes('launch_ack_timeout') && !fs.existsSync(claimFile)) {
918
+ if (retryCount < 1) return launch(payload, retryCount + 1);
919
+ throw waitError;
920
+ }
921
+ throw waitError;
922
+ }
849
923
  }
850
924
 
851
925
  if (process.env.WINMATRIX_AGENT_ENGINE_TEST_INLINE === 'true') {
@@ -860,20 +934,41 @@ async function launch(payload) {
860
934
 
861
935
  const worker = spawn(process.execPath, [SCRIPT_PATH, 'supervise-run'], {
862
936
  detached: true,
863
- stdio: ['pipe', 'ignore', 'ignore'],
937
+ stdio: ['pipe', 'ignore', fs.openSync(path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.worker.log`), 'a')],
864
938
  env: process.env,
865
939
  });
940
+ atomicWriteFile(claimFile, JSON.stringify({
941
+ fingerprint: invocationFingerprint,
942
+ ownerPid: worker.pid,
943
+ createdAt: new Date().toISOString(),
944
+ }));
945
+ let workerExited = false;
946
+ let workerExitCode;
947
+ let workerSignal;
948
+ let workerError;
949
+ worker.once('error', (error) => {
950
+ workerError = error;
951
+ });
952
+ worker.once('exit', (code, signal) => {
953
+ workerExited = true;
954
+ workerExitCode = code;
955
+ workerSignal = signal;
956
+ });
866
957
  worker.stdin.end(JSON.stringify(payload));
867
958
  worker.unref();
868
- const deadline = Date.now() + 2_000;
869
- while (!loadRunMetadata(runKey) && Date.now() < deadline) {
959
+ const deadline = Date.now() + LAUNCH_ACK_TIMEOUT_MS;
960
+ while (!loadRunMetadata(runKey) && !workerExited && !workerError && Date.now() < deadline) {
870
961
  await new Promise((resolve) => setTimeout(resolve, 20));
871
962
  }
872
- if (!loadRunMetadata(runKey)) {
963
+ if (loadRunMetadata(runKey)) {
964
+ return buildRunAck(payload, 'accepted');
965
+ }
966
+ if (workerError || workerExited) {
873
967
  fs.rmSync(claimFile, { force: true });
874
- throw new Error(`Run worker failed to start: ${runKey}`);
968
+ const detail = workerError?.message ?? `exitCode=${workerExitCode ?? 'null'} signal=${workerSignal ?? 'none'}`;
969
+ throw new Error(`Run worker failed to start: runKey=${runKey} code=worker_start_failed ${detail}; log=${path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.worker.log`)}`);
875
970
  }
876
- return buildRunAck(payload, 'accepted');
971
+ throw new Error(`Launch ack timeout: runKey=${runKey} code=launch_ack_timeout workerPid=${worker.pid ?? 'unknown'}`);
877
972
  }
878
973
 
879
974
  function listEvents(runKey, afterSequence = 0) {
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { createRequire } from 'node:module';
17
17
  import { isSdkFailureSubtype } from './engine-run-success.mjs';
18
+ import { createWorkstationOpenClawAdapterConfig } from './openclaw-device-store.mjs';
18
19
 
19
20
  const require = createRequire(import.meta.url);
20
21
 
@@ -201,7 +202,12 @@ async function main() {
201
202
  process.exit(1);
202
203
  }
203
204
 
204
- const adapterConfig = mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {};
205
+ const adapterConfig = options.engineId === 'openclaw'
206
+ ? {
207
+ ...createWorkstationOpenClawAdapterConfig(),
208
+ ...(mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {}),
209
+ }
210
+ : (mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {});
205
211
  const adapter = registry.create(options.engineId, adapterConfig);
206
212
 
207
213
  /** @type {import('@winmatrix/agent-sdk').AgentTaskContext} */