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.
Files changed (64) hide show
  1. package/README.md +101 -15
  2. package/README.zh-CN.md +68 -13
  3. package/dist/bin/hub.d.ts +16 -0
  4. package/dist/bin/hub.d.ts.map +1 -0
  5. package/dist/bin/hub.js +41 -0
  6. package/dist/bin/hub.js.map +1 -0
  7. package/dist/handlers/account.d.ts +43 -0
  8. package/dist/handlers/account.d.ts.map +1 -0
  9. package/dist/handlers/account.js +59 -0
  10. package/dist/handlers/account.js.map +1 -0
  11. package/dist/handlers/io.d.ts +20 -1
  12. package/dist/handlers/io.d.ts.map +1 -1
  13. package/dist/handlers/io.js +57 -2
  14. package/dist/handlers/io.js.map +1 -1
  15. package/dist/handlers/replay.d.ts +79 -0
  16. package/dist/handlers/replay.d.ts.map +1 -0
  17. package/dist/handlers/replay.js +252 -0
  18. package/dist/handlers/replay.js.map +1 -0
  19. package/dist/handlers/session.d.ts.map +1 -1
  20. package/dist/handlers/session.js +64 -65
  21. package/dist/handlers/session.js.map +1 -1
  22. package/dist/handlers/slash.d.ts +23 -1
  23. package/dist/handlers/slash.d.ts.map +1 -1
  24. package/dist/handlers/slash.js +67 -6
  25. package/dist/handlers/slash.js.map +1 -1
  26. package/dist/index.js +47 -17
  27. package/dist/index.js.map +1 -1
  28. package/dist/remote/broadcast.d.ts +47 -0
  29. package/dist/remote/broadcast.d.ts.map +1 -0
  30. package/dist/remote/broadcast.js +121 -0
  31. package/dist/remote/broadcast.js.map +1 -0
  32. package/dist/remote/config.d.ts +32 -0
  33. package/dist/remote/config.d.ts.map +1 -0
  34. package/dist/remote/config.js +65 -0
  35. package/dist/remote/config.js.map +1 -0
  36. package/dist/remote/endpoint.d.ts +30 -0
  37. package/dist/remote/endpoint.d.ts.map +1 -0
  38. package/dist/remote/endpoint.js +213 -0
  39. package/dist/remote/endpoint.js.map +1 -0
  40. package/dist/remote/hub-server.d.ts +41 -0
  41. package/dist/remote/hub-server.d.ts.map +1 -0
  42. package/dist/remote/hub-server.js +346 -0
  43. package/dist/remote/hub-server.js.map +1 -0
  44. package/dist/server.d.ts +41 -7
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +69 -11
  47. package/dist/server.js.map +1 -1
  48. package/dist/utils.d.ts +1 -1
  49. package/dist/utils.d.ts.map +1 -1
  50. package/dist/utils.js +17 -1
  51. package/dist/utils.js.map +1 -1
  52. package/docs/ARCHITECTURE.md +47 -15
  53. package/docs/BACKLOG.md +3 -1
  54. package/docs/DEVELOPMENT.md +26 -0
  55. package/docs/PROTOCOL.md +67 -27
  56. package/docs/REMOTE-CLIENTS.md +260 -0
  57. package/docs/REPLAY-GUIDE.md +131 -0
  58. package/docs/TROUBLESHOOTING.md +39 -6
  59. package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
  60. package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
  61. package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
  62. package/docs/proposals/0001-tail-session-replay.md +136 -0
  63. package/docs/proposals/0002-plan-quota-usage.md +81 -0
  64. 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.2.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.1.0"
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",