Skip to main content

microclaw_engine/internal/storage/db/learning/
mod.rs

1use super::*;
2
3pub fn wilson_lower_bound(passed: i64, total: i64, z: f64) -> f64 {
4    if total <= 0 || passed < 0 || passed > total || !z.is_finite() || z < 0.0 {
5        return 0.0;
6    }
7    let n = total as f64;
8    let p = passed as f64 / n;
9    let z2 = z * z;
10    ((p + z2 / (2.0 * n) - z * ((p * (1.0 - p) + z2 / (4.0 * n)) / n).sqrt()) / (1.0 + z2 / n))
11        .clamp(0.0, 1.0)
12}
13
14pub(crate) fn erase_chat_learning(
15    tx: &Transaction<'_>,
16    chat_id: i64,
17) -> Result<usize, MicroClawError> {
18    let mut stmt = tx.prepare(
19        "SELECT DISTINCT o.skill_name, o.skill_version
20         FROM skill_outcomes o
21         JOIN experience_runs r ON r.run_id=o.run_id
22         WHERE r.chat_id=?1",
23    )?;
24    let affected_skills = stmt
25        .query_map(params![chat_id], |row| {
26            Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
27        })?
28        .collect::<Result<Vec<_>, _>>()?;
29    drop(stmt);
30
31    let mut affected = 0usize;
32    let promoted_from_chat = {
33        let mut stmt = tx.prepare(
34            "SELECT DISTINCT k.skill_name, k.candidate_version, k.base_version
35             FROM skill_candidates k
36             JOIN learning_claims c ON c.claim_id=k.claim_id
37             JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
38             JOIN experience_runs s ON s.run_id=x.success_run_id
39             JOIN experience_runs f ON f.run_id=x.failure_run_id
40             WHERE k.status='promoted' AND (s.chat_id=?1 OR f.chat_id=?1)",
41        )?;
42        let rows = stmt
43            .query_map(params![chat_id], |row| {
44                Ok((
45                    row.get::<_, String>(0)?,
46                    row.get::<_, i64>(1)?,
47                    row.get::<_, i64>(2)?,
48                ))
49            })?
50            .collect::<Result<Vec<_>, _>>()?;
51        rows
52    };
53    for (skill_name, candidate_version, base_version) in promoted_from_chat {
54        tx.execute(
55            "UPDATE skill_lifecycle SET state='trusted', active_version=?2,
56             previous_trusted_version=NULL,
57             state_reason='comparative evidence erased; restored base version',
58             updated_at=?3 WHERE skill_name=?1 AND active_version=?4",
59            params![
60                skill_name,
61                base_version,
62                chrono::Utc::now().to_rfc3339(),
63                candidate_version
64            ],
65        )?;
66        tx.execute(
67            "DELETE FROM skill_versions WHERE skill_name=?1 AND version=?2
68             AND source='comparative-reflection'",
69            params![skill_name, candidate_version],
70        )?;
71    }
72    affected += tx.execute(
73        "DELETE FROM learning_journal_events WHERE
74           (entity_type='learning_claim' AND entity_id IN (
75             SELECT c.claim_id FROM learning_claims c
76             JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
77             JOIN experience_runs s ON s.run_id=x.success_run_id
78             JOIN experience_runs f ON f.run_id=x.failure_run_id
79             WHERE s.chat_id=?1 OR f.chat_id=?1
80           )) OR
81           (entity_type='skill_candidate' AND entity_id IN (
82             SELECT k.candidate_id FROM skill_candidates k
83             JOIN learning_claims c ON c.claim_id=k.claim_id
84             JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
85             JOIN experience_runs s ON s.run_id=x.success_run_id
86             JOIN experience_runs f ON f.run_id=x.failure_run_id
87             WHERE s.chat_id=?1 OR f.chat_id=?1
88           ))",
89        params![chat_id],
90    )?;
91    affected += tx.execute(
92        "DELETE FROM learning_journal_events
93         WHERE entity_type='skill' AND entity_id IN (
94           SELECT DISTINCT o.skill_name FROM skill_outcomes o
95           JOIN experience_runs r ON r.run_id=o.run_id WHERE r.chat_id=?1
96         )",
97        params![chat_id],
98    )?;
99    affected += tx.execute(
100        "DELETE FROM shadow_observations WHERE
101           run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)
102           OR candidate_id IN (
103             SELECT k.candidate_id FROM skill_candidates k
104             JOIN learning_claims c ON c.claim_id=k.claim_id
105             JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
106             JOIN experience_runs s ON s.run_id=x.success_run_id
107             JOIN experience_runs f ON f.run_id=x.failure_run_id
108             WHERE s.chat_id=?1 OR f.chat_id=?1
109           )",
110        params![chat_id],
111    )?;
112    affected += tx.execute(
113        "DELETE FROM shadow_evaluations WHERE candidate_id IN (
114           SELECT k.candidate_id FROM skill_candidates k
115           JOIN learning_claims c ON c.claim_id=k.claim_id
116           JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
117           JOIN experience_runs s ON s.run_id=x.success_run_id
118           JOIN experience_runs f ON f.run_id=x.failure_run_id
119           WHERE s.chat_id=?1 OR f.chat_id=?1
120         )",
121        params![chat_id],
122    )?;
123    affected += tx.execute(
124        "DELETE FROM skill_candidates WHERE claim_id IN (
125           SELECT c.claim_id FROM learning_claims c
126           JOIN experience_comparisons x ON x.comparison_id=c.comparison_id
127           JOIN experience_runs s ON s.run_id=x.success_run_id
128           JOIN experience_runs f ON f.run_id=x.failure_run_id
129           WHERE s.chat_id=?1 OR f.chat_id=?1
130         )",
131        params![chat_id],
132    )?;
133    affected += tx.execute(
134        "DELETE FROM learning_claims WHERE comparison_id IN (
135           SELECT x.comparison_id FROM experience_comparisons x
136           JOIN experience_runs s ON s.run_id=x.success_run_id
137           JOIN experience_runs f ON f.run_id=x.failure_run_id
138           WHERE s.chat_id=?1 OR f.chat_id=?1
139         )",
140        params![chat_id],
141    )?;
142    affected += tx.execute(
143        "DELETE FROM experience_comparisons WHERE
144           success_run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)
145           OR failure_run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
146        params![chat_id],
147    )?;
148    affected += tx.execute(
149        "DELETE FROM skill_failure_pattern_evidence
150         WHERE run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
151        params![chat_id],
152    )?;
153    affected += tx.execute(
154        "DELETE FROM skill_outcomes
155         WHERE run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
156        params![chat_id],
157    )?;
158    affected += tx.execute(
159        "DELETE FROM verifier_results
160         WHERE run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
161        params![chat_id],
162    )?;
163    affected += tx.execute(
164        "DELETE FROM experience_feedback
165         WHERE run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
166        params![chat_id],
167    )?;
168    affected += tx.execute(
169        "DELETE FROM outcome_envelopes
170         WHERE run_id IN (SELECT run_id FROM experience_runs WHERE chat_id=?1)",
171        params![chat_id],
172    )?;
173    affected += tx.execute(
174        "DELETE FROM experience_retrieval_logs
175         WHERE querying_run_id IN (
176             SELECT run_id FROM experience_runs WHERE chat_id=?1
177         ) OR source_run_id IN (
178             SELECT run_id FROM experience_runs WHERE chat_id=?1
179         )",
180        params![chat_id],
181    )?;
182    affected += tx.execute(
183        "DELETE FROM experience_retrieval_rejections
184         WHERE querying_run_id IN (
185             SELECT run_id FROM experience_runs WHERE chat_id=?1
186         ) OR source_run_id IN (
187             SELECT run_id FROM experience_runs WHERE chat_id=?1
188         )",
189        params![chat_id],
190    )?;
191    affected += tx.execute(
192        "DELETE FROM skill_activation_logs
193         WHERE chat_id=?1 OR experience_run_id IN (
194             SELECT run_id FROM experience_runs WHERE chat_id=?1
195         )",
196        params![chat_id],
197    )?;
198    affected += tx.execute(
199        "DELETE FROM experience_runs WHERE chat_id=?1",
200        params![chat_id],
201    )?;
202    affected += tx.execute("DELETE FROM goal_states WHERE chat_id=?1", params![chat_id])?;
203    for (skill_name, version) in affected_skills {
204        tx.execute(
205            "DELETE FROM skill_failure_pattern_evidence
206             WHERE pattern_id IN (
207               SELECT pattern_id FROM skill_failure_patterns
208               WHERE skill_name=?1 AND skill_version=?2
209             )",
210            params![skill_name, version],
211        )?;
212        tx.execute(
213            "DELETE FROM skill_failure_patterns
214             WHERE skill_name=?1 AND skill_version=?2",
215            params![skill_name, version],
216        )?;
217        let remaining = {
218            let mut stmt = tx.prepare(
219                "SELECT run_id FROM skill_outcomes
220                 WHERE skill_name=?1 AND skill_version=?2 ORDER BY created_at ASC",
221            )?;
222            let rows = stmt
223                .query_map(params![skill_name, version], |row| row.get::<_, String>(0))?
224                .collect::<Result<Vec<_>, _>>()?;
225            rows
226        };
227        for run_id in remaining {
228            sync_skill_failure_patterns_for_run(tx, &run_id)?;
229        }
230        recompute_skill_lifecycle(tx, &skill_name, version, "chat learning evidence erased")?;
231    }
232    Ok(affected)
233}
234
235pub mod experience;
236pub mod skills;
237pub mod tracks;
238
239pub use self::experience::*;
240pub use self::skills::*;
241pub use self::tracks::*;
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::internal::storage::db::test_support::*;
247
248    #[test]
249    fn test_claim_due_tasks_is_single_consumer() {
250        let (db, dir) = test_db();
251        db.create_scheduled_task(100, "task1", "cron", "0 * * * * *", "2024-01-01T00:00:00Z")
252            .unwrap();
253
254        let first = db.claim_due_tasks("2024-06-01T00:00:00Z", 50).unwrap();
255        assert_eq!(first.len(), 1);
256        assert_eq!(first[0].status, "running");
257
258        // A second claim in the same due window should not see the same task again.
259        let second = db.claim_due_tasks("2024-06-01T00:00:00Z", 50).unwrap();
260        assert!(second.is_empty());
261        cleanup(&dir);
262    }
263
264    #[test]
265    fn wilson_utility_lower_bound_is_conservative_and_monotonic() {
266        let three_of_three = wilson_lower_bound(3, 3, 1.96);
267        let four_of_four = wilson_lower_bound(4, 4, 1.96);
268        assert!(three_of_three > 0.4 && three_of_three < 0.5);
269        assert!(four_of_four > 0.5);
270        assert!(four_of_four > three_of_three);
271        assert_eq!(wilson_lower_bound(0, 0, 1.96), 0.0);
272    }
273}