ReviewOS

also looking at this

stacks/uplink

feat: let a text switch one thread between Claude and Codex

#52
Merged glennmichael123 wants to merge feat/per-thread-engine into main
16 files +316 -27
9 days ago

Closes #10. The per-installation half shipped in #7; this is the per-thread half, and the last user-facing piece of #4.

What it does

Text codex and that thread switches. Text claude and it switches back. Both are whole-message words, like stop, so "codex keeps timing out on ~/Code/x" is still a task and not a switch.

you    codex
uplink Switched to Codex. Starting a fresh conversation, because a session
       cannot move between agents.
you    what is failing in ~/Code/api
uplink ...
you    status
uplink Working 40s on: what is failing in ~/Code/api
       Agent: Codex

Three things it has to get right

A session cannot move between agents. A Claude session id resumes nothing in Codex and the reverse is equally true. Switching clears it and says so, and the resume guard in begin() gained the engine alongside the cwd and the idle window, so no path can quietly hand one agent's session to the other. The working directory stays, because that belongs to the thread and not to the agent.

A queued task keeps the agent it was sent to. The engine is fixed on the job when the text arrives, not read when the run starts. Otherwise a switch would silently move a task already waiting in the queue onto an agent its author never chose. There's a test for exactly that ordering.

An unavailable agent says so when it is chosen. codex probes before switching, and an engine that isn't installed replies with the install command. Finding out on the next real task costs the task, and nobody switching from a phone can see the Mac.

The column, and why it's nullable

conversations.engine and runs.engine are both nullable, and on conversations the null means something: follow whatever the installation is set to. A thread that has never asked keeps tracking the menubar's picker, so changing it there still works. Only a thread that has been switched pins itself.

On runs it's nullable because rows written before a thread could choose predate the question, and calling them all Claude would be a guess recorded as a fact.

This is the first real exercise of #9's migration path. Before it, this column would have reached every new install and no existing one, and the first text after the update would have failed for everyone who already had the app. The guard test that migrates a populated 0.1.3 database now actually runs an ALTER, where before it passed trivially.

Verification

  • 147 unit tests pass, 8 new, covering both acceptance criteria in the issue plus the switch-back, the already-on case, the missing-engine case, and help
  • ./buddy lint and ./buddy typecheck clean
  • bun build --compile still produces a binary

One thing worth knowing for review: the test harness's config() had drifted from UplinkConfig (it still had model rather than claudeModel, and none of the Codex fields). Tests aren't in the typecheck project, so nothing caught it. Fixed here because the new behaviour reads config.engine and would otherwise have been reading undefined.

🤖 Generated with Claude Code

16 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.