Part of #4. The per-installation half shipped in #7; this is the per-thread half.
A global setting alone cannot answer "use Codex for this one thread" from a phone, which is the point
of the product. A per-text prefix is easy to forget and has to survive both the filter and
detectWorkdir. The control-word vocabulary already solves this shape of problem.
What is there today
UPLINK_ENGINE and a settings.json field choose the engine for the whole installation, and the
menubar offers a picker (app/Uplink/engines.ts, app/Uplink/desktop-agent.ts). Nothing reaches
that from a phone, and every thread on a Mac uses the same engine.
parseControl (app/Uplink/commands.ts#L24) matches only a whole message, after stripping a leading
slash and trailing punctuation. That is why stop cancels a run while "stop the dev server in
~/Code/x" still reaches the agent as a task. ALIASES (app/Uplink/commands.ts#L8) is the whole
vocabulary and HELP_TEXT (#L29) is what help texts back.
A conversation stores one sessionId and one cwd (app/Uplink/store.ts), and begin() resumes
when the session id, the cwd and the idle window all agree (app/Uplink/uplink.ts#L299).
Proposal
- Add
claudeandcodextoControlCommand(app/Uplink/commands.ts#L6) andALIASES. A barecodexswitches the thread and replies with confirmation; "codex keeps timing out on ~/Code/x" still reaches the agent, by the same rule that already protectsstop. - Add
enginetoconversationsand torunsasdefineModelattributes,schema.enum(['claude', 'codex']), default'claude'so existing rows read correctly. Runbuddy generate:migrations. Mirror both in the downloadable app'sSCHEMAandRUN_COLUMNS(app/Uplink/sqlite-store.ts), which needs the migration path from the separate issue first. - Switching engines must drop the stored session id. A Claude session id cannot be resumed by
Codex and the reverse is equally true. The resume guard at
app/Uplink/uplink.ts#L299gains the engine to its list of things that must agree, and the control word clearssessionIdthe waynewdoes at#L415. - Pin the engine on the
Job(app/Uplink/uplink.ts#L46) at enqueue time, so a queued task runs on the engine it was sent to rather than whatever the thread switched to while it waited. - Add the two words to
HELP_TEXTand to the homepage's list of words it always answers (resources/views/index.stx#L39).
Requirements
statussays which engine is working. A thread that can switch needs to be able to say what it is on (app/Uplink/uplink.ts#L390).- Default stays Claude. A user who never texts
codexsees no change at all. - An unavailable engine must say so when it is selected, not when the next text fails. If
codexis texted andfindCodex()found nothing, reply with how to install it.
Acceptance
- A test texts
codex, then a task, and asserts the run recordedengine = 'codex'and that no--resumeorresumesubcommand carried the Claude session id across the switch. - A test asserts a queued job keeps the engine it was enqueued with.
helplists both words.