-
Notifications
You must be signed in to change notification settings - Fork 1
feat(store): journaled multi-note transactions and omind recover (v8.0.0)
#213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
28dbc9c
5cd386a
8203d71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -66,7 +66,7 @@ def build_parser() -> argparse.ArgumentParser: | |||||||||||||||
| parser.add_argument("--version", action="version", version=f"omind {__version__}") | ||||||||||||||||
| sub = parser.add_subparsers( | ||||||||||||||||
| dest="command", | ||||||||||||||||
| metavar="{help,setup,quickstart,serve,doctor,self-update,backup,ai,export,import,reindex,note,rollup,hook}", | ||||||||||||||||
| metavar="{help,setup,quickstart,serve,doctor,self-update,backup,ai,export,import,reindex,note,rollup,recover,hook}", | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| help_p = sub.add_parser( | ||||||||||||||||
|
|
@@ -477,6 +477,27 @@ def build_parser() -> argparse.ArgumentParser: | |||||||||||||||
| for gp in (g_neighbors, g_path, g_orphans, g_dangling, g_stats, g_frontier, g_export): | ||||||||||||||||
| _add_vault_args(gp) | ||||||||||||||||
|
|
||||||||||||||||
| recover = sub.add_parser( | ||||||||||||||||
| "recover", | ||||||||||||||||
| help="roll back a multi-note write that was interrupted mid-apply", | ||||||||||||||||
| description=( | ||||||||||||||||
| "Roll back any journaled multi-note transaction that did not reach its\n" | ||||||||||||||||
| "commit record — an `omind consolidate --apply` killed mid-write, a\n" | ||||||||||||||||
| "power loss, an OOM. A no-op when the journal is clean, which is the\n" | ||||||||||||||||
| "normal case.\n" | ||||||||||||||||
| "\n" | ||||||||||||||||
| "A note whose bytes match neither the pre-image nor what the interrupted\n" | ||||||||||||||||
| "run intended to write was edited after the crash, and is reported as a\n" | ||||||||||||||||
| "conflict and left alone: that edit is newer than anything this could\n" | ||||||||||||||||
| "restore. Its journal is kept so you can inspect it." | ||||||||||||||||
| ), | ||||||||||||||||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||||||||||||||||
| ) | ||||||||||||||||
| recover.add_argument( | ||||||||||||||||
| "--dry-run", action="store_true", help="list what would be rolled back, change nothing" | ||||||||||||||||
| ) | ||||||||||||||||
| _add_vault_args(recover) | ||||||||||||||||
|
|
||||||||||||||||
| checkpoint = sub.add_parser( | ||||||||||||||||
| "checkpoint", | ||||||||||||||||
| help="summarize recent activity (journal + compliance log) into a daily " | ||||||||||||||||
|
|
@@ -1218,6 +1239,38 @@ def _run_graph(args: argparse.Namespace) -> int: | |||||||||||||||
| return 0 | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def _run_recover(args: argparse.Namespace) -> int: | ||||||||||||||||
| """``omind recover``: roll back interrupted multi-note transactions.""" | ||||||||||||||||
| from omind import txn | ||||||||||||||||
| from omind.store import OmiStore | ||||||||||||||||
|
|
||||||||||||||||
| omi_dir = (args.vault / args.folder).expanduser() | ||||||||||||||||
| store = OmiStore(omi_dir) | ||||||||||||||||
| if not txn.pending(omi_dir): | ||||||||||||||||
| print("nothing to recover: no interrupted transactions") | ||||||||||||||||
| return 0 | ||||||||||||||||
| # Take the same write lock a normal write takes, so recovery cannot race a | ||||||||||||||||
| # concurrent MCP/web/cron writer touching the very notes it is restoring. | ||||||||||||||||
| with store.write_lock(): | ||||||||||||||||
| reports = txn.recover(omi_dir, dry_run=args.dry_run) | ||||||||||||||||
| conflicts = 0 | ||||||||||||||||
| for report in reports: | ||||||||||||||||
| print(("would roll back " if args.dry_run else "") + report.format()) | ||||||||||||||||
| for name in report.conflicts: | ||||||||||||||||
| print(f" CONFLICT: {name} changed after the interruption — left as-is") | ||||||||||||||||
| conflicts += 1 | ||||||||||||||||
| if conflicts: | ||||||||||||||||
| print( | ||||||||||||||||
| "\nSome notes were edited after the interrupted write. Their journals are kept;\n" | ||||||||||||||||
| "inspect the notes, then delete the journal directory when you are satisfied.", | ||||||||||||||||
| file=sys.stderr, | ||||||||||||||||
| ) | ||||||||||||||||
| return 1 | ||||||||||||||||
| if not args.dry_run: | ||||||||||||||||
| store.update_index() | ||||||||||||||||
| return 0 | ||||||||||||||||
|
Comment on lines
+1269
to
+1271
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm _signal_write is the standard post-write nudge and how other CLI write paths call it.
rg -nP --type=py -C4 '_signal_write' src/omindRepository: CryptoJones/omind Length of output: 2900 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== cli relevant section =="
sed -n '1200,1285p' src/omind/cli.py
echo
echo "== store _signal_write and update_index =="
sed -n '840,865p' src/omind/store.py
rg -n -C 3 'def update_index|def update_index_locked|_signal_write|_invalidate|mesh|sync' src/omind/store.py
echo
echo "== programmatic probe for _run_recover return path signal_write calls =="
python3 - <<'PY'
from pathlib import Path
import ast
cli = Path('src/omind/cli.py').read_text()
tree = ast.parse(cli)
class Finder(ast.NodeVisitor):
def __init__(self):
self.in_func = set()
self.finders = []
def visit_FunctionDef(self, node):
old = self.in_func.copy()
self.in_func.add(node.name)
self.generic_visit(node)
self.in_func = old
def visit_Attribute(self, node):
if node.attr == '_signal_write' and isinstance(node.value, ast.Name):
pos = node.lineno, node.col_offset
enclosing = sorted(self.in_func, key=lambda x:-len(x))
self.finders.append((pos, node.value.id + '.' + node.attr, enclosing))
self.generic_visit(node)
f = Finder()
f.visit(tree)
print("All _signal_write attribute accesses:")
for pos, expr, stack in f.finders:
print(f" line {pos[0]}: {expr} (enclosing: {stack})")
target_lines = {1269,1270,1271}
runs = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_run_recover']
if not runs:
print("ERROR: _run_recover not found")
raise SystemExit(1)
store_update = []
for n in ast.walk(runs[0]):
if isinstance(n, ast.Attribute) and n.attr == 'update_index':
store_update.append(n)
print(f"\n_run_recover update_index calls: {[s.lineno for s in store_update]}")
print(f"_run_recover contains _signal_write: {any(pos[0] in target_lines for pos,_,_ in f.finders)}")
PYRepository: CryptoJones/omind Length of output: 15504 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== cli relevant section =="
sed -n '1200,1285p' src/omind/cli.py
echo
echo "== store _signal_write and update_index =="
sed -n '840,865p' src/omind/store.py
rg -n -C 3 'def update_index|def update_index_locked|_signal_write|_invalidate|mesh|sync' src/omind/store.py
echo
echo "== programmatic probe for _run_recover return path signal_write calls =="
python3 - <<'PY'
from pathlib import Path
import ast
cli = Path('src/omind/cli.py').read_text()
tree = ast.parse(cli)
class Finder(ast.NodeVisitor):
def __init__(self):
self.in_func = set()
self.finders = []
def visit_FunctionDef(self, node):
old = self.in_func.copy()
self.in_func.add(node.name)
self.generic_visit(node)
self.in_func = old
def visit_Attribute(self, node):
if node.attr == '_signal_write' and isinstance(node.value, ast.Name):
pos = node.lineno, node.col_offset
enclosing = sorted(self.in_func, key=lambda x:-len(x))
self.finders.append((pos, node.value.id + '.' + node.attr, enclosing))
self.generic_visit(node)
f = Finder()
f.visit(tree)
print("All _signal_write attribute accesses:")
for pos, expr, stack in f.finders:
print(f" line {pos[0]}: {expr} (enclosing: {stack})")
target_lines = {1269,1270,1271}
runs = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_run_recover']
if not runs:
print("ERROR: _run_recover not found")
raise SystemExit(1)
store_update = []
for n in ast.walk(runs[0]):
if isinstance(n, ast.Attribute) and n.attr == 'update_index':
store_update.append(n)
print(f"\n_run_recover update_index calls: {[s.lineno for s in store_update]}")
print(f"_run_recover contains _signal_write at lines {target_lines}: {any(pos[0] in target_lines for pos,_,_ in f.finders)}")
PYRepository: CryptoJones/omind Length of output: 15532 Signal the write after recovery updates notes.
🐛 Recommended fix if not args.dry_run:
store.update_index()
+ store._signal_write()
return 0📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def _run_checkpoint(args: argparse.Namespace) -> int: | ||||||||||||||||
| from omind import checkpoint | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -1535,6 +1588,8 @@ def main(argv: list[str] | None = None) -> int: | |||||||||||||||
| return _run_bench(args) | ||||||||||||||||
| if args.command == "lint": | ||||||||||||||||
| return _run_lint(args) | ||||||||||||||||
| if args.command == "recover": | ||||||||||||||||
| return _run_recover(args) | ||||||||||||||||
| if args.command == "graph": | ||||||||||||||||
| return _run_graph(args) | ||||||||||||||||
| if args.command == "checkpoint": | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add blank lines after the release-note headings.
Add one blank line after
### Addedat Line 10 and after### Changedat Line 49. This resolves the reported markdownlint MD022 warnings.Also applies to: 49-49
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Source: Linters/SAST tools