zcode-acp-server 0.2.0 → 0.3.1
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 +256 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +83 -68
- 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 +43 -0
- package/dist/remote/endpoint.d.ts.map +1 -0
- package/dist/remote/endpoint.js +222 -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 +62 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +99 -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 +264 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +51 -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,CAgBzB;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,256 @@
|
|
|
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, warn } 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
|
+
// Swallowed on purpose (replay must not crash the load) — but loudly: a
|
|
149
|
+
// silent empty here renders the whole conversation blank for the client.
|
|
150
|
+
warn(`session/messages failed for ${zcodeSid}: ${resp.error.message ?? ""}`);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const result = (resp.result ?? {});
|
|
154
|
+
return result.messages ?? [];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Strip harness-injected reminder blocks from user text. The agent runtime
|
|
158
|
+
* appends `<system-reminder>…</system-reminder>` blocks (TodoWrite nudges,
|
|
159
|
+
* context handoffs) to user turns as context plumbing — they are not user
|
|
160
|
+
* speech, and replaying them verbatim makes clients render them as user input.
|
|
161
|
+
*/
|
|
162
|
+
function stripSystemReminders(text) {
|
|
163
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Replay messages as session/update notifications, oldest → newest.
|
|
167
|
+
*
|
|
168
|
+
* MUST run inside `withReplayBatch` for the session: this is the one sender
|
|
169
|
+
* that bypasses the per-message lock in sendSessionUpdate (the batch already
|
|
170
|
+
* holds it), which is what makes the batch atomic against live dispatch.
|
|
171
|
+
*/
|
|
172
|
+
export async function replayMessages(cx, acpSid, messages) {
|
|
173
|
+
let replayed = 0;
|
|
174
|
+
for (const m of messages) {
|
|
175
|
+
const info = m.info ?? {};
|
|
176
|
+
const role = info.role;
|
|
177
|
+
const mid = info.id ?? `hist_${randomUUID().slice(0, 12)}`;
|
|
178
|
+
for (const p of m.parts ?? []) {
|
|
179
|
+
if (!p || typeof p !== "object")
|
|
180
|
+
continue;
|
|
181
|
+
const ptype = p.type;
|
|
182
|
+
if (ptype === "text") {
|
|
183
|
+
let text = p.text ?? "";
|
|
184
|
+
if (!text)
|
|
185
|
+
continue;
|
|
186
|
+
if (role === "user") {
|
|
187
|
+
text = stripSystemReminders(text);
|
|
188
|
+
if (!text)
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
await cx.notify("session/update", {
|
|
192
|
+
sessionId: acpSid,
|
|
193
|
+
update: {
|
|
194
|
+
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
|
195
|
+
content: { type: "text", text },
|
|
196
|
+
messageId: mid,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (ptype === "reasoning") {
|
|
201
|
+
const rp = p;
|
|
202
|
+
const text = rp.text ?? rp.content ?? "";
|
|
203
|
+
if (text) {
|
|
204
|
+
await cx.notify("session/update", {
|
|
205
|
+
sessionId: acpSid,
|
|
206
|
+
update: {
|
|
207
|
+
sessionUpdate: "agent_thought_chunk",
|
|
208
|
+
content: { type: "text", text },
|
|
209
|
+
messageId: `thought_${mid}`,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (ptype === "tool") {
|
|
215
|
+
const tp = p;
|
|
216
|
+
const title = tp.title ?? tp.tool ?? "tool call";
|
|
217
|
+
const histToolName = tp.tool ?? "";
|
|
218
|
+
await cx.notify("session/update", {
|
|
219
|
+
sessionId: acpSid,
|
|
220
|
+
update: {
|
|
221
|
+
sessionUpdate: "tool_call",
|
|
222
|
+
toolCallId: tp.id ?? `histtool_${randomUUID().slice(0, 8)}`,
|
|
223
|
+
title,
|
|
224
|
+
kind: "other",
|
|
225
|
+
status: tp.status ?? "completed",
|
|
226
|
+
...(histToolName ? { _meta: { claudeCode: { toolName: histToolName } } } : {}),
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// patch / step-start / other: skipped (history replay focuses on text + tool summary)
|
|
231
|
+
}
|
|
232
|
+
replayed += 1;
|
|
233
|
+
}
|
|
234
|
+
return replayed;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* `session/load_earlier` — deliver one page of history strictly older than
|
|
238
|
+
* the `before` cursor, oldest → newest (clients prepend). Requires the
|
|
239
|
+
* session to already be attached in this bridge; pagination never triggers
|
|
240
|
+
* an implicit backend resume.
|
|
241
|
+
*/
|
|
242
|
+
export async function loadEarlier(server, params, cx) {
|
|
243
|
+
const acpSid = params.sessionId;
|
|
244
|
+
const zcodeSid = server.resolveSid(acpSid);
|
|
245
|
+
if (!zcodeSid) {
|
|
246
|
+
return throwError(-32602, "session not registered — attach via session/load first");
|
|
247
|
+
}
|
|
248
|
+
if (!params.before)
|
|
249
|
+
return throwError(-32602, "before (cursor) required");
|
|
250
|
+
const messages = await fetchMessages(server, zcodeSid);
|
|
251
|
+
const slice = sliceBefore(messages, params.before, params.limit ?? DEFAULT_EARLIER_LIMIT);
|
|
252
|
+
await withReplayBatch(acpSid, () => replayMessages(cx, acpSid, slice.batch));
|
|
253
|
+
log(`session/load_earlier: ${slice.meta.replayedMessages} messages before cursor`);
|
|
254
|
+
return { replayMeta: slice.meta };
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/handlers/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAMzC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AA+BxC;;;GAGG;AACH,SAAS,UAAU,CAAC,QAAwB;IAC1C,MAAM,MAAM,GAAa,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACxB,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,IAAI,CAAC,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gDAAgD;AAChD,SAAS,YAAY,CAAC,MAAgB,EAAE,KAAa,EAAE,GAAW;IAChE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;AAC5D,CAAC;AAED,SAAS,YAAY,CAAC,QAAwB,EAAE,KAAa,EAAE,UAAkB;IAC/E,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;IACzC,MAAM,OAAO,GAAkB,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC3D,IAAI,MAAM;QAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC;IAChC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAkB,CAAC;QAC3F,IACE,GAAG,EAAE,CAAC,KAAK,CAAC;YACZ,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YAC5B,GAAG,CAAC,KAAK,GAAG,CAAC;YACb,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,EACjC,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;QACtE,OAAO,UAAU,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CACjB,QAAwB,EACxB,MAAgB,EAChB,KAAa,EACb,GAAW;IAEX,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;IACjC,OAAO;QACL,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC;QACjC,IAAI,EAAE;YACJ,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC;YACjD,OAAO,EAAE,KAAK,GAAG,CAAC;YAClB,gBAAgB,EAAE,GAAG,GAAG,KAAK;YAC7B,aAAa,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC;YAC/C,aAAa,EAAE,QAAQ,CAAC,MAAM;YAC9B,UAAU;SACX;KACF,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,gBAAgB,CAAC,MAAgB,EAAE,GAAW;IACrD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC,CAAC;;YACrB,MAAM;IACb,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,QAAwB,EAAE,KAAa;IAC/D,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;QAClB,0EAA0E;QAC1E,iDAAiD;QACjD,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,OAAO,IAAI,QAAQ,CAAC,MAAM;QAAE,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxF,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;IAClE,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,QAAwB;IAChD,OAAO,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,QAAwB,EAAE,MAAc,EAAE,KAAa;IACjF,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QAClE,OAAO,UAAU,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;QAC7F,OAAO,UAAU,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAC9C,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC;IACtB,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC;IACzF,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAA8B;IAC1D,MAAM,KAAK,GAAI,MAAM,CAAC,KAAqD,EAAE,KAAK,CAAC;IACnF,MAAM,GAAG,GAAG,KAAK,EAAE,KAAK,CAAC;IACzB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAsB,EACtB,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAChC,MAAM,CAAC,MAAM,EAAE,EACf,kBAAkB,EAClB,EAAE,SAAS,EAAE,QAAQ,EAAE,EACvB,IAAI,CACL,CAAC;IACF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,wEAAwE;QACxE,yEAAyE;QACzE,IAAI,CAAC,+BAA+B,QAAQ,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7E,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAwB,CAAC;IAC1D,OAAO,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,+CAA+C,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAoB,EACpB,MAAc,EACd,QAAwB;IAExB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,IAAI,QAAQ,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC3D,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,SAAS;YAC1C,MAAM,KAAK,GAAI,CAAuB,CAAC,IAAI,CAAC;YAC5C,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBACrB,IAAI,IAAI,GAAI,CAAuB,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC/C,IAAI,CAAC,IAAI;oBAAE,SAAS;gBACpB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,IAAI,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;oBAClC,IAAI,CAAC,IAAI;wBAAE,SAAS;gBACtB,CAAC;gBACD,MAAM,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;oBAChC,SAAS,EAAE,MAAM;oBACjB,MAAM,EAAE;wBACN,aAAa,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,qBAAqB;wBAC7E,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;wBAC/B,SAAS,EAAE,GAAG;qBACf;iBACF,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;gBACjC,MAAM,EAAE,GAAG,CAAwC,CAAC;gBACpD,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC;gBACzC,IAAI,IAAI,EAAE,CAAC;oBACT,MAAM,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;wBAChC,SAAS,EAAE,MAAM;wBACjB,MAAM,EAAE;4BACN,aAAa,EAAE,qBAAqB;4BACpC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;4BAC/B,SAAS,EAAE,WAAW,GAAG,EAAE;yBAC5B;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;iBAAM,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBAC5B,MAAM,EAAE,GAAG,CAKV,CAAC;gBACF,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,IAAI,WAAW,CAAC;gBACjD,MAAM,YAAY,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;gBACnC,MAAM,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;oBAChC,SAAS,EAAE,MAAM;oBACjB,MAAM,EAAE;wBACN,aAAa,EAAE,WAAW;wBAC1B,UAAU,EAAE,EAAE,CAAC,EAAE,IAAI,YAAY,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;wBAC3D,KAAK;wBACL,IAAI,EAAE,OAAO;wBACb,MAAM,EAAG,EAAE,CAAC,MAA6B,IAAI,WAAW;wBACxD,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC/E;iBACF,CAAC,CAAC;YACL,CAAC;YACD,sFAAsF;QACxF,CAAC;QACD,QAAQ,IAAI,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAsB,EACtB,MAAyB,EACzB,EAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,UAAU,CAAC,CAAC,KAAK,EAAE,wDAAwD,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,UAAU,CAAC,CAAC,KAAK,EAAE,0BAA0B,CAAC,CAAC;IAE1E,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,qBAAqB,CAAC,CAAC;IAC1F,MAAM,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7E,GAAG,CAAC,yBAAyB,KAAK,CAAC,IAAI,CAAC,gBAAgB,yBAAyB,CAAC,CAAC;IACnF,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -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)
|
|
@@ -155,6 +160,8 @@ export async function ensureRealSession(server, acpSid) {
|
|
|
155
160
|
throw new Error("zcode create returned no sessionId");
|
|
156
161
|
server.pendingSessions.delete(acpSid);
|
|
157
162
|
server.registerSession(acpSid, sid);
|
|
163
|
+
// session/create loads the session into this backend process.
|
|
164
|
+
server.backendLoadedSessions.add(acpSid);
|
|
158
165
|
// Keep the durable alias in sync so a later bridge restart can still
|
|
159
166
|
// resume this session via the placeholder id.
|
|
160
167
|
recordMaterializedSession(acpSid, sid, pending.cwd);
|
|
@@ -201,13 +208,47 @@ export async function listSessions(server, params) {
|
|
|
201
208
|
log(`session/list → ${sessions.length} sessions`);
|
|
202
209
|
return { sessions };
|
|
203
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Adopt the backend's stored title for a loaded/resumed session.
|
|
213
|
+
*
|
|
214
|
+
* The prompt loop's auto-title only fires for freshly created sessions
|
|
215
|
+
* (`titleEligibleSessions`), so a session resumed across a bridge restart
|
|
216
|
+
* would otherwise appear title-less in the hub's discovery API — remote
|
|
217
|
+
* clients have no editor-side session storage to fall back on. The backend's
|
|
218
|
+
* session/list is the only title source for sessions born in a previous
|
|
219
|
+
* bridge lifetime. Best-effort: failures log and leave the session untitled.
|
|
220
|
+
*/
|
|
221
|
+
async function adoptStoredTitle(server, acpSid, zcodeSid) {
|
|
222
|
+
if (server.sessionTitles.has(acpSid))
|
|
223
|
+
return;
|
|
224
|
+
try {
|
|
225
|
+
const backend = server.ensureBackend();
|
|
226
|
+
const resp = await backend.request(server.nextId(), "session/list", {}, 15000);
|
|
227
|
+
if (resp.error)
|
|
228
|
+
return;
|
|
229
|
+
const result = (resp.result ?? {});
|
|
230
|
+
const hit = (result.sessions ?? []).find((s) => s.sessionId === zcodeSid);
|
|
231
|
+
if (hit?.title) {
|
|
232
|
+
server.sessionTitles.set(acpSid, hit.title);
|
|
233
|
+
server.touchSessionSummary(acpSid, hit.title);
|
|
234
|
+
log(`adopted stored title for ${acpSid.slice(0, 8)}: ${hit.title}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
log(`stored title lookup failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
204
241
|
/**
|
|
205
242
|
* Resolve the backend session id for `session/resume` / `session/load`.
|
|
206
243
|
*
|
|
207
244
|
* A `session/new` placeholder has no backend counterpart until first use, yet
|
|
208
245
|
* the editor may resume it anyway (panel reopen, bridge restart) — resolving it
|
|
209
246
|
* here prevents an otherwise unavoidable "Session not found". Resolution order:
|
|
210
|
-
* 1. in-memory mapping →
|
|
247
|
+
* 1. in-memory mapping → live only if verified loaded in this backend
|
|
248
|
+
* subprocess (`backendLoadedSessions`); a bare mapping may have been
|
|
249
|
+
* re-registered from the durable store without a resume, and the backend
|
|
250
|
+
* only serves messages for sessions it has loaded — those must fall
|
|
251
|
+
* through to the resume RPC or the replay comes back empty;
|
|
211
252
|
* 2. pending placeholder → materialize it (an empty session, matching the
|
|
212
253
|
* pre-lazy behavior where a never-used session/new always resumed);
|
|
213
254
|
* 3. durable store → a placeholder from a previous bridge lifetime: with a
|
|
@@ -219,8 +260,9 @@ export async function listSessions(server, params) {
|
|
|
219
260
|
*/
|
|
220
261
|
async function resolveResumeTarget(server, acpSid) {
|
|
221
262
|
const mapped = server.resolveSid(acpSid);
|
|
222
|
-
if (mapped)
|
|
223
|
-
return { zcodeSid: mapped, alreadyLive:
|
|
263
|
+
if (mapped) {
|
|
264
|
+
return { zcodeSid: mapped, alreadyLive: server.backendLoadedSessions.has(acpSid) };
|
|
265
|
+
}
|
|
224
266
|
if (server.pendingSessions.has(acpSid)) {
|
|
225
267
|
return { zcodeSid: await ensureRealSession(server, acpSid), alreadyLive: true };
|
|
226
268
|
}
|
|
@@ -269,10 +311,13 @@ export async function resumeSession(server, params, cx) {
|
|
|
269
311
|
// registered to even process the resume turn.
|
|
270
312
|
await syncProviderRegistry(server, cwd);
|
|
271
313
|
await resumeBackendSession(server, zcParams);
|
|
314
|
+
// The resume RPC succeeded — the session is now loaded in this backend.
|
|
315
|
+
server.backendLoadedSessions.add(acpSid);
|
|
272
316
|
}
|
|
273
317
|
server.registerSession(acpSid, zcodeSid);
|
|
274
318
|
log(`session/resume -> ${zcodeSid}`);
|
|
275
319
|
server.ensureBackgroundListener(zcodeSid);
|
|
320
|
+
await adoptStoredTitle(server, acpSid, zcodeSid);
|
|
276
321
|
// Initial usage_update so the editor shows the context bar immediately for a
|
|
277
322
|
// resumed session (mirrors Python _on_session_resume → _emit_initial_usage).
|
|
278
323
|
await emitInitialUsage(server, cx, acpSid, zcodeSid, getOrCreateDiffer(server, zcodeSid));
|
|
@@ -308,61 +353,26 @@ export async function loadSession(server, params, cx) {
|
|
|
308
353
|
// registered to process it.
|
|
309
354
|
await syncProviderRegistry(server, cwd);
|
|
310
355
|
await resumeBackendSession(server, zcParams);
|
|
356
|
+
// The resume RPC succeeded — the session is now loaded in this backend.
|
|
357
|
+
server.backendLoadedSessions.add(acpSid);
|
|
311
358
|
}
|
|
312
359
|
server.registerSession(acpSid, zcodeSid);
|
|
313
360
|
log(`session/load → ${zcodeSid}`);
|
|
314
361
|
server.ensureBackgroundListener(zcodeSid);
|
|
362
|
+
await adoptStoredTitle(server, acpSid, zcodeSid);
|
|
315
363
|
const messages = await fetchMessages(server, zcodeSid);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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`);
|
|
364
|
+
// History on disk = real interaction (covers untitled sessions resumed from
|
|
365
|
+
// a previous bridge lifetime) — make the session discoverable remotely.
|
|
366
|
+
if (messages.length > 0)
|
|
367
|
+
server.markSessionActive(acpSid);
|
|
368
|
+
// Tail replay (Proposal 0001): a `_meta.zcode.limit` replays only the last
|
|
369
|
+
// N messages aligned to turn boundaries — the full replay stays the default
|
|
370
|
+
// for editors that send no `_meta` (Zed path unchanged).
|
|
371
|
+
const limit = readTailLimit(params);
|
|
372
|
+
const slice = limit === null ? fullSlice(messages) : sliceTail(messages, limit);
|
|
373
|
+
await withReplayBatch(acpSid, () => replayMessages(cx, acpSid, slice.batch));
|
|
374
|
+
log(`session/load: replayed ${slice.meta.replayedMessages} messages` +
|
|
375
|
+
`${limit === null ? "" : ` (tail limit ${limit}, total ${slice.meta.totalMessages})`}`);
|
|
366
376
|
// Replay the existing todo list as an initial plan so a loaded session shows
|
|
367
377
|
// its todos immediately (filter to PlanUpdate only — text/tools were already
|
|
368
378
|
// replayed above and the differ hasn't mark_seen'd this history).
|
|
@@ -381,10 +391,13 @@ export async function loadSession(server, params, cx) {
|
|
|
381
391
|
await emitInitialUsage(server, cx, acpSid, zcodeSid, getOrCreateDiffer(server, zcodeSid));
|
|
382
392
|
const modes = await buildModes(server, zcodeSid);
|
|
383
393
|
server.lastMode.set(acpSid, modes.currentModeId);
|
|
384
|
-
|
|
394
|
+
const result = {
|
|
385
395
|
modes,
|
|
386
396
|
configOptions: await buildConfigOptions(server, zcodeSid),
|
|
397
|
+
// Additive replay metadata — the anchor for load_earlier pagination.
|
|
398
|
+
replayMeta: slice.meta,
|
|
387
399
|
};
|
|
400
|
+
return result;
|
|
388
401
|
}
|
|
389
402
|
/** `session/prompt` → subscribe-before-send, run the event-driven turn loop. */
|
|
390
403
|
export async function prompt(server, params, cx, requestId) {
|
|
@@ -400,11 +413,17 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
400
413
|
// empty-prompt check so an invalid request doesn't create a backend session.
|
|
401
414
|
const zcodeSid = await ensureRealSession(server, params.sessionId);
|
|
402
415
|
// Slash-command interception: dispatches directly to ZCode methods and
|
|
403
|
-
// returns end_turn without entering the turn loop.
|
|
404
|
-
|
|
416
|
+
// returns end_turn without entering the turn loop. Known passthrough
|
|
417
|
+
// commands and unknown /x both return null for the normal turn loop.
|
|
418
|
+
const { handleSlashCommand, neutralizeSlashText } = await import("./slash.js");
|
|
405
419
|
const intercepted = await handleSlashCommand(server, cx, params.sessionId, zcodeSid, text);
|
|
406
420
|
if (intercepted)
|
|
407
421
|
return intercepted;
|
|
422
|
+
// Wire text for the backend: unknown `/x` prompts (not advertised commands)
|
|
423
|
+
// are neutralized so the backend's command resolver never sees them — an
|
|
424
|
+
// unresolvable name can hard-fail the turn. Known commands pass through
|
|
425
|
+
// unchanged. The title/auto-compact paths below keep using the raw `text`.
|
|
426
|
+
const sendText = neutralizeSlashText(text);
|
|
408
427
|
// Register self + preempt others under a per-session lock. The lock
|
|
409
428
|
// serializes the critical section so that two concurrent prompts (B, C) for
|
|
410
429
|
// the same session can't both miss each other and register at once: C waits
|
|
@@ -491,8 +510,8 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
491
510
|
const SEND_RETRY_INTERVAL_MS = 500;
|
|
492
511
|
const SEND_RETRY_TIMEOUT_MS = 30_000;
|
|
493
512
|
const sendParams = attachments.length > 0
|
|
494
|
-
? { sessionId: zcodeSid, content:
|
|
495
|
-
: { sessionId: zcodeSid, content:
|
|
513
|
+
? { sessionId: zcodeSid, content: sendText, attachments }
|
|
514
|
+
: { sessionId: zcodeSid, content: sendText };
|
|
496
515
|
const sendT0 = Date.now();
|
|
497
516
|
let sendAttempt = 0;
|
|
498
517
|
while (true) {
|
|
@@ -556,6 +575,7 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
556
575
|
.find((l) => l.length > 0)
|
|
557
576
|
?.slice(0, 80) ?? text.slice(0, 80);
|
|
558
577
|
server.sessionTitles.set(params.sessionId, title);
|
|
578
|
+
server.touchSessionSummary(params.sessionId, title);
|
|
559
579
|
const { updateSessionTitle } = await import("../tasks-index.js");
|
|
560
580
|
void updateSessionTitle(zcodeSid, title, text);
|
|
561
581
|
await sendSessionUpdate(cx, params.sessionId, {
|
|
@@ -603,6 +623,10 @@ export async function prompt(server, params, cx, requestId) {
|
|
|
603
623
|
finally {
|
|
604
624
|
backend.unregisterEventListener(zcodeSid, listener);
|
|
605
625
|
server.pendingTurns.delete(requestId);
|
|
626
|
+
// Turn end = session activity — refresh the discovery summary and mark the
|
|
627
|
+
// session discoverable regardless of outcome (end_turn, cancelled, retries
|
|
628
|
+
// exhausted).
|
|
629
|
+
server.markSessionActive(params.sessionId);
|
|
606
630
|
}
|
|
607
631
|
}
|
|
608
632
|
/**
|
|
@@ -914,15 +938,6 @@ async function resumeBackendSession(server, zcParams) {
|
|
|
914
938
|
await sleep(1000);
|
|
915
939
|
}
|
|
916
940
|
}
|
|
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
941
|
/** Get or create the session-level ProjectionDiffer (persists across turns). */
|
|
927
942
|
function getOrCreateDiffer(server, zcodeSid) {
|
|
928
943
|
let d = server.differs.get(zcodeSid);
|