You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/22 05:36:27 UTC

(maka) branch feat/runtime-host-rust updated: refactor(process): replace host terminal V8 bridge with Alacritty

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
     new e07af6ac00 refactor(process): replace host terminal V8 bridge with Alacritty
e07af6ac00 is described below

commit e07af6ac004d2a7139d0771c4cc80173ab2f8511
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Sep 22 13:36:08 2026 +0800

    refactor(process): replace host terminal V8 bridge with Alacritty
    
    Keep terminal state in native PTY workers, bound parser output and preserve Unicode across parser cuts. Remove the headless JavaScript bundle and retain cross-platform PTY regression coverage.
    
    Generated-by: Codex
---
 Cargo.lock                                         | 131 +++++++-
 crates/js-runtime/Cargo.toml                       |   3 -
 crates/js-runtime/TERMINAL_SOURCE.md               |  55 ----
 crates/js-runtime/build.rs                         |  20 +-
 crates/js-runtime/src/lib.rs                       |   1 -
 crates/js-runtime/src/plugin.rs                    |   2 +-
 crates/js-runtime/src/terminal.rs                  | 135 --------
 crates/js-runtime/src/trusted.rs                   |  72 +----
 crates/js-runtime/src/trusted/engine.rs            |  16 -
 crates/js-runtime/src/trusted/worker.rs            | 114 +------
 crates/js-runtime/terminal.js                      | 197 ------------
 crates/js-runtime/tests/integration/main.rs        |   1 -
 .../tests/integration/terminal_screen.rs           | 241 ---------------
 crates/js-runtime/trusted/dispatch.js              |  20 --
 crates/model/tests/integration/provider_ingress.rs |  10 +-
 .../model/tests/integration/trusted_concurrency.rs |  16 +-
 crates/process/Cargo.toml                          |   3 +
 crates/process/README.md                           |  37 +++
 crates/process/README.zh-CN.md                     |  33 ++
 crates/process/src/lib.rs                          |   1 +
 crates/process/src/terminal.rs                     | 280 +++++++++++++++++
 crates/process/src/terminal/handler.rs             | 344 +++++++++++++++++++++
 crates/process/tests/conpty.rs                     |   9 +-
 crates/process/tests/terminal.rs                   | 217 +++++++++++++
 crates/runtime-host/src/execution.rs               |   3 +-
 crates/runtime-host/src/shell.rs                   |  16 +-
 crates/runtime-host/src/shell/terminal.rs          |  63 ++--
 crates/runtime-host/src/shell/worker.rs            |   9 +-
 crates/runtime-host/src/shell/worker/control.rs    |   2 +-
 docs/rust-runtime.md                               |   3 +-
 docs/rust-runtime.zh-CN.md                         |   3 +-
 scripts/asf-license-headers.mjs                    |   1 -
 scripts/rust/bundle-terminal.mjs                   |  54 ----
 tests/fixtures/terminal-screen-oracle.mjs          |  67 ----
 34 files changed, 1112 insertions(+), 1067 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b9941f5613..804ece24bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -75,6 +75,30 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alacritty_terminal"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59"
+dependencies = [
+ "base64 0.22.1",
+ "bitflags 2.13.2",
+ "home",
+ "libc",
+ "log",
+ "miow",
+ "parking_lot",
+ "piper",
+ "polling",
+ "regex-automata",
+ "rustix 1.1.4",
+ "rustix-openpty",
+ "signal-hook 0.4.4",
+ "unicode-width",
+ "vte",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "alloc-no-stdlib"
 version = "2.0.4"
@@ -148,6 +172,12 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
 
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
 [[package]]
 name = "asn1-rs"
 version = "0.5.2"
@@ -764,6 +794,15 @@ version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414"
 
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "const-hex"
 version = "1.19.1"
@@ -1047,6 +1086,12 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "cursor-icon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+
 [[package]]
 name = "curve25519-dalek"
 version = "4.1.3"
@@ -1482,7 +1527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6254849daafc1887fdd2272dacc7021f3465b4a667803de1f28633d804410f6"
 dependencies = [
  "libc",
- "signal-hook",
+ "signal-hook 0.3.18",
  "thiserror 2.0.20",
  "tokio",
  "windows-sys 0.59.0",
@@ -3452,6 +3497,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -4448,7 +4499,6 @@ dependencies = [
  "deno_webidl",
  "futures-util",
  "maka-network",
- "maka-process",
  "maka-runtime",
  "reqwest",
  "serde",
@@ -4545,6 +4595,7 @@ dependencies = [
 name = "maka-process"
 version = "0.2.0"
 dependencies = [
+ "alacritty_terminal",
  "dunce",
  "libc",
  "maka-network",
@@ -4555,8 +4606,10 @@ dependencies = [
  "serde",
  "serde_json",
  "tempfile",
+ "thiserror 2.0.20",
  "tokio",
  "tokio-util",
+ "unicode-width",
  "uuid",
  "windows-sys 0.61.2",
 ]
@@ -4894,6 +4947,15 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "miow"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "moka"
 version = "0.12.16"
@@ -5400,6 +5462,17 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
 [[package]]
 name = "pkcs1"
 version = "0.7.5"
@@ -5437,6 +5510,20 @@ version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
 
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "polyval"
 version = "0.6.2"
@@ -6046,6 +6133,17 @@ dependencies = [
  "rustix 1.1.4",
 ]
 
+[[package]]
+name = "rustix-openpty"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393"
+dependencies = [
+ "errno",
+ "libc",
+ "rustix 1.1.4",
+]
+
 [[package]]
 name = "rustls"
 version = "0.23.40"
@@ -6546,6 +6644,16 @@ dependencies = [
  "signal-hook-registry",
 ]
 
+[[package]]
+name = "signal-hook"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.8"
@@ -7554,6 +7662,12 @@ version = "1.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
 
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
 [[package]]
 name = "unicode-xid"
 version = "0.2.6"
@@ -7696,6 +7810,19 @@ dependencies = [
  "nix 0.31.3",
 ]
 
+[[package]]
+name = "vte"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.13.2",
+ "cursor-icon",
+ "log",
+ "memchr",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.5.0"
diff --git a/crates/js-runtime/Cargo.toml b/crates/js-runtime/Cargo.toml
index 14e75bb812..177b90bde5 100644
--- a/crates/js-runtime/Cargo.toml
+++ b/crates/js-runtime/Cargo.toml
@@ -44,6 +44,3 @@ thiserror.workspace = true
 tokio.workspace = true
 tokio-util.workspace = true
 tokio-tungstenite.workspace = true
-
-[target.'cfg(unix)'.dev-dependencies]
-maka-process.workspace = true
diff --git a/crates/js-runtime/TERMINAL_SOURCE.md b/crates/js-runtime/TERMINAL_SOURCE.md
deleted file mode 100644
index b22f5ba64f..0000000000
--- a/crates/js-runtime/TERMINAL_SOURCE.md
+++ /dev/null
@@ -1,55 +0,0 @@
-<!--
-  Licensed to the Apache Software Foundation (ASF) under one
-  or more contributor license agreements.  See the NOTICE file
-  distributed with this work for additional information
-  regarding copyright ownership.  The ASF licenses this file
-  to you under the Apache License, Version 2.0 (the
-  "License"); you may not use this file except in compliance
-  with the License.  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing,
-  software distributed under the License is distributed on an
-  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-  KIND, either express or implied.  See the License for the
-  specific language governing permissions and limitations
-  under the License.
--->
-
-# Embedded terminal parser
-
-The build embeds the repository-locked `@xterm/headless` 6.0.0 and
-`@xterm/addon-unicode11` 0.9.0 in the existing V8 runtime. Node and esbuild are
-build-time tools only. The client and server use the same terminal parser and
-Unicode tables; Rust owns native processes, resource lifecycle and persistence.
-
-Upstream: https://github.com/xtermjs/xterm.js/tree/6.0.0
-
-Both packages are MIT licensed. The following notices are retained in the
-embedded JavaScript bundle as well as this source distribution.
-
-Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
-Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
-Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
-
-Unicode11 addon:
-Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/crates/js-runtime/build.rs b/crates/js-runtime/build.rs
index 1066d55bb1..c31d0b1322 100644
--- a/crates/js-runtime/build.rs
+++ b/crates/js-runtime/build.rs
@@ -18,15 +18,7 @@
  */
 
 fn main() {
-    bundle_providers();
-    for source in [
-        "terminal.js",
-        "TERMINAL_SOURCE.md",
-        "../../scripts/rust/bundle-terminal.mjs",
-        "../../package-lock.json",
-    ] {
-        println!("cargo:rerun-if-changed={source}");
-    }
+    println!("cargo:rerun-if-changed=../../package-lock.json");
     println!("cargo:rerun-if-env-changed=MAKA_JS_DEPS");
     let dependencies = std::env::var_os("MAKA_JS_DEPS")
         .map(std::path::PathBuf::from)
@@ -37,15 +29,7 @@ fn main() {
             dependencies.join(source).display()
         );
     }
-    let status = std::process::Command::new("node")
-        .arg("../../scripts/rust/bundle-terminal.mjs")
-        .arg(std::env::var_os("OUT_DIR").expect("Cargo OUT_DIR"))
-        .status()
-        .expect("Node is required to bundle the terminal engine at build time");
-    assert!(
-        status.success(),
-        "terminal bundle failed; install repository npm dependencies first"
-    );
+    bundle_providers();
 }
 
 fn bundle_providers() {
diff --git a/crates/js-runtime/src/lib.rs b/crates/js-runtime/src/lib.rs
index ea5e27bfc2..c8ead73bd8 100644
--- a/crates/js-runtime/src/lib.rs
+++ b/crates/js-runtime/src/lib.rs
@@ -22,7 +22,6 @@ mod evaluate;
 mod execution_budget;
 pub mod plugin;
 mod result;
-pub mod terminal;
 pub mod trusted;
 
 use bridge::{Admission, ToolScope, maka_code};
diff --git a/crates/js-runtime/src/plugin.rs b/crates/js-runtime/src/plugin.rs
index 118002214f..636f19de39 100644
--- a/crates/js-runtime/src/plugin.rs
+++ b/crates/js-runtime/src/plugin.rs
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-//! Trusted plugin VMs, separate from the model/terminal runtime.
+//! Trusted plugin VMs, separate from the model runtime.
 //! A VM multiplexes modules and promises; module handles own code retention.
 mod bridge;
 mod engine;
diff --git a/crates/js-runtime/src/terminal.rs b/crates/js-runtime/src/terminal.rs
deleted file mode 100644
index 95a865235f..0000000000
--- a/crates/js-runtime/src/terminal.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::trusted::TrustedRuntime;
-use maka_runtime::terminal::{TerminalScreen, TerminalSize};
-use serde::Deserialize;
-use serde::de::DeserializeOwned;
-use serde_json::{Value, json};
-use tokio::sync::oneshot;
-
-const MAX_WRITE_BYTES: usize = 64 * 1024;
-
-#[derive(Debug, thiserror::Error)]
-#[error("terminal screen failed: {0}")]
-pub struct ScreenError(String);
-
-/// Replies and screen belong to the same completed parser cut.
-#[derive(Debug, Deserialize)]
-pub struct ScreenCut {
-    pub replies: String,
-    pub screen: TerminalScreen,
-}
-
-/// One terminal's serial parser state, hosted by a shared trusted JS worker.
-/// A dropped/failed cut poisons only this handle, never another terminal/model.
-pub struct Screen {
-    runtime: TrustedRuntime,
-    id: Option<u32>,
-    available: bool,
-}
-
-pub(super) enum Operation {
-    Write(String),
-    Resize(TerminalSize),
-    ResetAfterGap,
-    Snapshot,
-}
-
-impl Operation {
-    pub fn arguments(self) -> (&'static str, Value) {
-        match self {
-            Self::Write(data) => ("write", json!(data)),
-            Self::Resize(size) => ("resize", json!(size)),
-            Self::ResetAfterGap => ("resetAfterGap", Value::Null),
-            Self::Snapshot => ("snapshot", Value::Null),
-        }
-    }
-}
-
-impl Screen {
-    pub fn new(size: TerminalSize) -> Result<Self, ScreenError> {
-        Self::with_runtime(TrustedRuntime::default(), size)
-    }
-
-    pub fn with_runtime(runtime: TrustedRuntime, size: TerminalSize) -> Result<Self, ScreenError> {
-        let id = runtime.create_terminal(json!(size)).map_err(failure)?;
-        Ok(Self {
-            runtime,
-            id: Some(id),
-            available: true,
-        })
-    }
-
-    pub async fn write(&mut self, data: &str) -> Result<ScreenCut, ScreenError> {
-        if data.len() > MAX_WRITE_BYTES {
-            return Err(ScreenError("write exceeds 64 KiB".into()));
-        }
-        self.call(Operation::Write(data.into())).await
-    }
-
-    pub async fn resize(&mut self, size: TerminalSize) -> Result<TerminalScreen, ScreenError> {
-        self.call(Operation::Resize(size)).await
-    }
-
-    pub async fn reset_after_gap(&mut self) -> Result<(), ScreenError> {
-        self.call(Operation::ResetAfterGap).await
-    }
-
-    pub async fn snapshot(&mut self) -> Result<TerminalScreen, ScreenError> {
-        self.call(Operation::Snapshot).await
-    }
-
-    pub async fn close(mut self) {
-        if let Some(id) = self.id.take() {
-            let (reply, done) = oneshot::channel();
-            self.runtime.dispose(id, Some(reply));
-            let _ = done.await;
-        }
-    }
-
-    async fn call<T: DeserializeOwned>(&mut self, operation: Operation) -> Result<T, ScreenError> {
-        if !self.available {
-            return Err(ScreenError(
-                "parser is closed after an incomplete or failed operation".into(),
-            ));
-        }
-        self.available = false;
-        let json = self
-            .runtime
-            .terminal(self.id.expect("open screen"), operation)
-            .await
-            .map_err(failure)?;
-        let result = serde_json::from_str(&json).map_err(failure)?;
-        self.available = true;
-        Ok(result)
-    }
-}
-
-impl Drop for Screen {
-    fn drop(&mut self) {
-        if let Some(id) = self.id.take() {
-            self.runtime.dispose(id, None);
-        }
-    }
-}
-
-fn failure(error: impl std::fmt::Display) -> ScreenError {
-    ScreenError(error.to_string())
-}
diff --git a/crates/js-runtime/src/trusted.rs b/crates/js-runtime/src/trusted.rs
index 31393e60f0..725127a906 100644
--- a/crates/js-runtime/src/trusted.rs
+++ b/crates/js-runtime/src/trusted.rs
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-//! One lazily started, trusted isolate per Host. Network waits and terminal
-//! cuts are multiplexed; generated Code Mode JavaScript never enters it.
+//! One lazily started, trusted model isolate per Host. Network waits are
+//! multiplexed; generated Code Mode JavaScript never enters it.
 mod budget;
 mod engine;
 mod http;
@@ -50,7 +50,7 @@ pub enum TrustedError {
 }
 
 pub(super) type Result<T> = std::result::Result<T, TrustedError>;
-pub(super) type Reply = oneshot::Sender<Result<String>>;
+pub(super) type Reply = oneshot::Sender<Result<()>>;
 
 /// Queued SDK output retains its share of the runtime-wide byte budget until
 /// the consumer takes it. Queue count alone is not a useful memory boundary
@@ -78,8 +78,7 @@ struct Inner {
 }
 
 struct Service {
-    // The queue is structurally bounded by the 128 live object permits:
-    // one start/cut plus one cancel/dispose per object, never arbitrary messages.
+    // The queue is bounded by 128 live requests: one start and one cancel each.
     commands: mpsc::UnboundedSender<Command>,
     health: Arc<Health>,
 }
@@ -99,20 +98,6 @@ pub(super) enum Command {
         reply: Reply,
     },
     Cancel(u32),
-    Create {
-        id: u32,
-        size: Value,
-        permit: OwnedSemaphorePermit,
-    },
-    Terminal {
-        id: u32,
-        operation: super::terminal::Operation,
-        reply: Reply,
-    },
-    Dispose {
-        id: u32,
-        reply: Option<Reply>,
-    },
 }
 
 impl Default for TrustedRuntime {
@@ -235,7 +220,7 @@ impl TrustedRuntime {
                 biased;
                 result = &mut done => {
                     cancel_on_drop.disarm();
-                    let result = result.map_err(|_| service.health.error())?.map(|_| ());
+                    let result = result.map_err(|_| service.health.error())?;
                     return if cancellation.is_cancelled() { Err(TrustedError::Cancelled) } else { result };
                 },
                 _ = cancellation.cancelled() => break TrustedError::Cancelled,
@@ -257,53 +242,6 @@ impl TrustedRuntime {
         }
         Err(cause)
     }
-
-    pub(super) fn create_terminal(&self, size: Value) -> Result<u32> {
-        let permit = self
-            .0
-            .slots
-            .clone()
-            .try_acquire_owned()
-            .map_err(|_| failed("trusted runtime object capacity exhausted"))?;
-        let service = self.service()?;
-        let id = self.next_id()?;
-        service
-            .commands
-            .send(Command::Create { id, size, permit })
-            .map_err(|_| service.health.error())?;
-        Ok(id)
-    }
-
-    pub(super) async fn terminal(
-        &self,
-        id: u32,
-        operation: super::terminal::Operation,
-    ) -> Result<String> {
-        let service = self.service()?;
-        let (reply, mut done) = oneshot::channel();
-        service
-            .commands
-            .send(Command::Terminal {
-                id,
-                operation,
-                reply,
-            })
-            .map_err(|_| service.health.error())?;
-        if let Ok(result) = tokio::time::timeout(Duration::from_secs(10), &mut done).await {
-            return result.map_err(|_| service.health.error())?;
-        }
-        service
-            .health
-            .fail("terminal cut failed to settle within 10 seconds");
-        let _ = done.await;
-        Err(service.health.error())
-    }
-
-    pub(super) fn dispose(&self, id: u32, reply: Option<Reply>) {
-        if let Ok(service) = self.service() {
-            let _ = service.commands.send(Command::Dispose { id, reply });
-        }
-    }
 }
 
 pub(super) fn failed(error: impl std::fmt::Display) -> TrustedError {
diff --git a/crates/js-runtime/src/trusted/engine.rs b/crates/js-runtime/src/trusted/engine.rs
index eb4302066c..4f14b08c04 100644
--- a/crates/js-runtime/src/trusted/engine.rs
+++ b/crates/js-runtime/src/trusted/engine.rs
@@ -105,10 +105,6 @@ pub(super) fn create(health: &Arc<Health>) -> Result<JsRuntime> {
             "maka:trusted/providers",
             include_str!(concat!(env!("OUT_DIR"), "/providers.js")),
         ),
-        (
-            "maka:trusted/terminal",
-            include_str!(concat!(env!("OUT_DIR"), "/terminal.js")),
-        ),
         (
             "maka:trusted/dispatch",
             include_str!("../../trusted/dispatch.js"),
@@ -122,9 +118,6 @@ pub(super) fn create(health: &Arc<Health>) -> Result<JsRuntime> {
 pub(super) struct Functions {
     pub model: v8::Global<v8::Function>,
     pub cancel: v8::Global<v8::Function>,
-    pub create: v8::Global<v8::Function>,
-    pub terminal: v8::Global<v8::Function>,
-    pub dispose: v8::Global<v8::Function>,
 }
 
 impl Functions {
@@ -141,9 +134,6 @@ impl Functions {
         Ok(Self {
             model: get(runtime, "model")?,
             cancel: get(runtime, "cancel")?,
-            create: get(runtime, "create")?,
-            terminal: get(runtime, "terminal")?,
-            dispose: get(runtime, "dispose")?,
         })
     }
 }
@@ -167,12 +157,6 @@ pub(super) fn call(
     Ok(Box::pin(async move { promise.await.map_err(failed) }))
 }
 
-pub(super) fn string(runtime: &mut JsRuntime, value: v8::Global<v8::Value>) -> Result<String> {
-    deno_core::scope!(scope, runtime);
-    let value = v8::Local::new(scope, value);
-    deno_core::serde_v8::from_v8(scope, value).map_err(failed)
-}
-
 #[cfg(test)]
 mod tests {
     #[test]
diff --git a/crates/js-runtime/src/trusted/worker.rs b/crates/js-runtime/src/trusted/worker.rs
index 754ed49a62..8e10ea1a22 100644
--- a/crates/js-runtime/src/trusted/worker.rs
+++ b/crates/js-runtime/src/trusted/worker.rs
@@ -29,24 +29,11 @@ use serde_json::json;
 use std::{collections::HashMap, future::poll_fn, sync::Arc, task::Poll};
 use tokio::sync::{OwnedSemaphorePermit, mpsc};
 
-struct Terminal {
-    _permit: OwnedSemaphorePermit,
-    busy: bool,
-    failed: bool,
-    closing: bool,
-    closed: Option<Reply>,
-}
 struct Model {
     _permit: OwnedSemaphorePermit,
     _input: OwnedSemaphorePermit,
     reply: Reply,
 }
-#[derive(Default)]
-struct Objects {
-    terminals: HashMap<u32, Terminal>,
-    models: HashMap<u32, Model>,
-    cuts: HashMap<u32, Reply>,
-}
 type Pending = FuturesUnordered<LocalBoxFuture<'static, (u32, Result<v8::Global<v8::Value>>)>>;
 
 pub(super) async fn run(
@@ -56,27 +43,19 @@ pub(super) async fn run(
     // Completion/permits stay outside the isolate's scope. On a fatal failure,
     // first drop all promise globals and the runtime (including HTTP resources),
     // only then acknowledge cleanup and release capacity.
-    let mut objects = Objects::default();
+    let mut models = HashMap::new();
     let result = async {
         let mut runtime = engine::create(&health)?;
-        drive(&mut runtime, &mut objects, &mut commands, &health).await
+        drive(&mut runtime, &mut models, &mut commands, &health).await
     }
     .await;
     if let Err(error) = &result {
         health.fail(error.to_string());
     }
     let error = health.error();
-    for (_, model) in objects.models {
+    for (_, model) in models {
         let _ = model.reply.send(Err(error.clone()));
     }
-    for (_, reply) in objects.cuts {
-        let _ = reply.send(Err(error.clone()));
-    }
-    for (_, terminal) in objects.terminals {
-        if let Some(reply) = terminal.closed {
-            let _ = reply.send(Err(error.clone()));
-        }
-    }
     // Queued commands were never executed; drop them only after runtime cleanup.
     commands.close();
     while let Some(command) = commands.recv().await {
@@ -87,7 +66,7 @@ pub(super) async fn run(
 
 async fn drive(
     runtime: &mut JsRuntime,
-    objects: &mut Objects,
+    models: &mut HashMap<u32, Model>,
     commands: &mut mpsc::UnboundedReceiver<Command>,
     health: &Health,
 ) -> Result<()> {
@@ -165,7 +144,7 @@ async fn drive(
                                 endpoint,
                             },
                         );
-                    objects.models.insert(
+                    models.insert(
                         id,
                         Model {
                             _permit: permit,
@@ -179,80 +158,16 @@ async fn drive(
                 Command::Cancel(id) => {
                     sync(runtime, &functions.cancel, &[json!(id)])?;
                 }
-                Command::Create { id, size, permit } => {
-                    objects.terminals.insert(
-                        id,
-                        Terminal {
-                            _permit: permit,
-                            busy: false,
-                            failed: false,
-                            closing: false,
-                            closed: None,
-                        },
-                    );
-                    if sync(runtime, &functions.create, &[json!(id), size]).is_err() {
-                        objects.terminals.get_mut(&id).unwrap().failed = true;
-                        sync(runtime, &functions.dispose, &[json!(id)])?;
-                    }
-                }
-                Command::Terminal {
-                    id,
-                    operation,
-                    reply,
-                } => {
-                    let Some(terminal) = objects.terminals.get_mut(&id) else {
-                        let _ = reply.send(Err(failed("terminal parser closed")));
-                        continue;
-                    };
-                    if terminal.busy || terminal.closing || terminal.failed {
-                        let _ = reply.send(Err(failed("terminal parser has an incomplete cut")));
-                        continue;
-                    }
-                    terminal.busy = true;
-                    objects.cuts.insert(id, reply);
-                    let (operation, argument) = operation.arguments();
-                    let call = engine::call(
-                        runtime,
-                        &functions.terminal,
-                        &[json!(id), json!(operation), argument],
-                    )?;
-                    pending.push(async move { (id, call.await) }.boxed_local());
-                }
-                Command::Dispose { id, reply } => {
-                    if let Some(terminal) = objects.terminals.get_mut(&id) {
-                        terminal.closing = true;
-                        terminal.closed = reply;
-                        if !terminal.busy {
-                            dispose(runtime, &functions, objects, id)?;
-                        }
-                    } else if let Some(reply) = reply {
-                        let _ = reply.send(Ok(String::new()));
-                    }
-                }
             },
             Ready::Complete(id, result) => {
-                if let Some(model) = objects.models.remove(&id) {
+                if let Some(model) = models.remove(&id) {
                     runtime
                         .op_state()
                         .borrow_mut()
                         .borrow_mut::<Models>()
                         .active
                         .remove(&id);
-                    let _ = model.reply.send(result.map(|_| String::new()));
-                } else if let Some(reply) = objects.cuts.remove(&id) {
-                    let result = result.and_then(|value| engine::string(runtime, value));
-                    let failed = result.is_err() || reply.is_closed();
-                    let terminal = objects.terminals.get_mut(&id).expect("live terminal cut");
-                    terminal.busy = false;
-                    terminal.failed |= failed;
-                    if terminal.closing {
-                        dispose(runtime, &functions, objects, id)?;
-                    } else if failed {
-                        // Keep the live handle's permit through its eventual
-                        // Dispose command, even though the parser is poisoned.
-                        sync(runtime, &functions.dispose, &[json!(id)])?;
-                    }
-                    let _ = reply.send(result);
+                    let _ = model.reply.send(result.map(|_| ()));
                 }
             }
         }
@@ -271,18 +186,3 @@ fn sync(
         .ok_or_else(|| failed("synchronous trusted function returned a pending promise"))??;
     Ok(())
 }
-
-fn dispose(
-    runtime: &mut JsRuntime,
-    functions: &Functions,
-    objects: &mut Objects,
-    id: u32,
-) -> Result<()> {
-    sync(runtime, &functions.dispose, &[json!(id)])?;
-    if let Some(terminal) = objects.terminals.remove(&id)
-        && let Some(reply) = terminal.closed
-    {
-        let _ = reply.send(Ok(String::new()));
-    }
-    Ok(())
-}
diff --git a/crates/js-runtime/terminal.js b/crates/js-runtime/terminal.js
deleted file mode 100644
index 11b26d2ed3..0000000000
--- a/crates/js-runtime/terminal.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { Terminal } from '@xterm/headless';
-import { Unicode11Addon } from '@xterm/addon-unicode11';
-
-// Only the headless parser lives here. Rust owns resource lifecycle and every
-// native effect. No model code, filesystem, network or credentials enter this isolate.
-globalThis.createTerminal = ({ cols, rows }) => {
-  const terminal = new Terminal({
-    cols,
-    rows,
-    scrollback: 500,
-    allowProposedApi: true,
-    scrollOnEraseInDisplay: false,
-    windowOptions: {},
-    logLevel: 'off',
-  });
-  terminal.loadAddon(new Unicode11Addon());
-  terminal.unicode.activeVersion = '11';
-  let visible = true,
-    mouseEncoding = 'default',
-    lastAlternateScreen;
-  let truncated = false,
-    atHistoryLimit = false,
-    suppressScroll = false;
-  let replyBytes = 0,
-    replies,
-    failure;
-  let expansion = 0;
-  const alternate = (params) => params.some((p) => [47, 1047, 1049].includes(p));
-  const flat = (params) => params.flatMap((p) => (Array.isArray(p) ? p : [p]));
-  terminal.onData((data) => {
-    if (replies === undefined) {
-      failure ||= new Error('Terminal reply escaped the parser write boundary');
-      return;
-    }
-    replyBytes += Deno.core.byteLength(data);
-    if (replyBytes > 1024 * 1024) {
-      failure ||= new Error('Terminal protocol reply budget exceeded');
-      return;
-    }
-    replies += data;
-  });
-  for (const id of [0, 1, 2, 7, 8, 9, 52, 777]) {
-    terminal.parser.registerOscHandler(id, () => true);
-  }
-  // These handlers loop or allocate directly from Ps. Input byte and V8 heap
-  // limits do not bound REP's external ArrayBuffer memory or repeated tab work.
-  for (const final of ['I', 'Z', 'L', 'M', 'S', 'T', 'b']) {
-    terminal.parser.registerCsiHandler({ final }, (params) => {
-      const count = flat(params)[0] || 1;
-      let unit = ['L', 'M', 'S', 'T'].includes(final) ? terminal.cols : 1;
-      if (final === 'b') {
-        const active = terminal.buffer.active;
-        const line = active.getLine(active.baseY + active.cursorY);
-        // Include both cells of a wide character and combined Unicode clusters.
-        for (const x of [active.cursorX - 1, active.cursorX - 2]) {
-          if (x >= 0) unit = Math.max(unit, line?.getCell(x)?.getChars().length || 1);
-        }
-      }
-      expansion += count * unit;
-      if (expansion <= 64 * 1024 && !failure) return false;
-      failure ||= new Error('Terminal escape expansion budget exceeded');
-      return true;
-    });
-  }
-  terminal.onScroll(() => {
-    if (terminal.buffer.active.type !== 'normal') return;
-    const full = terminal.buffer.normal.baseY >= 500;
-    if (full && atHistoryLimit && !suppressScroll) truncated = true;
-    atHistoryLimit = full;
-    suppressScroll = false;
-  });
-  terminal.parser.registerCsiHandler({ prefix: '?', final: 'h' }, (params) => {
-    const values = flat(params);
-    if (alternate(values)) {
-      lastAlternateScreen = undefined;
-      suppressScroll = true;
-    }
-    if (values.includes(25)) visible = true;
-    for (const value of values) {
-      if (value === 1006) mouseEncoding = 'sgr';
-      if (value === 1016) mouseEncoding = 'sgr_pixels';
-    }
-    return false;
-  });
-  terminal.parser.registerCsiHandler({ prefix: '?', final: 'l' }, (params) => {
-    const values = flat(params);
-    if (alternate(values)) {
-      if (terminal.buffer.active.type === 'alternate') {
-        const retained = render(terminal.buffer.active, terminal.rows, true);
-        lastAlternateScreen = retained.screen || undefined;
-        truncated ||= retained.truncated;
-      }
-      suppressScroll = true;
-    }
-    if (values.includes(25)) visible = false;
-    if (values.includes(1006) || values.includes(1016)) mouseEncoding = 'default';
-    return false;
-  });
-  terminal.parser.registerEscHandler({ final: 'c' }, () => {
-    visible = true;
-    mouseEncoding = 'default';
-    lastAlternateScreen = undefined;
-    return false;
-  });
-  terminal.parser.registerCsiHandler({ intermediates: '!', final: 'p' }, () => {
-    visible = true;
-    return false;
-  });
-  const write = async (data) => {
-    expansion = 0;
-    replies = '';
-    await new Promise((resolve) => terminal.write(data, resolve));
-    if (failure) throw failure;
-    suppressScroll = false;
-    const result = replies;
-    replies = undefined;
-    return result;
-  };
-  return {
-    write,
-    resize(size) {
-      terminal.resize(size.cols, size.rows);
-    },
-    async resetAfterGap() {
-      // A dropped escape terminator can leave the parser inside OSC/DCS.
-      // RIS resets parser state as well as the screen; terminal.reset() does not.
-      await write('\x1bc');
-      truncated = true;
-      atHistoryLimit = false;
-      suppressScroll = false;
-    },
-    snapshot() {
-      if (failure) throw failure;
-      const active = terminal.buffer.active;
-      const output = render(active, terminal.rows, false);
-      return {
-        screen: output.screen,
-        scrollback: output.scrollback,
-        ...(lastAlternateScreen === undefined ? {} : { lastAlternateScreen }),
-        size: { cols: terminal.cols, rows: terminal.rows },
-        cursor: { x: active.cursorX, y: active.cursorY, visible },
-        alternateScreen: active.type === 'alternate',
-        truncated: truncated || output.truncated,
-        input: {
-          applicationCursorKeysMode: terminal.modes.applicationCursorKeysMode,
-          mouseTrackingMode: terminal.modes.mouseTrackingMode,
-          mouseEncoding,
-        },
-      };
-    },
-    dispose() {
-      terminal.dispose();
-    },
-  };
-};
-
-function render(buffer, rows, screenOnly) {
-  const start = buffer.type === 'normal' ? buffer.baseY : buffer.viewportY;
-  const lines = [];
-  for (let i = screenOnly ? start : 0; i < Math.min(buffer.length, start + rows); i++) {
-    const line = buffer.getLine(i);
-    lines.push({ text: line.translateToString(true), wrapped: line.isWrapped, index: i });
-  }
-  let truncated = false;
-  while (lines[0]?.wrapped) {
-    lines.shift();
-    truncated = true;
-  }
-  const screen = Array(rows).fill(''),
-    scrollback = [];
-  for (const line of lines) {
-    if (line.index >= start) screen[line.index - start] = line.text;
-    else if (line.wrapped && scrollback.length) scrollback[scrollback.length - 1] += line.text;
-    else scrollback.push(line.text);
-  }
-  while (screen.at(-1) === '') screen.pop();
-  return { screen: screen.join('\n'), scrollback: scrollback.join('\n'), truncated };
-}
diff --git a/crates/js-runtime/tests/integration/main.rs b/crates/js-runtime/tests/integration/main.rs
index 1698d528a4..14a2e1720d 100644
--- a/crates/js-runtime/tests/integration/main.rs
+++ b/crates/js-runtime/tests/integration/main.rs
@@ -20,4 +20,3 @@
 mod code_mode;
 mod plugin_vm;
 mod result_contract;
-mod terminal_screen;
diff --git a/crates/js-runtime/tests/integration/terminal_screen.rs b/crates/js-runtime/tests/integration/terminal_screen.rs
deleted file mode 100644
index 3e15f64a3a..0000000000
--- a/crates/js-runtime/tests/integration/terminal_screen.rs
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use maka_js_runtime::terminal::Screen;
-use maka_runtime::terminal::TerminalSize;
-use serde_json::{Value, json};
-use std::{
-    io::Write,
-    path::Path,
-    process::{Command, Stdio},
-    time::Duration,
-};
-
-#[cfg(unix)]
-#[tokio::test]
-async fn real_pty_queries_and_mode_aware_input_cross_the_screen_cut() {
-    use maka_process::pty;
-    use maka_runtime::terminal::input::{InputAction, encode_actions};
-
-    let expected = "\u{1b}OA中\u{1b}[<0;2;2M\u{1b}[<0;2;2m\r";
-    let hex = expected
-        .bytes()
-        .map(|b| format!("{b:02x}"))
-        .collect::<String>();
-    let mut command = pty::PtyCommand::new("/bin/sh", std::env::current_dir().unwrap());
-    command.args([
-        "-c",
-        &format!(
-            r#"
-        stty raw -echo
-        printf '\033[6n'
-        reply=$(dd bs=1 count=6 2>/dev/null | od -An -tx1 | tr -d ' \n')
-        test "$reply" = 1b5b313b3152 || exit 3
-        printf '\033[?1h\033[?1006h\033[?1002hready'
-        input=$(dd bs=1 count={} 2>/dev/null | od -An -tx1 | tr -d ' \n')
-        test "$input" = {hex} || exit 4
-        printf '\r\nverified'
-        exit 42
-    "#,
-            expected.len()
-        ),
-    ]);
-    let size = TerminalSize::new(80, 24).unwrap();
-    let (mut child, io) = pty::spawn(command, size).await.unwrap();
-    let mut screen = Screen::new(size).unwrap();
-    let actions = json!([
-        {"type":"key","key":"arrow_up"},
-        {"type":"text","text":"中"},
-        {"type":"mouse","event":"click","button":"left","x":1,"y":1},
-        {"type":"key","key":"enter"}
-    ])
-    .as_array()
-    .unwrap()
-    .iter()
-    .cloned()
-    .map(InputAction::parse)
-    .collect::<Result<Vec<_>, _>>()
-    .unwrap();
-    tokio::time::timeout(Duration::from_secs(5), async {
-        let mut sent = false;
-        let mut bytes = [0; 1024];
-        loop {
-            let count = io.read(&mut bytes).await.unwrap();
-            if count == 0 {
-                break;
-            }
-            // This fixture emits ASCII; native UTF-8 input is checked by the
-            // child byte-for-byte, independent of read chunk boundaries.
-            let cut = screen
-                .write(std::str::from_utf8(&bytes[..count]).unwrap())
-                .await
-                .unwrap();
-            write_pty(&io, cut.replies.as_bytes()).await;
-            let cut = cut.screen;
-            if !sent && cut.screen.contains("ready") {
-                let input = encode_actions(&actions, cut.input, cut.size).unwrap();
-                assert_eq!(input, expected);
-                write_pty(&io, input.as_bytes()).await;
-                sent = true;
-            }
-        }
-        assert!(sent);
-        assert_eq!(child.wait().await.unwrap().code(), Some(42));
-        assert!(screen.snapshot().await.unwrap().screen.contains("verified"));
-    })
-    .await
-    .unwrap();
-
-    async fn write_pty(io: &pty::PtyIo, mut input: &[u8]) {
-        while !input.is_empty() {
-            let count = io.write(input).await.unwrap();
-            assert!(count > 0);
-            input = &input[count..];
-        }
-    }
-}
-
-#[tokio::test]
-async fn screen_matches_original_collector_across_parser_cuts_modes_and_reflow() {
-    let actions = json!([
-        {"write":"line\r\n".repeat(502)},
-        {"write":"\u{1b}[?1049l"},
-        {"write":"overflow\r\n"},
-        {"write":"\u{1b}c"},
-        {"write":"hello 中文😀\r\nnext\u{1b}[6n"},
-        {"cols":5,"rows":3},
-        {"write":"\r\n\u{1b}[?1h\u{1b}[?1002h\u{1b}[?1006h\u{1b}[?25l"},
-        {"write":"\u{1b}[?1049hALT 中文\r\nsecond"},
-        {"write":"\u{1b}[?1049l\u{1b}[?1016h"},
-        {"write":"\u{1b}]52;c;ignored"},
-        {"write":"\u{7}OK\u{1b}[5n\u{1b}[6n"},
-        {"write":"\r\n0123456789\r\n".repeat(520)},
-        {"cols":20,"rows":5},
-        {"write":"\u{1b}creset\u{1b}[6n"}
-    ]);
-    let mut screen = Screen::new(TerminalSize::new(10, 3).unwrap()).unwrap();
-    let mut observations = vec![];
-    for action in actions.as_array().unwrap() {
-        let (replies, cut) = if let Some(data) = action["write"].as_str() {
-            let cut = screen.write(data).await.unwrap();
-            (cut.replies, cut.screen)
-        } else {
-            let cut = screen
-                .resize(
-                    TerminalSize::new(
-                        action["cols"].as_u64().unwrap() as u16,
-                        action["rows"].as_u64().unwrap() as u16,
-                    )
-                    .unwrap(),
-                )
-                .await
-                .unwrap();
-            (String::new(), cut)
-        };
-        observations.push(json!({"screen":cut,"replies":replies}));
-    }
-    let oracle = Path::new(env!("CARGO_MANIFEST_DIR"))
-        .join("../../tests/fixtures/terminal-screen-oracle.mjs");
-    let mut child = Command::new("node")
-        .arg(oracle)
-        .stdin(Stdio::piped())
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .spawn()
-        .unwrap();
-    child
-        .stdin
-        .take()
-        .unwrap()
-        .write_all(&serde_json::to_vec(&actions).unwrap())
-        .unwrap();
-    let output = child.wait_with_output().unwrap();
-    assert!(
-        output.status.success(),
-        "{}",
-        String::from_utf8_lossy(&output.stderr)
-    );
-    let expected: Vec<Value> = serde_json::from_slice(&output.stdout).unwrap();
-    for (index, (actual, expected)) in observations.iter().zip(&expected).enumerate() {
-        assert_eq!(actual, expected, "parser cut {index}");
-    }
-    assert_eq!(observations.len(), expected.len());
-}
-
-#[tokio::test]
-async fn gaps_reset_escape_state_and_incomplete_cuts_cannot_be_reused() {
-    let size = TerminalSize::new(10, 3).unwrap();
-    let mut screen = Screen::new(size).unwrap();
-    screen
-        .write("before\u{1b}]52;c;unterminated")
-        .await
-        .unwrap();
-    screen.reset_after_gap().await.unwrap();
-    screen.write("after").await.unwrap();
-    let snapshot = screen.snapshot().await.unwrap();
-    assert_eq!(snapshot.screen, "after");
-    assert!(snapshot.truncated);
-    assert!(screen.write(&"x".repeat(65537)).await.is_err());
-    assert_eq!(
-        screen.snapshot().await.unwrap(),
-        snapshot,
-        "pre-admission rejection has no effect"
-    );
-
-    let mut interrupted = Screen::new(size).unwrap();
-    {
-        let write = interrupted.write("unfinished");
-        tokio::pin!(write);
-        assert!(
-            std::future::poll_fn(|cx| match write.as_mut().poll(cx) {
-                std::task::Poll::Pending => std::task::Poll::Ready(true),
-                std::task::Poll::Ready(_) => std::task::Poll::Ready(false),
-            })
-            .await
-        );
-    }
-    assert!(interrupted.snapshot().await.is_err());
-
-    let mut flooded = Screen::new(size).unwrap();
-    let queries = "\u{1b}[6n".repeat(16384);
-    let outcome = tokio::time::timeout(Duration::from_secs(10), async {
-        loop {
-            if flooded.write(&queries).await.is_err() {
-                break;
-            }
-        }
-    })
-    .await;
-    assert!(
-        outcome.is_ok(),
-        "reply flooding must fail within its lifetime budget"
-    );
-    assert!(flooded.snapshot().await.is_err());
-
-    // Reject short escape sequences requesting huge work before their handlers
-    // allocate backing memory or enter loops proportional to the parameter.
-    for final_byte in ['I', 'Z', 'L', 'M', 'S', 'T', 'b'] {
-        let mut amplified = Screen::new(size).unwrap();
-        let result = amplified
-            .write(&format!("x\u{1b}[2147483647{final_byte}"))
-            .await;
-        assert!(result.unwrap_err().to_string().contains("expansion budget"));
-        assert!(amplified.snapshot().await.is_err());
-    }
-}
diff --git a/crates/js-runtime/trusted/dispatch.js b/crates/js-runtime/trusted/dispatch.js
index 35eb3a4440..170a34db84 100644
--- a/crates/js-runtime/trusted/dispatch.js
+++ b/crates/js-runtime/trusted/dispatch.js
@@ -19,7 +19,6 @@
 
 (() => {
   const requests = new Map();
-  const terminals = new Map();
   globalThis.makaTrusted = {
     async model(id, request) {
       const controller = new AbortController();
@@ -39,24 +38,5 @@
     cancel(id) {
       requests.get(id)?.abort();
     },
-    create(id, size) {
-      terminals.set(id, createTerminal(size));
-    },
-    async terminal(id, operation, argument) {
-      const terminal = terminals.get(id);
-      if (!terminal) throw new Error('terminal parser closed');
-      let result = await terminal[operation](argument);
-      if (operation === 'write') result = { replies: result, screen: terminal.snapshot() };
-      else if (operation === 'resize') result = terminal.snapshot();
-      const json = JSON.stringify(result ?? null);
-      if (Deno.core.byteLength(json) > 2 * 1024 * 1024)
-        throw new Error('terminal snapshot budget exceeded');
-      return json;
-    },
-    dispose(id) {
-      const terminal = terminals.get(id);
-      terminals.delete(id);
-      terminal?.dispose();
-    },
   };
 })();
diff --git a/crates/model/tests/integration/provider_ingress.rs b/crates/model/tests/integration/provider_ingress.rs
index b6e52a4cd0..35575675ff 100644
--- a/crates/model/tests/integration/provider_ingress.rs
+++ b/crates/model/tests/integration/provider_ingress.rs
@@ -18,9 +18,8 @@
  */
 
 use super::{provider_compatible::body as read_request, provider_stream::request};
-use maka_js_runtime::{terminal::Screen, trusted::TrustedRuntime};
+use maka_js_runtime::trusted::TrustedRuntime;
 use maka_model::{ModelError, ModelEvent, ModelExecutor, ProviderKind, StepBuilder};
-use maka_runtime::terminal::TerminalSize;
 use serde_json::json;
 use std::{io, time::Duration};
 use tokio::{
@@ -45,8 +44,7 @@ enum Response {
 async fn ingress_limits_close_only_offending_http_and_preserve_bounded_sse_records() {
     tokio::time::timeout(Duration::from_secs(30), async {
         let runtime = TrustedRuntime::default();
-        let models = ModelExecutor::with_runtime(runtime.clone(), 1, Duration::from_secs(20)).unwrap();
-        let mut screen = Screen::with_runtime(runtime, TerminalSize::new(20, 3).unwrap()).unwrap();
+        let models = ModelExecutor::with_runtime(runtime, 1, Duration::from_secs(20)).unwrap();
         let exact_record = format!(":{}\n\n", "x".repeat(LIMIT - 3));
         let cr_records = format!(":{}\r\r", "x".repeat(LIMIT / 2)).repeat(3);
         let multiline = "data: {}\r\n".repeat(LIMIT / 10 + 1);
@@ -139,11 +137,7 @@ async fn ingress_limits_close_only_offending_http_and_preserve_bounded_sse_recor
                 }
                 assert!(step.finish().is_err());
             }
-            screen.write(&index.to_string()).await.unwrap();
-            assert!(screen.snapshot().await.unwrap().screen.ends_with(&index.to_string()));
         }
-        // Reuse the same parser after every response was settled.
-        screen.close().await;
     }).await.expect("ingress failure must settle without a shared-runtime failure");
 }
 
diff --git a/crates/model/tests/integration/trusted_concurrency.rs b/crates/model/tests/integration/trusted_concurrency.rs
index 70b9380822..d23b23202d 100644
--- a/crates/model/tests/integration/trusted_concurrency.rs
+++ b/crates/model/tests/integration/trusted_concurrency.rs
@@ -18,9 +18,8 @@
  */
 
 use super::provider_stream::{read_request, request};
-use maka_js_runtime::{terminal::Screen, trusted::TrustedRuntime};
+use maka_js_runtime::trusted::TrustedRuntime;
 use maka_model::{ModelEvent, ModelExecutor, ProviderKind, StepBuilder};
-use maka_runtime::terminal::TerminalSize;
 use serde_json::json;
 use std::time::Duration;
 use tokio::{
@@ -31,13 +30,10 @@ use tokio::{
 use tokio_util::sync::CancellationToken;
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn shared_sdk_and_terminals_progress_through_backpressure_cancellation_and_parser_failure() {
+async fn shared_sdk_requests_progress_through_backpressure_and_cancellation() {
     tokio::time::timeout(Duration::from_secs(20), async {
         let runtime = TrustedRuntime::default();
         let models = ModelExecutor::with_runtime(runtime.clone(), 4, Duration::from_secs(15)).unwrap();
-        let size = TerminalSize::new(20, 3).unwrap();
-        let mut screen = Screen::with_runtime(runtime.clone(), size).unwrap();
-        let mut broken = Screen::with_runtime(runtime.clone(), size).unwrap();
 
         let waiting = TcpListener::bind("127.0.0.1:0").await.unwrap();
         let waiting_base = format!("http://{}/v1", waiting.local_addr().unwrap());
@@ -96,8 +92,6 @@ async fn shared_sdk_and_terminals_progress_through_backpressure_cancellation_and
             stream.cancel_and_wait().await;
         }
         complete(&models, &base).await;
-        screen.write("one").await.unwrap();
-        assert_eq!(screen.snapshot().await.unwrap().screen, "one");
 
         held.cancel_and_wait().await;
         pending_http.await.unwrap();
@@ -122,17 +116,11 @@ async fn shared_sdk_and_terminals_progress_through_backpressure_cancellation_and
         abandoned_call.abort();
         assert!(abandoned_call.await.unwrap_err().is_cancelled());
         abandoned_http.await.unwrap();
-        assert!(broken.write("x\u{1b}[2147483647I").await.unwrap_err().to_string().contains("expansion budget"));
-        assert!(broken.snapshot().await.is_err());
-        broken.close().await;
         complete(&models, &base).await;
 
         slow.cancel_and_wait().await;
         flood_http.await.unwrap();
-        screen.write("\r\ntwo").await.unwrap();
-        assert_eq!(screen.snapshot().await.unwrap().screen, "one\ntwo");
         complete(&models, &base).await;
-        screen.close().await;
         healthy_http.await.unwrap();
     }).await.expect("one trusted V8 must make progress across independent lifetimes");
 }
diff --git a/crates/process/Cargo.toml b/crates/process/Cargo.toml
index 3c1b004799..dc758b21b3 100644
--- a/crates/process/Cargo.toml
+++ b/crates/process/Cargo.toml
@@ -24,14 +24,17 @@ license.workspace = true
 publish.workspace = true
 
 [dependencies]
+alacritty_terminal = { version = "0.26.0", default-features = false }
 maka-sandbox.workspace = true
 maka-network.workspace = true
 maka-runtime.workspace = true
 serde.workspace = true
 schemars.workspace = true
 serde_json.workspace = true
+thiserror.workspace = true
 tokio = { workspace = true, features = ["process", "net"] }
 tokio-util.workspace = true
+unicode-width = "0.2.2"
 
 [target.'cfg(unix)'.dependencies]
 libc.workspace = true
diff --git a/crates/process/README.md b/crates/process/README.md
new file mode 100644
index 0000000000..4ff2fca955
--- /dev/null
+++ b/crates/process/README.md
@@ -0,0 +1,37 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# maka-process
+
+[中文](README.zh-CN.md)
+
+Native process and PTY transport, cancellation, sandbox launch integration and
+headless terminal state for Linux, macOS and Windows.
+
+`terminal::Screen` uses `alacritty_terminal` without its PTY event loop or renderer.
+The existing PTY worker owns the parser; no JavaScript runtime, extra thread or
+cross-runtime serialization is needed. The Host retains admission, canonical
+records, backpressure and final output drain. Desktop rendering remains xterm.
+
+Snapshots expose the viewport, 500 rows of history, cursor/input modes and the
+last alternate screen. History loss or text-budget clipping sets `truncated`.
+Writes, escape expansion, combining characters, OSC storage and protocol replies
+are bounded. A failed parser cannot publish partial state; the Host retains its
+last successful snapshot. Clipboard, title and hyperlink effects are suppressed.
+Terminal escape handling follows Alacritty, not exact xterm emulation.
diff --git a/crates/process/README.zh-CN.md b/crates/process/README.zh-CN.md
new file mode 100644
index 0000000000..c744279a4e
--- /dev/null
+++ b/crates/process/README.zh-CN.md
@@ -0,0 +1,33 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# maka-process
+
+[English](README.md)
+
+Linux、macOS 和 Windows 的原生进程/PTY 传输、取消、沙箱启动接入和无界面终端状态。
+
+`terminal::Screen` 使用 `alacritty_terminal`,不启用其 PTY 事件循环或渲染器。
+解析器归现有 PTY worker 所有,不需要 JavaScript runtime、额外线程或跨 runtime 序列化。
+Host 保留执行准入、规范日志、背压和最终输出排空;Desktop 仍用 xterm 渲染。
+
+快照提供可见屏幕、500 行历史、光标/输入模式和最后一次备用屏幕。
+历史丢失或文本超限会设置 `truncated`。写入、转义序列扩展、组合字符、OSC 存储和
+协议应答均有资源上限。解析失败不发布部分状态,Host 保留最后一次成功快照。
+剪贴板、标题和超链接副作用不会执行。转义序列行为遵循 Alacritty,不模拟全部 xterm 差异。
diff --git a/crates/process/src/lib.rs b/crates/process/src/lib.rs
index 81848b0a3c..199ea4d6a7 100644
--- a/crates/process/src/lib.rs
+++ b/crates/process/src/lib.rs
@@ -37,6 +37,7 @@ mod process;
 pub mod pty;
 mod shell;
 mod tail;
+pub mod terminal;
 #[cfg(windows)]
 mod windows;
 #[cfg(windows)]
diff --git a/crates/process/src/terminal.rs b/crates/process/src/terminal.rs
new file mode 100644
index 0000000000..f151fbf413
--- /dev/null
+++ b/crates/process/src/terminal.rs
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use alacritty_terminal::{
+    Term,
+    grid::Dimensions,
+    index::{Column, Line},
+    term::{Config, TermMode, cell::Flags},
+    vte::ansi,
+};
+use maka_runtime::terminal::{
+    MouseEncoding, MouseTracking, TerminalCursor, TerminalInputModes, TerminalScreen, TerminalSize,
+};
+use std::{cell::RefCell, rc::Rc};
+
+mod handler;
+use handler::{Events, State};
+
+const HISTORY: usize = 500;
+const MAX_WRITE: usize = 64 * 1024;
+const MAX_TEXT: usize = 2 * 1024 * 1024;
+
+#[derive(Debug, thiserror::Error)]
+#[error("terminal screen failed: {0}")]
+pub struct ScreenError(&'static str);
+
+/// Pure terminal state. No OS handles, runtime, threads, or external effects.
+/// Parsing is synchronous; callers own scheduling, backpressure and persistence.
+pub struct Screen {
+    parser: ansi::Processor,
+    state: State,
+    strings: StringLimit,
+    failed: bool,
+}
+
+impl Screen {
+    pub fn new(size: TerminalSize) -> Self {
+        let events = Events(Rc::new(RefCell::new(Default::default())));
+        let term = Term::new(
+            Config {
+                scrolling_history: HISTORY,
+                ..Default::default()
+            },
+            &Size(size),
+            events.clone(),
+        );
+        Self {
+            parser: ansi::Processor::new(),
+            state: State::new(term, events),
+            strings: StringLimit::default(),
+            failed: false,
+        }
+    }
+
+    pub fn write(&mut self, mut text: &str) -> Result<String, ScreenError> {
+        self.check()?;
+        if text.len() > MAX_WRITE {
+            return Err(ScreenError("write exceeds 64 KiB"));
+        }
+        self.state.begin();
+        // Native transport decoding belongs to the caller. Keep parser cuts
+        // on UTF-8 boundaries too: vte's partial-codepoint path can consume a
+        // following ASCII character when its four-byte lookahead spans both.
+        while !text.is_empty() {
+            let end = text.floor_char_boundary(256.min(text.len()));
+            let chunk = &text.as_bytes()[..end];
+            text = &text[end..];
+            if self.strings.feed(chunk).is_err() {
+                self.failed = true;
+                return Err(ScreenError("terminal string exceeds 64 KiB"));
+            }
+            self.parser.advance(&mut self.state, chunk);
+            // We have no renderer to batch. Flush synchronized output at the
+            // parser cut so replies and persisted state never wait for repaint.
+            self.parser.stop_sync(&mut self.state);
+            if self.state.failed() {
+                self.failed = true;
+                return Err(ScreenError("terminal expansion or reply budget exceeded"));
+            }
+        }
+        Ok(self.state.take_replies())
+    }
+
+    pub fn resize(&mut self, size: TerminalSize) -> Result<(), ScreenError> {
+        self.check()?;
+        let previous = self.size();
+        self.state.term.resize(Size(size));
+        self.state.resized();
+        self.state.truncated |= self.state.term.history_size() == HISTORY
+            && (size.cols() < previous.cols() || size.rows() < previous.rows());
+        Ok(())
+    }
+
+    pub fn reset_after_gap(&mut self) {
+        let size = self.size();
+        *self = Self::new(size);
+        self.state.truncated = true;
+    }
+
+    pub fn size(&self) -> TerminalSize {
+        TerminalSize::new(
+            self.state.term.columns() as u16,
+            self.state.term.screen_lines() as u16,
+        )
+        .expect("validated terminal dimensions")
+    }
+
+    pub fn snapshot(&self) -> Result<TerminalScreen, ScreenError> {
+        self.check()?;
+        let term = &self.state.term;
+        let mode = term.mode();
+        let (screen, scrollback, clipped) = render(term, true);
+        let point = term.grid().cursor.point;
+        Ok(TerminalScreen {
+            screen,
+            scrollback,
+            last_alternate_screen: self.state.last_alternate.clone(),
+            size: self.size(),
+            cursor: TerminalCursor {
+                x: point.column.0 as u16,
+                y: point.line.0.max(0) as u16,
+                visible: mode.contains(TermMode::SHOW_CURSOR),
+            },
+            alternate_screen: mode.contains(TermMode::ALT_SCREEN),
+            truncated: self.state.truncated || clipped,
+            input: TerminalInputModes {
+                application_cursor_keys_mode: mode.contains(TermMode::APP_CURSOR),
+                mouse_tracking_mode: self.state.mouse,
+                mouse_encoding: self.state.encoding,
+            },
+        })
+    }
+
+    fn check(&self) -> Result<(), ScreenError> {
+        if self.failed {
+            Err(ScreenError("parser is closed after a failed operation"))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+struct Size(TerminalSize);
+impl Dimensions for Size {
+    fn columns(&self) -> usize {
+        self.0.cols().into()
+    }
+    fn screen_lines(&self) -> usize {
+        self.0.rows().into()
+    }
+    fn total_lines(&self) -> usize {
+        self.screen_lines()
+    }
+}
+
+/// Bound upstream's OSC accumulator across arbitrary native read boundaries.
+/// Other escape strings have no unbounded storage in vte.
+#[derive(Default)]
+struct StringLimit {
+    escaped: bool,
+    osc: Option<usize>,
+}
+impl StringLimit {
+    fn feed(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        for &byte in bytes {
+            if let Some(length) = &mut self.osc {
+                if matches!(byte, 7 | 0x18 | 0x1a | 0x1b) {
+                    self.osc = None;
+                    self.escaped = byte == 0x1b;
+                } else {
+                    *length += 1;
+                    if *length > MAX_WRITE {
+                        return Err(());
+                    }
+                }
+            } else if byte == 0x1b {
+                self.escaped = true;
+            } else if self.escaped {
+                match byte {
+                    b']' => {
+                        self.osc = Some(0);
+                        self.escaped = false;
+                    }
+                    0..=0x17 | 0x19 | 0x1c..=0x1f => {}
+                    _ => self.escaped = false,
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+fn render(term: &Term<Events>, history: bool) -> (String, String, bool) {
+    let grid = term.grid();
+    let mut screen = String::new();
+    let mut clipped = false;
+    let mut remaining = MAX_TEXT;
+    // Preserve the current viewport before spending the snapshot budget on
+    // history. Keep the newest history when old output must be dropped.
+    for y in 0..term.screen_lines() as i32 {
+        let (text, overflow) = render_row(term, y, remaining.saturating_sub(1));
+        if y != 0 {
+            screen.push('\n');
+        }
+        screen.push_str(&text);
+        remaining = MAX_TEXT.saturating_sub(screen.len());
+        clipped |= overflow;
+        if overflow {
+            break;
+        }
+    }
+    while screen.ends_with('\n') {
+        screen.pop();
+    }
+    remaining = MAX_TEXT - screen.len();
+    let mut lines = Vec::new();
+    if history {
+        for y in (-(term.history_size() as i32)..0).rev() {
+            let (text, overflow) = render_row(term, y, remaining.saturating_sub(1));
+            if overflow || remaining == 0 {
+                clipped = true;
+                break;
+            }
+            remaining = remaining.saturating_sub(text.len() + 1);
+            let wrapped = grid[Line(y)][Column(term.columns() - 1)]
+                .flags
+                .contains(Flags::WRAPLINE);
+            lines.push((text, wrapped));
+        }
+    }
+    let mut scrollback = String::new();
+    let mut wrapped = true;
+    for (text, continues) in lines.into_iter().rev() {
+        if !wrapped {
+            scrollback.push('\n');
+        }
+        scrollback.push_str(&text);
+        wrapped = continues;
+    }
+    (screen, scrollback, clipped)
+}
+
+fn render_row(term: &Term<Events>, y: i32, budget: usize) -> (String, bool) {
+    let row = &term.grid()[Line(y)];
+    let mut text = String::new();
+    for x in 0..term.columns() {
+        let cell = &row[Column(x)];
+        if cell
+            .flags
+            .intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
+        {
+            continue;
+        }
+        for c in std::iter::once(cell.c).chain(cell.zerowidth().unwrap_or_default().iter().copied())
+        {
+            if text.len() + c.len_utf8() > budget {
+                return (text, true);
+            }
+            text.push(c);
+        }
+    }
+    text.truncate(text.trim_end_matches(' ').len());
+    (text, false)
+}
diff --git a/crates/process/src/terminal/handler.rs b/crates/process/src/terminal/handler.rs
new file mode 100644
index 0000000000..049c818875
--- /dev/null
+++ b/crates/process/src/terminal/handler.rs
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+use alacritty_terminal::{
+    event::{Event, EventListener},
+    grid::GridCell,
+    vte::ansi::*,
+};
+use unicode_width::UnicodeWidthChar;
+
+const MAX_REPLIES: usize = 1024 * 1024;
+
+#[derive(Default)]
+pub(super) struct ReplyBuffer {
+    text: String,
+    failed: bool,
+}
+#[derive(Clone)]
+pub(super) struct Events(pub Rc<RefCell<ReplyBuffer>>);
+impl EventListener for Events {
+    fn send_event(&self, event: Event) {
+        // UI effects (including OSC clipboard/title requests) never escape the
+        // headless parser. Only protocol replies can reach the native PTY.
+        if let Event::PtyWrite(text) = event {
+            let mut replies = self.0.borrow_mut();
+            if replies.text.len() + text.len() > MAX_REPLIES {
+                replies.failed = true;
+            } else if !replies.failed {
+                replies.text.push_str(&text);
+            }
+        }
+    }
+}
+
+pub(super) struct State {
+    pub term: Term<Events>,
+    events: Events,
+    pub truncated: bool,
+    pub last_alternate: Option<String>,
+    pub mouse: MouseTracking,
+    pub encoding: MouseEncoding,
+    work: usize,
+    failed: bool,
+    region: std::ops::Range<usize>,
+}
+impl State {
+    pub fn new(term: Term<Events>, events: Events) -> Self {
+        let rows = term.screen_lines();
+        Self {
+            term,
+            events,
+            truncated: false,
+            last_alternate: None,
+            mouse: MouseTracking::None,
+            encoding: MouseEncoding::Default,
+            work: 0,
+            failed: false,
+            region: 0..rows,
+        }
+    }
+    pub fn begin(&mut self) {
+        self.work = 0;
+    }
+    pub fn failed(&self) -> bool {
+        self.failed || self.events.0.borrow().failed
+    }
+    pub fn take_replies(&self) -> String {
+        std::mem::take(&mut self.events.0.borrow_mut().text)
+    }
+    fn charge(&mut self, work: usize) -> bool {
+        self.work = self.work.saturating_add(work);
+        self.failed |= self.work > MAX_WRITE;
+        !self.failed
+    }
+    fn loses_history(&mut self, origin: usize, lines: usize) {
+        self.truncated |= !self.term.mode().contains(TermMode::ALT_SCREEN)
+            && origin == 0
+            && self.term.history_size().saturating_add(lines) > HISTORY;
+    }
+    fn line_wrap(&mut self) {
+        if self.term.grid().cursor.point.line.0 as usize == self.region.end - 1 {
+            self.loses_history(self.region.start, 1);
+        }
+    }
+    pub fn resized(&mut self) {
+        self.region = 0..self.term.screen_lines();
+    }
+    fn alternate(&mut self, enabled: bool) {
+        let active = self.term.mode().contains(TermMode::ALT_SCREEN);
+        if enabled && !active {
+            self.last_alternate = None;
+        } else if !enabled && active {
+            let (screen, _, clipped) = render(&self.term, false);
+            self.last_alternate = (!screen.is_empty()).then_some(screen);
+            self.truncated |= clipped;
+        }
+        if active != enabled {
+            self.term.swap_alt();
+        }
+    }
+}
+
+macro_rules! forward {
+    ($($name:ident($($arg:ident: $ty:ty),*);)*) => {$(
+        fn $name(&mut self, $($arg: $ty),*) {
+            if self.charge(1) { self.term.$name($($arg),*); }
+        }
+    )*};
+}
+
+impl Handler for State {
+    forward! {
+        set_cursor_style(style: Option<CursorStyle>);
+        set_cursor_shape(shape: CursorShape);
+        goto(line: i32, col: usize);
+        goto_line(line: i32);
+        goto_col(col: usize);
+        insert_blank(count: usize);
+        move_up(count: usize);
+        move_down(count: usize);
+        identify_terminal(intermediate: Option<char>);
+        device_status(kind: usize);
+        move_forward(count: usize);
+        move_backward(count: usize);
+        move_down_and_cr(count: usize);
+        move_up_and_cr(count: usize);
+        backspace();
+        carriage_return();
+        substitute();
+        set_horizontal_tabstop();
+        erase_chars(count: usize);
+        delete_chars(count: usize);
+        save_cursor_position();
+        restore_cursor_position();
+        clear_line(mode: LineClearMode);
+        clear_tabs(mode: TabulationClearMode);
+        set_tabs(interval: u16);
+        reverse_index();
+        terminal_attribute(attr: Attr);
+        set_mode(mode: Mode);
+        unset_mode(mode: Mode);
+        report_mode(mode: Mode);
+        set_keypad_application_mode();
+        unset_keypad_application_mode();
+        set_active_charset(index: CharsetIndex);
+        configure_charset(index: CharsetIndex, charset: StandardCharset);
+        set_color(index: usize, color: Rgb);
+        reset_color(index: usize);
+        decaln();
+    }
+
+    fn input(&mut self, c: char) {
+        if !self.charge(1) {
+            return;
+        }
+        let grid = self.term.grid();
+        if c.width() == Some(0) {
+            let point = grid.cursor.point;
+            let mut col = point.column.0;
+            if !grid.cursor.input_needs_wrap {
+                col = col.saturating_sub(1);
+            }
+            if grid[point.line][Column(col)]
+                .flags
+                .contains(Flags::WIDE_CHAR_SPACER)
+            {
+                col = col.saturating_sub(1);
+            }
+            if grid[point.line][Column(col)]
+                .zerowidth()
+                .is_some_and(|chars| chars.len() >= 32)
+            {
+                self.failed = true;
+                return;
+            }
+        } else if self.term.mode().contains(TermMode::LINE_WRAP)
+            && (grid.cursor.input_needs_wrap
+                || (c.width() == Some(2) && grid.cursor.point.column.0 + 1 >= self.term.columns()))
+        {
+            self.line_wrap();
+        }
+        self.term.input(c);
+    }
+
+    fn linefeed(&mut self) {
+        if self.charge(1) {
+            self.line_wrap();
+            self.term.linefeed();
+        }
+    }
+    fn newline(&mut self) {
+        if self.charge(1) {
+            self.line_wrap();
+            self.term.newline();
+        }
+    }
+    fn scroll_up(&mut self, count: usize) {
+        if self.charge(count.saturating_mul(self.term.columns())) {
+            self.loses_history(self.region.start, count.min(self.region.len()));
+            self.term.scroll_up(count);
+        }
+    }
+    fn scroll_down(&mut self, count: usize) {
+        if self.charge(count.saturating_mul(self.term.columns())) {
+            self.term.scroll_down(count);
+        }
+    }
+    fn insert_blank_lines(&mut self, count: usize) {
+        if self.charge(count.saturating_mul(self.term.columns())) {
+            self.term.insert_blank_lines(count);
+        }
+    }
+    fn delete_lines(&mut self, count: usize) {
+        if self.charge(count.saturating_mul(self.term.columns())) {
+            let origin = self.term.grid().cursor.point.line.0 as usize;
+            if self.region.contains(&origin) {
+                self.loses_history(origin, count.min(self.region.len()));
+            }
+            self.term.delete_lines(count);
+        }
+    }
+    fn put_tab(&mut self, count: u16) {
+        if self.charge(count.into()) {
+            if self.term.grid().cursor.input_needs_wrap
+                && self.term.mode().contains(TermMode::LINE_WRAP)
+            {
+                self.line_wrap();
+            }
+            self.term.put_tab(count);
+        }
+    }
+    fn move_forward_tabs(&mut self, count: u16) {
+        if self.charge(count.into()) {
+            self.term.move_forward_tabs(count);
+        }
+    }
+    fn move_backward_tabs(&mut self, count: u16) {
+        if self.charge(count.into()) {
+            self.term.move_backward_tabs(count);
+        }
+    }
+    fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) {
+        let bottom = bottom
+            .unwrap_or(self.term.screen_lines())
+            .min(self.term.screen_lines());
+        if top > 0 && top < bottom {
+            self.region = top - 1..bottom;
+        }
+        self.term.set_scrolling_region(top, Some(bottom));
+    }
+    fn clear_screen(&mut self, mode: ClearMode) {
+        if self.charge(self.term.columns().saturating_mul(self.term.screen_lines())) {
+            if matches!(mode, ClearMode::All) {
+                let lines = (0..self.term.screen_lines())
+                    .rev()
+                    .find(|&y| {
+                        (0..self.term.columns())
+                            .any(|x| !self.term.grid()[Line(y as i32)][Column(x)].is_empty())
+                    })
+                    .map_or(0, |y| y + 1);
+                self.loses_history(0, lines);
+            }
+            self.term.clear_screen(mode);
+        }
+    }
+    fn reset_state(&mut self) {
+        self.term.reset_state();
+        self.last_alternate = None;
+        self.mouse = MouseTracking::None;
+        self.encoding = MouseEncoding::Default;
+        self.resized();
+    }
+    fn set_private_mode(&mut self, mode: PrivateMode) {
+        match mode.raw() {
+            47 | 1047 | 1049 => {
+                self.alternate(true);
+                return;
+            }
+            9 => self.mouse = MouseTracking::X10,
+            1000 => self.mouse = MouseTracking::Vt200,
+            1002 => self.mouse = MouseTracking::Drag,
+            1003 => self.mouse = MouseTracking::Any,
+            1006 => self.encoding = MouseEncoding::Sgr,
+            1016 => self.encoding = MouseEncoding::SgrPixels,
+            _ => {}
+        }
+        self.term.set_private_mode(mode);
+    }
+    fn unset_private_mode(&mut self, mode: PrivateMode) {
+        match mode.raw() {
+            47 | 1047 | 1049 => {
+                self.alternate(false);
+                return;
+            }
+            9 | 1000 | 1002 | 1003 => self.mouse = MouseTracking::None,
+            1006 | 1016 => self.encoding = MouseEncoding::Default,
+            _ => {}
+        }
+        self.term.unset_private_mode(mode);
+    }
+    fn report_private_mode(&mut self, mode: PrivateMode) {
+        // Do not advertise keyboard encodings the Host's typed input API does
+        // not implement. Pixel mouse input is likewise rejected by that API.
+        match mode.raw() {
+            9 | 1016 => {
+                let enabled = if mode.raw() == 9 {
+                    self.mouse == MouseTracking::X10
+                } else {
+                    self.encoding == MouseEncoding::SgrPixels
+                };
+                self.events.send_event(Event::PtyWrite(format!(
+                    "\x1b[?{};{}$y",
+                    mode.raw(),
+                    if enabled { 1 } else { 2 }
+                )));
+            }
+            _ => self.term.report_private_mode(mode),
+        }
+    }
+    fn text_area_size_chars(&mut self) {
+        self.events.send_event(Event::PtyWrite(format!(
+            "\x1b[8;{};{}t",
+            self.term.screen_lines(),
+            self.term.columns()
+        )));
+    }
+}
diff --git a/crates/process/tests/conpty.rs b/crates/process/tests/conpty.rs
index 7506b627f8..cede393844 100644
--- a/crates/process/tests/conpty.rs
+++ b/crates/process/tests/conpty.rs
@@ -58,11 +58,13 @@ async fn conpty_console_unicode_resize_exit_259_and_final_output_drain() {
     let (mut child, io) = pty::spawn(command, TerminalSize::new(80, 24).unwrap())
         .await
         .unwrap();
-    until(&io, "ready").await;
+    let mut screen = maka_process::terminal::Screen::new(TerminalSize::new(80, 24).unwrap());
+    screen.write(&until(&io, "ready").await).unwrap();
     child
         .resize(TerminalSize::new(101, 37).unwrap())
         .await
         .unwrap();
+    screen.resize(TerminalSize::new(101, 37).unwrap()).unwrap();
     write(&io, "中文😀\r".as_bytes()).await;
     let (status, output) = tokio::time::timeout(Duration::from_secs(15), async {
         tokio::join!(
@@ -81,6 +83,11 @@ async fn conpty_console_unicode_resize_exit_259_and_final_output_drain() {
     assert!(output.contains("received:中文😀"), "{output:?}");
     assert!(output.contains("size:101x37"), "{output:?}");
     assert!(output.contains("final-frame"), "{output:?}");
+    screen.write(&output).unwrap();
+    let snapshot = screen.snapshot().unwrap();
+    assert!(snapshot.screen.contains("received:中文😀"), "{snapshot:?}");
+    assert!(snapshot.screen.contains("size:101x37"), "{snapshot:?}");
+    assert!(snapshot.screen.contains("final-frame"), "{snapshot:?}");
     child.close().await.unwrap();
     assert!(
         child
diff --git a/crates/process/tests/terminal.rs b/crates/process/tests/terminal.rs
new file mode 100644
index 0000000000..381d2d8504
--- /dev/null
+++ b/crates/process/tests/terminal.rs
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_process::terminal::Screen;
+use maka_runtime::terminal::TerminalSize;
+
+#[cfg(unix)]
+#[tokio::test]
+async fn real_pty_queries_and_mode_aware_input_cross_the_screen_cut() {
+    use maka_process::pty;
+    use maka_runtime::terminal::input::{InputAction, encode_actions};
+    use serde_json::json;
+    use std::time::Duration;
+
+    let expected = "\u{1b}OA中\u{1b}[<0;2;2M\u{1b}[<0;2;2m\r";
+    let hex = expected
+        .bytes()
+        .map(|b| format!("{b:02x}"))
+        .collect::<String>();
+    let mut command = pty::PtyCommand::new("/bin/sh", std::env::current_dir().unwrap());
+    command.args([
+        "-c",
+        &format!(
+            r#"
+        stty raw -echo
+        printf '\033[6n'
+        reply=$(dd bs=1 count=6 2>/dev/null | od -An -tx1 | tr -d ' \n')
+        test "$reply" = 1b5b313b3152 || exit 3
+        printf '\033[?1h\033[?1006h\033[?1002hready'
+        input=$(dd bs=1 count={} 2>/dev/null | od -An -tx1 | tr -d ' \n')
+        test "$input" = {hex} || exit 4
+        printf '\r\nverified'
+        exit 42
+    "#,
+            expected.len()
+        ),
+    ]);
+    let size = TerminalSize::new(80, 24).unwrap();
+    let (mut child, io) = pty::spawn(command, size).await.unwrap();
+    let mut screen = Screen::new(size);
+    let actions = json!([
+        {"type":"key","key":"arrow_up"},
+        {"type":"text","text":"中"},
+        {"type":"mouse","event":"click","button":"left","x":1,"y":1},
+        {"type":"key","key":"enter"}
+    ])
+    .as_array()
+    .unwrap()
+    .iter()
+    .cloned()
+    .map(InputAction::parse)
+    .collect::<Result<Vec<_>, _>>()
+    .unwrap();
+    tokio::time::timeout(Duration::from_secs(5), async {
+        let mut sent = false;
+        let mut bytes = [0; 1024];
+        loop {
+            let count = io.read(&mut bytes).await.unwrap();
+            if count == 0 {
+                break;
+            }
+            // This fixture emits ASCII; native UTF-8 input is checked by the
+            // child byte-for-byte, independent of read chunk boundaries.
+            let replies = screen
+                .write(std::str::from_utf8(&bytes[..count]).unwrap())
+                .unwrap();
+            write_pty(&io, replies.as_bytes()).await;
+            let cut = screen.snapshot().unwrap();
+            if !sent && cut.screen.contains("ready") {
+                let input = encode_actions(&actions, cut.input, cut.size).unwrap();
+                assert_eq!(input, expected);
+                write_pty(&io, input.as_bytes()).await;
+                sent = true;
+            }
+        }
+        assert!(sent);
+        assert_eq!(child.wait().await.unwrap().code(), Some(42));
+        assert!(screen.snapshot().unwrap().screen.contains("verified"));
+    })
+    .await
+    .unwrap();
+
+    async fn write_pty(io: &pty::PtyIo, mut input: &[u8]) {
+        while !input.is_empty() {
+            let count = io.write(input).await.unwrap();
+            assert!(count > 0);
+            input = &input[count..];
+        }
+    }
+}
+
+#[test]
+fn unicode_modes_alternate_screen_reflow_and_history_are_observable() {
+    let mut screen = Screen::new(TerminalSize::new(10, 3).unwrap());
+    assert_eq!(
+        screen.write("hello 中文😀\r\nnext\x1b[6n").unwrap(),
+        "\x1b[3;5R"
+    );
+    let first = screen.snapshot().unwrap();
+    assert!(first.screen.contains("中文"));
+    screen
+        .write("\x1b[?1h\x1b[?1002h\x1b[?1006h\x1b[?25l")
+        .unwrap();
+    let modes = screen.snapshot().unwrap();
+    assert!(!modes.cursor.visible);
+    assert!(modes.input.application_cursor_keys_mode);
+    assert_eq!(
+        modes.input.mouse_tracking_mode,
+        maka_runtime::terminal::MouseTracking::Drag
+    );
+    screen.write("𝄞e\u{301}").unwrap();
+    screen
+        .write("\x1b[?1049h\x1b[HALT\r\nsecond\x1b[?1049l")
+        .unwrap();
+    let restored = screen.snapshot().unwrap();
+    assert!(!restored.alternate_screen);
+    assert_eq!(
+        restored.last_alternate_screen.as_deref(),
+        Some("ALT\nsecond")
+    );
+    assert!(restored.screen.contains("next"));
+    screen.resize(TerminalSize::new(5, 4).unwrap()).unwrap();
+    assert_eq!(screen.snapshot().unwrap().size.cols(), 5);
+    screen.write("\x1bc").unwrap();
+    screen.write(&"line\r\n".repeat(520)).unwrap();
+    let history = screen.snapshot().unwrap();
+    assert!(history.truncated);
+    assert!(history.scrollback.lines().count() <= 500);
+    assert!(history.screen.contains("line"));
+
+    let mut reflow = Screen::new(TerminalSize::new(6, 3).unwrap());
+    reflow.write("abcdefghijkl").unwrap();
+    reflow.resize(TerminalSize::new(4, 4).unwrap()).unwrap();
+    let resized = reflow.snapshot().unwrap();
+    assert_eq!(
+        format!("{}{}", resized.scrollback, resized.screen).replace('\n', ""),
+        "abcdefghijkl"
+    );
+}
+
+#[test]
+fn unicode_cuts_gaps_and_adversarial_streams_preserve_bounded_state() {
+    let size = TerminalSize::new(10, 3).unwrap();
+    let mut screen = Screen::new(size);
+    for c in "中文😀e\u{301}".chars() {
+        screen.write(c.encode_utf8(&mut [0; 4])).unwrap();
+    }
+    assert_eq!(screen.snapshot().unwrap().screen, "中文😀e\u{301}");
+    screen.write("\x1b]52;c;unterminated").unwrap();
+    screen.reset_after_gap();
+    screen.write("after").unwrap();
+    let snapshot = screen.snapshot().unwrap();
+    assert_eq!(snapshot.screen, "after");
+    assert!(snapshot.truncated);
+    assert!(screen.write(&"x".repeat(65537)).is_err());
+    assert_eq!(screen.snapshot().unwrap(), snapshot);
+    for bytes in [
+        b"\x1b]52;c;"
+            .iter()
+            .copied()
+            .chain(std::iter::repeat_n(b'x', 65540))
+            .collect::<Vec<_>>(),
+        format!("x{}", "\u{301}".repeat(100)).into_bytes(),
+        b"x\x1b[65535b\x1b[65535b".to_vec(),
+        b"\x1b[65535S".to_vec(),
+    ] {
+        let mut screen = Screen::new(size);
+        let mut failed = false;
+        for chunk in bytes.chunks(1024) {
+            if screen.write(std::str::from_utf8(chunk).unwrap()).is_err() {
+                failed = true;
+                break;
+            }
+        }
+        assert!(failed, "unbounded sequence accepted");
+        assert!(screen.snapshot().is_err());
+    }
+    // Suppressed OSC capabilities cannot emit clipboard data or grow a title stack.
+    let mut safe = Screen::new(size);
+    assert_eq!(
+        safe.write("\x1b]52;c;aGVsbG8=\x07\x1b]52;c;?\x07OK\x1b[5n")
+            .unwrap(),
+        "\x1b[0n"
+    );
+    assert_eq!(safe.snapshot().unwrap().screen, "OK");
+
+    // Large historical grapheme clusters must not crowd the current viewport
+    // out of the bounded snapshot.
+    let mut history = Screen::new(TerminalSize::new(240, 4).unwrap());
+    let row = format!("{}\r\n", format!("x{}", "\u{301}".repeat(32)).repeat(240));
+    for index in 0..150 {
+        history
+            .write(&row)
+            .unwrap_or_else(|error| panic!("row {index}: {error}"));
+    }
+    history.write("current-frame").unwrap();
+    let snapshot = history.snapshot().unwrap();
+    assert!(snapshot.truncated);
+    assert!(snapshot.screen.contains("current-frame"));
+    assert!(snapshot.screen.len() + snapshot.scrollback.len() <= 2 * 1024 * 1024);
+}
diff --git a/crates/runtime-host/src/execution.rs b/crates/runtime-host/src/execution.rs
index a639d661bb..bc02c2b3b6 100644
--- a/crates/runtime-host/src/execution.rs
+++ b/crates/runtime-host/src/execution.rs
@@ -119,10 +119,9 @@ impl Executions {
             plugin_calls,
             oauth: crate::oauth::Authority::new(workers.clone(), shutdown.clone()),
             controllers: Default::default(),
-            shells: Arc::new(crate::shell::ShellResources::with_runtime(
+            shells: Arc::new(crate::shell::ShellResources::new(
                 log.clone(),
                 shutdown.clone(),
-                runtime.clone(),
             )),
             engine: Engine::new(
                 log.clone(),
diff --git a/crates/runtime-host/src/shell.rs b/crates/runtime-host/src/shell.rs
index 47fd7120d5..c38a0e4c2e 100644
--- a/crates/runtime-host/src/shell.rs
+++ b/crates/runtime-host/src/shell.rs
@@ -50,7 +50,7 @@ pub enum ShellError {
     #[error(transparent)]
     Io(#[from] std::io::Error),
     #[error(transparent)]
-    Screen(#[from] maka_js_runtime::terminal::ScreenError),
+    Screen(#[from] maka_process::terminal::ScreenError),
     #[error(transparent)]
     Process(#[from] maka_runtime::tools::ToolError),
 }
@@ -60,10 +60,9 @@ type Key = (String, String);
 type Active = Arc<Mutex<HashMap<Key, ShellHandle>>>;
 pub(super) type Update = Option<std::result::Result<Arc<ShellRun>, Arc<ShellError>>>;
 
-/// At most eight native PTYs; parser objects share the Host's trusted JS worker.
+/// At most eight native PTYs; each worker owns its Rust terminal parser.
 /// This is an internal execution API, not a client authorization boundary.
 pub struct ShellResources {
-    runtime: maka_js_runtime::trusted::TrustedRuntime,
     log: Arc<EventLog>,
     active: Active,
     output_changes: watch::Sender<()>,
@@ -74,16 +73,7 @@ pub struct ShellResources {
 
 impl ShellResources {
     pub fn new(log: Arc<EventLog>, host_drain: CancellationToken) -> Self {
-        Self::with_runtime(log, host_drain, Default::default())
-    }
-
-    pub fn with_runtime(
-        log: Arc<EventLog>,
-        host_drain: CancellationToken,
-        runtime: maka_js_runtime::trusted::TrustedRuntime,
-    ) -> Self {
         Self {
-            runtime,
             log,
             active: Arc::default(),
             output_changes: watch::channel(()).0,
@@ -147,7 +137,6 @@ impl ShellResources {
         };
         let log = self.log.clone();
         let host_drain = self.host_drain.clone();
-        let shared_runtime = self.runtime.clone();
         active.insert(key, handle.clone());
         // Release the map before spawn: failure drops Residency on this thread.
         drop(active);
@@ -160,7 +149,6 @@ impl ShellResources {
                         .build()?;
                     runtime.block_on(
                         worker::Worker {
-                            runtime: shared_runtime,
                             log: log.clone(),
                             record,
                             commands: receiver,
diff --git a/crates/runtime-host/src/shell/terminal.rs b/crates/runtime-host/src/shell/terminal.rs
index f0100f31cb..fed83ff1cd 100644
--- a/crates/runtime-host/src/shell/terminal.rs
+++ b/crates/runtime-host/src/shell/terminal.rs
@@ -22,8 +22,8 @@ use super::{
     control::{ControlError, WriteReceipt},
     output::Output,
 };
-use maka_js_runtime::terminal::Screen;
 use maka_process::pty::PtyIo;
+use maka_process::terminal::Screen;
 use maka_runtime::terminal::{TerminalScreen, TerminalSize};
 use std::collections::VecDeque;
 use std::time::Duration;
@@ -38,60 +38,57 @@ pub(super) struct Terminal {
     output: Output,
 }
 impl Terminal {
-    pub async fn new(
-        runtime: maka_js_runtime::trusted::TrustedRuntime,
-        size: TerminalSize,
-        output: Output,
-    ) -> Result<Self> {
-        let mut screen = Screen::with_runtime(runtime, size)?;
-        let snapshot = match screen.snapshot().await {
-            Ok(snapshot) => snapshot,
-            Err(error) => {
-                screen.close().await;
-                return Err(error.into());
-            }
-        };
-        Ok(Self {
-            screen: Some(screen),
-            snapshot,
+    pub fn new(size: TerminalSize, output: Output) -> Self {
+        Self {
+            screen: Some(Screen::new(size)),
+            snapshot: TerminalScreen::new(size),
             decoder: Decoder::default(),
             writes: VecDeque::new(),
             output,
-        })
+        }
     }
 
-    pub async fn output(&mut self, bytes: &[u8], eof: bool) -> Result<()> {
+    pub fn output(&mut self, bytes: &[u8], eof: bool) -> Result<()> {
         let text = self.decoder.decode(bytes, eof);
         let Some(screen) = &mut self.screen else {
             return Ok(());
         };
-        match screen.write(&text).await {
-            Ok(cut) => {
-                self.snapshot = cut.screen;
+        match screen.write(&text) {
+            Ok(replies) => {
+                self.snapshot = screen.snapshot()?;
                 // Observational stream, not a durable outcome. Publish only a
                 // completed parser cut, including final output during drain.
                 self.output.publish(&text)?;
-                if !cut.replies.is_empty() {
+                if !replies.is_empty() {
+                    let pending: usize = self
+                        .writes
+                        .iter()
+                        .map(|write| write.remaining().len())
+                        .sum();
+                    if pending.saturating_add(replies.len()) > 1024 * 1024 {
+                        self.close();
+                        return Err(ShellError::Rejected("terminal reply queue exceeds 1 MiB"));
+                    }
                     self.writes
-                        .push_back(PendingWrite::new(cut.replies, None, false, false));
+                        .push_back(PendingWrite::new(replies, None, false, false));
                 }
                 Ok(())
             }
             Err(error) => {
-                // Last good cut survives. Await disposal before the native
-                // worker can finish and release resource residency.
-                self.close().await;
+                // Preserve the last good cut and release parser state immediately.
+                self.close();
                 Err(error.into())
             }
         }
     }
 
-    pub async fn resize(&mut self, size: TerminalSize) -> Result<()> {
+    pub fn resize(&mut self, size: TerminalSize) -> Result<()> {
         let screen = self
             .screen
             .as_mut()
             .ok_or(ShellError::Rejected("terminal parser unavailable"))?;
-        self.snapshot = screen.resize(size).await?;
+        screen.resize(size)?;
+        self.snapshot = screen.snapshot()?;
         self.output.resize(size);
         Ok(())
     }
@@ -105,10 +102,8 @@ impl Terminal {
         }
     }
 
-    pub async fn close(&mut self) {
-        if let Some(screen) = self.screen.take() {
-            screen.close().await;
-        }
+    pub fn close(&mut self) {
+        self.screen = None;
     }
 }
 
@@ -218,7 +213,7 @@ pub(super) async fn drain(
                 Err(error) => { io.discard_output()?; return Err(error.into()); }
             }
         };
-        if let Err(error) = terminal.output(&buffer[..count], count == 0).await {
+        if let Err(error) = terminal.output(&buffer[..count], count == 0) {
             failure.get_or_insert(error);
         }
         // After root exit replies have no consumer and are not input effects.
diff --git a/crates/runtime-host/src/shell/worker.rs b/crates/runtime-host/src/shell/worker.rs
index 7eae674961..1c2714f588 100644
--- a/crates/runtime-host/src/shell/worker.rs
+++ b/crates/runtime-host/src/shell/worker.rs
@@ -42,7 +42,6 @@ use tokio::sync::{mpsc, watch};
 use tokio_util::sync::CancellationToken;
 
 pub(super) struct Worker {
-    pub runtime: maka_js_runtime::trusted::TrustedRuntime,
     pub log: Arc<EventLog>,
     pub record: ShellRun,
     pub commands: mpsc::Receiver<Control>,
@@ -55,9 +54,9 @@ pub(super) struct Worker {
 }
 impl Worker {
     pub async fn run(mut self, command: PtyCommand, size: TerminalSize) -> Result<()> {
-        let mut terminal = Terminal::new(self.runtime.clone(), size, self.output.clone()).await?;
+        let mut terminal = Terminal::new(size, self.output.clone());
         let result = self.run_terminal(command, size, &mut terminal).await;
-        terminal.close().await;
+        terminal.close();
         result
     }
 
@@ -196,8 +195,8 @@ impl Worker {
                             None => break,
                         }
                     }
-                    // Parser cuts are awaited to completion outside select.
-                    terminal.output(&buffer[..count], eof).await?;
+                    // Complete the parser cut before persisting or handling controls.
+                    terminal.output(&buffer[..count], eof)?;
                     self.persist(terminal, None).await?;
                     if let Some(error) = failure { return Err(error.into()); }
                 }
diff --git a/crates/runtime-host/src/shell/worker/control.rs b/crates/runtime-host/src/shell/worker/control.rs
index 611aa402dd..3f67a57779 100644
--- a/crates/runtime-host/src/shell/worker/control.rs
+++ b/crates/runtime-host/src/shell/worker/control.rs
@@ -66,7 +66,7 @@ impl Worker {
                 }
                 resized = true;
                 if resize_changed {
-                    terminal.resize(size).await?;
+                    terminal.resize(size)?;
                 }
                 self.persist(terminal, None).await
             }
diff --git a/docs/rust-runtime.md b/docs/rust-runtime.md
index 3a17d67f38..452de7190a 100644
--- a/docs/rust-runtime.md
+++ b/docs/rust-runtime.md
@@ -254,7 +254,8 @@ user/project content paths stay separate from private journals.
 - Rust owns storage, network routing, tools and native process/PTY lifetimes.
   Plugins enter through catalogs and scoped Host services, sharing
   journaled effects, permissions and draining rather than replacing the Engine.
-  One lazy, long-lived V8 serves concurrent model requests and terminal parsers.
+  One lazy, long-lived V8 serves concurrent model requests. Native PTY workers
+  own their Alacritty terminal state without JavaScript or cross-runtime messages.
   Code Mode cells use separate short-lived isolates. Count and byte limits provide
   backpressure; V8 heap limits are not process-memory containment.
 - Code Mode budgets cumulative VM execution, excluding asynchronous tool waits
diff --git a/docs/rust-runtime.zh-CN.md b/docs/rust-runtime.zh-CN.md
index 20cd0c49eb..93895938c3 100644
--- a/docs/rust-runtime.zh-CN.md
+++ b/docs/rust-runtime.zh-CN.md
@@ -194,7 +194,8 @@ Host 不可连接不代表进程已停止。`host logs --root-id <rootId>` 返
   Client bundle 提供管理、选择器和草稿建议;Desktop 提供目标绑定的 Slot 和授权原生文件操作。
   停用后新显式引用失败,普通聊天与已接受回执不受影响。Plan 模式执行属于独立领域,尚未实现。
 - Rust 管理存储、网络路由、工具和原生进程/PTY。一个惰性启动的长期 V8 并发处理模型
-  请求与终端解析;Code Mode 使用独立短生命周期 isolate。数量与字节限制提供背压,
+  请求;原生 PTY worker 直接拥有 Alacritty 终端状态,无需 JavaScript 或跨 runtime 消息。
+  Code Mode 使用独立短生命周期 isolate。数量与字节限制提供背压,
   V8 heap 限制不等于进程内存隔离。
 - 插件通过目录注册和有作用域的 Host 服务接入,复用日志、权限与排空机制,不替换 Engine。
 - Code Mode 限制累计 VM 执行时间,不计异步工具等待或收尾时间。
diff --git a/scripts/asf-license-headers.mjs b/scripts/asf-license-headers.mjs
index 60d7c5f948..aec09b0c65 100644
--- a/scripts/asf-license-headers.mjs
+++ b/scripts/asf-license-headers.mjs
@@ -339,7 +339,6 @@ const provenanceMarkers = [
  */
 const reviewedProvenance = new Map([
   [
-    'crates/js-runtime/TERMINAL_SOURCE.md',
     'Original Maka provenance documentation quoting MIT notices for the locked xterm bundle; the upstream code retains its MIT license.',
   ],
   [
diff --git a/scripts/rust/bundle-terminal.mjs b/scripts/rust/bundle-terminal.mjs
deleted file mode 100644
index 52ef153b82..0000000000
--- a/scripts/rust/bundle-terminal.mjs
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { createRequire } from 'node:module';
-import { readFileSync } from 'node:fs';
-import { resolve, dirname } from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
-const dependencies = process.env.MAKA_JS_DEPS || root;
-const require = createRequire(resolve(dependencies, 'package.json'));
-const { build } = require('esbuild');
-const locked = JSON.parse(readFileSync(resolve(root, 'package-lock.json'), 'utf8'));
-const modules = ['@xterm/headless', '@xterm/addon-unicode11'];
-for (const name of modules) {
-  const installed = require(`${name}/package.json`);
-  if (installed.version !== locked.packages[`node_modules/${name}`]?.version) {
-    throw new Error(`Terminal dependency does not match repository lockfile: ${name}`);
-  }
-}
-await build({
-  entryPoints: [resolve(root, 'crates/js-runtime/terminal.js')],
-  outfile: resolve(process.argv[2], 'terminal.js'),
-  bundle: true,
-  platform: 'browser',
-  format: 'iife',
-  banner: {
-    js: `/*\n${readFileSync(resolve(root, 'crates/js-runtime/TERMINAL_SOURCE.md'), 'utf8')}\n*/`,
-  },
-  plugins: [
-    {
-      name: 'locked-terminal-dependencies',
-      setup(build) {
-        build.onResolve({ filter: /^@xterm\// }, ({ path }) => ({ path: require.resolve(path) }));
-      },
-    },
-  ],
-});
diff --git a/tests/fixtures/terminal-screen-oracle.mjs b/tests/fixtures/terminal-screen-oracle.mjs
deleted file mode 100644
index fee9f4d191..0000000000
--- a/tests/fixtures/terminal-screen-oracle.mjs
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { createRequire } from 'node:module';
-import { resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { withSourceModule } from '../support/source.mjs';
-
-const root = fileURLToPath(new URL('../../', import.meta.url));
-const require = createRequire(resolve(process.env.MAKA_JS_DEPS || root, 'package.json'));
-const { Terminal } = require('@xterm/headless');
-const { Unicode11Addon } = require('@xterm/addon-unicode11');
-let input = '';
-for await (const chunk of process.stdin) input += chunk;
-await withSourceModule(
-  'packages/runtime/src/pty-screen-collector.ts',
-  async ({ PtyScreenCollector }) => {
-    const observations = [];
-    let replies = '',
-      failure;
-    const collector = new PtyScreenCollector({
-      stack: { Terminal, Unicode11Addon },
-      cols: 10,
-      rows: 3,
-      onProtocolReply: (data) => {
-        replies += data;
-      },
-      onDirty() {},
-      onFailure: (error) => {
-        failure = error;
-      },
-    });
-    try {
-      for (const action of JSON.parse(input)) {
-        replies = '';
-        if (action.write !== undefined) collector.accept(action.write);
-        else await collector.mutateAtCut(() => collector.resize(action.cols, action.rows));
-        const { output } = await collector.snapshotAtCut();
-        if (failure) throw failure;
-        const { mode, cols, rows, redacted, ...screen } = output;
-        if (mode !== 'pty' || redacted)
-          throw new Error('Oracle fixture must remain unredacted PTY data');
-        const { cols: _cols, rows: _rows, ...modes } = collector.currentInputState();
-        observations.push({ screen: { ...screen, size: { cols, rows }, input: modes }, replies });
-      }
-      process.stdout.write(JSON.stringify(observations));
-    } finally {
-      collector.dispose();
-    }
-  },
-);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org