LearnNewsExamplesServices
Frontmatter
id10339
titleTrack 2C: TTL/Expired sweeper — cron-driven stale-task transition to Expired state
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtApr 25, 2026, 10:05 PM
updatedAtJun 7, 2026, 7:21 PM
githubUrlhttps://github.com/neomjs/neo/issues/10339
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtApr 26, 2026, 3:39 AM

Track 2C: TTL/Expired sweeper — cron-driven stale-task transition to Expired state

Closed v13.0.0/archive-v13-0-0-chunk-6 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Apr 25, 2026, 10:05 PM

Author's Note: Filed by Claude Opus 4.7 (Claude Code) during session b5a17132-7324-46e1-b73e-038825bb4d55 as a Track 2 sub-ticket graduating from Discussion #10313 per @neo-gemini-pro's Option C greenlight. Sibling to Track 2B (state-machine + transition authority + idempotency) and consumer of #10334's task.expiresAt field.

Context

A2A_TASK substrate (per #10334 + Track 2B sibling) introduces a task.expiresAt ISO timestamp field per Discussion #10313 OQ7 resolution. The schema captures TTL intent; this ticket implements the consumer — a cron-driven sweeper that scans for stale Submitted or Working tasks past their expiresAt and atomically transitions them to Expired state.

The Problem

Without TTL enforcement:

  • Tasks Submitted to offline agents accumulate eternally pending
  • Tasks Working on agents that crashed mid-execution stay locked indefinitely (claim-and-lock from Track 2B is permanent absent expiration)
  • Mailbox queue grows unboundedly with zombie work-items
  • Heartbeat reports stale unread counts that don't reflect actionable work

The sweeper is the operational primitive that maintains the queue's actionable-set bounded.

The Architectural Reality

Track 1's swarm-heartbeat.sh (#10335, merging) already polls SQLite at fixed intervals. Adding a sweep step to that cron is the natural integration point — same poll-cycle, different SQL query (sweep vs read).

Two integration paths:

  1. Inline in swarm-heartbeat.sh: add a sweep SQL query alongside the existing unread-count + issues-count polling
  2. Separate sweeper script (ai/scripts/sweepExpiredTasks.sh or .mjs): dedicated process/cron, decoupled from heartbeat

Recommendation: Path 1 (inline). Simpler operationally; same cron cadence; no new process/script. Trade-off: heartbeat script grows; if scope-creep risk, switch to Path 2 later.

The Fix

Phase 1 (this ticket's MVP scope):

  1. Add SQL sweep query to swarm-heartbeat.sh poll cycle:
       UPDATE Nodes SET 
      data = json_set(data, '$.properties.task.state', 'Expired'),
      ... lastModifiedAt update
    WHERE 
      json_extract(data, '$.label') = 'MESSAGE' AND
      json_extract(data, '$.properties.task.state') IN ('Submitted', 'Working', 'InputRequired') AND
      json_extract(data, '$.properties.task.expiresAt') IS NOT NULL AND
      datetime(json_extract(data, '$.properties.task.expiresAt')) < datetime('now');
  2. Sweep fires at every poll interval (5 min default)
  3. Token-economy preserved: heartbeat fast-path still skips inference if zero unread + zero issues (sweeper writes are silent unless inference is otherwise needed)
  4. Observability: log count of swept tasks per cycle to syslog or designated audit-log file

Phase 2 (separate sub-ticket if scope grows):

  • Configurable per-task-type TTL defaults
  • Transition-history annotations on swept tasks (e.g., "expired by sweeper at T")
  • Stale Working task notification to assignee before sweep (warning grace period)

Acceptance Criteria

  • swarm-heartbeat.sh extended with TTL sweep SQL query (or split to separate script if scope justifies)
  • Sweep correctly transitions Submitted/Working/InputRequired tasks past expiresAt to Expired state
  • lastModifiedAt updated atomically with sweep transitions (so subsequent polling cycles see the change)
  • Tasks WITHOUT expiresAt field unaffected by sweep (TTL is opt-in)
  • Tasks already in terminal states (Completed, Canceled, Failed, Rejected, Expired) not re-swept (idempotent)
  • Observability: per-cycle log of swept-count (zero-default to avoid noise)
  • Test: integration test using ai/examples/ script-style verification — populate test task with past-expiresAt, run sweep, verify state transitions
  • No regression on heartbeat token-economy fast-path (if no actionable state, no inject)
  • Update OpenAPI schema if Sweeper exposes any new tool surface (likely none — sweeper is internal cron, no MCP tool)

Out of Scope

  • TTL configuration UI — TTL set at task-creation time via task.expiresAt; no admin override needed for v1
  • Sub-second precisiondatetime('now') granularity is fine for poll-cycle TTL (5-min intervals); microseconds not needed
  • Cross-tenant sweep authority — sweeper runs with operational identity; doesn't enforce per-tenant RLS (since it's a maintenance cron, not user query)
  • Re-queueing or retrying expired tasksExpired is terminal; agents wanting retry must explicitly create new task. Out of scope.
  • Notification to originator on sweep — if originator wants notification, they subscribe to state changes (future Phase 3 mechanism); not in this scope

Avoided Traps

  • Sweeper as a separate long-running process — rejected for MVP; cron-driven discrete sweeps are simpler. Long-running process introduces own lifecycle/restart concerns.
  • Sweep-then-notify-then-transition — rejected; race-y. Atomic UPDATE-WHERE-state-IS-stale-AND-expiresAt-past is single-step.
  • Per-state TTL — rejected for v1; one TTL field per task. Per-state TTL granularity could be Phase 2 if empirically needed.
  • Sweeper claiming permission to transition Working tasks — handled correctly via the optimistic-concurrency UPDATE pattern: the sweeper's cron-identity isn't subject to the same RBAC matrix as agent-originator/agent-assignee. Sweeper is a maintenance role with explicit authority over expired-state transitions only.
  • Auto-renewing TTL on activity — rejected for v1; TTL set at creation, no extension. If task is making progress, agent transitions to Working (per Track 2B); the expiresAt doesn't shift. Future Phase 2 could add explicit extendTtl primitive if needed.

Related

  • #10334 — A2A Task envelope primitive; introduces the task.expiresAt field this ticket consumes
  • Track 2B sibling (state-machine + transition authority) — introduces the Expired state primitive this ticket transitions to
  • #10335 / #10312 — Track 1 cron-heartbeat; integration point for sweep query
  • Discussion #10313 — Track 2 substrate convergence (graduated)
  • #10311 — Epic: Institutionalizing Swarm Autonomy. This ticket is a Track 2 sub.
  • External: A2A Protocol SpecificationCanceled is closest A2A-spec analog (Neo extends with Expired for timeout-specific semantics; external consumers can fallback-interpret as Canceled-with-reason)

Origin Session ID: b5a17132-7324-46e1-b73e-038825bb4d55 Retrieval Hint: "TTL Expired sweeper cron stale-task transition expiresAt heartbeat-integration Track-2C optimistic-concurrency"

tobiu referenced in commit bc5f50e - "feat(mailbox): TTL/Expired sweeper for stale A2A tasks (#10339) (#10350) on Apr 26, 2026, 3:39 AM
tobiu closed this issue on Apr 26, 2026, 3:39 AM