fix: local-bridge robustness, scope-gate order, sentinel contract; packaging/docs closeout (P8-P11, P2/P3/P15)

Closes review #003 (prepublish review for v0.1.0).

- P8: StdinSink::poll_shutdown parks an inflight reserve+send on a
  full channel (waker registered) — a stdin blast followed by EOF
  delivers the EOF instead of stranding it
- P9: five poisoned-lock .expect() sites -> unwrap_or_else(into_inner)
- P10: three thread-spawn .expect() sites -> TtyError::AllocFailed
- P5: tty:open scope gate runs before carriage/cmd/backend-lookup
  checks (no backend-name enumeration differential for unscoped ids)
- P11: recv_stdout terminates on the zero-length drained sentinel;
  the sentinel is no longer yielded as an item (doc was already the
  contract); stderr has no sentinel (doc noted)
- P2: exclude AGENTS.md + docs/plans/, drop dead Cargo.lock and
  docs/research/ entries (package list: 42 files, 659.2KiB)
- P3: AGENTS.md phase status (all five landed), ADR range 001..009
  (+ alktty-native ADR-009 in the mapping), alkcall guidance
  corrected to v0.4.x / pin "0.4.0"; architecture README ADR-009 row
  + landed-phase status
- P15: backend.rs doc typo; redundant tokio-stream dev-dep removed;
  NegotiationError::Io arm logs; set_identity failure logs;
  input_pump.abort() at session end; TtySessionError::Open carries
  the accept_bi StreamError (no io::Error flattening); borrowing
  deserialize in open_via_channels (no params.clone());
  error_response_bytes guards an "error" key in fields; trivial
  inline comments promoted/removed; plan-doc test counts + doc
  front-matter refreshed; session tests that raced session teardown
  under the abort change use a GatedBackend (exit held until
  released)

Verification: cargo test 104 lib / --all-features 147; clippy
(all-targets + wasm32) -D warnings; fmt; wasm check; doc 0 warnings;
publish dry-run OK.
This commit is contained in:
2026-09-05 16:47:45 +00:00
parent 7b0bac0671
commit 3baa993edb
14 changed files with 372 additions and 105 deletions
+6 -6
View File
@@ -19,7 +19,7 @@ Syntax Tree) document for the wire format, and the ADRs.
## Applicable ADRs
Ported from the alknet mono-repo and renumbered into alktty's ADR
range (001..008). The alknet originals at
range (001..009; ADR-009 is alktty-native). The alknet originals at
`/workspace/@alkdev/alknet/docs/architecture/decisions/` remain the
authoritative source for any ADR not yet ported, and for the alknet
ADRs referenced by alknet number in the docs below (which are not
@@ -35,6 +35,7 @@ tty-specific and therefore not ported into alktty's ADR range).
| [006](decisions/006-negotiation-framing-self-contained.md) | Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) | alknet ADR-057 | Accepted |
| [007](decisions/007-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | alknet ADR-077 | Accepted (**reversed by ADR-008** — kept for historical context) |
| [008](decisions/008-channels-pure-channel-multiplexing.md) | Channels Pure Channel Multiplexing (8-Byte Header, No `stream_type`) | alknet ADR-093 | Accepted (amends alknet ADR-071/074; reverses ADR-007) |
| [009](decisions/009-channels-open-op-is-the-negotiation.md) | The Channels Open Op's `input` Is the Negotiation | alktty-native | Accepted (resolves review #001 L1; amended by review #002 R4 — parse failure is a client-visible error frame) |
## Key Design Principles
@@ -135,11 +136,10 @@ tty-specific and therefore not ported into alktty's ADR range).
## References
- `docs/plans/project-setup.md` — the current plan (phases 05).
Phases 03 are landed; Phase 4 (this directory: architecture docs +
BAST schema + renumbered ADRs) is landed by this commit; Phase 5
(tests, including integration tests in `tests/` at the crate root) is
not yet done.
- `docs/plans/project-setup.md` — the current plan (phases 05). All
five phases are landed: Phase 4 landed this directory (architecture
docs + BAST schema + renumbered ADRs); Phase 5 landed the tests,
including the integration tests in `tests/` at the crate root.
- alknet originals of the ported ADRs (alknet ADR-052, 053, 054, 055,
056, 057, 077, 093) at
`/workspace/@alkdev/alknet/docs/architecture/decisions/` — the
+2 -2
View File
@@ -2,8 +2,8 @@
status: draft (ported from alknet 2026-08-17; alknet-tty → alktty,
alknet-tty-local → alktty's `local` feature module, alknet/tty →
alk/tty, alknet-core → alkcall::core, alknet-call → alkcall, ADRs
renumbered 052..093 → 001..008)
last_updated: 2026-08-17
renumbered 052..093 → 001..009)
last_updated: 2026-09-05
---
# alktty — Overview
+4 -4
View File
@@ -1,8 +1,8 @@
---
status: draft (ported from alknet 2026-08-17; alknet-tty → alktty,
alknet/tty → alk/tty, alknet-core → alkcall::core, alknet-call →
alkcall, ADRs renumbered 052..093 → 001..008)
last_updated: 2026-08-17
status: draft (ported from alknet 2026-08-17; alknet/tty → alk/tty,
alknet-core → alkcall::core, alknet-call → alkcall, ADRs renumbered
052..093 → 001..009)
last_updated: 2026-09-05
---
# alktty — TtyAdapter and Session Lifecycle
+4 -2
View File
@@ -527,8 +527,10 @@ either). `AGENTS.md` was updated to match this mapping.
`MockBackend` from `backend.rs` for the producer side so no
real PTY is needed.
Total: 80 lib tests + 19 integration tests = 99 passing under
`--all-features`; 80 lib tests under default (wasm-clean) build.
Total: 119 lib tests + 19 integration tests = 138 passing under
`--all-features`; 104 lib tests under default (wasm-clean) build
(counts as of the review #003 remediation; the suites have grown since
the 80/19 the plan originally recorded).
## Open Questions
+114 -10
View File
@@ -1,7 +1,7 @@
---
status: open
status: closed
last_updated: 2026-09-05
resolved: 1, 4, 6, 7, 12, 13, 16 (P14 closed)
resolved: 1-16 (P14 closed; P13's accept-loop harness deferred by disposition)
reviewed_artifacts:
- src/lib.rs
- src/wire.rs
@@ -30,6 +30,7 @@ base_commit: 918af40 (review #002 resolved)
resolutions:
- "P1 + P13 resolved 2026-09-05 (87c52e5) — see the Session-1 Resolution section"
- "P12 + P6 + P7 + P4 + P16 resolved 2026-09-05 — see the Session-2 Resolution section"
- "P8 + P9 + P10 + P5 + P11 + P2 + P3 + P15 resolved 2026-09-05 — see the Session-3 Resolution section"
---
# Code Review #003 — Prepublish Review (v0.1.0)
@@ -467,20 +468,20 @@ theirs; don't replicate that nit.
| ID | Finding | Fix | Effort | Risk | Status |
|----|---------|-----|--------|------|--------|
| P1 | pump_session deadlock ≥63 chunks | concurrent drainer + regression test | small | medium (core pump) | ✅ resolved (`87c52e5`) |
| P2 | AGENTS.md ships; dead exclude entries | fix `exclude` | trivial | none | open |
| P3 | stale AGENTS.md + architecture README | refresh phase/ADR/alkcall text | trivial | none | open |
| P2 | AGENTS.md ships; dead exclude entries | fix `exclude` | trivial | none | ✅ resolved (Session 3) |
| P3 | stale AGENTS.md + architecture README | refresh phase/ADR/alkcall text | trivial | none | ✅ resolved (Session 3) |
| P4 | no README + `readme` field | write README (after P5/P6/P12) | medium | none | ✅ resolved (Session 2) |
| P5 | scope gate after backend lookup | reorder checks | trivial | low (test updates) | open |
| P5 | scope gate after backend lookup | reorder checks | trivial | low (test updates) | ✅ resolved (Session 3) |
| P6 | write paths accept invalid chunks | validate on write; unify empty-payload shape | small | low | ✅ resolved (Session 2) |
| P7 | read-after-peek framing corruption | peeked-state guard | small | none | ✅ resolved (Session 2) |
| P8 | poll_shutdown missing waker | inflight-future pattern | small | low | open |
| P9 | poisoned-lock `.expect()` × 5 | `into_inner()` | trivial | none | open |
| P10 | thread-spawn `.expect()` × 3 | map to `TtyError` | trivial | none | open |
| P11 | recv_stdout sentinel contract | terminate stream on sentinel (or fix doc) | small | low (semver-visible) | open |
| P8 | poll_shutdown missing waker | inflight-future pattern | small | low | ✅ resolved (Session 3) |
| P9 | poisoned-lock `.expect()` × 5 | `into_inner()` | trivial | none | ✅ resolved (Session 3) |
| P10 | thread-spawn `.expect()` × 3 | map to `TtyError` | trivial | none | ✅ resolved (Session 3) |
| P11 | recv_stdout sentinel contract | terminate stream on sentinel (or fix doc) | small | low (semver-visible) | ✅ resolved (Session 3) |
| P12 | no crate-root re-exports | `pub use` primary types | trivial | none | ✅ resolved (Session 2) |
| P13 | test gaps | backpressure (>64 chunks) + boundary tests | small | none | ✅ resolved (`87c52e5`; accept-loop harness deferred) |
| P14 | `Result<(), String>` on register_openable | closed — alkcall parity | none | none | ✅ closed |
| P15 | minor polish batch | one batched commit | small | none | open |
| P15 | minor polish batch | one batched commit | small | none | ✅ resolved (Session 3) |
| P16 | no CHANGELOG.md | start changelog with [0.1.0] | small | none | ✅ resolved (Session 2) |
## Suggested Session Breakdown
@@ -603,6 +604,109 @@ clippy (all-targets + wasm32) `-D warnings`, fmt, wasm check, doc
(0 warnings), `cargo publish --dry-run --allow-dirty` (44 files,
702.8KiB) — all clean.
## Session-3 Resolution (P8 + P9 + P10 + P5 + P11 + P2 + P3 + P15, 2026-09-05)
The `local` robustness batch, the producer-hygiene batch, and the
packaging/docs closeout — the review's suggested session breakdown
items 3, 4, and 5. This closes review #003.
- **P8 — `poll_shutdown` waker** (`src/local/pty.rs`): `StdinSink`
gained an `inflight_close` slot mirroring `poll_write`'s inflight
pattern — on a full stdin channel, `poll_shutdown` parks a
`reserve + send(StdinCmd::Eof)` future and polls it on re-poll, so
the poller's waker is registered and a stdin blast followed by EOF
delivers the EOF instead of stranding it. Distinct from the
byte-write slot so the two sends never share a future.
- **P9 — poisoned-lock `.expect()` ×5** (`src/local/pty.rs`): the five
non-test lock sites (`PtyControl::resize`, both `PtyControl::signal`
arms, the reader-thread and writer-thread master-lock acquisitions)
use `unwrap_or_else(|e| e.into_inner())` per convention 2.
- **P10 — thread-spawn `.expect()` ×3** (`src/local/pty.rs`): the
reader/writer/waiter `thread::Builder::spawn` results map to
`TtyError::AllocFailed` (the error path existed — `allocate_pty`
returns `Result`); fd/thread exhaustion surfaces as an allocation
error, not a panic.
- **P5 — scope gate first** (`src/adapter.rs`):
`validate_and_allocate` now runs the `tty:open` scope check before
the carriage/cmd checks and the backend lookup. An
authenticated-but-unscoped identity gets `forbidden` regardless of
the request body — the `unknown_backend`-vs-`forbidden` name-
enumeration differential is gone. The existing
`scope_gate_forbidden_without_tty_open` test asserted the scoped
behavior and still passes (it uses a *registered* backend name, so
the reorder doesn't change its outcome); no test asserted
`unknown_backend` with an unscoped identity.
- **P11 — sentinel contract** (`src/session.rs`): `recv_stdout` now
terminates the stream ON the zero-length stdout sentinel ("drained")
— the sentinel is not yielded as an item. This matches the
documented contract and the sentinel's meaning; consumers no longer
filter. `recv_stderr`'s doc now states stderr has no sentinel (the
adapter's stderr pump emits none; that stream ends when the read
pump terminates). The three session tests that filtered the
sentinel manually now assert the stream ends on it (no filter).
The README example's manual `is_empty` break was removed.
- **P2 — packaging** (`Cargo.toml`): `exclude` is now
`[".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/",
"docs/sdd_process.md"]` — AGENTS.md no longer ships (alkcall
parity), `docs/plans/` (absolute `/workspace/...` paths) no longer
ships, and both dead entries (`Cargo.lock` — force-included anyway;
`docs/research/` — nonexistent) are dropped. Package list verified:
42 files, 659.2KiB (was 44 files, 702.8KiB).
- **P3 — stale docs**: AGENTS.md now says all five phases are landed,
the ADR range is 001..009 with the alktty-native ADR-009 noted in
the mapping list, and the alkcall guidance is corrected to "v0.4.x —
Pin `alkcall = "0.4.0"` (lockfile resolves 0.4.1)" (was "v0.1.x —
pin 0.1.1", which would have downgraded the dependency).
`docs/architecture/README.md` gained the ADR-009 table row, the
001..009 preamble, and the landed-phase status.
- **P15 — polish batch** (one commit, as planned):
- `src/backend.rs` doc typo fixed ("`local` feature module) is the
reference implementation").
- `tokio-stream` dev-dependency removed (inherited from the main
dep).
- `src/adapter.rs` — the `NegotiationError::Io(_)` arm now logs
(`debug!`) for parity with the catch-all arm; `set_identity`
failure is logged (`AlreadySet` is benign-by-construction, noted
in a comment); `drop(input_pump)``input_pump.abort()` at
session end (no lingering client→backend task holding the backend
stdin half after the exit chunk).
- `src/session.rs``connect_direct`/`from_bidi_stream_via` map the
`accept_bi` `StreamError` into a new `TtySessionError::Open`
variant (`#[from]`) instead of flattening it into an anonymous
`io::Error(ConnectionReset)` (additive pre-consumers, same logic
as review #002 R5); the fail-fast params parse uses
`NegotiateRequest::deserialize(&params)` (borrowing) instead of
`from_value(params.clone())` — no clone.
- `src/negotiation.rs``error_response_bytes` skips an `"error"`
key in `fields` so it cannot overwrite the error code.
- Two trivial inline comments promoted or removed (the split-idiom
comment in `adapter.rs` removed; the "Already taken" comment in
`session.rs` now points at the documented behavior).
- `docs/plans/project-setup.md` test counts refreshed (119 lib + 19
integration = 138 under `--all-features` at the review baseline,
with a note that the suites have grown); `overview.md` and
`tty-adapter.md` front-matter `last_updated` dates and the ADR
range updated for the ADR-009 edits.
- The `input_pump.abort()` change surfaced a latent test race: the
`send_stdin_round_trips_to_backend` and
`resize_and_signal_dont_error` tests used `MockBackend` (exit
resolves on allocate) and were writing while the session data
plane was already closing — previously masked by the lingering
input pump the abort now reclaims. Both use a new `GatedBackend`
(exit held until the test releases it), making the writes
deterministic and asserting the exit code.
Verification on the resolution tree: `cargo test` 104 lib (unchanged) /
`cargo test --all-features` 128 lib + 19 integration = 147
(unchanged; the two reworked tests kept their names); clippy
(all-targets + wasm32) `-D warnings`, fmt, wasm check, doc
(0 warnings), `cargo publish --dry-run --allow-dirty` (42 files,
659.2KiB) — all clean.
**Review #003 is closed.** Remaining before publish: tag `v0.1.0` (the
CHANGELOG link definition points at it) and `cargo publish` (dry-run
clean).
## Notes
- Line numbers refer to the tree at `918af40` (review base).