Skip to content

feat(write-gate): shadow/enforce modes, per-intent auto-capture mode, and gate_decision telemetry#93

Open
RobertSigmundsson wants to merge 4 commits into
acidkill:mainfrom
RobertSigmundsson:upstream-pr/write-gate-shadow-enforce
Open

feat(write-gate): shadow/enforce modes, per-intent auto-capture mode, and gate_decision telemetry#93
RobertSigmundsson wants to merge 4 commits into
acidkill:mainfrom
RobertSigmundsson:upstream-pr/write-gate-shadow-enforce

Conversation

@RobertSigmundsson

Copy link
Copy Markdown
Contributor

Problem

WriteGateConfig on main is 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 enforce false-rejects real conversational turns and summaries, so the gate ends up permanently off.

Change

1. Three modes instead of a boolean.

mode: str = "off"            # off | shadow | enforce — overrides `enabled` when not "off"
auto_capture_mode: str = ""  # "" (inherit) | off | shadow | enforce

shadow evaluates the gate and records the decision without rejecting anything. Backward compatible:
effective_mode falls back to the legacy enabled bool (Trueenforce) whenever mode is "off",
so existing configs behave exactly as today.

2. Per-intent override. auto_capture_mode applies to intent=auto only; summaries and turns keep
mode. This is the practically useful combination: enforce the gate on junk passive auto-captures
while interactive writes stay in shadow.

3. gate_decision telemetry (engine/gate_telemetry.py, new) wired into the paths that actually
write: mcp/remember_handler.py, hooks/stop.py, hooks/pre_compact.py. This is what makes shadow mode
worth 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

Check Result
cherry-pick onto origin/main clean (4 commits)
import surreal_memory OK
ruff check All checks passed!
ruff format --check 702 files already formatted
mypy src/ --ignore-missing-imports Success: no issues found in 370 source files (369 + gate_telemetry)
pytest -m "not stress" -n auto 6541 passed, 84 skipped, 1 xfailed (baseline 6535 → +6)

tests/unit/test_write_gate.py +70 lines covering effective_mode / effective_auto_mode resolution
including the legacy-enabled fallback.

Risks / notes for the reviewer

  • Largest of the eight PRs (8 files). Splittable if preferred: commits 1+3+4 (config + test mocks) are
    independent of commit 2 (telemetry + wiring), though shadow mode is not much use without the telemetry.
  • Two config knobs where there was one boolean. effective_mode / effective_auto_mode centralise the
    precedence so no call site re-derives it — worth a look, that is where a subtle bug would live.
  • Telemetry writes: gate_decision records are persisted. A reviewer should confirm the retention story
    is acceptable — this branch does not add pruning for them.
  • Backend-agnostic: no storage-layer schema change.

RobertSigmundsson and others added 4 commits July 26, 2026 01:42
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant