zcode-acp-server 0.2.0 → 0.3.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 +101 -15
- package/README.zh-CN.md +68 -13
- package/dist/bin/hub.d.ts +16 -0
- package/dist/bin/hub.d.ts.map +1 -0
- package/dist/bin/hub.js +41 -0
- package/dist/bin/hub.js.map +1 -0
- package/dist/handlers/account.d.ts +43 -0
- package/dist/handlers/account.d.ts.map +1 -0
- package/dist/handlers/account.js +59 -0
- package/dist/handlers/account.js.map +1 -0
- package/dist/handlers/io.d.ts +20 -1
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +57 -2
- package/dist/handlers/io.js.map +1 -1
- package/dist/handlers/replay.d.ts +79 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +252 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +64 -65
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +23 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +67 -6
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +47 -17
- package/dist/index.js.map +1 -1
- package/dist/remote/broadcast.d.ts +47 -0
- package/dist/remote/broadcast.d.ts.map +1 -0
- package/dist/remote/broadcast.js +121 -0
- package/dist/remote/broadcast.js.map +1 -0
- package/dist/remote/config.d.ts +32 -0
- package/dist/remote/config.d.ts.map +1 -0
- package/dist/remote/config.js +65 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/endpoint.d.ts +30 -0
- package/dist/remote/endpoint.d.ts.map +1 -0
- package/dist/remote/endpoint.js +213 -0
- package/dist/remote/endpoint.js.map +1 -0
- package/dist/remote/hub-server.d.ts +41 -0
- package/dist/remote/hub-server.d.ts.map +1 -0
- package/dist/remote/hub-server.js +346 -0
- package/dist/remote/hub-server.js.map +1 -0
- package/dist/server.d.ts +41 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +69 -11
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +17 -1
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +47 -15
- package/docs/BACKLOG.md +3 -1
- package/docs/DEVELOPMENT.md +26 -0
- package/docs/PROTOCOL.md +67 -27
- package/docs/REMOTE-CLIENTS.md +260 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +39 -6
- package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
- package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
- package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
- package/docs/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- package/package.json +5 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Proposal 0001 — Tail session replay with incremental history fetch
|
|
2
|
+
|
|
3
|
+
Status: implemented (2026-08-17; decisions in ADR-0003, contract documented in
|
|
4
|
+
REMOTE-CLIENTS.md "Tail replay and history pagination") · Date: 2026-08-17 ·
|
|
5
|
+
Affects: ACP endpoint (`session/load`), remote clients
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`session/load` replays the **entire** conversation as `session/update`
|
|
10
|
+
notifications. This is the attach path AND the reconnect catch-up path
|
|
11
|
+
(REMOTE-CLIENTS.md: "history replay is the recovery mechanism"), so its cost is
|
|
12
|
+
paid on every attach and every reconnect. For long-lived sessions the cost is
|
|
13
|
+
unbounded:
|
|
14
|
+
|
|
15
|
+
- A real working session measured today replays **2,000+ update chunks**
|
|
16
|
+
(user/agent/thought/tool) per attach. The mobile client (zcode-acp-app)
|
|
17
|
+
saturated its main thread for tens of seconds even after shipping windowed
|
|
18
|
+
rendering — every chunk used to trigger a full React commit. The client now
|
|
19
|
+
batches replay into single store writes, which fixes rendering, but the
|
|
20
|
+
**wire transfer, JSON parse, and state application remain O(full history)**
|
|
21
|
+
on every attach. That part is irreducible client-side.
|
|
22
|
+
- Mobile clients reconnect often (background suspension kills the socket), so
|
|
23
|
+
the most latency-sensitive clients pay the largest cost, repeatedly.
|
|
24
|
+
- Recovery after a network flap should be fast; instead it grows with session
|
|
25
|
+
age. The catch-up path degrades exactly when the session is most valuable.
|
|
26
|
+
|
|
27
|
+
Clients render only the tail (the app renders the last ~30 messages and loads
|
|
28
|
+
older on scroll-up), yet the protocol forces them to receive and process all
|
|
29
|
+
of it up front.
|
|
30
|
+
|
|
31
|
+
## Transport constraint (verified against SDK 1.3.0)
|
|
32
|
+
|
|
33
|
+
The SDK registers spec methods with zod `z.object` params schemas; zod's
|
|
34
|
+
default `.parse()` **strips unknown top-level keys**. A top-level `limit` on
|
|
35
|
+
`session/load` would never reach the handler. `_meta` is the preserved
|
|
36
|
+
extension channel, so all bridge extension parameters on spec methods ride in
|
|
37
|
+
`_meta.zcode`. Response fields are unaffected (the SDK's response mapping is a
|
|
38
|
+
passthrough), so `replayMeta` rides top-level in the result. Our own method
|
|
39
|
+
`session/load_earlier` takes a bridge-provided parser, so its params stay
|
|
40
|
+
top-level.
|
|
41
|
+
|
|
42
|
+
## Proposed API
|
|
43
|
+
|
|
44
|
+
Additive and backward compatible — omitting the new fields keeps today's
|
|
45
|
+
full-replay behavior byte-identical (Zed sends neither, and is unaffected).
|
|
46
|
+
|
|
47
|
+
### 1. `session/load` gains an optional tail limit (in `_meta.zcode`)
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{ "sessionId": "…", "cwd": "…", "mcpServers": [], "_meta": { "zcode": { "limit": 30 } } }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Replays at most the **last `limit` messages, aligned back to the start of
|
|
54
|
+
the turn containing the oldest one** — never a mid-turn cut, never an
|
|
55
|
+
orphaned tool_call. A turn spans from a user message to the next; leading
|
|
56
|
+
non-user messages belong to the first turn.
|
|
57
|
+
- `limit: 0` attaches with **metadata only** (no replay) — for clients that
|
|
58
|
+
build the connection first and page history on demand.
|
|
59
|
+
- Clamped to `[0, 500]`; invalid values clamp, never error.
|
|
60
|
+
- Result gains replay metadata:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"replayMeta": {
|
|
65
|
+
"cursor": "…",
|
|
66
|
+
"hasMore": true,
|
|
67
|
+
"replayedMessages": 47,
|
|
68
|
+
"replayedTurns": 12,
|
|
69
|
+
"totalMessages": 1893,
|
|
70
|
+
"totalTurns": 412
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`cursor` is an opaque bridge-chosen handle identifying the oldest replayed
|
|
76
|
+
turn (clients never interpret it). Both `session/load` and
|
|
77
|
+
`session/load_earlier` results use this same shape.
|
|
78
|
+
|
|
79
|
+
### 2. New request `session/load_earlier` (params top-level)
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "sessionId": "…", "before": "…", "limit": 50 }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- Delivers updates strictly older than `before` as `session/update`
|
|
86
|
+
notifications — the same delivery mechanism as replay, so clients reuse
|
|
87
|
+
their existing apply path. Within a batch, updates arrive oldest → newest;
|
|
88
|
+
the client prepends at the head of its history.
|
|
89
|
+
- Requires the session to already be registered in this bridge (attached via
|
|
90
|
+
`session/load`); unknown sessions error — pagination never triggers an
|
|
91
|
+
implicit backend resume.
|
|
92
|
+
- `hasMore: false` ends pagination. A cursor only expires when the history
|
|
93
|
+
shrank (session compacted/truncated, no longer matching the cursor's
|
|
94
|
+
anchor); appended turns keep it valid. An expired or unknown cursor returns
|
|
95
|
+
a fixed `"cursor expired"` error the client maps to a full re-
|
|
96
|
+
`session/load`.
|
|
97
|
+
|
|
98
|
+
## Semantics & edge cases
|
|
99
|
+
|
|
100
|
+
- **Cursor representation** (bridge-internal): opaque base64 of
|
|
101
|
+
`{ id?, index, totalTurns }`. Valid only while `index` is in range AND the
|
|
102
|
+
history it points into is unchanged; stable backend message ids are used
|
|
103
|
+
when present, with index/total as the consistency check otherwise.
|
|
104
|
+
- **Concurrent live turn.** A turn may stream while replay or pagination runs.
|
|
105
|
+
A per-session replay lock (patterned on `preemptLocks`) is held for the
|
|
106
|
+
duration of each replay batch, and live-turn dispatch for the same session
|
|
107
|
+
acquires the same lock — batches are atomic and never interleave with live
|
|
108
|
+
forwards. Concurrent `load_earlier` calls serialize naturally.
|
|
109
|
+
- **Bridge-side slicing.** The backend `session/messages` RPC has no
|
|
110
|
+
pagination; the bridge fetches full history (local stdio IPC, cheap) and
|
|
111
|
+
slices in memory — the wire to the client carries only the tail. A future
|
|
112
|
+
backend limit parameter can slot in without contract change.
|
|
113
|
+
- **Editor/stdio clients are unaffected** — no `_meta.zcode` ⇒ today's
|
|
114
|
+
behavior.
|
|
115
|
+
|
|
116
|
+
## Alternatives considered
|
|
117
|
+
|
|
118
|
+
- **Top-level `limit` on `session/load`**: rejected — the SDK's zod params
|
|
119
|
+
parsing strips unknown top-level keys; it cannot work.
|
|
120
|
+
- **Turn-count limit**: rejected — tool-heavy turns make turn budgets
|
|
121
|
+
unpredictable for UI; message count with turn alignment serves both.
|
|
122
|
+
- **`limit` only, no `load_earlier`**: simpler, but "scroll up for older" then
|
|
123
|
+
forces a full re-load — the exact cost this proposal removes.
|
|
124
|
+
- **Client-side windowing alone** (shipped in zcode-acp-app today): bounds
|
|
125
|
+
rendering but not wire/parse/apply; reconnect cost still grows unboundedly.
|
|
126
|
+
- **Push-only history with gap-fill by update id**: requires the bridge to
|
|
127
|
+
retain an update log keyed by id and clients to track continuity — heavier
|
|
128
|
+
than cursor pagination for the same result.
|
|
129
|
+
|
|
130
|
+
## Rollout
|
|
131
|
+
|
|
132
|
+
1. `_meta.zcode.limit` on `session/load` + `replayMeta` + replay lock
|
|
133
|
+
(unblocks fast mobile attach).
|
|
134
|
+
2. `session/load_earlier` (unblocks infinite-scroll into history).
|
|
135
|
+
|
|
136
|
+
REMOTE-CLIENTS.md gains the two parameters once implemented.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Proposal 0002 — Expose plan quota usage to remote clients
|
|
2
|
+
|
|
3
|
+
Status: implemented (bridge `account/usage_stats`, 2026-08-17) · Date: 2026-08-17 · Affects: bridge (new ACP method), remote clients
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The mobile client now shows the session **context bar** (`usage_update
|
|
8
|
+
{used, size}`) — that part is done. The other "usage" users care about is the
|
|
9
|
+
**plan quota**: how much of the current billing window (e.g. a coding plan's
|
|
10
|
+
prompt allowance) is consumed and when it resets. The editor shows this; a
|
|
11
|
+
remote client has no way to see it.
|
|
12
|
+
|
|
13
|
+
Today the bridge surfaces nothing for quotas:
|
|
14
|
+
|
|
15
|
+
- app-server has the RPC (`usageStats: "usage/stats"` in the method enum, plus
|
|
16
|
+
the `zcode quotas` CLI and `v4/usage/stats` backend endpoint), but the
|
|
17
|
+
bridge's BACKLOG lists `usage/stats` under **Not planned** ("desktop client
|
|
18
|
+
/ config layer").
|
|
19
|
+
- `session/usage` (per-session tokens) is also unwired (BACKLOG candidate
|
|
20
|
+
table) — lower value, the context bar already covers session-level usage.
|
|
21
|
+
|
|
22
|
+
## Proposed API (minimal, pull-only)
|
|
23
|
+
|
|
24
|
+
Quota is **account-level**, not session-level, so it does not fit a
|
|
25
|
+
`session/update` kind. One request method on the bridge, callable any time
|
|
26
|
+
after `initialize` (no session required):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "id": 7, "method": "account/usage_stats", "params": {} }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Response — shape to mirror whatever app-server's `usage/stats` actually
|
|
33
|
+
returns (fields below are the client's expectation, not a hard contract):
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plans": [
|
|
38
|
+
{
|
|
39
|
+
"id": "bigmodel-coding-plan",
|
|
40
|
+
"name": "GLM Coding Plan",
|
|
41
|
+
"used": 42,
|
|
42
|
+
"limit": 120,
|
|
43
|
+
"unit": "prompts",
|
|
44
|
+
"windowHours": 5,
|
|
45
|
+
"resetsAt": 1723812000000
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Semantics:
|
|
52
|
+
|
|
53
|
+
- Pull-only v1: the client fetches once after attach and on demand (or every
|
|
54
|
+
few minutes). No push notification needed yet — quota changes are slow.
|
|
55
|
+
- Non-standard, additive method name (`account/…`); nothing existing changes.
|
|
56
|
+
- Failure should degrade gracefully: error → the client hides the quota UI.
|
|
57
|
+
|
|
58
|
+
Implementation notes (2026-08-17):
|
|
59
|
+
|
|
60
|
+
- Data source is the bridge's own `quota/` pipeline (GLM usage API + 10s
|
|
61
|
+
cache, same as `/quota`), NOT the app-server `usage/stats` RPC — inspected
|
|
62
|
+
live, that RPC returns token analytics over a time range (per-day token
|
|
63
|
+
counts, model/tool breakdowns), not billing-window quotas. The wire shape
|
|
64
|
+
above is adapted accordingly: `usedPercent` is always present; `used`/
|
|
65
|
+
`limit` only when the API reports absolute counts; `windowHours` is derived
|
|
66
|
+
from the window id (5h → 5, week → 168).
|
|
67
|
+
- Failures map to JSON-RPC `-32003` with the kind in `data.kind`
|
|
68
|
+
(`auth_error` | `rate_limited` | `unavailable`).
|
|
69
|
+
|
|
70
|
+
## Client UI (once available)
|
|
71
|
+
|
|
72
|
+
Drawer section under Session config: one row per plan showing
|
|
73
|
+
`used/limit` with a small progress bar and a "resets in Xh" hint. Reuse of
|
|
74
|
+
the existing context-bar styling.
|
|
75
|
+
|
|
76
|
+
## Alternatives considered
|
|
77
|
+
|
|
78
|
+
- **`usage_update` extension**: wrong scope — that kind is per-session and
|
|
79
|
+
replayed on attach; quota is account-wide and would replay stale values.
|
|
80
|
+
- **Hub-level `/api/usage`**: violates ADR-0002 (the hub is a stateless byte
|
|
81
|
+
proxy with no backend connection; only bridges talk to app-server).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zcode-acp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent Client Protocol (ACP) server bridging headless ZCode to editors like Zed and JetBrains.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"bin": {
|
|
26
26
|
"zcode-acp-server": "dist/index.js",
|
|
27
|
+
"zcode-acp-hub": "dist/bin/hub.js",
|
|
27
28
|
"zcode-quota": "dist/bin/quota.js"
|
|
28
29
|
},
|
|
29
30
|
"main": "dist/index.js",
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"docs"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@agentclientprotocol/sdk": "^1.
|
|
39
|
+
"@agentclientprotocol/sdk": "^1.3.0",
|
|
40
|
+
"ws": "^8.21.3"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"zod": "^3.25.0 || ^4.0.0"
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@eslint/js": "^9.0.0",
|
|
45
47
|
"@types/node": "^22.0.0",
|
|
48
|
+
"@types/ws": "^8.18.1",
|
|
46
49
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
47
50
|
"@typescript-eslint/parser": "^8.0.0",
|
|
48
51
|
"eslint": "^9.0.0",
|