diff --git a/README.ko.md b/README.ko.md
index fc43ef4..200e19c 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -19,7 +19,7 @@ CodeSpace는 외부 코딩 에이전트가 작업 공간의 파일을 읽고 수
| --- | --- |
| 실행 환경과 파일 확인 | `workspace_info`, `find`, `read` |
| 패치 적용과 기록된 상태 조회 | `apply_patch`, `operation_status` |
-| 프로세스 실행과 제어 | `exec_command`, `read_process`, `process_status`, `write_stdin`, `terminate_process` |
+| 프로세스 실행과 제어 | `exec_command`, `read_process`, `process_status`, `process_resize`, `write_stdin`, `terminate_process` |
| 작업과 사용자 추가 지시 관리 | `work_open`, `steer_status`, `steer_claim_next`, `steer_complete`, `work_finish` |
두 전송 방식에서 같은 도구를 사용할 수 있습니다. HTTP `/inbox` API는 사용자용 클라이언트가 지시 초안을 관리하는 JSON API입니다. 브라우저에서 사용하는 받은 편지함 화면은 제공하지 않습니다.
@@ -37,8 +37,9 @@ CodeSpace는 외부 코딩 에이전트가 작업 공간의 파일을 읽고 수
- 프로세스 종료는 `process_status`로 판정하세요. `read_process`의 EOF는 성공이 아닙니다.
- 프로세스 출력의 보관 크기가 제한되어 있습니다. `output_lost`가 참이면 보관 창이 전체 로그가 아닙니다.
+- 실행 중인 PTY 크기는 `process_resize`로 바꿉니다. spawn 크기는 24×80이며 `tty_size`는 `exec_command` 인자가 아닙니다.
- 명령이 실행 중이면 같은 작업 공간에서 다른 명령이나 패치를 실행할 수 없습니다. 개발 서버를 켜 둔 채 같은 작업 공간을 수정하는 흐름에는 제약이 있습니다.
-- 서버를 재시작하면 프로세스 핸들이 사라집니다. 패치 작업 기록은 데이터베이스 경로를 설정한 경우에만 유지됩니다.
+- 서버를 재시작하면 프로세스 핸들이 사라집니다. HTTP/MCP 클라이언트 끊김은 프로세스를 죽이지 않습니다. UDS worker 손실과 게이트웨이 종료는 죽입니다. 패치 작업 기록은 데이터베이스 경로를 설정한 경우에만 유지됩니다.
- 컨테이너 실행과 완전한 OAuth 서버는 구현되어 있지 않습니다. 실제 ChatGPT 계정 연결도 아직 검증되지 않았습니다.
diff --git a/README.md b/README.md
index b2b7ff6..77befb9 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Then use the [Agent Loop integration guide](docs/agent-integration.md) for the r
| --- | --- |
| Inspect the environment and files | `workspace_info`, `find`, `read` |
| Apply a patch and retrieve its recorded state | `apply_patch`, `operation_status` |
-| Run and control a process | `exec_command`, `read_process`, `process_status`, `write_stdin`, `terminate_process` |
+| Run and control a process | `exec_command`, `read_process`, `process_status`, `process_resize`, `write_stdin`, `terminate_process` |
| Track a logical job and queued user instructions | `work_open`, `steer_status`, `steer_claim_next`, `steer_complete`, `work_finish` |
Both transports expose the same tools. The HTTP `/inbox` API lets a user-facing client manage instruction drafts; it is a JSON API, not a browser inbox application.
@@ -35,8 +35,9 @@ For agent integrations, account for these limits:
- Judge process exit with `process_status`. EOF from `read_process` is not success.
- Process output is bounded. `output_lost` means the retained window is not the complete log.
+- Resize a running PTY with `process_resize`. Spawn size stays 24×80; `tty_size` is not an `exec_command` argument.
- A live command blocks another command or patch in the same workspace. A development server cannot remain running while that workspace is patched.
-- Process handles do not survive server restart. Patch-operation records persist only when a database path is configured.
+- Process handles do not survive server restart. HTTP/MCP client disconnect does not kill them. UDS worker loss and gateway shutdown do. Patch-operation records persist only when a database path is configured.
- Container dispatch and a full OAuth server are not implemented. A live ChatGPT account connection remains unverified.
## License
diff --git a/crates/codex-runtime/Cargo.lock b/crates/codex-runtime/Cargo.lock
index ce26833..b2c5b24 100644
--- a/crates/codex-runtime/Cargo.lock
+++ b/crates/codex-runtime/Cargo.lock
@@ -761,6 +761,8 @@ dependencies = [
name = "codespace-codex-runtime"
version = "0.6.0"
dependencies = [
+ "codespace-domain",
+ "codespace-policy",
"codespace-runner",
"codex-process-hardening",
"codex-uds",
diff --git a/crates/codex-runtime/Cargo.toml b/crates/codex-runtime/Cargo.toml
index 22bbe1b..89a0014 100644
--- a/crates/codex-runtime/Cargo.toml
+++ b/crates/codex-runtime/Cargo.toml
@@ -28,6 +28,8 @@ tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "4fffad30fe373adbdcffab9545e9e9bf4f2fc19f" }
[dev-dependencies]
+codespace-domain = { path = "../domain" }
+codespace-policy = { path = "../policy" }
codespace-runner = { path = "../runner" }
serde_json = "1"
tempfile = "3"
diff --git a/crates/codex-runtime/tests/runtime_binary.rs b/crates/codex-runtime/tests/runtime_binary.rs
index be38151..e87fa83 100644
--- a/crates/codex-runtime/tests/runtime_binary.rs
+++ b/crates/codex-runtime/tests/runtime_binary.rs
@@ -3,10 +3,14 @@
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
+use std::sync::Arc;
use std::time::Duration;
+use codespace_domain::{ProcessId, Profile, WorkspaceId};
+use codespace_policy::Workspace;
use codespace_runner::{
- read_frame, write_frame, RunnerOp, RunnerOpResult, WireEnvelope, WIRE_PROTOCOL,
+ read_frame, write_frame, Runner, RunnerExecRequest, RunnerOp, RunnerOpResult, UdsRunner,
+ WireEnvelope, WIRE_PROTOCOL,
};
use tokio::net::UnixStream;
@@ -134,8 +138,70 @@ async fn hello_on_live_socket() {
match parsed.result {
Some(RunnerOpResult::Hello { protocol }) => {
assert_eq!(protocol, WIRE_PROTOCOL);
- assert_eq!(protocol, 4);
+ assert_eq!(protocol, 5);
}
other => panic!("unexpected hello {other:?}"),
}
}
+
+#[tokio::test]
+async fn worker_socket_drop_terminates_owned_child() {
+ let dir = tempfile::tempdir().unwrap();
+ let socket = runner_socket(dir.path());
+ let mut worker = spawn_worker(&socket).await;
+ let stream = wait_connect(&socket).await;
+ let runner = UdsRunner::from_stream(stream, Arc::new(|_| {}));
+ runner.handshake().await.expect("hello");
+
+ let ws_dir = tempfile::tempdir().unwrap();
+ let beat = ws_dir.path().join("heartbeat");
+ let ws = Workspace::new(
+ WorkspaceId("demo".into()),
+ ws_dir.path().to_path_buf(),
+ Profile::WorkspaceWrite,
+ );
+ let req = RunnerExecRequest::for_host(
+ vec![
+ "/bin/sh".into(),
+ "-c".into(),
+ format!(
+ "while :; do date +%s > '{}'; sleep 0.1; done",
+ beat.display()
+ ),
+ ],
+ ProcessId("proc-uds-disconnect".into()),
+ Profile::WorkspaceWrite,
+ );
+ runner.exec(&ws, req).await.expect("exec heartbeat");
+
+ let mut last = String::new();
+ for _ in 0..50 {
+ if let Ok(raw) = std::fs::read_to_string(&beat) {
+ if !raw.trim().is_empty() {
+ last = raw;
+ break;
+ }
+ }
+ tokio::time::sleep(Duration::from_millis(40)).await;
+ }
+ assert!(
+ !last.is_empty(),
+ "child should write a heartbeat before disconnect"
+ );
+
+ runner.close().await;
+ drop(runner);
+ let _ = tokio::time::timeout(Duration::from_secs(5), worker.wait())
+ .await
+ .expect("worker should exit after socket close")
+ .expect("wait worker");
+
+ tokio::time::sleep(Duration::from_millis(400)).await;
+ let after = std::fs::read_to_string(&beat).unwrap_or_default();
+ tokio::time::sleep(Duration::from_millis(400)).await;
+ let later = std::fs::read_to_string(&beat).unwrap_or_default();
+ assert_eq!(
+ after, later,
+ "owned child must stop updating heartbeat after worker disconnect (after={after:?} later={later:?})"
+ );
+}
diff --git a/crates/domain/src/error.rs b/crates/domain/src/error.rs
index e8cf091..98e2209 100644
--- a/crates/domain/src/error.rs
+++ b/crates/domain/src/error.rs
@@ -24,6 +24,8 @@ pub enum ErrorCode {
OperationKeyConflict,
OperationNotFound,
ProcessNotFound,
+ ProcessNotTty,
+ ProcessNotRunning,
OutputLimit,
Timeout,
WorkNotFound,
@@ -108,6 +110,10 @@ mod tests {
assert_eq!(json, "\"INVALID_COMMAND\"");
let json = serde_json::to_string(&ErrorCode::ProcessSpawnFailed).unwrap();
assert_eq!(json, "\"PROCESS_SPAWN_FAILED\"");
+ let json = serde_json::to_string(&ErrorCode::ProcessNotTty).unwrap();
+ assert_eq!(json, "\"PROCESS_NOT_TTY\"");
+ let json = serde_json::to_string(&ErrorCode::ProcessNotRunning).unwrap();
+ assert_eq!(json, "\"PROCESS_NOT_RUNNING\"");
let json = serde_json::to_string(&ErrorCode::FileNotFound).unwrap();
assert_eq!(json, "\"FILE_NOT_FOUND\"");
let json = serde_json::to_string(&ErrorCode::PathNotDirectory).unwrap();
diff --git a/crates/domain/src/execution.rs b/crates/domain/src/execution.rs
index 12ec14a..54a9d5d 100644
--- a/crates/domain/src/execution.rs
+++ b/crates/domain/src/execution.rs
@@ -38,6 +38,40 @@ pub struct EffectivePermissionInfo {
pub exec: bool,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProcessLifetimeOwner {
+ Runner,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProcessDisconnectAction {
+ KeepRunning,
+ Terminate,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProcessRestartRecovery {
+ None,
+}
+
+/// Advertised process-lifetime contract. The runner instance owns the
+/// process, not the MCP session.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct ProcessLifetimeInfo {
+ pub owner: ProcessLifetimeOwner,
+ /// Streamable HTTP session end, request cancel, or MCP client detach.
+ pub client_disconnect: ProcessDisconnectAction,
+ /// Gateway↔worker UDS loss.
+ pub runner_disconnect: ProcessDisconnectAction,
+ /// Gateway process exit, including stdio EOF.
+ pub gateway_shutdown: ProcessDisconnectAction,
+ /// Restart does not restore `process_id`. Lost spawn responses are not discovered.
+ pub restart_recovery: ProcessRestartRecovery,
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PtyCapabilityInfo {
pub supported: bool,
@@ -62,6 +96,7 @@ pub struct ProcessCapabilityInfo {
/// master stream.
pub output_combined: bool,
pub tty: PtyCapabilityInfo,
+ pub lifetime: ProcessLifetimeInfo,
}
/// Static eligibility of a file operation in this workspace.
@@ -193,7 +228,14 @@ impl WorkspaceExecutionInfo {
default: false,
initial_rows: PTY_INITIAL_ROWS,
initial_cols: PTY_INITIAL_COLS,
- resize_supported: false,
+ resize_supported: true,
+ },
+ lifetime: ProcessLifetimeInfo {
+ owner: ProcessLifetimeOwner::Runner,
+ client_disconnect: ProcessDisconnectAction::KeepRunning,
+ runner_disconnect: ProcessDisconnectAction::Terminate,
+ gateway_shutdown: ProcessDisconnectAction::Terminate,
+ restart_recovery: ProcessRestartRecovery::None,
},
}),
};
@@ -305,8 +347,22 @@ mod tests {
assert!(!caps.tty.default);
assert_eq!(caps.tty.initial_rows, 24);
assert_eq!(caps.tty.initial_cols, 80);
- assert!(!caps.tty.resize_supported);
+ assert!(caps.tty.resize_supported);
assert!(caps.output_combined);
+ assert_eq!(caps.lifetime.owner, ProcessLifetimeOwner::Runner);
+ assert_eq!(
+ caps.lifetime.client_disconnect,
+ ProcessDisconnectAction::KeepRunning
+ );
+ assert_eq!(
+ caps.lifetime.runner_disconnect,
+ ProcessDisconnectAction::Terminate
+ );
+ assert_eq!(
+ caps.lifetime.gateway_shutdown,
+ ProcessDisconnectAction::Terminate
+ );
+ assert_eq!(caps.lifetime.restart_recovery, ProcessRestartRecovery::None);
assert_eq!(exec.isolation.command_sandbox, CommandSandboxState::None);
assert_eq!(exec.network.policy, NetworkPolicyState::Restricted);
assert_eq!(exec.network.enforcement, NetworkEnforcementState::None);
@@ -325,6 +381,30 @@ mod tests {
assert_eq!(json["network"]["enforcement"], "none");
assert_eq!(json["process"]["available"], true);
assert_eq!(json["process"]["capabilities"]["tty"]["supported"], true);
+ assert_eq!(
+ json["process"]["capabilities"]["tty"]["resize_supported"],
+ true
+ );
+ assert_eq!(
+ json["process"]["capabilities"]["lifetime"]["owner"],
+ "runner"
+ );
+ assert_eq!(
+ json["process"]["capabilities"]["lifetime"]["client_disconnect"],
+ "keep_running"
+ );
+ assert_eq!(
+ json["process"]["capabilities"]["lifetime"]["runner_disconnect"],
+ "terminate"
+ );
+ assert_eq!(
+ json["process"]["capabilities"]["lifetime"]["gateway_shutdown"],
+ "terminate"
+ );
+ assert_eq!(
+ json["process"]["capabilities"]["lifetime"]["restart_recovery"],
+ "none"
+ );
}
#[test]
diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs
index 5fd5ef3..a5c2ef3 100644
--- a/crates/domain/src/lib.rs
+++ b/crates/domain/src/lib.rs
@@ -24,7 +24,8 @@ pub use error::{
pub use execution::{
ClientEnvironmentKind, CommandSandboxState, EffectivePermissionInfo, EnvironmentExecutionInfo,
FileExecutionInfo, FileOperationInfo, IsolationInfo, NetworkEnforcementState, NetworkInfo,
- NetworkPolicyState, ProcessCapabilityInfo, ProcessExecutionInfo, PtyCapabilityInfo,
+ NetworkPolicyState, ProcessCapabilityInfo, ProcessDisconnectAction, ProcessExecutionInfo,
+ ProcessLifetimeInfo, ProcessLifetimeOwner, ProcessRestartRecovery, PtyCapabilityInfo,
WorkspaceExecutionInfo, WorkspaceSerializationInfo, PTY_INITIAL_COLS, PTY_INITIAL_ROWS,
};
pub use files::{FindParams, FindResult, ReadParams, ReadResult};
@@ -36,15 +37,16 @@ pub use patch::{
OperationEventName, OperationKind, OperationStatusParams, OperationStatusResult, PatchStatus,
};
pub use process::{
- ExecCommandParams, ExecCommandResult, ExecDispatchStatus, ProcessState, ProcessStatusParams,
- ProcessStatusResult, ProcessTermination, ReadProcessParams, ReadProcessResult,
- TerminateProcessParams, WriteStdinParams,
+ ExecCommandParams, ExecCommandResult, ExecDispatchStatus, ProcessResizeParams,
+ ProcessResizeResult, ProcessState, ProcessStatusParams, ProcessStatusResult,
+ ProcessTermination, ReadProcessParams, ReadProcessResult, TerminateProcessParams,
+ WriteStdinParams,
};
pub use profile::Profile;
pub use tools::{
LIVE_TOOLS, SERVER_NAME, SERVER_VERSION, TOOL_APPLY_PATCH, TOOL_APPROVAL_CREATE,
TOOL_APPROVAL_RESOLVE, TOOL_EXEC_COMMAND, TOOL_FIND, TOOL_OPERATION_RESUME,
- TOOL_OPERATION_STATUS, TOOL_PROCESS_STATUS, TOOL_READ, TOOL_READ_PROCESS,
+ TOOL_OPERATION_STATUS, TOOL_PROCESS_RESIZE, TOOL_PROCESS_STATUS, TOOL_READ, TOOL_READ_PROCESS,
TOOL_STEER_CLAIM_NEXT, TOOL_STEER_COMPLETE, TOOL_STEER_STATUS, TOOL_TERMINATE_PROCESS,
TOOL_WORKSPACE_INFO, TOOL_WORK_FINISH, TOOL_WORK_OPEN, TOOL_WRITE_STDIN, TRANSPORT_STDIO,
TRANSPORT_STREAMABLE_HTTP, W03_EXPOSED_TOOLS,
diff --git a/crates/domain/src/process.rs b/crates/domain/src/process.rs
index 3f9d972..ff369a9 100644
--- a/crates/domain/src/process.rs
+++ b/crates/domain/src/process.rs
@@ -116,6 +116,22 @@ pub struct TerminateProcessParams {
pub process_id: ProcessId,
}
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct ProcessResizeParams {
+ pub process_id: ProcessId,
+ pub rows: u16,
+ pub cols: u16,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct ProcessResizeResult {
+ pub ok: bool,
+ pub rows: u16,
+ pub cols: u16,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub coordination: Option,
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -302,4 +318,24 @@ mod tests {
assert!(!dumped.contains("tty_size"), "{dumped}");
assert!(!dumped.contains("signal"), "{dumped}");
}
+
+ #[test]
+ fn process_resize_params_require_rows_and_cols() {
+ let schema = serde_json::to_value(schemars::schema_for!(ProcessResizeParams)).unwrap();
+ let dumped = schema.to_string();
+ assert!(dumped.contains("process_id"), "{dumped}");
+ assert!(dumped.contains("rows"), "{dumped}");
+ assert!(dumped.contains("cols"), "{dumped}");
+ assert!(!dumped.contains("tty_size"), "{dumped}");
+ let json = serde_json::to_value(ProcessResizeResult {
+ ok: true,
+ rows: 40,
+ cols: 120,
+ coordination: None,
+ })
+ .unwrap();
+ assert_eq!(json["ok"], true);
+ assert_eq!(json["rows"], 40);
+ assert_eq!(json["cols"], 120);
+ }
}
diff --git a/crates/domain/src/tools.rs b/crates/domain/src/tools.rs
index d6945a6..eb121ae 100644
--- a/crates/domain/src/tools.rs
+++ b/crates/domain/src/tools.rs
@@ -9,6 +9,7 @@ pub const TOOL_EXEC_COMMAND: &str = "exec_command";
pub const TOOL_WRITE_STDIN: &str = "write_stdin";
pub const TOOL_READ_PROCESS: &str = "read_process";
pub const TOOL_PROCESS_STATUS: &str = "process_status";
+pub const TOOL_PROCESS_RESIZE: &str = "process_resize";
pub const TOOL_TERMINATE_PROCESS: &str = "terminate_process";
pub const TOOL_OPERATION_STATUS: &str = "operation_status";
pub const TOOL_WORK_OPEN: &str = "work_open";
@@ -35,6 +36,7 @@ pub const LIVE_TOOLS: &[&str] = &[
TOOL_WRITE_STDIN,
TOOL_READ_PROCESS,
TOOL_PROCESS_STATUS,
+ TOOL_PROCESS_RESIZE,
TOOL_TERMINATE_PROCESS,
TOOL_WORK_OPEN,
TOOL_STEER_STATUS,
diff --git a/crates/pty/src/lib.rs b/crates/pty/src/lib.rs
index 539791c..020cdea 100644
--- a/crates/pty/src/lib.rs
+++ b/crates/pty/src/lib.rs
@@ -1,5 +1,5 @@
//! Isolated PTY adapter. Wraps `codex-utils-pty` and exposes only
-//! CodeSpace-owned types. Not an MCP tool. Resize stays off this API.
+//! CodeSpace-owned types. Not an MCP tool.
use std::collections::HashMap;
use std::path::Path;
@@ -34,6 +34,13 @@ impl PtySession {
self.handle.has_exited()
}
+ /// Change the PTY size in character cells. Codex types stay inside.
+ pub fn resize(&self, rows: u16, cols: u16) -> Result<(), String> {
+ self.handle
+ .resize(codex_utils_pty::TerminalSize { rows, cols })
+ .map_err(|err| err.to_string())
+ }
+
/// Kill the child (Unix process group) without exposing Codex signals.
pub fn kill(&self) {
self.handle.request_terminate();
@@ -46,8 +53,7 @@ pub const DEFAULT_ROWS: u16 = 24;
pub const DEFAULT_COLS: u16 = 80;
/// Spawn `program` + `args` on a PTY at [`DEFAULT_ROWS`]×[`DEFAULT_COLS`].
-/// Resize is not exposed on this API. `env` is the full environment after the
-/// caller applied runner-local defaults.
+/// `env` is the full environment after the caller applied runner-local defaults.
pub async fn spawn(
program: &str,
args: &[String],
@@ -82,6 +88,7 @@ pub async fn spawn(
mod tests {
use super::*;
use std::time::Duration;
+ use tokio::sync::mpsc;
#[test]
fn crate_is_isolated_adapter() {
@@ -132,4 +139,59 @@ mod tests {
.expect("exit recv");
assert_eq!(code, 0, "stdin should be a TTY");
}
+
+ async fn collect_until(output: &mut mpsc::Receiver>, needle: &str) -> String {
+ let mut text = String::new();
+ let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
+ while tokio::time::Instant::now() < deadline {
+ let chunk = tokio::time::timeout(Duration::from_millis(500), output.recv())
+ .await
+ .ok()
+ .flatten();
+ let Some(chunk) = chunk else {
+ continue;
+ };
+ text.push_str(&String::from_utf8_lossy(&chunk));
+ if text.replace("\r\n", "\n").contains(needle) {
+ return text;
+ }
+ }
+ panic!("timed out waiting for {needle:?}, got {text:?}");
+ }
+
+ #[tokio::test]
+ async fn resize_changes_stty_size() {
+ let dir = tempfile::tempdir().unwrap();
+ let env = HashMap::from([
+ ("PATH".into(), "/usr/bin:/bin".into()),
+ ("HOME".into(), dir.path().display().to_string()),
+ ("LANG".into(), "C".into()),
+ ("TERM".into(), "xterm".into()),
+ ]);
+ let script =
+ "stty -echo; printf 'start:%s\\n' \"$(stty size)\"; IFS= read _line; printf 'after:%s\\n' \"$(stty size)\"";
+ let mut session = spawn("/bin/sh", &["-c".into(), script.into()], dir.path(), &env)
+ .await
+ .expect("spawn stty");
+ let mut output = session.take_stdout().expect("stdout");
+ let writer = session.writer();
+ let start = collect_until(&mut output, "start:24 80").await;
+ assert!(
+ start.replace("\r\n", "\n").contains("start:24 80"),
+ "initial size, got {start:?}"
+ );
+ session.resize(40, 120).expect("resize");
+ writer.send(b"go\n".to_vec()).await.expect("write");
+ let rest = collect_until(&mut output, "after:40 120").await;
+ let combined = format!("{start}{rest}").replace("\r\n", "\n");
+ assert!(
+ combined.contains("after:40 120"),
+ "resized size, got {combined:?}"
+ );
+ let code = tokio::time::timeout(Duration::from_secs(5), session.take_exit().unwrap())
+ .await
+ .expect("exit timeout")
+ .expect("exit receiver");
+ assert_eq!(code, 0);
+ }
}
diff --git a/crates/runner/src/api.rs b/crates/runner/src/api.rs
index c06a30f..fb461ce 100644
--- a/crates/runner/src/api.rs
+++ b/crates/runner/src/api.rs
@@ -135,6 +135,12 @@ pub struct RunnerProcessStatus {
pub eof: bool,
}
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RunnerResizeResult {
+ pub rows: u16,
+ pub cols: u16,
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnerApplyPatchRequest {
pub patch: String,
diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs
index 4480349..cdd8d3b 100644
--- a/crates/runner/src/lib.rs
+++ b/crates/runner/src/lib.rs
@@ -116,7 +116,7 @@ pub use api::{
default_exec_timeout_ms, runner_local_exec_env, RunnerApplyPatchRequest,
RunnerApplyPatchResult, RunnerCwd, RunnerError, RunnerExecEnv, RunnerExecPolicy,
RunnerExecRequest, RunnerExecResult, RunnerProcessStatus, RunnerReadProcess, RunnerReadResult,
- RunnerWriteStdin, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES,
+ RunnerResizeResult, RunnerWriteStdin, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES,
};
pub use files::{DEFAULT_FIND_LIMIT, DEFAULT_READ_LIMIT, VERSION_ABSENT};
pub use patch_helper::ensure_helper_for_tests;
@@ -182,6 +182,12 @@ pub trait Runner: Send + Sync {
&self,
process_id: &ProcessId,
) -> impl std::future::Future