lark-remote 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/dist/bridge/approval-coordinator.d.ts +30 -1
- package/dist/bridge/approval-coordinator.js +93 -5
- package/dist/bridge/index.d.ts +36 -6
- package/dist/bridge/index.js +56 -10
- package/dist/card/approval-render.js +287 -48
- package/dist/card/card-shared.d.ts +6 -4
- package/dist/card/card-shared.js +6 -4
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +22 -11
- package/dist/config/index.d.ts +24 -1
- package/dist/config/index.js +34 -3
- package/dist/index.js +40 -16
- package/dist/order/alias-resolve.d.ts +6 -2
- package/dist/order/index.d.ts +9 -2
- package/dist/order/index.js +33 -0
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/config/claude.js +8 -3
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/index.js +4 -1
- package/dist/router/config/types.d.ts +6 -0
- package/dist/router/index.d.ts +39 -4
- package/dist/router/index.js +471 -90
- package/dist/router/utils.js +12 -2
- package/dist/runner/agent-choices-common.js +3 -0
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +105 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
- package/dist/runner/codex/app-server/protocol-types.js +1 -1
- package/dist/runner/codex/app-server/runner.d.ts +10 -0
- package/dist/runner/codex/app-server/runner.js +56 -1
- package/dist/runner/codex/app-server/translator.d.ts +7 -0
- package/dist/runner/codex/app-server/translator.js +45 -0
- package/dist/runner/common/acp/protocol-types.d.ts +33 -0
- package/dist/runner/common/acp/protocol-types.js +2 -0
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +211 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +1 -0
- package/dist/runner/index.js +1 -0
- package/dist/runner/kimi/acp/runner.js +84 -1
- package/dist/runner/kimi/acp/translator.d.ts +14 -0
- package/dist/runner/kimi/acp/translator.js +108 -3
- package/dist/runner/pi/rpc/runner.d.ts +1 -7
- package/dist/runner/pi/rpc/runner.js +8 -12
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/types.d.ts +94 -10
- package/dist/session/claude/sessions.js +37 -15
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +44 -0
- package/dist/session/dsh/sessions.js +252 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/workspace/index.d.ts +12 -0
- package/dist/workspace/index.js +28 -2
- package/package.json +2 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
package/dist/runner/types.d.ts
CHANGED
|
@@ -25,11 +25,18 @@ export interface ApprovalRequestedEvent {
|
|
|
25
25
|
type: 'approval_requested';
|
|
26
26
|
/** JSON-RPC id of the server request. Wire type is string | integer — keep raw. */
|
|
27
27
|
requestId: number | string;
|
|
28
|
-
kind: 'command' | 'file' | 'permissions' | 'question';
|
|
28
|
+
kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
|
|
29
29
|
threadId: string;
|
|
30
30
|
turnId: string;
|
|
31
31
|
itemId: string;
|
|
32
32
|
view: ApprovalView;
|
|
33
|
+
/**
|
|
34
|
+
* Per-request 审批超时覆盖(毫秒)。由 runner 在翻译时填入
|
|
35
|
+
* (Codex autoResolutionMs),coordinator 优先使用;
|
|
36
|
+
* 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,
|
|
37
|
+
* 保证公共层无 agent 分支。
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
33
40
|
timestamp?: string;
|
|
34
41
|
}
|
|
35
42
|
export interface ApprovalResolvedEvent {
|
|
@@ -62,27 +69,74 @@ export type ApprovalAction = {
|
|
|
62
69
|
/** Claude:允许当前请求 + 本会话后续所有权限请求自动放行(允许所有)。 */
|
|
63
70
|
| {
|
|
64
71
|
action: 'accept_all';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 计划审批「拒绝并附修改意见」:deny + message,Claude 留在 plan 模式
|
|
75
|
+
* 按意见修订(等价 TUI「Tell Claude what to change」)。
|
|
76
|
+
*/
|
|
77
|
+
| {
|
|
78
|
+
action: 'decline_with_feedback';
|
|
79
|
+
message: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 计划审批「批准并采纳修改意见」:allow + updatedInput.plan(原计划追加
|
|
83
|
+
* 意见),触发 Claude 侧 planWasEdited → tool_result 回显
|
|
84
|
+
* "## Approved Plan (edited by user)"(等价 TUI shift+tab approve with feedback)。
|
|
85
|
+
*/
|
|
86
|
+
| {
|
|
87
|
+
action: 'accept_with_feedback';
|
|
88
|
+
plan: string;
|
|
65
89
|
} | {
|
|
66
90
|
action: 'decline';
|
|
67
91
|
} | {
|
|
68
92
|
action: 'cancel';
|
|
69
93
|
}
|
|
70
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* AskUserQuestion 答案(question → label 或 label[])。
|
|
96
|
+
* notes:Codex user_note 补充说明(question → 文本),runner 编码为
|
|
97
|
+
* "user_note: <text>" 条目;其他 agent 忽略该字段。
|
|
98
|
+
*/
|
|
71
99
|
| {
|
|
72
100
|
action: 'answer';
|
|
73
101
|
answers: Record<string, string | string[]>;
|
|
102
|
+
notes?: Record<string, string>;
|
|
74
103
|
};
|
|
75
|
-
/**
|
|
76
|
-
export interface
|
|
104
|
+
/** AskUserQuestion 公共契约(Agent 无关)的单个选项。 */
|
|
105
|
+
export interface UserQuestionOption {
|
|
77
106
|
label: string;
|
|
78
107
|
description?: string;
|
|
79
108
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
109
|
+
/**
|
|
110
|
+
* AskUserQuestion 公共契约(Agent 无关)的单个问题。
|
|
111
|
+
*
|
|
112
|
+
* 接入约定(新 agent 接入 AskUserQuestion 时在这里扩展,禁止在 coordinator /
|
|
113
|
+
* renderer / router 里写 agent 分支):
|
|
114
|
+
* 1. 各 agent runner 把协议提问翻译为 `ApprovalView{ kind:'question', questions }`;
|
|
115
|
+
* 2. 用户回答经 ApprovalCoordinator 收集,以 `ApprovalAction{action:'answer',
|
|
116
|
+
* answers:{问题文本: string|string[]}}` 回传 runner;
|
|
117
|
+
* 3. runner 侧把文本 key 答案翻译回自己协议的 shape(Claude 直接用文本 key;
|
|
118
|
+
* Codex 需按问题 id 映射)。
|
|
119
|
+
*/
|
|
120
|
+
export interface UserQuestion {
|
|
121
|
+
/** Agent 侧问题唯一标识(Codex request_user_input 的 id 预留;Claude 无此字段)。 */
|
|
122
|
+
id?: string;
|
|
82
123
|
question: string;
|
|
83
124
|
header?: string;
|
|
84
125
|
multiSelect?: boolean;
|
|
85
|
-
options:
|
|
126
|
+
options: UserQuestionOption[];
|
|
127
|
+
/** 是否允许自定义答案(Codex isOther 预留;当前单选渲染层恒提供 Other 输入)。 */
|
|
128
|
+
isOther?: boolean;
|
|
129
|
+
/** 敏感输入(Codex isSecret 预留)。 */
|
|
130
|
+
isSecret?: boolean;
|
|
131
|
+
/** 自由文本题的输入框占位提示(渲染层自由文本题 input)。 */
|
|
132
|
+
placeholder?: string;
|
|
133
|
+
/**
|
|
134
|
+
* 是否渲染「补充说明(可选)」输入(Codex user_note;数据驱动无 agent 分支)。
|
|
135
|
+
* note 是选项之外的附加文本,随答案提交,不单独构成答案。
|
|
136
|
+
*/
|
|
137
|
+
allowNote?: boolean;
|
|
138
|
+
/** 当前已填写的补充说明(卡片回显;coordinator answerNote 回流)。 */
|
|
139
|
+
note?: string;
|
|
86
140
|
/**
|
|
87
141
|
* 当前已选选项 label(卡片渲染与多选切换用;单选在协调器内即时提交)。
|
|
88
142
|
* 与 permissions.items.selected 同模式:协调器原地更新后以
|
|
@@ -92,10 +146,38 @@ export interface ClaudeUserQuestion {
|
|
|
92
146
|
}
|
|
93
147
|
export interface ApprovalView {
|
|
94
148
|
requestId: number | string;
|
|
95
|
-
kind: 'command' | 'file' | 'permissions' | 'question';
|
|
149
|
+
kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
|
|
96
150
|
reason?: string;
|
|
151
|
+
/**
|
|
152
|
+
* 提问卡顶部概要行(数据驱动,无 agent 分支):Kimi elicitation form 的
|
|
153
|
+
* 题面只在 form 级 message 里合并出现,逐字段 title 可能只是短 header,
|
|
154
|
+
* 概要行用于展示完整题干。
|
|
155
|
+
*/
|
|
156
|
+
intro?: string;
|
|
97
157
|
command?: string;
|
|
98
158
|
commandCwd?: string;
|
|
159
|
+
/**
|
|
160
|
+
* 被审批的工具名(kind === 'tool' 时展示,如 ExitPlanMode)。非命令工具的
|
|
161
|
+
* 权限请求(plan 退出/通知型工具)input 无 command 语义,用工具名 + reason
|
|
162
|
+
* 表达审批内容,避免落入 command 槽位显示无意义 `{}`。
|
|
163
|
+
*/
|
|
164
|
+
toolName?: string;
|
|
165
|
+
/**
|
|
166
|
+
* 计划全文(kind === 'tool' 且 toolName === 'ExitPlanMode' 时存在)。
|
|
167
|
+
* 来源:control_request input.plan(CLI normalizeToolInput 从磁盘注入,
|
|
168
|
+
* 非稳定)→ 计划文件读取(input.planFilePath / 会话内 Write/Edit 跟踪)。
|
|
169
|
+
*/
|
|
170
|
+
plan?: string;
|
|
171
|
+
/** 计划文件路径(ExitPlanMode;input.planFilePath 或会话内跟踪所得)。 */
|
|
172
|
+
planFilePath?: string;
|
|
173
|
+
/**
|
|
174
|
+
* 用户已填写的计划修改意见(ExitPlanMode 反馈输入框;coordinator
|
|
175
|
+
* planFeedback 回流,卡片回显)。供「拒绝并附意见」/「批准并采纳修改」
|
|
176
|
+
* 两个决策复用。
|
|
177
|
+
*/
|
|
178
|
+
planFeedback?: string;
|
|
179
|
+
/** 原始 input 的 JSON 字符串(kind === 'tool' 且 input 非空时展示)。 */
|
|
180
|
+
toolInput?: string;
|
|
99
181
|
fileChanges?: Array<{
|
|
100
182
|
path: string;
|
|
101
183
|
kind: 'add' | 'update' | 'delete';
|
|
@@ -115,7 +197,7 @@ export interface ApprovalView {
|
|
|
115
197
|
}>;
|
|
116
198
|
};
|
|
117
199
|
/** Claude AskUserQuestion 问题列表(kind === 'question' 时存在)。 */
|
|
118
|
-
questions?:
|
|
200
|
+
questions?: UserQuestion[];
|
|
119
201
|
availableDecisions: string[];
|
|
120
202
|
/** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
|
|
121
203
|
decisionPayloads?: Record<string, unknown>;
|
|
@@ -283,12 +365,14 @@ export interface Runner {
|
|
|
283
365
|
*/
|
|
284
366
|
unregisterExitHandlers?(): void;
|
|
285
367
|
}
|
|
286
|
-
export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi';
|
|
368
|
+
export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi' | 'dsh';
|
|
287
369
|
/** Unified session descriptor returned by every AgentSessionReader. */
|
|
288
370
|
export interface AgentSession {
|
|
289
371
|
sessionId: string;
|
|
290
372
|
summary: string;
|
|
291
373
|
mtime: number;
|
|
374
|
+
/** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
|
|
375
|
+
agentPreset?: string;
|
|
292
376
|
}
|
|
293
377
|
/** A single renderable event from session history. */
|
|
294
378
|
export interface AgentSessionContentEvent {
|
|
@@ -154,7 +154,7 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
|
|
|
154
154
|
// Explicit terminal markers — regardless of mtime. Claude CLI 2.x uses
|
|
155
155
|
// `last-prompt` / `mode` (no `result` event) to mark turn end, so a
|
|
156
156
|
// 24-minute-old session with a last-prompt tail is already done
|
|
157
|
-
// (regression 2026-06-21:
|
|
157
|
+
// (regression 2026-06-21: previously falsely reported as active).
|
|
158
158
|
const TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
|
|
159
159
|
if (typeof obj.type === 'string' && TERMINAL_TYPES.has(obj.type))
|
|
160
160
|
return false;
|
|
@@ -224,7 +224,16 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
224
224
|
*/
|
|
225
225
|
function scalarScan(filePath) {
|
|
226
226
|
const acc = new UsageAccumulator();
|
|
227
|
-
|
|
227
|
+
// Per-message-id best usage record (per-field max). A streaming assistant
|
|
228
|
+
// message spans multiple jsonl lines sharing the same message id; some
|
|
229
|
+
// backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
|
|
230
|
+
// the early placeholder lines (empty thinking block) and the real usage
|
|
231
|
+
// only on the final line carrying stop_reason. First-occurrence-wins dedup
|
|
232
|
+
// would pin the zero record and discard the real one (regression
|
|
233
|
+
// 2026-08-19: auto-resume card per-run stats all 0). Per-field max
|
|
234
|
+
// reconstructs the full usage regardless of line order — on the official
|
|
235
|
+
// API every line repeats identical usage, so max is a no-op there.
|
|
236
|
+
const usageByMessageId = new Map();
|
|
228
237
|
let lastPostTokens = 0;
|
|
229
238
|
let tailOffset = -1;
|
|
230
239
|
let lastAiTitle;
|
|
@@ -244,13 +253,13 @@ function scalarScan(filePath) {
|
|
|
244
253
|
const usage = msg?.usage;
|
|
245
254
|
if (usage) {
|
|
246
255
|
const msgId = msg?.id;
|
|
247
|
-
if (msgId
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
input: usage.input_tokens ?? 0,
|
|
251
|
-
output: usage.output_tokens ?? 0,
|
|
252
|
-
cacheRead: usage.cache_read_input_tokens ?? 0,
|
|
253
|
-
cacheCreation: usage.cache_creation_input_tokens ?? 0,
|
|
256
|
+
if (msgId) {
|
|
257
|
+
const prev = usageByMessageId.get(msgId);
|
|
258
|
+
usageByMessageId.set(msgId, {
|
|
259
|
+
input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
|
|
260
|
+
output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
|
|
261
|
+
cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
|
|
262
|
+
cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
|
|
254
263
|
});
|
|
255
264
|
}
|
|
256
265
|
}
|
|
@@ -305,6 +314,12 @@ function scalarScan(filePath) {
|
|
|
305
314
|
}
|
|
306
315
|
}
|
|
307
316
|
});
|
|
317
|
+
// Feed one best record per message id, in first-appearance order (Map
|
|
318
|
+
// preserves insertion order), so acc.last is the LAST message's full usage
|
|
319
|
+
// and totals sum each API response exactly once.
|
|
320
|
+
for (const record of usageByMessageId.values()) {
|
|
321
|
+
acc.add(record);
|
|
322
|
+
}
|
|
308
323
|
// Build usage result (same logic as old aggregateSessionUsage)
|
|
309
324
|
const t = acc.totals;
|
|
310
325
|
const l = acc.last;
|
|
@@ -319,14 +334,19 @@ function scalarScan(filePath) {
|
|
|
319
334
|
if (contextLength === 0 || lastWindow > contextLength) {
|
|
320
335
|
contextLength = lastWindow;
|
|
321
336
|
}
|
|
337
|
+
// 非累计字段 = 末轮(本 run)scope,累计字段 = session 总和(所有 run)。
|
|
338
|
+
// 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
|
|
339
|
+
// `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
|
|
340
|
+
// 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
|
|
341
|
+
const lastTotal = l ? l.input + l.output + l.cacheRead + l.cacheCreation : 0;
|
|
322
342
|
const usage = {
|
|
323
|
-
inputTokens:
|
|
324
|
-
outputTokens:
|
|
343
|
+
inputTokens: l ? l.input : 0,
|
|
344
|
+
outputTokens: l ? l.output : 0,
|
|
325
345
|
contextLength,
|
|
326
346
|
compactCount: acc.compactCount,
|
|
327
|
-
cacheReadTokens:
|
|
328
|
-
cacheCreationTokens:
|
|
329
|
-
totalTokens:
|
|
347
|
+
cacheReadTokens: l && l.cacheRead > 0 ? l.cacheRead : undefined,
|
|
348
|
+
cacheCreationTokens: l && l.cacheCreation > 0 ? l.cacheCreation : undefined,
|
|
349
|
+
totalTokens: lastTotal,
|
|
330
350
|
cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
331
351
|
cumulativeInputTokens: t.input,
|
|
332
352
|
cumulativeOutputTokens: t.output,
|
|
@@ -435,7 +455,9 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
435
455
|
// ~3×). Total parse count adds the index-miss fallback guard's fixed
|
|
436
456
|
// early-stop overhead (~2 parses, independent of line count).
|
|
437
457
|
const scan = scalarScan(filePath);
|
|
438
|
-
|
|
458
|
+
// 有 usage 即返回:非累计字段已是末轮 scope,命中判定用 session 累计兜底
|
|
459
|
+
// (末轮可能 input/output 为 0 而仍有 cacheRead,累计口径不会被末轮遮挡)。
|
|
460
|
+
const usage = (scan.usage.cumulativeTotalTokens ?? 0) > 0 ? scan.usage : undefined;
|
|
439
461
|
// Extract events AFTER the last user message by re-reading + re-parsing
|
|
440
462
|
// only the tail. If no user message was found (tailOffset === -1), fall
|
|
441
463
|
// back to re-reading the whole file so the card still has something to
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DshSessionReader } from './sessions.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
|
|
3
|
+
*
|
|
4
|
+
* The AgentSessionReader contract is fully synchronous (file-style readers),
|
|
5
|
+
* but DSH exposes only async HTTP. The reader therefore bridges with
|
|
6
|
+
* `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
|
|
7
|
+
* — a synchronous HTTP round-trip to the local DSH host. This keeps the
|
|
8
|
+
* router/bridge interface contract intact without touching any upstream logic.
|
|
9
|
+
*
|
|
10
|
+
* listSessions: session.list → filter by cwd → sort by updatedAt desc → local
|
|
11
|
+
* pagination (DSH v1 has no cursor pagination).
|
|
12
|
+
* readSessionContent: session.history → map SessionEvents to display events +
|
|
13
|
+
* session-wide usage accumulation (assistant/message.usage).
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
16
|
+
interface DshSessionReaderOptions {
|
|
17
|
+
host?: string;
|
|
18
|
+
/** 每次调用时解析当前 host(/config 修改 host 后 runner 重建、reader 也跟随新 host)。
|
|
19
|
+
* 优先于固定的 `host`。 */
|
|
20
|
+
hostProvider?: () => string | undefined;
|
|
21
|
+
/** Injectable synchronous unary transport. Defaults to spawnSync curl. */
|
|
22
|
+
syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
|
|
23
|
+
}
|
|
24
|
+
export declare class DshSessionReader implements AgentSessionReader {
|
|
25
|
+
private readonly hostProvider;
|
|
26
|
+
private readonly syncRequest;
|
|
27
|
+
constructor(opts?: DshSessionReaderOptions);
|
|
28
|
+
/** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
|
|
29
|
+
private resolveBaseUrl;
|
|
30
|
+
private unary;
|
|
31
|
+
listSessions(cwd: string, opts?: {
|
|
32
|
+
limit?: number;
|
|
33
|
+
offset?: number;
|
|
34
|
+
}): {
|
|
35
|
+
sessions: AgentSession[];
|
|
36
|
+
total: number;
|
|
37
|
+
};
|
|
38
|
+
getNewestSession(cwd: string): AgentSession | null;
|
|
39
|
+
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
40
|
+
maxEvents?: number;
|
|
41
|
+
}): SessionContent;
|
|
42
|
+
isSessionActive(sessionId: string, cwd: string): boolean;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
|
|
3
|
+
*
|
|
4
|
+
* The AgentSessionReader contract is fully synchronous (file-style readers),
|
|
5
|
+
* but DSH exposes only async HTTP. The reader therefore bridges with
|
|
6
|
+
* `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
|
|
7
|
+
* — a synchronous HTTP round-trip to the local DSH host. This keeps the
|
|
8
|
+
* router/bridge interface contract intact without touching any upstream logic.
|
|
9
|
+
*
|
|
10
|
+
* listSessions: session.list → filter by cwd → sort by updatedAt desc → local
|
|
11
|
+
* pagination (DSH v1 has no cursor pagination).
|
|
12
|
+
* readSessionContent: session.history → map SessionEvents to display events +
|
|
13
|
+
* session-wide usage accumulation (assistant/message.usage).
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { getLogger } from '../../logger/index.js';
|
|
17
|
+
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
18
|
+
import { capEvents, paginate } from '../common/pagination.js';
|
|
19
|
+
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
20
|
+
/**
|
|
21
|
+
* CC-05: history 拉取上限。DSH session.history 的 maxMessages 同时限制返回条数,
|
|
22
|
+
* 若用它同时充当展示截断,>50 事件的会话累计 usage 会少算。这里拉取一个足够大的
|
|
23
|
+
* 上限保证 usage 累加覆盖整个会话;展示仍由 capEvents(opts?.maxEvents ?? 50) 截断。
|
|
24
|
+
* (若 DSH 支持分页 cursor 可改成分页,当前用有界大拉取,见 review-fix-plan-cc.md)
|
|
25
|
+
*/
|
|
26
|
+
const DSH_HISTORY_FETCH_LIMIT = 10_000;
|
|
27
|
+
/** Synchronous unary POST via curl (the reader interface is synchronous). */
|
|
28
|
+
function defaultSyncRequest(baseUrl, method, payload) {
|
|
29
|
+
const body = JSON.stringify({
|
|
30
|
+
type: 'client-request',
|
|
31
|
+
rpcId: `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
32
|
+
method,
|
|
33
|
+
payload,
|
|
34
|
+
});
|
|
35
|
+
const res = spawnSync('curl', [
|
|
36
|
+
'-s',
|
|
37
|
+
'-X',
|
|
38
|
+
'POST',
|
|
39
|
+
`${baseUrl}/api/${method}`,
|
|
40
|
+
'-H',
|
|
41
|
+
'content-type: application/json',
|
|
42
|
+
'--data',
|
|
43
|
+
body,
|
|
44
|
+
], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
|
|
45
|
+
if (res.error || res.status !== 0) {
|
|
46
|
+
throw new Error(`DSH ${method} failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
|
|
47
|
+
}
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(res.stdout);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
|
|
54
|
+
cause: err,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (parsed.result?.ok !== true) {
|
|
58
|
+
throw new Error(parsed.result?.error?.message ?? `DSH ${method} failed`);
|
|
59
|
+
}
|
|
60
|
+
return parsed.result.value;
|
|
61
|
+
}
|
|
62
|
+
export class DshSessionReader {
|
|
63
|
+
hostProvider;
|
|
64
|
+
syncRequest;
|
|
65
|
+
constructor(opts = {}) {
|
|
66
|
+
const fixed = (opts.host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
|
|
67
|
+
this.hostProvider = opts.hostProvider ?? (() => fixed);
|
|
68
|
+
this.syncRequest = opts.syncRequest ?? defaultSyncRequest;
|
|
69
|
+
}
|
|
70
|
+
/** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
|
|
71
|
+
resolveBaseUrl() {
|
|
72
|
+
const host = this.hostProvider();
|
|
73
|
+
return (host && host.length > 0 ? host : DEFAULT_DSH_HOST).replace(/\/$/, '');
|
|
74
|
+
}
|
|
75
|
+
unary(method, payload) {
|
|
76
|
+
return this.syncRequest(this.resolveBaseUrl(), method, payload);
|
|
77
|
+
}
|
|
78
|
+
listSessions(cwd, opts) {
|
|
79
|
+
try {
|
|
80
|
+
const value = this.unary('session.list', {});
|
|
81
|
+
const items = value.items ?? [];
|
|
82
|
+
const filtered = items
|
|
83
|
+
.filter((i) => i.cwd === cwd)
|
|
84
|
+
.sort((a, b) => b.updatedAt - a.updatedAt || String(a.sessionId).localeCompare(String(b.sessionId)))
|
|
85
|
+
.map((i) => ({
|
|
86
|
+
sessionId: String(i.sessionId),
|
|
87
|
+
summary: '',
|
|
88
|
+
mtime: i.updatedAt,
|
|
89
|
+
...(i.agentPreset ? { agentPreset: i.agentPreset } : {}),
|
|
90
|
+
}));
|
|
91
|
+
const { items: page, total } = paginate(filtered, opts ?? {});
|
|
92
|
+
return { sessions: page, total };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
getLogger().error(`[dsh-session-reader] listSessions error: ${error.message}`);
|
|
96
|
+
return { sessions: [], total: 0 };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getNewestSession(cwd) {
|
|
100
|
+
return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
|
|
101
|
+
}
|
|
102
|
+
readSessionContent(sessionId, cwd, opts) {
|
|
103
|
+
try {
|
|
104
|
+
const value = this.unary('session.history', {
|
|
105
|
+
sessionId,
|
|
106
|
+
maxMessages: DSH_HISTORY_FETCH_LIMIT,
|
|
107
|
+
});
|
|
108
|
+
const events = [];
|
|
109
|
+
let displayTitle;
|
|
110
|
+
let cumInput = 0;
|
|
111
|
+
let cumOutput = 0;
|
|
112
|
+
let cumCacheRead = 0;
|
|
113
|
+
let cumCacheWrite = 0;
|
|
114
|
+
let lastUsage;
|
|
115
|
+
for (const entry of value.events ?? []) {
|
|
116
|
+
const ev = entry.event;
|
|
117
|
+
if (!ev)
|
|
118
|
+
continue;
|
|
119
|
+
const contentEvents = mapEvent(ev);
|
|
120
|
+
events.push(...contentEvents.events);
|
|
121
|
+
if (contentEvents.title)
|
|
122
|
+
displayTitle = contentEvents.title;
|
|
123
|
+
const u = contentEvents.usage;
|
|
124
|
+
if (u) {
|
|
125
|
+
// Cumulative (session-wide): sum across ALL assistant/message usage.
|
|
126
|
+
cumInput += u.inputTokens ?? 0;
|
|
127
|
+
cumOutput += u.outputTokens ?? 0;
|
|
128
|
+
cumCacheRead += u.cacheReadTokens ?? 0;
|
|
129
|
+
cumCacheWrite += u.cacheWriteTokens ?? 0;
|
|
130
|
+
// Per-turn "current window" fields reflect the LAST assistant/message
|
|
131
|
+
// with usage (ccusage/`last_token_usage` 口径, AGENTS.md §9.21) — a
|
|
132
|
+
// multi-turn session's current context water-mark is the last turn's,
|
|
133
|
+
// not the accumulated total.
|
|
134
|
+
lastUsage = u;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let sessionUsage;
|
|
138
|
+
if (lastUsage) {
|
|
139
|
+
const input = lastUsage.inputTokens ?? 0;
|
|
140
|
+
const output = lastUsage.outputTokens ?? 0;
|
|
141
|
+
const cacheRead = lastUsage.cacheReadTokens ?? 0;
|
|
142
|
+
const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
|
|
143
|
+
sessionUsage = {
|
|
144
|
+
inputTokens: input,
|
|
145
|
+
outputTokens: output,
|
|
146
|
+
contextLength: input + cacheRead + cacheWrite,
|
|
147
|
+
cacheReadTokens: cacheRead,
|
|
148
|
+
cacheCreationTokens: cacheWrite,
|
|
149
|
+
totalTokens: input + output + cacheRead + cacheWrite,
|
|
150
|
+
cumulativeTotalTokens: cumInput + cumOutput + cumCacheRead + cumCacheWrite,
|
|
151
|
+
cumulativeInputTokens: cumInput,
|
|
152
|
+
cumulativeOutputTokens: cumOutput,
|
|
153
|
+
cumulativeCacheReadTokens: cumCacheRead,
|
|
154
|
+
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
// CC-05: 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
|
|
159
|
+
// 完整 history 累加。
|
|
160
|
+
events: capEvents(events, opts?.maxEvents ?? 50),
|
|
161
|
+
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
162
|
+
...(displayTitle ? { displayTitle } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
getLogger().error(`[dsh-session-reader] readSessionContent error: ${error.message}`);
|
|
167
|
+
return { events: [] };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
isSessionActive(sessionId, cwd) {
|
|
171
|
+
try {
|
|
172
|
+
const value = this.unary('session.list', {});
|
|
173
|
+
const found = (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd === cwd);
|
|
174
|
+
return found?.running === true;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
getLogger().error(`[dsh-session-reader] isSessionActive error: ${error.message}`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Map one DSH SessionEvent to display content events (+ title/usage side effects). */
|
|
183
|
+
function mapEvent(ev) {
|
|
184
|
+
const out = { events: [] };
|
|
185
|
+
switch (ev.type) {
|
|
186
|
+
case 'user/message': {
|
|
187
|
+
const content = ev.data.content ?? [];
|
|
188
|
+
const text = content
|
|
189
|
+
.filter((c) => c.type === 'text')
|
|
190
|
+
.map((c) => c.text ?? '')
|
|
191
|
+
.join('');
|
|
192
|
+
out.events.push({ type: 'user', content: text, timestamp: new Date(ev.time).toISOString() });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'assistant/message': {
|
|
196
|
+
const content = ev.data.message
|
|
197
|
+
?.content ?? [];
|
|
198
|
+
const text = content
|
|
199
|
+
.filter((c) => c.type === 'text')
|
|
200
|
+
.map((c) => c.text ?? '')
|
|
201
|
+
.join('');
|
|
202
|
+
if (text)
|
|
203
|
+
out.events.push({
|
|
204
|
+
type: 'assistant',
|
|
205
|
+
content: text,
|
|
206
|
+
timestamp: new Date(ev.time).toISOString(),
|
|
207
|
+
});
|
|
208
|
+
const usage = ev.data.usage;
|
|
209
|
+
if (usage && typeof usage === 'object')
|
|
210
|
+
out.usage = usage;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'tool/call': {
|
|
214
|
+
const name = String(ev.data.name ?? '');
|
|
215
|
+
const args = typeof ev.data.arguments === 'string' ? ev.data.arguments : '';
|
|
216
|
+
out.events.push({
|
|
217
|
+
type: 'tool_use',
|
|
218
|
+
content: `${name}(${truncateUtf8(args, 200)})`,
|
|
219
|
+
timestamp: new Date(ev.time).toISOString(),
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'tool/result': {
|
|
224
|
+
const msg = ev.data.message;
|
|
225
|
+
const raw = stringifyContent(msg?.content);
|
|
226
|
+
out.events.push({
|
|
227
|
+
type: 'tool_result',
|
|
228
|
+
content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
|
|
229
|
+
timestamp: new Date(ev.time).toISOString(),
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function stringifyContent(content) {
|
|
239
|
+
if (content == null)
|
|
240
|
+
return '[tool result]';
|
|
241
|
+
if (typeof content === 'string')
|
|
242
|
+
return content;
|
|
243
|
+
if (Array.isArray(content)) {
|
|
244
|
+
return content
|
|
245
|
+
.map((c) => c && typeof c === 'object' && 'text' in c
|
|
246
|
+
? String(c.text)
|
|
247
|
+
: JSON.stringify(c))
|
|
248
|
+
.join('');
|
|
249
|
+
}
|
|
250
|
+
return JSON.stringify(content);
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=sessions.js.map
|
package/dist/session/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { CodexSessionReader } from './codex/index.js';
|
|
|
5
5
|
export { OpencodeSessionReader } from './opencode/index.js';
|
|
6
6
|
export { PiSessionReader } from './pi/index.js';
|
|
7
7
|
export { KimiSessionReader } from './kimi/index.js';
|
|
8
|
+
export { DshSessionReader } from './dsh/index.js';
|
package/dist/session/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { CodexSessionReader } from './codex/index.js';
|
|
|
8
8
|
export { OpencodeSessionReader } from './opencode/index.js';
|
|
9
9
|
export { PiSessionReader } from './pi/index.js';
|
|
10
10
|
export { KimiSessionReader } from './kimi/index.js';
|
|
11
|
+
export { DshSessionReader } from './dsh/index.js';
|
|
11
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,6 +11,12 @@ export interface WorkspaceEntry {
|
|
|
11
11
|
* mid-write cannot leave a truncated JSON file. On load, a corrupt file
|
|
12
12
|
* is treated as empty (with a warning) so the bridge can still start.
|
|
13
13
|
*
|
|
14
|
+
* lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
|
|
15
|
+
* 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
|
|
16
|
+
* 对新保存的 workspace 同样生效(新条目应排在列表最前,而非沉底)。
|
|
17
|
+
* 同一毫秒内连续操作时 touch 会用 max+1 保证严格递增,避免 Date.now 相同
|
|
18
|
+
* 导致最近排序退化为字母序。
|
|
19
|
+
*
|
|
14
20
|
* Data migration: old format `{ alias: "path" }` is normalized to
|
|
15
21
|
* `{ alias: { path, lastUsedAt: 0 } }` on first load and persisted.
|
|
16
22
|
*/
|
|
@@ -27,6 +33,12 @@ export declare class WorkspaceStore {
|
|
|
27
33
|
/**
|
|
28
34
|
* Update lastUsedAt to now for the given alias and persist.
|
|
29
35
|
* No-op if the alias does not exist.
|
|
36
|
+
*
|
|
37
|
+
* To keep "recent" ordering stable even for rapid successive saves/uses in
|
|
38
|
+
* the same millisecond, never allow another entry to share the same
|
|
39
|
+
* timestamp while this entry is being touched. If a peer already has a
|
|
40
|
+
* timestamp at or after the current wall-clock time, use max+1 so the
|
|
41
|
+
* touched entry always becomes the most recently used.
|
|
30
42
|
*/
|
|
31
43
|
touch(name: string): void;
|
|
32
44
|
/**
|
package/dist/workspace/index.js
CHANGED
|
@@ -10,6 +10,12 @@ import { getLogger } from '../logger/index.js';
|
|
|
10
10
|
* mid-write cannot leave a truncated JSON file. On load, a corrupt file
|
|
11
11
|
* is treated as empty (with a warning) so the bridge can still start.
|
|
12
12
|
*
|
|
13
|
+
* lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
|
|
14
|
+
* 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
|
|
15
|
+
* 对新保存的 workspace 同样生效(新条目应排在列表最前,而非沉底)。
|
|
16
|
+
* 同一毫秒内连续操作时 touch 会用 max+1 保证严格递增,避免 Date.now 相同
|
|
17
|
+
* 导致最近排序退化为字母序。
|
|
18
|
+
*
|
|
13
19
|
* Data migration: old format `{ alias: "path" }` is normalized to
|
|
14
20
|
* `{ alias: { path, lastUsedAt: 0 } }` on first load and persisted.
|
|
15
21
|
*/
|
|
@@ -60,8 +66,11 @@ export class WorkspaceStore {
|
|
|
60
66
|
atomicWriteJson(this.filePath, obj);
|
|
61
67
|
}
|
|
62
68
|
save(name, dirPath) {
|
|
69
|
+
// 保存即视为使用(语义对齐):新建/覆盖的 workspace 都应拿到当前
|
|
70
|
+
// lastUsedAt,而不是 0,否则「最近使用」排序下新条目会沉底。
|
|
71
|
+
// touch() 负责统一写入时间戳并 persist,消除 save 写 0 / use 写 now 的不对称。
|
|
63
72
|
this.data.set(name, { path: dirPath, lastUsedAt: 0 });
|
|
64
|
-
this.
|
|
73
|
+
this.touch(name);
|
|
65
74
|
}
|
|
66
75
|
get(name) {
|
|
67
76
|
return this.data.get(name)?.path;
|
|
@@ -76,12 +85,29 @@ export class WorkspaceStore {
|
|
|
76
85
|
/**
|
|
77
86
|
* Update lastUsedAt to now for the given alias and persist.
|
|
78
87
|
* No-op if the alias does not exist.
|
|
88
|
+
*
|
|
89
|
+
* To keep "recent" ordering stable even for rapid successive saves/uses in
|
|
90
|
+
* the same millisecond, never allow another entry to share the same
|
|
91
|
+
* timestamp while this entry is being touched. If a peer already has a
|
|
92
|
+
* timestamp at or after the current wall-clock time, use max+1 so the
|
|
93
|
+
* touched entry always becomes the most recently used.
|
|
79
94
|
*/
|
|
80
95
|
touch(name) {
|
|
81
96
|
const entry = this.data.get(name);
|
|
82
97
|
if (!entry)
|
|
83
98
|
return;
|
|
84
|
-
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
let max = Math.max(now, entry.lastUsedAt);
|
|
101
|
+
let hasPeerAtOrAfterNow = false;
|
|
102
|
+
for (const [otherName, other] of this.data) {
|
|
103
|
+
if (otherName === name)
|
|
104
|
+
continue;
|
|
105
|
+
if (other.lastUsedAt > max)
|
|
106
|
+
max = other.lastUsedAt;
|
|
107
|
+
if (other.lastUsedAt >= now)
|
|
108
|
+
hasPeerAtOrAfterNow = true;
|
|
109
|
+
}
|
|
110
|
+
entry.lastUsedAt = hasPeerAtOrAfterNow ? max + 1 : max;
|
|
85
111
|
this.persist();
|
|
86
112
|
}
|
|
87
113
|
/**
|