Skip to content

Commit 097a5de

Browse files
committed
merge: sync upstream v0.1.15 (PR ValueCell-ai#119 gemini apikey fix, PR ValueCell-ai#120 gateway start waiting) and bump version to 0.1.16
2 parents 64f55b8 + f32521b commit 097a5de

7 files changed

Lines changed: 161 additions & 71 deletions

File tree

electron/gateway/manager.ts

Lines changed: 94 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -229,23 +229,26 @@ export class GatewayManager extends EventEmitter {
229229
this.setStatus({ state: 'starting', reconnectAttempts: 0 });
230230

231231
try {
232-
// Check if Python environment is ready (self-healing)
233-
const pythonReady = await isPythonReady();
234-
if (!pythonReady) {
235-
logger.info('Python environment missing or incomplete, attempting background repair...');
236-
// We don't await this to avoid blocking Gateway startup,
237-
// as uv run will handle it if needed, but this pre-warms it.
238-
void setupManagedPython().catch(err => {
239-
logger.error('Background Python repair failed:', err);
240-
});
241-
}
232+
// Check if Python environment is ready (self-healing) asynchronously
233+
void isPythonReady().then(pythonReady => {
234+
if (!pythonReady) {
235+
logger.info('Python environment missing or incomplete, attempting background repair...');
236+
// We don't await this to avoid blocking Gateway startup,
237+
// as uv run will handle it if needed, but this pre-warms it.
238+
void setupManagedPython().catch(err => {
239+
logger.error('Background Python repair failed:', err);
240+
});
241+
}
242+
}).catch(err => {
243+
logger.error('Failed to check Python environment:', err);
244+
});
242245

243246
// Check if Gateway is already running
244247
logger.debug('Checking for existing Gateway...');
245248
const existing = await this.findExistingGateway();
246249
if (existing) {
247250
logger.debug(`Found existing Gateway on port ${existing.port}`);
248-
await this.connect(existing.port);
251+
await this.connect(existing.port, existing.externalToken);
249252
this.ownsProcess = false;
250253
this.setStatus({ pid: undefined });
251254
this.startHealthCheck();
@@ -318,33 +321,39 @@ export class GatewayManager extends EventEmitter {
318321
// Kill process and wait for it to actually exit (so the port is released)
319322
if (this.process && this.ownsProcess) {
320323
const child = this.process;
321-
this.process = null;
322-
logger.info(`Sending SIGTERM to Gateway (pid=${child.pid ?? 'unknown'})`);
323-
child.kill('SIGTERM');
324-
325-
// Wait for the process to exit, with a SIGKILL fallback
324+
326325
await new Promise<void>((resolve) => {
327-
const killTimer = setTimeout(() => {
326+
// If process already exited, resolve immediately
327+
if (child.exitCode !== null || child.signalCode !== null) {
328+
return resolve();
329+
}
330+
331+
logger.info(`Sending SIGTERM to Gateway (pid=${child.pid ?? 'unknown'})`);
332+
child.kill('SIGTERM');
333+
334+
// Force kill after timeout
335+
const timeout = setTimeout(() => {
328336
if (child.exitCode === null && child.signalCode === null) {
329337
logger.warn(`Gateway did not exit in time, sending SIGKILL (pid=${child.pid ?? 'unknown'})`);
330-
try { child.kill('SIGKILL'); } catch { /* already dead */ }
338+
child.kill('SIGKILL');
331339
}
332-
}, 3000);
333-
334-
const done = () => {
335-
clearTimeout(killTimer);
336340
resolve();
337-
};
341+
}, 5000);
338342

339-
// If the process already exited before we got here
340-
if (child.exitCode !== null || child.signalCode !== null) {
341-
done();
342-
return;
343-
}
344-
child.once('exit', done);
345-
// Safety cap: resolve after 6s even if exit event never fires
346-
setTimeout(done, 6000);
343+
child.once('exit', () => {
344+
clearTimeout(timeout);
345+
resolve();
346+
});
347+
348+
child.once('error', () => {
349+
clearTimeout(timeout);
350+
resolve();
351+
});
347352
});
353+
354+
if (this.process === child) {
355+
this.process = null;
356+
}
348357
}
349358
this.ownsProcess = false;
350359

@@ -364,18 +373,6 @@ export class GatewayManager extends EventEmitter {
364373
async restart(): Promise<void> {
365374
logger.debug('Gateway restart requested');
366375
await this.stop();
367-
// Wait for any in-flight start() to finish unwinding after abort
368-
const maxWait = 10;
369-
for (let i = 0; i < maxWait && this.startLock; i++) {
370-
await new Promise(resolve => setTimeout(resolve, 500));
371-
}
372-
if (this.startLock) {
373-
logger.warn('Gateway restart: startLock still held after waiting, forcing release');
374-
this.startLock = false;
375-
this.startAbort = null;
376-
}
377-
// Brief delay before restart
378-
await new Promise(resolve => setTimeout(resolve, 500));
379376
await this.start();
380377
}
381378

@@ -487,11 +484,46 @@ export class GatewayManager extends EventEmitter {
487484
/**
488485
* Find existing Gateway process by attempting a WebSocket connection
489486
*/
490-
private async findExistingGateway(): Promise<{ port: number } | null> {
487+
private async findExistingGateway(): Promise<{ port: number, externalToken?: string } | null> {
491488
try {
492489
const port = PORTS.OPENCLAW_GATEWAY;
490+
491+
try {
492+
const { stdout } = await new Promise<{ stdout: string }>((resolve) => {
493+
import('child_process').then(cp => {
494+
cp.exec(`lsof -i :${port} | grep LISTEN`, (err, stdout) => {
495+
if (err) resolve({ stdout: '' });
496+
else resolve({ stdout });
497+
});
498+
});
499+
});
500+
501+
if (stdout.trim()) {
502+
// A process is listening on the port
503+
const pids = stdout.split('\n')
504+
.map(line => line.trim().split(/\s+/)[1])
505+
.filter(pid => pid && pid !== 'PID');
506+
507+
if (pids.length > 0) {
508+
// Try to kill it if it's not us to avoid connection issues
509+
// This happens frequently on HMR / dev reloads
510+
if (!this.process || !pids.includes(String(this.process.pid))) {
511+
logger.info(`Found orphaned process listening on port ${port} (PID: ${pids[0]}), attempting to kill...`);
512+
for (const pid of pids) {
513+
try { process.kill(parseInt(pid), 'SIGKILL'); } catch { /* ignore */ }
514+
}
515+
// Wait a moment for port to be released
516+
await new Promise(r => setTimeout(r, 500));
517+
return null; // Return null so we start a fresh instance
518+
}
519+
}
520+
}
521+
} catch (err) {
522+
logger.debug('Error checking for existing process on port:', err);
523+
}
524+
493525
// Try a quick WebSocket connection to check if gateway is listening
494-
return await new Promise<{ port: number } | null>((resolve) => {
526+
return await new Promise<{ port: number, externalToken?: string } | null>((resolve) => {
495527
const testWs = new WebSocket(`ws://localhost:${port}/ws`);
496528
const timeout = setTimeout(() => {
497529
testWs.close();
@@ -717,8 +749,10 @@ export class GatewayManager extends EventEmitter {
717749
/**
718750
* Wait for Gateway to be ready by checking if the port is accepting connections
719751
*/
720-
private async waitForReady(retries = 600, interval = 1000): Promise<void> {
752+
private async waitForReady(retries = 2400, interval = 250): Promise<void> {
753+
const child = this.process;
721754
for (let i = 0; i < retries; i++) {
755+
<<<<<<< HEAD
722756
// Abort if stop() was called while we are still waiting
723757
if (this.startAbort?.signal.aborted) {
724758
logger.info('waitForReady aborted by stop request');
@@ -733,6 +767,12 @@ export class GatewayManager extends EventEmitter {
733767
if (this.process.exitCode !== null || this.process.signalCode !== null) {
734768
const code = this.process.exitCode;
735769
const signal = this.process.signalCode;
770+
=======
771+
// Early exit if the gateway process has already exited
772+
if (child && (child.exitCode !== null || child.signalCode !== null)) {
773+
const code = child.exitCode;
774+
const signal = child.signalCode;
775+
>>>>>>> upstream/main
736776
logger.error(`Gateway process exited before ready (${this.formatExit(code, signal)})`);
737777
throw new Error(`Gateway process exited before becoming ready (${this.formatExit(code, signal)})`);
738778
}
@@ -779,9 +819,7 @@ export class GatewayManager extends EventEmitter {
779819
/**
780820
* Connect WebSocket to Gateway
781821
*/
782-
private async connect(port: number): Promise<void> {
783-
// Get token for WebSocket authentication
784-
const gatewayToken = await getSetting('gatewayToken');
822+
private async connect(port: number, _externalToken?: string): Promise<void> {
785823
logger.debug(`Connecting Gateway WebSocket (ws://localhost:${port}/ws)`);
786824

787825
return new Promise((resolve, reject) => {
@@ -826,6 +864,9 @@ export class GatewayManager extends EventEmitter {
826864
this.ws.on('open', async () => {
827865
logger.debug('Gateway WebSocket opened, sending connect handshake');
828866

867+
// Re-fetch token here before generating payload just in case it updated while connecting
868+
const currentToken = await getSetting('gatewayToken');
869+
829870
// Send proper connect handshake as required by OpenClaw Gateway protocol
830871
// The Gateway expects: { type: "req", id: "...", method: "connect", params: ConnectParams }
831872
// Since 2026.2.15, scopes are only granted when a signed device identity is included.
@@ -838,14 +879,15 @@ export class GatewayManager extends EventEmitter {
838879

839880
const device = (() => {
840881
if (!this.deviceIdentity) return undefined;
882+
841883
const payload = buildDeviceAuthPayload({
842884
deviceId: this.deviceIdentity.deviceId,
843885
clientId,
844886
clientMode,
845887
role,
846888
scopes,
847889
signedAtMs,
848-
token: gatewayToken ?? null,
890+
token: currentToken ?? null,
849891
});
850892
const signature = signDevicePayload(this.deviceIdentity.privateKeyPem, payload);
851893
return {
@@ -871,7 +913,7 @@ export class GatewayManager extends EventEmitter {
871913
mode: clientMode,
872914
},
873915
auth: {
874-
token: gatewayToken,
916+
token: currentToken,
875917
},
876918
caps: [],
877919
role,
@@ -1113,7 +1155,7 @@ export class GatewayManager extends EventEmitter {
11131155
// Try to find existing Gateway first
11141156
const existing = await this.findExistingGateway();
11151157
if (existing) {
1116-
await this.connect(existing.port);
1158+
await this.connect(existing.port, existing.externalToken);
11171159
this.ownsProcess = false;
11181160
this.setStatus({ pid: undefined });
11191161
this.reconnectAttempts = 0;

electron/main/ipc-handlers.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function registerIpcHandlers(
6666
registerOpenClawHandlers();
6767

6868
// Provider handlers
69-
registerProviderHandlers();
69+
registerProviderHandlers(gatewayManager);
7070

7171
// Shell handlers
7272
registerShellHandlers();
@@ -794,7 +794,7 @@ function registerWhatsAppHandlers(mainWindow: BrowserWindow): void {
794794
/**
795795
* Provider-related IPC handlers
796796
*/
797-
function registerProviderHandlers(): void {
797+
function registerProviderHandlers(gatewayManager: GatewayManager): void {
798798
// Get all providers with key info
799799
ipcMain.handle('provider:list', async () => {
800800
return await getAllProvidersWithKeyInfo();
@@ -1009,6 +1009,7 @@ function registerProviderHandlers(): void {
10091009
if (providerKey) {
10101010
saveProviderKeyToOpenClaw(provider.type, providerKey);
10111011
}
1012+
10121013
} catch (err) {
10131014
console.warn('Failed to set OpenClaw default model:', err);
10141015
}
@@ -1288,12 +1289,8 @@ async function validateGoogleQueryKey(
12881289
apiKey: string,
12891290
baseUrl?: string
12901291
): Promise<{ valid: boolean; error?: string }> {
1291-
const trimmedBaseUrl = baseUrl?.trim();
1292-
if (!trimmedBaseUrl) {
1293-
return { valid: false, error: `Base URL is required for provider "${providerType}" validation` };
1294-
}
1295-
1296-
const base = normalizeBaseUrl(trimmedBaseUrl);
1292+
// Default to the official Google Gemini API base URL if none is provided
1293+
const base = normalizeBaseUrl(baseUrl || 'https://generativelanguage.googleapis.com/v1beta');
12971294
const url = `${base}/models?pageSize=1&key=${encodeURIComponent(apiKey)}`;
12981295
return await performProviderValidationRequest(providerType, url, {});
12991296
}

electron/utils/channel-config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ export function saveChannelConfig(
172172
}
173173
}
174174

175+
// Special handling for Feishu: default to open DM policy with wildcard allowlist
176+
if (channelType === 'feishu') {
177+
const existingConfig = currentConfig.channels[channelType] || {};
178+
transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingConfig.dmPolicy ?? 'open';
179+
180+
let allowFrom = transformedConfig.allowFrom ?? existingConfig.allowFrom ?? ['*'];
181+
if (!Array.isArray(allowFrom)) {
182+
allowFrom = [allowFrom];
183+
}
184+
185+
// If dmPolicy is open, OpenClaw schema requires '*' in allowFrom
186+
if (transformedConfig.dmPolicy === 'open' && !allowFrom.includes('*')) {
187+
allowFrom = [...allowFrom, '*'];
188+
}
189+
190+
transformedConfig.allowFrom = allowFrom;
191+
}
192+
175193
// Merge with existing config
176194
currentConfig.channels[channelType] = {
177195
...currentConfig.channels[channelType],

electron/utils/openclaw-auth.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string
229229
config.agents = agents;
230230

231231
// Configure models.providers for providers that need explicit registration.
232-
// For built-in providers this comes from registry; for custom/ollama-like
233-
// providers callers can supply runtime overrides.
232+
// Built-in providers (anthropic, google) are part of OpenClaw's pi-ai catalog
233+
// and must NOT have a models.providers entry — it would override the built-in.
234234
const providerCfg = getProviderConfig(provider);
235235
if (providerCfg) {
236236
const models = (config.models || {}) as Record<string, unknown>;
@@ -246,7 +246,6 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string
246246
: [];
247247
const registryModels = (providerCfg.models ?? []).map((m) => ({ ...m })) as Array<Record<string, unknown>>;
248248

249-
// Merge model entries by id and ensure the selected/default model id exists.
250249
const mergedModels = [...registryModels];
251250
for (const item of existingModels) {
252251
const id = typeof item?.id === 'string' ? item.id : '';
@@ -262,13 +261,25 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string
262261
...existingProvider,
263262
baseUrl: providerCfg.baseUrl,
264263
api: providerCfg.api,
265-
apiKey: providerCfg.apiKeyEnv,
264+
apiKey: `\${${providerCfg.apiKeyEnv}}`,
266265
models: mergedModels,
267266
};
268267
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
269268

270269
models.providers = providers;
271270
config.models = models;
271+
} else {
272+
// Built-in provider: remove any stale models.providers entry that may
273+
// have been written by an earlier version. Leaving it in place would
274+
// override the native pi-ai catalog and can break streaming/auth.
275+
const models = (config.models || {}) as Record<string, unknown>;
276+
const providers = (models.providers || {}) as Record<string, unknown>;
277+
if (providers[provider]) {
278+
delete providers[provider];
279+
console.log(`Removed stale models.providers.${provider} (built-in provider)`);
280+
models.providers = providers;
281+
config.models = models;
282+
}
272283
}
273284

274285
// Ensure gateway mode is set
@@ -355,7 +366,7 @@ export function setOpenClawDefaultModelWithOverride(
355366
models: freshModels,
356367
};
357368
if (override.apiKeyEnv) {
358-
nextProvider.apiKey = override.apiKeyEnv;
369+
nextProvider.apiKey = `\${${override.apiKeyEnv}}`;
359370
}
360371

361372
providers[provider] = nextProvider;

electron/utils/provider-registry.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,8 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
5353
google: {
5454
envVar: 'GEMINI_API_KEY',
5555
defaultModel: 'google/gemini-3-pro-preview',
56-
providerConfig: {
57-
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
58-
api: 'google',
59-
apiKeyEnv: 'GEMINI_API_KEY',
60-
},
56+
// google is built-in to OpenClaw's pi-ai catalog, no providerConfig needed.
57+
// Adding models.providers.google overrides the built-in and can break Gemini.
6158
},
6259
openrouter: {
6360
envVar: 'OPENROUTER_API_KEY',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "clawx",
3-
"version": "0.1.15",
3+
"version": "0.1.16",
44
"pnpm": {
55
"onlyBuiltDependencies": [
66
"@whiskeysockets/baileys",

0 commit comments

Comments
 (0)