Files
boxmaker/docs/plans/M5c1/09-memoryd-honcho-turns.md
2026-09-28 09:58:26 -07:00

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. loopd waits for memoryd's answer to SessionUpdated before it sends the turn's reply to the person, and a question to brokerd may take seconds. So memoryd answers loopd exactly 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 slow brokerd holds up neither loopd nor a memory call.
  • Every waiting turn. memoryd keeps, in memory, each session with undecided turns and the highest turn loopd reported 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 while brokerd was 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; loopd never sends one, but the number comes over a socket.
  • One question to brokerd per session per walk, on its state.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.
  • brokerd does 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 entry broker-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 one loopd is still writing, and is left out, silently.
  • The ledger at start. memoryd reads <home>/memory/honcho.jsonl through before anything else in serve::start. A damaged or unreadable ledger stops memoryd: exit 1, one line with the pointer honcho-ledger-damaged, no socket bound (as task 06 does for honcho-config-invalid). A cut-off last line is dropped by the ledger (task 07) and logged once. A ledger that cannot be written while memoryd runs 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

(A copy command names <file>-taskNN where a later task gives a newer version of that file: this task's version is the one named.)

  • 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, and crates/memoryd/tests/honcho_serve.rs from honcho_serve.rs-task09 (this task's version; task 11 gives a later one)
    • new test support: crates/memoryd/tests/support/brokerd.rs (a fake brokerd state socket), crates/memoryd/tests/support/turns.rs (logs of turns with speakers)
    • the skeletons crates/memoryd/src/honcho/state.rs and crates/memoryd/src/honcho/turns.rs
  • Append, from docs/plans/M5c1/files/, to the end of docs/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.rs and support/tmp.rs in particular, which the new tests use), any file of another crate, any Cargo.toml, and in the skeletons anything but the 7 todo!() bodies.

Facts you need

  • The socket. brokerd serves state.sock (task 05): one frame in, one final frame out, the connection then closed. The question is Envelope { v: PROTOCOL_VERSION, id: 1, r#final: true, msg: Message::SessionState(SessionState { session }) }; the answer Message::SessionStateReply(SessionStateReply { class, untrusted }), or Message::Error (for example a damaged state file, broker-state-damaged in brokerd's own log). Frames are bxcore::write_frame and read_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", as loopd's MemoryPort does it with set_read_timeout and set_write_timeout. There is no connect timeout: std::os::unix::net::UnixStream has none, and a connect fails at once when nothing listens. A read that times out is frame I/O error: Resource temporarily unavailable (os error 11) on Linux.
  • The log. sessions/<id>/0.jsonl under [paths] home (Config::sessions_dir()), one LogRecord per line. loopd writes a record and its newline in two writes, so memoryd can read a last line without its newline: it is left out. A turn is finished when its last Assistant record asks for no tool (finished); it need not have text (task 08 skips an empty reply as the turn has no reply).
  • The walk is TurnSync::updated, given whole with walk: it calls your records, decidable, state and task 08's decide. 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 (for brokerd, a readable log, or a reply).
  • The ledger's time is the walk's now (Timestamp::now() in serve.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 decidable so that a running turn is never decided) is task 11's.
  • The given tests.
    • honcho_state.rs (5 tests): the question as brokerd reads 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): TurnSync with brokerd as 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::start on a thread with a fake brokerd: a turn decided after loopd has its answer (and a report on the wrong socket deciding nothing); a silent brokerd holding 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 stopping start before any socket is bound; a cut-off last line logged; and the memoryd process exiting with exactly the ledger's line. It takes about 6 s.
  • The calls the bodies use:
    Call Signature, and what it gives
    UnixStream::connect UnixStream::connect(socket) -> io::Result<UnixStream>
    set_read_timeout, set_write_timeout stream.set_read_timeout(Some(wait)) -> io::Result<()>
    write_frame, read_frame write_frame(&mut stream, &env) -> Result<(), FrameError>; read_frame(&mut stream) -> Result<Envelope, FrameError>
    std::fs::read std::fs::read(path) -> io::Result<Vec<u8>>
    Iterator::rposition text.iter().rposition(|b| *b == b'\n') -> Option<usize>
    slice::split_inclusive bytes.split_inclusive(|b| *b == b'\n'): each line with its newline, no empty last piece
    serde_json::from_slice serde_json::from_slice::<LogRecord>(line) -> serde_json::Result<LogRecord>
    Path::join sessions.join(session.as_str()).join("0.jsonl")
    BTreeSet::insert, remove self.unreadable.insert(s) -> bool (true when it was not there); remove(s: &str)
    turns, turn_records task 08's: turns(records) -> u64; turn_records(records, turn) -> Option<&[LogRecord]>
    Option::is_some_and turn_records(records, limit).is_some_and(finished)
    matches! with a guard matches!(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-task09 crates/memoryd/tests/honcho_turns.rs
    cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_serve.rs-task09 crates/memoryd/tests/honcho_serve.rs
    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-task09 crates/memoryd/tests/support/turns.rs
    
  • 2. See them fail. cargo build --tests --keep-going -p memoryd, with a shell timeout of 600000 ms: three test targets do not compile, one E0432 each: honcho_state (unresolved import `memoryd::honcho::state` ), honcho_turns (unresolved import `memoryd::honcho::turns` ) and honcho_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: four unused import lines (std::os::unix::net::UnixStream; Envelope, Message, PROTOCOL_VERSION, SessionState, read_frame and write_frame; crate::honcho::state; turn_records and turns), fourteen unused variable (session four times, log twice, and socket, wait, path, sessions, records, reported, turn, ask once each), and fields `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 memoryd after each: ask in state.rs, then in turns.rs log_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, updated and walk are given. After the seventh, cargo check -p memoryd prints 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 in main.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 serve and honcho_start tests 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.md
    

    Then grep -c '^## broker-state-unavailable$' docs/runbook.md prints 1, and grep -c 'does not start its Honcho sync' docs/runbook.md prints 0.

  • 8. Check. Three commands:

    • grep -n 'decide_turns\|let _ = write_frame(&mut stream, &env);' crates/memoryd/src/serve.rs prints three lines, in this order: the write_frame line, the call self.decide_turns(&update);, and fn decide_turns(. loopd has its answer first.
    • grep -rn 'runbook.md#broker-state-unavailable' crates/memoryd/src prints exactly one line, the RUNBOOK constant in honcho/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.rs prints nothing.
  • 9. Run the gate. cargo fmt --all, then make 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 in honcho_state.rs, 16 in honcho_turns.rs, 6 in honcho_serve.rs).

  • 10. Log and commit. Add your row to docs/implementer-log.md; its Task cell is M5c1/09-memoryd-honcho-turns. git status --short lists honcho/mod.rs, serve.rs, main.rs, docs/runbook.md and your log as modified, and honcho/state.rs, honcho/turns.rs, the three test files and the two support files as new. Stage with git add crates/memoryd docs/runbook.md docs/implementer-log.md Cargo.lock, then commit as AGENTS.md says. Cargo.lock does not change in this task; the git add is harmless.

Done when

  • make gate prints gate: ok with 1257 tests.
  • git diff HEAD~1 --stat lists 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 check reports 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 memoryd logs". 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 while brokerd is asked other than honcho'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 in decide.