zcode-acp-server 0.1.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 (223) hide show
  1. package/README.md +192 -9
  2. package/README.zh-CN.md +143 -8
  3. package/dist/backend/client.d.ts +12 -4
  4. package/dist/backend/client.d.ts.map +1 -1
  5. package/dist/backend/client.js +72 -7
  6. package/dist/backend/client.js.map +1 -1
  7. package/dist/backend/credentials.d.ts.map +1 -1
  8. package/dist/backend/credentials.js +2 -1
  9. package/dist/backend/credentials.js.map +1 -1
  10. package/dist/backend/listener.d.ts +12 -4
  11. package/dist/backend/listener.d.ts.map +1 -1
  12. package/dist/backend/listener.js +68 -12
  13. package/dist/backend/listener.js.map +1 -1
  14. package/dist/backend/types.d.ts +1 -1
  15. package/dist/backend/types.d.ts.map +1 -1
  16. package/dist/bin/hub.d.ts +16 -0
  17. package/dist/bin/hub.d.ts.map +1 -0
  18. package/dist/bin/hub.js +41 -0
  19. package/dist/bin/hub.js.map +1 -0
  20. package/dist/bin/quota.d.ts +54 -0
  21. package/dist/bin/quota.d.ts.map +1 -0
  22. package/dist/bin/quota.js +333 -0
  23. package/dist/bin/quota.js.map +1 -0
  24. package/dist/config/auto-compact.d.ts +23 -0
  25. package/dist/config/auto-compact.d.ts.map +1 -0
  26. package/dist/config/auto-compact.js +67 -0
  27. package/dist/config/auto-compact.js.map +1 -0
  28. package/dist/config/mcp-discovery.d.ts +34 -0
  29. package/dist/config/mcp-discovery.d.ts.map +1 -0
  30. package/dist/config/mcp-discovery.js +153 -0
  31. package/dist/config/mcp-discovery.js.map +1 -0
  32. package/dist/config/model-cache.d.ts +12 -1
  33. package/dist/config/model-cache.d.ts.map +1 -1
  34. package/dist/config/model-cache.js +30 -8
  35. package/dist/config/model-cache.js.map +1 -1
  36. package/dist/config/options.d.ts +87 -9
  37. package/dist/config/options.d.ts.map +1 -1
  38. package/dist/config/options.js +248 -45
  39. package/dist/config/options.js.map +1 -1
  40. package/dist/config/plugin-commands.d.ts +24 -0
  41. package/dist/config/plugin-commands.d.ts.map +1 -0
  42. package/dist/config/plugin-commands.js +107 -0
  43. package/dist/config/plugin-commands.js.map +1 -0
  44. package/dist/config/provider-registry.d.ts +60 -0
  45. package/dist/config/provider-registry.d.ts.map +1 -0
  46. package/dist/config/provider-registry.js +128 -0
  47. package/dist/config/provider-registry.js.map +1 -0
  48. package/dist/config/runtime-model.d.ts +54 -18
  49. package/dist/config/runtime-model.d.ts.map +1 -1
  50. package/dist/config/runtime-model.js +105 -61
  51. package/dist/config/runtime-model.js.map +1 -1
  52. package/dist/config/skill-discovery.d.ts +35 -0
  53. package/dist/config/skill-discovery.d.ts.map +1 -0
  54. package/dist/config/skill-discovery.js +188 -0
  55. package/dist/config/skill-discovery.js.map +1 -0
  56. package/dist/handlers/account.d.ts +43 -0
  57. package/dist/handlers/account.d.ts.map +1 -0
  58. package/dist/handlers/account.js +59 -0
  59. package/dist/handlers/account.js.map +1 -0
  60. package/dist/handlers/background-tasks.d.ts +72 -0
  61. package/dist/handlers/background-tasks.d.ts.map +1 -0
  62. package/dist/handlers/background-tasks.js +330 -0
  63. package/dist/handlers/background-tasks.js.map +1 -0
  64. package/dist/handlers/dispatch.d.ts.map +1 -1
  65. package/dist/handlers/dispatch.js +134 -22
  66. package/dist/handlers/dispatch.js.map +1 -1
  67. package/dist/handlers/extensions.d.ts +15 -0
  68. package/dist/handlers/extensions.d.ts.map +1 -1
  69. package/dist/handlers/extensions.js +43 -27
  70. package/dist/handlers/extensions.js.map +1 -1
  71. package/dist/handlers/io.d.ts +31 -3
  72. package/dist/handlers/io.d.ts.map +1 -1
  73. package/dist/handlers/io.js +108 -9
  74. package/dist/handlers/io.js.map +1 -1
  75. package/dist/handlers/replay.d.ts +79 -0
  76. package/dist/handlers/replay.d.ts.map +1 -0
  77. package/dist/handlers/replay.js +252 -0
  78. package/dist/handlers/replay.js.map +1 -0
  79. package/dist/handlers/server-requests.d.ts +20 -4
  80. package/dist/handlers/server-requests.d.ts.map +1 -1
  81. package/dist/handlers/server-requests.js +293 -63
  82. package/dist/handlers/server-requests.js.map +1 -1
  83. package/dist/handlers/session.d.ts +113 -10
  84. package/dist/handlers/session.d.ts.map +1 -1
  85. package/dist/handlers/session.js +932 -277
  86. package/dist/handlers/session.js.map +1 -1
  87. package/dist/handlers/slash.d.ts +37 -1
  88. package/dist/handlers/slash.d.ts.map +1 -1
  89. package/dist/handlers/slash.js +143 -8
  90. package/dist/handlers/slash.js.map +1 -1
  91. package/dist/index.js +76 -12
  92. package/dist/index.js.map +1 -1
  93. package/dist/interaction/adapter.d.ts +19 -33
  94. package/dist/interaction/adapter.d.ts.map +1 -1
  95. package/dist/interaction/adapter.js +80 -79
  96. package/dist/interaction/adapter.js.map +1 -1
  97. package/dist/lazy-sessions.d.ts +35 -0
  98. package/dist/lazy-sessions.d.ts.map +1 -0
  99. package/dist/lazy-sessions.js +98 -0
  100. package/dist/lazy-sessions.js.map +1 -0
  101. package/dist/quota/cache.d.ts +18 -0
  102. package/dist/quota/cache.d.ts.map +1 -0
  103. package/dist/quota/cache.js +32 -0
  104. package/dist/quota/cache.js.map +1 -0
  105. package/dist/quota/client.d.ts +30 -0
  106. package/dist/quota/client.d.ts.map +1 -0
  107. package/dist/quota/client.js +57 -0
  108. package/dist/quota/client.js.map +1 -0
  109. package/dist/quota/color.d.ts +58 -0
  110. package/dist/quota/color.d.ts.map +1 -0
  111. package/dist/quota/color.js +95 -0
  112. package/dist/quota/color.js.map +1 -0
  113. package/dist/quota/combined.d.ts +66 -0
  114. package/dist/quota/combined.d.ts.map +1 -0
  115. package/dist/quota/combined.js +179 -0
  116. package/dist/quota/combined.js.map +1 -0
  117. package/dist/quota/format.d.ts +105 -0
  118. package/dist/quota/format.d.ts.map +1 -0
  119. package/dist/quota/format.js +219 -0
  120. package/dist/quota/format.js.map +1 -0
  121. package/dist/quota/index.d.ts +23 -0
  122. package/dist/quota/index.d.ts.map +1 -0
  123. package/dist/quota/index.js +42 -0
  124. package/dist/quota/index.js.map +1 -0
  125. package/dist/quota/opencode-go/cache.d.ts +17 -0
  126. package/dist/quota/opencode-go/cache.d.ts.map +1 -0
  127. package/dist/quota/opencode-go/cache.js +31 -0
  128. package/dist/quota/opencode-go/cache.js.map +1 -0
  129. package/dist/quota/opencode-go/client.d.ts +28 -0
  130. package/dist/quota/opencode-go/client.d.ts.map +1 -0
  131. package/dist/quota/opencode-go/client.js +48 -0
  132. package/dist/quota/opencode-go/client.js.map +1 -0
  133. package/dist/quota/opencode-go/config.d.ts +37 -0
  134. package/dist/quota/opencode-go/config.d.ts.map +1 -0
  135. package/dist/quota/opencode-go/config.js +58 -0
  136. package/dist/quota/opencode-go/config.js.map +1 -0
  137. package/dist/quota/opencode-go/format.d.ts +36 -0
  138. package/dist/quota/opencode-go/format.d.ts.map +1 -0
  139. package/dist/quota/opencode-go/format.js +87 -0
  140. package/dist/quota/opencode-go/format.js.map +1 -0
  141. package/dist/quota/opencode-go/index.d.ts +30 -0
  142. package/dist/quota/opencode-go/index.d.ts.map +1 -0
  143. package/dist/quota/opencode-go/index.js +108 -0
  144. package/dist/quota/opencode-go/index.js.map +1 -0
  145. package/dist/quota/opencode-go/parse.d.ts +41 -0
  146. package/dist/quota/opencode-go/parse.d.ts.map +1 -0
  147. package/dist/quota/opencode-go/parse.js +75 -0
  148. package/dist/quota/opencode-go/parse.js.map +1 -0
  149. package/dist/quota/opencode-go/types.d.ts +48 -0
  150. package/dist/quota/opencode-go/types.d.ts.map +1 -0
  151. package/dist/quota/opencode-go/types.js +11 -0
  152. package/dist/quota/opencode-go/types.js.map +1 -0
  153. package/dist/quota/parse.d.ts +33 -0
  154. package/dist/quota/parse.d.ts.map +1 -0
  155. package/dist/quota/parse.js +200 -0
  156. package/dist/quota/parse.js.map +1 -0
  157. package/dist/quota/types.d.ts +72 -0
  158. package/dist/quota/types.d.ts.map +1 -0
  159. package/dist/quota/types.js +10 -0
  160. package/dist/quota/types.js.map +1 -0
  161. package/dist/remote/broadcast.d.ts +47 -0
  162. package/dist/remote/broadcast.d.ts.map +1 -0
  163. package/dist/remote/broadcast.js +121 -0
  164. package/dist/remote/broadcast.js.map +1 -0
  165. package/dist/remote/config.d.ts +32 -0
  166. package/dist/remote/config.d.ts.map +1 -0
  167. package/dist/remote/config.js +65 -0
  168. package/dist/remote/config.js.map +1 -0
  169. package/dist/remote/endpoint.d.ts +30 -0
  170. package/dist/remote/endpoint.d.ts.map +1 -0
  171. package/dist/remote/endpoint.js +213 -0
  172. package/dist/remote/endpoint.js.map +1 -0
  173. package/dist/remote/hub-server.d.ts +41 -0
  174. package/dist/remote/hub-server.d.ts.map +1 -0
  175. package/dist/remote/hub-server.js +346 -0
  176. package/dist/remote/hub-server.js.map +1 -0
  177. package/dist/server.d.ts +139 -1
  178. package/dist/server.d.ts.map +1 -1
  179. package/dist/server.js +179 -4
  180. package/dist/server.js.map +1 -1
  181. package/dist/tasks-index.d.ts +14 -4
  182. package/dist/tasks-index.d.ts.map +1 -1
  183. package/dist/tasks-index.js +145 -46
  184. package/dist/tasks-index.js.map +1 -1
  185. package/dist/translators/event-translator.d.ts +33 -0
  186. package/dist/translators/event-translator.d.ts.map +1 -1
  187. package/dist/translators/event-translator.js +101 -0
  188. package/dist/translators/event-translator.js.map +1 -1
  189. package/dist/translators/index.d.ts +1 -1
  190. package/dist/translators/index.d.ts.map +1 -1
  191. package/dist/translators/index.js +1 -1
  192. package/dist/translators/index.js.map +1 -1
  193. package/dist/translators/projection-differ.d.ts +8 -0
  194. package/dist/translators/projection-differ.d.ts.map +1 -1
  195. package/dist/translators/projection-differ.js +19 -9
  196. package/dist/translators/projection-differ.js.map +1 -1
  197. package/dist/translators/tool-helpers.d.ts +28 -0
  198. package/dist/translators/tool-helpers.d.ts.map +1 -1
  199. package/dist/translators/tool-helpers.js +104 -0
  200. package/dist/translators/tool-helpers.js.map +1 -1
  201. package/dist/translators/types.d.ts +38 -1
  202. package/dist/translators/types.d.ts.map +1 -1
  203. package/dist/translators/types.js.map +1 -1
  204. package/dist/utils.d.ts +36 -7
  205. package/dist/utils.d.ts.map +1 -1
  206. package/dist/utils.js +69 -5
  207. package/dist/utils.js.map +1 -1
  208. package/docs/ARCHITECTURE.md +130 -40
  209. package/docs/BACKLOG.md +59 -0
  210. package/docs/DEVELOPMENT.md +31 -0
  211. package/docs/PROTOCOL.md +328 -23
  212. package/docs/REMOTE-CLIENTS.md +260 -0
  213. package/docs/REPLAY-GUIDE.md +131 -0
  214. package/docs/TROUBLESHOOTING.md +131 -14
  215. package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
  216. package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
  217. package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
  218. package/docs/agents/domain.md +51 -0
  219. package/docs/agents/issue-tracker.md +22 -0
  220. package/docs/agents/triage-labels.md +15 -0
  221. package/docs/proposals/0001-tail-session-replay.md +136 -0
  222. package/docs/proposals/0002-plan-quota-usage.md +81 -0
  223. package/package.json +10 -6
@@ -0,0 +1,23 @@
1
+ # Remote access: stateless hub over per-bridge ACP endpoints
2
+
3
+ Remote clients must reach any active bridge through a single tunneled port
4
+ (Cloudflare Tunnel / frp); exposing one port per bridge does not survive that
5
+ constraint. We decided each bridge serves its own ACP endpoint on loopback
6
+ only (auto-incrementing ports from 8378), and a machine-singleton hub
7
+ (`zcode-acp-hub`, fixed port 8377, auto-spawned detached by bridges, idle-exits
8
+ after ~10 minutes without registrations) does exactly three things: token
9
+ authentication, instance discovery, and byte-level WebSocket proxying
10
+ (`WS /acp?instance=<id>` → the chosen bridge).
11
+
12
+ The hub has no ACP semantics and no business state; session authority stays
13
+ in the bridges. A remote connection binds to one instance for its whole
14
+ lifetime; switching instances means opening a new connection. We rejected a
15
+ globally-routing gateway that aggregates sessions across bridges and speaks
16
+ ACP itself — it would re-create session management outside the bridges, which
17
+ is the "fat hub" design this project deliberately avoids.
18
+
19
+ The hub is the only public entry point, so it is the only place that enforces
20
+ the token; the tunnel maps exactly this one port. WebSocket ping/pong
21
+ heartbeat (~30s) is mandatory on both hub and proxy connections because ACP
22
+ streams are silent when idle and proxy layers (notably Cloudflare) drop idle
23
+ connections.
@@ -0,0 +1,40 @@
1
+ # Tail replay: extension params ride in _meta, history pages by cursor
2
+
3
+ `session/load` replays full history to attaching clients, so attach and
4
+ reconnect cost grow with session age (Proposal 0001). Extending the protocol
5
+ for tail replay required three decisions that are now wire contract and hard
6
+ to reverse.
7
+
8
+ **Extension parameters on spec methods ride in `_meta.zcode`, not top-level.**
9
+ The ACP SDK registers spec methods like `session/load` with a zod
10
+ `z.object` params schema (`zLoadSessionRequest`), and zod's default behavior
11
+ strips unknown keys during `.parse()` — a top-level `limit` would be silently
12
+ removed before our handler ever sees it. `_meta` is the one channel the schema
13
+ preserves (`record(string, unknown)`), and it is also where the ACP spec
14
+ points extension payloads. Responses need no escape hatch: the SDK's response
15
+ mapping for `session/load` is a passthrough, so `replayMeta` rides top-level
16
+ in the result. Our own non-standard method `session/load_earlier` takes a
17
+ bridge-provided parser, so its params stay top-level — the asymmetry is
18
+ intentional and documented in REMOTE-CLIENTS.md.
19
+
20
+ **`limit` counts messages, aligned back to turn boundaries.** Clients render
21
+ messages (the app shows the last ~30), but turns are the atomic semantic unit
22
+ (a cut must not orphan a tool_call from its updates). The bridge replays at
23
+ most the last `limit` messages, extended backwards to the start of the turn
24
+ containing the oldest one; `limit: 0` attaches with metadata only. A turn
25
+ spans from a user message to the next; leading non-user messages belong to the
26
+ first turn. We rejected turn-count limits — tool-heavy turns make them
27
+ unpredictable for UI budgets.
28
+
29
+ **Cursor pagination over a full fetch, with expiry.** The backend's
30
+ `session/messages` has no pagination, but the fetch is local stdio IPC — the
31
+ expensive part is the wire to the client, so the bridge fetches all, slices in
32
+ memory, and ships only the tail. The cursor is an opaque base64 of
33
+ `{ id?, index, totalTurns }`: it validates only while the history it points
34
+ into is unchanged (compaction/truncation expires it). An expired or unknown
35
+ cursor returns a fixed `"cursor expired"` error the client maps to a full
36
+ re-`session/load` — we rejected a push-only update log with id-gap fill as
37
+ heavier machinery for the same result. During any replay batch the bridge
38
+ holds a per-session replay lock (patterned on `preemptLocks`) that live-turn
39
+ dispatch for the same session also acquires, so a batch is never interleaved
40
+ with live updates.
@@ -0,0 +1,51 @@
1
+ # Domain Docs
2
+
3
+ How engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`CONTEXT.md`** at the repo root, or
8
+ - **`CONTEXT-MAP.md`** at the repo root (if it exists) — it points to one `CONTEXT.md` per context. Read each file relevant to the current topic.
9
+ - **`docs/adr/`** — read ADRs related to the area you are about to work on. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If these files don't exist, **continue silently**. Don't flag the absence; don't proactively suggest creating them. The producer skill (`/grill-with-docs`) will lazily create them when terms or decisions are actually resolved.
12
+
13
+ ## File structure
14
+
15
+ Single-context repo (most repos):
16
+
17
+ ```
18
+ /
19
+ ├── CONTEXT.md
20
+ ├── docs/adr/
21
+ │ ├── 0001-event-sourced-orders.md
22
+ │ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ Multi-context repo (root has `CONTEXT-MAP.md`):
27
+
28
+ ```
29
+ /
30
+ ├── CONTEXT-MAP.md
31
+ ├── docs/adr/ ← system-wide decisions
32
+ └── src/
33
+ ├── ordering/
34
+ │ ├── CONTEXT.md
35
+ │ └── docs/adr/ ← context-specific decisions
36
+ └── billing/
37
+ ├── CONTEXT.md
38
+ └── docs/adr/
39
+ ```
40
+
41
+ ## Use the glossary's vocabulary
42
+
43
+ When your output names a domain concept (issue title, refactor proposal, hypothesis, test name), use the term defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
44
+
45
+ If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider), or there's a genuine gap (note it for `/grill-with-docs`).
46
+
47
+ ## Flag ADR conflicts
48
+
49
+ If your output contradicts an existing ADR, call it out explicitly rather than silently overriding:
50
+
51
+ > _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
@@ -0,0 +1,22 @@
1
+ # Issue tracker: GitHub
2
+
3
+ This repo's issues and PRDs live in GitHub issues. All operations use the `gh` CLI.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filter comments with `jq`, and fetch labels at the same time.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'`, add `--label` and `--state` filters as needed.
10
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
11
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
12
+ - **Close**: `gh issue close <number> --comment "..."`
13
+
14
+ The repo is inferred from `git remote -v`; when running inside a clone, `gh` handles this automatically.
15
+
16
+ ## When a skill says "publish to the issue tracker"
17
+
18
+ Create a GitHub issue.
19
+
20
+ ## When a skill says "fetch the relevant ticket"
21
+
22
+ Run `gh issue view <number> --comments`.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ Skills use five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill refers to a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right column to match the vocabulary you actually use.
@@ -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,17 +1,17 @@
1
1
  {
2
2
  "name": "zcode-acp-server",
3
- "version": "0.1.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",
7
7
  "author": "ZCode ACP Server Contributors",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/zcode-org/zcode-acp-server.git"
10
+ "url": "https://github.com/william0wang/zcode-acp.git"
11
11
  },
12
- "homepage": "https://github.com/zcode-org/zcode-acp-server#readme",
12
+ "homepage": "https://github.com/william0wang/zcode-acp#readme",
13
13
  "bugs": {
14
- "url": "https://github.com/zcode-org/zcode-acp-server/issues"
14
+ "url": "https://github.com/william0wang/zcode-acp/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "acp",
@@ -23,7 +23,9 @@
23
23
  "zcode"
24
24
  ],
25
25
  "bin": {
26
- "zcode-acp-server": "dist/index.js"
26
+ "zcode-acp-server": "dist/index.js",
27
+ "zcode-acp-hub": "dist/bin/hub.js",
28
+ "zcode-quota": "dist/bin/quota.js"
27
29
  },
28
30
  "main": "dist/index.js",
29
31
  "types": "dist/index.d.ts",
@@ -34,7 +36,8 @@
34
36
  "docs"
35
37
  ],
36
38
  "dependencies": {
37
- "@agentclientprotocol/sdk": "^1.1.0"
39
+ "@agentclientprotocol/sdk": "^1.3.0",
40
+ "ws": "^8.21.3"
38
41
  },
39
42
  "peerDependencies": {
40
43
  "zod": "^3.25.0 || ^4.0.0"
@@ -42,6 +45,7 @@
42
45
  "devDependencies": {
43
46
  "@eslint/js": "^9.0.0",
44
47
  "@types/node": "^22.0.0",
48
+ "@types/ws": "^8.18.1",
45
49
  "@typescript-eslint/eslint-plugin": "^8.0.0",
46
50
  "@typescript-eslint/parser": "^8.0.0",
47
51
  "eslint": "^9.0.0",