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.
- package/README.md +101 -15
- package/README.zh-CN.md +68 -13
- 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/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/io.d.ts +20 -1
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +57 -2
- 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/session.d.ts.map +1 -1
- package/dist/handlers/session.js +64 -65
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +23 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +67 -6
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +47 -17
- package/dist/index.js.map +1 -1
- 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 +41 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +69 -11
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +17 -1
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +47 -15
- package/docs/BACKLOG.md +3 -1
- package/docs/DEVELOPMENT.md +26 -0
- package/docs/PROTOCOL.md +67 -27
- package/docs/REMOTE-CLIENTS.md +260 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +39 -6
- 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/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- package/package.json +5 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tail replay kernel — slicing, cursor pagination, and the `session/load`
|
|
3
|
+
* replay/`session/load_earlier` handlers (Proposal 0001 / ADR-0003).
|
|
4
|
+
*
|
|
5
|
+
* session/load replays history as session/update notifications; for long
|
|
6
|
+
* sessions that cost is O(full history) on every attach and reconnect. The
|
|
7
|
+
* helpers here slice the fetched messages into turn-aligned batches and page
|
|
8
|
+
* backwards with an opaque cursor. Batches are sent under the per-session
|
|
9
|
+
* replay lock (`withReplayBatch` in io.ts) so a batch never interleaves with
|
|
10
|
+
* live-turn updates for the same session; `replayMessages` is the one sender
|
|
11
|
+
* that bypasses the per-message lock — it only runs inside a batch.
|
|
12
|
+
*/
|
|
13
|
+
import type * as acp from "@agentclientprotocol/sdk";
|
|
14
|
+
import type { ZcodeMessage } from "../backend/types.js";
|
|
15
|
+
import type { ZcodeAcpServer } from "../server.js";
|
|
16
|
+
/** Upper bound for a requested tail/page size (values above clamp to this). */
|
|
17
|
+
export declare const MAX_REPLAY_LIMIT = 500;
|
|
18
|
+
/** Page size for `session/load_earlier` when the request omits `limit`. */
|
|
19
|
+
export declare const DEFAULT_EARLIER_LIMIT = 50;
|
|
20
|
+
/** Wire metadata describing one delivered batch (additive-only over time). */
|
|
21
|
+
export interface ReplayMeta {
|
|
22
|
+
cursor: string;
|
|
23
|
+
hasMore: boolean;
|
|
24
|
+
replayedMessages: number;
|
|
25
|
+
replayedTurns: number;
|
|
26
|
+
totalMessages: number;
|
|
27
|
+
totalTurns: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ReplaySlice {
|
|
30
|
+
batch: ZcodeMessage[];
|
|
31
|
+
meta: ReplayMeta;
|
|
32
|
+
}
|
|
33
|
+
/** `session/load_earlier` params (top-level — our parser, not an ACP spec method). */
|
|
34
|
+
export interface LoadEarlierParams {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
before?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Slice the last `limit` messages, aligned back to the start of the turn
|
|
41
|
+
* containing the oldest one — never a mid-turn cut. `limit: 0` attaches with
|
|
42
|
+
* metadata only (cursor anchors at the end of history).
|
|
43
|
+
*/
|
|
44
|
+
export declare function sliceTail(messages: ZcodeMessage[], limit: number): ReplaySlice;
|
|
45
|
+
/** Full-history slice (no `_meta.zcode.limit` on session/load). */
|
|
46
|
+
export declare function fullSlice(messages: ZcodeMessage[]): ReplaySlice;
|
|
47
|
+
/**
|
|
48
|
+
* Slice up to `limit` messages strictly older than the `before` cursor.
|
|
49
|
+
* The cursor points into a prefix of history, so turns appended after it was
|
|
50
|
+
* minted keep it valid; only a history that shrank (compaction/truncation)
|
|
51
|
+
* throws `cursor expired` — clients map that to a full re-`session/load`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sliceBefore(messages: ZcodeMessage[], before: string, limit: number): ReplaySlice;
|
|
54
|
+
/**
|
|
55
|
+
* Read the tail limit from `session/load`'s `_meta.zcode.limit`. The SDK's
|
|
56
|
+
* zod params schema strips unknown top-level keys, so bridge extensions ride
|
|
57
|
+
* in `_meta` (ADR-0003). Returns null when absent/non-finite = full replay.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readTailLimit(params: acp.LoadSessionRequest): number | null;
|
|
60
|
+
/** Fetch session/messages from zcode (the bridge's only history source). */
|
|
61
|
+
export declare function fetchMessages(server: ZcodeAcpServer, zcodeSid: string): Promise<ZcodeMessage[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Replay messages as session/update notifications, oldest → newest.
|
|
64
|
+
*
|
|
65
|
+
* MUST run inside `withReplayBatch` for the session: this is the one sender
|
|
66
|
+
* that bypasses the per-message lock in sendSessionUpdate (the batch already
|
|
67
|
+
* holds it), which is what makes the batch atomic against live dispatch.
|
|
68
|
+
*/
|
|
69
|
+
export declare function replayMessages(cx: acp.AgentContext, acpSid: string, messages: ZcodeMessage[]): Promise<number>;
|
|
70
|
+
/**
|
|
71
|
+
* `session/load_earlier` — deliver one page of history strictly older than
|
|
72
|
+
* the `before` cursor, oldest → newest (clients prepend). Requires the
|
|
73
|
+
* session to already be attached in this bridge; pagination never triggers
|
|
74
|
+
* an implicit backend resume.
|
|
75
|
+
*/
|
|
76
|
+
export declare function loadEarlier(server: ZcodeAcpServer, params: LoadEarlierParams, cx: acp.AgentContext): Promise<{
|
|
77
|
+
replayMeta: ReplayMeta;
|
|
78
|
+
}>;
|
|
79
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/handlers/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqFD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAW9E;AAED,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,WAAW,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAchG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAK3E;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAYD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,GAAG,CAAC,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,YAAY,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,CA+DjB;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,iBAAiB,EACzB,EAAE,EAAE,GAAG,CAAC,YAAY,GACnB,OAAO,CAAC;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC,CAarC"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tail replay kernel — slicing, cursor pagination, and the `session/load`
|
|
3
|
+
* replay/`session/load_earlier` handlers (Proposal 0001 / ADR-0003).
|
|
4
|
+
*
|
|
5
|
+
* session/load replays history as session/update notifications; for long
|
|
6
|
+
* sessions that cost is O(full history) on every attach and reconnect. The
|
|
7
|
+
* helpers here slice the fetched messages into turn-aligned batches and page
|
|
8
|
+
* backwards with an opaque cursor. Batches are sent under the per-session
|
|
9
|
+
* replay lock (`withReplayBatch` in io.ts) so a batch never interleaves with
|
|
10
|
+
* live-turn updates for the same session; `replayMessages` is the one sender
|
|
11
|
+
* that bypasses the per-message lock — it only runs inside a batch.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { log } from "../utils.js";
|
|
15
|
+
import { throwError, withReplayBatch } from "./io.js";
|
|
16
|
+
/** Upper bound for a requested tail/page size (values above clamp to this). */
|
|
17
|
+
export const MAX_REPLAY_LIMIT = 500;
|
|
18
|
+
/** Page size for `session/load_earlier` when the request omits `limit`. */
|
|
19
|
+
export const DEFAULT_EARLIER_LIMIT = 50;
|
|
20
|
+
/**
|
|
21
|
+
* Indices where a turn starts: every user message, plus 0 so leading
|
|
22
|
+
* non-user messages (system preambles) belong to the first turn.
|
|
23
|
+
*/
|
|
24
|
+
function turnStarts(messages) {
|
|
25
|
+
const starts = messages.length > 0 ? [0] : [];
|
|
26
|
+
messages.forEach((m, i) => {
|
|
27
|
+
if (m.info?.role === "user" && i > 0)
|
|
28
|
+
starts.push(i);
|
|
29
|
+
});
|
|
30
|
+
return starts;
|
|
31
|
+
}
|
|
32
|
+
/** Count of turn starts inside [start, end). */
|
|
33
|
+
function turnsInRange(starts, start, end) {
|
|
34
|
+
return starts.filter((s) => s >= start && s < end).length;
|
|
35
|
+
}
|
|
36
|
+
function encodeCursor(messages, index, totalTurns) {
|
|
37
|
+
const anchor = messages[index]?.info?.id;
|
|
38
|
+
const payload = { v: 1, index, totalTurns };
|
|
39
|
+
if (anchor)
|
|
40
|
+
payload.id = anchor;
|
|
41
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
42
|
+
}
|
|
43
|
+
function decodeCursor(before) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(Buffer.from(before, "base64url").toString("utf8"));
|
|
46
|
+
if (raw?.v !== 1 ||
|
|
47
|
+
!Number.isInteger(raw.index) ||
|
|
48
|
+
raw.index < 0 ||
|
|
49
|
+
!Number.isInteger(raw.totalTurns)) {
|
|
50
|
+
throw new Error("bad shape");
|
|
51
|
+
}
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Garbage or foreign cursors are indistinguishable from expired ones.
|
|
56
|
+
return throwError(-32602, "cursor expired");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function buildSlice(messages, starts, start, end) {
|
|
60
|
+
const totalTurns = starts.length;
|
|
61
|
+
return {
|
|
62
|
+
batch: messages.slice(start, end),
|
|
63
|
+
meta: {
|
|
64
|
+
cursor: encodeCursor(messages, start, totalTurns),
|
|
65
|
+
hasMore: start > 0,
|
|
66
|
+
replayedMessages: end - start,
|
|
67
|
+
replayedTurns: turnsInRange(starts, start, end),
|
|
68
|
+
totalMessages: messages.length,
|
|
69
|
+
totalTurns,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** The greatest turn start at or before `pos` (0 when none — starts include 0). */
|
|
74
|
+
function alignToTurnStart(starts, pos) {
|
|
75
|
+
let aligned = 0;
|
|
76
|
+
for (const s of starts) {
|
|
77
|
+
if (s <= pos)
|
|
78
|
+
aligned = s;
|
|
79
|
+
else
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return aligned;
|
|
83
|
+
}
|
|
84
|
+
function clampLimit(limit) {
|
|
85
|
+
return Math.max(0, Math.min(Math.floor(limit), MAX_REPLAY_LIMIT));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Slice the last `limit` messages, aligned back to the start of the turn
|
|
89
|
+
* containing the oldest one — never a mid-turn cut. `limit: 0` attaches with
|
|
90
|
+
* metadata only (cursor anchors at the end of history).
|
|
91
|
+
*/
|
|
92
|
+
export function sliceTail(messages, limit) {
|
|
93
|
+
const starts = turnStarts(messages);
|
|
94
|
+
const clamped = clampLimit(limit);
|
|
95
|
+
if (clamped === 0) {
|
|
96
|
+
// Metadata-only attach: an empty batch whose cursor anchors at the end of
|
|
97
|
+
// history, so load_earlier pages the whole tail.
|
|
98
|
+
return buildSlice(messages, starts, messages.length, messages.length);
|
|
99
|
+
}
|
|
100
|
+
if (clamped >= messages.length)
|
|
101
|
+
return buildSlice(messages, starts, 0, messages.length);
|
|
102
|
+
const start = alignToTurnStart(starts, messages.length - clamped);
|
|
103
|
+
return buildSlice(messages, starts, start, messages.length);
|
|
104
|
+
}
|
|
105
|
+
/** Full-history slice (no `_meta.zcode.limit` on session/load). */
|
|
106
|
+
export function fullSlice(messages) {
|
|
107
|
+
return buildSlice(messages, turnStarts(messages), 0, messages.length);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Slice up to `limit` messages strictly older than the `before` cursor.
|
|
111
|
+
* The cursor points into a prefix of history, so turns appended after it was
|
|
112
|
+
* minted keep it valid; only a history that shrank (compaction/truncation)
|
|
113
|
+
* throws `cursor expired` — clients map that to a full re-`session/load`.
|
|
114
|
+
*/
|
|
115
|
+
export function sliceBefore(messages, before, limit) {
|
|
116
|
+
const starts = turnStarts(messages);
|
|
117
|
+
const cur = decodeCursor(before);
|
|
118
|
+
if (cur.index > messages.length || cur.totalTurns > starts.length) {
|
|
119
|
+
return throwError(-32602, "cursor expired");
|
|
120
|
+
}
|
|
121
|
+
if (cur.id != null && cur.index < messages.length && messages[cur.index].info?.id !== cur.id) {
|
|
122
|
+
return throwError(-32602, "cursor expired");
|
|
123
|
+
}
|
|
124
|
+
const end = cur.index;
|
|
125
|
+
if (end === 0)
|
|
126
|
+
return buildSlice(messages, starts, 0, 0);
|
|
127
|
+
const clamped = clampLimit(limit);
|
|
128
|
+
const start = clamped === 0 ? end : alignToTurnStart(starts, Math.max(0, end - clamped));
|
|
129
|
+
return buildSlice(messages, starts, start, end);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read the tail limit from `session/load`'s `_meta.zcode.limit`. The SDK's
|
|
133
|
+
* zod params schema strips unknown top-level keys, so bridge extensions ride
|
|
134
|
+
* in `_meta` (ADR-0003). Returns null when absent/non-finite = full replay.
|
|
135
|
+
*/
|
|
136
|
+
export function readTailLimit(params) {
|
|
137
|
+
const zcode = params._meta?.zcode;
|
|
138
|
+
const raw = zcode?.limit;
|
|
139
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
140
|
+
return null;
|
|
141
|
+
return clampLimit(raw);
|
|
142
|
+
}
|
|
143
|
+
/** Fetch session/messages from zcode (the bridge's only history source). */
|
|
144
|
+
export async function fetchMessages(server, zcodeSid) {
|
|
145
|
+
const backend = server.ensureBackend();
|
|
146
|
+
const resp = await backend.request(server.nextId(), "session/messages", { sessionId: zcodeSid }, 8000);
|
|
147
|
+
if (resp.error)
|
|
148
|
+
return [];
|
|
149
|
+
const result = (resp.result ?? {});
|
|
150
|
+
return result.messages ?? [];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Strip harness-injected reminder blocks from user text. The agent runtime
|
|
154
|
+
* appends `<system-reminder>…</system-reminder>` blocks (TodoWrite nudges,
|
|
155
|
+
* context handoffs) to user turns as context plumbing — they are not user
|
|
156
|
+
* speech, and replaying them verbatim makes clients render them as user input.
|
|
157
|
+
*/
|
|
158
|
+
function stripSystemReminders(text) {
|
|
159
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Replay messages as session/update notifications, oldest → newest.
|
|
163
|
+
*
|
|
164
|
+
* MUST run inside `withReplayBatch` for the session: this is the one sender
|
|
165
|
+
* that bypasses the per-message lock in sendSessionUpdate (the batch already
|
|
166
|
+
* holds it), which is what makes the batch atomic against live dispatch.
|
|
167
|
+
*/
|
|
168
|
+
export async function replayMessages(cx, acpSid, messages) {
|
|
169
|
+
let replayed = 0;
|
|
170
|
+
for (const m of messages) {
|
|
171
|
+
const info = m.info ?? {};
|
|
172
|
+
const role = info.role;
|
|
173
|
+
const mid = info.id ?? `hist_${randomUUID().slice(0, 12)}`;
|
|
174
|
+
for (const p of m.parts ?? []) {
|
|
175
|
+
if (!p || typeof p !== "object")
|
|
176
|
+
continue;
|
|
177
|
+
const ptype = p.type;
|
|
178
|
+
if (ptype === "text") {
|
|
179
|
+
let text = p.text ?? "";
|
|
180
|
+
if (!text)
|
|
181
|
+
continue;
|
|
182
|
+
if (role === "user") {
|
|
183
|
+
text = stripSystemReminders(text);
|
|
184
|
+
if (!text)
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
await cx.notify("session/update", {
|
|
188
|
+
sessionId: acpSid,
|
|
189
|
+
update: {
|
|
190
|
+
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
|
191
|
+
content: { type: "text", text },
|
|
192
|
+
messageId: mid,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (ptype === "reasoning") {
|
|
197
|
+
const rp = p;
|
|
198
|
+
const text = rp.text ?? rp.content ?? "";
|
|
199
|
+
if (text) {
|
|
200
|
+
await cx.notify("session/update", {
|
|
201
|
+
sessionId: acpSid,
|
|
202
|
+
update: {
|
|
203
|
+
sessionUpdate: "agent_thought_chunk",
|
|
204
|
+
content: { type: "text", text },
|
|
205
|
+
messageId: `thought_${mid}`,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (ptype === "tool") {
|
|
211
|
+
const tp = p;
|
|
212
|
+
const title = tp.title ?? tp.tool ?? "tool call";
|
|
213
|
+
const histToolName = tp.tool ?? "";
|
|
214
|
+
await cx.notify("session/update", {
|
|
215
|
+
sessionId: acpSid,
|
|
216
|
+
update: {
|
|
217
|
+
sessionUpdate: "tool_call",
|
|
218
|
+
toolCallId: tp.id ?? `histtool_${randomUUID().slice(0, 8)}`,
|
|
219
|
+
title,
|
|
220
|
+
kind: "other",
|
|
221
|
+
status: tp.status ?? "completed",
|
|
222
|
+
...(histToolName ? { _meta: { claudeCode: { toolName: histToolName } } } : {}),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// patch / step-start / other: skipped (history replay focuses on text + tool summary)
|
|
227
|
+
}
|
|
228
|
+
replayed += 1;
|
|
229
|
+
}
|
|
230
|
+
return replayed;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* `session/load_earlier` — deliver one page of history strictly older than
|
|
234
|
+
* the `before` cursor, oldest → newest (clients prepend). Requires the
|
|
235
|
+
* session to already be attached in this bridge; pagination never triggers
|
|
236
|
+
* an implicit backend resume.
|
|
237
|
+
*/
|
|
238
|
+
export async function loadEarlier(server, params, cx) {
|
|
239
|
+
const acpSid = params.sessionId;
|
|
240
|
+
const zcodeSid = server.resolveSid(acpSid);
|
|
241
|
+
if (!zcodeSid) {
|
|
242
|
+
return throwError(-32602, "session not registered — attach via session/load first");
|
|
243
|
+
}
|
|
244
|
+
if (!params.before)
|
|
245
|
+
return throwError(-32602, "before (cursor) required");
|
|
246
|
+
const messages = await fetchMessages(server, zcodeSid);
|
|
247
|
+
const slice = sliceBefore(messages, params.before, params.limit ?? DEFAULT_EARLIER_LIMIT);
|
|
248
|
+
await withReplayBatch(acpSid, () => replayMessages(cx, acpSid, slice.batch));
|
|
249
|
+
log(`session/load_earlier: ${slice.meta.replayedMessages} messages before cursor`);
|
|
250
|
+
return { replayMeta: slice.meta };
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/handlers/replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/handlers/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/handlers/session.ts"],"names":[],"mappings":"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"}
|
package/dist/handlers/session.js
CHANGED
|
@@ -22,7 +22,8 @@ import { lookupLazySession, recordMaterializedSession, rememberLazySession, } fr
|
|
|
22
22
|
import { buildDiffContent, EventTranslator, extractLocations, formatTurnError, isTransientTurnError, ProjectionDiffer, } from "../translators/index.js";
|
|
23
23
|
import { log, warn } from "../utils.js";
|
|
24
24
|
import { dispatchEvent } from "./dispatch.js";
|
|
25
|
-
import { sendSessionUpdate, sendTextChunk } from "./io.js";
|
|
25
|
+
import { sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
|
|
26
|
+
import { fetchMessages, fullSlice, readTailLimit, replayMessages, sliceTail } from "./replay.js";
|
|
26
27
|
import { handleServerRequests } from "./server-requests.js";
|
|
27
28
|
/** Workspace descriptor used in session create/resume calls. */
|
|
28
29
|
function workspaceFor(cwd) {
|
|
@@ -70,6 +71,9 @@ export async function newSession(server, params) {
|
|
|
70
71
|
// backend session materializes; never shown in session/list.
|
|
71
72
|
const acpSid = randomUUID();
|
|
72
73
|
server.pendingSessions.set(acpSid, { cwd, mcpServers: params.mcpServers });
|
|
74
|
+
// Persists past materialization (pendingSessions is cleared on first use) so
|
|
75
|
+
// the remote discovery payload can still label the workspace.
|
|
76
|
+
server.sessionCwds.set(acpSid, cwd);
|
|
73
77
|
// Durable alias so the placeholder survives a bridge restart and session/
|
|
74
78
|
// resume can still resolve it (best-effort; failures are swallowed inside
|
|
75
79
|
// the store).
|
|
@@ -113,6 +117,7 @@ export async function ensureRealSession(server, acpSid) {
|
|
|
113
117
|
if (record) {
|
|
114
118
|
pending = { cwd: record.cwd };
|
|
115
119
|
server.pendingSessions.set(acpSid, pending);
|
|
120
|
+
server.sessionCwds.set(acpSid, record.cwd);
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
if (!pending)
|
|
@@ -201,6 +206,36 @@ export async function listSessions(server, params) {
|
|
|
201
206
|
log(`session/list → ${sessions.length} sessions`);
|
|
202
207
|
return { sessions };
|
|
203
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Adopt the backend's stored title for a loaded/resumed session.
|
|
211
|
+
*
|
|
212
|
+
* The prompt loop's auto-title only fires for freshly created sessions
|
|
213
|
+
* (`titleEligibleSessions`), so a session resumed across a bridge restart
|
|
214
|
+
* would otherwise appear title-less in the hub's discovery API — remote
|
|
215
|
+
* clients have no editor-side session storage to fall back on. The backend's
|
|
216
|
+
* session/list is the only title source for sessions born in a previous
|
|
217
|
+
* bridge lifetime. Best-effort: failures log and leave the session untitled.
|
|
218
|
+
*/
|
|
219
|
+
async function adoptStoredTitle(server, acpSid, zcodeSid) {
|
|
220
|
+
if (server.sessionTitles.has(acpSid))
|
|
221
|
+
return;
|
|
222
|
+
try {
|
|
223
|
+
const backend = server.ensureBackend();
|
|
224
|
+
const resp = await backend.request(server.nextId(), "session/list", {}, 15000);
|
|
225
|
+
if (resp.error)
|
|
226
|
+
return;
|
|
227
|
+
const result = (resp.result ?? {});
|
|
228
|
+
const hit = (result.sessions ?? []).find((s) => s.sessionId === zcodeSid);
|
|
229
|
+
if (hit?.title) {
|
|
230
|
+
server.sessionTitles.set(acpSid, hit.title);
|
|
231
|
+
server.touchSessionSummary(acpSid, hit.title);
|
|
232
|
+
log(`adopted stored title for ${acpSid.slice(0, 8)}: ${hit.title}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
log(`stored title lookup failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
204
239
|
/**
|
|
205
240
|
* Resolve the backend session id for `session/resume` / `session/load`.
|
|
206
241
|
*
|
|
@@ -273,6 +308,7 @@ export async function resumeSession(server, params, cx) {
|
|
|
273
308
|
server.registerSession(acpSid, zcodeSid);
|
|
274
309
|
log(`session/resume -> ${zcodeSid}`);
|
|
275
310
|
server.ensureBackgroundListener(zcodeSid);
|
|
311
|
+
await adoptStoredTitle(server, acpSid, zcodeSid);
|
|
276
312
|
// Initial usage_update so the editor shows the context bar immediately for a
|
|
277
313
|
// resumed session (mirrors Python _on_session_resume → _emit_initial_usage).
|
|
278
314
|
await emitInitialUsage(server, cx, acpSid, zcodeSid, getOrCreateDiffer(server, zcodeSid));
|
|
@@ -312,57 +348,16 @@ export async function loadSession(server, params, cx) {
|
|
|
312
348
|
server.registerSession(acpSid, zcodeSid);
|
|
313
349
|
log(`session/load → ${zcodeSid}`);
|
|
314
350
|
server.ensureBackgroundListener(zcodeSid);
|
|
351
|
+
await adoptStoredTitle(server, acpSid, zcodeSid);
|
|
315
352
|
const messages = await fetchMessages(server, zcodeSid);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const ptype = p.type;
|
|
325
|
-
if (ptype === "text") {
|
|
326
|
-
const text = p.text ?? "";
|
|
327
|
-
if (!text)
|
|
328
|
-
continue;
|
|
329
|
-
const sessionUpdate = role === "user" ? "user_message_chunk" : "agent_message_chunk";
|
|
330
|
-
await sendSessionUpdate(cx, acpSid, {
|
|
331
|
-
sessionUpdate,
|
|
332
|
-
content: { type: "text", text },
|
|
333
|
-
messageId: mid,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
else if (ptype === "reasoning") {
|
|
337
|
-
const rp = p;
|
|
338
|
-
const text = rp.text ?? rp.content ?? "";
|
|
339
|
-
if (text) {
|
|
340
|
-
await sendSessionUpdate(cx, acpSid, {
|
|
341
|
-
sessionUpdate: "agent_thought_chunk",
|
|
342
|
-
content: { type: "text", text },
|
|
343
|
-
messageId: `thought_${mid}`,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else if (ptype === "tool") {
|
|
348
|
-
const tp = p;
|
|
349
|
-
const title = tp.title ?? tp.tool ?? "tool call";
|
|
350
|
-
const histToolName = tp.tool ?? "";
|
|
351
|
-
const update = {
|
|
352
|
-
sessionUpdate: "tool_call",
|
|
353
|
-
toolCallId: tp.id ?? `histtool_${randomUUID().slice(0, 8)}`,
|
|
354
|
-
title,
|
|
355
|
-
kind: "other",
|
|
356
|
-
status: tp.status ?? "completed",
|
|
357
|
-
...(histToolName ? { _meta: { claudeCode: { toolName: histToolName } } } : {}),
|
|
358
|
-
};
|
|
359
|
-
await sendSessionUpdate(cx, acpSid, update);
|
|
360
|
-
}
|
|
361
|
-
// patch / step-start / other: skipped (history replay focuses on text + tool summary)
|
|
362
|
-
}
|
|
363
|
-
replayed += 1;
|
|
364
|
-
}
|
|
365
|
-
log(`session/load: replayed ${replayed} messages`);
|
|
353
|
+
// Tail replay (Proposal 0001): a `_meta.zcode.limit` replays only the last
|
|
354
|
+
// N messages aligned to turn boundaries — the full replay stays the default
|
|
355
|
+
// for editors that send no `_meta` (Zed path unchanged).
|
|
356
|
+
const limit = readTailLimit(params);
|
|
357
|
+
const slice = limit === null ? fullSlice(messages) : sliceTail(messages, limit);
|
|
358
|
+
await withReplayBatch(acpSid, () => replayMessages(cx, acpSid, slice.batch));
|
|
359
|
+
log(`session/load: replayed ${slice.meta.replayedMessages} messages` +
|
|
360
|
+
`${limit === null ? "" : ` (tail limit ${limit}, total ${slice.meta.totalMessages})`}`);
|
|
366
361
|
// Replay the existing todo list as an initial plan so a loaded session shows
|
|
367
362
|
// its todos immediately (filter to PlanUpdate only — text/tools were already
|
|
368
363
|
// replayed above and the differ hasn't mark_seen'd this history).
|
|
@@ -381,10 +376,13 @@ export async function loadSession(server, params, cx) {
|
|
|
381
376
|
await emitInitialUsage(server, cx, acpSid, zcodeSid, getOrCreateDiffer(server, zcodeSid));
|
|
382
377
|
const modes = await buildModes(server, zcodeSid);
|
|
383
378
|
server.lastMode.set(acpSid, modes.currentModeId);
|
|
384
|
-
|
|
379
|
+
const result = {
|
|
385
380
|
modes,
|
|
386
381
|
configOptions: await buildConfigOptions(server, zcodeSid),
|
|
382
|
+
// Additive replay metadata — the anchor for load_earlier pagination.
|
|
383
|
+
replayMeta: slice.meta,
|
|
387
384
|
};
|
|
385
|
+
return result;
|
|
388
386
|
}
|
|
389
387
|
/** `session/prompt` → subscribe-before-send, run the event-driven turn loop. */
|
|
390
388
|
export async function prompt(server, params, cx, requestId) {
|
|
@@ -400,11 +398,17 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
400
398
|
// empty-prompt check so an invalid request doesn't create a backend session.
|
|
401
399
|
const zcodeSid = await ensureRealSession(server, params.sessionId);
|
|
402
400
|
// Slash-command interception: dispatches directly to ZCode methods and
|
|
403
|
-
// returns end_turn without entering the turn loop.
|
|
404
|
-
|
|
401
|
+
// returns end_turn without entering the turn loop. Known passthrough
|
|
402
|
+
// commands and unknown /x both return null for the normal turn loop.
|
|
403
|
+
const { handleSlashCommand, neutralizeSlashText } = await import("./slash.js");
|
|
405
404
|
const intercepted = await handleSlashCommand(server, cx, params.sessionId, zcodeSid, text);
|
|
406
405
|
if (intercepted)
|
|
407
406
|
return intercepted;
|
|
407
|
+
// Wire text for the backend: unknown `/x` prompts (not advertised commands)
|
|
408
|
+
// are neutralized so the backend's command resolver never sees them — an
|
|
409
|
+
// unresolvable name can hard-fail the turn. Known commands pass through
|
|
410
|
+
// unchanged. The title/auto-compact paths below keep using the raw `text`.
|
|
411
|
+
const sendText = neutralizeSlashText(text);
|
|
408
412
|
// Register self + preempt others under a per-session lock. The lock
|
|
409
413
|
// serializes the critical section so that two concurrent prompts (B, C) for
|
|
410
414
|
// the same session can't both miss each other and register at once: C waits
|
|
@@ -491,8 +495,8 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
491
495
|
const SEND_RETRY_INTERVAL_MS = 500;
|
|
492
496
|
const SEND_RETRY_TIMEOUT_MS = 30_000;
|
|
493
497
|
const sendParams = attachments.length > 0
|
|
494
|
-
? { sessionId: zcodeSid, content:
|
|
495
|
-
: { sessionId: zcodeSid, content:
|
|
498
|
+
? { sessionId: zcodeSid, content: sendText, attachments }
|
|
499
|
+
: { sessionId: zcodeSid, content: sendText };
|
|
496
500
|
const sendT0 = Date.now();
|
|
497
501
|
let sendAttempt = 0;
|
|
498
502
|
while (true) {
|
|
@@ -556,6 +560,7 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
556
560
|
.find((l) => l.length > 0)
|
|
557
561
|
?.slice(0, 80) ?? text.slice(0, 80);
|
|
558
562
|
server.sessionTitles.set(params.sessionId, title);
|
|
563
|
+
server.touchSessionSummary(params.sessionId, title);
|
|
559
564
|
const { updateSessionTitle } = await import("../tasks-index.js");
|
|
560
565
|
void updateSessionTitle(zcodeSid, title, text);
|
|
561
566
|
await sendSessionUpdate(cx, params.sessionId, {
|
|
@@ -603,6 +608,9 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
603
608
|
finally {
|
|
604
609
|
backend.unregisterEventListener(zcodeSid, listener);
|
|
605
610
|
server.pendingTurns.delete(requestId);
|
|
611
|
+
// Turn end = session activity — refresh the discovery summary timestamp
|
|
612
|
+
// regardless of outcome (end_turn, cancelled, retries exhausted).
|
|
613
|
+
server.touchSessionSummary(params.sessionId);
|
|
606
614
|
}
|
|
607
615
|
}
|
|
608
616
|
/**
|
|
@@ -914,15 +922,6 @@ async function resumeBackendSession(server, zcParams) {
|
|
|
914
922
|
await sleep(1000);
|
|
915
923
|
}
|
|
916
924
|
}
|
|
917
|
-
/** Fetch session/messages from zcode. */
|
|
918
|
-
async function fetchMessages(server, zcodeSid) {
|
|
919
|
-
const backend = server.ensureBackend();
|
|
920
|
-
const resp = await backend.request(server.nextId(), "session/messages", { sessionId: zcodeSid }, 8000);
|
|
921
|
-
if (resp.error)
|
|
922
|
-
return [];
|
|
923
|
-
const result = (resp.result ?? {});
|
|
924
|
-
return result.messages ?? [];
|
|
925
|
-
}
|
|
926
925
|
/** Get or create the session-level ProjectionDiffer (persists across turns). */
|
|
927
926
|
function getOrCreateDiffer(server, zcodeSid) {
|
|
928
927
|
let d = server.differs.get(zcodeSid);
|