[Desktop] Bound native host memory and prevent invisible orphan processes · Issue #65 · superWorldSavior/casys-digital-thread · GitHub
Skip to content

[Desktop] Bound native host memory and prevent invisible orphan processes #65

Open

Description

@superWorldSavior

Problem

During the real packaged macOS Desktop qualification on 2026-09-30, the native window disappeared while the Desktop main process, Control Plane, Workbench, Chat Host and the owned Build123d provider were still alive.

A later system sample attributed 77 GB of memory to the Desktop main process and showed 42.2 GB of swap in use. A simple RSS sample taken after the window disappeared was much smaller, so the allocator and ownership of that footprint are not yet established. The run also used repeated macOS Accessibility tree inspection; that instrumentation must be isolated from ordinary product use before assigning root cause.

SIGTERM did not terminate the invisible host within 30 seconds. The main process required SIGKILL, and the owned Build123d container was then stopped explicitly. Swap subsequently fell to about 7.5 GB and system-disk availability returned from 36 GiB to about 70 GiB.

This blocks further unattended native qualification.

Confirmed lifecycle mismatch

Deno Desktop 2.9.6 removes the native window before dispatching its JavaScript close event. That event is not cancelable. The current Desktop code instead calls preventDefault(), assumes the window remains visible during resource draining, and offers a retry path that cannot work once the native window is gone.

A local correction now treats close as a one-way notification, performs one bounded cleanup, and exits the invisible host with success or failure. It is not yet packaged or qualified.

Required evidence

  • Add an external native-test guard that samples process-tree RSS, physical footprint and system swap at a short interval.
  • Stop the test automatically on abnormal growth. The next run should warn around 4 GB and terminate around 6 GB, or sooner on a steep growth rate or material swap increase.
  • Compare a short manual journey with a journey using Accessibility inspection to distinguish a product leak from test-instrumentation pressure.
  • Record memory samples around viewer open/close, Canvas navigation and window close.
  • Prove that native close drains or terminates the Desktop process, helpers and owned provider within the declared bound; unrelated containers remain untouched.
  • Repeat viewer open/close and Canvas navigation enough times to establish a documented stable bound before resuming [MCP host] Download Casys and reach a saved Build123d result without manual Docker setup #53.
  • Update the qualification record with exact observed metrics and keep the result unverified until this passes.

Scope

This issue concerns Desktop host stability and qualification safety. It does not change engineering authority, project truth, MCP operation identity or provider contracts.

Activity

  1. superWorldSavior commented on Sep 30, 2026

    @superWorldSavior
    OwnerAuthor

    Static diagnosis found a concrete unbounded refresh loop in Desktop Chat:

    • every snapshot parse creates a fresh conversations array;
    • the polling effect depended on that array;
    • every completed snapshot therefore restarted the effect and immediately called the binding again, bypassing the intended 250/1000 ms delay;
    • refresh calls were not serialized.

    Local correction now schedules the next poll only after the previous refresh settles, uses the current active/idle state only to select the delay, and shares one in-flight refresh across timer and command callers. Two targeted polling tests and the UI type check pass.

    A native memory guard was also added locally. It samples exact process-tree RSS, periodic macOS physical footprint and system swap growth, logs JSONL evidence, and stops only the named PID after declared warning/stop bounds. Three parser/tree tests pass; a smoke test terminated only a disposable sleep target at the configured limit.

    The Deno 2.9.6 close-event mismatch is corrected locally as well: close is treated as a one-way notification, bounded cleanup runs once, then the invisible host exits 0 or 1. Seven shutdown tests, targeted lint/format and deno check main.ts pass.

    No native app was relaunched for these checks. The causal contribution of the polling loop to the earlier 42.2 GB swap observation remains to be measured under the guard.

  2. superWorldSavior commented on Sep 30, 2026

    @superWorldSavior
    OwnerAuthor

    Guarded native qualification completed on 2026-09-30 with the locally finalized macOS/arm64 bundle. This remains local, ad-hoc signed work; it is not merged or published.

    The first guard implementation was tightened before the app run:

    • the guard now launches and retains the exact root ChildProcess instead of attaching to a numeric PID;
    • descendants are pinned by PID plus start time, included in the bound, and verified gone after TERM/KILL escalation;
    • logging failures cannot bypass a required stop;
    • ps, footprint, sysctl, and kill subprocesses are time-bounded;
    • the system swap metric is named and treated as a system-wide net safety bound, not target attribution.

    Targeted validation: 3 parser/tree tests, deno check, deno lint, deno fmt --check, an owned-root smoke test, and a resistant parent/child smoke test. The latter required SIGKILL and left neither process alive.

    Two guarded native cycles used warning/stop bounds of 4/6 GiB and a 2 GiB system-wide net swap-growth bound:

    1. 172.5 s: maximum process-tree RSS 803.1 MiB; maximum macOS tree footprint 248.2 MiB; maximum system-wide net swap growth 0 MiB.
    2. 579.3 s: maximum process-tree RSS 837.0 MiB; maximum macOS tree footprint 318.1 MiB; maximum system-wide net swap growth 1,073.8 MiB.

    No guard warning or stop threshold fired. The swap increase in cycle 2 is system-wide and cannot be attributed to Casys; the app's own tree metrics stayed bounded.

    Both window closes exited the owned Desktop root with code 0. No packaged Desktop, control-plane, Workbench, Chat Host, or Node process remained. The exact owned Build123d container ended stopped (ExitCode 143, OOMKilled false), and the shutdown report recorded stopped: ["build123d"], unresolved: [].

    The native reopen restored the persisted standalone conversation, Build123d with 4 tools, the open session whiteboard, and 3 saved results. The persisted layout still contains 3 viewer nodes and the note for the 10 × 10 × 20 mm part. STEP/STL/GLB/source downloads and digests remained unchanged.

    This run gives strong evidence that the fixed unbounded snapshot polling loop caused or amplified the earlier runaway behavior: with serialized delayed polling, the same persisted native journey stayed below 1 GiB tree RSS for nearly ten minutes. It does not establish that polling was the only contributor. Large in-flight MCP App resource reads still lack cancellation and remain a follow-up risk.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions