lark-remote 0.5.10 → 0.5.13
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 +51 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/binder.js +7 -1
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +89 -230
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/approval-render.d.ts +14 -0
- package/dist/card/approval-render.js +155 -29
- package/dist/card/bash-card-session.js +10 -5
- package/dist/card/bash-renderer.js +11 -23
- package/dist/card/card-budget.js +24 -4
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +150 -10
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/common/text-width.js +5 -0
- package/dist/common/tool-summary.js +4 -2
- package/dist/config/codex-config.js +5 -3
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +19 -5
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -391
- package/dist/instance-lock.d.ts +9 -1
- package/dist/instance-lock.js +42 -7
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/platform/identity.js +4 -3
- package/dist/platform/path.js +3 -1
- package/dist/platform/terminator-win32.d.ts +13 -1
- package/dist/platform/terminator-win32.js +4 -14
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +93 -1133
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/runner.js +12 -0
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +15 -8
- package/dist/runner/common/acp/base-translator.js +38 -7
- package/dist/runner/common/jsonl-stream.js +12 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
- package/dist/runner/common/jsonrpc/transport.js +7 -1
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/approval.js +16 -3
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +32 -7
- package/dist/runner/dsh/client.js +115 -149
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +63 -24
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +42 -5
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +42 -5
- package/dist/runner/kimi/acp/translator.d.ts +8 -0
- package/dist/runner/kimi/acp/translator.js +11 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/runner.js +33 -5
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/resolve-agent-choices.js +5 -1
- package/dist/runner/sync-agent-choices.js +4 -1
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +13 -4
- package/dist/session/codex/rollout-reader.js +51 -26
- package/dist/session/common/usage-accumulator.d.ts +10 -1
- package/dist/session/common/usage-accumulator.js +10 -2
- package/dist/session/dsh/sessions.js +29 -9
- package/dist/session/kimi/sessions.js +11 -7
- package/dist/session/opencode/sessions.js +15 -7
- package/dist/session/pi/sessions.js +3 -2
- package/dist/session/session-store.js +4 -3
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { DshApprovalStreamItem, DshCommandResult, DshModelCatalogValue, DshModelSelection, DshOpenTurnState, DshPermissionCatalogValue, DshJobView, DshPresetListValue, DshProjectionItem, DshProjectionSnapshot, DshQuestion, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem, DshTokenUsage } from './types.js';
|
|
23
23
|
import type { GoalView } from '../types.js';
|
|
24
|
+
export { DshError } from './errors.js';
|
|
24
25
|
interface WebSocketMessageEvent {
|
|
25
26
|
data: unknown;
|
|
26
27
|
}
|
|
@@ -40,11 +41,6 @@ export interface WebSocketLike {
|
|
|
40
41
|
close(): void;
|
|
41
42
|
}
|
|
42
43
|
export type WebSocketFactory = (url: string, options?: WebSocketOptions) => WebSocketLike;
|
|
43
|
-
export declare class DshError extends Error {
|
|
44
|
-
readonly code: string;
|
|
45
|
-
readonly details: Record<string, unknown>;
|
|
46
|
-
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
47
|
-
}
|
|
48
44
|
export interface DshClientOptions {
|
|
49
45
|
/** Configured DSH home; selects `.credentials.yaml` when no explicit file is given. */
|
|
50
46
|
dshHome?: string;
|
|
@@ -256,7 +252,20 @@ export declare class DshClient {
|
|
|
256
252
|
* Fallback probe: the newest durable event's type tells whether a turn is open.
|
|
257
253
|
*
|
|
258
254
|
* 只在拿不到 `running` 时用。名单刻意保守——`job/*`(后台任务可在 turn 外
|
|
259
|
-
* 结算)、`
|
|
255
|
+
* 结算)、`turn/end` 都不算,判不出来就报告 `unknown`。
|
|
256
|
+
*
|
|
257
|
+
* 落盘顺序已从 DSH 源码验证(packages/core/agent-loop:agent.ts 的 turn()/
|
|
258
|
+
* step() + inbox.ts 的 mutate()):prompt 提交只落 `agent/inbox/spliced`,
|
|
259
|
+
* `turn/start` 由 turn() 在认领前 append,`user/message` 在 step() 认领时才
|
|
260
|
+
* append——顺序是 spliced → turn/start → user/message。两个边界:
|
|
261
|
+
*
|
|
262
|
+
* - 尾部 `agent/inbox/spliced` → closed 是**对的**:turn 尚未打开,cancel
|
|
263
|
+
* 空闲 session 无害,随后 withdraw 探测会撤走排队 prompt;
|
|
264
|
+
* - 尾部 `user/message` → **unknown**:step() 里 user/message append 后要等
|
|
265
|
+
* 一整个 LLM 往返才有下一条 durable 事件(assistant 流帧是 process-local
|
|
266
|
+
* 的),这个秒级窗口内 turn 已打开;且压缩 checkpoint 也以 turn:null 落
|
|
267
|
+
* user/message——两种解释并存时只能报 unknown(stop 按 unconfirmed 处理,
|
|
268
|
+
* 不假报已停止)。此前报 closed 会让一个 LLM 往返宽的窗口假报干净停止。
|
|
260
269
|
*/
|
|
261
270
|
private probeOpenTurnByEvents;
|
|
262
271
|
/**
|
|
@@ -290,6 +299,23 @@ export declare class DshClient {
|
|
|
290
299
|
* keeps the card "running" until this run's turn actually finishes.
|
|
291
300
|
*/
|
|
292
301
|
sessionEvents(sessionId: string, signal: AbortSignal, initialSeq?: number, promptRequestId?: string): AsyncGenerator<DshStreamItem>;
|
|
302
|
+
/**
|
|
303
|
+
* 快照缺口回填(经 DSH 源码验证后实现)。
|
|
304
|
+
*
|
|
305
|
+
* follow 开场快照按**消息**截取:host 的 paginate 只数 user/assistant
|
|
306
|
+
* message(工具事件随所在消息组搭车进快照,不占预算),`maxMessages` 内
|
|
307
|
+
* 装不下的更早前缀被截掉并以 `hasMore: true` 标记。断连期间积压超过
|
|
308
|
+
* FOLLOW_MAX_MESSAGES 条消息时,`lastSeq` 与快照起点之间的缺口若不补,
|
|
309
|
+
* 这些事件对本次 follow 永久缺失(卡片丢中段工具行;seq 去重只防重放
|
|
310
|
+
* 不补缺)。`session/page` 的 `throughSeq` 游标正是服务端为此设计的回填
|
|
311
|
+
* 通道——从缺口尾部向前翻页直到盖住 lastSeq、翻到日志头或达到页数上限。
|
|
312
|
+
*
|
|
313
|
+
* best-effort:翻页失败保留已取回的部分(缺口剩余与旧行为一致地丢失,
|
|
314
|
+
* 但只发生在超出 BACKFILL_MAX_PAGES×PAGE_MESSAGES 条消息的极端断连)。
|
|
315
|
+
* 返回 seq 升序的完整回放序列(回填 + 快照记录;与 lastSeq 重叠的条目
|
|
316
|
+
* 由回放循环的 seq 去重兜住)。
|
|
317
|
+
*/
|
|
318
|
+
private replaySnapshotGap;
|
|
293
319
|
/**
|
|
294
320
|
* Host-wide live projection stream (`session/control`).
|
|
295
321
|
*
|
|
@@ -342,4 +368,3 @@ export declare function dshQuestionsOf(request: Record<string, unknown>): DshQue
|
|
|
342
368
|
* silently on the server side.
|
|
343
369
|
*/
|
|
344
370
|
export declare function isTimedQuestionRequest(request: Record<string, unknown>): boolean;
|
|
345
|
-
export {};
|
|
@@ -25,6 +25,9 @@ import { getLogger } from '../../logger/index.js';
|
|
|
25
25
|
import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecret, resolveDshCredentialsFile, } from './auth.js';
|
|
26
26
|
import { DEFAULT_REQUEST_TIMEOUT_MS } from '../common/defaults.js';
|
|
27
27
|
import { goalFromProjection } from './goal-view.js';
|
|
28
|
+
import { DshError } from './errors.js';
|
|
29
|
+
import { openMuxStream } from './mux-stream.js';
|
|
30
|
+
export { DshError } from './errors.js';
|
|
28
31
|
function defaultWebSocketFactory(url, options) {
|
|
29
32
|
const socket = new WebSocket(url, {
|
|
30
33
|
...(options?.headers ? { headers: options.headers } : {}),
|
|
@@ -35,16 +38,6 @@ function defaultWebSocketFactory(url, options) {
|
|
|
35
38
|
});
|
|
36
39
|
return socket;
|
|
37
40
|
}
|
|
38
|
-
export class DshError extends Error {
|
|
39
|
-
code;
|
|
40
|
-
details;
|
|
41
|
-
constructor(code, message, details = {}) {
|
|
42
|
-
super(message);
|
|
43
|
-
this.name = 'DshError';
|
|
44
|
-
this.code = code;
|
|
45
|
-
this.details = details;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
41
|
function isDshAuthError(error) {
|
|
49
42
|
return (error instanceof DshError &&
|
|
50
43
|
error.code === 'http' &&
|
|
@@ -52,8 +45,19 @@ function isDshAuthError(error) {
|
|
|
52
45
|
}
|
|
53
46
|
/** Backoff between mux reconnects to avoid a hot loop on a dropping server. */
|
|
54
47
|
const RECONNECT_BACKOFF_MS = 200;
|
|
55
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Follow snapshot budget. `session/follow` also stays live after the snapshot.
|
|
50
|
+
*
|
|
51
|
+
* 语义(经 DSH 源码验证,packages/api/session-controller/src/history.ts
|
|
52
|
+
* 的 paginate):按**消息**计数——只有 user/assistant message 占预算,工具
|
|
53
|
+
* 事件随所在消息组搭进快照。断连期间积压超过该预算时快照首条会越过
|
|
54
|
+
* lastSeq,缺口经 {@link replaySnapshotGap} 用 session/page 回填。
|
|
55
|
+
*/
|
|
56
56
|
const FOLLOW_MAX_MESSAGES = 200;
|
|
57
|
+
/** 快照缺口回填的单页消息数(session/page 的 maxMessages)。 */
|
|
58
|
+
const FOLLOW_GAP_BACKFILL_PAGE_MESSAGES = 200;
|
|
59
|
+
/** 快照缺口回填的页数上限(×PAGE_MESSAGES = 单次重连最多回补 4000 条消息)。 */
|
|
60
|
+
const FOLLOW_GAP_BACKFILL_MAX_PAGES = 20;
|
|
57
61
|
/**
|
|
58
62
|
* 只在 turn 内部落盘的会话事件类型:看到它们说明 turn 还没结束。
|
|
59
63
|
* 名单刻意保守——`user/message`(压缩 checkpoint 会以 turn:null 落盘)、
|
|
@@ -116,7 +120,15 @@ export function extractDshToken(raw) {
|
|
|
116
120
|
return value;
|
|
117
121
|
try {
|
|
118
122
|
const url = new URL(value.startsWith('?') ? `http://dsh.invalid/${value}` : value);
|
|
119
|
-
|
|
123
|
+
const token = url.searchParams.get('token');
|
|
124
|
+
if (token)
|
|
125
|
+
return token;
|
|
126
|
+
// 带 scheme 却没有 token 参数的输入是误贴的 host URL(如
|
|
127
|
+
// `http://127.0.0.1:3080`):整串不是合法 token,参与握手只会得到
|
|
128
|
+
// 指向性缺失的 401。返回空串让上游走「token 缺失」的诊断路径。
|
|
129
|
+
if (value.includes('://'))
|
|
130
|
+
return '';
|
|
131
|
+
return value;
|
|
120
132
|
}
|
|
121
133
|
catch {
|
|
122
134
|
return value;
|
|
@@ -540,7 +552,20 @@ export class DshClient {
|
|
|
540
552
|
* Fallback probe: the newest durable event's type tells whether a turn is open.
|
|
541
553
|
*
|
|
542
554
|
* 只在拿不到 `running` 时用。名单刻意保守——`job/*`(后台任务可在 turn 外
|
|
543
|
-
* 结算)、`
|
|
555
|
+
* 结算)、`turn/end` 都不算,判不出来就报告 `unknown`。
|
|
556
|
+
*
|
|
557
|
+
* 落盘顺序已从 DSH 源码验证(packages/core/agent-loop:agent.ts 的 turn()/
|
|
558
|
+
* step() + inbox.ts 的 mutate()):prompt 提交只落 `agent/inbox/spliced`,
|
|
559
|
+
* `turn/start` 由 turn() 在认领前 append,`user/message` 在 step() 认领时才
|
|
560
|
+
* append——顺序是 spliced → turn/start → user/message。两个边界:
|
|
561
|
+
*
|
|
562
|
+
* - 尾部 `agent/inbox/spliced` → closed 是**对的**:turn 尚未打开,cancel
|
|
563
|
+
* 空闲 session 无害,随后 withdraw 探测会撤走排队 prompt;
|
|
564
|
+
* - 尾部 `user/message` → **unknown**:step() 里 user/message append 后要等
|
|
565
|
+
* 一整个 LLM 往返才有下一条 durable 事件(assistant 流帧是 process-local
|
|
566
|
+
* 的),这个秒级窗口内 turn 已打开;且压缩 checkpoint 也以 turn:null 落
|
|
567
|
+
* user/message——两种解释并存时只能报 unknown(stop 按 unconfirmed 处理,
|
|
568
|
+
* 不假报已停止)。此前报 closed 会让一个 LLM 往返宽的窗口假报干净停止。
|
|
544
569
|
*/
|
|
545
570
|
async probeOpenTurnByEvents(sessionId, opts) {
|
|
546
571
|
try {
|
|
@@ -555,6 +580,8 @@ export class DshClient {
|
|
|
555
580
|
const last = value.records.at(-1)?.event;
|
|
556
581
|
if (last === undefined)
|
|
557
582
|
return 'closed';
|
|
583
|
+
if (last.type === 'user/message')
|
|
584
|
+
return 'unknown';
|
|
558
585
|
return last.type === 'turn/start' || IN_TURN_EVENT_TYPES.has(last.type) ? 'open' : 'closed';
|
|
559
586
|
}
|
|
560
587
|
catch (err) {
|
|
@@ -568,140 +595,15 @@ export class DshClient {
|
|
|
568
595
|
* close, error, or `signal` abort.
|
|
569
596
|
*/
|
|
570
597
|
async *muxStream(endpoint, payload, signal) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (waiter) {
|
|
580
|
-
const resolve = waiter;
|
|
581
|
-
waiter = null;
|
|
582
|
-
resolve(frame);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (frame)
|
|
586
|
-
queue.push(frame);
|
|
587
|
-
else
|
|
588
|
-
ended = true;
|
|
589
|
-
};
|
|
590
|
-
socket.onopen = () => {
|
|
591
|
-
if (ended)
|
|
592
|
-
return;
|
|
593
|
-
try {
|
|
594
|
-
socket.send(JSON.stringify({
|
|
595
|
-
type: 'open',
|
|
596
|
-
streamId,
|
|
597
|
-
endpoint,
|
|
598
|
-
payload,
|
|
599
|
-
}));
|
|
600
|
-
}
|
|
601
|
-
catch (err) {
|
|
602
|
-
failure = new DshError('transport', `DSH remote mux send error: ${err.message}`);
|
|
603
|
-
ended = true;
|
|
604
|
-
settle(undefined);
|
|
605
|
-
}
|
|
606
|
-
};
|
|
607
|
-
socket.onmessage = (event) => {
|
|
608
|
-
if (ended)
|
|
609
|
-
return;
|
|
610
|
-
const text = typeof event.data === 'string'
|
|
611
|
-
? event.data
|
|
612
|
-
: Buffer.isBuffer(event.data)
|
|
613
|
-
? event.data.toString('utf8')
|
|
614
|
-
: String(event.data);
|
|
615
|
-
let message;
|
|
616
|
-
try {
|
|
617
|
-
message = JSON.parse(text);
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
getLogger().warn('[dsh] dropped malformed remote mux frame');
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (message.streamId !== streamId)
|
|
624
|
-
return;
|
|
625
|
-
if (message.type === 'item' && message.value) {
|
|
626
|
-
settle(message.value);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (message.type === 'end') {
|
|
630
|
-
ended = true;
|
|
631
|
-
settle(undefined);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
if (message.type === 'error') {
|
|
635
|
-
failure = new DshError(message.error?.code ?? 'stream', message.error?.message ?? 'DSH remote stream failed', message.error?.details ?? {});
|
|
636
|
-
ended = true;
|
|
637
|
-
settle(undefined);
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
socket.onerror = () => {
|
|
641
|
-
if (ended)
|
|
642
|
-
return;
|
|
643
|
-
failure = new DshError('transport', 'DSH remote mux WebSocket error');
|
|
644
|
-
ended = true;
|
|
645
|
-
settle(undefined);
|
|
646
|
-
};
|
|
647
|
-
socket.onunexpectedresponse = (event) => {
|
|
648
|
-
if (ended)
|
|
649
|
-
return;
|
|
650
|
-
const status = event.statusCode;
|
|
651
|
-
failure = new DshError('http', `DSH remote mux WebSocket HTTP ${status === undefined ? 'error' : status}`, status === undefined ? {} : { status });
|
|
652
|
-
ended = true;
|
|
653
|
-
settle(undefined);
|
|
654
|
-
};
|
|
655
|
-
socket.onclose = () => {
|
|
656
|
-
if (ended)
|
|
657
|
-
return;
|
|
658
|
-
ended = true;
|
|
659
|
-
settle(undefined);
|
|
660
|
-
};
|
|
661
|
-
const onAbort = () => {
|
|
662
|
-
if (ended)
|
|
663
|
-
return;
|
|
664
|
-
ended = true;
|
|
665
|
-
settle(undefined);
|
|
666
|
-
try {
|
|
667
|
-
socket.close();
|
|
668
|
-
}
|
|
669
|
-
catch {
|
|
670
|
-
/* ignore close error */
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
674
|
-
const next = () => new Promise((resolve) => {
|
|
675
|
-
if (queue.length > 0) {
|
|
676
|
-
resolve(queue.shift());
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (ended) {
|
|
680
|
-
resolve(undefined);
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
waiter = resolve;
|
|
598
|
+
yield* openMuxStream({
|
|
599
|
+
baseUrl: this.baseUrl,
|
|
600
|
+
endpoint,
|
|
601
|
+
payload,
|
|
602
|
+
signal,
|
|
603
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
604
|
+
authCookie: () => this.authCookie(),
|
|
605
|
+
webSocketFactory: this.webSocketImpl,
|
|
684
606
|
});
|
|
685
|
-
try {
|
|
686
|
-
while (!signal.aborted) {
|
|
687
|
-
const frame = await next();
|
|
688
|
-
if (frame === undefined) {
|
|
689
|
-
if (failure && !signal.aborted)
|
|
690
|
-
throw failure;
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
yield frame;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
finally {
|
|
697
|
-
signal.removeEventListener('abort', onAbort);
|
|
698
|
-
try {
|
|
699
|
-
socket.close();
|
|
700
|
-
}
|
|
701
|
-
catch {
|
|
702
|
-
/* ignore close error */
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
607
|
}
|
|
706
608
|
/** One logical `session/follow` stream. */
|
|
707
609
|
async *followStream(sessionId, signal) {
|
|
@@ -734,6 +636,12 @@ export class DshClient {
|
|
|
734
636
|
const frame = raw;
|
|
735
637
|
if (frame.type === 'ready') {
|
|
736
638
|
authRefreshAttempted = false;
|
|
639
|
+
// 成功建连后补充重连预算:goal 驱动的 turn 可跑数小时,DSH 重启 +
|
|
640
|
+
// 网络抖动累计掉线超过 3 次后,旧实现会让 $events 永久 unavailable
|
|
641
|
+
// ——已渲染的提问卡持有过期 clientId(点答案必失败)、Host 的 cancel
|
|
642
|
+
// 帧也收不到,违反「同一 waterfall 在 $events 重连后会被重投」的
|
|
643
|
+
// 不变量。预算语义是「单次断连窗口内的连续失败上限」,不是终身配额。
|
|
644
|
+
reconnectAttempts = 0;
|
|
737
645
|
sawReady = true;
|
|
738
646
|
streamClientId = frame.clientId;
|
|
739
647
|
yield { kind: 'ready', clientId: streamClientId };
|
|
@@ -904,9 +812,10 @@ export class DshClient {
|
|
|
904
812
|
return;
|
|
905
813
|
authRefreshAttempted = false;
|
|
906
814
|
if (frame.type === 'snapshot') {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
815
|
+
// 快照可能只覆盖尾部窗口(见 replaySnapshotGap):先补缺口再回放,
|
|
816
|
+
// 回放循环与原逻辑逐条相同(去重 / admit / turn/end 早退)。
|
|
817
|
+
for (const ev of await this.replaySnapshotGap(sessionId, signal, state.lastSeq, frame)) {
|
|
818
|
+
if (ev.seq <= state.lastSeq)
|
|
910
819
|
continue;
|
|
911
820
|
state.lastSeq = ev.seq;
|
|
912
821
|
if (!admit(ev))
|
|
@@ -963,6 +872,63 @@ export class DshClient {
|
|
|
963
872
|
await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS));
|
|
964
873
|
}
|
|
965
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* 快照缺口回填(经 DSH 源码验证后实现)。
|
|
877
|
+
*
|
|
878
|
+
* follow 开场快照按**消息**截取:host 的 paginate 只数 user/assistant
|
|
879
|
+
* message(工具事件随所在消息组搭车进快照,不占预算),`maxMessages` 内
|
|
880
|
+
* 装不下的更早前缀被截掉并以 `hasMore: true` 标记。断连期间积压超过
|
|
881
|
+
* FOLLOW_MAX_MESSAGES 条消息时,`lastSeq` 与快照起点之间的缺口若不补,
|
|
882
|
+
* 这些事件对本次 follow 永久缺失(卡片丢中段工具行;seq 去重只防重放
|
|
883
|
+
* 不补缺)。`session/page` 的 `throughSeq` 游标正是服务端为此设计的回填
|
|
884
|
+
* 通道——从缺口尾部向前翻页直到盖住 lastSeq、翻到日志头或达到页数上限。
|
|
885
|
+
*
|
|
886
|
+
* best-effort:翻页失败保留已取回的部分(缺口剩余与旧行为一致地丢失,
|
|
887
|
+
* 但只发生在超出 BACKFILL_MAX_PAGES×PAGE_MESSAGES 条消息的极端断连)。
|
|
888
|
+
* 返回 seq 升序的完整回放序列(回填 + 快照记录;与 lastSeq 重叠的条目
|
|
889
|
+
* 由回放循环的 seq 去重兜住)。
|
|
890
|
+
*/
|
|
891
|
+
async replaySnapshotGap(sessionId, signal, lastSeq, frame) {
|
|
892
|
+
const records = (frame.records ?? [])
|
|
893
|
+
.map((record) => record.event)
|
|
894
|
+
.filter((ev) => !!ev);
|
|
895
|
+
if (frame.hasMore !== true || signal.aborted)
|
|
896
|
+
return records;
|
|
897
|
+
const firstSeq = records[0]?.seq;
|
|
898
|
+
const gapEnd = firstSeq !== undefined ? firstSeq - 1 : frame.cursor;
|
|
899
|
+
if (gapEnd <= lastSeq)
|
|
900
|
+
return records;
|
|
901
|
+
const pages = [];
|
|
902
|
+
try {
|
|
903
|
+
let throughSeq = gapEnd;
|
|
904
|
+
for (let page = 0; page < FOLLOW_GAP_BACKFILL_MAX_PAGES; page++) {
|
|
905
|
+
const value = await this.page(sessionId, throughSeq, FOLLOW_GAP_BACKFILL_PAGE_MESSAGES, {
|
|
906
|
+
signal,
|
|
907
|
+
});
|
|
908
|
+
const events = (value.records ?? [])
|
|
909
|
+
.map((record) => record.event)
|
|
910
|
+
.filter((ev) => !!ev);
|
|
911
|
+
if (events.length > 0)
|
|
912
|
+
pages.push(events);
|
|
913
|
+
const oldest = events[0]?.seq;
|
|
914
|
+
// hasMore=false(日志头)或本页已盖住 lastSeq:缺口补齐,停。
|
|
915
|
+
if (value.hasMore !== true || oldest === undefined || oldest <= lastSeq + 1)
|
|
916
|
+
break;
|
|
917
|
+
throughSeq = oldest - 1;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
getLogger().warn(`[dsh] follow snapshot gap backfill incomplete sessionId=${sessionId}: ${err.message}`);
|
|
922
|
+
}
|
|
923
|
+
// pages 是「从新到旧」收集的;倒序拼接得到 seq 升序回放。
|
|
924
|
+
const backfill = [];
|
|
925
|
+
for (let index = pages.length - 1; index >= 0; index--)
|
|
926
|
+
backfill.push(...pages[index]);
|
|
927
|
+
if (backfill.length > 0) {
|
|
928
|
+
getLogger().info(`[dsh] backfilled ${backfill.length} events before follow snapshot sessionId=${sessionId} gapEnd=${gapEnd} lastSeq=${lastSeq}`);
|
|
929
|
+
}
|
|
930
|
+
return [...backfill, ...records];
|
|
931
|
+
}
|
|
966
932
|
/**
|
|
967
933
|
* Host-wide live projection stream (`session/control`).
|
|
968
934
|
*
|
|
@@ -28,8 +28,6 @@ interface SpawnedDaemon {
|
|
|
28
28
|
failure(): Error | undefined;
|
|
29
29
|
exited(): boolean;
|
|
30
30
|
}
|
|
31
|
-
/** Resolve the same Harness home DSH resolves: configured, env, then ~/.dsh. */
|
|
32
|
-
export declare function resolveDshHome(configured?: string, env?: NodeJS.ProcessEnv): string;
|
|
33
31
|
/** True only for the loopback HTTP address this process may start and probe. */
|
|
34
32
|
export declare function isLoopbackHttpUrl(value: string): boolean;
|
|
35
33
|
/**
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import fsp from 'node:fs/promises';
|
|
15
|
-
import os from 'node:os';
|
|
16
15
|
import path from 'node:path';
|
|
17
16
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
18
17
|
import { mergeProcessEnv, spawnProcess } from '../../platform/spawn.js';
|
|
18
|
+
import { resolveDshHome } from './home.js';
|
|
19
19
|
const DEFAULT_START_TIMEOUT_MS = 30_000;
|
|
20
20
|
const READY_POLL_MS = 100;
|
|
21
21
|
/** User-facing daemon failure; the bridge renders `message` in the chat card. */
|
|
@@ -30,11 +30,6 @@ export class DshDaemonError extends Error {
|
|
|
30
30
|
function stripTrailingSlash(value) {
|
|
31
31
|
return value.replace(/\/+$/, '');
|
|
32
32
|
}
|
|
33
|
-
/** Resolve the same Harness home DSH resolves: configured, env, then ~/.dsh. */
|
|
34
|
-
export function resolveDshHome(configured, env = process.env) {
|
|
35
|
-
const value = configured?.trim() || env.DSH_HOME?.trim();
|
|
36
|
-
return path.resolve(value && value.length > 0 ? value : path.join(os.homedir(), '.dsh'));
|
|
37
|
-
}
|
|
38
33
|
/** True only for the loopback HTTP address this process may start and probe. */
|
|
39
34
|
export function isLoopbackHttpUrl(value) {
|
|
40
35
|
try {
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
* curl transport the session reader already uses: bounded by `timeoutMs` and
|
|
13
13
|
* never throws.
|
|
14
14
|
*/
|
|
15
|
-
/** Upper bound on the exit-path cancel:退出不能被一个卡住的 host 拖住。 */
|
|
16
|
-
export declare const EXIT_CANCEL_TIMEOUT_MS = 2000;
|
|
17
15
|
export interface SyncCancelOptions {
|
|
18
16
|
baseUrl: string;
|
|
19
17
|
sessionId: string;
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
* curl transport the session reader already uses: bounded by `timeoutMs` and
|
|
13
13
|
* never throws.
|
|
14
14
|
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
15
18
|
import { getLogger } from '../../logger/index.js';
|
|
16
19
|
import { isWin32 } from '../../platform/select.js';
|
|
17
20
|
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
18
21
|
import { DSH_COOKIE_LIFETIME_MS, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from './auth.js';
|
|
19
22
|
/** Upper bound on the exit-path cancel:退出不能被一个卡住的 host 拖住。 */
|
|
20
|
-
|
|
23
|
+
const EXIT_CANCEL_TIMEOUT_MS = 2_000;
|
|
21
24
|
/**
|
|
22
25
|
* Fire one `session/cancel` synchronously. Never throws: a failed cancel must
|
|
23
26
|
* not block the exit path or mask the real exit reason.
|
|
@@ -44,29 +47,65 @@ export function cancelSessionSync(opts) {
|
|
|
44
47
|
method: 'session/cancel',
|
|
45
48
|
payload: { args: { request: { sessionId: opts.sessionId } } },
|
|
46
49
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
`Cookie: ${cookie}`,
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
// Cookie 不走 argv(`-H "Cookie: …"` 会在 curl 运行期间对本机任何进程的
|
|
51
|
+
// ps 可见——1 小时有效的 v1 凭据泄露面):写 0600 临时文件用 `-H @file`
|
|
52
|
+
// 传入,用完即删。临时目录必须用 mkdtemp(0700、随机名):Linux 的 /tmp
|
|
53
|
+
// 所有用户可写,固定名(如 `<pid>.hdr`)可被本地用户预建软链,而
|
|
54
|
+
// writeFileSync 会跟随软链把 cookie 写进攻击者可读的文件;后续 chmod
|
|
55
|
+
// 也救不回已被打开的 fd。文件写失败时回退 argv——退出路径的 cancel
|
|
56
|
+
// 不能因此放弃。
|
|
57
|
+
let headerDir;
|
|
58
|
+
let cookieHeaderArgs;
|
|
59
|
+
try {
|
|
60
|
+
headerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lark-remote-exit-cancel-'));
|
|
61
|
+
const headerFile = path.join(headerDir, 'cookie.hdr');
|
|
62
|
+
fs.writeFileSync(headerFile, `Cookie: ${cookie}\n`, { mode: 0o600, flag: 'wx' });
|
|
63
|
+
cookieHeaderArgs = ['-H', `@${headerFile}`];
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.warn(`[dsh] exit-cancel header file unavailable: ${err.message}`);
|
|
67
|
+
removeDir(headerDir);
|
|
68
|
+
headerDir = undefined;
|
|
69
|
+
cookieHeaderArgs = ['-H', `Cookie: ${cookie}`];
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const res = spawnProcessSync('curl', [
|
|
73
|
+
'-s',
|
|
74
|
+
'-o',
|
|
75
|
+
isWin32() ? 'NUL' : '/dev/null',
|
|
76
|
+
'-w',
|
|
77
|
+
'%{http_code}',
|
|
78
|
+
'-X',
|
|
79
|
+
'POST',
|
|
80
|
+
`${opts.baseUrl.replace(/\/$/, '')}/api/session/cancel`,
|
|
81
|
+
'-H',
|
|
82
|
+
'content-type: application/json',
|
|
83
|
+
...cookieHeaderArgs,
|
|
84
|
+
'--max-time',
|
|
85
|
+
String(Math.max(1, Math.ceil(timeoutMs / 1000))),
|
|
86
|
+
'--data',
|
|
87
|
+
body,
|
|
88
|
+
], { encoding: 'utf-8', timeout: timeoutMs, maxBuffer: 1024 * 1024 });
|
|
89
|
+
const status = String(res.stdout ?? '').trim();
|
|
90
|
+
const ok = status === '200';
|
|
91
|
+
if (!ok) {
|
|
92
|
+
logger.warn(`[dsh] exit-cancel failed for ${opts.sessionId}: ${res.error?.message ?? `http ${status || 'n/a'}`}`);
|
|
93
|
+
}
|
|
94
|
+
return ok;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
removeDir(headerDir);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** 删除 cookie 临时目录(best-effort);目录未创建时是空操作。 */
|
|
101
|
+
function removeDir(dir) {
|
|
102
|
+
if (!dir)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* best-effort cleanup */
|
|
69
109
|
}
|
|
70
|
-
return ok;
|
|
71
110
|
}
|
|
72
111
|
//# sourceMappingURL=exit-cancel.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Resolve the same DSH home DSH itself uses: configured, env, then ~/.dsh. */
|
|
4
|
+
export function resolveDshHome(configured, env = process.env) {
|
|
5
|
+
const value = configured?.trim() || env.DSH_HOME?.trim();
|
|
6
|
+
return path.resolve(value && value.length > 0 ? value : path.join(os.homedir(), '.dsh'));
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=home.js.map
|
|
@@ -58,5 +58,3 @@ export interface DshInflightReconcileResult {
|
|
|
58
58
|
* entries are left in the file so a later startup can retry.
|
|
59
59
|
*/
|
|
60
60
|
export declare function reconcileInflightDshRuns(opts: DshInflightReconcileOptions): Promise<DshInflightReconcileResult>;
|
|
61
|
-
/** One short owner-facing notice; session ids are truncated, not paths. */
|
|
62
|
-
export declare function formatInflightWarning(entries: DshInflightEntry[]): string;
|
|
@@ -126,7 +126,7 @@ export async function reconcileInflightDshRuns(opts) {
|
|
|
126
126
|
return { removed, open: open.length, skipped: false };
|
|
127
127
|
}
|
|
128
128
|
/** One short owner-facing notice; session ids are truncated, not paths. */
|
|
129
|
-
|
|
129
|
+
function formatInflightWarning(entries) {
|
|
130
130
|
const lines = entries.slice(0, 3).map((entry) => {
|
|
131
131
|
const id = entry.sessionId.length > 8 ? entry.sessionId.slice(0, 8) : entry.sessionId;
|
|
132
132
|
return `- session ${id}(${entry.cwd ?? '未知目录'})`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface MuxSocket {
|
|
2
|
+
onopen: ((event: unknown) => void) | null;
|
|
3
|
+
onmessage: ((event: {
|
|
4
|
+
data: unknown;
|
|
5
|
+
}) => void) | null;
|
|
6
|
+
onerror: ((event: unknown) => void) | null;
|
|
7
|
+
onclose: ((event: unknown) => void) | null;
|
|
8
|
+
onunexpectedresponse?: ((event: {
|
|
9
|
+
statusCode?: number;
|
|
10
|
+
}) => void) | null;
|
|
11
|
+
send(data: string): void;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface OpenMuxStreamOptions {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
payload: unknown;
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
requestTimeoutMs: number;
|
|
20
|
+
authCookie(): Promise<string | undefined>;
|
|
21
|
+
webSocketFactory(url: string, options?: {
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}): MuxSocket;
|
|
24
|
+
}
|
|
25
|
+
export declare function openMuxStream(opts: OpenMuxStreamOptions): AsyncGenerator<unknown>;
|
|
26
|
+
export {};
|