返回 CodeWhale
lens.rs
根目录 / crates / memory / src / lens.rs
1 //! Engine-owned Context Lens and Whalesong event projection.
2 //!
3 //! A prepared packet is NOT proof of context delivery. Only the trusted engine
4 //! can acknowledge append/dispatch. No tool can upgrade its own authority.
5 use crate::store::Tx;
6 use crate::{
7 Access, Capability, ContextBudget, ContextPacket, Error, Freshness, Memory, MemoryBackend,
8 MemoryRef, Recall, RecallReport, Result, Scope, Snapshot, Store, TokenCounter, compile_context,
9 policy,
10 };
11 use rusqlite::{OptionalExtension, params};
12 use serde::{Deserialize, Serialize};
13 use serde_json::{Value, json};
14 use std::collections::BTreeMap;
15 use uuid::Uuid;
16
17 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
18 pub struct Preferences {
19 pub revision: i64,
20 pub pinned: bool,
21 pub suppressed: bool,
22 }
23 #[derive(Debug, Clone, Serialize, Deserialize)]
24 pub struct LensEntry {
25 pub memory: Memory,
26 pub freshness: Freshness,
27 pub preferences: Preferences,
28 pub in_contexts: Vec<String>,
29 }
30 #[derive(Debug, Clone, Serialize, Deserialize)]
31 pub struct ContextReceipt {
32 pub id: String,
33 pub trace_id: String,
34 pub stage: String,
35 pub packet_hash: String,
36 pub created_at: i64,
37 pub appended_at: Option<i64>,
38 pub dispatched_at: Option<i64>,
39 pub used_units: usize,
40 pub unit: String,
41 pub omitted: usize,
42 pub selected: Vec<MemoryRef>,
43 /// Current knowledge cannot rewrite what a previous request contained.
44 pub invalidated_ids: Vec<String>,
45 }
46 #[derive(Debug, Clone, Serialize, Deserialize)]
47 pub struct LensSnapshot {
48 pub schema: String,
49 pub entries: Vec<LensEntry>,
50 pub next_after: Option<String>,
51 pub contexts: Vec<ContextReceipt>,
52 pub as_of: i64,
53 pub can_review: bool,
54 pub can_forget: bool,
55 pub can_dispatch: bool,
56 }
57 #[derive(Debug, Clone, Serialize, Deserialize)]
58 pub struct PreparedContext {
59 pub receipt: ContextReceipt,
60 pub packet: ContextPacket,
61 }
62 #[derive(Debug, Clone, Serialize, Deserialize)]
63 pub struct EventPage {
64 pub schema: String,
65 pub producer: String,
66 pub epoch: String,
67 pub events: Vec<Value>,
68 pub next_after: i64,
69 pub has_more: bool,
70 /// A filtered feed's seq gaps are not evidence of dropped records.
71 pub sequence_domain: String,
72 }
73 #[derive(Debug, Clone, Serialize, Deserialize)]
74 pub struct HistoricalMemory {
75 pub memory: Memory,
76 pub known_at: i64,
77 pub valid_at: i64,
78 pub history_starts_at: i64,
79 pub valid: bool,
80 /// Historical file contents are not reconstructed by this API.
81 pub repository_freshness: String,
82 }
83
84 fn identity(value: &str) -> Result<()> {
85 if value.is_empty()
86 || value.len() > 128
87 || !value
88 .bytes()
89 .all(|b| b.is_ascii_alphanumeric() || b"_-.:".contains(&b))
90 {
91 return Err(Error::Invalid("expected a bounded opaque identity".into()));
92 }
93 Ok(())
94 }
95 #[allow(clippy::too_many_arguments)]
96 fn metadata_event(
97 conn: &rusqlite::Connection,
98 scope: &Scope,
99 entity: &str,
100 revision: i64,
101 action: &str,
102 now: i64,
103 trace: Option<&str>,
104 context: Option<&str>,
105 units: Option<usize>,
106 unit: Option<&str>,
107 ) -> Result<()> {
108 conn.execute("INSERT INTO memory_outbox(scope,entity_id,revision,action,recorded_at,trace_id,context_id,units,unit) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9)",
109 params![scope.key()?,entity,revision,action,now,trace,context,units.map(|n|n as i64),unit])?;
110 Ok(())
111 }
112
113 impl Store {
114 pub fn preferences(&self, access: &Access, id: &str) -> Result<Preferences> {
115 self.get(access, id)?;
116 Ok(self
117 .conn
118 .query_row(
119 "SELECT revision,pinned,suppressed FROM memory_preferences WHERE memory_id=?1",
120 [id],
121 |r| {
122 Ok(Preferences {
123 revision: r.get(0)?,
124 pinned: r.get(1)?,
125 suppressed: r.get(2)?,
126 })
127 },
128 )
129 .optional()?
130 .unwrap_or_default())
131 }
132 /// A pin affects context packing; it never bypasses validity, scope or review.
133 /// Suppression affects future recall only, not context already sent.
134 pub fn set_preferences(
135 &self,
136 access: &Access,
137 id: &str,
138 expected: i64,
139 pinned: bool,
140 suppressed: bool,
141 ) -> Result<Preferences> {
142 if !(0..i64::MAX).contains(&expected) {
143 return Err(Error::Invalid("invalid preference revision".into()));
144 }
145 let tx = Tx::begin(&self.conn)?;
146 let m = self.get(access, id)?;
147 access.write(&m.draft.scope, Capability::Review)?;
148 let old = self.preferences(access, id)?;
149 if old.revision == expected + 1 && old.pinned == pinned && old.suppressed == suppressed {
150 tx.commit()?;
151 return Ok(old);
152 }
153 if old.revision != expected {
154 return Err(Error::RevisionConflict);
155 }
156 self.conn.execute("INSERT INTO memory_preferences(memory_id,revision,pinned,suppressed) VALUES(?1,?2,?3,?4) ON CONFLICT(memory_id) DO UPDATE SET revision=excluded.revision,pinned=excluded.pinned,suppressed=excluded.suppressed",
157 params![id,expected+1,pinned,suppressed])?;
158 metadata_event(
159 &self.conn,
160 &m.draft.scope,
161 id,
162 m.revision,
163 if suppressed {
164 "suppressed"
165 } else if pinned {
166 "pinned"
167 } else {
168 "unrestricted"
169 },
170 self.timestamp(),
171 None,
172 None,
173 None,
174 None,
175 )?;
176 let flags = match (pinned, suppressed) {
177 (true, true) => "preferences:11",
178 (true, false) => "preferences:10",
179 (false, true) => "preferences:01",
180 (false, false) => "preferences:00",
181 };
182 self.conn.execute(
183 "UPDATE memory_outbox SET reason_code=?1 WHERE seq=last_insert_rowid()",
184 [flags],
185 )?;
186 tx.commit()?;
187 Ok(Preferences {
188 revision: expected + 1,
189 pinned,
190 suppressed,
191 })
192 }
193 pub fn lens_snapshot(
194 &self,
195 access: &Access,
196 trace: Option<&str>,
197 after: Option<&str>,
198 limit: usize,
199 snapshot: &Snapshot,
200 ) -> Result<LensSnapshot> {
201 let tx = Tx::begin(&self.conn)?;
202 let limit = limit.clamp(1, 100);
203 let mut records = self.list(access, after, limit + 1)?;
204 let more = records.len() > limit;
205 records.truncate(limit);
206 let contexts = self.context_receipts(access, trace, snapshot)?;
207 let mut entries = Vec::new();
208 for m in records {
209 let preferences = self.preferences(access, &m.id)?;
210 let freshness = self.freshness(access, &m, snapshot)?;
211 let in_contexts = contexts
212 .iter()
213 .filter(|c| c.stage != "prepared" && c.selected.iter().any(|r| r.id == m.id))
214 .map(|c| c.id.clone())
215 .collect();
216 entries.push(LensEntry {
217 memory: m,
218 freshness,
219 preferences,
220 in_contexts,
221 });
222 }
223 let next_after = if more {
224 entries.last().map(|e| e.memory.id.clone())
225 } else {
226 None
227 };
228 let out = LensSnapshot {
229 schema: "codewhale.memory.lens/v1".into(),
230 entries,
231 next_after,
232 contexts,
233 as_of: self.timestamp(),
234 can_review: access.has(Capability::Review),
235 can_forget: access.has(Capability::Forget),
236 can_dispatch: access.has(Capability::ContextDispatch),
237 };
238 tx.commit()?;
239 Ok(out)
240 }
241 /// Two clocks: status known by `known_at`, fact valid at `valid_at`.
242 /// Later supersession is not leaked into historical status. Forgotten content
243 /// is unavailable at every cutoff. Migrated stores expose only known history.
244 pub fn memory_as_of(
245 &self,
246 access: &Access,
247 id: &str,
248 known_at: i64,
249 valid_at: i64,
250 ) -> Result<HistoricalMemory> {
251 if known_at < 0 || valid_at < 0 || known_at > self.timestamp() {
252 return Err(Error::Invalid("invalid historical cutoff".into()));
253 }
254 let tx = Tx::begin(&self.conn)?;
255 let mut memory = self.get(access, id)?;
256 let (revision,status,recorded): (i64,String,i64)=self.conn.query_row(
257 "SELECT revision,status,recorded_at FROM memory_history WHERE memory_id=?1 AND recorded_at<=?2 ORDER BY seq DESC LIMIT 1",
258 params![id,known_at],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?.ok_or(Error::NotFound)?;
259 let starts = self.conn.query_row(
260 "SELECT min(recorded_at) FROM memory_history WHERE memory_id=?1",
261 [id],
262 |r| r.get(0),
263 )?;
264 memory.revision = revision;
265 memory.status = crate::Status::parse(&status)?;
266 memory.updated_at = recorded;
267 let d = &memory.draft;
268 let valid = d.valid_from.is_none_or(|t| t <= valid_at)
269 && d.valid_until.is_none_or(|t| valid_at < t)
270 && d.expires_at.is_none_or(|t| valid_at < t);
271 let result = HistoricalMemory {
272 memory,
273 known_at,
274 valid_at,
275 history_starts_at: starts,
276 valid,
277 repository_freshness: "not_reconstructed".into(),
278 };
279 tx.commit()?;
280 Ok(result)
281 }
282 /// The read-only half of `prepare_context`: filtered recall plus explicit
283 /// pins, compiled into a packet — with no receipt recorded. Host surfaces
284 /// that assemble the same bytes but have no session scope to attest
285 /// (previews, reports, frozen prompts) must use this so the traced and
286 /// untraced paths can never diverge on pin membership or packet shape.
287 pub fn context_packet(
288 &self,
289 access: &Access,
290 query: &Recall,
291 counter: &dyn TokenCounter,
292 budget: &ContextBudget,
293 ) -> Result<(RecallReport, ContextPacket)> {
294 let mut q = query.clone();
295 q.include_stale = false;
296 let mut report = self.recall(access, &q)?;
297 // Explicit pins are included even when outside the lexical shortlist.
298 let pinned: Vec<String> = {
299 let mut stmt=self.conn.prepare("SELECT p.memory_id FROM memory_preferences p JOIN memories m ON m.id=p.memory_id WHERE p.pinned=1 AND p.suppressed=0 AND m.scope IN(SELECT value FROM json_each(?1)) ORDER BY p.memory_id LIMIT 64")?;
300 stmt.query_map([access.scope_json()?], |r| r.get(0))?
301 .collect::<rusqlite::Result<Vec<_>>>()?
302 };
303 for id in &pinned {
304 if report.hits.iter().any(|h| &h.memory.id == id) {
305 continue;
306 }
307 let memory = self.get(access, id)?;
308 let freshness = self.freshness(access, &memory, &query.snapshot)?;
309 if freshness == Freshness::Current {
310 report.hits.push(crate::Hit {
311 memory,
312 freshness,
313 score: 0.0,
314 reasons: vec!["explicit_pin".into()],
315 });
316 }
317 }
318 report.hits.sort_by_key(|h| !pinned.contains(&h.memory.id));
319 let packet = compile_context(&report.hits, counter, budget)?;
320 Ok((report, packet))
321 }
322 /// Called by the host at a meaningful retrieval boundary, not every token.
323 /// Idempotent within a trace + host request key. Text is returned, not logged.
324 #[allow(clippy::too_many_arguments)]
325 pub fn prepare_context(
326 &self,
327 access: &Access,
328 context_scope: &Scope,
329 trace: &str,
330 request_key: &str,
331 query: &Recall,
332 counter: &dyn TokenCounter,
333 budget: &ContextBudget,
334 ) -> Result<PreparedContext> {
335 access.write(context_scope, Capability::ContextDispatch)?;
336 identity(trace)?;
337 identity(request_key)?;
338 if context_scope.session.is_none() {
339 return Err(Error::Invalid(
340 "context receipts require a session scope".into(),
341 ));
342 }
343 let tx = Tx::begin(&self.conn)?;
344 let input_hash = policy::sha256(&serde_json::to_vec(
345 &json!({"q":query.query,"snapshot":query.snapshot,
346 "limit":query.limit,"embedding":query.embedding,"graph":query.expand_graph,"vector_cap":query.vector_scan_limit,
347 "scopes":access.scope_json()?,"budget":[budget.max_units,budget.max_bytes,budget.max_entries],"unit":counter.unit()}),
348 )?);
349 let (report, packet) = self.context_packet(access, query, counter, budget)?;
350 let packet_hash = policy::sha256(packet.text.as_bytes());
351 let old:Option<(String,String,String)>=self.conn.query_row("SELECT id,input_hash,packet_hash FROM memory_contexts WHERE scope=?1 AND trace_id=?2 AND request_key=?3",
352 params![context_scope.key()?,trace,request_key],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?;
353 if let Some((id, old_input, old_packet)) = old {
354 if old_input != input_hash {
355 return Err(Error::IdempotencyConflict);
356 }
357 if old_packet != packet_hash {
358 return Err(Error::RevisionConflict);
359 }
360 let receipt = self
361 .context_receipts(access, Some(trace), &query.snapshot)?
362 .into_iter()
363 .find(|c| c.id == id)
364 .ok_or(Error::NotFound)?;
365 tx.commit()?;
366 return Ok(PreparedContext { receipt, packet });
367 }
368 let id = Uuid::new_v4().to_string();
369 let now = self.timestamp();
370 self.conn.execute("INSERT INTO memory_contexts(id,scope,trace_id,request_key,input_hash,packet_hash,stage,created_at,used_units,unit,omitted) VALUES(?1,?2,?3,?4,?5,?6,'prepared',?7,?8,?9,?10)",
371 params![id,context_scope.key()?,trace,request_key,input_hash,packet_hash,now,packet.used_units as i64,packet.unit,packet.omitted as i64])?;
372 for (position, r) in packet.selected.iter().enumerate() {
373 self.conn.execute("INSERT INTO memory_context_refs(context_id,memory_id,revision,content_hash,position) VALUES(?1,?2,?3,?4,?5)",params![id,r.id,r.revision,r.content_hash,position as i64])?;
374 }
375 for hit in &report.hits {
376 metadata_event(
377 &self.conn,
378 &hit.memory.draft.scope,
379 &hit.memory.id,
380 hit.memory.revision,
381 "retrieved",
382 now,
383 Some(trace),
384 Some(&id),
385 None,
386 None,
387 )?;
388 }
389 for r in &packet.selected {
390 let memory = self.get(access, &r.id)?;
391 metadata_event(
392 &self.conn,
393 &memory.draft.scope,
394 &r.id,
395 r.revision,
396 "context_prepared",
397 now,
398 Some(trace),
399 Some(&id),
400 Some(packet.used_units),
401 Some(&packet.unit),
402 )?;
403 }
404 let receipt = self
405 .context_receipts(access, Some(trace), &query.snapshot)?
406 .into_iter()
407 .find(|c| c.id == id)
408 .ok_or(Error::NotFound)?;
409 tx.commit()?;
410 Ok(PreparedContext { receipt, packet })
411 }
412 /// Before-action gate. The host must serialize this check and append/dispatch
413 /// against mutations within its single owner. The acknowledgement is separate.
414 pub fn preflight_context(
415 &self,
416 access: &Access,
417 id: &str,
418 packet_hash: &str,
419 snapshot: &Snapshot,
420 ) -> Result<()> {
421 access.require(Capability::ContextDispatch)?;
422 let tx = Tx::begin(&self.conn)?;
423 let stored:Option<String>=self.conn.query_row("SELECT packet_hash FROM memory_contexts WHERE id=?1 AND scope IN(SELECT value FROM json_each(?2))",params![id,access.scope_json()?],|r|r.get(0)).optional()?;
424 if stored.as_deref() != Some(packet_hash) {
425 return Err(Error::RevisionConflict);
426 }
427 for r in self.context_refs(id)? {
428 let m = self.get(access, &r.id)?;
429 if m.revision != r.revision
430 || m.content_hash != r.content_hash
431 || self.freshness(access, &m, snapshot)? != Freshness::Current
432 || self.preferences(access, &r.id)?.suppressed
433 {
434 return Err(Error::RevisionConflict);
435 }
436 }
437 tx.commit()?;
438 Ok(())
439 }
440 /// Invoke ONLY after the engine has durably appended these exact packet bytes.
441 /// An error means missing acknowledgement, not permission to append twice.
442 pub fn acknowledge_append(
443 &self,
444 access: &Access,
445 id: &str,
446 packet_hash: &str,
447 history_sequence: i64,
448 snapshot: &Snapshot,
449 ) -> Result<()> {
450 access.require(Capability::ContextDispatch)?;
451 if history_sequence < 0 {
452 return Err(Error::Invalid("invalid history sequence".into()));
453 }
454 self.advance_context(
455 access,
456 id,
457 packet_hash,
458 Some(history_sequence),
459 None,
460 snapshot,
461 )
462 }
463 /// Invoke after a transport dispatch. Does not claim provider acceptance,
464 /// successful execution, useful recall, or causal attribution of an outcome.
465 pub fn acknowledge_dispatch(
466 &self,
467 access: &Access,
468 id: &str,
469 packet_hash: &str,
470 transport_key: &str,
471 snapshot: &Snapshot,
472 ) -> Result<()> {
473 access.require(Capability::ContextDispatch)?;
474 identity(transport_key)?;
475 self.advance_context(access, id, packet_hash, None, Some(transport_key), snapshot)
476 }
477 fn advance_context(
478 &self,
479 access: &Access,
480 id: &str,
481 hash: &str,
482 history: Option<i64>,
483 transport: Option<&str>,
484 snapshot: &Snapshot,
485 ) -> Result<()> {
486 let tx = Tx::begin(&self.conn)?;
487 let (scope,trace,stage,stored_hash,old_history,old_transport):(String,String,String,String,Option<i64>,Option<String>)=
488 self.conn.query_row("SELECT scope,trace_id,stage,packet_hash,history_sequence,transport_key FROM memory_contexts WHERE id=?1 AND scope IN(SELECT value FROM json_each(?2))",params![id,access.scope_json()?],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?,r.get(5)?))).optional()?.ok_or(Error::NotFound)?;
489 let scope: Scope = serde_json::from_str(&scope)?;
490 access.write(&scope, Capability::ContextDispatch)?;
491 if hash != stored_hash {
492 return Err(Error::RevisionConflict);
493 }
494 if let Some(n) = history
495 && let Some(old) = old_history
496 {
497 return if old == n {
498 Ok(())
499 } else {
500 Err(Error::IdempotencyConflict)
501 };
502 }
503 if let Some(key) = transport
504 && let Some(old) = old_transport
505 {
506 return if old == key {
507 Ok(())
508 } else {
509 Err(Error::IdempotencyConflict)
510 };
511 }
512 let expected = if history.is_some() {
513 "prepared"
514 } else {
515 "appended"
516 };
517 if stage != expected {
518 return Err(Error::InvalidState);
519 }
520 let refs = self.context_refs(id)?;
521 // This records an already-observed engine action, even when knowledge
522 // became stale afterwards. `preflight_context` is the BEFORE-action gate.
523 // Lens inspection marks invalidated references without rewriting history.
524 let _ = snapshot;
525 let now = self.timestamp();
526 let (next, action) = if history.is_some() {
527 ("appended", "context_appended")
528 } else {
529 ("dispatched", "context_dispatched")
530 };
531 if let Some(sequence) = history {
532 self.conn.execute("UPDATE memory_contexts SET stage='appended',appended_at=?2,history_sequence=?3 WHERE id=?1",params![id,now,sequence])?;
533 } else {
534 self.conn.execute("UPDATE memory_contexts SET stage='dispatched',dispatched_at=?2,transport_key=?3 WHERE id=?1",params![id,now,transport])?;
535 }
536 for r in refs {
537 let m = self.get(access, &r.id)?;
538 metadata_event(
539 &self.conn,
540 &m.draft.scope,
541 &r.id,
542 r.revision,
543 action,
544 now,
545 Some(&trace),
546 Some(id),
547 None,
548 None,
549 )?;
550 }
551 let _ = next;
552 tx.commit()?;
553 Ok(())
554 }
555 fn context_refs(&self, id: &str) -> Result<Vec<MemoryRef>> {
556 let mut q=self.conn.prepare("SELECT memory_id,revision,content_hash FROM memory_context_refs WHERE context_id=?1 ORDER BY position")?;
557 Ok(q.query_map([id], |r| {
558 Ok(MemoryRef {
559 id: r.get(0)?,
560 revision: r.get(1)?,
561 content_hash: r.get(2)?,
562 })
563 })?
564 .collect::<rusqlite::Result<Vec<_>>>()?)
565 }
566 pub fn context_receipts(
567 &self,
568 access: &Access,
569 trace: Option<&str>,
570 snapshot: &Snapshot,
571 ) -> Result<Vec<ContextReceipt>> {
572 access.require(Capability::Read)?;
573 if let Some(t) = trace {
574 identity(t)?;
575 }
576 let mut stmt=self.conn.prepare("SELECT id,trace_id,stage,packet_hash,created_at,appended_at,dispatched_at,used_units,unit,omitted FROM memory_contexts WHERE scope IN(SELECT value FROM json_each(?1)) AND (?2 IS NULL OR trace_id=?2) ORDER BY created_at DESC,rowid DESC LIMIT 100")?;
577 let mut receipts = stmt
578 .query_map(params![access.scope_json()?, trace], |r| {
579 Ok(ContextReceipt {
580 id: r.get(0)?,
581 trace_id: r.get(1)?,
582 stage: r.get(2)?,
583 packet_hash: r.get(3)?,
584 created_at: r.get(4)?,
585 appended_at: r.get(5)?,
586 dispatched_at: r.get(6)?,
587 used_units: r.get::<_, i64>(7)? as usize,
588 unit: r.get(8)?,
589 omitted: r.get::<_, i64>(9)? as usize,
590 selected: vec![],
591 invalidated_ids: vec![],
592 })
593 })?
594 .collect::<rusqlite::Result<Vec<_>>>()?;
595 for c in &mut receipts {
596 let refs = self.context_refs(&c.id)?;
597 // Fail closed on a narrower capability grant; never reveal a referenced
598 // global/private identity merely because the caller can read a session.
599 for r in refs {
600 let m = match self.get(access, &r.id) {
601 Ok(m) => m,
602 Err(Error::NotFound) => continue,
603 Err(e) => return Err(e),
604 };
605 if m.revision != r.revision
606 || m.content_hash != r.content_hash
607 || self.freshness(access, &m, snapshot)? != Freshness::Current
608 || self.preferences(access, &r.id)?.suppressed
609 {
610 c.invalidated_ids.push(r.id.clone());
611 }
612 c.selected.push(r);
613 }
614 }
615 Ok(receipts)
616 }
617 /// Metadata-only Whalesong event-v1 export. UUIDs/hashes/timing are linkable,
618 /// not anonymous. No body/title/evidence URI/query/embedding enters the feed.
619 pub fn event_page(&self, access: &Access, after: i64, limit: usize) -> Result<EventPage> {
620 access.require(Capability::Read)?;
621 if after < 0 {
622 return Err(Error::Invalid("negative cursor".into()));
623 }
624 let (producer, epoch): (String, String) = self.conn.query_row(
625 "SELECT producer,epoch FROM memory_observer WHERE singleton=1",
626 [],
627 |r| Ok((r.get(0)?, r.get(1)?)),
628 )?;
629 let mut q=self.conn.prepare("SELECT seq,scope,entity_id,revision,action,recorded_at,trace_id,context_id,units,unit,reason_code FROM memory_outbox WHERE seq>?1 AND scope IN(SELECT value FROM json_each(?2)) ORDER BY seq LIMIT ?3")?;
630 let limit = limit.clamp(1, 500);
631 let records = q
632 .query_map(
633 params![after, access.scope_json()?, (limit + 1) as i64],
634 |r| {
635 Ok((
636 r.get::<_, i64>(0)?,
637 r.get::<_, String>(1)?,
638 r.get::<_, String>(2)?,
639 r.get::<_, i64>(3)?,
640 r.get::<_, String>(4)?,
641 r.get::<_, i64>(5)?,
642 r.get::<_, Option<String>>(6)?,
643 r.get::<_, Option<String>>(7)?,
644 r.get::<_, Option<i64>>(8)?,
645 r.get::<_, Option<String>>(9)?,
646 r.get::<_, Option<String>>(10)?,
647 ))
648 },
649 )?
650 .collect::<rusqlite::Result<Vec<_>>>()?;
651 let more = records.len() > limit;
652 let mut cursor = after;
653 let mut events = Vec::new();
654 for (seq, scope, id, revision, action, time, trace, context, units, unit, reason_code) in
655 records.into_iter().take(limit)
656 {
657 cursor = seq;
658 let mut attributes = BTreeMap::<String, Value>::new();
659 for (k, v) in [
660 ("memory.schema", json!(1)),
661 ("memory.id", json!(id)),
662 ("memory.scope", json!(policy::sha256(scope.as_bytes()))),
663 ("memory.revision", json!(revision)),
664 ("memory.action", json!(action)),
665 ("memory.producer", json!(producer)),
666 ("memory.epoch", json!(epoch)),
667 ("memory.sequence", json!(seq)),
668 ("memory.receipt_delay_ms", json!(0)),
669 ("memory.origin", json!("engine")),
670 ("memory.sequence_domain", json!("filtered_store")),
671 ] {
672 attributes.insert(k.into(), v);
673 }
674 if let Some(flags) = reason_code
675 .as_deref()
676 .and_then(|s| s.strip_prefix("preferences:"))
677 && matches!(flags, "00" | "01" | "10" | "11")
678 {
679 attributes.insert("memory.pinned".into(), json!(flags.starts_with('1')));
680 attributes.insert("memory.suppressed".into(), json!(flags.ends_with('1')));
681 }
682 if let Some(c) = context {
683 attributes.insert("memory.context_id".into(), json!(c));
684 }
685 if let Some(n) = units {
686 attributes.insert("memory.budget_units".into(), json!(n));
687 }
688 if let Some(u) = unit {
689 attributes.insert("memory.budget_unit".into(), json!(u));
690 }
691 events.push(json!({"schemaVersion":1,"id":format!("memory-{producer}-{seq}"),
692 "traceId":trace.unwrap_or_else(||format!("memory-{producer}")),"startTime":time.saturating_mul(1000),"endTime":time.saturating_mul(1000),
693 "agentId":"memory-engine","category":"memory","subtype":format!("memory.{action}"),"name":format!("memory.{action}"),"status":"success","attributes":attributes}));
694 }
695 Ok(EventPage {
696 schema: "codewhale.memory.events/v1".into(),
697 producer,
698 epoch,
699 events,
700 next_after: cursor,
701 has_more: more,
702 sequence_domain: "filtered_store".into(),
703 })
704 }
705 }
706
707 impl Store {
708 pub fn numeric_id(&self, access: &Access, id: &str) -> Result<i64> {
709 self.get(access, id)?;
710 Ok(self.conn.query_row(
711 "SELECT id FROM memory_numeric_aliases WHERE memory_id=?1",
712 [id],
713 |r| r.get(0),
714 )?)
715 }
716 pub fn get_numeric(&self, access: &Access, id: i64) -> Result<Memory> {
717 access.require(Capability::Read)?;
718 let memory: Option<String> = self
719 .conn
720 .query_row(
721 "SELECT memory_id FROM memory_numeric_aliases WHERE id=?1",
722 [id],
723 |r| r.get(0),
724 )
725 .optional()?
726 .flatten();
727 self.get(access, &memory.ok_or(Error::NotFound)?)
728 }
729 pub fn replacement_of(&self, access: &Access, id: &str) -> Result<Option<Memory>> {
730 self.get(access, id)?;
731 let replacement: Option<String> = self
732 .conn
733 .query_row(
734 "SELECT new_id FROM replacements WHERE old_id=?1",
735 [id],
736 |r| r.get(0),
737 )
738 .optional()?;
739 replacement.map(|id| self.get(access, &id)).transpose()
740 }
741 /// Explicit local-maintenance inventory, not a model-facing cross-repo read.
742 /// Requires the user scope plus Maintenance and returns no source text.
743 pub fn local_scope_inventory(&self, access: &Access) -> Result<Vec<Scope>> {
744 access.require(Capability::Maintenance)?;
745 let owner = access
746 .scopes()
747 .find(|s| s.workspace.is_none() && s.session.is_none())
748 .ok_or(Error::Denied)?;
749 let mut stmt=self.conn.prepare("SELECT DISTINCT scope FROM memories WHERE json_extract(scope,'$.tenant')=?1 AND json_extract(scope,'$.user')=?2 ORDER BY scope LIMIT 10000")?;
750 let rows = stmt
751 .query_map(params![owner.tenant, owner.user], |r| r.get::<_, String>(0))?
752 .collect::<rusqlite::Result<Vec<_>>>()?;
753 rows.into_iter()
754 .map(|s| serde_json::from_str(&s).map_err(Into::into))
755 .collect()
756 }
757 }
758
758 lines RUST