Repository navigation
Wire model-picker apply step into the daemon - #130
Merged
Merged
Conversation
bin/model-picker.mjs could probe the fleet, raise a CHOICE intent and apply the tap, but nothing invoked it when the decision came from a channel other than the CLI itself (CodeWatch tap, GroupMind web tap, chat-reply poller) — a phone tap decided the intent and then nothing consumed the decision. - src/confirmations.mjs: createIntent() takes an optional `kind` tag, exposed via getIntent/listIntents. decideIntent() is the one chokepoint every decision path already funnels through (HTTP /intent/:id/decision, the chat-reply poller, approve_intent/ deny_intent), so it now fires a registered per-kind handler (registerKindHandler/pendingKindHandler) once, there, regardless of which channel settled the intent. - src/model-selection.mjs (new): the probe/offer/apply core factored out of bin/model-picker.mjs, including a new applyChoice() — the re-probe-then-write-or-refuse step, usable standalone (no offer-time reading required) as well as from pickModel()'s own flow. - bin/iak-mcp-daemon.mjs: registers a "model" kind handler at startup that calls applyChoice() and logs the result. model-picker.mjs's own CLI-raised intents are not kind-tagged, so a CLI run still applies via its own in-process re-probe exactly as before — a tap is never applied twice. - src/mcp-server.mjs: new request_model_choice MCP tool, the daemon-side way to raise a model choice without shelling out to the CLI (probes the registry, raises a kind:"model" CHOICE intent, follows the same daemon-forward/in-process pattern as request_choice). - bin/model-picker.mjs: thin CLI shell over src/model-selection.mjs now (arg parsing, exit codes, rendering); same exported names, same behaviour. - test/model-selection.test.mjs (new): exercises the wiring itself, not just applyChoice() in isolation — a decided model intent that probes UP writes the selection file, one that goes DOWN between offer and tap writes nothing and records CHANGED_SINCE_OFFER, and a non-model CHOICE intent triggers no apply. Verified against a reverted decideIntent hook: the first two fail as expected, the negative control still passes. npm test: 729/729 passing (726 pre-existing + 3 new), exit 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Three issues from review of the model-picker daemon wiring:
- P2: bin/iak-mcp-daemon.mjs read mcp.confirmations.model_registry (via
`cc`) while src/mcp-server.mjs read mcp.model_registry - one level
up, a different key - so a custom registry path would make the
offer (request_model_choice) and the apply (the daemon's kind
handler) probe different files with no error either way. Added
resolveModelRegistryPath()/resolveModelSelectionPath() to
src/model-selection.mjs and made both files call them instead of
reading the config keys inline. Same for model_selection_path,
which mcp-server.mjs never read at all before.
- P3: applyChoice() applied any re-probed entry that was UP,
regardless of whether it still named the model shown in the offer.
createIntent()/decideIntent() now thread an optional `offeredModels`
map (option -> model name, recorded at raise time) through the
decided-intent kind-handler payload; applyChoice() takes an
`offeredModel` and, when the re-probed model differs, still applies
(the box IS up; refusing would just leave a possibly-worse previous
selection in place) but records `modelChanged: {offered, applied}`
on the outcome. bin/iak-mcp-daemon.mjs's kind handler passes this
through and logs it.
- P3: request_model_choice's tool description now says plainly that
without a running daemon the decision is returned but NOT applied -
no selection file is written by this tool or anything else in that
mode.
test/model-selection.test.mjs: 7 new tests, each checked to fail
against a reverted version of the fix it covers (resolver key
mismatch, missing modelChanged detection in both directions, missing
tool-doc wording) before being left in the passing state below.
npm test: 736/736 passing (729 prior + 7 new), exit 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Summary
The 2026-09-22 switcher audit found that
bin/model-picker.mjs(PR #119) can probe the fleet, raise a CHOICE intent, and apply the tap — but nothing invoked it when the decision came from a channel other than the CLI's own blocking poll. A CodeWatch tap or a GroupMind web tap decided the intent and then nothing consumed the decision.src/confirmations.mjs:createIntent()takes an optionalkindtag (exposed viagetIntent/listIntents) and an optionalofferedModelsmap (option → model name shown in the offer, recorded at raise time).decideIntent()— the one chokepoint every decision path already funnels through (HTTP/intent/:id/decision, the chat-reply poller,approve_intent/deny_intent) — now fires a registered per-kindhandler once, there, regardless of which channel settled the intent (registerKindHandler/unregisterKindHandler/pendingKindHandler), passingofferedModelsthrough to it.src/model-selection.mjs(new): the probe/offer/apply core factored out ofbin/model-picker.mjs, including a newapplyChoice()— re-probe the chosen entry, write the selection file if it's still usable, otherwise refuse withCHANGED_SINCE_OFFER(previous selection untouched). Usable standalone (no offer-time reading required, e.g. from the daemon) as well as frompickModel()'s own CLI flow. Also exportsresolveModelRegistryPath()/resolveModelSelectionPath()— the one placemcp.confirmations.model_registry/.model_selection_pathare read from config, so the daemon (apply) and the MCP tool (offer) can never silently disagree on which registry/selection file they mean.bin/iak-mcp-daemon.mjs: registers a"model"kind handler at startup that callsapplyChoice()(passing through the offered model for the chosen option) and logs the outcome, including a[model changed since offer: X -> Y]note when the re-probed entry is UP but now names a different model than it did in the offer.model-picker.mjs's own CLI-raised intents are not kind-tagged, so running the CLI against this daemon still applies via its own in-process re-probe exactly as before — a tap is never applied twice.src/mcp-server.mjs: newrequest_model_choiceMCP tool — the daemon-side way to raise a model choice without shelling out to the CLI. Probes the registry, raises akind: "model"CHOICE intent (withofferedModelsattached), follows the same daemon-forward / in-process patternrequest_choicealready uses. Its description states plainly that without a running daemon the decision is returned but is NOT applied — no selection file is written in that mode, by this tool or anything else.bin/model-picker.mjs: now a thin CLI shell oversrc/model-selection.mjs(arg parsing, exit codes, rendering). Same exported names, same behaviour —test/model-picker.test.mjsis unchanged and still passes.Update (review round 2): fixed three issues from review — (1) the daemon and the MCP tool were reading the custom registry/selection path from two different config keys (
mcp.confirmations.model_registryvsmcp.model_registry), so a custom path configured once could make the offer and the apply probe different files; both now go through the sharedresolveModelRegistryPath/resolveModelSelectionPath. (2)applyChoice()applied any re-probed UP entry regardless of whether it still named the model shown in the offer; it now still applies (the box IS up) but reportsmodelChanged: {offered, applied}on the outcome, and the daemon logs it. (3)request_model_choice's description was reworded to say plainly that the no-daemon fallback does not apply the decision.What is tested, and what is not
Tested (
test/model-selection.test.mjs, new, 10 tests):kind: "model"intent whose chosen entry still probes UP: the registered handler fires andapplyChoice()writes the selection file with the expectedselectedId/model/selectedFrom.applyChoice()returnsCHANGED_SINCE_OFFER, the error names what it was and is now, and no selection file is written.model) CHOICE intent triggers no apply — the registered"model"handler is not called and no file is written.resolveModelRegistryPath()/resolveModelSelectionPath(): a custom path undermcp.confirmationsis honoured; the old (wrong) one-level-up keymcp.model_registryis explicitly asserted NOT to be picked up; the default falls back to<root>/config/models.json.bin/iak-mcp-daemon.mjsandsrc/mcp-server.mjsactually call the shared resolver (and don't read the old keys directly any more).applyChoice()still applies a box that is UP but now serves a different model than offered, and reportsmodelChanged: {offered, applied}; a matching case with no drift confirmsmodelChangedisundefinedwhen nothing changed.createIntent()withofferedModels→decideIntent()→ the daemon-shaped kind handler →applyChoice()reports the correctmodelChangedfor the specific option that was picked.request_model_choice's description text states both that a live daemon applies the choice and that the no-daemon fallback does not.decideIntent()hook, the resolver call insrc/mcp-server.mjs, themodelChangedcomputation inapplyChoice(), the tool-description wording) and confirming the corresponding test(s) fail, then restored the fix and confirmed they pass again.npm test→ 736/736 passing, exit 0 (726 original + 10 new across two review rounds). Ran the new/changed test file alone first (node --test test/model-selection.test.mjs), then the full suite both times.node bin/model-picker.mjs --dry-runin this worktree: there is noconfig/models.jsonhere (onlyconfig/models.example.json), so it correctly reportsNO_REGISTRYand exits 2 — no live boxes were touched, and I did not fabricate a registry to probe hosts that aren't reachable from here.Not tested / known gaps:
bin/iak-mcp-daemon.mjsitself end-to-end (it needsmcp.confirmations.room/codewatch_gate_urlconfigured and a live GroupMind room to be meaningful) — the daemon's ownregisterKindHandler('model', ...)callback and its[model changed since offer: ...]log line are exercised only by inspection and by a test that mirrors its shape againstsrc/confirmations.mjsdirectly, not by importing/running the daemon script itself (which has top-level side effects —loadConfig,process.exitwithout channels configured — that make it awkward to import in a unit test).request_model_choiceinsrc/mcp-server.mjsend-to-end (the MCP tool itself, its daemon-forward path, or its in-process fallback) — only its description text is checked structurally. Existing MCP-server tests don't cover it either.request_model_choicein-process fallback (no daemon running) still has no consumer of the decision at all — same limitationrequest_choicealready has in that mode; now stated explicitly in the tool description, not silently left.config/models.json, or real boxes — everything above is unit-level with fake registries/probes. No process was restarted, no config file was touched, and/Users/petrus/ide-agent-kit(the live daemon's checkout) was never touched — all work happened in the~/tmp/iak-pickerworktree onfeat/wire-model-picker.🤖 Generated with Claude Code