perf(gateway): skip auth overlays during startup secrets preflight#68327
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: efc32536f2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await Promise.race([prev, timeoutPromise]); | ||
| if (timedOut) { | ||
| release?.(); | ||
| throw new AsyncLockTimeoutError(timeoutMs); |
There was a problem hiding this comment.
Ensure timeout path releases async lock
When a waiter hits the timeout, await Promise.race([prev, timeoutPromise]) rejects immediately, so execution never reaches the if (timedOut) { release?.(); ... } block. That leaves this call's lock promise unresolved, which wedges the queue: every later withLock(...) call will wait on an unreleased lock and then time out as well, even if the original holder eventually finishes. Please release the lock in the rejection path (e.g., via try/catch/finally) so a single timeout cannot permanently poison the lock.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR reduces gateway startup latency by ~36s through several parallel improvements: skipping plugin loading during secrets resolution, parallelizing channel maintenance and session migration, disabling bonjour on Windows, and adding a 30s async lock timeout. The lock timeout implementation in Confidence Score: 2/5Not safe to merge — the discovery refactor breaks the TypeScript build and silently disables mDNS on all platforms. A P0 finding: src/gateway/server-discovery-runtime.ts — incomplete plugin refactor with missing type definition and no caller population of the new services parameter. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/gateway/server-discovery-runtime.ts
Line: 4-49
Comment:
**Incomplete plugin-discovery refactor: type missing + caller never populates services**
`PluginGatewayDiscoveryServiceRegistration` is imported on line 4 but is not exported from `src/plugins/registry-types.ts` (or anywhere else in the codebase), causing a TypeScript build error.
Even if the type were added, mDNS/Bonjour would still silently never start: the only caller — `startGatewayEarlyRuntime` in `server-startup-early.ts` (line 59) — doesn't pass `gatewayDiscoveryServices`, so the `params.gatewayDiscoveryServices ?? []` loop on line 49 is always empty. `stops` stays empty, `bonjourStop` stays `null`, and no service is advertised on any platform.
To complete this migration you need to:
1. Export `PluginGatewayDiscoveryServiceRegistration` (and the matching `GatewayDiscoveryService` interface with `advertise`) from `registry-types.ts`
2. Register the existing bonjour advertiser (`startGatewayBonjourAdvertiser`) as a discovery service in the plugin registry
3. Pass the populated `gatewayDiscoveryServices` list when calling `startGatewayDiscovery` from `startGatewayEarlyRuntime`
How can I resolve this? If you propose a fix, please make it concise.Reviews (5): Last reviewed commit: "fix: address Greptile review feedback (a..." | Re-trigger Greptile |
CI 检查失败说明4 个失败的检查项与本次修改无关:
通过的关键检查:
本次修改仅涉及 4 个文件,改动很小且都是性能优化:
请维护者重新运行失败的检查,确认是 CI 环境问题而非代码问题。 |
更新:修复 Windows 路径空格问题之前提交的 ui.js 修复不够完整。 最终修复方案:使用命令 basename(如 // 当路径包含空格时
const spawnCmd = path.basename(cmd); // "pnpm.CMD"
spawn(spawnCmd, args, { shell: true, cwd: uiDir });验证结果: 完整修改清单
|
Summary of ChangesThis PR addresses several performance and stability issues on Windows: 1. Async Lock Timeout (
|
| Metric | Before | After (est.) |
|---|---|---|
| Startup time | 137s | ~100-110s |
| Memory growth | Continuous leak | Stable |
| TUI handshake | 30-74s | 5-15s (normal) |
| Lock timeout risk | Infinite wait | Max 30s |
| UI build on Windows | Fails with spaces | Works |
Note on CI Failures
The 4 failing checks (checks-node-core-fast, checks-node-agentic-control-plane, checks-windows-node-test, checks-node-core) appear to be CI infrastructure issues unrelated to this PR:
- All are test timeout/failure issues
- No test code was modified in this PR
build-smoke,build-artifacts, andGreptile Reviewall passed- Request: please re-run the failed checks
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3378dc7eb0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const spawnOpts = useQuotedShell | ||
| ? { cwd: uiDir, stdio: "inherit", env: process.env, shell: true } | ||
| : createSpawnOptions(cmd, args); |
There was a problem hiding this comment.
Reapply arg sanitizer before quoted shell spawn
When useQuotedShell is true, this branch sets shell: true but bypasses createSpawnOptions, so assertSafeWindowsShellArgs is never run. Because main() forwards user CLI args directly into args, Windows metacharacters (for example &/%) can now be interpreted by cmd.exe instead of being rejected, which reopens the command-injection/control-flow risk this script previously guarded against.
Useful? React with 👍 / 👎.
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| setTimeout(() => { | ||
| timedOut = true; | ||
| reject(new AsyncLockTimeoutError(timeoutMs)); | ||
| }, timeoutMs); |
There was a problem hiding this comment.
Clear async-lock timeout timer after race settles
A timeout is started for every withLock call, but the timer handle is never cleared when prev wins the race. This leaves a pending 30s timer after successful acquisitions, which can accumulate under load and can keep short-lived processes alive until timers fire. Capture the timeout handle and clearTimeout it once the wait finishes.
Useful? React with 👍 / 👎.
|
CI is improving — |
e30affb to
17059a2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17059a20d8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (!agentDir || authPath === mainAuthPath) { | ||
| return store; | ||
| } |
There was a problem hiding this comment.
Preserve external auth overlays in secrets-runtime loader
loadAuthProfileStoreForSecretsRuntime now returns the raw persisted store for the main-agent path, which drops overlayExternalAuthProfiles entries. That regression affects non-secrets callers too: OAuth refresh/recovery paths in src/agents/auth-profiles/oauth.ts call this loader and then immediately bail when store.profiles[profileId] is missing, so runtime-only external CLI profiles (for example Codex/Claude CLI-provided OAuth) can no longer be recovered when local persisted credentials are missing or stale.
Useful? React with 👍 / 👎.
| const bonjourEnabled = | ||
| mdnsMode !== "off" && | ||
| process.env.OPENCLAW_DISABLE_BONJOUR !== "1" && | ||
| process.env.NODE_ENV !== "test" && | ||
| !process.env.VITEST; | ||
| !process.env.VITEST && | ||
| process.platform !== "win32"; |
There was a problem hiding this comment.
Honor explicit mDNS mode on Windows
The new process.platform !== "win32" gate makes bonjourEnabled false on every Windows run, even when users explicitly set discovery.mdns.mode to minimal or full. This is a behavior regression from a configurable feature to a hard disable, and there is no corresponding opt-in override path in this function, so Windows users cannot re-enable mDNS discovery despite configuration intent.
Useful? React with 👍 / 👎.
|
@greptile-apps All issues addressed in the latest commit:
Please re-trigger the review. |
| @@ -211,6 +212,8 @@ export class GatewayClient { | |||
| if (this.closed) { | |||
| return; | |||
| } | |||
| this._connectStartTs = Date.now(); | |||
| console.error(`[client-profile] WebSocket connecting to ${this.opts.url ?? "ws://127.0.0.1:18789"}...`); | |||
There was a problem hiding this comment.
Debug profiling logs left in production code
These [client-profile] console.error calls appear to be temporary diagnostic instrumentation added to measure startup latency but not removed before merging. They will emit to stderr on every WebSocket connect in production. The same pattern appears in src/gateway/server.impl.ts ([startup-profile]) and src/tui/gateway-chat.ts ([tui-profile]). If these logs are intentional, they should route through the structured log object and be gated behind a debug flag rather than unconditionally written to stderr.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/client.ts
Line: 216
Comment:
**Debug profiling logs left in production code**
These `[client-profile]` `console.error` calls appear to be temporary diagnostic instrumentation added to measure startup latency but not removed before merging. They will emit to stderr on every WebSocket connect in production. The same pattern appears in `src/gateway/server.impl.ts` (`[startup-profile]`) and `src/tui/gateway-chat.ts` (`[tui-profile]`). If these logs are intentional, they should route through the structured `log` object and be gated behind a debug flag rather than unconditionally written to stderr.
How can I resolve this? If you propose a fix, please make it concise.|
@greptile-apps Pushed additional fixes on top of previous commit:
Please re-review. |
|
@greptile-apps Latest commits pushed. Key fixes since last review:
Please re-review the latest HEAD. |
|
@greptile-apps Fixes have been pushed to the correct branch (perf/startup-latency-fixes, commit 95d6596259):
Please re-review the latest HEAD. |
| @@ -17,43 +18,66 @@ export async function startGatewayDiscovery(params: { | |||
| tailscaleMode: "off" | "serve" | "funnel"; | |||
| /** mDNS/Bonjour discovery mode (default: minimal). */ | |||
| mdnsMode?: "off" | "minimal" | "full"; | |||
| gatewayDiscoveryServices?: readonly PluginGatewayDiscoveryServiceRegistration[]; | |||
| logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void }; | |||
| }) { | |||
| let bonjourStop: (() => Promise<void>) | null = null; | |||
| const mdnsMode = params.mdnsMode ?? "minimal"; | |||
| // mDNS can be disabled via config (mdnsMode: off) or env var. | |||
| // On Windows, bonjour is disabled by default due to stability issues with @homebridge/ciao | |||
| // causing memory leaks from repeated watchdog restarts when services fail to announce. | |||
| const bonjourEnabled = | |||
| mdnsMode !== "off" && | |||
| process.env.OPENCLAW_DISABLE_BONJOUR !== "1" && | |||
| !isTruthyEnvValue(process.env.OPENCLAW_DISABLE_BONJOUR) && | |||
| (process.env.OPENCLAW_ENABLE_BONJOUR === "1" || process.platform !== "win32") && | |||
| process.env.NODE_ENV !== "test" && | |||
| !process.env.VITEST; | |||
| const localDiscoveryEnabled = bonjourEnabled; | |||
| const mdnsMinimal = mdnsMode !== "full"; | |||
| const tailscaleEnabled = params.tailscaleMode !== "off"; | |||
| const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled; | |||
| const needsTailnetDns = localDiscoveryEnabled || params.wideAreaDiscoveryEnabled; | |||
| const tailnetDns = needsTailnetDns | |||
| ? await resolveTailnetDnsHint({ enabled: tailscaleEnabled }) | |||
| : undefined; | |||
| const sshPortEnv = mdnsMinimal ? undefined : process.env.OPENCLAW_SSH_PORT?.trim(); | |||
| const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN; | |||
| const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : Number.NaN; | |||
| const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined; | |||
| const cliPath = mdnsMinimal ? undefined : resolveBonjourCliPath(); | |||
|
|
|||
| if (bonjourEnabled) { | |||
| try { | |||
| const bonjour = await startGatewayBonjourAdvertiser({ | |||
| instanceName: formatBonjourInstanceName(params.machineDisplayName), | |||
| gatewayPort: params.port, | |||
| gatewayTlsEnabled: params.gatewayTls?.enabled ?? false, | |||
| gatewayTlsFingerprintSha256: params.gatewayTls?.fingerprintSha256, | |||
| canvasPort: params.canvasPort, | |||
| sshPort, | |||
| tailnetDns, | |||
| cliPath, | |||
| minimal: mdnsMinimal, | |||
| }); | |||
| bonjourStop = bonjour.stop; | |||
| } catch (err) { | |||
| params.logDiscovery.warn(`bonjour advertising failed: ${String(err)}`); | |||
| if (localDiscoveryEnabled) { | |||
| const stops: Array<() => void | Promise<void>> = []; | |||
| for (const entry of params.gatewayDiscoveryServices ?? []) { | |||
There was a problem hiding this comment.
Incomplete plugin-discovery refactor: type missing + caller never populates services
PluginGatewayDiscoveryServiceRegistration is imported on line 4 but is not exported from src/plugins/registry-types.ts (or anywhere else in the codebase), causing a TypeScript build error.
Even if the type were added, mDNS/Bonjour would still silently never start: the only caller — startGatewayEarlyRuntime in server-startup-early.ts (line 59) — doesn't pass gatewayDiscoveryServices, so the params.gatewayDiscoveryServices ?? [] loop on line 49 is always empty. stops stays empty, bonjourStop stays null, and no service is advertised on any platform.
To complete this migration you need to:
- Export
PluginGatewayDiscoveryServiceRegistration(and the matchingGatewayDiscoveryServiceinterface withadvertise) fromregistry-types.ts - Register the existing bonjour advertiser (
startGatewayBonjourAdvertiser) as a discovery service in the plugin registry - Pass the populated
gatewayDiscoveryServiceslist when callingstartGatewayDiscoveryfromstartGatewayEarlyRuntime
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-discovery-runtime.ts
Line: 4-49
Comment:
**Incomplete plugin-discovery refactor: type missing + caller never populates services**
`PluginGatewayDiscoveryServiceRegistration` is imported on line 4 but is not exported from `src/plugins/registry-types.ts` (or anywhere else in the codebase), causing a TypeScript build error.
Even if the type were added, mDNS/Bonjour would still silently never start: the only caller — `startGatewayEarlyRuntime` in `server-startup-early.ts` (line 59) — doesn't pass `gatewayDiscoveryServices`, so the `params.gatewayDiscoveryServices ?? []` loop on line 49 is always empty. `stops` stays empty, `bonjourStop` stays `null`, and no service is advertised on any platform.
To complete this migration you need to:
1. Export `PluginGatewayDiscoveryServiceRegistration` (and the matching `GatewayDiscoveryService` interface with `advertise`) from `registry-types.ts`
2. Register the existing bonjour advertiser (`startGatewayBonjourAdvertiser`) as a discovery service in the plugin registry
3. Pass the populated `gatewayDiscoveryServices` list when calling `startGatewayDiscovery` from `startGatewayEarlyRuntime`
How can I resolve this? If you propose a fix, please make it concise.|
@greptile-apps The two P0 findings in are pre-existing issues in the upstream codebase, not introduced by this PR:
This PR's scope is limited to performance fixes (async lock timeout, Windows bonjour guard, startup parallelization, ui.js spawn fix). The discovery-service plugin refactor is incomplete upstream on the branch base — please consider this a pre-existing condition outside this PR's scope. |
|
Fair point — if both issues predate this PR and aren't touched by the changes here, they're out of scope for this review. I'll mark those findings as resolved. The performance changes themselves (async lock timeout, Windows bonjour guard, startup parallelization, spawn fix) look reasonable for the goals described. Tip: You can give me more context with .greptile/rules so I get this right next time. |
|
Field note from OpenClaw After setting
This makes the Bonjour collision a separate problem from the remaining startup delay, at least on this machine. |
|
Codex review: needs maintainer review before merge. Summary Reproducibility: yes. for the review surface: local current-main source shows the startup activator omits a Next step before merge Security Review detailsBest possible solution: Merge the scoped auth-overlay startup optimization after the targeted secrets tests and Testbox changed gate pass, while leaving broader startup latency work to the related gateway performance PRs. Do we have a high-confidence way to reproduce the issue? Yes for the review surface: local current-main source shows the startup activator omits a Is this the best way to solve the issue? Yes. The caller-side override is the narrowest maintainable change because it uses the existing What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against a55b2af7a5d3. |
95d6596 to
0ee6fa2
Compare
0ee6fa2 to
8aa4612
Compare
Summary
Test plan
pnpm test src/gateway/server-startup-config.secrets.test.ts src/secrets/runtime.fast-path.test.tsOPENCLAW_TESTBOX=1 pnpm check:changed