Skip to content

stop_status reports completed journals over 64 KiB as unreadable, and the recommended transaction recover cannot clear it #164

Description

@jungsek

Summary

stop_status() reports healthy, completed transaction journals as "unreadable" whenever the journal exceeds 64 KiB. The journals are valid JSON with "state": "complete" — they are simply larger than the probe's read cap, so the truncated slice fails to parse.

The warning cannot be cleared by the command it recommends, and the count grows permanently as a vault accumulates large transactions.

Root cause

claude_obsidian/hook_adapter.py:270 reads a bounded prefix and immediately parses it as complete JSON:

payload = _bounded_regular_bytes(root, journal, 64 * 1024)
...
state = strict_json_loads(payload.decode("utf-8")).get("state")
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
    unreadable_journals += 1

_bounded_regular_bytes returns handle.read(limit + 1) (line 98), so any journal over 64 KiB yields a truncated 65537-byte slice, strict_json_loads raises JSONDecodeError, and the file is counted as unreadable.

The probe only needs the state key, but it pays the cost of parsing the entire document.

Why 64 KiB is too small

The same package explicitly permits transactions far larger than this cap allows.

claude_obsidian/transaction.py:130 sets MAX_TRANSACTION_WRITES = 1024, enforced at lines 3061, 3071, and 3533. Observed journal size scales at roughly 350–450 bytes per write entry, so a transaction at the sanctioned ceiling produces a journal in the region of 400 KB — about 6× the probe's limit.

In practice the probe fails at roughly 150 writes, or ~15% of the documented maximum. Measured from a real vault:

journal bytes writes readable by probe
124,820 278 no
111,548 275 no
50,659 145 yes (barely)
25,076 63 yes

Batched operations are documented usage — skills/wiki-ingest/SKILL.md describes ingest as "a single source or bounded batch" and lists "batch ingest" among its triggers — so ordinary use reaches this size.

Worth noting that every other bound in hook_adapter.py is a named module constant (MAX_CONTEXT_BYTES, MAX_STATUS_BYTES, MAX_STATUS_ITEMS, MAX_TRANSACTION_SCAN), while this one is an inline 64 * 1024 at the call site — it reads like a generic small-file bound applied to a file the package guarantees can be much larger.

Reproduction

  1. Apply any transaction with more than ~150 writes (or hand-write a journal over 64 KiB with "state": "complete" and a matching valid changed-paths.json).
  2. Trigger the Stop hook, or call the probe directly:
from pathlib import Path
from claude_obsidian.hook_adapter import stop_status
print(stop_status(start=Path("/path/to/vault")))
# CLAUDE_OBSIDIAN_STATUS: 1 unreadable transaction journal(s) detected.
# Run `claude-obsidian transaction recover` before the next mutation.
  1. Confirm the journal is actually fine:
import json
raw = open(journal_path, "rb").read()
print(len(raw), json.loads(raw)["state"])   # 124820 complete
json.loads(raw[:65537].decode("utf-8"))     # JSONDecodeError — what the probe sees
  1. claude-obsidian transaction recover --vault <vault> returns {"recovered": [], ...} and the warning persists.

Second issue: the recommended remedy cannot apply

The message directs the user to run claude-obsidian transaction recover. That command cannot resolve this state.

_recover_incomplete_locked (transaction.py:3958) acts only on journals whose state is in {"prepared", "applying", "rollback-failed"}. A journal that is already complete with a valid changed-paths.json hits the continue at line 3956 and is never touched. Since these journals are complete — and only unreadable because of the size cap — the prescribed command is a guaranteed no-op.

This is arguably the more damaging half of the bug. The warning is persistent, self-diagnosing in a way that points somewhere wrong, and the suggested fix silently succeeds while changing nothing. In our case two separate sessions concluded the vault was corrupt and invented an out-of-tree transactions-archive/ directory to hide the oversized journals from os.scandir — a workaround that only worked because the scan does not recurse. Nothing prunes transaction directories, so the count only ever grows.

Suggested fix

Any of these would resolve it:

  • Raise the limit to match what transactions may legitimately produce — MAX_TRANSACTION_RUNTIME_JSON_BYTES (8 MiB, transaction.py:132) is already the package's bound for runtime JSON of this kind, and would be a consistent choice.
  • Extract state without parsing the whole document, so journal size stops mattering to a probe that wants one key.
  • Distinguish "too large to probe" from "corrupt" in the warning text, and drop the transaction recover suggestion for states that command cannot address.

The last point matters independently of the size cap: whatever the limit becomes, a warning should not recommend a command that provably cannot clear it.

Environment

  • claude-obsidian 2.0.0
  • Python 3.14.6
  • macOS (Darwin 25.5.0), arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions