feat(spawn): Remote Control as a typed spawn knob — one lab setting, providers do what they want #164
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!164
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/163"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #163
Remote Control was hardcoded on for every claude run — and it was never the isolated flag it looked like. The claude.ai deep link is a byproduct of it, so the "Open" affordance on a run row is Remote Control. This turns it into a knob, and the interesting half is everything that happens when it is off.
The seam
Remotejoins provider/model/effort as a fourth typed spawn knob (provider.SpawnSpec.Remote), deliberately not an entry in the ADR-0021 options bag: that bag is provider-declared and provider-filtered, so "one setting every provider sees" isn't expressible in it — and lab must act on the value (deep-link gating), which is ADR-0021's own criterion for a lab-domain field.Providers advertise only whether they honor it, via a new optional capability
RemoteCapable. claude-code adds/omits--remote-control; codex receives the bool and ignores it. Lab never learns the mechanism.This buys a sharp Tier-1 conformance check (
spawn-remote): aRemoteCapableprovider's argv must differ betweenRemote:true/false; a non-capable provider's must be byte-identical. I verified it's load-bearing by reverting the claude adapter to always-on — the suite caught it with a named error.Deliverable 1 — the live spike: GATE CLEAR
Default-off put a pinned invariant on trial (
CONTEXT.md: "session exit is never the done-signal, because--remote-controlidles after finishing"). A Tier-2 live A/B against real claude 2.1.206 (evidence ininternal/compat/compat.md§12) found:CONTEXT.mdis amended (an interactive CLI doesn't quit when a turn ends, remote or not).tool_useto the transcript immediately, so the ADR-0020 dialog-spool overlay self-suppresses and dialogs instead surface throughinternal/chat's transcript-tail scan — the path whose own comment called it "the dormant fallback for a provider that DOES flush pending tool_use". That fallback is now the LIVE path for every non-remote run. No code change needed (answering verified end to end), but it must never be deleted as dead code — pinned in the code comment and the ADR.Three things the issue got wrong, corrected here
RemoteCapableis the FIFTH instance of ADR-0017's type-assertion pattern (afterConnectingReporter,DeepLinker,LiveSignals,LoginCodeReporter), not the third.!run.remote. Since the resolver clamps non-capable providers tofalse, a bare check would also have stripped thetmux attachbutton codex shows today — an unintended regression and a violation of honest degradation. The gate isprovider.supports_remote && !run.remote; codex is untouched in every case.The bool trap
Every knob so far spells "unset" as
"", which is only safe because""is never a legal model id.falseIS a legal value here, so unset can never be spelledfalse.*boolat every layer that must distinguish unset-from-explicitly-off (request, both repo columns, PATCH decoders); nullable repo columns;""/absent row at the settings layer. Pinned by tests at three levels: a repofalsebeats a globaltrue,null≠falsethrough PATCH, and an explicit{"remote": false}beats a globaltrueall the way toSpawnSpec.What's in it
repos.remote_default/afk_remote_default;runs.remoteNOT NULL — a real column, because deep-link capture arms at boot re-adoption as well as at Start, so after a restart the run row is the only thing that knows. Existing rows backfilled totrueso their Open links keep working (verified by a test that drives goose to 0010, inserts a legacy row, migrates, and asserts).ArmCapturechoke point — covering Start and re-adoption, sointernal/reconcileneeded no change. Without it every non-remote run would burn the 30s bridge timeout and log ADR-0017's loud capture-miss (the spike measured exactly that:elapsed=30.152s+ WARN).…popover, and a disabled-with-note state when the resolved provider reportssupports_remote: false.Accepted regression
New claude runs come up without Remote Control and therefore without a claude.ai Open link until the operator opts in. Lab's own chat is the primary surface; the link is redundant with it.
Verification
gofmtclean,go vetclean,go build ./..., all 40 packages pass (forced-count=1on every touched package).tsc --noEmit,eslint,prettier --checkall clean; 822 vitest tests pass (up from 795); productionvite buildsucceeds.golangci-lintis not installed in this sandbox, somake lintwas substituted withgo vet+gofmt.Follow-up spotted (not fixed here)
The spike incidentally found that on 2.1.206 the 60s dialog auto-dismiss did not fire (pickers sat undriven 5+ min with no
afkTimeoutMs), and hook payloads reportpermission_mode:"default"despite--permission-mode auto. Both are version drift in both arms, unrelated to this change, but compat §5/§7/§11 pin that 60s self-resolve — it deserves its own re-verification issue.