Commit Graph
8 Commits
Author SHA1 Message Date
glm-5.3-flash af9dbaedea feat: allocation moves into the channels establisher (alkcall 0.6.0 / review 007 R-01) — bump to 0.3.0
- Bump alkcall 0.5.0 -> 0.6.0 (Establishment plan payload, ADR-049
  amendment 2); alktty 0.2.0 -> 0.3.0 (breaking channels-path
  allocation-failure shape)
- make_tty_establisher runs backend.allocate; failure maps to
  EstablishmentError::DialFailed (channel:open_failed reason
  dial_failed); the TtyHandle crosses to the pump handler via a
  private per-open AllocatedHandle one-shot slot (the handle is not
  Sync — it cannot be the ChannelPlan directly)
- make_tty_open_handler gains the Option<ChannelPlan> parameter; a
  plan drives the new drive_session_pre_allocated (pumps only), a
  None plan falls back to drive_session_pre_negotiated (inline
  validate-and-allocate — defense-in-depth for no-establisher
  registrations)
- tty_open_spec's channel:open_failed ErrorDefinition declares
  dial_failed (four reachable reasons)
- Pinned in-band allocate test flipped:
  allocate_failure_fails_open_as_dial_failed (end-to-end, no channel
  survives); establisher unit gate for the plan slot + failure
  mapping
- ADR-010 amended (§2A; §2 kept as historical record), tty-adapter.md,
  session/channels/adapter docs, CHANGELOG, AGENTS.md alkcall pin
  updated

Verification: cargo test (113) + --all-features (137), clippy
-D warnings (native + wasm32, default + all-features), fmt, doc,
wasm32 check — all clean
2026-09-07 09:53:57 +00:00
glm-5.3-flash 3baa993edb 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.
2026-09-05 16:47:45 +00:00
glm-5.3-flash 87c52e5e4d fix: pump_session deadlock when backend emits >64 chunks before exit (P1)
The stdout drainer started only after the pumps+exit join completed,
but the pumps share a bounded (64-slot) channel with it. Once the
channel filled, pump_stdout parked on send, the join never completed,
and the session hung with the client actively reading (review #003 P1;
reproduced empirically at N=62 ok / N=63 hang). Any real session (cat,
ls -R) exceeds 64 chunks instantly.

pump_session now spawns drain_chunks (owning writer_rx + client_write)
before the join; the exit chunk is still enqueued after the join and
writer_tx is dropped after it, so the single FIFO preserves the
exit-chunk-is-last invariant (ADR-005).

Tests (review #003 P13):
- backpressure regression: 80 chunks through a real drive_session,
  in-order delivery + sentinel + exit asserted, 10s per-read timeout
  so a regression fails instead of hanging (validated against the bug:
  fails with the old order reinstated)
- wire: MAX_CHUNK_LEN exact-boundary round trip (limit is inclusive)
- session: stream_type 0/3 from the server ignored mid-stream without
  desynchronizing framing or losing stdout/exit

Verification: cargo test 98 lib (default) / 141 --all-features,
clippy all-targets + wasm32 -D warnings, fmt --check, wasm check, doc
--no-deps all clean.
2026-09-05 10:05:38 +00:00
glm-5.3-flash 8ee9216a07 fix: channels parse-failure path writes the negotiation error frame (R4)
Review #002 R4 — a NegotiateRequest parse failure of the open op's
schema-validated input died silently (log + return, channel teardown,
consumer observed NoExitChunk — indistinguishable from a crashed
producer), while the other post-open failure classes (unknown backend,
allocate_failed, ownership denial) wrote the 0x00-prefixed error frame.

- make_tty_open_handler now accepts the channel's BiStream and writes
  a malformed_negotiation frame via the shared
  crate::adapter::send_negotiation_error (now pub(crate)) before
  returning; the consumer's M1 peek surfaces NegotiationRejected
  unchanged
- the frame type and layout are unchanged (ADR-001 wire-stable
  contract); no new frame type, no wire change
- tests: make_tty_open_handler seam test with a hand-built
  schema-bypassing input (cwd: 42) + a real-registry end-to-end test
  via ChannelClient::open_channel (bypasses open_via_channels's local
  fail-fast parse — R5's path — so it exercises the producer handler)
- docs: ADR-009 amended (Parse-failure error frame section);
  tty-adapter.md malformed_negotiation row covers both paths;
  session.rs post-open failure lists updated; review #002 R4 resolved

Note: the review's "unreachable end-to-end" premise was refined —
open_via_channels parses params locally (fail-fast) so a TtySession
consumer never hits the producer-side parse failure, but direct
ChannelClient callers do; the schema is deliberately partial so a
schema-valid value (cwd typed as a number) reaches the handler.

Verification: cargo test 95 lib (default) / 138 (--all-features);
clippy -D warnings native + wasm clean; fmt clean; doc 0 warnings.
2026-09-05 08:37:02 +00:00
glm-5.3-flash 96692d3b6a feat: channels path carries the negotiation in the open op (L1, L3)
The channels path no longer carries a second negotiation frame on the
channel's data stream (ADR-009). The open op's registry-validated
input IS the negotiation:

- producer: make_tty_open_handler parses the open op's input into a
  NegotiateRequest and drives the new drive_session_pre_negotiated
  (same three-pump driver as drive_session, minus the wire-frame
  negotiation phase; validate/allocate factored into
  validate_and_allocate, shared by both paths). Post-open failures
  (unknown backend, allocate_failed, ownership denial) still go to the
  client as a 0x00-prefixed negotiation error frame, so the consumer's
  M1 disambiguation read applies unchanged. The tty:open scope gate is
  enforced by the registry's AccessControl (not re-checked in the
  handler).
- consumer: open_via_channels parses params locally (fail-fast before
  a channel is allocated), opens the channel, and starts raw-chunk
  mode directly (from_halves_raw — no negotiation write; the
  0x00-error-frame peek retained).
- tty_open_spec's input schema is now the partial NegotiateRequest
  shape (carriage/backend/cmd required; backend params stay free-form
  — raw JSON Schema is permissive on unknown keys).

Prerequisites landed upstream: alkcall 0.4.0 enforces
OperationSpec.input_schema at dispatch (the registry check this design
leans on never existed before); alkcall 0.4.1 parks early-arrival
chunks for un-adopted channels instead of dropping them — the open
response / producer's-first-write race was silently losing the first
chunks (found by L3's test; the session never resolved).

L3: open_via_channels + from_bidi_stream_via now covered end-to-end
(5 session tests + pre-negotiated adapter test + shared-harness tests
in the new crate::testing module; the channels harness moved there so
session tests share it).

Verification: cargo test 93 lib (default), 116 lib + 19 integration
(--all-features); clippy -D warnings native + wasm clean; fmt clean;
doc 0 warnings; wasm check clean.
2026-09-05 07:08:17 +00:00
deepseek-v4-pro 99441530ab fix: address code review #001 findings (M1, M2, L2, L4, L5, N1-N3, N5)
- M1: TtySession now handles the negotiation-rejection error frame.
  from_halves peeks the first response byte (ADR-052 §5 disambiguation)
  and returns NegotiationRejected on a 0x00-prefixed error frame;
  ChunkReader gains peek_stream_type/read_chunk_after_peek.
- M2: wait() now surfaces MalformedExitChunk instead of collapsing it
  to NoExitChunk. The exit watch channel carries a cloneable
  ExitOutcome enum; MalformedExitChunk carries a String.
- L2: add EmittingBackend + recv_stdout_and_stderr_route_backend_data
  test covering the consumer read-pump stdout/stderr routing.
- L4: MockBackend/MockControl/MockStdinSink are now #[cfg(test)]
  pub(crate), removing them from the public API.
- L5: cargo fmt (the BAST drift test was unformatted).
- N1: fix all 9 rustdoc intra-doc links.
- N2: fix stale doc paths (crates/tty/ and docs/research/).
- N3: amend AGENTS.md §14 to accurately describe the local module's
  libc::kill unsafe blocks.
- N5: consolidate nanos_seed into tests/common/mod.rs.

Verification: cargo test (84), cargo test --all-features (107),
clippy clean (native + wasm), fmt clean, doc clean, wasm check clean.
Coverage: session.rs 79.71% -> 87.43%, total 90.74% -> 91.47%.
2026-08-17 12:07:44 +00:00
glm-5.2 e1610c2825 phase 3: port alknet-tty-local behind the local feature
Folds the alknet-tty-local crate into alktty as a feature-gated local
submodule (the single-crate + local-feature design from ADR-054; the
cyclic-dep workaround the mono-repo needed doesn't apply to one crate).

src/local/mod.rs:
- Module root, re-exports LocalTtyBackend, declares backend/pipe/pty
- Doc-commented as feature-gated + non-wasm by design (portable-pty +
  tokio::process need a real OS; enabling local on a wasm target is a
  build error by design)

src/local/backend.rs:
- LocalTtyBackend: implements crate::backend::TtyBackend
- allocate() dispatches on params.terminal: Some -> pty::allocate_pty,
  None -> pipe::allocate_pipe (ADR-054)
- 7 tests: PTY/pipe dispatch, empty-cmd rejection (both modes),
  resource_id returns None, new() constructs

src/local/pty.rs:
- portable_pty + 3 std threads (reader/writer/waiter) feeding tokio
  mpsc/oneshot (REQ-TTY-01 blocking->async bridge)
- PtyControl: resize + REQ-TTY-02 process-group signal forwarding
  (libc::kill(-pgid, sig) + kill(pid, sig) fallback + ChildKiller::kill
  for unknown names)
- LocalExitFuture: ADR-056 kill-on-Drop guard (ChildKiller on cancel,
  Option::take disarms on resolve)
- StdinSink: AsyncWrite over mpc::Sender<StdinCmd> with in-flight
  reserve+send parking for full-channel backpressure
- 7 tests incl. process-group reach (bash -c sleep), cancel-cleanup,
  unknown-signal fallback

src/local/pipe.rs:
- tokio::process::Command + tokio_util::io::ReaderStream
- PipeControl: no-op resize, libc::kill(pid, sig) with SIGKILL
  fallback for unknown names (pid-only, no process group - documented
  limitation of the runner case)
- PipeExitFuture: in-place Child::wait() poll (avoids self-referential
  borrow) + ADR-056 kill-on-Drop guard (start_kill on cancel)
- BytesStream: wraps ReaderStream, strips io::Error to EOF
- 9 tests incl. separate stderr, SIGTERM=-15, SIGKILL=-9 fallback,
  cancel-cleanup pid probe (kill(pid,0) returns ESRCH)

Import migration: alknet_tty::backend::{...} -> crate::backend::{...},
alknet_tty::control::signal_from_name -> crate::control::signal_from_name.

Cargo.toml: no changes needed (portable-pty + tokio-util optional, libc
under cfg(unix), and the local feature wiring tokio/process +
tokio/rt-multi-thread were already in the scaffold per Phase 0).

Also fixes pre-existing rustfmt drift in adapter.rs/channels.rs/
session.rs/lib.rs left by the phase 2 commit (cargo fmt --check without
--features local reported 28 diffs; cargo fmt does not accept
--features, so the earlier 'clean' check was a false negative — the
check errored on the unknown flag and grepped an empty stdout). Lesson:
run cargo fmt --check with no feature flags; cargo fmt doesn't gate on
features.

Verification:
- cargo test --features local -> 103/103 pass (was 80 at phase 2 end;
  +23 new tests across the 3 local modules: 7 backend, 7 pty, 9 pipe)
- cargo test (no features) -> 80/80 pass (local module not compiled)
- cargo clippy --features local --all-targets -> clean
- cargo clippy --all-targets (no features) -> clean
- cargo check --target wasm32-unknown-unknown -> clean (default crate
  stays wasm-clean; local is feature-gated and non-wasm by design)
- cargo fmt --check -> clean
2026-08-17 10:19:22 +00:00
glm-5.2 29dfa1a6af phase 1: port core types from alknet-tty
Port wire.rs, control.rs, negotiation.rs, backend.rs, adapter.rs from
alknet-tty (the protocol half of the alknet mono-repo split) into the
single alktty crate. All 65 unit tests pass; cargo check on
wasm32-unknown-unknown is clean (no features); clippy is clean.

Migration changes:
- adapter.rs imports: alknet_core::{auth, ownership, types} →
  alkcall::core::{auth, ownership, types + Connection/HandlerError/
  ProtocolHandler/StreamError re-exported at the crate root}
- adapter.rs TtyAdapter::alpn(): b"alknet/tty" → b"alk/tty"
- backend.rs BoxFuture type alias: Pin<Box<dyn Future + Send +
  'static>> → futures::future::BoxFuture<'static, T> (matches alkcall
  convention; alktty declares futures = 0.3 directly)
- doc comments: alknet/tty → alk/tty, alknet-tty → alktty, the
  alknet-tty-poc and findings.md cross-references trimmed to local
  docs

Test shape: alkcall's Identity.resources is HashMap<String, Vec<String>>
(was HashMap<String, String> in alknet-core); the tests construct with
StdHashMap::new() and infer the new shape from the struct, so no test
edits were needed. alkcall's OwnershipStore::record is 3-arg (no
action) and OwnershipProvider::owns is 4-arg (with action) — both
already match what the ported code calls.

Cargo.lock committed (matches alkcall/alktype convention; still
excluded from the published package via Cargo.toml's exclude list).
2026-08-17 09:36:24 +00:00