# pi-bridge wire specification

> **This document is not a second source of truth.** The authoritative values are the
> constants and builders in code: `WIRE_ID`, `HANDSHAKE_VERSION`,
> `PLAN_CONTRACT_VERSION`, `REQUIRED_FEATURES`, `DEFAULT_CAPABILITIES`,
> `DEFAULT_DENIALS`, `ERROR_CODES`, `MAX_REPAIRS` in `handshake.ts`, and
> `RESPONSE_CONTRACT` in `protocol.ts`. This file explains what those mean, how the
> state machine moves, and what the safety rules are. If the two ever disagree, the
> code wins and this file is the bug.

Every sample below is generated by the real builders, not written by hand.

## Three versions, three jobs

| Constant | Value | Owns |
|---|---|---|
| `WIRE_ID` | `pi-bridge/1` | Framing only. Bumped when the message shapes stop being parseable at all. |
| `HANDSHAKE_VERSION` (HV) | `1` | Negotiation semantics. |
| `PLAN_CONTRACT_VERSION` (CV) | `2` | The PLAN body. |

They are separate because collapsing them into one number makes every semantic
change look like a wire change.

The contract hash covers the **whole** normative contract, including the code-block
rules, so changing those rules changes the hash. Current values (reproducible from the constants):

```
cv  = p2.478d1ef2
cap = c1.5fc20bbe
```

## Who decides what

The split is deliberately four domains rather than two. "Do not execute" and "the
other side is at fault" are different judgements, and having only one axis for
them is how a gate gets bypassed.

| Domain | Owner | Contents |
|---|---|---|
| Protocol truth | pi | Handshake, FRAME, schema, round correlation, contract version, parseability |
| **Transport truth** | pi | Whether the complete reply was really obtained; whether copy/DOM/CDP can be trusted; whether the renderer is wedged |
| Plan semantics | ChatGPT | `continue` / `done` / `blocked` / `need_info`, the technical plan, what information is needed, risk |
| Runtime truth | pi | Whether a capability actually exists, whether a guard or boundary forbids continuing, whether a checkpoint is genuinely settled |

The one-line rule:

> **ChatGPT decides what should be done. pi decides whether this message can be
> trusted, whether the current environment permits execution, and how far
> execution has actually got.**

## Session identity

| Field | Meaning |
|---|---|
| `sid` | 16 hex chars, `crypto.randomBytes(8)`. Generated once per logical session. |
| `r` | Monotonic round number, from 1. |
| `p` (prev) | `planKey` of the last plan that was **validated AND executed AND settled**, or `-`. |
| `q` (qid) | 8 hex chars, hash of the user's original request. |
| `cv` | Contract tag, `p2.<8hex>`. |
| `cap` | Capability tag, `c1.<8hex>`. |

`FRAME: s=<sid> r=<n> p=<prev> q=<qid>`

### Canonicalization

Anything that gets hashed is first canonicalized: CRLF to LF, trailing whitespace
stripped per line, runs of blank lines collapsed, trimmed. Capability hashes sort
both the capability list and the denial list before hashing, so declaration order
does not matter.

Without a canonical form you get the worst protocol failure available: same
version number, different content, so every hash comparison fails and nothing can
be diagnosed.

## Messages

### HELLO — sent once per conversation

```
# HELLO pi-bridge/1
S: 9f83a7c1d42e650b
HV: 1
CV: 2 p2.478d1ef2
CAP: c1.5fc20bbe
QID: e1db9b58
REQ: frame,resume,repair,need_info

ACTION: handshake-only
NOTE:
本轮是握手轮，不是任务轮。请只输出下面 ACK-TEMPLATE 那个代码块并填好字段，
不要产出方案，不要回答 OBJECTIVE 里的目标。真正的任务在紧接着的下一条消息里。

CAPS:
file.read,file.write,shell.local,test.repo,git.local

DENY:
net,gui,account,remote.config,remote.api

ACK-TEMPLATE:
# HS pi-bridge/1
TYPE: ack
S: 9f83a7c1d42e650b
HV: 1
CV: 2 p2.478d1ef2
CAP: c1.5fc20bbe
FEAT: frame,resume,repair,need_info

OBJECTIVE:
示例目标

CONTRACT:
# PLAN pi-bridge/1
FRAME: s=<sid> r=<round> p=<prev-plan-key> q=<qid>
DECISION: continue | done | blocked | need_info
TASK: <短标识，如 fix-selector-drift>

## GOAL
一句话说明本轮要达成的目标。

## STEPS
1. 步骤一（可执行、单一职责）
2. 步骤二

## FILES
- <相对路径> :: <要做什么改动>

## COMMANDS
- <要执行的命令，没有就写 无>

## RISKS
- <风险或不确定点，没有就写 无>

## VERIFY
- <如何验证本轮成功了>

## NOTES
<可选补充>

## 代码块规则
- 整个回复只允许有 1 个代码块，就是承载上面这份方案的那一个。
- 方案里要给出代码、命令或配置时，写在这个块内部（缩进或行内反引号），不要另开代码块。
- 不要把一份方案拆成多个代码块：复制按钮一次只能取到其中一块，pi 会拿到残缺的方案。
- 块内首行必须是 # PLAN ${PROTOCOL_ID}，第二行必须是 FRAME 行，供 pi 做轮次关联。
```

The `ACTION: handshake-only` line and the `NOTE` are load-bearing, not politeness.
Observed live: while the HELLO still carried the user's request, ChatGPT answered it
and returned a well-formed PLAN instead of an ACK — the FRAME matched, so the reply
looked legitimate — and the establish gate correctly refused to start the session. A
handshake turn must not contain anything that reads like a question.

Therefore **the request travels in ROUND 1, not in HELLO**. In full, exactly once,
per qid.

`CAPS` and `DENY` are separate on purpose. `shell.local` deliberately does **not**
imply network access, and spelling out the denials matters more than the
capabilities: "you have a shell" is what makes a model propose `curl`.

### HS — the acknowledgement

```
# HS pi-bridge/1
TYPE: ack
S: 9f83a7c1d42e650b
HV: 1
CV: 2 p2.478d1ef2
CAP: c1.5fc20bbe
FEAT: frame,resume,repair,need_info
```

### ROUND — steady state

```
# ROUND pi-bridge/1
FRAME: s=9f83a7c1d42e650b r=2 p=a1b2c3d4 q=e1db9b58

## 上轮执行回执
npm test 全绿
```

One FRAME line plus what actually changed. The contract, the capabilities and the
root request are **not** repeated here — that is where the token saving comes
from (measured at 59% over 8 rounds). The root request is sent in full exactly
once, in HELLO.

### RESUME — continuity was lost, the protocol did not change

```
# RESUME pi-bridge/1
S: 9f83a7c1d42e650b
HV: 1
CV: 2 p2.478d1ef2
CAP: c1.5fc20bbe
CK: 3:a1b2c3d4
NEXT: 4
```

`CK` is the last confirmed checkpoint, `NEXT` the round to resume at. No contract
or capability text is resent.

### REPAIR — fatal protocol error, same round

```
# REPAIR pi-bridge/1
FRAME: s=9f83a7c1d42e650b r=2 p=a1b2c3d4 q=e1db9b58
ERR: E_BLOCKS,E_FRAME
ACTION: resend-same-round
```

The round is **not** advanced and nothing is executed. Bounded by `MAX_REPAIRS`
(2); after that the loop stops with `protocol_violation`.

### RESYNC — the static protocol context was lost

```
# RESYNC pi-bridge/1

# HELLO pi-bridge/1
...
```

Same conversation, same checkpoint, but the static state is redeclared. Used when
the model has clearly lost the early protocol context, or when HV/CV/capability
hashes changed.

### PLAN — the reply

```
# PLAN pi-bridge/1
FRAME: s=<sid> r=<round> p=<prev-plan-key> q=<qid>
DECISION: continue | done | blocked | need_info
TASK: <short identifier>

## GOAL
## STEPS
## FILES
## COMMANDS
## RISKS
## VERIFY
## NOTES
```

**Exactly one fenced code block**, carrying this structure, and nothing outside
it. A code-block copy button returns only its own block, so a reply split across
several blocks cannot be captured whole.

### Decisions

| Value | Meaning |
|---|---|
| `continue` | Plan is executable; pi runs it and reports back. |
| `done` | The goal is achieved; pi stops. **Terminal.** |
| `blocked` | Depends on something outside pi's capabilities (a human, a GUI, the network, account access). |
| `need_info` | Information is missing but pi can gather it itself. Not `blocked`, not `done`. |

## Establish gate

All of these must hold before any task is sent. A half-established session is
worse than none: pi would execute plans while believing a contract is in force
that the other side never agreed to.

- header present and `WIRE_ID` matches
- `TYPE: ack`
- `S` equals the offered `sid`
- `HV` equals the offered `HV`
- `CV` and the contract hash match
- capability hash matches
- every required feature is acknowledged

## Error codes and fault attribution

| Code | Domain | Meaning |
|---|---|---|
| `E_BLOCKS` | remote | Code-block count is not exactly 1 |
| `E_HEADER` | remote | Missing or wrong `# PLAN pi-bridge/1` header |
| `E_FRAME` | remote | FRAME missing, or sid/round/prev/qid mismatch |
| `E_REQUIRED` | remote | A required field is absent |
| `E_DECISION` | remote | DECISION missing or outside the enum |
| `E_PARSE` | remote | Plan could not be parsed |
| `E_VERSION` | **version** | HV mismatch |
| `E_CONTRACT` | **version** | CV or hash mismatch |
| `E_CAPTURE` | **local** | pi could not prove it obtained the complete reply |
| `E_SOURCE_UNCERTAIN` | **local** | The source of the text is not trustworthy |

Domain determines the action:

- **remote** → REPAIR
- **version** → RESYNC (reformatting cannot fix a version mismatch)
- **local** → pi recovers its own capture. **No REPAIR**, because blaming the other
  side for our own capture failure is precisely the mis-attribution the four-domain
  split exists to prevent.

`fatal-to-execute` is **not** the same as `fault-attributed-to-the-other-side`. Both
block execution; only one justifies a REPAIR.

The dangerous option is never "warn and continue". Executing a plan you are not
sure you fully read corrupts FRAME, planKey and checkpoint credibility — the
recovery chain itself.

## Send-state: the exactly-once approximation

The web UI cannot provide transactional exactly-once. What it can provide is this:

| State | Evidence | Permitted |
|---|---|---|
| `not-sent` | Mechanically confirmed not submitted | May resend the same ROUND |
| `sent` | Mechanically confirmed submitted | Only re-read and wait; never resend |
| `unknown` | Cannot tell | **Treat as `sent`.** Never auto-resend |

> When in doubt, send one time too few — never one time too many.

This is the single most important safety rule for automated recovery. A duplicate
send is a duplicate execution; a missed send costs one retry.

Corollary: **"retry the same round" means re-attach, wait, read and capture the
same round. It does not mean re-sending the ROUND.**

## Checkpoints

```
checkpoint = the last plan that was validated AND executed AND agent settled
terminal   = the plan whose DECISION was done
status     = open | closed
```

A checkpoint advances in exactly one place: after a plan has been validated,
executed, and the agent has settled. The remote confirmation of that checkpoint is
the **next** ROUND's `p=`. A PLAN reply by itself proves only that the plan was
written, never that it ran.

`done` is recorded as `terminal` and sets `status = closed`. It is **not** folded
into `checkpoint`: the done round has no execution phase, and recording it as
"executed and settled" would make recovery guess.

**An unconfirmed plan is never replayed automatically.** Recovery resumes at the
last confirmed checkpoint, and `replayFromRound` is never earlier than
`checkpoint + 1`.

## Recovery state machine

```
NEW → HELLO_SENT → ESTABLISHED → ROUND_N → PLAN_N → EXECUTING_N → SETTLED_N → ROUND_N+1

PLAN_N_INVALID       → REPAIR_N → PLAN_N
DISCONNECTED         → RESUME   → ROUND_CURRENT
STATE_UNCERTAIN      → LAST_CONFIRMED_CHECKPOINT → RESUME
VERSION/CONTRACT_LOST→ RESYNC   → ESTABLISHED
```

### Decision table

| Failure | Preconditions | Action | Forbidden |
|---|---|---|---|
| Capture failure (`E_CAPTURE`, `E_SOURCE_UNCERTAIN`) | Target alive, conversation unchanged, frame known | Retry same round: re-read, wait, choose a trustworthy source | No REPAIR, no ROUND resend |
| Reply wait timeout, no fresh turn yet | Page alive, no blocker, FRAME consistent | Retry same round with stall backoff | Do not advance the round |
| CDP session dropped, browser/tab/conversation alive, pi runtime intact | sid/r/in-flight state in memory | Re-attach → discovery → retry same round | No new HELLO, no blind ROUND |
| `Page.reload`, re-attach, same conversation | sid/r preserved | Discovery, continue the same round; consume a valid PLAN if present | No RESYNC |
| Browser relaunched, local session cache intact | HV/CV/CAP/sid/checkpoint consistent | Discovery; RESUME if in-flight state cannot be resumed | Do not create a new sid |
| pi restarted, runtime lost, cache + history recoverable | sid/version/hash/checkpoint cross-checkable | RESUME at last confirmed checkpoint | Do not replay unconfirmed plans |
| Cache lost, FRAME chain in history complete | sid/frame/version recoverable | RESUME from last confirmed checkpoint | Do not guess whether the latest PLAN ran |
| HELLO/ACK no longer provable, conversation + checkpoint still trustworthy | Identity and checkpoint determinable | RESYNC (redeclare contract/caps) | Do not execute a new PLAN first |
| Local CV/CAP changed vs the handshake hashes | A real protocol/capability change | RESYNC / renegotiate | Do not pretend the old ACK still holds |
| FRAME chain conflict (sid clash, round regression/jump, prev unmatched) | No way to tell which history is authoritative | Give up to manual recovery | Do not pick a branch |
| Cannot determine whether a side-effecting PLAN already ran | No reliable checkpoint evidence | Give up to manual recovery | Never auto-replay |
| Captcha / logout / rate limit / account blocker | Confirmed by the guard | Give up | No generic reconnect loop |
| RESUME ACK invalid, RESYNC still inconsistent | Recovery budget exhausted | Give up | No unbounded recovery |
| Boundary already hit | `checkBoundaries()` says stop | Give up / stop | Recovery must not bypass a boundary |
| Non-stall exception (code bug, parser bug, invariant) | `classifyStallError() === null` | Throw and stop automation | Do not treat a software bug as a network fault |

### RESUME vs RESYNC

- **RESUME** — the protocol did not change; only transport/runtime continuity was
  lost. No static text is resent.
- **RESYNC** — the static protocol context was lost, or HV/CV/CAP changed. Static
  text is redeclared inside the same conversation, keeping the checkpoint.

Keeping these separate matters: if one can stand in for the other, every future
recovery bug becomes ambiguous.

## Implementation status

Verified against the real site:

- HELLO → ACK validation → session established
- contract v2 in steady state (59% fewer tokens over 8 rounds)
- fatal gate → REPAIR → the other side corrected its format and resubmitted
- frame discovery + recovery decision from real conversation history

Not yet automated:

- `planRecovery()` is reachable through `/brb resume` but is **not** wired into
  the automatic failure path of `relayWithRecovery()`. Disconnections still need a
  human to run `/brb resume`.
- `scripts/live/cycle.mjs` still speaks the pre-handshake envelope.
- No deterministic fault injection exists, so automated recovery cannot be
  verified without provoking a real disconnect.
