feat(moments): a mount that keeps arriving where it does not apply proposes its own removal (milestone 458 step 7b, #4955)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m3s
CI & Build / Build & push image (push) Successful in 18s

The open-after-moment signal proposes a mount; nothing proposed taking one
off, so a wrong mount was noise at every occurrence until someone happened
to notice. rule_misfired(rule_id, moment, why, reached_by) records a report
against a MOUNTED pair, counted per distinct day (the MCP door carries no
session id) on a new rule_moment_judgments.misfire column (migration 0119,
backup v24). At three days the response carries a line asking the agent to
offer the operator the fix - reject takes the rule off, unmap_action stops
the action reaching the moment, confirm keeps the mount and stops the
asking - and Settings > Moments lists it as an unmount proposal with the
reasons and the actions that reached it. A re-mount clears the count.

Taught in moments.md, missed-retrieval.md and the reply hold's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 18:19:59 -04:00
co-authored by Claude Opus 5.5
parent 8afd6da8af
commit 4e1320120d
19 changed files with 636 additions and 44 deletions
+7 -1
View File
@@ -109,8 +109,11 @@ logger = logging.getLogger(__name__)
# proposals waiting on a mount, and the rejections and "no moment fits"
# answers that stop a pass or the open-after-moment signal proposing the same
# pair again. Losing them puts every answered question back on the list.
# v24 (2026-10) added rule_moment_judgments.misfire (milestone 458 step 7b):
# the reports that a mounted rule arrived where it did not apply, and the
# operator's "keep it" that stops them being proposed as an unmount again.
# Bump when the serialized schema changes.
BACKUP_VERSION = 23
BACKUP_VERSION = 24
# Every table this backup carries, by its REAL name. Paired with _NOT_INCLUDED
# below, these two lists must together account for the entire schema — which is
@@ -772,6 +775,7 @@ def _rule_moment_judgment_rows(rows) -> list[dict]:
{
"rule_id": r.rule_id, "moment": r.moment, "state": r.state,
"source": r.source, "note": r.note, "evidence": r.evidence,
"misfire": r.misfire,
"judged_at": r.judged_at.isoformat() if r.judged_at else None,
"created_at": r.created_at.isoformat() if r.created_at else None,
}
@@ -1580,6 +1584,8 @@ def _build_rule_moment_judgment(row: dict, maps: _Maps) -> RuleMomentJudgment |
source=row.get("source") or "pass",
note=row.get("note") or None,
evidence=row.get("evidence"),
# Archives before v24 carry no misfire reports.
misfire=row.get("misfire"),
# Absent stays absent: a suggestion was never judged.
judged_at=_dt_or_none(row.get("judged_at")),
created_at=_dt(row.get("created_at")),