microclaw_engine/internal/storage/db/learning/
mod.rs1use 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 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}