LearnNewsExamplesServices
Frontmatter
id10595
titlesweepExpiredTasks.mjs direct-invocation fails with "Neo is not defined"
stateClosed
labels
bugairegression
assigneesneo-opus-4-7
createdAtMay 1, 2026, 8:36 PM
updatedAtMay 1, 2026, 10:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/10595
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 1, 2026, 10:28 PM

sweepExpiredTasks.mjs direct-invocation fails with "Neo is not defined"

Closedbugairegression
neo-opus-4-7
neo-opus-4-7 commented on May 1, 2026, 8:36 PM

Context

Surfaced empirically by @neo-gpt during #10318 heartbeat token-economy measurement (PR #10594, see ## TTL Sweeper Caveat section of learn/agentos/measurements/heartbeat-token-economy-2026-05.md).

ai/scripts/sweepExpiredTasks.mjs direct-invocation currently exits non-zero after ~116ms with:

ReferenceError: Neo is not defined

The swarm-heartbeat.sh script invokes the sweeper before its token-economy fast path, redirecting stderr to /dev/null. This silently masks the failure — expired falls back to 0 and the heartbeat loop continues without surfacing the regression.

The Problem

Two compounding harms:

  1. Heartbeat-driven task expiration is structurally broken. Epic #10311 (Track 2C, #10339 closed) shipped the sweeper as the cron-driven mechanism for transitioning stale tasks to the Expired state. The current Neo is not defined regression means no automated task expiration is occurring — task-state-machine claims about TTL behavior are aspirational, not empirical.

  2. Silent stderr-redirect masks the failure. The heartbeat loop runs the sweeper roughly 288 times/day (5-min cadence) with no operator-visible signal that it's failing every single invocation. ~116ms × 288 × N agents = wall-clock waste that contributes nothing while the regression persists. Per the empirical anchor in PR #10594's measurement page: "the local graph contained no A2A Task nodes during measurement, so no task state was changed by this probe" — the regression's behavioral impact is invisible until A2A Task nodes accumulate, at which point expiration silently doesn't happen.

The Architectural Reality

The error message — ReferenceError: Neo is not defined — is the canonical symptom of a script that consumes the Neo class system without first importing the substrate (src/Neo.mjs + src/core/_export.mjs). Sibling scripts in buildScripts/ai/ (e.g., runSandman.mjs) explicitly import:

import Neo from '../../src/Neo.mjs';
import * as core from '../../src/core/_export.mjs';
import InstanceManager from '../../src/manager/Instance.mjs';

If sweepExpiredTasks.mjs either doesn't import these or imports a SystemLifecycleService / GraphService entry that depends on Neo.setupClass() being available at module-load time, the failure shape matches. Empirical inspection of the script's imports + dependency chain is Phase 1 of the fix.

The Fix

Phase 1 — Diagnose: Read ai/scripts/sweepExpiredTasks.mjs head, identify import chain gap by comparing against working sibling buildScripts/ai/runSandman.mjs.

Phase 2 — Targeted patch: Typically one of: missing import Neo + import * as core; reorder imports; or move the script under buildScripts/ai/ if path-resolution differs.

Phase 3 — Stop the silent mask: swarm-heartbeat.sh currently redirects sweeper stderr to /dev/null. After regression fix, surface to operator log instead.

Phase 4 — Lock with test: Add Playwright unit-test under test/playwright/unit/ai/scripts/sweepExpiredTasks.spec.mjs against an empty-graph fixture.

Acceptance Criteria

  • (AC1) node ai/scripts/sweepExpiredTasks.mjs exits 0 on a clean local checkout (currently exits non-zero with ReferenceError).
  • (AC2) Phase 1 diagnosis comment posted: identifies the exact import chain gap or class-registration ordering issue (NOT speculative; empirically captured).
  • (AC3) Phase 2 patch shipped: minimal-surface fix matching the diagnosed cause.
  • (AC4) Phase 3 stderr-redirect tightening shipped in swarm-heartbeat.sh — sweeper failures surface to operator log, not /dev/null.
  • (AC5) Phase 4 Playwright spec added — covers the script's main entry path against an empty-graph fixture.
  • (AC6) Post-fix: re-run @neo-gpt's PR #10594 measurement methodology and confirm sweeper cycle is sub-50ms (well under the current 116ms broken-fast-fail).

Out of Scope

  • Refactoring swarm-heartbeat.sh itself beyond the stderr-redirect tightening.
  • Changing the heartbeat cadence (5-min default validated by #10318 measurement).
  • Migrating the sweeper to a different substrate (#10186 / #10311 govern the broader substrate; this is the narrow regression fix).
  • Re-enabling autoDream / autoGoldenPath (per #10569 hard-stop).

Avoided Traps

  • Trap: re-write the sweeper to bypass Neo.setupClass. Rejected — Neo is the canonical substrate; bypassing diverges from the rest of buildScripts/ai. Single-import-fix shape is right.
  • Trap: leave the stderr redirect alone. Rejected — the silent-mask is itself a discipline gap that allowed this regression to persist undetected. Tightening prevents the next regression from hiding for as long.
  • Trap: bundle with #10319 heartbeat concurrency. Rejected — different mechanical concern (sweeper invocation vs heartbeat-pulse mutex).

Related

  • Empirical anchor: PR #10594## TTL Sweeper Caveat section.
  • Parent epic: #10311 Track 2C task expiration.
  • Adjacent: #10339 (closed; original sweeper implementation).

Origin Session ID: 86b7a3a0-7b14-4bd1-b707-52c5741aaeeb Retrieval Hint: "sweepExpiredTasks.mjs Neo is not defined ReferenceError TTL sweeper heartbeat regression"

tobiu referenced in commit d49fd5d - "fix(ai): import Neo prelude in sweepExpiredTasks + tighten heartbeat stderr (#10595) (#10597) on May 1, 2026, 10:28 PM
tobiu closed this issue on May 1, 2026, 10:28 PM
tobiu referenced in commit 4a136a4 - "refactor(memory-core): migrate services + managers + helpers to flat SDK boundary (#10996) (#11001) on May 9, 2026, 12:33 PM