Skip to content

fix(deck): unmask sleep + add the reachability watchdog that makes it safe - #343

Open
mabry1985 wants to merge 2 commits into
mainfrom
fix/deck-unmask-sleep
Open

fix(deck): unmask sleep + add the reachability watchdog that makes it safe#343
mabry1985 wants to merge 2 commits into
mainfrom
fix/deck-unmask-sleep

Conversation

@mabry1985

@mabry1985 mabry1985 commented Jul 31, 2026

Copy link
Copy Markdown
Member

Fixes the black-screen failure on the Deck, and puts the right mechanism underneath it.

The bug

The deck-deploy skill recommended masking the sleep targets so deep-suspend couldn't take the Deck offline mid-remote-op. Applied to the live Deck 2026-07-22. It produced exactly the failure the 2026-06-21 notes predicted.

Mechanism: Game Mode blanks the panel before calling suspend. With suspend.target masked the suspend call fails — but the screen is already off, and since nothing suspended, no resume ever turns the panel back on. The Deck sits awake on a black screen with no wake path.

The 2026-07-22 change dismissed the earlier warning as a separate "display-wake" concern distinct from S3 suspend. They're the same failure.

Part 1 — sleep unmasked (already applied to the device)

  • systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target → all four back to static
  • the four mask paths dropped from /etc/atomic-update.conf.d/protopen-keep.conf (backup kept; alfa/hackrf/btusb entries untouched)

The masking was only ever a manual step from the skill doc — not in install.sh/bootstrap.sh — so no fresh install re-applies it.

Part 2 — the watchdog that makes sleep safe

Unmasking alone would restore the original problem: suspend freezes wifi, tailscaled and the runtime container, and they don't reliably return. So protopen-watchdog.timer + .sh (user scope, installed and enabled by deck/install.sh) probe the tailnet and runtime every 30s and repair only what's broken:

  • Repair, don't restart. A runtime restart costs ~70s and kills in-flight work, so each service is probed first; a healthy tick does nothing and logs nothing.
  • Boot grace (180s) + a one-tick wait before acting, so it never fights a service that's merely still starting, and won't thrash on a transient blip.
  • Respects operator intent — skips any unit deliberately disabled.

Why a user timer, not a /usr/lib/systemd/system-sleep hook. That directory is root-owned on the rootfs and needs an atomic-update keep-list entry to survive an OS update — the exact fragility that produced this bug. install.sh deliberately lives entirely in /home; this follows that rule, so there's nothing to preserve. The timer is realtime, so a tick that elapses during suspend runs as soon as the Deck wakes — it doubles as the post-resume trigger, and additionally covers wifi drops and container crashes that a resume-only hook would miss.

Verification

Deployed and enabled on the Deck. Dry run on a healthy system: silent, rc=0.

Repair path tested live — systemctl --user stop protopen-runtime (an explicit stop, which suppresses the unit's own Restart=on-failure, so recovery is attributable to the watchdog):

13:16:02  stop issued
13:16:12  [watchdog] protopen-runtime.service not active — starting it
13:16:20  active
13:16:40  serving 200

Not verified: the tailscaled repair branch. Breaking the tailnet would sever the only access path to the Deck, so it's left to a real resume to exercise.

Suspend/resume itself is for the operator to confirm at the device — I deliberately didn't trigger a suspend, since that drops SSH and needs a physical wake.

🤖 Generated with Claude Code

… black screen

The deck-deploy skill told operators to mask the sleep targets so deep-suspend
couldn't take the Deck offline during remote ops. Applied on the Deck 2026-07-22;
reverted today after it produced the failure the 2026-06-21 notes predicted.

Game Mode blanks the panel *before* calling suspend, so with suspend.target
masked the suspend fails while the screen is already off — and since nothing
suspended, no resume ever turns the panel back on. The Deck sits awake on a
black screen with no wake path.

Replace the recommendation with the revert steps and point at the actual fix for
the underlying problem: a resume hook that restarts tailscaled + protopen-runtime
after suspend, rather than preventing sleep at all.

Device already reverted: the four targets are unmasked (now `static`) and the
mask paths dropped from /etc/atomic-update.conf.d/protopen-keep.conf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The deep-suspend guidance now explains S3 disconnect behavior, removes sleep-target masking instructions, documents black-screen recovery commands, and recommends a resume hook that restarts tailscaled and protopen-runtime.

Changes

Deep-suspend guidance

Layer / File(s) Summary
S3 recovery and resume handling
.claude/skills/deck-deploy/SKILL.md
The guidance documents S3-related disconnections, masking-related black screens, unmasking and cleanup commands, and a resume hook for restarting services.

Estimated code review effort: 1 (Trivial) | ~3 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title correctly mentions unmasking sleep, but it incorrectly states that the pull request adds a reachability watchdog that is not included. Rename the title to describe the documentation change and sleep-target recovery, without claiming that the resume hook or watchdog was added.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deck-unmask-sleep

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/deck-deploy/SKILL.md:
- Around line 110-112: The resume-hook instructions must restart the
user-managed tailscaled.service and protopen-runtime.service in the deck
user-manager context, rather than treating them as root system services. Update
the system-sleep hook guidance to use the deck machine target and execute after
resume networking is available, before relying on the proxy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b2a92a1a-e9bc-46a2-a4e1-679152f0879f

📥 Commits

Reviewing files that changed from the base of the PR and between 55e0566 and 928ab05.

📒 Files selected for processing (1)
  • .claude/skills/deck-deploy/SKILL.md

Comment thread .claude/skills/deck-deploy/SKILL.md Outdated
Comment on lines +110 to +112
The right way to keep the Deck reachable across a suspend is a resume hook
(`/usr/lib/systemd/system-sleep/`) that restarts tailscaled + `protopen-runtime`,
not preventing sleep.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  '/usr/lib/systemd/system-sleep|systemctl --user|systemctl --machine|tailscaled(\.service)?|protopen-runtime(\.service)?' \
  --glob '*.sh' --glob '*.service' --glob '*.md' .

Repository: protoLabsAI/protoPen

Length of output: 27658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SKILL.md excerpt =="
cat -n .claude/skills/deck-deploy/SKILL.md | sed -n '1,150p'

echo
echo "== related files if present =="
for f in .claude/skills/deck-deploy/*.sh .claude/skills/deck-deploy/*.service; do
  [ -e "$f" ] && { echo "--- $f"; cat -n "$f" | sed -n '1,220p'; }
done

echo
echo "== broad sleep references =="
rg -n -C 3 'system-sleep|sleep.target|suspend.target|hibernate.target|hybridsleep|tailscaled|protopen-runtime' .

Repository: protoLabsAI/protoPen

Length of output: 25741


Make the resume hook restart the systemd user services under deck.

.config/systemd/user/ installs tailscaled.service and protopen-runtime.service as user units, while /usr/lib/systemd/system-sleep/ runs as root. A resume hook needs the deck user-manager context, for example systemctl --machine deck@deck restart tailscaled.service protopen-runtime.service, and should run after the resume network is available before relying on the proxy.

🧰 Tools
🪛 SkillSpector (2.4.4)

[warning] 108: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope.

(Privilege Escalation (PE2))


[warning] 117: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope.

(Privilege Escalation (PE2))


[error] 116: [TM1] Tool Parameter Abuse: Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Remediation: Validate all tool parameters against an allowlist. Reject dangerous parameter values (shell=True, --force, -rf /) and use safe defaults.

(Tool Misuse (TM1))


[error] 116: [YR1] YARA rule 'agent_skill_destructive_autonomous_actions': Autonomous destructive filesystem, shell history, or repository actions in AI agent skills [agent_skills]: YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Remediation: Remove the malware payload or compromised file entirely. Investigate how it entered the skill and audit all other artifacts for additional indicators of compromise.

(YARA Match (YR1))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/deck-deploy/SKILL.md around lines 110 - 112, The resume-hook
instructions must restart the user-managed tailscaled.service and
protopen-runtime.service in the deck user-manager context, rather than treating
them as root system services. Update the system-sleep hook guidance to use the
deck machine target and execute after resume networking is available, before
relying on the proxy.

Completes the sleep revert. Sleep was masked on 2026-07-22 because deep-suspend
freezes wifi, tailscaled and the runtime container and they don't reliably come
back — leaving the Deck unreachable. Masking traded that for a worse failure (a
black screen with no wake path), so unmasking needs the real fix underneath it.

`protopen-watchdog.timer` + `.sh` probe the tailnet and the runtime and repair
only what's actually broken:

- REPAIR, DON'T RESTART — a runtime restart costs ~70s and kills in-flight work,
  so a healthy tick does nothing and logs nothing.
- A boot grace (180s) plus a one-tick wait before acting keeps it from fighting a
  service that is merely still starting, or thrashing on a transient blip.
- Skips any unit the operator has deliberately disabled.

Why a user timer rather than a /usr/lib/systemd/system-sleep hook: that directory
is root-owned on the rootfs and needs an atomic-update keep-list entry to survive
an OS update — precisely the fragility that produced the black screen. install.sh
deliberately lives entirely in /home and this follows suit. The timer is realtime,
so a tick elapsed during suspend runs on resume; it therefore covers resume *and*
wifi drops / container crashes, which a resume-only hook would not.

Verified live: `systemctl --user stop protopen-runtime` (an explicit stop, which
suppresses the unit's own Restart=on-failure) → watchdog logged "not active —
starting it" 10s later → serving again within 40s. The tailscaled repair branch is
deliberately untested in place, since breaking the tailnet would sever the only
access path to the Deck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mabry1985 mabry1985 changed the title docs(deck): stop recommending masked sleep — it strands the Deck on a black screen fix(deck): unmask sleep + add the reachability watchdog that makes it safe Jul 31, 2026
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