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
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:
@@ -7,11 +7,13 @@ import { useMomentsStore } from "@/stores/moments";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
|
||||
/**
|
||||
* Proposals that a rule belongs on a moment, waiting on a person (milestone
|
||||
* 458 step 7). They come from a pass that read the rule, or from the rule
|
||||
* being opened just after the moment fired in several sessions. Nothing here
|
||||
* is mounted until "Mount" is pressed — the same judgment `judge_rule_moments`
|
||||
* makes in a session, through the same service.
|
||||
* Proposals about a rule's moments, waiting on a person. A mount proposal
|
||||
* (milestone 458 step 7) comes from a pass that read the rule, or from the
|
||||
* rule being opened just after the moment fired in several sessions; nothing
|
||||
* is mounted until "Mount" is pressed. An unmount proposal (step 7b) comes
|
||||
* from sessions reporting that the mount arrived where it did not apply;
|
||||
* nothing is taken off until "Take it off" is pressed. Both are the judgment
|
||||
* `judge_rule_moments` makes in a session, through the same service.
|
||||
*/
|
||||
const store = useMomentsStore();
|
||||
const toast = useToastStore();
|
||||
@@ -35,27 +37,48 @@ function key(rule: RuleProposals, p: MomentProposal): string {
|
||||
return `${rule.id}:${p.moment}`;
|
||||
}
|
||||
|
||||
function plural(n: number, one: string, many: string): string {
|
||||
return `${n} ${n === 1 ? one : many}`;
|
||||
}
|
||||
|
||||
function sourceLabel(p: MomentProposal): string {
|
||||
if (p.proposal === "unmount") {
|
||||
return `reported as not applying here on ${plural(p.evidence.situations, "day", "days")}`;
|
||||
}
|
||||
if (p.source === "signal") {
|
||||
return `opened just after this moment in ${p.evidence.situations} sessions`;
|
||||
}
|
||||
return "proposed from the rule's text";
|
||||
}
|
||||
|
||||
// What the action that reached the moment was, most frequent first — so the
|
||||
// operator can see whether it is the rule or one action that keeps misfiring.
|
||||
function actions(p: MomentProposal): string[] {
|
||||
return Object.entries(p.reached_by ?? {})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([action, n]) => `${action} ×${n}`);
|
||||
}
|
||||
|
||||
function note(p: MomentProposal, verdict: Verdict): string {
|
||||
if (p.proposal === "unmount") {
|
||||
return verdict === "confirm" ? "Kept in Settings." : "Taken off in Settings.";
|
||||
}
|
||||
return verdict === "confirm" ? "Mounted in Settings." : "Declined in Settings.";
|
||||
}
|
||||
|
||||
async function decide(rule: RuleProposals, p: MomentProposal, verdict: Verdict) {
|
||||
busy.value = key(rule, p);
|
||||
try {
|
||||
const out = await judgeProposals([{
|
||||
rule_id: rule.id, moment: p.moment, verdict,
|
||||
note: verdict === "confirm" ? "Mounted in Settings." : "Declined in Settings.",
|
||||
rule_id: rule.id, moment: p.moment, verdict, note: note(p, verdict),
|
||||
}]);
|
||||
if (out.refused.length) {
|
||||
toast.show(out.refused[0].error, "error");
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
// A mount changes the per-moment counts the list below shows.
|
||||
if (verdict === "confirm") await store.load(true);
|
||||
// A mount or an unmount changes the per-moment counts the list below shows.
|
||||
if ((p.proposal === "unmount") === (verdict === "reject")) await store.load(true);
|
||||
} catch (e) {
|
||||
toast.show(apiErrorMessage(e, "Could not record that"), "error");
|
||||
} finally {
|
||||
@@ -75,8 +98,10 @@ onMounted(load);
|
||||
</p>
|
||||
<template v-else>
|
||||
<p class="field-hint">
|
||||
Moments these rules may belong on. Mounting one makes the rule arrive whenever that
|
||||
moment happens, whatever the work is about; declining keeps it from being proposed again.
|
||||
Moments these rules may belong on, or may not. Mounting one makes the rule arrive
|
||||
whenever that moment happens, whatever the work is about; declining keeps it from being
|
||||
proposed again. A mount that sessions keep reporting as beside the point can be taken
|
||||
off — or, when one action is reaching the moment wrongly, unmapped in the list below.
|
||||
</p>
|
||||
<ul class="proposal-list">
|
||||
<li v-for="rule in rules" :key="rule.id" class="proposal-rule">
|
||||
@@ -89,12 +114,28 @@ onMounted(load);
|
||||
<p class="rule-statement">{{ rule.statement }}</p>
|
||||
<ul class="proposal-items">
|
||||
<li v-for="p in rule.proposals" :key="key(rule, p)" class="proposal-item">
|
||||
<span v-if="p.proposal === 'unmount'" class="proposal-kind">take off</span>
|
||||
<code class="moment-name">{{ p.moment }}</code>
|
||||
<span class="proposal-why">
|
||||
{{ p.why || sourceLabel(p) }}
|
||||
<span v-if="p.why" class="proposal-source">· {{ sourceLabel(p) }}</span>
|
||||
<span v-if="actions(p).length" class="proposal-source">
|
||||
· reached by {{ actions(p).join(", ") }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="proposal-actions">
|
||||
<span v-if="p.proposal === 'unmount'" class="proposal-actions">
|
||||
<button
|
||||
type="button" class="btn-primary btn-sm"
|
||||
:disabled="busy !== null"
|
||||
@click="decide(rule, p, 'reject')"
|
||||
>Take it off</button>
|
||||
<button
|
||||
type="button" class="btn-text"
|
||||
:disabled="busy !== null"
|
||||
@click="decide(rule, p, 'confirm')"
|
||||
>Keep it</button>
|
||||
</span>
|
||||
<span v-else class="proposal-actions">
|
||||
<button
|
||||
type="button" class="btn-primary btn-sm"
|
||||
:disabled="busy !== null"
|
||||
@@ -106,6 +147,9 @@ onMounted(load);
|
||||
@click="decide(rule, p, 'reject')"
|
||||
>Not this</button>
|
||||
</span>
|
||||
<ul v-if="p.reasons && p.reasons.length > 1" class="proposal-reasons">
|
||||
<li v-for="r in p.reasons.slice(0, -1).reverse()" :key="r.at">{{ r.why }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -133,6 +177,14 @@ onMounted(load);
|
||||
.proposal-why { flex: 1 1 16rem; color: var(--fs-text-secondary); }
|
||||
.proposal-source { color: var(--fs-text-tertiary); font-size: var(--fs-size-tiny); }
|
||||
.proposal-actions { display: inline-flex; gap: var(--fs-space-2); margin-left: auto; }
|
||||
.proposal-kind { font-size: var(--fs-size-tiny); color: var(--fs-text-tertiary); }
|
||||
.proposal-reasons {
|
||||
flex-basis: 100%;
|
||||
margin: 0;
|
||||
padding-left: var(--fs-space-4);
|
||||
font-size: var(--fs-size-tiny);
|
||||
color: var(--fs-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style src="@/assets/moments-shared.css" />
|
||||
|
||||
Reference in New Issue
Block a user