Repository navigation
FlyBot plugin layer (ctx.flybot) with social and tree plugins - #2
Conversation
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.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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.
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 } } |
There was a problem hiding this comment.
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 👍 / 👎.
| 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)) |
There was a problem hiding this comment.
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 👍 / 👎.
| const tree = createTree(fb, { limits: resolveLimits(config), rootName: config.rootName }) | ||
| const disposers = [ | ||
| fb.useSend('tree', treeSendMiddleware(tree, { bypassSocial: config.bypassSocial === true, isRemote: fb.isRemote })), |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 👍 / 👎.
|
|
||
| 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 |
There was a problem hiding this comment.
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) ` })) |
There was a problem hiding this comment.
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 👍 / 👎.
| for (const child of childrenOf(nodes, botId)) fb.setField(child.id, 'parent', up) | ||
| if (up) refreshPrompt(up) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 👍 / 👎.
…fuses a bare set)
… 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).
… 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
hsliuustc0106
left a comment
There was a problem hiding this comment.
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
plugins/tree/index.js—apply()never callstree.ensureRoot(). On a fresh install no Bot has tree fields,visibleTois false for everyone, andcreate_child_botis invisible: the plugin is permanently inert. Tests only work because they callensureRoot()through the return value.plugins/tree/client.js:48— callsflybot.on('bot.removed'), but the browsercreateClientApiexposes noon, and the hostchangesfeed only carries fieldchangeevents anyway. The TreeSection effect throws on mount (flybot.on is not a function) and leaks the already-registered fields listener.Safedoes not contain this — it only wrapscreateElement, not effects.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'throughrt.deliver) hitsDENY_OUTSIDEand is silently dropped — onlymessage_botchecksoutcomeOf, soturns.jsnever learns the reply was denied. Chief → Root → Chief loses its answer.- Same gate, worse:
src/host/tools.js:165delivers thestop_botnotice withrole: 'stopped', which is inBOT_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
plugins/social/src/social.js:228—pollInboxadvances 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 middlewaredenyis also counted as delivered. Set the cursor only after successful delivery, with per-event error handling.src/host/hooks.js:354— inbound remote messages carry no return address (delivery.js:56setssenderSessionIdonly for local senders), so the local Bot's normal turn-end reply fails withUnknown Bot undefinedand is hidden. Replies only work if the Bot explicitly callsmessage_friend_bot.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
src/host/delivery.js:7—fenceUntrustedstrips</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 insocial.jsget_friend_file. Strip repeatedly until stable, or reject content containing marker fragments.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_botssimilarly 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.plugins/social/src/social.js:75+ the approval card —message_friend_bot'sfilestakes 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. Approvingflybot-social.json · 1 KBcan ship the connector token or~/.ssh/id_rsa; withapproval.homes: auto, per-botautoApprove, orrequireApproval: falseit 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.plugins/social/src/social.js:397— saved credentials are not bound to the server and the saved token overrides an explicitly configured one: changingconfig.serversends the previous server's bearer token to the new endpoint. Bind credentials to (and key them by) the server URL.plugins/social/src/social.js:40—slug()collapses every non-ASCII name tobot: 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
src/host/hooks.js:287— approving a held send callsdispatch()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.src/host/hooks.js:440— any@handleanywhere 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.plugins/socialstartup races: concurrentadd_home_memberfor the same Bot (no in-flight guard), startup can race ds-bot's initial state load (bots missed → never registered, since disk load emits nobot.created), delete-during-registration orphans the server identity, and a failedstartSocialleaves a live, undisposable instance (middleware/interval already registered, entry point never gets the object to dispose).- Read-only hosts: the
READSallowlist insrc/host/channel.jsexcludes everyflybot/*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. plugins/tree/src/tools.js:59—create_child_botis 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:
changeSeqresets per process while the browser watermark never regresses → field events silently stop after every host restart (persist the seq or add an epoch).store.jscorrupt-fieldsreset doesn't setmigrated, so it re-sanitizes every start.change.valuehands 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.jsonper hold), send text has no size cap, and an approve whose dispatch fails becomesstatus: 'failed'which the approvals UI never shows (it listspendingonly) — the message is lost to both sides with no Retry. - Tree:
createChildis check-then-act across an awaited create — parallel tool calls (which the coordinator prompt explicitly encourages) exceedmaxChildren/duplicate siblings. Reparenting after a deletion refreshes only the grandparent's prompt (children keep stale traces) and ignoresmaxChildren. - 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
statSyncsize check can be bypassed by a file that grows beforereadFileSync. - MCP client: no timeout/AbortSignal anywhere (a hung server stalls polling, transport, and
approvals.decideindefinitely), and it advertisestext/event-streaminacceptbut would crash parsing an SSE response. - Docs/conventions:
README.zh.mdnot 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
filesomitsplugins/,@flybot/socialis"private": true(tree isn't), both peer-depend on an unpublishedds-bot, and there is no workspace package.json — as shipped there's no installable artifact for the documented plugin setup. The rootpackage.json"types"also mis-points the package's entry types atflybot.d.ts. - API surface:
api.handlesilently 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 (
Safeonly wrapscreateElement; render-body and effect errors still take down the sidebar). New strings in ds-bot's core client (flybot-slots.js,at-menu.js) bypasst()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
- Tree items 1–4 (plugin-works-at-all), social items 5–7 (delivery reliability), trust items 8–12.
- Decide item 17 explicitly.
- 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.
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.
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>
## 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>
…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.
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.jsis one new host module, installed inMODULESafterresumeand beforechannel. It wrapsrt.deliver, so every Bot-to-Bot send (message_bot, groups, schedules) goes through one send-middleware chain. It is exposed as theflybotservice.state.json(bot.fields,state.flybot.approvals,state.flybot.remote). Saves bump the roster revision, so the UI refreshes.src/client/flybot-registry.js(no React) andflybot-slots.jsprovide the frontend slots:flybot.sidebar.sectionflybot.bot.detailsflybot.approvals@menu rowsctx.flybot(bots(),fields.on,call). The committedclient.jsis rebuilt, and CI checks it withbuild-client.mjs --check.ds-bot/plugins/*. Their tests run undernpm test, andscripts/lint.mjslintsplugins/.ds-bot/types/flybot.d.ts.API summary (
ctx.flybot)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}.on('change' | 'bot.created' | 'bot.removed').bot.removedfires after removal and carries the last record.bot.list/get/find/create({name, systemPrompt, fields})/setSystemPrompt/isRemote.bot.send(msg)andbot.send.use({name, before, after, handle}). A middleware returns allow, deny or hold; returningundefinedmeans allow.bypassonly takes effect whenconfig.flybot.bypassenables it. Remote targets appear asmsg.target = {remote, home, handle}.approvals.list/get/decide/onChange. Held sends show up as approval cards.transports.add(kind, transport)andtools.register({name, parameters, execute(args, {botId}), visibleTo}).parametersaccepts JSON Schema.remote.publish/list/getmark friends' remote Bots as chat members. They show in the sidebar and@menu with a remote badge but are not inbot.list().@handle-mentions one, the message goes throughbot.send, so the plugin's approval still applies.<untrusted-message>.api.handle(name, fn)servesflybot/<name>.Plugins
/v1/onboard/flybot) and keeps credentials in a mode-600 file.list_friend_botsandmessage_friend_bot.list_childrenandcreate_child_bot, with depth and child-count limits.Tests
npm testpasses 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
hookswrapsrt.deliver. A middleware that throws counts as deny, so a buggy plugin can block sends.@handlementions are matched only against published remote handles. Text that looks like a handle but isn't one is not sent.