fix(poll-loop): apply pre-task scripts to follow-up injections too#2114
Merged
gavrielc merged 3 commits intoApr 30, 2026
Merged
Conversation
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.
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 <mzazon@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
Collaborator
|
@robbyczgw-cla Thank you for the contribution!! |
mzazon
added a commit
to mzazon/nanoclaw
that referenced
this pull request
May 6, 2026
Brings in 202 upstream commits since our last sync. Key additions: - Circuit breaker for crash loop protection - Outbox path-confinement security fix (nanocoai#2001) - Inbound DB fresh-open fix for virtiofs/NFS (nanocoai#2160) - Orphan processing_ack cleanup (nanocoai#2151) - Pre-task scripts on follow-up poll injections (nanocoai#2114) - Attachment naming/safety refactor - Setup flow improvements (splash, headless, env reuse) - Channel approval flow enhancements Conflict resolution: - Dockerfile: kept OPENCODE_VERSION, adopted upstream's pinned Vercel 52.2.1 - poll-loop.ts: took upstream's async pre-task handling (subsumes our try/catch guard) - agent-route.ts: took upstream's factored-out isSafeAttachmentName - src/index.ts: kept both readEnvFile and new circuit-breaker imports - setup/verify.ts, agent-ping.ts: took upstream's simplified verify flow - package.json: took upstream version 2.0.25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pre-task scripts were applied to the initial message batch but not to messages arriving as follow-ups during an active query. A scheduled task with a
scriptgate that returnedwakeAgent: falsewould therefore wake the agent every time it landed mid-stream — silently bypassing the gate.Reproduction
scriptthat returns{ wakeAgent: false }and an interval that fires while the agent is mid-turn (e.g.*/10 * * * *monitoring cron).[task-script]log lines never appear for those follow-up firings.Expected: the script gate suppresses the tick exactly as it does for the initial batch.
Fix
container/agent-runner/src/poll-loop.ts— thesetIntervalfollow-up poller now runsapplyPreTaskScriptson the new-message batch, mirroring the initial-batch path at line ~141. Behavior:wakeAgent: false(or script error) → task row marked completed and dropped from the follow-up pushwakeAgent: true→scriptOutputenrichment carried through into the prompt, same as the initial pathpollInFlightguard serializes async runs so an interval tick won't overlap with a still-running script executionThe work is wrapped in a
MODULE-HOOK:scheduling-pre-task-followupmarker block to match the convention used by the initial-batch hook.Why now
The follow-up branch existed before scheduling/script-gating was added. When
applyPreTaskScriptswas introduced for the initial batch it was never extended to the concurrent-poll path — the bug has been latent since the script-gate feature landed.Test plan
pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit) clean*/10monitoring cron + active query → cron fired every 10 min before fix; gated correctly after fix (no[task-script]log lines = drop, with output = inject)pollInFlightguard semantics — interval-fires-while-script-runs is the only realistic overlap path, and the guard makes that a no-op rather than queueing.Risk
Low. The follow-up branch already issued an async
query.push; the only behavior change is gating that push behind the same script-result rules the initial batch already uses. If the scheduling module is absent (notask-script.js), the dynamic import throws, thetry/finallyclearspollInFlight, and the next tick retries — same failure-recovery posture as the rest of the poller.