Skip to content

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

Merged
gavrielc merged 3 commits into
nanocoai:mainfrom
robbyczgw-cla:fix/poll-loop-prescripts-on-followups
Apr 30, 2026
Merged

fix(poll-loop): apply pre-task scripts to follow-up injections too#2114
gavrielc merged 3 commits into
nanocoai:mainfrom
robbyczgw-cla:fix/poll-loop-prescripts-on-followups

Conversation

@robbyczgw-cla
Copy link
Copy Markdown
Contributor

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 script gate that returned wakeAgent: false would therefore wake the agent every time it landed mid-stream — silently bypassing the gate.

Reproduction

  1. Schedule a task with a script that returns { wakeAgent: false } and an interval that fires while the agent is mid-turn (e.g. */10 * * * * monitoring cron).
  2. Send a chat message that triggers a multi-step turn (anything taking >10s).
  3. Observe: the cron task wakes the agent every 10 min anyway. [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 — the setInterval follow-up poller now runs applyPreTaskScripts on 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 push
  • wakeAgent: truescriptOutput enrichment carried through into the prompt, same as the initial path
  • A pollInFlight guard serializes async runs so an interval tick won't overlap with a still-running script execution

The work is wrapped in a MODULE-HOOK:scheduling-pre-task-followup marker 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 applyPreTaskScripts was 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

  • TypeScript compile (pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit) clean
  • Reproduced on a real install with a */10 monitoring cron + active query → cron fired every 10 min before fix; gated correctly after fix (no [task-script] log lines = drop, with output = inject)
  • Reviewer: confirm pollInFlight guard 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 (no task-script.js), the dynamic import throws, the try/finally clears pollInFlight, and the next tick retries — same failure-recovery posture as the rest of the poller.

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.
gavrielc and others added 2 commits May 1, 2026 00:34
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>
@gavrielc
Copy link
Copy Markdown
Collaborator

@robbyczgw-cla Thank you for the contribution!!

@gavrielc gavrielc merged commit d13f338 into nanocoai:main Apr 30, 2026
1 check passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants