diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index b8c2d03da3806..bdb88f0611137 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -1926,6 +1926,23 @@ function resolveGitBinary() { if (localAppData) { candidates.push(path.join(localAppData, 'Programs', 'Git', 'cmd', 'git.exe')) + + // UGit ships a portable Git under %LOCALAPPDATA%\UGit\app-*\resources\app\git\cmd\git.exe. + // The versioned app-* subdirectory changes with each UGit update, so enumerate + // all app-* directories dynamically instead of hard-coding a version (#61494). + const ugitDir = path.join(localAppData, 'UGit') + try { + const entries = fs.readdirSync(ugitDir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('app-')) { + candidates.push( + path.join(ugitDir, entry.name, 'resources', 'app', 'git', 'cmd', 'git.exe') + ) + } + } + } catch { + // UGit not installed or inaccessible — skip gracefully + } } _gitBinaryCache = candidates.find(fileExists) || findOnPath('git') || 'git' diff --git a/tools/cronjob_tools.py b/tools/cronjob_tools.py index 430cd1dda4014..ece887038ff1a 100644 --- a/tools/cronjob_tools.py +++ b/tools/cronjob_tools.py @@ -615,6 +615,15 @@ def _execute_job_now(job: Dict[str, Any]) -> Dict[str, Any]: failure delivery, ``[SILENT]`` handling, and live-adapter delivery stay identical across paths and can't drift. + When a live gateway is running, this function retrieves the gateway's + adapters and event loop and passes them to ``run_one_job`` so that job + delivery goes through the live-adapter path (``safe_schedule_threadsafe`` + on the gateway loop) rather than creating a new event loop via + ``asyncio.run()``. The standalone path would send adapter HTTP calls + from a different loop than the adapter's internal aiohttp session was + created on, causing "Timeout context manager should be used inside a task" + failures for platforms such as Matrix (#61495). + Returns {"claimed": bool, "success": bool, "error": str|None}. """ job_id = job["id"] @@ -636,9 +645,24 @@ def _execute_job_now(job: Dict[str, Any]) -> Dict[str, Any]: reason = "Job is already being fired by the scheduler; not run again." return {"claimed": False, "success": False, "error": reason} + # Resolve the gateway's adapters and event loop so delivery uses the + # live-adapter path (safe_schedule_threadsafe on the gateway loop). + # Without these, _deliver_result falls back to asyncio.run() in a new + # event loop, causing cross-loop adapter usage failures (#61495). + _adapters = None + _loop = None + try: + from gateway.run import _gateway_runner_ref + _runner = _gateway_runner_ref() + if _runner is not None: + _adapters = getattr(_runner, "adapters", None) + _loop = getattr(_runner, "_gateway_loop", None) + except Exception: + pass + # run_one_job records last_run_at/last_status via mark_job_run (which # also clears the fire claim) and returns True iff it processed the job. - processed = run_one_job(job) + processed = run_one_job(job, adapters=_adapters, loop=_loop) refreshed = get_job(job_id) or {} ok = refreshed.get("last_status") == "ok" return { diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index c143ea8064f16..1c6cdd81d656d 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -1695,9 +1695,24 @@ async def _send_matrix_via_adapter(pconfig, chat_id, message, media_files=None, # disconnect it. Correctness here depends on this branch returning # before the ephemeral ``adapter`` is constructed below, so the # ephemeral ``finally`` disconnect never touches the live session. - return await _matrix_send_core( - live_adapter, chat_id, message, media_files, metadata - ) + try: + return await _matrix_send_core( + live_adapter, chat_id, message, media_files, metadata + ) + except RuntimeError as _matrix_loop_err: + # When the cron standalone path (asyncio.run() in a new event + # loop) uses the live adapter, the adapter's internal aiohttp + # session was created on the gateway's loop and cannot be used + # from a different loop. Fall through to the ephemeral adapter + # rather than failing delivery (#61495). The primary fix is in + # _execute_job_now (which passes the gateway loop so delivery + # uses safe_schedule_threadsafe), but this cross-loop guard + # catches any remaining path. + logger.warning( + "Matrix: live adapter unusable from this event loop " + "(%s: %s); falling back to ephemeral adapter", + type(_matrix_loop_err).__name__, _matrix_loop_err, + ) # --- Fallback: ephemeral adapter (standalone / cron context) --- try: