Skip to content

feat(service): minimal Windows SCM service host prototype - #8

Draft
nullStack65 wants to merge 7 commits into
mainfrom
feat/windows-service-host-prototype-20260926
Draft

nullStack65 wants to merge 7 commits into
mainfrom
feat/windows-service-host-prototype-20260926

Conversation

@nullStack65

@nullStack65 nullStack65 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

T3 Code has no native Windows background service. BootService supports Linux systemd user units and macOS LaunchAgents and returns unsupported for Windows; a desktop-owned child or a WSL systemd unit is a different lifecycle, not SCM support. Registering a console t3 serve with sc.exe is not a service either.

How

Add native/windows-service-host/**, a small T3-owned SCM host prototype, plus its design in docs/internals/windows-background-service.md.

  • Real SCM contract: StartServiceCtrlDispatcherW, ServiceMain, RegisterServiceCtrlHandlerExW, and SetServiceStatus with START/RUNNING/STOP_PENDING/STOPPED, checkpoints and wait hints. The control handler only records intent and wakes the supervisor, so control handling never blocks.
  • Existing launcher authority: the production child is the existing pinned t3.exe __service-launcher. The host never manages updates or rollback.
  • Identity/launch: explicit canonical --home and pinned --runtime required, correct command-line/argv handling, child stdout/stderr to --log, T3CODE_HOME set, credential arguments refused, LocalSystem refused by default, optional expected-account enforcement.
  • Ownership: the child is created suspended, assigned to a kill-on-close job object, then resumed. Admission is checked and unwound; if a created process cannot be admitted and its reclaim is not confirmed, the failure carries that cleanup outcome structurally and the host stops without a second spawn rather than retrying into a possible orphan. A graceful/forced stop while the root may still be alive verifies the recorded PID + creation time, so stale/foreign PIDs are never cleaned up and a failed query is unknown, not "stopped"; whole-job cleanup after the root exits uses the retained job handle instead.
  • Bounded stop: stop marker + graceful request, checkpoints, then force-terminate the owned tree at the drain deadline. Whole-service stop is kept distinct from the launcher's normal child replacement.
  • Whole-job completion: a root exit is not proof the tree is empty. Before finishing clean or starting a replacement the host reclaims the owned job through its retained handle and confirms emptiness with job accounting (ActiveProcesses) under a bounded drain; a still-nonempty job, an ineffective TerminateJobObject request or a failed query is never a confirmed stop. Drop is only a last-resort safeguard.
  • Honest failures: bounded restart budget, planned-stop vs unexpected-exit, repeated-failure specific exit code, slow drain, stale/foreign PID, and a recovery-required specific code for an unconfirmed created-process or owned-job cleanup.

The portable config/control/restart-budget/drain core is unit tested; the Windows SCM and job-object module is cross-compiled with cargo check --target x86_64-pc-windows-msvc.

Not in this PR

BootService/service CLI (R6-T3-STATUS), serviceLauncher.ts, root Cargo/package workspaces, the existing resource monitor, desktop/WSL lifecycle and release plumbing are untouched. The design doc specifies the exact launcher control adaptation (a parent stop IPC message plus graceful child shutdown over the existing IPC) and the BootService scm adapter recipe for a later single integration owner. Also specified: account constraints, artifact/provenance inputs, rollback, and the unexecuted native SCM acceptance checklist.

Windows background support remains disabled and unqualified until the host, launcher adaptation, adapter and packaged artifacts are joined and run against real SCM.

Model: openrouter/deepseek/deepseek-v4.1-flash · Harness: T3 Code (OpenCode)

Add native/windows-service-host, a T3-owned SCM entry point that runs the
pinned t3.exe __service-launcher under a job object and reports service
state. The portable config/control/restart-budget/drain core is unit tested;
the SCM dispatch and job-object tree are Windows-only and cross-checked.

This is a prototype only: the launcher control adaptation and BootService
adapter remain a later single integration owner, and Windows background
support stays unqualified until a real SCM run is executed.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 27, 2026
@nullStack65

Copy link
Copy Markdown
Owner Author

ENV-1:R6-WIN-SERVICE — RESULT

Identity: ENV-1:R6-WIN-SERVICE (fresh session, no continuation).
Hub START: https://github.com/nullStack65/closura-agent-config/pull/237#issuecomment-5851493505
Hub link: posted separately on pingdotgg#237.

Exact base / head (read back from remote)

Changed paths (15 files, +2978 / −0; all new)

native/windows-service-host/{Cargo.toml,Cargo.lock,README.md},
native/windows-service-host/src/{lib,config,control,host,run,supervise,main}.rs,
native/windows-service-host/src/windows/{mod,job,service}.rs,
native/windows-service-host/tests/portable_host.rs,
docs/internals/windows-background-service.md.

No other path changed: BootService/CLI, serviceLauncher.ts, root Cargo/package workspaces, native/resource-monitor, desktop/WSL, workflows and release plumbing are untouched.

Execution platform/capacity

macOS 26.6.2 (25G83), Intel x86_64, Crown-Rain-Gutters.local, 12 CPU / 8 GiB. Rust 1.98.1; added x86_64-pc-windows-msvc std target and rustfmt (additive components). No Windows SCM runtime and no reserved disposable Windows environment reachable, so SCM/job-object behavior is cross-compiled, not executed. No service was registered; no MSVC/SDK/WDAC/host-policy change; no R720 load.

Commands, counts, skips

Command Result
cargo test --locked (default) 33 unit passed, 1 integration passed, 0 failed, 0 skipped
cargo check --locked --target x86_64-pc-windows-msvc clean, no warnings
cargo check --locked --features test-child (host + windows target) clean
cargo fmt --manifest-path native/windows-service-host/Cargo.toml -- --check clean
cargo build --locked no warnings

Portable (executed here): config/argv/account parsing; SCM control mapping; stop/shutdown; unexpected exit; repeated-failure budget; budget-window expiry; delayed drain; forced termination; invalid launch config; stale/foreign/unknown identity gating; the run loop's refusal to touch foreign or unqueryable trees; and a real-subprocess CommandChildHost stop/terminate test.

Cross-compiled (not executed): the SCM FFI (StartServiceCtrlDispatcherW, ServiceMain, RegisterServiceCtrlHandlerExW, SetServiceStatus, GetUserNameW) and the job-object spawn (CreateProcessW suspended → AssignProcessToJobObject → ResumeThread).

Actual CI

At head 9b3bc5f5 the PR checks are queued (runner capacity; Rust, Test, Test Server 1–3, Release Smoke, Native fingerprint diff all pending after ~10 min). No CI result is claimed. The existing Rust job hardcodes resource-monitor kde-snap-shot hyprland-snap-shot, so it does not currently run this crate even when it completes; adding windows-service-host to that list is recorded in the design doc as a later integration touch, not changed here.

Synthetic-service cleanup

Not applicable: no disposable Windows environment was available, so no synthetic service was registered and none was left behind. The exact native recipe (unique service name, dummy children incl. a grandchild, LocalService account, bounded drain, sc.exe delete, temp root removal) is in the design doc and is unexecuted. No console-mode run is presented as SCM proof.

Focused cases → coverage

  • Stop/shutdown: control.rs + supervise::on_control + run forced-terminate test.
  • Unexpected child exit: unexpected_exit_restarts_within_budget.
  • Repeated failure: repeated_failure_exhausts_the_budget (+ window expiry).
  • Delayed drain: delayed_drain_forces_termination_once + real-subprocess test.
  • Invalid launch config: fatal_launch_configuration_does_not_restart + config parsing.
  • Path/argv handling: Windows drive/UNC absolutness, on-both-separators splitting, spaces/= forms, quoting (push_quoted).
  • No foreign-process cleanup: identity_verdicts_gate_cleanup + run-loop foreign/unknown tests.

Deliverables (all in docs/internals/windows-background-service.md)

  • Proposed BootService interface: add "scm" to the manager union + windowsManager(...) with an sc.exe create/config/start/stop/delete step set; render emits the host command (hostPath --home --runtime <activeVersion>/t3.exe --log --service-name [--expected-account]); selectBootServiceManager returns it for win32; status separates support/registration/enabled/observed-running.
  • Required launcher control: a parent→launcher {type:"stop"} IPC message handled like SIGTERM (writes the stop marker), plus graceful child shutdown over the launcher's existing child IPC because Windows has no POSIX signals. Not implemented here; serviceLauncher.ts/serviceProtocol.ts untouched.
  • Account constraints: dedicated account (prefer virtual NT SERVICE\T3Code), no LocalSystem default, no interactive-user profile/home, password only via sc.exe ... obj= password= (LSA-stored); profile-less-account limitations.
  • Artifact/provenance inputs: signed host binary + digest bound in ImagePath; pinned runtime archive/sentinel; sc.exe qc record; feat(release): fork release pipeline with fork update isolation #5 freeze untouched.
  • Rollback + native acceptance checklist: stop/delete or restore ImagePath; six-step native checklist; session-0/GUI/browser-auth/profile limits; NSSM/WinSW explicitly not adopted without a concrete comparison.

Remaining integration / native gates

  1. Launcher control adaptation (parent stop + graceful child shutdown).
  2. BootService scm adapter and sc.exe step set (R6-T3-STATUS owns those files).
  3. Packaging the host beside the pinned runtime and binding its digest.
  4. Add windows-service-host to the CI Rust crate list.
  5. Real SCM acceptance on a disposable Windows environment (unexecuted).
  6. Windows support remains disabled/unqualified until all of the above join and pass.

Source vs live

Source/portable and cross-compiled evidence only. No service was registered, no live T3/proxy/auth/network change, no merge or publication. pingdotgg#237 hub link follows.

Copy link
Copy Markdown
Owner Author

ENV-1 manager review — changes required before SCM integration

Reviewed head: 9b3bc5f50a545848a1d0af17fdad1019f900591a, base bcc1a58b19a9d610a4f08fed191a364767bc65b3. Sole hub: ENV-1 #237. Preserve this PR and its narrow new-subtree scope.

Credit R6 RESULT 5851553826: 33 portable unit tests, one real-subprocess test, Windows-target cargo check and formatting are reported. No native SCM run was reported. This manager inspected the actual portable loop, Windows FFI implementation, configuration and design; it did not run cargo or Windows. Current CI 36284611275 is queued, not a pass.

F1 — actual SCM lifecycle does not match the internal state (blocking)

Supervisor::on_child_spawned changes only the internal state to Running. execute(SpawnChild) does not emit a Running report, and normal Running ticks emit nothing. Thus the real reporter remains at START_PENDING after a successful spawn; emit_status consequently advertises no STOP/SHUTDOWN controls. The existing startup test checks only internal state and misses this.

Conversely, Stopped is reported once by the loop's finish action and again by service_main after run returns. Microsoft requires a single final STOPPED report and cleanup first; subsequent SetServiceStatus use can access a closed RPC context (API contract).

Make the reporter/SCM transition trace authoritative: successful spawn/restart must report Running; cleanup completes before exactly one final Stopped; check status-publication failures rather than silently ignoring them. Test through the actual run/reporter boundary, not just Supervisor.state().

F2 — the Windows path does not apply the explicit T3 home (blocking)

WindowsChildHost::spawn calls CreateProcessW with lpEnvironment=NULL. It sets cwd but never overrides T3CODE_HOME. The portable CommandChildHost does override it, so its subprocess test cannot prove the native path. The actual pinned serviceLauncher.main requires T3CODE_HOME and reads state from it. An absent value therefore fails; an inherited different value selects the wrong home. CreateProcessW NULL-environment semantics.

Bind the native child environment to the explicit configured home, preserving only the intended inherited environment. Test absent and conflicting ambient values via the native spawn boundary or a narrowly injectable API seam. Preserve Unicode/argument handling and validate observation of the intended target. Do not modify the real launcher or implement its future stop IPC in this repair.

Also reconcile identity/docs within this subtree: a qualified expected account must not be compared incorrectly to a bare GetUserNameW name. Require unambiguous identity proof when that binding is supplied. Dedicated-account provisioning or reuse/migration of credentials has not been selected by ENV; document choices as unqualified, not a new mandate. Remove the plaintext sc.exe ... password=... recipe. The installer/auth design stays outside this source repair.

F3 — failed process admission can leave an orphaned suspended child (blocking)

After CreateProcessW succeeds suspended, AssignProcessToJobObject failure calls TerminateJobObject on the job the process did not join, then merely closes process/thread handles. That does not terminate the created process (CloseHandle contract). ResumeThread's return is also ignored, and failed creation-time inspection becomes a fabricated zero identity.

Use checked, scoped cleanup for every post-create/pre-resume failure, retaining the created process handle until its outcome is established. Check assignment, identity capture and resume results. No broad PID/name termination and no fabricated identity. Add fault-injection regressions proving resources are reclaimed or an explicit cleanup failure is reported.

F4 — refusal or failed termination becomes clean success; Drop hides a side effect (blocking)

The ForceTerminateTree arm unconditionally clears child and calls on_forced_termination, which returns Clean even when identity verification refused termination. terminate_tree returns no outcome and ignores TerminateJobObject/WaitForSingleObject failure or timeout. Dropping WindowsChild closes a kill-on-close job, so the stated no-termination path can still terminate that owned job as an implicit side effect. Portable fake tests do not model that destructor.

Make attempted/confirmed/failed/unknown cleanup distinct. A bounded supervisor exit may be necessary, but must not claim successful child shutdown without evidence. Reconcile ownership around held process/job handles, explicit termination and kill-on-close destruction; the job handle denotes its actual membership, not an arbitrary newly reused PID. Preserve unrelated processes. Tests must observe final outcome and destructor/handle effects, not only absence of a terminate method call.

Repair and acceptance scope

Fresh ENV-1:R7-WIN-SERVICE will own only native/windows-service-host/** and docs/internals/windows-background-service.md, adding normal commits to this branch. Reproduce these paths with focused regression tests, repair them together, and correct the native test recipe so feature flags, executable paths, account access and try/finally cleanup are coherent and test-owned. No native qualification may be inferred from cross-compilation.

Do not edit BootService/service CLI, serviceLauncher/protocol, root workspaces/workflows or release tooling. Do not register services on an active workstation, change accounts/WDAC/firewalls or run real T3 state. A genuinely reserved disposable Windows environment may perform the already-scoped dummy-service qualification with exact cleanup; otherwise publish the executable native gate as unrun. The #5 release owner remains independent. Windows service support stays disabled/unqualified pending reviewed helper, adapter, launcher, artifact and native evidence.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENV-1:R7-WIN-SERVICE — START

Identity: ENV-1:R7-WIN-SERVICE (fresh session, new worktree). Not a continuation of ENV-1:R6-WIN-SERVICE.
Sole hub: https://github.com/nullStack65/closura-agent-config/pull/237
Implementation PR (same branch, follow-up commits only, no replacement/rewrite): this PR.

Exact base / head (read back from remote + local worktree)

  • Base: nullStack65/t3code main @ bcc1a58b19a9d610a4f08fed191a364767bc65b3 (unchanged).
  • Input head: 9b3bc5f50a545848a1d0af17fdad1019f900591a (confirmed equal to origin/feat/windows-service-host-prototype-20260926).
  • Worktree: new one at /Users/businessaccount/w-t3/win-svc-r7, branch feat/windows-service-host-prototype-20260926.

Actual platform / tooling observed now

  • macOS 26.6.2 (25G83), Intel x86_64, host Crown-Rain-Gutters.local, user businessaccount.
  • git 2.55.0; gh 2.98.0.
  • rustc/cargo 1.98.1, rustfmt 1.9.0; installed std targets x86_64-apple-darwin, x86_64-pc-windows-msvc.
  • No Windows SCM runtime and no reserved disposable Windows environment reachable. So the SCM dispatch, job-object and native spawn paths remain cross-compiled, not executed. Every unrun native gate will be stated in RESULT. No service will be registered; no MSVC/SDK/WDAC/host-policy change.

Scope and exclusive writes

Repair F1–F4 at the actual run/reporter/native-API boundaries, preserving the existing portable core and its reported tests:

  • native/windows-service-host/**
  • docs/internals/windows-background-service.md

No edits to BootService/service CLI, real serviceLauncher/serviceProtocol, root Cargo/package workspaces, existing resource-monitor, desktop/WSL, or release tooling/workflows.

Controlling review

F1–F4 changes-required 5852040624. Manager findings are source-level; I will reproduce them as regressions at the real boundaries rather than another simulated replacement.

Ownership check

Branch head unchanged at the reviewed head. Only two comments exist on #8 (R6 RESULT, manager review); no later commits and no other open PR touches native/windows-service-host. No demonstrably active conflicting writer.

Planned checkpoint order

  1. F1 reporter/SCM lifecycle (Running report + single final Stopped + publish-failure propagation) with a full-trace run test.
  2. F2 native environment block binding T3CODE_HOME with a portable env-block seam test; identity reconciliation + doc/recipe corrections.
  3. F3 scoped checked admission/unwind with fault-injection tests through a portable admission seam.
  4. F4 explicit cleanup outcomes + drop/handle effects with tests.
  5. Portable tests + fmt + Windows-target check; fresh commits pushed to the same branch; substantive RESULT on feat(service): minimal Windows SCM service host prototype #8 linked to Failing to create PR on Windows pingdotgg/t3code#237.

Windows support remains disabled/unqualified. No merge, live service/network/auth change, inference, telemetry or release.

…n unwind and stop outcomes

Address the F1-F4 source review of the Windows SCM host at 9b3bc5f.

F1 — publish the real SCM lifecycle. A successful spawn now returns and
publishes the RUNNING report, so the SCM leaves START_PENDING and accepts
STOP/SHUTDOWN. The loop remains the single authority for the final STOPPED:
service_main no longer emits a second stopped status after run returns.
Reporter::report is fallible; a SetServiceStatus failure is surfaced, aborts
the run once, cleans the owned child and ends with an unknown cause.

F2 — bind the native child environment. CreateProcessW now receives a Unicode
environment block with the selected home as T3CODE_HOME, overriding an absent or
conflicting ambient value, while preserving the rest of the environment. The
block builder is portable and unit tested; the pinned launcher's required
variable is now proven at the native spawn request. Expected-account binding
uses qualified GetUserNameExW identities (SAM/UPN); a bare --expected-account is
refused. The plaintext-password recipe is removed.

F3 — scoped, checked process admission. Assignment, creation-time identity
capture and ResumeThread are checked through a portable admission seam; any
failure terminates the freshly created process and reports the cleanup outcome.
A failed identity query is unknown, not a fabricated zero, and a suspended child
is never left orphaned.

F4 — honest stop outcomes and drop effects. terminate_tree returns an explicit
CleanupOutcome; only a confirmed cleanup is a clean stop. Failed, unknown and
refused cleanups report STOPPED with an unknown cause. Dropping the kill-on-close
job is treated as the termination effect it is, bounded and explicit, and only
touches this host's owned job members.

The dummy native recipe is corrected: matching test-child build, actual binary
path, explicit test-account ACL grant, uniquely captured child/grandchild PIDs
and try/finally cleanup.

Portable tests: 56 unit + 1 integration passed. Windows-target cargo check (with
and without test-child) and rustfmt are clean. No native SCM run was possible;
that gate remains unrun.
…the subprocess test

Move the Windows command-line quoting into a portable module so the exact
production launch request ("<runtime>" "__service-launcher", spaces, trailing
backslashes, embedded quotes and Unicode) is asserted on the developer host.
Bound the portable subprocess test's PID read so its child's asynchronous write
cannot race the assertion.
@nullStack65

Copy link
Copy Markdown
Owner Author

ENV-1:R7-WIN-SERVICE — RESULT

Identity: ENV-1:R7-WIN-SERVICE (fresh session, new worktree, follow-up commits on the existing branch; no replacement PR, no rewrite).
Sole hub: https://github.com/nullStack65/closura-agent-config/pull/237 · START: #8 (comment)

Exact base / head (read back from remote)

  • Base: nullStack65/t3code main @ bcc1a58b19a9d610a4f08fed191a364767bc65b3 (unchanged).
  • Input head: 9b3bc5f50a545848a1d0af17fdad1019f900591a.
  • New head: 6ab38b23f6266691c06f5c4a0967f289529c587e (confirmed equal to origin/feat/windows-service-host-prototype-20260926).
  • Commits added (ordinary follow-ups): b4ab6c78e (F1–F4 repair), f9dc35e33 (portable argv proof + subprocess-test hardening), 6ab38b23f (help-text honesty).
  • Worktree: /Users/businessaccount/w-t3/win-svc-r7.

Changed paths (all inside the exclusive scope; nothing else)

native/windows-service-host/** and docs/internals/windows-background-service.md only — verified: no file outside those paths changed (git diff --name-only base..head filtered to none). BootService/service CLI, serviceLauncher/serviceProtocol, root workspaces/workflows, native/resource-monitor, desktop/WSL and release tooling are untouched.

Credit / preservation

The R6 portable core and its tests are preserved and extended, not replaced. Reported suite grew from 33 unit + 1 integration to 61 unit + 1 integration.

F1 — SCM state publication (before → after)

  • Before: on_child_spawned changed only internal state; execute(SpawnChild) emitted no report, so the real SCM stayed START_PENDING with no accepted stop controls. The loop and service_main both reported STOPPED.
  • After: on_child_spawned returns the RUNNING Report; the runner publishes it on success and on every restart. service_main no longer reports STOPPED after run returns — run is the single authority for exactly one final STOPPED, emitted after the child resources are cleared. Reporter::report is now fallible; a SetServiceStatus failure is logged, abandons the run once (after cleaning the owned child) and ends with an unknown cause instead of a clean stop.
  • Tests at the actual run/reporter boundary (run.rs): successful_spawn_publishes_running_before_stop, there_is_exactly_one_final_stopped_report, graceful_child_exit_publishes_running_then_one_stopped, publish_failure_is_surfaced_and_does_not_claim_clean; plus supervise cases start_reports_pending_then_runs, restart_publishes_running_again, a_publish_failure_finishes_with_an_unknown_cause. These assert the full reporter trace and control acceptance, not internal enum state.

F2 — native target binding (before → after)

  • Before: CreateProcessW received lpEnvironment = NULL, inheriting an absent/conflicting ambient T3CODE_HOME; only the portable host set it. --expected-account was compared, incorrectly, against a bare GetUserNameW name.
  • After: the native spawn builds a Unicode environment block with the selected --home as T3CODE_HOME, overriding an absent or conflicting ambient value while preserving the rest of the environment (CREATE_UNICODE_ENVIRONMENT; cwd retained but not relied on). Identity binding uses qualified GetUserNameExW SAM/UPN forms; a bare --expected-account is refused in configuration. The plaintext password= recipe is removed and account provisioning/credential migration is documented as unselected.
  • New portable seams and tests: environment.rs (supplies_the_selected_home_when_absent, overrides_a_conflicting_ambient_home_case_insensitively, preserves_unicode_values_and_terminates_the_block, sorts_names_case_insensitively); command_line.rs proves the exact production launch request ("<runtime>" "__service-launcher", spaces, trailing backslashes, embedded quotes, Unicode); account.rs and config.rs (refuses_an_unqualified_expected_account, accepts_a_qualified_expected_account).

F3 — process admission / unwind (before → after)

  • Before: AssignProcessToJobObject failure terminated an empty job and closed handles, leaving the suspended child orphaned; ResumeThread was ignored; failed creation-time observation fabricated 0.
  • After: admission runs through a portable, checked seam (admission.rs): assign → capture creation-time identity → resume. Any failure terminates the freshly created process with the created handle retained until the cleanup outcome is known, and reports the stage plus cleanup outcome. A failed identity query is an error, not zero.
  • Fault-injection tests: assignment_failure_reclaims_the_created_process, identity_failure_reclaims_the_created_process, resume_failure_reclaims_the_created_process, an_unconfirmed_cleanup_is_reported_with_the_failure, success_does_not_terminate_the_created_process.

F4 — stop outcome and Drop (before → after)

  • Before: ForceTerminateTree cleared the child and returned Clean even when verification refused termination or the native terminate/wait failed; a dropped kill-on-close job was an unmodeled termination effect; the old fake test only proved "no terminate call".
  • After: terminate_tree returns an explicit CleanupOutcome (Confirmed/Failed/Unknown/Refused). Only Confirmed maps to a clean stop; all others report STOPPED with an unknown cause. WindowsChild::Drop makes the implicit kill-on-close termination explicit and bounded, and only this host's owned job members are affected (no PID/name matching).
  • Tests: foreign_pid_is_not_terminated_and_not_reported_clean, query_failure_is_not_treated_as_an_owned_tree, failed_termination_is_not_reported_clean, a_refused_tree_does_not_touch_unrelated_processes, and supervise::cleanup_outcomes_map_to_honest_exit_codes; the portable fake now models the job destructor (job-drop observable) and asserts unrelated processes are untouched.

Dummy native recipe

Corrected in docs/internals/windows-background-service.md: matching --features test-child build, the actual binary path (native/windows-service-host/target/release/...), an explicit icacls grant for the test account to only the probe root, uniquely captured child/grandchild PIDs, and try/finally stop+delete+remove cleanup. A generic process-name listing is no longer treated as cleanup proof.

Counts / skips (executed here)

Command Result
cargo test --locked 61 unit passed, 1 integration passed, 0 failed, 0 skipped
cargo check --locked --target x86_64-pc-windows-msvc clean
cargo check --locked --features test-child --target x86_64-pc-windows-msvc clean
cargo check --locked --features test-child (host) clean
cargo fmt -- --check clean
cargo build --locked clean

Actual CI

At head 6ab38b23f, CI run 36291920160 and Native fingerprint 36291920145 are queued (runner capacity; same queueing seen at the input head). PR Vouch/PR Size/labels passed. Note: the Rust job hardcodes the native crate list (resource-monitor kde-snap-shot hyprland-snap-shot), so even when it completes it does not run windows-service-host; adding it there remains a later integration touch. No CI result is claimed.

Native cleanup evidence

None. No Windows SCM runtime and no reserved disposable Windows environment was reachable, so no service was registered and none was left behind; the exact native recipe is unexecuted. No MSVC/SDK/WDAC/host-policy change; no live T3/proxy/auth/network change.

Remaining gates (unchanged, all unrun)

  1. Launcher stop-channel adaptation (parent stop IPC + graceful Windows child shutdown) — declared integration gap, not part of this repair.
  2. BootService scm adapter / sc.exe step set (R7-T3-STATUS owns those files).
  3. Packaging the host beside the pinned runtime and binding its digest.
  4. Add windows-service-host to the CI Rust crate list.
  5. Real SCM acceptance on a disposable Windows environment (native recipe unrun).

Windows background support remains disabled/unqualified until those join and a real SCM run passes. No merge, admin bypass, inference, telemetry or release.

Model: openrouter/deepseek/deepseek-v4.1-flash · Harness: T3 Code (OpenCode)

Copy link
Copy Markdown
Owner Author

ENV-1 R8 review — preserve R7 fixes; close F3/F4 cleanup propagation

Reviewed 6ab38b23f6266691c06f5c4a0967f289529c587e and RESULT 5852353368. Credit the actual RUNNING report, removal of service_main's duplicate final report, fallible Reporter, explicit native T3CODE_HOME block, qualified account comparison, checked admission stages and explicit cleanup enum. Preserve those changes and the author's reported 61 unit + 1 subprocess tests / Windows-target checks. This manager reviewed source; it did not run cargo or native SCM. Current CI 36291920160 is queued; no native qualification is inferred.

Two residuals in the existing F3/F4 scope prevent native-service acceptance:

C1 — failed admission cleanup is reduced to a retryable string

admission::admit returns AdmissionFailure.cleanup, but windows/job.rs maps every failure to SpawnError::Launch(describe_admission_failure(...)). run.rs then calls on_spawn_failed(..., false) and may start another child. In particular, assignment failure plus failed/unknown TerminateProcess/wait leaves a created suspended process outside the job; dropping PendingProcess closes its handles, and the empty job cannot reclaim it. The new test verifies that the error mentions failed cleanup, not that the real host stops retrying and retains/report its unresolved ownership.

Carry the cleanup result structurally through native spawn and the supervisor. A failed/unconfirmed reclaim must be a bounded non-retrying recovery-required/unknown outcome, not another launch attempt. Retain created-handle ownership until the explicit cleanup decision and report any unresolved process honestly. Do not promise guaranteed removal when the OS refuses it, invent a recovery daemon, or use PID/name-wide cleanup. Add an end-to-end fault-injected spawn→run case for assignment failure plus failed/timeout cleanup and assert no second spawn.

C2 — root-process exit is still substituted for whole-job cleanup

WindowsChild::terminate_tree returns Confirmed after TerminateJobObject succeeds and only the root process wait signals; it does not observe the remaining job members. More importantly, the ordinary try_wait -> Some(code) path in run.rs immediately sets child=None. WindowsChild::Drop then requests termination and waits only on that already-exited root, ignores the result, and on_child_exited can publish Clean or restart. A root exit with a remaining grandchild is therefore not covered by the revised forced-stop tests; cleanup/drop effects can still be lost on the natural-exit path.

Finish explicit bounded owned-job cleanup before releasing the handle/reporting final success or starting a replacement. Observe whole-job state through the existing Windows job API (rather than assuming a root wait proves emptiness), propagate timeout/query/termination failure conservatively, and retain separate forced-versus-graceful evidence. Drop should be a documented last-resort ownership safeguard, not the source of a Confirmed result. Tests must include root exited/grandchild alive, successful request but nonempty/unknown job, natural exit during planned stop, and unexpected exit before replacement. No unrelated process may be touched.

Primary API references: https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-setservicestatus ; https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-terminatejobobject ; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_basic_accounting_information . Cleanup must precede the one final STOPPED publication; a pending/unknown job is not confirmed empty.

Next owner: fresh ENV-1:R8-WIN-SERVICE, same PR/branch and exclusive native/windows-service-host/** + docs/internals/windows-background-service.md scope. Reproduce at production admission/run/native seams before repair; use existing portable fakes and bounded real-subprocess proof where possible. No new framework, broad refactor, BootService/launcher/protocol/package/workflow changes or service enablement. Keep native SCM unavailable explicitly when no already-reserved disposable Windows environment exists. No active-host service registration, live T3/proxy/auth/network change, inference, merge, release or R720 load. Update stale PR claims and post/read back an exact-head RESULT linked to pingdotgg#237.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENV-1:R8-WIN-SERVICE — START

Identity: ENV-1:R8-WIN-SERVICE (fresh session; no predecessor chat/shell/worktree assumed). Not a continuation of ENV-1:R7-WIN-SERVICE.
Sole hub: https://github.com/nullStack65/closura-agent-config/pull/237
Implementation PR (same branch, ordinary follow-up commits only, no replacement/rewrite, no force-push): this PR.
Controlling review: C1–C2 5852544884; R8 dispatch/hub pack: https://github.com/nullStack65/closura-agent-config/pull/237#issuecomment-5852562005 and docs/agent-environment/env-1-round-8-prompts.md @ 24777aa6.

Exact base / head (read back from remote + local worktree)

  • Base: nullStack65/t3code main @ bcc1a58b19a9d610a4f08fed191a364767bc65b3 (unchanged).
  • Input head: 6ab38b23f6266691c06f5c4a0967f289529c587e (confirmed equal to origin/feat/windows-service-host-prototype-20260926; no newer commit).
  • New isolated worktree (outside any cloud dir): /Users/businessaccount/w-t3/win-svc-r8, detached at the fetched head. The R7 worktree /Users/businessaccount/w-t3/win-svc-r7 is clean at the same head and is left untouched.

Actual platform / tooling observed now

  • macOS 26.6.2 (25G83), Intel x86_64, host Crown-Rain-Gutters.local, user businessaccount.
  • git 2.55.0; gh 2.98.0.
  • rustc/cargo 1.98.1, rustfmt 1.9.0-stable; installed std targets x86_64-apple-darwin, x86_64-pc-windows-msvc.
  • No Windows SCM runtime and no reserved disposable Windows environment reachable. SCM dispatch and job-object behavior remain cross-compiled, not executed. Every unrun native gate will be stated in RESULT. No service will be registered; no MSVC/SDK/WDAC/host-policy change; no live T3/proxy/auth/network change.

Ownership check

Branch head equals the reviewed head; the only comments on #8 are R6 RESULT, the R6 manager review, R7 START/RESULT and the R8 C1–C2 review. No later commit and no other open PR writes native/windows-service-host/**. No demonstrably active conflicting writer.

Exclusive writes

  • native/windows-service-host/**
  • docs/internals/windows-background-service.md

Preserving R7's RUNNING publication, single loop-owned final STOPPED, fallible Reporter, explicit T3CODE_HOME environment block, qualified account handling and checked admission stages. No edits to BootService/CLI (R8-T3-STATUS), real serviceLauncher/serviceProtocol, root workspaces/workflows, native/resource-monitor, desktop/WSL, release/build tooling or other owners' source. Windows support stays disabled/unqualified.

Planned repair (reproduce at the real seams first, then fix)

  1. C1: carry the admission cleanup outcome structurally (SpawnError::Admission) through native spawn and supervision; only a confirmed reclaim may take the ordinary bounded retry; failed/unknown/timeout reclaim must finish non-retrying with a recovery-required/unknown outcome and report the unresolved ownership. Fault-inject assignment + termination failure/timeout through spawn→run and assert no second spawn.
  2. C2: make explicit bounded whole-job cleanup precede final success or replacement. Use Windows job accounting (JobObjectBasicAccountingInformation) for remaining-member evidence instead of a root-only wait; a nonempty/unknown/query-failed job cannot become Confirmed. Drop stays a last-resort safeguard, not evidence.
  3. Portable tests + regressions, cargo fmt --check, cargo check --target x86_64-pc-windows-msvc; update the design doc and stale claims; push coherent checkpoints; substantive RESULT on feat(service): minimal Windows SCM service host prototype #8 linked to Failing to create PR on Windows pingdotgg/t3code#237.

No merge, live service/network/auth change, inference, telemetry or R720 load.

…ole-job cleanup

Carry the admission cleanup outcome structurally (SpawnError::Admission)
instead of flattening it into a launch string. run only takes the ordinary
bounded retry when the created process was confirmed reclaimed; a failed,
unknown or timed-out reclaim finishes non-retrying with a recovery-required
cause so a second child cannot leak behind an unresolved process. The created
handles are retained through the explicit cleanup decision.

Treat a root-process exit as ownership evidence only for that root. Before a
natural exit may finish clean or start a replacement, WindowsChild reclaims the
owned job through the retained job handle and confirms emptiness with job
accounting (QueryInformationJobObject/ActiveProcesses) under a bounded drain.
TerminateJobObject is a request, so a still-nonempty job, an ineffective
request or a failed query is never Confirmed. Drop stays a last-resort
safeguard, not evidence.
@nullStack65

Copy link
Copy Markdown
Owner Author

ENV-1:R8-WIN-SERVICE — RESULT

Identity: ENV-1:R8-WIN-SERVICE (fresh session, new isolated worktree; ordinary follow-up commits on the existing branch — no replacement PR, no rewrite, no force-push).
Sole hub: https://github.com/nullStack65/closura-agent-config/pull/237
START (this PR): #8 (comment) · hub START: https://github.com/nullStack65/closura-agent-config/pull/237#issuecomment-5852631865
Controlling review: C1–C2 5852544884.

Exact base / head (read back from remote)

  • Base: nullStack65/t3code main @ bcc1a58b19a9d610a4f08fed191a364767bc65b3 (unchanged).
  • Input head: 6ab38b23f6266691c06f5c4a0967f289529c587e.
  • New head: 7455a7b6c12f01644b874bdf9f790846f6d88a59 (confirmed equal to origin/feat/windows-service-host-prototype-20260926).
  • Commits added (ordinary fast-forward follow-ups): dac8a3dab (C1+C2 repair), 7455a7b6c (spawn→run test now drives the real admit decision).
  • Worktree: new non-cloud /Users/businessaccount/w-t3/win-svc-r8, detached at the fetched input head. The R7 worktree was clean at the input head and was left untouched.

Changed paths (exclusive scope only)

native/windows-service-host/** and docs/internals/windows-background-service.md only. git diff --name-only base..head yields exactly those paths; no BootService/CLI, serviceLauncher/serviceProtocol, root workspace, resource-monitor, desktop/WSL, workflow or release file changed.

Preserved R7 behavior

RUNNING publication on spawn/restart, the single loop-owned final STOPPED, the fallible Reporter + publish-failure abort, the explicit native T3CODE_HOME environment block, qualified account handling, and the checked admission stages are all intact; the portable suite grew from 61 unit + 1 integration to 72 unit + 1 integration by extension, not replacement.

C1 — unconfirmed admission cleanup must stop retries (before → after)

  • Before: admission::admit returned AdmissionFailure{stage, reason, cleanup} but windows/job.rs flattened it to SpawnError::Launch(describe_admission_failure(...)); run then called on_spawn_failed(now, false) and could start another child. Assignment failure plus a failed/timeout TerminateProcess/wait could leave the created suspended process outside the empty job while a second child was launched. The test only asserted the message mentioned failed cleanup.
  • After: the cleanup outcome is carried structurally as SpawnError::Admission(AdmissionFailure). run takes the ordinary bounded retry (on_spawn_failed) only when cleanup.is_clean(); a Failed/Unknown reclaim finishes non-retrying via Supervisor::on_admission_cleanup_unconfirmed() → ExitCode::RecoveryRequired (Win32 specific code 5) and logs the unresolved ownership. PendingProcess keeps its process/thread/job handles until admit has made the explicit cleanup decision. No PID/name-wide cleanup and no guaranteed-removal promise.
  • Proof through the real seams: admission::tests::a_timed_out_or_unknown_cleanup_is_reported_with_the_failure; and run::tests::{unconfirmed_admission_cleanup_stops_without_a_second_spawn, confirmed_admission_cleanup_takes_the_bounded_retry}, which drive the real admit decision through a ChildHost and assert attempt counts (1 vs. the bounded 4) and the truthful exit code.

C2 — root exit is not whole-job completion (before → after)

  • Before: WindowsChild::terminate_tree called TerminateJobObject and waited only on the root process; the natural try_wait -> Some(code) path set child = None, Drop requested termination and awaited only the already-exited root while ignoring the result, and on_child_exited could report Clean or replace the child. A root exit with a surviving grandchild was not covered.
  • After: a new ChildHandle::cleanup_after_exit reclaims the owned job through the retained job handle and confirms emptiness with job accounting (QueryInformationJobObject + JobObjectBasicAccountingInformation.ActiveProcesses) under a bounded drain poll. terminate_tree now proves whole-job emptiness the same way (TerminateJobObject is only a request). run calls cleanup_after_exit on natural exit before releasing the handle and passes the outcome to on_child_exited(now, code, cleanup); any non-Confirmed outcome finishes RecoveryRequired with no replacement. Drop remains a documented last-resort safeguard, never the source of Confirmed. Forced vs. graceful outcomes are preserved; graceful/forced stop while the root may live still verifies PID + creation time first, while whole-job cleanup never re-opens a possibly-reused PID.
  • Proof: supervise::tests::an_unconfirmed_post_exit_cleanup_is_recovery_required_not_clean_or_restarted; run::tests::{root_exit_with_a_living_grandchild_is_not_clean, successful_termination_request_with_a_nonempty_job_is_not_clean, job_query_failure_on_a_natural_exit_is_not_clean, natural_exit_during_a_planned_stop_is_clean_only_when_the_job_is_empty, unexpected_exit_with_an_unconfirmed_job_does_not_replace_the_child, a_natural_exit_does_not_touch_an_unrelated_process}.

Tests / counts / skips (executed here at 7455a7b6c)

Command Result
cargo test --locked 72 unit passed, 1 integration passed, 0 failed, 0 skipped
cargo fmt -- --check clean
cargo check --locked --target x86_64-pc-windows-msvc clean
cargo check --locked --features test-child --target x86_64-pc-windows-msvc clean
cargo check --locked --features test-child (host) clean
cargo build --locked clean

New regressions: admission::a_timed_out_or_unknown_cleanup_is_reported_with_the_failure; supervise::{an_unconfirmed_post_exit_cleanup_is_recovery_required_not_clean_or_restarted, an_unconfirmed_admission_cleanup_finishes_without_retrying}; run::{unconfirmed_admission_cleanup_stops_without_a_second_spawn, confirmed_admission_cleanup_takes_the_bounded_retry, root_exit_with_a_living_grandchild_is_not_clean, successful_termination_request_with_a_nonempty_job_is_not_clean, job_query_failure_on_a_natural_exit_is_not_clean, natural_exit_during_a_planned_stop_is_clean_only_when_the_job_is_empty, unexpected_exit_with_an_unconfirmed_job_does_not_replace_the_child, a_natural_exit_does_not_touch_an_unrelated_process}.

Actual CI

At head 7455a7b6c, checks are queued (Rust, Test, Test Server 1–3, Release Smoke, Native fingerprint diff pending, runner capacity; same as at the input head). Collect PR targets / Label PR* passed; CodeRabbit skipped for a draft. Run 36302126570. No CI result is claimed. The Rust job still hardcodes resource-monitor kde-snap-shot hyprland-snap-shot, so even when it completes it does not build/test this crate; adding it is recorded as later integration, not changed here.

Cleanup observations

  • Portable: the real-subprocess portable_host test still stops and terminates the child after the drain (1 passed).
  • Native: no Windows SCM runtime and no reserved disposable Windows environment was reachable, so no service was registered and none was left behind; the job-accounting drain, QueryInformationJobObject, and Drop paths are cross-compiled, not executed. No MSVC/SDK/WDAC/host-policy change; no live T3/proxy/auth/network change.

Stale claims corrected

  • PR body "How" updated: ownership now describes structural admission-cleanup propagation and whole-job completion (job accounting; Drop as last resort) instead of "identity verified before any stop/terminate".
  • docs/internals/windows-background-service.md: admission-unwind paragraph, whole-job-completion paragraph, ownership paragraph, honest-failure-states (new recovery-required specific code 5), and the native acceptance checklist (grandchild alive; job-query failure = unknown, not confirmed-empty).

Unrun native gates (unchanged)

  1. Launcher stop-channel adaptation (parent stop IPC + graceful Windows child shutdown).
  2. BootService scm adapter / sc.exe step set (R8-T3-STATUS owns those files).
  3. Packaging the host beside the pinned runtime and binding its digest.
  4. Add windows-service-host to the CI Rust crate list.
  5. Real SCM acceptance on an already-reserved disposable Windows environment (recipe unexecuted).

Windows background support remains disabled/unqualified until those join and a real SCM run passes. No merge, inference, telemetry or release; #5 assets and other owners' work untouched.

Model: openrouter/deepseek/deepseek-v4.1-flash · Harness: T3 Code (OpenCode)

Copy link
Copy Markdown
Owner Author

ENV-1 R8 review — C1/C2 accepted for subsequent source integration

Reviewed 7455a7b6c12f01644b874bdf9f790846f6d88a59 against prior 6ab38b23f6266691c06f5c4a0967f289529c587e, the R8 RESULT, and current CI. The C1/C2 repair in 5852544884 is accepted as source integration input; do not start another cleanup-repair wave by default.

C1: native admission failure now carries cleanup state structurally into the real run/supervisor boundary. Unconfirmed created-process cleanup stops retries and returns an explicit recovery-required outcome instead of launching another child. An OS refusal is not falsely claimed to guarantee removal.

C2: natural root exit now invokes explicit cleanup_after_exit before release/replacement. The Windows path queries the held job's active-process count and waits within a bound for the whole job to empty; a remaining member, failed termination or failed query cannot become confirmed cleanup. Drop remains fallback rather than success evidence. This matches the distinction between job-state queries and terminating an owned job.

The earlier real RUNNING/single-final-STOPPED reporting, explicit child home, account handling and checked admission are retained. Worker evidence reports 72 unit + one subprocess integration test, formatting and Windows-target checks; I reviewed source and that receipt, not a new manager cargo/Windows execution.

Still unqualified: actual native SCM/job-object execution, helper CI coverage, helper-to-launcher graceful control, the server shutdown consumer, Windows BootService adapter, packaging/provenance and real host lifecycle tests. Current substantive CI 36302126570 is queued, not passed; unrelated skipped previews do not close it. No installed or fleet-ready claim.

The next Windows source stage should join this accepted helper with the existing launcher/protocol and corrected #9 observations under one integration owner. This checkpoint does not assign overlapping launcher/BootService writers or mutate this branch. No live service, account, credential, Tailscale, proxy, release or merge is authorized for #8. ENV-1 pingdotgg#237 remains the sole coordinator.

@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 7455a7b.

This comment will update automatically after the next completed run.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant