feat: durable HITL workspace with explicit model controls - #1
phoenixfire808 wants to merge 36 commits into
Conversation
Add the local-first React Flow and FastAPI/LangGraph workspace with the shared Nanbeige4.2-3B route on the RTX 2070 SUPER, bounded workspace tools, opt-in LFM agent chat, SQLite persistence, and Windows startup documentation.
Record the open GitHub pull request and final loopback verification, and keep Next generated route metadata aligned with the isolated acceptance dist directory.
Record the follow-up commit that published the isolated Next metadata and canonical tracker state.
Add the backend-owned local resource registry, templates, approval previews, Ollama inventory, named runtime profiles, terminal classification, read-only upgrade controls, and matching React Flow Library/Control Center surfaces. Keep runtime activation, shell execution, downloads, and provider fallback explicitly disabled.
Add bounded keyboard-accessible splitters, collapsible side panels, persisted layout state, and a taller vertically resizable resource list.
Nanbeige is retired from M⊕ model routing: - Removed all _nanbeige_output and _lfm_output generation paths - Removed Nanbeige-specific HTTP probe helpers - Removed nanbeige/lfm from model library static routes and templates - Removed Nanbeige endpoint profile and runtime profile - Removed Nanbeige from frontend provider choices and Canvas indicators New persistent global model setting (SQLite-backed): - New workspace_model_settings table - New GET/PUT /api/settings/model routes - Resolution hierarchy: node → workflow → global - Fallback locked to explicit_only; unknown models fail closed - Control Center now shows installed Ollama model dropdown and Save button New default: hf.co/mradermacher/LFM2.5-2.6B-UNCENSORED-ABLITERATED-PHILADELPHIA-CLASS-GGUF:Q4_K_M Ollama preflight confirms: exact installed, 1.67 GB, ready=true 26/26 tests pass. Generation smoke: 6.8s, empty (ablation model has no chat template) Option registry updated: - model.endpoint_profile default: local-nanbeige → local-ollama - model.exact_model_id: now ready, scope expanded to global, default set Launcher updated: - START_BACKEND.cmd: removed Nanbeige vars, uses WORKSPACE_MODEL_PROVIDER=ollama - .env.example: removed obsolete Nanbeige/LFM vars Other: - CoderNode: Nanbeige-specific thinking toggle removed; shows global default - Canvas health: model_ready replaces nanbeige/lfm readiness indicators - schema: active_hardware_lane default renamed from shared-rtx-2070-super to ollama-auto - upgrade_control: removed SM120 artifacts, reports workspace_model setting
…to ollama-auto - SSE run_started event now uses _lfm_model() instead of hardcoded DEFAULT_LFM_MODEL so it reflects the actual saved/resolved model (llama3.2:3b, not the old LFM ablation tag) - ChatPanel hardware_lane changed from stale 'shared-rtx-2070-super' (Nanbeige) to 'ollama-auto' to match the Ollama-only routing
- Replace astream_events() with sync iter_lfm_events() in ThreadPoolExecutor - Add /api/chat/sync endpoint for plain JSON HITL responses - Add /api/debug/llm-test for isolated LLM hang diagnostics - Remove stale @lru_cache from get_lfm_workflow() causing stale model binding - Fix hardware lane in SSE run_started to use resolved LFM model ID - Collect events in thread, stream as SSE to sidestep LangGraph async hangs
Adds a non-disruptive Docker Compose deployment for the M⊕ AI Visual Workspace. All four published ports are bound to 127.0.0.1 only, no cloud model fallback is enabled, and the Ollama service is pinned to the exact user-locked model tag: hf.co/mradermacher/LFM2.5-2.6B-UNCENSORED-ABLITERATED-PHILADELPHIA-CLASS-GGUF:Q4_K_M A dedicated ollama-model one-shot service verifies the exact model is present in the host-bound Ollama store before the backend becomes healthy, preventing accidental model substitution at startup. CUDA 12 runtime is requested via OLLAMA_LLM_LIBRARY with NVIDIA device passthrough. What ships: - docker-compose.yml: frontend (3000), backend (8000), ollama (11434), searxng (8888), and the model-init verification sidecar. Service-level healthchecks gate backend startup on Ollama readiness. - backend/Dockerfile + frontend/Dockerfile: workspace bind-mounted into the backend image so SQLite + project files persist on the host. - docker/searxng-config/settings.yml: project-relative SearXNG config with JSON output enabled for the research tool contract. - DOCKER_RUNBOOK.md: first-launch, smoke, and rollback instructions. - .env.docker.example: public template (no secrets). - .dockerignore: keeps .env*, build caches, and node_modules out of image context. - .gitignore: whitelists .env.docker.example template only. Tests (8/8 pass, backend/.venv): - test_docker_routing: only the internal ollama/searxng service hosts are accepted; loopback remains the default when WORKSPACE_DOCKER_MODE=0. - test_exact_model_policy: alternate model/endpoint/provider saves are rejected; only the exact LFM tag is accepted. Frontend typecheck passes. Live stack already running on smoke ports 3100/8100/11435/8889 with the exact model ready. Rollback baseline is the unchanged native frontend/backend/Ollama services on 3000/8000/11434. Refs: PROJECT_TRACKER.md phase 43 (Docker migration). PR #1.
…nd coder UI Hardens the exact-LFM-only contract introduced in the previous Docker slice so it is enforced at every call site, not just inside the Ollama process. The exact Ollama model tag hf.co/mradermacher/LFM2.5-2.6B-UNCENSORED-ABLITERATED-PHILADELPHIA-CLASS-GGUF:Q4_K_M is now the only value the workspace will accept for the local LLM route. Any save or graph invocation that requests llama3.2, hermes3, Qwen, Nanbeige, BF16, MiniMax, or any other tag is rejected before the backend calls Ollama. Backend: - model_profiles / model_settings / ollama_control: thread the exact tag through DEFAULT_OLLAMA_MODEL, the resolved-model helper, the capability matrix, and the /api/ollama/models /api/settings/model save path. Stale DEFAULT_MINIMAX_MODEL import removed. - runtime_control: hardware profile lookup tolerates the exact tag's uppercase characters and rejects empty model strings. - web_research: fix container-side SearXNG URL construction so the port appears inside the network location (http://searxng:8080) instead of producing http://searxng/8080. test_docker_routing covers both the in-container and host loopback defaults. - graph.py / library.py / main.py: drop legacy Nanbeige / multi-provider branches; chat and library paths route through the single exact-only Ollama adapter. Frontend: - Canvas: workspace banner now shows the exact LFM tag plus a pinned-global indicator. - ControlCenterPanel / ModelRouteSettings: any non-exact model save is rejected with a visible exact-tag-required message; the dropdown is read-only when exact-only policy is active. - nodes/CoderNode / nodes/types: the per-node model selector collapses to the exact tag; the route card shows model_policy=exact_only. - backend/.env.example: OLLAMA_MODEL is the exact tag; comment notes that alternate provider settings are intentionally unsupported. Verification: - backend/.venv -m unittest discover backend/tests -> 8/8 OK. - frontend npm run typecheck -> OK. - POST /api/settings/model with llama3.2:3b -> HTTP 400. - GET /api/health on smoke stack -> model_ready=true, model_policy=exact_only, ollama_model = exact tag. Refs: PROJECT_TRACKER.md phase 43. PR #1.
Append-only update to PROJECT_TRACKER.md covering the two commits just landed on this branch (Docker slice + exact-LFM lock). Records the live smoke URLs, the 8/8 backend test receipt, the frontend typecheck receipt, and the rollback baseline so future sessions can resume without re-reading prior context.
ROADMAP.md is the maintained one-page summary that points at: - Frozen detailed plans under .hermes/plans/ (unified library, resizable panels, durable HITL runtime, option-complete workspace) - The append-only PROJECT_TRACKER.md for implementation receipts Sections: mission + hard constraints, status snapshot, frozen-plan index, phase breakdown (25, 26, 27, 28, 43, 50, 60, 70, 80) with status legend, parent todo, maintainer protocol, and append-only changelog. Maintainer protocol: every shipped commit updates the phase status table and appends a dated changelog row in one atomic commit alongside the change. Stale entries are edited, not duplicated. Frozen plans remain the authoritative detailed specifications. Refs: PROJECT_TRACKER.md phase 43. PR #1.
- README now points at ROADMAP.md and PROJECT_TRACKER.md so visitors landing on the GitHub repo page can find both the maintained plan summary and the append-only implementation log. - ROADMAP.md changelog self-reference for the ROADMAP addition now uses its real commit SHA instead of the (next) placeholder. Refs: PROJECT_TRACKER.md phase 43. PR #1.
Phase 43 — Docker migration · exact-LFM lock · ROADMAP (5 commits)Five new commits landed on top of PR #1 (push
|
…sting searxng-hermes Phase 44 rename and SearXNG standalone handoff in one atomic commit so the rollback story is single-step. Rename: M⊕ AI Visual Workspace → Refactor Workflow Studio ======================================================== - Display: FastAPI title, frontend <title>, brand banner, ChatPanel assistant badge, ControlCenterPanel save notice, planner note, docker/searxng-config header, runbook header. - Identifiers: package.json name -> refactor-workflow-studio, compose project -> refactor, container_name -> rws-*, docker network -> refactor_default, named volume -> rws-searxng-cache. - Backend labels: model_profiles, runtime_control, upgrade_control, options_registry header, graph lock messages. - Docs: README, ROADMAP (header + Phase 44 changelog entry), PROJECT_TRACKER header + status block, M_PLUS_OPTION_COMPLETE_ROADMAP, MARKDOWN_INDEX, DOCKER_RUNBOOK. Historical PROJECT_TRACKER entries intentionally preserved as written per the append-only maintainer protocol in ROADMAP.md. - SearXNG secret_key rotated from mo-ai-workspace- to refactor-workflow-studio- prefix during rename. - GitHub repo name (phoenixfire808/ai-workspace) and PR #1 preserved per Drew's scope decision. Standalone SearXNG handoff ========================== SearXNG is now OUT of the main Refactor compose stack and reuses the existing healthy container (searxng-hermes, image searxng/searxng:latest) already running on the host loopback at 127.0.0.1:8888. - docker-compose.yml: removed the in-stack searxng service, the rws-searxng-cache named volume, and the SEARXNG_HOST_PORT env. Backend now sets WORKSPACE_DOCKER_MODE=0 and SEARXNG_URL=http://host.docker.internal:8888, with extra_hosts mapping host.docker.internal to host-gateway. - backend/web_research.py: _searxng_url() now accepts host.docker.internal as a valid URL even when WORKSPACE_DOCKER_MODE=0 (it resolves to the Docker Desktop host loopback, so this is no less safe than 127.0.0.1). - backend/tests/test_docker_routing.py: new test test_host_docker_internal_accepted_for_standalone_searxng covers both the explicit host.docker.internal URL and the bare loopback default. - docker/searxng/compose.yml (NEW): standalone optional compose file for managing searxng-hermes explicitly. Pulls the official image, bind-mounts its own settings.yml and cache volume, binds only to loopback. Project name: refactor-searxng. Default host port: 8890 (does not collide with searxng-hermes on 8888). - docker/searxng/settings.yml (NEW): tuned config for the standalone container, with a freshly rotated secret_key and the upstream default_doi_resolver key required by searxng/searxng:latest. - docker/searxng-config/ (REMOVED): moved to .hermes/pre-rename-backup/ so the in-stack config is no longer tracked. - .env.docker.smoke: removed SEARXNG_HOST_PORT and SEARXNG_CONFIG_DIR. - DOCKER_RUNBOOK.md: rewrote the service table and acceptance section to reflect the standalone SearXNG reality. - Cleaned up orphan volumes mo-searxng-cache and rws-searxng-cache. Verification (live, after the rename + standalone cutover) - 9/9 backend tests pass (was 8/8; +1 new test). - npm run typecheck passes (refactor-workflow-studio@0.1.0). - docker compose --env-file .env.docker.smoke config --quiet -> valid. - Refactor stack: rws-ollama, rws-ollama-model-init (exited 0), rws-backend, rws-frontend all healthy on loopback-only ports. - searxng-hermes unchanged: Up About an hour, healthy on :8888. - docker exec rws-backend -> http://host.docker.internal:8888/ -> HTTP 200. - /api/health: status=ok, model_ready=true, model_policy=exact_only, exact LFM tag locked, SEARXNG_URL=http://host.docker.internal:8888. - Frontend <title>: Refactor Workflow Studio. - Orphan searxng-cache volumes removed from previous attempts. Refs: PROJECT_TRACKER.md Phase 44, ROADMAP.md Phase 44, PR #1.
Phase 44 — Rename to Refactor Workflow Studio · standalone SearXNGOne commit landed on PR #1 (push Rename:
|
Phase 44 improvement dossier generated 2026-08-09 via the
MiniMax research-agent route with local SearXNG discovery
(searxng-hermes on 127.0.0.1:8888) and bounded crawler. No cloud
fallback was used; local Nanbeige GPU synthesis was attempted but
the service was offline (port 8080 refused connection), so synthesis
was authored by the parent agent per the deep-web-research skill rule
prohibiting silent model fallback.
Source set: 18 documents across 14 domains, 8 query variants.
7 sources were actually cited (S2 n8n README, S4 ComfyUI, S6
freeCodeCamp n8n guide, S7/S8/S16 LangGraph docs, S9 Linear).
4 discoveries were off-topic and pruned. Coverage summary and
limitation disclosure included inline.
Method (verbatim from deep-web-research skill):
python deep_research.py --brief-file .hermes/research-brief-rws-improvements.json --max-pages 32 --depth 1 --per-domain 2
Top-5 ranked themes:
1. Run Inspector as first-class debugging surface
(state-diff, replay-from-node, diff vs previous run,
model provenance breadcrumb).
2. Canvas: optimistic UI + deterministic reproducibility
(snapshots, run badges on nodes, optimistic drag,
snapshot diff).
3. Library/Options/Templates unified surface
(category counts, cross-link to Option Catalog,
recently-used chip).
4. Templates as ready-to-run bundles
(preview drawer, recent-runs tab, diff-from-canvas,
save-canvas-as-template, template parameters).
5. Approval & HITL: inline diff-style prompts
(per-action inline badges, state preview,
reject-with-reason, trust-mode workflow setting).
23 concrete recommendations in total, each tagged with project file
path + effort + risk. All recommendations are advisory; none are
binding without Drew's explicit approval.
Brief: .hermes/research-brief-rws-improvements.json
Evidence: .hermes/research/rws-improvements/{dossier.md,
evidence.jsonl, manifest.json, research.log}
Refs: PROJECT_TRACKER.md Phase 44, ROADMAP.md Phase 44, PR #1.
Phase 45: Drew asked for explicit on-demand load/unload of Ollama
models — no auto-warming, no implicit keep-alive beyond his choice.
This slice adds the symmetric pair (load / unload) as one-click UI
actions in Control Center, plus a live VRAM panel that polls the
loaded-state every 5 s. The chat-tool approval modal is bypassed for
the preload/unload pair because it is the user's explicit on-demand
request by definition; the exact-model policy is still enforced
upstream so a non-exact tag cannot slip through.
Backend
-------
- backend/ollama_control.py: three new functions
* list_ollama_running_models() -> reads Ollama /api/ps
* preload_ollama_model(model, keep_alive=5m) -> empty-prompt
/api/generate with keep_alive to force-load without producing
any generation
* unload_ollama_model(model) -> keep_alive=0 for immediate evict
All three honor the exact-model policy: only DEFAULT_OLLAMA_MODEL
is accepted; non-exact tags return structured failure rather than
silently operating on the wrong model.
- backend/main.py: three new routes
* GET /api/ollama/ps -> live loaded state
* POST /api/ollama/preload -> load (model via query param)
* POST /api/ollama/unload -> unload (model via query param)
Model name comes through the query string, not the URL path,
because Ollama tags contain colons (e.g. ':Q4_K_M') that conflict
with FastAPI segment-level path matching.
- safe_ollama_base_url() validator: now accepts host.docker.internal
as a valid hostname regardless of WORKSPACE_DOCKER_MODE, mirroring
the standalone-SearXNG pattern from commit 5ef8335. Validates port,
scheme, and lack of credentials the same way as before.
- backend/tests/test_ollama_load_unload.py: NEW, 5 tests covering
preload/unload policy enforcement (rejects empty + non-exact,
accepts exact default).
- backend/tests/test_docker_routing.py: +1 test for the new
host.docker.internal Ollama acceptance.
Docker
------
- docker-compose.yml: OLLAMA_BASE_URL changed from http://ollama:11434
to http://host.docker.internal:11435 to match the standalone Ollama
container pattern. WORKSPACE_DOCKER_MODE stays at 0; both Ollama
and SearXNG now run as independent host-loopback containers
reached via host.docker.internal from inside the backend image.
Comment block updated to document both containers.
Frontend
--------
- frontend/components/ControlCenterPanel.tsx: new 'VRAM · LIVE LOAD
STATE' section under the Ollama inventory block. Polls /api/ollama/ps
every 5 s with cancellation on unmount. Renders loaded model(s)
with VRAM in GB, exact-model target, and two buttons:
* 'Preload exact model' -> POST /api/ollama/preload?model=...
* 'Unload now' -> POST /api/ollama/unload?model=...
Force-refreshes /api/ps immediately after each action so the panel
updates without waiting for the next poll. Status messages include
the exact failure_class when the backend rejects (e.g. policy
mismatch) so the user sees why a non-exact tag was refused.
- frontend/app/globals.css: .ollama-vram-panel, .vram-actions,
.vram-message styles added (mobile-safe flex wrap).
Verification (live, post-deploy)
--------------------------------
- 15/15 backend tests pass (was 9; +6 new).
- npm run typecheck clean (refactor-workflow-studio@0.1.0).
- docker compose --env-file .env.docker.smoke build backend -> ok.
- docker compose --env-file .env.docker.smoke build frontend -> ok.
- docker compose --env-file .env.docker.smoke up -d --force-recreate
backend + frontend -> healthy.
- HTML now contains 'VRAM · LIVE LOAD STATE' marker.
- /api/ollama/ps: status=ready, 0 models loaded (idle).
- POST /api/ollama/preload?model=hf.co%2F...%3AQ4_K_M&keep_alive=10m
-> HTTP 200, status=preloaded, mutation=preload.
- /api/ollama/ps after preload: 1 model loaded, size_vram=2.25 GB.
- POST /api/ollama/unload?model=hf.co%2F...%3AQ4_K_M
-> HTTP 200, status=unloaded, mutation=unload.
- /api/ollama/ps after unload: 0 models loaded (immediate evict).
- POST /api/ollama/preload?model=llama3.2:3b
-> HTTP 200, status=model-policy-mismatch (rejected with
structured failure rather than silently loaded).
Refs: PROJECT_TRACKER.md Phase 45, ROADMAP.md Phase 45, PR #1.
…ngelog Append-only changelog entry per the maintainer protocol in ROADMAP.md section 5. Phase 45 ships the symmetric preload/unload pair, the live VRAM panel, the host.docker.internal validator update for both Ollama and SearXNG, and the compose URL switch to match the standalone container pattern.
Phase 45 — On-demand Ollama preload / unload · live VRAM panelTwo commits landed on PR #1 (push WhyDrew asked for explicit on-demand load / unload — no auto-warming, no What shipsBackend (3 routes + 3 functions + 6 new tests):
Model name comes through the query string, not the URL path, because Ollama tags contain colons ( All three enforce the exact-model policy upstream: non-exact tags return Validator update: Docker compose fix: Frontend (Control Center — VRAM · LIVE LOAD STATE):
Verification (live, post-deploy)
Files (7 changed, +539 / −9)Refs: |
feat: durable HITL workspace with explicit model controls
Phase 27 — Nanbeige retirement · persistent global model · option catalog
Model cutover
llama3.2:3b— saved in SQLite, confirmedready=trueby/api/healthexplicit_only— no silent local-to-cloud substitutionFix commits (this PR)
0fd2797— fix: use resolved LFM model in SSE run_started and fix hardware lane to ollama-autorun_startedevent now uses_lfm_model()instead of hardcodedDEFAULT_LFM_MODELso it reflects the actual saved modelChatPanelhardware_lane changed from stale'shared-rtx-2070-super'(old Nanbeige lane) to'ollama-auto'05c71ee— docs: update tracker status for Phase 27 Nanbeige retirement and LFM global defaultc69c3ff— feat: retire Nanbeige from M⊕, add LFM2.5-2.6B Q4_K_M as global defaultArchitecture
New persistent global model setting (SQLite-backed):
workspace_model_settingsnode → workflow → globalPUT /api/settings/model— save one exact installed Ollama modelGET /api/settings/model— read current global selectionnanbeige_retired_from_workspaceControl Center model selector:
Option registry
backend/options_registry.py— 39 option definitions, 20 node contracts, 33 control surfaces:OptionDefinition,OptionResolution,OptionEvidencevalidate_resolution()— rejects unsafe inheritance, unknown conflicts, unmapped surfacesoption_inventory()+option_inventory_markdown()GET /api/optionsandGET /api/options/export.md(read-only)frontend/components/OptionCatalogPanel.tsx:/api/optionsVerified
Changed files
Acceptance gate: Model generates a bounded non-empty response when invoked from M⊕.
Deployment: Backend at
http://127.0.0.1:8000, frontend athttp://127.0.0.1:3000.Phase 28 — Async LLM hang fix (5fee07c)
Problem:
astream_events()andainvoke()from LangGraph hang when called inside FastAPI's async event loop with httpx.Solution: Replace async streaming with synchronous iteration in a
ThreadPoolExecutor.Changes:
backend/agent_engine.py— newiter_lfm_events()sync iterator replacesstream_lfm_events()async generator;@lru_cacheremoved fromget_lfm_workflow()(was binding LLM at compile time); new_run_hlt_loop()for thread-pool executionbackend/main.py— newPOST /api/chat/sync(plain JSON); newPOST /api/debug/llm-test(isolated hang diagnostics); SSE endpoint refactored to collect sync events then streamVerification:
backend/.venv/Scripts/python.exe -m py_compile backend/agent_engine.py backend/main.py— compile OKbackend/.venv/Scripts/python.exe -m unittest discover -s tests— RC 0 (all tests pass)Commit:
5fee07c— "fix: resolve async LLM hangs with sync ThreadPoolExecutor HITL loop"