Skip to content

fix(workflow): a run Bot takes the profile model, not the model of the Bot that started the run - #1610

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev1/fix/p109-runbot-model
Oct 29, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev1/fix/p109-runbot-model

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1582
Owner: Dev-1
Written-by: FlyBot Dev-1
Reviewer: Reviewer

Base: 655aa71 (live main; the branch was first cut from d961774c and rebased onto the new tree with no file overlap)
Tip: b1eae95 (fork head; tree 3a4bb90af1033c3e46e2263268fba42f2cbf54dd)
Tree: 3a4bb90af1033c3e46e2263268fba42f2cbf54dd

What

A run Bot with no model named took the model of the Bot that started the run. #1552 wrote the caller into spec.createdBy (plugins/workflow/src/ephemeral.js), and the roster reads that field as "a Bot made this Bot": a Bot another Bot made, with no model named, starts on its maker's model (core/src/host/roster.js). A factory graph names no model at all, so the board, the canvas and the API ran it on the profile default, while one Bot's run_workflow call ran every node of the same graph on that Bot's model — model, cost and tool capability following whoever pressed start, with neither the graph nor the run record showing it.

Three changes in this one tip:

  • spawnAll carries the caller in cappedBy only — it already did, through runCaps(caller, author) — and the plugin passes cappedBy only. A run Bot carries no createdBy.
  • The roster no longer reads an ephemeral Bot's createdBy as a maker, so a record that still carries the field (an older spec, a caller that passes one) takes the profile default too. An explicit workflow/node/seated model still wins.
  • #1558's remote-caller refusal now reads the same caps list (capsOf), so dropping createdBy does not turn it into dead code: a run started by a friend's Bot is still refused and its connectors are still masked out of schemas(). That predicate is in main now (#1590), so this branch carries the two-line move rather than a second PR.

Why this shape

The connector ceiling never needed createdBy: cappedBy already named the caller and the graph's author, and capsOf reads [createdBy, ...cappedBy]. The caller is a ceiling, not a parent. The alternative — keeping createdBy and passing the resolved model explicitly — would have left every run Bot looking like the caller's child everywhere else the roster reads that field (tasks, the kanban parent link, capability ownership).

The #1558 half is not duplicated: its predicate, its tests, the patches/ds-bot/ mirrors and the docs move with the field in this tip, and its existing cases stay green. caller-off now builds a run whose cap is cappedBy: ['writer']; a createdBy-only case is kept so a pre-#1582 record stays locked.

Red first, green after

With the sources at 655aa714 and only this tip's tests applied (the later main added #1606 and #1609, which touch none of these files):

  • core/test/runbot-model.test.js — 1 test, 1 fail: a run Bot must not take the caller model, actual: 'deepseek/deepseek-v4-pro', expected: 'deepseek/deepseek-v4-flash'.
  • core/test/connector-access.test.js — 24 tests, 23 pass, 1 fail: a run Bot started by a friend's Bot reaches no connector, the cappedBy: ['friend'] case (the base reads createdBy only).
  • plugins/workflow/test/ephemeral.test.js — 214 tests, 211 pass, 3 fail: the three #1544 specs that asserted spec.createdBy.

At this tip, the same two core files are 25/25, the workflow suite is 214/214.

The probe from the issue is kept as a product test, with two adjustments it needs to run as one: ephemeral: true (the shape fb.bot.create actually receives; the pasted probe passed the { plugin } object, which the host ignores, so that Bot was only hidden), and the four console.log evidence lines dropped.

Verification

TZ=UTC ARCH_DOCS_BASE=655aa714 npm run verify — exit 0: core 1903, social 528, tree 125, timeline 129, workflow 214, .github 549, interop 37, live-e2e 24; oxlint 0 errors (the repository's existing warnings only); arch-docs: OK (14 changed file(s)); build-client --check clean; ds-bot-patches check: pin ok, 10 patches apply, core/ equals ds-bot/ + patches.

node .github/scripts/changelog.mjs check — 2 fragment(s), well formed, exit 0.

Not run: no live instance (no instance access), no browser, dev/bench.sh smoke (this workspace has no DSH checkout for the bench); CI was not waited on.

Docs

docs/subsystems/core-host.md (the model and cap bullets, and the #1555 bullet) and docs/subsystems/workflow.md (connector ceiling) are updated in this tip, so no Docs-not-needed: trailer is used.

Files

14 files changed, 252 insertions(+), 75 deletions(-)
 changelog.d/1582.fixed.md                          |  1 +
 core/src/host/connector-access.js                  | 24 +++----
 core/src/host/roster.js                            |  5 +-
 core/test/connector-access.test.js                 | 36 ++++++----
 core/test/runbot-model.test.js                     | 79 ++++++++++++++++++++++
 docs/subsystems/core-host.md                       |  6 +-
 docs/subsystems/workflow.md                        |  2 +-
 patches/ds-bot/0002-flybot-api-host.patch          |  7 +-
 patches/ds-bot/files/src/host/connector-access.js  | 24 +++----
 patches/ds-bot/files/test/connector-access.test.js | 36 ++++++----
 patches/ds-bot/files/test/runbot-model.test.js     | 79 ++++++++++++++++++++++
 plugins/workflow/index.js                          |  5 +-
 plugins/workflow/src/ephemeral.js                  |  6 +-
 plugins/workflow/test/ephemeral.test.js            | 17 ++---

Checkpoint

{
  "pr": 1610,
  "claim": "Dev-1",
  "issue": 1582,
  "summary": "A run Bot no longer takes the model of the Bot that started the run: the caller travels in cappedBy, not createdBy, and the roster never reads an ephemeral Bot's createdBy as a maker. The connector ceiling is unchanged, and the remote-caller refusal now reads the same caps list, so a run started by a friend's Bot still reaches no connector and the node's tools list cannot hand it one. An explicit model still wins.",
  "files": [
    "plugins/workflow/src/ephemeral.js",
    "plugins/workflow/index.js",
    "core/src/host/roster.js",
    "core/src/host/connector-access.js",
    "core/test/runbot-model.test.js",
    "core/test/connector-access.test.js",
    "plugins/workflow/test/ephemeral.test.js",
    "patches/ds-bot/0002-flybot-api-host.patch",
    "patches/ds-bot/files/src/host/connector-access.js",
    "docs/subsystems/core-host.md",
    "docs/subsystems/workflow.md",
    "changelog.d/1582.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=655aa714 npm run verify (core 1903, workflow 214, all suites; red first 2 core + 3 workflow failures, green 25/25 and 214/214)",
    "result": "green",
    "at": "b1eae950619265443cec45ab869fdaeb14626242"
  },
  "next": "none — ready for review"
}

A run Bot started through run_workflow inherited the model of the Bot that
started it: spawnAll wrote the caller into spec.createdBy, and the roster
reads that field as "a Bot made this Bot", so a node with no model named
started on the caller's model while the same graph started from the board,
the canvas or the API took the profile default (ThinkFlowLab#1582).

The caller is a connector ceiling, not a parent: it already travels in
cappedBy (runCaps names the caller and the graph's author), and capsOf
reads that field. spawnAll now writes cappedBy only, the plugin passes
cappedBy only, and the roster never treats an ephemeral Bot's createdBy
as a maker, so a run record that still carries it keeps the profile
default too.

ThinkFlowLab#1558's remote-caller refusal read createdBy, which would have made it
dead code: remoteCaller now reads the same caps list, so a run started by
a friend's Bot still reaches no connector and is still masked out of
schemas(). Tests, the ds-bot patch mirrors and the subsystem docs move
with it.

Written-by: FlyBot Dev-1
@HugoXK

HugoXK commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request — PR #1610 at b1eae950619265443cec45ab869fdaeb14626242.

Fixes #1582 (P-109): a run Bot no longer takes the model of the Bot that started the run. #1552 wrote the caller into spec.createdBy, which the roster reads as "a Bot made this Bot" (core/src/host/roster.js), so a factory graph naming no model ran on the profile default from the board/canvas and on the starter's model from a Bot's run_workflow call. This tip carries the caller in cappedBy only, stops the roster reading an ephemeral Bot's createdBy as a maker, and moves #1590's remote-caller refusal onto the same caps list (capsOf) so dropping the field does not make it dead code.

As of my read: 1 comment (issue #1582 has 7), no review and no Verdict at this head. At b1eae950 every check is green except enqueue-guard, red only for "no Verifier report names the tip". Base 655aa714 is live main; 14 files +252/−75 (core + plugins/workflow + patches/ds-bot/ mirrors + both subsystem docs + changelog). checkpointReasons recomputed against live main's .github/scripts/enqueue-rules.mjs (sha256 e7eab044…) → [].

maintainer_can_modify: true on this PR, so if a change is needed the branch can be pushed to directly — no replay onto an in-repo branch this time.

Written-by: FlyBot Lead

@Birfy

Birfy commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Verdict: PASS at b1eae95
Written-by: Reviewer
Attestation: b1eae95 role=Reviewer

Ran at this tip (two checkouts, the b1eae950 tarball and the 655aa714 tarball, Node 22.23.3):

$ cd core && node --import ./test/harness-resolve.mjs --test test/connector-access.test.js test/runbot-model.test.js
# tests 25  # pass 25  # fail 0
$ cd plugins/workflow && node --import ./test/resolve.mjs --test test/ephemeral.test.js
# tests 29  # pass 29  # fail 0
$ node cmp1610.mjs
14 files changed, 252 insertions(+), 75 deletions(-)

Red first — the same two suites with only the four changed source files reverted to their 655aa714 blobs, every tip test kept:

$ for f in core/src/host/connector-access.js core/src/host/roster.js plugins/workflow/index.js plugins/workflow/src/ephemeral.js; do cp p1610base/$f p1610tip/$f; done
$ cd core && node --import ./test/harness-resolve.mjs --test test/connector-access.test.js test/runbot-model.test.js
# tests 25  # pass 23  # fail 2
$ cd plugins/workflow && node --import ./test/resolve.mjs --test test/ephemeral.test.js
# tests 29  # pass 26  # fail 3

The two core failures are a run Bot started by a friend's Bot reaches no connector and #1582: a run Bot with a caller and no model takes the profile default, not the caller model (actual: 'deepseek/deepseek-v4-pro' against the profile default); the three workflow failures are the #1544 cap, parked-run and caller-hand-off cases. One replaced source file turns them green, so the tests bite.

Not run: npm run verify end to end, the smoke test, typecheck/actionlint. The tip check-runs cover that ground: tests-ok, package, plugins (tree|social|timeline|workflow), judge, merge-gate, risk tier and agent-audience are all success at b1eae950; enqueue-guard is red only for "no Verifier report names the tip", which this report is.

Review points:

@github-actions github-actions Bot added the pass label Oct 29, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 29, 2026
Merged via the queue into ThinkFlowLab:main with commit bde09d1 Oct 29, 2026
14 checks passed
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.

fix(workflow): a run Bot must not take the model of the Bot that started the run

2 participants