lark-remote 0.1.7 → 0.1.9
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/CHANGELOG.md +46 -0
- package/dist/bridge/approval-coordinator.d.ts +30 -1
- package/dist/bridge/approval-coordinator.js +93 -5
- package/dist/bridge/index.d.ts +36 -6
- package/dist/bridge/index.js +56 -10
- package/dist/card/approval-render.js +287 -48
- package/dist/card/card-shared.d.ts +6 -4
- package/dist/card/card-shared.js +6 -4
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +22 -11
- package/dist/config/index.d.ts +24 -1
- package/dist/config/index.js +34 -3
- package/dist/index.js +40 -16
- package/dist/order/alias-resolve.d.ts +6 -2
- package/dist/order/index.d.ts +9 -2
- package/dist/order/index.js +33 -0
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/config/claude.js +8 -3
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/index.js +4 -1
- package/dist/router/config/types.d.ts +6 -0
- package/dist/router/index.d.ts +39 -4
- package/dist/router/index.js +471 -90
- package/dist/router/utils.js +12 -2
- package/dist/runner/agent-choices-common.js +3 -0
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +105 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
- package/dist/runner/codex/app-server/protocol-types.js +1 -1
- package/dist/runner/codex/app-server/runner.d.ts +10 -0
- package/dist/runner/codex/app-server/runner.js +56 -1
- package/dist/runner/codex/app-server/translator.d.ts +7 -0
- package/dist/runner/codex/app-server/translator.js +45 -0
- package/dist/runner/common/acp/protocol-types.d.ts +33 -0
- package/dist/runner/common/acp/protocol-types.js +2 -0
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +211 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +1 -0
- package/dist/runner/index.js +1 -0
- package/dist/runner/kimi/acp/runner.js +84 -1
- package/dist/runner/kimi/acp/translator.d.ts +14 -0
- package/dist/runner/kimi/acp/translator.js +108 -3
- package/dist/runner/pi/rpc/runner.d.ts +1 -7
- package/dist/runner/pi/rpc/runner.js +8 -12
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/types.d.ts +94 -10
- package/dist/session/claude/sessions.js +37 -15
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +44 -0
- package/dist/session/dsh/sessions.js +252 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/workspace/index.d.ts +12 -0
- package/dist/workspace/index.js +28 -2
- package/package.json +2 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DshClient — HTTP client for the DSH Web Host proxy.
|
|
3
|
+
*
|
|
4
|
+
* Two transports:
|
|
5
|
+
* 1. unary POST /api/<method> (client-request → server-response).
|
|
6
|
+
* 2. SSE GET /api/events.mux (all-session aggregated stream). Frames are
|
|
7
|
+
* ServerRequest envelopes; `method` equals the MuxFrame's `type`.
|
|
8
|
+
*
|
|
9
|
+
* The client is stateless apart from the base URL: each `run()` opens its own
|
|
10
|
+
* mux subscription and closes it on terminal. `sessionEvents()` wraps the raw
|
|
11
|
+
* mux with per-session filtering plus reconnect + history-replay dedup
|
|
12
|
+
* (DSH v1 has no `since` resume — reconnection = reopen stream + refetch
|
|
13
|
+
* history, dedup by event seq).
|
|
14
|
+
*/
|
|
15
|
+
import type { DshModelCatalogValue, DshModelSelection, DshPresetListValue, DshServerRequest, DshSessionModelsValue, DshStreamItem, DshTokenUsage } from './types.js';
|
|
16
|
+
interface WebSocketMessageEvent {
|
|
17
|
+
data: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface WebSocketLike {
|
|
20
|
+
onmessage: ((event: WebSocketMessageEvent) => void) | null;
|
|
21
|
+
onerror: ((event: unknown) => void) | null;
|
|
22
|
+
onclose: ((event: unknown) => void) | null;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
export type WebSocketFactory = (url: string) => WebSocketLike;
|
|
26
|
+
export declare class DshError extends Error {
|
|
27
|
+
readonly code: string;
|
|
28
|
+
readonly details: Record<string, unknown>;
|
|
29
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
30
|
+
}
|
|
31
|
+
export declare class DshClient {
|
|
32
|
+
readonly baseUrl: string;
|
|
33
|
+
private readonly fetchImpl;
|
|
34
|
+
private readonly webSocketImpl;
|
|
35
|
+
constructor(baseUrl: string, fetchImpl?: typeof fetch, webSocketImpl?: WebSocketFactory);
|
|
36
|
+
/** Perform a unary POST and return the ok value; throws DshError on failure. */
|
|
37
|
+
unary(method: string, payload: unknown): Promise<unknown>;
|
|
38
|
+
/** Create a session. Pass `agentPreset` to pin the session's preset (fixed at
|
|
39
|
+
* creation). */
|
|
40
|
+
createSession(payload: {
|
|
41
|
+
cwd: string;
|
|
42
|
+
agentPreset?: string;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
sessionId: string;
|
|
45
|
+
agentPreset?: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Align the session's model/reasoningEffort (also persists as global default
|
|
48
|
+
* server-side). Failures are surfaced as DshError for the caller to downgrade. */
|
|
49
|
+
selectModel(payload: DshModelSelection & {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
}): Promise<DshModelSelection>;
|
|
52
|
+
/** Read the session's current model selection + full model catalog. */
|
|
53
|
+
sessionModels(sessionId: string): Promise<DshSessionModelsValue>;
|
|
54
|
+
/** Read the model catalog (session-independent, llm.models). */
|
|
55
|
+
listModels(): Promise<DshModelCatalogValue>;
|
|
56
|
+
/** Read the preset catalog (agentPreset.list). */
|
|
57
|
+
listPresets(): Promise<DshPresetListValue>;
|
|
58
|
+
/**
|
|
59
|
+
* Return the highest event `seq` currently persisted in session.history.
|
|
60
|
+
*
|
|
61
|
+
* The caller snapshots this before `session.prompt`. `session.history` is a
|
|
62
|
+
* full-session replay rather than a delta, so this high-water mark is what
|
|
63
|
+
* lets a subsequent replay skip every already-completed turn.
|
|
64
|
+
*/
|
|
65
|
+
latestEventSeq(sessionId: string): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* Raw SSE mux stream: yields ServerRequest envelopes until the stream ends,
|
|
68
|
+
* errors, or `signal` aborts. Does NOT reconnect (the caller owns retry).
|
|
69
|
+
*/
|
|
70
|
+
mux(signal: AbortSignal): AsyncGenerator<DshServerRequest>;
|
|
71
|
+
/**
|
|
72
|
+
* WebSocket mux stream. The real DSH `/api/events.mux` endpoint is a
|
|
73
|
+
* WebSocket upgrade, not HTTP SSE: each text message is already a
|
|
74
|
+
* `DshServerRequest` JSON envelope. This yields those envelopes and ends on
|
|
75
|
+
* close/abort, letting `sessionEvents()` reconnect through the same
|
|
76
|
+
* history-replay loop.
|
|
77
|
+
*/
|
|
78
|
+
muxWebSocket(signal: AbortSignal): AsyncGenerator<DshServerRequest>;
|
|
79
|
+
/**
|
|
80
|
+
* Replay session.history events that are newer than `state.lastSeq`, updating
|
|
81
|
+
* `state.lastSeq` as it goes. Yields each non-duplicate SessionEvent. Used for
|
|
82
|
+
* both the initial baseline (first subscription) and reconnect gap-fill.
|
|
83
|
+
*/
|
|
84
|
+
private replayHistory;
|
|
85
|
+
/**
|
|
86
|
+
* Per-session subscription: yields session events (and approval notices) for
|
|
87
|
+
* `sessionId`, auto-reconnecting on stream drop/error and replaying
|
|
88
|
+
* `session.history` to fill the gap (dedup by seq). Terminates on turn/end
|
|
89
|
+
* or `signal` abort.
|
|
90
|
+
*/
|
|
91
|
+
sessionEvents(sessionId: string, cwd: string, signal: AbortSignal, initialSeq?: number): AsyncGenerator<DshStreamItem>;
|
|
92
|
+
/** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
|
|
93
|
+
static usageOf(data: Record<string, unknown>): DshTokenUsage | undefined;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DshClient — HTTP client for the DSH Web Host proxy.
|
|
3
|
+
*
|
|
4
|
+
* Two transports:
|
|
5
|
+
* 1. unary POST /api/<method> (client-request → server-response).
|
|
6
|
+
* 2. SSE GET /api/events.mux (all-session aggregated stream). Frames are
|
|
7
|
+
* ServerRequest envelopes; `method` equals the MuxFrame's `type`.
|
|
8
|
+
*
|
|
9
|
+
* The client is stateless apart from the base URL: each `run()` opens its own
|
|
10
|
+
* mux subscription and closes it on terminal. `sessionEvents()` wraps the raw
|
|
11
|
+
* mux with per-session filtering plus reconnect + history-replay dedup
|
|
12
|
+
* (DSH v1 has no `since` resume — reconnection = reopen stream + refetch
|
|
13
|
+
* history, dedup by event seq).
|
|
14
|
+
*/
|
|
15
|
+
import { getLogger } from '../../logger/index.js';
|
|
16
|
+
function defaultWebSocketFactory(url) {
|
|
17
|
+
const Ctor = globalThis.WebSocket;
|
|
18
|
+
if (!Ctor)
|
|
19
|
+
throw new Error('DSH WebSocket is unavailable');
|
|
20
|
+
return new Ctor(url);
|
|
21
|
+
}
|
|
22
|
+
export class DshError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
details;
|
|
25
|
+
constructor(code, message, details = {}) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'DshError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.details = details;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Backoff between mux reconnects to avoid a hot loop on a dropping server. */
|
|
33
|
+
const RECONNECT_BACKOFF_MS = 200;
|
|
34
|
+
export class DshClient {
|
|
35
|
+
baseUrl;
|
|
36
|
+
fetchImpl;
|
|
37
|
+
webSocketImpl;
|
|
38
|
+
constructor(baseUrl, fetchImpl = fetch, webSocketImpl = defaultWebSocketFactory) {
|
|
39
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
40
|
+
this.fetchImpl = fetchImpl;
|
|
41
|
+
this.webSocketImpl = webSocketImpl;
|
|
42
|
+
}
|
|
43
|
+
/** Perform a unary POST and return the ok value; throws DshError on failure. */
|
|
44
|
+
async unary(method, payload) {
|
|
45
|
+
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
46
|
+
let res;
|
|
47
|
+
try {
|
|
48
|
+
res = await this.fetchImpl(`${this.baseUrl}/api/${method}`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw new DshError('transport', `DSH unary ${method} transport error: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new DshError('http', `DSH HTTP ${res.status} for ${method}`);
|
|
59
|
+
}
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = (await res.json());
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new DshError('protocol', `DSH ${method} returned non-JSON: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
if (body.result?.ok !== true) {
|
|
68
|
+
const err = body.result?.error;
|
|
69
|
+
throw new DshError(err?.code ?? 'error', err?.message ?? `DSH ${method} failed`, err?.details ?? {});
|
|
70
|
+
}
|
|
71
|
+
return body.result.value;
|
|
72
|
+
}
|
|
73
|
+
/** Create a session. Pass `agentPreset` to pin the session's preset (fixed at
|
|
74
|
+
* creation). */
|
|
75
|
+
async createSession(payload) {
|
|
76
|
+
return (await this.unary('session.create', payload));
|
|
77
|
+
}
|
|
78
|
+
/** Align the session's model/reasoningEffort (also persists as global default
|
|
79
|
+
* server-side). Failures are surfaced as DshError for the caller to downgrade. */
|
|
80
|
+
async selectModel(payload) {
|
|
81
|
+
const value = (await this.unary('session.selectModel', payload));
|
|
82
|
+
return value.selected;
|
|
83
|
+
}
|
|
84
|
+
/** Read the session's current model selection + full model catalog. */
|
|
85
|
+
async sessionModels(sessionId) {
|
|
86
|
+
return (await this.unary('session.models', { sessionId }));
|
|
87
|
+
}
|
|
88
|
+
/** Read the model catalog (session-independent, llm.models). */
|
|
89
|
+
async listModels() {
|
|
90
|
+
return (await this.unary('llm.models', {}));
|
|
91
|
+
}
|
|
92
|
+
/** Read the preset catalog (agentPreset.list). */
|
|
93
|
+
async listPresets() {
|
|
94
|
+
return (await this.unary('agentPreset.list', {}));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Return the highest event `seq` currently persisted in session.history.
|
|
98
|
+
*
|
|
99
|
+
* The caller snapshots this before `session.prompt`. `session.history` is a
|
|
100
|
+
* full-session replay rather than a delta, so this high-water mark is what
|
|
101
|
+
* lets a subsequent replay skip every already-completed turn.
|
|
102
|
+
*/
|
|
103
|
+
async latestEventSeq(sessionId) {
|
|
104
|
+
const value = (await this.unary('session.history', {
|
|
105
|
+
sessionId,
|
|
106
|
+
maxMessages: 1,
|
|
107
|
+
}));
|
|
108
|
+
let max = -1;
|
|
109
|
+
for (const entry of value.events ?? []) {
|
|
110
|
+
const seq = entry.event?.seq;
|
|
111
|
+
if (typeof seq === 'number' && seq > max)
|
|
112
|
+
max = seq;
|
|
113
|
+
}
|
|
114
|
+
return max;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Raw SSE mux stream: yields ServerRequest envelopes until the stream ends,
|
|
118
|
+
* errors, or `signal` aborts. Does NOT reconnect (the caller owns retry).
|
|
119
|
+
*/
|
|
120
|
+
async *mux(signal) {
|
|
121
|
+
let res;
|
|
122
|
+
try {
|
|
123
|
+
res = await this.fetchImpl(`${this.baseUrl}/api/events.mux`, {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: { accept: 'text/event-stream' },
|
|
126
|
+
signal,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (signal.aborted)
|
|
131
|
+
return;
|
|
132
|
+
throw new DshError('transport', `DSH mux open error: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
if (!res.ok || !res.body) {
|
|
135
|
+
throw new DshError('http', `DSH mux HTTP ${res.status}`, { status: res.status });
|
|
136
|
+
}
|
|
137
|
+
const reader = res.body.getReader();
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
let buffer = '';
|
|
140
|
+
try {
|
|
141
|
+
for (;;) {
|
|
142
|
+
if (signal.aborted)
|
|
143
|
+
break;
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await reader.read();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (signal.aborted)
|
|
150
|
+
break;
|
|
151
|
+
throw new DshError('transport', `DSH mux read error: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
if (result.done)
|
|
154
|
+
break;
|
|
155
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
156
|
+
let idx;
|
|
157
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
158
|
+
const chunk = buffer.slice(0, idx);
|
|
159
|
+
buffer = buffer.slice(idx + 2);
|
|
160
|
+
const dataLine = chunk.split('\n').find((l) => l.startsWith('data:'));
|
|
161
|
+
if (!dataLine)
|
|
162
|
+
continue;
|
|
163
|
+
const json = dataLine.slice(5).trim();
|
|
164
|
+
if (!json)
|
|
165
|
+
continue;
|
|
166
|
+
try {
|
|
167
|
+
const frame = JSON.parse(json);
|
|
168
|
+
if (frame?.type === 'server-request')
|
|
169
|
+
yield frame;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
getLogger().warn('[dsh] dropped malformed mux frame');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
reader.releaseLock();
|
|
179
|
+
try {
|
|
180
|
+
await res.body.cancel();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* ignore cancel error */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* WebSocket mux stream. The real DSH `/api/events.mux` endpoint is a
|
|
189
|
+
* WebSocket upgrade, not HTTP SSE: each text message is already a
|
|
190
|
+
* `DshServerRequest` JSON envelope. This yields those envelopes and ends on
|
|
191
|
+
* close/abort, letting `sessionEvents()` reconnect through the same
|
|
192
|
+
* history-replay loop.
|
|
193
|
+
*/
|
|
194
|
+
async *muxWebSocket(signal) {
|
|
195
|
+
let socket;
|
|
196
|
+
try {
|
|
197
|
+
socket = this.webSocketImpl(`${this.baseUrl.replace(/^http/, 'ws')}/api/events.mux`);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
throw new DshError('transport', `DSH mux WebSocket open error: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
const queue = [];
|
|
203
|
+
let waiter = null;
|
|
204
|
+
let ended = false;
|
|
205
|
+
let failure;
|
|
206
|
+
const settle = (frame) => {
|
|
207
|
+
if (waiter) {
|
|
208
|
+
const resolve = waiter;
|
|
209
|
+
waiter = null;
|
|
210
|
+
resolve(frame);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (frame)
|
|
214
|
+
queue.push(frame);
|
|
215
|
+
else
|
|
216
|
+
ended = true;
|
|
217
|
+
};
|
|
218
|
+
socket.onmessage = (event) => {
|
|
219
|
+
if (ended)
|
|
220
|
+
return;
|
|
221
|
+
if (typeof event.data !== 'string')
|
|
222
|
+
return;
|
|
223
|
+
try {
|
|
224
|
+
const frame = JSON.parse(event.data);
|
|
225
|
+
if (frame?.type === 'server-request')
|
|
226
|
+
settle(frame);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
getLogger().warn('[dsh] dropped malformed WebSocket mux frame');
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
socket.onerror = () => {
|
|
233
|
+
if (ended)
|
|
234
|
+
return;
|
|
235
|
+
failure = new DshError('transport', 'DSH mux WebSocket error');
|
|
236
|
+
ended = true;
|
|
237
|
+
settle(undefined);
|
|
238
|
+
};
|
|
239
|
+
socket.onclose = () => {
|
|
240
|
+
if (ended)
|
|
241
|
+
return;
|
|
242
|
+
ended = true;
|
|
243
|
+
settle(undefined);
|
|
244
|
+
};
|
|
245
|
+
const onAbort = () => {
|
|
246
|
+
if (ended)
|
|
247
|
+
return;
|
|
248
|
+
ended = true;
|
|
249
|
+
settle(undefined);
|
|
250
|
+
try {
|
|
251
|
+
socket.close();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
/* ignore close error */
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
258
|
+
const next = () => new Promise((resolve) => {
|
|
259
|
+
if (queue.length > 0) {
|
|
260
|
+
resolve(queue.shift());
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (ended) {
|
|
264
|
+
resolve(undefined);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
waiter = resolve;
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
while (!signal.aborted) {
|
|
271
|
+
const frame = await next();
|
|
272
|
+
if (frame === undefined) {
|
|
273
|
+
if (failure && !signal.aborted)
|
|
274
|
+
throw failure;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
yield frame;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
signal.removeEventListener('abort', onAbort);
|
|
282
|
+
try {
|
|
283
|
+
socket.close();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
/* ignore close error */
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Replay session.history events that are newer than `state.lastSeq`, updating
|
|
292
|
+
* `state.lastSeq` as it goes. Yields each non-duplicate SessionEvent. Used for
|
|
293
|
+
* both the initial baseline (first subscription) and reconnect gap-fill.
|
|
294
|
+
*/
|
|
295
|
+
async *replayHistory(sessionId, signal, state, logTag) {
|
|
296
|
+
try {
|
|
297
|
+
const value = (await this.unary('session.history', {
|
|
298
|
+
sessionId,
|
|
299
|
+
maxMessages: 100,
|
|
300
|
+
}));
|
|
301
|
+
for (const entry of value.events ?? []) {
|
|
302
|
+
if (signal.aborted)
|
|
303
|
+
return;
|
|
304
|
+
const ev = entry.event;
|
|
305
|
+
if (!ev)
|
|
306
|
+
continue;
|
|
307
|
+
const seq = ev.seq;
|
|
308
|
+
if (typeof seq === 'number' && seq <= state.lastSeq)
|
|
309
|
+
continue;
|
|
310
|
+
if (typeof seq === 'number')
|
|
311
|
+
state.lastSeq = seq;
|
|
312
|
+
yield ev;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (signal.aborted)
|
|
317
|
+
return;
|
|
318
|
+
getLogger().warn(`[dsh] ${logTag} failed for session ${sessionId}: ${err.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Per-session subscription: yields session events (and approval notices) for
|
|
323
|
+
* `sessionId`, auto-reconnecting on stream drop/error and replaying
|
|
324
|
+
* `session.history` to fill the gap (dedup by seq). Terminates on turn/end
|
|
325
|
+
* or `signal` abort.
|
|
326
|
+
*/
|
|
327
|
+
async *sessionEvents(sessionId, cwd, signal, initialSeq = -1) {
|
|
328
|
+
const state = { lastSeq: initialSeq };
|
|
329
|
+
// Initial baseline: before opening the first live mux subscription, replay
|
|
330
|
+
// history once so events produced between prompt-accept and the mux opening
|
|
331
|
+
// (e.g. a fast turn that already ended) are not lost. Dedup by seq keeps the
|
|
332
|
+
// replay and the live stream convergent.
|
|
333
|
+
for await (const ev of this.replayHistory(sessionId, signal, state, 'initial history baseline')) {
|
|
334
|
+
if (signal.aborted)
|
|
335
|
+
return;
|
|
336
|
+
yield { kind: 'event', sessionId, event: ev };
|
|
337
|
+
if (ev.type === 'turn/end')
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let muxMode = 'fetch';
|
|
341
|
+
while (!signal.aborted) {
|
|
342
|
+
let dropped = false;
|
|
343
|
+
try {
|
|
344
|
+
const frames = muxMode === 'websocket' ? this.muxWebSocket(signal) : this.mux(signal);
|
|
345
|
+
for await (const frame of frames) {
|
|
346
|
+
if (signal.aborted)
|
|
347
|
+
return;
|
|
348
|
+
if (frame.method === 'stream/error') {
|
|
349
|
+
dropped = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
if (frame.method === 'approval/requested') {
|
|
353
|
+
const p = frame.payload;
|
|
354
|
+
if (p?.sessionId === sessionId && p.approvalId) {
|
|
355
|
+
yield {
|
|
356
|
+
kind: 'approval',
|
|
357
|
+
sessionId,
|
|
358
|
+
approvalId: p.approvalId,
|
|
359
|
+
toolName: p.toolName ?? '',
|
|
360
|
+
reason: p.reason,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (frame.method !== 'session/event')
|
|
366
|
+
continue;
|
|
367
|
+
const payload = frame.payload;
|
|
368
|
+
if (!payload || payload.sessionId !== sessionId || !payload.event)
|
|
369
|
+
continue;
|
|
370
|
+
const seq = payload.event.seq;
|
|
371
|
+
if (typeof seq === 'number' && seq <= state.lastSeq)
|
|
372
|
+
continue; // dedup
|
|
373
|
+
if (typeof seq === 'number')
|
|
374
|
+
state.lastSeq = seq;
|
|
375
|
+
yield { kind: 'event', sessionId, event: payload.event };
|
|
376
|
+
if (payload.event.type === 'turn/end')
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
dropped = true; // stream closed normally → treat as drop, replay history
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
if (signal.aborted)
|
|
383
|
+
return;
|
|
384
|
+
if (muxMode === 'fetch' &&
|
|
385
|
+
err instanceof DshError &&
|
|
386
|
+
err.code === 'http' &&
|
|
387
|
+
err.details.status === 426) {
|
|
388
|
+
getLogger().info(`[dsh] mux endpoint requires WebSocket upgrade; switching transports for session ${sessionId}`);
|
|
389
|
+
muxMode = 'websocket';
|
|
390
|
+
dropped = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
getLogger().warn(`[dsh] mux disconnected for session ${sessionId}: ${err.message}; replaying history`);
|
|
394
|
+
dropped = true;
|
|
395
|
+
}
|
|
396
|
+
if (signal.aborted)
|
|
397
|
+
return;
|
|
398
|
+
if (!dropped)
|
|
399
|
+
break;
|
|
400
|
+
// Reconnect baseline: replay history events after lastSeq (dedup keeps
|
|
401
|
+
// the live stream and the replay convergent).
|
|
402
|
+
for await (const ev of this.replayHistory(sessionId, signal, state, 'history replay')) {
|
|
403
|
+
if (signal.aborted)
|
|
404
|
+
return;
|
|
405
|
+
yield { kind: 'event', sessionId, event: ev };
|
|
406
|
+
if (ev.type === 'turn/end')
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
// Bound reconnect frequency.
|
|
410
|
+
await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
|
|
414
|
+
static usageOf(data) {
|
|
415
|
+
return data.usage ?? undefined;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fake DSH Web Host HTTP server for agent-dsh tests.
|
|
3
|
+
*
|
|
4
|
+
* Mimics the real DSH wire protocol (deepseek-harness apiproxy handler.ts):
|
|
5
|
+
* - POST /api/<method>: body {type:'client-request',rpcId,method,payload},
|
|
6
|
+
* response {type:'server-response',rpcId,result:{ok:true,value}|{ok:false,error}}.
|
|
7
|
+
* - SSE GET /api/events.mux: frames `data: {type:'server-request',rpcId,
|
|
8
|
+
* method:<frame.type>, payload:<frame>}\n\n`.
|
|
9
|
+
*
|
|
10
|
+
* The mux frame queue is consumed ACROSS connections (shared pointer): the
|
|
11
|
+
* first connection drains a prefix then closes; a reconnect drains the rest —
|
|
12
|
+
* which is exactly how a server that drops mid-turn is modelled. Unary
|
|
13
|
+
* handlers and the history value are registered by the test.
|
|
14
|
+
*/
|
|
15
|
+
import { type Server } from 'node:http';
|
|
16
|
+
export interface FakeRpcResult {
|
|
17
|
+
ok: true;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface FakeRpcError {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: {
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export type FakeUnaryHandler = (payload: unknown, rpcId: string) => FakeRpcResult | FakeRpcError;
|
|
29
|
+
export interface DshServerRequestRecord {
|
|
30
|
+
method: string;
|
|
31
|
+
rpcId: string;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
}
|
|
34
|
+
export declare class FakeDshServer {
|
|
35
|
+
readonly server: Server;
|
|
36
|
+
private readonly unaryHandlers;
|
|
37
|
+
private muxFrames;
|
|
38
|
+
private historyValue;
|
|
39
|
+
private port;
|
|
40
|
+
private closedConnections;
|
|
41
|
+
readonly requests: DshServerRequestRecord[];
|
|
42
|
+
constructor();
|
|
43
|
+
get baseUrl(): string;
|
|
44
|
+
get connectionCount(): number;
|
|
45
|
+
start(): Promise<void>;
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
register(method: string, handler: FakeUnaryHandler): void;
|
|
48
|
+
setMuxFrames(frames: Array<object | {
|
|
49
|
+
close: true;
|
|
50
|
+
}>): void;
|
|
51
|
+
setHistoryValue(value: unknown): void;
|
|
52
|
+
/** Synchronous unary dispatch (no HTTP) — used by tests that inject the
|
|
53
|
+
* reader's sync transport to avoid the spawnSync-in-process deadlock. */
|
|
54
|
+
dispatchSync(method: string, payload: unknown, rpcId?: string): {
|
|
55
|
+
type: string;
|
|
56
|
+
rpcId: string;
|
|
57
|
+
result: FakeRpcResult | FakeRpcError;
|
|
58
|
+
};
|
|
59
|
+
private handleMux;
|
|
60
|
+
}
|