Skip to content

fix(server): idle-reap detached terminals whose only activity is repaint noise - #539

Merged
danshapiro merged 7 commits into
mainfrom
fix/idle-repaint-noise
Jul 27, 2026
Merged

fix(server): idle-reap detached terminals whose only activity is repaint noise#539
danshapiro merged 7 commits into
mainfrom
fix/idle-repaint-noise

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

Problem

The idle auto-kill sweep (enforce_idle_kills, crates/freshell-terminal/src/registry.rs) uses last_activity_at, which is bumped by every PTY output frame. A detached, abandoned coding-CLI TUI that repaints (codex braille spinner / ticking "(Ns • esc to interrupt)" counter, claude's ticking "Crunched for Ns", status-bar clocks) refreshes it continuously and is never reaped. Second of two holes behind the 2026-07-25 orphaned-terminal incident (first was the client attach leak, fixed in #534; verify hardening in #535; analysis doc landed in #538, D-series findings).

Fix (rust server only, legacy untouched)

  • New NoiseScanner module (17 unit tests): strips digits, ANSI escape sequences, and spinner glyphs from each output frame and fingerprints the residue against a 32-slot memory of recent fingerprints — repeated fingerprints = repaint noise, novel residue = meaningful output.
  • Registry now maintains a private "last meaningful activity" clock for the reaper (5 new tests); the client-visible lastActivityAt is unchanged.
  • Detach grace period: detaching (or connection loss) resets the idle clock, so a terminal is never killed moments after its watcher disconnects.
  • autoKillIdleMinutes <= 0 remains disabled; attached terminals remain exempt; fails safe (when in doubt, keep alive).
  • Deviation ledger entry DEV-0009 (status: proposed) records the rust-vs-legacy behavior change and accepted limitations.

Accepted trade-offs (documented in DEV-0009 / plan doc docs/plans/2026-07-26-idle-repaint-noise.md)

AD-1: a detached program whose only changing output is a bare numeric counter (e.g. raw curl/dd byte counts) is indistinguishable from a ticking clock and will be reaped after the threshold; unit transitions (kB→MB) and bar-style meters (wget/pip/docker) reset the clock. Mitigation: user-adjustable threshold.

AD-2: extremely chatty frames (4KiB+ per read) overflow the detector and are never reaped — same as today's behavior, fail-safe.

Verification

TDD throughout; load-bearing assumption audit falsified 6 assumptions and the plan was corrected for each (including pinning the real codex shimmer animation behavior); all suites green (153/153, 319/319, 255/255 — 18 pre-existing baseline failures in an unrelated area predate this branch, verified no NEW failures); clippy + lint clean; independent whole-branch review "ready to merge"; fresh-eyes delta review passed round 1 with zero blocking issues.

danshapiro and others added 7 commits July 26, 2026 23:48
TDD plan: NoiseScanner repaint-noise fingerprinter (new module) + a
last_meaningful_activity_at reap clock for enforce_idle_kills, plus
DEV-0009 oracle deviation entry. Wire-visible lastActivityAt semantics
unchanged.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ion findings

Stage-2 assumption validation (6 verified, 6 falsified; ledger + evidence
in .worktrees/.the-usual-logs/idle-repaint-noise/). Plan fixes:

- Ring 8 -> 32: live capture of codex 0.145.0 shows its shimmer header
  emits ~13-16 letter-subset fingerprints per cycle; a ring of 8 never
  settles and codex would never be reaped. New shimmer pinning test.
- Strip set gains claude's real glyph (U+2736) and bullets U+2022/U+25E6.
- New detach-grace wiring (detach() + remove_connection(), gated bump)
  with two pinning tests: a freshly-detached terminal gets one full
  threshold instead of dying on the next 30s sweep.
- Frame-delivery facts corrected to measured reality (kernel-read
  boundaries <=4095 B, coalescing multiplicities, env-override limit).
- Numeric-only-progress reaping (curl/dd-class) recorded as an explicit
  accepted product decision (AD-1) in contract, semantics note, DEV-0009.
- DEV-0009 reshaped to the oracle's canonical seven-field unbolded
  schema; lib.rs module-visibility wording fixed.
- Cross-crate 'Expected: PASS' re-scoped to 'no NEW failures' over the
  recorded pre-existing 18-test freshell-ws baseline.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…yes review

- Task 3 Step 1: cargo test rejects two positional TESTNAME filters;
  split into three single-filter invocations and correct the expected
  test counts (1, 1, then 2).
- Task 1 Steps 1/5/6: the not-yet-wired pub(crate) NoiseScanner emits
  dead_code warnings, contradicting Step 6's 'no new warnings' gate.
  Make the #[allow(dead_code)] unconditional on the mod idle_noise;
  declaration in Step 1, align Step 5/6 expectations, and have Task 2
  remove the attribute (Files list, Step 3a, git add, commit message)
  when registry.rs wires the scanner in.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…idle reap clock

Stateful per-terminal frame classifier: escape-stripped, digit/spinner-
glyph-stripped FNV-1a fingerprints checked against a ring of the 32 most
recent frames (sized for codex's shimmer animation, which emits ~16
distinct letter-subset fingerprints per cycle). Repaint noise (spinners,
shimmer churn, ticking counters, status-bar redraws) classifies false;
genuinely new content classifies true.
Groundwork for keying enforce_idle_kills on meaningful activity
(DEV-0009). Fails open: unrecognized content counts as activity.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…oise can't exempt detached terminals

enforce_idle_kills now reads a new private last_meaningful_activity_at
clock, refreshed by input writes and by output frames the NoiseScanner
classifies as genuinely new content. Spinner frames, ticking elapsed
counters, and status-bar redraws no longer reset the reap clock, so an
abandoned detached animated TUI is reaped after autoKillIdleMinutes
(DEV-0009; second server-side hole behind the 2026-07-25 orphaned-
terminal incident — the client-side hole was PR #534).

Transition-to-detached (explicit detach or socket close) grants one
full threshold of grace: the meaningful clock may have gone stale while
a watcher was attached (attached terminals are reaper-exempt), and a
just-backgrounded terminal must not be killed by the next 30s sweep.

Wire-visible lastActivityAt semantics are unchanged: still bumped on
every PTY output frame and every input write (terminal-core.md §1.3).
Attached terminals stay exempt; autoKillIdleMinutes <= 0 stays a no-op.
Also drops the temporary #[allow(dead_code)] on mod idle_noise now that
the scanner has a consumer.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Ledger entry (status: proposed) for keying enforce_idle_kills on the
meaningful-activity clock instead of the every-frame lastActivityAt.
Objective defect bar: resource leak (detached animated TUIs were never
reapable, 2026-07-25 incident). Pinning tests referenced and passing.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Fix two issues in the DEV-0009 entry of port/oracle/DEVIATIONS.md:
- Replace wrong glyphs (U+23FB '⏻' and U+23F6 '⏶') with correct ones (U+273B '✻'
  and U+2736 '✶') in the objective_defect and port_behavior fields. These now
  match the glyphs used in idle_noise.rs spinner-glyph set.
- Update the doc comment on registry.rs:input() to mention both the lastActivityAt
  bump and the DEV-0009 meaningful-activity reap clock bump that the function performs.

Verified with hexdump: the bytes are now e2 9c bb (✻) and e2 9c b6 (✶).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@danshapiro
danshapiro merged commit cab6c95 into main Jul 27, 2026
2 checks passed
@danshapiro
danshapiro deleted the fix/idle-repaint-noise branch July 27, 2026 08:59
pull Bot pushed a commit to HinchK/freshell that referenced this pull request Jul 27, 2026
…nal yields clean respawn

waveC integration interaction pin (C2 x PR danshapiro#539): a detached terminal reaped
by enforce_idle_kills while its pane is mid-reconcile must converge to a
respawn verdict on the next round - never attach to the reaped id, never a
wedge. Exercises the real seam: the sweep marks the terminal not-live, so
the verdict scan falls through to the recovery rows.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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