Auto-exit server after 30 minutes idle, add liveness check to skill

Server tracks activity (HTTP requests, WebSocket messages, file
changes) and exits after 30 minutes of inactivity. On exit, deletes
.server-info and writes .server-stopped with reason. Visual companion
guide now instructs agents to check .server-info before each screen
push and restart if needed. Works on all harnesses, not just CC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesse Vincent
2026-03-11 18:32:09 -07:00
parent 85cab6eff0
commit 263e3268f4
2 changed files with 31 additions and 1 deletions

View File

@@ -124,6 +124,7 @@ function getNewestScreen() {
// ========== HTTP Request Handler ==========
function handleRequest(req, res) {
touchActivity();
if (req.method === 'GET' && req.url === '/') {
const screenFile = getNewestScreen();
let html = screenFile
@@ -225,6 +226,7 @@ function handleMessage(text) {
console.error('Failed to parse WebSocket message:', e.message);
return;
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
@@ -239,6 +241,15 @@ function broadcast(msg) {
}
}
// ========== Activity Tracking ==========
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
let lastActivity = Date.now();
function touchActivity() {
lastActivity = Date.now();
}
// ========== File Watching ==========
const debounceTimers = new Map();
@@ -267,6 +278,7 @@ function startServer() {
const filePath = path.join(SCREEN_DIR, filename);
if (!fs.existsSync(filePath)) return; // file was deleted
touchActivity();
if (!knownFiles.has(filename)) {
knownFiles.add(filename);
@@ -282,6 +294,23 @@ function startServer() {
});
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
// Exit if no activity for 30 minutes (prevents orphaned servers)
const idleCheck = setInterval(() => {
if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) {
console.log(JSON.stringify({ type: 'server-stopped', reason: 'idle timeout' }));
const infoFile = path.join(SCREEN_DIR, '.server-info');
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
fs.writeFileSync(
path.join(SCREEN_DIR, '.server-stopped'),
JSON.stringify({ reason: 'idle timeout', timestamp: Date.now() }) + '\n'
);
watcher.close();
clearInterval(idleCheck);
server.close(() => process.exit(0));
}
}, 60 * 1000); // check every minute
idleCheck.unref(); // don't keep process alive just for the timer
server.listen(PORT, HOST, () => {
const info = JSON.stringify({
type: 'server-started', port: Number(PORT), host: HOST,