lark-remote 0.1.8 → 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 +18 -0
- package/dist/bridge/approval-coordinator.d.ts +15 -0
- package/dist/bridge/approval-coordinator.js +58 -3
- package/dist/bridge/index.d.ts +17 -0
- package/dist/bridge/index.js +31 -2
- package/dist/card/approval-render.js +105 -8
- package/dist/index.js +13 -3
- 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/index.d.ts +19 -3
- package/dist/router/index.js +359 -84
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +90 -13
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/runner.js +94 -74
- package/dist/runner/kimi/acp/runner.js +15 -0
- package/dist/runner/pi/rpc/runner.js +6 -1
- package/dist/runner/types.d.ts +31 -0
- package/dist/session/dsh/sessions.d.ts +6 -1
- package/dist/session/dsh/sessions.js +20 -5
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.js +20 -1
- package/package.json +1 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
|
@@ -46,6 +46,11 @@ export interface ClaudeSessionOptions {
|
|
|
46
46
|
export interface PermissionResult {
|
|
47
47
|
behavior: 'allow' | 'deny';
|
|
48
48
|
message?: string;
|
|
49
|
+
/**
|
|
50
|
+
* allow 时显式覆盖 updatedInput(默认回写 pendingToolInputs 原始 input)。
|
|
51
|
+
* 计划审批「批准并采纳修改意见」用它携带改写的 plan。
|
|
52
|
+
*/
|
|
53
|
+
updatedInput?: unknown;
|
|
49
54
|
}
|
|
50
55
|
/**
|
|
51
56
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
@@ -74,6 +79,12 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
74
79
|
private waitResolve;
|
|
75
80
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
76
81
|
private pendingToolInputs;
|
|
82
|
+
/**
|
|
83
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
84
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
85
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
86
|
+
*/
|
|
87
|
+
private planFilePath;
|
|
77
88
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
78
89
|
private autoApprove;
|
|
79
90
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -160,6 +171,21 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
160
171
|
*/
|
|
161
172
|
private writeStdin;
|
|
162
173
|
private translateEvent;
|
|
174
|
+
/**
|
|
175
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
176
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
177
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
178
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
179
|
+
*/
|
|
180
|
+
private trackPlanFilePath;
|
|
181
|
+
/**
|
|
182
|
+
* ExitPlanMode 计划解析,优先级:
|
|
183
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
184
|
+
* 2. input.planFilePath 指向的文件;
|
|
185
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
186
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
187
|
+
*/
|
|
188
|
+
private resolvePlanContent;
|
|
163
189
|
private translateControlRequest;
|
|
164
190
|
/** 统一方案:所有 runner 自己生成 timestamp(缺失时补)。 */
|
|
165
191
|
private withTimestamp;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
|
|
28
28
|
* 时经 ProcessStopper 组杀,下条消息按 SessionStore 的 sessionId --resume。
|
|
29
29
|
*/
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
30
31
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
31
32
|
import { getLogger } from '../../logger/index.js';
|
|
32
33
|
import { SpawningRunner } from '../common/spawning-runner.js';
|
|
@@ -35,6 +36,8 @@ import { authErrorEvent, syntheticInitEvent } from '../common/runner-utils.js';
|
|
|
35
36
|
import { makeQuestionApprovalEvent } from '../question-common.js';
|
|
36
37
|
/** 拒绝权限时的默认提示(进入 claude 上下文,中文用户可读)。 */
|
|
37
38
|
const DENY_MESSAGE = '用户拒绝了此工具调用,请停止并等待用户指令。';
|
|
39
|
+
/** ExitPlanMode 计划文件读取上限(超出截断,防超大计划拖垮事件翻译)。 */
|
|
40
|
+
const MAX_PLAN_CHARS = 60_000;
|
|
38
41
|
/** 会话级空闲回收默认 TTL(对齐 codex appServer.idleTtlMs 默认 30 分钟)。 */
|
|
39
42
|
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
40
43
|
/** result 事件里表示「turn 中途压缩」的 subtype(不是 turn 结束)。 */
|
|
@@ -66,6 +69,12 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
66
69
|
waitResolve = null;
|
|
67
70
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
68
71
|
pendingToolInputs = new Map();
|
|
72
|
+
/**
|
|
73
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
74
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
75
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
76
|
+
*/
|
|
77
|
+
planFilePath = '';
|
|
69
78
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
70
79
|
autoApprove = false;
|
|
71
80
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -182,7 +191,9 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
182
191
|
if (result.behavior === 'allow') {
|
|
183
192
|
await this.writeControlResponse(requestId, {
|
|
184
193
|
behavior: 'allow',
|
|
185
|
-
updatedInput
|
|
194
|
+
// 显式 updatedInput 优先(计划审批「批准并采纳修改」携带改写 plan);
|
|
195
|
+
// 缺省回写原始 input,保证其他工具审批行为不变。
|
|
196
|
+
updatedInput: result.updatedInput !== undefined ? result.updatedInput : (input ?? {}),
|
|
186
197
|
});
|
|
187
198
|
}
|
|
188
199
|
else {
|
|
@@ -344,7 +355,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
344
355
|
for await (const raw of stream) {
|
|
345
356
|
if (generation !== this.processGeneration)
|
|
346
357
|
return; // 旧进程残留事件丢弃
|
|
347
|
-
const events = this.translateEvent(raw);
|
|
358
|
+
const events = await this.translateEvent(raw);
|
|
348
359
|
if (events.length > 0) {
|
|
349
360
|
this.eventQueue.push(...events);
|
|
350
361
|
this.wakeWaiters();
|
|
@@ -530,7 +541,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
530
541
|
// =========================================================================
|
|
531
542
|
// Internal: event translation
|
|
532
543
|
// =========================================================================
|
|
533
|
-
translateEvent(raw) {
|
|
544
|
+
async translateEvent(raw) {
|
|
534
545
|
const event = raw;
|
|
535
546
|
const type = event.type;
|
|
536
547
|
// review P3-3:--resume 会先重放上一轮历史事件(旧 result 之外还可能有
|
|
@@ -538,6 +549,10 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
538
549
|
// 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
|
|
539
550
|
if (type !== 'system' && !this.sawInit)
|
|
540
551
|
return [];
|
|
552
|
+
// 跟踪会话内计划文件写入(ExitPlanMode 审批卡的 plan 文件兜底)。
|
|
553
|
+
if (type === 'assistant') {
|
|
554
|
+
this.trackPlanFilePath(event);
|
|
555
|
+
}
|
|
541
556
|
if (type === 'control_request') {
|
|
542
557
|
return this.translateControlRequest(event);
|
|
543
558
|
}
|
|
@@ -588,7 +603,63 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
588
603
|
// 下游 reducer 忽略,保留诊断价值。
|
|
589
604
|
return [this.withTimestamp(event)];
|
|
590
605
|
}
|
|
591
|
-
|
|
606
|
+
/**
|
|
607
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
608
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
609
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
610
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
611
|
+
*/
|
|
612
|
+
trackPlanFilePath(raw) {
|
|
613
|
+
const content = raw.message?.content;
|
|
614
|
+
if (!Array.isArray(content))
|
|
615
|
+
return;
|
|
616
|
+
for (const block of content) {
|
|
617
|
+
const b = block;
|
|
618
|
+
if (b?.type !== 'tool_use')
|
|
619
|
+
continue;
|
|
620
|
+
if (b.name !== 'Write' && b.name !== 'Edit')
|
|
621
|
+
continue;
|
|
622
|
+
const fp = b.input?.file_path;
|
|
623
|
+
if (typeof fp === 'string' && fp.includes('/plans/')) {
|
|
624
|
+
this.planFilePath = fp;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* ExitPlanMode 计划解析,优先级:
|
|
630
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
631
|
+
* 2. input.planFilePath 指向的文件;
|
|
632
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
633
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
634
|
+
*/
|
|
635
|
+
async resolvePlanContent(input, trackedPath) {
|
|
636
|
+
const direct = typeof input.plan === 'string' && input.plan.trim() ? input.plan : undefined;
|
|
637
|
+
if (direct !== undefined) {
|
|
638
|
+
// 计划已随 input 注入时仍携带文件路径(input.planFilePath 或会话内跟踪),
|
|
639
|
+
// 供卡片展示「计划落盘位置」。
|
|
640
|
+
const filePath = typeof input.planFilePath === 'string' && input.planFilePath
|
|
641
|
+
? input.planFilePath
|
|
642
|
+
: trackedPath;
|
|
643
|
+
return filePath ? { plan: direct, planFilePath: filePath } : { plan: direct };
|
|
644
|
+
}
|
|
645
|
+
const candidate = typeof input.planFilePath === 'string' && input.planFilePath
|
|
646
|
+
? input.planFilePath
|
|
647
|
+
: trackedPath;
|
|
648
|
+
if (!candidate)
|
|
649
|
+
return {};
|
|
650
|
+
try {
|
|
651
|
+
const content = await readFile(candidate, 'utf8');
|
|
652
|
+
const trimmed = content.trim();
|
|
653
|
+
if (trimmed) {
|
|
654
|
+
return { plan: trimmed.slice(0, MAX_PLAN_CHARS), planFilePath: candidate };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// 计划文件缺失/不可读:fall through,渲染层兜底。
|
|
659
|
+
}
|
|
660
|
+
return {};
|
|
661
|
+
}
|
|
662
|
+
async translateControlRequest(raw) {
|
|
592
663
|
const requestId = raw.request_id;
|
|
593
664
|
if (requestId === null || requestId === undefined) {
|
|
594
665
|
getLogger().warn(`[${this.logTag}] control_request missing request_id, dropping`);
|
|
@@ -642,21 +713,27 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
642
713
|
}
|
|
643
714
|
return [makeQuestionApprovalEvent(requestId, questions, this.sessionId)];
|
|
644
715
|
}
|
|
645
|
-
// ExitPlanMode
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
// 标题/工具名 + reason 表达审批内容。
|
|
716
|
+
// ExitPlanMode:计划已写入计划文件(或经 input.plan 注入)。没有 command
|
|
717
|
+
// 语义,塞进 command 槽位会显示无意义 `{}` → 用独立 tool kind,把计划
|
|
718
|
+
// 全文 + 文件路径带给渲染层,决策对齐 TUI 四种交互。
|
|
649
719
|
if (toolName === 'ExitPlanMode') {
|
|
650
|
-
const
|
|
720
|
+
const resolved = await this.resolvePlanContent(input, this.planFilePath);
|
|
651
721
|
const view = {
|
|
652
722
|
requestId,
|
|
653
723
|
kind: 'tool',
|
|
654
724
|
toolName,
|
|
655
725
|
reason: typeof request.description === 'string' ? request.description : undefined,
|
|
656
|
-
|
|
657
|
-
...(
|
|
658
|
-
//
|
|
659
|
-
|
|
726
|
+
...(resolved.plan !== undefined ? { plan: resolved.plan } : {}),
|
|
727
|
+
...(resolved.planFilePath !== undefined ? { planFilePath: resolved.planFilePath } : {}),
|
|
728
|
+
// 对齐 TUI:批准并执行 / 批准并自动放行(acceptAll)/ 拒绝并附意见 /
|
|
729
|
+
// 批准并采纳修改 / 拒绝。
|
|
730
|
+
availableDecisions: [
|
|
731
|
+
'accept',
|
|
732
|
+
'acceptAll',
|
|
733
|
+
'declineWithFeedback',
|
|
734
|
+
'acceptWithFeedback',
|
|
735
|
+
'decline',
|
|
736
|
+
],
|
|
660
737
|
};
|
|
661
738
|
getLogger().info(`[${this.logTag}] permission request requestId=${requestId} tool=${toolName}`);
|
|
662
739
|
return [
|
|
@@ -104,18 +104,23 @@ export class ConnectionBasedRunner {
|
|
|
104
104
|
* unblock; otherwise cancel now via cancelCurrentTurn().
|
|
105
105
|
*/
|
|
106
106
|
async stop(_opts) {
|
|
107
|
-
if (!this._isRunning
|
|
107
|
+
if (!this._isRunning)
|
|
108
108
|
return;
|
|
109
|
+
// CC-01: 启动阶段(client/session 未建立)也必须记录停止意图,否则 /stop 被吞、
|
|
110
|
+
// turn 照常执行。executeTurn 在 setup 完成后检查 stopRequested 补发 cancel。
|
|
111
|
+
this.stopRequested = true;
|
|
112
|
+
this.forceFinish = true;
|
|
113
|
+
this.wakeWaiters();
|
|
114
|
+
if (!this.currentClient || !this.currentSessionId()) {
|
|
115
|
+
getLogger().warn(`[${this.logTag}] stop during setup (client/session not ready); will cancel after setup`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
if (this.shouldDeferStop()) {
|
|
110
119
|
// turn/start 或 prompt 尚未响应:先标记,等 run() 完成后再补发 cancel。
|
|
111
|
-
this.stopRequested = true;
|
|
112
|
-
this.forceFinish = true;
|
|
113
|
-
this.wakeWaiters();
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
// turn/start 或 prompt 已 resolved:立即 cancel;同时置 stopRequested 抑制
|
|
117
123
|
// 尚未结算的 prompt 结果翻译(opencode 语义),对已结算方无害。
|
|
118
|
-
this.stopRequested = true;
|
|
119
124
|
try {
|
|
120
125
|
await this.cancelCurrentTurn();
|
|
121
126
|
}
|
|
@@ -51,99 +51,119 @@ export class DshRunner {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
async *run(message, opts) {
|
|
54
|
+
if (this.running) {
|
|
55
|
+
throw new Error('DshRunner is already running');
|
|
56
|
+
}
|
|
54
57
|
const abort = new AbortController();
|
|
55
58
|
this.activeAbort = abort;
|
|
56
59
|
this.running = true;
|
|
57
60
|
let sessionId = opts.sessionId;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 模型/推理强度变更 = 保留 session,每次 run 前对齐(§4.3):
|
|
67
|
-
// 该调用同时会写服务端全局默认;失败仅告警不阻断 run(模型校验由 DSH 负责)。
|
|
68
|
-
if (this.configuredModel) {
|
|
69
|
-
try {
|
|
70
|
-
await this.client.selectModel({
|
|
71
|
-
sessionId,
|
|
72
|
-
provider: DSH_MODEL_PROVIDER,
|
|
73
|
-
model: this.configuredModel,
|
|
74
|
-
...(this.reasoningEffort ? { reasoningEffort: this.reasoningEffort } : {}),
|
|
61
|
+
let terminalEmitted = false;
|
|
62
|
+
try {
|
|
63
|
+
// CC-03: session.create 必须包进同一 try/finally,否则创建失败(服务端不可达/报错)
|
|
64
|
+
// 会在进入旧 try 之前抛异常,finally 不执行 → running 恒 true、activeAbort 残留。
|
|
65
|
+
if (!sessionId) {
|
|
66
|
+
const created = await this.client.createSession({
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
|
|
75
69
|
});
|
|
70
|
+
sessionId = created.sessionId;
|
|
76
71
|
}
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
this.activeSessionId = sessionId;
|
|
73
|
+
// 模型/推理强度变更 = 保留 session,每次 run 前对齐(§4.3):
|
|
74
|
+
// 该调用同时会写服务端全局默认;失败仅告警不阻断 run(模型校验由 DSH 负责)。
|
|
75
|
+
if (this.configuredModel) {
|
|
76
|
+
try {
|
|
77
|
+
await this.client.selectModel({
|
|
78
|
+
sessionId,
|
|
79
|
+
provider: DSH_MODEL_PROVIDER,
|
|
80
|
+
model: this.configuredModel,
|
|
81
|
+
...(this.reasoningEffort ? { reasoningEffort: this.reasoningEffort } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
getLogger().warn(`[dsh] selectModel failed for session ${sessionId} (run continues): ${err.message}`);
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
let terminalEmitted = false;
|
|
89
|
-
try {
|
|
90
|
-
// Snapshot the pre-prompt high-water mark before mutating the session.
|
|
91
|
-
// DSH session.history returns the whole session, so starting from seq 0
|
|
92
|
-
// would replay the first completed turn on every follow-up prompt.
|
|
93
|
-
const prePromptSeq = await this.client.latestEventSeq(sessionId);
|
|
94
|
-
await this.client.unary('session.prompt', {
|
|
95
|
-
sessionId,
|
|
96
|
-
mode: 'queue',
|
|
97
|
-
content: [{ type: 'text', text: message }],
|
|
98
|
-
});
|
|
99
|
-
const translator = new DshTranslator();
|
|
88
|
+
yield {
|
|
89
|
+
type: 'system',
|
|
90
|
+
subtype: 'init',
|
|
91
|
+
session_id: sessionId,
|
|
92
|
+
cwd: opts.cwd,
|
|
93
|
+
model: this.configuredModel ?? 'DSH',
|
|
94
|
+
};
|
|
100
95
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
96
|
+
// Snapshot the pre-prompt high-water mark before mutating the session.
|
|
97
|
+
// DSH session.history returns the whole session, so starting from seq 0
|
|
98
|
+
// would replay the first completed turn on every follow-up prompt.
|
|
99
|
+
const prePromptSeq = await this.client.latestEventSeq(sessionId);
|
|
100
|
+
await this.client.unary('session.prompt', {
|
|
101
|
+
sessionId,
|
|
102
|
+
mode: 'queue',
|
|
103
|
+
content: [{ type: 'text', text: message }],
|
|
104
|
+
});
|
|
105
|
+
const translator = new DshTranslator();
|
|
106
|
+
try {
|
|
107
|
+
for await (const item of this.client.sessionEvents(sessionId, opts.cwd, abort.signal, prePromptSeq)) {
|
|
108
|
+
if (abort.signal.aborted)
|
|
109
|
+
break;
|
|
110
|
+
if (item.kind === 'approval') {
|
|
111
|
+
getLogger().warn(`[dsh] approval requested approvalId=${item.approvalId} tool=${item.toolName} — resolve in DSH Web UI`);
|
|
112
|
+
yield {
|
|
113
|
+
type: 'assistant',
|
|
114
|
+
message: {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `⚠️ 需要授权(${item.toolName}):请在 DSH Web UI 处理 ${this.client.baseUrl}`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const ev of translator.eventToAgentEvents(item.event, sessionId)) {
|
|
126
|
+
yield ev;
|
|
127
|
+
if (ev.type === 'result') {
|
|
128
|
+
terminalEmitted = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (abort.signal.aborted)
|
|
136
|
+
throw err;
|
|
137
|
+
getLogger().error(`[dsh] run consume error: ${err.message}`);
|
|
138
|
+
yield {
|
|
139
|
+
type: 'result',
|
|
140
|
+
subtype: 'error',
|
|
141
|
+
session_id: sessionId,
|
|
142
|
+
errorMessage: `DSH stream error: ${err.message}`,
|
|
143
|
+
};
|
|
144
|
+
terminalEmitted = true;
|
|
145
|
+
}
|
|
127
146
|
}
|
|
128
147
|
catch (err) {
|
|
129
|
-
if (abort.signal.aborted)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
terminalEmitted = true;
|
|
148
|
+
if (!abort.signal.aborted) {
|
|
149
|
+
yield {
|
|
150
|
+
type: 'result',
|
|
151
|
+
subtype: 'error',
|
|
152
|
+
session_id: sessionId,
|
|
153
|
+
errorMessage: `DSH run failed: ${err.message}`,
|
|
154
|
+
};
|
|
155
|
+
terminalEmitted = true;
|
|
156
|
+
}
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
catch (err) {
|
|
160
|
+
// CC-03: createSession 等启动期失败也产出 error result(而非抛异常),
|
|
161
|
+
// 由本 catch 统一转 error result;state 清理在 finally。
|
|
142
162
|
if (!abort.signal.aborted) {
|
|
143
163
|
yield {
|
|
144
164
|
type: 'result',
|
|
145
165
|
subtype: 'error',
|
|
146
|
-
session_id: sessionId,
|
|
166
|
+
session_id: sessionId ?? '',
|
|
147
167
|
errorMessage: `DSH run failed: ${err.message}`,
|
|
148
168
|
};
|
|
149
169
|
terminalEmitted = true;
|
|
@@ -158,7 +178,7 @@ export class DshRunner {
|
|
|
158
178
|
yield {
|
|
159
179
|
type: 'result',
|
|
160
180
|
subtype: abort.signal.aborted ? 'interrupted' : 'error',
|
|
161
|
-
session_id: sessionId,
|
|
181
|
+
session_id: sessionId ?? '',
|
|
162
182
|
errorMessage: abort.signal.aborted ? 'run stopped' : 'turn ended without a terminal event',
|
|
163
183
|
};
|
|
164
184
|
}
|
|
@@ -405,6 +405,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
405
405
|
sessionId = newResult.sessionId;
|
|
406
406
|
}
|
|
407
407
|
this.activeSessionId = sessionId;
|
|
408
|
+
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
409
|
+
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型(旧 KimiRunner 通过 -m 传模型,
|
|
410
|
+
// 迁移到纯 ACP 后丢失)。仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
411
|
+
if (this.model) {
|
|
412
|
+
try {
|
|
413
|
+
await client.request('session/set_config_option', {
|
|
414
|
+
sessionId,
|
|
415
|
+
configId: 'model',
|
|
416
|
+
value: this.model,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
408
423
|
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
409
424
|
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
410
425
|
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
@@ -134,7 +134,12 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
134
134
|
this.currentTurnId = turnId;
|
|
135
135
|
this.pushEvents([translator.produceTurnStarted(sessionId, turnId)]);
|
|
136
136
|
this.promptSettled = false;
|
|
137
|
-
await client.request({ type: 'prompt', message });
|
|
137
|
+
const response = await client.request({ type: 'prompt', message });
|
|
138
|
+
if (!response.success) {
|
|
139
|
+
// CC-08: pi 返回 success:false(无效模型/provider 错误/忙)时立即失败,
|
|
140
|
+
// 不能当成成功 ACK 继续等 agent_settled(否则最长 turn idle timeout)。
|
|
141
|
+
throw new Error(`pi prompt failed: ${response.error ?? 'unknown error'}`);
|
|
142
|
+
}
|
|
138
143
|
this.promptSettled = true;
|
|
139
144
|
}
|
|
140
145
|
/**
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -69,6 +69,23 @@ export type ApprovalAction = {
|
|
|
69
69
|
/** Claude:允许当前请求 + 本会话后续所有权限请求自动放行(允许所有)。 */
|
|
70
70
|
| {
|
|
71
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;
|
|
72
89
|
} | {
|
|
73
90
|
action: 'decline';
|
|
74
91
|
} | {
|
|
@@ -145,6 +162,20 @@ export interface ApprovalView {
|
|
|
145
162
|
* 表达审批内容,避免落入 command 槽位显示无意义 `{}`。
|
|
146
163
|
*/
|
|
147
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;
|
|
148
179
|
/** 原始 input 的 JSON 字符串(kind === 'tool' 且 input 非空时展示)。 */
|
|
149
180
|
toolInput?: string;
|
|
150
181
|
fileChanges?: Array<{
|
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
16
16
|
interface DshSessionReaderOptions {
|
|
17
17
|
host?: string;
|
|
18
|
+
/** 每次调用时解析当前 host(/config 修改 host 后 runner 重建、reader 也跟随新 host)。
|
|
19
|
+
* 优先于固定的 `host`。 */
|
|
20
|
+
hostProvider?: () => string | undefined;
|
|
18
21
|
/** Injectable synchronous unary transport. Defaults to spawnSync curl. */
|
|
19
22
|
syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
|
|
20
23
|
}
|
|
21
24
|
export declare class DshSessionReader implements AgentSessionReader {
|
|
22
|
-
private readonly
|
|
25
|
+
private readonly hostProvider;
|
|
23
26
|
private readonly syncRequest;
|
|
24
27
|
constructor(opts?: DshSessionReaderOptions);
|
|
28
|
+
/** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
|
|
29
|
+
private resolveBaseUrl;
|
|
25
30
|
private unary;
|
|
26
31
|
listSessions(cwd: string, opts?: {
|
|
27
32
|
limit?: number;
|
|
@@ -17,6 +17,13 @@ import { getLogger } from '../../logger/index.js';
|
|
|
17
17
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
18
18
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
19
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;
|
|
20
27
|
/** Synchronous unary POST via curl (the reader interface is synchronous). */
|
|
21
28
|
function defaultSyncRequest(baseUrl, method, payload) {
|
|
22
29
|
const body = JSON.stringify({
|
|
@@ -53,14 +60,20 @@ function defaultSyncRequest(baseUrl, method, payload) {
|
|
|
53
60
|
return parsed.result.value;
|
|
54
61
|
}
|
|
55
62
|
export class DshSessionReader {
|
|
56
|
-
|
|
63
|
+
hostProvider;
|
|
57
64
|
syncRequest;
|
|
58
65
|
constructor(opts = {}) {
|
|
59
|
-
|
|
66
|
+
const fixed = (opts.host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
|
|
67
|
+
this.hostProvider = opts.hostProvider ?? (() => fixed);
|
|
60
68
|
this.syncRequest = opts.syncRequest ?? defaultSyncRequest;
|
|
61
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
|
+
}
|
|
62
75
|
unary(method, payload) {
|
|
63
|
-
return this.syncRequest(this.
|
|
76
|
+
return this.syncRequest(this.resolveBaseUrl(), method, payload);
|
|
64
77
|
}
|
|
65
78
|
listSessions(cwd, opts) {
|
|
66
79
|
try {
|
|
@@ -90,7 +103,7 @@ export class DshSessionReader {
|
|
|
90
103
|
try {
|
|
91
104
|
const value = this.unary('session.history', {
|
|
92
105
|
sessionId,
|
|
93
|
-
maxMessages:
|
|
106
|
+
maxMessages: DSH_HISTORY_FETCH_LIMIT,
|
|
94
107
|
});
|
|
95
108
|
const events = [];
|
|
96
109
|
let displayTitle;
|
|
@@ -142,7 +155,9 @@ export class DshSessionReader {
|
|
|
142
155
|
};
|
|
143
156
|
}
|
|
144
157
|
return {
|
|
145
|
-
|
|
158
|
+
// CC-05: 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
|
|
159
|
+
// 完整 history 累加。
|
|
160
|
+
events: capEvents(events, opts?.maxEvents ?? 50),
|
|
146
161
|
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
147
162
|
...(displayTitle ? { displayTitle } : {}),
|
|
148
163
|
};
|
|
@@ -14,6 +14,8 @@ export interface WorkspaceEntry {
|
|
|
14
14
|
* lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
|
|
15
15
|
* 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
|
|
16
16
|
* 对新保存的 workspace 同样生效(新条目应排在列表最前,而非沉底)。
|
|
17
|
+
* 同一毫秒内连续操作时 touch 会用 max+1 保证严格递增,避免 Date.now 相同
|
|
18
|
+
* 导致最近排序退化为字母序。
|
|
17
19
|
*
|
|
18
20
|
* Data migration: old format `{ alias: "path" }` is normalized to
|
|
19
21
|
* `{ alias: { path, lastUsedAt: 0 } }` on first load and persisted.
|
|
@@ -31,6 +33,12 @@ export declare class WorkspaceStore {
|
|
|
31
33
|
/**
|
|
32
34
|
* Update lastUsedAt to now for the given alias and persist.
|
|
33
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.
|
|
34
42
|
*/
|
|
35
43
|
touch(name: string): void;
|
|
36
44
|
/**
|