# RFD 025: Live Re-Attachment to Detached Queries

- **Status**: Abandoned
- **Category**: Design
- **Authors**: Jean Mertz <git@jeanmertz.com>
- **Date**: 2025-07-19

> [!IMPORTANT]
> This RFD is **Abandoned**.
> 
> Superseded by [RFD 027: Client-Server Query Architecture][RFD 027], which
> makes live attachment the default execution model rather than an add-on.
> The socket protocol and attach flow from this RFD are carried forward into RFD
> 027.
> 
> The original text below is preserved for historical context.

## Summary

This RFD introduces `jp conversation attach` for connecting a terminal to a
running detached query process.
The attached client receives live streaming output and can answer inquiries
interactively.
When the client disconnects, the detached process continues with the configured
detached policy.

This RFD depends on [RFD 024] (Detached Query Execution) for the `--detach`
flag, process registry, and daemonization, on [RFD 021] (Printer Live
Redirection) for runtime output swapping, and on [RFD 019] (Non-Interactive
Mode) for prompt routing.

## Motivation

[RFD 024] introduces `--detach` for running queries in the background.
When a detached process hits an inquiry, it persists the incomplete turn and
exits.
The user later resumes with `--continue`.
This works well when the user intends to come back later.

But there's a gap: the user detaches a query, then wants to check on it while
it's still running.
Maybe the LLM is streaming a long response.
Maybe several tool calls are executing and the user wants to see progress.
Maybe the user knows an inquiry is likely and wants to be there to answer it
immediately rather than waiting for the process to exit and resuming later.

Without live re-attachment, the user's only option is to wait for the process to
finish (or exit at an inquiry), then inspect the results via `conversation
print`.
There is no way to observe or interact with a running detached process.

## Design

### `jp conversation attach`

```sh
jp conversation attach <cid>
jp conversation attach                # session's active conversation
```

Connects to the running detached process for the specified conversation.
If no process is running:

- If the conversation has an incomplete turn (pending inquiry), the command
  suggests `--continue` instead:
  
  ```sh
  $ jp conversation attach jp-c17528832001
  Error: No running process for jp-c17528832001.
    Conversation has a pending inquiry (fs_modify_file).
  
      jp query --continue --id=jp-c17528832001
                              Resume the incomplete turn.
  ```

- If the conversation is idle, the command errors:
  
  ```sh
  $ jp conversation attach jp-c17528831000
  Error: No running process for jp-c17528831000.
  ```

`attach` is a read-write connection to the running process.
It is not a new query — there is no `--attach` flag on `jp query`.

### IPC via Unix Domain Sockets

Each detached process listens on a Unix domain socket:

```txt
~/.local/share/jp/workspace/<workspace-id>/processes/<conversation-id>.sock
```

This sits alongside the process registry entry from [RFD 024].
The socket is created when the detached process starts and removed when it
exits.

The socket path is derived from the conversation ID, so `attach` can find it
without consulting the registry entry (though it checks PID liveness from the
registry to confirm the process is actually running before connecting).

### Attach Protocol

The protocol uses newline-delimited JSON over the Unix domain socket.
The detached process is the server; the attach client connects.

#### Message types

**Server -\> Client:**

```rust
enum ServerMessage {
    /// Rendered output chunk. The client writes this to its terminal.
    Output { target: PrintTarget, data: String },

    /// An inquiry that needs the user's answer.
    Inquiry { inquiry: Inquiry, metadata: InquiryMetadata },

    /// The turn completed. The client can disconnect.
    TurnComplete,

    /// The process is exiting (error, shutdown).
    ProcessExiting { reason: String },
}
```

**Client -\> Server:**

```rust
enum ClientMessage {
    /// Answer to a pending inquiry.
    InquiryResponse { id: InquiryId, answer: Value },

    /// Client is disconnecting gracefully.
    Disconnect,
}
```

#### Connection lifecycle

1. Client connects to the socket.
2. Server detects the connection and sets `has_client = true` in its prompt
   routing state.
3. Server begins forwarding rendered output to the client via `Output` messages.
4. If an inquiry arrives, the server sends an `Inquiry` message instead of
   applying the detached policy.
   The client renders the prompt and collects the user's answer.
5. Client sends `InquiryResponse`.
   Server delivers the answer to the tool and continues execution.
6. When the turn completes, server sends `TurnComplete`.
7. Client can disconnect at any time by sending `Disconnect` or closing the
   socket.
   Server reverts to `has_client = false` and the detached policy resumes.

#### Single client

Only one client can be attached at a time.
If a second client attempts to connect while one is already attached, the
connection is rejected:

```sh
$ jp conversation attach jp-c17528832001
Error: Another client is already attached to jp-c17528832001.
```

### Output Redirection on Attach

When a client attaches to a running process, the process needs to start sending
output to the client.
This uses the `Printer::swap_writers()` mechanism from [RFD 021].

#### Attach flow

1. **Replay from disk.** The client reads persisted events from the
   conversation's event stream and renders them using `conversation print`
   logic.
   By default, events from the current in-progress turn are shown, giving the
   user context for what's happening.

2. **Flush and swap.** The server calls `printer.flush_instant()` followed by
   `printer.swap_writers(socket_writer)`.
   All subsequent rendered output goes to the client via the socket instead of
   the sink.

3. **Live streaming.** From this point, the client's terminal shows the same
   output the user would see in a foreground query: LLM response chunks, tool
   call headers, progress indicators.

The gap between "events persisted to disk" and "live output from the printer" is
the content currently in the `EventBuilder` and renderer buffers that hasn't
been flushed as a complete event yet.
This content is captured by the `flush_instant()` call, which forces all
buffered content through the printer before the swap.

#### Detach flow

When the client disconnects:

1. Server calls `printer.flush_instant()` to send any pending output to the
   client.
2. Server calls `printer.swap_writers(sink)` to revert to discarding output.
3. Server sets `has_client = false`.
4. Subsequent inquiries follow the detached policy (`queue` by default).

### Context on Attach

When a client attaches mid-stream, it needs enough context to understand what's
happening.
The client renders recent history before switching to live output:

| Flag       | Behavior                                 |
| ---------- | ---------------------------------------- |
| (default)  | Show events from the current in-progress |
|            | turn.                                    |
| `--tail=N` | Show the last N content events, plus the |
|            | current turn.                            |
| `--tail=0` | Skip history, show only live output from |
|            | this point.                              |

Content events are `ChatRequest`, `ChatResponse`, `ToolCallRequest`,
`ToolCallResponse`.
Structural markers like `TurnStart` and `ConfigDelta` are skipped when counting.

The client renders history locally from the persisted conversation file.
This avoids the server needing to replay or buffer historical output — the disk
is the source of truth for completed events.

### Relationship to Conversation Locks

The detached process holds the conversation lock ([RFD 020]) for its entire
execution.
The attach client does **not** acquire a lock — it communicates with the
lock-holding process via the socket.
The attach client is a viewer and input provider, not a writer to the
conversation stream.

### Relationship to Prompt Routing

The attach changes the prompt routing state dynamically:

- **Client attached**: `has_client = true`.
  Inquiries are sent to the client via the socket
  (`PromptAction::PromptClient`).
- **Client detaches**: `has_client = false`.
  Inquiries follow the detached policy (`queue` → persist and exit).

This means a detached query that would normally exit at an inquiry can instead
have the inquiry answered interactively if a client attaches before the inquiry
arrives.
The process doesn't need to know in advance whether a client will be available
— the routing decision happens at inquiry time.

## Drawbacks

**IPC complexity.** Unix domain sockets, a message protocol, and output
redirection add meaningful infrastructure.
This is the most complex piece of the detached conversations feature set.

**Platform constraints.** Unix domain sockets are not available on Windows.
Windows support would require named pipes or a different IPC mechanism.
The initial implementation targets macOS and Linux only.

**Partial output gap.** Between the last persisted event and the current
renderer state, there may be content that hasn't been flushed as a complete
event (partial `ChatResponse` chunks in the `EventBuilder`).
The `flush_instant()` call minimizes this gap but doesn't eliminate it for
content that hasn't reached the printer yet.
In practice, this is a few tokens of LLM output at most.

**Socket cleanup on crash.** If the detached process is killed with SIGKILL, the
socket file orphans on disk.
`conversation attach` checks PID liveness before connecting, so stale sockets
don't cause connection attempts to hang.
Stale socket files are cleaned up alongside stale registry entries during
`conversation ls`.

## Alternatives

### HTTP server for IPC

Use a local HTTP server instead of Unix domain sockets.

Rejected because it requires port allocation, firewall considerations, and is
heavier than needed.
Unix domain sockets are the standard local IPC mechanism on Unix with better
security properties (filesystem permissions).

### Named pipe (FIFO) instead of socket

Use a named pipe for the output stream and a separate pipe for input.

Rejected because named pipes are unidirectional.
The bidirectional communication (output to client, inquiry answers from client)
would require two pipes and coordination logic.
Unix domain sockets provide bidirectional streams natively.

### No live output, just inquiry forwarding

Only forward inquiries to the attached client, not streaming output.
The client would see inquiry prompts but not the LLM's response or tool
execution progress.

Rejected because the primary value of attaching is seeing what's happening.
Inquiry forwarding alone doesn't justify the IPC infrastructure — the user
could just wait for the process to exit and use `--continue`.

### Shared memory buffer

Use shared memory (mmap) for the output stream instead of a socket.

Rejected because it adds complexity (synchronization, capacity management)
without meaningful benefit.
The output rate is bounded by LLM token generation speed, which is well within
socket throughput.
Shared memory is useful for high-bandwidth IPC; this is a low-bandwidth text
stream.

## Non-Goals

- **Multi-client attachment.** Only one client at a time.
  Concurrent attachment would require deciding which client receives output and
  which answers inquiries.
  Out of scope.

- **Persistent output buffer.** The detached process does not buffer output for
  future attach clients.
  If nobody is attached, output is discarded.
  The persisted event stream on disk is the record of what happened.

- **Cross-machine attachment.** Sockets are local.
  No network protocol.

- **Attaching to foreground queries.** `attach` only targets detached processes.
  A foreground query already has a terminal — there's nothing to attach to.

## Risks and Open Questions

### Renderer state after swap

When `swap_writers()` redirects output to the socket, the `ChatResponseRenderer`
and markdown buffer continue mid-stream.
Their internal state (open code blocks, list nesting, ANSI color stack) carries
over.
The client receives output that continues from wherever the renderer was — not
from the start of the current chunk.

This means the client may see output that starts mid-paragraph or
mid-code-block.
The replay-from-disk step provides context for completed events, but the live
transition may be visually rough for a few tokens until the renderer reaches a
natural boundary (end of paragraph, end of code block).

This is acceptable for an initial implementation.
A smoother transition could reset the renderer state and replay from the last
complete event, but that adds complexity for marginal UX improvement.

### Socket permissions

The socket file inherits the umask of the creating process.
On most systems this means owner-only access (0700 on the parent directory).
This is correct — only the same user should be able to attach.

If the user runs JP under different UIDs (e.g., via sudo), the socket may not be
accessible.
This is an edge case that can be documented rather than engineered around.

### Attach during the persist-and-exit window

When a detached process decides to persist and exit (queue policy, inquiry hit),
there's a brief window where it's persisting the incomplete turn and shutting
down.
If a client attaches during this window, the process has already committed to
exiting.

The simplest behavior: the server sends `ProcessExiting` and closes the socket.
The client sees the message and suggests `--continue`.
No special coordination needed.

### Protocol versioning

The NDJSON protocol has no version field.
If the message format changes in a future release, an older client connecting to
a newer server (or vice versa) would see parse errors.

Adding a `version` field to the initial handshake is straightforward and should
be included from the start, even if the first version is just `1`.

## Implementation Plan

### Phase 1: Socket Listener

Each detached process opens a Unix domain socket alongside its registry entry.
The socket accepts connections but does nothing — no messages are sent.
This validates the socket lifecycle (creation, cleanup, stale detection).

Depends on [RFD 024] Phase 3 (`--detach` and daemonization).

Can be merged independently.

### Phase 2: Output Forwarding

When a client connects, the server swaps the printer to a socket-backed writer
([RFD 021]).
Rendered output flows to the client.
On disconnect, the printer reverts to a sink.
No inquiry handling yet — inquiries follow the detached policy as before.

The client renders received output chunks to its terminal.

Depends on Phase 1 and [RFD 021].

### Phase 3: Inquiry Forwarding

When a client is attached and an inquiry arrives, the server sends an `Inquiry`
message instead of applying the detached policy.
The client renders the prompt, collects the answer, and sends `InquiryResponse`.
The server delivers the answer and continues execution.

Depends on Phase 2 and [RFD 019] Phase 2 (routing integration for `has_client`).

### Phase 4: `jp conversation attach` CLI

Implement the `attach` subcommand with history replay (`--tail`), live output
rendering, and inquiry prompting.
Handle connection errors, stale sockets, and single-client enforcement.

Depends on Phase 3.

## References

- [RFD 024: Detached Query Execution][RFD 024] — `--detach`, process registry,
  and the `queue` policy that this RFD builds on.
- [RFD 023: Resumable Conversation Turns][RFD 023] — incomplete turn
  persistence; the fallback when no client is attached.
- [RFD 021: Printer Live Redirection][RFD 021] — `swap_writers()` used for
  routing output to the attached client.
- [RFD 020: Parallel Conversations][RFD 020] — conversation locks; the detached
  process holds the lock, the attach client does not.
- [RFD 019: Non-Interactive Mode][RFD 019] — prompt routing and `has_client`
  state that changes dynamically on attach/detach.

[RFD 019]: 019-non-interactive-mode.md
[RFD 020]: 020-parallel-conversations.md
[RFD 021]: 021-printer-live-redirection.md
[RFD 023]: 023-resumable-conversation-turns.md
[RFD 024]: 024-detached-query-execution.md
[RFD 027]: 027-client-server-query-architecture.md
