From ef8e3aa1b8ab1091abc1e3890fa79ccc1eb19170 Mon Sep 17 00:00:00 2001 From: robbyczgw-cla Date: Wed, 29 Apr 2026 14:55:47 +0000 Subject: [PATCH 1/5] 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 { From 8dd004ca75c881d19139efde85dab8c6526d06f5 Mon Sep 17 00:00:00 2001 From: Mike Nolet Date: Thu, 30 Apr 2026 08:13:59 +0200 Subject: [PATCH 2/5] fix(scheduling): include routing in schedule_task content JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schedule_task MCP tool wrote routing fields (platform_id, channel_type, thread_id) onto the outbound system message's row columns, but handleSystemAction (src/delivery.ts) parses content JSON and forwards only that to handlers. handleScheduleTask (src/modules/scheduling/actions.ts) reads content.platformId/channelType/threadId — which the writer never populated — so every kind='task' row landed in messages_in with all-null routing. When host-sweep wakes a scheduled task, dispatchResultText's fast path requires routing on the message and bails when it's null, falling through to the "Routing recovery" retry prompt. End-user delivery still works because the agent can pick a destination from its destinations table on retry — so the bug went undetected, silently costing one extra LLM turn per scheduled-task wake. Sessions whose destinations table has no channel row (e.g. agent-only destinations) fail outright with a recovery loop. Fix: add the routing fields to the content JSON so the writer matches the contract handleScheduleTask already expects. cancel/pause/resume/update_task operate by id alone and don't need routing. --- container/agent-runner/src/mcp-tools/scheduling.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/container/agent-runner/src/mcp-tools/scheduling.ts b/container/agent-runner/src/mcp-tools/scheduling.ts index 00e41bb..9b8451d 100644 --- a/container/agent-runner/src/mcp-tools/scheduling.ts +++ b/container/agent-runner/src/mcp-tools/scheduling.ts @@ -89,6 +89,9 @@ export const scheduleTask: McpToolDefinition = { script, processAfter, recurrence, + platformId: r.platform_id, + channelType: r.channel_type, + threadId: r.thread_id, }), }); From 58d875b3c35dec20695b270480a62c7143f62b62 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 21:31:18 +0000 Subject: [PATCH 3/5] chore: bump version to 2.0.21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 556269c..ecdc1aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "2.0.20", + "version": "2.0.21", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "packageManager": "pnpm@10.33.0", From 5ab1a2733c0fde0eb6879180b026e06e9f8e9725 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 1 May 2026 00:55:46 +0300 Subject: [PATCH 4/5] review: catch follow-up poll errors + re-check done before push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes on top of the follow-up pre-task-script work: 1. The void async IIFE inside the interval handler had no catch, so a throw from the dynamic import or applyPreTaskScripts escaped as an unhandled rejection — terminating the container. The initial-batch path is wrapped by processQuery's outer try/catch; the follow-up path needs its own. Now logs the error and lets the next tick retry. 2. Re-check `done` immediately before query.push. The flag can flip true while applyPreTaskScripts is awaited (outer stream finishes during the script execution); without the re-check we'd push into a closed query. Claimed messages get released by the host's processing-claim sweep — same recovery posture as the rest of the poller. Co-Authored-By: Michael Zazon Co-Authored-By: Claude Opus 4.7 (1M context) --- container/agent-runner/src/poll-loop.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts index 589b80b..986489f 100644 --- a/container/agent-runner/src/poll-loop.ts +++ b/container/agent-runner/src/poll-loop.ts @@ -302,12 +302,23 @@ async function processQuery( // MODULE-HOOK:scheduling-pre-task-followup:end if (keep.length === 0) return; + // Re-check done — the outer query may have finished while the script + // was awaited. Pushing into a closed stream is wasted work; the + // claimed messages get released by the host's processing-claim sweep. + if (done) 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); + } catch (err) { + // Without this catch the rejection escapes the void IIFE and Node + // terminates the container on unhandled-rejection. The initial-batch + // path is wrapped by processQuery's outer try/catch; the follow-up + // path is not, so it needs its own. + const errMsg = err instanceof Error ? err.message : String(err); + log(`Follow-up poll error: ${errMsg}`); } finally { pollInFlight = false; } From 8977f0d0be1a4bf9dd6c1bc1ad5c2ec4c96614de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 21:57:45 +0000 Subject: [PATCH 5/5] chore: bump version to 2.0.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecdc1aa..df9cb99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "2.0.21", + "version": "2.0.22", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "packageManager": "pnpm@10.33.0",