When all LLM nodes timeout/fail, the conductor's except block only
printed to stdout — users saw their message disappear with no feedback.
Now broadcasts a role="error" chat item via the existing WebSocket
pipeline so the frontend renders a red error bubble inline.
Every message called _persist(), which rewrote the ENTIRE
desktop_sessions.json (all sessions, full messages + llm_history) under
the lock. Write cost was O(all sessions), so performance degraded badly
as sessions piled up.
Now each session is its own temp/desktop_sessions/<id>.json:
- _persist_session(s): atomic per-session write, O(one session)
- _delete_session_file(sid): remove on delete
- add_message / create / plan-bind / set_model / patch now write only
the touched session; write cost no longer scales with session count
- _load_sessions reads the dir; one-time migration from the legacy
monolithic file, which is then renamed to .json.migrated
- import_sessions reads both source formats (per-file dir + legacy),
persists only newly added sessions
Adds a "GA source directory" setting so the packaged desktop app can run
against a standalone GenericAgent checkout instead of its own bundled
runtime/app — memory, sessions, model_responses and the agent code all
come from that 本体, live-shared with other frontends.
- lib.rs: ga_source_override settings key honored in get_or_discover_config
BEFORE the bundle default; validated (agentmain.py + desktop_bridge.py),
invalid/missing override falls back to the bundle (本体-moved safety net)
- set_ga_source / clear_ga_source / get_ga_source commands: write setting,
stop current bridge, respawn from resolved config, reload webview
- startup uses the override-aware config for takeover + spawn
- ACL grants for the three new commands
- Settings > Features UI: pick source dir, show current, clear; zh/en i18n
loadSessions() only refreshes the data model; the sidebar DOM is drawn by
renderSessionList(). Imported sessions therefore only appeared after the
next render (e.g. sending a message). Call renderSessionList() right after.
Import now also merges the source's temp/desktop_sessions.json into the
current session list (dedup by id), updating both memory and the on-disk
file so past desktop chats reappear in the sidebar immediately.
- manager.import_sessions(): id-dedup merge, in-memory + persist
- import handler returns sessionsAdded/sessionsSkipped
- frontend shows session count in toast and reloads the sidebar
- button label now says "select GenericAgent root"
- pick_directory accepts a title; native dialog shows the hint
- browser-fallback prompt clarifies root dir vs memory folder
The import-memory button failed with "Command pick_directory not allowed
by ACL" because Tauri v2 requires custom commands to be granted in the
capability. Add allow-pick-directory permission and grant it.
Lets users import memory/ and temp/model_responses/ from an existing
GenericAgent install into the desktop app's ga_root, so past sessions
and memory are not lost when running the standalone desktop bundle.
- bridge: POST /memory/import copies memory (backup-then-overwrite) and
model_responses (fill-missing), returns counts + backup dir
- tauri: pick_directory command (rfd native folder picker)
- frontend: "Import memory" button in Settings > Features + zh/en i18n
Co-locate desktop packaging scripts with the Tauri app, declutter repo
root. Drop legacy manual-install flow (install.bat/start_windows.bat/
README_WINDOWS.md) and unused apps/ placeholder tree. Update CI workflow
paths and TODO references accordingly.
_apply_desktop_model is called before every conductor task to make the agent
reflect the current desktop config. But the mykey reload only happened as a
side effect of next_llm(), which was gated on a bound model index (no != None).
On a fresh install nothing is selected (conductor.llmNo and ui.llmNo both null),
so no == None, next_llm was never called, and a conductor that started on the
empty template never picked up imported keys — the next message crashed on the
stale empty mixin ('dict' object has no attribute 'backend').
Add the no-bound-model branch: call agent.load_llm_sessions() (mtime-guarded,
rebuilds only when mykey actually changed) so a live conductor reflects imported
keys on its next task without any restart or task interruption.
conductor and scheduler are separate subprocesses that read mykey once at
startup. After a mykey rewrite (import keys / edit channel config), a healthy
running conductor already hot-reloads mykey on its next task via its own mtime
check, so it needs no restart — restarting it would only interrupt an in-flight
task.
So restart an extra only when it is genuinely broken: the process exited
unexpectedly (covers scheduler dying on a bad config), or the process is alive
but its "Exception in thread conductor-agent" crash signature is in the captured
log (uvicorn still up but the agent thread is dead and can no longer process
tasks). A fresh log buffer per start means the signature only reflects the
current process. Bridge-only; conductor core and agentmain untouched.
loadModelProfiles only updated the main model chip, never
state.conductorModelName. The mykey-import path (and add/remove-mixin,
reorder, add/delete model) call loadModelProfiles without
loadBridgeConfig, so after importing keys the conductor chip stayed on
its stale label (e.g. the empty "渠道组(自动故障转移)") until a manual
model switch recomputed it. Recompute conductorModelName from the fresh
profiles when a conductor model is bound.
Remove the implicit model-write that rode along with each prompt. The
frontend no longer sends llmNo on session/prompt, and the bridge no longer
reads/forwards it there — sending a message now uses only the session's
bound sess.llm_no (falling back to the global default). Model switching
goes through the single POST /session/{sid}/model entry point.
selectModel now rolls back the chip + shows a toast when the switch request
fails, so the frontend display never diverges from the backend.
Session.llm_no is now the authoritative per-session model selection
(persisted to desktop_sessions.json). Frontend state.llmNo becomes a
display cache; switching a session re-displays its bound model, and the
chip follows the backend's live model for native models too (not just
mixin failover).
- bridge: Session.llm_no field + persistence; submit_prompt/run_agent_turn
read sess.llm_no with global ui.llmNo as fallback; make_agent/restore
seed from it; new POST /session/{sid}/model; _live_model reports llmNo
- app.js: applyLiveModel supports native; setActiveSession re-displays
bound model; selectModel posts to session route + updates global default;
remove ga_llm_no localStorage
- conductor unchanged: keeps reading global default ui.llmNo
The prior merge resolved conflicts to the desktop/main (graft) side,
which silently reverted lsdefine's recent upstream fix (inMixin check,
ask_user schema, turn-25 prompt). Restore the 4 files to origin/main.
Selected item in the model dropdown now uses only a tinted background + bold
(both light and dark); the inset left bar is removed per design preference.
- Send button: light mode now black (was green), dark mode bright blue
(#2559d8 / hover #3a6ef0).
- Model dropdown selected item is now obvious: light mode gets an accent
left bar + tinted bg + bold; dark mode gets a tinted bg + soft-white left
bar + bold.
- Main window now opens maximized (maximized: true); 1280x800 stays as the
restore size. Previously it always opened windowed.
- Uninstaller now also removes %LOCALAPPDATA%\com.genericagent.app (WebView2
EBWebView cache + localStorage), which lives outside the bundle. Only the
Tauri desktop shell uses it, so removal is safe.
Back out the no_cache_middleware added in f3a85d1. Fresh installs have no
cache so it is not needed for first run; cache during testing is cleared
manually. The uninstaller arg-bug fix from the same commit is kept.
Bug 1 (uninstall.bat could not delete the folder): %~dp0 ends with a
backslash, so -BundleDir "%~dp0" passed "...\dir\" — the trailing \" escaped
the closing quote and PowerShell received a path with a literal quote
("Illegal characters in path"). Resolve-Path failed, the deleter ran rd on a
garbage path, and nothing was removed. Strip the trailing backslash in the
.bat before passing, and defensively trim stray quotes/separators in the ps1.
Bug 2 root cause (shortcut prompt / UI showed stale code after updates):
the bridge served app.js via aiohttp's static handler with only
ETag/Last-Modified and no Cache-Control, so WebView2 heuristically cached it
and kept serving an old app.js after rebuilds. Add a no_cache_middleware
that sets Cache-Control: no-cache, no-store, must-revalidate on GET/HEAD.
Bug 2 root cause was WebView2 serving a stale cached app.js (no
Cache-Control header on the bridge static response). With a fresh
extract / cleared cache the first-run prompt works. Revert the diagnostic
instrumentation back to the clean maybeAskDesktopShortcut.
Not a fix — instruments maybeAskDesktopShortcut with an on-page banner
reporting __TAURI__ availability, shortcut_should_ask result, and whether
window.confirm fires/returns, so we can locate why the first-run prompt
never shows. To be reverted once the cause is confirmed.
Bug 1 (shortcut prompt only showed on the second launch): a native rfd
dialog spawned from the Rust startup thread has no parent window and got
buried behind the main window on first launch. Move the prompt to the web
UI: add shortcut_should_ask / shortcut_decide commands; the frontend asks
once via window.confirm after load (always on top). The startup thread now
only self-heals the shortcut path when already enabled.
Bug 2 (uninstall.bat could not delete the folder): kill ANY process whose
exe lives inside the bundle (not just GenericAgent/python by name), and run
the folder delete from a detached temp .bat that retries the recursive
remove ~20x to cover the window where uninstall.bat releases its handle.
Windows portable bundle now offers to create a desktop shortcut on first
launch via a native yes/no dialog, remembers the choice in
~/.ga_desktop_settings.json (desktop_shortcut), and self-heals the .lnk
target to current_exe() on every enabled launch so a relocated bundle
keeps working.
Also add read_settings/merge_settings and route the three existing
settings writers through merge, so sibling keys (like desktop_shortcut)
are no longer clobbered by the python_path/project_dir rewrite.
Add settings "Features" section with mykey import and service manager entry.
Split spawnBridge (panel) from test-style startBridge boot; fix bridge-exit
UI race; restore takeover_stale_bridge on launch; allow Tauri IPC for
start_bridge from remote bridge UI.