---
title: ExecHandle
description: One exec, addressed by its caller-minted id.
---

```python
@final
class ExecHandle
```

One exec, addressed by its caller-minted id.

The id is the idempotency key, so a handle survives a process restart: rebuild it
through `Session.exec(exec_id)` and every method still addresses the same server-side
exec.

## Constructor

`ExecHandle` declares no `__new__` or `__init__`, so it is not constructed directly: an instance comes back from another call in this package.

## Properties

### `exec_id`

```python
exec_id: str
```

## Methods

### `ack`

```python
def ack() -> ExecResult
```

Releases the buffered output and starts the TTL clock.

### `ack_async`

```python
def ack_async() -> ExecResult
```

The awaitable twin of `ack`.

### `close_stdin`

```python
def close_stdin() -> StdinAck
```

Sends EOF. Nothing else closes stdin: the daemon's copy of the pipe outlives the
child's wait, so a child blocked reading stdin hangs until its timeout otherwise.

### `close_stdin_async`

```python
def close_stdin_async() -> StdinAck
```

The awaitable twin of `close_stdin`.

### `kill`

```python
def kill() -> bool
```

Signals the whole process group. `False` means nothing was signalled because the
child had already been reaped — which is the outcome a kill wanted.

### `kill_async`

```python
def kill_async() -> bool
```

The awaitable twin of `kill`.

### `poll`

```python
def poll() -> ExecResult
```

Reads current status and output. Read-only server-side; safe to spin on.

### `poll_async`

```python
def poll_async() -> ExecResult
```

The awaitable twin of `poll`.

### `stream`

```python
def stream(
    *,
    offset: SupportsIndex = 0,
    reconnect: bool = True,
    max_reconnects: SupportsIndex | None = None,
    error_on_gap: bool = False,
    idle_timeout: SupportsFloat | SupportsIndex | None = None,
) -> ExecStream
```

An iterator over output as it arrives, reconnecting at the last good offset.

`error_on_gap=True` turns an evicted byte range into an exception instead of a
`Gap` event, which is what a caller that must have complete output wants.
`reconnect=False` ends the iterator at a cut instead, for a caller doing its own
reconnection.

### `wait`

```python
def wait(timeout: SupportsFloat | SupportsIndex = ...) -> ExecResult
```

Polls until the exec is done, or raises `TimeoutError`.

A timeout has not touched the exec — polling is read-only and output lives until
it is acked — so a caller that gives up can come back and poll again.

### `wait_and_ack`

```python
def wait_and_ack(timeout: SupportsFloat | SupportsIndex = ...) -> ExecResult
```

Wait, then ack, returning the result that carries the output.

Which result comes back matters: the ack response carries the released output and
a poll issued after the ack reports `acked` with none, so returning the wrong one
is a silent empty-output bug. The core sequences it.

### `wait_and_ack_async`

```python
def wait_and_ack_async(timeout: SupportsFloat | SupportsIndex = ...) -> ExecResult
```

The awaitable twin of `wait_and_ack`. Cancelled during the wait it leaves the exec
untouched; cancelled during the ack, the ack may have landed.

### `wait_async`

```python
def wait_async(timeout: SupportsFloat | SupportsIndex = ...) -> ExecResult
```

The awaitable twin of `wait`. Cancelling it is giving up as a timeout does: the exec
is untouched.

### `write_stdin`

```python
def write_stdin(data: bytes, *, eof: bool = False) -> StdinAck
```

Writes to the child's stdin. Requires the exec to have been started with
`stdin=True`, or the daemon answers 409.

`eof` in the same call is the common case for feeding a prompt: two round trips
would leave a window where the child has the bytes but not the EOF that says the
input is complete.

### `write_stdin_async`

```python
def write_stdin_async(data: bytes, *, eof: bool = False) -> StdinAck
```

The awaitable twin of `write_stdin`.

## Special methods

### `__repr__`

```python
def __repr__() -> str
```

## Provenance

This page is generated from `bindings/microvms-py/microvms.pyi` by Griffe 2.3.0, run from `site/scripts/gen-reference.mjs` on every `pnpm run sync`, so an edit made here is overwritten by the next run.

To change the page, change the doc comment in the binding's Rust source, then regenerate the declarations with `mise run stubs`. `mise run stubs:check` fails when the committed file no longer matches the source.