# Flows

How key ops cross package/runtime boundaries. Read [surfaces](../surfaces.md) 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()`.

```diagram
 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](./dispatch.md).

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](./sandbox-daytona.md) box (or a legacy [sprite](./sandbox-sprite.md#detached-work-arbe-pi-runner)).


## 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.

```diagram
        ╭──────────────────────────────╮
        │ 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.

```diagram
 [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
```
