Repository navigation
Description
Area
Service lifecycle
What are you trying to accomplish?
A Home must be able to move or remove a Remote Link Child over SSH (ocx disconnect, ocx restart, ocx sync) with nobody at the Child's screen, whatever owns the Child's runtime. #8529 makes that work when the OpenCodex desktop app owns the runtime, or a service manager whose current marker (OCX_SERVICE_MANAGED, the Windows wrapper marker) proves it launched the process. This issue covers the remaining owner: a client runtime whose only supervisor evidence is a bare OCX_SERVICE=1.
What prevents this today?
A bare OCX_SERVICE=1 is carried both by every service registered before v2.49.0 (the managed marker landed in ff524ae; v2.48.0's launchd plist, systemd unit and Windows wrapper set only the bare flag) and by the detached proxies of ocx claude, ocx codex and ocx opencode. Nothing the runtime can read today tells these apart. #8529 therefore leaves them exactly as origin/dev does: ocx restart refuses while isServiceViable(), and otherwise restarts with a replacement; ocx disconnect does not restart them; ocx sync defers or restarts as before. A Home-driven leave can still fail on such a Child.
Cases found while reviewing #8529 (final review rounds 1 to 4), each of which a design must handle:
- A legacy systemd unit whose unit file was removed or became unreadable still owns its running process. systemd keeps the process in the unit's cgroup, and
KillMode=control-groupkills a detached replacement when the unit stops. A missing unit file is not evidence that no manager owns the process. - A companion proxy on a machine where a disabled service is also installed: the service diagnostic says installed, the process is not the service's.
- A clean SIGTERM to a service child: the client runtime's signal handler exits 0 on Linux (
src/lib/handled-signal-exit.ts), and anon-failureunit or a launchdKeepAlive/SuccessfulExit=falsejob does not relaunch it. A CLI that then starts a detached proxy silently moves ownership away from the service. - A connection landing before a stop: any stop or restart decided from a state read must be fenced against
connectClientcommitting in between, or a new connection can be cut mid-transition. - PID reuse between attestation and signal: proving the attestation secret over
/healthzand then signalling a numeric pid does not bind the signal to that process generation, and a staleruntime-port.jsoncan keep authorising escalation. - An inherited companion marker: a launcher-set environment variable can be inherited by a legacy service through the manager's environment or a login shell (
/bin/sh -lcin the systemd unit), so an environment value alone cannot prove "no manager". - Sync fan-out drops the Child's stderr for exit 1 (
src/link/sync-fanout-classify.ts), so any advice a Child prints for this case does not reach the Home.
What should OpenCodex do?
Restart and disconnect of such a runtime should either hand off to its real owner or keep it and say exactly what is left, with no outcome that depends on guessing the owner. A design (reviewed before any code) must decide:
- Positive ownership evidence going forward: what a companion launcher records about its proxy that cannot be inherited or forged by a legacy service (bound to the launch generation, not only an environment value), and how a manager-owned process is proven independently of its environment.
- The one-time case of an upgraded machine with an old process still running (no evidence either way): what
ocx restart/ocx disconnectdo once, and how the Home learns it. - An authenticated stop that the target runtime itself checks (generation, connection state inside the target) instead of a CLI-side signal to a pid.
- The fence against connect: one lock or recoverable transaction shared by the stop/recycle decision and
connectClient's commit. - How the Child's advice reaches the Home through the sync fan-out.
Example usage or interface
# On a Child whose runtime was started by a pre-2.49 systemd unit (bare OCX_SERVICE=1):
ocx disconnect # today: disconnected, runtime still a client, exit 0
ocx restart # today: refused while the unit is viable ("Its background service owns it")
# Desired: one documented outcome per owner, with exit codes a Home can act on.Alternatives or workarounds
ocx service repair rewrites a legacy unit with the managed marker, after which #8529's manager path applies. A companion proxy can be stopped with ocx stop and started again with ocx start.
Additional context
Split out of #8529 by the coordinator after final review round 4; the removed attempt (owner-unproven verdict, companion marker, CLI-side forced stop) is in that PR's history (707b92b, c4b83b6, reverted in fc57a28 and b720710). Needs its own design review before code.
Checks
- I searched existing issues and documentation.
- This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
- I removed secrets and personal data.
Activity
coderabbitai commentedon Nov 11, 2026
🔗 Related PRs
#7484 - feat(link): parity on apply — restart the Child runtime, record and report the apply (OCX-6) [merged]
#8529 - fix(link): Children owned by the desktop app or a service can leave and re-link from the Home [open]
#7530 - [WRONG BRANCH] release: promote dev 1025acd to main as 2.91.0 [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
lidge-jun commentedon Nov 11, 2026
Two decisions from #8529's final review round 5 that this design inherits:
ocx disconnectcannot classify the client runtime (the liveness probe is inconclusive, or the live runtime's record cannot be captured), it exits 1 before sending any request. origin/dev returned the disconnect's exit 0 there. The exit 1 stops nothing and stops a Home's leave before its unconditionalocx restart, which would otherwise act on an unidentified runtime. A design here should decide whether a classified bare-flag runtime ever needs a different answer.restartOwner, andocx disconnecttreats them like a bare-flag runtime: no restart request, origin/dev's exit 0. Their own restart handler is still origin/dev's code, so an explicitocx restartagainst such a runtime keeps the legacy hazards listed above until it is restarted on a version with this design.lidge-jun commentedon Nov 11, 2026
Windows case from #8529's final review round 6: a legacy Windows registration whose child carries only
OCX_SERVICE=1can be recognised from its parent's command line (this install's Task Scheduler wrapper script, its launcher, or the WinSW host:isSupervisedServiceChildinsrc/service/service-child-ownership.ts). That is inference, not a current marker, so #8529 treats such a runtime as a bare flag: origin/dev's restart decision, no recorded owner, no disconnect restart. A design here should decide whether parent-command-line evidence can prove ownership, including a wrapper that is still running after its task was disabled. In that case the diagnostic is not viable, and origin/dev attempts an ordinary replacement restart under the still-running wrapper.lidge-jun commentedon Nov 11, 2026
Design input from #8529's final review round 7: an environment marker (
OCX_SERVICE_MANAGED, the Windows wrapper marker,OCX_DESKTOP_SUPERVISED) proves only that a process was started with it, not which manager or which service generation owns it. A shell a manager-owned runtime hands to a tool inherits its environment, so a proxy started from that shell, possibly in another OpenCodex home, carried the marker. #8529 now clears every supervisor marker at every detached runtime launch and tool shell (src/lib/supervisor-markers.ts). A design here should still bind ownership evidence to the manager and the launch generation, and not to an environment value alone.