Repository navigation
Spawn ephemeral workflow Bots from node config - #1380
Merged
Merged
Conversation
A run creates one hidden Bot per node from that node's prompt, role, model and tools, asks it to do the step, and removes it when the run finishes, fails, or is cancelled. Settings, the flowchart, and define_workflow edit the same prompt and graph. Co-authored-by: Danyang Chen <birfychen@gmail.com>
cursor
Bot
force-pushed
the
ephemeral-workflow-bots-1373
branch
from
October 27, 2026 07:57
bb81d63 to
1bb1aab
Compare
Collaborator
Author
|
Verdict: PASS at 1bb1aab Ran / reviewed:
CI Test suite still settling at comment time; enqueue after tests-ok. Ready for merge queue once checks green. |
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 27, 2026
…kFlowLab#1325) fix(rooms): keep a peer's fence, and gate the peer's own exemptions Fixes ThinkFlowLab#1251 Owner: Dev1 Reviewer: Reviewer Written-by: Dev-1 Tip SHA: `66cc192412ef6dea0150f136b3d18afb2be09b1a` Base: `bcb039ef` (`main`, `ThinkFlowLab#1380`) Tree: `9f3f3892a0d59cb3fa81ca3b89d17e7edf5de90e` ## What changed `room.log` has two writers — `groups.js` `appendRoom` (a group round) and `room-membership.js` `append` (`postRoomMessage` from `room-post`, a remote seat's `line` op through `ingestSeatShare`, a face-room event, `ingestRemoteRoomLine`). They no longer each carry a copy of the rule: there is **one** rule, `room-context.js` `prepareRoomLine`, and both writers call it. - `prepareRoomLine` now recognises a complete `<untrusted-message from="…">…</untrusted-message>` wrap **before** the exemptions. While a line is fenced, neither `kind` nor `authorKind` exempts it: both travel from the other home (`room-membership.js` copies the peer's `kind` onto a seat line), so a peer could otherwise mark its essay `notice` or type it as a person and store it whole. The inner body is compacted with `compactStatus` and wrapped again with the same `from` via `fenceUntrusted`, because `compactStatus` → `oneLine` flattens `<` and `>` on purpose — compacting the fenced string stores a guest's essay as this home's own text. Unfenced lines keep the old rule: a status-shaped essay from this home's Bot becomes the stub; a person's line, a local `kind` event line and a short handoff come back unchanged. - `room-membership.js` `storeLine` degenerates to `prepareRoomLine(line)` — the second writer's private copy is gone. - `groups.js` `appendRoom` inherits the rule unchanged (it already called `prepareRoomLine`), so a peer line that reaches the group round keeps its fence; `fitRoomLines` uses the same function as its `prepare`, so the stored line and the next turn cannot disagree. - Prose this PR made false, one true sentence each: the budget comment in `groups.js` `roomPrompt` ("the stored log is unchanged") and the header of `room-context.js` ("Both writers … store the same stub"). - `docs/subsystems/core-host.md`: the Room context budget paragraph now says the fence decides before the `kind`/`botId` exemptions and that the `kind` line coming back unchanged is one **from this home**. That hunk changes exactly one paragraph (`git diff bcb039e -- docs/subsystems/core-host.md` = 1 insertion, 1 deletion). The reviewer's patch (comment `6435940635`, `git apply` against `802bfa54`) is the change itself. It was applied with `git apply --recount` because the posted docs hunk is one trailing blank context line short in the comment; the patch bytes are the reviewer's, and `--recount` only lets git infer the hunk's 6/6 line count from the lines actually present. The merge that follows (`upstream/main` → `bcb039ef`) is a merge, not a rebase. 15 files, `+492/−29` against base `bcb039ef`: `core/src/host/room-context.js` +25/−5, `core/src/host/room-membership.js` +10/−2, `core/src/host/groups.js` +9/−6, `core/test/group-work.test.js` +67, `core/test/remote-room.test.js` +76, `core/test/room-context.test.js` +28, `core/test/room-membership.test.js` +26, `docs/subsystems/core-host.md` +1/−1; the six generated mirrors (`patches/ds-bot/0001-core-fixes.patch` +75/−1, `0012-room-membership.patch` +10/−7, `files/src/host/room-context.js` +25/−5, `files/src/host/room-membership.js` +10/−2, `files/test/remote-room.test.js` +76, `files/test/room-context.test.js` +28, `files/test/room-membership.test.js` +26) are written by `node scripts/ds-bot-patches.mjs generate`, never hand-edited. ## Tests — red at base `bcb039ef`, green at this tip The four test files were run at the base tree with only the test files added (`git checkout bcb039e`, copy the four `core/test/*.test.js`), then the same commands at this tip. | command (`cd core && …`) | base `bcb039ef` | tip | | --- | --- | --- | | `node --import ./test/harness-resolve.mjs --test test/group-work.test.js` | exit 1 · 18/19 | exit 0 · 19/19 | | `node --import ./test/harness-resolve.mjs --test test/room-context.test.js` | exit 1 · 16/17 | exit 0 · 17/17 | | `node --import ./test/harness-resolve.mjs --test test/room-membership.test.js` | exit 1 · 15/16 | exit 0 · 16/16 | | `node --import ./test/harness-resolve.mjs --test test/remote-room.test.js` | exit 1 · 69/71 | exit 0 · 71/71 | Base failures, same output: ``` not ok 16 - a Bot status essay is stored as the stub the next speaker sees (ThinkFlowLab#1251) not ok 5 - a fenced peer essay is collapsed inside its wrap, never unwrapped (ThinkFlowLab#1251) not ok 3 - ThinkFlowLab#1251: postRoomMessage stores a Bot essay as the stub the room reads back not ok 43 - ThinkFlowLab#1251: a seat's Bot essay is stored as a fenced stub, not unfenced peer text not ok 44 - ThinkFlowLab#1251: a peer-chosen kind or authorKind does not exempt a seat line from the stub ``` They fail **at the writer, not at a helper**: the channel case returns the whole 1060-character essay where `[status on the board, 1060 chars] …` is expected; the seat case stores the fence around the whole 1519-character essay (`ESSAY_TAIL` present); the fenced-line case returns `[status on the board, 740 chars] untrusted-message from="Guest Bot" …` — the fence already flattened. ## The two blockers, measured on the trees Probes ran on the test harness at `802bfa54` (pre-patch) and at this tip (temp files, removed after measuring; the committed tests pin the same shapes). | behaviour | pre-patch `802bfa54` | this tip | | --- | --- | --- | | seat `line` op, `kind:"notice"`, 1,519-char probe essay (`test/tmp-probe-seat.test.js`; the committed `remote-room.test.js` "ThinkFlowLab#1251: a peer-chosen kind or authorKind…" uses a 1,060-char essay and asserts `< 300`) | stored **1593** whole (fence intact, `ESSAY_TAIL` present); same-turn slice **1595** | stored **209** — one `<untrusted-message from="@chief.alice.home (Alice)">`, one close, inner = `compactStatus(essay)`, no tail; slice **211** | | `appendRoom` given a complete fence (`test/tmp-probe-round.test.js`; now `room-context.test.js` "ThinkFlowLab#1251: a fenced peer essay is collapsed inside its wrap…") | stored **135** chars, **0** opens / **0** closes, text starts `[status on the board, 1575 chars] untrusted-message from="Chief (Bob)…` — the guest's line became this home's | stored **195** chars, **1** open / **1** close, inner text = `compactStatus(essay)`, `from="Chief (Bob…)"` unchanged | | peer claiming `authorKind:"human"` (`tmp-probe-seat.test.js`) | stored **1573** whole, `ESSAY_TAIL` present; slice **3176** | stored **189** fenced stub, no tail; slice **408** | | `prepareRoomLine(prepareRoomLine(x))` and a local `kind` line | idempotent; local `kind` notice and `ThinkFlowLab#1234 PASS @abcdef1 → @Writer` unchanged | idempotent (deep-equal); local `kind` line and handoff byte-identical | Numbers, itemised on this tree with one rule at both ends: the stored seat line is `27` (opener `<untrusted-message from="…">`) + `25` (this fixture's label `@chief.alice.home (Alice)`) + `1` (newline) + `135` (`compactStatus` of the 1,519-char essay, a 4-digit count) + `1` (newline) + `20` (closer `</untrusted-message>`) = **209**, and that turn's slice is `211` = `209` + the two brackets a `kind` line gets. The reviewer's `192` is the same shape at a 9-char label (`Guest Bot`) and a 3-digit count (`134`, an essay under 1,000 chars): `27 + 9 + 1 + 134 + 1 + 20 = 192`, measured through `appendRoom` with that complete fence, so it is not a seat-path length here — this path's 25-char label gives `208` at 3 digits and `209` at 4, and another independent fixture's 192 does not apply to this seat path. `399` does not reproduce from the inputs the comment names: that line's next-turn slice is `211`, so the missing `188` characters would be log lines `6435991528` does not name. Test (comment `6437090457`) and Security (comment `6437231581`) each measured `209 / 211` independently at this tree. ## Ran at the tip tree — exit 0 - `TZ=UTC npm run verify` at tree `9f3f3892a0d59cb3fa81ca3b89d17e7edf5de90e`: exit 0. **core 1804**; the same command at base `bcb039ef` is **1796**, delta **+8** (4 `group-work` + 1 channel + 1 seat + 1 fenced-peer + 1 peer-kind case). social 525, tree 125, timeline 125, workflow 27, `.github/scripts` 543, interop 37, live-e2e 24 — every suite `# fail 0`. - core lint 61 warnings / 0 errors; plugins 36 / 9 / 2 / 0 warnings / 0 errors. - `cd core && ARCH_DOCS_BASE=bcb039ef node scripts/check-arch-docs.mjs` → `arch-docs: OK (15 changed file(s))`, exit 0 (the base is named because an unbased run reads the local `origin/main` ref). - `node scripts/ds-bot-patches.mjs check` → `ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)` / `ok 10 patches apply cleanly in order` / `ok core/ equals ds-bot/ + patches/ds-bot/`, exit 0. - `node core/scripts/build-client.mjs --check`, exit 0. - Not run: the smoke test, the `package` job's typecheck/actionlint, and `dev/bench.sh`. ## Out of scope — stated, not done - **The read-side rule did not widen.** `statusShaped`, `compactStatus` and `fitRoomLines`' budget logic are unchanged; what changed is the store rule, which now also recognises a fence it used to compact through. `fitRoomLines` gets the same function it already used. - **A long Bot line that is not status-shaped is still stored whole.** Reproducible: `cd core && node --input-type=module -e "import { statusShaped } from './src/host/room-context.js'; const t='The tip is abcdef12 and I re-read the thread carefully. ' + 'Here is the narration of what I did and what I saw. '.repeat(32); console.log('chars', t.length, 'statusShaped', statusShaped(t))"` → `chars 1720 statusShaped false`. The store is bounded for status-shaped essays only; widening `statusShaped` stays out of this card. - `?`-only dispatch stays out of scope, as does anything in `groups.js:1032-1162` (`ThinkFlowLab#1312`'s territory); `groups.js:33` is untouched. `ThinkFlowLab#1335`/`ThinkFlowLab#1340` are not touched, commented on, or claimed. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1325, "claim": "Dev1", "issue": 1251, "summary": "Both writers of room.log now run one rule, prepareRoomLine, which recognises a peer's fence before the kind and authorKind exemptions. A fenced line is compacted inside its wrap and wrapped again with the same from, so a guest's essay is never stored as this home's own text, and a peer cannot exempt itself by naming a kind or typing it as a person. appendRoom inherits the rule and storeLine degenerates to it, so neither writer can drift. The read-side budget is unchanged; a long non-status line is still stored whole.", "files": ["core/src/host/room-context.js", "core/src/host/room-membership.js", "core/src/host/groups.js", "core/test/remote-room.test.js", "core/test/room-context.test.js", "core/test/room-membership.test.js", "core/test/group-work.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/files/src/host/room-context.js", "patches/ds-bot/files/src/host/room-membership.js"], "checks": { "ran": "TZ=UTC npm run verify; core lint; node scripts/lint-plugins.mjs; node scripts/ds-bot-patches.mjs check; node core/scripts/build-client.mjs --check; ARCH_DOCS_BASE=bcb039ef node core/scripts/check-arch-docs.mjs", "result": "green", "at": "66cc192412ef6dea0150f136b3d18afb2be09b1a" }, "next": "none — ready for review at the new tip" } ``` <!-- /flybot:checkpoint -->
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written-by: Dev
Owner: Dev
Reviewer: Reviewer
Fixes #1373
What changed
Starting a workflow creates one temporary Bot per node from that node's prompt, role, model and tools. Those Bots do the step and are removed when the run finishes, fails for good, or is cancelled. They stay out of the sidebar. The prompt and the graph are edited in Settings → Workflow graphs, on the flowchart, or with
define_workflow.Rebased onto current
main(c2a7b9ce8ed57764876afb08cb132a0cf95bcedc). The sidebar portal from #1377 and the Settings → Workflows page from #1374 stay. A sidebar click still opens the stage and leaves the Workflows section in place.Verification at the tip
Tip SHA: 1bb1aab
The local commit is
8c45bad11740cf45e6d0249c2579f483607d5c26. Both have tree6db2784b6aadf7f36dbe5c16e21342a4a8b31434and parentc2a7b9ce8ed57764876afb08cb132a0cf95bcedc. GitHub re-encoded the signature header; GitHub reports that signature verified.Ran:
node --test plugins/workflow/test/*.test.js— 27 pass, including the sidebar click that keeps the section and the settings/flowchart prompt save.node --import ./test/harness-resolve.mjs --test test/flybot.test.jsincore/— 45 pass.node scripts/ds-bot-patches.mjs check— ok.git merge-tree --write-tree HEAD origin/main— clean, same tree.TZ=UTC npm run verifyreached the core suite (lint, plugin lint, client check, patch check green). One core cleanup hook failed withENOTEMPTYon a Feishu temp dir (core/test/feishu.test.js); that file reran alone and passed 9/9. It is not part of this diff.Not run: bench smoke (
core/dev/bench.sh). No../deepseek-harnesscheckout.Checkpoint
{ "pr": 1380, "claim": "Dev", "issue": 1373, "summary": "Starting a workflow creates one temporary Bot for each node from that node's prompt, role, model and tools. Those Bots carry out the step and are removed when the run finishes, fails for good, or is cancelled, and they stay out of the sidebar. The same prompt and graph can be edited in Settings, on the flowchart, or with define_workflow. A fail-once retry keeps the run Bots until the run actually ends. The sidebar portal and Settings → Workflows page from main stay.", "files": ["plugins/workflow/src/ephemeral.js", "plugins/workflow/src/service.js", "plugins/workflow/index.js", "plugins/workflow/client.js", "core/src/host/hooks.js", "core/src/host/roster.js", "core/src/host/channel.js", "core/src/host/tools.js", "docs/subsystems/workflow.md", "changelog.d/1373.added.md"], "checks": { "ran": "node --test plugins/workflow/test/*.test.js", "result": "green", "at": "8c45bad11740cf45e6d0249c2579f483607d5c26" }, "next": "none — ready for review" }