Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ 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
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
Expand Down
26 changes: 26 additions & 0 deletions docs/REPLAY-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion registry/zcode-acp-server/agent.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/handlers/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -287,13 +300,15 @@ 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: {
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
content: { type: "text", text },
messageId: mid,
},
...(collapsed ? { _meta: handoffMeta() } : {}),
});
} else if (ptype === "reasoning") {
const rp = p as { text?: string; content?: string };
Expand Down
67 changes: 67 additions & 0 deletions tests/load-tail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>> = [];
const cx = {
notify: async (_method: string, params: Record<string, unknown>) => {
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<string> {
const { cx } = collectCx();
Expand Down
Loading