The Activity panel showed a 12-day hole with two entries stamped "10s ago". Neither is true: the entries exist in ~/.opencrabs/rsi/improvements.md with correct dates, and the parser misreads them.
Mechanism
src/brain/mission_control/activity_service.rs only opens a new draft on a ## line, and applies any field line to whatever draft is currently open:
if let Some(rest) = line.strip_prefix("## ") {
...
current = Some(EntryDraft::from_header(rest));
} else if let Some(draft) = current.as_mut() {
apply_field_line(line, draft); // no guard: applies to the PREVIOUS entry
}
Two compounding defects:
apply_field_line overwrites unconditionally, and a parse failure wipes a good value:
draft.date = parse_date(rest.trim()); // returns None on an unparseable string
finish() invents a timestamp when none survived:
let date = self.date.unwrap_or_else(Utc::now);
Evidence
Two non-conforming blocks in a 722-entry journal.
A headerless block (opens with ---, not ## ). Having never opened a draft, all four of its fields land on the preceding entry:
## [Updated] Add rule about accepting user-confirmed data
**Date:** 2026-07-17 12:16 UTC
**Status:** Updated (surgical replace)
---
**Date:** 2026-07-18 (RSI cycle)
**Target:** SOUL.md
**Status:** Applied
2026-07-18 (RSI cycle) fails %Y-%m-%d %H:%M, so parse_date returns None and destroys the good 2026-07-17 date. The later entry is swallowed whole, and the earlier one renders with the wrong rationale under a fabricated timestamp.
A config-sync block with the date in the header and no **Date:** field:
## 2026-07-26 23:33 UTC - config.toml config sync
Date falls back to now; the title becomes the raw date string, which is what the panel displays.
Blast radius: 1 unparseable date, 2 headers without the [Status] prefix, 721 **Date:** lines for 722 headers.
Fix
The parser is one half; the journal having three writers emitting three shapes is the other.
- Do not let a field line mutate a draft it does not belong to. A
**Date:** arriving when the current draft already has one signals a headerless block: flush and start a new draft rather than overwrite.
- Never overwrite a parsed value with a parse failure.
- Drop the
Utc::now() fallback. An entry with no date should sort and render as undated, not as this instant. Inventing a timestamp is what made a 9-day-old entry look like it happened during the render.
- Accept the date-in-header shape so config-sync entries carry their real time.
Impact
- Activity feed shows fabricated "seconds ago" timestamps for entries days or weeks old
- One journal entry is lost entirely and its fields corrupt a neighbour
- The apparent gap made a working RSI loop look dead for 12 days
The Activity panel showed a 12-day hole with two entries stamped "10s ago". Neither is true: the entries exist in
~/.opencrabs/rsi/improvements.mdwith correct dates, and the parser misreads them.Mechanism
src/brain/mission_control/activity_service.rsonly opens a new draft on a##line, and applies any field line to whatever draft is currently open:Two compounding defects:
apply_field_lineoverwrites unconditionally, and a parse failure wipes a good value:finish()invents a timestamp when none survived:Evidence
Two non-conforming blocks in a 722-entry journal.
A headerless block (opens with
---, not##). Having never opened a draft, all four of its fields land on the preceding entry:2026-07-18 (RSI cycle)fails%Y-%m-%d %H:%M, soparse_datereturnsNoneand destroys the good2026-07-17date. The later entry is swallowed whole, and the earlier one renders with the wrong rationale under a fabricated timestamp.A config-sync block with the date in the header and no
**Date:**field:## 2026-07-26 23:33 UTC - config.toml config syncDate falls back to now; the title becomes the raw date string, which is what the panel displays.
Blast radius: 1 unparseable date, 2 headers without the
[Status]prefix, 721**Date:**lines for 722 headers.Fix
The parser is one half; the journal having three writers emitting three shapes is the other.
**Date:**arriving when the current draft already has one signals a headerless block: flush and start a new draft rather than overwrite.Utc::now()fallback. An entry with no date should sort and render as undated, not as this instant. Inventing a timestamp is what made a 9-day-old entry look like it happened during the render.Impact