Skip to Content
Design DecisionsSerial Task Processor

Strictly serial task processing with per-request git branches

Status: Accepted · Date: 2026-05-30 (aggregation/summary follow-up 2026-05-31) · Area: Agents

Context

The Orbit agent plans a request into worker tasks and executes them against the solution repository. Before 2026-05-30, each HTTP request ran its own serial loop inside the team lead — tasks were serial within a request but ran in parallel across concurrent requests, with multiple executors mutating one working tree. The agent also needed a defensible git story: partial work must not leak into the base branch, and a crash must not leave the tree in an ambiguous state.

Decision

  • Execution is strictly one task at a time across all requests, owned by a single process-wide TaskProcessor. TeamLead only plans and enqueues (batched task creation, then kick()); it no longer executes or commits.
  • Kick-on-enqueue, no polling timer. Producers call kick(); a double-check after the drain clears the running flag (re-check hasPending(), re-kick) closes the race where a task enqueued in the gap would be stranded — that check is load-bearing.
  • Per-request git branch lifecycle: a branch is created lazily from base on a request’s first task; workspace/ is committed per task (also on failure, keeping the tree clean); when all tasks complete, the branch is merged --no-ff into base, pushed, and deleted; on any failure, cancel, or merge conflict the request fails and the branch is kept unmerged with the working tree returned to base. Confirmed decisions: a failure cancels the request’s pending siblings; merge only on all-completed (a mid-failure multi-task request will not auto-merge after a single-task continue); branches are pushed for visibility.
  • The base branch is persisted at .orbit/base-branch on first clean boot, so crash recovery on a req-* branch cannot mistake it for base. Startup recovery commits the interrupted task’s partial work, marks it failed, finalizes the request, returns to base, and drains the rest.
  • The pipeline has exactly two LLM boundaries, both via the SDK’s structured output: planning (with one corrective retry on malformed output) and request aggregation on the success path; the processor owns the deterministic summary backbone (.orbit/requests/<id>/summary.yaml).

Consequences

  • One writer, well-defined settled moments — the invariant that later made the in-memory solution model and its reload hooks safe (ADR-007), and that pairs with the single-replica cap in ADR-004.
  • Throughput is deliberately bounded: requests queue behind each other.
  • Resume needs no separate code path — a task with a persisted sdkSessionId continues, one without starts fresh.
  • A historical note: this work gitignored .orbit/ so branches carry only solution deliverables. The three-repo split supersedes that — .orbit/ becomes tracked in the internal repo, and the gitignore/untrack mechanism is to be removed (ADR-006).

Evidence

  • implementation/maxq-orbit-agent/codebase/src/orchestrator/TaskProcessor.ts, TeamLead.ts, TaskList.ts — processor, planner, index locking
  • .orbit/base-branch and .orbit/requests/<id>/summary.yaml in a solution’s internal working state
  • memory/orbit-agent-task-processor.md (invariants and confirmed decisions), memory/repo-split.md (the superseded .orbit gitignore)