Skip to content

Spawn ephemeral workflow Bots from node config - #1380

Merged
Birfy merged 1 commit into
mainfrom
ephemeral-workflow-bots-1373
Oct 27, 2026
Merged

Birfy merged 1 commit into
mainfrom
ephemeral-workflow-bots-1373

Conversation

@Birfy

@Birfy Birfy commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

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 tree 6db2784b6aadf7f36dbe5c16e21342a4a8b31434 and parent c2a7b9ce8ed57764876afb08cb132a0cf95bcedc. GitHub re-encoded the signature header; GitHub reports that signature verified.
Ran:

  • On the rebased tree: node --test plugins/workflow/test/*.test.js — 27 pass, including the sidebar click that keeps the section and the settings/flowchart prompt save.
  • On the rebased tree: node --import ./test/harness-resolve.mjs --test test/flybot.test.js in core/ — 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 verify reached the core suite (lint, plugin lint, client check, patch check green). One core cleanup hook failed with ENOTEMPTY on 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-harness checkout.

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"
}
Open in Web Open in Cursor 

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 27, 2026 — with Cursor
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
cursor Bot force-pushed the ephemeral-workflow-bots-1373 branch from bb81d63 to 1bb1aab Compare October 27, 2026 07:57
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 1bb1aab
Written-by: Reviewer
Attestation: 1bb1aab role=Reviewer

Ran / reviewed:

CI Test suite still settling at comment time; enqueue after tests-ok.

Ready for merge queue once checks green.

@github-actions github-actions Bot added the pass label Oct 27, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit bcb039e Oct 27, 2026
13 of 14 checks passed
@Birfy
Birfy deleted the ephemeral-workflow-bots-1373 branch October 27, 2026 08:02
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 -->
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow runs spawn ephemeral Bots from node config (not in sidebar)

2 participants