design adapter prewarm - sinrimin/harness-nexus GitHub Wiki
Issue-driven era. Anchors: issue #3 (dsh investigation: open decomposition, CPU profile of the 1.3s
dsh --profile acpboot β a module-resolution I/O storm inherent to dsh;NODE_COMPILE_CACHEmeasured and rejected) and issue #4 (opencode: open 2.5s = ~1.6s binary boot + ~0.93s per-process lazy firstsession/new; idle RSS ~316MB β the heaviest of the set). Daily-work orientation lives in the repo'sAGENTS.mdfeature index.
Issues #2's per-spawn costs were gone, but opening a chat channel still paid the adapter process boot on every click, because the daemon spawns a fresh adapter per channel:
| target | open (steady) | spawn+initialize | note |
|---|---|---|---|
| deepseek | ~1.5s | ~1.30s | the bulk |
| claude-code | ~1.2s | ~0.4s (wrapper) | its CLI child boots at session/load regardless |
| codex | ~2.3s | ~2.1s | wrapper shape |
| opencode (#4) | ~2.5s | ~1.6s | native binary boot; slowest of all |
The structural fix is the reference portal's resident model: boot the adapter BEFORE the click.
Pre-warm to "initialized adapter, no session" when the user is about to
need it; consume it on chat:session.open:
-
machines.chat_prewarmJSON column (migration 16), written through the ordinaryPATCH /api/machines/:id(chatPrewarmarm, owner-or-admin + 404-hiding β the same gates as rename/remote-chat). Redesigned same-day from the first instance-global cut: the idle processes live on the machine, so its owner decides. - Absent =
DEFAULT_CHAT_PREWARM_SETTINGS: deepseek ON; claude-code/codex/opencode OFF (opencode #4: biggest absolute saving after codex, but ~316MB idle argues for opt-in). - The map is strict replace-semantics (all four keys; a legacy 3-key
payload 400s). The web normalizes
{...DEFAULT, ...machine.chatPrewarm}before sending, which heals pre-#4 rows on first toggle. - The toggle lives in the machine page's 代η tab β one Switch per Agent
runtime card (
PrewarmToggleinagents-tab.tsx; only the pi card renders none).PREWARM_ADAPTER_TARGETS(shared, re-exported by the SDK alongsideDEFAULT_CHAT_PREWARM_SETTINGS) enumerates the pool targets.
- AgentSession mount β
chat:adapter.prewarm {agentInstanceId}(browserβserver, owner-gated like open, ack{accepted}β false when off/unsupported/offline) βchat:adapter.prewarm {target}over/ctlβ the daemon pool. -
chat:session.openalso stamps the current switch ontochat:session.startas an optionalprewarm: trueso the daemon re-arms AFTER a channel took one.
- At most ONE prewarmed adapter per target, spawned to a completed
initialize(the payload carries the FULLAcpAgentConnection.startresult β caps and agent identity were already paid). -
consume()onchat:session.startskips spawn+initialize and goes straight to session establishment. Pending (mid-boot) entries are NOT consumed β the channel spawns fresh, the prewarm stays for the next click. - Idle TTL
HN_PREWARM_TTL_MS(default 120s;0disables prewarm) kills ready entries; teardown on /ctl loss (teardownAll) and the W11 boot sweep/audit own the rest.
- The prewarm arms the W7.1 event tap AT PREWARM TIME (own listener +
--patch+ env), so a consumed prewarm keeps tap-grade streaming; a failed handshake closes the listener and the channel degrades to the transcript tail exactly like a tap-less spawn. The tap is dsh-ONLY (armTapno-ops elsewhere) β an opencode adoption is a plain native-ACP connection.
- Prewarms ledger under
prewarm-<target>pseudo ids (the WIRE_ID regex allows no colons β hence the hyphen); adoption DELETES the pseudo file and re-ledgers under the channel's id with the same pgid; TTL/teardown kills deliberately never unlink (audit-only, per W11 A).deleteAdapterLedgerEntryexists for exactly this rename.
| target | cold | prewarmed |
|---|---|---|
| deepseek | 1.5s | 59β206ms (fully warm pool vs settled boot) |
| codex | 2.3s | 199ms |
| claude-code | 1.2s | 666ms |
| opencode (#4) | 2.5s | 846β887ms |
Switches off β baseline unchanged (1.48s dsh). The remaining ~0.9s on
opencode is its per-process lazy first session/new β deliberately NOT
warmed: a throwaway warm-up session would persist a junk row in opencode's
session store (ACP has no session delete). Daemon 0.24.0-i4.
-
ChatHandlersHandle.prewarmReady(target)exposes pool readiness for deterministic waits;cli/test/prewarm.test.tscovers pool semantics (dedupe, consume-once, pending-miss, TTL, dead-replace, late-resolve kill, teardownAll). - The server suite covers the strict-map PATCH semantics (incl. the legacy 3-key 400) and each target's independent switch; smoke covers the machine-level PATCH flow end to end.