Skip to content

[RT-E2] refactor: the launcher runs on its seams' clock, and shipped src bans the system clock - #442

Merged
Blind-Striker merged 2 commits into
masterfrom
feat/rt-e2-c-launcher
Oct 17, 2026
Merged

Blind-Striker merged 2 commits into
masterfrom
feat/rt-e2-c-launcher

Conversation

@Blind-Striker

@Blind-Striker Blind-Striker commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

What

The standalone launcher now measures every bound on LauncherSeams.Time, and shipped src can no longer read the system clock outside five counted composition roots. This is RT-E2 PR C of three; A (#429, the background service) and B (#432, terminals) have merged.

Closes

  • U3-04, the rest. RT-E1b moved the readiness bound and the reap wait onto the seams' clock. This PR moves what was left:
    • PosixServerLadder: its Stopwatch becomes GetTimestamp/GetElapsedTime, its raw Task.Delay becomes a provider delay, and its root WaitAsync gets the provider;
    • the pump release bound;
    • the taskkill tree-kill bound.
  • D-C8, the clock ban.

The change

  • Launcher.

    • PosixServerLadder and TaskkillTreeKill take a required TimeProvider.
    • PipeOutputPump.ReleaseAsync takes the owned child's clock.
    • BoundedWait loses its TimeProvider.System overload.
    • LauncherSeams.ForCurrentProcess chooses the clock once and passes the same provider to the tree kill.
    • No bound value or start point moved:
      • the ladder's grace starts after SIGTERM, and the root wait plus the later group probes spend that same grace;
      • the forced-root wait starts after SIGKILL;
      • the release bound starts after the readers are disposed;
      • the tree-kill bound starts after the spawn and the watch.
  • The ban.

    • BannedSymbols.Clock.txt has 14 entries:
      • Task.Delay ×4;
      • the CancellationTokenSource timed constructors ×2 and CancelAfter ×2;
      • Stopwatch;
      • DateTime.UtcNow, DateTimeOffset.UtcNow, Environment.TickCount and TickCount64;
      • TimeProvider.System.
    • A Directory.Build.props condition applies it to OpenCode.Sdk and OpenCode.Sdk.Extensions only. Tests and tools keep their real-clock hang guards.
    • DateTime.Now and DateTimeOffset.Now stay banned everywhere in BannedSymbols.txt; listing them twice is RS0031.
    • A pre-dispatch build of both shipped projects on all four TFMs, with the four new UtcNow/TickCount entries on, had 0 hits in the whole compilation, generated output and Polyfill sources included. A deliberate DateTimeOffset.UtcNow was red first.
  • Five composition roots, the D-C8 exemption count. Each is a #pragma warning disable RS0030 // <reason> around the single TimeProvider.System expression:

    • the standalone launcher (LauncherSeams.ForCurrentProcess);
    • the client pipeline's fallback;
    • Discover, Stop and Ensure in OpenCodeServer.BackgroundService.

    The suppression ledger records all five.

  • Real-time sites in hand-written src: 12 → 6. What remains is the five roots and PosixChildExitWatch's 100 ms probe cadence, a polling cadence on a dedicated thread that RT-E6 owns. D-C8 does not ban Thread.Sleep.

  • Second commit (the lead's scope addition). BannedSymbols.txt's two .Now messages pointed src at UtcNow, which the clock ban now forbids there. They now read "Use the injected TimeProvider (src) or DateTime[Offset].UtcNow (tests)".

  • Canon. client-runtime.md gets a paragraph naming every launcher bound on LauncherSeams.Time and the D-C8 sentence. platform-and-packaging.md gets a one-sentence relay.

Proof

  • The ban. A deliberate violation in a src file is red with RS0030 on net10.0 and net472, one spelling at a time:

    • Task.Delay(TimeSpan);
    • new CancellationTokenSource(TimeSpan);
    • CancelAfter(TimeSpan);
    • Stopwatch.StartNew();
    • DateTimeOffset.UtcNow;
    • Environment.TickCount64 (net10.0 only; it does not exist on net472);
    • TimeProvider.System outside a root.

    The same Task.Delay in a test file compiles and passes. msbuild -getItem:AdditionalFiles shows the clock file in both shipped projects and in no test or tool project.

  • New seam proofs on ManualTimeProvider. Each waits for its own armed bound (WaitForArmedAsync), then checks that the bound has not fired at bound − ε and has fired at the bound. The mutant class is "the bound removed while the signature is kept".

    Bound Test Mutant result
    ladder root grace PosixServerLadderTests.RunAsync_Should_Spend_The_Root_Grace_On_The_Supplied_Clock analyzer-rejected (second guard); analyzers-off demo fails
    ladder group remainder …_Should_Probe_Only_For_The_Grace_Remaining_On_The_Supplied_Clock analyzer-rejected; demo fails
    forced-root wait …_Should_Bound_The_Forced_Root_Wait_On_The_Supplied_Clock analyzer-rejected; demo fails
    pump release OwnedServerChildTests.DisposeAsync_Should_Bound_Pump_Release_On_The_Owned_Clock analyzer-rejected; demo fails
    tree-kill bound TaskkillTreeKillTests.KillTreeAsync_Should_Run_The_Supplied_Bound_On_The_Supplied_Clock compiles with analyzers on; killed
  • Compile-forced edits to existing tests. No test name or assertion changed:

    • explicit TimeProvider.System for the ladder helper, two lifecycle BoundedWait calls and the taskkill constructors;
    • in OpenCodeServerReadinessTests.AdvanceEarlyEndAsync, the manual clock now advances by PipeOutputPump.ReleaseBound, but only when that bound arms. Why it can arm there:
      • The recorded-exit rows replace stdout with Support/GatedStream, which ignores Dispose.
      • On net472, StreamReader.ReadLineAsync() passes no token, so those readers end only when the 5 s release bound expires.
      • Measured at master on net472: exactly those two rows took 5.07 s and 5.05 s, while the real-pipe rows took 1 s or less. A real pipe ends on close.
      • Once the bound moved onto the rows' never-advanced manual clock, they hung (56 s) until this edit; afterwards they take about 60 ms.
      • The fixture gap is tracked separately as GatedStream.Dispose does not end pending reads, so net472 readiness rows wait out the 5 s release bound #441.
  • Size. Production +25 lines, tests +202, against budgets of 160 and 380. PublicApi.verified.txt is unchanged, and no internal type was added or removed.

  • Checks.

    • Whole-solution Release build: 0 warnings, 0 errors.
    • The whole OpenCode.Sdk.Tools.Tests project (Category!=Packaging&Category!=NativeAot): green.
    • verify-clones: 0 new. Slopwatch: 0.
    • The Launcher and Platform namespaces: green on net10.0 and net472.
  • Local check: GATE PASS on 656db84f (this branch on master 3b235436): slopwatch, build, both format passes, clone-detector, verify-clones, 10,500 tests including net472 with 0 failed, and election.

  • Reviews.

    • Fresh Sol review: no findings.
    • Coordinator review: three should-fix items and a nit, all fixed in a second worker pass.
      1. The tree-kill proof read ArmedIn before the product could have armed the bound, which was a race: the watch signal fires inside Watch, before the bound arms.
      2. The ladder proofs read ArmedIn immediately, against the "wait for the specific armed bound" rule.
      3. A clause in platform-and-packaging.md changed what "CancelAsync is one" referred to.
      4. (Nit) A paragraph break was missing in client-runtime.md.
    • The harness verify caught the net472 hang above. The first worker pass had run its tests on net10.0 only.
    • Grok was not used: this PR adds no new lock or race code.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

… the system clock (RT-E2 C)

PosixServerLadder, the pump release and the taskkill tree kill now measure
their bounds on LauncherSeams.Time; BoundedWait loses its system-clock
overload. BannedSymbols.Clock.txt (14 entries) applies to the two shipped src
projects only, with five counted composition roots exempted narrowly.
@Blind-Striker
Blind-Striker added this pull request to the merge queue Oct 17, 2026
Merged via the queue into master with commit f8f0d74 Oct 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant