Close the nine open SDK tasks: one engine per directory, a tabbed dashboard, re-pairing, run recovery
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s

serve: refuse a second engine for one data directory whatever port it was
asked for, using the pidfile and a token this directory signed. The check
runs before the database is touched and before the credential is written,
which is what left every later CLI call pointing at a dead port.

The terminal dashboard is three tabs (Overview, Runs, Logs) with the toolbar
following the focused pane, the engine's output goes to serve.log rather than
down a pipe, and closing the screen stops both reader threads so the prompt
comes back. It adopts a running engine on every start, so stop/start and
restart work on one it did not start, and a stop waits for the process to be
gone before the next start. Enrolment reports itself in the modal.

enroll: a new claim code replaces the pairing instead of being refused. The
code is redeemed before anything is written, mappings to a portal being left
are cleared, and a running engine redials when the stored enrolment changes.

runs: an engine re-queues the runs left `queued` by the one before it, and
`fluksio retry <id>` / `retry --group <sweep>` submits an interrupted run
again with the same inputs and group, recorded through Run.parent_id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9BoNGq6V9MdRWAte7JBuC
This commit is contained in:
2026-08-31 17:37:13 +02:00
co-authored by Claude Opus 5
parent bdad6d7fc2
commit 8a94bf10d7
14 changed files with 873 additions and 140 deletions
+32 -8
View File
@@ -186,9 +186,9 @@ def _prepare(data_dir: Path) -> None:
def _enroll(portal: str, code: str, as_email: str | None) -> int:
from sqlmodel import Session
from fluksio.cloud import config as cloud_config
from fluksio.cloud import enroll as enroll_mod
from fluksio.core.bootstrap import ensure_superuser, pick_superuser
from fluksio.core.config import settings
from fluksio.core.db import engine
with Session(engine) as session:
@@ -203,19 +203,15 @@ def _enroll(portal: str, code: str, as_email: str | None) -> int:
# Read before the session closes: the instance is detached after it,
# and touching an attribute then goes back to a database that is gone.
email = user.email
previous = cloud_config.load()
try:
config = enroll_mod.enroll(session, user, portal, code)
except enroll_mod.AlreadyEnrolled as exc:
print(
f"error: {exc.detail} (see {settings.CLOUD_CONFIG_FILE}).",
file=sys.stderr,
flush=True,
)
return 1
except enroll_mod.EnrollError as exc:
print(f"error: {exc.detail}", file=sys.stderr, flush=True)
return 1
if previous is not None and previous.instance_id != config.instance_id:
_say(f"Replaced the connection to {previous.portal_url}.")
_say(
f"Connected to {config.portal_url} as {email} (instance {config.instance_id})."
)
@@ -387,6 +383,26 @@ def _free_port(host: str, start: int) -> int:
return start
def already_serving(data_dir: Path, host: str) -> str:
"""Where this directory's engine is answering, if one is.
Two engines on one SQLite file is not a supported shape, and the second
one does more than fail: it repoints `client.json` at itself, so every
later CLI call goes to a port that dies with it. The pidfile says where to
look and the token says whether what answers there is ours.
"""
running = read_pidfile(data_dir)
if running is None:
return ""
reachable = "127.0.0.1" if host in ("0.0.0.0", "::", "") else host
url = f"http://{reachable}:{running['port']}"
# A pid can be reused, so the file alone is not proof. What answers has to
# accept this directory's token as well.
if probe_engine(url, _token_for(data_dir)) != "ours":
return ""
return f"{url} (pid {running['pid']})"
def cmd_serve(args: argparse.Namespace) -> int:
# At a terminal this is a dashboard with the engine as a child of it. The
# import is here rather than at the top because it is only ever needed on
@@ -397,6 +413,14 @@ def cmd_serve(args: argparse.Namespace) -> int:
return run_tui(args)
data_dir = _data_dir(args.data_dir, args.shared)
# Before `_prepare`, so a refusal never migrates the database another
# engine is serving out of, and before `_sign_in`, so the credential keeps
# pointing at the engine that is actually up.
where = already_serving(data_dir, args.host)
if where:
_say(f"An engine for {data_dir} is already serving at {where}.")
_say(" fluksio status talks to it; stop it to start another.")
return 0
for flag, name in CONCURRENCY_FLAGS.items():
value = getattr(args, flag, None)
if value is not None: