From 557bb87f3b6efeb9d3a1ad09532abe451171df06 Mon Sep 17 00:00:00 2001 From: William Wang Date: Mon, 17 Aug 2026 21:28:24 +0800 Subject: [PATCH 1/2] feat: flag context handoff summaries as collapsed in replay --- CHANGELOG.md | 8 +++++ docs/REPLAY-GUIDE.md | 26 ++++++++++++++++ src/handlers/replay.ts | 15 +++++++++ tests/load-tail.test.ts | 67 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82379e0..c678224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Replayed context-handoff summaries ("This session is being continued from a + previous conversation…") now carry `_meta.zcode.collapsed` on the + `session/update` notification so capable clients can render them folded + behind an expand control instead of as pseudo-user text. The full text stays + in the chunk; clients that ignore `_meta` (e.g. Zed) are unaffected. + ## [0.3.2] - 2026-08-17 ### Fixed diff --git a/docs/REPLAY-GUIDE.md b/docs/REPLAY-GUIDE.md index cad7886..b9dc9b0 100644 --- a/docs/REPLAY-GUIDE.md +++ b/docs/REPLAY-GUIDE.md @@ -72,6 +72,32 @@ differently: - `usage_update` / `available_commands_update` are session-level metadata, not list items. +### Collapsed harness blocks (`_meta.zcode.collapsed`) + +Replayed user messages that are harness plumbing rather than user speech may +carry a top-level `_meta` on the `session/update` notification: + +```json +{ + "sessionId": "…", + "update": { + "sessionUpdate": "user_message_chunk", + "content": { "type": "text", "text": "This session is being continued …" }, + "messageId": "…" + }, + "_meta": { "zcode": { "collapsed": true, "kind": "context-handoff" } } +} +``` + +- `kind: "context-handoff"` — the context-window continuation summary. The + full text is in the chunk as usual; render it **collapsed behind an expand + control** (e.g. a one-line label like "前序会话摘要") instead of a wall of + text attributed to the user. +- Harness noise that carries no value (TodoWrite/Read usage nudges) is + stripped before replay — you never see it. +- Unknown `kind`s: render collapsed too (or fall back to plain text). Ignoring + `_meta` entirely is always safe — the text is complete without it. + ## Scroll-up pagination ``` diff --git a/src/handlers/replay.ts b/src/handlers/replay.ts index 56b4c2f..9f293fd 100644 --- a/src/handlers/replay.ts +++ b/src/handlers/replay.ts @@ -260,6 +260,19 @@ function stripSystemReminders(text: string): string { .trim(); } +/** + * Harness context-handoff summaries ("This session is being continued from a + * previous conversation…") are informative plumbing: replayed IN FULL but + * flagged via `_meta.zcode.collapsed` so capable clients render them folded + * behind an expand control instead of as a wall of pseudo-user text. Clients + * that ignore `_meta` (e.g. Zed) display the text unchanged. + */ +const CONTEXT_HANDOFF = /^\s*This session is being continued from a previous conversation/; + +function handoffMeta(): { zcode: { collapsed: true; kind: "context-handoff" } } { + return { zcode: { collapsed: true, kind: "context-handoff" } }; +} + /** * Replay messages as session/update notifications, oldest → newest. * @@ -287,6 +300,7 @@ export async function replayMessages( text = stripSystemReminders(text); if (!text) continue; } + const collapsed = role === "user" && CONTEXT_HANDOFF.test(text); await cx.notify("session/update", { sessionId: acpSid, update: { @@ -294,6 +308,7 @@ export async function replayMessages( content: { type: "text", text }, messageId: mid, }, + ...(collapsed ? { _meta: handoffMeta() } : {}), }); } else if (ptype === "reasoning") { const rp = p as { text?: string; content?: string }; diff --git a/tests/load-tail.test.ts b/tests/load-tail.test.ts index f4c25ea..50c0cae 100644 --- a/tests/load-tail.test.ts +++ b/tests/load-tail.test.ts @@ -300,6 +300,73 @@ describe("message dedup in replay", () => { }); }); +describe("context handoff collapse marker", () => { + /** cx that captures FULL session/update params (update + _meta). */ + function collectParams(): { + cx: acp.AgentContext; + sent: Array<{ + update?: { sessionUpdate?: string; content?: { text?: string } }; + _meta?: { zcode?: { collapsed?: boolean; kind?: string } }; + }>; + } { + const sent: Array> = []; + const cx = { + notify: async (_method: string, params: Record) => { + sent.push(params); + }, + request: async () => ({}), + } as unknown as acp.AgentContext; + return { + cx, + sent: sent as Array<{ + update?: { sessionUpdate?: string; content?: { text?: string } }; + _meta?: { zcode?: { collapsed?: boolean; kind?: string } }; + }>, + }; + } + + it("flags the continuation summary with a collapse hint and keeps full text", async () => { + const history = hist(); + history[6] = { + info: { id: "u3", role: "user" }, + parts: [ + { + type: "text", + text: + "This session is being continued from a previous conversation that " + + "ran out of context. The summary below covers the earlier portion " + + "of the conversation.", + }, + ], + }; + const server = new ZcodeAcpServer(); + server.backend = fakeBackend(history); + const { cx, sent } = collectParams(); + + await loadSession(server, loadParams(), cx); + + const handoff = sent.find( + (p) => + p.update?.sessionUpdate === "user_message_chunk" && + (p.update?.content?.text ?? "").includes("continued from a previous"), + ); + expect(handoff).toBeDefined(); + expect(handoff!._meta).toEqual({ zcode: { collapsed: true, kind: "context-handoff" } }); + expect(handoff!.update!.content!.text).toContain("The summary below covers"); + }); + + it("ordinary user and agent text carries no _meta", async () => { + const server = new ZcodeAcpServer(); + server.backend = fakeBackend(hist()); + const { cx, sent } = collectParams(); + + await loadSession(server, loadParams(), cx); + + expect(sent.length).toBeGreaterThan(0); + expect(sent.every((p) => p._meta === undefined)).toBe(true); + }); +}); + describe("session/load_earlier", () => { async function attachTail(server: ZcodeAcpServer): Promise { const { cx } = collectCx(); From 2ec7c741788014254c91ee4e036c7f1a8d93f05f Mon Sep 17 00:00:00 2001 From: William Wang Date: Mon, 17 Aug 2026 21:29:02 +0800 Subject: [PATCH 2/2] chore: release 0.4.0 --- CHANGELOG.md | 2 ++ package.json | 2 +- registry/zcode-acp-server/agent.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c678224..8e14513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] - 2026-08-17 + ### Added - Replayed context-handoff summaries ("This session is being continued from a diff --git a/package.json b/package.json index a0e7e22..ac40cdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-acp-server", - "version": "0.3.2", + "version": "0.4.0", "description": "Agent Client Protocol (ACP) server bridging headless ZCode to editors like Zed and JetBrains.", "type": "module", "license": "Apache-2.0", diff --git a/registry/zcode-acp-server/agent.json b/registry/zcode-acp-server/agent.json index e3a4754..24e73df 100644 --- a/registry/zcode-acp-server/agent.json +++ b/registry/zcode-acp-server/agent.json @@ -1,7 +1,7 @@ { "id": "zcode-acp-server", "name": "ZCode", - "version": "0.3.2", + "version": "0.4.0", "description": "Standalone ACP server bridging the headless ZCode app-server (GLM-5.2) to editors like Zed and JetBrains. Supports streaming, tool calls, session fork/resume, mode switching, and reads GLM credentials locally — no editor-side API key required.", "repository": "https://github.com/william0wang/zcode-acp", "website": "https://github.com/william0wang/zcode-acp",