Repository navigation
Replies: 5 comments
First-Round Diagnostic Findings & First-Principles RecommendationsFull code audit of The core problem: "add more time" is an anti-patternThe codebase has accumulated 14+ explicit The fundamental issue isn't that timeouts are too short or too long — it's that most of these waits shouldn't be time-based at all. Inventory: What's actually waiting, and whyCategory A: Sleeps papering over race conditions (~7 instances)These
First-principles fix: Replace every one of these with a readiness probe on the actual resource:
Category B: Polling loops with fixed intervals (~6 loops)These are better than blind sleeps (they at least check a condition), but they use fixed intervals that waste time on fast systems and can still timeout on slow ones:
First-principles fix: Deadline-based polling with adaptive backoff. Instead of async function waitUntil(check: () => boolean, opts: {
deadline: number, // absolute wall-clock deadline
initialInterval: number, // start fast (e.g. 200ms)
maxInterval: number, // cap backoff (e.g. 5s)
factor: number, // e.g. 1.5
}): Promise<boolean> {
let interval = opts.initialInterval;
while (Date.now() < opts.deadline) {
if (check()) return true;
await sleep(Math.min(interval, opts.deadline - Date.now()));
interval = Math.min(interval * opts.factor, opts.maxInterval);
}
return false;
}Benefits:
Category C: Validation probes with no connection reuseEvery inference validation probe spawns a fresh
The probe sequence in On WSL2, where TLS handshakes are already slow enough to require 20s connect timeouts, this compounds badly. First-principles fix options (in order of impact/difficulty):
Category D: Sequential orchestration where parallelism is safeThe onboard flow is almost entirely sequential: Some of these have genuine dependencies. But some don't:
The interactive prompts (provider selection, model selection) are dead time from the system's perspective. Use it. Platform-specific timeout widening: the symptom, not the diseaseThe WSL2 timeout doubling (
First-principles alternative: adaptive timeouts.
This eliminates every The
|
| File | Lines | What |
|---|---|---|
src/lib/onboard.ts |
2097-2099 | sleep() implementation (spawnSync) |
src/lib/onboard.ts |
1259-1264 | getValidationProbeCurlArgs() — WSL2 timeout doubling |
src/lib/onboard.ts |
2596-2612 | Gateway health polling loop (ARM64 branching) |
src/lib/onboard.ts |
3346-3395 | Sandbox + dashboard readiness polling |
src/lib/onboard.ts |
1322-1509 | probeOpenAiLikeEndpoint() — sequential probe chain |
src/lib/onboard.ts |
1453-1480 | Retry with doubled timeouts on failure |
src/lib/onboard.ts |
1791, 1807, 1832 | Ollama proxy sleep(1) calls |
src/lib/onboard.ts |
3988, 4056 | Ollama serve sleep(2) calls |
src/lib/http-probe.ts |
94-155 | runCurlProbe() — subprocess-per-probe |
src/lib/local-inference.ts |
126-154 | Local provider health check |
src/lib/registry.ts |
104 | Atomics.wait() — better sleep pattern |
src/lib/platform.ts |
8-29 | isWsl() detection |
Summary
The onboard/validation latency isn't primarily a networking problem — it's a waiting strategy problem. The network is as fast as it is; the code just doesn't know when to stop waiting. Replacing guessed durations with observed readiness, reusing connections instead of re-establishing them, and running independent checks in parallel would cut worst-case onboard time significantly without adding a single timeout.
Design: Systemic Latency EliminationBased on the diagnostic findings above, here's the proposed design to fix this systemically rather than continuing to widen timeouts. New Module 1:
|
| Current code | Replacement |
|---|---|
sleep(1) after proxy spawn (×3) |
waitForPort(PROXY_PORT, "127.0.0.1", {deadlineMs: now+10_000}) |
sleep(2) after ollama serve (×2) |
waitForHttp("http://127.0.0.1:11434/api/tags", {deadlineMs: now+15_000}) |
sleep(5) after CoreDNS patch |
waitUntil(corednsReady, {deadlineMs: now+15_000}) |
| Gateway health: 5×2s / 30×10s | waitUntil(isGatewayHealthy, {deadlineMs: now+120_000}) |
| Sandbox ready: 30×2s | waitUntil(isSandboxReady, {deadlineMs: now+120_000}) |
| Dashboard ready: 15×2s | waitForHttp(dashboardUrl, {deadlineMs: now+60_000}) |
| Gateway recovery: 10×2s | waitUntil(isConnected, {deadlineMs: now+30_000}) |
| Policy preset retries: 3×sleep(2) | waitUntil(presetApplySucceeds, {deadlineMs: now+30_000}) |
New Module 2: src/lib/net-calibrate.ts — Adaptive Timeout Calibration
Replaces all isWsl() and isArm64 timeout branches with measured network conditions.
interface NetworkProfile {
dnsMs: number; // measured DNS resolution time
connectMs: number; // measured TCP connect time
tlsMs: number; // measured TLS handshake time
firstByteMs: number; // total first-byte latency
platform: string; // for logging only, not branching
connectTimeoutMs: number; // recommended: max(5000, connectMs * 10)
requestTimeoutMs: number; // recommended: max(15000, firstByteMs * 5)
pollDeadlineMs: number; // recommended: max(30000, firstByteMs * 20)
}
function calibrate(endpointUrl: string): NetworkProfile;
function defaultProfile(): NetworkProfile; // fallback if calibration failsHow it works: Single curl --write-out '%{time_namelookup} %{time_connect} %{time_appconnect}' call. Extracts DNS, TCP, TLS timings. Scales all subsequent timeouts as multiples of observed latency.
What it eliminates:
getValidationProbeCurlArgs()with itsisWsl()branch (onboard.ts:1259-1264)isArm64poll count/interval branches (onboard.ts:2596-2599)- Retry-with-doubled-timeouts hack (onboard.ts:1453-1480)
NEMOCLAW_HEALTH_POLL_COUNT/NEMOCLAW_HEALTH_POLL_INTERVALenv vars- The WSL2 hint message (onboard.ts:1499-1503)
Calibration timing: Runs once after gateway health confirmed, before validation probes. ~1-3s.
New Module 3: src/lib/probe-pool.ts — Connection-Reused Probes
interface ProbePool {
preResolve(hostname: string): string | null;
probe(argv: string[], url: string, opts?: CurlProbeOptions): CurlProbeResult;
streamingProbe(argv: string[], url: string, opts?: CurlProbeOptions): StreamingProbeResult;
getTimingArgs(): string[];
}
function createProbePool(opts?: { resolveMap?: Map<string,string>; profile?: NetworkProfile }): ProbePool;How it works:
- On first probe for a hostname, resolves DNS and caches the IP
- Injects
--resolve hostname:port:ipinto all subsequent curl calls - Uses
NetworkProfilefor timeout values instead of hardcoded constants - Backward compatible:
runCurlProbe()gets optionalpoolparameter
Impact: 4 sequential probes to same endpoint → DNS resolved once, not 4×. Critical on WSL2 where DNS goes through the Windows resolver.
New Module 4: src/lib/onboard-timing.ts — Instrumentation
function createTimer(): {
start(label: string): void;
end(label: string): void;
report(): OnboardTimingReport;
};Wraps each onboard step. Prints summary on completion:
Onboard completed in 47s
preflight 3.2s
gateway 12.4s
provider 8.1s (user prompt)
inference 2.3s
sandbox 11.8s
openclaw 1.1s
policies 2.1s
Network: dns=45ms tcp=12ms tls=89ms
Controlled by NEMOCLAW_TIMING=1 (off by default, always on in CI).
Orchestration Changes in src/lib/onboard.ts
Parallel step execution:
CURRENT: [Preflight] → [Gateway] → [Provider] → [Inference] → [Messaging] → [Sandbox] → ...
PROPOSED: [Preflight] → [Gateway start ←―――――――――――――――――――――――→]
[Provider + Messaging prompts (parallel)] → [Inference] → [Sandbox] → ...
↑ DNS pre-resolve in background ↑
- Gateway start runs as async background operation while user answers prompts
- DNS pre-resolution happens during user prompt dead time (free parallelism)
- Sandbox-ready and dashboard-ready polls run in parallel (saves up to 30s)
Phased PR Plan
Each PR is independently shippable and testable:
PR 1: wait.ts foundation + replace sleep() primitive
- New:
src/lib/wait.ts,test/wait.test.js - Swap
spawnSync("sleep")→sleepMs()import (behavioral no-op, better implementation) - Risk: Low — same behavior, no subprocess spawned
PR 2: Replace race-condition sleeps with readiness probes
sleep(1)after proxy →waitForPort()sleep(2)after Ollama →waitForHttp(ollamaHealthUrl)sleep(5)after CoreDNS →waitUntil(corednsReady)- Policy preset retries →
waitUntil(presetApplySucceeds) - Risk: Low-medium — each is a strict improvement; test on WSL2 + ARM64
PR 3: Replace polling loops with deadline-based waits
- All 6 polling loops →
waitUntil()with deadlines - Delete
isArm64poll count/interval branches - Delete
NEMOCLAW_HEALTH_POLL_COUNT/NEMOCLAW_HEALTH_POLL_INTERVALenv vars - Risk: Medium — changes observable timing; needs E2E validation
PR 4: Network calibration + adaptive timeouts
- New:
src/lib/net-calibrate.ts,test/net-calibrate.test.js - Delete
getValidationProbeCurlArgs()and itsisWsl()branch - Delete retry-with-doubled-timeouts hack
- Risk: Medium — changes timeout values for all platforms; needs WSL2 + ARM64 testing
PR 5: Probe connection pooling
- New:
src/lib/probe-pool.ts,test/probe-pool.test.js - Add optional
poolparameter torunCurlProbe()/runStreamingEventProbe() - DNS pre-resolution +
--resolveinjection - Risk: Low — additive, backward compatible
PR 6: Parallel orchestration
- Gateway start concurrent with user prompts
- Sandbox-ready ∥ dashboard-ready
- DNS pre-resolution during prompt dead time
- Risk: Medium-high — changes control flow; needs thorough E2E
PR 7: Timing instrumentation
- New:
src/lib/onboard-timing.ts - Wrap each step, print summary behind
NEMOCLAW_TIMING=1 - CI job with wall-clock performance budget
- Risk: Low — observability only
What Gets Deleted
| Code | Location | Replaced by |
|---|---|---|
spawnSync("sleep", [seconds]) implementation |
onboard.ts:2097, agent-onboard.ts:102 | sleepMs() via Atomics.wait |
sleep(1) after proxy (×3) |
onboard.ts:1791, 1807, 1832 | waitForPort() |
sleep(2) after ollama serve (×2) |
onboard.ts:3988, 4056 | waitForHttp() |
sleep(5) after CoreDNS |
onboard.ts:2673 | waitUntil(corednsReady) |
sleep(2) in preset retries (×3 loops) |
onboard.ts:4680-5340 | waitUntil(presetApplySucceeds) |
getValidationProbeCurlArgs() + isWsl branch |
onboard.ts:1259-1264 | pool.getTimingArgs() from calibration |
| Retry-with-doubled-timeouts hack | onboard.ts:1453-1480 | Calibrated timeouts |
isArm64 poll count/interval branches |
onboard.ts:2596-2599 | Deadline-based waitUntil() |
NEMOCLAW_HEALTH_POLL_COUNT env var |
onboard.ts:2598 | Deadline parameter |
NEMOCLAW_HEALTH_POLL_INTERVAL env var |
onboard.ts:2599 | Adaptive backoff |
retriedAfterTimeout + WSL hint |
onboard.ts:1457, 1499-1503 | Calibrated timeouts |
| Gateway health for-loop | onboard.ts:2600-2612 | waitUntil(isGatewayHealthy, {deadlineMs}) |
| Sandbox ready for-loop | onboard.ts:3352-3358 | waitUntil(isSandboxReady, {deadlineMs}) |
| Dashboard ready for-loop | onboard.ts:3381-3393 | waitForHttp(dashboardUrl, {deadlineMs}) |
| Recovery poll for-loop | onboard.ts:2726-2740 | waitUntil(isConnected, {deadlineMs}) |
Total: ~150 lines of dead code eliminated, replaced by ~200 lines of framework + ~50 lines of integration.
Regression Risk AnalysisDeep analysis of every external surface the design plan touches. Organized by risk level. RISK: HIGH — Tests that will break and must be rewritten1.
|
| PR | Regression | Mitigation | Effort |
|---|---|---|---|
| PR 1 | None | N/A | — |
| PR 2 | Proxy/Ollama may not bind port in time on very slow systems | Set generous deadline (10-15s), test on slow hardware | Low |
| PR 3 | test/onboard.test.ts breaks (HEALTH_POLL_COUNT=0) |
Add NEMOCLAW_HEALTH_DEADLINE_MS env var; update test |
Medium |
| PR 3 | test/cli.test.ts breaks (both env vars) |
Update runWithEnv() to use new env var |
Low |
| PR 3 | External consumers using HEALTH_POLL env vars | Deprecated alias: convert count×interval to deadline, log warning | Medium |
| PR 3 | "within 60s" error message becomes inaccurate | Parameterize with actual deadline value | Low |
| PR 4 | test/wsl2-probe-timeout.test.ts — entire file |
Rewrite: test calibrate() and probe-pool instead | High |
| PR 4 | getValidationProbeCurlArgs export removed |
Remove from exports, update any external importers | Low |
| PR 5 | None (additive, backward compatible) | N/A | — |
| PR 6 | Resume message ordering may change under parallelism | Test that resume skip messages still appear correctly | Medium |
| PR 6 | Race conditions if gateway fails while prompts are active | Cancel prompt + clean up on gateway failure signal | Medium |
| PR 7 | None (observability only) | N/A | — |
Critical finding: PR 6 (parallel orchestration) has a hidden race condition
If gateway startup fails while the user is in the middle of answering provider selection prompts, the current sequential flow handles this cleanly (gateway failure happens before prompts start). With parallel execution:
- Gateway fails → but user is mid-prompt → need to cancel the prompt and surface the error
- Gateway succeeds slowly → provider selection completes first → inference setup must wait for gateway → need a
Promiseto coordinate - User cancels (Ctrl+C) during prompts → need to abort the background gateway start → AbortSignal needed
Recommendation: PR 6 should include explicit cancellation handling via AbortController. The waitUntil framework already supports signal?: AbortSignal for this reason. Gateway failure should propagate as an abort signal to any concurrent operations.
|
Hey @ericksoa, I got out a PR to refactor the NemoClaw CLI to replace inefficient and platform-dependent spawnSync("sleep") subprocess calls with a native Node.js wait utility. In the roadmap outlined above, this corresponds to PR1. Low risk. |
|
Cross-link audit note: several pieces of this latency plan have already landed or are in-flight, but were easy to miss from the issue thread: Landed:
Still open / in-flight:
Suggested bookkeeping: update the checklist in this issue to mark the sleep primitive and race-condition sleep pieces as done, then decide whether #2492 should be rebased or superseded by the newer wait utility structure. |
Uh oh!
There was an error while loading. Please reload this page.
Problem
Multiple PRs have independently worked around slow networking by bumping timeouts, adding retries, and inserting sleep delays. Each fix addresses a symptom, but the cumulative effect is a fragile onboard experience that breaks on slower hardware and adds minutes of wall-clock time on every platform.
Examples:
sleep()calls scattered across onboard, recovery, and validation pathsAs @brandonpelfrey noted in #1998: "Across multiple PRs from different folks I'm seeing things like add 10 seconds here and there so $thing doesn't time out. Want to make sure we're questioning why networking seems to be so fiddly." These timeout bumps are fragile — they work on the machines that were tested but will break on slower hardware.
Scope
This is a diagnostic and optimization effort, not a single bug fix. The goal is to understand why networking is slow and fix root causes rather than continuing to widen timeouts.
Phase 1: Diagnose
host.openshell.internalresolution is slow on WSL2 (goes through Windows DNS?)sleep()calls — which are covering real async settling vs. papering over race conditions?Phase 2: Optimize
Based on diagnosis, potential fixes (non-exhaustive):
curlper attempt — a persistent connection (or at least keepalive) would skip repeated TCP+TLS handshakessleep(2)calls are waiting for a process or pod state — replace withkubectl wait, readiness probes, or file watches where possiblePhase 3: Harden
Current timeout inventory
onboard.ts)http-probe.ts)local-inference.ts)nemoclaw.ts)References
All reactions