Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ coverage.*
/.claude/worktrees/
/.worktrees/
/.superpowers/
/.omo/
/CLAUDE.md
/HANDOFF.md
/PLAN.md
graphify_out/
graphify-out/
94 changes: 85 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ uam restart [--allow-latest] <id> # stop the agent and resume it in place
uam rm <id> # kill the session and remove record
uam kill-all # stop every managed session
uam version
uam doctor [<session-id>] [--json]
uam profile ls [--json]
uam profile show <name> [--json]
uam profile set <name> [profile flags]
uam profile rm <name>
uam profile default <name|none>
uam profile assign <session-id> <name|none>
uam profile override <session-id> [profile flags]
uam profile effective <session-id> [--json]
```

## TUI keys
Expand Down Expand Up @@ -140,10 +149,21 @@ thresholds, filtering, mobile guidance, lifecycle labels, and accessibility.
## Attached sessions

`uam attach` (or `Enter` in the TUI) bridges your terminal straight to the
agent's PTY:

- `Ctrl+B d` detaches and returns to the dashboard (`Ctrl+B Ctrl+B` sends a
literal `Ctrl+B` to the agent; `Ctrl+B c` sends a literal `Ctrl+C`)
agent's PTY. An attach client is temporary client state, not part of the
Managed Session record. The host permits one controller at a time; additional
interactive clients wait as standbys, and observers receive output without
being allowed to send input, resize the PTY, or answer terminal queries. See
[terminal client and session ownership](docs/adr/0003-terminal-client-session-ownership-and-protocol-v2.md)
for the normative ownership and protocol rules.

- `Ctrl+B d` detaches and returns to the dashboard by default. `prefix prefix`
sends a literal configured prefix (`Ctrl+B Ctrl+B` only when the profile uses
`C-b`); `prefix c` sends a literal `Ctrl+C`. A profile can change the prefix;
use the profile's `C-x` spelling, such as `C-a`, when configuring it.
- `prefix r` requests control, `prefix o` transfers control when used by the
current controller, `prefix i` reports the current role, and `prefix m`
toggles mouse passthrough for this attachment only. A prefix command never
enters provider input.
- Plain `Ctrl+C` is swallowed while attached so terminal copy shortcuts do not
cancel the agent
- `←` (left arrow) also detaches when you haven't typed anything since the
Expand All @@ -165,7 +185,9 @@ agent's PTY:

Bracketed-paste payload is forwarded byte-for-byte. Control bytes inside a paste
do not trigger UAM's attach shortcuts. UAM cannot access the client clipboard or
turn an unsent mouse gesture into remote input.
turn an unsent mouse gesture into remote input. Terminal names and color hints
reported by an attachment are diagnostics metadata, not proof that the client
supports a terminal feature.

For PowerShell SSH, use Windows Terminal and configure a keyboard paste binding
such as `Ctrl+V`, `Ctrl+Shift+V`, or `Shift+Insert`. Provider mouse reporting is
Expand Down Expand Up @@ -218,7 +240,10 @@ Provider behavior:
- **Hermes**: no provider resume command is configured. A stopped Hermes record
cannot be resumed; dispatch a new Managed Session instead.
- After a reboot, records survive in the store and resume on attach — a
scenario where a tmux session would simply be gone.
provider-aware relaunch, not a surviving PTY. The old terminal process and
its screen modes are gone. A SIGKILL similarly prevents normal terminal
cleanup; start a fresh terminal or run `reset` if the local terminal is left
in an unusable mode.

### Multiple sessions in one workspace

Expand Down Expand Up @@ -291,6 +316,59 @@ run `loginctl enable-linger`.
> them first (`tmux -L uam kill-server`); stored session records carry over
> unchanged and remain resumable.

### Profiles and diagnostics

Profiles supply stable launch and attach defaults. Resolution is ordered; a
later layer can refine only fields it is allowed to control:

1. **Hard safety invariants** fix the provider `TERM` value to
`xterm-256color` and reject profile-supplied environment, capability, or
resume-policy changes.
2. **Global defaults** select the default provider, yolo mode, scrollback,
mouse policy, `C-b` prefix, and quick-detach behavior.
3. **Built-in provider policy** selects the provider identity, native key
protocol, and outer-screen policy. OpenAI Codex is the primary-screen
exception; the other current providers use a UAM outer screen.
4. **Selected named profile** applies the default profile or a session-selected
profile. A provider-constrained profile must match the session provider.
5. **Per-session overrides** refine the selected profile for that durable
session.
6. **Client-local attachment overrides** can refine mouse, prefix, and
quick-detach for only the current attachment.
7. **Capability constraints** apply the client's negotiated capabilities. For
example, local mouse filtering and an owned outer screen require that the
client supports them; terminal hints are not capability proof.

Launch-time fields are provider, approval mode, command alias, and scrollback.
The default profile, named profiles, selected session profile, and per-session
overrides are persistent configuration. Mouse, control prefix, and quick-detach
can also be attachment-local; client identity, role, dimensions, protocol, and
capabilities are runtime-only and never enter `sessions.json`.

```sh
uam profile set focused --provider claude --mode safe --mouse off --prefix C-a --back-detach off --scrollback 8000
uam profile default focused
uam profile show focused --json
uam profile ls --json
uam doctor --json
```

Use `uam profile assign <session-id> <name|none>` to select a profile for one
session, `uam profile override <session-id> [profile flags]` for its final
overrides, and `uam profile effective <session-id> --json` to inspect the
resolved result. `uam profile rm <name>` refuses to delete a default or a
profile still selected by a session; clear those references first. `uam doctor
<session-id> --json` reports runtime roles, supported protocol versions,
resolved profile, provider terminal policy, and fallback reasons. Diagnostics
redact secret-like values and do not print terminal input or output.

Schema v4 migrates older records atomically. Before a migration, UAM writes an
exact adjacent `sessions.json.bak.*` backup; if the write fails, the original
remains in place. To roll back, stop UAM, replace `sessions.json` with the
chosen backup, then start a compatible binary. Same-schema unknown fields round
trip. A config from a newer schema opens read-only so an older binary cannot
clobber fields it does not understand.

## Safety model

`uam` launches providers in their full-access or auto-approve ("yolo") mode by
Expand All @@ -316,11 +394,9 @@ for its own execution model.
- [Terminology glossary](CONTEXT.md)
- [Managed Session vs. Provider Conversation](docs/adr/0001-managed-session-vs-provider-conversation.md)
- [Terminal ownership over SSH](docs/adr/0002-terminal-ownership-over-ssh.md)
- [Terminal client/session ownership and protocol v2](docs/adr/0003-terminal-client-session-ownership-and-protocol-v2.md)
- [Responsive TUI design and operations](docs/responsive-tui.md)

Session storage remains schema version 3. Updates do not require a data migration,
and unknown JSON fields are preserved when existing records are rewritten.

## Development

```sh
Expand Down
7 changes: 5 additions & 2 deletions docs/adr/0001-managed-session-vs-provider-conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,8 @@ session.
- A Managed Session can outlive several provider-side conversation switches,
which is visible in the terminology but does not multiply dashboard rows.
- Users retain control over worktrees and other repository isolation.
- Schema version 3 remains unchanged. Provider identity fits the existing
record, and unknown fields continue to round-trip.
- Schema v4 adds profile selection and overrides. Older records migrate with an
adjacent backup before the replacement write; unknown fields still round-trip.
- Runtime attachment state is deliberately absent from the durable record. See
[terminal client/session ownership and protocol v2](0003-terminal-client-session-ownership-and-protocol-v2.md)
for the live ownership contract.
45 changes: 29 additions & 16 deletions docs/adr/0002-terminal-ownership-over-ssh.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,27 @@ selection and paste.

## Decision

### Attach owns its terminal screen

When output is a terminal, the attach client enters an alternate screen that it
owns. Input independently enters raw mode when it is a terminal. Provider output
is rendered inside that boundary. Seven-bit DEC-private CSI sequences used by
supported providers to enter or leave the alternate screen are contained so
they cannot escape into the user's primary shell screen. The filter deliberately
does not interpret a bare C1 byte as CSI because that byte value can occur inside
UTF-8 input; supported provider output uses the seven-bit `ESC [` form.

On detach or return, UAM drains output within the owned screen and restores the
terminal contract: reset attributes, disable mouse/focus tracking, disable
bracketed paste, show the cursor, leave the owned alternate screen, and return
to a clean line. The dashboard separately repaints after an attached process
returns. Cleanup is intentionally targeted; UAM does not reset the entire
terminal or erase scrollback.
### Screen ownership follows provider policy

When output is a terminal, input independently enters raw mode. The provider's
terminal policy decides the outer screen:

| Outer-screen policy | Current providers | Attach behavior |
|---|---|---|
| UAM | Claude Code, Copilot, Hermes, Oh My Pi, OpenCode | UAM enters and owns an alternate screen. Provider alternate-screen sequences are contained inside that boundary. |
| Primary | OpenAI Codex | UAM attaches on the primary screen and does not create an outer alternate screen. |

The Codex primary-screen exception is deliberate. It does not change the host's
PTY ownership, the one-controller input/resize/reply rule, or native provider
keys. Seven-bit DEC-private CSI handling remains bounded; UAM does not interpret
a bare C1 byte as CSI because that byte can occur inside UTF-8 input.

On detach or return, UAM drains output and restores the terminal contract it
owns: reset attributes, disable mouse/focus tracking, disable bracketed paste,
show the cursor, leave an outer alternate screen only when one was entered, and
return to a clean line. The dashboard separately repaints after an attached
process returns. Cleanup is targeted; it does not reset the entire terminal or
erase scrollback.

Non-terminal streams do not receive alternate-screen control sequences.

Expand Down Expand Up @@ -117,3 +122,11 @@ mouse policy.
shortcuts.
- Terminal-client configuration remains necessary when the client does not send
a paste operation.
- `TERM` and color hints received through SSH are diagnostic metadata only. They
are not capability proof and do not authorize a different terminal protocol.
- Normal detach, interrupt, hangup, and termination paths restore the owned
terminal contract. SIGKILL cannot run cleanup, so it cannot promise restored
modes; use a fresh terminal or `reset` when the local terminal is unusable.

The controller, standby, observer, and protocol-v2 ownership rules are defined
by [ADR 0003](0003-terminal-client-session-ownership-and-protocol-v2.md).
128 changes: 128 additions & 0 deletions docs/adr/0003-terminal-client-session-ownership-and-protocol-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# ADR 0003: Terminal client/session ownership and protocol v2

- Status: Accepted
- Date: 2026-07-23

## Context

A Managed Session is durable; an attached terminal client is not. One host can
have several attached terminals, each with different size, terminal behavior,
and network lifetime. Persisting a client ID, terminal dimensions, capabilities,
or controller role would make a later attach trust stale state. Allowing every
client to write would also corrupt provider input and make resize nondeterministic.

## Decision

The host owns the PTY, session runtime, provider launch, and durable session
record. The provider owns its conversation. A client owns only its local screen,
raw-mode lifetime, and its own input stream. Client state is live runtime state;
it is never written to the session configuration.

### Roles and one-writer rule

| Role | Receives provider output | May write stdin, resize, or reply | Transition |
|---|---|---|---|
| Controller | Yes | Yes; the only writer | Starts as first non-observer, or receives a transfer/promotion. |
| Standby | Yes | No | Requests control or is promoted when the controller leaves. |
| Observer | Yes | No | Remains read-only. |

There is exactly one controller. PTY stdin, terminal resize, and provider reply
are a single ownership domain: while a controller exists, other client writes
and out-of-band replies/resizes are rejected or ignored. Controller changes use
an ownership generation, so delayed frames from an old controller cannot regain
write access. A controller can transfer to the next standby; controller loss
promotes the next standby. Observers are never promoted merely by waiting.

### Attach controls and modes

The default control prefix is `Ctrl+B`; a profile can set `C-a` through `C-z`.
Outside bracketed paste, prefix commands are local to UAM:

| Command | Result |
|---|---|
| `prefix d` | Detach. |
| `prefix c` | Send literal `Ctrl+C` to the provider. |
| `prefix r` | Request control. |
| `prefix o` | Transfer control when used by the controller. |
| `prefix i` | Show client role and selected/effective profile. |
| `prefix m` | Toggle mouse passthrough for this attachment only. |
| `prefix prefix` | Send a literal prefix byte to the provider. |

Bracketed-paste payload bypasses prefix parsing completely. Mouse policy controls
whether UAM locally filters provider mouse modes; it does not turn a mouse event
that the terminal or SSH client never sent into input. The temporary mouse toggle
is client-local and expires when that attachment exits.

### Protocol compatibility matrix

Protocol v1 is the legacy attach stream. Protocol v2 adds a client hello,
role/generation events, and framed server output. Both keep provider key bytes
native; UAM does not translate a provider keyboard protocol.

| Host | Client | Result |
|---|---|---|
| v1 | v1 | Legacy raw PTY output; one legacy controller. |
| v1 | v2 | v2 client accepts an unversioned legacy response and consumes raw PTY output; no v2 ownership features. |
| v2 | v1 | v2 host preserves the v1 raw stream and one-controller legacy behavior. |
| v2 | v2 | Framed PTY/control output, hello validation, controller/standby/observer roles, and generation checks. |
| Either | Explicit unsupported version | Reject the attach; do not silently downgrade an explicit version. |

A terminal name, color hint, or claimed capability in a hello is metadata used
for bounded diagnostics and safe local decisions. It is not evidence of terminal
support. Unsupported or secret-like hints are redacted in diagnostics.

### Provider terminal policy

Every provider keeps native provider input. The current outer-screen policy is:

| Provider | Outer screen | Key protocol |
|---|---|---|
| Claude Code | UAM | Native |
| OpenAI Codex | Primary | Native |
| GitHub Copilot CLI | UAM | Native |
| Hermes Agent | UAM | Native |
| Oh My Pi | UAM | Native |
| OpenCode | UAM | Native |

`TERM` supplied to the provider is fixed to the UAM-supported value; a client
TERM hint does not override it. Profiles may select provider, approval mode,
command alias, mouse policy, control prefix, quick-detach policy, and
scrollback. They cannot inject environment, terminal capability, provider
resume, or unsafe-resume policy.

### Persistence and recovery

Schema v4 stores session and profile data, not runtime client state. Older
schema files are copied to an adjacent backup before migration; migration then
writes atomically. Restore a chosen backup only while UAM is stopped and use a
binary compatible with that backup. Equal-schema unknown fields are preserved;
a newer schema is read-only to an older binary.

Stopping or rebooting ends the host and PTY. Retained records support a
provider-aware relaunch/resume according to the provider policy; they do not
preserve a live PTY, terminal modes, controller, or attached clients. Normal
detach and handled signals restore terminal modes. SIGKILL cannot execute that
cleanup, so a terminal may need `reset` or replacement.

### Diagnostics and SSH

`uam doctor --json` reports store/runtime health, profile validity, provider
terminal policy, and available protocol versions. `uam doctor <session-id>
--json` also reports role counts, selected/effective profile, and fallback
reasons. It excludes terminal content and redacts secret-like identifiers.

SSH transports bytes. It does not transport the local clipboard, right-click
meaning, or a guarantee that a terminal supports a named TERM. Use keyboard
paste bindings on the SSH client. `UAM_ATTACH_MOUSE=off` favors local selection
and paste; `auto` and `on` preserve provider mouse reporting. See
[ADR 0002](0002-terminal-ownership-over-ssh.md) for cleanup and client limits.

## Consequences

- A reconnect gets a fresh client identity and negotiated state, never an old
controller lease.
- Operators can predict which terminal may change provider input or geometry.
- Old hosts and clients keep working within the explicit v1 boundary instead of
receiving invented v2 behavior.
- Terminal smoke evidence records observed behavior; it does not turn TERM
names into a capability database.
Loading
Loading