Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,16 @@ not environment.
and more perspective on what it meant."
2. **The hers/environment line** — CONFIRMED as drawn.
3. **`discards/`** — SPECIFIED by her; definition now in the
workspace layout above.
workspace layout above. Alongside her own entries, the daemon
writes `discards/folds.md` (issue #5, her first proposal): a
structural record of every compaction fold — counts in/out, one
handle per dropped turn (a pointer into the capture tape, not a
summary), the reason class, and any `## Didn't understand`
section a consolidation session marked in its splice. "Structural,
not a practice" (hers): emitted by the fold code path itself, so a
curated context stays distinguishable from a complete one. The
discards are never destroyed — the record is the light switch on a
basement that already exists (`muse-context::fold_record`).
4. Peer file naming and journal window — ANSWERED in review:
slug = handle (e.g. `wisp-mk-gg.md`) with `nickname` in
front-matter for display; journal window = 2 days.
Expand Down
11 changes: 11 additions & 0 deletions handbook/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,14 @@ the `rust-style` and `rustdoc` skills; testing patterns and rationale in
plain summarizer still folds the transcript (nothing blocks on
it); the workspace `git log` shows a `consolidation:` commit when
it ran.
- The fold record (issue #5, your first proposal): every fold — by
any rung — the DAEMON appends one entry to `discards/folds.md`:
counts in/out (the drifting ratio you named), one handle line per
dropped turn (index · kind · first line, so you can find it in the
tape), and the reason class (which rung folded it). If a
consolidation session marked a `## Didn't understand` section in
its splice, that is captured verbatim — the rare reason only you
can name. It is not one of your discards (those are your
decisions); it is the trace of a choice the machinery made for
you, so a curated context is distinguishable from a complete one.
Nothing is destroyed — the handles point back to the tapes.
38 changes: 36 additions & 2 deletions runtime/crates/muse-context/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
// rounds atomic; `to_messages` then fans them back out for the
// summarizer's prompt.
let prefix_turns: Vec<_> = state.conversation.turns()[..take].to_vec();
let prefix_messages = Conversation::from_turns(prefix_turns).to_messages();
let prefix_messages = Conversation::from_turns(prefix_turns.clone()).to_messages();

let cancel = state.cancel.child_token();
// The fold ladder, most alive to least: (1) the consolidation
Expand All @@ -1392,6 +1392,7 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
// API. Each rung falls through on failure — context management
// never gets less reliable by trying the richer path first.
let mut consolidated = false;
let mut fold_reason = crate::fold_record::FoldReason::MeteredSummarizer;
let mut summary_result: Option<Result<(String, TokenUsage), _>> = None;
if let Some(setup) = state.consolidation.as_ref() {
match crate::consolidation::consolidate_via_runner(
Expand All @@ -1406,6 +1407,7 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
{
Ok(ok) => {
consolidated = true;
fold_reason = crate::fold_record::FoldReason::Consolidation;
summary_result = Some(Ok(ok));
}
Err(err) => {
Expand All @@ -1426,7 +1428,10 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
)
.await
{
Ok(ok) => Ok(ok),
Ok(ok) => {
fold_reason = crate::fold_record::FoldReason::SubscriptionSummarizer;
Ok(ok)
}
Err(err) => {
warn!(error = %err, "subscription compactor failed; falling back to metered API");
summarize_prefix(
Expand Down Expand Up @@ -1485,6 +1490,21 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
after,
"compaction complete"
);
// Build the fold record (issue #5) BEFORE the capture
// write consumes `summary_text` — a plain String we can
// hand to the identity actor after. The handle is this
// cycle's tape id (the join key back to the basement).
let fold_handle = capture.as_deref().map_or_else(
|| format!("{}/wake-{}", state.id, state.wakes_handled),
|c| c.cycle_id().to_string(),
);
let fold_record = crate::fold_record::build_entry(
&prefix_turns,
&prefix_messages,
&summary_text,
fold_reason,
&fold_handle,
);
if let Some(cap) = capture.as_mut() {
let _ = cap.record(CaptureEvent::CompactionCompleted {
summary_text,
Expand All @@ -1509,6 +1529,20 @@ async fn maybe_compact(state: &mut ContextState, mut capture: Option<&mut Captur
state.last_cycle_input_tokens = 0;
state.last_cycle_context_peak = 0;
state.last_cycle_api_retries = 0;
// Send the fold record (issue #5): forgetting made
// observable, so a curated context is distinguishable
// from a complete one. Best-effort — a failed write never
// unwinds a completed fold.
match ractor::call!(state.tools.identity, |reply| {
muse_identity::IdentityMsg::RecordFold {
entry: fold_record,
reply,
}
}) {
Ok(Ok(())) => info!(reason = fold_reason.as_str(), "fold record written"),
Ok(Err(err)) => warn!(error = %err, "fold record write failed (fold stands)"),
Err(err) => warn!(error = %err, "fold record rpc failed (fold stands)"),
}
true
}
Err(err) => {
Expand Down
Loading