Skip to content

fix(webui): reap the mcode acp process group and group models by provider - #23

Open
weekbin wants to merge 1 commit into
mainfrom
feat/webui-absorb-pr22
Open

weekbin wants to merge 1 commit into
mainfrom
feat/webui-absorb-pr22

Conversation

@weekbin

@weekbin weekbin commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Recovers the two increments from #22 that still apply to main, reimplemented
against main rather than merged from it. Where the branch overlapped main,
main's implementation wins outright; nothing from the branch's webui-react
package is carried over.

1. Reap the mcode acp process group

packages/webui/acp.mjs — the engine's acp entry spawns its own MCP plugin
tree. stop() signalled only the direct child, so the tree was orphaned on
every restart. The child is now spawned detached on POSIX (making its pid the
process-group id), and stop() signals the group — SIGTERM, then SIGKILL after
3s. Windows has no group to signal and keeps the direct-child kill.

packages/webui/server/lib/acp-client.js — stop a non-alive singleton before
overwriting the reference. A client whose alive is false can still own a child
on its way down; overwriting the pointer leaked it the same way.

Regression test spawns a real child with a real grandchild and asserts neither
outlives stop(). Verified it fails against the previous implementation
(child(alive=false) grandchild(alive=true)), so it pins the actual leak rather
than restating the new code.

2. Group /api/models by provider

packages/webui/server/routes/model.js — the engine encodes a selection as
m:<provider>:<model>:v:<variant>, so the provider falls out of the id and
groups is always consistent with models.

An optional models.json (MCODE_WEBUI_MODELS_CONFIG, else ./models.json) is
re-read per request and overlays display metadata: group label, model label,
contextLimit. It is deliberately an overlay, not a replacement — the
engine's session config option still decides which models exist, so an overlay
cannot invent a model the engine does not offer, and a missing or malformed file
is not an error.

The client renders one section per provider when more than one exists, and
otherwise keeps the flat list exactly as before.

Deliberately not carried over

  • browseWorkspace defaulting to the first allowed root — reopens the
    directory enumeration oracle that lib-workspace-containment pins closed.
  • handleAnswer dropping deprecated: true — a response-contract
    regression that the branch's own test catches.
  • fs.js losing the shared readJson body-size cap — main has the cap.
  • The branch's WebSocket transport. Its server side ports cleanly (all four
    files' dependencies exist on main), but main's frontend consumes the state
    stream over SSE and the only WebSocket client lives in webui-react — so
    adding the endpoint alone ships a transport with no consumer, while using it
    means replacing main's stream client. That is a transport decision rather
    than an absorbed increment, and it interacts with the fact that Feat/webui nextjs framework #18 removed
    feat(webui): ws event stream, worker embed transport, capability negotiation #20's WebSocket implementation; it belongs in its own change.

Verification

pnpm verify — 17 gates on linux, all passing. Within that: pnpm build
(6253 source files), pnpm typecheck, pnpm test:webui (1337 tests, 0 fail),
pnpm test:webapp (186/186), check:source, check:standalone,
check:webui-bundle, check:tsconfig, test:release-tools.

release/public-source.json regenerated for the one added test file.
Skipped on this platform: test:windows, test:sandbox, test:release-package.

Relates to #22. Does not close it — that PR's frontend and its three
regressions above still need a decision.

…ider

Two increments recovered from the feat/webui-react branch, rebased onto the
current main rather than merged from it. Where the branch and main overlap,
main's implementation wins outright; nothing from the branch's frontend
package is carried over.

1. acp.mjs: the engine's acp entry spawns its own MCP plugin tree. `stop()`
   signalled only the direct child, so the whole tree was orphaned on every
   restart. The child is now spawned `detached` on POSIX, making its pid the
   process-group id, and `stop()` signals the group (SIGTERM, then SIGKILL
   after 3s) instead of just the child. Windows has no group to signal and
   keeps the direct-child kill.

   acp-client.js: stop a non-alive singleton before overwriting the
   reference. A client whose `alive` is false can still own a child on its
   way down; overwriting the pointer leaked it the same way.

2. routes/model.js: partition /api/models by provider. The engine encodes a
   selection as m:<provider>:<model>:v:<variant>, so the provider falls out
   of the id and `groups` is always consistent with `models`. An optional
   models.json (MCODE_WEBUI_MODELS_CONFIG, else ./models.json) is re-read per
   request and overlays display metadata — group label, model label,
   contextLimit. It is an overlay, not a replacement: the engine's session
   config option still decides which models exist, so an overlay cannot
   invent a model the engine does not offer, and a missing or malformed file
   is not an error.

   The client renders one section per provider when more than one exists and
   otherwise keeps the flat list exactly as before.

The branch's changes to browseWorkspace's no-path behaviour, to
handleAnswer's response contract, and to the fs.js body-size limit are not
carried over: the first reopens a directory enumeration oracle that
lib-workspace-containment pins closed, the second is a contract regression its
own test catches, and the third would drop a cap main already has.

The branch's WebSocket transport is also not carried over. Its server side
ports cleanly, but main's frontend consumes the state stream over SSE, and
its only WebSocket client lives in the branch's webui-react package — so
adding the endpoint alone would ship a transport with no consumer, while
using it means replacing main's stream client. That is a transport decision
rather than an absorbed increment, and it belongs in its own change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant