Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
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.
2026-11-04 20:28:00 +00:00

8.6 KiB
Raw Permalink Blame History

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
w2-dispatch-decline-core
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-status sent 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.md rules 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-status is 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 --check pass

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) and report_exit_status_then_close(&Handle, channel, N) (the cloned-handle report with the mapping.md exit_status row's ordering baked in: exit-status before close). Both are re-exported #[doc(hidden)] from the crate root, the SessionHandler §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-3 mapping.md material; 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_stub clones session.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 plain fns (not async), because an async fn holding &Session across the spawn makes the handler future non-Send (russh requires + Send on handler futures; Session is not Sync), so the clone is taken synchronously and only the handle moves into the spawned future. Wire shapes are unchanged (all w2-dispatch-decline-core rows 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's SessionHandler (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_path of a release-gated serving future reporting exit-status(7) + close through the cloned handle. Two rows, each with a Contract 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_returned with the client observing ExitStatus{7} strictly before Close (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 a Notify-with-notify_waiters shape could miss against multiple spawned paths); the handler-side timestamps ride parking_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.rs with cargo test-pinned mechanics: the spawn_serving_path choke point (the ADR-006 decision-4 rule, in code, never awaited inline) and report_exit_status_then_close (exit-status before close through the cloned Session::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-Send shape that forcing async fn stubs 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 two Contract 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.