feat(hypervisor): give the ACP bridge a serve mode for the Builds tab (#639) - #652
feat(hypervisor): give the ACP bridge a serve mode for the Builds tab (#639)#652umi-appcoder[bot] wants to merge 2 commits into
Conversation
…#639) Fourth slice of #639, stacked on the adapter PR. Still inert — nothing launches this yet. The Hypervisor drives one prompt per process, but the Builds tab runs a CLI interactively in a tmux pane and pastes prompt after prompt into it. `dsh` has no REPL we can use for that: its `tui` profile is not among the bundles the npm package ships (only base / web / headless / sdk / sdk-minimal / acp), and `--profile headless` is one-shot prose. So the bridge grows the shape kc-harness already established for exactly this job — read stdin, answer, repeat. `--serve` keeps ONE ACP connection and session alive across prompts. That is not tidiness: booting dsh's plugin tree is the slow part of a turn, and a fresh session per prompt would also discard the conversation and the harness's KV-cache prefix. A failed turn does not end the session, so a user can fix the key or the prompt and retry in the same pane. Reading a tmux pane needs the idle-timeout terminator (paste never sends EOF). kc-harness solved that once already, so serve mode imports `harness.read_prompt` rather than growing a second, subtly-different copy — both modules land in the same directory at pod boot. `--format stream-json` renders a turn for a pane instead of for the adapter: Claude-shaped JSONL plus human-readable lines on the same stdout, same contract as kc-harness. The tool blocks deliberately use the field names `_claude_assistant_events` / `_claude_user_events` already read (`tool_use` with id/name/input, `tool_result` with tool_use_id/content/is_error) rather than kc-harness's slightly different spelling, so a Build transcript needs no special-casing. Pretty lines are glyph-prefixed so they can never start with `{` and be mistaken for events. Only a settled turn emits the closing `result`; the error branch emits its own, and two would read as two turns. Both renderings share one `emit()` call site via a small Sink seam, so there is no second event-mapping path to drift. 10 new tests, 40 in the file: session reuse across two prompts, clean exit on closed stdin and on `/exit`, a failed turn leaving the session usable, and the full stream-json shape including that no plain line starts with `{`. Also driven against a real `dsh --profile acp`: two prompts and `/exit` over one session, rendering the auth failure once and exiting 0. Refs #639
|
Note on the empty checks list. CI is configured as In the meantime everything CI runs was run locally on this exact branch: |
Found reviewing the serve path. StreamJsonSink is reused for every prompt
in a serve-mode session, and it remembers the turn's answer so `turn_end`
can put it in the closing `result` event. Nothing cleared it at the START
of a turn, and the failure path never reaches `turn_end` at all — so:
turn 1 fails → _last_text = "error: …", result emitted, no turn_end
turn 2 succeeds → but says nothing (a tool-only turn), so turn_end
closes it with turn 1's error as its result
A Build pane would report a successful turn as the previous turn's
failure. Same shape with a successful turn: turn 2's result would echo
turn 1's answer.
Fixed by giving Sink a turn_start() alongside turn_end() and calling it
in prompt(), where the other per-turn state (text buffer, tool table) is
already reset. EventSink needs neither, so the adapter path is unchanged.
5 tests, driving the sink directly — the bug only surfaces on a turn that
settles without saying anything, which is awkward to script through a
stub agent and trivial to state here. They also pin that a thought is
rendered but is never mistaken for the turn's answer, and that no plain
pane line can start with `{` and be misread as an event.
Refs #639
715dab5 to
a3dd411
Compare
|
Pushed a fix found reviewing this branch (
A Build pane would report a successful turn as the previous turn's failure. Same shape with a successful turn: turn 2's result would echo turn 1's answer. Fixed with a |
Fourth of six PRs for #639. Stacked on #651 — base is
kc/639-dsh-adapter, so the diff here is just the serve mode. Still inert: nothing launches it until the registry PR.Why the Builds tab needs more than the bridge already had
The Hypervisor drives one prompt per process. The Builds tab is different: it runs a CLI interactively in a tmux pane and pastes prompt after prompt into it.
dshhas no REPL we can use for that —tuiprofile is not among the bundles the npm package ships (dsh-base,dsh-web-app,dsh-headless,dsh-sdk-app,dsh-sdk-minimal,dsh-acp-app— no tui), and the issue rules it out anyway;--profile headlessis one-shot prose with no tool output.So the bridge grows the shape
kc-harnessalready established for exactly this job: read stdin, answer, repeat.--serveKeeps one ACP connection and session alive across prompts. Not tidiness: booting dsh's plugin tree is the slow part of a turn, and a fresh session per prompt would also discard the conversation and the harness's KV-cache prefix. A failed turn does not end the session — the user can fix the key or the prompt and retry in the same pane.
Reading a tmux pane needs the idle-timeout terminator, because
tmux paste-buffertypes the prompt in and never sends EOF.kc-harnesssolved that once already, so serve mode importsharness.read_promptrather than growing a second, subtly-different copy — both modules land in the same directory at pod boot.--format stream-jsonRenders a turn for a pane instead of for the adapter: Claude-shaped JSONL plus human-readable lines on the same stdout, the same contract
kc-harnessuses.The tool blocks deliberately use the field names
_claude_assistant_events/_claude_user_eventsinhypervisor_session.pyalready read —tool_usewithid/name/input,tool_resultwithtool_use_id/content/is_error— rather than kc-harness's slightly different spelling, so a Build transcript needs no special-casing. (Worth flagging for review:harness.py's comment points at a dashboard functionformatStreamJsonOutput()that no longer exists in the SPA, so in practice the pane rendering rests on the plain lines and the JSONL is parity with kc-harness. I did not change kc-harness here.)Pretty lines are glyph-prefixed, so one can never start with
{and be mistaken for an event — there is a test for that. Only a settled turn emits the closingresult; the error branch emits its own, and two would read as two turns.Both renderings share one
emit()call site through a smallSinkseam, so there is no second event-mapping path to drift out of sync.Verification
10 new tests, 40 in the file: session reuse across two prompts (asserting exactly one
initializeand onesession/newfor twosession/prompts), clean exit on closed stdin and on/exit, a failed turn leaving the session usable for the next prompt, and the full stream-json shape.Driven against a real
dsh --profile acptoo — two prompts and/exitover one session:The auth failure is reported once and the pane exits 0 on
/exit, as designed. A successful turn still needs a realDEEPSEEK_API_KEY.Next
server.py(ASSISTANTS, gating,assistant_command(), model + effort tables),values.yaml,deployment.yaml, docsmcp_registry, skills providerRefs #639
🤖 Generated with Claude Code