perf(cli): answer --help without booting the engine, draw the startup spinner first - #1758
Merged
Merged
Conversation
`--help` builds the whole runtime before printing: migrations, settings, ModelRuntime (models.json + models-store.json + availability), the full resource load, and an AgentSession. Measured on this host: 790ms warm on bun, 959ms on node, 8.8-13.6s cold; the reporter of code-yeongyu/oh-my-openagent#8371 measured 47.8s on Windows. The probe extension appends one line per factory invocation, so the line count is the number of times extensions were loaded - the contract is pinned by observable side effects, never by timings. Refs code-yeongyu/oh-my-openagent#8371
Help needs the static usage text plus the flags extensions registered, yet main.ts reached printHelp only after building the whole runtime: ModelRuntime (models.json + models-store.json + availability), a session manager, the full resource load and an AgentSession. Measured on this host: 790ms warm on bun, 959ms on node, 8.8-13.6s cold; the reporter of code-yeongyu/oh-my-openagent#8371 measured 47.8s on Windows. Two layers: - main.ts resolves a plain --help from a flags-only extension load (no skills, prompt templates, themes, context files, models or session) and records the flags in <agentDir>/cache/help-flags.json. Every full launch refreshes that record too, so it stays warm without a help run of its own. A help screen never prompts for project trust and never runs project-local extension code the user has not already trusted. - cli.ts answers the next --help from that cache before the engine module graph is imported: 26-28ms on this host, byte-identical output. The cache is validated by mtime/size stamps of every discovery input (extension files and directories, settings, trust.json, the CLI --extension paths) and by the engine version, so an upgrade, an added/changed extension, a settings edit or a trust decision rejects it and the normal path refills it. Refs code-yeongyu/oh-my-openagent#8371
…gent The inspector case used --help as its probe. A cached --help is now answered before the isolation decision - no agent runs for a help screen, so there is no debugger socket to hand over - which left that case asserting a second process that correctly never appears. Refs code-yeongyu/oh-my-openagent#8371
…ork it covers On a real pty the first spinner byte arrived at 2.27s, one frame before the TUI took over: the 120ms grace timer cannot fire while the synchronous extension imports it was meant to announce are running, so the indicator drew only after they finished. The first frame must be written in start() and setPhase() must render before any timer fires. Refs code-yeongyu/oh-my-openagent#8371
The grace delay kept fast startups flash-free, but the work the spinner covers is synchronous module loading, which starves every timer until it is done: measured on a real pty, the first frame landed at 2.27s, after the whole extension load ran on a blank terminal. start() now writes the first frame itself and the grace delay gates the animation only; resume() redraws the same way. Refs code-yeongyu/oh-my-openagent#8371
This was referenced Sep 16, 2026
code-yeongyu
enabled auto-merge
September 16, 2026 08:08
code-yeongyu
added a commit
that referenced
this pull request
Sep 16, 2026
… path dist/cli.js is the one tracked file under dist/ (a bun global install needs it before any build runs). #1758 changed src/cli.ts, so the release job's build rewrote it and scripts/release.mjs refused to run on a dirty tree (run 35072616238). Same convention as f37f589: the rebuilt file ships with the source change. Refs code-yeongyu/oh-my-openagent#8371
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
--helpbooted the whole engine before it could print one line: migrations, the settings manager,ModelRuntime.create()(models.json + models-store.json + availability), the full resource load (extensions plus skills, prompt templates, themes, context files) and anAgentSession. The reporter of code-yeongyu/oh-my-openagent#8371 measured 47.8s foromo --helpon Windows (node 24); on an Apple M4 Pro the same boot costs 790ms warm on bun, 959ms on node, and 8.8-13.6s with a cold cache.Two layers, both in
packages/coding-agent:main.ts. A plain--helpstops right after the CLI paths are resolved: extensions are loaded for their registered flags only (cli/help-extension-flags.ts: aDefaultResourceLoaderwith skills, prompt templates, themes and context files disabled - noModelRuntime, noSessionManager, noAgentSession), help is printed, and the flag set is recorded in<agentDir>/cache/help-flags.json. Every full launch refreshes that record from its own loaded extensions, so the cache stays warm without a help run.cli.ts. The next plain--helpis answered from that cache beforecli-main(the engine graph) is imported. The cache is validated by the engine version and by mtime/size stamps of every discovery input - each loaded extension file and its directory, the user/projectextensionsdirectories,settings.json,trust.json, the--extensionpaths - so an upgrade, an added or edited extension, a settings edit or a trust decision rejects the entry and the normal path refills it. Output is byte-identical to the cold path.Project trust for a help screen is override → recorded
trust.jsondecision → trusted only when the project carries nothing trust-requiring. It never prompts and never loads project-local extension code the user has not already trusted (regression case included).Also fixed on the way, same root cause family (work that blocks the terminal): the startup spinner's first frame waited on a 120ms timer that the synchronous extension imports starve, so on a real pty it appeared at 2.27s - after the entire load, one frame before the TUI.
start()now draws the first frame itself; the grace delay gates the animation only.Measurements (Apple M4 Pro, bun 1.4.2 / node v26.7, hyperfine N=10 warm,
--shell=none)--helpwith the omo plugin, bun, cache hitomo --helpthrough the installed launcher (beta.65), bunThe phase attribution behind these numbers (PI_TIMING tables, module-graph import costs, CPU profiles) is recorded in the tracking issue.
Tests
test/suite/regressions/help-fast-path.test.ts(new, 7 cases; RED at 6e6dcd6 → GREEN at d080d82 on a clean worktree with a real install+build): first help loads extensions once and never constructs the model runtime, leaves the project directory untouched; second help is byte-identical with no extension load; changed/added extension, changed settings each invalidate; untrusted project extension never loaded nor listed;--no-extensionsloads nothing.test/startup-loading-indicator.test.ts: grace-delay cases flipped to the synchronous first frame (RED at 77967cf → GREEN at 50a891a).test/cli-inprocess-fast-path.test.ts: the inspector case used--helpas its probe; a cached help is now answered before the isolation decision (no agent runs, no socket to hand over), so it probes with a launch that reaches the agent.Follow-ups (outside this PR)
<cwd>/.omo/senpi-task/**at factory time, so even a flags-only help touches the project directory; that moves to first use on the omo side.Refs code-yeongyu/oh-my-openagent#8371
Summary by cubic
Answers
--helpfrom a cached flag set instead of booting the whole engine, and draws the startup spinner's first frame immediately. A plain--helpused to build migrations, the model runtime, the full resource load, and an agent session before printing; it now prints byte-identical output from<agentDir>/cache/help-flags.json(refreshed by every full launch), cutting warm--helpfrom 790ms to 28ms on bun and 959ms to 59ms on node (M4 Pro; 47.8s on Windows incode-yeongyu/oh-my-openagent#8371).start(); the 120ms grace delay now gates only the animation, since the synchronous extension imports previously starved the timer and the first pty byte arrived at 2.27s.Written for commit 2496395. Summary will update on new commits.