Skip to content

feat(studio): KonfAI Studio web app over konfai-mcp, released in lockstep#65

Merged
vboussot merged 19 commits into
mainfrom
feat/konfai-studio
Jul 21, 2026
Merged

feat(studio): KonfAI Studio web app over konfai-mcp, released in lockstep#65
vboussot merged 19 commits into
mainfrom
feat/konfai-studio

Conversation

@vboussot

@vboussot vboussot commented Jul 21, 2026

Copy link
Copy Markdown
Member

What

Adds KonfAI Studio — a FastAPI BFF + built React front over konfai-mcp — as the konfai-studio
package, taking the release matrix to 9 packages. A clinician-researcher points at a dataset and drives
train / infer / visualize / reproduce from one chat surface; the LLM brain is pluggable (Claude Code /
Anthropic / local OpenAI-compatible via KONFAI_STUDIO_LLM), and data never leaves the machine.

Why this is one lockstep PR

konfai_studio.server imports konfai_mcp.live_parse, so Studio and konfai-mcp must ship the same
version
. This PR therefore lands the coupled set for v1.6.1:

  • core — live training control + on-demand validation (feeds the Studio live panel)
  • apps — tunables, session-root run isolation, download_bundle
  • konfai-mcp — studio-driving tools + the "run published apps as normal experiments" pivot
    (import_apprun_prediction / run_resume / run_evaluation) + tests
  • studio — BFF, React chat / NiiVue viewer / live feed / leaderboard, user docs, smoke tests,
    CI matrix entry, Apache-2.0 LICENSE

Quality / consistency

  • Redundancy pass — factored the duplicated idioms an elegance review flagged across all packages
    (net line reduction; adds _jail, _all_jobs, _mcp_detail, front api.ts/useJson/readSSE, …),
    and fixed a latent job-ordering bug (_experiment_info sorted by mtime).
  • BFF splitserver.py (1900 → 866 lines) into focused modules (paths, registry with a
    SessionState dataclass, auth, terminal, tensorboard, jobs); App.tsx per-session state
    consolidated into one Record<string, SessionUiState>.
  • Uniform gatesstudio and konfai-mcp are now under the shared root ruff + mypy gates
    (pixi tasks + pre-commit hooks). All four packages share one [tool.ruff] config and type-check clean;
    wiring them in surfaced and fixed real drift (unformatted mcp files, a cross-package
    AppRepositoryInfo._download access in studio). bandit stays scoped to core + apps.

Verification

  • pixi typecheck green across all four packages; the strict mypy pre-commit hook passes --all-files
  • studio smoke tests + the apps and mcp suites stay green (behaviour preserved through the refactors)
  • clean fast-forward onto main; wheel ships the built React front (package-data = web/**/*),
    konfai-mcp=={version} lockstep pin, tag-derived version

Release

After merge, tag v1.6.1publish.yml builds + publishes all 9 packages (Studio front built via Node in
CI) to PyPI over OIDC, then the Docker image. The konfai-studio PyPI trusted-publisher is registered.

Summary by CodeRabbit

  • New Features

    • Introduced KonfAI Studio, a chat-based web UI for datasets, experiments, training, evaluation, visualization, and deployment.
    • Added app importing, run deletion, on-demand validation, and live training controls (including live learning-rate and validation scheduling).
    • Added weights-only warm-start fine-tuning and browser-based ONNX inference.
  • Bug Fixes

    • Hardened app/run workspace safety, improved fine-tuning loss validation, and fixed console/log streaming resilience when output readers disconnect.
  • Documentation

    • Updated README and usage/MCP guides to reflect the new app lifecycle (import/predict/resume/export) and Studio workflows.

vboussot added 18 commits July 21, 2026 13:11
…udio feed

A LiveControl channel steers lr and it_validation mid-run (recorded in the config as an audit trail), an on-demand validation pass runs on SIGUSR1, and a run stays alive when its console pipe closes -- the core hooks the Studio live feed drives.
Honour per-op tunables, keep an app run in the caller's session workspace (no throwaway sub-dir), add download_bundle (resolve + install into a directory), and refuse to fine-tune a config with no training loss.
Adds set_live_tunables, delete_run, request_validation and richer live_parse, and retires the app-execution wrappers in favour of import_app: a published app is copied into the session and run through the ordinary run_prediction / run_resume(weights_only) / run_evaluation tools.
Streams the chat over SSE, serves the front, streams volumes to NiiVue, and drives konfai-mcp through a pluggable brain (claude-code / openai / anthropic). A stream-broken agent is rebuilt and resumes its transcript.
…e wheel

setup.py pins konfai-mcp to the exact release version (like apps/* pin konfai), and the package-data glob is recursive so web/assets/ (the JS/CSS/WASM) ships, not just web/index.html.
The build job runs npm ci && npm run build first (the React front is git-ignored) then python -m build --wheel (wheel-only: the sdist file-finder drops the built web/). Test job installs and smoke-tests studio.
brief-for-matt.html (a strategy pitch), PLAN.md (the build plan), and the spec-visual/ui-mockup HTML were dev artifacts, not part of the public package; the ops guide (REMOTE.md), the spec, and the screenshot stay.
Factor out the duplicated idioms the elegance review flagged, across the
lockstep set. Behaviour preserved except the _experiment_info job ordering fix.

- core: hoist the poll cadence into _LIVE_POLL_INTERVAL and fold the rank-0
  produce+broadcast into _poll_from_rank0 (shared by both pollers)
- apps: apply _clear_dataset in fine_tune teardown and add _collect_result,
  the mirror of _stage_result_dir, for infer/evaluate/uncertainty
- mcp: drop the dead Job.set_parameters plumbing and the orphaned
  _parse_live_metric_line wrapper left by the live_parse extraction
- studio BFF: _jail path guard (removes the drift across 5 sites), _all_jobs
  as the sole job.json reader (fixes _experiment_info ordering by mtime),
  _require_train_job, _apps_after, _mcp_detail/_mcp_json, _one_shot_claude
- studio front: api.ts (getJson/postJson), useJson hook, readSSE, status.ts,
  and SPDX headers on the TS/TSX files
Break the ~1900-line server.py into focused modules and consolidate the
front's per-session state. Behaviour preserved.

- studio BFF: extract paths.py (jail + workspace/history), registry.py
  (a SessionState dataclass held in one dict[str, SessionState], replacing
  the 8 parallel dicts; sessions.json schema unchanged), auth.py, terminal.py,
  tensorboard.py, jobs.py. server.py (866 lines) is now app creation + route
  wiring; entangled experiment/job-control routes stay there by design.
- studio front: consolidate nine per-session Record maps into one
  Record<string, SessionUiState> (sessionState.ts) with patchSession /
  replaceSessionField helpers.

Verified: every route group serves 200 on relaunch, the registry round-trips
the legacy sessions.json, the smoke tests pass, and tsc + build are clean.
Bring studio/konfai_studio under the same quality gates as the core:
- pixi lint / format / format-check / typecheck now include studio
- pre-commit ruff, ruff-format and mypy hooks now cover studio
- fix the type smells this surfaces: SimpleCookie type-arg, an
  unannotated lambda -> functools.partial, an out/out_path type clash,
  and the str|None brain/model resolution narrowed via `x or default`
- refresh the agent module docstring to list all three LLM backends
  (claude-code default, anthropic, openai)

bandit stays scoped to core + apps: studio's findings are all Low
(assert, try/except/pass, controlled TB/PTY subprocess) and would need
per-line nosec noise for little value.
Wire konfai-mcp/konfai_mcp into the root ruff lint/format tasks and the
pre-commit ruff/ruff-format hooks, matching studio and the core. The shared
root [tool.ruff] config already governs it (line length 120, same rules), so
this only aligns where the gates run, not the rules. Reformats the two files
this surfaces (an import_app Annotated wrap and server_apps) that had drifted
because konfai-mcp was outside the commit-time gate.
Bring apps and mcp under pixi typecheck + the mypy pre-commit hook, so all four
packages are type-checked under the shared config. Fixes the errors this
surfaces, type-only and behaviour-preserving:
- apps: importlib spec guard, PackageMetadata read via subscript, explicit cast
  for the always-set upload group names, a typed files list (keeps the original
  single-zip dataset upload), gpu_list annotation
- mcp: declare the host attributes MetricsServiceMixin borrows (TYPE_CHECKING
  stubs), guard the mean-delta operands, Message cast, isinstance guards
- studio: guard repo._download behind isinstance(LocalAppRepository) -- a remote
  repo has no downloadable bundle, now a clean 404

Verified: pixi typecheck green across all four, the strict mypy hook passes
--all-files, and the apps + mcp suites stay green (upload format unchanged).
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9181c320-65af-4d50-915f-938df22f659f

📥 Commits

Reviewing files that changed from the base of the PR and between 351d62d and e856550.

📒 Files selected for processing (10)
  • konfai-mcp/konfai_mcp/server_jobs.py
  • konfai/trainer.py
  • studio/frontend/src/Deploy.tsx
  • studio/frontend/src/FolderBrowser.tsx
  • studio/frontend/src/Login.tsx
  • studio/frontend/src/RightPanel.tsx
  • studio/frontend/src/api.ts
  • studio/frontend/src/styles.css
  • studio/konfai_studio/server.py
  • studio/konfai_studio/tensorboard.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • studio/frontend/src/Login.tsx
  • studio/frontend/src/FolderBrowser.tsx
  • studio/frontend/src/api.ts
  • studio/frontend/src/Deploy.tsx
  • studio/konfai_studio/tensorboard.py
  • konfai/trainer.py
  • studio/frontend/src/RightPanel.tsx
  • konfai-mcp/konfai_mcp/server_jobs.py
  • studio/frontend/src/styles.css
  • studio/konfai_studio/server.py

📝 Walkthrough

Walkthrough

This PR adds KonfAI Studio, replaces app-specific execution with import-based workflows, adds live training controls and log parsing, refactors fine-tuning artifacts, and updates packaging, documentation, tests, and release automation.

Changes

KonfAI Studio

Layer / File(s) Summary
Studio backend and runtime
studio/konfai_studio/*
Adds the FastAPI BFF, LLM/MCP agents, session registry, authentication, SSE job feeds, terminal, TensorBoard integration, workspace APIs, and packaging configuration.
Studio frontend
studio/frontend/*
Adds the React/Vite chat workspace, experiment controls, live metrics, viewers, app gallery, deployment flow, browser inference bindings, and styling.

Import-Based App Workflow

Layer / File(s) Summary
App import and workflow routing
konfai-mcp/konfai_mcp/{server,server_apps,runner,guide,workflows}.py
Adds import_app, delete_run, and weights_only resume behavior while removing app-specific execution entrypoints and routing guidance through standard workflows.
Workflow validation
konfai-mcp/tests/*
Updates app routing tests and adds coverage for import security, run deletion, weights-only checkpoints, and workflow-kind changes.

Live Training and Fine-Tuning

Layer / File(s) Summary
Live controls and metrics
konfai/trainer.py, konfai/network/network.py, konfai/utils/live_control.py, konfai-mcp/konfai_mcp/live_parse.py
Adds revisioned live tunables, on-demand validation signaling, synchronized trainer polling, persistent LR rebasing, and shared runtime-log parsing.
Fine-tune artifacts and validation
konfai-apps/konfai_apps/app.py, konfai-apps/konfai_apps/app_repository.py, konfai-apps/tests/unit/*
Adds loss detection, configurable artifact roots, staged local outputs, safer bundle handling, and fine-tuning tests.

Documentation and Release Wiring

Layer / File(s) Summary
Workflow and Studio documentation
README.md, docs/source/*, studio/docs/*, .claude/skills/*, konfai-mcp/README.md
Documents Studio installation and operation and updates published-app guidance to use import_app, run_prediction, and run_resume(weights_only=True).
Build and quality checks
.github/workflows/publish.yml, pyproject.toml, .pre-commit-config.yaml, AGENTS.md
Adds Studio frontend builds and tests, packages the Studio wheel, and broadens linting and type-checking coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • fideus-labs/KonfAI#44: Broadens Ruff formatting and lint coverage in related configuration.
  • fideus-labs/KonfAI#63: Continues the MCP architecture refactor with shared live-log parsing and workflow-driven tool routing.

Poem

A rabbit hops through Studio’s glow,
Imports apps where old paths go.
Live curves wiggle, checkpoints sing,
Chat and metrics sprout a wing.
“Run it fresh!” the bunny cheers—
“Fine-tune softly through the years!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Clear Conventional Commit title that summarizes the main change: Studio over konfai-mcp with lockstep release.
Description check ✅ Passed Detailed and focused on the change, motivation, verification, and release plan, though it doesn't follow the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/konfai-studio

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (3)
studio/konfai_studio/registry.py (1)

174-196: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Global self._create lock serializes agent creation across all sessions.

agent() holds the single registry-wide self._create lock while awaiting make_agent(...).__aenter__(), which spawns the konfai-mcp subprocess and completes the MCP handshake. A slow or hung spawn for one session blocks first-turn agent creation for every other session (head-of-line blocking), which works against the stated "run concurrently" design. Consider a per-session creation lock (or reuse the per-session turn lock) so building one task's agent can't stall others.

Please confirm whether concurrent first-turns on distinct sessions are expected to build in parallel, or whether global serialization here is intentional to avoid a race in make_agent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@studio/konfai_studio/registry.py` around lines 174 - 196, Replace the
registry-wide self._create lock in agent() with per-session synchronization,
reusing the session’s existing creation or turn lock if available, so
stale-agent cleanup and make_agent(...).__aenter__() remain serialized only
within the same session while distinct sessions can initialize concurrently.
Preserve the existing state.agent reuse and stale reset behavior, and verify any
shared make_agent race protection remains intact.
konfai-apps/konfai_apps/app.py (2)

1518-1541: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Support-file scan doesn't exclude ./Dataset (unlike the bundle-collection loop below).

Path(".").rglob("*") here walks the freshly-created ./Dataset (line 1514) with no exclusion. Recursive ** globbing normally skips symlinked directories, so this is harmless when KonfAIApp.symlink() succeeds — but that helper falls back to shutil.copytree/copy2 when os.symlink fails (Windows without Developer Mode, or a filesystem without symlink support), making ./Dataset a real directory that IS traversed. On that path this loop walks the entire user dataset and can pick up/copy any incidentally-named .py/.yml/.yaml file that happens to live inside it into the training scratch dir. The later bundle-collection loop (line 1601: skip = {"Dataset"}) was already hardened against exactly this; this loop should mirror it.

♻️ Proposed fix
             for support in Path(".").rglob("*"):
-                if not support.is_file() or "__pycache__" in support.parts:
+                if not support.is_file() or "__pycache__" in support.parts or "Dataset" in support.parts:
                     continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai-apps/konfai_apps/app.py` around lines 1518 - 1541, Exclude the
top-level Dataset directory while scanning support files in the
keep_training_artifacts branch. Update the Path(".").rglob("*") loop to skip
entries whose path is rooted under Dataset, matching the existing
bundle-collection skip behavior, while continuing to copy eligible support files
elsewhere.

1367-1369: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Pass evaluations_dir by keyword here too.
Use the same explicit call style as the sibling evaluate() call to keep this independent of parameter order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai-apps/konfai_apps/app.py` around lines 1367 - 1369, Update the
evaluate() call in the Uncertainties flow to pass evaluations_dir explicitly by
keyword, matching the sibling evaluation call and avoiding dependence on
positional parameter order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.pre-commit-config.yaml:
- Around line 36-38: Update the file-matching regex for both the ruff and
ruff-format hooks to include the package-local test directories
konfai-apps/tests/, konfai-mcp/tests/, and studio/tests/, while preserving the
existing source and root tests/ matches.

In `@konfai-mcp/konfai_mcp/server.py`:
- Around line 2764-2785: Update request_validation to reject any job whose kind
is not "train" before checking active status or sending SIGUSR1, matching the
guard used by set_live_tunables. Preserve the existing response for valid
training jobs and return the appropriate failure response for non-training jobs.

In `@konfai/trainer.py`:
- Around line 233-237: Preload existing interventions from the config snapshot
when initializing _Trainer, so OOM-created instances preserve the audit trail
written by earlier retries. Update the initialization near _config_snapshot and
_interventions, and ensure _record_interventions continues from the loaded list
while retaining the existing overwrite behavior for the complete accumulated
history.

In `@studio/frontend/src/api.ts`:
- Around line 6-17: Update getJson and postJson to check the fetch Response.ok
status and throw for non-success HTTP responses before calling r.json().
Preserve JSON parsing for successful responses so callers can handle 401/500
failures through their existing catch paths.

In `@studio/frontend/src/Console.tsx`:
- Around line 44-51: Update the ws.onclose handler in the Console effect to
dispose the existing terminal instance before clearing references. Call
term.dispose(), then set termRef.current and fitRef.current to null so reopening
creates a clean terminal without duplicate DOM.

In `@studio/frontend/src/Deploy.tsx`:
- Around line 90-97: Update the run() flow in Deploy.tsx to track the backend
returned or selected during execution in a local variable, rather than using the
closed-over backend state value in the completion message. Use that local
backend variable in the setMessage interpolation while preserving the existing
setBackend state update.

In `@studio/frontend/src/FolderBrowser.tsx`:
- Around line 22-39: Update the FolderBrowser fetch flow to track an error
state, set a user-visible message when the `/api/browse` request fails or
parsing fails, and clear the error before each new request or after a successful
response. Render the error state near `modal-path` using the existing modal
error styling, while preserving the current directory and file updates on
success.

In `@studio/frontend/src/Login.tsx`:
- Around line 25-32: Update the session confirmation logic in the login flow to
stop converting getJson("/api/auth") failures into authenticated results. Let
the request rejection reach the existing error-handling path, and call
onAuthed() only when the actual response has authenticated set to true.

In `@studio/frontend/src/RightPanel.tsx`:
- Around line 1182-1186: Update the apply function to validate the numeric
results assigned to body.lr and body.it_validation, rejecting any non-finite
values before sending the request. Preserve the existing empty-input guard and
ensure invalid tunable text returns without proceeding to /api/job/tunables.

In `@studio/frontend/src/styles.css`:
- Around line 23-24: Update the --fsans and --fmono font-family values to
satisfy Stylelint’s value-keyword-case rule by lowercasing unquoted font names
or quoting proper/multi-word names; keep the intended fallback order and fonts
unchanged.
- Line 604: Replace deprecated word-break: break-word with overflow-wrap:
break-word in .tool pre, .console-body .ln, .toast, .app-name, and .feed-log .ln
at studio/frontend/src/styles.css lines 604, 1007, 1435, 2537, and 2962.

In `@studio/konfai_studio/tensorboard.py`:
- Around line 116-144: Expose a shutdown_servers() helper next to the
_TB_SERVERS cache that terminates each still-running TensorBoard process, safely
ignores termination errors, and clears the cache. Import and invoke
shutdown_servers() from server.py’s _lifespan finally block alongside await
_reg.close().

---

Nitpick comments:
In `@konfai-apps/konfai_apps/app.py`:
- Around line 1518-1541: Exclude the top-level Dataset directory while scanning
support files in the keep_training_artifacts branch. Update the
Path(".").rglob("*") loop to skip entries whose path is rooted under Dataset,
matching the existing bundle-collection skip behavior, while continuing to copy
eligible support files elsewhere.
- Around line 1367-1369: Update the evaluate() call in the Uncertainties flow to
pass evaluations_dir explicitly by keyword, matching the sibling evaluation call
and avoiding dependence on positional parameter order.

In `@studio/konfai_studio/registry.py`:
- Around line 174-196: Replace the registry-wide self._create lock in agent()
with per-session synchronization, reusing the session’s existing creation or
turn lock if available, so stale-agent cleanup and make_agent(...).__aenter__()
remain serialized only within the same session while distinct sessions can
initialize concurrently. Preserve the existing state.agent reuse and stale reset
behavior, and verify any shared make_agent race protection remains intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08ac53c2-5f35-4d6d-89a4-51f2e269f362

📥 Commits

Reviewing files that changed from the base of the PR and between 1316fe7 and 351d62d.

⛔ Files ignored due to path filters (6)
  • docs/source/_static/konfai-studio.png is excluded by !**/*.png
  • studio/docs/screenshot.png is excluded by !**/*.png
  • studio/frontend/package-lock.json is excluded by !**/package-lock.json
  • studio/frontend/src/konfai-rs/konfai_rs_bg.wasm is excluded by !**/*.wasm
  • studio/konfai_studio/web/konfai-logo-light.png is excluded by !**/*.png
  • studio/konfai_studio/web/konfai-logo.png is excluded by !**/*.png
📒 Files selected for processing (87)
  • .claude/skills/konfai-experiments/SKILL.md
  • .claude/skills/konfai-experiments/references/tool-reference.md
  • .github/workflows/publish.yml
  • .pre-commit-config.yaml
  • AGENTS.md
  • README.md
  • docs/source/index.rst
  • docs/source/usage/index.rst
  • docs/source/usage/mcp.md
  • docs/source/usage/studio.md
  • konfai-apps/konfai_apps/app.py
  • konfai-apps/konfai_apps/app_repository.py
  • konfai-apps/konfai_apps/app_server.py
  • konfai-apps/tests/unit/test_finetune_lr_override.py
  • konfai-apps/tests/unit/test_finetune_requires_loss.py
  • konfai-mcp/README.md
  • konfai-mcp/konfai_mcp/capabilities.py
  • konfai-mcp/konfai_mcp/extensions.py
  • konfai-mcp/konfai_mcp/guide.py
  • konfai-mcp/konfai_mcp/live_parse.py
  • konfai-mcp/konfai_mcp/metrics_service.py
  • konfai-mcp/konfai_mcp/runner.py
  • konfai-mcp/konfai_mcp/server.py
  • konfai-mcp/konfai_mcp/server_apps.py
  • konfai-mcp/konfai_mcp/server_jobs.py
  • konfai-mcp/konfai_mcp/server_support.py
  • konfai-mcp/konfai_mcp/workflows.py
  • konfai-mcp/tests/test_live_parse.py
  • konfai-mcp/tests/test_mcp_server_apps.py
  • konfai-mcp/tests/test_mcp_server_delete_run.py
  • konfai-mcp/tests/test_mcp_server_fixes.py
  • konfai-mcp/tests/test_mcp_server_refine_loop.py
  • konfai-mcp/tests/test_mcp_server_set_live_tunables.py
  • konfai-mcp/tests/test_mcp_server_tool_index.py
  • konfai-mcp/tests/test_review_mcp_apps_misc.py
  • konfai-mcp/tests/test_workflow_registry.py
  • konfai/network/network.py
  • konfai/trainer.py
  • konfai/utils/live_control.py
  • konfai/utils/runtime.py
  • pyproject.toml
  • studio/.gitignore
  • studio/LICENSE
  • studio/README.md
  • studio/docs/REMOTE.md
  • studio/docs/STUDIO_SPEC.md
  • studio/frontend/.gitignore
  • studio/frontend/index.html
  • studio/frontend/package.json
  • studio/frontend/src/App.tsx
  • studio/frontend/src/AppZoo.tsx
  • studio/frontend/src/Chat.tsx
  • studio/frontend/src/Console.tsx
  • studio/frontend/src/Deploy.tsx
  • studio/frontend/src/FolderBrowser.tsx
  • studio/frontend/src/Login.tsx
  • studio/frontend/src/RightPanel.tsx
  • studio/frontend/src/Viewer.tsx
  • studio/frontend/src/api.ts
  • studio/frontend/src/konfai-rs/konfai_rs.d.ts
  • studio/frontend/src/konfai-rs/konfai_rs.js
  • studio/frontend/src/konfai-rs/konfai_rs_bg.wasm.d.ts
  • studio/frontend/src/main.tsx
  • studio/frontend/src/sessionState.ts
  • studio/frontend/src/sse.ts
  • studio/frontend/src/status.ts
  • studio/frontend/src/styles.css
  • studio/frontend/src/useJobStream.ts
  • studio/frontend/src/useJson.ts
  • studio/frontend/tsconfig.json
  • studio/frontend/vite.config.ts
  • studio/konfai_studio/__init__.py
  • studio/konfai_studio/__main__.py
  • studio/konfai_studio/agent.py
  • studio/konfai_studio/auth.py
  • studio/konfai_studio/cli.py
  • studio/konfai_studio/jobs.py
  • studio/konfai_studio/paths.py
  • studio/konfai_studio/registry.py
  • studio/konfai_studio/server.py
  • studio/konfai_studio/tensorboard.py
  • studio/konfai_studio/terminal.py
  • studio/pyproject.toml
  • studio/setup.py
  • studio/tests/test_smoke.py
  • tests/unit/test_live_control.py
  • tests/unit/test_trainer.py
💤 Files with no reviewable changes (2)
  • konfai-mcp/tests/test_mcp_server_refine_loop.py
  • konfai-mcp/tests/test_review_mcp_apps_misc.py

Comment thread .pre-commit-config.yaml
Comment on lines +36 to +38
files: ^(konfai/|konfai-apps/konfai_apps/|konfai-mcp/konfai_mcp/|studio/konfai_studio/|tests/)
- id: ruff-format
files: ^(konfai/|konfai-apps/konfai_apps/|tests/)
files: ^(konfai/|konfai-apps/konfai_apps/|konfai-mcp/konfai_mcp/|studio/konfai_studio/|tests/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== .pre-commit-config.yaml relevant lines ==\n'
nl -ba .pre-commit-config.yaml | sed -n '28,50p'

printf '\n== test directories in repo ==\n'
git ls-files | awk '
  /(^|\/)tests\// { print }
' | sort

printf '\n== package-local test paths presence checks ==\n'
for p in 'konfai-apps/tests' 'konfai-mcp/tests' 'studio/tests' 'tests'; do
  printf '%s: ' "$p"
  if git ls-files "$p" | grep -q .; then
    echo present
    git ls-files "$p" | sed -n '1,20p'
  else
    echo absent
  fi
done

Repository: fideus-labs/KonfAI

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== .pre-commit-config.yaml relevant lines ==\n'
awk 'NR>=28 && NR<=50 { printf "%4d  %s\n", NR, $0 }' .pre-commit-config.yaml

printf '\n== test directories in repo ==\n'
git ls-files | awk '
  /(^|\/)tests\// { print }
' | sort

printf '\n== package-local test paths presence checks ==\n'
for p in 'konfai-apps/tests' 'konfai-mcp/tests' 'studio/tests' 'tests'; do
  printf '%s: ' "$p"
  if git ls-files "$p" | grep -q .; then
    echo present
    git ls-files "$p" | sed -n '1,20p'
  else
    echo absent
  fi
done

Repository: fideus-labs/KonfAI

Length of output: 9394


Include package-local test trees in Ruff checks.

The regex matches the root tests/ tree, but not konfai-apps/tests/, konfai-mcp/tests/, or studio/tests/, so those tests bypass ruff and ruff-format.

Proposed fix
-        files: ^(konfai/|konfai-apps/konfai_apps/|konfai-mcp/konfai_mcp/|studio/konfai_studio/|tests/)
+        files: ^(konfai/|konfai-apps/(konfai_apps/|tests/)|konfai-mcp/(konfai_mcp/|tests/)|studio/(konfai_studio/|tests/)|tests/)

Apply the same pattern to ruff-format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pre-commit-config.yaml around lines 36 - 38, Update the file-matching regex
for both the ruff and ruff-format hooks to include the package-local test
directories konfai-apps/tests/, konfai-mcp/tests/, and studio/tests/, while
preserving the existing source and root tests/ matches.

Comment on lines +2764 to +2785
def request_validation(
kind: Annotated[
WorkflowKind, Field(description="Job kind to target when job_id is omitted (default 'train').")
] = "train",
job_id: Annotated[
str | None, Field(description="Exact job to validate (default: the latest job of the given kind).")
] = None,
) -> dict[str, Any]:
"""Ask a running training job to run a validation pass now, via SIGUSR1."""
job = JOB_REGISTRY.get(job_id) if job_id is not None else SESSION.discover_latest_job(kind)
if job is None or job.status not in ACTIVE_JOB_STATES:
return {"ok": False, "detail": "No running training job to validate."}
delivered = JOB_REGISTRY.notify(job, signal.SIGUSR1)
return {
"ok": delivered,
"job_id": job.job_id,
"detail": (
"Validation requested; it runs at the next iteration boundary."
if delivered
else "Could not signal the job (already finished, or not signalable)."
),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Where SIGUSR1 is handled across the runtimes.
rg -nP 'SIGUSR1|signal\.signal|sigaction' konfai konfai-mcp -C3

Repository: fideus-labs/KonfAI

Length of output: 6878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant job-discovery and signal-handling code.
sed -n '1,220p' konfai-mcp/konfai_mcp/server_jobs.py
printf '\n--- server.py around request_validation ---\n'
sed -n '2748,2798p' konfai-mcp/konfai_mcp/server.py
printf '\n--- server.py around set_live_tunables ---\n'
sed -n '2798,2860p' konfai-mcp/konfai_mcp/server.py

Repository: fideus-labs/KonfAI

Length of output: 14574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how jobs are launched and how latest-job selection works.
rg -n "_run_job\(" konfai-mcp/konfai_mcp/server.py konfai-mcp/konfai_mcp/server_jobs.py
printf '\n--- discover_latest_job ---\n'
sed -n '560,760p' konfai-mcp/konfai_mcp/server_jobs.py

Repository: fideus-labs/KonfAI

Length of output: 8395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect WorkflowKind and latest-job selection semantics.
rg -n "class WorkflowKind|WorkflowKind =|discover_latest_job|active_states|kind ==" konfai-mcp/konfai_mcp/server.py konfai-mcp/konfai_mcp/server_jobs.py konfai-mcp/konfai_mcp/workflows.py -C3

Repository: fideus-labs/KonfAI

Length of output: 7648


Restrict request_validation to training jobs SIGUSR1 is ignored by the job wrapper for non-training runs, so this can return success without triggering validation. Mirror set_live_tunables and reject non-train jobs up front.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai-mcp/konfai_mcp/server.py` around lines 2764 - 2785, Update
request_validation to reject any job whose kind is not "train" before checking
active status or sending SIGUSR1, matching the guard used by set_live_tunables.
Preserve the existing response for valid training jobs and return the
appropriate failure response for non-training jobs.

Comment thread konfai/trainer.py
Comment thread studio/frontend/src/api.ts
Comment on lines +44 to +51
ws.onclose = () => {
setConnected(false);
term.write("\r\n\x1b[2m[shell ended — reopen the drawer to start a new one]\x1b[0m\r\n");
termRef.current = null; // let a reopen spawn a fresh shell
};
term.onData((d) => ws.readyState === WebSocket.OPEN && ws.send(JSON.stringify({ type: "input", data: d })));
term.onResize(({ cols, rows }) => ws.readyState === WebSocket.OPEN && ws.send(JSON.stringify({ type: "resize", cols, rows })));
}, [open]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and inspect the relevant region with line numbers.
git ls-files studio/frontend/src/Console.tsx
wc -l studio/frontend/src/Console.tsx
cat -n studio/frontend/src/Console.tsx | sed -n '1,220p'

# Search for terminal lifecycle handling in the frontend.
rg -n "term\.dispose|hostRef|termRef|ws\.onclose|fitRef|term\.open|new Terminal|new FitAddon" studio/frontend/src -S

Repository: fideus-labs/KonfAI

Length of output: 5475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Identify the xterm package version and any local cleanup patterns.
rg -n '"`@xterm/xterm`"|"`@xterm/addon-fit`"|xterm' studio/frontend/package.json studio/frontend/package-lock.json studio/frontend/yarn.lock package.json package-lock.json yarn.lock -S

# Inspect any docs or comments around the console/terminal behavior.
rg -n "dispose\\(|open\\(hostRef|term\\.open|term\\.dispose|shell ended|reopen the drawer" studio/frontend/src -S

# Read the frontend package manifest if present.
if [ -f studio/frontend/package.json ]; then
  cat -n studio/frontend/package.json
fi

Repository: fideus-labs/KonfAI

Length of output: 1052


🌐 Web query:

xterm.js Terminal open dispose host container DOM append behavior dispose removes terminal DOM open same host without dispose

💡 Result:

In xterm.js, the terminal.dispose() method is designed to be a terminal operation for a terminal instance [1][2]. When called, dispose() removes the terminal's main DOM element from the parent container [3], cleans up internal event listeners, and releases references to prevent memory leaks [4][5]. Crucially, once dispose() has been called, the terminal instance should be considered unusable [6][1][7]. Attempting to reuse the same instance (e.g., calling open() again on a disposed terminal) is unsupported and likely to cause errors or broken behavior, such as missing render cycles or unresponsive event handlers [1][8][2]. If you need to "reset" or "re-open" a terminal in the same DOM container: 1. Do not attempt to reuse the disposed instance. 2. Instead, create a new Terminal instance using new Terminal() [2]. 3. Manually ensure the previous terminal's container is clear (if necessary), then call open(parent) with the new instance [1]. While some users have historically attempted to work around this by manually managing DOM elements or patching internal states, the recommended and stable pattern is to treat the Terminal object as a single-use lifecycle object that is destroyed and recreated when needed [1][2].

Citations:


Dispose the terminal on ws.onclose. termRef.current = null alone leaves the old xterm instance mounted; reopening the drawer creates a new terminal in the same host and can stack duplicate DOM. Call term.dispose() there, then clear termRef/fitRef.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@studio/frontend/src/Console.tsx` around lines 44 - 51, Update the ws.onclose
handler in the Console effect to dispose the existing terminal instance before
clearing references. Call term.dispose(), then set termRef.current and
fitRef.current to null so reopening creates a clean terminal without duplicate
DOM.

Comment on lines +25 to +32
// Confirm the session cookie actually stuck. Over plain http a Secure cookie is silently dropped,
// which would otherwise bounce straight back to this screen with no explanation.
const st = await getJson<{ authenticated?: boolean }>("/api/auth").catch(() => ({ authenticated: true }));
if (st.authenticated) {
onAuthed();
return;
}
setErr("Signed in, but the session cookie wasn't stored — are you on HTTPS? (see docs/REMOTE.md)");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat a failed session check as authenticated.

The fallback returns authenticated: true when confirmation fails, so onAuthed() runs without verifying the cookie. Let the request failure reach the existing error handler instead.

Proposed fix
-        const st = await getJson<{ authenticated?: boolean }>("/api/auth").catch(() => ({ authenticated: true }));
+        const st = await getJson<{ authenticated?: boolean }>("/api/auth");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Confirm the session cookie actually stuck. Over plain http a Secure cookie is silently dropped,
// which would otherwise bounce straight back to this screen with no explanation.
const st = await getJson<{ authenticated?: boolean }>("/api/auth").catch(() => ({ authenticated: true }));
if (st.authenticated) {
onAuthed();
return;
}
setErr("Signed in, but the session cookie wasn't stored — are you on HTTPS? (see docs/REMOTE.md)");
// Confirm the session cookie actually stuck. Over plain http a Secure cookie is silently dropped,
// which would otherwise bounce straight back to this screen with no explanation.
const st = await getJson<{ authenticated?: boolean }>("/api/auth");
if (st.authenticated) {
onAuthed();
return;
}
setErr("Signed in, but the session cookie wasn't stored — are you on HTTPS? (see docs/REMOTE.md)");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@studio/frontend/src/Login.tsx` around lines 25 - 32, Update the session
confirmation logic in the login flow to stop converting getJson("/api/auth")
failures into authenticated results. Let the request rejection reach the
existing error-handling path, and call onAuthed() only when the actual response
has authenticated set to true.

Comment thread studio/frontend/src/RightPanel.tsx
Comment thread studio/frontend/src/styles.css Outdated
Comment thread studio/frontend/src/styles.css Outdated
Comment on lines +116 to +144
@router.get("/api/tensorboard")
async def tensorboard_link(session: str = Query("default")) -> dict[str, Any]:
"""Ensure a TensorBoard server is running for the task's Statistics dir and return its URL — the full
TensorBoard UI (scalars, images, histograms) for every run, alongside Studio's own live feed. One
server per task, reused while alive, bound to 127.0.0.1 (a remote deployment must proxy the port)."""
name = _sane_session(session)
live = _TB_SERVERS.get(name)
if live and live["proc"].poll() is None:
return {"ok": True, "url": live["url"]}
# The session root, not Statistics/: TensorBoard recurses, so it surfaces every "*/tb" at any depth —
# session-root runs AND isolated app-output runs (<app_output>-<hash>/Statistics/<run>/tb) alike.
session_root = _workspace_root() / "sessions" / name
if not session_root.is_dir() or not any(session_root.glob("**/tb")):
return {"ok": False, "detail": "no TensorBoard events yet — run a training first"}
# Look next to the running interpreter first: the server is launched as a console script, so the env's
# bin dir may not be on PATH and shutil.which alone would miss it.
sibling = Path(sys.executable).with_name("tensorboard")
binary = str(sibling) if sibling.exists() else shutil.which("tensorboard")
if not binary:
return {"ok": False, "detail": "tensorboard is not installed (pip install konfai[tensorboard])"}
port = _free_port()
proc = subprocess.Popen(
[binary, "--logdir", str(session_root), "--host", "127.0.0.1", "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
url = f"http://127.0.0.1:{port}/"
_TB_SERVERS[name] = {"proc": proc, "url": url}
return {"ok": True, "url": url}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cached TensorBoard subprocesses are never reaped on shutdown.

_TB_SERVERS caches one long-lived tensorboard subprocess per session, but nothing terminates them — server.py's _lifespan only closes _reg. On Studio shutdown/restart these processes are orphaned, keep holding their 127.0.0.1 ports and memory, and accumulate across restarts.

Consider exposing a reaper and wiring it into the app lifespan.

🧹 Suggested reaper + lifespan wiring

In tensorboard.py:

def shutdown_servers() -> None:
    """Terminate all cached TensorBoard subprocesses."""
    import signal
    for entry in _TB_SERVERS.values():
        proc = entry.get("proc")
        if proc is not None and proc.poll() is None:
            with suppress(Exception):
                proc.send_signal(signal.SIGTERM)
    _TB_SERVERS.clear()

Then call shutdown_servers() from server.py's _lifespan finally block alongside await _reg.close().

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 136-140: Command coming from incoming request
Context: subprocess.Popen(
[binary, "--logdir", str(session_root), "--host", "127.0.0.1", "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@studio/konfai_studio/tensorboard.py` around lines 116 - 144, Expose a
shutdown_servers() helper next to the _TB_SERVERS cache that terminates each
still-running TensorBoard process, safely ignores termination errors, and clears
the cache. Import and invoke shutdown_servers() from server.py’s _lifespan
finally block alongside await _reg.close().

- core: SIGUSR1 does not exist on Windows, so installing the handler raised
  AttributeError (past the ValueError/OSError suppress) and broke every
  _Trainer construction there -- this is what turned the Windows CI red.
  getattr-guard it, and the mcp job wrapper's SIG_IGN the same way.
- core: merge the intervention audit trail into the config snapshot instead of
  overwriting it, so an OOM auto-restart no longer wipes the interventions the
  previous instance had already recorded.
- studio BFF: reap the cached TensorBoard subprocesses on app shutdown; they
  were orphaned across restarts, holding their ports.
- studio front: throw on non-ok responses in getJson/postJson; never treat a
  failed session check as authenticated; report the backend actually chosen in
  the deploy done-message; surface /api/browse failures; reject non-finite
  tunables; quote font names and replace deprecated word-break: break-word.
@vboussot
vboussot merged commit 352a2a2 into main Jul 21, 2026
33 of 34 checks passed
@vboussot
vboussot deleted the feat/konfai-studio branch July 21, 2026 21:14
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.

1 participant