feat: increase timeout to 30s (#43)

Seth Troisi committed Jun 4, 2026 at 10:50 UTC 889d09f82c87bc7eb26300a2237ca1b097434a3a
6 files changed +28 -17
docs/02_execution_and_interactive.md
+3 -1
@@ -3,6 +3,8 @@ 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`)
@@ -26,7 +28,7 @@ Execution involves sending Python code (or shell commands) to the Jupyter kernel
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.
29 -- **Timeout Configuration**: Exposes a `--timeout` flag (default 10s) to allow long-running silent tasks (like model compilation or data downloading) to execute without being prematurely killed.
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`.
docs/05_run_command.md
+2 -1
@@ -2,6 +2,7 @@
2 log:
3 2026-05-12: Initial design and implementation of `colab run <script.py> [args...]`. Combines `colab new` + `colab exec` + `colab stop` into a single fire-and-forget invocation so a Python file can use `#!/usr/bin/env -S colab run` as a shebang line and execute on a freshly-allocated Colab VM. Adds `--keep` (skip auto-stop), `--gpu` / `--tpu` (passthrough to session creation), `-s/--session` (name the ephemeral session), and propagates the script's exit status (non-zero on any uncaught exception in the kernel). The script's `sys.argv` is re-set inside the kernel to mirror native `python script.py arg1 arg2` semantics, and `__name__` is set to `"__main__"`.
4 2026-05-12: Native CPython exit-code semantics for `sys.exit()` / `raise SystemExit(...)` from the script body. The Colab kernel reports a `SystemExit` as `output_type=='error'`, which under the previous logic would have (a) printed the IPython traceback (`An exception has occurred, use %tb...`) and (b) flagged the run as a failure regardless of the integer exit code. Now: `sys.exit()` / `sys.exit(0)` exit 0 silently; `sys.exit(N)` exits N; `sys.exit('msg')` exits 1 (matching CPython). The IPython "To exit: use 'exit', 'quit', or Ctrl-D." UserWarning is filtered via the prelude. Encoded after running `examples/gpu_hello.py` end-to-end and seeing the noisy `SystemExit: 0` traceback at the end of an otherwise-successful GPU run.
5 +2026-06-04: Bumped the default value of the `--timeout` flag from 10.0s to 30.0s so short-but-silent tasks aren't prematurely killed out of the box. Mirrors the same change for `colab exec`.
6 ---
7
8 # Design: `colab run` — Shebang-Compatible One-Shot Execution
@@ -29,7 +30,7 @@ colab run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
30 | `--gpu` | str | None | Same set as `colab new --gpu` (T4, L4, G4, H100, A100). |
31 | `--tpu` | str | None | Same set as `colab new --tpu` (v5e1, v6e1). |
32 | `--keep` | bool | False | Do **not** stop the session after the script finishes. |
32 -| `--timeout` | float | 10.0 | Timeout in seconds for code execution to prevent hanging on silent tasks. |
33 +| `--timeout` | float | 30.0 | Timeout in seconds for code execution to prevent hanging on silent tasks. |
34
35 ### Shebang usage
36 With `--keep` and `--gpu` baked into the shebang line, an entire one-file workload becomes:
src/colab_cli/commands/execution.py
+5 -4
@@ -109,8 +109,9 @@ def exec_command(
109 Optional[str], typer.Option("--output-image", help="Path to save plot")
110 ] = None,
111 timeout: Annotated[
112 - Optional[float], typer.Option("--timeout", help="Timeout in seconds for code execution")
113 - ] = 10.0,
112 + Optional[float],
113 + typer.Option("--timeout", help="Timeout in seconds for code execution"),
114 + ] = 30.0,
115 ):
116 """Execute code in a session"""
117 from colab_cli.common import state
@@ -208,9 +209,9 @@ def exec_command(
209 state.store.add(s)
210
211 outputs = runtime.execute_code(
211 - code,
212 + code,
213 output_hook=lambda o: display_output(o, output_image),
213 - timeout=timeout
214 + timeout=timeout,
215 )
216 if "cell" in block:
217 save_output(outputs, block["cell"])
src/colab_cli/commands/run.py
+4 -5
@@ -234,8 +234,9 @@ def run_command(
234 ),
235 ] = False,
236 timeout: Annotated[
237 - Optional[float], typer.Option("--timeout", help="Timeout in seconds for code execution")
238 - ] = 10.0,
237 + Optional[float],
238 + typer.Option("--timeout", help="Timeout in seconds for code execution"),
239 + ] = 30.0,
240 ):
241 """Run a Python script on a fresh Colab VM, then release the VM
242
@@ -390,9 +391,7 @@ def run_command(
391
392 try:
393 outputs = runtime.execute_code(
393 - payload,
394 - output_hook=_make_run_output_hook(),
395 - timeout=timeout
394 + payload, output_hook=_make_run_output_hook(), timeout=timeout
395 )
396 except Exception:
397 # Genuine transport-level failure. Cleanup still happens via the
tests/test_exec.py
+12 -4
@@ -58,7 +58,9 @@ def test_cli_exec_file(mock_store, mock_runtime_class, mock_common_state, tmp_pa
58 mock_runtime.execute_code.assert_any_call(
59 "import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
60 )
61 - mock_runtime.execute_code.assert_any_call("print('hello')", output_hook=ANY, timeout=10.0)
61 + mock_runtime.execute_code.assert_any_call(
62 + "print('hello')", output_hook=ANY, timeout=30.0
63 + )
64
65
66 def test_cli_exec_stdin(mock_store, mock_runtime_class, mock_common_state):
@@ -80,7 +82,9 @@ def test_cli_exec_stdin(mock_store, mock_runtime_class, mock_common_state):
82 assert mock_session.last_execution[1] is None
83 assert mock_session.last_execution[2] is not None
84 mock_store.add.assert_called_with(mock_session)
83 - mock_runtime.execute_code.assert_any_call("print(42)", output_hook=ANY, timeout=10.0)
85 + mock_runtime.execute_code.assert_any_call(
86 + "print(42)", output_hook=ANY, timeout=30.0
87 + )
88
89
90 def test_cli_exec_not_found(mock_common_state):
@@ -194,6 +198,10 @@ def test_cli_exec_timeout(mock_store, mock_runtime_class, mock_common_state, tmp
198 script = tmp_path / "script.py"
199 script.write_text("print('hello')")
200
197 - result = runner.invoke(app, ["exec", "-s", "s1", "-f", str(script), "--timeout", "3600"])
201 + result = runner.invoke(
202 + app, ["exec", "-s", "s1", "-f", str(script), "--timeout", "3600"]
203 + )
204 assert result.exit_code == 0
199 - mock_runtime.execute_code.assert_any_call("print('hello')", output_hook=ANY, timeout=3600.0)
205 + mock_runtime.execute_code.assert_any_call(
206 + "print('hello')", output_hook=ANY, timeout=3600.0
207 + )
tests/test_ipynb_exec.py
+2 -2
@@ -98,10 +98,10 @@ class TestIpynbExec(unittest.TestCase):
98 "os.chdir", mock_runtime.execute_code.call_args_list[0].args[0]
99 )
100 mock_runtime.execute_code.assert_any_call(
101 - "print('cell 1')", output_hook=ANY, timeout=10.0
101 + "print('cell 1')", output_hook=ANY, timeout=30.0
102 )
103 mock_runtime.execute_code.assert_any_call(
104 - "print('cell 2')", output_hook=ANY, timeout=10.0
104 + "print('cell 2')", output_hook=ANY, timeout=30.0
105 )
106
107 @patch("colab_cli.commands.execution.ColabRuntime")