Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
8.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w2-spawn-return | Wave 2: spawn-and-return plumbing + exit/status reporting mechanics | completed |
|
narrow | medium | phase | implementation |
Description
Land the structural constraint in code (ADR-006 decision 4): russh at this pin runs server handler callbacks inline inside the session's packet loop, so a blocking handler suspends all packet routing connection and deadlocks against the peer's immediate follow-up bytes (POC-3 proved by construction with receive-pack). Therefore:
- Every serving path is
tokio::spawned; handlers return fast. The dispatch's routing stubs become spawned serving paths (still declining stubs, since the seams are wave 3), so the pattern is in place before any real mapping hangs on it. - Exit/status reporting mechanics: reporting from spawned tasks
rides the cloned
Session::handle():exit-statussent before channel close, from the spawned task. The derivation contract's rules (which code → which SSH exit shape) are wave-3 session-mapping material (mapping.mdrules 1–6); this task lands the mechanics (the handle clone, the exit-status send, the ordering-before-close), proven end to end. - The proof: a stub serving path that spawns, completes
asynchronously (after the handler has returned), and reports
exit-status(N)+ close through the cloned handle; the loopback harness client observes the exit-status arriving after the handler already returned. A handler-returns-fast assertion (the handler completes while the spawned task is still running) pins the constraint as a test, not a convention.
This is a load-bearing correctness constraint of the whole spine, not an implementation choice; the wave-3 mappings inherit the pattern mechanically from the stubs this task leaves behind.
Acceptance Criteria
- Every serving path (the dispatch stubs) runs in a
tokio::spawn; no handler hook awaits a serving path inline - Exit/status reporting from spawned tasks rides the cloned
Session::handle();exit-statusis sent before channel close - A harness test proves the mechanics end to end: the client
observes
exit-status(N)from a spawned stub that completed after the handler returned - A handler-returns-fast assertion pins the structural constraint as a test
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
References
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md (decision 4, the structural constraint)
- docs/architecture/mapping.md §The structural constraint: spawn-and-return; §exit_status row (sent before close, from the spawned task)
- docs/plans/implementation.md §Wave 2 (the spawn-and-return bullet)
- docs/research/poc-3-mapping-bakeoff.md (the deadlock-by-construction evidence)
Notes
src/serve.rs(new): the spawn-and-return plumbing as mechanics, not convention:spawn_serving_path(serving future)(the one choke point for the ADR-006 decision-4 spawn; nothing it takes is ever awaited inline) andreport_exit_status_then_close(&Handle, channel, N)(the cloned-handle report with themapping.mdexit_statusrow's ordering baked in: exit-status before close). Both are re-exported#[doc(hidden)]from the crate root, theSessionHandler§Test-surface pattern, as the contract-test surface the mechanics rows compose over the real wire. A unit row (spawn_returns_while_the_serving_path_is_still_pending, latch-gated future, spawn call returns first) pins spawn-then-return directly. The derivation contract's rules (which exit code → which SSH exit shape) stay wave-3mapping.mdmaterial; no rule logic landed here.src/handler.rs: the four session-row seam stubs (git_row_stub,pipe_row_stub,pty_row_stub,shell_row_stub) are now spawned serving paths:spawn_seam_stubclonessession.handle(),seam_stub_decline(handle, channel)runs the failure-then-close decline on the spawned task, and the hooks return without awaiting anything. A compile-level fact coded around: the row stubs are plainfns (notasync), because anasync fnholding&Sessionacross the spawn makes the handler future non-Send(russh requires+ Sendon handler futures;Sessionis notSync), so the clone is taken synchronously and only the handle moves into the spawned future. Wire shapes are unchanged (allw2-dispatch-decline-corerows pass unmodified with the spawned decline); the tunnel seam stub and every open-level/capability decline stay inline by class, as they answer the open request before any channel is owned, so nothing spawns yet (documented in the new handler module-doc section §The spawn-and-return structural constraint; wave 3's accept-only-after-fabric-open flow is where the tunnel row gains its spawn).tests/spawn_return.rs(new): the end-to-end mechanics proof over the real wire, using the russh server stack (duplex transport, harness client/harness transport posture) mounted over a minimal proof handler that is not the crate'sSessionHandler(the auth and dispatch hooks have their own suites; the proof subject is the mechanics) and that serves execs by composing the exported plumbing:spawn_serving_pathof a release-gated serving future reportingexit-status(7)+ close through the cloned handle. Two rows, each with aContract stamp::handler_returns_fast_while_the_spawned_serving_path_is_pending, the structural constraint as a wire test: after the exec hook has provably returned (a bounded wait on the handler-side timestamp; the client's exec returns when the request is on the wire, not when the hook ran) and with the serving path still gated, the client's follow-up traffic (a second session channel + second exec) is routed and dispatched normally, with POC-3's follow-up-bytes deadlock shape inverted, and the still-gated no-report state is asserted before the release.spawned_stub_reports_exit_status_before_close_after_the_handler_returnedwith the client observingExitStatus{7}strictly beforeClose(message-by-message, no draining that could hide ordering), and the recorded timestamps prove the exit-status left after the handler had already returned (the literal criterion).
- The proof-handler design note: the gate is
tokio::sync::watch- based (no lost-wakeup race; a late subscriber sees the release, which aNotify-with-notify_waitersshape could miss against multiple spawned paths); the handler-side timestamps rideparking_lot::Mutex. - Feature independence: the mechanics are feature-independent (the
helpers exist in every build and the proof rows run in every build
they were verified under (default,
git,session,tunnel,sftp,--all-features); no feature-gated code was added, so no new decline-path coverage obligations arise.
Summary
Landed the spawn-and-return plumbing + exit/status reporting
mechanics (w2-spawn-return) in three files:
src/serve.rswithcargo test-pinned mechanics: thespawn_serving_pathchoke point (the ADR-006 decision-4 rule, in code, never awaited inline) andreport_exit_status_then_close(exit-status before close through the clonedSession::handle()), both#[doc(hidden)]contract-test re-exports; a unit row pins spawn-returns-while-pending directly.src/handler.rs: the four session-row seam stubs became spawned serving paths (handle clone taken synchronously, given the non-Sendshape that forcingasync fnstubs would produce is coded around and recorded); failure-then-close wire shapes unchanged (all dispatch-core rows pass unmodified); open-level declines and the tunnel seam stub stay inline by class, documented; a new module-doc section carries the constraint and the wave-3 inherit-the-pattern note.tests/spawn_return.rs: the end-to-end mechanics proof, with twoContract stamp:rows over the real wire (handler-returns-fast under follow-up traffic with the spawn still pending; exit-status before close, timestamps proving the report outlived the handler's return), driven by a proof handler composing the exported plumbing.
Verification (all green): cargo test · cargo test --features {git,session,tunnel,sftp} --test dispatch_core ·
cargo test --all-features · cargo clippy --all-targets -- -D warnings · cargo clippy --all-features --all-targets -- -D warnings · cargo fmt --check · cargo doc --no-deps (no
warnings). 93 lib/unit rows + the per-build dispatch matrices + the
two mechanics rows across 7 test binaries.