# Ticket #13 B0 - Backend transport inventory

Date: 2026-07-15
Branch: `feature/ticket13-server-client-mode`

Scope: read-only inventory of the current Tauri backend transport surface in
`crates/app-tauri`. No production code change is part of B0.

Architect ticket #13 target: keep desktop Tauri and add server/client mode on a
shared backend core. Future web transport is HTTP request/response plus a
dedicated WebSocket for PTY/live streams.

## Source anchors

- Tauri command registration: `crates/app-tauri/src/lib.rs:153`
- Composition root and `AppState`: `crates/app-tauri/src/state.rs:738`,
  `crates/app-tauri/src/state.rs:1058`
- PTY Tauri Channel bridge: `crates/app-tauri/src/pty.rs:1`
- Structured chat Tauri Channel bridge: `crates/app-tauri/src/chat.rs:1`
- Domain event relay: `crates/app-tauri/src/events.rs:23`,
  `crates/app-tauri/src/events.rs:1107`
- Desktop window commands/events: `crates/app-tauri/src/commands.rs:2325`
- Shutdown/window lifecycle wiring: `crates/app-tauri/src/lib.rs:80`

## Classification rules

- `request/response`: one Tauri `invoke` maps to one backend result. HTTP
  candidate.
- `stream`: command uses `tauri::ipc::Channel` or controls a live stream/session.
  WebSocket candidate, sometimes with a small HTTP control equivalent.
- `event`: backend push currently emitted through Tauri global events. WebSocket
  live/event frame candidate unless desktop-specific.
- `desktop-only`: OS windows, Tauri path/lifecycle/dialog composition. Not part
  of the web client transport.

## Exposed Tauri commands

### Core/projects/permissions

| Command | Class | Current boundary |
| --- | --- | --- |
| `health` | request/response | `State<AppState>.health` |
| `create_project` | request/response | `create_project` use case |
| `open_project` | request/response | `open_project`, plus open-time reconciliation/wiring |
| `close_project` | request/response | `close_project` |
| `list_projects` | request/response | `list_projects` |
| `read_project_context` | request/response | project FS read use case |
| `update_project_context` | request/response | project FS write use case |
| `get_project_permissions` | request/response | permission read use case |
| `update_project_permissions` | request/response | permission write use case |
| `update_agent_permissions` | request/response | permission override write use case |
| `resolve_agent_permissions` | request/response | effective permission read use case |

### PTY terminals

| Command | Class | Current boundary |
| --- | --- | --- |
| `open_terminal` | stream | Spawns PTY, registers `Channel<PtyChunk>`, starts output pump |
| `write_terminal` | stream/control | Writes bytes to live PTY |
| `resize_terminal` | stream/control | Resizes live PTY |
| `close_terminal` | stream/control | Kills PTY and unregisters channel |
| `reattach_terminal` | stream | Reads scrollback, replaces `Channel<PtyChunk>`, starts new pump |

Notes:

- `PtyChunk = Vec<u8>` today (`crates/app-tauri/src/pty.rs:29`).
- The transport-specific registry is `PtyBridge`, stored in `AppState`.
- High-frequency PTY bytes are explicitly excluded from global Tauri events.

### Layouts

| Command | Class | Current boundary |
| --- | --- | --- |
| `load_layout` | request/response | layout read use case |
| `mutate_layout` | request/response | layout mutation use case, emits `LayoutChanged` |
| `list_layouts` | request/response | named layout list |
| `create_layout` | request/response | named layout create |
| `rename_layout` | request/response | named layout rename |
| `delete_layout` | request/response | named layout delete |
| `set_active_layout` | request/response | active layout write |

### First-run, profiles, local models, embedders

| Command | Class | Current boundary |
| --- | --- | --- |
| `first_run_state` | request/response | first-run read use case |
| `reference_profiles` | request/response | reference profile catalog |
| `detect_profiles` | request/response | runtime detection use case |
| `list_profiles` | request/response | profile store read |
| `save_profile` | request/response | profile upsert |
| `clone_opencode_profile_from_seed` | request/response | profile clone |
| `delete_profile` | request/response | profile delete |
| `configure_profiles` | request/response | first-run save |
| `list_model_servers` | request/response | local model server config read |
| `save_model_server` | request/response | local model server config write |
| `preview_model_server_command` | request/response | pure command preview |
| `delete_model_server` | request/response | local model server config delete |
| `list_embedder_profiles` | request/response | embedder profile read |
| `save_embedder_profile` | request/response | embedder profile write |
| `delete_embedder_profile` | request/response | embedder profile delete |
| `describe_embedder_engines` | request/response | embedder engine capabilities |
| `dismiss_embedder_suggestion` | request/response | suggestion state write |

### Agents, live state, chat and resume

| Command | Class | Current boundary |
| --- | --- | --- |
| `create_agent` | request/response | agent manifest/context creation |
| `list_agents` | request/response | manifest read |
| `get_project_work_state` | request/response | read-model aggregation |
| `read_conversation_page` | request/response | paginated transcript read |
| `list_live_agents` | request/response | in-memory live registry read |
| `attach_live_agent` | request/response | live session rebind use case |
| `stop_live_agent` | request/response | live session stop use case |
| `read_agent_context` | request/response | agent Markdown read |
| `update_agent_context` | request/response | agent Markdown write |
| `delete_agent` | request/response | manifest/context delete |
| `inspect_conversation` | request/response | best-effort conversation inspection |
| `launch_agent` | stream | Spawns/reuses PTY or structured session, may register `Channel<PtyChunk>` |
| `change_agent_profile` | request/response | hot-swap use case; may affect live session |
| `agent_send` | stream | Structured chat turn, registers `Channel<ReplyChunk>`, pumps reply stream |
| `cancel_resume` | request/response | cancels scheduled auto-resume |
| `set_resume_at` | request/response | human-set resume time |
| `interrupt_agent` | request/response | orchestrator interrupt |
| `delegation_delivered` | request/response | frontend write-portal ack |
| `set_front_attached` | request/response | frontend mounted/unmounted signal |
| `reattach_agent_chat` | stream | Replaces `Channel<ReplyChunk>`, returns chat scrollback |
| `close_agent_session` | request/response | closes live agent/structured state |
| `list_resumable_agents` | request/response | resumable agent inventory |

Structured chat stream:

- `ReplyChunk` is tagged by `kind`: `textDelta`, `toolActivity`, `final`,
  `error` (`crates/app-tauri/src/dto.rs:1760`).
- `ChatBridge` retains bounded scrollback and the current optional channel.
- `agent_send` is a turn command plus streaming response; in HTTP/WS terms it
  should become a WS client frame that starts a turn on an attached session.

### Templates

| Command | Class | Current boundary |
| --- | --- | --- |
| `create_template` | request/response | template create |
| `update_template` | request/response | template update, emits drift/update events |
| `list_templates` | request/response | template list |
| `delete_template` | request/response | template delete |
| `create_agent_from_template` | request/response | agent creation from template |
| `detect_agent_drift` | request/response | drift inventory |
| `sync_agent_with_template` | request/response | sync write |

### Git

| Command | Class | Current boundary |
| --- | --- | --- |
| `git_status` | request/response | git status use case |
| `git_stage` | request/response | git stage use case |
| `git_unstage` | request/response | git unstage use case |
| `git_commit` | request/response | git commit use case |
| `git_branches` | request/response | branch list |
| `git_checkout` | request/response | branch checkout |
| `git_log` | request/response | log read |
| `git_init` | request/response | repo init |
| `git_graph` | request/response | graph read |

### Tickets and sprints

| Command | Class | Current boundary |
| --- | --- | --- |
| `ticket_create` | request/response | ticket create |
| `ticket_read` | request/response | ticket read |
| `ticket_delete` | request/response | ticket delete |
| `open_ticket_chat` | request/response | ephemeral ticket assistant open |
| `close_ticket_chat` | request/response | ephemeral ticket assistant close |
| `ticket_list` | request/response | ticket list |
| `ticket_update` | request/response | ticket update |
| `ticket_read_carnet` | request/response | carnet read |
| `ticket_update_carnet` | request/response | carnet write |
| `ticket_link` | request/response | link tickets |
| `ticket_unlink` | request/response | unlink tickets |
| `ticket_assign` | request/response | assign/unassign ticket agent |
| `sprint_create` | request/response | sprint create |
| `sprint_list` | request/response | sprint list |
| `sprint_rename` | request/response | sprint rename |
| `sprint_reorder` | request/response | sprint reorder |
| `sprint_delete` | request/response | sprint delete |
| `ticket_assign_sprint` | request/response | ticket sprint assignment |
| `ticket_unassign_sprint` | request/response | ticket sprint removal |

### Skills and memory

| Command | Class | Current boundary |
| --- | --- | --- |
| `create_skill` | request/response | skill create |
| `update_skill` | request/response | skill update |
| `list_skills` | request/response | skill list |
| `delete_skill` | request/response | skill delete |
| `assign_skill_to_agent` | request/response | skill assignment |
| `unassign_skill_from_agent` | request/response | skill unassignment |
| `create_memory` | request/response | memory create |
| `update_memory` | request/response | memory update |
| `list_memories` | request/response | memory list |
| `get_memory` | request/response | memory read |
| `delete_memory` | request/response | memory delete |
| `read_memory_index` | request/response | memory index read |
| `recall_memory` | request/response | memory recall |
| `resolve_memory_links` | request/response | memory link resolution |

### Background tasks

| Command | Class | Current boundary |
| --- | --- | --- |
| `spawn_background_command` | request/response | creates command-backed background task |
| `cancel_background_task` | request/response | cancels task |
| `retry_background_task` | request/response | retries task |
| `list_background_tasks` | request/response | task read-model list |

Lifecycle is pushed through `BackgroundTaskChanged` domain events; initial list is
request/response.

### Desktop windows and focus

| Command | Class | Current boundary |
| --- | --- | --- |
| `set_focused_project` | desktop-only + event | Tauri window/panel focus state; emits `focused-project://changed` |
| `get_focused_project` | desktop-only | Tauri panel state read |
| `list_open_view_windows` | desktop-only | `AppHandle.webview_windows()` |
| `open_view_window` | desktop-only | `WebviewWindowBuilder` |
| `close_view_window` | desktop-only | closes Tauri OS window |
| `move_tab_to_new_window` | desktop-only | use case plus `WebviewWindowBuilder` |

These do not migrate as-is to web. The shared backend may still keep workspace
topology use cases, but the OS-window adapter remains desktop-only.

## Tauri Channel usage

| Location | Channel type | Producer | Consumer | Web mapping |
| --- | --- | --- | --- | --- |
| `open_terminal` | `Channel<PtyChunk>` | PTY output pump | xterm view | WS `output` frames after `open_terminal` |
| `reattach_terminal` | `Channel<PtyChunk>` | PTY output pump | xterm view | WS `attached` + `output` frames after `attach_terminal` |
| `launch_agent` | `Channel<PtyChunk>` | PTY output pump for raw CLI agents | xterm/chat terminal view | WS `output` frames; structured sessions do not use this channel |
| `agent_send` | `Channel<ReplyChunk>` | structured reply pump | chat view | WS `chat.output` or generic live frames |
| `reattach_agent_chat` | `Channel<ReplyChunk>` | structured reply pump | chat view | WS attached/replay for structured session |

Transport-owned registries:

- `PtyBridge`: session id -> `(generation, Channel<PtyChunk>)`.
- `ChatBridge`: session id -> `{generation, Option<Channel<ReplyChunk>>, scrollback}`.

These should not descend into domain/application. Their backend-core equivalent
should be a transport-neutral outbound sink/subscription registry.

## Events emitted today

### Global domain event relay

`events::spawn_relay` subscribes to `TokioBroadcastEventBus` and emits:

- Tauri event name `domain://event`
- Payload `DomainEventDto`, tagged by `type`
- Skips `DomainEvent::PtyOutput`; PTY bytes use per-session channels

Current `DomainEventDto` variants:

- Project/lifecycle: `projectCreated`, `layoutChanged`, `remoteConnected`
- Agents: `agentLaunched`, `agentLaunchFailed`, `agentExited`,
  `agentBusyChanged`, `agentProfileChanged`, `agentLivenessChanged`,
  `agentRateLimited`, `agentResumeScheduled`, `agentResumeCancelled`,
  `agentResumed`, `agentRateLimitSuspected`
- Delegation/orchestration: `delegationReady`, `agentReplied`,
  `agentAnnouncement`, `orchestratorRequestProcessed`, `orchestratorChanged`
- Background work: `backgroundTaskChanged`, `agentInboxChanged`,
  `agentWakeChanged`
- Templates/skills: `templateUpdated`, `agentDriftDetected`, `agentSynced`,
  `skillAssigned`
- Git: `gitStateChanged`
- Tickets/sprints: `issueCreated`, `issueUpdated`, `issueDeleted`,
  `issueStatusChanged`, `issuePriorityChanged`, `issueCarnetUpdated`,
  `issueLinked`, `issueUnlinked`, `issueAgentAssigned`,
  `issueAgentUnassigned`, `sprintCreated`, `sprintRenamed`,
  `sprintReordered`, `sprintDeleted`, `issueSprintChanged`
- Memory/embedder: `memorySaved`, `memoryDeleted`, `memoryIndexRebuilt`,
  `embedderSuggested`
- Model server: `modelServerStatusChanged`
- PTY placeholder: `ptyOutput` exists in DTO but is not emitted by the global
  relay

### Dedicated global events

- `model_server_status_changed`: emitted in addition to `domain://event` for
  `ModelServerStatusChanged`.
- `agent_launch_failed`: emitted in addition to `domain://event` for
  `AgentLaunchFailed`.
- `view-window://lifecycle`: desktop-only, payload `{kind, panel, label}`.
- `focused-project://changed`: desktop-only, payload `{project}`.

For web, the domain-event set maps naturally to a low-frequency live WS stream.
The duplicate dedicated events should be normalized in the shared core contract
or kept as compatibility aliases only in the Tauri adapter.

## `tauri::State<AppState>` composition root inventory

`AppState::build(app_data_dir)` is the current composition root. It constructs
concrete adapters and use cases, then Tauri stores it through `app.manage`.

### Concrete adapters and infra state currently built there

- `TokioBroadcastEventBus`
- `SystemClock`
- `UuidGenerator`
- `LocalFileSystem`
- `FsProjectStore`
- `FsWindowStateStore`
- `PortablePtyAdapter` with sandbox enforcer
- `TerminalSessions`
- `StructuredSessions`
- `StructuredSessionFactory`
- `LocalProcessSpawner`
- `CliAgentRuntime`
- `FsProfileStore`
- local model server manager/use cases
- issue/ticket stores and providers
- `PtyBridge` and `ChatBridge` transport bridges
- profile/template/git/skill/memory/embedder stores and use cases
- orchestrator service, watchers and per-project MCP servers
- session-limit service, resume contexts and turn watcher
- background task runner/store
- focused project mutex
- home/app-data derived paths

### Boundary that must be extracted

Current shape:

```text
React -> @tauri invoke/listen/Channel
      -> app-tauri commands/events/bridges
      -> AppState concrete composition root
      -> application use cases + infrastructure adapters
```

Target shape:

```text
React -> TS gateways
      -> Tauri adapter OR HTTP+WS adapter
      -> driving adapter calls shared backend core facade
      -> application use cases + infrastructure adapters
```

The extraction line is inside `app-tauri::state`:

- Move reusable construction into a shared backend core crate/facade, e.g.
  `BackendCore::build(config)`.
- Keep Tauri-only concerns in `app-tauri`: resolving `app_data_dir` via Tauri
  path API, `app.manage`, command registration, `AppHandle`/`WebviewWindow`,
  shutdown hooks, Tauri event emission, Tauri `Channel` bridges, dialog plugin.
- The future web server adapter should own HTTP route registration, TLS/pairing
  integration, WS connection lifecycle, CORS/origin checks and WS sink mapping.
- The core should expose transport-neutral methods for the request/response
  use cases and transport-neutral subscription/sink APIs for PTY/chat/live
  output.

Do not move `PtyBridge` or `ChatBridge` as-is. They are Tauri transport
adapters. Move the underlying concept: attach/detach a sink to a live
session, replay bounded scrollback, and deliver ordered output.

## HTTP DTO first draft

Naming below keeps current DTO semantics and camelCase fields. Exact paths are a
proposal for B1/B3; they can be namespaced by project where useful.

### Request/response examples

```http
GET /api/health
POST /api/projects
GET /api/projects
POST /api/projects/{projectId}/open
POST /api/projects/{projectId}/close
GET /api/projects/{projectId}/context
PUT /api/projects/{projectId}/context
GET /api/projects/{projectId}/layout
POST /api/projects/{projectId}/layout/mutate
GET /api/projects/{projectId}/agents
POST /api/projects/{projectId}/agents
GET /api/projects/{projectId}/agents/{agentId}/context
PUT /api/projects/{projectId}/agents/{agentId}/context
DELETE /api/projects/{projectId}/agents/{agentId}
GET /api/projects/{projectId}/work-state
GET /api/projects/{projectId}/conversation-page
GET /api/projects/{projectId}/git/status
POST /api/projects/{projectId}/git/stage
POST /api/projects/{projectId}/git/commit
GET /api/tickets
POST /api/tickets
GET /api/tickets/{ref}
PATCH /api/tickets/{ref}
GET /api/templates
POST /api/templates
GET /api/background-tasks
POST /api/background-tasks
```

Errors should preserve `ErrorDto`:

```json
{
  "code": "NOT_FOUND",
  "message": "project ..."
}
```

## WebSocket first draft

One dedicated WS endpoint is enough for B5 V1:

```text
GET /ws/live?token=...   (token normally via Authorization/cookie, not URL in prod)
```

Authentication note from ticket #13: pairing/token must travel only over HTTPS
and not as a URL secret. The query above is only a sketch; production should use
`Authorization: Bearer` during WS upgrade or an HTTPS-only secure cookie.

### Common envelope

Client to server:

```json
{
  "id": "client-msg-uuid",
  "kind": "terminal.input",
  "payload": {}
}
```

Server to client:

```json
{
  "id": "server-msg-uuid",
  "replyTo": "client-msg-uuid",
  "kind": "terminal.status",
  "payload": {}
}
```

`replyTo` is present for command acknowledgements, absent for unsolicited live
events/output.

### PTY client -> server frames

`attach_terminal`

```json
{
  "id": "1",
  "kind": "terminal.attach",
  "payload": {
    "sessionId": "uuid",
    "lastSeq": 42
  }
}
```

`open_terminal`

```json
{
  "id": "2",
  "kind": "terminal.open",
  "payload": {
    "projectId": "uuid",
    "nodeId": "uuid-or-null",
    "cwd": "/abs/path",
    "rows": 30,
    "cols": 120
  }
}
```

`launch_agent`

```json
{
  "id": "3",
  "kind": "agent.launch",
  "payload": {
    "projectId": "uuid",
    "agentId": "uuid",
    "nodeId": "uuid-or-null",
    "rows": 30,
    "cols": 120,
    "conversationId": "engine-conversation-id-or-null"
  }
}
```

`input`

```json
{
  "id": "4",
  "kind": "terminal.input",
  "payload": {
    "sessionId": "uuid",
    "bytesBase64": "DQ=="
  }
}
```

`resize`

```json
{
  "id": "5",
  "kind": "terminal.resize",
  "payload": {
    "sessionId": "uuid",
    "rows": 40,
    "cols": 140
  }
}
```

`detach`

```json
{
  "id": "6",
  "kind": "terminal.detach",
  "payload": {
    "sessionId": "uuid"
  }
}
```

`close`

```json
{
  "id": "7",
  "kind": "terminal.close",
  "payload": {
    "sessionId": "uuid"
  }
}
```

`ping`

```json
{
  "id": "8",
  "kind": "ping",
  "payload": {
    "atMs": 1784123456789
  }
}
```

### PTY server -> client frames

`attached`

```json
{
  "kind": "terminal.attached",
  "replyTo": "1",
  "payload": {
    "session": { "sessionId": "uuid", "projectId": "uuid", "nodeId": "uuid", "rows": 30, "cols": 120 },
    "nextSeq": 43,
    "scrollback": [
      { "seq": 1, "bytesBase64": "..." }
    ],
    "gap": false
  }
}
```

`output`

```json
{
  "kind": "terminal.output",
  "payload": {
    "sessionId": "uuid",
    "seq": 43,
    "bytesBase64": "..."
  }
}
```

`status`

```json
{
  "kind": "terminal.status",
  "payload": {
    "sessionId": "uuid",
    "status": "running"
  }
}
```

Suggested status values: `starting`, `running`, `exited`, `closed`,
`detached`, `reattached`.

`error`

```json
{
  "kind": "error",
  "replyTo": "7",
  "payload": {
    "code": "NOT_FOUND",
    "message": "terminal session ..."
  }
}
```

`pong`

```json
{
  "kind": "pong",
  "replyTo": "8",
  "payload": {
    "atMs": 1784123456789
  }
}
```

### Structured chat frames

The ticket #13 PTY contract does not require structured chat in B5, but the
current backend already has a second stream. To avoid a later incompatibility,
reserve a parallel namespace:

Client:

```json
{ "id": "9", "kind": "chat.attach", "payload": { "sessionId": "uuid" } }
{ "id": "10", "kind": "chat.send", "payload": { "sessionId": "uuid", "prompt": "..." } }
{ "id": "11", "kind": "chat.detach", "payload": { "sessionId": "uuid" } }
```

Server:

```json
{
  "kind": "chat.attached",
  "replyTo": "9",
  "payload": {
    "sessionId": "uuid",
    "scrollback": [
      { "kind": "textDelta", "text": "..." }
    ]
  }
}
```

```json
{
  "kind": "chat.output",
  "payload": {
    "sessionId": "uuid",
    "chunk": { "kind": "final", "content": "..." }
  }
}
```

### Low-frequency live events

Domain events can be pushed on the same WS connection:

```json
{
  "kind": "event.domain",
  "payload": {
    "type": "agentBusyChanged",
    "agentId": "uuid",
    "busy": true
  }
}
```

This replaces Tauri `listen("domain://event")` for web. Desktop can keep the
existing event relay.

## Plan impact / risks found

No finding invalidates the ticket #13 lot plan.

Points to decide early in B1/B2:

- The duplicate Tauri registration of `detect_agent_drift` in
  `generate_handler!` is harmless for B0 but should be cleaned in a separate fix.
- `open_project` currently also triggers open-time reconciliation and per-project
  runtime side effects. The shared backend core facade should make those explicit
  methods, not hide them in transport commands.
- `launch_agent` has both raw PTY and structured-session branches. B5 PTY V1 can
  implement raw PTY first, but the WS namespace should reserve chat/live frames.
- `PtyBridge` and `ChatBridge` contain transport scrollback/replay behavior.
  Extract the behavior as a core subscription contract; do not leak Tauri
  `Channel` into the future core.
- Desktop window/focus commands are cleanly separable as desktop-only. Workspace
  topology can remain shared, OS-window creation cannot.
