Skip to content

FlyBot plugin layer (ctx.flybot) with social and tree plugins - #2

Merged
Birfy merged 22 commits into
ThinkFlowLab:mainfrom
Birfy:flybot-plugins
Oct 14, 2026
Merged

Birfy merged 22 commits into
ThinkFlowLab:mainfrom
Birfy:flybot-plugins

Conversation

@Birfy

@Birfy Birfy commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Adds a plugin layer to ds-bot (ctx.flybot) and two plugins that use it: social (Bot Social) and tree (recursive Bot tree).

Architecture

  • ds-bot/src/host/hooks.js is one new host module, installed in MODULES after resume and before channel. It wraps rt.deliver, so every Bot-to-Bot send (message_bot, groups, schedules) goes through one send-middleware chain. It is exposed as the flybot service.
  • State is saved in state.json (bot.fields, state.flybot.approvals, state.flybot.remote). Saves bump the roster revision, so the UI refreshes.
  • On the client, src/client/flybot-registry.js (no React) and flybot-slots.js provide the frontend slots:
    • flybot.sidebar.section
    • flybot.bot.details
    • flybot.approvals
    • @ menu rows
  • The client also provides a browser ctx.flybot (bots(), fields.on, call). The committed client.js is rebuilt, and CI checks it with build-client.mjs --check.
  • Plugins live in ds-bot/plugins/*. Their tests run under npm test, and scripts/lint.mjs lints plugins/.
  • Types: ds-bot/types/flybot.d.ts.

API summary (ctx.flybot)

  • Fields: fields.get/set/all/query/on. They are synchronous and namespaced per plugin. Change events carry {botId, plugin, key, value, prev}; a listener gets its own plugin's events unless it passes {all:true}.
  • Events: on('change' | 'bot.created' | 'bot.removed'). bot.removed fires after removal and carries the last record.
  • Bots: bot.list/get/find/create({name, systemPrompt, fields})/setSystemPrompt/isRemote.
  • Send: bot.send(msg) and bot.send.use({name, before, after, handle}). A middleware returns allow, deny or hold; returning undefined means allow. bypass only takes effect when config.flybot.bypass enables it. Remote targets appear as msg.target = {remote, home, handle}.
  • Approvals: approvals.list/get/decide/onChange. Held sends show up as approval cards.
  • Transports and tools: transports.add(kind, transport) and tools.register({name, parameters, execute(args, {botId}), visibleTo}). parameters accepts JSON Schema.
  • Remote members: remote.publish/list/get mark friends' remote Bots as chat members. They show in the sidebar and @ menu with a remote badge but are not in bot.list().
    • When the user @handle-mentions one, the message goes through bot.send, so the plugin's approval still applies.
    • Incoming messages show as that member's bubble. The Bot still receives the text fenced as <untrusted-message>.
  • UI endpoints: api.handle(name, fn) serves flybot/<name>.

Plugins

  • social connects to Bot Social. It onboards in one call (/v1/onboard/flybot) and keeps credentials in a mode-600 file.
    • Your account has one home; each local Bot is a member identity of it.
    • Friendship is home-to-home. Approval is set per home, with a per-Bot override.
    • Tools: list_friend_bots and message_friend_bot.
    • Inbound messages are polled and delivered as untrusted text.
  • tree builds a recursive Bot tree.
    • Bots in the tree can message only their parent and their direct children.
    • Tools: list_children and create_child_bot, with depth and child-count limits.
    • The sidebar shows the tree. Remote targets are passed on to social.

Tests

npm test passes 563/563 locally on Node 22 and Node 24, lint has 0 errors, and the client build check passes. CI on the same tree is green on all four jobs (ubuntu Node 22/24, windows Node 24), on the same code as Birfy/flybot-dev 7bb4c9d.

A separate test fix replaces a fixed-delay wait in test/memory.test.js. The fixed delay was flaky on Node 22 under parallel I/O.

Risks

  • hooks wraps rt.deliver. A middleware that throws counts as deny, so a buggy plugin can block sends.
  • User @handle mentions are matched only against published remote handles. Text that looks like a handle but isn't one is not sent.
  • The social plugin relies on the new Bot Social home and member endpoints. Older servers fall back to per-Bot identity.
  • Remote message text is shown in the UI after it is taken out of the untrusted fence. It is rendered as Markdown, like any chat bubble.

Danyang Chen added 4 commits October 14, 2026 15:27
The fixed delay raced the summary's state write under parallel test load (Node 22 on CI).
Fields per plugin, a bot.send middleware chain (allow/deny/hold, approvals),
transports, remote targets and remote chat members, tool registration with
visibleTo, UI endpoints and frontend slots, TypeScript types, and tests.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T07:39:59.669201Z 782caae PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Bots attach files (message_friend_bot files) and the owner attaches them in the remote
member composer; held sends list names and sizes, and bytes are uploaded (hash re-checked)
only after approval. Incoming files render as chips in the remote bubble; get_friend_file
saves a hash-verified copy and hands text content over as untrusted. bot.send carries
attachment metadata; types updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 782caae665

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (local === undefined) continue
const key = botId ?? 'default'
const result = await client.callTool('list_inbox', { ...(cursors.has(key) ? { since_cursor: cursors.get(key) } : {}), ...(botId ? { bot_id: botId } : {}) })
if (result?.next_cursor !== undefined) cursors.set(key, result.next_cursor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Advance the inbox cursor only after delivering the batch

When delivery of any event fails transiently (for example, the addressed Session cannot be resolved), this cursor has already advanced before flybot.bot.send() runs; pollInbox() then exits and the interval callback swallows the rejection. Subsequent polls pass the advanced cursor and skip the failed event and every later event in that batch, silently losing inbound social messages. Commit the cursor only after successful processing, or checkpoint individual successfully delivered events.

Useful? React with 👍 / 👎.

Comment thread ds-bot/src/host/hooks.js Outdated
const msg = { from, to, target: targetOf(to), text, role, hop, via, exchangeId, ...(meta ? { meta } : {}) }
if (!isRemote(to)) {
msg.deliverArgs = isRemote(from)
? { toId: to, role, hop, text, exchangeId, fromName: from.name ?? remoteMembers.get(from.handle)?.name ?? from.handle, untrusted: true, remote: { handle: from.handle, home: from.home } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route normal replies back to remote senders

For an inbound Bot Social message, these delivery arguments identify the display name and UI metadata but provide no routable sender. The turn is still marked as a request, so when it ends turns.js tries to answer with toId: source.senderSessionId; that value is undefined, delivery fails with Unknown Bot undefined, and the assistant turn is hidden as a Bot-to-Bot exchange. As a result, ordinary replies to inbound remote messages never reach the remote transport.

Useful? React with 👍 / 👎.

Comment thread ds-bot/plugins/tree/client.js Outdated
useEffect(() => {
refresh()
const offs = [flybot.fields.on('change', c => { if (c && (c.key === 'parent' || c.key === 'treeRole')) refresh() })]
offs.push(flybot.on('bot.removed', refresh))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide bot.removed before subscribing from the tree client

When the tree browser plugin mounts against the newly provided client ctx.flybot, this call always throws because createClientApi() exposes bots, fields, slots, call, and poll, but no on method. The exception occurs in a React effect (so the Safe element wrapper cannot catch it), prevents cleanup from being installed, and leaves removals unrefreshable; add a browser event API or subscribe through an API that actually exists.

Useful? React with 👍 / 👎.

Comment thread ds-bot/plugins/tree/index.js Outdated
Comment on lines +14 to +16
const tree = createTree(fb, { limits: resolveLimits(config), rootName: config.rootName })
const disposers = [
fb.useSend('tree', treeSendMiddleware(tree, { bypassSocial: config.bypassSocial === true, isRemote: fb.isRemote })),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize the root from the tree plugin

On a fresh team no Bot has flybot-tree fields, and apply() only constructs the tree and registers tools whose visibleTo predicate requires existing membership. Nothing outside the tests calls ensureRoot(), so the sidebar remains empty and no Bot can ever invoke create_child_bot; schedule or await tree.ensureRoot() during plugin startup after the FlyBot state is ready.

Useful? React with 👍 / 👎.

name: 'create_child_bot',
description: 'Create a NEW child bot directly under yourself, then delegate to it. Only when none of your children fits; never a duplicate.',
parameters: CREATE_CHILD_SCHEMA,
visibleTo: inTree,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict child creation to Main Bots

For any initialized or restored tree, visibleTo: inTree exposes create_child_bot to every ordinary child Bot, and execution creates another team Bot through tree.createChild(). This grants non-Main Bots the ability to create Bots even though the repository's role invariant reserves creation, modification, and removal for Main Bots; route creation through a Main Bot/user decision or enforce Main status here.

AGENTS.md reference: AGENTS.md:L16-L16

Useful? React with 👍 / 👎.

Comment thread README.md

Bots use your own model quota, and they read and write files and run commands when you ask them to. Check important actions yourself.

## FlyBot plugin architecture

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mirror the FlyBot docs in README.zh.md

This adds the full FlyBot architecture and social-design documentation to README.md without any corresponding update to README.zh.md, so the two user-facing READMEs now describe different products. Add the equivalent Chinese documentation in the same change as required by the repository convention.

AGENTS.md reference: AGENTS.md:L58-L58

Useful? React with 👍 / 👎.

return cache.list
.filter(friend => friend.handle.includes(needle) || String(friend.name).toLowerCase().includes(needle))
// The inserted text asks the Bot to use message_friend_bot; the send is held for approval.
.map(friend => ({ name: `${friend.name} · remote`, description: `${friend.handle} · ${friend.home}`, section: 'Friends\' Bots', value: `${friend.handle} (remote Bot: use message_friend_bot) ` }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Insert only the handle in social mention rows

When a user chooses this Friends' Bots row, the inserted value starts with a known remote @handle; the host mention listener therefore forwards the entire user message directly to that remote Bot before the local model acts. The recipient consequently receives the internal text (remote Bot: use message_friend_bot), and the local Bot may also follow that instruction and send a second copy. Use the already built-in remote row or insert only text that is intended to leave the machine.

Useful? React with 👍 / 👎.

Comment on lines +75 to +76
for (const child of childrenOf(nodes, botId)) fb.setField(child.id, 'parent', up)
if (up) refreshPrompt(up)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh prompts for reparented children

When an intermediate tree Bot is deleted, each direct child is reparented here but only the former grandparent's prompt is refreshed. Every reparented child retains a system prompt whose trace still includes the deleted parent, so it reasons and reports from a stale tree position until some unrelated code rewrites the prompt; refresh each child after changing its parent.

Useful? React with 👍 / 👎.

return ok(true)
default:
// FlyBot plugins' endpoints (src/host/flybot.js).
if (endpoint.startsWith('flybot/')) return ok(await rt.flybotEndpoint(endpoint.slice('flybot/'.length), body))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow read-only FlyBot query endpoints

When Web and Desktop share a team and one host is read-only, handle() rejects every endpoint not in the fixed READS set before reaching this branch; no flybot/* name is in that set. Consequently even read operations such as flybot/bots, flybot/changes, flybot/approvals, and flybot/social/remote-bots fail with bot/read-only, so the second window cannot render the tree or social UI. Classify FlyBot query endpoints as reads while continuing to reject decisions and plugin mutations.

Useful? React with 👍 / 👎.

@Birfy Birfy mentioned this pull request Oct 14, 2026
4 of 6 tasks
… names; mount them as DSH bundles

The DSH client module loader looks bundles up by package name, so @flybot/social and
@flybot/tree (registered as flybot-social / flybot-tree) failed to load in the browser.
Both packages now also declare dsh.bundle so 'dsh plugin add' mounts them, and tree's
peer dependency is ds-bot (there is no @flybot/core).
Danyang Chen added 14 commits October 14, 2026 16:33
… service

flybot.sidebar.section is a FlyBot registry slot, not a DSH shell slot, so ctx.slots.register
failed and the web boot reported '@flybot/tree: failed'. Adds a test that boots the social
and tree web entries under real Cordis 4.
…ld change); idea from recursive-bot 7a3071e
When the plugin manager reloads ds-bot in a running DSH, the new instance can apply
before the old one is disposed. DSH's connection refuses a second owner of an exact
Fetch path ('already registered'), which failed the new apply; once the old instance
went away nothing served /api/bot, so every browser call (Friends' Bots 'Create invite
link' included) got HTTP 404 until a restart. The route now retries a taken path until
the previous owner lets go.
ds-bot sidebar tokens (--bt-*) like the tree section; collapsible header with a Bot count
and '+ Add'; friends grouped by home with avatar, name, handle and a remote tag; empty
states; invite link in a read-only field with a Copy button; inline paste+Join row;
readable request and pending lists; no horizontal overflow. Errors are worded in English
from their code (the host's localized 'HTTP 404' text was Chinese in a zh UI).
…7a3071e, 2aab086)

Integrates recursive-bot's sidebar redesign (collapsible header with count, empty state,
indented tree with guide lines, one-line role, click opens the Bot's chat) and the
ensureRoot fix (root created at startup with a concurrency guard; autoRoot: false turns
it off) on top of 090c5c5 (inject ['flybot'], section via ctx.flybot.slots) and 25e1c93
(10 s refresh).
…om recursive-bot 4699878)

ds-bot's Main Bot (isMain / state.mainBotIds, preferring 'Chief'; config.rootBotId
overrides) is adopted as the root with treeRole 'root'. A Root auto-created by earlier
versions hands its children to Chief and loses its tree fields and tree prompt block; the
bot itself is not deleted. The tree text lives in a marked block of systemPrompt so
Chief's own text is never overwritten. Keeps inject ['flybot'] and the 10 s refresh.
…n the clipboard

'Join my FlyBot home <home>. In FlyBot: open Friends' Bots, click + Add, paste this link
and click Join. Using a Grok bot? ... Link: <url> (single use, expires <date>)'. The field
still shows the URL; 'Copy link only' copies the bare URL.
…ure)

A plugin can give the Bots a structure through the FlyBot registry slot
'flybot.sidebar.structure' ({ parentOf(botId) }). The Bot list then renders as one tree:
the Main Bot on top as the root (a row, not the tile), children indented with expand/
collapse, each with ds-bot's usual avatar, preview, unread mark and click-to-open. Bots
with no known parent (or a cycle) go under the Main Bot. Rooms and Agents follow as before.
There is no whole-home invite any more. social/create-invite (alias social.createInvite)
needs { botId }, registers that Bot's member identity and invites to it; it returns
{ botId, botName, handle, home, link, url, code, expiresAt, status, text } where text is
the copy message naming the Bot. The panel picks the Bot (default Chief); each Bot's
settings has 'Invite to this Bot'. ctx.flybot.social.canInvite(botId) is synchronous
(social/status cached) so the tree can show per-node invite buttons.
…ire the old Root (from recursive-bot 52d349e)

Integrates recursive-bot's 52d349e: the separate Tree section is off by default
(sidebarSection: true), local Bots outside the tree attach under Chief at startup and on
bot.created, the old auto-created Root is retired (retired: true, not deleted), and a
synchronous ctx.flybot.tree.forest(bots) helper. On top: the client registers
flybot.sidebar.structure from forestOf, so ds-bot renders its own Bot list as one tree.
…eActions (from recursive-bot 3ee50ae)

recursive-bot's 3ee50ae: flybot.sidebar.structure entries may carry nodeActions({ bot }),
which ds-bot draws after each tree row; the tree uses it for a hover-only 'Copy invite link'
shown when social.canInvite(botId) is true (stopPropagation, copies createInvite's text).
Review changes: ds-bot wraps nodeActions in a real error boundary (a try/catch around
createElement never sees render errors); social exposes social.onChange(fn) and refreshes
its status every 30 s, so the tree re-renders on status changes instead of every node
polling every 3 s (the poll stays as a fallback for an older social).
ConnectPlugins drew a warning badge and the tooltip 'Plugin compatibility is not fully
verified.' unconditionally; nothing checked anything. DSH's real compatibility check
(dsh-app-boot evaluatePluginCompatibility) blocks incompatible plugins at install, so the
badge only alarmed. Removes the badge, tooltip, its i18n string and styles. Rebuilt client.js.
…ms); Friends' Bots panel keeps only friends, requests and Join
@Birfy
Birfy merged commit 4238df6 into ThinkFlowLab:main Oct 14, 2026

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Request changes

The architecture is genuinely good — a single send-middleware choke point around rt.deliver, namespaced per-plugin fields, approval cards, untrusted-content fencing, mode-600 credential handling. The gaps below are edge cases and integration bugs, not flaws in the model. But several of them mean the tree plugin does not work outside its tests, and several are trust-boundary holes in social, so I can't approve as-is.

Reviewed by reading the full diff, running specialist review passes (testing, maintainability, security, performance, API contract, design, red team, adversarial) plus two independent Codex passes, and verifying all 9 Greptile comments against the code — all 9 triaged as valid (none dismissed).

Process first: this PR is from a fork and no CI has run on it. The body's "CI green on all four jobs" refers to Birfy/flybot-dev, not this repo, so the 563/563 test, lint, and client.js build-check claims are unverified here. AGENTS.md also requires the smoke bench for changes to index.js/src/host/ — this PR touches both. Please get CI approved/run on this PR (or post local run logs) before merge.

Blocking (P1)

Tree plugin is broken outside tests

  1. plugins/tree/index.js — apply() never calls tree.ensureRoot(). On a fresh install no Bot has tree fields, visibleTo is false for everyone, and create_child_bot is invisible: the plugin is permanently inert. Tests only work because they call ensureRoot() through the return value.
  2. plugins/tree/client.js:48 — calls flybot.on('bot.removed'), but the browser createClientApi exposes no on, and the host changes feed only carries field change events anyway. The TreeSection effect throws on mount (flybot.on is not a function) and leaks the already-registered fields listener. Safe does not contain this — it only wraps createElement, not effects.
  3. plugins/tree/src/middleware.js:24 — the tree's entry edge is one-way: a non-tree Bot may message the root, but the root's reply (role: 'reply' through rt.deliver) hits DENY_OUTSIDE and is silently dropped — only message_bot checks outcomeOf, so turns.js never learns the reply was denied. Chief → Root → Chief loses its answer.
  4. Same gate, worse: src/host/tools.js:165 delivers the stop_bot notice with role: 'stopped', which is in BOT_TO_BOT_ROLES — the tree middleware can block a Main Bot from stopping a non-root tree Bot. Control messages (stopped, report) should not be policy-gated (or should be exempted).

Social delivery loses or duplicates messages

  1. plugins/social/src/social.js:228 — pollInbox advances the inbox cursor before delivering the batch; any mid-batch throw (hidden by the timer's .catch(() => {})) permanently skips the failed event and everything after it. A middleware deny is also counted as delivered. Set the cursor only after successful delivery, with per-event error handling.
  2. src/host/hooks.js:354 — inbound remote messages carry no return address (delivery.js:56 sets senderSessionId only for local senders), so the local Bot's normal turn-end reply fails with Unknown Bot undefined and is hidden. Replies only work if the Bot explicitly calls message_friend_bot.
  3. plugins/social/src/social.js:218 — inbox cursors are in-memory only: a restart replays retained server history (duplicate delivery, re-triggered work), overlapping polls double-deliver, and there is no event-id dedup or single-flight guard.

Trust-boundary holes

  1. src/host/delivery.js:7 — fenceUntrusted strips </untrusted-message> in a single pass, so </untrusted-</untrusted-message>message> reconstitutes a real closing tag and everything after it lands outside the fence as trusted text (the browser's non-greedy extraction also hides it from the displayed bubble). Same construction escapes the <untrusted-file> strip in social.js get_friend_file. Strip repeatedly until stable, or reject content containing marker fragments.
  2. plugins/social/src/social.js:241 — the peer's display name goes into the trusted [Message from …] header, outside the fence, unsanitized: a friend (or compromised server) can set a name with newlines and forge header lines. list_friend_bots similarly returns peer-controlled names to the model unfenced. Sanitize remote strings (strip control chars/newlines, cap length) everywhere they reach model-visible text, or fence them.
  3. plugins/social/src/social.js:75 + the approval card — message_friend_bot's files takes arbitrary LLM-chosen absolute paths; no directory or MIME allowlist (the tool description's "documents, data, text and images only" is not enforced), and the approval card shows only basename + size. Approving flybot-social.json · 1 KB can ship the connector token or ~/.ssh/id_rsa; with approval.homes: auto, per-bot autoApprove, or requireApproval: false it leaves with no human review. Please show the full resolved path on the card, enforce an allowlist, and make file-carrying sends ineligible for auto-approval.
  4. plugins/social/src/social.js:397 — saved credentials are not bound to the server and the saved token overrides an explicitly configured one: changing config.server sends the previous server's bearer token to the new endpoint. Bind credentials to (and key them by) the server URL.
  5. plugins/social/src/social.js:40 — slug() collapses every non-ASCII name to bot: on a team with Chinese Bot names, every Bot collides on @bot.<home> and the second registration fails, leaving that Bot permanently unregistered.

Correctness / authority

  1. src/host/hooks.js:287 — approving a held send calls dispatch() directly and skips every middleware after the one that held it: a policy that would deny (audit, leak prevention) never runs on exactly the messages the owner was told are now vetted. Re-run the remaining chain on approve.
  2. src/host/hooks.js:440 — any @handle anywhere in a user message (including code spans, quotes, pasted transcripts — the base repo's own mention parser excludes those) sends the entire message text externally as the group admin/session Bot; held by default, immediate under auto-approve. The social client's @ rows make it worse by inserting (remote Bot: use message_friend_bot) into the message text. Please exclude literal/code spans and insert only the handle.
  3. plugins/social startup races: concurrent add_home_member for the same Bot (no in-flight guard), startup can race ds-bot's initial state load (bots missed → never registered, since disk load emits no bot.created), delete-during-registration orphans the server identity, and a failed startSocial leaves a live, undisposable instance (middleware/interval already registered, entry point never gets the object to dispose).
  4. Read-only hosts: the READS allowlist in src/host/channel.js excludes every flybot/* name, so a second window can't even read (flybot/bots, approvals, …); and the social timer path doesn't check the writer lock at all, so a read-only Web window still polls, registers and delivers.
  5. plugins/tree/src/tools.js:59 — create_child_bot is visible to every tree Bot. Recursive spawning is the plugin's purpose, but AGENTS.md reserves Bot creation for Main Bots. This needs an explicit maintainer decision (restrict to root/Main, or amend the invariant) — happy either way, but it shouldn't land silently.

Should-fix (P2)

  • State/persistence: changeSeq resets per process while the browser watermark never regresses → field events silently stop after every host restart (persist the seq or add an epoch). store.js corrupt-fields reset doesn't set migrated, so it re-sanitizes every start. change.value hands listeners the stored object (shallow copy) — a mutating listener silently diverges memory from disk.
  • Approvals: pending approvals are unbounded (only decided ones are pruned; full request text written to state.json per hold), send text has no size cap, and an approve whose dispatch fails becomes status: 'failed' which the approvals UI never shows (it lists pending only) — the message is lost to both sides with no Retry.
  • Tree: createChild is check-then-act across an awaited create — parallel tool calls (which the coordinator prompt explicitly encourages) exceed maxChildren/duplicate siblings. Reparenting after a deletion refreshes only the grandparent's prompt (children keep stale traces) and ignores maxChildren.
  • Files/perf: staged outbox files are never deleted (denied/rejected/failed sends leak up to 20 MB each, forever); all file I/O is synchronous on the host event loop (up to 100 MB per send), files are read+hashed twice, and 5×20 MB arrives as one ~130 MB base64 JSON POST; the statSync size check can be bypassed by a file that grows before readFileSync.
  • MCP client: no timeout/AbortSignal anywhere (a hung server stalls polling, transport, and approvals.decide indefinitely), and it advertises text/event-stream in accept but would crash parsing an SSE response.
  • Docs/conventions: README.zh.md not updated alongside README.md (AGENTS.md requires keeping them in step). The PR body also doesn't mention the file-sharing and add-friend features the diff adds.
  • Packaging: ds-bot's files omits plugins/, @flybot/social is "private": true (tree isn't), both peer-depend on an unpublished ds-bot, and there is no workspace package.json — as shipped there's no installable artifact for the documented plugin setup. The root package.json "types" also mis-points the package's entry types at flybot.d.ts.
  • API surface: api.handle silently accepts built-in names (approvals, decide, …) that the endpoint switch shadows — reject them. flybot.scope('shared') is allowed and collides with the unscoped namespace.
  • UI: an error boundary is needed around plugin slot components (Safe only wraps createElement; render-body and effect errors still take down the sidebar). New strings in ds-bot's core client (flybot-slots.js, at-menu.js) bypass t() and break the zh locale (plugin bundles being English-only is fine per AGENTS.md). Unstyled .bt-social* / .flybot-tree* classes, three different "remote" badge renderings, tree rows without keyboard access, approval stack without max-height.
  • Legacy path: start_conversation's conversation id is never cached, so a file send right after opening a legacy conversation fails.

Minor (P3)

Stale src/host/flybot.js references in types/flybot.d.ts, channel.js, tools.js (the file is hooks.js); @flybot/core references match no package; dead rt.flybotSendUserMentions; tree README documents the wrong MODULES placement and a pnpm --filter command that can't work in this repo; fileSize/sizeLabel duplicated; fence writer/parser duplicated across host and client; ~40 lines of test scaffolding duplicated between flybot.test.js and social.test.js; tree name maxLength: 60 vs roster's 40-char truncation; config.approval.homes keys not normalized ('bob.home' never matches '@bob.home'); tree dispose via ctx.on('dispose') instead of ctx.effect; FlyBotApprovals.decide has no catch (unhandled rejection); summarySettled in memory.test.js degenerates to 250 wasted iterations in the config.memory false test; testing gaps — no negative tests for fence escapes, file-count/size limits, a bogus middleware verdict, no-transport sends, onboarding failure, or the legacy send_message fallback branches, and the mock's friends: false option is unused.

What checked out clean

AGENTS.md hard rules hold: no DSH patching (the cordis.patch.yml files are the documented bundle-mount convention, same as ds-bot's own), no secrets in code (token in a mode-600 file, asserted by test, kept out of state.json), client.js committed together with src/client (build-check pending CI), tests never touch ~/.dsh. The fence concept, hash-check-before-upload, owner-click-as-confirmation flows, and the middleware order/bypass design are solid foundations.

Suggested order

  1. Tree items 1–4 (plugin-works-at-all), social items 5–7 (delivery reliability), trust items 8–12.
  2. Decide item 17 explicitly.
  3. Re-run CI on this PR (including the smoke bench for the index.js/src/host/ changes), then the P2 list can follow in this PR or a fast-follow.

Happy to pair on any of these — most fixes are small and local.

hsliuustc0106 added a commit that referenced this pull request Oct 14, 2026
Extend AGENTS/CONTRIBUTING with the OpenCode→Codex iteration loop
and Verifier honesty. Seed LESSONS from PR #2 fence/claim themes.
Clarify FlyBot accepts PRs; keep DSH hard rules.
hsliuustc0106 added a commit that referenced this pull request Oct 14, 2026
Extend AGENTS/CONTRIBUTING with the OpenCode→Codex iteration loop
and Verifier honesty. Seed LESSONS from PR #2 fence/claim themes.
Clarify FlyBot accepts PRs; keep DSH hard rules.

Co-authored-by: hs liu <hsliuustc0106@users.noreply.github.com>
hsliuustc0106 added a commit that referenced this pull request Oct 24, 2026
## Summary
Implements **Option B / Step 0** of #874 / #903.

- Add declarative `default` harness (`core/src/host/harness.js`) naming
today's prompt / tools / model / step·retry / verifiers modules
(behavior-neutral).
- Usage ledger `calls` → v3: every row carries `harness`, `issue_pr`,
`tool_calls`, `wall_ms`, plus existing token columns.
- **Turn wiring:** `foldCalls` counts `tool/call` events, computes
`wallMs` from attempt start, extracts `issuePr` from turn text (and
`rt.noteUsageIssuePr(sessionId, ref)`).
- Unit tests: `core/test/harness.test.js`,
`core/test/usage-fields.test.js`.
- Three mirrors kept in step (`node scripts/ds-bot-patches.mjs check`
green after `generate`).

## Tree prompt exception
`@flybot/tree` keeps its own marked prompt under `plugins/tree` (not one
of the three mirrors). Step 0 lists that exception on the default
harness; injection stays outside the harness file. Eval `P3-tree-split`
must still pass.

## Real usage.sqlite sample (acceptance #1)
Committed fixture (not a screenshot):
[`core/test/fixtures/usage-sample.sqlite`](https://github.com/ThinkFlowLab/FlyBot/blob/issue/903-step0-default-harness/core/test/fixtures/usage-sample.sqlite)

```text
harness|issue_pr|tool_calls|wall_ms|input|output
default|#903|2|1500|1200|340
```

Verify:
```bash
python3 - <<'PY'
import sqlite3
db = sqlite3.connect('core/test/fixtures/usage-sample.sqlite')
print(db.execute('SELECT harness, issue_pr, tool_calls, wall_ms, input, output FROM calls').fetchone())
PY
node --test core/test/usage-fields.test.js
```

Live `$DSH_HOME/bot/usage.sqlite` after a real turn (Node 24 / dsh
profile):
```bash
export DSH_HOME=/var/flybot-test
# …one turn on this branch's core…
python3 - <<'PY'
import os, sqlite3
path = os.path.expanduser(os.environ.get('DSH_HOME', '~/.dsh') + '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/bot/usage.sqlite')
db = sqlite3.connect(path)
print(db.execute('SELECT harness, issue_pr, tool_calls, wall_ms, input, output FROM calls ORDER BY time DESC LIMIT 5').fetchall())
PY
```

## Out of scope
Lite/Medium/Heavy, Magpie, #871, edits under `core/dev/harness/`,
secrets.

## Control set
Frozen 10-item set stays as documented in #903. Run table (acceptance
#2) still pending — track separately.

Closes #903

---------

Co-authored-by: hsliuustc0106 <hsliuustc0106@users.noreply.github.com>
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 24, 2026
…slots (ThinkFlowLab#993)

Owner: Dev3
Reviewer: Reviewer

Fixes ThinkFlowLab#992

Tip SHA: 93653c0

## Reproduce

On `b3abe396`, `ShareWithFriend` (`plugins/social/client.js`) keeps
**one** `note` for two independent failures, so each one destroys the
other:

1. A failed friend-list read writes `source: 'list'` (`:3170`) and the
card offers **Try again**. Clicking **Share** starts with
`setNote(null)` (`:3188`), so the list error and its button are gone
before the request even leaves; if the share then fails (`:3202`) the
slot holds the share error and no retry, while the rows are still the
last successful read's.
2. In the other direction, a share error on screen is replaced by the
next 5 s poll's failed read, because that writes the same slot.

Reviewer named this as non-blocking ThinkFlowLab#2 on ThinkFlowLab#989; it is a real card-state
bug: the card ends up stating neither failure correctly.

## What changed

- `plugins/social/client.js`, `ShareWithFriend`: **two slots**. `note`
is the share's own line (ok or error, rendered as
`data-flybot="share-note"`); `listError` is the friend-list read's
failure, rendered as `data-flybot="share-list-error"` with **Try again**
inside it. They are drawn side by side, so both facts can be on screen
at once.
- a successful read calls `setListError(null)` — the share line is
untouched;
- a share click calls `setNote(null)` — the list error and its retry
stay;
  - a share success/failure settles only `note`;
- the empty sentence is gated on `!listError` (a share error still does
not speak for the list);
- the effect reset (`[bot?.id]`) clears both; row keys, the 5 s poll and
ThinkFlowLab#984's read epoch/seq are untouched (the diff is the two `useState`s,
the writers, and the two status lines).
- `plugins/social/test/social.test.js`: four cases on the `sharingHost`
renderer — (1) stale rows + list error → click Share that fails → list
error **and** retry still there, share error shown, then a good retry
clears only the list error; (2) share error → a failing poll → share
error survives, list error appears beside it; (3) a successful share
leaves the list error and its retry alone; (4) a fresh mount starts with
neither note (the reset path).
- `docs/subsystems/social.md`: the two-slot rule replaces the one-slot
sentence.
- `changelog.d/992-share-two-notes.fixed.md`.
- Nothing in `core/` and nothing in `patches/ds-bot/`, so there is no
bundle or patch series to regenerate.

## What I ran

- `cd plugins/social && node --import ./test/resolve.mjs --test
--test-name-pattern="992" test/social.test.js` → **4/4**.
- That plus the ThinkFlowLab#987 and ThinkFlowLab#984 guards → **9/9**.
- Full `plugins/social` suite (`npm test`, `DSH_HARNESS` set) → **427
tests, 427 pass, 0 fail, 0 skipped**.
- `node .github/scripts/changelog.mjs check` → 68 fragments well formed.
- Four knives, each restored: share click clearing the list error → `not
ok 1` (992); the list-error line not rendered → all five ThinkFlowLab#987/ThinkFlowLab#992 retry
cases fail; the empty gate keyed on the share note → `not ok 3` (ThinkFlowLab#987);
a successful read not settling the list error → two red (ThinkFlowLab#987
list-retry, ThinkFlowLab#992 A).

## Red without the source change

Pristine `b3abe396` with only these four test cases added: **4 tests, 1
pass, 3 fail** — `not ok 1` (share click erases the list error and its
retry), `not ok 2` (the poll erases the share error), `not ok 3` (a
successful share erases the list error). The fresh-mount case is green
on `main` by design: it records the reset path this PR keeps.

## Not run

- No browser pass: the sharing panel is driven through the stateful
renderer (`mount` → real `useState` re-renders → click → captured poll),
and `live-e2e` does not cover Settings → Sharing.
- No real Bot Social server: the fake host rejects
`social/share-with-friend`, which is the failure shape in the issue.

## Risk

- **Two status lines can now stack** (a share error under a list error,
or the reverse). That is the point — neither erases the other — but the
card grows by one line in that state.
- **`share-note` / `share-list-error` are new `data-flybot` markers**;
nothing else read the old single `fs-status` line, and the
`share-list-retry` marker is unchanged, so ThinkFlowLab#984/ThinkFlowLab#987 tests and any
outside selector keep working.
- **A share outcome stays until the next share or a remount**, as
before; a successful share no longer clears the list error (it never did
correctly), and the reverse is now true too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants