Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
30 KiB
M5c-1 task 09: memoryd decides a reported turn, after loopd has its answer
Branch: m5c1 (run git switch m5c1; git status --short must be empty, otherwise stop)
Commit subject: memoryd: a reported turn decided after loopd has its answer, from brokerd and the log
Log row: the Task cell is M5c1/09-memoryd-honcho-turns.
Goal
When a turn ends, loopd sends memoryd a SessionUpdated { session, turn } (task 02). From
this task on, with a [honcho] section, memoryd then decides where that turn goes in Honcho
(task 08's decide) and appends the decision to the ledger (task 07) at once (M5c spec, section
5). Nothing is sent to Honcho yet: that is task 11.
- Answer first.
loopdwaits formemoryd's answer toSessionUpdatedbefore it sends the turn's reply to the person, and a question tobrokerdmay take seconds. Somemorydanswersloopdexactly as today, closes the connection, and only then, on the same connection's thread, decides. The Honcho state has a lock of its own, never the index lock, so a slowbrokerdholds up neitherloopdnor a memory call. - Every waiting turn.
memorydkeeps, in memory, each session with undecided turns and the highest turnloopdreported for it. At each report it walks every such session, in the order of their ids: it reads the session's log, and decides every undecided turn from 1 to the smaller of the reported turn and the log's turns. So a turn left undecided whilebrokerdwas down is decided at the next report of any session (spec 5, step 3). - The log's last turn waits for its reply. A log's last turn without a final answer may still
be running: it is not decided (and never skipped) until it has one. This only matters when a
report names a turn past the log's end;
loopdnever sends one, but the number comes over a socket. - One question to
brokerdper session per walk, on itsstate.sock(task 05), and none for a session with nothing undecided. The answer is the session's class and trust; an error frame, no listener or 5 s without a byte means no answer. brokerddoes not answer: nothing is decided, the walk stops there (the next sessions would fail the same way), and the outage is logged once, with the new runbook entrybroker-state-unavailable. The first answer after it is logged once too.- A log that does not read (a damaged complete line): nothing is decided for that session,
logged once with
session-log-damaged; the walk goes on with the next session. A last line without its newline is oneloopdis still writing, and is left out, silently. - The ledger at start.
memorydreads<home>/memory/honcho.jsonlthrough before anything else inserve::start. A damaged or unreadable ledger stopsmemoryd: exit 1, one line with the pointerhoncho-ledger-damaged, no socket bound (as task 06 does forhoncho-config-invalid). A cut-off last line is dropped by the ledger (task 07) and logged once. A ledger that cannot be written whilememorydruns is logged with the same pointer; the walk stops, and the turn is decided at a later report. - Without
[honcho]nothing changes: no ledger, no question, no line. Every existing test passes unchanged.
Epochs are out of scope: loopd writes only sessions/<id>/0.jsonl and numbers turns in it, and
so does this task. (A later milestone that adds compaction must settle how turns are numbered
across epochs.)
Interfaces
In crates/memoryd/src/honcho/state.rs (the module memoryd::honcho::state):
pub const WAIT: Duration = Duration::from_secs(5);
pub const RUNBOOK: &str = "see docs/runbook.md#broker-state-unavailable";
pub fn ask(socket: &Path, session: &SessionId, wait: Duration) -> Result<SessionStateReply, String>;
In crates/memoryd/src/honcho/turns.rs (the module memoryd::honcho::turns):
pub const LEDGER: &str = "honcho.jsonl";
pub const LOG_RUNBOOK: &str = "see docs/runbook.md#session-log-damaged";
pub type Ask<'a> = &'a dyn Fn(&SessionId) -> Result<SessionStateReply, String>;
pub fn log_path(sessions: &Path, session: &SessionId) -> PathBuf;
pub fn read_log(path: &Path) -> Result<Vec<LogRecord>, String>;
pub fn finished(turn: &[LogRecord]) -> bool;
pub fn decidable(records: &[LogRecord], reported: u64) -> (u64, bool);
pub struct TurnSync { /* private */ }
impl TurnSync {
pub fn new(ledger: Ledger, agent: &str, sessions: &Path) -> TurnSync;
pub fn ledger(&self) -> &Ledger;
pub fn waiting(&self) -> Vec<(SessionId, u64)>;
pub fn updated(&mut self, session: &SessionId, turn: u64, ask: Ask<'_>, now: Timestamp,
log: &dyn Fn(&str));
}
In crates/memoryd/src/serve.rs: pub enum StartError { Failed(String), Ledger(LedgerError) }
(Debug, Display, Error, From<String>), and start returns Result<Running, StartError>.
Task 11 uses TurnSync, decidable and waiting when it rebuilds what a restart forgets.
The lines memoryd logs
| When | The line |
|---|---|
| the ledger's last line was cut off (at start) | memoryd: <home>/memory/honcho.jsonl: a last line cut off by a crash was dropped |
brokerd gives no answer, the first time |
memoryd: <reason>; turns wait undecided; see docs/runbook.md#broker-state-unavailable |
brokerd answers again |
memoryd: brokerd answers on its state socket again; turns are decided |
| a session's log does not read, the first time | memoryd: <path>:<line>: <reason>; see docs/runbook.md#session-log-damaged |
| the ledger cannot be written | memoryd: <path>: <error>; see docs/runbook.md#honcho-ledger-damaged |
<reason> for brokerd is ask's error: <socket path>: <error> (connect, timeout, frame) or
brokerd: <detail> for an error frame. At start, a damaged ledger is memoryd: <path>:<line>: <why>; see docs/runbook.md#honcho-ledger-damaged on stderr, and an unreadable one memoryd: <path>: <error>; see docs/runbook.md#honcho-ledger-damaged: one line each, then exit 1.
Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
crates/memoryd/src/honcho/mod.rs |
pub mod state; and pub mod turns; (edit H1) |
crates/memoryd/src/honcho/state.rs |
new: a skeleton you copy, then fill 1 body |
crates/memoryd/src/honcho/turns.rs |
new: a skeleton you copy, then fill 6 bodies |
crates/memoryd/src/serve.rs |
module comment, imports, Memoryd.honcho, StartError, start, connection, decide_turns (edits S1 to S7) |
crates/memoryd/src/main.rs |
a ledger error printed with its own pointer (edit M1) |
docs/runbook.md |
session-log-damaged and honcho-ledger-damaged changed (edits R1 to R4); broker-state-unavailable appended |
everything else (decide.rs, ledger.rs, config.rs, other crates) |
no change |
Files
- Copy, from
docs/plans/M5c1/files/(the same path under it):- new tests:
crates/memoryd/tests/honcho_state.rs,crates/memoryd/tests/honcho_turns.rs,crates/memoryd/tests/honcho_serve.rs - new test support:
crates/memoryd/tests/support/brokerd.rs(a fakebrokerdstate socket),crates/memoryd/tests/support/turns.rs(logs of turns with speakers) - the skeletons
crates/memoryd/src/honcho/state.rsandcrates/memoryd/src/honcho/turns.rs
- new tests:
- Append, from
docs/plans/M5c1/files/, to the end ofdocs/runbook.md, with one blank line before it:runbook-broker-state-unavailable.md(step 7 gives the command). - Modify:
crates/memoryd/src/honcho/mod.rs,crates/memoryd/src/serve.rs,crates/memoryd/src/main.rs,docs/runbook.md,docs/implementer-log.md. - Do not touch: any other source file, any other test or support file (
support/logs.rsandsupport/tmp.rsin particular, which the new tests use), any file of another crate, anyCargo.toml, and in the skeletons anything but the 7todo!()bodies.
Facts you need
- The socket.
brokerdservesstate.sock(task 05): one frame in, one final frame out, the connection then closed. The question isEnvelope { v: PROTOCOL_VERSION, id: 1, r#final: true, msg: Message::SessionState(SessionState { session }) }; the answerMessage::SessionStateReply(SessionStateReply { class, untrusted }), orMessage::Error(for example a damaged state file,broker-state-damagedinbrokerd's own log). Frames arebxcore::write_frameandread_frame, as on every socket.Config::state_socket()(task 06) is the path. - Timeouts. The read and the write timeouts are 5 s (
WAIT): "no byte for 5 s", asloopd'sMemoryPortdoes it withset_read_timeoutandset_write_timeout. There is no connect timeout:std::os::unix::net::UnixStreamhas none, and a connect fails at once when nothing listens. A read that times out isframe I/O error: Resource temporarily unavailable (os error 11)on Linux. - The log.
sessions/<id>/0.jsonlunder[paths] home(Config::sessions_dir()), oneLogRecordper line.loopdwrites a record and its newline in two writes, somemorydcan read a last line without its newline: it is left out. A turn is finished when its lastAssistantrecord asks for no tool (finished); it need not have text (task 08 skips an empty reply asthe turn has no reply). - The walk is
TurnSync::updated, given whole withwalk: it calls yourrecords,decidable,stateand task 08'sdecide. Read both before you fill the bodies. A session leaves the map when every turn up to its reported one (as far as its log goes) is decided; it stays while a turn waits (forbrokerd, a readable log, or a reply). - The ledger's time is the walk's
now(Timestamp::now()inserve.rs), so a decision made late carries the time it was made. - What a restart forgets. The map lives in memory. After a restart a waiting session is
walked again only at its next report. Rebuilding the map at start (from the ledger and the
logs, with
decidableso that a running turn is never decided) is task 11's. - The given tests.
honcho_state.rs(5 tests): the question asbrokerdreads it and its answer; an error frame; no listener (an error at once); silence (an error after the wait, here 300 ms).honcho_turns.rs(16 tests):TurnSyncwithbrokerdas a closure: every undecided turn up to the report and the log; the running last turn; one question per session, none when nothing waits; the outage and the return logged once; the walk stopped at the first failure; an earlier report never lowering the turn; a damaged log and a line being written; a ledger that cannot be written;read_log,finished,decidable.honcho_serve.rs(6 tests):serve::starton a thread with a fakebrokerd: a turn decided afterloopdhas its answer (and a report on the wrong socket deciding nothing); a silentbrokerdholding up neither a second report nor a memory call (each answered within 1 s), then logged once after 5 s; nothing asked or written without[honcho]; a damaged or unreadable ledger stoppingstartbefore any socket is bound; a cut-off last line logged; and thememorydprocess exiting with exactly the ledger's line. It takes about 6 s.
- The calls the bodies use:
Call Signature, and what it gives UnixStream::connectUnixStream::connect(socket) -> io::Result<UnixStream>set_read_timeout,set_write_timeoutstream.set_read_timeout(Some(wait)) -> io::Result<()>write_frame,read_framewrite_frame(&mut stream, &env) -> Result<(), FrameError>;read_frame(&mut stream) -> Result<Envelope, FrameError>std::fs::readstd::fs::read(path) -> io::Result<Vec<u8>>Iterator::rpositiontext.iter().rposition(|b| *b == b'\n') -> Option<usize>slice::split_inclusivebytes.split_inclusive(|b| *b == b'\n'): each line with its newline, no empty last pieceserde_json::from_sliceserde_json::from_slice::<LogRecord>(line) -> serde_json::Result<LogRecord>Path::joinsessions.join(session.as_str()).join("0.jsonl")BTreeSet::insert,removeself.unreadable.insert(s) -> bool(true when it was not there);remove(s: &str)turns,turn_recordstask 08's: turns(records) -> u64;turn_records(records, turn) -> Option<&[LogRecord]>Option::is_some_andturn_records(records, limit).is_some_and(finished)matches!with a guardmatches!(last, Some(LogRecord::Assistant { tool_calls, .. }) if tool_calls.is_empty())
The edits, exactly
Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file.
crates/memoryd/src/honcho/mod.rs
H1. The line
pub mod ledger;
becomes
pub mod ledger;
pub mod state;
pub mod turns;
crates/memoryd/src/serve.rs
S1. The lines
//! Serving: three sockets, one request per connection, each answered by one frame (M5a spec,
//! section 7). One lock covers the files and the index, so writes never interleave.
become
//! Serving: three sockets, one request per connection, each answered by one frame (M5a spec,
//! section 7). One lock covers the files and the index, so writes never interleave. With
//! `[honcho]`, a reported turn is decided after `loopd` has its answer, under a lock of its own
//! (M5c spec, section 5).
S2. The lines
MemoryCall, MemoryCommand, Message, PROTOCOL_VERSION, RecallArgs, Reindexed, Timestamp,
ToolResponse, WireError, read_frame, write_frame,
};
become
MemoryCall, MemoryCommand, Message, PROTOCOL_VERSION, RecallArgs, Reindexed, SessionId,
SessionUpdated, Timestamp, ToolResponse, WireError, read_frame, write_frame,
};
S3. The line
use crate::files::Store;
becomes
use crate::files::Store;
use crate::honcho::ledger::{self, Ledger, LedgerError};
use crate::honcho::state;
use crate::honcho::turns::{self, TurnSync};
S4. In pub struct Memoryd, the lines
state: Mutex<State>,
log: Log,
}
become
state: Mutex<State>,
/// With `[honcho]`: the turns reported and not yet decided, and the ledger. Never taken
/// while `state` is held, and never held by a request that waits for its answer.
honcho: Option<Mutex<TurnSync>>,
log: Log,
}
/// Why `start` failed. `main` prints a start failure with the pointer `memoryd-start-failed`, and
/// a ledger that does not read through with its own.
#[derive(Debug)]
pub enum StartError {
Failed(String),
Ledger(LedgerError),
}
impl std::fmt::Display for StartError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StartError::Failed(why) => write!(f, "{why}"),
// `Damaged` names its pointer itself.
StartError::Ledger(e @ LedgerError::Damaged { .. }) => write!(f, "{e}"),
StartError::Ledger(e) => write!(f, "{e}; {}", ledger::RUNBOOK),
}
}
}
impl std::error::Error for StartError {}
impl From<String> for StartError {
fn from(why: String) -> StartError {
StartError::Failed(why)
}
}
S5. The lines
/// Create what is missing under `memory/`, open the index and catch it up, then bind the three
/// sockets. The error names what failed; the caller points at `memoryd-start-failed`.
pub fn start(config: Config, log: Log) -> Result<Running, String> {
let store = Store::new(&config.memory_dir());
become
/// With `[honcho]`, read the ledger through first. Then create what is missing under `memory/`,
/// open the index and catch it up, and bind the three sockets. The error names what failed.
pub fn start(config: Config, log: Log) -> Result<Running, StartError> {
let honcho = match config.honcho_config()? {
Some(honcho) => {
let path = config.memory_dir().join(turns::LEDGER);
let ledger = Ledger::open(&path).map_err(StartError::Ledger)?;
if ledger.repaired() {
(log)(
format!(
"memoryd: {}: a last line cut off by a crash was dropped",
path.display()
)
.as_str(),
);
}
Some(Mutex::new(TurnSync::new(
ledger,
&honcho.agent,
&config.sessions_dir(),
)))
}
None => None,
};
let store = Store::new(&config.memory_dir());
S6. In start, where Memoryd is built, the lines
state: Mutex::new(State { index }),
log,
become
state: Mutex::new(State { index }),
honcho,
log,
S7. In Memoryd::connection, the lines
let reply = self.handle(request.msg, which);
let env = Envelope {
v: PROTOCOL_VERSION,
id: request.id,
r#final: true,
msg: reply,
};
let _ = write_frame(&mut stream, &env);
}
become
let update = match (which, &request.msg) {
(Which::Loop, Message::SessionUpdated(update)) => Some(update.clone()),
_ => None,
};
let reply = self.handle(request.msg, which);
let env = Envelope {
v: PROTOCOL_VERSION,
id: request.id,
r#final: true,
msg: reply,
};
let _ = write_frame(&mut stream, &env);
// `loopd` has its answer before `brokerd` is asked: it sends the turn's reply only after
// this frame, and `brokerd` may take seconds.
drop(stream);
if let Some(update) = update {
self.decide_turns(&update);
}
}
/// With `[honcho]`: decide the reported turn, and every other that waits (`TurnSync`).
fn decide_turns(&self, update: &SessionUpdated) {
let Some(honcho) = &self.honcho else {
return;
};
let socket = self.config.state_socket();
let ask = |session: &SessionId| state::ask(&socket, session, state::WAIT);
let mut sync = honcho.lock().unwrap_or_else(|p| p.into_inner());
sync.updated(
&update.session,
update.turn,
&ask,
Timestamp::now(),
&*self.log,
);
}
crates/memoryd/src/main.rs
M1. The lines
Ok(running) => running,
Err(e) => {
eprintln!("memoryd: {e}\n{START_FAILED}");
become
Ok(running) => running,
// The ledger's line names its own pointer, `honcho-ledger-damaged`.
Err(e @ serve::StartError::Ledger(_)) => {
eprintln!("memoryd: {e}");
return ExitCode::from(1);
}
Err(e) => {
eprintln!("memoryd: {e}\n{START_FAILED}");
docs/runbook.md
R1. In session-log-damaged, the lines
**What you see.** A turn on an existing session fails with `<file>:<line>: <reason>` naming
`sessions/<id>/<epoch>.jsonl`, and this entry. Other sessions work.
become
**What you see.** A turn on an existing session fails with `<file>:<line>: <reason>` naming
`sessions/<id>/<epoch>.jsonl`, and this entry. Other sessions work. With `[honcho]`, `memoryd`
logs the same line once, starting `memoryd: `, and that session's turns wait undecided for Honcho
until the log reads again.
R2. In honcho-ledger-damaged (the entry task 07 appended), the lines
**What you see.** `memoryd` logs a line naming `memory/honcho.jsonl`, a line number and a reason,
then this entry:
```text
/srv/bx/memory/honcho.jsonl:12: turn 3 of session chat-1: decided already; see docs/runbook.md#honcho-ledger-damaged
```
become
**What you see.** `memoryd` exits 1 at start, before it binds its sockets, with one line naming
`memory/honcho.jsonl`, a line number and a reason, then this entry:
```text
memoryd: /srv/bx/memory/honcho.jsonl:12: turn 3 of session chat-1: decided already; see docs/runbook.md#honcho-ledger-damaged
```
Or, while it runs, `memoryd: <path>: <error>; see docs/runbook.md#honcho-ledger-damaged` when the
ledger cannot be written (a full disk, a wrong owner). The turn waits undecided, and is decided at
a later turn's end once the ledger can be written.
R3. In honcho-ledger-damaged, the lines
does not guess: it does not start its Honcho sync, and nothing is decided or sent to Honcho until
the ledger reads through. The rules: an item is decided once (`decided already`), and it is sent
or refused once, only after it is decided and only when it was not decided to go nowhere (`not
waiting to be sent`). A last line cut off by a crash is not damage: `memoryd` drops it and says so
in its log.
become
does not guess: it does not start. While it is stopped no memory is served either, and `loopd`
cannot start a session (`memory-unavailable`). The rules: an item is decided once (`decided
already`), and it is sent or refused once, only after it is decided and only when it was not
decided to go nowhere (`not waiting to be sent`). A last line cut off by a crash is not damage:
`memoryd` drops it and says so in its log.
R4. In honcho-ledger-damaged, the lines
**Fix.** Keep a copy first: `cp -a "$f" "$f.damaged-$(date +%s)"`. The ledger is written only by
`memoryd`, so a damaged line was changed by hand or by the disk. If you know what the line should
say, correct it. Otherwise remove it (`sed -i '<line>d' "$f"`), knowing what that costs: a removed
`sent` or `failed` line means the item is sent again (a turn's messages appear twice in Honcho, a
conclusion is made twice); a removed `decided` line means the item is decided again, and any
later `sent` or `failed` line of the same item then breaks the rules, so remove those too. Honcho
cannot delete a message, so a turn sent twice stays twice.
become
**Fix.** If the ledger cannot be written, free space or correct the owner of `memory/`
(`df -h`, `ls -l "$f"`); nothing else is needed. For a damaged line, keep a copy first:
`cp -a "$f" "$f.damaged-$(date +%s)"`. The ledger is written only by `memoryd`, so a damaged line
was changed by hand or by the disk. If you know what the line should say, correct it. Otherwise
remove it (`sed -i '<line>d' "$f"`), knowing what that costs: a removed `sent` or `failed` line
means the item is sent again (a turn's messages appear twice in Honcho, a conclusion is made
twice); a removed `decided` line means the item is decided again, and any later `sent` or
`failed` line of the same item then breaks the rules, so remove those too. Honcho cannot delete a
message, so a turn sent twice stays twice.
Steps
-
1. Copy the given tests. From the repository root, five commands:
cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_state.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_turns.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_serve.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/support/brokerd.rs crates/memoryd/tests/support/ cp docs/plans/M5c1/files/crates/memoryd/tests/support/turns.rs crates/memoryd/tests/support/ -
2. See them fail.
cargo build --tests --keep-going -p memoryd, with a shell timeout of 600000 ms: three test targets do not compile, oneE0432each:honcho_state(unresolved import `memoryd::honcho::state`),honcho_turns(unresolved import `memoryd::honcho::turns`) andhoncho_serve(unresolved import `memoryd::serve::StartError`). Every other target compiles. -
3. The modules. Make edit H1, then copy the two skeletons:
cp docs/plans/M5c1/files/crates/memoryd/src/honcho/state.rs crates/memoryd/src/honcho/ cp docs/plans/M5c1/files/crates/memoryd/src/honcho/turns.rs crates/memoryd/src/honcho/cargo check -p memoryd: no error, and 19 warnings, all from the unfilled bodies: fourunused importlines (std::os::unix::net::UnixStream;Envelope,Message,PROTOCOL_VERSION,SessionState,read_frameandwrite_frame;crate::honcho::state;turn_recordsandturns), fourteenunused variable(sessionfour times,logtwice, andsocket,wait,path,sessions,records,reported,turn,askonce each), andfields `sessions`, `broker_down`, and `unreadable` are never read. They go as you fill the bodies. -
4. Fill the seven bodies, one at a time, each from its comment, and run
cargo check -p memorydafter each:askinstate.rs, then inturns.rslog_path,read_log,finished,records,state,decidable. When a body is filled, delete its instruction comment (the//lines inside the body) and keep the function's doc comment (///) as it is. Change nothing else in the two files:TurnSync,new,waiting,updatedandwalkare given. After the seventh,cargo check -p memorydprints no warning, and this command passes all its tests (5 and 16):cargo test -p memoryd --test honcho_state --test honcho_turns -
5. Serving. Make edits S1 to S7 in
serve.rs, then M1 inmain.rs.cargo check -p memoryd: no error and no warning. -
6. The tests. This command, with a shell timeout of 600000 ms, and every test passes (6, 16, 5, and the existing
serveandhoncho_starttests unchanged):cargo test -p memoryd --test honcho_serve --test honcho_turns --test honcho_state \ --test serve --test honcho_start -
7. The runbook. Make edits R1 to R4 in
docs/runbook.md, then from the repository root:{ echo; cat docs/plans/M5c1/files/runbook-broker-state-unavailable.md; } >> docs/runbook.mdThen
grep -c '^## broker-state-unavailable$' docs/runbook.mdprints1, andgrep -c 'does not start its Honcho sync' docs/runbook.mdprints0. -
8. Check. Three commands:
grep -n 'decide_turns\|let _ = write_frame(&mut stream, &env);' crates/memoryd/src/serve.rsprints three lines, in this order: thewrite_frameline, the callself.decide_turns(&update);, andfn decide_turns(.loopdhas its answer first.grep -rn 'runbook.md#broker-state-unavailable' crates/memoryd/srcprints exactly one line, theRUNBOOKconstant inhoncho/state.rs.git diff --stat -- crates/bxcore crates/loopd crates/brokerd crates/gatewayd crates/bxctl Cargo.toml crates/memoryd/Cargo.toml crates/memoryd/src/honcho/decide.rs crates/memoryd/src/honcho/ledger.rs crates/memoryd/src/config.rsprints nothing.
-
9. Run the gate.
cargo fmt --all, thenmake gate, with a shell timeout of 600000 ms. Expected last line:gate: ok, with 1257 tests: 1230 before this task, plus 27 new ones (5 inhoncho_state.rs, 16 inhoncho_turns.rs, 6 inhoncho_serve.rs). -
10. Log and commit. Add your row to
docs/implementer-log.md; its Task cell isM5c1/09-memoryd-honcho-turns.git status --shortlistshoncho/mod.rs,serve.rs,main.rs,docs/runbook.mdand your log as modified, andhoncho/state.rs,honcho/turns.rs, the three test files and the two support files as new. Stage withgit add crates/memoryd docs/runbook.md docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.Cargo.lockdoes not change in this task; thegit addis harmless.
Done when
make gateprintsgate: okwith 1257 tests.git diff HEAD~1 --statlists only the files of step 10.
Stop and report if
- A "before" block of H1, S1 to S7, M1 or R1 to R4 is not in its file exactly as shown, or is there more than once.
- Step 2 or 3 shows a different result: other test targets, errors of another kind, or other warnings.
cargo checkreports an error after a body you filled or an edit you made, and the comment or the edit does not tell you how to fix it. Quote the error, and stop.- A comment in a skeleton does not fit the code under it, or disagrees with this task's text or
"The lines
memorydlogs". Quote both and stop. - After step 4 or 6 a test fails although each body follows its comment and each edit is as shown. Do not change a test, a support file or the skeletons' given code: stop, and give the failing tests' names and their messages.
- The fix seems to need a change anywhere else:
decide.rs,ledger.rs,config.rs, another crate, a new thread, or a lock held whilebrokerdis asked other thanhoncho's. It does not; stop and say why you think so. - A test outside the new files fails in the gate: check once whether it fails without your change, then stop and report both results.
- The gate passes with a count other than 1257.
What the given tests pin
Each of these changes to the finished code makes at least one given test fail (42 in all): the
map keeping the lowest or the latest report instead of the highest; walking only the reported
session; keeping a finished session, or dropping one whose last turn waits; going on after a
silent brokerd, or after a ledger write failed; ignoring the log's length or the report; deciding
a running last turn; deciding a turn twice; asking with nothing undecided; logging the outage, the
return or a damaged log more than once, or not at all, or without its pointer; deciding a
session whose log does not read; parsing a line being written; line numbers from 0; a missing log
taken as an error; any Assistant record, or the first, ending a turn; another epoch's file;
deciding before loopd has its answer, or under the index lock, or not at all, or for a report on
another socket; the repaired ledger not logged; a damaged ledger ignored, or read after a socket
is bound; the ledger's error printed without its pointer, with it twice, or with the start
pointer too; no read timeout; an error frame taken for a clean session; the wrong session asked;
another frame id.
After this task
- A restart forgets the sessions that wait. Task 11 rebuilds them at start (from the logs and the
ledger, with
decidable, so that a running turn is never decided). - Whether turns from before
[honcho]was turned on are decided at all is the owner's open question; if not, a later task adds a cutoff as a new skip row indecide.