Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c4456bd
feat(service): add checked Windows stop primitives and operations, un…
lidge-jun Nov 11, 2026
7279152
fix(service): floor fractional timeouts in the terminate fallback
lidge-jun Nov 11, 2026
b5d9910
test(service): pin each fail-closed rule of the checked Windows stop
lidge-jun Nov 11, 2026
029ff62
docs(service): record the ARM64 decision and the uncapped steps
lidge-jun Nov 11, 2026
ac48e6a
Merge remote-tracking branch 'origin/dev' into feat/windows-checked-s…
lidge-jun Nov 11, 2026
07e08dd
feat(service): wire the checked Windows stop into every stop path
lidge-jun Nov 11, 2026
990e973
Merge remote-tracking branch 'origin/dev' into feat/windows-checked-s…
lidge-jun Nov 11, 2026
8d172a7
fix(service): the updater re-checks its authority right before each m…
lidge-jun Nov 11, 2026
2082b45
refactor(service): move commandLineHasPathToken to a leaf module to b…
lidge-jun Nov 11, 2026
a9ef1c8
fix(service): the own-wrapper refusal is unknown even with nothing re…
lidge-jun Nov 12, 2026
398b727
fix(cli): Part B owns the port wait on the guarded and uninstall path…
lidge-jun Nov 12, 2026
3c560d9
fix(service): removal claims its teardown receipt before the child stop
lidge-jun Nov 12, 2026
968290c
fix(service): a not-running task promotes its survivors only once it …
lidge-jun Nov 12, 2026
7c2ec3e
fix(service): an unreadable listener identity is unknown, not foreign
lidge-jun Nov 12, 2026
d2fc61e
fix(service): the Part B port sample honors its timeout
lidge-jun Nov 12, 2026
34c3c38
fix(service): orphan discovery takes the cap its caller admitted
lidge-jun Nov 12, 2026
205e9ad
fix(service): only ocx stop measures its deadline from command start
lidge-jun Nov 12, 2026
f7219cf
test(cli): uninstall's authorization expectations name their platform
lidge-jun Nov 12, 2026
9f14ef8
docs(windows): record the wiring review's timing and evidence rules
lidge-jun Nov 12, 2026
da2d557
fix(cli): uninstall's port wait asks the port-wait question
lidge-jun Nov 12, 2026
e7f11f8
test(service): pin the not-running definition check with a bindable tree
lidge-jun Nov 12, 2026
0fd4870
Merge remote-tracking branch 'origin/dev' into review-fix-5/windows-c…
lidge-jun Nov 12, 2026
d5516a4
fix(cli): uninstall's proxy stop claims the teardown receipt first
lidge-jun Nov 12, 2026
29419fd
fix(cli): uninstall never forces a stop its Part A could not prove
lidge-jun Nov 12, 2026
940bc11
fix(service): an absent Part A is not proof the proxy is down
lidge-jun Nov 12, 2026
61087d8
test(service): stop the absent-Part-A caller test waiting on the real…
lidge-jun Nov 12, 2026
3608136
fix(service): install preparation resolves the stop port before the t…
lidge-jun Nov 12, 2026
3e9e4c1
fix(service): Part A proves the WinSW registration is this home's bef…
lidge-jun Nov 12, 2026
29f862d
fix(server): clear only an exited owner's TCP rows on the stopped port
lidge-jun Nov 12, 2026
8319c31
docs: name repair as the remaining stop exception and record the wiri…
lidge-jun Nov 12, 2026
70dd91c
test(service): follow the uninstall stop into its helper when pinning…
lidge-jun Nov 12, 2026
399e781
fix(service): the WinSW ownership read shares one cap with the state …
lidge-jun Nov 12, 2026
1f7381b
fix(lib): keep stopProxy's forced kill a literal killProxy(pid) call
lidge-jun Nov 12, 2026
65840a9
fix(cli): resolve the uninstall stop target before claiming its receipt
lidge-jun Nov 12, 2026
1f2b9dd
fix(lib): make an unconfirmed stop's port wait observational
lidge-jun Nov 12, 2026
ac285b2
fix(service): prove an absent-Part-A endpoint down before deleting th…
lidge-jun Nov 12, 2026
1fd6c72
fix(cli): gate Windows service removal on a proven stop and re-check …
lidge-jun Nov 12, 2026
ad343f6
docs(windows): record the uninstall stop wiring and removal gates
lidge-jun Nov 12, 2026
1ce8e8f
refactor(cli): keep the uninstall removal gate out of src/cli/index.ts
lidge-jun Nov 12, 2026
a10c079
fix(cli): prove the endpoint down when the uninstall stop is unconfirmed
lidge-jun Nov 13, 2026
ff2e276
fix(service): prove every candidate endpoint before Windows removal
lidge-jun Nov 13, 2026
56d563a
fix(service): probe Windows removal before deleting anything
lidge-jun Nov 13, 2026
ceafcad
test(lib): spawn the runtime's own binary for pid-taking stop tests
lidge-jun Nov 13, 2026
f2e42c0
fix(service): expose the candidate-endpoint seam on the removal stop'…
lidge-jun Nov 13, 2026
18d2183
fix(service): claim the install's teardown receipt before the stopped…
lidge-jun Nov 13, 2026
e01a973
fix(service): prove every endpoint down when install preparation find…
lidge-jun Nov 13, 2026
72e2f6c
docs(structure): record the round-9 checked-stop wiring decisions
lidge-jun Nov 13, 2026
d0110a1
Hold Windows child stop when Part A throws
lidge-jun Nov 13, 2026
2626100
Refuse Windows stop when teardown receipt fails
lidge-jun Nov 13, 2026
5474c6d
Check wrappers for WinSW-only stops
lidge-jun Nov 13, 2026
9cff82d
Guard updater WinSW stops with checked stop
lidge-jun Nov 13, 2026
3993358
Require dead endpoint proof before service restore
lidge-jun Nov 13, 2026
b9c70a5
Bind service stop receipts to resolved proxy targets
lidge-jun Nov 13, 2026
18a0787
Align Windows stop design notes with checked behavior
lidge-jun Nov 13, 2026
348e737
Merge remote-tracking branch 'origin/dev' into review-fix-10/windows-…
invalid-email-address Nov 13, 2026
b8c24aa
fix(cli): an absent-Part-A uninstall proves every candidate endpoint …
invalid-email-address Nov 13, 2026
5b816d6
docs(service): describe every receipt writer and the all-endpoint proof
invalid-email-address Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion devlog/_plan/261111_windows_service_stop/000_plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 000 — Windows service stop that frees the port (#7771 Option B): design

Status: approved (030, bounded ARM64 fallback). Implementation is split into three pull requests (060); the primitives and the two operations land first, unwired.
Status: approved (030). Implementation is split into three pull requests (060): PR1 (primitives and the two operations, #8564) and PR3 (Desktop, #8560) have landed; PR2 wires every caller. ARM64 ships the strict variant until the 040 ARM64 device case runs (050, 060).

## Problem

Expand Down
26 changes: 15 additions & 11 deletions devlog/_plan/261111_windows_service_stop/030_design.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- `ocx uninstall` runs part B after a part A `stopped` and keeps client configuration on any other outcome; service uninstall/install refuse before deleting/replacing the task on a non-stopped outcome.
- `/api/stop` with a receipt skips part A; the whole consented stop fits the stated budget with injected clocks.
- Guarded path: a stray wrapper bound by the rule is `bound-stray` and reaches part A; an unbindable one stays approval-changed.
- Output mapping per path: `ServiceStopOutcome`, `ocx-stop/1` (no new outcome/proxy enum values; additive fields only), `/api/stop` codes, Desktop parser unit tests with old and new documents; updater variant refuses without authorization at entry and before each terminate.
- Output mapping per path: `ServiceStopOutcome`, `ocx-stop/1` (no new outcome/proxy enum values; additive fields only), `/api/stop` codes, Desktop parser unit tests with old and new documents; updater variant refuses without authorization at entry, before each manager stop and before each terminate.
- Plain `ocx stop`: part A `denied`, `survived` and `unknown` each send no `/api/stop` POST, set `ownershipBlocked`, skip the shared restore, and record `outcome: failed`, `proxy: unknown`, `sharedTeardown: skipped`.
- `absent`: no registered task or WinSW service and no bound wrapper → `ServiceStopOutcome` `absent`, no "Service manager stopped.", the POST is sent, part B does not run (no 6 s hold); the same with an unreadable process list → still `absent`, and plain `ocx stop` still stops the proxy.
- Budget, with injected clocks: admission refuses with `unverified-budget` and changes nothing (no `/end`, terminate or POST) when the remaining time is below the worst case still to come; once admitted, every step at its cap ends before the deadline; the graceful exit wait is never shortened; part B never gets less than 6 s; `--deadline-ms` from both Desktop paths is honored, and a missing flag means 60 s; macOS/Linux ignore it.
Expand Down
52 changes: 48 additions & 4 deletions devlog/_plan/261111_windows_service_stop/060_implementation.md

Large diffs are not rendered by default.

42 changes: 41 additions & 1 deletion docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ ocx start --port 10100 --socks5
ocx start --socks5-off
```

### `ocx stop [--json]`
### `ocx stop [--json] [--deadline-ms <ms>]`

An unsupervised Link Child handling a shutdown signal restores native Codex and managed Grok before releasing its listener, rechecking the served connection before each write. Service/Desktop supervision, sibling ownership, restart/recycle handoff and pending parent stop receipts preserve integrations. Shutdown rejects bound restart with `409 runtime_stopping`. Forced termination can bypass signal handlers; parent `ocx stop` restoration remains necessary. To route Codex through Home again after such a stop, start the Child (`ocx start`, or `ocx service start` for an installed service) and then run `ocx sync --restart-codex`. Starting alone does not reapply routing. A sync run while the Child is stopped still reapplies routing, which points Codex at a listener that is not running, so start the Child first.

Expand Down Expand Up @@ -141,6 +141,46 @@ and its catalog to the restarted proxy instead of restoring native Codex. With 8
down, and the restarted proxy releases the kept teardown after its first sync; if it never starts,
the next `ocx stop` restores native Codex.

#### Windows service stop

On Windows, `ocx stop` stops a registered Task Scheduler task or native WinSW service with a
checked stop. It ends the task, then ends only the `wscript.exe` and `cmd.exe` wrapper processes
it can tie to this installation (this home's launcher or service script, a parent chain that leads
to the proxy or to the Task Scheduler service, and a recorded start time), and waits for them to
exit. Only then does it stop the proxy gracefully. Afterwards it confirms that the port stays free
for about six seconds and that no wrapper started again, so a wrapper's restart loop cannot bring
the proxy back behind a successful stop.

When the service cannot be proven stopped, `ocx stop` sends no stop request to the proxy, leaves
your client configuration alone, and fails. With `--json` the summary keeps `outcome: "failed"`
and `proxy: "unknown"`, and adds `serviceStopReason`:

| `serviceStopReason` | Meaning | What to do |
|---|---|---|
| `access-denied` | Ending the task or a wrapper was denied, usually an elevated task stopped from an unelevated terminal | Run `ocx stop` from an elevated terminal |
| `unbound-wrapper` | A wrapper running this home's service script could not be tied to the service, for example one started by hand | End that `wscript.exe`/`cmd.exe`, then retry |
| `evidence-unreadable` | The service or process state could not be read, or the port could not be confirmed free | Check `ocx service status`, then retry |
| `unverified-budget` | The time left could not hold a verified stop, so nothing was changed | Retry from a terminal |

If another program is listening on the port after this installation's processes are gone, the
stop still succeeds and `--json` reports `portHeldByForeign: true`. Both fields are additive and
no new `outcome` value exists. `serviceStopReason` is omitted when there is nothing to report;
`portHeldByForeign` appears (`true` or `false`) whenever the port check ran, and is omitted otherwise.

`--deadline-ms <ms>` bounds the whole stop, measured from the moment the command started. Before
changing anything the stop checks that the time left can hold its worst case (about 57 seconds for
a registered Task Scheduler service with default settings) and otherwise refuses with
`unverified-budget`. Without the flag the stop reads `OCX_STOP_DEADLINE_MS` (the desktop app sets
it) and otherwise uses 60 seconds. macOS and Linux ignore both.

The stop ends the running instance only. The task's logon and unlock triggers stay enabled, so the
proxy comes back at the next logon or unlock; use `ocx service uninstall` to remove the service.
`ocx service stop`, `ocx service uninstall`, `ocx uninstall` and `ocx service install` use the
same checked stop: uninstall and install refuse before deleting or replacing the task when the
service cannot be proven stopped, and install also stops when another program holds the port. On
Windows on Arm the wrappers cannot be ended yet, so a stop that would need to end one reports
`evidence-unreadable`; end the wrapper by hand, then retry.

For a desktop-approved takeover, `ocx stop --json` also accepts the complete set of
`--expect-*` values supplied by `ocx resolve --json`. On Windows, this guarded stop can
stop a Task Scheduler task only when its registered definition, running state, and wrapper
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ the 503 without reporting model capacity; provider overload errors retain their
| --- | --- | --- |
| `GET /api/system/memory` | Return scalar process, heap, stream, response-state, watchdog, and active-turn metrics. Response-state diagnostics include spill-write status, consecutive failures, fixed privacy-safe failure class, and last failure/success timestamps. `spillLastWriteFailureOrigin` is `retry_returned_timeout`, `timeout_memo_refusal`, or null; cumulative `spillAclRetryReturnedTimeouts` and `spillAclTimeoutMemoRefusals` count terminal failed publications; cumulative `spillCapacityRefusals` and `spillHeadroomEvictions` count continuations refused at the spill ceiling and continuations evicted to make room for a newer one. See [Windows spill diagnostics](/troubleshooting/windows-memory/) for process-local semantics. Raw errors and paths are never returned. | — |
| `POST /api/system/restart` | Begin a drain-aware process restart without removing client injection; optional JSON `drainGraceMs` explicitly shortens the default 60s grace | Returns 202; repeated calls report the existing grace; 400 invalid body or grace |
| `POST /api/stop` | Stop the service, restore native Codex, remove managed Grok injection, and drain the proxy | 409 service ownership conflict; 409 `respawnable_service` when a Windows Task Scheduler wrapper could respawn the proxy and the caller is not `ocx stop` (nothing is changed); 409 `self_unload_service` when this proxy is running as the installed launchd/systemd service, because stopping the manager from inside it would end the process before native Codex is restored — run `ocx stop` instead (nothing is changed); 409 when the installed manager refuses to stop; 409 `service_state_unknown` when the Task Scheduler state cannot be read (nothing is changed; repair the query and retry) |
| `POST /api/stop` | Stop the service, restore native Codex, remove managed Grok injection, and drain the proxy | 409 service ownership conflict; 409 `respawnable_service` when a Windows Task Scheduler wrapper could respawn the proxy and the caller is not `ocx stop` (nothing is changed); 409 `self_unload_service` when this proxy is running as the installed launchd/systemd service, because stopping the manager from inside it would end the process before native Codex is restored — run `ocx stop` instead (nothing is changed); 409 when the installed manager refuses to stop; 409 `service_state_unknown` when the Task Scheduler state cannot be read (nothing is changed; repair the query and retry), or when the Windows checked service stop does not finish within the route's cap (6 s; 4 s for a leftover wrapper of a deleted task). On Windows a request from `ocx stop` whose receipt records that the CLI already ran the checked service stop does not repeat it |
| `GET /api/system/codex-app-server` | Report whether running Codex app-servers predate the current model catalog | — |
| `POST /api/system/codex-restart` | Refresh the catalog, then restart Codex app-servers. A running Codex desktop app is given a restart attempt so the model picker can reload; the `desktopApp` response fields report a refusal, surviving targets, or a failed relaunch. A shell newer than the catalog is still reported as `desktopApp.pickerState: "unknown"`, because its start time cannot prove which `model_catalog_json` binding it loaded; an earlier sync may have rebound an already-existing catalog. A shell no newer than the catalog, or one whose binding this request changed, is `stale`. A closed desktop returns `desktopApp.reason: "no_targets"`; its `pickerState` may be `not_running`, or `stale` when this request changed the catalog binding. When the proxy itself is running inside the Codex app, the desktop restart is refused rather than handed off. | Returns 200 with `code: partially_stopped` when a target survives |

Expand Down
4 changes: 2 additions & 2 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1925,8 +1925,8 @@
"windows-deploy-close-regressions.test.ts": "windows",
"windows-elevation-spawn.test.ts": "windows",
"windows-elevation.test.ts": "windows",
"windows-guarded-manager.test.ts": "windows", "windows-checked-stop.test.ts": "windows", "windows-stop-binding.test.ts": "windows",
"windows-popup-fix.test.ts": "windows", "windows-stop-confirm.test.ts": "windows", "windows-process-evidence.test.ts": "windows",
"windows-guarded-manager.test.ts": "windows", "windows-checked-stop.test.ts": "windows", "windows-stop-binding.test.ts": "windows", "windows-checked-stop-live.test.ts": "windows", "windows-service-stop-wiring.test.ts": "windows",
"windows-popup-fix.test.ts": "windows", "windows-stop-confirm.test.ts": "windows", "windows-process-evidence.test.ts": "windows", "windows-stop-callers.test.ts": "windows", "windows-stop-ocx-stop.test.ts": "windows",
"windows-process-priority.test.ts": "windows",
"windows-scheduler-errors.test.ts": "windows", "windows-scheduler-install-verification.test.ts": "windows",
"windows-lifecycle-root-acl.test.ts": "windows",
Expand Down
5 changes: 4 additions & 1 deletion src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
import { isJsonOption, takeFlag } from "./runtime-api";
import { printStopSummary, type StopOutcome } from "./stop-report";
import { parseStopApproval, type StopApproval } from "./stop-approval";
import { resolveStopDeadlineMs, setStopDeadlineMs } from "../service/windows-stop-deadline";
import type { ResolveArgs } from "./resolve";
import type { ClientConnectionState } from "../client/state";
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
Expand Down Expand Up @@ -111,9 +112,11 @@ const commandRunners: Record<string, CommandRunner> = {
stop: async deps => {
const parsed = parseStopApproval(deps.args.slice(1));
if (!parsed.ok) {
console.error("Usage: ocx stop [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
console.error("Usage: ocx stop [--deadline-ms <ms>] [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
return 64;
}
// #7771: the Windows checked stop's deadline, from this process's start (flag, then env, then 60 s).
setStopDeadlineMs(resolveStopDeadlineMs(parsed.deadlineMs));
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
// re-start the proxy immediately, so warning there would contradict the next line.
const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
Expand Down
Loading
Loading