Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixed

- Prefer PowerShell 7 for model-selected PowerShell commands on Windows, never
downgrade an explicit `pwsh` request to Windows PowerShell 5.1, and use
`cmd.exe` as the implicit fallback when PowerShell 7 is unavailable.
- Run the persistent Windows service launcher in Codexify instead of
`powershell.exe`, containing and restarting the native supervisor independently
of user PowerShell processes. Migrate recognized encoded and file-based legacy
launchers while preserving custom Scheduled Task actions.
- Preserve text-only MCP results in response envelopes while bounding the
generated structured text and retaining upstream metadata and full user notices.

Expand Down
29 changes: 18 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -807,9 +807,12 @@ commands manage one
per-user native definition: an XDG-aware systemd user unit on Linux, a launchd agent on
macOS, or an at-logon Scheduled Task on Windows. Installation stores the current
executable path and the selected config path as absolute arguments. The hidden
`service run` command is the native manager's entry point. The updater also uses
the hidden `service wait-ready` command to poll the loopback health endpoint under
a deadline after a restart.
`service run` command is the systemd/launchd entry point and the supervised
server runner. On Windows, the Scheduled Task enters through hidden `service
launch --scheduled-task`, which detaches the task console before its native
restart loop starts `service run`. The updater also uses
the hidden `service wait-ready` command to poll the loopback health endpoint
under a deadline after a restart.

`service status [--json]` reuses the native `service::status()` inspection used by
Doctor, without loading `AppConfig` or probing server health. Its human and JSON
Expand All @@ -822,17 +825,21 @@ The service runner does not construct a second server configuration. It waits if
the selected file does not exist, then starts the ordinary Codexify executable
with `--config <absolute-path>`. Child stdout and stderr are serialized into a
private 10 MiB log with five numbered generations. Child failures use bounded
exponential restart backoff; systemd, launchd, and Task Scheduler separately
restart the runner if the runner itself fails. On shutdown, the runner terminates
the server process group/tree before exiting. Windows children are assigned to a
kill-on-close Job Object so Task Scheduler termination cannot orphan the server
or its descendants.
exponential restart backoff; systemd and launchd restart the runner if it fails.
On Windows, the native launcher applies the same bounded restart policy to the
runner, while Task Scheduler restarts the launcher itself. On shutdown, each
layer terminates its child process group/tree before exiting. The Windows
launcher and runner use nested kill-on-close Job Objects so Task Scheduler
termination cannot orphan the runner, server, or their descendants.

`start`, `stop`, and `restart` preserve the manager's enablement state. The older
`enable` and `disable` commands intentionally combine enablement and runtime
changes. On Windows, the scheduled action launches the fixed supervisor command
through the active PowerShell engine with a hidden window and Interactive logon;
the supervisor sets `CREATE_NO_WINDOW` on its server child as a second boundary.
directly through `codexify.exe service launch --scheduled-task` with Interactive
logon. The native
launcher starts the supervisor with `CREATE_NO_WINDOW`, and the supervisor sets
the same flag on its server child. Start/restart/enable migrate exact recognized
PowerShell launchers from older releases; custom actions are not rewritten.

Service-supervised tracing is emitted without terminal escapes. The public log
viewer reassembles partial lines, strips escapes from older files, highlights
Expand Down Expand Up @@ -987,7 +994,7 @@ the original order and rejects duplicate names.
| `project_clone.rs` | Strict provider-agnostic HTTPS/SSH Git repository URL parsing plus GitHub branch/PR/commit target parsing, conservative normalized remote matching, existing-checkout discovery, exact GitHub target-ref or object-ID fetching, bounded non-interactive cloning below `projectCloneDir`, cross-process repository locks, collision refusal, and post-clone verification. |
| `worktrees.rs` | Per-task managed Git worktree lifecycle: create a detached checkout under `worktrees.root` via `git worktree add`, optionally at an exact fetched commit, dual source/worktree root tracking, startup sweep bounded by `keepCount`, Windows `\\?\`-prefix handling, and the opt-in `allowSetupScript` gate for per-worktree environment setup. |
| `project_catalog.rs` | Live, read-only project discovery from native Codex plus explicit metadata; canonical access-root filtering, deduplication, deterministic query ranking, sanitized MCP warnings, and local diagnostics. |
| `exec_sessions.rs` | Generic-client project/scratch fallback plus task-owned unified-exec sessions and transport-local diff state: private persistent scratch storage with transport-local bindings, trusted configured-shell resolution, Codex-compatible model shell-type selection by basename, PowerShell exit-code wrapping, background stdout/stderr drain tasks, process-group kill, idle cleanup, and output truncation (UTF-16 units to match the TS). |
| `exec_sessions.rs` | Generic-client project/scratch fallback plus task-owned unified-exec sessions and transport-local diff state: private persistent scratch storage with transport-local bindings, trusted configured-shell resolution, Codex-compatible model shell-type selection by basename, strict PowerShell 7 requests and legacy-host avoidance on Windows, PowerShell exit-code wrapping, background stdout/stderr drain tasks, process-group kill, idle cleanup, and output truncation (UTF-16 units to match the TS). |
| `diff.rs` | Project-scoped Git snapshots, persistent task refs, transport-local fallbacks, incremental comparisons whose emitted snapshot can advance the private diff cursor through compare-and-swap, legacy review-ref migration, diff parsing and component-payload budgets. |
| `diff_ui.rs` | Embedded MCP Apps resource, component-only diff-result metadata, legacy review-card compatibility, and persisted private interaction state for the interactive `show_diff` card. |
| `setup_ui.rs` / `setup_ui.html` | Compact setup/status MCP App with a searchable project/scratch chooser, selected direct/worktree/scratch path rendering, full-task continuation token generation, retired-owner read-only state, cached-bypass update checks, background structured doctor diagnostics, direct connector-settings Refresh routing, conversational Autofix, and debug timing display. |
Expand Down
76 changes: 51 additions & 25 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,21 +683,33 @@ trust state, warnings, and diagnostics. `--json`, redirected output, and
## Background service

The installation scripts register a per-user native service, start it
immediately, and enable it for subsequent user logins. The service definition
invokes the installed executable with an absolute config path:
immediately, and enable it for subsequent user logins. Linux and macOS invoke
the installed executable with an absolute config path:

```text
codexify service run --config /absolute/path/to/codexify.config.json
```

The Windows Scheduled Task instead invokes the hidden native launcher:

```text
codexify service launch --scheduled-task --config C:\absolute\path\to\codexify.config.json
```

That long-lived launcher starts `service run`, records its exit, and restarts it
with bounded backoff. Keeping the persistent task action native prevents a user
PowerShell process and the service launcher from sharing the same host or
failure domain.

The hidden `service run` command supervises the ordinary Codexify server. It
waits when the config file has not been created yet, restarts a failed server
with bounded exponential backoff, and forwards stdout and stderr into
`~/.codexify/logs/codexify.log`. The current log rotates at 10 MiB, with five
numbered generations retained. The native service manager also restarts the
supervisor if the supervisor itself fails. On Windows, the supervised server is
contained in a kill-on-close Job Object so stopping the scheduled task cannot
leave its process tree behind.
numbered generations retained. The native service manager also restarts its
entry point if that process fails. On Windows, the launcher contains the native
supervisor tree in a kill-on-close Job Object, and the supervisor separately
contains the server tree, so stopping the Scheduled Task cannot leave either
layer or its descendants behind.

Persisted log files remain plain text. `codexify service logs` applies terminal
colors only while displaying them, strips legacy ANSI control sequences, moves
Expand Down Expand Up @@ -733,15 +745,18 @@ codexify service remove
enable or disable login startup. Use `enable` to enable and start the definition,
or `disable` to stop it and disable future automatic startup.

On Windows, the Task Scheduler action starts the supervisor through the current
PowerShell host with `-WindowStyle Hidden -File` and a private UTF-8 launcher at
`~/.codexify/service/supervisor.ps1`. Registration uses the adjacent `install.ps1`;
registration failures include both paths so the exact definition can be inspected.
Neither script contains the configuration's credentials. Management helpers and
diagnostic probes use isolated consoles rather than the foreground server's.
The supervisor also creates its ordinary server child with `CREATE_NO_WINDOW`. This
prevents the otherwise empty console window without changing the Interactive
logon principal required for tunnel and network access.
On Windows, the Task Scheduler action points directly at `codexify.exe service
launch --scheduled-task`; the private marker detaches the task-launched console
before the asynchronous runtime starts, and no persistent PowerShell process is
part of the service tree.
Registration still uses the private UTF-8
`~/.codexify/service/install.ps1` as a one-shot management helper, and that file
contains no configuration credentials. Management helpers and diagnostic probes
use isolated consoles rather than the foreground server's. The native launcher
creates the supervisor with `CREATE_NO_WINDOW`, and the supervisor applies the
same boundary to its ordinary server child. This prevents empty console windows
without changing the Interactive logon principal required for tunnel and
network access.

The Windows supervisor begins liveness monitoring after the server first answers
`/health` successfully. Six consecutive failed probes (with a five-second pause
Expand All @@ -755,14 +770,14 @@ panic markers to the service log with their process ID. Normal Ctrl+C delivery
and server returns are logged separately. Forced termination can bypass these
records; the absence of a marker does not establish a crash cause.

The scheduled-task launcher waits for the supervisor's process handle and records
its exit code. It restarts unexpectedly terminated supervisors, including code
zero, with a two-to-sixty-second backoff, independently of Task Scheduler's
failure retry behavior for on-demand starts. Explicit `service stop` and
`service disable` remain stopped.
Recognized older launchers are upgraded by `service start`, `restart`, or `enable`,
including the `enable` call made by older self-update workers; custom task actions
are preserved.
The native Scheduled Task launcher waits for the supervisor's process handle and
records its exit code. It restarts unexpectedly terminated supervisors,
including code zero, with a two-to-sixty-second backoff, independently of Task
Scheduler's failure retry behavior for on-demand starts. Explicit `service stop`
and `service disable` remain stopped. `service start`, `restart`, or `enable`
upgrades exact recognized encoded and file-based PowerShell launchers to the
native action, including the `enable` call made by older self-update workers;
modified and custom task actions are preserved.

`service status` queries the native service manager without loading the server
configuration, starting the MCP server, or changing the service. It works even
Expand Down Expand Up @@ -1555,7 +1570,7 @@ The `exec` block governs resident-process resources and the default shell for
|-----|---------|-------------|
| `maxSessions` | `8` | Cap on concurrent background sessions per ChatGPT conversation, or per MCP transport for clients without conversation metadata |
| `idleTimeoutMs` | `300000` | Milliseconds without a tool interaction before a resident process is killed and forgotten; `0` disables idle expiry |
| `defaultShell` | `$SHELL`, else PowerShell on Windows and `/bin/sh` elsewhere | Shell used when an `exec_command` call names none |
| `defaultShell` | `$SHELL`; otherwise PowerShell 7 then `cmd.exe` on Windows, and `/bin/sh` elsewhere | Shell used when an `exec_command` call names none |

`exec_command` does not tokenize, filter, or allow-list commands. Its `cmd` value
is passed to the selected shell as written after ordinary input and working-directory
Expand Down Expand Up @@ -3092,7 +3107,18 @@ Windows, macOS and Linux are supported natively. Which shell runs is decided by
| `powershell`, `pwsh` | `<shell> -NoProfile -Command "<cmd>"` |
| `cmd` | `cmd /c "<cmd>"` |

The default comes from `$SHELL` on every platform, so starting the server from Git Bash on Windows gets bash — with real `ls -la`, pipes and `$VAR` — rather than PowerShell. Set `exec.defaultShell` to override, or pass `shell` on an individual `exec_command` call.
The default comes from `$SHELL` on every platform, so starting the server from
Git Bash on Windows gets bash — with real `ls -la`, pipes and `$VAR` — rather
than PowerShell. Without that explicit environment choice, Windows selects
PowerShell 7 from `PATH` or its standard installation directory and falls back
to `cmd.exe`; it never implicitly selects Windows PowerShell 5.1. Set
`exec.defaultShell` to override, or pass `shell` on an individual
`exec_command` call.

On Windows, an explicit `pwsh` request is strict and never downgrades to
`powershell.exe`. The broader `powershell` name prefers PowerShell 7 and retains
Windows PowerShell 5.1 only as a compatibility fallback. A configured absolute
`exec.defaultShell` remains authoritative.

Two Windows-specific details are handled: `powershell -Command` collapses every non-zero child exit code to `1`, so commands are wrapped to re-raise `$LASTEXITCODE`; and `exec_command`'s description gains Codex's PowerShell rules (`-LiteralPath` over `-Path`, `-WindowStyle Hidden`) when the server runs there.

Expand Down
30 changes: 30 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ pub enum ServiceCommand {
/// Run the service supervisor under the native service manager.
#[command(hide = true)]
Run,
/// Run the persistent Windows task launcher.
#[command(hide = true)]
Launch(ServiceLaunchArgs),
/// Wait until the restarted service and its tunnel are ready.
#[command(hide = true)]
WaitReady(ServiceWaitReadyArgs),
Expand All @@ -334,6 +337,12 @@ pub struct ServiceStatusArgs {
pub json: bool,
}

#[derive(Args, Debug)]
pub struct ServiceLaunchArgs {
#[arg(long, hide = true)]
pub scheduled_task: bool,
}

#[derive(Args, Debug)]
pub struct ServiceLogsArgs {
/// Continue printing new log output until interrupted.
Expand Down Expand Up @@ -3732,6 +3741,27 @@ mod tests {
));
}

#[test]
fn service_launcher_cli_accepts_definition_argument_order() {
let parsed = Cli::try_parse_from([
"codexify",
"service",
"launch",
"--scheduled-task",
"--config",
"/tmp/codexify.config.json",
])
.unwrap();
assert_eq!(parsed.config.as_deref(), Some("/tmp/codexify.config.json"));
assert!(matches!(
parsed.command,
Some(CliCommand::Service {
command: ServiceCommand::Launch(args)
})
if args.scheduled_task
));
}

#[test]
fn existing_server_cli_syntax_remains_valid() {
let parsed = Cli::try_parse_from([
Expand Down
Loading
Loading