Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ S15/S16 target terms below refine product intent. Definitions explicitly marked

**Persona:** Owner-authored configuration of address, language and tone. It is trusted configuration, not model-authored memory.

**Bot proposal:** A revisioned draft for creating or changing a permanent Bot. Accepting it does not grant accounts or wider tools.
**Bot proposal:** A revisioned draft for creating or changing a permanent Bot. Accepting it does not grant accounts or wider tools. In S16 (ADR 0088) the owner accepts one exact proposal with one press, from the chat card the main Bot drafted or from the New Bot form; acceptance adds the Bot to the standing policy with the team defaults, and any change to the draft, the rules or the listed resources needs a new acceptance.

**Persistent Bot (S16):** A Bot the owner accepted from a proposal, with its own name, responsibility, route choice and conversations. One conversation talks to one Bot, fixed when the conversation starts. Archiving stops its unfinished work and refuses new messages while keeping its history and memory; restoring brings back the same identity. The main Bot cannot be archived.

**Conversation:** A durable discussion with one or more Bot participants. Membership, a display label and an @mention do not grant execution or resource access.

Expand All @@ -64,6 +66,8 @@ S15/S16 target terms below refine product intent. Definitions explicitly marked

**Delegation:** A bounded assignment to an enabled Bot under the originating authority and current restrictions. S15 may use team-shared approved resources without another sharing ceremony. It grants no new purpose, account, provider or permanent Bot. S14 retains its per-Bot capability/share intersection.

**Temporary worker (S16):** A short-lived helper a Bot starts through delegation. It works as the assigning Bot, with that Bot's route, only on the task text and inside the parent request's scope and charged slice; it has no memory and is never kept as a Bot. A *peer* assignment instead goes to another persistent Bot that accepts assignments (ADR 0088).

**Context handoff:** A labelled subset of accessible command, evidence and memory references supplied to a delegated run. It is not unrestricted transcript/session sharing.

**Job:** A bounded, authorized unit of background execution, separate from an owner's obligation.
Expand Down
45 changes: 45 additions & 0 deletions docs/adr/0088-s16-persistent-bots-and-delegation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
status: implemented for BOT16-01 (#412) and DELEG16-01 (#413); deterministic and real-bundle browser evidence; live account evidence separate (#431)
date: 2026-11-07
scope: BOT16-01 (#412), DELEG16-01 (#413); S16-AC01/08/20; FR-01/02/03/04/05/10/14/16/18/20/21/22/24/27/29/31/35; NFR-02/05/06/07/08/11/12
---

# Persistent Bots from exact proposals, and bounded Bot-to-Bot delegation

## Context

S16-AC01 asks for accepted persistent Bots that the owner can configure, talk to across restarts, archive, restore, duplicate and export without copying credentials or silently widening resources. S16-AC08 asks a Bot to hand bounded assignments to an enabled peer or a temporary worker, with results returning to the originating responsibility, failure or cancellation of one worker preserving sibling evidence, a finite shared budget, and no new permanent Bot.

The S15/S16 runtime had exactly one Bot: `conversation.bot` from the installation config. The standing policy names Bots (`StandingPolicy.bots`, each rule's `bots`), so any new Bot needs a policy revision, and POLICY treated every newer revision as `stale_authority` for all running work. There was no protected place for Bot profiles, no conversation-to-Bot link and no way for a running work to start another bounded work.

Owner decisions on 2026-11-07, before this work: one PR for #412 and #413; a Bot is created from the chat (the main Bot drafts a proposal card) or from a New Bot form, both ending in a one-press acceptance of the exact proposal, and a changed proposal needs a new acceptance; a new Bot gets the team defaults (shared computer, team resources, the main Bot's rules), listed on the card and removable before accepting; Bots delegate on their own within a cap and the parent's budget, peers accept assignments by default, a temporary worker uses the parent's model and a narrowed context and is not kept; one conversation talks to one Bot, chosen when it starts; archiving stops running work at once, fences new messages, pauses routines and keeps unknown effects for reconciliation, and restore needs routines to be turned on again; per-Bot model/effort is picked only from approved routes (switching routes mid-work and real usage reporting stay with #421); the main Bot cannot be archived; importing an export makes a new proposal that lists what is missing.

## Decision

1. **Explicit opt-in.** `s15-runtime/v1` gains `bots: true` (composition `bots: {}`), set by the generated installer. It adds the additive store `bots.sqlite`. Without it every conversation still talks to `conversation.bot` and nothing below applies.
2. **Bot directory** (`s16-bots/v1`, `src/s16/bots/service.ts`). Protected records: profiles (name, responsibility, route choice, excluded team resources, accepts-assignments, state), exact proposals and immutable conversation-to-Bot bindings (triggers refuse update/delete). A created Bot owns its POLICY catalog identity (`bot_<24 hex>` from the accepting request, revision 1, generation 1); the directory answers the `bot` catalog for it and for a configured Bot it archived. The main Bot and configured Bots keep their config catalog identity and get a profile row only when edited.
3. **Exact proposals.** A proposal (`s16-bot-proposal/v1`) binds the draft, the standing-policy revision and the listed team resources in `proposalHash`. The New Bot form, an edited card, duplicate (`bot.duplicate`), import (`s16-bot-export/v1` → proposal with `missing`) and the main Bot's `propose_bot` tool all create proposals; none creates a Bot. `bot.accept` names the proposal and its hash; a changed draft, policy revision or resource list refuses `stale_proposal`.
4. **Team defaults through POLICY.** Acceptance installs one standing-policy revision that adds the Bot to `bots` and to every rule that names the main Bot, except separately approved personal GitHub rules and recurring payments. Nothing else changes. The default-policy proposal (POLICY15-02) names created Bots too (`extraBots`), so renewal keeps them and the rules page does not show a change. An excluded resource is refused by the composed catalog `allows` for that Bot, so POLICY refuses admission and dispatch, and it is not offered to interpretation.
5. **Membership-only revisions keep running work.** POLICY's work guard accepts a work bound to an older revision when every revision from it to the current one differs from its predecessor only in other Bots' references: each consecutive pair, compared with every other Bot removed from the top-level and rule Bot lists, must be equal, and an unchanged reinstall anywhere in between is still a fresh start (so a work once made stale never becomes current again). Creating a Bot therefore does not stop another Bot's work; a change to a work's own Bot memberships or to any other field still does.
6. **One conversation, one Bot.** `conversation.bind` binds a not-yet-started conversation to a Bot; a started conversation keeps its Bot, and an unbound one is the main Bot's. The conversation policy resolves the Bot per conversation for interpretation and admission (`bot`, `selection` and `selectionPin` are the Bot's route, falling back to the installation default when the chosen route is no longer approved; the interpretation send uses the same route), and the Bot's owner-confirmed name and responsibility reach the model as owner decisions. Memory scope, computers and TEAM resources already key on the work's Bot.
7. **Archive and restore.** Archive (`bot.archive`, not the main Bot) first marks the Bot's catalog identity inactive, so POLICY refuses every work of it at the next guard (`authority_revoked`), then stops the unfinished requests of all its conversations and assignments (which revokes their works) and pauses its active routines. The record keeps the archive's request ID until this cleanup finishes; a retry of the same command or a restore finishes it first. Every other command commits its directory change and its receipt in one transaction, so a retry returns the first result. Settled and reconcile-only requests are kept as they are. The conversation ingress refuses `bot_archived` for every new message, routine occurrence or assignment. Restore (`bot.restore`) reactivates the same identity without a policy change, so the Bot's history and sourced memory are readable again; nothing revoked resumes, and paused routines stay paused.
8. **Delegation** (`s16-delegation-run/v1`). A non-delegated, non-file work gets the host tool `delegate` (up to 3 assignments per call): `to` is `temporary` or an active peer that accepts assignments. In one conversation-store transaction `submitDelegation` checks the parent is running in its work phase, at most 6 children per request and 3 unfinished at once, charges the child's slice to the parent's own counters (model calls `max(2, ⌊limit/3⌋)`, tool calls `max(1, ⌊limit/3⌋)`, never refunded, keeping one model call for the parent) and records a queued child request in its own conversation (`dc_…`, bound to the assignee) with an immutable pin of the parent's frozen purpose, resources and access. A child has no interpretation call, no memory retrieval or retention and no `delegate` tool; it is admitted under current policy for its Bot (a temporary worker is the parent's Bot with the parent work's route, kept in the pin) with limits capped by its slice, and `named` recipients never carry over, so a send the child starts itself still waits for the owner. The tool waits for every child to settle and returns their states and published results; a held child keeps its own Needs-you item. Children run in their own drain pool (at most 3) bounded by the default computer's sessions minus running ordinary requests (waiting parents included); when none is free a new assignment is refused `capacity_busy` and the parent does the work itself, so a waiting parent cannot strand its children. Stopping a parent stops its unfinished children; stopping one child leaves its siblings. Child conversations are not listed as chats; the parent row lists its children and each child row names its parent.
9. **Narrowed context and privacy ancestry.** The task text is captured as a TEAM source whose parents are the TEAM sources of every claim in the parent's context packet and whose file pins are that packet's. If any of those claims is Bot-private, a peer assignment refuses `private_context` before anything starts; a temporary worker (the same Bot) may take it. DATA15 derives the task from everything the parent work read. Before every model send of a child, the TEAM handoff from the still-current parent work to the child must admit the task's full ancestry for the child's Bot (a stopped parent, withdrawn source or file refuses `lineage_withdrawn`), and DATA15 merges the task's sources into the child, so the send is checked for them. A child's result reaches the parent only through its own publication guard, after DATA15 merges the child's read ancestry into the parent.
10. **Restart.** Running requests (parent and children) become reconcile-only as before; a queued child becomes `blocked/context_lost`. Child results, slices and the child list are stored once, so nothing attaches or charges twice and a settled parent starts no further child.
11. **Owner surface.** `GET /api/s16/client/bots` (`s16-client-bots/v1`), `GET /bots/:id` (`s16-client-bot/v1`, with conversations and the archive impact), `GET /bots/:id/export`, `POST /bots/proposals`, `POST /bots/imports`, `POST /bots/:id/duplicate` and `POST /bots/commands` (`s16-bot-command/v1`: `bot.accept`, `bot.dismiss`, `bot.update`, `bot.archive`, `bot.restore`, `conversation.bind`), idempotent per `requestId` with CSRF rechecked after the body. Capabilities gain `bots`; bootstrap gains the roster and each conversation's `botId`; snapshots gain `botId`. The React client adds the Bots pages (`/s16/bots`, `/s16/bots/new`, `/s16/bots/:id`), the Bot roster in the rail, a "Talk to" choice for a new chat, the chat proposal card, child cards and the assignment banner, in English and Thai.

## Consequences

- Several Bots share one standing policy and the team's resources by default. A Bot's narrower scope is an exclusion list; giving one Bot a resource the others lack still needs a separately reviewed rule.
- The persona text is owner-confirmed data the model sees; it never widens scope or decides an action.
- Delegated work costs part of the parent's own finite limits; three peers cannot be given three full budgets. On the default computer (2 sessions) children run one at a time while the parent waits, and a second conversation working at the same time makes new assignments refuse `capacity_busy`.
- File-bearing requests do not offer `delegate` yet (their tool port is the file workflow's); a temporary worker or peer cannot see the parent's attachments except through the parent's task text.
- During an archive, memory produced by the Bot is unavailable to every Bot (its producer is not current) and returns on restore.
- Live account evidence (real provider routes per Bot, a real peer on a second route) belongs to LIVE16-01 (#431).

## Alternatives considered

- *Bots outside POLICY (a Jit-side allow list)*: avoids policy revisions, but POLICY could no longer prove which Bot a work is for; rejected.
- *Restore under a new generation*: makes old authorities unusable, but also withdraws all memory and hides every earlier result of the Bot; archive already revokes the unfinished work, so the same identity is safe and keeps continuity.
- *Delegation inside the parent's own work authority (a sub-run)*: shares the budget automatically, but a peer runs as another Bot with its own route and persona, which needs its own authority.
- *Interpreting the task text like an owner message*: adds a model call that would see the parent's context and could reinterpret scope; the pin already fixes the narrowed scope.
Loading
Loading