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
1 change: 1 addition & 0 deletions changelog.d/816-one-roster-read.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The tree no longer asks the host for the Bot list twice per refresh: the sidebar's structure entry and the archived-Bots list share one feed, so a mounted sidebar reads the roster once every 10 s instead of once per consumer, and the poll stops when the last of them goes away ([#816](https://github.com/ThinkFlowLab/FlyBot/issues/816)). The README now says the archived list is registered at `order: 88` and the reorganization notice at `order: 89`, and a slot renders ascending — so the archived Bots sit above the notice instead of being pushed down by it.
10 changes: 9 additions & 1 deletion docs/subsystems/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ The tree organizes itself by role instead of leaving Bots outside it or a parent
- **Leads.** A suitable existing lead is a member already marked as that group's lead, already coordinating that group (it has children and the key, project and domain only), or named for it ("Life", "FlyBot Lead") or as a lead ("Kitchen Manager", 负责人). It gets `lead { key, label }`. Only when none fits a new lead is created with a clear role (`<Label> Lead`, role "Lead of the <Label> group: coordinates …"); its members move under it before it joins the level. A lead's own group key, and broad areas below any lead, are ignored inside it, so there is no second "Dev Lead" under "Dev Lead".
- **Repair**: a parent over `maxChildren` (after a deletion re-attached a deleted Bot's children) is regrouped the same way, at once when the lead exists, else in the queue.
- **Daily tidy** (checked hourly, runs once a day; the first check after install only records the time; `dailyTidy: false` turns it off): (1) leaf Bots with no activity for `tidy.idleDays` (14) get `idle` (any turn or tool call clears it) and are **archived** after `tidy.archiveIdleDays` (30) more days; Bots with children, granted Bots and the root are never idle; (2) duplicates: two Bots with essentially the same role (same name once generic words are dropped and no conflicting description, or near-identical descriptions in one domain), both at least `tidy.mergeGraceDays` (1) in the tree, are **merged**: the newer one's children move to the kept Bot, which lists it in `mergedFrom` and is told in its prompt; the old one is archived; (3) project and domain groups of three or more (or two with an existing lead) on every level, never a group of a whole level without a lead.
- **Archive, never delete**: an archived Bot leaves the tree (`archived { at, reason, parent, treeRole, into? }`, tree fields and block removed) but keeps its chat, memory and grants. It is **hidden from the default Bot list** (`bot.hidden` / structure `include`; Search still finds it) (#452), and the sidebar's `flybot-tree-archived` section is the way back to its chat (#810): collapsed by default, header `Archived (N)`, drawn only while N > 0, one row per archived Bot with the same tag `statusOf()` gives (`archived`, `archived · merged into <name>`) opening the chat through `actions.openSession`. `include()` still returns false, so the list and `botTree()` never see it. Deleting it stays the owner's own click. On a merge (#450 item 1), the archived Bot's own memory is carried into the kept Bot through `flybot.memory` (at most 20 entries, tagged `source: merged`), so a merge loses nothing; without the host's memory seam the merge still stands.
- **Archive, never delete**: an archived Bot leaves the tree (`archived { at, reason, parent, treeRole, into? }`, tree fields and block removed) but keeps its chat, memory and grants. It is **hidden from the default Bot list** (`bot.hidden` / structure `include`; Search still finds it) (#452), and the sidebar's `flybot-tree-archived` section is the way back to its chat (#810): collapsed by default, header `Archived (N)`, drawn only while N > 0, one row per archived Bot with the same tag `statusOf()` gives (`archived`, `archived · merged into <name>`) opening the chat through `actions.openSession`. `include()` still returns false, so the list and `botTree()` never see it. Deleting it stays the owner's own click. That section is registered at `order: 88` and the reorganization notice below at `order: 89`; a slot renders ascending, so the archived Bots sit **above** the notice. On a merge (#450 item 1), the archived Bot's own memory is carried into the kept Bot through `flybot.memory` (at most 20 entries, tagged `source: merged`), so a merge loses nothing; without the host's memory seam the merge still stands.
- **Move, never rename-to-归档 + recreate** (#452): grouping always reparents (`parent`); it never renames a Bot to `归档-…` / `Archive-…` and never creates a replacement with the old name. A one-shot repair strips those prefixes when the bare name is free, or archives+hides the prefixed Bot when a live Bot already has that name.
- **Journal and undo**: every move, new lead, lead mark, merge, archive and idle mark of a run is an action in the root's `reorg { runs (last 5), dismissed, dailyAt }` and a `flybot-tree: reorganized (<trigger>): …` log line. The tree view's notice (sidebar section `flybot-tree-reorg`, `reorgNotice: false` hides it) shows the last run with **Undo** and **Dismiss** (`tree/reorg`, `tree/undo`, `tree/dismiss`). Undo reverses the newest run: moves go back where the old parent has room and the Bot was not moved again, archived Bots come back, a lead the run created is archived once empty; what it cannot restore is listed as kept. Restored Bots are pinned for `tidy.pinDays` (14) so the daily tidy leaves them alone.
- **The owner is told afterwards**, no plan card first: the notice in the tree view, and the Main Bot's prompt names the last run (within 7 days, not undone) so Chief can say what moved.
Expand Down Expand Up @@ -112,6 +112,14 @@ The rules above place a Bot by the words in its name, role label and description
- The Bot list itself uses the core `.bt-tree-toggle` (see [core-client](core-client.md)). When `sidebarSection: true` brings this plugin's separate Tree section back, `.flybot-tree-toggle` matches that control: 28×28 hit target, 16px inner `.flybot-tree-caret`, hover, focus ring, and rotation of the glyph only. Leaf spacers (`.flybot-tree-dot`) keep the same 28px width. `aria-expanded` and Expand/Collapse `{name}` labels stay on the button.
- The right-click menu entry from `menuItems({ bot })` is a single **Copy invite link** (bare URL, shown when `social.canInvite(bot.id)`). The invite with instructions is offered only in the Bot's Sharing section, so the menu does not list the same invite twice (#173).

### One roster read for the sidebar (#816)

Both sidebar entries — the `flybot.sidebar.structure` entry ds-bot draws its Bot list from and the `flybot-tree-archived` list — need the same `ctx.flybot.bots()`. Each used to poll it on its own 10 s timer, so a mounted sidebar asked the host for the list twice per tick.

- `client.js` keeps one `rosterFeed(flybot, { interval })`: the first subscriber starts the poll, a later subscriber joins it and is handed the current list without a read of its own, a read already in flight sets `again` instead of stacking up behind the timer, and the last subscriber to leave clears it. `apply()` builds one feed and passes it to `registerStructure` and `makeArchivedSection`; called with no feed, each makes its own (tests and other callers keep working).
- A field change that matters to either consumer (`archived`, `parent`, `treeRole`, `retired`, `idle`) and `bot.removed` call `refresh()`, so a removal is still immediate instead of waiting for the next tick.
- Tests: `plugins/tree/test/tree.test.js`, `#816: one roster read per tick…` (one read per tick for two subscribers, the timer stops with the last one, and mounting the archived section inside `apply()` adds no read) and `#816: the archived list is ordered above the reorganization notice`.

### Discarded promise `.catch` (#213)

The plugin's discarded `.catch(() => {})` calls carry a one-line reason comment or `void`; `check-empty-catch` enforces it (see `docs/code-standards.md`).
Expand Down
2 changes: 1 addition & 1 deletion plugins/tree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Recursive bot tree plugin for FlyBot (built on ds-bot / DeepSeek Harness).
- **`create_child_bot` is serialized per parent:** parallel calls never exceed `maxChildren` or create duplicate-name siblings. Two calls that are **in flight at the same time** and belong to the same project or domain become one group instead of each landing in whatever broader group it fits: one lead for that key (reused when it exists, created once otherwise) with both new Bots under it (#810). The overlap is the definition: one turn of the event loop, no timer, and a call that starts after the previous one returned keeps the ordinary placement.
- Delegation is level by level: coordinators delegate to direct children whose scope fits and aggregate upward; a bot with no children does the work itself. A one-line trace like `Chief → Work → Dev` is added only to answers a tree bot sends to its parent tree bot; replies to the user or to a remote (friend's, Bot Social) bot never carry it, and the root (Chief) never adds one.
- Tools (visible to every tree bot, not only Main/root — #16 Option B): `list_children()`, `find_bots(query?)`, `create_child_bot(name, role_description, parent?)`, `create_team(team, members, parent?)` and — root only — `move_bot(bot, parent)` (#635), which moves an existing Bot with its subtree under another parent after checking for cycles, `maxChildren`, `maxDepth` and duplicate sibling names, then refreshes every prompt whose trace changed. Unlike `create_child_bot` it never creates a Bot; use it to reorganise a team, and note that deleting a Bot still re-attaches its children to its parent. A bot creates only in its own downstream: `parent` (id or name) must be the caller or one of its descendants, and limits are checked at that parent. **Forward allocation (#447, `src/allocate.js`):** `find_bots` (read-only) lists the bots in the caller's branch (Chief: the whole tree) with role, parent, depth and path, matches first; `create_child_bot` reuses a bot with the same role anywhere in the caller's branch instead of creating a duplicate (`{ reused, note }`), and without `parent` puts the new bot under a marked group lead in the branch that its role belongs to ("FlyBot Docs" under "FlyBot Lead"), else under the caller; `create_team` makes a project team as a lead plus children (`Chief → FlyBot Lead → {Dev, Test, …}`, `members` as `"Name: role"` strings), one slot under the parent, reusing an existing lead and same-role members (a flat one moves under the lead, journaled and undoable). Allocation uses the self-organization's primitives (role profiles, leads, moves, journal); a concurrent burst of two or more `create_child_bot` calls in one group is placed as one leader plus members (#810), and an explicit `parent` keeps the caller's own placement; a full level, orphans (a bot whose parent is gone is reattached by role on the next repair pass) and duplicates are left to the self-organization, which never asks first. There is no plan card. Limits: depth 5 (root = 1), 8 children per node; duplicate sibling names (case/space-insensitive) are rejected; names are 1–40 characters (ds-bot's roster limit). Every new child gets the same recursive system prompt template (`src/prompt.js`). A new child starts with no person-grant even if its parent is granted; sharing it still needs an independent Sharing → Copy invite link (default deny).
- Frontend (`client.js`, bundle id `@flybot/tree`, injects `flybot`): the tree is drawn by ds-bot's own Bot list, so this plugin's separate "Tree" sidebar section is **off by default** (`sidebarSection: true` in the client config brings it back). It always exposes the synchronous helper `ctx.flybot.tree.forest(bots)` (also `globalThis.__flybot__.tree.forest(bots)`), see **Tree data** below. It also registers `flybot.sidebar.structure` (`{ parentOf(botId) }`, parents from `forestOf` over `ctx.flybot.bots()`, refreshed on field changes and every 10 s); ds-bot's sidebar uses it to draw its Bot list as one expandable tree with Chief on top. Retired, archived and outside Bots and other Main Bots have no parent there, so ds-bot shows them directly under Chief; the entry's `nodeActions` draws a tag after such a row ("archived", "archived · merged into …", "outside the tree") and after an idle Bot ("idle"), so the sidebar and the tree model agree. A `flybot.sidebar.section` entry `flybot-tree-archived` lists the archived Bots the Bot list hides (#810): collapsed by default, header `Archived (N)`, nothing at all while N is 0, one row per Bot carrying the same tag (`archived`, or `archived · merged into …` after a merge) that opens that Bot's chat through `actions.openSession`; expanding is local state, so the 10 s refresh re-renders the rows without closing it (`archivedSection: false` hides it). A `flybot.sidebar.section` entry `flybot-tree-reorg` shows the last reorganization (what moved) with Undo and Dismiss, through the host endpoints `tree/reorg`, `tree/undo`, `tree/dismiss`; it draws nothing when there is none in the last 7 days (`reorgNotice: false` in the client config hides it). The entry also has `menuItems({ bot })`, which ds-bot adds to each Bot's right-click menu: one item, `{ id: 'copy-invite', label: 'Copy invite link' }` (with `visible`, `run`); the invite with instructions is offered in the Bot's Sharing section only, so the menu does not list the same invite twice (#173). `visible` is `social.canInvite(bot.id) === true` (false without the social plugin); `run` calls `social.createInvite(botId)` (or `flybot.call('social.createInvite', { botId })`). **Copy invite link** copies only the bare URL (`link`); `copyInvite(flybot, botId, 'text')` copies the full message `text` for other callers; a missing field is an error, never swapped for the other. The clipboard write starts inside the click via `ClipboardItem` with the text as a promise, so it keeps the click's permission while the invite is created; `writeText` then a textarea are the fallbacks. It returns "Invite link for <name> copied", which ds-bot shows as a toast; a failure throws and shows as the error toast. Right-clicking never opens the Bot's chat.
- Frontend (`client.js`, bundle id `@flybot/tree`, injects `flybot`): the tree is drawn by ds-bot's own Bot list, so this plugin's separate "Tree" sidebar section is **off by default** (`sidebarSection: true` in the client config brings it back). It always exposes the synchronous helper `ctx.flybot.tree.forest(bots)` (also `globalThis.__flybot__.tree.forest(bots)`), see **Tree data** below. It also registers `flybot.sidebar.structure` (`{ parentOf(botId) }`, parents from `forestOf` over `ctx.flybot.bots()`, refreshed on field changes and every 10 s); ds-bot's sidebar uses it to draw its Bot list as one expandable tree with Chief on top. Retired, archived and outside Bots and other Main Bots have no parent there, so ds-bot shows them directly under Chief; the entry's `nodeActions` draws a tag after such a row ("archived", "archived · merged into …", "outside the tree") and after an idle Bot ("idle"), so the sidebar and the tree model agree. A `flybot.sidebar.section` entry `flybot-tree-archived` lists the archived Bots the Bot list hides (#810): collapsed by default, header `Archived (N)`, nothing at all while N is 0, one row per Bot carrying the same tag (`archived`, or `archived · merged into …` after a merge) that opens that Bot's chat through `actions.openSession`; expanding is local state, so the 10 s refresh re-renders the rows without closing it (`archivedSection: false` hides it). It is registered with `order: 88` and the reorganization notice with `order: 89`: a slot renders in ascending order, so the archived list sits **above** the notice (a smaller number is higher) instead of the card pushing it down. The structure entry and the archived list read `ctx.flybot.bots()` through **one** shared feed (`rosterFeed`, `{ interval }`): the first subscriber starts the poll, later ones join it without a read of their own, a field change or a removal asks for an immediate read (`refresh()`), and a mounted sidebar asks the host once per 10 s tick — not once per consumer (#816). A `flybot.sidebar.section` entry `flybot-tree-reorg` shows the last reorganization (what moved) with Undo and Dismiss, through the host endpoints `tree/reorg`, `tree/undo`, `tree/dismiss`; it draws nothing when there is none in the last 7 days (`reorgNotice: false` in the client config hides it). The entry also has `menuItems({ bot })`, which ds-bot adds to each Bot's right-click menu: one item, `{ id: 'copy-invite', label: 'Copy invite link' }` (with `visible`, `run`); the invite with instructions is offered in the Bot's Sharing section only, so the menu does not list the same invite twice (#173). `visible` is `social.canInvite(bot.id) === true` (false without the social plugin); `run` calls `social.createInvite(botId)` (or `flybot.call('social.createInvite', { botId })`). **Copy invite link** copies only the bare URL (`link`); `copyInvite(flybot, botId, 'text')` copies the full message `text` for other callers; a missing field is an error, never swapped for the other. The clipboard write starts inside the click via `ClipboardItem` with the text as a promise, so it keeps the click's permission while the invite is created; `writeText` then a textarea are the fallbacks. It returns "Invite link for <name> copied", which ds-bot shows as a toast; a failure throws and shows as the error toast. Right-clicking never opens the Bot's chat.

### Config
```yaml
Expand Down
Loading
Loading