Skip to content

[macOS] attach takes seconds to connect and render first TUI frame after serve is ready #48051

Closed as not planned

Description

@shuxueshuxue

Summary

On macOS, opencode attach has a repeatable multi-second gap after the server is already healthy and a session has already been created. The gap is visible before the first usable TUI frame, independent of the model's first reply.

Environment

  • OpenCode 1.18.25 (/opt/homebrew/bin/opencode)
  • macOS 26.5.1, Apple Silicon Mac Studio
  • opencode serve bound to loopback, then a separate pty running opencode attach <url> --session <id>
  • The host used an isolated OpenCode XDG home and set OPENCODE_DISABLE_PROJECT_CONFIG=true for its task-home; no owner process or profile was used.

Measurement

The host clicked New task, submitted a prompt, waited for GET /global/health = 200, observed the task-home session from GET /session, then spawned the exact attach command. attach connected is the attach PID's loopback TCP connection to the serve port. TUI first frame is the first visible xterm instance after the attach instance changed and the OpenCode status bar was rendered. Times below are cumulative from the New task click; t7 is reported separately from the OpenCode message ledger.

Profile attach spawn attach TCP connected TUI first frame spawn→connected connected→first frame t0→t5
Cold: fresh host profile + fresh OpenCode XDG home 2.448s 5.598s 9.880s 3.150s 4.282s 9.880s
Warm: same profile after app restart, OpenCode cache present 2.336s 5.573s 9.257s 3.237s 3.684s 9.257s
Hot: same app process, second new task 2.191s 6.435s 10.271s 4.244s 3.836s 10.271s

The host-side preceding milestones were similarly stable: HTTP ready at 1.871–2.025s, session observed at 2.180–2.432s, and attach spawn at 2.191–2.448s. The first prompt reached the OpenCode session at 2.396–2.672s. First replies completed at 11.379–17.801s and are intentionally not included in the TUI startup claim.

Questions

Please profile the two attach-side intervals:

  1. attach process spawn → first TCP connection to the healthy serve;
  2. TCP connection → first rendered/interactive TUI frame.

In particular, can startup work such as external plugin loading, provider/model discovery, models.dev fetch, update checks, terminal capability queries, or replay be identified and deferred?

Flag probe

OpenCode 1.18.25 exposes attach --pure (run without external plugins), but no attach-specific provider/update skip flag. In an isolated serve --pure + attach --pure pty with OPENCODE_DISABLE_AUTOUPDATE=true and OPENCODE_DISABLE_MODELS_FETCH=true, the first pty output arrived 5.232s after attach spawn. This is a first-output probe, not a substitute for the full TUI table above; it shows the pure path still has a multi-second attach startup.

Related: #14965 (slow startup), #31463 (cold npm plugin resolution), #31006 (--pure plugin startup workaround), #37060 (attach TUI init hang on Windows).

Activity

  1. shuxueshuxue commented on Sep 9, 2026

    @shuxueshuxue
    Author

    Follow-up measurements from 2026-09-09 (OpenCode 1.18.25, macOS, isolated XDG HOME/project). These are bare controls outside Gugu. Three runs per row; median (range), seconds. TCP is the first lsof ESTABLISHED connection to the serve port. First frame is the first captured alternate-screen sequence (ESC[?1049h).

    machine / condition serve->health health->POST /session attach spawn->TCP TCP->first frame direct TUI spawn->frame
    Mac mini, bare cold 0.58 (0.58-0.71) 0.19 (0.17-0.20) 0.68 (0.62-0.83) 1.58 (1.55-1.67) 2.55 (2.55-2.72)
    Mac mini, bare warm 0.66 (0.62-0.69) 0.17 (0.17-0.33) 0.94 (0.72-1.23) 1.74 (1.69-2.39) -
    Mac mini, --pure 0.90 (0.69-1.64) 0.18 (0.16-0.33) 0.72 (0.69-1.31) 1.81 (1.67-2.26) -
    Mac mini, models/update disabled + pure 0.69 (0.69-0.95) 0.06 (0.06-0.08) 0.90 (0.81-1.23) 1.78 (1.72-2.24) -
    Mac mini, one project plugin, cold 0.59 (0.59-1.70) 8.31 (8.13-8.44) 0.79 (0.64-0.82) 1.60 (1.50-1.62) -
    Mac mini, same plugin, warm dependency cache 0.57 (0.55-1.65) 1.10 (1.07-8.88) 0.69 (0.63-0.91) 1.59 (1.57-1.60) -
    Mac1, bare cold 0.56 (0.55-0.56) 0.16 (0.15-0.16) 3.32 (2.07-5.55) 1.58 (1.52-1.60) -
    Mac1, bare warm 0.55 (0.54-0.56) 0.15 (0.15-0.16) 3.61 (3.59-5.22) 1.59 (1.49-1.63) -
    Mac1, one project plugin, cold 0.56 (0.54-0.58) 12.00 (11.41-12.14) 1.98 (1.87-3.51) 1.57 (1.54-1.61) -
    Mac1, same plugin, warm dependency cache 0.56 (0.55-0.58) 1.44 (1.43-12.86) 2.32 (1.92-3.44) 1.56 (1.54-1.60) -
    Mac1, models fetch disabled 0.56 (0.54-0.57) 0.16 (0.15-0.16) 3.12 (3.12-3.26) 1.60 (1.55-1.64) -
    Mac1, auto-update disabled 0.55 (0.54-0.56) 0.15 (0.15-0.16) 3.20 (2.10-3.25) 1.55 (1.53-1.61) -
    Mac1, models.dev URL -> 127.0.0.1:9 0.56 (0.56-0.70) 0.15 (0.15-0.15) 2.13 (2.12-3.40) 1.57 (1.55-1.61) -

    The project-plugin control is repeatable across machines: OpenCode logs show config/plugin bootstrap and an unconditional dependency install before POST /session; a warm dependency cache reduces that to about 1.1-1.4s. OPENCODE_DISABLE_PROJECT_CONFIG returns it to about 0.1s. This explains the historical intermittent /session timeout class, but it is separate from attach-side latency.

    The attach gap remains after --pure, model fetch disable, update disable, and a positive network blackhole. In the blackhole runs, the server log explicitly reports repeated Failed to fetch models.dev, while attach->TCP and TCP->frame remain unchanged. The attach sampler saw the expected loopback connection; no attach-child external socket was identified, but unrelated processes also owned external sockets, so external-socket absence is UNMEASURED rather than claimed as zero.

    For comparison, a same-machine direct TUI invocation (the in-process worker/server path, without external attach validation) rendered in 2.55s on the Mac mini and 4.46s on Mac1. This bounds the additional two-process/validation overhead, but is not a drop-in replacement for attach.

    Source cross-check against tag v1.18.25 (cb7d8b2f5e44876ef98b661dc10590c915af3a9f): cli/cmd/attach.ts awaits TuiConfig.get(), then validateSession() (session.get with --session), then imports/runs the TUI layer. packages/tui/src/app.tsx creates the renderer, waits up to 1s for theme mode, then calls Solid render(). SSE, provider/config/agent/project sync, non-blocking session/MCP/LSP/formatter/status/auth/VCS/workspace refreshes, and external TUI plugin startup happen after mount. tui.ts schedules checkUpgrade() 1s after validation, outside the render-critical await chain.

    The two-machine controls support profiling the attach/runtime/TUI path upstream; there is no measured Gugu-owned wait inside attach. Full raw logs and captures are available in gugu-base commit d17c92f22ef0218c7902a7f6e2614a4f3e3303a6 under evidence/2026-09-09-opencode-serve-attach-5288/.

  2. shuxueshuxue commented on Sep 9, 2026

    @shuxueshuxue
    Author

    Additional machine controls for the attach-startup gap (2026-09-09). OpenCode is 1.18.25 on both hosts; each command was run five fresh times with isolated HOME/XDG only for the attach experiment. /usr/bin/time -p real, seconds:

    machine command median (range) load sample
    Mac mini (macOS 15.6.1) opencode --version 0.27 (0.27-0.28) load 1.81-1.89; pages free 12,364-12,661
    Mac mini opencode attach --help 0.28 (0.27-0.30) pages active ~362k
    Mac1 (macOS 26.5.1) opencode --version 0.26 (0.26-0.26) load 10.87-14.93; pages free 19.59-19.60m
    Mac1 opencode attach --help 0.27 (0.27-0.27) pages active 5.80-5.81m

    The earlier bare controls measured attach spawn -> TCP at 0.68s (0.62-0.83) on the mini and 3.32s (2.07-5.55) on Mac1. Since version/help startup is the same on both machines, the approximately 2.6s delta is not binary load, code-signing, or page-cache startup. It is in attach-specific work after argument parsing and before its first socket activity (TuiConfig.get(), validateSession(), and dynamic TUI/Effect imports). Both probes used a tiny cwd (/private/tmp/opencode-probe-*/project).

    The attach-side DEBUG first-log timestamp remains unmeasured in this build: --print-logs produced no attach stderr/log records in this path. Source inspection at tag v1.18.25 shows TuiConfig.get() resolving ancestor .opencode candidates plus global XDG tui.json/tui.jsonc; the isolated project had no non-empty TUI file. We therefore do not claim a zero-duration config read. The machine gap is reproducible, but the exact internal sub-interval needs an upstream per-stage trace.

    Raw command logs are in gugu-base evidence 2026-09-09-opencode-serve-attach-5288/ (follow-up v2 files).

  3. shuxueshuxue commented on Sep 9, 2026

    @shuxueshuxue
    Author

    Follow-up measurements from two hosts (OpenCode 1.18.25, isolated XDG homes/cwd; raw evidence is in gugu-base commit dc82f25).

    host sampler result during first 3s of attach bare attach spawn -> loopback TCP
    Mac1, macOS 26.5.1, /opt/homebrew/bin/opencode 3/3 samples: main thread in dyld4::APIs::dlopen -> Loader::getLoader -> makeDiskLoader -> mapSegments -> fcntl; helper threads mostly __psynch_cvwait prior control median 3.32s (2.07-5.55)
    Mac mini, macOS 15.6.1, local opencode main thread kevent64 (1456/1657/1617 samples), shorter secondary dlopen/mapSegments (400/217/222); no stat/opendir hotspot prior control median 0.68s (0.62-0.83)

    Binary-only controls are indistinguishable (opencode --version: 0.26-0.28s; attach --help: 0.27-0.30s), so the Mac1 gap is after argument parsing and is consistent with dynamic module/dyld segment mapping rather than binary launch or project scanning. A mini CPU control (8 and 16 yes workers, 8 CPUs) increased attach to medians 1.241s and 1.455s, but did not reproduce Mac1's 3-9s; load is a contributor, not the sole cause.

    The isolated XDG server log is present and shows config/instance/project loads, but this path emits no attach-side TuiConfig/session.get line to stderr or home/data/opencode/log; attach-first-log timing remains unmeasured rather than being claimed zero.

    One more same-ruler product check on Gugu (Mac1, temporary renderer probes, independent build c1037975ff62) found, warm n=3:

    attach PID -> TCP TCP -> first ESC[?1049h alt -> visible gate visible -> pane mount
    4.887s (4.837-5.129) 3.563s (3.458-3.647) 0.439s (0.432-0.453) 0.025s (0.024-0.025)

    The prior Gugu TCP-to-status-bar 3.7-4.3s is therefore accounted for: alt-screen arrives within the TUI interval, and only ~0.46s remains after it for Gugu visible gating/mount. No stable Gugu-owned >0.5s segment was found. Cold Electron restarts had large OpenCode/TUI tails before alt-screen (TCP->alt median 18.724s, range 2.733-33.160), reinforcing that the long segment is upstream/machine startup. I do not think this needs an upstream code change, but the dyld stack and host/load split may be useful for the issue.

  4. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions