---
id: 10595
title: sweepExpiredTasks.mjs direct-invocation fails with "Neo is not defined"
state: CLOSED
labels:
  - bug
  - ai
  - regression
assignees:
  - neo-opus-ada
createdAt: '2026-05-01T18:36:48Z'
updatedAt: '2026-05-01T20:28:49Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10595'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-01T20:28:49Z'
---
# sweepExpiredTasks.mjs direct-invocation fails with "Neo is not defined"

## Context

Surfaced empirically by [@neo-gpt](https://github.com/neo-gpt) during [#10318](https://github.com/neomjs/neo/issues/10318) heartbeat token-economy measurement (PR [#10594](https://github.com/neomjs/neo/pull/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](https://github.com/neomjs/neo/issues/10311) (Track 2C, [#10339](https://github.com/neomjs/neo/issues/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:

```js
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](https://github.com/neomjs/neo/issues/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](https://github.com/neomjs/neo/pull/10594) — `## TTL Sweeper Caveat` section.
- Parent epic: [#10311](https://github.com/neomjs/neo/issues/10311) Track 2C task expiration.
- Adjacent: [#10339](https://github.com/neomjs/neo/issues/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"

## Timeline

- 2026-05-01T18:36:50Z @neo-opus-ada added the `bug` label
- 2026-05-01T18:36:50Z @neo-opus-ada added the `ai` label
- 2026-05-01T18:36:50Z @neo-opus-ada added the `regression` label
- 2026-05-01T18:37:00Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-01T18:41:58Z @neo-opus-ada cross-referenced by PR #10597
- 2026-05-01T20:28:49Z @tobiu referenced in commit `d49fd5d` - "fix(ai): import Neo prelude in sweepExpiredTasks + tighten heartbeat stderr (#10595) (#10597)

* fix(ai): import Neo prelude in sweepExpiredTasks + tighten heartbeat stderr (#10595)

* fix(ai): address PR #10597 cycle 1 RAs — drop unsafe DB-write test + mkdir guard (#10595)

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-01T20:28:49Z @tobiu closed this issue
- 2026-05-01T21:25:10Z @neo-opus-ada cross-referenced by PR #10602
- 2026-05-01T22:17:47Z @neo-opus-ada cross-referenced by PR #10607
- 2026-05-03T14:10:09Z @neo-opus-ada cross-referenced by PR #10639
- 2026-05-09T09:55:24Z @neo-opus-ada referenced in commit `0be1f62` - "fix(memory-core): update SDK migration regex assertion in sweepExpiredTasks spec (#10996)

The structural import-order regression guard (#10595) at
test/playwright/unit/ai/scripts/sweepExpiredTasks.spec.mjs:75 used a
regex literal asserting the OLD path
'../mcp/server/memory-core/services/lifecycle/SystemLifecycleService.mjs'.
The bulk migration script's structural rewrite missed it because the
regex literal escapes slashes as `\/`, so the substring search did not
match. Update the regex to assert the NEW canonical SDK path
'../services/memory-core/lifecycle/SystemLifecycleService.mjs'.

Test passes locally on the new path; CI unit row will go green on next run.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-09T10:33:20Z @tobiu referenced in commit `4a136a4` - "refactor(memory-core): migrate services + managers + helpers to flat SDK boundary (#10996) (#11001)

* refactor(memory-core): migrate services + managers + helpers to flat SDK boundary (#10996)

Move 21 entities (14 top-level services + 3 lifecycle + 4 managers) plus 1
helper from `ai/mcp/server/memory-core/{services,managers,helpers}/` to
`ai/services/memory-core/`. Mirrors the M6 sub-1/2/3 precedent (KB #10995,
GH-WF #10997, NL #10998). Class names and `Memory_*` SDK aliases unchanged;
custom `boot()` override, identity-spoof guard, wake substrate, sibling-
concurrency log, and `bindAgentIdentity` retry pattern preserved.

Test specs relocated to canonical SDK location at
`test/playwright/unit/ai/services/memory-core/` with depth-corrected relative
paths. Consumer test files (daemons, scripts, integration) and learn/agentos
docs updated to reference the new shape.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(memory-core): correct dynamic import path for logger (#11001)

* fix(memory-core): update SDK migration regex assertion in sweepExpiredTasks spec (#10996)

The structural import-order regression guard (#10595) at
test/playwright/unit/ai/scripts/sweepExpiredTasks.spec.mjs:75 used a
regex literal asserting the OLD path
'../mcp/server/memory-core/services/lifecycle/SystemLifecycleService.mjs'.
The bulk migration script's structural rewrite missed it because the
regex literal escapes slashes as `\/`, so the substring search did not
match. Update the regex to assert the NEW canonical SDK path
'../services/memory-core/lifecycle/SystemLifecycleService.mjs'.

Test passes locally on the new path; CI unit row will go green on next run.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(memory-core): add compatibility shim at old EmbeddingProviderConfig path (#10996)

Per @neo-gpt's PR #11001 Cycle 1 review (RA1): existing gitignored
ai/mcp/server/memory-core/config.mjs files (per `.gitignore:104`) that
predate this migration may import `./helpers/EmbeddingProviderConfig.mjs`.
After the M6 helper move that path resolves to ERR_MODULE_NOT_FOUND.

Shim at the old path re-exports the canonical surface from the new SDK
location at `ai/services/memory-core/helpers/EmbeddingProviderConfig.mjs`,
preserving runtime compatibility for stale gitignored configs without
forcing operators to re-cp from config.template.mjs.

Verified: `node --input-type=module -e "await import('./ai/mcp/server/
memory-core/helpers/EmbeddingProviderConfig.mjs')"` exposes both
resolveEmbeddingProvider and normalizeEmbeddingProviderConfig.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
Co-authored-by: neo-gemini-pro <neo-gemini-pro@users.noreply.github.com>"

