feat(write-gate): shadow/enforce modes, per-intent auto-capture mode, and gate_decision telemetry#93
Open
RobertSigmundsson wants to merge 4 commits into
Conversation
Add a tri-state operating mode to WriteGateConfig on top of the legacy `enabled` bool: off | shadow | enforce (via `mode`) - `effective_mode` resolves `mode` first, falling back to the legacy `enabled` bool (True -> enforce) so existing configs keep working. - `auto_capture_mode` is a per-intent override for passive captures (intent=auto): a global enforce is known to false-reject real turn / summary content, so junk auto-captures can be enforced while interactive writes stay in shadow. `effective_auto_mode` inherits `effective_mode` when unset. - The TOML serializer now persists `mode` and `auto_capture_mode`; without this a config save() silently demoted an active shadow/enforce gate back to off on the next load. quality_scorer.check_write_gate is unchanged — this only decides whether its verdict blocks (enforce), logs (shadow), or is skipped (off). (cherry picked from commit cad5aa2) (cherry picked from commit 7a36bfe) (cherry picked from commit 209666b)
…ne paths Wire the write-gate through every engine-side capture path (MCP remember, stop-hook turns + session summary, pre-compact flush) using effective_mode / effective_auto_mode instead of the binary `enabled`: - SHADOW logs the decision without blocking; ENFORCE rejects; off skips. - Each decision (both shadow and enforce) is appended to the SCHEMALESS `gate_decision` table via a best-effort, fire-and-forget helper (engine/gate_telemetry.py) so scripts/gate_stats.py sees BOTH the Hermes plugin decisions and the smem auto-capture / stop-hook decisions in one report. - The gate now scores the tags it actually saves (base_tags), matching what ends up on the fiber. - pre-compact previously encoded with NO gate at all, so junk auto-captures bypassed both telemetry and enforcement; it now runs the same intent=auto gate as the stop hook. Telemetry never breaks a write (all failures swallowed and debug-logged). Local to Uruboros: the gate_decision table is shared with the Hermes plugin, so this is a soft-fork overlay rather than an upstream candidate. (cherry picked from commit 0c1f437) (cherry picked from commit 81c4d14) (cherry picked from commit f211fad)
The MCP server test mocks set only cfg.write_gate.enabled, which worked while the gate checked `enabled is True` (a truthy MagicMock is not True, so the gate was skipped). The mode-based gate resolves effective_mode, and a bare MagicMock.mode is truthy -> the gate would run against MagicMock thresholds and raise TypeError. Give every get_config mock a real WriteGateConfig(enabled=False) so the gate resolves to off, matching production defaults. (cherry picked from commit ddd9932) (cherry picked from commit 1523d56) (cherry picked from commit d5ad0b9)
The write-gate check our infra adds to the remember path (7a36bfe) runs on the v2.10.0 geo remember flow too, but TestRememberLocationBoundary's server mock left write_gate as a bare MagicMock, so effective_mode() returned a truthy mock (not "off") and check_write_gate tripped on a MagicMock min_length. Give the mock a real WriteGateConfig() (mode="off") so the gate is skipped — same pattern as 1523d56 for the server mocks. (cherry picked from commit fe335fc) (cherry picked from commit 73857ea)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
WriteGateConfigonmainis a single boolean:enabled. That forces an all-or-nothing choice.Turning it on immediately starts rejecting writes with thresholds nobody has measured on their own
corpus; leaving it off means the gate never produces any data you could tune it with. In practice a
global
enforcefalse-rejects real conversational turns and summaries, so the gate ends up permanently off.Change
1. Three modes instead of a boolean.
shadowevaluates the gate and records the decision without rejecting anything. Backward compatible:effective_modefalls back to the legacyenabledbool (True→enforce) whenevermodeis"off",so existing configs behave exactly as today.
2. Per-intent override.
auto_capture_modeapplies tointent=autoonly; summaries and turns keepmode. This is the practically useful combination: enforce the gate on junk passive auto-captureswhile interactive writes stay in shadow.
3.
gate_decisiontelemetry (engine/gate_telemetry.py, new) wired into the paths that actuallywrite:
mcp/remember_handler.py,hooks/stop.py,hooks/pre_compact.py. This is what makes shadow modeworth anything — you get the accept/reject record you need to pick thresholds before you enforce them.
4. Two test commits replace hand-rolled config mocks in the MCP and geo server tests with a real
WriteGateConfig, so the mocks cannot drift from the dataclass.Why upstream benefits
Shadow mode is the standard way to ship a policy engine: measure first, enforce second. Without it the
write gate is a feature most users will never safely turn on. Full backward compatibility means zero
impact on anyone who does not set
mode.Evidence
origin/mainimport surreal_memoryruff checkruff format --checkmypy src/ --ignore-missing-importsgate_telemetry)pytest -m "not stress" -n autotests/unit/test_write_gate.py+70 lines coveringeffective_mode/effective_auto_moderesolutionincluding the legacy-
enabledfallback.Risks / notes for the reviewer
independent of commit 2 (telemetry + wiring), though shadow mode is not much use without the telemetry.
effective_mode/effective_auto_modecentralise theprecedence so no call site re-derives it — worth a look, that is where a subtle bug would live.
gate_decisionrecords are persisted. A reviewer should confirm the retention storyis acceptable — this branch does not add pruning for them.