From 2ae175f38148512da0a3e669caafc28b1c7c64f7 Mon Sep 17 00:00:00 2001 From: Csaba Kertesz Date: Sat, 27 Jun 2026 12:08:46 +0200 Subject: [PATCH] Override the default log file path by environment variable MAGIC_CONTEXT_LOG_PATH --- README.md | 2 +- packages/dashboard/README.md | 2 +- .../dashboard/src-tauri/src/log_parser.rs | 81 +++++++++++++++++++ .../src/content/docs/reference/dashboard.md | 2 +- .../magic-context/dreamer/task-executor.ts | 1 - .../magic-context/storage-schema-helpers.ts | 2 +- packages/plugin/src/shared/data-path.test.ts | 25 ++++++ packages/plugin/src/shared/data-path.ts | 2 + 8 files changed, 112 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b57508e8..7c0d6d78 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ Magic Context also writes to a few other locations: |---|---|---| | `~/.local/share/cortexkit/magic-context/context.db` | SQLite database — tags, compartments, memories, all durable state (XDG-equivalent on Windows) | **Must persist.** Losing it loses your memory/history. | | `~/.local/share/cortexkit/magic-context/models/` | Local embedding model cache (~90 MB `Xenova/all-MiniLM-L6-v2` ONNX), downloaded on first use when local embeddings are enabled | Should persist, else re-downloaded each run. Not used when `memory.enabled: false` or an `openai_compatible`/`ollama` embedding backend is configured. | -| `${TMPDIR}/opencode/magic-context/magic-context.log` (`pi/` for Pi) | Diagnostic log | Disposable. | +| `MAGIC_CONTEXT_LOG_PATH` (fallback: `${TMPDIR}/opencode/magic-context/magic-context.log`, `pi/` for Pi) | Diagnostic log | Disposable. | **Sandboxed / ephemeral environments (Docker, CI, disposable containers):** mount the `~/.local/share/cortexkit/magic-context/` directory on a persistent volume so the database and model cache survive between runs. If only the model cache is ephemeral, the model is simply re-downloaded; if the database is ephemeral, memory and history don't accumulate. To avoid the ~90 MB model download entirely, set `memory.enabled: false` or point `embedding` at a remote `openai_compatible`/`ollama` backend. diff --git a/packages/dashboard/README.md b/packages/dashboard/README.md index 9d7c576b..55c32614 100644 --- a/packages/dashboard/README.md +++ b/packages/dashboard/README.md @@ -71,7 +71,7 @@ packages/dashboard/ The dashboard reads from the same SQLite database the plugin writes to: - **Database**: `~/.local/share/cortexkit/magic-context/context.db` - **Config**: `~/.config/cortexkit/magic-context.jsonc` (user) · `/.cortexkit/magic-context.jsonc` (project) -- **Logs**: `${TMPDIR}/opencode/magic-context/magic-context.log` (`pi/` for Pi) +- **Logs**: `MAGIC_CONTEXT_LOG_PATH` (fallback: `${TMPDIR}/opencode/magic-context/magic-context.log`, `pi/` for Pi) Database access uses WAL mode for safe concurrent reads while the plugin writes. Write operations (memory edits, dream queue entries) use `busy_timeout` to handle contention. diff --git a/packages/dashboard/src-tauri/src/log_parser.rs b/packages/dashboard/src-tauri/src/log_parser.rs index 758019b1..f0f14631 100644 --- a/packages/dashboard/src-tauri/src/log_parser.rs +++ b/packages/dashboard/src-tauri/src/log_parser.rs @@ -33,6 +33,14 @@ impl Harness { /// in sync manually because the dashboard doesn't import any TypeScript /// source. pub fn resolve_log_path_for(harness: Harness) -> PathBuf { + if let Some(env_path) = std::env::var("MAGIC_CONTEXT_LOG_PATH") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + { + return PathBuf::from(env_path); + } + std::env::temp_dir() .join(harness.as_str()) .join("magic-context") @@ -436,3 +444,76 @@ pub fn read_log_tail(path: &PathBuf, max_lines: usize) -> Vec { .filter_map(|line| parse_log_line(line)) .collect() } + +#[cfg(test)] +mod tests { + use super::{resolve_log_path_for, Harness}; + use std::path::PathBuf; + use std::sync::{Mutex, OnceLock}; + + static ENV_LOCK: OnceLock> = OnceLock::new(); + + fn env_lock() -> std::sync::MutexGuard<'static, ()> { + ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap_or_else(|e| e.into_inner()) + } + + #[test] + fn resolve_log_path_for_uses_harness_fallback_when_env_unset() { + let _guard = env_lock(); + std::env::remove_var("MAGIC_CONTEXT_LOG_PATH"); + + assert_eq!( + resolve_log_path_for(Harness::Opencode), + std::env::temp_dir() + .join("opencode") + .join("magic-context") + .join("magic-context.log") + ); + assert_eq!( + resolve_log_path_for(Harness::Pi), + std::env::temp_dir() + .join("pi") + .join("magic-context") + .join("magic-context.log") + ); + } + + #[test] + fn resolve_log_path_for_honors_magic_context_log_path_override() { + let _guard = env_lock(); + let custom = std::env::temp_dir() + .join("custom") + .join("magic-context.log"); + std::env::set_var( + "MAGIC_CONTEXT_LOG_PATH", + custom.to_string_lossy().to_string(), + ); + + assert_eq!( + resolve_log_path_for(Harness::Opencode), + PathBuf::from(&custom) + ); + assert_eq!(resolve_log_path_for(Harness::Pi), PathBuf::from(&custom)); + + std::env::remove_var("MAGIC_CONTEXT_LOG_PATH"); + } + + #[test] + fn resolve_log_path_for_ignores_blank_magic_context_log_path() { + let _guard = env_lock(); + std::env::set_var("MAGIC_CONTEXT_LOG_PATH", " "); + + assert_eq!( + resolve_log_path_for(Harness::Pi), + std::env::temp_dir() + .join("pi") + .join("magic-context") + .join("magic-context.log") + ); + + std::env::remove_var("MAGIC_CONTEXT_LOG_PATH"); + } +} diff --git a/packages/docs/src/content/docs/reference/dashboard.md b/packages/docs/src/content/docs/reference/dashboard.md index a71fd248..f595e51a 100644 --- a/packages/docs/src/content/docs/reference/dashboard.md +++ b/packages/docs/src/content/docs/reference/dashboard.md @@ -148,4 +148,4 @@ Use [Configuration](/reference/configuration/) for the full generated key refere ## Logs -Optional **log tail** for `magic-context.log` with filtering, useful alongside Cache when correlating busts with plugin log lines. +Optional **log tail** for `MAGIC_CONTEXT_LOG_PATH` (fallback: `${TMPDIR}/opencode/magic-context/magic-context.log`, `pi/` for Pi) with filtering — useful alongside Cache when correlating busts with plugin log lines. Open from the sidebar **Logs** item. diff --git a/packages/plugin/src/features/magic-context/dreamer/task-executor.ts b/packages/plugin/src/features/magic-context/dreamer/task-executor.ts index e1205691..f422e092 100644 --- a/packages/plugin/src/features/magic-context/dreamer/task-executor.ts +++ b/packages/plugin/src/features/magic-context/dreamer/task-executor.ts @@ -91,7 +91,6 @@ export interface DreamTaskExecutorDeps { ) => Promise | RawMessageProvider | null; language?: string; } - /** A failed task either hot-retries (transient: provider/network/rate-limit/ * timeout/abort/lease/busy) or advances to the next cron slot (permanent: * model-not-found, validation, parse). Classify off the error shape. */ diff --git a/packages/plugin/src/features/magic-context/storage-schema-helpers.ts b/packages/plugin/src/features/magic-context/storage-schema-helpers.ts index 808e2953..23b1f159 100644 --- a/packages/plugin/src/features/magic-context/storage-schema-helpers.ts +++ b/packages/plugin/src/features/magic-context/storage-schema-helpers.ts @@ -1,4 +1,4 @@ -import { Database } from "../../shared/sqlite"; +import type { Database } from "../../shared/sqlite"; /** * Schema-mutation helpers shared by storage-db (fresh-DB init) and migrations diff --git a/packages/plugin/src/shared/data-path.test.ts b/packages/plugin/src/shared/data-path.test.ts index eadca3da..5f8c599f 100644 --- a/packages/plugin/src/shared/data-path.test.ts +++ b/packages/plugin/src/shared/data-path.test.ts @@ -7,6 +7,7 @@ import { getCacheDir, getDataDir, getLegacyOpenCodeMagicContextStorageDir, + getMagicContextLogPath, getMagicContextStorageDir, getOpenCodeCacheDir, getOpenCodeStorageDir, @@ -18,6 +19,7 @@ const savedEnv = { XDG_CACHE_HOME: process.env.XDG_CACHE_HOME, XDG_DATA_HOME: process.env.XDG_DATA_HOME, LOCALAPPDATA: process.env.LOCALAPPDATA, + MAGIC_CONTEXT_LOG_PATH: process.env.MAGIC_CONTEXT_LOG_PATH, }; describe("data-path", () => { @@ -25,10 +27,12 @@ describe("data-path", () => { process.env.XDG_CACHE_HOME = undefined; process.env.XDG_DATA_HOME = undefined; process.env.LOCALAPPDATA = undefined; + process.env.MAGIC_CONTEXT_LOG_PATH = undefined; // Bun's env handling: explicit delete for unset delete process.env.XDG_CACHE_HOME; delete process.env.XDG_DATA_HOME; delete process.env.LOCALAPPDATA; + delete process.env.MAGIC_CONTEXT_LOG_PATH; }); afterEach(() => { @@ -37,6 +41,9 @@ describe("data-path", () => { if (savedEnv.XDG_DATA_HOME !== undefined) process.env.XDG_DATA_HOME = savedEnv.XDG_DATA_HOME; if (savedEnv.LOCALAPPDATA !== undefined) process.env.LOCALAPPDATA = savedEnv.LOCALAPPDATA; + if (savedEnv.MAGIC_CONTEXT_LOG_PATH !== undefined) + process.env.MAGIC_CONTEXT_LOG_PATH = savedEnv.MAGIC_CONTEXT_LOG_PATH; + else delete process.env.MAGIC_CONTEXT_LOG_PATH; }); test("getCacheDir falls back to /.cache when XDG_CACHE_HOME is unset (all platforms)", () => { @@ -158,6 +165,24 @@ describe("data-path", () => { path.join("/some/project/", ".cortexkit", "magic-context"), ); }); + + test("getMagicContextLogPath falls back to harness temp dir when env unset", () => { + expect(getMagicContextLogPath("opencode")).toBe( + path.join(os.tmpdir(), "opencode", "magic-context", "magic-context.log"), + ); + }); + + test("getMagicContextLogPath honors MAGIC_CONTEXT_LOG_PATH", () => { + process.env.MAGIC_CONTEXT_LOG_PATH = "/tmp/custom/magic-context.log"; + expect(getMagicContextLogPath("pi")).toBe("/tmp/custom/magic-context.log"); + }); + + test("getMagicContextLogPath ignores blank MAGIC_CONTEXT_LOG_PATH", () => { + process.env.MAGIC_CONTEXT_LOG_PATH = " "; + expect(getMagicContextLogPath("pi")).toBe( + path.join(os.tmpdir(), "pi", "magic-context", "magic-context.log"), + ); + }); }); describe("ensureCortexKitArtifactGitignore", () => { diff --git a/packages/plugin/src/shared/data-path.ts b/packages/plugin/src/shared/data-path.ts index 6d46135e..e21d1b4c 100644 --- a/packages/plugin/src/shared/data-path.ts +++ b/packages/plugin/src/shared/data-path.ts @@ -46,6 +46,8 @@ export function getMagicContextTempDir(harness: HarnessId = getHarness()): strin * reflected in the next flush. */ export function getMagicContextLogPath(harness: HarnessId = getHarness()): string { + const envPath = process.env.MAGIC_CONTEXT_LOG_PATH?.trim(); + if (envPath) return envPath; return path.join(getMagicContextTempDir(harness), "magic-context.log"); }