In-Process SPDM Requester — Implementation Plan

Companion to DESIGN_IN_PROCESS_REQUESTER.md. Breaks the design into landable phases, each independently compileable and testable so nothing goes dark on main between merges.

Status

PhaseStatusNotes
0 — baseline build + assumption check✅ doneRequired two pre-Phase-1 fixes (see Phase 0 Resolution Log)
1 — feature flag restructuring✅ donedirect-client is now the shared capability; role features in-process-responder / in-process-requester pick the FSM. Mutual-exclusion compile_error! in main.rs.
2 — compileable requester stub✅ doneForced creation of a second system_image package //target/ast1060-evb/mctp-requester so the standalone rust_app target actually compiles (standalone rust_app hits a pw_kernel/userspace platform-constraint error without a system image).
3 — FSM happy path✅ doneEight-state VCA FSM, one step per polling-loop iteration.
4 — safety net + counters✅ doneAWAIT_STEP_BUDGET = 10_000, await_try! macro, counters req_send_ok / req_recv_ok / req_recv_pending.
5 — Bazel target + system image✅ done (merged into Phase 2)See Phase 2 note above.
6 — two-board integration test⏸ blockedNo on-target hardware available in this environment. See IMPLEMENTATION_PLAN.md §A for measurements to collect when hardware is accessible.
6a — QEMU integration test (virt-ast1060-evb)✅ doneQEMU patched with ASPEED I2C emulation; //target/virt-ast1060-evb/mctp-requester:mctp_requester_test added. No two-board setup needed.
7 — docs + follow-up TODOs✅ doneINITIALIZATION.md updated with requester glossary + setup steps; follow-up design refs annotated at the FSM in main.rs.

Build verification at end of Phase 7:

bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp                                          # responder (hw) — clean
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester                      # requester (hw) — clean
bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester             # requester (QEMU) — clean
bazel test  --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test        # requester QEMU test

Legend:

  • Goal — what this phase must achieve.
  • Deliverables — concrete artifacts produced.
  • Files — edited / added in this phase.
  • Exit criteria — what proves the phase is done.
  • Risks / rollback — what can go wrong; how to back out.

How To Build Which Role

Role (requester vs. responder) is selected at build time, not at runtime. Same src/main.rs source, two binaries, chosen by which Bazel system-image target you build. Standalone rust_app targets do not build in isolation — pw_kernel/userspace requires a target_codegenkernel_config chain that only exists inside a system_image.

Developer commands:

# Responder image (default, hardware)
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp

# Requester image (hardware)
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester

# Requester image (QEMU — requires patched QEMU with ASPEED I2C emulation)
bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester
bazel test  --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test

Running any test or integration probe uses the same prefix (bazel test --config=k_ast1060_evb <target>).

Selection chain — each layer picks the next:

 System image                                    →  rust_app label                              →  Cargo features                                           →  #[cfg] in main.rs
 ────────────────────────────────────────────────   ─────────────────────────────────────────────   ───────────────────────────────────────────────────────   ─────────────────────────
 //target/ast1060-evb/mctp:mctp                  →  //services/mctp/server:mctp_server          →  ["i2c-polling", "direct-client", "in-process-responder"] →  responder setup + step
 //target/ast1060-evb/mctp-requester:mctp_requester →  //services/mctp/server:mctp_server_requester  →  ["i2c-polling", "direct-client", "in-process-requester"] →  requester setup + FSM

Each rust_app has its own codegen_crate_nameapp_mctp_server for the responder, app_mctp_server_requester for the requester — because two rust_app rules in the same Bazel package cannot share the codegen name. src/main.rs picks the right one via a cfg-gated use.

Guards:

  • Both roles at once → rejected by compile_error! at the top of main.rs.
  • Neither role + i2c-polling → polling loop compiles with no SPDM in-process (still valid; drains I2C only).
  • No i2c-polling → the notification (WG + IRQ) loop compiles; role features are ignored because in-process-* code is also gated on i2c-polling.
  • direct-client is listed explicitly in both crate_features sets; Bazel's rust_binary does not resolve Cargo-level feature implications (in-process-* = ["direct-client"]), so the transitive dep must be named directly.

What flashes: each system image binds one of the two rust_app labels via its apps = [...] list, together with i2c_server. The two images are mutually exclusive — flash one or the other, not both.

Direct cargo build is not a supported path for the binary — the package sets autobins = false and main.rs is a Pigweed binary built only through Bazel. The Cargo features still matter because Bazel passes them through; they're just never consumed by a bare cargo invocation of main.rs.


Phase 0 — Baseline & Assumption Verification

Goal: confirm the starting source tree builds clean before any new code is written. Source-level only — no on-target execution is required or assumed at this stage.

Deliverables:

  • Successful Bazel build of //target/ast1060-evb/mctp:mctp (responder system image) at current ocp-emea-demo-spdm-spdm-req-resp-anthony-i2c-polling HEAD.
  • Source-level re-read of the existing polling loop at src/main.rs:322-445 to confirm the interleaving assumptions in design §6.5 still match the code (in particular, that wait_for_messages(..., None) paired with the e.is_timeout() arm at src/main.rs:390 matches constraint §6.5.3).
  • Skipped: standalone build of //target/ast1060-evb/spdm-req-resp-test:spdm_requester_app. The app is marked # DISABLED in the sibling system_image and fails platform analysis when built alone. The reference constants in spdm_requester.rs are consumed as source, not as a binary, so skipping this build does not block later phases.

Files: none modified initially. See Phase 0 Resolution Log below.

Exit criteria: the responder system image builds clean; the §6.5 cross-check is recorded as “verified against HEAD” in the commit message of Phase 1.

Risks / rollback: none — read-only phase by default; any unblocker applied here gets its own commit (see log below).

Phase 0 Resolution Log

  • Blocker discovered: at branch HEAD, commit 023e75e "Update mctp-lib (to hotfix branch)" changed mctp_lib::Router::inbound to return Result<Option<AppCookie>, MctpError>, but the services/mctp/server/src/server.rs:267-271 wrapper Server::inbound still declared Result<(), MctpError>. The error surfaces as E0308 and fails every Bazel target that depends on mctp_server_lib (responder system image, req-resp test image, etc.).

  • Verification that the break is isolated: a worktree at d4719a0 "Chasing down an MCTP packet issue." (the commit immediately preceding the mctp-lib hotfix) built //target/ast1060-evb/mctp:mctp clean. So the break is entirely within 023e75e.

  • Resolution — Fix A (discard the new cookie):

    pub fn inbound(&mut self, pkt: &[u8]) -> Result<(), MctpError> {
        self.stack
            .inbound(pkt)
            .map(|_| ())
            .map_err(mctp_error_to_server_error)
        }
    

    Chosen over Fix B (propagate Option<AppCookie> to callers) because: no caller reads the return value today, the propagation case is cheap to add later if Shape-B from design §9.1 ever lands, and keeping the unblocker orthogonal to the requester work eases git blame. Landed as its own small commit before any Phase 1 work.

  • Second blocker (surfaced after Fix A unblocked rustc past the first error): the same mctp-lib hotfix changed MctpI2cReceiver::decode's return type from (&[u8], u8) to (&[u8], MctpI2cHeader) — a struct { dest: u8, source: u8, byte_count: usize } — but src/main.rs:350-361 still treated the second tuple element as a bare u8 source address. as u32 on a struct triggers E0605.

  • Resolution — Fix A′ (match the new type, minimum diff): rename the binding from src_addr to hdr and read the source address field explicitly: hdr.source as u32. Landed in the same “pre-Phase-1 unblocker” commit as Fix A.

Deferred to on-target bringup: measuring actual idle-poll rate, wait_for_messages latency, and worst-case fragment count is out of scope for Phase 0. AWAIT_STEP_BUDGET will ship with a generous compile-time default (§6.4) and be tuned when a runtime becomes available — see Appendix §A.


Phase 1 — Feature Flag Restructuring

Goal: rename the responder role feature and add the requester role feature, without changing behavior. Pure refactor.

Deliverables:

  • services/mctp/server/Cargo.toml:
    • Add in-process-responder = ["direct-client"].
    • Add in-process-requester = ["direct-client"].
    • Update default to ["i2c-polling", "in-process-responder"].
    • Keep direct-client as-is (library-exposed capability).
  • services/mctp/server/src/main.rs:
    • Add at top:
      #[cfg(all(feature = "in-process-requester", feature = "in-process-responder"))]
      compile_error!("features `in-process-requester` and `in-process-responder` are mutually exclusive");
      
    • Replace every #[cfg(feature = "direct-client")] inside the polling loop and its setup block with #[cfg(feature = "in-process-responder")].
    • Leave the mod direct_client; export and DirectMctpClient untouched — those belong to the direct-client feature, which still exists.
  • services/mctp/server/BUILD.bazel:
    • mctp_server target: replace crate_features = ["i2c-polling", "direct-client"] with ["i2c-polling", "in-process-responder"].
    • Keep rust_library mctp_server_lib with crate_features = ["direct-client"].

Files: Cargo.toml, src/main.rs, BUILD.bazel.

Exit criteria:

  • bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp succeeds (the standalone rust_app target cannot be built in isolation because pw_kernel/userspace needs a system-image codegen context).
  • Built binary is byte-identical in behavior to Phase 0 baseline (responder loop still runs; log lines at startup unchanged).
  • Attempting to build with both features enabled emits the compile_error!.

Risks / rollback:

  • Risk: downstream Bazel targets still set direct-client directly and silently lose the responder. Mitigation: grep the repo for direct-client in crate_features; the only consumer is services/mctp/server/BUILD.bazel.
  • Rollback: revert the single refactor commit; no logic changed.

Phase 2 — Compileable Requester Stub

Goal: add the requester setup skeleton behind in-process-requester as a no-op Phase 2 step. Proves feature gating, imports, and the init sequence compile — no FSM logic yet.

Deliverables:

  • services/mctp/server/src/main.rs:
    • Add constant const REMOTE_RESPONDER_EID: u8 = 42; (module-scope, not cfg-gated — a single #[allow(dead_code)] keeps it clean when the feature is off).
    • Add imports gated on #[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))]: mock platform types, MctpSpdmTransport, SpdmContext, VCA generate_* functions, DemoPeerCertStore.
    • Inside mctp_loop() under the same cfg, add the full init sequence from §7 of the design doc:
      1. DirectMctpClient::new(&server)
      2. MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID)
      3. transport.init_sequence() with error log + return
      4. Mock platform instances (cert_store, hash×3, rng, evidence, peer_cert_store)
      5. CapabilityFlags / DeviceCapabilities matching spdm_requester.rs
      6. LocalDeviceAlgorithms (factor into a helper to avoid duplicating the responder's inline block)
      7. SpdmContext::new(..., Some(&mut peer_cert_store), ...)
      8. MessageBuf on stack buffer
      9. let mut req_state = ReqState::SendVersion;but Phase 2 step body is still just #[allow(unused_variables)] let _ = &mut req_state; with a log "requester FSM stub: state=..." every 0xfff loops.
    • Add the ReqState enum (full variant set — §6.1) even though only one variant is used, so Phase 3 just fills in match arms.

Files: src/main.rs.

Exit criteria:

  • bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester succeeds. (Phase 2 verification required pulling Phase 5's system-image package forward — a standalone rust_app target is not buildable on its own in this tree; see mctp-requester/ for the system image created here.)
  • The responder build is unaffected.
  • Running the requester build on-target logs the init sequence, then idle-poll messages with the stub state — no SPDM traffic emitted yet.

Risks / rollback:

  • Risk: Rust borrow checker rejects SpdmContext::new arg layout because transport and server share a lifetime. Mitigation: mirror the responder's exact let-binding order, which is already known to compile.
  • Risk: DemoPeerCertStore import path differs from responder (responder uses None). Mitigation: path already resolved in spdm_requester.rs — copy.
  • Rollback: revert commit; responder build unaffected.

Phase 3 — FSM Implementation (Happy Path)

Goal: wire the six Send/Await states so a full VCA flow completes on the wire.

Deliverables:

  • Replace the Phase-2 stub body with the match from §6.2 of the design doc. Each Send state:
    1. msg_buf.reset()
    2. generate_<step>(...)
    3. ctx.requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID)
    4. Advance to matching Await* state.
  • Each Await state:
    1. msg_buf.reset()
    2. ctx.requester_process_message(&mut msg_buf)
    3. Ok(_) → advance; Err(_) → stay (counted, not yet budgeted).
  • On entry to ReqState::Done, log once: "SPDM VCA completed: version/caps/algs OK".
  • Done and Failed states become no-ops in Phase 2 (Phase 1 keeps draining I2C unconditionally, so late inbound traffic is still handled).

Files: src/main.rs.

Exit criteria:

  • End-to-end test on AST1060-EVB against the existing spdm_responder app running on a second device (or the same device with the responder built separately) produces three successful request/response pairs.
  • Protocol-analyzer capture (or I2C frame logs already present) shows GET_VERSION / GET_CAPABILITIES / NEGOTIATE_ALGORITHMS emitted in order with matching response consumption.
  • spdm_err / equivalent pending-counter stays bounded (not monotonically growing past expected poll-while-waiting count).

Risks / rollback:

  • Risk: a protocol error wedges the FSM in an Await state forever (design §6.3, option 1 not yet implemented). Mitigation: Phase 4 adds the step budget; until then, a power-cycle is acceptable for bringup.
  • Risk: include_supported_algorithms = false / other capability fields diverge between the in-process requester and the responder's expectations. Mitigation: copy verbatim from spdm_requester.rs; already known to interoperate with spdm_responder.rs.
  • Rollback: revert to Phase 2 stub.

Phase 4 — Safety Net & Observability

Goal: bound failure latency and give on-target debugging the same counter set as the responder.

Deliverables:

  • AWAIT_STEP_BUDGET const + await_steps counter per design §6.4, reset on every state transition, transitions FSM to Failed on exhaustion with an error log naming the state that timed out.
  • Counters from design §8, each u32 wrapping, logged on first event and every 256th:
    • req_send_ok, req_send_err, req_recv_ok, req_recv_pending, req_recv_err_terminal.
  • State-transition log (one line per transition, not rate-limited — low volume).
  • Done transition log augmented with "completed in {N} steps, ~{idle_polls} idle polls".

Files: src/main.rs.

Exit criteria:

  • Forcing a fault (e.g. responder offline) causes Failed transition within the budget and emits the named-state error; device does not hang.
  • Counters visible in logs match observed behavior (send counts == 3 on success path; pending spikes and settles at each Await entry).

Risks / rollback:

  • Risk: step budget too tight → spurious failures under a legitimately multi-fragment response (§6.5.2). Mitigation: ship with a deliberately generous default (AWAIT_STEP_BUDGET = 10_000) and defer tightening until on-target measurements exist (see Appendix §A). Erring high costs only wall-clock on genuine failures; erring low wedges the happy path.
  • Rollback: revert budget block, keep counters (useful regardless).

Phase 5 — Bazel Target & System Image

Goal: make the requester role selectable as a first-class Bazel build without touching the responder's target.

Deliverables:

  • New target in services/mctp/server/BUILD.bazel:
    rust_app(
        name = "mctp_server_requester",
        codegen_crate_name = "app_mctp_server",
        srcs = ["src/main.rs"],
        crate_features = ["i2c-polling", "in-process-requester"],
        edition = "2024",
        system_config = "//target/ast1060-evb/mctp:system_config",
        tags = ["kernel"],
        visibility = ["//visibility:public"],
        deps = [ # identical to mctp_server
            ":mctp_server_lib",
            "//services/i2c/api:i2c_api",
            "//services/i2c/client:i2c_client",
            "//services/mctp/api:mctp_api",
            "//services/mctp/transport-i2c:mctp_transport_i2c",
            "//services/spdm/transport-mctp:spdm_transport_mctp",
            "//target/ast1060-evb/spdm-req-resp-test:mock_platform",
            "@rust_crates//:mctp",
            "@rust_crates//:mctp-lib",
            "@pigweed//pw_kernel/syscall:syscall_user",
            "@pigweed//pw_kernel/userspace",
            "@pigweed//pw_log/rust:pw_log",
            "@pigweed//pw_status/rust:pw_status",
            "@oot_crates_no_std//:spdm-lib",
        ],
    )
    
  • Choose whether the default system image (target/ast1060-evb/mctp/BUILD.bazel) wires the requester or the responder. Do not change the default in this phase — add a second system image build instead (e.g. system_requester) so both can be flashed side by side during bringup.

Files actually landed:

  • services/mctp/server/BUILD.bazel — second rust_app(name = "mctp_server_requester", codegen_crate_name = "app_mctp_server_requester", ...) target.
  • target/ast1060-evb/mctp-requester/BUILD.bazel — new sibling package with its own system_image, target_codegen, target_linker_script, rust_binary(name="target",...), and uart_boot_image.
  • target/ast1060-evb/mctp-requester/system.json5 — copy of the mctp image's config with the MCTP app renamed to mctp_server_requester.
  • target/ast1060-evb/mctp-requester/target.rs — copy of the kernel entry shim.

Exit criteria:

  • Both system images build clean from scratch:
    bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp
    bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester
    
  • Responder system image continues to boot unchanged.
  • Requester system image boots and reaches the FSM entry log within the expected startup window (deferred to on-target bringup — Phase 6).

Risks / rollback:

  • Risk: codegen_crate_name collision when both rust_app targets live in the same Bazel package. Resolved: the two targets use distinct codegen crate names (app_mctp_server, app_mctp_server_requester) and src/main.rs selects between them with a cfg-gated use.
  • Risk: platform-constraint analysis error when building rust_app standalone. Resolved: the rust_app target must be consumed by a system_image, never built directly.
  • Rollback: delete the new system-image package; responder unaffected.

Phase 6 — Integration Test (two-board, on-target hardware)

Goal: a repeatable test that exercises the in-process requester against the in-process responder on the bench.

Status: ⏸ blocked — no on-target hardware available. See §A for measurements to collect when hardware is accessible.

Deliverables:

  • Two-board test config: board A flashed with mctp_server_requester system image, board B flashed with mctp_server (responder). Both use current OWN_EID / OWN_I2C_ADDR constants, with board A targeting board B at REMOTE_RESPONDER_EID = 42 — board B must be re-configured to EID 42 (its current OWN_EID = 0x08; this requires a new const RESPONDER_OWN_EID on the responder image, or swapping A↔B roles).
  • Documented test recipe in README.md (new section) or a dedicated tests/ON_TARGET_README.md: flash steps, expected log lines, expected duration-to-Done.
  • Automated log-checking script is out of scope; bringup-level manual verification is sufficient.

Files: docs only, plus any const bump in main.rs if we change OWN_EID on the responder side.

Exit criteria:

  • Manual test produces a SPDM VCA completed line on the requester side and three matching processed-request lines on the responder side.

Risks / rollback:

  • Risk: EID mismatch between the two images. Mitigation: the spec prescribes explicit named constants for both ends; document the pairing in the test recipe.
  • Rollback: none needed — docs only.

Phase 6a — QEMU Integration Test (virt-ast1060-evb)

Goal: automated QEMU test that exercises the full I2C + MCTP + SPDM requester stack without physical hardware, using QEMU's ASPEED I2C device emulation.

Prerequisite: QEMU patched with ASPEED i2c device model (user confirmed April 2026).

Deliverables:

  • target/virt-ast1060-evb/mctp-requester/system.json5 — two-process (i2c_server + mctp_server_requester) virt image with ASPEED I2C MMIO mappings at the real hardware addresses (emulated by QEMU).
  • target/virt-ast1060-evb/mctp-requester/target.rs — semihosting kernel shim; shutdown() calls exit(EXIT_SUCCESS/FAILURE) so QEMU terminates and the test harness can read the exit code.
  • target/virt-ast1060-evb/mctp-requester/BUILD.bazelsystem_image, system_image_test, target_codegen, target_linker_script, rust_binary(name="target") wired to //target/virt-ast1060-evb platform and linker template.
  • services/mctp/server/BUILD.bazelmctp_server_requester_virt rust_app, same feature set as mctp_server_requester but with system_config = "//target/virt-ast1060-evb/mctp-requester:system_config" so codegen (handle constants + linker script) is correct for QEMU.

Files:

  • target/virt-ast1060-evb/mctp-requester/system.json5 (new)
  • target/virt-ast1060-evb/mctp-requester/target.rs (new)
  • target/virt-ast1060-evb/mctp-requester/BUILD.bazel (new)
  • services/mctp/server/BUILD.bazel (add mctp_server_requester_virt)

Build / test commands:

bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester
bazel test  --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test

Exit criteria:

  • bazel test passes: QEMU starts, the FSM reaches Done, semihosting exit(EXIT_SUCCESS) fires, QEMU terminates with code 0, Bazel marks the test green.
  • Responder hardware image (//target/ast1060-evb/mctp:mctp) and hardware requester image (//target/ast1060-evb/mctp-requester:mctp_requester) still build clean.

Key design notes:

  • mctp_server_requester_virt shares codegen_crate_name = "app_mctp_server_requester" with the hardware target — the handle layout is identical (I2C object is second in the process objects list in both system configs), so no source changes are needed.
  • Memory layout (640KB) mirrors virt-ast1060-evb/spdm-req-resp-test; vector_table_size_bytes = 1280 (matches multi-process virt convention).
  • ASPEED I2C MMIO mappings (0x7e7b0000 / 0x7e6e2000) are kept so the i2c_server binary can run unmodified against the QEMU device model.

Risks / rollback:

  • Risk: QEMU‘s ASPEED I2C model doesn’t emulate bus-2 slave-mode loopback precisely enough for the polling loop. Mitigation: QEMU patch is user-supplied and already tested; fall back to the two-board (Phase 6) path if emulation fidelity is insufficient.
  • Rollback: delete the new target/virt-ast1060-evb/mctp-requester/ package and remove mctp_server_requester_virt from services BUILD.bazel; hardware targets are unaffected.

Phase 7 — Cleanup & Follow-Up Tickets

Goal: close loose ends the design doc called out and make sure nothing is left under-documented.

Deliverables:

  • File follow-up issues (or local TODO comments tagged with a tracking tag) for:
    • Design §6.3 option 2 — propagate TransportError out of requester_process_message so TimedOut can be discriminated.
    • Design §9.1 — pumping-DirectMctpClient variant (Shape B).
    • Design §9.3 — external trigger mechanism (IPC / GPIO / policy).
    • Design §9.4 — dual-role configuration.
  • Update INITIALIZATION.md with a new “Polling mode (requester)” subsection mirroring the responder's, so the glossary stays truthful.
  • Update the build-modes table in both main.rs module docs and INITIALIZATION.md to list the new requester row.

Files: INITIALIZATION.md, src/main.rs (module-level doc comment only).

Exit criteria: follow-up issues filed; docs reviewed; no undocumented feature flag.

Risks / rollback: none — docs and issue filing.


Phase Dependency Graph

Phase 0 ──► Phase 1 ──► Phase 2 ──► Phase 3 ──► Phase 4 ──► Phase 5 ──► Phase 6 ──► Phase 7
                          │                                    ▲
                          └────────────────────────────────────┘
                    (Phase 5 can start after Phase 2 in parallel
                     with Phase 3/4 if a separate hand is available)

Phases 1 and 2 are small (half-day each). Phase 3 is the bulk of the work — expect a full day including on-target debug. Phases 4–6 are each half-day. Phase 7 is a loose-ends sweep.


A. Deferred Measurements (on-target bringup)

These values are not required to land Phases 0–5. Capture them once hardware execution is available (Phase 6 onward) and revisit §6.4 of the design doc if any value contradicts the back-of-envelope 10_000 default.

MetricMeasured valueSource
idle_polls increment rate (per wall-clock second)deferredresponder log, counted over a fixed window
wait_for_messages average return latency when idledeferredderived from idle_polls rate
Largest expected VCA message fragment countdeferredreceiver.decode SOM/EOM logs
Observed peak await_steps on happy pathdeferredrequester log at Done transition
Tuned AWAIT_STEP_BUDGET (≥ ~3× observed peak)deferredcomputed

Until these exist, the design ships with the default and relies on the Phase-4 state-timeout log line to surface a bad choice loudly rather than silently.