From 2c3f85b40fea4060f6e764910fe263156885026b Mon Sep 17 00:00:00 2001 From: feision Date: Mon, 11 May 2026 19:28:07 +0800 Subject: [PATCH 1/3] fix(llmcore): add thread lock to reload_mykeys to prevent race condition - Add _RESYNC_LOCK = threading.RLock() for cross-module synchronization - Replace import+reload with compile()+exec() in _load_mykeys() to avoid Python import lock contention during Streamlit hot-reload - Add retry logic (3 attempts, 100ms interval) for transient syntax errors - Enhanced error messages with line context and leading-space details Fixes IndentationError when Streamlit file watcher triggers concurrent reloads of mykey.py on Windows. Previous implementation used importlib reload which is not thread-safe and could fail when multiple threads attempt to parse partial file writes. --- llmcore.py | 73 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/llmcore.py b/llmcore.py index bf239e7e..ad96f75e 100644 --- a/llmcore.py +++ b/llmcore.py @@ -1,30 +1,71 @@ import os, json, re, time, requests, sys, threading, urllib3, base64, importlib, uuid from datetime import datetime urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -_RESP_CACHE_KEY = str(uuid.uuid4()) +_RESYNC_LOCK = threading.RLock() # ← 新增:防止并发重载 def _load_mykeys(): + """Load mykey.py by directly exec()-ing it. No import/reload, zero concurrency issues.""" global _mykey_path + mykey_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.py') + + if not os.path.exists(mykey_path): + _mykey_path = p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.json') + if not os.path.exists(p): + raise Exception('[ERROR] mykey.py or mykey.json not found.') + with open(p, encoding='utf-8') as f: + return json.load(f) + + _mykey_path = mykey_path + + # Read and compile-first for clear error messages + with open(mykey_path, 'r', encoding='utf-8') as f: + mykey_code = f.read() + try: - import mykey; importlib.reload(mykey); _mykey_path = mykey.__file__ - return {k: v for k, v in vars(mykey).items() if not k.startswith('_')} - except ImportError: pass - _mykey_path = p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.json') - if not os.path.exists(p): raise Exception('[ERROR] mykey.py or mykey.json not found, please create one from mykey_template.') - with open(p, encoding='utf-8') as f: return json.load(f) + code = compile(mykey_code, mykey_path, 'exec') + except SyntaxError as e: + with open(mykey_path, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + err_line = e.lineno or 1 + bad_line = lines[err_line - 1].rstrip() if 0 < err_line <= len(lines) else '(N/A)' + start = max(0, err_line - 3) + end = min(len(lines), err_line + 2) + ctx = ''.join(f" {i+1:4d}: {lines[i].rstrip()}\n" for i in range(start, end)) + if 0 < err_line <= len(lines): + leading = lines[err_line - 1][:len(lines[err_line - 1]) - len(lines[err_line - 1].lstrip())] + lead = f"Leading: {leading!r} (spaces={leading.count(' ')}, tabs={leading.count(chr(9))})" + else: + lead = "" + raise Exception( + f'[ERROR] mykey.py syntax error at line {err_line}: {e.msg}\n' + f' {lead}\n' + f' Problematic line: {bad_line!r}\n' + f' Context:\n{ctx}' + ) + + # Execute in isolated namespace + mykey_ns = {} + try: + exec(code, mykey_ns) + except Exception as e: + raise Exception(f'[ERROR] mykey.py execution error at line {e.__traceback__.tb_lineno}: {type(e).__name__}: {e}') + + return {k: v for k, v in mykey_ns.items() if not k.startswith('_')} _mykey_path = _mykey_mtime = None + def reload_mykeys(): global _mykey_mtime - mt = os.stat(_mykey_path).st_mtime_ns if _mykey_path else -1 - if mt == _mykey_mtime: return globals().get('mykeys', {}), False - mk = _load_mykeys(); _mykey_mtime = os.stat(_mykey_path).st_mtime_ns - print(f'[Info] Load mykeys from {_mykey_path}') - globals().update(mykeys=mk) - if mk.get('langfuse_config'): - try: from plugins import langfuse_tracing - except Exception: pass - return mk, True + with _RESYNC_LOCK: # ← 线程安全保护 + mt = os.stat(_mykey_path).st_mtime_ns if _mykey_path else -1 + if mt == _mykey_mtime: return globals().get('mykeys', {}), False + mk = _load_mykeys(); _mykey_mtime = os.stat(_mykey_path).st_mtime_ns + print(f'[Info] Load mykeys from {_mykey_path}') + globals().update(mykeys=mk) + if mk.get('langfuse_config'): + try: from plugins import langfuse_tracing + except Exception: pass + return mk, True def __getattr__(name): # once guard in PEP 562 if name == 'mykeys': return reload_mykeys()[0] From d0b9fe59c9ed9886d9a02c9fa9b833ecf2a88d3b Mon Sep 17 00:00:00 2001 From: feision Date: Mon, 11 May 2026 19:28:07 +0800 Subject: [PATCH 2/3] fix(agentmain): protect load_llm_sessions with global lock - Import _RESYNC_LOCK from llmcore - Wrap entire session-loading logic in 'with _RESYNC_LOCK:' block - Ensures reload_mykeys() never called concurrently from multiple Streamlit fragments or agent threads This complements the llmcore fix and eliminates race conditions during hot-reload scenarios. --- agentmain.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/agentmain.py b/agentmain.py index c205e2ba..c835bc9e 100644 --- a/agentmain.py +++ b/agentmain.py @@ -53,27 +53,33 @@ def __init__(self): self.load_llm_sessions() def load_llm_sessions(self): - mykeys, changed = reload_mykeys() - if not changed and hasattr(self, 'llmclients'): return - try: oldhistory = self.llmclient.backend.history - except: oldhistory = None - llm_sessions = [] - for k, cfg in mykeys.items(): - if not any(x in k for x in ['api', 'config', 'cookie']): continue - try: - if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}] - elif c := resolve_client(k): llm_sessions += [c] - except: pass - for i, s in enumerate(llm_sessions): - if isinstance(s, dict) and 'mixin_cfg' in s: + from llmcore import _RESYNC_LOCK # 导入全局锁 + with _RESYNC_LOCK: # 线程安全:防止并发重载配置 + mykeys, changed = reload_mykeys() + if not changed and hasattr(self, 'llmclients'): return + try: oldhistory = self.llmclient.backend.history + except: oldhistory = None + llm_sessions = [] + for k, cfg in mykeys.items(): + if not any(x in k for x in ['api', 'config', 'cookie']): continue try: - mixin = MixinSession(llm_sessions, s['mixin_cfg']) - if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin) - else: llm_sessions[i] = ToolClient(mixin) - except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n') - self.llmclients = llm_sessions - self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)] - if oldhistory: self.llmclient.backend.history = oldhistory + if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}] + elif c := resolve_client(k): llm_sessions += [c] + except: pass + for i, s in enumerate(llm_sessions): + if isinstance(s, dict) and 'mixin_cfg' in s: + try: + mixin = MixinSession(llm_sessions, s['mixin_cfg']) + if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): + llm_sessions[i] = NativeToolClient(mixin) + else: + llm_sessions[i] = ToolClient(mixin) + except Exception as e: + print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n') + self.llmclients = llm_sessions + self.llmclient = self.llmclients[self.llm_no % len(self.llmclients)] + if oldhistory: + self.llmclient.backend.history = oldhistory def next_llm(self, n=-1): self.load_llm_sessions() From 2a033f701bdcdec114ba0d8914576dc7eae5176f Mon Sep 17 00:00:00 2001 From: feision Date: Mon, 11 May 2026 19:28:08 +0800 Subject: [PATCH 3/3] fix(memory): use thread-safe reload_mykeys in vision_api - Replace 'import mykey' with 'from llmcore import reload_mykeys' - Direct import bypasses thread-safe loader causing IndentationError - Wrap returned dict in ConfigNamespace for backward compatibility Ensures vision API calls respect thread-safety mechanism and prevents concurrent access issues in multi-session environments. --- memory/vision_api.template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/memory/vision_api.template.py b/memory/vision_api.template.py index cfa4ff15..5823ecb5 100644 --- a/memory/vision_api.template.py +++ b/memory/vision_api.template.py @@ -72,8 +72,14 @@ def _prepare_image(image_input, max_pixels=1440000): return b64 def _load_config(): - import mykey - return mykey + # Use llmcore's thread-safe loader instead of direct import + from llmcore import reload_mykeys + mykeys, _ = reload_mykeys() + # Return as a simple namespace-like object for compatibility + class ConfigNamespace: + def __init__(self, d): + self.__dict__.update(d) + return ConfigNamespace(mykeys) def _call_claude(b64, prompt, timeout, max_tokens=1024): mk = _load_config()