Task files, the files they copy in (byte-identical to the reference on m3a-ref), each area's check record, and a README with the per-task table of what each check exposed. The handoff note is done with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.7 KiB
M3a checks, fork C: loopd (tasks 16 and 17)
Reference tree: ~/src/boxmaker-ref-c, branch m3a-ref-c. The rest of the workspace does not
build its tests there (proto's records.rs and wire.rs belong to other forks), so every
command below is -p loopd. cargo clippy -p loopd -p bxctl --all-targets -- -D warnings is
clean, and scripts/check-lines.sh passes.
Task 16: the tool port, the registry and denials
Check: in effect a reference. The task changes working code, and the smallest change that lets
the updated tests compile is the whole change (about 150 lines in tools.rs, 30 in turn.rs). A
todo!() skeleton was not possible: the existing turn-loop tests run through this code.
Run to green, three times: tools (10), turn (6), turn_broker (5, new), and unchanged
limits (9), channel (6), session (7), baseline (7), serve (3) and the rest of the suite.
Defects the check exposed:
- Spec, section 8. "
echostays inFakeToolsfor tests only" is not enough: two recorded conversations (find_tool.http,call_tool.http) and four test files discoverechothrough the registry. RemovingRegistry::m2b()would have meant new copies ofbaseline.rs,channel.rsandsupport/turn.rsfor a one-word change each. Resolved:m2b()stays as the test registry,m3a()is whatloopd serveuses. Proposed wording for section 8: "echostays inFakeToolsand in the test registryRegistry::m2b();loopd serveusesRegistry::m3a()." - Spec, section 8, unclear.
tool_denied { name, reason }does not say which name. For acall_toolcall,call.nameiscall_tool, which tells the owner nothing. Resolved: both new events carry the toolbrokerddecides on (request.tool);tool_call_startedandtool_resultkeepcall.name. A test pins it (a_denied_call_tool_names_the_target_tool). Task 20 (bxctl chat) should know thatApprovalPending.toolis the target tool. - Spec, silent. What
run_calldoes if a port returnsPendingApprovalas its answer. It cannot be removed (thematchmust be exhaustive whileToolPortreturnsToolResponse). Resolved: a plain failure, "The tool failed: the tool broker gave no final answer", no event. It is a failure of the port, not a decision about the call. - Tests.
turn.rsgrew past 500 lines; the new tests moved toturn_broker.rs. - Verified claim. "The registry change affects only new
find_toolresults; the tools array is unchanged" is true:Baseline::assembletakes onlyregistry.core_schemas(), which isclock,find_tool,call_toolwith identical schemas form2b()andm3a(); resumed sessions use their snapshot.the_m3a_registry_has_the_same_core…asserts equality. - Plan, outside this fork:
make verify-devicebreaks at task 16.device.rsasks Ornith to "use the echo tool" throughloopd serve, and asserts thatfind_toolandcall_toolwere called. Withm3a()there is noechoto find. Task 17's files carry a changeddevice.rs(find a file-reading tool, call it on/etc/hostname, which fails with "no tool broker is configured"; the restart check asks for the first turn's words instead of the echoed word). It compiles and is clippy-clean. It has not been run on straylight (this fork was told not to contact it), and it depends on model behaviour, so the owner should run it once before the plan is handed over. Between tasks 16 and 17verify-deviceis broken either way.
Task 17: BrokerPort, [broker] and the pointers
Check: skeleton for BrokerPort::call (todo!()); everything else in the task is small
enough that the skeleton is the code (config::Broker, NoBroker, the two line functions, the
main.rs wiring, the three pointers).
Run to green: config (11, two new), pointers (7, new; three of them run the loopd binary),
broker_port::without_a_broker… (1).
Compiled only, never run to green: the other 14 tests of broker_port and both tests of
broker_port_bad. They were desk-checked against the exit list in the task, not executed. The
likeliest faults, if any: a timing bound too tight under load (a_broker_that_never_answers…
allows 250 to 1,100 ms for a 300 ms timeout; a_pending_call_nobody_answers… allows 550 to
1,700 ms), and a fake-broker thread that panics noisily, but harmlessly, when the port closes
without sending.
Defects the check exposed:
- Spec, section 8, ambiguous. "The read timeout is
timeout_msuntil the first frame" can be read as a per-read socket timeout. That lets a peer that trickles bytes hold a turn for ever, and it cannot express "untilexpiresplustimeout_ms" at all. Resolved as a deadline per frame wait, with a test whose answer (not its timing) tells the two apart. Note this is a total deadline, unlike the inference path's liveness rule; the spec already implies it ("M3b must keep its tool time limit undertimeout_ms"). Proposed wording: "BrokerPortwaits for the first frame untiltimeout_msafter the call began, and after a pending frame until the frame'sexpiresplustimeout_ms. These are deadlines, not per-read timeouts." - Spec, silent: a request too large for a frame.
argumentsnear 1 MiB makeswrite_framefail withTooLarge. Calling that "the tool broker is unavailable" with a runbook pointer would send the owner looking for an outage. Resolved: its own failure text, "the request is too large for the tool broker", nothing printed. - Spec, silent:
expiresis a peer's number. A far-futureexpiresoverflowsInstant + Duration, which panics. Resolved:checked_add, andNoneis exit 10. - Spec, silent: the envelope
id. Resolved:request.call.0; every answer must carry it. - Spec, silent:
timeout_ms = 0.set_write_timeout(Some(0))is an error in std. Resolved: no config validation; the call fails closed at exit 3, and a test checks there is no panic. - Spec:
core-memory-unreadableneeds a new error variant.BaselineError::Readserves bothsystem.mdandcore.md; the pointer belongs to one. AddedBaselineError::Core. The M2b test that checks the unreadablecore.mdonly looks for "core.md", so it still passes. - Testability. "
loopdprints …" cannot be asserted in-process througheprintln!. Resolved withBrokerPort::with_logand the two*_line()functions; the startup and self-test lines are asserted by running the binary. - Task-writing. The line-limit hit again:
broker_port.rswas 546 lines. Split intobroker_port.rs,broker_port_bad.rsandsupport/broker.rs.
For the record of the experiment
What skeleton-only cost here: 16 of about 40 new or changed tests in this area were never run. What
it saved: about 110 lines of BrokerPort. The five spec gaps under task 17 (1 to 5) were all found
while writing the exit list and the tests, not by running anything, so a reference would not have
found them sooner; what a reference would add is only confidence in the timing bounds.
Run on straylight (2026-09-18, at the merge)
Task 17's changed device.rs was run with make verify-device on the merged reference tree
(f9195ec: areas A to D, loopd at the task 17 state with no broker configured) against Ornith
on straylight, slot 0, after checking /slots showed both slots idle. It passed twice in a row,
6 of 6 each time (43 s and 39 s). What the test asserts is that Ornith called find_tool and
then call_tool, that clock, find_tool and call_tool each left a result, and that the
restart check passed; which tool it found and what the call returned are not asserted. Between
tasks 16 and 17 verify-device is still broken, as noted above; the owner should not run it there.