Given tests: Action::Turn (not To), send_turn of epoch 1, clippy on
compact_summary, jsonl-without-baseline, live turn after compact sends
the carry. send_turn maps Damaged to Local("log") as read_turn did.
Gate counts 1484 → 1508. Mutations recorded in the README.
Co-Authored-By: Grok 4.6 <noreply@x.ai>
9.9 KiB
M5d-1 task 08: compact after a turn, and retry once on SessionFull
Branch: m5d1. Check it: git branch --show-current prints m5d1 and git status --short
is empty; otherwise stop. Never switch branch.
Commit subject: loopd: compact after a full turn and retry SessionFull once
Log row: the Task cell is M5d1/08-loopd-compact-turn.
Goal
After run_turn returns, finish_turn decides whether to compact (spec 3.3):
- A successful turn whose last
Usageisdueis compacted before the caller returns. If compact fails, the turn stays successful and the session stays on its epoch;loopdlogs the compact error with the runbook pointer. TurnError::SessionFullis compacted, then that user message isrun_turn'd once in the new epoch. If compact fails, the error staysSessionFull. The retry is not compacted again.- Any other
TurnErroris returned as it is. No compact.
Runtime does not gain MemorySource (given turn tests build a Runtime without one).
finish_turn takes memory as its own argument.
The channel holds the session busy for this whole call: handle runs finish_turn before
drop(held). A compact is not an idle timer and does not start after the busy set releases.
Live turns still send slots.main (task 03). Only the summary call uses the background slot.
Every place that changes
| Place | What happens there |
|---|---|
crates/loopd/src/compact.rs |
finish_turn and its two helpers (edit F1) |
crates/loopd/src/channel.rs |
handle calls finish_turn before drop(held) (edits H1, H2) |
crates/loopd/tests/compact_turn.rs |
new, given |
| everything else | no change: not turn.rs, not Runtime, not existing turn / channel tests |
Files
- Copy, from
docs/plans/M5d1/files/(the same path under it): the new testcrates/loopd/tests/compact_turn.rs. - Modify:
crates/loopd/src/compact.rs,crates/loopd/src/channel.rs,docs/implementer-log.md. - Do not touch: any other file.
Facts you need
-
duelooks atsession.last_usage(), which is the current epoch's lastUsage. After a successfulrun_turnthat record is there. AfterSessionFull,run_turnhas already appended theUserand returned before anyUsage; compact runs because of the error, not because ofdue. -
Compact helper.
summarisethencommit. Log failures withloopd: could not compact session <id>: <CompactError Display>Displayalready includes the runbook pointer, matching the runbook's example line. -
Retry.
run_turnappends the user message again in the new epoch. The old epoch keeps itsUser(Honcho skips it: no reply).turns()is 2. That is accepted (README, known limits). -
Existing channel tests stay green. Their
n_ctxis 131072 and fixtureprompt_nis tens, sodueis false.SessionFullwithoutcompact.mdinHomestaysSessionFull(the successful-turn-when-compact-fails behaviour, on the error path). -
compact.rsstays ≤ 500 lines. After this task it is about 250. -
channel.rsstays ≤ 500 lines. After this task it is about 290. -
The items the edits use. This table is complete.
Item What it does duetask 05 summarise/committasks 06, 07 run_turnretry the same textandspeakerSession::last_usagecurrent epoch Config.compact.threshold,expect.n_ctxthe dueargumentsMemorySourcefinish_turn's extra argument; channel passes&*ctx.memoryHeldalready in channel.rs; drop it afterfinish_turn
The edits, exactly
Each quoted "before" block occurs exactly once after task 07 (channel.rs is still as on
master, except rustfmt).
crates/loopd/src/compact.rs
F1. Extend the crate uses. After task 07 they include MemorySource, Session, Config,
Registry. Add:
use crate::turn::{Runtime, TurnError, TurnOutcome, run_turn};
use bxcore::TurnEvent;
Keep rustfmt's order of use lines; if it moves them, accept that.
Then after commit, add these three functions whole. There is no todo!().
/// After `run_turn`: compact when the last `Usage` is due, or on `SessionFull` (then retry the
/// user message once). Compact failure on a successful turn keeps the success; on `SessionFull`
/// keeps `SessionFull`. Other errors skip compact.
pub fn finish_turn(
session: &mut Session,
rt: &Runtime<'_>,
memory: &dyn MemorySource,
text: &str,
speaker: Option<&str>,
on_event: &mut dyn FnMut(&TurnEvent),
outcome: Result<TurnOutcome, TurnError>,
) -> Result<TurnOutcome, TurnError> {
match outcome {
Err(TurnError::SessionFull) => match compact_session(session, rt, memory) {
Ok(()) => run_turn(session, rt, text, speaker, on_event),
Err(e) => {
log_compact(session, &e);
Err(TurnError::SessionFull)
}
},
Ok(done) => {
if let Some(usage) = session.last_usage()
&& due(&usage, rt.cfg.compact.threshold, rt.cfg.expect.n_ctx)
&& let Err(e) = compact_session(session, rt, memory)
{
log_compact(session, &e);
}
Ok(done)
}
Err(e) => Err(e),
}
}
fn compact_session(
session: &mut Session,
rt: &Runtime<'_>,
memory: &dyn MemorySource,
) -> Result<(), CompactError> {
let summary = summarise(rt.cfg, rt.client, session)?;
commit(session, rt.cfg, rt.registry, memory, summary)
}
fn log_compact(session: &Session, e: &CompactError) {
eprintln!(
"loopd: could not compact session {}: {e}",
session.id().as_str()
);
}
The Ok(done) arm uses && let Err(e) = ... so a compact failure is logged and the original
done is still returned. Do not ? that error.
crates/loopd/src/channel.rs
H1. The run_turn call and the drop(held) that follows it. The lines
let outcome = run_turn(
&mut session,
&runtime,
&turn.content,
turn.speaker.as_deref(),
&mut |event| {
let _ = write_frame(
&mut stream,
&Envelope {
v: PROTOCOL_VERSION,
id: request.id,
r#final: false,
msg: Message::TurnEvent(event.clone()),
},
);
},
);
// 5. Release the session before the final frame, so the next turn does not find it busy.
drop(held);
become
let mut emit = |event: &bxcore::TurnEvent| {
let _ = write_frame(
&mut stream,
&Envelope {
v: PROTOCOL_VERSION,
id: request.id,
r#final: false,
msg: Message::TurnEvent(event.clone()),
},
);
};
let outcome = run_turn(
&mut session,
&runtime,
&turn.content,
turn.speaker.as_deref(),
&mut emit,
);
let outcome = crate::compact::finish_turn(
&mut session,
&runtime,
&*ctx.memory,
&turn.content,
turn.speaker.as_deref(),
&mut emit,
outcome,
);
// 5. Release the session before the final frame, so the next turn does not find it busy.
// Compaction ran inside the hold: a second turn on this session waits until the new
// epoch is on disk (or compact failed and the epoch is unchanged).
drop(held);
H2. Do not add MemorySource to Runtime. Do not change turn_code. The memory.updated
block after drop(held) stays: a successful compact-and-retry still reports session.turns(),
which now spans epochs.
Steps
-
1. Copy the given test.
cp docs/plans/M5d1/files/crates/loopd/tests/compact_turn.rs crates/loopd/tests/ -
2. See it fail.
cargo build --tests --keep-going -p loopd, timeout 600000 ms: test targetcompact_turndoes not compile (E0599no functionfinish_turn). Every other target compiles, includingturnandchannel. -
3. The edits. F1, H1.
cargo check -p loopdprints no error and no warning.wc -l crates/loopd/src/compact.rsandwc -l crates/loopd/src/channel.rseach print a number ≤ 500. -
4. The tests.
cargo test -p loopd --test compact_turn --test turn --test channel --test compact_advance: every test passes, 6 incompact_turnand the others as before. -
5. Check.
git diff --statlists exactlycrates/loopd/src/compact.rsandcrates/loopd/src/channel.rs.git status --shortshowscompact_turn.rsas new. -
6. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1508 (1502 before, plus 6). -
7. Log and commit. Task cell
M5d1/08-loopd-compact-turn. Stage withgit add crates/loopd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1508 tests.git diff HEAD~1 --statlists onlycrates/loopd/src/compact.rs,crates/loopd/src/channel.rs,crates/loopd/tests/compact_turn.rsand the log.
Stop and report if
- The branch check fails; a "before" block is not found exactly once; step 2 differs;
cargo checkwarns; a test fails; the gate count is not 1508; a file exceeds 500 lines; you addMemorySourcetoRuntime; you compact insiderun_turn; you callfinish_turnafterdrop(held); you add an idle timer; you compact aTurnErrorother thanSessionFull; you retrySessionFullmore than once.
What the given tests pin
Usage 849/1000 leaves epoch 0 (one chat); 850/1000 compact before return (live slot then
background, no tools on the summary); SessionFull compact-and-retry (three chats, user logged
in both epochs, turns() == 2); SessionFull stays SessionFull when compact fails; a
successful due turn stays successful when compact.md is missing (epoch stays 0); another
infer error is not compacted.