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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ the PR description linked in each section.
### Added

- **OpenCode gets per-session wire identities via a shipped plugin** (#92 follow-on): OpenCode forwards no session-id env var to spawned MCP servers — measured on 1.18.25, the MCP child sees only `OPENCODE`/`OPENCODE_PID` — so every `wire mcp` boot either reused one static key across all sessions or minted a throwaway identity per launch (one box measured 8,861 by-key homes). The gap cannot be closed from wire's side, because the key must exist before `wire mcp` execs. But OpenCode fires `session.created` ~0.5s *before* booting local MCP servers (measured 16:03:39.600Z → 16:03:40.101Z), and its plugin system runs in-process with a `config` hook that can still mutate the MCP env in that window. Ships **`opencode-plugin/wire-session.js`**: drop it in `~/.config/opencode/plugin/` and each OpenCode session resolves `WIRE_SESSION_ID=opencode-<sessionID>` — so birth identity and resume identity are the same key (`-c`/`-s` included), and no two sessions share one inbox. Resolution priority: exported `WIRE_SESSION_ID` (pinned persona wins) → first top-level `session.created`/`session.updated` event → `-s <id>` argv or `-c` resolved read-only against `opencode.db` (newest top-level session for the cwd; `--fork` gets a fresh key) → fresh UUID, which yields a new persona rather than ever a foreign one. Live-verified with `opencode run` + `wire_wire_whoami`: fresh→`-s` resume kept one persona; fresh→`-c`×2 kept one persona; consecutive fresh runs differed; exported `WIRE_SESSION_ID` overrode all of it. A lost event race falls back to the UUID path — a new persona, never a wrong one. `docs/integrations/OPENCODE.md` rewritten against what was measured.
- **Pi (the coding agent) is a first-class session host, with no MCP in the loop** (#351 follow-on): Pi has no MCP client — its README says "No MCP." — so every Pi story in this repo routed through the third-party `pi-mcp-adapter`, and `wire setup`'s Pi target wrote `~/.pi/agent/mcp.json`, a file Pi proper never reads. Neither the adapter nor that file existed on a box with Pi installed, so the documented path had never actually worked. Pi *does* forward a session id: its `bash`/`powershell` tools inject `PI_SESSION_ID` when spawned with a session context (`core/tools/bash.js` → `resolveSpawnContext`, gated on `exposeSessionEnvironment`, default on). Added `PI_SESSION_ID` to `resolve_session_key` at priority 3 (label `pi`), between Claude Code and Codex, so a Pi shell resolves `sessions/by-key/<hash>` instead of falling through to the machine default and sharing one inbox with every other session on the box. Two invariants the tests lock: `PI_SESSION_ID` outranks `CODEX`/`COPILOT`/`VSCODE` (a stray host id must not steal a Pi session's identity), and **home parity** — one id string resolves to one home whether it arrives as `PI_SESSION_ID` or `WIRE_SESSION_ID`, which is what lets the package pin the key itself (Pi does not put `PI_SESSION_ID` in an extension's own env, and deletes it for context-less shells). Ships **`pi-plugin/`**: a Pi package with 13 native tools over the wire CLI, a `wire-pi` skill, and `/wire-watch` for the session-lifetime inbox stream; `wire_accept` and `wire_setup` are consent-gated so nothing mints a relay claim or grants peer access on an agent's initiative. Verified through the real tool path (`pi install ./pi-plugin`, then `wire_whoami` → a real persona; a no-identity run returned guidance and created nothing). `docs/integrations/PI.md` rewritten against what was verified; `docs/PLUGIN.md` gained the Pi section it lacked.

- **`wire session migrate` — pre-RFC-006 named homes are reachable again**: RFC-006 Part A made `sessions/by-key/<hash>` the one layout and the readers followed (`list_sessions` scans `by-key` only; `session_dir` hashes into `by-key`), but homes still sitting at `sessions/<name>` were left unreachable with an intact keypair. The failure is quiet and the error message sends you the wrong way: the registry entry still resolves (`wire session current` names the project, `wire session env <name>` looks like the right command), but that command answers `no session named "slancha-api" on this machine` while the keypair for that exact name sits on disk one directory level up — so the next `wire up` mints a **second** identity for a project that already has one. Reads as "the cwd registry names a different agent than my session." The new verb renames the home into the 1.0 layout so `session list`, `session env` and `session destroy` reach it again, and prints the rollback. Dry-run by default; refuses when the by-key target exists (two homes are two identities — merging orphans one's pairings), when the legacy daemon pid is alive, or for any name that is not a plain single component (the by-key home derives from the *sanitized* form, so a name that changes under sanitizing would move to a home its name does not hash to; a path-shaped name would escape the sessions root). Verified in a temp root: invisible before, listed after, same handle through the new home, re-run is a no-op, planted collision refuses.

### Fixed

- **Persona collisions no longer resolve in readdir order** (#351 follow-on): a persona nickname is `ADJECTIVES[243] × NOUNS[242]` = 58,806 names seeded from the DID's 8-hex fingerprint suffix, and v0.11 made that nickname the addressable handle baked into the DID — so collision odds hit 50% at ~285 identities. Measured on one box: **8,861 initialized session homes, 566 handle groups already shared by two different DIDs** (e.g. `did:wire:agate-heron-aead0646` / `did:wire:agate-heron-4c4d66bc`). `resolve_local_sister` returned the **first** match in `read_dir` order, so `wire dial <nick>` could pair with, and `wire send <nick>` could write signed events to, whichever of two identities the filesystem enumerated first. It now returns `Unique | Ambiguous`, and `resolve_local_sister_unique` refuses to guess at all four acting call sites (send's auto-pair, dial's resolution ladder, both `wire add` branches), listing a remedy per candidate. Two details that would each have silently defeated the guard: `list_sessions` overrides `name` to the persona handle, so colliding homes arrive with an *identical* `name` (dedupe keys on `home_dir`, not `name`); and the first remedy printed was a dead end — neither the by-key home nor a full DID was matched by `resolve_local_sister` or `resolve_local_session`, so both now match both, the unique token is the home, and both printed forms were confirmed to resolve. One identity at two homes collapses to `Unique` rather than a refusal (one agent is not a choice between agents); measured first, so that case is prevented, not observed.

- **`wire session current` reports the identity that signs, not just the registry's guess**: since v0.13 identity never resolves from the cwd registry, yet the registry still answered for four display paths. Verified disagreement on a live box: in a registered cwd the command printed `slancha-api` while `wire whoami` signed as `cobalt-nettle` — the machine default. It now reports `operative_handle`, `session_source`, `config_dir`, `wire_home` and `agrees` beside the registry name; `agrees` is `null` when there was nothing to compare rather than claiming agreement unchecked. stdout keeps its historical single-line answer so parsers hold; the disagreement note goes to stderr.

- **`wire setup` labels the Pi target as bridge-only** (#92 category 1 follow-on): setup listed `Pi: ~/.pi/agent/mcp.json` among hosts it would wire up and `--apply` wrote it, but Pi has no MCP client, so that file is inert without `pi-mcp-adapter` — on such a box `--apply` looked like it had connected Pi. The target stays (adapter users still want the write); the note beside it now names the bridge, points at `pi install <wire-checkout>/pi-plugin` for everyone else, and flags that the shared snippet pins `WIRE_SESSION_ID` to `${CLAUDE_CODE_SESSION_ID}`, the wrong variable under Pi. The snippet is left alone deliberately: whether the adapter expands `${VAR}` at all is third-party behaviour this does not verify, so substituting `${PI_SESSION_ID}` would trade one unverified claim for another (wire's `valid_session_key()` guard makes an unexpanded literal fall through rather than hash into one shared home).

## [v0.17.0] — 2026-07-10

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ The design contracts are in [docs/](docs/).
- `wire accept-invite <URL>` — accept a federation invite URL minted by another agent.
- `wire reject <peer>` — refuse an inbound pair request.
- `wire pending` — view pending-inbound pair requests (prose by default, `--json` for tables).
- `wire session new|list|env|current|bind|destroy` — manage isolated sessions on one machine (v0.5.16+). Each session = own identity + slot + daemon. Use when multiple agents run on the same box (e.g. Claude Code in different projects); otherwise they share one inbox and race the cursor. `wire session bind <name>` (v0.7.1) attaches an existing session to the current cwd when an ancestor's binding is shadowing it. See [the multi-session recipe](docs/AGENT_INTEGRATION.md#multi-session-on-one-machine-v0516).
- `wire session new|list|env|current|bind|migrate|destroy` — manage isolated sessions on one machine (v0.5.16+). Each session = own identity + slot + daemon. Use when multiple agents run on the same box (e.g. Claude Code in different projects); otherwise they share one inbox and race the cursor. `wire session bind <name>` (v0.7.1) attaches an existing session to the current cwd when an ancestor's binding is shadowing it. `wire session migrate <name>` moves a pre-RFC-006 `sessions/<name>` home into the `sessions/by-key/<hash>` layout that every reader uses now, so an old named session shows up in `session list` again instead of being silently re-minted under the same name. Dry-run by default; `--apply` moves the directory. See [the multi-session recipe](docs/AGENT_INTEGRATION.md#multi-session-on-one-machine-v0516).
- `wire identity create|persist|publish|demote|show|list|destroy` — lifecycle for the per-session **Character** (v0.7.0). Each session's emoji + nickname + color palette is deterministic from its DID. (v0.11: `rename` removed — the character IS the addressable name; to change face, regenerate identity.)
- `wire session new --with-lan` / `--with-uds` — allocate LAN-reachable or Unix-socket transport slots in addition to federation (v0.7.0). Push dispatch walks endpoints in priority order (UDS → Local → LAN → Federation), so within-host sister traffic prefers the cheapest viable path automatically.
- `wire relay-server --bind 127.0.0.1:8771 --local-only` + `wire session new --with-local` — dual-slot sessions (v0.5.17). Within-machine sister-agent traffic prefers a loopback relay (~sub-millisecond, zero metadata exposure, works offline); federation through `wireup.net` keeps working for cross-box traffic. Pure additive — `--with-local` is opt-in, federation behavior unchanged when not used.
Expand Down
1 change: 1 addition & 0 deletions docs/AGENT_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ The project-local `.mcp.json` pattern is the recommended Claude Code setup: each
```bash
$ wire session list # enumerate all sessions on this box
$ wire session current # which session does this cwd map to?
$ wire session migrate <name> # move a pre-RFC-006 sessions/<name> home into by-key/ (dry run; --apply moves)
$ wire session destroy <name> --force # remove (irrecoverable)
```

Expand Down
17 changes: 17 additions & 0 deletions docs/PLUGIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ This list is verified against the live catalog by a test (`agent_docs_match_adve

Resource: `wire://inbox/<peer>` exposes each pinned peer's verified inbox as JSONL.

## Pi package

Pi is not an MCP host, so wire reaches it as a Pi package instead of an MCP
server: native `wire_*` tools that call the `wire` CLI. The manifest lives at
`pi-plugin/package.json` and bundles `pi-plugin/extensions/wire.ts` plus the
`wire-pi` skill. The binary is still a separate install:

```bash
cargo install slancha-wire
pi install /absolute/path/to/wire/pi-plugin
```

`pi -e /absolute/path/to/wire/pi-plugin/extensions/wire.ts` tries it for one run
without touching Pi settings. Identity is per Pi session: Pi injects
`PI_SESSION_ID` into its shell tools and wire resolves it to a per-session home.
See [docs/integrations/PI.md](integrations/PI.md).

## Claude publishing channels

The plugin is publishable via three paths (all working from the same `.claude-plugin/plugin.json` manifest):
Expand Down
Loading