From ef8e3aa1b8ab1091abc1e3890fa79ccc1eb19170 Mon Sep 17 00:00:00 2001 From: robbyczgw-cla Date: Wed, 29 Apr 2026 14:55:47 +0000 Subject: [PATCH] fix(poll-loop): apply pre-task scripts to follow-up injections too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks arriving during an active query were pushed into the stream as follow-ups without running their `script` gate — so a wakeAgent=false pre-script that was supposed to suppress the tick silently leaked through and woke the agent every time. Evidence: monitoring cron firing every 10 min with [task-script] log lines never showing. Run applyPreTaskScripts on the follow-up batch too: wakeAgent=false tasks get marked completed and dropped; wakeAgent=true tasks have scriptOutput enriched exactly like the initial-batch path. Added a pollInFlight guard to serialize async runs and avoid overlapping script executions when the interval fires while one is still going. Wrapped in a MODULE-HOOK:scheduling-pre-task-followup marker block to match the existing initial-batch hook convention. --- container/agent-runner/src/poll-loop.ts | 69 +++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts index bd48db2..589b80b 100644 --- a/container/agent-runner/src/poll-loop.ts +++ b/container/agent-runner/src/poll-loop.ts @@ -260,31 +260,58 @@ async function processQuery( // Stream liveness is decided host-side via the heartbeat file + processing // claim age (see src/host-sweep.ts); if something is truly stuck, the host // will kill the container and messages get reset to pending. + let pollInFlight = false; const pollHandle = setInterval(() => { - if (done) return; + if (done || pollInFlight) return; + pollInFlight = true; - // Skip system messages (MCP tool responses) and /clear (needs fresh query). - // Thread routing is the router's concern — if a message landed in this - // session, the agent should see it. Per-thread sessions already isolate - // threads into separate containers; shared sessions intentionally merge - // everything. Filtering on thread_id here caused deadlocks when the - // initial batch and follow-ups had mismatched thread_ids (e.g. a - // host-generated welcome trigger with null thread vs a Discord DM reply). - const newMessages = getPendingMessages().filter((m) => { - if (m.kind === 'system') return false; - if ((m.kind === 'chat' || m.kind === 'chat-sdk') && isClearCommand(m)) return false; - return true; - }); - if (newMessages.length > 0) { - const newIds = newMessages.map((m) => m.id); - markProcessing(newIds); + void (async () => { + try { + // Skip system messages (MCP tool responses) and /clear (needs fresh query). + // Thread routing is the router's concern — if a message landed in this + // session, the agent should see it. Per-thread sessions already isolate + // threads into separate containers; shared sessions intentionally merge + // everything. Filtering on thread_id here caused deadlocks when the + // initial batch and follow-ups had mismatched thread_ids (e.g. a + // host-generated welcome trigger with null thread vs a Discord DM reply). + const newMessages = getPendingMessages().filter((m) => { + if (m.kind === 'system') return false; + if ((m.kind === 'chat' || m.kind === 'chat-sdk') && isClearCommand(m)) return false; + return true; + }); + if (newMessages.length === 0) return; - const prompt = formatMessages(newMessages); - log(`Pushing ${newMessages.length} follow-up message(s) into active query`); - query.push(prompt); + const newIds = newMessages.map((m) => m.id); + markProcessing(newIds); - markCompleted(newIds); - } + // Run pre-task scripts on follow-ups too — without this, a task that + // arrives during an active query (e.g. a */10 monitoring cron) bypasses + // its script gate and always wakes the agent, defeating the gate. + // Mirrors the initial-batch hook above. + let keep = newMessages; + let skipped: string[] = []; + // MODULE-HOOK:scheduling-pre-task-followup:start + const { applyPreTaskScripts } = await import('./scheduling/task-script.js'); + const preTask = await applyPreTaskScripts(newMessages); + keep = preTask.keep; + skipped = preTask.skipped; + if (skipped.length > 0) { + markCompleted(skipped); + log(`Pre-task script skipped ${skipped.length} follow-up task(s): ${skipped.join(', ')}`); + } + // MODULE-HOOK:scheduling-pre-task-followup:end + + if (keep.length === 0) return; + + const keptIds = keep.map((m) => m.id); + const prompt = formatMessages(keep); + log(`Pushing ${keep.length} follow-up message(s) into active query`); + query.push(prompt); + markCompleted(keptIds); + } finally { + pollInFlight = false; + } + })(); }, ACTIVE_POLL_INTERVAL_MS); try {