Technical Spec β Issue #155: Task Distribution & Assignment#
Source ticket:
spec/tickets/Task_Distribution_&_Assignment.md
GitHub issue: #155
Branch:155-featscheduler-task-distribution-assignment-...
Track: Phase 1 β Foundation, Step 1 of 11 (Scheduler)
1. Issue Summary#
Land the complete assignment layer for HashHive's scheduler so campaigns actually
distribute end-to-end: strict DB-predicate task assignment, hybrid sync/async
generation, reassignment background job, retry logic, priority queuing, and a
heartbeat hasHighPriorityTasks flag.
2. Problem Statement#
The original ticket called out that assignNextTask() selected the first globally
pending task then post-filtered for project scope and capabilities β meaning agents
saw "no task" even when their project had matching pending work.
Current reality (after exploration): the assignment query has already been
rewritten to use an atomic CTE with FOR UPDATE β¦ SKIP LOCKED, project-scoped join,
and a capability predicate in the WHERE clause (packages/backend/src/services/tasks.ts:415-454).
Significant adjacent work has also landed:
- Capability predicate builder (
buildCapabilityPredicate) - Skip-reason diagnosis (
diagnoseAssignmentSkip) - Priority queue infrastructure (
config/queue.ts:TASKS_HIGH/NORMAL/LOW) - Heartbeat
hasHighPriorityTasksflag (services/agents.ts:738, route atroutes/agent/index.ts:103) - Reassignment via heartbeat-monitor worker (cadence
HEARTBEAT_SCHEDULER_INTERVAL_MS = 2 min) - Retry ceiling
MAX_RETRIES = 3with permanent-fail branch (services/tasks.ts:683) - Hybrid inline/async generation via
resolveGenerationStrategy(services/campaigns.ts:638)
This spec therefore narrows scope to verification of every acceptance criterion
plus closing any concrete gaps found rather than re-implementing the layer.
3. Technical Approach#
Verify-first, fix-only-where-needed. For each AC bullet:
- Locate the code path
- Confirm a test exercises it
- Patch the implementation or add a test if either is missing
- Update OpenAPI spec when crossing the agent API boundary
Hard constraints (per AGENTS.md):
- Cross-API-boundary types live in
@hashhive/shared(Zodz.infer) - OpenAPI
agent-api.yamlupdated in lock-step withhasHighPriorityTasksshape - Agent API is sacred β never break it for dashboard convenience
just checkandjust ci-checkmust be green at commit time
4. Implementation Plan#
Phase A β Verification Sweep (read-only)#
- Map every AC checkbox in #155 to its concrete code symbol and test file.
- Build a coverage matrix (AC β code β test) and list orphans.
- Run
just test-backend -- tasks heartbeat campaignsto confirm green.
Phase B β Close Verified Gaps#
For each orphan in the matrix:
- Write the failing test first (TDD red).
- Implement the minimum change to pass (green).
- Re-run
just check.
Phase C β Spec & Index Sync#
- Ensure
tasks(status, project_id)composite index exists in a Drizzle
migration; add one if missing. - Verify
agent-api.yaml'sHeartbeatResponseschema lists
hasHighPriorityTasksas an optional boolean. - Run the contract test (
tests/unit/agent-api-contract.test.ts).
Phase D β Final Gate#
just check(format, lint, type-check, build)just ci-check(full test suite)- Open PR linked to #155
5. Test Plan#
TDD discipline per ~/.claude/rules/testing.md β every behavior gets a failing
test before any production code moves.
Unit (bun, backend)#
assignNextTask- returns null when no pending task matches project + capabilities
- assigns highest-priority task first when priorities differ
- is race-safe under concurrent calls (mock
FOR UPDATE SKIP LOCKED) - logs
agent_not_eligiblewhen agent status is wrong - logs
claim_race_loston diagnose-failure fallback
reassignStaleTasks- resets
assignedtasks whose agent is offline β₯ 5 min - increments
retry_countand clearsagent_id - permanently fails tasks once
retry_count β₯ MAX_RETRIES
- resets
getTaskQueueForPriorityβ priority value β queue name table
Integration#
- Heartbeat returns
hasHighPriorityTasks: truewhen high-priority pending tasks
exist for the agent's project + capabilities; absent (omitted) otherwise. - Campaign start with < 100 estimated tasks generates inline; β₯ 100 enqueues to
jobs-task-generationwith priority mapped from campaign priority.
Contract#
tests/unit/agent-api-contract.test.tsproves the heartbeat response shape
matchespackages/openapi/agent-api.yaml.
Coverage gate#
- β₯ 80 % per
~/.claude/rules/testing.md; verify viajust test-backend
coverage output.
6. Files to Modify / Create#
| Path | Action |
|---|---|
packages/backend/src/services/tasks.ts | Verify; patch only gaps from matrix |
packages/backend/src/services/agents.ts | Verify hasHighPriorityTasks query path |
packages/backend/src/services/campaigns.ts | Verify hybrid generation branch |
packages/backend/src/queue/workers/heartbeat-monitor.ts | Verify reassignment cadence |
packages/backend/src/config/queue.ts | Verify priority queue mapping |
packages/backend/src/db/migrations/<new>.sql | Add tasks(status, project_id) composite index if absent |
packages/openapi/agent-api.yaml | Confirm hasHighPriorityTasks field in HeartbeatResponse |
packages/shared/src/schemas/index.ts | Confirm HeartbeatResponseSchema carries the field |
packages/backend/tests/integration/agent-heartbeat.test.ts | Extend if AC gaps found |
packages/backend/tests/unit/tasks.test.ts | Extend if AC gaps found |
7. Success Criteria#
All six AC blocks in #155 close with green tests and just ci-check clean:
- Strict assignment β atomic
UPDATE β¦ WHEREwith project + capability filter inWHERE, returnsnullon empty. - Hybrid generation β < 100 inline, β₯ 100 enqueued to
jobs-task-generation. - Reassignment β 2-min cadence; resets
assigned+stale tasks whose agent is offline β₯ 5 min. - Retry β
retry_count < 3βpending;β₯ 3β permanentfailed. - Priority queuing β campaign priority β
tasks-high/tasks-normal/tasks-low. - Heartbeat flag β
hasHighPriorityTasksreflects pending high-priority work for the agent's scope.
Definition of done:
just checkgreenjust ci-checkgreen- OpenAPI spec β shared types β contract test all in sync
- PR linked to #155 with a test plan checklist
8. Out of Scope#
- Advanced keyspace optimization (issue #99 / #103)
- Task preemption β stopping a running task for higher-priority work (issue #97)
- Dynamic task splitting
- Agent API v2 surface changes (issue #111)
9. Risks & Mitigations#
| Risk | Mitigation |
|---|---|
Hidden race in FOR UPDATE SKIP LOCKED path under concurrency | Concurrency unit test that fires N parallel assignNextTask calls against a 1-task fixture |
| Composite index missing in production migration history | Audit db/migrations/; add a forward-only Drizzle migration if absent |
hasHighPriorityTasks flag false-positive under capability mismatch | Integration test exercises capability boundary |
| Hybrid generation rollback path leaks campaign state | Verify rollback resets status, startedAt, completedAt, progress together |
10. Open Questions#
- Does the heartbeat-monitor schedule survive Redis disconnect/reconnect (per the
reconnect hook inqueue/manager.ts)? Worth an explicit test if not covered. - Is the
FOR UPDATE SKIP LOCKEDcandidate-then-update CTE provably equivalent
to a singleUPDATE β¦ FROM β¦ WHEREunder PostgreSQL? If not, the
diagnose-skip path may misclassify a real lock collision asclaim_race_lost.