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.
- package/README.md +192 -9
- package/README.zh-CN.md +143 -8
- package/dist/backend/client.d.ts +12 -4
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +72 -7
- package/dist/backend/client.js.map +1 -1
- package/dist/backend/credentials.d.ts.map +1 -1
- package/dist/backend/credentials.js +2 -1
- package/dist/backend/credentials.js.map +1 -1
- package/dist/backend/listener.d.ts +12 -4
- package/dist/backend/listener.d.ts.map +1 -1
- package/dist/backend/listener.js +68 -12
- package/dist/backend/listener.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/backend/types.d.ts.map +1 -1
- 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/bin/quota.d.ts +54 -0
- package/dist/bin/quota.d.ts.map +1 -0
- package/dist/bin/quota.js +333 -0
- package/dist/bin/quota.js.map +1 -0
- package/dist/config/auto-compact.d.ts +23 -0
- package/dist/config/auto-compact.d.ts.map +1 -0
- package/dist/config/auto-compact.js +67 -0
- package/dist/config/auto-compact.js.map +1 -0
- package/dist/config/mcp-discovery.d.ts +34 -0
- package/dist/config/mcp-discovery.d.ts.map +1 -0
- package/dist/config/mcp-discovery.js +153 -0
- package/dist/config/mcp-discovery.js.map +1 -0
- package/dist/config/model-cache.d.ts +12 -1
- package/dist/config/model-cache.d.ts.map +1 -1
- package/dist/config/model-cache.js +30 -8
- package/dist/config/model-cache.js.map +1 -1
- package/dist/config/options.d.ts +87 -9
- package/dist/config/options.d.ts.map +1 -1
- package/dist/config/options.js +248 -45
- package/dist/config/options.js.map +1 -1
- package/dist/config/plugin-commands.d.ts +24 -0
- package/dist/config/plugin-commands.d.ts.map +1 -0
- package/dist/config/plugin-commands.js +107 -0
- package/dist/config/plugin-commands.js.map +1 -0
- package/dist/config/provider-registry.d.ts +60 -0
- package/dist/config/provider-registry.d.ts.map +1 -0
- package/dist/config/provider-registry.js +128 -0
- package/dist/config/provider-registry.js.map +1 -0
- package/dist/config/runtime-model.d.ts +54 -18
- package/dist/config/runtime-model.d.ts.map +1 -1
- package/dist/config/runtime-model.js +105 -61
- package/dist/config/runtime-model.js.map +1 -1
- package/dist/config/skill-discovery.d.ts +35 -0
- package/dist/config/skill-discovery.d.ts.map +1 -0
- package/dist/config/skill-discovery.js +188 -0
- package/dist/config/skill-discovery.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/background-tasks.d.ts +72 -0
- package/dist/handlers/background-tasks.d.ts.map +1 -0
- package/dist/handlers/background-tasks.js +330 -0
- package/dist/handlers/background-tasks.js.map +1 -0
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +134 -22
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/extensions.d.ts +15 -0
- package/dist/handlers/extensions.d.ts.map +1 -1
- package/dist/handlers/extensions.js +43 -27
- package/dist/handlers/extensions.js.map +1 -1
- package/dist/handlers/io.d.ts +31 -3
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +108 -9
- 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/server-requests.d.ts +20 -4
- package/dist/handlers/server-requests.d.ts.map +1 -1
- package/dist/handlers/server-requests.js +293 -63
- package/dist/handlers/server-requests.js.map +1 -1
- package/dist/handlers/session.d.ts +113 -10
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +932 -277
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +37 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +143 -8
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +76 -12
- package/dist/index.js.map +1 -1
- package/dist/interaction/adapter.d.ts +19 -33
- package/dist/interaction/adapter.d.ts.map +1 -1
- package/dist/interaction/adapter.js +80 -79
- package/dist/interaction/adapter.js.map +1 -1
- package/dist/lazy-sessions.d.ts +35 -0
- package/dist/lazy-sessions.d.ts.map +1 -0
- package/dist/lazy-sessions.js +98 -0
- package/dist/lazy-sessions.js.map +1 -0
- package/dist/quota/cache.d.ts +18 -0
- package/dist/quota/cache.d.ts.map +1 -0
- package/dist/quota/cache.js +32 -0
- package/dist/quota/cache.js.map +1 -0
- package/dist/quota/client.d.ts +30 -0
- package/dist/quota/client.d.ts.map +1 -0
- package/dist/quota/client.js +57 -0
- package/dist/quota/client.js.map +1 -0
- package/dist/quota/color.d.ts +58 -0
- package/dist/quota/color.d.ts.map +1 -0
- package/dist/quota/color.js +95 -0
- package/dist/quota/color.js.map +1 -0
- package/dist/quota/combined.d.ts +66 -0
- package/dist/quota/combined.d.ts.map +1 -0
- package/dist/quota/combined.js +179 -0
- package/dist/quota/combined.js.map +1 -0
- package/dist/quota/format.d.ts +105 -0
- package/dist/quota/format.d.ts.map +1 -0
- package/dist/quota/format.js +219 -0
- package/dist/quota/format.js.map +1 -0
- package/dist/quota/index.d.ts +23 -0
- package/dist/quota/index.d.ts.map +1 -0
- package/dist/quota/index.js +42 -0
- package/dist/quota/index.js.map +1 -0
- package/dist/quota/opencode-go/cache.d.ts +17 -0
- package/dist/quota/opencode-go/cache.d.ts.map +1 -0
- package/dist/quota/opencode-go/cache.js +31 -0
- package/dist/quota/opencode-go/cache.js.map +1 -0
- package/dist/quota/opencode-go/client.d.ts +28 -0
- package/dist/quota/opencode-go/client.d.ts.map +1 -0
- package/dist/quota/opencode-go/client.js +48 -0
- package/dist/quota/opencode-go/client.js.map +1 -0
- package/dist/quota/opencode-go/config.d.ts +37 -0
- package/dist/quota/opencode-go/config.d.ts.map +1 -0
- package/dist/quota/opencode-go/config.js +58 -0
- package/dist/quota/opencode-go/config.js.map +1 -0
- package/dist/quota/opencode-go/format.d.ts +36 -0
- package/dist/quota/opencode-go/format.d.ts.map +1 -0
- package/dist/quota/opencode-go/format.js +87 -0
- package/dist/quota/opencode-go/format.js.map +1 -0
- package/dist/quota/opencode-go/index.d.ts +30 -0
- package/dist/quota/opencode-go/index.d.ts.map +1 -0
- package/dist/quota/opencode-go/index.js +108 -0
- package/dist/quota/opencode-go/index.js.map +1 -0
- package/dist/quota/opencode-go/parse.d.ts +41 -0
- package/dist/quota/opencode-go/parse.d.ts.map +1 -0
- package/dist/quota/opencode-go/parse.js +75 -0
- package/dist/quota/opencode-go/parse.js.map +1 -0
- package/dist/quota/opencode-go/types.d.ts +48 -0
- package/dist/quota/opencode-go/types.d.ts.map +1 -0
- package/dist/quota/opencode-go/types.js +11 -0
- package/dist/quota/opencode-go/types.js.map +1 -0
- package/dist/quota/parse.d.ts +33 -0
- package/dist/quota/parse.d.ts.map +1 -0
- package/dist/quota/parse.js +200 -0
- package/dist/quota/parse.js.map +1 -0
- package/dist/quota/types.d.ts +72 -0
- package/dist/quota/types.d.ts.map +1 -0
- package/dist/quota/types.js +10 -0
- package/dist/quota/types.js.map +1 -0
- 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 +139 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +179 -4
- package/dist/server.js.map +1 -1
- package/dist/tasks-index.d.ts +14 -4
- package/dist/tasks-index.d.ts.map +1 -1
- package/dist/tasks-index.js +145 -46
- package/dist/tasks-index.js.map +1 -1
- package/dist/translators/event-translator.d.ts +33 -0
- package/dist/translators/event-translator.d.ts.map +1 -1
- package/dist/translators/event-translator.js +101 -0
- package/dist/translators/event-translator.js.map +1 -1
- package/dist/translators/index.d.ts +1 -1
- package/dist/translators/index.d.ts.map +1 -1
- package/dist/translators/index.js +1 -1
- package/dist/translators/index.js.map +1 -1
- package/dist/translators/projection-differ.d.ts +8 -0
- package/dist/translators/projection-differ.d.ts.map +1 -1
- package/dist/translators/projection-differ.js +19 -9
- package/dist/translators/projection-differ.js.map +1 -1
- package/dist/translators/tool-helpers.d.ts +28 -0
- package/dist/translators/tool-helpers.d.ts.map +1 -1
- package/dist/translators/tool-helpers.js +104 -0
- package/dist/translators/tool-helpers.js.map +1 -1
- package/dist/translators/types.d.ts +38 -1
- package/dist/translators/types.d.ts.map +1 -1
- package/dist/translators/types.js.map +1 -1
- package/dist/utils.d.ts +36 -7
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +69 -5
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +130 -40
- package/docs/BACKLOG.md +59 -0
- package/docs/DEVELOPMENT.md +31 -0
- package/docs/PROTOCOL.md +328 -23
- package/docs/REMOTE-CLIENTS.md +260 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +131 -14
- 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/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +22 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- 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.
|
|
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/
|
|
10
|
+
"url": "https://github.com/william0wang/zcode-acp.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.com/
|
|
12
|
+
"homepage": "https://github.com/william0wang/zcode-acp#readme",
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
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.
|
|
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",
|