Skip to main content
  • Version 0.1.0
  • Purity pure, browser-safe (no node:*, no I/O; every side effect arrives through injected deps)
  • Purpose The single engine that turns structured delegation signals into durable board mutations, plus the small pure utilities it needs. Extracted so one engine drives every team with no fork.
  • Workspace deps @clawboo/board-core (TaskStatus), @clawboo/executor (types), @clawboo/governance (checkFanoutCap)
  • External deps none
  • Subpath exports . and ./contract
The engine is deps-injected: the board, delivery, narration, and cost all arrive as functions, so the same core binds server-side (apps/web/server/lib/teamChat) against real SQLite and, in tests, against a FakeBoard. Team orchestration runs server-side for every team; the browser is a thin REST/SSE client.
deliver is not runTaskOnRuntime. The engine owns the board lifecycle: spawn does create → claim → createExecutiondeliver, and completeForSession does updateStatus(done). A deliver that re-claims would 409 and a deliver that re-completes would double-complete against the engine. The server binding therefore reuses only adapter construction plus an event drain, piping events to orchestrator.onEvent.

Public API

The engine (boardOrchestration.ts)

Key constants, each a load-bearing invariant:

The board seam (boardClient.ts)

The BoardClient interface plus CreateTaskInput, BoardTask, ClaimResult, TaskDetail, ExecutionRef, ExecutionSummary, CompleteExecutionOutcome. listExecutions returns ExecutionSummary[] | null, and null means the ledger could not be READ, which a caller must treat as “unknown, do nothing this pass” rather than collapsing into []: an empty ledger is the strongest evidence a task is fireable, so a transient read failure would turn a user-Stopped delegation into an auto-fired one. ClaimReason is 'conflict' | 'not_found' | 'error'.
A 'conflict' claim means another worker won the race. It is data, never an error to retry: the atomic claim is the concurrency primitive, so a 409 is never retried.

Delivery (nudgeQueue.ts)

createNudgeQueue returns a non-destructive queue: a message to a busy session is queued and flushed at its turn boundary, never interrupting an in-flight run. deliver marks a session busy synchronously, which closes the double-send race.

Parsing (delegationTags.ts)

findDelegationBlocks, parseStructuredDelegations, stripDelegationBlocks, detectDelegationIntent, findPlanBlocks, stripPlanBlocks, plus DelegationIntent / DelegationBlock / PlanStep / PlanBlock. The matchers are deliberately drift-tolerant: the reliable anchor is the closing </delegate> plus the to="…"> attribute shape, so a weaker model that drops the opening < is still parsed. Stripping removes the whole tag, so no fragment leaks into rendered prose.

Reflection (taskUpdate.ts)

buildTaskUpdateMessage(items) renders the batched leader stimulus. TaskUpdateOutcome is 'done' | 'error' | 'aborted' | 'timeout' | 'max_turns'; a non-done outcome renders as a “did not complete” entry, so the leader is told to decide rather than wait.

Turn origin (turnOrigin.ts)

TurnOrigin is { kind: 'human' } | { kind: 'delegation'; fromAgentId } | { kind: 'system' }, stamped by whoever asks for a turn and passed as deliver’s fourth argument. It is required rather than inferred: the host used to work out whether a turn was a delegated worker’s or the user-facing leader’s by asking the engine whether the session still held a task, and that answer goes stale the moment completeForSession forgets the session. classifyTurn({ origin, targetAgentId, leaderAgentId, hasBoardTask }) returns a TurnFraming of three independent booleans: isWorker (executing a delegated task), isLeader (the team’s reduce point, a property of who the agent is), and isUserFacing (this turn’s reply reaches the human, so it may carry the user’s self-intro). HUMAN_TURN and SYSTEM_TURN are singletons for the two payload-free variants. schedule is deliberately not a variant: routines dispatch through their own path and never reach deliver.

Turn envelope (turnEnvelope.ts)

buildTurnEnvelope({ ambient, addressed }) frames a run’s waiting context as two channels with different authority: addressed items are routed to this agent and need a response; ambient items are evidence about the state of the work and carry no authority to change the task, the policies, or the Team Rules. Addressed renders first so a long ambient block cannot bury the ask, and an empty section is omitted entirely so a quiet turn adds zero tokens (returns null). Section membership is clawboo’s decision, taken from provenance (the mailbox row’s kind, and which reader produced the item) and never from the text, so a peer’s message can never promote itself out of the ambient half. The builder frames items without touching them, which is what keeps formatPeerPost’s safety-critical isUser=false token intact.

Session keys (sessionUtils.ts)

buildTeamSessionKey(agentId, teamId), agentIdFromSessionKey, isTeamSessionKey.

Control tokens (controlTokens.ts)

shouldDropAssistantTurn plus isOpenclawControlToken, isClawbooControlToken, isLikelyRefusal, RESUME_ACK_TOKEN (__resumed__), SKIP_ACK_TOKEN (__skipped__), MIN_SUBSTANTIVE_LENGTH. This is the cascade-safety filter that keeps control tokens and short refusals out of the transcript.

The contract suite (./contract)

runCascadeContract(harness) exports the 59 cascade scenarios (stop-clean-release, the idle watchdog and its open-tool-call allowance, sessionToTask 1:1 serialize-don’t-orphan, reflect batching, the fan-out cap, plan-dep cancel-on-fail, loop breakers, dedupe, claim-409-never-retried, detach-on-release, markStopped, and the pump’s ledger fire policy). CascadeBoard keeps scenarios board-agnostic. It runs against both the in-package FakeBoard (proving the cascade logic) and the real serverBoardClient over SQLite (proving the invariants hold against the real state machine), so a FakeBoard-vs-real divergence is caught.
vitest is external in the build, so the app-safe barrel never pulls the test runner; only the ./contract subpath does. Same pattern as @clawboo/executor/contract.

Used by

  • apps/web (server); lib/teamChat/teamOrchestrator.ts builds one long-lived orchestrator per active team over createBoardOrchestrator, with serverBoardClient and serverDeliver as deps.
  • apps/web (server); lib/teamChat/serverDeliver.ts and persistTeamChatEntry.ts consume sessionUtils and controlTokens.
  • apps/web (SPA); lib/teamProtocol.ts re-exports the control-token helpers so browser consumers are unchanged.

Source

Barrel: packages/team-orchestration/src/index.ts. Contract: src/contract.ts.

See also

Last modified on August 21, 2026