fix(poll-loop): apply pre-task scripts to follow-up injections too

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.
This commit is contained in:
robbyczgw-cla
2026-04-29 14:55:47 +00:00
parent 9e45845000
commit ef8e3aa1b8

View File

@@ -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 {