Skip to content
View as .md

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+1

Thread 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