179 lines
35 KiB
Markdown
179 lines
35 KiB
Markdown
# Implementer log
|
|
|
|
Kept by the implementing model, one row per task. The column meanings are in `AGENTS.md`. The
|
|
owner fills in the Model column, since the implementer may not know which model it is. The
|
|
reviewer adds findings under "Reviews" once per milestone.
|
|
|
|
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
|
|---|---|---|---|---|---|---|---|
|
|
| M1/01-workspace-and-gate | 2026-09-17 | done | 1 | pass | none | Crate skeletons, Cargo files and the given Makefile/deny.toml/test-gate-scripts.sh were already present untracked from a prior attempt; I verified them against the plan and created only the missing gate scripts, dependencies.md, egress.md and this log row. | Laguna S 2.1 |
|
|
| M1/02-proto-values | 2026-09-17 | done | 1 | pass | none | Implemented ValueError, SessionId, Epoch, CallId, Hash32 and Timestamp in crates/proto/src/ids.rs and DataClass in class.rs, using serde try_from/into for string-backed JSON validation, a hand-written hex encoder and humantime for RFC 3339 parsing with canonical re-serialization. | Laguna S 2.1 |
|
|
| M1/03-proto-wire | 2026-09-17 | done | 2 | pass | none | Added Envelope, Message, WireError, ErrorCode, ToolRequest, ToolResponse and DenyReason in crates/proto/src/wire.rs, re-exported from lib.rs; all 9 fixture tests pass and `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M1/04-proto-frame | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/frame.rs (MAX_FRAME, FrameError, write_frame, read_frame) re-exported from lib.rs; 13 fixture tests pass. Two compile fixes: mapped read_bytes io::Error to FrameError::Io and annotated serde_json::from_slice::<Envelope>; cargo-fmt reordered the lib.rs re-export lines; `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M1/05-proto-grant | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/grant.rs (Mode, Constraints with Default, Grant with serde defaults + deny_unknown_fields) re-exported from lib.rs and toml 1.1.6 as a proto dev-dependency (workspace dep + dependencies.md row); 4 fixture tests pass. cargo-fmt reordered the lib.rs re-exports before the gate. | Laguna S 2.1 |
|
|
| M1/06-proto-records | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/audit.rs (DecisionRecord, AuditRecord) and crates/proto/src/log.rs (ToolCall, LogRecord) re-exported from lib.rs; 3 fixture tests pass, 40 total across the five proto test files. cargo-fmt reordered the lib.rs re-exports before the gate. | Laguna S 2.1 |
|
|
| M1/07-brokerd-decision | 2026-09-17 | done | 1 | pass | none | Added Decision (Debug only, private fields), decide (Err(NoGrant) until M3) and the run stub (ToolResponse::Failed) in crates/brokerd; Decision::new carries expect(dead_code). 2 unit + 3 doctests (2 compile_fail) pass; verified the compile_fail guards by temporarily making new pub. `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M1/08-proto-strictness | 2026-09-17 | done | 1 | pass | none | Added deny_unknown_fields to AuditRecord and ToolCall in crates/proto; bounded Timestamp (MAX const, from_unix_millis -> Result, parse bounds via from_unix_millis, now clamps to MAX) in ids.rs. 45 proto tests pass; `cargo fmt --all` and `make gate` print `gate: ok`. | Laguna S 2.1 |
|
|
| M1/09-gate-scripts-table-form | 2026-09-17 | done | 1 | pass | none | Rewrote check-lines, check-crate-deps and check-dep-docs to parse table-form (`[dependencies.x]`) and dotted (`x.path`) dependencies and to fail closed (exit 1 when crates/Cargo.toml/docs/dependencies.md is missing); check-lines now prints `file has N lines (limit 500)`. Self-test passes with 0 failures, all three scripts pass on the real tree, and `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M2a/01-proto-sha256 | 2026-09-17 | done | 1 | pass | none | Added crates/proto/src/hash.rs wrapping emsha 1.0.4 (HashError, Sha256 with new/update/finish, sha256, Default); re-exported from lib.rs, added emsha workspace dep and dependencies.md row. One compile fix: finish needed `mut self` to call finalize. 4 hash tests pass, `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M2a/02-inferproxy | 2026-09-17 | done | 3 | fail | none | Implemented Limits/TokenBucket/OpenGuard/serve/handle/forward in lib.rs (std only: the inference server's JSON is upstream's concern) and arg parsing + stale-socket handling in main.rs; TokenBucket counts thousandths of a token in u128 so partial refills stay additive and `saturating_duration_since` on an earlier `now` never underflows, capping at burst; serve refuses by rate-limit then open-connection limit; handle drops its OpenGuard before closing the client so a freshly-finished client is not refused on re-connect (rule 4). The scaffold `proto` dependency in `crates/inferproxy/Cargo.toml` was left untouched since the task's Modify/Copy lists cover only lib.rs, main.rs, the two test files and this log. First `make gate` failed on `clippy::map_clone` (`main.rs` used `.map(String::clone)`); switched to `.cloned()` and re-ran, then re-ran once more after restoring the proto scaffold — both pass. Hand test against straylight returned `{"status":"ok"}`; `forward.rs` passed 6/6 ten runs in a row. | Laguna S 2.1 |
|
|
| M2a/03-loopd-config | 2026-09-17 | done | 1 | pass | none | Wrote crates/loopd/src/config.rs: Config + Infer/Slots/Expect/Sampling/Limits with deny_unknown_fields on all six and struct-level `#[serde(deny_unknown_fields, default)]` on Sampling and Limits; manual Default impls for the two; hand-written Display/std::error::Error ConfigError named by file. Everywhere check: all six structs (Infer, Slots, Expect, Sampling, Limits, Config) carry deny_unknown_fields. One local fix: Display used `{path}` on a PathBuf and failed to build, switched to `path.display()`. 6 config tests pass; `make gate` prints `gate: ok`. | Laguna S 2.1 |
|
|
| M2a/04-loopd-http | 2026-09-18 | done | 2 | fail | none | Added `pub mod http;` to crates/loopd/src/lib.rs and wrote crates/loopd/src/http.rs (423 lines): Request/Head/HttpError, send (exact header order, no `Content-Length` for GET), read_head (buffers across timeouts; `Timeout`/`Closed`/`TooLarge`/`Malformed`), parse_status+parse_head (`HTTP/1.1`/`HTTP/1.0`, status 100-599, lowercased names, duplicates kept, trimmed values), body (chunked/length/close; chunk extensions + trailers skipped), and read_capped. Two fixes: the chunk `Data` phase jumped to `Crlf` on `take==want`, but `want` was capped by the caller's buffer so it switched mid-chunk and returned `malformed chunk` on the recorded fixture — changed to switch on `chunk_remaining==0`; `read_length` reads straight into the caller buffer instead of an 8192 temp slice that would panic for readers larger than 8192. First gate failed on three clippy lints (needless borrows of `format!` results and `map_or` -> `is_some_and`), fixed on the second run. All 15 tests pass and `make gate` prints `gate: ok`. | Laguna S 2.1 (abandoned after two sessions), then Ornith-1.5-35B-A3B |
|
|
| M2a/05-loopd-sse | 2026-09-18 | done | 2 | fail | none | Added `pub mod sse;` to crates/loopd/src/lib.rs and wrote crates/loopd/src/sse.rs: SseItem (Data, Done), SseError (Io, Timeout, Truncated, LineTooLong, NotUtf8) with Display/std::error::Error, and Events<R> which reads a line in bounded 4096-byte chunks, skipping blank/comment/other-field lines and stripping `data:` plus one leading space, decoding UTF-8 only once a whole line has arrived. Two fixes: `drain(..pos)` left the newline in the buffer so blank lines never advanced — changed to `drain(..=pos)` and pop the endings; `process_line` returns `Ok(None)` for a skipped line, which collided with `next_item`'s "stream ended" `Ok(None)` — restructured so a skip continues the loop and only a clean EOF sets `ended`. Both LineTooLong checks (mid-arrival and whole-read) verified by checking the accumulated length before reading and the finished line length. All 7 tests pass; first gate run failed on an unused import, fixed. | Ornith-1.5-35B-A3B |
|
|
| M2a/06-llama-request | 2026-09-18 | done | 1 | pass | none | Added `pub mod llama;` to crates/loopd/src/lib.rs and wrote crates/loopd/src/llama/mod.rs (ChatMessage, ToolSchema, ChatRequest, ChatEvent, FinishReason, Timings with the server's deserialize shape, Completion, InferError with Display/std::error::Error, and a Client holding the config) and crates/loopd/src/llama/request.rs. build_body serializes the body from `#[derive(Serialize)]` structs so key order is fixed; each message kind is serialized with its own struct (the assistant renders `content` as `""` when `None`, and leaves `reasoning_content`/`tool_calls` out when absent; the top-level `tools` array is omitted when empty; `type` comes from `#[serde(rename = "type")]`); the first `cargo build` after writing the structs missed the `role` field on every message struct, caught by the failing test compile, added. 5 request tests pass; `make gate` prints `gate: ok`. | Ornith-1.5-35B-A3B |
|
|
| M2a/07-llama-assemble | 2026-09-18 | done | 3 | fail | none | Wrote crates/loopd/src/llama/assemble.rs (Assembler plus server-format Chunk/Choice/Delta/ToolCallPiece/FunctionPiece/PromptProgress structs with no deny_unknown_fields) and registered `pub mod assemble;`. Accumulation: text goes through get_or_insert_with so content/reasoning stay None until a non-empty piece arrives; tool-call pieces land by index via u32::try_from then usize::try_from and get_mut, a new call only at exactly the end, a skip-ahead or an out-of-range index is Protocol; timings update before the reasoning-token count reads predicted_n; finish checks finish_reason (StreamClosedEarly), then id, then every tool call has id and name. First gate failed on two clippy collapsible_if lints; rewrote the two nested ifs as edition-2024 let-chains and re-ran. All 7 assemble tests pass; `make gate` prints `gate: ok`. | GLM-5.3 (z.ai, default settings) |
|
|
| M2a/08-llama-info | 2026-09-18 | done | 2 | fail | none | Wrote crates/loopd/src/llama/info.rs and registered `pub mod info;`. `call` is one exchange: open socket, set read timeout to `liveness_ms`, send, read head, `read_capped` with `MAX_BODY`; non-200 returns `InferError::Http { status, error_text(&bytes) }`, everything else maps through `map_http` (Connect->Connect, Timeout->Stalled, Closed->StreamClosedEarly, else->Protocol). `error_text` reads the full body via `from_utf8_lossy` then backs up from byte 4096 to a char boundary, so a cut mid-character does not panic. `props` reads `chat_template`, `total_slots`, and `default_generation_settings.n_ctx` from the JSON (unknown fields ignored); `slots` deserializes `Vec<SlotInfo>`; `tokenize` POSTs `{"model","content"}` via `serde_json` and returns `tokens.len()`. `cache_outcome` uses `saturating_add` and `current.cache_n + CACHE_TOLERANCE >= expected`. All 7 tests pass; first gate run failed on rustfmt import order, fixed with `cargo fmt --all`. | Ornith-1.5-35B-A3B |
|
|
| M2a/11-llama-gate-retry | 2026-09-18 | done | 3 | fail | none | Prerequisite `chat` (M2a/09) now exists, so the task was possible. Implemented `SlotGate` in gate.rs: per-slot holder + a `VecDeque` of arrival tickets, `notify_all`, a woken waiter takes the slot only if free and its ticket is at the front (and claims it by setting holder), `Drop` frees and wakes; mutex/condvar poison recovered via `unwrap_or_else(...into_inner)`, no `unwrap`. Implemented `chat_with_retry` + `is_retryable` (all nine variants, a new one is a compile error) + `backoff_ms` in retry.rs: base is `schedule[retry-1]` or last or 0, jitter clamped and computed in `i128` so `u64::MAX` never overflows, jitter from sub-second nanos. `chat` acquires the gate for `req.slot` and maps `GateFull`->`InferError::Busy`; `Client` gained a `gate` field. First gate failed on three clippy lints (derivable `Default`, `or_insert_with`->`or_default`), fixed. One logic bug caught by `waiters_are_served_in_order`: `take_if_front` claimed the ticket but not `holder`, letting two permits overlap — set holder on claim. All 79 loopd tests pass; retry 13/13 over ten runs; `make gate` prints `gate: ok`. | Ornith-1.5-35B-A3B |
|
|
| M2a/09-llama-chat | 2026-09-18 | done | 1 | pass | none | Wrote crates/loopd/src/llama/chat.rs (`chat`, with the head wait in a separate `wait_for_head`) and registered `pub mod chat;`. `chat` builds the body (build error -> Protocol), opens and POSTs, then `wait_for_head` loops `read_head` at `poll_ms`: a Timeout is classified Idle/Busy/Unavailable by `received_any` then a `slots()` poll, emits `Waiting { slot_busy }` on every poll, keeps a per-state `since` that resets on state change, and returns `WaitTimeout`/`LoadTimeout`/`Stalled` at the right limits; 200 streams via `Events`+`Assembler` mapping `Timeout->Stalled`, `Truncated->StreamClosedEarly`, else `Protocol`, then `finish(false)`; non-200 returns `Http { status, error_text }`. All 13 chat tests pass five runs in a row. Table-to-test map: state (table 1) Busy -> a_busy_slot_is_waited_out / a_slot_that_stays_busy_is_a_wait_timeout, Idle-not-processing -> only_the_requests_own_slot_counts_as_busy, Unavailable -> an_unavailable_server_is_a_load_timeout, Idle-received_any -> a_slow_but_steady_stream_is_not_a_stall (turn1 head is 738 B, pieces are 3140/8=392 B, so the head-wait times out with a partial head); thresholds (table 2) -> a_slot_that_stays_busy_is_a_wait_timeout / an_unavailable_server_is_a_load_timeout / only_the_requests_own_slot_counts_as_busy; stream (table 3) Data/Done/None -> the recorded completion and trickle tests, Timeout -> silence_in_mid_stream_is_a_stall, Truncated -> a_stream_that_is_cut_is_closed_early_wherever_the_cut_falls, other -> garbage_in_the_stream_is_a_protocol_error; clock-restart -> the_wait_clocks_restart_when_the_state_changes. One path fix: info and request live under crate::llama, not crate::, so the imports use crate::llama::{info::..., request::...}. | Ornith-1.5-35B-A3B |
|
|
| M2a/10-llama-cap | 2026-09-18 | done | 1 | pass | none | Added `Client::end_reasoning` to info.rs: POSTs `{"id","action":"reasoning_end","model"}` to `/v1/chat/completions/control` via `call`, reads `success` as a bool from the server's JSON (ignoring `message`), non-200 stays an Err through `call`, a missing/non-bool `success` is Protocol. Threaded the cap into `chat` step 4: after passing a chunk's events on, when `assembler.in_reasoning()`, a local `cap_at: Option<u64>` holds where the cap fired (None while it has not fired); on `tokens >= thinking_cap` it calls `end_reasoning(assembler.id())` once, remembers `tokens` and emits `ThinkingCapped` on `Ok(true)`, returns `ThinkingOverrun` on `Ok(false)`/`Err`, and after firing returns `ThinkingOverrun` once `tokens >= at + thinking_overrun`; `finish(cap_at.is_some())`. The `the_allowance_is_exact` test passes with `>=` in both rows (63 is not `20+44`, and is `>= 20+43`). One guard: a reasoning chunk with no id at cap time is Protocol rather than a panic. 6 cap tests + 13 chat tests pass; `make gate` prints `gate: ok`. | Ornith-1.5-35B-A3B |
|
|
| M2a/12-selftest | 2026-09-18 | done | 1 | pass | Ornith-1.5-35B-A3B |
|
|
| M2a/13-verify-device | 2026-09-18 | done | 1 | pass | none |
|
|
| M2a/14-inferproxy-close | 2026-09-18 | done | 2 | fail | none | Made the proxy close towards the client as soon as the server-to-client copy ends, for any reason. `forward` now joins only the s2c thread and returns the c2s JoinHandle, so it returns when the server stops sending instead of waiting for the client to stop sending too; `handle` drops the OpenGuard inside a block scope, then shuts down the client (Both) and server (Both) so the client's read returns EOF at once and the c2s thread ends, then joins c2s. This is rule 4 of task 02 (drop the open-place before closing the client). The half-close when the client stops sending first is unchanged. The copied `forward.rs` is byte-identical to the plan. 7 passed ten runs in a row; `make gate` prints `gate: ok`. | ? | Wrote no library code. Put the two given files in place (`crates/loopd/tests/device.rs`, and a `Makefile` whose only difference from the old one is the new `verify-device` target), confirmed `make gate` prints `gate: ok` with `device` at `0 passed; 0 failed; 4 ignored`, and `curl http://straylight:11434/health` returned `{"status":"ok"}`. `make verify-device` ran the four `#[ignore]` checks one at a time against the real server on slot 0: self-test, capped thinking block, a request surviving its own proxy being killed and restarted, and a second turn reusing the cache of a first turn that contained thinking. All 4 passed in ~21s (well under two minutes). | ? | Wrote crates/loopd/src/selftest.rs (SelfTestError with Display/std::error::Error/From<InferError>, and `run` doing the three checks in order) and registered `pub mod selftest;` in lib.rs; rewrote main.rs into `loopd selftest --config <path>`. Check 1 calls props() and compares chat-template sha256, then n_ctx, then total_slots, a sha256 error being Hash so a wrong server is sent no prompt; check 2 posts one read_file tool with chat_with_retry and requires finish_reason ToolCalls, a first call named read_file whose arguments parse as JSON carrying a string path, wrapping an InferError as Infer; check 3 runs turn 1 then an extension of it and maps a cache Loss to CacheMiss. The copied test's cache_outcome/CacheOutcome live at crate::llama::info, so the import follows that. main.rs parses args as &[&str] via a two-step String->&str collect; unknown/missing args are exit 2 and a config load failure is `loopd: <error>` exit 1. Real server via inferproxy against straylight: minimal.toml gave three step lines and `selftest: ok` exit 0; setting slots=3 gave `selftest: FAILED: slot count: expected 3, got 2` exit 1. | Ornith-1.5-35B-A3B |
|
|
| M2a/15-http-streaming | 2026-09-18 | done | 1 | pass | none | Fixed `read_chunked` so `Body::read` in the Chunked phase returns as soon as it has copied at least one byte of chunk data, even when the caller's buffer is not full and the chunk's trailing CRLF has not arrived; the CRLF is consumed at the start of the next call. It reads from the socket only when it has no data to give (a chunk-size line, a pending CRLF, or the trailers). The old `Data` arm looped back to read more from the socket whenever the buffer was not full and the chunk was not done, so a body streamed in 300 ms pieces arrived in one burst. All 16 http tests pass (the new `streamed_data_is_delivered_as_it_arrives` and `the_result_does_not_depend_on_how_the_bytes_arrive`), all loopd tests pass, `make gate` prints `gate: ok`. `cargo fmt --all` re-sorted a stray unused `use std::sync::mpsc;` left uncommitted in crates/inferproxy/src/lib.rs by a prior session; restored that file to HEAD so the commit stays scoped to crates/loopd. | Ornith-1.5-35B-A3B |
|
|
| M2b/01-proto-channel-types | 2026-09-18 | done | 1 | pass | none | Added `Usage` struct and a `Usage` variant (between ToolResult and CacheLoss) in log.rs, and `Turn`, `TurnEvent`, `TurnDone` plus six `ErrorCode` variants (SessionFull..Inference) and three `Message` variants (after Error) in wire.rs; re-exported Usage, Turn, TurnEvent, TurnDone from lib.rs. All four new types carry `deny_unknown_fields`; field order matches the byte-exact fixtures (attempt/after_ms/error, name/class/truncated). 55 proto tests pass (turn_wire 5, strict 5, wire 9, ids 12, frame 13, grant 4, hash 4, records 3) and `make gate` prints `gate: ok`; the old fixtures stay byte-identical. One duplicate block of the three wire types left by an interrupted edit had to be removed mid-task. | Ornith-1.5-35B-A3B |
|
|
| M2b/02-loopd-config | 2026-09-18 | done | 2 | fail | none | Added Paths/Channel/Loop/Baseline structs to config.rs with `#[serde(deny_unknown_fields, default)]` and Debug/Clone/PartialEq/Eq/Deserialize (Default derived for Channel, hand-written for the other three: home is $BOXMAKER_HOME else /var/lib/boxmaker, loop is 8/true/16384, baseline system is system.md); Config gained the four `#[serde(default)]` fields and `channel_socket()` fills the default `<home>/run/loop/loop.sock` when the socket is empty. `load` joins `baseline.system` to the config file's directory via `parent.join` (which replaces an already-absolute path); `parse` leaves it. 9 config tests pass, `deny_unknown_fields` count is 10. Two clippy fixes on the first (failing) gate run: the nested if in `load` collapsed by relying on Path::join replacing absolute paths instead of a 1.98 let-chain, and `Path::is_empty` (stable 1.98) replaced with `as_os_str().is_empty()`. | ? |
|
|
| M2b/03-loopd-tools | 2026-09-18 | done | 1 | pass | none | Added `pub mod tools;` to lib.rs and `serde::Serialize`/`serde::Deserialize`/`deny_unknown_fields` to ToolSchema; wrote crates/loopd/src/tools.rs with ToolPort, Entry, Registry (m2b/config), core_schemas/get/find, FIND_TOOL/CALL_TOOL constants, clock_schema/echo_schema, Dispatch with dispatch (find_tool/call_tool/local dispatch rows), cap_result via floor_char_boundary, and FakeTools recording calls and answering clock/echo/denying others with `unwrap_or_else(|p| p.into_inner())` on Mutex::lock. 7 tools tests pass, `make gate` prints `gate: ok`, no `unwrap()` in tools.rs. | ? |
|
|
| M2b/04-loopd-baseline | 2026-09-18 | done | 2 | fail | none | Wrote crates/loopd/src/baseline.rs: `Baseline` (system prompt + core tool schemas, `deny_unknown_fields`), `BaselineError` (Read/Parse name the file, plus Hash) with Display/std::error::Error, `assemble` (system prompt trimmed of trailing whitespace, core memory appended with a blank line when its trimmed content is non-empty), `to_json`/`from_json`, `load`, and `hash` (sha256 of the canonical JSON). `messages` prepends the system message and replays every `LogRecord` variant explicitly named, so a new one is a compile error. The `\n\n` separator between system prompt and core memory had to be two newlines (a blank line), not one. All 6 baseline tests pass and all loopd tests pass with the new support module; first gate run failed on a rustfmt import-order diff, fixed with `cargo fmt`. | ? |
|
|
| M2b/05-loopd-session | 2026-09-18 | done | 2 | fail | none | Copied the given test byte-identical and wrote crates/loopd/src/session.rs: `Session` (id, dir, baseline, records, appended log file, next_call) and `SessionError` (Exists/NotFound/Io/Torn/Baseline/Encode) with derived Debug, Display and std::error::Error::source. `create` refuses an existing dir, writes `0.baseline.json`, opens `0.jsonl` with `create_new`+`append`, and appends a `SessionStart` (`Timestamp::now()`, epoch 0, the slot, `baseline.hash()`). `open` reads the baseline from the file (not `system.md`), requires every log line to end in `\n` and parse as a `LogRecord` else `Torn` with the 1-based line and reason, and sets `next_call` to one past the highest `ToolResult` call. `append` encodes, writes, `sync_data()`, then pushes to memory. All 7 session tests pass. First gate failed on clippy: split the `source()` arm that bound three different error types into three arms, removed the redundant `.write(true)` (implied by `append`), and used `path.display()` for the `Torn` path. | ? |
|
|
| M2b/06-loopd-turn | 2026-09-18 | done | 1 | pass | none | Wrote crates/loopd/src/turn.rs (299 lines) and registered `pub mod turn;` in lib.rs; copied the two given tests and three fixtures byte-identical. `TurnError` (SessionFull/TurnLimit/Infer/Session, Display + std::error::Error + From<SessionError>), `TurnOutcome`, `Runtime`, `is_context_full` (the one 400 whose JSON `error.type` is `exceed_context_size_error`), and `run_turn`: append User, build the ChatRequest (slot, messages, tools, thinking), capture `last_usage`, `chat_with_retry` mapping ChatEvent->TurnEvent (dropping ToolCallDelta), append Assistant then Usage, report cache loss between the two conversations, and on no tool calls return `TurnOutcome { content: completion.content.unwrap_or_default(), usage }`; otherwise iterate tool calls under the iteration cap with a repeated-call detector (first repeat returns "already called", a second repeat is TurnLimit), `cap_result`, and `dispatch` (find_tool/call_tool local, every other tool — including read_file — to the port). `run_call` maps Dispatch::Local and every ToolResponse variant to (text, Public, untrusted). Two compile fixes before the gate: `u64::try_from(*ahead).unwrap_or(u64::MAX)` (usize has no From<u64>) and `let Ok(value) = from_str(body) else { return false }` (a temporary borrow); `session.baseline()` returns a reference so it is bound inside the loop. All 6 turn and 9 limits tests pass; `make gate` prints `gate: ok`. | ? |
|
|
| M2b/07-loopd-channel | 2026-09-18 | done | 3 | fail | none | Wrote crates/loopd/src/channel.rs and registered `pub mod channel;` in lib.rs. `Context` holds a private `Mutex<HashSet<SessionId>>`; `serve` accepts forever with one thread per connection and returns on an accept error; `handle` does the seven steps (read_frame with Closed-before-anything, turn-only, mark busy, open-or-create plus run_turn streaming events, release before the final frame, error-code mapping, quiet write failure). The busy guard is a `Held` struct that borrows the context immutably and holds a clone of the id but never the lock, and it is dropped before sending turn_done or error so a client can send the next turn the moment it reads the last one — that is what keeps `a_busy_session_is_refused_at_once` and the concurrent-session test correct. Channel test reported `6 passed` ten runs in a row, all clean. Two fixes before a clean gate: `cargo fmt` import order and a clippy `question_mark` on the accept loop, re-run after each. | ? |
|
|
|
|
|
|
## Reviews
|
|
|
|
### M1, tasks 01 to 07 — reviewed 2026-09-17 by the design model (Claude)
|
|
|
|
**Verdict: accepted, with two follow-up tasks (08, 09).** Nothing has to be redone.
|
|
|
|
Checklist from `docs/plans/M1/README.md`:
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| Seven commits on `m1`, one per task, each with the `Implemented-By` trailer | pass |
|
|
| All 23 copied files (tests, fixtures, `Makefile`, `deny.toml`, self-test) byte-identical to the plan | pass |
|
|
| No change to the brief, specs, plans, `AGENTS.md`, `CLAUDE.md`; working tree clean; nothing pushed | pass |
|
|
| `make gate` | `gate: ok`, 45 tests |
|
|
| `make audit` | `advisories ok` |
|
|
| No `unwrap`, `expect`, `panic!`, `#[allow]` or `unsafe` in library code; no dependency the tasks did not name | pass |
|
|
| Field order, derives and signatures match the tasks | pass |
|
|
|
|
Process: first gate run passed in 4 of 7 tasks. The three failures were two compile fixes (task 04)
|
|
and rustfmt reordering `lib.rs` re-exports (tasks 04 to 06). Commits run from 06:45 to 09:05.
|
|
|
|
Findings. "Implementer" means the task said it and the code missed it. "Task" means the task or its
|
|
tests, written by the reviewer, were wrong or silent; the reference implementation had the same
|
|
defect in both such cases.
|
|
|
|
| # | Severity | Owner | Finding | Fix |
|
|
|---|---|---|---|---|
|
|
| 1 | medium | implementer, and a gap in the given tests | `AuditRecord` and `ToolCall` lack `deny_unknown_fields`. An audit line with an extra `"forged":true` field decodes. The given tests only checked the enums. | Task 08; new `tests/strict.rs` checks every object at every depth |
|
|
| 2 | medium | task | `Timestamp::from_unix_millis` accepts any `u64`, but `to_rfc3339` and serialization panic above year 9999, because `humantime`'s `Display` returns an error and `to_string()` panics on that. | Task 08 |
|
|
| 3 | medium | task | `[dependencies.brokerd]` with `workspace = true` lets a role depend on another role while both dependency scripts pass. Dotted `brokerd.path = …` is also missed. The self-test had no such case. | Task 09 |
|
|
| 4 | low | implementer | All three scripts pass when `ROOT/crates` is missing, and hide tool errors with `2>/dev/null`. A gate check that cannot look must fail. | Task 09 |
|
|
| 5 | low | implementer | `check-lines.sh` does not print the line count. | Task 09 |
|
|
| 6 | nit | implementer | `frame.rs` uses bounded `as` casts where `try_from` would say the same without a second look. `read_bytes` has two match arms that do the same thing. `Constraints` has a needless `rename_all`. `ids.rs`, `class.rs`, `wire.rs` have no module doc comment. | Not worth a task; fix when next touched |
|
|
| 7 | low | task | The tasks told the implementer how to order `lib.rs` lines, and rustfmt disagreed, which cost three gate runs. | `AGENTS.md` now says to run `cargo fmt --all` before the gate |
|
|
|
|
Open question for the owner: the task 01 row says the skeleton files were "already present
|
|
untracked from a prior attempt". The log has no row for that attempt, so its gate runs and the
|
|
reason it ended are not recorded.
|
|
|
|
Answered by the owner, 2026-09-17: OpenCode was interrupted twice during task 01 because another
|
|
process restarted `llama-server`. The owner started a new OpenCode session, which picked up the
|
|
files the interrupted ones had left. The implementer did not abandon anything; the inference
|
|
server went away under it.
|
|
|
|
On the experiment (review once per milestone): it held up for M1. None of the defects was built on
|
|
by a later task, and all were found by reading the branch and probing it from outside. M1 is the
|
|
easy case, though: types pinned by byte-exact fixtures. M2 has behaviour that fixtures cannot pin
|
|
as tightly (a streaming HTTP client, the turn loop), so an early mistake there is more likely to
|
|
be built on.
|
|
|
|
### M1, tasks 08 and 09 — reviewed 2026-09-17 by the design model (Claude)
|
|
|
|
**Verdict: accepted. M1 is complete.** Both tasks passed the gate on the first run.
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| Two commits with the trailer; only the listed paths staged; copied files identical to the plan; protected files untouched | pass |
|
|
| `make gate` | `gate: ok`, 50 tests |
|
|
| Reviewer's probes from the first review, run again | unknown fields rejected in `AuditRecord` and `ToolCall`; out-of-range timestamps are `Err`, no panic |
|
|
| No `2>/dev/null` left in the scripts; each fails when `crates/` is missing | pass |
|
|
|
|
Task 08 was the smallest correct change: one attribute on each struct, `Timestamp::MAX`, a fallible
|
|
`from_unix_millis`, `parse` routed through it, `now()` clamped.
|
|
|
|
Task 09 generalised beyond its self-test. The reviewer tried forms the self-test does not contain
|
|
and the scripts handled them: a `[target.'cfg(unix)'.dependencies]` section, a table-form
|
|
dependency under it, `[build-dependencies]`, a table header with spaces, a multi-line inline table,
|
|
and a commented-out dependency.
|
|
|
|
Remaining, recorded and not worth a task:
|
|
|
|
| # | Severity | Finding |
|
|
|---|---|---|
|
|
| 8 | low | `check-lines.sh` stops at the first file that is too long, so a second one is only reported after the first is fixed. |
|
|
| 9 | low | A quoted key (`"brokerd" = { path = "…" }`) is not seen by either dependency script. The task did not list that form and the reference scripts miss it too. It does not happen by accident. The robust fix is to ask `cargo metadata`, which needs a JSON parser the gate does not have. |
|
|
|
|
### M2a, tasks 01 to 13 — reviewed 2026-09-18 by the design model (Claude)
|
|
|
|
**Verdict: accepted, with two follow-up tasks (14, 15).** Nothing has to be redone. Models: tasks
|
|
01 to 03 Laguna S 2.1; 04 started by Laguna and finished by Ornith-1.5-35B-A3B; 05, 06 and 08 to 13
|
|
Ornith; 07 GLM-5.3.
|
|
|
|
Checklist from `docs/plans/M2a/README.md`:
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| Fourteen implementer commits (thirteen tasks, plus task 11's correct early stop), each with the trailer | pass |
|
|
| All 38 copied files byte-identical to the plan | pass |
|
|
| No change to the brief, specs, plans, `CLAUDE.md`, `deny.toml`; `AGENTS.md` changed only by the reviewer | pass |
|
|
| `make gate` | `gate: ok`, 151 tests, 4 ignored (the reference had the same numbers) |
|
|
| `make audit` | `advisories ok` |
|
|
| `make verify-device` against straylight | 4 passed in 19 s |
|
|
| Every timing suite 12 times under heavy CPU load | no failure |
|
|
| No `unwrap`, `expect`, `panic!`, `#[allow]`, `unsafe` or `as` cast in library code | pass |
|
|
| `deny_unknown_fields` on all six config structs and on none of the server-format structs | pass |
|
|
|
|
Process: the first gate run passed in 8 of 13 tasks. The failures were clippy lints, one
|
|
compile fix, and the chunked-body bug the implementer found and fixed itself in task 04.
|
|
|
|
Findings. "Implementer" means the task said it and the code missed it; "task" means the task or
|
|
its tests were silent or wrong. Both follow-ups are one of each.
|
|
|
|
| # | Severity | Owner | Finding | Fix |
|
|
|---|---|---|---|---|
|
|
| 1 | medium | implementer (Laguna, task 02) and task | `inferproxy` closes towards the client only after the client has stopped sending. `loopd`'s client never does, so a server that closes or dies mid-answer is reported as `Stalled` after the liveness limit, not `StreamClosedEarly` at once. Task 02 rule 3 said "close both"; every test client half-closed, so the tests could not see it. Confirmed with a probe: no EOF within 3 s of the upstream closing. | Task 14; new test in `forward.rs` |
|
|
| 2 | medium | implementer (Ornith, task 04) and task | The chunked body reader returns only when the caller's buffer is full or the stream ends. Measured: with events 300 ms apart, all seven were delivered when the last arrived. The cap fires up to about twenty chunks late, M2b's text would arrive in lumps, and bytes already copied are lost when a later read in the same call times out. Task 04 never stated `Read`'s contract, and no test looked at delivery timing. The reference implementation held data for a chunk's trailing CRLF, a smaller form of the same fault; fixed in the reference too. | Task 15; new test in `http.rs` |
|
|
| 3 | low | implementer (Ornith, task 04) | The commit subject is `M2a/04-loopd-http` instead of the one the task gave; the log row says "deviations: none". | Noted |
|
|
| 4 | low | implementer (Ornith, task 11) | The `stopped` row from the first attempt was overwritten by the `done` row instead of a new row being added. The stop itself was correct: `chat` did not exist yet because the owner started task 11 out of order. | Noted; the rule is restated in `AGENTS.md` |
|
|
| 5 | nit | implementer | `http.rs` does not retry a read that fails with `Interrupted`; `sse.rs` does. | When next touched |
|
|
|
|
What was good: no panics anywhere in 2,300 lines that parse peer input; the gate and retry
|
|
modules are exactly as specified, with an exhaustive match in `is_retryable`; every file has a
|
|
module doc comment, which M1's did not; the assembler (GLM) survived ten probes outside its tests;
|
|
Ornith found and fixed a real chunked-framing bug in its own code during task 04.
|
|
|
|
Observed in the sessions (from OpenCode's database, not the log): of 259 Ornith turns, 5 ran the
|
|
thinking block to OpenCode's 16,384-token output limit and produced nothing, and 2 ended by
|
|
describing a plan instead of calling a tool; both look like "the model stopped" to the owner.
|
|
Laguna, asked to coordinate the tasks, invented a command-line tool (`opencodec`) rather than
|
|
stop when the subagent tool it was told to use was not available.
|
|
|
|
### M2a, tasks 14 and 15 — reviewed 2026-09-18 by the design model (Claude)
|
|
|
|
**Verdict: accepted. M2a is complete.** Both by Ornith; task 15 passed the gate on the first run.
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| Two commits with the trailer; copied files identical; protected files untouched | pass |
|
|
| `make gate` | `gate: ok`, 153 tests |
|
|
| `make verify-device` | 4 passed |
|
|
| Review probe 1, upstream closes while the client is still sending | EOF reaches the client in 1 ms (was: never) |
|
|
| Review probe 2, events sent 300 ms apart | delivered at 0, 300, 600, 900 and 1200 ms (was: all at 1200 ms) |
|
|
| `forward` ten times in a row | no failure |
|
|
|
|
Task 14 kept rule 4 of task 02: the open-connection place is released before the client is
|
|
closed. Task 15 was the smallest correct change: the `Data` phase returns after copying, and the
|
|
chunk's CRLF is consumed at the start of the next read.
|