lark-remote 0.5.5 → 0.5.7
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 +48 -7
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/approval-coordinator.d.ts +13 -0
- package/dist/bridge/approval-coordinator.js +34 -1
- package/dist/bridge/index.d.ts +48 -1
- package/dist/bridge/index.js +86 -14
- package/dist/card/approval-render.js +23 -1
- package/dist/card/card-budget.d.ts +37 -0
- package/dist/card/card-budget.js +128 -5
- package/dist/card/run-renderer.d.ts +24 -0
- package/dist/card/run-renderer.js +256 -60
- package/dist/card/run-state.d.ts +26 -0
- package/dist/card/run-state.js +45 -0
- package/dist/card/time.d.ts +14 -0
- package/dist/card/time.js +30 -0
- package/dist/card/tool-render.d.ts +15 -2
- package/dist/card/tool-render.js +40 -66
- package/dist/common/text-width.d.ts +24 -0
- package/dist/common/text-width.js +93 -0
- package/dist/common/tool-summary.d.ts +11 -0
- package/dist/common/tool-summary.js +153 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/index.js +63 -8
- package/dist/platform/command.js +1 -1
- package/dist/platform/shell.d.ts +2 -2
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/router/card-helpers.d.ts +18 -16
- package/dist/router/card-helpers.js +65 -4
- package/dist/router/index.d.ts +9 -0
- package/dist/router/index.js +57 -7
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +3 -3
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +3 -3
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +2 -2
- package/dist/runner/dsh/auth.js +2 -1
- package/dist/runner/dsh/client.d.ts +124 -6
- package/dist/runner/dsh/client.js +398 -12
- package/dist/runner/dsh/exit-cancel.d.ts +32 -0
- package/dist/runner/dsh/exit-cancel.js +72 -0
- package/dist/runner/dsh/inflight.d.ts +62 -0
- package/dist/runner/dsh/inflight.js +140 -0
- package/dist/runner/dsh/runner.d.ts +117 -7
- package/dist/runner/dsh/runner.js +607 -21
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +103 -1
- package/dist/runner/dsh/types.d.ts +91 -1
- package/dist/runner/types.d.ts +86 -2
- package/dist/session/claude/sessions.js +2 -2
- package/dist/session/dsh/sessions.js +100 -15
- package/dist/update/install.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
|
11
11
|
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
|
|
12
12
|
import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, COMPACT_ACTION_CMDS, } from './router/index.js';
|
|
13
13
|
import { DshDaemon } from './runner/dsh/daemon.js';
|
|
14
|
+
import { DshClient } from './runner/dsh/client.js';
|
|
15
|
+
import { DshInflightStore, dshInflightPath, reconcileInflightDshRuns, } from './runner/dsh/inflight.js';
|
|
14
16
|
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
15
17
|
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
16
18
|
import { actionFeedbackText } from './router/card-action-feedback.js';
|
|
@@ -29,7 +31,7 @@ import { WorkspaceStore } from './workspace/index.js';
|
|
|
29
31
|
import { InboundTurnAssembler } from './inbound/turn-assembler.js';
|
|
30
32
|
import { InboundAuthorizer } from './inbound/authorize.js';
|
|
31
33
|
import { stripPlaceholders } from './inbound/placeholder.js';
|
|
32
|
-
import { buildSessionHistoryCard } from './router/card-helpers.js';
|
|
34
|
+
import { buildSessionHistoryCard, pickResumeCardEvents } from './router/card-helpers.js';
|
|
33
35
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
34
36
|
import path from 'node:path';
|
|
35
37
|
import fs from 'node:fs';
|
|
@@ -43,16 +45,18 @@ function isStopCommand(text) {
|
|
|
43
45
|
return t === '/stop' || t === '/t';
|
|
44
46
|
}
|
|
45
47
|
/** Send auto-resume card for a restored session */
|
|
46
|
-
async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
|
|
48
|
+
async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false, stillRunning = false) {
|
|
47
49
|
if (!contact.userId)
|
|
48
50
|
return;
|
|
49
51
|
const recipient = contact.chatId ?? contact.userId;
|
|
50
52
|
if (!recipient)
|
|
51
53
|
return;
|
|
52
|
-
// Limit history events to prevent card size exceeded error (11310)
|
|
54
|
+
// Limit history events to prevent card size exceeded error (11310).
|
|
55
|
+
// 窗口按"最后 N 条"截,但强制保住最后一条用户消息与最后一条 assistant 正文,
|
|
56
|
+
// 否则工具往返会把它俩一起挤出卡(2026-10-19 恢复卡只剩 5 个 bash 面板)。
|
|
53
57
|
const MAX_HISTORY_EVENTS = 5;
|
|
54
|
-
const eventsToShow = state.events
|
|
55
|
-
const hiddenCount = state.events.length -
|
|
58
|
+
const eventsToShow = pickResumeCardEvents(state.events, MAX_HISTORY_EVENTS);
|
|
59
|
+
const hiddenCount = state.events.length - eventsToShow.length;
|
|
56
60
|
// Action buttons: Compact (runCompact-capable runner) + new session.
|
|
57
61
|
const agentKind = state.agentKind ?? 'claude';
|
|
58
62
|
const actionButtons = [];
|
|
@@ -73,6 +77,7 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
|
|
|
73
77
|
headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
|
|
74
78
|
title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
|
|
75
79
|
hiddenCount,
|
|
80
|
+
stillRunning,
|
|
76
81
|
actions: actionButtons,
|
|
77
82
|
});
|
|
78
83
|
await connector.sendWithRetry(recipient, { card });
|
|
@@ -300,6 +305,9 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
300
305
|
provider: dshConf?.provider,
|
|
301
306
|
model: dshConf?.model,
|
|
302
307
|
reasoningEffort: dshConf?.reasoningEffort,
|
|
308
|
+
// Durable markers for turns that outlive this process; reconciled on the
|
|
309
|
+
// next startup (see reconcileInflightDshRuns).
|
|
310
|
+
inflightPath: dshInflightPath(configDir),
|
|
303
311
|
});
|
|
304
312
|
});
|
|
305
313
|
sessionReaderRegistry.register('dsh', dshSessionReader);
|
|
@@ -631,8 +639,8 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
631
639
|
// queue.input / config.save / approval.respond / approval.toggle /
|
|
632
640
|
// approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
|
|
633
641
|
// 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
|
|
634
|
-
// fire-and-forget,会吞掉返回值(
|
|
635
|
-
//
|
|
642
|
+
// fire-and-forget,会吞掉返回值(answer 家族曾漏在直返列表外,
|
|
643
|
+
// 过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
|
|
636
644
|
// 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
|
|
637
645
|
// 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
|
|
638
646
|
// order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
|
|
@@ -834,6 +842,33 @@ async function main() {
|
|
|
834
842
|
})();
|
|
835
843
|
}
|
|
836
844
|
}
|
|
845
|
+
// 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
|
|
846
|
+
// turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
|
|
847
|
+
// 已绑定;没有收件人时只是不发提示。
|
|
848
|
+
const dshConf = getAgentConfig(config, 'dsh');
|
|
849
|
+
if (dshConf) {
|
|
850
|
+
const contact = startupContactStore.getContact();
|
|
851
|
+
const recipient = contact?.chatId ?? contact?.userId;
|
|
852
|
+
void reconcileInflightDshRuns({
|
|
853
|
+
store: new DshInflightStore(dshInflightPath(configDir)),
|
|
854
|
+
client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
|
|
855
|
+
dshHome: dshConf.dshHome,
|
|
856
|
+
token: dshConf.token,
|
|
857
|
+
credentialsFile: dshConf.credentialsFile,
|
|
858
|
+
// Host may be down: bound the probe instead of waiting the default 60s.
|
|
859
|
+
requestTimeoutMs: 3_000,
|
|
860
|
+
}),
|
|
861
|
+
...(recipient
|
|
862
|
+
? {
|
|
863
|
+
notify: async (text) => {
|
|
864
|
+
await connector.sendWithRetry(recipient, { text });
|
|
865
|
+
},
|
|
866
|
+
}
|
|
867
|
+
: {}),
|
|
868
|
+
}).catch((err) => {
|
|
869
|
+
logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
837
872
|
// Auto-restore: resume the persisted sessionId if available
|
|
838
873
|
const restoredContact = startupContactStore.getContact();
|
|
839
874
|
if (restoredContact?.userId) {
|
|
@@ -845,6 +880,26 @@ async function main() {
|
|
|
845
880
|
const startupReader = sessionReaderRegistry.get(config.defaultAgent);
|
|
846
881
|
sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
|
|
847
882
|
const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
|
|
883
|
+
// 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
|
|
884
|
+
// 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
|
|
885
|
+
//
|
|
886
|
+
// 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
|
|
887
|
+
// reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
|
|
888
|
+
// 每次自动恢复时误报。
|
|
889
|
+
const stillRunning = config.defaultAgent === 'dsh'
|
|
890
|
+
? (() => {
|
|
891
|
+
try {
|
|
892
|
+
return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
})()
|
|
899
|
+
: false;
|
|
900
|
+
if (stillRunning) {
|
|
901
|
+
getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
|
|
902
|
+
}
|
|
848
903
|
// 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
|
|
849
904
|
// 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
|
|
850
905
|
const usage = content.usage;
|
|
@@ -862,7 +917,7 @@ async function main() {
|
|
|
862
917
|
agentKind: config.defaultAgent,
|
|
863
918
|
},
|
|
864
919
|
// Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
|
|
865
|
-
bridge.hasRunCompact(restoredCwd, config.defaultAgent));
|
|
920
|
+
bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
|
|
866
921
|
}
|
|
867
922
|
catch (err) {
|
|
868
923
|
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
package/dist/platform/command.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 自更新安装源检测 update/install.ts)。**执行**统一走 platform/spawn.ts 的
|
|
6
6
|
* cross-spawn 包装(PATHEXT 解析 + .cmd 经 cmd.exe 受控执行),本模块不再
|
|
7
7
|
* 解析 .cmd/.bat 垫片内容——垫片格式是 npm/pnpm 的上游实现细节(历史变过、
|
|
8
|
-
* pnpm
|
|
8
|
+
* pnpm 生成形态不同),手写解析器等于耦合该细节。
|
|
9
9
|
*
|
|
10
10
|
* posix:PATH 目录顺序 + X_OK(现状语义迁移)。
|
|
11
11
|
* win32:PATH 目录顺序 × PATHEXT(.exe/.cmd/.bat) 解析,.cmd 命中即报告垫片
|
package/dist/platform/shell.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 由调用方转成明确错误卡(提示需要 Git Bash)。
|
|
9
9
|
*
|
|
10
10
|
* kimi ACP 的 terminal/create 固定 `bash -c`,因此 Git Bash 不是 `!` 的专属
|
|
11
|
-
* 需求,而是 kimi agent 在 Windows
|
|
11
|
+
* 需求,而是 kimi agent 在 Windows 上可用的前置条件。
|
|
12
12
|
*/
|
|
13
13
|
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
14
14
|
import { type ResolveOptions, type LaunchSpec } from './command.js';
|
|
@@ -31,7 +31,7 @@ export interface ShellBackend {
|
|
|
31
31
|
* - posix:原样透传(与迁移前 `spawnProcess(command, args, ...)` 逐字一致);
|
|
32
32
|
* - win32:首 token 是 shell(bash/sh/zsh/dash)时按 Git Bash 解析,缺失则抛
|
|
33
33
|
* {@link ShellUnavailableError}——kimi agent 在 Windows 上可用的前置条件
|
|
34
|
-
*
|
|
34
|
+
* 。其余命令交给 cross-spawn 做 PATH/PATHEXT 解析,不做命令翻译。
|
|
35
35
|
*/
|
|
36
36
|
spawnArgv(file: string, args: readonly string[], opts?: {
|
|
37
37
|
cwd?: string;
|
package/dist/platform/spawn.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function useDetachedProcessGroup(platform?: NodeJS.Platform): boolean;
|
|
37
37
|
/**
|
|
38
|
-
* win32 command-not-found
|
|
38
|
+
* win32 command-not-found 行嗅探:命中即视为「命令缺失」失败而非
|
|
39
39
|
* 普通非零退出。platform 注入使其在任意宿主可测。
|
|
40
40
|
*/
|
|
41
41
|
export declare function isWindowsCommandNotFoundLine(line: string, platform?: NodeJS.Platform): boolean;
|
package/dist/platform/spawn.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 必须经它合并(先按不区分大小写删旧键再设新值);
|
|
11
11
|
* - isWindowsCommandNotFoundLine:win32 经 cmd 垫片启动失败不是 spawn 的
|
|
12
12
|
* error 事件 ENOENT,而是子进程往 stderr 打
|
|
13
|
-
* "'xxx' is not recognized as an internal or external command"
|
|
13
|
+
* "'xxx' is not recognized as an internal or external command"(见本文件 command-not-found 嗅探)。
|
|
14
14
|
*/
|
|
15
15
|
import crossSpawn from 'cross-spawn';
|
|
16
16
|
import { currentPlatform, isWin32 } from './select.js';
|
|
@@ -68,7 +68,7 @@ export function useDetachedProcessGroup(platform = currentPlatform) {
|
|
|
68
68
|
return !isWin32(platform);
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* win32 command-not-found
|
|
71
|
+
* win32 command-not-found 行嗅探:命中即视为「命令缺失」失败而非
|
|
72
72
|
* 普通非零退出。platform 注入使其在任意宿主可测。
|
|
73
73
|
*/
|
|
74
74
|
export function isWindowsCommandNotFoundLine(line, platform = currentPlatform) {
|
package/dist/platform/types.d.ts
CHANGED
|
@@ -10,22 +10,6 @@ import { markdownDiv } from '../card/collapsible.js';
|
|
|
10
10
|
import type { AgentSessionContentEvent } from '../runner/index.js';
|
|
11
11
|
import { formatUsageStats } from './usage-format.js';
|
|
12
12
|
type SessionUsageLike = Parameters<typeof formatUsageStats>[0];
|
|
13
|
-
/**
|
|
14
|
-
* Build a collapsible panel for a single session content event.
|
|
15
|
-
*
|
|
16
|
-
* Header carries a type emoji (👤 user / 🤖 assistant·text / 💭 thinking /
|
|
17
|
-
* 🔧 tool_use / 🟢🔴 tool_result) with optional timestamp.
|
|
18
|
-
* Body: the event's content (markdown).
|
|
19
|
-
*
|
|
20
|
-
* The last `tailExpandedCount` events are expanded by default so the user
|
|
21
|
-
* sees the most recent context without clicking; older events are collapsed.
|
|
22
|
-
*
|
|
23
|
-
* @param ev - Session content event
|
|
24
|
-
* @param index - Index in the events array
|
|
25
|
-
* @param totalEvents - Total number of events
|
|
26
|
-
* @param tailExpandedCount - How many events from the end to keep expanded (default 2)
|
|
27
|
-
* @param agentKind - Agent type for display name (e.g. 'claude', 'pi'). Defaults to 'claude'.
|
|
28
|
-
*/
|
|
29
13
|
export declare function sessionEventPanel(ev: AgentSessionContentEvent, index: number, totalEvents: number, tailExpandedCount?: number, agentKind?: string): object;
|
|
30
14
|
export { markdownDiv };
|
|
31
15
|
/** 页码输入非法时的统一提示文案(/ls /ws /resume /active /order 共用)。 */
|
|
@@ -115,6 +99,19 @@ export declare function searchBar(opts: {
|
|
|
115
99
|
currentQuery?: string;
|
|
116
100
|
extra?: Record<string, unknown>;
|
|
117
101
|
}): object;
|
|
102
|
+
/**
|
|
103
|
+
* 选出 resume/历史卡该展示的事件。
|
|
104
|
+
*
|
|
105
|
+
* 旧口径是 `events.slice(-max)`:按**原始事件条数**截尾。工具往返(bash 调用 →
|
|
106
|
+
* job 登记 → 结果)一段就能把窗口占满,于是重返会话的用户真正需要的两条信息
|
|
107
|
+
* ——最后一句我说了什么、最后一句它答了什么——被挤出窗口。2026-10-19 实测:
|
|
108
|
+
* 自动恢复卡 5 个面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
|
|
109
|
+
*
|
|
110
|
+
* 契约:先取最新 `max` 条,再把最后一条 `user` 与最后一条 `assistant`(正文)
|
|
111
|
+
* 不在窗口内时补进来——补位时淘汰最早的**非锚点**事件。输出保持时间顺序,
|
|
112
|
+
* 长度不超过 `max`。
|
|
113
|
+
*/
|
|
114
|
+
export declare function pickResumeCardEvents(events: AgentSessionContentEvent[], max: number): AgentSessionContentEvent[];
|
|
118
115
|
/**
|
|
119
116
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
120
117
|
* config-switch resume, and run-completion notification).
|
|
@@ -145,6 +142,11 @@ export declare function buildSessionHistoryCard(state: {
|
|
|
145
142
|
emptyPlaceholder?: string;
|
|
146
143
|
/** Hidden earlier-events count (auto-restore only). */
|
|
147
144
|
hiddenCount?: number;
|
|
145
|
+
/**
|
|
146
|
+
* 该会话此刻仍有在途 turn(仅 DSH 的 `isSessionActive` 是服务端 running
|
|
147
|
+
* 事实)。为 true 时卡上显式提示"这是历史快照、实时输出不会落在这张卡上"。
|
|
148
|
+
*/
|
|
149
|
+
stillRunning?: boolean;
|
|
148
150
|
/** Action buttons to render (already built, protocol-specific). */
|
|
149
151
|
actions?: object[];
|
|
150
152
|
/** Optional card header template (e.g. 'green' for completion cards). */
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { agentDisplayName } from '../card/card-shared.js';
|
|
10
10
|
import { collapsibleMarkdownPanel, markdownDiv } from '../card/collapsible.js';
|
|
11
|
-
import {
|
|
11
|
+
import { formatMonthDayClock } from '../card/time.js';
|
|
12
|
+
import { displayWidth, truncateMiddleToDisplayWidth } from '../common/text-width.js';
|
|
12
13
|
import { formatUsageStats } from './usage-format.js';
|
|
13
14
|
/**
|
|
14
15
|
* Build a collapsible panel for a single session content event.
|
|
@@ -26,10 +27,21 @@ import { formatUsageStats } from './usage-format.js';
|
|
|
26
27
|
* @param tailExpandedCount - How many events from the end to keep expanded (default 2)
|
|
27
28
|
* @param agentKind - Agent type for display name (e.g. 'claude', 'pi'). Defaults to 'claude'.
|
|
28
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* resume / 历史卡片的事件面板标题宽度上限(半角单位)。
|
|
32
|
+
*
|
|
33
|
+
* 与 run 卡的工具面板同口径:手机上一行约 42 个半角宽,标题超了会折行。
|
|
34
|
+
*/
|
|
35
|
+
const SESSION_PANEL_TITLE_MAX = 42;
|
|
29
36
|
export function sessionEventPanel(ev, index, totalEvents, tailExpandedCount = 2, agentKind = 'claude') {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
// reader 可以给事件一个"标题即摘要"的名字(工具调用:`🔧 bash — npm test`);
|
|
38
|
+
// 缺省用类型标签。标题连同时间戳一起收进一行显示宽度——手机上折叠面板只有
|
|
39
|
+
// 一行可用(2026-10-19 工具标题被长 URL 撑成两行那次)。
|
|
40
|
+
const label = ev.title ?? eventLabel(ev, agentKind);
|
|
41
|
+
const ts = formatMonthDayClock(ev.timestamp);
|
|
42
|
+
const suffix = ts ? ` (${ts})` : '';
|
|
43
|
+
const labelMax = Math.max(8, SESSION_PANEL_TITLE_MAX - displayWidth(suffix));
|
|
44
|
+
const title = `${truncateMiddleToDisplayWidth(label, labelMax)}${suffix}`;
|
|
33
45
|
const expanded = index >= totalEvents - tailExpandedCount;
|
|
34
46
|
return collapsibleMarkdownPanel({
|
|
35
47
|
title,
|
|
@@ -269,6 +281,50 @@ export function searchBar(opts) {
|
|
|
269
281
|
}
|
|
270
282
|
return { tag: 'column_set', columns };
|
|
271
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* 选出 resume/历史卡该展示的事件。
|
|
286
|
+
*
|
|
287
|
+
* 旧口径是 `events.slice(-max)`:按**原始事件条数**截尾。工具往返(bash 调用 →
|
|
288
|
+
* job 登记 → 结果)一段就能把窗口占满,于是重返会话的用户真正需要的两条信息
|
|
289
|
+
* ——最后一句我说了什么、最后一句它答了什么——被挤出窗口。2026-10-19 实测:
|
|
290
|
+
* 自动恢复卡 5 个面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
|
|
291
|
+
*
|
|
292
|
+
* 契约:先取最新 `max` 条,再把最后一条 `user` 与最后一条 `assistant`(正文)
|
|
293
|
+
* 不在窗口内时补进来——补位时淘汰最早的**非锚点**事件。输出保持时间顺序,
|
|
294
|
+
* 长度不超过 `max`。
|
|
295
|
+
*/
|
|
296
|
+
export function pickResumeCardEvents(events, max) {
|
|
297
|
+
if (max <= 0)
|
|
298
|
+
return [];
|
|
299
|
+
if (events.length <= max)
|
|
300
|
+
return events.slice();
|
|
301
|
+
const picked = new Set();
|
|
302
|
+
for (let i = events.length - 1; i >= 0 && picked.size < max; i -= 1)
|
|
303
|
+
picked.add(i);
|
|
304
|
+
const lastIndexOf = (predicate) => {
|
|
305
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
306
|
+
const ev = events[i];
|
|
307
|
+
if (ev && predicate(ev))
|
|
308
|
+
return i;
|
|
309
|
+
}
|
|
310
|
+
return -1;
|
|
311
|
+
};
|
|
312
|
+
const anchors = [
|
|
313
|
+
lastIndexOf((ev) => ev.type === 'user'),
|
|
314
|
+
lastIndexOf((ev) => ev.type === 'assistant'),
|
|
315
|
+
].filter((i) => i >= 0);
|
|
316
|
+
for (const anchor of anchors) {
|
|
317
|
+
if (picked.has(anchor))
|
|
318
|
+
continue;
|
|
319
|
+
// 腾位置时只淘汰非锚点:两个锚点之间不互相挤掉。
|
|
320
|
+
const victim = [...picked].filter((i) => !anchors.includes(i)).sort((a, b) => a - b)[0];
|
|
321
|
+
if (victim === undefined)
|
|
322
|
+
break;
|
|
323
|
+
picked.delete(victim);
|
|
324
|
+
picked.add(anchor);
|
|
325
|
+
}
|
|
326
|
+
return [...picked].sort((a, b) => a - b).map((i) => events[i]);
|
|
327
|
+
}
|
|
272
328
|
/**
|
|
273
329
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
274
330
|
* config-switch resume, and run-completion notification).
|
|
@@ -297,6 +353,11 @@ export function buildSessionHistoryCard(state, opts) {
|
|
|
297
353
|
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
298
354
|
}
|
|
299
355
|
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
356
|
+
// 恢复时该会话仍有一个 turn 在跑(典型来源:上一个桥接实例退出/崩溃后,
|
|
357
|
+
// turn 留在常驻 host 里继续执行)。卡片只是快照,别让用户以为这就是全部。
|
|
358
|
+
if (opts.stillRunning) {
|
|
359
|
+
elements.push(markdownDiv('⚠️ **该会话仍有一个 turn 在运行**(可能来自已退出的实例):以下内容是历史快照,实时输出不会出现在这张卡上。'));
|
|
360
|
+
}
|
|
300
361
|
if (opts.hiddenCount && opts.hiddenCount > 0) {
|
|
301
362
|
elements.push(markdownDiv(`📜 还有 ${opts.hiddenCount} 个更早的事件未显示`));
|
|
302
363
|
}
|
package/dist/router/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface CardActionPayload {
|
|
|
33
33
|
path?: string;
|
|
34
34
|
name?: string;
|
|
35
35
|
runId?: string;
|
|
36
|
+
/** 终态卡「停止」的目标后台任务 id(DSH job/list 的 jobId)。 */
|
|
37
|
+
jobId?: string;
|
|
36
38
|
sessionId?: string;
|
|
37
39
|
/** 目标工作目录(queue.* 卡片动作定位队列;stop 等卡片回传执行目录)。 */
|
|
38
40
|
cwd?: string;
|
|
@@ -352,6 +354,13 @@ export declare class CommandRouter {
|
|
|
352
354
|
* Handle stop card action: interrupt current run.
|
|
353
355
|
*/
|
|
354
356
|
private handleCardStop;
|
|
357
|
+
/**
|
|
358
|
+
* Handle a terminal card's 「停止」 on one background job.
|
|
359
|
+
*
|
|
360
|
+
* 任务跨 run 生命周期,所以卡片按钮是唯一入口;失败必须给出可读原因
|
|
361
|
+
* (任务已自行结束、服务重启后快照被回收),不能点了没反应。
|
|
362
|
+
*/
|
|
363
|
+
private handleJobKill;
|
|
355
364
|
/**
|
|
356
365
|
* Resolve an order's text at the enqueue boundary so order.exec can enter
|
|
357
366
|
* the serial queue as an equivalent user message (Plan A) — the same path a
|
package/dist/router/index.js
CHANGED
|
@@ -646,6 +646,9 @@ export class CommandRouter {
|
|
|
646
646
|
case 'stop':
|
|
647
647
|
await this.handleCardStop(value, ctx);
|
|
648
648
|
return;
|
|
649
|
+
case 'job.kill':
|
|
650
|
+
await this.handleJobKill(value, ctx);
|
|
651
|
+
return;
|
|
649
652
|
case 'queue.cancel':
|
|
650
653
|
// 撤销:从队列中删除这个消息
|
|
651
654
|
await this.handleQueueCancel(value, ctx);
|
|
@@ -832,12 +835,17 @@ export class CommandRouter {
|
|
|
832
835
|
// 2. Stop current running process
|
|
833
836
|
// (Note: queue executing count is reset inside Bridge.interruptCurrentRun
|
|
834
837
|
// so all stop paths — /stop, /t, card stop, queue.immediate — are covered.)
|
|
835
|
-
const
|
|
838
|
+
const interrupt = await this.bridge.interruptCurrentRunDetailed({
|
|
836
839
|
userId: ctx.userId,
|
|
837
840
|
chatId: ctx.chatId,
|
|
838
841
|
cwd,
|
|
839
842
|
});
|
|
840
|
-
|
|
843
|
+
const stopped = interrupt.stopped;
|
|
844
|
+
// A remote (DSH) stop is only a request; when it cannot be confirmed the
|
|
845
|
+
// target must not be promised "immediate" execution — its prompt would sit
|
|
846
|
+
// behind the still-running turn with no visible progress.
|
|
847
|
+
const stopUnconfirmed = interrupt.stop?.status === 'unconfirmed';
|
|
848
|
+
getLogger().info(`[router] queue.immediate: stopped=${stopped} unconfirmed=${stopUnconfirmed}`);
|
|
841
849
|
// 3. Remove all tasks BEFORE this one (by position in the current queue).
|
|
842
850
|
// The target may have begun while interruptCurrentRun was awaiting (the chain
|
|
843
851
|
// advances concurrently): then it is already out of the snapshot and every
|
|
@@ -910,7 +918,14 @@ export class CommandRouter {
|
|
|
910
918
|
return;
|
|
911
919
|
}
|
|
912
920
|
const workspaceBusy = this.bridge.isBusyFor(cwd);
|
|
913
|
-
if (
|
|
921
|
+
if (stopUnconfirmed) {
|
|
922
|
+
const name = interrupt.agentKind ? agentDisplayName(interrupt.agentKind) : 'agent';
|
|
923
|
+
await this.bridge.sendResult({
|
|
924
|
+
text: `⚠️ 未能确认 ${name} 的 turn 已停止,它可能仍在后台运行。\n` +
|
|
925
|
+
`已清除 ${removedCount} 条排队消息;您的消息已提交,但可能要等该 turn 结束后才开始。`,
|
|
926
|
+
}, ctx);
|
|
927
|
+
}
|
|
928
|
+
else if (!workspaceBusy) {
|
|
914
929
|
// Normal path: nothing new began during the stop, the target is next.
|
|
915
930
|
// Mark its own card as executing now so buttons grey out immediately.
|
|
916
931
|
// Done BEFORE any removal so getQueuedTask still returns the task with
|
|
@@ -1351,16 +1366,42 @@ export class CommandRouter {
|
|
|
1351
1366
|
await this.bridge.sendResult({ text: '⚠️ 无效的停止请求,缺少必要信息' }, ctx);
|
|
1352
1367
|
return;
|
|
1353
1368
|
}
|
|
1354
|
-
const
|
|
1369
|
+
const interrupt = await this.bridge.interruptCurrentRunDetailed({
|
|
1355
1370
|
userId: ctx.userId,
|
|
1356
1371
|
chatId: ctx.chatId,
|
|
1357
1372
|
runId: value.runId,
|
|
1358
1373
|
});
|
|
1374
|
+
const stopped = interrupt.stopped;
|
|
1359
1375
|
// Regression 2026-06-22: card stop button silently no-op'd when the
|
|
1360
1376
|
// run had already exited. Give the user a visible result.
|
|
1361
1377
|
if (!stopped) {
|
|
1362
1378
|
await this.bridge.sendResult({ text: '该任务已结束,无需终止' }, ctx);
|
|
1379
|
+
return;
|
|
1363
1380
|
}
|
|
1381
|
+
// DSH: `session/cancel` is only a request. If the host still reports the
|
|
1382
|
+
// turn open, say so instead of letting the card imply a clean stop.
|
|
1383
|
+
if (interrupt.stop?.status === 'unconfirmed') {
|
|
1384
|
+
const name = interrupt.agentKind ? agentDisplayName(interrupt.agentKind) : 'agent';
|
|
1385
|
+
await this.bridge.sendResult({
|
|
1386
|
+
text: `⚠️ 未能确认 ${name} 的 turn 已停止,它可能仍在后台运行。\n` +
|
|
1387
|
+
'请在 DSH Web UI 检查;需要终止时再点一次停止。',
|
|
1388
|
+
}, ctx);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Handle a terminal card's 「停止」 on one background job.
|
|
1393
|
+
*
|
|
1394
|
+
* 任务跨 run 生命周期,所以卡片按钮是唯一入口;失败必须给出可读原因
|
|
1395
|
+
* (任务已自行结束、服务重启后快照被回收),不能点了没反应。
|
|
1396
|
+
*/
|
|
1397
|
+
async handleJobKill(value, ctx) {
|
|
1398
|
+
const runId = typeof value.runId === 'string' ? value.runId : '';
|
|
1399
|
+
const jobId = typeof value.jobId === 'string' ? value.jobId : '';
|
|
1400
|
+
if (!runId || !jobId)
|
|
1401
|
+
return;
|
|
1402
|
+
if (await this.bridge.killBackgroundJob(runId, jobId, ctx))
|
|
1403
|
+
return;
|
|
1404
|
+
await this.bridge.sendResult({ text: '停止失败:任务可能已自行结束,或记录已回收' }, ctx);
|
|
1364
1405
|
}
|
|
1365
1406
|
/**
|
|
1366
1407
|
* Resolve an order's text at the enqueue boundary so order.exec can enter
|
|
@@ -2086,7 +2127,7 @@ export class CommandRouter {
|
|
|
2086
2127
|
switchSessionId = '';
|
|
2087
2128
|
}
|
|
2088
2129
|
}
|
|
2089
|
-
// DSH preset 变更 = 下次 run 新建 session
|
|
2130
|
+
// DSH preset 变更 = 下次 run 新建 session:
|
|
2090
2131
|
// preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
|
|
2091
2132
|
// 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
|
|
2092
2133
|
// preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
|
|
@@ -2431,11 +2472,20 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2431
2472
|
};
|
|
2432
2473
|
}
|
|
2433
2474
|
async cmdStop(ctx) {
|
|
2434
|
-
const
|
|
2475
|
+
const interrupt = await this.bridge.interruptCurrentRunDetailed({
|
|
2435
2476
|
userId: ctx.userId,
|
|
2436
2477
|
chatId: ctx.chatId,
|
|
2437
2478
|
});
|
|
2438
|
-
|
|
2479
|
+
if (!interrupt.stopped)
|
|
2480
|
+
return { text: '当前没有运行中的进程' };
|
|
2481
|
+
if (interrupt.stop?.status === 'unconfirmed') {
|
|
2482
|
+
const name = interrupt.agentKind ? agentDisplayName(interrupt.agentKind) : 'agent';
|
|
2483
|
+
return {
|
|
2484
|
+
text: `⚠️ 未能确认 ${name} 的 turn 已停止,它可能仍在后台运行。\n` +
|
|
2485
|
+
'请在 DSH Web UI 检查;需要终止时再发一次 /stop。',
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
return null;
|
|
2439
2489
|
}
|
|
2440
2490
|
async cmdExit() {
|
|
2441
2491
|
// Signal handle() to invoke exitHandler after the reply is sent.
|
|
@@ -147,7 +147,7 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
147
147
|
private consumeTurn;
|
|
148
148
|
/**
|
|
149
149
|
* 构造进程退出且无终态 result 时的统一错误结果(对齐原 SpawningRunner
|
|
150
|
-
* buildResultEvent 语义,含
|
|
150
|
+
* buildResultEvent 语义,含 win32 command-not-found 的双条件定性)。
|
|
151
151
|
*/
|
|
152
152
|
private buildStreamEndedError;
|
|
153
153
|
private wakeWaiters;
|
|
@@ -306,7 +306,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
306
306
|
this.cwd = opts.cwd;
|
|
307
307
|
this.autoApprove = false;
|
|
308
308
|
this.pendingToolInputs.clear();
|
|
309
|
-
//
|
|
309
|
+
// win32 的 command-not-found 嫌疑标记每次 spawn 重置(spawnChild 置位,收尾双条件消费)。
|
|
310
310
|
this.commandNotFoundSeen = false;
|
|
311
311
|
// 每次 run 独立:上次 run 会话内跟踪到的计划文件不得泄漏到本次
|
|
312
312
|
// (否则本次 ExitPlanMode 无 plan/planFilePath 时 resolvePlanContent
|
|
@@ -422,7 +422,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
422
422
|
}
|
|
423
423
|
/**
|
|
424
424
|
* 构造进程退出且无终态 result 时的统一错误结果(对齐原 SpawningRunner
|
|
425
|
-
* buildResultEvent 语义,含
|
|
425
|
+
* buildResultEvent 语义,含 win32 command-not-found 的双条件定性)。
|
|
426
426
|
*/
|
|
427
427
|
async buildStreamEndedError() {
|
|
428
428
|
// 进程已死但 proc 'close' 事件可能略晚于 stdout 关闭:等退出登记拿可靠
|
|
@@ -438,7 +438,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
438
438
|
errorMessage = `${this.binary} killed by signal ${exited.signal}${stderrTail ? `: ${stderrTail}` : ''}`;
|
|
439
439
|
}
|
|
440
440
|
else if (exited.code !== null && exited.code !== 0) {
|
|
441
|
-
//
|
|
441
|
+
// 双条件定性:本分支已保证「用户未停 + signal 为 null + 非零退出」,
|
|
442
442
|
// 这里只补嗅探嫌疑标记这第二个条件。
|
|
443
443
|
const commandNotFound = this.commandNotFoundSeen;
|
|
444
444
|
if (commandNotFound) {
|
|
@@ -30,7 +30,7 @@ export declare class JsonlRpcTransport {
|
|
|
30
30
|
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
31
31
|
private writeQueue;
|
|
32
32
|
private flushing;
|
|
33
|
-
/**
|
|
33
|
+
/** win32 嗅探嫌疑标记(双条件定性见下方 onExit)。 */
|
|
34
34
|
private commandNotFoundSeen;
|
|
35
35
|
constructor(opts: {
|
|
36
36
|
binary: string;
|
|
@@ -34,7 +34,7 @@ export class JsonlRpcTransport {
|
|
|
34
34
|
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
35
35
|
writeQueue = [];
|
|
36
36
|
flushing = false;
|
|
37
|
-
/**
|
|
37
|
+
/** win32 嗅探嫌疑标记(双条件定性见下方 onExit)。 */
|
|
38
38
|
commandNotFoundSeen = false;
|
|
39
39
|
constructor(opts) {
|
|
40
40
|
this.binary = opts.binary;
|
|
@@ -106,7 +106,7 @@ export class JsonlRpcTransport {
|
|
|
106
106
|
const text = chunk.toString('utf-8');
|
|
107
107
|
stderrBuf = (stderrBuf + text).slice(-MAX_STDERR_BYTES);
|
|
108
108
|
getLogger().debug(`[jsonrpc-transport] stderr: ${text.trimEnd()}`);
|
|
109
|
-
//
|
|
109
|
+
// win32 command-not-found 嗅探:命中只记嫌疑标记,不立即杀——
|
|
110
110
|
// agent 正常输出可能引用该错误文本(调试 Windows 报错场景),见行即杀
|
|
111
111
|
// 会误杀 run;定性由 onExit 的「标记 + 非零退出」双条件完成,真挂起由
|
|
112
112
|
// 上层超时兜底
|
|
@@ -169,7 +169,7 @@ export class JsonlRpcTransport {
|
|
|
169
169
|
// ignore trailing incomplete line
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
//
|
|
172
|
+
// 双条件定性:嫌疑标记 + 非零正常退出 → command-not-found。
|
|
173
173
|
// reason 仅供日志/诊断,消费方不解析该字符串(baseOnClose 无参)。
|
|
174
174
|
let reason = signal ? `signal:${signal}` : `exit:${code}`;
|
|
175
175
|
if (this.commandNotFoundSeen && signal === null && code !== null && code !== 0) {
|
|
@@ -84,7 +84,7 @@ export declare abstract class SpawningRunner {
|
|
|
84
84
|
*/
|
|
85
85
|
protected stoppedByUser: boolean;
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* win32 command-not-found 嗅探嫌疑标记:stderr 命中特征行时置位,
|
|
88
88
|
* 但**不**立即杀进程——agent 正常输出可能引用该错误文本(调试 Windows
|
|
89
89
|
* 报错场景),见行即杀会误杀 run。定性走双条件:子类收尾时「标记在 +
|
|
90
90
|
* 非零退出」才判 command-not-found;真挂起由既有 grace/超时兜底。
|
|
@@ -127,7 +127,7 @@ export class SpawningRunner {
|
|
|
127
127
|
*/
|
|
128
128
|
stoppedByUser = false;
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
130
|
+
* win32 command-not-found 嗅探嫌疑标记:stderr 命中特征行时置位,
|
|
131
131
|
* 但**不**立即杀进程——agent 正常输出可能引用该错误文本(调试 Windows
|
|
132
132
|
* 报错场景),见行即杀会误杀 run。定性走双条件:子类收尾时「标记在 +
|
|
133
133
|
* 非零退出」才判 command-not-found;真挂起由既有 grace/超时兜底。
|
|
@@ -253,7 +253,7 @@ export class SpawningRunner {
|
|
|
253
253
|
const text = chunk.toString('utf-8').trim();
|
|
254
254
|
if (text) {
|
|
255
255
|
this.spawnStderr = (this.spawnStderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
|
|
256
|
-
//
|
|
256
|
+
// win32 command-not-found 嗅探:经 cmd 垫片启动失败不是 spawn 的
|
|
257
257
|
// ENOENT,而是 stderr 的 "is not recognized..." 行。命中只记嫌疑标记
|
|
258
258
|
// (双条件之一),不立即杀——定性由子类收尾的「标记 + 非零退出」
|
|
259
259
|
// 双条件完成;子进程真挂起由既有 grace/超时看门狗兜底。
|
package/dist/runner/dsh/auth.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import YAML from 'yaml';
|
|
16
|
+
import { isWin32 } from '../../platform/select.js';
|
|
16
17
|
/** Credential record that owns the browser-cookie signing secret. */
|
|
17
18
|
export const DSH_BROWSER_CREDENTIAL_KEY = 'client-connection/browser-session';
|
|
18
19
|
/** Short lifetime keeps every request safely inside DSH's configurable max age. */
|
|
@@ -40,7 +41,7 @@ function assertOwnerOnly(file, stats) {
|
|
|
40
41
|
throw new Error(`DSH credentials file ${file} is not a regular file`);
|
|
41
42
|
}
|
|
42
43
|
// Windows has no POSIX mode bits; DSH itself skips the same check there.
|
|
43
|
-
if (
|
|
44
|
+
if (isWin32())
|
|
44
45
|
return;
|
|
45
46
|
if ((stats.mode & 0o077) !== 0) {
|
|
46
47
|
throw new Error(`DSH credentials file ${file} is readable beyond its owner ` +
|