Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.4.0] - 2026-08-14

### Added
- **Governing rule text now rides inside the guard's own block message**
(#241). When repo work is blocked pending the git-rules consult, the deny
carries the note's summary and leading excerpt (capped at 1,600 chars) under
a "Governing memory (excerpt)" divider — an instruction adjacent to the
action wins attention that one injected 200 turns earlier has lost. The
demand sentence stays first and the recall ceremony still feeds consult
telemetry; a missing note degrades to the bare demand.
- **Turn preflight surfaces a runner-up match.** `relevant_titles` now returns
two; the second appears as "Also possibly relevant: [[title]] — summary"
only (never a body), and is skipped on the economy profile.

### Changed
- **Action-shaped turns re-inject the full preflight excerpt.** The repeated-
note summary-only downgrade now applies only to conversational turns; a turn
matching git/push/commit/merge/deploy/release/sudo/rm/delete/publish/
provision gets the full excerpt every time (#241).

## [8.3.2] - 2026-08-14

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "omind"
version = "8.3.2"
version = "8.4.0"
description = "Reproduce the OMI/Obsidian memory integration for AI agents, plus a local web app to view, edit, and add memory entries."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/omind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright 2026 Aaron K. Clark
"""omind — OMI/Obsidian memory tooling for AI agents."""

__version__ = "8.3.2"
__version__ = "8.4.0"
86 changes: 83 additions & 3 deletions src/omind/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,26 @@
return Verdict(allow=False, reason=f"omi-gate: {GATE_MESSAGE}", rule_id="omi-gate")


#: Hard ceiling for the embedded excerpt so a huge note can't bloat every deny.
_EXCERPT_CAP = 1_600


def _governing_excerpt(omi_dir: Path | str, note: str) -> str:
"""Summary + leading excerpt of ``note``, capped, for embedding in a deny
message (#241). Best-effort: any failure returns ``""`` — the deny still
stands on its demand sentence alone."""
try:
from omind import recall

memory = recall.compact_recall(omi_dir, note, max_chars=1_200)
summary = str(memory.get("summary") or "").strip()
content = str(memory.get("content") or "").strip()
text = "\n\n".join(part for part in (summary, content) if part)
return text[:_EXCERPT_CAP]
except Exception:
return ""


def check_action(action: dict[str, Any], omi_dir: Path | None = None) -> Verdict:
"""Decide an action and log a real policy-rule deny to the compliance log.

Expand All @@ -1232,6 +1252,25 @@
routine ``omi-gate`` "you didn't consult" deny is friction, not logged.
"""
verdict = decide(action)
if (
not verdict.allow
and verdict.rule_id == "repo-work-read-git-rules"
and omi_dir is not None
):
# #241: place the governing rule text adjacent to the action it blocks.
# The demand sentence stays first — the recall ceremony still runs and
# feeds consult telemetry — but the rule itself rides along, because an
# instruction next to the action wins attention that one injected 200
# turns earlier has lost.
excerpt = _governing_excerpt(omi_dir, GIT_RULES_NOTE)
if excerpt:
verdict = Verdict(
allow=False,
reason=(
f"{verdict.reason}\n\n--- Governing memory (excerpt) ---\n{excerpt}"
),
rule_id=verdict.rule_id,
)
if not verdict.allow and verdict.rule_id == "omi-gate" and omi_dir is not None:
from omind import retrieve

Expand Down Expand Up @@ -1360,6 +1399,37 @@
return bool(os.environ.get(MISS_STRICT_ENV))


#: Turns that look like an action rather than a conversation (#241). Fixed by
#: design — an env knob here would be one more thing that silently degrades.
_ACTION_TURN_RE = re.compile(
r"\b(git|push|commit|merge|deploy|release|sudo|rm|delete|publish|provision)\b",
re.IGNORECASE,
)


def _second_title_line(omi_dir: Path | str, titles: list[str], first: str) -> str:
"""Title + summary of the runner-up preflight match, never a full body
(#241). Skipped on the economy profile, where the preflight budget is too
tight for a second note. Best-effort — a failure adds nothing."""
if len(titles) < 2:
return ""
try:
from omind import ai_usage, recall

if ai_usage.policy(omi_dir).preflight_chars < 2_000:
return ""
filename = recall.filename_for_title(omi_dir, titles[1])
if filename is None or filename == first:
return ""
memory = recall.compact_recall(omi_dir, filename, max_chars=recall.MIN_RECALL_CHARS)
title = str(memory.get("title") or Path(filename).stem)
summary = str(memory.get("summary") or "").strip()
line = f"\n\nAlso possibly relevant: [[{title}]]"
return line + (f" — {summary}" if summary else "")
except Exception:
return ""


def preflight_turn(data: dict[str, Any], omi_dir: Path | None) -> str:
"""Prepare one turn with compact relevant memory and satisfy the soft gate.

Expand All @@ -1383,7 +1453,7 @@

from omind import ai_usage, recall, retrieve

titles = retrieve.relevant_titles(task, omi_dir, limit=1) if task else []
titles = retrieve.relevant_titles(task, omi_dir, limit=2) if task else []
filename = recall.filename_for_title(omi_dir, titles[0]) if titles else None
if filename is None:
if task and not titles and not _miss_strict():
Expand Down Expand Up @@ -1416,9 +1486,14 @@
)
version = str(memory.get("version") or "")
repeated = _injected_versions(session).get(filename) == version
# #241: the summary-only optimization for repeated notes loses to attention
# decay exactly when it matters — re-inject the full excerpt whenever the
# turn looks like an action (git/deploy/sudo/…), keep the optimization for
# conversational turns.
action_shaped = bool(_ACTION_TURN_RE.search(task))
summary = str(memory.get("summary") or "").strip()
excerpt = str(memory.get("content") or "").strip()
content = summary if repeated else "\n\n".join(
content = summary if repeated and not action_shaped else "\n\n".join(
part for part in (summary, excerpt) if part and part != summary
)
if not content:
Expand All @@ -1428,12 +1503,17 @@
_record_injected(session, filename, version)
context = (
f"OMI turn preflight recalled [[{memory.get('title') or Path(filename).stem}]]"
+ (" (full excerpt already injected earlier this session)" if repeated else "")
+ (
" (full excerpt already injected earlier this session)"
if repeated and not action_shaped
else ""
)
+ ". This is a standing operator instruction/memory relevant to this "
"turn — apply it unless the user's current message explicitly "
"overrides it. Silence is not an override.\n\n"
+ content
)
context += _second_title_line(omi_dir, titles, filename)
ai_usage.record_context(omi_dir, "recall", len(context), session_id=session)
return context

Expand Down
97 changes: 97 additions & 0 deletions tests/test_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,3 +1464,100 @@ def test_truncated_demanded_read_keeps_git_rules_unconsulted() -> None:
assert guard._has_consulted_git_rules(session) # full re-read clears it
guard.begin_turn(session, "next turn")
assert guard.incomplete_consult(session) == "" # per-turn state resets


# -- #241: rule text adjacent to the action -----------------------------------


def test_repo_block_message_embeds_governing_excerpt(tmp_path: Path) -> None:
from omind.store import NoteFields, OmiStore

omi = tmp_path / "OMI"
omi.mkdir()
OmiStore(omi).create_note(
NoteFields(
title=guard.GIT_RULES_NOTE,
summary="Branch plus PR on public repos; private goes straight to main.",
details="EXCEPTION TABLE: repo-x pushes directly to master.",
)
)
session = "adj1"
guard.begin_turn(session, "push it")
verdict = guard.check_action(
{"tool": "Bash", "command": "git push origin main", "session": session},
omi_dir=omi,
)
assert not verdict.allow
assert "Governing memory (excerpt)" in verdict.reason
assert "Branch plus PR on public repos" in verdict.reason
assert "EXCEPTION TABLE" in verdict.reason
assert verdict.reason.index("ACTION BLOCKED") < verdict.reason.index("Governing")
assert len(verdict.reason) < 2_400 # demand + capped excerpt


def test_repo_block_message_survives_a_missing_note(tmp_path: Path) -> None:
omi = tmp_path / "OMI"
omi.mkdir()
guard.begin_turn("adj2", "push it")
verdict = guard.check_action(
{"tool": "Bash", "command": "git push origin main", "session": "adj2"},
omi_dir=omi,
)
assert not verdict.allow # the demand still stands on its own
assert "Governing memory" not in verdict.reason


def test_preflight_reinjects_full_excerpt_on_action_shaped_turns(tmp_path: Path) -> None:
from omind.store import NoteFields, OmiStore

omi = tmp_path / "OMI"
omi.mkdir()
OmiStore(omi).create_note(
NoteFields(
title="Deploy Rules",
summary="Deploys are gated.",
details="Always deploy from a tagged release build.",
)
)
event = {"session_id": "act-turn", "prompt": "deploy the release build"}
first = guard.preflight_turn(event, omi)
assert "tagged release" in first
repeated = guard.preflight_turn(event, omi)
# Action-shaped turn: full excerpt again, no summary-only downgrade (#241).
assert "tagged release" in repeated
assert "already injected earlier this session" not in repeated


def test_preflight_adds_second_title_summary_only(tmp_path: Path) -> None:
from omind import ai_usage
from omind.store import NoteFields, OmiStore

omi = tmp_path / "OMI"
omi.mkdir()
store = OmiStore(omi)
store.create_note(
NoteFields(
title="Token Budget Alpha",
summary="primary token budget note",
details="ALPHA-BODY token budget usage bounds",
)
)
store.create_note(
NoteFields(
title="Token Budget Beta",
summary="secondary token budget note",
details="BETA-BODY token budget usage bounds",
)
)
ai_usage.set_profile(omi, "full")
context = guard.preflight_turn(
{"session_id": "second-1", "prompt": "token budget usage bounds"}, omi
)
assert "Also possibly relevant: [[" in context
assert "BETA-BODY" not in context or "ALPHA-BODY" not in context # runner-up is summary-only
ai_usage.set_profile(omi, "economy")
guard.clear_gate("second-2")
economy = guard.preflight_turn(
{"session_id": "second-2", "prompt": "token budget usage bounds"}, omi
)
assert "Also possibly relevant" not in economy # skipped on economy
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.