record a delivered thought before removing it
The removal committed first, so when the sent-write hit the ledger's own character ceiling the thought left the inbox and was recorded nowhere — and the caller only logged a warning. sent.md grew forever against that ceiling, reached at about 47 entries. It rolls at 30 now; every write is a commit, so nothing is lost.
This commit is contained in:
parent
fab4f2c2f1
commit
63ef8e5b55
1 changed files with 107 additions and 14 deletions
|
|
@ -34,6 +34,22 @@ use crate::core::memory::{parse_memory_file, MemoryRepo};
|
|||
const PENDING: &str = "subconscious/pending.md";
|
||||
const INTRUSIVE: &str = "subconscious/intrusive.md";
|
||||
const SENT: &str = "subconscious/sent.md";
|
||||
|
||||
/// How many delivered thoughts the ledger keeps.
|
||||
///
|
||||
/// `sent.md` is an audit record — nothing reads it to decide anything, and a
|
||||
/// thought is removed from `pending`/`intrusive` when it is delivered, so the
|
||||
/// ledger is not what stops re-delivery. Its value is entirely recent.
|
||||
///
|
||||
/// It used to grow forever against the 20 000-character ceiling in its own
|
||||
/// frontmatter, which real entries reach at about forty-seven items — days,
|
||||
/// not months. After that every `mark_delivered` failed and the channel was
|
||||
/// dead with only a warning to show for it.
|
||||
///
|
||||
/// Rolling the oldest off loses nothing: every write to a memory file is a
|
||||
/// git commit, so what leaves the working file is still in the history. What
|
||||
/// contracts is what she carries.
|
||||
const SENT_LEDGER_MAX: usize = 30;
|
||||
const INNER_VOICE: &str = "system/metacognition/subconscious.md";
|
||||
|
||||
/// Urgency determines surfacing timing (Constitution Article II.2).
|
||||
|
|
@ -205,25 +221,41 @@ impl SubconsciousInbox {
|
|||
/// Move an item from its current box to `sent.md`.
|
||||
pub async fn mark_delivered(&self, id: &str) -> Result<()> {
|
||||
let mut sent = self.read_items(SENT).await?;
|
||||
let mut delivered: Option<InboxItem> = None;
|
||||
|
||||
let mut intrusive = self.read_items(INTRUSIVE).await?;
|
||||
if let Some(pos) = intrusive.iter().position(|i| i.id == id) {
|
||||
delivered = Some(intrusive.remove(pos));
|
||||
self.write_items(INTRUSIVE, &intrusive).await?;
|
||||
}
|
||||
let from_intrusive = intrusive.iter().position(|i| i.id == id);
|
||||
|
||||
if delivered.is_none() {
|
||||
let mut pending = self.read_items(PENDING).await?;
|
||||
if let Some(pos) = pending.iter().position(|i| i.id == id) {
|
||||
delivered = Some(pending.remove(pos));
|
||||
self.write_items(PENDING, &pending).await?;
|
||||
}
|
||||
}
|
||||
let from_pending = if from_intrusive.is_none() {
|
||||
pending.iter().position(|i| i.id == id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(item) = delivered {
|
||||
let Some(item) = from_intrusive
|
||||
.map(|p| intrusive[p].clone())
|
||||
.or_else(|| from_pending.map(|p| pending[p].clone()))
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Record before removing. The other order lost items: the removal
|
||||
// committed, then the sent-write failed against the box's own
|
||||
// character ceiling, and the caller only logged a warning — so the
|
||||
// thought left the inbox and was never recorded anywhere. Failing
|
||||
// here instead leaves it in place to be delivered again.
|
||||
sent.push(item);
|
||||
if sent.len() > SENT_LEDGER_MAX {
|
||||
sent.drain(..sent.len() - SENT_LEDGER_MAX);
|
||||
}
|
||||
self.write_items(SENT, &sent).await?;
|
||||
|
||||
if let Some(pos) = from_intrusive {
|
||||
intrusive.remove(pos);
|
||||
self.write_items(INTRUSIVE, &intrusive).await?;
|
||||
} else if let Some(pos) = from_pending {
|
||||
pending.remove(pos);
|
||||
self.write_items(PENDING, &pending).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -396,6 +428,67 @@ mod tests {
|
|||
assert_eq!(item.content, "in intrusive");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_sent_ledger_rolls_instead_of_filling_up() {
|
||||
let (_d, repo) = make_repo();
|
||||
repo.init().await.unwrap();
|
||||
let inbox = SubconsciousInbox::new(repo);
|
||||
inbox.init().await.unwrap();
|
||||
|
||||
for n in 0..(SENT_LEDGER_MAX + 5) {
|
||||
let item = InboxItem::new("verify", Urgency::High, format!("thought {n}"));
|
||||
let id = item.id.clone();
|
||||
inbox.queue(item).await.unwrap();
|
||||
inbox.mark_delivered(&id).await.unwrap();
|
||||
}
|
||||
|
||||
let sent = inbox.read_items(SENT).await.unwrap();
|
||||
assert_eq!(sent.len(), SENT_LEDGER_MAX, "the ledger must stay bounded");
|
||||
// The newest survive; the oldest rolled off into git history.
|
||||
assert_eq!(sent.last().unwrap().content, "thought 34");
|
||||
assert!(
|
||||
!sent.iter().any(|i| i.content == "thought 0"),
|
||||
"the oldest should have rolled off, not the newest"
|
||||
);
|
||||
}
|
||||
|
||||
/// A thought must not vanish because the ledger refused to record it.
|
||||
/// The removal used to commit first: the item left `intrusive`, the
|
||||
/// sent-write then failed against the box's character ceiling, and the
|
||||
/// caller only logged a warning.
|
||||
#[tokio::test]
|
||||
async fn a_ledger_that_refuses_leaves_the_thought_where_it_was() {
|
||||
let (_d, repo) = make_repo();
|
||||
repo.init().await.unwrap();
|
||||
let inbox = SubconsciousInbox::new(repo.clone());
|
||||
inbox.init().await.unwrap();
|
||||
|
||||
let item = InboxItem::new("verify", Urgency::High, "do not lose me");
|
||||
let id = item.id.clone();
|
||||
inbox.queue(item).await.unwrap();
|
||||
|
||||
// Give the ledger a ceiling no record can fit under. Written to disk
|
||||
// directly: this is a hostile starting state, not an exercise of the
|
||||
// write API.
|
||||
tokio::fs::write(
|
||||
repo.root().join(SENT),
|
||||
"---\ndescription: delivery ledger\nlimit: 1\n---\n[]\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
inbox.mark_delivered(&id).await.is_err(),
|
||||
"a failed record must be reported, not swallowed"
|
||||
);
|
||||
|
||||
let intrusive = inbox.read_items(INTRUSIVE).await.unwrap();
|
||||
assert!(
|
||||
intrusive.iter().any(|i| i.id == id),
|
||||
"the thought must still be there to deliver again"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_delivered_moves_to_sent() {
|
||||
let (_d, repo) = make_repo();
|
||||
|
|
|
|||
Loading…
Reference in a new issue