Skip to content

FLUJO as a worker program for lite and cloud fleets; release 0.7.0 - #21

Merged
flujo-app merged 4 commits into
mainfrom
feat/flujo-worker-program
Nov 4, 2026
Merged

flujo-app merged 4 commits into
mainfrom
feat/flujo-worker-program

Conversation

@flujo-app

Copy link
Copy Markdown
Owner

FLUJO can now be a worker program next to Codex and Claude Code, on a laptop and on cloud lite fleets. Ships as 0.7.0. Plans that do not set workerProgram: 'flujo' behave as before, and the Codex and Claude Code bootstrap scripts are unchanged.

Fixes #16
Fixes #20

Worker program

  • plan.workerProgram: 'flujo'. A plan needs a modelProvider: FLUJO workers have no subscription login. A plan with a narrower sandbox is refused, because FLUJO's folder settings guide its tools but do not confine them.
  • WORKER_PROGRAM_APIS now lists the APIs each program can use, best first. FLUJO uses responses, then chat, then anthropic.
  • flujoProviderModel maps a provider to a FLUJO model record. FLUJO stores the key in its encrypted settings, so the FLUJO process and agent commands never have it in their environment.
  • runtime.mjs: what differs between programs is now one table instead of branches.

src/cloud-lite/flujo-driver.mjs

  • FlujoSessionDriver offers the same methods and notifications as the Codex app-server and the Claude Code driver, so NativeOperatorStop, NativeWorkflowWake, traces and the board are shared code.
  • One FLUJO workspace and one conversation per agent slot, so its board identity, model record and tools stay apart. With flujo.teamConcurrency an orchestrator gets the generic swarm_team flow with one native gate for its subflows.
  • A run belongs to FLUJO, not to the connection. A dropped event stream is followed again from its last event. A submission whose answer was lost is read back and never sent twice. A turn waits while FLUJO is under pressure, up to five minutes.
  • A turn ends, and a stop receipt can be issued, only after FLUJO says the conversation and all its children have ended. A readback asks FLUJO, not the driver's memory. A stop is requested again until FLUJO has stopped; if it never stops, no receipt is issued.
  • A message that arrives as a run ends is still delivered, in the same turn. A question or form from a tool is declined, because nobody is there to answer.
  • After a restart a turn that was running is closed as interrupted on the board and not replayed. The same now holds for Claude Code, whose board state stayed inProgress until the next turn.
  • FlujoProgram starts FLUJO and restarts it up to three times in ten minutes.

Install

  • The bootstrap installs one pinned flujo-ai tarball on the volume, checked by SHA-256, so a restarted Machine does not install it again. An image with flujo on its path skips this.
  • Install scripts stay off: they only download a browser that workers leave off.

Local: startLiteWorker({ provider: 'flujo' }) and factory-lite flujo attach to a FLUJO or start one with its own data folder, and keep the conversation across runs.

For apps moving their fleet onto FACTORY

  • packFactorySdk(file) builds the SDK bundle from the installed package, with no npm or tar on the host.
  • plan.network gives a fleet's app its own private network.

Fixed along the way

  • factory-selfhost retire waits for a stopping app and retries refused deletes. An app still stopping at the deadline or a delete that keeps failing stays uncertain, as before.
  • fleet.updateRuntime wrote a Codex bootstrap for every fleet, whatever its program.
  • A fleet that stopped part way, such as a refused Machine, could not be retired through fleet.retire(). It can now, and a foreign Machine or volume still blocks it.
  • fleet.collect() left FLUJO's key files in scope; they are excluded now, with the board credential file.

What the real program taught (each was invisible to a fake, and has a test or a note now)

  • FLUJO refuses a local tool server with a secret-looking variable at dispatch, with "Owner approval required". That is fixed on FLUJO's main (Compare a local MCP server's secret env by its resolved value at dispatch mario-andreschak/FLUJO#1338) but not in the pinned release. So the board server reads its credential from a private file, which works on both.
  • FLUJO refuses reasoningEffort for a model it does not know by name, so it is not sent.
  • With provider name openai, FLUJO moves the system message behind the conversation for OpenAI's prompt cache, and Qwen's chat template refuses that with a 400. Only OpenAI's own endpoint now carries that name; a self-hosted model is litellm.
  • FLUJO's tools start in its data folder, so a worker's prompt names its working folder.

Tests and docs: test/cloud-lite-flujo.test.mjs with a fake FLUJO server in test/fixtures/fake-flujo.mjs, added to the focused list. It covers the same operator lifecycle as Codex and Claude Code: start, jobs, messages during a turn, a verified stop receipt on the real board, restart, lost history, and workflow wakes. PUBLIC_SDK.md has a new section.

Live check on a real fleet

A real FactoryCloudLiteFleet on Fly: 2 Machines (shared, 2 CPUs, 4 GB), an official Node image by digest, the generated bootstrap, FLUJO 3.48.3 from its pinned tarball, a bundle from packFactorySdk, and a real vLLM endpoint (profile qwen, one H100) through FLUJO's Responses adapter.

Check Result
Both Machines bootstrap and register a session (209 s) pass
The model answers through FLUJO on a real Machine pass
The model calls a board tool and reports its identity pass
A shell command on the second Machine runs in its folder pass
The shell command does not see the key pass
A lead runs a real local subflow conversation pass
Operator stop during a long answer: verified receipt (idle, interrupted, matching command, thread and turn) pass
Traces and journal hold no key pass
fleet.quiesce() stops every session pass
fleet.collect() downloads an archive from each Machine, with conversations and worktrees but no key files or board credential pass
fleet.stop() and fleet.retire() remove the app and its volumes pass

The same checks of a model answer and a board call also passed through FLUJO's Chat Completions adapter, with the real machine code in a Linux container. factory-selfhost retire right after that busy replica finished in one run, and the app, secret and volumes were checked gone.

Real FLUJO with a scripted local model, in a Linux container with /data as the volume: the generated bootstrap on a plain Node image (6 min there), agent starts idle, jobs on one session, a message during a turn reaching the model, a verified stop receipt and a new job after it, no key in any file under the state and data folders, no board credential in FLUJO's settings, a hard kill of everything during a turn followed by a restart that keeps the session and closes the turn as interrupted, a lead with real subflows, and factory-lite flujo starting its own FLUJO, keeping the conversation across two runs and leaving no process behind.

Cost: Modal $1.46 by its billing report. Fly under $0.05 at list price for about six Machine-minutes each; that is an estimate, not an invoice. Everything was retired and checked gone.

Not run: a real FLUJO on Windows, FLUJO's Anthropic adapter against a real server (offline mapping only), a Machine restart on Fly (done in the container), and a team larger than two subflows.

Local CI (AGENTS.md)

Same steps as .github/workflows/public-sdk.yml (focused list read from the workflow file, 26 files), on a clean git archive export of de6b2b4 with fresh npm ci. Linux ran in disposable containers.

Platform Node / npm prod audit (high) Focused tests Python settings tests Packed install + verify-public-sdk CLI help + factory-selfhost profiles Installed MCP stdio Evidence regressions Left in temp
Windows 10, portable Node, Python 3.14 22.17.0 / 10.9.2 0 vulnerabilities 253/253 ok ok ok 1/1 7/7 0
Windows 10, portable Node, Python 3.14 24.22.2 / 11.19.1 0 vulnerabilities 253/253 ok ok ok 1/1 7/7 0
Linux, Docker node:22.17.0-bookworm-slim, Python 3.11 22.17.0 / 10.9.2 0 vulnerabilities 253/253 ok ok ok 1/1 7/7 0
Linux, Docker node:24.22.2-bookworm-slim, Python 3.11 24.22.2 / 11.19.1 0 vulnerabilities 253/253 ok ok ok 1/1 7/7 0

… a busy replica; 0.7.0

A plan can set workerProgram: 'flujo'. Everything shared stays shared: the board, operator
commands, stop receipts, traces, evidence and workflows talk to a driver through the contract
Codex and Claude Code already use.

- FlujoSessionDriver: one FLUJO conversation and workspace per agent, over FLUJO's HTTP API.
- flujoProviderModel: a model provider becomes a stored FLUJO model record, so the key never
  enters the environment agent commands inherit.
- startLiteWorker({ provider: 'flujo' }) attaches to a FLUJO or starts one.
- packFactorySdk builds the SDK bundle from the installed package; plan.network gives a fleet
  its own private network.
- runtime.mjs: what differs between programs is now one table.

factory-selfhost retire now waits for a stopping app and retries refused deletes.

Fixes #16
Fixes #20
@flujo-app
flujo-app merged commit 321b61b into main Nov 4, 2026
2 checks passed
@flujo-app
flujo-app deleted the feat/flujo-worker-program branch November 4, 2026 03:59
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.

factory-selfhost retire can need a second run right after a busy replica FLUJO as a worker program for lite and cloud fleets

1 participant