pi-remote-control 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/docs/adr/0001-package-extension-as-control-shim.md +19 -0
- package/docs/adr/0002-use-sqlite-for-daemon-state.md +19 -0
- package/docs/adr/0003-use-lock-file-as-process-state.md +19 -0
- package/docs/adr/0004-allow-loopback-pair-code-without-token.md +19 -0
- package/docs/adr/0005-defer-os-service-installation.md +19 -0
- package/docs/adr/0006-use-tui-activated-remote-control-sessions.md +24 -0
- package/docs/adr/0007-require-tui-originated-pairing.md +19 -0
- package/docs/adr/0008-use-qr-pairing-links.md +21 -0
- package/docs/adr/0009-rename-package-to-remote-control.md +19 -0
- package/docs/adr/0010-clean-stale-lock-on-status.md +19 -0
- package/docs/adr/0011-use-loopback-tui-control.md +19 -0
- package/docs/adr/0012-use-paginated-session-transcript-loading.md +37 -0
- package/docs/adr/0013-require-manual-reactivation-after-tui-entry.md +31 -0
- package/docs/adr/0014-read-transcripts-from-session-files.md +33 -0
- package/docs/adr/0015-normalize-transcript-messages-and-stream-events.md +35 -0
- package/docs/adr/0016-expose-turn-lifecycle-events.md +31 -0
- package/docs/adr/0017-bound-initial-websocket-session-state.md +31 -0
- package/docs/adr/0018-reregister-active-tui-session-on-heartbeat-miss.md +33 -0
- package/docs/adr/0019-display-only-pairing-qr-and-expiry.md +25 -0
- package/docs/adr/0020-expose-session-status-snapshots.md +31 -0
- package/docs/adr/0021-support-remote-compact-action.md +31 -0
- package/docs/adr/0022-rename-session-status-to-runtime-status.md +27 -0
- package/docs/adr/0023-return-remote-compact-results.md +29 -0
- package/docs/architecture.md +96 -0
- package/docs/data-model.md +284 -0
- package/docs/interfaces.md +470 -0
- package/package.json +37 -0
- package/scripts/http-smoke-test.sh +100 -0
- package/src/active-session-registry.ts +205 -0
- package/src/auth/pairing.ts +30 -0
- package/src/auth/tokens.ts +30 -0
- package/src/cli-runner.cjs +15 -0
- package/src/cli.ts +254 -0
- package/src/config.ts +26 -0
- package/src/extension/index.ts +422 -0
- package/src/index.ts +16 -0
- package/src/lock.ts +26 -0
- package/src/pairing-link.ts +15 -0
- package/src/paths.ts +21 -0
- package/src/persistence/daemon-store.ts +56 -0
- package/src/persistence/schema.ts +21 -0
- package/src/qr.ts +23 -0
- package/src/runtime-status.ts +116 -0
- package/src/server/http.ts +529 -0
- package/src/session-index.ts +9 -0
- package/src/session-transcript.ts +34 -0
- package/src/transcript-message.ts +76 -0
- package/src/transcript-pagination.ts +68 -0
- package/src/transcript-preview.ts +102 -0
- package/src/transcript-stream.ts +89 -0
- package/src/types.ts +116 -0
- package/tests/active-session-registry.test.ts +170 -0
- package/tests/auth.test.ts +18 -0
- package/tests/cli.test.ts +361 -0
- package/tests/config.test.ts +35 -0
- package/tests/daemon-store.test.ts +54 -0
- package/tests/extension.test.ts +617 -0
- package/tests/lock.test.ts +36 -0
- package/tests/pairing-link.test.ts +26 -0
- package/tests/pairing.test.ts +26 -0
- package/tests/paths.test.ts +29 -0
- package/tests/qr.test.ts +25 -0
- package/tests/schema.test.ts +18 -0
- package/tests/server-http.test.ts +932 -0
- package/tests/session-index.test.ts +10 -0
- package/tests/session-transcript.test.ts +75 -0
- package/tests/transcript-pagination.test.ts +54 -0
- package/tests/transcript-preview.test.ts +64 -0
- package/tests/transcript-stream.test.ts +103 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Pi Remote Control
|
|
2
|
+
|
|
3
|
+
Private relay daemon for iOS remote control of explicitly enabled Pi TUI sessions.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
Install as a Pi package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install https://github.com/zerray/pi-remote-control
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After installation, edit `~/.pi/remote-control/config.json` so iOS can reach the daemon. Use a LAN IP or Tailscale address. Exposing the daemon on a public IP is at your own risk.
|
|
14
|
+
|
|
15
|
+
LAN example:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"bindAddress": "192.168.1.23:17373",
|
|
20
|
+
"advertisedBaseUrl": "http://192.168.1.23:17373"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tailscale example:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"bindAddress": "100.86.12.34:17373",
|
|
29
|
+
"advertisedBaseUrl": "http://100.86.12.34:17373"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open a Pi TUI session and run:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
/remote-control-pair # display QR code for iOS pairing
|
|
37
|
+
/remote-control # toggle this TUI session for remote control
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Directory overview
|
|
41
|
+
|
|
42
|
+
- `scripts/http-smoke-test.sh` — curl/WebSocket smoke test for daemon HTTP endpoints.
|
|
43
|
+
- `docs/architecture.md` — daemon architecture, Pi package shape, and lifecycle boundaries.
|
|
44
|
+
- `docs/interfaces.md` — daemon public API and TUI control integration contract.
|
|
45
|
+
- `docs/data-model.md` — daemon state, pairing, device, active session, and stream structures.
|
|
46
|
+
- `docs/adr/` — accepted daemon decisions.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Package the Pi extension as a control shim
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The daemon should be installable as part of a Pi package so users can manage it from Pi. Pi extensions are loaded for Pi processes and are rebound across session replacement flows. A long-lived HTTP/WebSocket daemon should not be tied to that per-process or per-session lifecycle.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Distribute `pi-remote-control` as a Pi package containing both a daemon binary and a Pi extension. The extension is a thin control shim that registers commands for status, start, stop, and pairing. The daemon server runs as a separate singleton process started by an OS service, manual CLI, or explicit extension command.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
Installing the Pi package makes the control extension available but does not automatically start one daemon per Pi session. Multiple Pi sessions can load the extension safely because daemon startup goes through singleton health checks and lock acquisition. The daemon remains available even when no Pi TUI session is open.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Use SQLite for daemon-owned state
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The daemon needs durable state for paired devices, token hashes, short-lived pairing codes, allowed projects, daemon metadata, and stable app-facing session IDs. Full Pi transcripts are already persisted by Pi as JSONL session files and should not be duplicated by the daemon.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Store daemon-owned durable state in a SQLite database named `daemon.sqlite` under the daemon state directory. Store human-editable daemon configuration in `config.json` in the same directory. Keep Pi session transcripts in Pi's own session files and store only daemon IDs, references, and cached summary fields for sessions.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
The daemon gets transactional updates, simple backup behavior, and easy local inspection without running an external database. The daemon must manage schema migrations and filesystem permissions. If the SQLite session index is lost, it can be rebuilt from Pi session files, but device pairing state must be restored from backup or paired again.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Use daemon lock file as process state
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The daemon needs singleton enforcement and enough process metadata for `status` and `stop`. Earlier design mentioned both `daemon.lock` and `daemon.pid`, which duplicated process state and could become inconsistent.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Use only `daemon.lock` for daemon process state. The daemon creates it atomically on startup, writes its PID into it, and removes it on normal shutdown. `status` and `stop` read the PID from `daemon.lock`.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
There is a single process-state file to reason about. Duplicate starts are rejected by atomic lock creation. Stale lock handling is not yet implemented; if the daemon exits without releasing the lock, the user must remove `daemon.lock` manually before starting again.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Allow loopback pair code creation without token
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
Pairing is the bootstrap path that creates the first device bearer token. Requiring an existing bearer token for every `POST /v1/pair/code` request creates a circular dependency for first-time setup. At the same time, allowing unauthenticated pair code creation on Tailscale-reachable or public bind addresses would let any network peer initiate device pairing.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Allow unauthenticated `POST /v1/pair/code` only when the daemon is bound to a loopback address: `127.0.0.1`, `localhost`, or `::1`. For non-loopback bind addresses, pair code creation requires bearer authentication. `POST /v1/pair/claim` remains unauthenticated because the pair code itself is the short-lived proof.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
First-time setup works from the host through the CLI or Pi extension without a pre-existing token. Remote pair-code creation is still protected when the daemon is reachable over Tailscale or other non-loopback interfaces. Users who bind directly to a Tailscale IP need an existing token or must temporarily create the pair code through a loopback-bound daemon session.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Defer OS service installation
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The daemon can be started manually through the CLI or from Pi with `/remote-control start`. OS service installation through launchd or systemd would make the daemon persistent across logins and reboots, but it adds platform-specific service files, permissions, logging behavior, uninstall logic, and debugging surface.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Do not implement OS service installation for the MVP. Keep manual CLI start and Pi extension start as the supported startup paths.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
The MVP has fewer platform-specific moving parts. Users must start the daemon explicitly before using the iOS app. If repeated manual startup becomes a real usability problem, service installation can be added later with a new decision record.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Use TUI-activated remote-control sessions
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The daemon previously treated Pi sessions as daemon-owned resources: it would discover projects and sessions with Pi SDK `SessionManager`, then open, prompt, stream, and abort sessions with Pi SDK runtime or Pi RPC. That model can expose saved sessions that the user did not explicitly enable for mobile control and can conflict with a live Pi TUI process that already owns the active runtime.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
For the MVP, the Pi TUI extension owns remote-controlled sessions. The daemon lists only sessions that have been activated by the user from a Pi TUI command. The daemon does not use Pi SDK or RPC to discover, open, prompt, stream, or abort sessions. It stores pairing state and device tokens, tracks currently activated TUI sessions, relays iOS prompt and abort requests to the owning TUI extension, and broadcasts TUI-forwarded events to iOS clients.
|
|
16
|
+
|
|
17
|
+
The Pi command surface changes to two explicit commands:
|
|
18
|
+
|
|
19
|
+
- `/remote-control`: toggle remote control for the current TUI session. It starts the daemon if needed; when enabling, it registers the current session with the daemon; when disabling, it unregisters it.
|
|
20
|
+
- `/remote-control-pair`: start the daemon if needed and create a short-lived pairing code from the TUI.
|
|
21
|
+
|
|
22
|
+
# Consequences
|
|
23
|
+
|
|
24
|
+
The remote API reflects user-selected live TUI sessions rather than all saved Pi sessions or configured project roots. The daemon no longer competes with the TUI for session file ownership or runtime control. If a TUI process exits, reloads, or disables remote control, that session disappears from the iOS app. The previous `/remote-control` control-shim command shape and daemon-owned Pi runtime model are superseded.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Require TUI-originated pairing
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
Pairing creates bearer tokens for remote devices. Earlier design allowed unauthenticated pair-code creation from loopback and authenticated pair-code creation from remote clients. The new user flow is centered on explicit actions inside the Pi TUI, where the user is already present at the host-side control surface.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Pair-code creation is only available from the Pi TUI extension through `/remote-control-pair`. The remote HTTP API does not provide a pair-code creation endpoint. The iOS app may claim a short-lived pair code, but it cannot request a new code remotely.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
Pairing requires local intent from an active Pi TUI session and no longer depends on daemon bind address or remote address classification. Remote peers cannot initiate pairing-code generation. ADR 0004's loopback pair-code creation rule is superseded.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Use QR pairing links
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
Pair-code creation is restricted to the Pi TUI via `/remote-control-pair`, but a mobile device also needs the daemon endpoint to claim the code. The daemon bind address may be `127.0.0.1` or `0.0.0.0`, neither of which is a usable mobile endpoint. Manual entry of a Tailscale URL plus a short-lived code is poor setup UX.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
`/remote-control-pair` displays a QR code in the Pi TUI. The QR code encodes a `pi-remote://pair` link containing the advertised daemon base URL, pair code, and expiration time. The same information is also shown as text fallback.
|
|
16
|
+
|
|
17
|
+
The daemon config supports `advertisedBaseUrl`, which is the URL iOS should use when claiming a pair code and making future API calls.
|
|
18
|
+
|
|
19
|
+
# Consequences
|
|
20
|
+
|
|
21
|
+
The iOS app can pair by scanning one QR code. Users must configure `advertisedBaseUrl` when automatic endpoint inference would produce a loopback or wildcard address. Pair-code creation remains TUI-originated only; the QR link does not grant access beyond the short-lived claim proof.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Rename package to Pi Remote Control
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The package originally used a daemon-centered name. The current design exposes `/remote-control` and `/remote-control-pair`, with the daemon acting as the relay behind a remote-control user experience.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
Rename the package, CLI binary, daemon display name, default state directory, environment variables, logs, tests, and documentation references to `pi-remote-control` / Pi Remote Control.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
The naming matches the TUI command surface. Existing users of previous local builds may need to move state into `~/.pi/remote-control` and update environment variables to `PI_REMOTE_CONTROL_*`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
Clean stale lock on status
|
|
4
|
+
|
|
5
|
+
# Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
The Pi TUI command surface intentionally keeps `/remote-control` focused on toggling the current session. It no longer exposes daemon maintenance subcommands. The daemon still uses `daemon.lock` as the single process-state file, and stale locks can remain after crashes or forced exits.
|
|
12
|
+
|
|
13
|
+
# Decision
|
|
14
|
+
|
|
15
|
+
`pi-remote-control status` removes `daemon.lock` when the lock PID is not running. The command reports the daemon as stopped after removing the stale lock. Manual daemon termination remains an operator task outside the TUI command surface.
|
|
16
|
+
|
|
17
|
+
# Consequences
|
|
18
|
+
|
|
19
|
+
Users do not need a TUI daemon maintenance command solely to recover from stale locks. A later `/remote-control` invocation can start the daemon normally after `status` has cleaned the stale lock. `status` now has a small side effect, but only when the lock file no longer represents a live process.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Use loopback TUI control
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The daemon may bind to a Tailscale-reachable address so iOS can access it. The Pi TUI extension runs on the same host as the daemon and should not need a paired device token to register or control its own explicitly enabled session. If the extension derives its control URL from a Tailscale bind address, package-internal TUI calls can be treated like remote traffic and fail with `401`.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
The daemon listens on the configured bind address and, when that address is not loopback or wildcard, also listens on `127.0.0.1` on the same port. The Pi TUI extension uses `127.0.0.1:<configured-port>` for package-internal control calls unless `PI_REMOTE_CONTROL_LOCAL_URL` overrides it.
|
|
14
|
+
|
|
15
|
+
Package-internal `/v1/tui/*` endpoints accept unauthenticated requests from loopback clients. Non-loopback callers must still provide a valid bearer token. iOS-facing endpoints continue to require paired-device bearer authentication.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
TUI activation works without distributing device tokens to the local extension, even when iOS reaches the daemon through Tailscale. Remote peers cannot use the TUI control endpoints without authentication. The configured port must be available on both the selected remote-facing interface and loopback when binding to a specific non-loopback address.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Use paginated session transcript loading
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Use paginated session transcript loading
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Pi session histories can be long. Sending the full transcript in one session snapshot or one WebSocket message can exceed iOS WebSocket message limits and can make session detail loading slow or memory-heavy.
|
|
14
|
+
|
|
15
|
+
Raising the iOS WebSocket message-size limit treats the symptom rather than the protocol problem. The daemon needs a contract that bounds initial payload size and lets Pi Relay fetch older history only when the user asks for it by scrolling.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The daemon-to-iOS API will expose bounded transcript windows instead of unbounded session histories.
|
|
20
|
+
|
|
21
|
+
`GET /v1/sessions/{sessionId}` will return session metadata, compact tool state, streaming state, and only the most recent messages up to a requested `messageLimit`, subject to a daemon maximum.
|
|
22
|
+
|
|
23
|
+
Older messages will be loaded through `GET /v1/sessions/{sessionId}/messages?before={cursor}&limit={limit}`. Cursors are daemon values based on the oldest loaded message's `createdAt` timestamp. The encoded cursor remains opaque to the app, but it represents an exclusive timestamp upper bound for the next older page. Responses return messages in chronological order plus an optional cursor for the next older page.
|
|
24
|
+
|
|
25
|
+
The session WebSocket stream must not use an unbounded initial transcript message. Any initial session state sent over the stream follows the same bounded-message rule, and live events after subscription are incremental.
|
|
26
|
+
|
|
27
|
+
Pi Relay will initially request the latest N messages for the session detail view. When the user scrolls to the top of the loaded transcript and an older cursor is available, the app requests the next older page and prepends it to the in-memory transcript. The app de-duplicates all HTTP page and live stream merges by `ChatMessage.id`.
|
|
28
|
+
|
|
29
|
+
## Consequences
|
|
30
|
+
|
|
31
|
+
Initial session detail loads remain bounded even for long Pi histories.
|
|
32
|
+
|
|
33
|
+
The app can show recent context quickly and progressively load older history on demand.
|
|
34
|
+
|
|
35
|
+
The daemon must maintain a stable transcript paging model over Pi session history and must enforce a maximum page size.
|
|
36
|
+
|
|
37
|
+
The app must track transcript loading state, older-page cursors, and de-duplicate messages by `ChatMessage.id` when HTTP pages and live stream events overlap.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Require manual remote-control reactivation after TUI entry
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Require manual remote-control reactivation after TUI entry
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Remote control is explicitly enabled from a live Pi TUI session with `/remote-control`. If the TUI exits without cleanly unregistering, the daemon can temporarily retain an active session registration and the iOS app can continue to show that session until the daemon observes that the TUI control channel is gone.
|
|
14
|
+
|
|
15
|
+
Automatically restoring remote control when a user resumes a Pi session would make remote visibility persist across TUI process lifecycles, which weakens the explicit opt-in model.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Entering or resuming a Pi TUI session does not automatically enable or restore remote control.
|
|
20
|
+
|
|
21
|
+
On TUI session start, the extension clears local remote-control state. The user must run `/remote-control` to activate remote visibility for that TUI process.
|
|
22
|
+
|
|
23
|
+
The daemon treats missing TUI heartbeats as an inactive control channel, removes the active session registration, and notifies iOS subscribers with `session_closed`.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
Exiting the TUI is effectively a remote-control deactivation, even if shutdown cleanup is missed.
|
|
28
|
+
|
|
29
|
+
Stale sessions disappear from the iOS app after the heartbeat timeout.
|
|
30
|
+
|
|
31
|
+
Users must explicitly run `/remote-control` after entering or resuming a TUI session before the session is remotely visible or controllable again.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Read transcripts from session files
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Read transcripts from session files
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The daemon currently keeps active-session process state and can derive transcript messages from the TUI registration payload. That makes HTTP session snapshots depend on daemon memory that can become stale after new TUI messages arrive.
|
|
14
|
+
|
|
15
|
+
Pi session JSONL files are already the persisted source of truth for transcript history. Maintaining a second full transcript projection in the daemon duplicates Pi's state and requires event-specific update logic for message starts, deltas, ends, and tool output.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The daemon will read transcript history from the active session's `sessionFile` when serving HTTP session snapshots and transcript pages.
|
|
20
|
+
|
|
21
|
+
The active session registry will store control metadata, ownership, heartbeat state, command queues, compact live state, and the `sessionFile` path. It will not be the source of truth for completed transcript history.
|
|
22
|
+
|
|
23
|
+
`GET /v1/sessions/{sessionId}` and `GET /v1/sessions/{sessionId}/messages` will derive their bounded message windows from the Pi session JSONL file at request time. The WebSocket stream remains the live incremental channel for in-progress events; HTTP transcript reads represent persisted transcript state from the session file.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
HTTP snapshots and older pages reflect the latest transcript data that Pi has persisted to disk, even after daemon restarts or after messages arrive following remote-control activation.
|
|
28
|
+
|
|
29
|
+
The daemon avoids maintaining a duplicate full transcript state machine in memory.
|
|
30
|
+
|
|
31
|
+
In-progress streaming deltas that Pi has not yet persisted may only be visible through WebSocket live events until the session file is updated.
|
|
32
|
+
|
|
33
|
+
Long transcript reads must remain bounded by the API's `messageLimit` and page `limit` contracts.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Normalize transcript messages and stream events
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Normalize transcript messages and stream events
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
HTTP transcript snapshots are derived from Pi session JSONL files, while WebSocket streams currently forward raw Pi TUI extension events. Pi session messages contain structured content blocks such as text, thinking, and tool calls. Raw stream events contain lifecycle and delta events for the same assistant messages plus tool execution events.
|
|
14
|
+
|
|
15
|
+
Keeping different public shapes for historical transcript messages and live updates forces the iOS app to maintain separate rendering paths and causes fields such as thinking and tool calls to be lost when the daemon flattens transcript history to plain text.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The daemon-to-iOS API will use `TranscriptMessage` as the public transcript message shape for both HTTP transcript reads and WebSocket live updates.
|
|
20
|
+
|
|
21
|
+
HTTP session snapshots and transcript pages will return persisted `TranscriptMessage` values parsed from the Pi session JSONL `sessionFile`. The parser will preserve structured content blocks including text, thinking, tool calls, images, and tool-result metadata.
|
|
22
|
+
|
|
23
|
+
The WebSocket stream will not broadcast raw Pi extension events to iOS. The daemon will normalize TUI-forwarded Pi events into daemon-owned stream events that create, patch, replace, or close `TranscriptMessage` values and update tool execution state. The stream remains incremental and bounded; it does not replay unbounded history.
|
|
24
|
+
|
|
25
|
+
The Pi extension may continue to send raw Pi events to the daemon over the package-internal TUI control interface. Raw Pi event shapes are not part of the public iOS API.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
The iOS app renders historical transcript pages and live updates with one transcript model.
|
|
30
|
+
|
|
31
|
+
Thinking and tool-call content are preserved in HTTP snapshots, HTTP pages, and live stream updates.
|
|
32
|
+
|
|
33
|
+
The daemon owns the compatibility boundary between Pi event formats and the public iOS protocol.
|
|
34
|
+
|
|
35
|
+
The WebSocket protocol changes incompatibly for clients that consume raw Pi events.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Expose turn lifecycle events
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Expose turn lifecycle events
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The normalized transcript stream exposes message lifecycle events, tool execution events, and session closure. Clients can infer some progress from `transcript_message_end`, but a Pi turn can include assistant output, tool calls, tool results, and follow-up assistant output. The app needs an explicit daemon-owned signal for turn boundaries instead of relying on raw Pi events or session snapshots.
|
|
14
|
+
|
|
15
|
+
Pi emits `turn_start` and `turn_end` extension events for each turn. These events are package-internal inputs today and are not part of the public iOS stream protocol.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The daemon-to-iOS WebSocket protocol will include normalized `turn_start` and `turn_end` stream events.
|
|
20
|
+
|
|
21
|
+
The Pi extension will forward Pi `turn_start` and `turn_end` events to the daemon while remote control is active. The daemon will normalize them and broadcast only the public lifecycle fields needed by the app. Raw Pi turn event payloads are not exposed to iOS.
|
|
22
|
+
|
|
23
|
+
`turn_start` marks that a model/tool turn is active. `turn_end` marks that the turn is complete. Transcript content remains represented by `TranscriptMessage` and tool execution events.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
The iOS app can update per-turn loading state without waiting for a new session snapshot.
|
|
28
|
+
|
|
29
|
+
The stream protocol remains incremental and normalized.
|
|
30
|
+
|
|
31
|
+
Turn lifecycle events do not replace `transcript_message_end`; clients should still use message events to update transcript content.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Bound initial WebSocket session state
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Bound initial WebSocket session state
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The WebSocket stream sends an initial `session_state` event when an iOS client subscribes to a session. That state currently follows the transcript page default and can contain many `TranscriptMessage` values. After transcript normalization, a single message can include large structured payloads such as `write.arguments.content`, `edit.arguments.edits[].oldText`, `edit.arguments.edits[].newText`, or large tool-result text.
|
|
14
|
+
|
|
15
|
+
Large initial WebSocket messages can exceed iOS WebSocket message limits and fail with `NSPOSIXErrorDomain Code=40 "Message too long"`. The immediate observed failure is the initial `session_state`, not live incremental events.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The daemon will bound only the initial WebSocket `session_state` payload for now.
|
|
20
|
+
|
|
21
|
+
When a WebSocket subscriber connects, the daemon will request at most 20 recent transcript messages for the initial `session_state`. HTTP session snapshots and explicit transcript-page endpoints keep their existing requested-limit behavior.
|
|
22
|
+
|
|
23
|
+
Before sending the initial WebSocket `session_state`, the daemon will truncate oversized string payloads inside transcript messages to their first 10 KiB of UTF-8 data. Truncated fields will be marked so clients can display that the value is a preview. The daemon will not add a generic WebSocket bounded sender in this decision, and live incremental stream events are not changed by this decision.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
Initial WebSocket subscription is less likely to exceed iOS message-size limits while still giving the app recent context.
|
|
28
|
+
|
|
29
|
+
Clients that need older or fuller transcript data should load it through HTTP session snapshot and transcript-page endpoints.
|
|
30
|
+
|
|
31
|
+
Large live events may still need a later protocol decision if they exceed client limits.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Re-register active TUI session on heartbeat miss
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Re-register active TUI session on heartbeat miss
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The daemon tracks active TUI sessions in process memory and removes registrations when heartbeats stop. System sleep pauses the TUI heartbeat timer and the daemon sweep timer, but wall-clock time still advances. After wake, the daemon can prune the active session before the TUI extension sends another heartbeat.
|
|
14
|
+
|
|
15
|
+
The iOS app then correctly sees no active projects because the daemon registration is gone. The TUI can still show `Remote Control Active` because that status is local extension state and the polling heartbeat currently does not repair a missing daemon registration.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Keep the daemon's simple heartbeat timeout and in-memory active-session registry behavior.
|
|
20
|
+
|
|
21
|
+
When a TUI extension still considers a session locally remote-control active, but its heartbeat command poll receives `session_not_found` from the daemon, the extension re-registers the current TUI session with the daemon.
|
|
22
|
+
|
|
23
|
+
If re-registration succeeds, the extension keeps local remote control active and continues polling. If re-registration fails, the extension clears local remote-control state, removes the status indicator, stops polling, and notifies the user that remote control disconnected.
|
|
24
|
+
|
|
25
|
+
This recovery only applies to a session that was already locally active in the current TUI process. Entering or resuming a TUI session still does not automatically enable remote control.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
System sleep, daemon restart, or daemon in-memory state loss can be repaired by the next TUI heartbeat without requiring daemon sleep detection.
|
|
30
|
+
|
|
31
|
+
The iOS app may briefly show no active projects after wake until the TUI heartbeat runs and re-registers the session. Remote prompts sent during that gap can still fail with `session_not_active`.
|
|
32
|
+
|
|
33
|
+
The daemon remains simple and continues to remove stale sessions by heartbeat timeout and owner PID checks.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Display only pairing QR and expiry
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Display only pairing QR and expiry
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`/remote-control-pair` previously displayed the numeric pair code and raw pairing link as text fallback in addition to the QR code. The app pairing flow uses the QR code, and showing the raw bootstrap code and full link in the TUI adds unnecessary sensitive and noisy output.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
`/remote-control-pair` displays only the QR code and its expiration time. The pair code and pairing link remain encoded inside the QR code but are not printed as separate TUI text lines.
|
|
18
|
+
|
|
19
|
+
This supersedes the text-fallback display portion of ADR 0008.
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
|
|
23
|
+
The pairing output is shorter and exposes less bootstrap material in terminal text.
|
|
24
|
+
|
|
25
|
+
Users pair by scanning the QR code before the displayed expiration time.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Expose session status snapshots
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Expose session status snapshots
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The iOS app needs to display the same kind of session status currently shown in the Pi TUI footer: model, thinking level, token/cache usage, cost, and context window usage.
|
|
14
|
+
|
|
15
|
+
The Pi extension API exposes enough structured data to compute this status from the live TUI session context, including `ctx.model`, `pi.getThinkingLevel()`, `ctx.sessionManager`, and `ctx.getContextUsage()`. The daemon cannot compute the live TUI status by itself because active sessions are owned by the TUI extension and the daemon does not use Pi SDK session runtime APIs.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Add a package-level `session_status` snapshot for active remote-control sessions.
|
|
20
|
+
|
|
21
|
+
The TUI extension computes the snapshot from the live session context when a session is registered and whenever relevant status inputs change. The daemon stores the latest snapshot in the active-session registry, includes it in HTTP session snapshots and initial WebSocket session state, and broadcasts a `session_status` WebSocket event when the snapshot changes.
|
|
22
|
+
|
|
23
|
+
The status snapshot is structured data, not rendered TUI footer text. It includes current model metadata, thinking level, cumulative usage and cost, and context usage. Context token and percentage values may be `null` when Pi reports them as unknown.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
The iOS app can render session status without parsing terminal UI output.
|
|
28
|
+
|
|
29
|
+
The app's displayed status is eventually consistent with the TUI and updates when the live TUI extension reports changes.
|
|
30
|
+
|
|
31
|
+
The daemon remains independent of Pi SDK session runtime APIs and only relays status produced by the owning TUI extension.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Support remote compact action
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Support remote compact action
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The iOS app needs to trigger the same operation as the Pi TUI `/compact` command for an active remote-control session.
|
|
14
|
+
|
|
15
|
+
Pi's extension API exposes `ctx.compact()` for triggering compaction from the live TUI extension context. Pi does not expose all built-in interactive slash commands as a stable generic command registry suitable for remote passthrough, and many interactive commands require local TUI UI flows.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Support `/compact` as an explicit allowlisted remote session action, not as generic remote slash-command passthrough.
|
|
20
|
+
|
|
21
|
+
The daemon exposes an authenticated iOS endpoint for compacting an active session. The daemon enqueues a `remote_compact` command for the owning TUI extension. The TUI extension handles that command by calling `ctx.compact()` for the current live session.
|
|
22
|
+
|
|
23
|
+
If the target session is not active, the daemon returns `409 session_not_active`, matching remote prompt and abort behavior.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
The iOS app can request compaction for an active remote-control session with a small protocol addition.
|
|
28
|
+
|
|
29
|
+
The implementation avoids exposing arbitrary TUI slash commands remotely and avoids having to reproduce interactive TUI command flows in the app.
|
|
30
|
+
|
|
31
|
+
Additional remote slash-command-like operations require separate explicit protocol decisions.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Rename session status to runtime status
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Rename session status to runtime status
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
ADR 0020 introduced a `session_status` snapshot and WebSocket event for live model, usage, cost, and context information. The name is too similar to the existing initial WebSocket `session_state` event and can cause protocol confusion.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
Rename the snapshot concept to `RuntimeStatus`.
|
|
18
|
+
|
|
19
|
+
Public session state uses the field `runtimeStatus`. The WebSocket update event is `runtime_status`. Package-internal TUI status reports use `{ "type": "runtime_status", "status": RuntimeStatus }`.
|
|
20
|
+
|
|
21
|
+
This ADR supersedes only the naming chosen in ADR 0020. The underlying decision to expose live status snapshots is unchanged.
|
|
22
|
+
|
|
23
|
+
## Consequences
|
|
24
|
+
|
|
25
|
+
The protocol distinguishes initial full session state (`session_state`) from incremental runtime status updates (`runtime_status`).
|
|
26
|
+
|
|
27
|
+
Clients can treat `runtimeStatus` as live metadata about the owning TUI runtime rather than as another session-state payload.
|