lark-remote 0.5.19 → 0.5.21
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 +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -44,6 +44,49 @@ function isDshAuthError(error) {
|
|
|
44
44
|
error.code === 'http' &&
|
|
45
45
|
(error.details.status === 401 || error.details.status === 403));
|
|
46
46
|
}
|
|
47
|
+
/** 组合 abort/timeout signal 并生成本次 unary 的 rpcId(循环外一次算好)。 */
|
|
48
|
+
function prepareUnary(opts, requestTimeoutMs) {
|
|
49
|
+
const timeoutMs = opts.timeoutMs ?? requestTimeoutMs;
|
|
50
|
+
// 0/负数 = 不设超时(兼容「长请求不取消」的配置),其余走 AbortSignal.timeout。
|
|
51
|
+
const timeoutSignal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
52
|
+
const signal = opts.signal && timeoutSignal
|
|
53
|
+
? AbortSignal.any([opts.signal, timeoutSignal])
|
|
54
|
+
: (opts.signal ?? timeoutSignal);
|
|
55
|
+
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
56
|
+
return { signal, timeoutSignal, timeoutMs, rpcId };
|
|
57
|
+
}
|
|
58
|
+
/** Distinguish "run stopped" (abort) from "server did not answer" (timeout). */
|
|
59
|
+
function abortErrorOf(method, err, runSignal, timeoutSignal, timeoutMs) {
|
|
60
|
+
if (runSignal?.aborted) {
|
|
61
|
+
return new DshError('aborted', `DSH unary ${method} aborted`, { cause: err });
|
|
62
|
+
}
|
|
63
|
+
if (timeoutSignal?.aborted) {
|
|
64
|
+
return new DshError('timeout', `DSH unary ${method} timed out after ${timeoutMs}ms`, {
|
|
65
|
+
cause: err,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return new DshError('aborted', `DSH unary ${method} aborted: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
/** 校验 ok 响应并取 `result.value`;非 ok / 非 JSON / result 失败都抛 DshError。 */
|
|
71
|
+
async function readUnaryBody(method, res) {
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
throw new DshError('http', `DSH HTTP ${res.status} for ${method}`, {
|
|
74
|
+
status: res.status,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
let body;
|
|
78
|
+
try {
|
|
79
|
+
body = (await res.json());
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
throw new DshError('protocol', `DSH ${method} returned non-JSON: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
if (body.result?.ok !== true) {
|
|
85
|
+
const err = body.result?.error;
|
|
86
|
+
throw new DshError(err?.code ?? 'error', err?.message ?? `DSH ${method} failed`, err?.details ?? {});
|
|
87
|
+
}
|
|
88
|
+
return body.result.value;
|
|
89
|
+
}
|
|
47
90
|
/** Backoff between mux reconnects to avoid a hot loop on a dropping server. */
|
|
48
91
|
const RECONNECT_BACKOFF_MS = 200;
|
|
49
92
|
/**
|
|
@@ -112,6 +155,192 @@ function inboxItemsOf(values) {
|
|
|
112
155
|
}
|
|
113
156
|
return out;
|
|
114
157
|
}
|
|
158
|
+
/** Hand one waterfall delivery back with `next` (another answerer owns it). */
|
|
159
|
+
function delegateWaterfall(client, clientId, eventId, signal) {
|
|
160
|
+
void client
|
|
161
|
+
.answerRemoteEvent(clientId, eventId, { kind: 'next' }, { signal })
|
|
162
|
+
.catch((err) => {
|
|
163
|
+
if (!signal.aborted) {
|
|
164
|
+
getLogger().warn(`[dsh] waterfall delegation failed eventId=${eventId}: ${err.message}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** 一个 waterfall 帧 → 要 yield 的 items(含 question/request 委托)。 */
|
|
169
|
+
function waterfallToItems(client, clientId, frame, sessionId, signal) {
|
|
170
|
+
if (frame.event === 'user-questions/request' && frame.agentId === sessionId) {
|
|
171
|
+
if (isTimedQuestionRequest(frame.request)) {
|
|
172
|
+
// The timed wait owns its own deadline (and the Web UI renders the
|
|
173
|
+
// countdown); a card answer outside that window is lost.
|
|
174
|
+
getLogger().info(`[dsh] timed user-questions/request delegated to the Web UI eventId=${frame.eventId}`);
|
|
175
|
+
delegateWaterfall(client, clientId, frame.eventId, signal);
|
|
176
|
+
return { items: [], stop: false };
|
|
177
|
+
}
|
|
178
|
+
const questions = dshQuestionsOf(frame.request);
|
|
179
|
+
if (questions) {
|
|
180
|
+
return {
|
|
181
|
+
items: [{ kind: 'question', eventId: frame.eventId, clientId, questions }],
|
|
182
|
+
stop: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// The Feishu card cannot answer this batch safely (malformed, or duplicate
|
|
186
|
+
// texts — the coordinator keys answers by text).
|
|
187
|
+
getLogger().warn(`[dsh] user-questions/request not relayable; delegating eventId=${frame.eventId}`);
|
|
188
|
+
delegateWaterfall(client, clientId, frame.eventId, signal);
|
|
189
|
+
return { items: [], stop: false };
|
|
190
|
+
}
|
|
191
|
+
if (frame.event !== 'approval/request' || frame.agentId !== sessionId) {
|
|
192
|
+
// Every owner answerer receives the waterfall. A non-owner of the session
|
|
193
|
+
// must explicitly delegate, otherwise DSH waits.
|
|
194
|
+
delegateWaterfall(client, clientId, frame.eventId, signal);
|
|
195
|
+
return { items: [], stop: false };
|
|
196
|
+
}
|
|
197
|
+
const data = frame.request;
|
|
198
|
+
const toolName = typeof data.toolName === 'string' ? data.toolName : '';
|
|
199
|
+
const callId = typeof data.callId === 'string' ? data.callId : undefined;
|
|
200
|
+
const reason = typeof data.reason === 'string' ? data.reason : undefined;
|
|
201
|
+
return {
|
|
202
|
+
items: [
|
|
203
|
+
{
|
|
204
|
+
kind: 'request',
|
|
205
|
+
eventId: frame.eventId,
|
|
206
|
+
clientId,
|
|
207
|
+
toolName,
|
|
208
|
+
...(callId ? { callId } : {}),
|
|
209
|
+
...(reason ? { reason } : {}),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
stop: false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** 一个 `$events` 帧 → items(并按需更新 state)。 */
|
|
216
|
+
function processApprovalFrame(state, frame, sessionId, signal, client) {
|
|
217
|
+
if (frame.type === 'ready') {
|
|
218
|
+
state.authRefreshAttempted = false;
|
|
219
|
+
// 成功建连后补充重连预算:goal 驱动的 turn 可跑数小时,DSH 重启 + 网络抖动
|
|
220
|
+
// 累计掉线超过 3 次后旧实现会让 $events 永久 unavailable。
|
|
221
|
+
state.reconnectAttempts = 0;
|
|
222
|
+
state.sawReady = true;
|
|
223
|
+
state.streamClientId = frame.clientId;
|
|
224
|
+
return { items: [{ kind: 'ready', clientId: frame.clientId }], stop: false };
|
|
225
|
+
}
|
|
226
|
+
if (frame.type === 'waterfall') {
|
|
227
|
+
if (!state.streamClientId) {
|
|
228
|
+
return {
|
|
229
|
+
items: [
|
|
230
|
+
{ kind: 'unavailable', message: 'DSH approval stream sent a waterfall before ready' },
|
|
231
|
+
],
|
|
232
|
+
stop: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return waterfallToItems(client, state.streamClientId, frame, sessionId, signal);
|
|
236
|
+
}
|
|
237
|
+
if (frame.type === 'cancel') {
|
|
238
|
+
return { items: [{ kind: 'cancelled', eventId: frame.eventId }], stop: false };
|
|
239
|
+
}
|
|
240
|
+
return { items: [], stop: false };
|
|
241
|
+
}
|
|
242
|
+
/** 把 `$events` 流异常分类成 4 种动作(不改变重试/超时语义)。 */
|
|
243
|
+
function classifyApprovalStreamError(err, state, signal) {
|
|
244
|
+
if (signal.aborted)
|
|
245
|
+
return { kind: 'return' };
|
|
246
|
+
if (err instanceof DshError &&
|
|
247
|
+
err.code === 'http' &&
|
|
248
|
+
err.details.status === 401 &&
|
|
249
|
+
!state.authRefreshAttempted) {
|
|
250
|
+
return { kind: 'refresh-auth' };
|
|
251
|
+
}
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
const forbidden = (err instanceof DshError &&
|
|
254
|
+
(err.code === 'gateway/interactions-forbidden' ||
|
|
255
|
+
(err.code === 'http' && err.details.status === 403))) ||
|
|
256
|
+
message.includes('interactions-forbidden');
|
|
257
|
+
if (!forbidden && state.reconnectAttempts < 3) {
|
|
258
|
+
return { kind: 'reconnect', attempt: state.reconnectAttempts + 1 };
|
|
259
|
+
}
|
|
260
|
+
return { kind: 'unavailable', message };
|
|
261
|
+
}
|
|
262
|
+
/** 一次流正常结束(无异常)后的收尾决策。 */
|
|
263
|
+
function approvalStreamTail(state, signal) {
|
|
264
|
+
if (signal.aborted)
|
|
265
|
+
return 'aborted';
|
|
266
|
+
if (state.sawReady && state.reconnectAttempts < 3) {
|
|
267
|
+
state.reconnectAttempts += 1;
|
|
268
|
+
return 'reconnect';
|
|
269
|
+
}
|
|
270
|
+
return 'ended';
|
|
271
|
+
}
|
|
272
|
+
function reconnectBackoff() {
|
|
273
|
+
return new Promise((resolve) => setTimeout(resolve, 200));
|
|
274
|
+
}
|
|
275
|
+
/** Mark this run as the owner once its own prompt message is recorded. */
|
|
276
|
+
function ownsSessionEvent(state, ev) {
|
|
277
|
+
if (state.owns)
|
|
278
|
+
return true;
|
|
279
|
+
if (ev.type !== 'user/message')
|
|
280
|
+
return false;
|
|
281
|
+
const source = ev.data?.source;
|
|
282
|
+
if (source?.kind === 'user' && source.rpcId === state.promptRequestId) {
|
|
283
|
+
state.owns = true;
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function admitSessionEvent(state, ev) {
|
|
289
|
+
if (ownsSessionEvent(state, ev))
|
|
290
|
+
return true;
|
|
291
|
+
if (!state.foreignTailReported) {
|
|
292
|
+
state.foreignTailReported = true;
|
|
293
|
+
getLogger().info(`[dsh] prompt ${state.promptRequestId ?? '(none)'} is queued behind a running turn for session ${state.sessionId}; holding the card open until its own turn starts`);
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
/** Translate one durable event into a stream item (approvals are surfaced
|
|
298
|
+
* separately; everything else is forwarded to the runner). */
|
|
299
|
+
function sessionEventItem(sessionId, event) {
|
|
300
|
+
if (event.type === 'approval/asked') {
|
|
301
|
+
const data = event.data;
|
|
302
|
+
if (!data.id)
|
|
303
|
+
return undefined;
|
|
304
|
+
return {
|
|
305
|
+
kind: 'approval',
|
|
306
|
+
sessionId,
|
|
307
|
+
approvalId: data.id,
|
|
308
|
+
toolName: data.toolName ?? '',
|
|
309
|
+
...(data.callId ? { callId: data.callId } : {}),
|
|
310
|
+
...(data.reason ? { reason: data.reason } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { kind: 'event', sessionId, event };
|
|
314
|
+
}
|
|
315
|
+
/** 单条事件的 seq 去重 / admit / item / turn-end 早退;undefined 表示跳过。 */
|
|
316
|
+
function advanceSessionEvent(state, ev) {
|
|
317
|
+
if (ev.seq <= state.lastSeq)
|
|
318
|
+
return undefined;
|
|
319
|
+
state.lastSeq = ev.seq;
|
|
320
|
+
if (!admitSessionEvent(state, ev))
|
|
321
|
+
return undefined;
|
|
322
|
+
return { item: sessionEventItem(state.sessionId, ev), done: ev.type === 'turn/end' };
|
|
323
|
+
}
|
|
324
|
+
/** 一帧 follow → 待回放的 durable 事件(assistant-stream 不带 durable 事件)。 */
|
|
325
|
+
async function sessionFrameEvents(client, state, frame, signal) {
|
|
326
|
+
if (frame.type === 'snapshot') {
|
|
327
|
+
return client.replaySnapshotGap(state.sessionId, signal, state.lastSeq, frame);
|
|
328
|
+
}
|
|
329
|
+
if (frame.type === 'event')
|
|
330
|
+
return [frame.event];
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
/** 把 follow 流异常分类成 4 种动作(不改变重试/超时语义)。 */
|
|
334
|
+
function classifySessionStreamError(err, state, signal) {
|
|
335
|
+
if (signal.aborted)
|
|
336
|
+
return { kind: 'return' };
|
|
337
|
+
if (isDshAuthError(err)) {
|
|
338
|
+
if (!state.authRefreshAttempted)
|
|
339
|
+
return { kind: 'refresh-auth' };
|
|
340
|
+
return { kind: 'rethrow' };
|
|
341
|
+
}
|
|
342
|
+
return { kind: 'reconnect' };
|
|
343
|
+
}
|
|
115
344
|
export class DshClient {
|
|
116
345
|
baseUrl;
|
|
117
346
|
token;
|
|
@@ -214,101 +443,83 @@ export class DshClient {
|
|
|
214
443
|
* but never answers. Both are composed into the request's AbortSignal.
|
|
215
444
|
*/
|
|
216
445
|
async unary(method, payload, opts = {}) {
|
|
217
|
-
const
|
|
218
|
-
// 0/负数 = 不设超时(兼容「长请求不取消」的配置),其余走 AbortSignal.timeout。
|
|
219
|
-
const timeoutSignal = _timeoutMs > 0 ? AbortSignal.timeout(_timeoutMs) : undefined;
|
|
220
|
-
const signal = opts.signal && timeoutSignal
|
|
221
|
-
? AbortSignal.any([opts.signal, timeoutSignal])
|
|
222
|
-
: (opts.signal ?? timeoutSignal);
|
|
223
|
-
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
446
|
+
const ctx = prepareUnary(opts, this.requestTimeoutMs);
|
|
224
447
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let cookie;
|
|
229
|
-
try {
|
|
230
|
-
cookie = await this.authCookie(attempt > 0, signal);
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
if (opts.signal?.aborted || signal?.aborted) {
|
|
234
|
-
throw this.abortError(method, err, opts.signal, timeoutSignal, _timeoutMs);
|
|
235
|
-
}
|
|
236
|
-
// A token bootstrap can race a DSH restart; retry once with a fresh
|
|
237
|
-
// credentials read before surfacing the deterministic auth failure.
|
|
238
|
-
if (attempt === 0 &&
|
|
239
|
-
err instanceof DshError &&
|
|
240
|
-
err.code === 'http' &&
|
|
241
|
-
err.details.status === 401) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
throw err;
|
|
245
|
-
}
|
|
246
|
-
const headers = { 'content-type': 'application/json' };
|
|
247
|
-
if (cookie)
|
|
248
|
-
headers.cookie = cookie;
|
|
249
|
-
let res;
|
|
250
|
-
try {
|
|
251
|
-
res = await this.fetchImpl(`${this.baseUrl}/api/${method}`, {
|
|
252
|
-
method: 'POST',
|
|
253
|
-
headers,
|
|
254
|
-
body: JSON.stringify({
|
|
255
|
-
type: 'client-request',
|
|
256
|
-
rpcId,
|
|
257
|
-
method,
|
|
258
|
-
payload: { args: dshArgs(method, payload) },
|
|
259
|
-
}),
|
|
260
|
-
...(signal ? { signal } : {}),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
if (opts.signal?.aborted || signal?.aborted) {
|
|
265
|
-
throw this.abortError(method, err, opts.signal, timeoutSignal, _timeoutMs);
|
|
266
|
-
}
|
|
267
|
-
throw new DshError('transport', `DSH unary ${method} transport error: ${err.message}`);
|
|
268
|
-
}
|
|
269
|
-
if (opts.signal?.aborted) {
|
|
270
|
-
throw new DshError('aborted', `DSH unary ${method} aborted`);
|
|
271
|
-
}
|
|
272
|
-
if (res.status === 401 && attempt === 0) {
|
|
273
|
-
this.cookiePromise = undefined;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (res.status === 401) {
|
|
277
|
-
throw new DshError('http', `DSH HTTP 401 for ${method}: 认证失败;请确认 ${this.credentialsFile} 可读,` +
|
|
278
|
-
'或在 config.yaml 的 agents.dsh.token 填入当前 dsh web 启动 token。' +
|
|
279
|
-
'若凭据文件可读且格式正确,可能是 DSH 版本改变了 cookie 认证格式', { status: 401 });
|
|
280
|
-
}
|
|
281
|
-
if (!res.ok) {
|
|
282
|
-
throw new DshError('http', `DSH HTTP ${res.status} for ${method}`, {
|
|
283
|
-
status: res.status,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
let body;
|
|
287
|
-
try {
|
|
288
|
-
body = (await res.json());
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
throw new DshError('protocol', `DSH ${method} returned non-JSON: ${err.message}`);
|
|
292
|
-
}
|
|
293
|
-
if (body.result?.ok !== true) {
|
|
294
|
-
const err = body.result?.error;
|
|
295
|
-
throw new DshError(err?.code ?? 'error', err?.message ?? `DSH ${method} failed`, err?.details ?? {});
|
|
296
|
-
}
|
|
297
|
-
return body.result.value;
|
|
448
|
+
const result = await this.attemptUnary(method, payload, opts, ctx, attempt);
|
|
449
|
+
if (result.kind === 'done')
|
|
450
|
+
return result.value;
|
|
298
451
|
}
|
|
299
452
|
throw new DshError('auth', `DSH ${method} authentication failed after refresh`);
|
|
300
453
|
}
|
|
301
|
-
/**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
454
|
+
/** One unary POST attempt: obtain cookie, then post; both stages may ask to
|
|
455
|
+
* retry once (401 → re-mint cookie); all other failures throw DshError. */
|
|
456
|
+
async attemptUnary(method, payload, opts, ctx, attempt) {
|
|
457
|
+
const cookieResult = await this.obtainUnaryCookie(method, opts, ctx, attempt);
|
|
458
|
+
if (cookieResult.kind === 'retry')
|
|
459
|
+
return { kind: 'retry' };
|
|
460
|
+
return this.postUnary(method, payload, opts, ctx, cookieResult.cookie, attempt);
|
|
461
|
+
}
|
|
462
|
+
async obtainUnaryCookie(method, opts, ctx, attempt) {
|
|
463
|
+
if (opts.signal?.aborted) {
|
|
464
|
+
throw new DshError('aborted', `DSH unary ${method} aborted before send`);
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
return { kind: 'cookie', cookie: await this.authCookie(attempt > 0, ctx.signal) };
|
|
305
468
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
469
|
+
catch (err) {
|
|
470
|
+
if (opts.signal?.aborted || ctx.signal?.aborted) {
|
|
471
|
+
throw abortErrorOf(method, err, opts.signal, ctx.timeoutSignal, ctx.timeoutMs);
|
|
472
|
+
}
|
|
473
|
+
// A token bootstrap can race a DSH restart; retry once with a fresh
|
|
474
|
+
// credentials read before surfacing the deterministic auth failure.
|
|
475
|
+
if (attempt === 0 &&
|
|
476
|
+
err instanceof DshError &&
|
|
477
|
+
err.code === 'http' &&
|
|
478
|
+
err.details.status === 401) {
|
|
479
|
+
getLogger().warn(`[dsh] 401 while authenticating ${method}, re-minting cookie once`);
|
|
480
|
+
return { kind: 'retry' };
|
|
481
|
+
}
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async postUnary(method, payload, opts, ctx, cookie, attempt) {
|
|
486
|
+
const headers = { 'content-type': 'application/json' };
|
|
487
|
+
if (cookie)
|
|
488
|
+
headers.cookie = cookie;
|
|
489
|
+
let res;
|
|
490
|
+
try {
|
|
491
|
+
res = await this.fetchImpl(`${this.baseUrl}/api/${method}`, {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers,
|
|
494
|
+
body: JSON.stringify({
|
|
495
|
+
type: 'client-request',
|
|
496
|
+
rpcId: ctx.rpcId,
|
|
497
|
+
method,
|
|
498
|
+
payload: { args: dshArgs(method, payload) },
|
|
499
|
+
}),
|
|
500
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
309
501
|
});
|
|
310
502
|
}
|
|
311
|
-
|
|
503
|
+
catch (err) {
|
|
504
|
+
if (opts.signal?.aborted || ctx.signal?.aborted) {
|
|
505
|
+
throw abortErrorOf(method, err, opts.signal, ctx.timeoutSignal, ctx.timeoutMs);
|
|
506
|
+
}
|
|
507
|
+
throw new DshError('transport', `DSH unary ${method} transport error: ${err.message}`);
|
|
508
|
+
}
|
|
509
|
+
if (opts.signal?.aborted) {
|
|
510
|
+
throw new DshError('aborted', `DSH unary ${method} aborted`);
|
|
511
|
+
}
|
|
512
|
+
if (res.status === 401 && attempt === 0) {
|
|
513
|
+
this.cookiePromise = undefined;
|
|
514
|
+
getLogger().warn(`[dsh] 401 for ${method}, re-minting cookie once`);
|
|
515
|
+
return { kind: 'retry' };
|
|
516
|
+
}
|
|
517
|
+
if (res.status === 401) {
|
|
518
|
+
throw new DshError('http', `DSH HTTP 401 for ${method}: 认证失败;请确认 ${this.credentialsFile} 可读,` +
|
|
519
|
+
'或在 config.yaml 的 agents.dsh.token 填入当前 dsh web 启动 token。' +
|
|
520
|
+
'若凭据文件可读且格式正确,可能是 DSH 版本改变了 cookie 认证格式', { status: 401 });
|
|
521
|
+
}
|
|
522
|
+
return { kind: 'done', value: await readUnaryBody(method, res) };
|
|
312
523
|
}
|
|
313
524
|
/** Create a session. Pass `agentPreset` to pin the session's preset (fixed at
|
|
314
525
|
* creation). */
|
|
@@ -363,14 +574,6 @@ export class DshClient {
|
|
|
363
574
|
async answerRemoteEvent(clientId, eventId, outcome, opts) {
|
|
364
575
|
await this.unary('$events/result', { clientId, eventId, outcome }, opts);
|
|
365
576
|
}
|
|
366
|
-
/** Hand one waterfall delivery back with `next` (another answerer owns it). */
|
|
367
|
-
delegateWaterfall(clientId, eventId, signal) {
|
|
368
|
-
void this.answerRemoteEvent(clientId, eventId, { kind: 'next' }, { signal }).catch((err) => {
|
|
369
|
-
if (!signal.aborted) {
|
|
370
|
-
getLogger().warn(`[dsh] waterfall delegation failed eventId=${eventId}: ${err.message}`);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
577
|
/**
|
|
375
578
|
* Return the highest event `seq` currently persisted for the session.
|
|
376
579
|
*
|
|
@@ -576,147 +779,60 @@ export class DshClient {
|
|
|
576
779
|
* `next` so a different lark-remote workspace cannot park it.
|
|
577
780
|
*/
|
|
578
781
|
async *approvalEvents(sessionId, signal) {
|
|
579
|
-
|
|
580
|
-
|
|
782
|
+
const state = {
|
|
783
|
+
authRefreshAttempted: false,
|
|
784
|
+
reconnectAttempts: 0,
|
|
785
|
+
sawReady: false,
|
|
786
|
+
};
|
|
581
787
|
while (!signal.aborted) {
|
|
582
|
-
|
|
583
|
-
|
|
788
|
+
// 每次重连重置本次连接的 ready 身份;auth 刷新与重连预算跨连接保留。
|
|
789
|
+
state.sawReady = false;
|
|
790
|
+
state.streamClientId = undefined;
|
|
584
791
|
try {
|
|
585
792
|
for await (const raw of this.muxStream('$events', { args: {} }, signal)) {
|
|
586
793
|
if (signal.aborted)
|
|
587
794
|
return;
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// ——已渲染的提问卡持有过期 clientId(点答案必失败)、Host 的 cancel
|
|
594
|
-
// 帧也收不到,违反「同一 waterfall 在 $events 重连后会被重投」的
|
|
595
|
-
// 不变量。预算语义是「单次断连窗口内的连续失败上限」,不是终身配额。
|
|
596
|
-
reconnectAttempts = 0;
|
|
597
|
-
sawReady = true;
|
|
598
|
-
streamClientId = frame.clientId;
|
|
599
|
-
yield { kind: 'ready', clientId: streamClientId };
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
if (frame.type === 'waterfall') {
|
|
603
|
-
if (!streamClientId) {
|
|
604
|
-
yield {
|
|
605
|
-
kind: 'unavailable',
|
|
606
|
-
message: 'DSH approval stream sent a waterfall before ready',
|
|
607
|
-
};
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (frame.event === 'user-questions/request' && frame.agentId === sessionId) {
|
|
611
|
-
if (isTimedQuestionRequest(frame.request)) {
|
|
612
|
-
// The timed wait owns its own deadline (and the Web UI renders
|
|
613
|
-
// the countdown); a card answer outside that window is lost.
|
|
614
|
-
getLogger().info(`[dsh] timed user-questions/request delegated to the Web UI eventId=${frame.eventId}`);
|
|
615
|
-
this.delegateWaterfall(streamClientId, frame.eventId, signal);
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
const questions = dshQuestionsOf(frame.request);
|
|
619
|
-
if (questions) {
|
|
620
|
-
yield {
|
|
621
|
-
kind: 'question',
|
|
622
|
-
eventId: frame.eventId,
|
|
623
|
-
clientId: streamClientId,
|
|
624
|
-
questions,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
else {
|
|
628
|
-
// The Feishu card cannot answer this batch safely (malformed,
|
|
629
|
-
// or duplicate texts — the coordinator keys answers by text).
|
|
630
|
-
// Hand the one-shot answer back so the DSH Web UI stays in charge.
|
|
631
|
-
getLogger().warn(`[dsh] user-questions/request not relayable; delegating eventId=${frame.eventId}`);
|
|
632
|
-
this.delegateWaterfall(streamClientId, frame.eventId, signal);
|
|
633
|
-
}
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
if (frame.event !== 'approval/request' || frame.agentId !== sessionId) {
|
|
637
|
-
// Every owner answerer receives the waterfall. A non-owner of
|
|
638
|
-
// the session must explicitly delegate, otherwise DSH waits.
|
|
639
|
-
this.delegateWaterfall(streamClientId, frame.eventId, signal);
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
const data = frame.request;
|
|
643
|
-
const toolName = typeof data.toolName === 'string' ? data.toolName : '';
|
|
644
|
-
const callId = typeof data.callId === 'string' ? data.callId : undefined;
|
|
645
|
-
const reason = typeof data.reason === 'string' ? data.reason : undefined;
|
|
646
|
-
yield {
|
|
647
|
-
kind: 'request',
|
|
648
|
-
eventId: frame.eventId,
|
|
649
|
-
clientId: streamClientId,
|
|
650
|
-
toolName,
|
|
651
|
-
...(callId ? { callId } : {}),
|
|
652
|
-
...(reason ? { reason } : {}),
|
|
653
|
-
};
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
if (frame.type === 'cancel') {
|
|
657
|
-
yield { kind: 'cancelled', eventId: frame.eventId };
|
|
658
|
-
}
|
|
795
|
+
const result = processApprovalFrame(state, raw, sessionId, signal, this);
|
|
796
|
+
for (const item of result.items)
|
|
797
|
+
yield item;
|
|
798
|
+
if (result.stop)
|
|
799
|
+
return;
|
|
659
800
|
}
|
|
660
801
|
}
|
|
661
802
|
catch (err) {
|
|
662
|
-
|
|
803
|
+
const action = classifyApprovalStreamError(err, state, signal);
|
|
804
|
+
if (action.kind === 'return')
|
|
663
805
|
return;
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
err.details.status === 401 &&
|
|
667
|
-
!authRefreshAttempted) {
|
|
668
|
-
authRefreshAttempted = true;
|
|
806
|
+
if (action.kind === 'refresh-auth') {
|
|
807
|
+
state.authRefreshAttempted = true;
|
|
669
808
|
this.cookiePromise = undefined;
|
|
670
809
|
getLogger().warn(`[dsh] approval event stream authentication expired; refreshing credentials and reconnecting once`);
|
|
671
810
|
continue;
|
|
672
811
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
(
|
|
676
|
-
|
|
677
|
-
message.includes('interactions-forbidden');
|
|
678
|
-
if (!forbidden && reconnectAttempts < 3) {
|
|
679
|
-
reconnectAttempts += 1;
|
|
680
|
-
getLogger().warn(`[dsh] approval event stream dropped (attempt ${reconnectAttempts}/3); reconnecting`);
|
|
681
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
812
|
+
if (action.kind === 'reconnect') {
|
|
813
|
+
state.reconnectAttempts = action.attempt;
|
|
814
|
+
getLogger().warn(`[dsh] approval event stream dropped (attempt ${action.attempt}/3); reconnecting`);
|
|
815
|
+
await reconnectBackoff();
|
|
682
816
|
continue;
|
|
683
817
|
}
|
|
684
|
-
getLogger().warn(`[dsh] approval event stream unavailable: ${message}`);
|
|
685
|
-
yield { kind: 'unavailable', message };
|
|
818
|
+
getLogger().warn(`[dsh] approval event stream unavailable: ${action.message}`);
|
|
819
|
+
yield { kind: 'unavailable', message: action.message };
|
|
686
820
|
return;
|
|
687
821
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
reconnectAttempts += 1;
|
|
691
|
-
getLogger().warn(`[dsh] approval event stream ended (attempt ${reconnectAttempts}/3); reconnecting`);
|
|
692
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
// A clean end before ready means the Host does not expose `$events`;
|
|
696
|
-
// fall back to the Web UI prompt instead of retrying forever.
|
|
697
|
-
yield { kind: 'unavailable', message: 'DSH approval event stream ended' };
|
|
822
|
+
const tail = approvalStreamTail(state, signal);
|
|
823
|
+
if (tail === 'aborted')
|
|
698
824
|
return;
|
|
825
|
+
if (tail === 'reconnect') {
|
|
826
|
+
getLogger().warn(`[dsh] approval event stream ended (attempt ${state.reconnectAttempts}/3); reconnecting`);
|
|
827
|
+
await reconnectBackoff();
|
|
828
|
+
continue;
|
|
699
829
|
}
|
|
830
|
+
// A clean end before ready means the Host does not expose `$events`;
|
|
831
|
+
// fall back to the Web UI prompt instead of retrying forever.
|
|
832
|
+
yield { kind: 'unavailable', message: 'DSH approval event stream ended' };
|
|
833
|
+
return;
|
|
700
834
|
}
|
|
701
835
|
}
|
|
702
|
-
/** Translate one durable event into a stream item (approvals are surfaced
|
|
703
|
-
* separately; everything else is forwarded to the runner). */
|
|
704
|
-
eventItem(sessionId, event) {
|
|
705
|
-
if (event.type === 'approval/asked') {
|
|
706
|
-
const data = event.data;
|
|
707
|
-
if (!data.id)
|
|
708
|
-
return undefined;
|
|
709
|
-
return {
|
|
710
|
-
kind: 'approval',
|
|
711
|
-
sessionId,
|
|
712
|
-
approvalId: data.id,
|
|
713
|
-
toolName: data.toolName ?? '',
|
|
714
|
-
...(data.callId ? { callId: data.callId } : {}),
|
|
715
|
-
...(data.reason ? { reason: data.reason } : {}),
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
return { kind: 'event', sessionId, event };
|
|
719
|
-
}
|
|
720
836
|
/**
|
|
721
837
|
* Per-session subscription: yields session events, assistant stream chunks
|
|
722
838
|
* and approval notices for `sessionId`, auto-reconnecting on stream
|
|
@@ -731,92 +847,47 @@ export class DshClient {
|
|
|
731
847
|
* keeps the card "running" until this run's turn actually finishes.
|
|
732
848
|
*/
|
|
733
849
|
async *sessionEvents(sessionId, signal, initialSeq = -1, promptRequestId) {
|
|
734
|
-
const state = {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return false;
|
|
742
|
-
const source = ev.data
|
|
743
|
-
?.source;
|
|
744
|
-
if (source?.kind === 'user' && source.rpcId === promptRequestId) {
|
|
745
|
-
state.owns = true;
|
|
746
|
-
return true;
|
|
747
|
-
}
|
|
748
|
-
return false;
|
|
749
|
-
};
|
|
750
|
-
const admit = (ev) => {
|
|
751
|
-
if (ownsEvent(ev))
|
|
752
|
-
return true;
|
|
753
|
-
if (!foreignTailReported) {
|
|
754
|
-
foreignTailReported = true;
|
|
755
|
-
getLogger().info(`[dsh] prompt ${promptRequestId ?? '(none)'} is queued behind a running turn for session ${sessionId}; holding the card open until its own turn starts`);
|
|
756
|
-
}
|
|
757
|
-
return false;
|
|
850
|
+
const state = {
|
|
851
|
+
lastSeq: initialSeq,
|
|
852
|
+
owns: promptRequestId === undefined,
|
|
853
|
+
foreignTailReported: false,
|
|
854
|
+
sessionId,
|
|
855
|
+
promptRequestId,
|
|
856
|
+
authRefreshAttempted: false,
|
|
758
857
|
};
|
|
759
|
-
let authRefreshAttempted = false;
|
|
760
858
|
while (!signal.aborted) {
|
|
761
859
|
try {
|
|
762
860
|
for await (const frame of this.followStream(sessionId, signal)) {
|
|
763
861
|
if (signal.aborted)
|
|
764
862
|
return;
|
|
765
|
-
authRefreshAttempted = false;
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
for (const ev of await this.replaySnapshotGap(sessionId, signal, state.lastSeq, frame)) {
|
|
770
|
-
if (ev.seq <= state.lastSeq)
|
|
771
|
-
continue;
|
|
772
|
-
state.lastSeq = ev.seq;
|
|
773
|
-
if (!admit(ev))
|
|
774
|
-
continue;
|
|
775
|
-
const item = this.eventItem(sessionId, ev);
|
|
776
|
-
if (item)
|
|
777
|
-
yield item;
|
|
778
|
-
if (ev.type === 'turn/end')
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
if (frame.type === 'event') {
|
|
784
|
-
const ev = frame.event;
|
|
785
|
-
if (ev.seq <= state.lastSeq)
|
|
863
|
+
state.authRefreshAttempted = false;
|
|
864
|
+
for (const ev of await sessionFrameEvents(this, state, frame, signal)) {
|
|
865
|
+
const step = advanceSessionEvent(state, ev);
|
|
866
|
+
if (!step)
|
|
786
867
|
continue;
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const item = this.eventItem(sessionId, ev);
|
|
791
|
-
if (item)
|
|
792
|
-
yield item;
|
|
793
|
-
if (ev.type === 'turn/end')
|
|
868
|
+
if (step.item)
|
|
869
|
+
yield step.item;
|
|
870
|
+
if (step.done)
|
|
794
871
|
return;
|
|
795
|
-
continue;
|
|
796
872
|
}
|
|
797
|
-
if (frame.type === 'assistant-stream') {
|
|
873
|
+
if (frame.type === 'assistant-stream' && state.owns) {
|
|
798
874
|
// Mid-turn deltas of the turn this prompt is queued behind.
|
|
799
|
-
if (!state.owns)
|
|
800
|
-
continue;
|
|
801
875
|
yield { kind: 'assistant-stream', sessionId, frame: frame.frame };
|
|
802
876
|
}
|
|
803
877
|
}
|
|
804
878
|
}
|
|
805
879
|
catch (err) {
|
|
806
|
-
|
|
880
|
+
const action = classifySessionStreamError(err, state, signal);
|
|
881
|
+
if (action.kind === 'return')
|
|
807
882
|
return;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
authRefreshAttempted = true;
|
|
814
|
-
this.cookiePromise = undefined;
|
|
815
|
-
getLogger().warn(`[dsh] remote mux authentication expired for session ${sessionId}; refreshing credentials and reconnecting once`);
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
throw err;
|
|
883
|
+
if (action.kind === 'refresh-auth') {
|
|
884
|
+
state.authRefreshAttempted = true;
|
|
885
|
+
this.cookiePromise = undefined;
|
|
886
|
+
getLogger().warn(`[dsh] remote mux authentication expired for session ${sessionId}; refreshing credentials and reconnecting once`);
|
|
887
|
+
continue;
|
|
819
888
|
}
|
|
889
|
+
if (action.kind === 'rethrow')
|
|
890
|
+
throw err;
|
|
820
891
|
getLogger().warn(`[dsh] remote mux disconnected for session ${sessionId}: ${err.message}; reconnecting`);
|
|
821
892
|
}
|
|
822
893
|
if (signal.aborted)
|
|
@@ -978,10 +1049,6 @@ export class DshClient {
|
|
|
978
1049
|
return false;
|
|
979
1050
|
}
|
|
980
1051
|
}
|
|
981
|
-
/** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
|
|
982
|
-
static usageOf(data) {
|
|
983
|
-
return data.usage ?? undefined;
|
|
984
|
-
}
|
|
985
1052
|
}
|
|
986
1053
|
/** Stable client-side prompt identity persisted on the accepted user message. */
|
|
987
1054
|
export function newPromptRequestId() {
|