From 376ec034286b9f120b8fdc95a190e3adb9e31a9c Mon Sep 17 00:00:00 2001 From: Nic O'Connor Date: Mon, 14 Sep 2026 01:03:22 -0400 Subject: [PATCH] claude-obsidian: unattended overnight runs -- lock, URL fallback, config fix Needed to leave a full backlog drain running unattended: - Run lock (flock): enabling the timer fired an immediate catch-up run (Persistent=true) while a manual run was about to start; without a lock the two would race on state.json and usage.jsonl. - OBSIDIAN_API_URL accepts a comma-separated list, first reachable wins, so the permanent address and an interim port-forward share one config. - Fix: inline "# comments" in config.env were parsed into values, so BUDGET_USD_PER_RUN=0.50 # ... crashed float(). The shipped config.env.example had exactly that shape on several lines -- anyone copying it would have hit the crash. A '#' inside a value (URL fragment) still survives. Tested against both the example and a real config. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KF322NzozeaFjpgWvh4F4g --- claude-obsidian/README.md | 4 ++ .../claude-obsidian-synccpython-312.pyc | Bin 61341 -> 63148 bytes claude-obsidian/claude-obsidian-sync | 41 +++++++++++++++--- claude-obsidian/config.env.example | 7 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/claude-obsidian/README.md b/claude-obsidian/README.md index 877937d..132500e 100644 --- a/claude-obsidian/README.md +++ b/claude-obsidian/README.md @@ -80,6 +80,10 @@ claude mcp add --scope user --transport http obsidian http://:27123/mcp/ \ keys, bearer tokens, `password=`-style assignments, JWTs, provider tokens, ≥48-char hex, and random mixed-case strings. It over-redacts on purpose; git SHAs, md5s and paths survive. The summarizer is also told never to reproduce secrets. +- **One run at a time** (a `flock` on `run.lock`): an hourly timer firing during a long manual run + exits immediately instead of racing it on the state and ledger files. +- **Several vault URLs** can be listed; the first that answers wins, so one config covers both a + permanent address and a temporary `kubectl port-forward`. - **Incremental and idempotent.** State (`~/.local/state/claude-obsidian/state.json`) records each session's size+mtime. A resumed session is re-summarized into the *same* note path. A session is left alone until idle for `SETTLE_MINUTES`, so a live conversation isn't diff --git a/claude-obsidian/__pycache__/claude-obsidian-synccpython-312.pyc b/claude-obsidian/__pycache__/claude-obsidian-synccpython-312.pyc index d91fad68668cb49a985b326c0f151c4771927750..f3a7dcf73bf8bcf6f9332ea8724bcb14ba11875f 100644 GIT binary patch delta 12503 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 YZMuuh`gSczA!X#-Le~COnuyZ>2OuUyB>(^b delta 11438 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

yK zeIG;{(AVc01~69(nVK&%q9SgT!d)M(4kIvSh78>I9HfRYhAloVB|Xr15&ik#P$-8-J~&kUYbyN62veZOCCKn(go6V`u^~gYd8X7P|2rgsrx; zeP#OsgNY~q_8TGka>9r3X*&fjaMPeLh(i?_1_|LB3ZA?4%m4BN3^w=l;i zTgeL6|H%}xo;~-;OvhU6pA*M+E2cIu-KV`x#aI?yM(!|kV4>Ip>O;9;3joO)unY!x zBi3fv77Y2HQkMs=W!s_RKg7hTPq!1kTB^zI#tgR`dRTx0@{ype89J-b7~4#mZdnXl}d)CE4)wGvIHnT#l!p!##jn+<122 zKsmP%M?2(@b6L+}M|oR30_Hr79yH!aOSX|qCT)s%$rA+jpH|d5EC{1!>K|^BcV9cP z(YntgZ*G+MhT(@|yrClO9Nwfoe4!!EEehYnJm%vm9{6#|V^BQjRbp$0kqpbA{9JA^ zEd#D(8uE46GzH4Y`H4oXa?A7SASk-UuL0VHVU zHFaE%sp;74S|pj+pEK;q&u7P9z?^Czs%Z5z82l-rQ1Nxe{%*n07hXv-#R$>##n3)9({uh-cokhvFcCRt=5xj2>W{W@1d G(f<$qRty{f diff --git a/claude-obsidian/claude-obsidian-sync b/claude-obsidian/claude-obsidian-sync index 7352e2d..d1dcf03 100755 --- a/claude-obsidian/claude-obsidian-sync +++ b/claude-obsidian/claude-obsidian-sync @@ -18,6 +18,7 @@ from __future__ import annotations import argparse import concurrent.futures as cf import datetime as dt +import fcntl import hashlib import json import os @@ -46,6 +47,7 @@ def load_config() -> dict: line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) + v = re.split(r"\s+#", v, maxsplit=1)[0] # inline comment; a '#' inside a value survives cfg[k.strip()] = v.strip().strip('"').strip("'") cfg.update({k: v for k, v in os.environ.items() if k in DEFAULTS or k.startswith("LLM_") or k.startswith("OBSIDIAN_")}) for k, v in DEFAULTS.items(): @@ -474,7 +476,10 @@ class LLM: # --------------------------------------------------------------------------- vault class Vault: def __init__(self, cfg: dict): - self.base = cfg["OBSIDIAN_API_URL"].rstrip("/") + # OBSIDIAN_API_URL may list several URLs, comma-separated, tried in order at ping() -- e.g. the + # permanent address first and a port-forward second, so one config works either way. + self.candidates = [u.strip().rstrip("/") for u in cfg["OBSIDIAN_API_URL"].split(",") if u.strip()] + self.base = self.candidates[0] self.key = secret(cfg, "OBSIDIAN_API_KEY_FILE") def put(self, path: str, content: str) -> None: @@ -488,12 +493,19 @@ class Vault: def ping(self) -> None: """Checked BEFORE any summarizing, so an unreachable vault never spends usage limits.""" - req = urllib.request.Request(f"{self.base}/", headers={"Authorization": f"Bearer {self.key}"}) - try: - with urllib.request.urlopen(req, timeout=15) as r: - info = json.load(r) - except (urllib.error.URLError, TimeoutError, OSError) as e: - raise SystemExit(f"Obsidian REST API unreachable at {self.base}: {getattr(e, 'reason', e)}") from None + errors = [] + for base in self.candidates: + req = urllib.request.Request(f"{base}/", headers={"Authorization": f"Bearer {self.key}"}) + try: + with urllib.request.urlopen(req, timeout=10) as r: + info = json.load(r) + self.base = base + break + except (urllib.error.URLError, TimeoutError, OSError, ValueError) as e: + errors.append(f"{base}: {getattr(e, 'reason', e)}") + else: + raise SystemExit("Obsidian REST API unreachable -- " + "; ".join(errors)) + print(f"vault: {self.base}", flush=True) if not info.get("authenticated"): raise SystemExit("Obsidian REST API reachable but the API key was rejected") @@ -675,7 +687,21 @@ def cmd_stats(cfg, args) -> None: f"{sum(1 for r in rows if r[0] > CHUNK_CHARS)} need map-reduce") +def run_lock(cfg: dict): + """One run at a time: a timer run overlapping a manual one would race on state and ledger files.""" + path = pathlib.Path(os.path.expanduser(cfg["STATE_FILE"])).parent / "run.lock" + path.parent.mkdir(parents=True, exist_ok=True) + fh = path.open("w") + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + print("another claude-obsidian-sync run is in progress -- exiting") + sys.exit(0) + return fh # held until the process exits + + def cmd_sync(cfg, args) -> None: + _lock = run_lock(cfg) state = State(cfg["STATE_FILE"]) settle = dt.timedelta(minutes=int(cfg["SETTLE_MINUTES"])) now = time.time() @@ -719,6 +745,7 @@ def cmd_sync(cfg, args) -> None: def cmd_import(cfg, args) -> None: + _lock = run_lock(cfg) state = State(cfg["STATE_FILE"]) convs = parse_claude_ai_export(pathlib.Path(args.export)) work = [] diff --git a/claude-obsidian/config.env.example b/claude-obsidian/config.env.example index e7dcce5..42c1116 100644 --- a/claude-obsidian/config.env.example +++ b/claude-obsidian/config.env.example @@ -3,12 +3,15 @@ # Obsidian Local REST API plugin -- the plain-HTTP server (Settings -> Local REST API -> # Enable HTTP server). Point this at wherever that port is reachable from this machine. -OBSIDIAN_API_URL=http://obsidian.example.internal:27123 +# Several URLs, comma-separated, are tried in order -- e.g. a permanent address, then a port-forward. +OBSIDIAN_API_URL=http://obsidian.example.internal:27123,http://127.0.0.1:27123 OBSIDIAN_API_KEY_FILE=~/.config/claude-obsidian/api-key # Summarizer. claude-cli = headless `claude -p` on your own Claude plan (no API key needed). LLM_BACKEND=claude-cli -LLM_MODEL=haiku +LLM_MODEL=haiku # sonnet writes noticeably sharper notes for ~1-3x the usage; set prices to match below +# LLM_PRICE_IN_PER_M=1.0 # used only for the pre-call budget estimate (haiku list; sonnet 3.0 / 15.0) +# LLM_PRICE_OUT_PER_M=5.0 # CLAUDE_BIN=claude # Or any OpenAI-compatible endpoint (LiteLLM, a local model):