| 1 | --- |
| 2 | log: |
| 3 | 2026-05-07: Fixed `colab console` piped-stdin handling. Previously a piped invocation (e.g. `echo 'cmd' | colab console -s s`) sent the command and then hung indefinitely because the previous EOF handler emitted a bare `\x04` (Ctrl-D), which the remote `tmux`-wrapped bash treats as a literal character rather than a session terminator. The new handler sends `exit\n` (which bash actually exits on) and then closes the websocket from the client side after a short grace period (`PIPED_EOF_GRACE_SECONDS = 0.5s`) so any tail output (bash `logout`, tmux `[exited]`) makes it back to the user. TTY mode is unchanged: real-terminal EOF is left to the remote shell. Verified live: `echo 'echo HELLO' | colab console -s s` now exits in ~1.2s instead of hanging. |
| 4 | |
| 5 | 2026-05-07: Fixed `print_kitty` (used by `colab exec --output-image` and any image-producing exec) to no-op when `sys.stdout.isatty()` is false. The Kitty Graphics Protocol escape sequence is meaningless when stdout is a file or pipe and was visually corrupting captured output (a multi-KB base64 PNG blob would land in log files, grep targets, or showboat captures). Image bytes are still saved to disk via `handle_image`'s file-write path; only the inline-render attempt is suppressed. |
| 6 | |
| 7 | 2026-06-04: Bumped the default `--timeout` for `colab exec` from 10s to 30s (and the matching `colab run` default) so brief silent tasks are less likely to hit a premature `TimeoutError`. Explicit `--timeout` overrides are unaffected. |
| 8 | --- |
| 9 | |
| 10 | # Design: Execution and Interactive Interaction (`repl`, `exec`, `console`) |
| 11 | |
| 12 | ## Overview |
| 13 | Execution involves sending Python code (or shell commands) to the Jupyter kernel running on the Colab VM and processing the stream of output messages. |
| 14 | |
| 15 | ## Approach |
| 16 | |
| 17 | ### 1. REPL (`colab repl`) |
| 18 | - **Transport**: WebSockets (using `websockets` library if allowed, or a custom `http.client` based long-polling implementation if we're strictly stdlib). |
| 19 | - **Communication**: Jupyter Kernel Messaging Protocol. |
| 20 | - `execute_request`: Send code string. |
| 21 | - `execute_reply`: Get status. |
| 22 | - `iopub.stream`: Capture `stdout` and `stderr`. |
| 23 | - **Interactive Mode**: Standard Python `cmd.Cmd` or `code.InteractiveConsole` for local input/output. |
| 24 | - **Piping Support**: Detect `sys.stdin.isatty()`. If not a TTY, read all input and send as a single execution request. |
| 25 | |
| 26 | ### 2. Execution (`colab exec`) |
| 27 | - **File Handling**: |
| 28 | - If file path is local: Read content, send as code. |
| 29 | - If file path is remote: Execute `!python <path>`. |
| 30 | - **Multi-Modal Output**: Handle `display_data` messages (e.g., `image/png`, `text/html`). For the CLI, we'll save images to temporary files and print their paths, or if the terminal supports it (e.g., iTerm2), inline them. |
| 31 | - **Timeout Configuration**: Exposes a `--timeout` flag (default 30s) to allow long-running silent tasks (like model compilation or data downloading) to execute without being prematurely killed. |
| 32 | |
| 33 | ### 3. Console (`colab console`) |
| 34 | - **Implementation**: Connects directly to the backend terminal endpoint (`/colab/tty`) via WebSockets using `websocket-client`. |
| 35 | - **Interactive**: Bypasses the Jupyter kernel entirely to provide a raw, PTY-backed bash session on the Colab VM. |
| 36 | - **Terminal Management**: Configures `sys.stdin` to raw mode using `termios` and `tty`, passing single characters to the socket and writing raw ANSI escape sequences directly to `sys.stdout.buffer`. Hooks into `SIGWINCH` to communicate local terminal dimensions (`cols`/`rows`) to the remote bash environment so output rendering works perfectly during resizing. |
| 37 | - **Piped stdin**: Detected via `sys.stdin.isatty()`. When piped, the input characters are forwarded one at a time to the remote pty, and on EOF the client sends `exit\n` and then closes the websocket itself after `PIPED_EOF_GRACE_SECONDS` (0.5s) so the user's shell goodbye text drains back. The remote `/colab/tty` endpoint wraps bash in tmux, which intercepts a bare `\x04` as a literal character — that is why we send `exit\n` rather than Ctrl-D. |
| 38 | |
| 39 | ## Implementation Details |
| 40 | - **Kernel Management**: `ColabRuntime` (from `colab-agent`) already handles message signing and message types. |
| 41 | - **Output Streaming**: Continuous polling or asynchronous message handling to provide real-time output. |
| 42 | - **Piping Example**: `cat script.py | colab exec -s my-session`. |
| 43 | |
| 44 | ## Testing Strategy |
| 45 | TDD is mandatory for all execution features. |
| 46 | |
| 47 | ### 1. Mock Kernel Client |
| 48 | - **Test Case**: Verify `ColabRuntime` correctly sends an `execute_request` message over the websocket. |
| 49 | - **Test Case**: Verify `iopub.stream` messages are correctly handled and printed to `stdout` in real-time. |
| 50 | - **Test Case**: Verify `display_data` (specifically `image/png`) triggers the correct local handling (saving or display). |
| 51 | |
| 52 | ### 2. TTY and Piping |
| 53 | - **Test Case**: Mock `sys.stdin.isatty()` to verify `colab repl` correctly switches between interactive mode and one-shot piped execution. |
| 54 | - **Test Case**: Verify large piped inputs are handled without buffer overflow or truncation. |
| 55 | - **Test Case**: `colab console` with piped stdin sends `exit\n` and calls `ws.close()` on EOF (regression: previously sent `\x04` only and hung). |
| 56 | - **Test Case**: `colab console` in TTY mode does not synthesize an exit on EOF (the user owns the session lifecycle). |
| 57 | - **Test Case**: `print_kitty` is a no-op when `sys.stdout.isatty()` is false (regression: previously emitted ANSI/base64 into pipes and files). |