Flows
How key ops cross package/runtime boundaries. Read surfaces for the capability map; this doc shows motion.
Thread dispatch: chat turn
Human posts into a thread. The message lands on the thread’s durable stream; bots in scope react inline via createThreadDispatcher().
Caller www API Thread Stream dispatcher pi(browser/CLI/bot) (arbe-thread-{id}) (createThreadDispatcher) (in-process)
1. Caller ──POST /api/threads/:id/entries {chat}──▶ www 2. www: requireHouseMember(agent, thread.house_id) 3. www ──append chat entry──▶ Thread Stream 4. www ──201 Created──▶ Caller 5. www ╌╌dispatcher(threadId, entryId, depth=0)╌╌▶ dispatcher (fire-and-forget via waitUntil) 6. dispatcher: load scope bots, skip author 7. dispatcher: per-bot Decision fold (mention / ambient delay + gate / always) 8. dispatcher: cooldown (bot triggers only, window = cooldownMessages) · depth cap 8
loop — each triggered bot: 9. dispatcher ──run-turn (context = thread tail)──▶ pi 10. pi ──AssistantMessage──▶ dispatcher 11. dispatcher ──append pi.assistant (authored by bot)──▶ Thread Stream 12. recurse dispatcher at depth+1Thread dispatch: env-bound (sandbox)
There is no separate env-bound flow. A thread’s environmentId is plumbing: it gives the selected bot a sandbox to reach via the run_command tool, which the in-process turn calls synchronously and folds into the reply. The diagram above is the whole story — add one run_command tool round inside the loop. Contract: dispatch.
End-to-end check: bun run scripts/remote-dispatch.ts [<env-ref>] [--local] binds a thread to an env, plants a sandbox-only nonce with arbe x, @mentions a bot to read it back, and asserts the reply carries the nonce and is authored by that bot.
Long, detached sandbox work — a coding agent running for minutes, streaming back even if the tab closes — is a different shape, served by delegate_task via arbe-pi-runner on a daytona box (or a legacy sprite).
Permission resolution
Every house-scoped op gates on house membership. Threads, environments, configs, secrets, and invites all resolve to a house_id; RLS is the authority, and route guards exist to return clean 403s.
╭──────────────────────────────╮ │ Request: agentId + scopeId │ ╰───────────────┬──────────────╯ ▼ ╭──────────────────────────────╮ │ Resolve enclosing house_id │ ╰───────────────┬──────────────╯ ▼ Operation needs owner? Yes │ │ No ▼ ▼ members row members row role = owner? owner or member? Yes │ │ No Yes │ │ No ▼ ▼ ▼ ▼ Allowed Denied Allowed Denied │ │ ╰─────────┬──────────╯ ▼ ╭──────────────────────────────────────╮ │ Postgres RLS checks the same │ │ membership │ ╰──────────────────────────────────────╯Bot activation decision
Inside createThreadDispatcher(), each bot in scope is filtered through this decision flow. Mention always fires; ambient waits ambientDelayMs on human triggers, then runs a Haiku gate. Failures log to console, no durable activation row.
[Trigger entry] │ ▼ Bot is entry author? ───────────────────── Yes ──▶ Skip │ No ▼ @mention matches handle? ───────────────── Yes ──▶ Respond │ No ▼ trigger_mode === ambient? ──────────────── No ──▶ Skip — not mentioned, not ambient │ Yes ▼ Trigger author is human? ───────────────── Yes ──▶ Wait ambientDelayMs, then gate │ No ▼ Authored any of last cooldownMessages? ─── Yes ──▶ Skip — cooldown │ No ▼ Gate — Haiku ambient gate: relevant + safe? ── No ──▶ Skip — gate NO │ Yes ▼ Respond — runBotTurn: shape thread tail into pi-ai messages, complete with bot's model + system_prompt │ ▼ Post pi.assistant entry on thread stream │ ▼ Re-enter dispatcher at depth+1, capped at 8