# Liveness marker fresh through a long turn (#2 / mesh-hygiene)
Status: spec (commander: "lab up #2", 2026-07-13). Precondition for safe auto-boot โ
before any cell drops --no-respawn. Discovered by droplet; analysis converged (DMs 4622/4623).
1. Problem
The watchdog's liveness = freshest mtime of {drain-cursor, activity-marker}. The marker
($TMPDIR/) is touched by a Stop hook โ i.e. only at turn-end.
During a long autonomous turn (a multi-hour SDD run = one continuous turn, no turn-end), the
marker goes stale, the drain-cursor is also stale (not draining), and F3 pane-activity can go
stale too. Result: a false-stall โ the watchdog fires A1 (send-keys a wake INTO the working
session) โ and A1 is not gated by --no-respawn, so it disrupts even a shadow-staged cell.
droplet proved this live (marker 6.5h stale mid-turn). This is the hard precondition before
any cell can safely drop --no-respawn and let A2 actually recover cells.
2. Fix
Touch the marker on PreToolUse (every tool call), not just Stop. An actively-working cell makes tool calls throughout a long turn โ the marker stays fresh โ no false-stall. Turn it from an ad-hoc manual Stop hook into a proper swarph-cli bundled hook so every cell gets it.
3. The three constraints a naive fix breaks (why this is subtle)
1. Shared-user box: lab-ovh runs ~6 cells under one ubuntu user โ ONE
~/.claude/settings.json. A user-level hook fires for EVERY cell.
2. Project hooks are silently ignored (Claude Code โฅ2.1.186 โ proven with science-claude:
a Stop hook NEVER fired across a reload + reboot). So the
hook MUST be user-level; a per-cell project hook is a dead end.
3. SWARPH_CELL is contaminated (#20): the shared .bashrc sets it last-wins = gridiron
for all. So a hook resolving ${SWARPH_CELL}-claude-active.txt touches the WRONG cell's
marker. A wrong-path touch is WORSE than no fix โ it makes a dead cell look alive (marker
fresh from another cell's tool calls) โ the watchdog never recovers it. The recovery layer
must fail conservative; a mis-resolved marker fails aggressive.
4. Design โ resolve by CWD, agree with the watchdog by construction
The one per-cell-reliable signal on a shared-user box is cwd: each cell's Claude session runs in its own project dir, and hooks run in that cwd. So:
- New subcommand
swarph hooks touch-activity: resolves the marker path using the SAME
discover_cell_in_cwd() โ role โ _resolve_activity_marker_path),
then touches it. Because hook and watchdog call the identical resolver, they AGREE by
construction โ no path drift possible. cwd-keyed โ per-cell-correct despite SWARPH_CELL
contamination.
- New builtin
activity-markerHookBundle: a tiny script that runs
swarph hooks touch-activity (best-effort, always exit 0 โ a liveness hook must never fail a
turn), bound to PreToolUse ("" = all tools) + Stop + StopFailure.
- Fallback when no cell.yaml in cwd: use
\({TMPDIR:-/tmp}/\){SWARPH_CELL:-lab}-claude-active.txt
5. Scope / deployment discipline
- BUILD (safe, this session): the
touch-activitysubcommand + theactivity-marker
- DO NOT auto-install onto live cells โ that's a
~/.claude/settings.jsonchange on ~6
swarph hooks install activity-marker); let droplet validate it on its own box as part
of its watchdog staging (it's the one cell already proving the long-turn failure), then roll
out with the commander's eye.
- PreToolUse fires on every tool call; the touch is a microsecond
touchโ negligible latency.
6. Tests
touch-activityresolves the marker path via cell.yaml-in-cwd โ touches exactly the path the
- Fallback to
$TMPDIR/when no cell.yaml.-claude-active.txt - Never non-zero exit even on an unwritable path (a liveness hook must not fail the turn).
- The
activity-markerbundle binds PreToolUse + Stop + StopFailure; the script calls
swarph hooks touch-activity.
swarph hooks install activity-markermerges the three bindings into settings.json.