lark-remote 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/bridge/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +79 -70
- package/dist/card/card-shared.d.ts +1 -4
- package/dist/card/card-shared.js +2 -4
- package/dist/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.d.ts +22 -0
- package/dist/config/codex-config.js +120 -25
- package/dist/config/index.d.ts +0 -3
- package/dist/config/index.js +0 -5
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +1 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +3 -8
- package/dist/index.js +6 -0
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.js +229 -284
- package/dist/runner/claude/session.d.ts +0 -3
- package/dist/runner/claude/session.js +0 -5
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +31 -0
- package/dist/runner/common/acp/protocol-helpers.js +48 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- package/dist/runner/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +18 -11
- package/dist/runner/kimi/acp/runner.js +95 -87
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/kimi/acp/translator.js +5 -13
- package/dist/runner/opencode/acp/error-monitor.d.ts +61 -0
- package/dist/runner/opencode/acp/error-monitor.js +150 -0
- package/dist/runner/opencode/acp/runner.d.ts +17 -0
- package/dist/runner/opencode/acp/runner.js +28 -42
- package/dist/runner/opencode/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.js +3 -13
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- package/dist/runner/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/runner/types.d.ts +23 -0
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +3 -7
- package/dist/session/codex/rollout-reader.d.ts +12 -2
- package/dist/session/codex/rollout-reader.js +259 -173
- package/dist/session/codex/sessions.d.ts +2 -1
- package/dist/session/codex/sessions.js +4 -1
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +9 -6
- package/dist/session/kimi/sessions.d.ts +18 -4
- package/dist/session/kimi/sessions.js +49 -30
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +9 -7
- package/dist/session/pi/sessions.js +10 -22
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +1 -1
- package/package.json +1 -2
|
@@ -142,9 +142,6 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
142
142
|
* streamEnded 唤醒等待中的 turn 循环。
|
|
143
143
|
*/
|
|
144
144
|
private startStreamReader;
|
|
145
|
-
/** 最近一次进程退出的 code/signal(流结束错误结果构造用)。 */
|
|
146
|
-
private exitCode;
|
|
147
|
-
private exitSignal;
|
|
148
145
|
/**
|
|
149
146
|
* 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
|
|
150
147
|
* 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(P1-11 语义)。
|
|
@@ -337,8 +337,6 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
337
337
|
this.processExitPromise = new Promise((resolve) => {
|
|
338
338
|
proc.once('close', (code, signal) => {
|
|
339
339
|
this.spawnHeartbeat.clear();
|
|
340
|
-
this.exitCode = code;
|
|
341
|
-
this.exitSignal = signal;
|
|
342
340
|
if (code !== null && code !== 0 && !this.stoppedByUser) {
|
|
343
341
|
getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${this.spawnStderr.slice(-500)}`);
|
|
344
342
|
}
|
|
@@ -379,9 +377,6 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
379
377
|
}
|
|
380
378
|
})();
|
|
381
379
|
}
|
|
382
|
-
/** 最近一次进程退出的 code/signal(流结束错误结果构造用)。 */
|
|
383
|
-
exitCode = null;
|
|
384
|
-
exitSignal = null;
|
|
385
380
|
// =========================================================================
|
|
386
381
|
// Internal: turn consumption
|
|
387
382
|
// =========================================================================
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { AgentKind, AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
|
|
14
14
|
import { JsonRpcClient } from '../../common/jsonrpc/client.js';
|
|
15
15
|
import { type TranslatorEvent } from './translator.js';
|
|
16
|
-
import { type AskForApproval, type SandboxMode, type
|
|
16
|
+
import { type AskForApproval, type SandboxMode, type InitializeResult } from './protocol-types.js';
|
|
17
17
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
18
18
|
export interface CodexAppServerRunnerOptions {
|
|
19
19
|
kind: AgentKind;
|
|
@@ -38,21 +38,6 @@ export interface CodexAppServerRunnerOptions {
|
|
|
38
38
|
/** Configured approval policy (Codex 官方 AskForApproval 标准值). */
|
|
39
39
|
approvalPolicy?: AskForApproval;
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Map the client-side SandboxMode enum to the response-side SandboxPolicy
|
|
43
|
-
* object used by `thread/settings/update` (`sandboxPolicy`).
|
|
44
|
-
*/
|
|
45
|
-
export declare function sandboxModeToSandboxPolicy(mode: SandboxMode): SandboxPolicy;
|
|
46
|
-
/**
|
|
47
|
-
* Codex 官方 feature override(thread/start·resume 的 `config` 字段):
|
|
48
|
-
* 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
|
|
49
|
-
* config.toml。协议键与 openai/codex 的 feature registry 一致
|
|
50
|
-
* (key `default_mode_request_user_input`),app-server 官方测试
|
|
51
|
-
* (turn_start.rs)验证同一路径。
|
|
52
|
-
*/
|
|
53
|
-
export declare const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT: {
|
|
54
|
-
readonly 'features.default_mode_request_user_input': true;
|
|
55
|
-
};
|
|
56
41
|
export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcClient<InitializeResult>, TranslatorEvent> {
|
|
57
42
|
private connectionManager;
|
|
58
43
|
private currentTranslator;
|
|
@@ -72,7 +72,7 @@ function buildRequestUserInputResponse(action, questions, response) {
|
|
|
72
72
|
* Map the client-side SandboxMode enum to the response-side SandboxPolicy
|
|
73
73
|
* object used by `thread/settings/update` (`sandboxPolicy`).
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
function sandboxModeToSandboxPolicy(mode) {
|
|
76
76
|
switch (mode) {
|
|
77
77
|
case 'read-only':
|
|
78
78
|
return { type: 'readOnly', networkAccess: false };
|
|
@@ -95,7 +95,7 @@ export function sandboxModeToSandboxPolicy(mode) {
|
|
|
95
95
|
* (key `default_mode_request_user_input`),app-server 官方测试
|
|
96
96
|
* (turn_start.rs)验证同一路径。
|
|
97
97
|
*/
|
|
98
|
-
|
|
98
|
+
const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT = {
|
|
99
99
|
'features.default_mode_request_user_input': true,
|
|
100
100
|
};
|
|
101
101
|
export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
@@ -12,3 +12,34 @@ import type { PermissionOption } from './protocol-types.js';
|
|
|
12
12
|
* the documented names and the observed real names (§2.3).
|
|
13
13
|
*/
|
|
14
14
|
export declare function findOptionIdByKind(options: PermissionOption[], candidateKinds: string[]): string | undefined;
|
|
15
|
+
/** 各 ACP 服务端的 option kind 候选表(kimi/opencode 观测名不同,语义对齐)。 */
|
|
16
|
+
export interface AcpApprovalKinds {
|
|
17
|
+
accept: string[];
|
|
18
|
+
acceptForSession: string[];
|
|
19
|
+
decline: string[];
|
|
20
|
+
}
|
|
21
|
+
/** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
|
|
22
|
+
export declare const KIMI_APPROVAL_KINDS: AcpApprovalKinds;
|
|
23
|
+
/** opencode kind(permission.ts:219-223:once/always → approve,其余 → reject)。 */
|
|
24
|
+
export declare const OPENCODE_APPROVAL_KINDS: AcpApprovalKinds;
|
|
25
|
+
/**
|
|
26
|
+
* 把 bridge 审批动作(accept/accept_for_session/decline/cancel)映射为 ACP
|
|
27
|
+
* request_permission 响应:按 kind 候选表查 optionId 回显;找不到一律
|
|
28
|
+
* cancelled(安全兜底——服务端视为拒绝,不会悬挂)。
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildAcpPermissionOutcome(action: string, options: PermissionOption[], kinds: AcpApprovalKinds): {
|
|
31
|
+
outcome: {
|
|
32
|
+
outcome: 'cancelled';
|
|
33
|
+
} | {
|
|
34
|
+
outcome: 'selected';
|
|
35
|
+
optionId: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
|
|
40
|
+
* 带 always 类 option(kimi approve_always / opencode allow_always)才提供
|
|
41
|
+
* acceptForSession(§P4「本会话总是允许」)。kimi/opencode 两侧共用。
|
|
42
|
+
*/
|
|
43
|
+
export declare function deriveAcpAvailableDecisions(options: PermissionOption[]): string[];
|
|
44
|
+
/** 截断长文本并追加省略号(translator 审批 reason 用,原先两份相同拷贝)。 */
|
|
45
|
+
export declare function truncateWithEllipsis(text: string, maxLen: number): string;
|
|
@@ -18,4 +18,52 @@ export function findOptionIdByKind(options, candidateKinds) {
|
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
+
/** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
|
|
22
|
+
export const KIMI_APPROVAL_KINDS = {
|
|
23
|
+
accept: ['approve_once', 'approve_always', 'allow_once'],
|
|
24
|
+
acceptForSession: ['approve_always', 'allow_always'],
|
|
25
|
+
decline: ['reject', 'reject_once'],
|
|
26
|
+
};
|
|
27
|
+
/** opencode kind(permission.ts:219-223:once/always → approve,其余 → reject)。 */
|
|
28
|
+
export const OPENCODE_APPROVAL_KINDS = {
|
|
29
|
+
accept: ['allow_once', 'allow_always'],
|
|
30
|
+
acceptForSession: ['allow_always', 'approve_always'],
|
|
31
|
+
decline: ['reject_once', 'reject'],
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* 把 bridge 审批动作(accept/accept_for_session/decline/cancel)映射为 ACP
|
|
35
|
+
* request_permission 响应:按 kind 候选表查 optionId 回显;找不到一律
|
|
36
|
+
* cancelled(安全兜底——服务端视为拒绝,不会悬挂)。
|
|
37
|
+
*/
|
|
38
|
+
export function buildAcpPermissionOutcome(action, options, kinds) {
|
|
39
|
+
if (action === 'cancel') {
|
|
40
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
41
|
+
}
|
|
42
|
+
const candidates = action === 'accept'
|
|
43
|
+
? kinds.accept
|
|
44
|
+
: action === 'accept_for_session'
|
|
45
|
+
? kinds.acceptForSession
|
|
46
|
+
: kinds.decline;
|
|
47
|
+
const optionId = findOptionIdByKind(options, candidates);
|
|
48
|
+
return optionId
|
|
49
|
+
? { outcome: { outcome: 'selected', optionId } }
|
|
50
|
+
: { outcome: { outcome: 'cancelled' } };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
|
|
54
|
+
* 带 always 类 option(kimi approve_always / opencode allow_always)才提供
|
|
55
|
+
* acceptForSession(§P4「本会话总是允许」)。kimi/opencode 两侧共用。
|
|
56
|
+
*/
|
|
57
|
+
export function deriveAcpAvailableDecisions(options) {
|
|
58
|
+
const decisions = ['accept', 'decline', 'cancel'];
|
|
59
|
+
const hasAlwaysOption = options.some((opt) => opt.kind === 'allow_always' || opt.kind === 'approve_always');
|
|
60
|
+
if (hasAlwaysOption) {
|
|
61
|
+
decisions.push('acceptForSession');
|
|
62
|
+
}
|
|
63
|
+
return decisions;
|
|
64
|
+
}
|
|
65
|
+
/** 截断长文本并追加省略号(translator 审批 reason 用,原先两份相同拷贝)。 */
|
|
66
|
+
export function truncateWithEllipsis(text, maxLen) {
|
|
67
|
+
return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
|
|
68
|
+
}
|
|
21
69
|
//# sourceMappingURL=protocol-helpers.js.map
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
* updateApprovalMode, getStatusInfo) and provide the small abstract hooks below.
|
|
16
16
|
*/
|
|
17
17
|
import type { AgentKind, AgentSessionReader, AgentEvent, AgentRunner, AgentStatusInfo, SpawnOptions } from '../types.js';
|
|
18
|
-
/** How long to wait for turn output notifications before failing. */
|
|
19
|
-
export declare const TURN_IDLE_TIMEOUT_MS: number;
|
|
20
18
|
export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent> implements AgentRunner {
|
|
21
19
|
readonly kind: AgentKind;
|
|
22
20
|
readonly sessionReader: AgentSessionReader;
|
|
@@ -2,7 +2,7 @@ import { ConnectionLostError } from './jsonrpc/client.js';
|
|
|
2
2
|
import { getLogger } from '../../logger/index.js';
|
|
3
3
|
import { syntheticInitEvent } from './runner-utils.js';
|
|
4
4
|
/** How long to wait for turn output notifications before failing. */
|
|
5
|
-
|
|
5
|
+
const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
6
6
|
export class ConnectionBasedRunner {
|
|
7
7
|
kind;
|
|
8
8
|
sessionReader;
|
package/dist/runner/dsh/index.js
CHANGED
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../types.js';
|
|
16
16
|
import type { AgentRunner } from '../types.js';
|
|
17
17
|
import { DshClient, type WebSocketFactory } from './client.js';
|
|
18
|
-
/** DSH selectModel provider(deepseek-official 是 rc.7 模型目录的 provider)。 */
|
|
19
|
-
export declare const DSH_MODEL_PROVIDER = "deepseek-official";
|
|
20
18
|
export interface DshRunnerOptions {
|
|
21
19
|
kind: 'dsh';
|
|
22
20
|
sessionReader: AgentSessionReader;
|
|
@@ -16,7 +16,7 @@ import { DshClient } from './client.js';
|
|
|
16
16
|
import { DshTranslator } from './translator.js';
|
|
17
17
|
import { getLogger } from '../../logger/index.js';
|
|
18
18
|
/** DSH selectModel provider(deepseek-official 是 rc.7 模型目录的 provider)。 */
|
|
19
|
-
|
|
19
|
+
const DSH_MODEL_PROVIDER = 'deepseek-official';
|
|
20
20
|
export class DshRunner {
|
|
21
21
|
kind = 'dsh';
|
|
22
22
|
sessionReader;
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export { KimiAcpRunner } from './kimi/index.js';
|
|
|
15
15
|
export { DshRunner } from './dsh/index.js';
|
|
16
16
|
export { BashProcessRunner, type BashRunner } from './bash/index.js';
|
|
17
17
|
export { AgentRegistry } from './registry.js';
|
|
18
|
-
export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
|
|
18
|
+
export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
|
|
19
19
|
export { resolveAgentChoices } from './resolve-agent-choices.js';
|
|
20
20
|
export { syncAgentChoices } from './sync-agent-choices.js';
|
|
@@ -30,9 +30,11 @@ export interface KimiAcpRunnerOptions {
|
|
|
30
30
|
idleTtlMs?: number;
|
|
31
31
|
/** How long to wait for turn output before failing. Defaults to 10 min. */
|
|
32
32
|
turnIdleTimeoutMs?: number;
|
|
33
|
-
/**
|
|
34
|
-
* /
|
|
35
|
-
|
|
33
|
+
/** §5.2: compaction 等待的沉默超时——距上次观察到任何新 compaction 记录
|
|
34
|
+
* (begin/terminal)超过该时长仍未终态才放弃(outcome unknown,卡片 error,
|
|
35
|
+
* 不再「超时=成功」)。默认 10 分钟(对齐 turnIdleTimeoutMs)。压缩是完整
|
|
36
|
+
* LLM 请求(引擎带重试),时长无上限,旧的固定 30s 轮询超时已删除。 */
|
|
37
|
+
compactIdleTimeoutMs?: number;
|
|
36
38
|
model?: string;
|
|
37
39
|
/** Kimi permission mode (user-facing: manual/auto/yolo). */
|
|
38
40
|
permissionMode?: 'manual' | 'auto' | 'yolo';
|
|
@@ -43,7 +45,7 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
|
|
|
43
45
|
private activeSessionId;
|
|
44
46
|
private model?;
|
|
45
47
|
private permissionMode;
|
|
46
|
-
private readonly
|
|
48
|
+
private readonly compactIdleTimeoutMs;
|
|
47
49
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
48
50
|
private pendingApprovals;
|
|
49
51
|
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
@@ -107,15 +109,20 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
|
|
|
107
109
|
*/
|
|
108
110
|
protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
|
|
109
111
|
/**
|
|
110
|
-
*
|
|
111
|
-
* (context.apply_compaction / full_compaction.complete) appears.
|
|
112
|
+
* §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
|
|
112
113
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* 压缩是完整 LLM 请求(引擎 ≤5 次重试 + overflow 收缩重试),时长无上限;
|
|
115
|
+
* 旧「固定 30s 超时 = 成功」已删除。出口:
|
|
116
|
+
* completed — 观察到 full_compaction.complete / context.apply_compaction;
|
|
117
|
+
* cancelled — 观察到 full_compaction.cancel(API 失败/abort 与用户取消共用,
|
|
118
|
+
* 绝不当成功);
|
|
119
|
+
* stopped — 等待中 stop()/forceFinish(consumeTurn 产 interrupted 兜底);
|
|
120
|
+
* unknown — 距上次观察到任何新 compaction 记录(begin/terminal)超过
|
|
121
|
+
* compactIdleTimeoutMs(沉默超时,压缩可能仍在后台进行)。
|
|
122
|
+
* 观察到新记录即重置沉默时钟,并顺带重置 turn idle 时钟(lastEventAt +
|
|
123
|
+
* wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
|
|
117
124
|
*/
|
|
118
|
-
private
|
|
125
|
+
private waitForCompactionTerminal;
|
|
119
126
|
private handleNotification;
|
|
120
127
|
/**
|
|
121
128
|
* Filter kimi's own Bash tool notifications so the run card doesn't get a
|
|
@@ -16,15 +16,13 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
16
16
|
import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
17
17
|
import { KimiAcpTranslator } from './translator.js';
|
|
18
18
|
import { NotificationMethod, RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
|
|
19
|
-
import {
|
|
19
|
+
import { KIMI_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/acp/protocol-helpers.js';
|
|
20
20
|
import { mapAnswersByIndex } from '../../question-common.js';
|
|
21
21
|
import { getLogger } from '../../../logger/index.js';
|
|
22
22
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
23
23
|
import { ProcessStopper } from '../../common/process-stopper.js';
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
* context.apply_compaction ~8s after end_turn. */
|
|
27
|
-
const COMPACT_POLL_TIMEOUT_MS = 30_000;
|
|
24
|
+
/** §5.2: compaction 等待的默认沉默超时(对齐 turnIdleTimeoutMs 默认值)。 */
|
|
25
|
+
const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
28
26
|
/** Poll interval for the wire.jsonl compaction record. */
|
|
29
27
|
const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
30
28
|
// =============================================================================
|
|
@@ -36,6 +34,16 @@ const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
|
36
34
|
// =============================================================================
|
|
37
35
|
/** terminal/create 未显式给 outputByteLimit 时的缓冲上限(对齐服务端默认 4MB)。 */
|
|
38
36
|
const TERMINAL_DEFAULT_OUTPUT_LIMIT = 4 * 1024 * 1024;
|
|
37
|
+
/** 压缩终态记录:complete / apply_compaction 完成,cancel 为失败或取消(共用)。 */
|
|
38
|
+
function isCompactionTerminalLike(record) {
|
|
39
|
+
return (record.type === 'full_compaction.complete' ||
|
|
40
|
+
record.type === 'context.apply_compaction' ||
|
|
41
|
+
record.type === 'full_compaction.cancel');
|
|
42
|
+
}
|
|
43
|
+
/** 沉默窗口的展示文案(测试用小窗口显示秒,生产默认显示分钟)。 */
|
|
44
|
+
function formatIdleWindow(ms) {
|
|
45
|
+
return ms >= 60_000 ? `${Math.round(ms / 60_000)} 分钟` : `${Math.round(ms / 1000)} 秒`;
|
|
46
|
+
}
|
|
39
47
|
/**
|
|
40
48
|
* Map user-facing permission mode to ACP mode id.
|
|
41
49
|
*
|
|
@@ -58,53 +66,6 @@ function toAcpMode(mode) {
|
|
|
58
66
|
return 'yolo';
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the ACP protocol response for a permission approval decision.
|
|
63
|
-
*
|
|
64
|
-
* accept → {outcome:{outcome:'selected', optionId:<approve_once kind>}}
|
|
65
|
-
* accept_for_session → {outcome:{outcome:'selected', optionId:<approve_always kind>}}
|
|
66
|
-
* decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
|
|
67
|
-
* cancel → {outcome:{outcome:'cancelled'}}
|
|
68
|
-
*
|
|
69
|
-
* optionId is looked up from the request's options[] by kind (approval.ts:28-29):
|
|
70
|
-
* approve_once / approve_always / allow_once for accept;
|
|
71
|
-
* approve_always / allow_always for accept_for_session;
|
|
72
|
-
* reject / reject_once for decline. If not found, fall back to cancelled
|
|
73
|
-
* (safe universal default — the server treats it as "user declined").
|
|
74
|
-
*
|
|
75
|
-
* 2026-08-15 live test: approve_once + approve_always + reject observed.
|
|
76
|
-
*/
|
|
77
|
-
function buildApprovalResponse(action, pending) {
|
|
78
|
-
if (action === 'cancel') {
|
|
79
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
80
|
-
}
|
|
81
|
-
if (action === 'accept') {
|
|
82
|
-
const optionId = findOptionIdByKind(pending.options, [
|
|
83
|
-
'approve_once',
|
|
84
|
-
'approve_always',
|
|
85
|
-
'allow_once',
|
|
86
|
-
]);
|
|
87
|
-
if (optionId) {
|
|
88
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
89
|
-
}
|
|
90
|
-
// Fallback: can't find allow option → cancel is safe
|
|
91
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
92
|
-
}
|
|
93
|
-
if (action === 'accept_for_session') {
|
|
94
|
-
// §P4: 卡片「允许本次会话」→ always 类 optionId(kimi approve_always)
|
|
95
|
-
const optionId = findOptionIdByKind(pending.options, ['approve_always', 'allow_always']);
|
|
96
|
-
if (optionId) {
|
|
97
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
98
|
-
}
|
|
99
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
100
|
-
}
|
|
101
|
-
// decline
|
|
102
|
-
const optionId = findOptionIdByKind(pending.options, ['reject', 'reject_once']);
|
|
103
|
-
if (optionId) {
|
|
104
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
105
|
-
}
|
|
106
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
107
|
-
}
|
|
108
69
|
/**
|
|
109
70
|
* AskUserQuestion 回编(kind === 'question'):
|
|
110
71
|
* - elicitation form:answer → {action:'accept', content:{q0..qn}}(多选数组/
|
|
@@ -171,7 +132,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
171
132
|
activeSessionId = null;
|
|
172
133
|
model;
|
|
173
134
|
permissionMode;
|
|
174
|
-
|
|
135
|
+
compactIdleTimeoutMs;
|
|
175
136
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
176
137
|
pendingApprovals = new Map();
|
|
177
138
|
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
@@ -195,7 +156,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
195
156
|
super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
|
|
196
157
|
this.model = opts.model;
|
|
197
158
|
this.permissionMode = opts.permissionMode ?? 'manual';
|
|
198
|
-
this.
|
|
159
|
+
this.compactIdleTimeoutMs = opts.compactIdleTimeoutMs ?? COMPACT_IDLE_TIMEOUT_MS;
|
|
199
160
|
const managerOpts = {
|
|
200
161
|
binary: opts.binary ?? 'kimi',
|
|
201
162
|
args: opts.acpArgs ?? ['acp'],
|
|
@@ -316,17 +277,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
316
277
|
sessionId,
|
|
317
278
|
prompt: [{ type: 'text', text: '/compact' }],
|
|
318
279
|
};
|
|
319
|
-
//
|
|
320
|
-
// wire.jsonl
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
// never
|
|
280
|
+
// §5.2: compaction is a BACKGROUND task — the prompt settles before the
|
|
281
|
+
// wire.jsonl terminal record lands. Baseline the terminal record count,
|
|
282
|
+
// fire the prompt without awaiting (notifications flow through
|
|
283
|
+
// consumeTurn), then wait for a NEW terminal (complete/apply →
|
|
284
|
+
// completed; cancel → cancelled) before producing the result. There is
|
|
285
|
+
// NO total timeout — silence beyond compactIdleTimeoutMs yields
|
|
286
|
+
// 'unknown' (never a fake success). Keeps the connection alive while the
|
|
287
|
+
// background compaction finishes — otherwise dispose/exit kills it and
|
|
288
|
+
// the record never lands (S5/S6 打回复现 / 2026-08-31 事故).
|
|
326
289
|
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
327
|
-
const
|
|
328
|
-
? compactionReader.call(this.sessionReader, sessionId, opts.cwd)
|
|
329
|
-
:
|
|
290
|
+
const baselineRecords = compactionReader
|
|
291
|
+
? compactionReader.call(this.sessionReader, sessionId, opts.cwd)
|
|
292
|
+
: [];
|
|
293
|
+
const baselineTerminalCount = baselineRecords.filter(isCompactionTerminalLike).length;
|
|
294
|
+
const baselineTotalCount = baselineRecords.length;
|
|
330
295
|
this.promptSettled = false;
|
|
331
296
|
// ACP holds the session/prompt response for the ENTIRE turn (unlike
|
|
332
297
|
// codex turn/start), so the RPC-level timeout must not apply here —
|
|
@@ -337,13 +302,37 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
337
302
|
if (this.stopRequested)
|
|
338
303
|
return; // already cancelled
|
|
339
304
|
if (compactionReader) {
|
|
340
|
-
|
|
305
|
+
// §5.4 可选防线(日志探针):prompt settle 后既无新 begin 也无新
|
|
306
|
+
// terminal → compact 未被引擎接受(可能已在跑或被拒绝)。不解析
|
|
307
|
+
// "already running" 文本、不影响卡片。
|
|
308
|
+
const totalNow = compactionReader.call(this.sessionReader, sessionId, opts.cwd).length;
|
|
309
|
+
if (totalNow === baselineTotalCount) {
|
|
310
|
+
getLogger().warn(`[${this.logTag}] compact 未被引擎接受(无新 begin/terminal 记录,可能已在跑或被拒绝)`);
|
|
311
|
+
}
|
|
312
|
+
const outcome = await this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount);
|
|
313
|
+
if (this.forceFinish)
|
|
314
|
+
return; // stopped while polling
|
|
315
|
+
if (outcome === 'completed') {
|
|
316
|
+
// 压缩完成:正常翻译 prompt 结果(subtype success)。
|
|
317
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
318
|
+
this.pushEvents([resultEvent]);
|
|
319
|
+
}
|
|
320
|
+
else if (outcome === 'cancelled') {
|
|
321
|
+
this.pushEvents([
|
|
322
|
+
translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
|
|
323
|
+
]);
|
|
324
|
+
}
|
|
325
|
+
else if (outcome === 'unknown') {
|
|
326
|
+
this.pushEvents([
|
|
327
|
+
translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
// outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
334
|
+
this.pushEvents([resultEvent]);
|
|
341
335
|
}
|
|
342
|
-
if (this.forceFinish)
|
|
343
|
-
return; // stopped while polling
|
|
344
|
-
// Translate prompt result into a result event
|
|
345
|
-
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
346
|
-
this.pushEvents([resultEvent]);
|
|
347
336
|
}, (err) => {
|
|
348
337
|
this.promptSettled = true;
|
|
349
338
|
if (this.stopRequested) {
|
|
@@ -367,7 +356,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
367
356
|
const action = response?.action ?? 'decline';
|
|
368
357
|
const acpResponse = pending.kind === 'question'
|
|
369
358
|
? buildQuestionResponse(action, pending, response)
|
|
370
|
-
:
|
|
359
|
+
: buildAcpPermissionOutcome(action, pending.options, KIMI_APPROVAL_KINDS);
|
|
371
360
|
client.respond(requestId, acpResponse);
|
|
372
361
|
this.pendingApprovals.delete(requestId);
|
|
373
362
|
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
@@ -447,8 +436,8 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
447
436
|
}
|
|
448
437
|
this.activeSessionId = sessionId;
|
|
449
438
|
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
450
|
-
// 不带 model;不主动下发则实际跑 kimi
|
|
451
|
-
//
|
|
439
|
+
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型。
|
|
440
|
+
// 仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
452
441
|
if (this.model) {
|
|
453
442
|
try {
|
|
454
443
|
await client.request('session/set_config_option', {
|
|
@@ -511,28 +500,47 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
511
500
|
});
|
|
512
501
|
}
|
|
513
502
|
/**
|
|
514
|
-
*
|
|
515
|
-
* (context.apply_compaction / full_compaction.complete) appears.
|
|
503
|
+
* §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
|
|
516
504
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
505
|
+
* 压缩是完整 LLM 请求(引擎 ≤5 次重试 + overflow 收缩重试),时长无上限;
|
|
506
|
+
* 旧「固定 30s 超时 = 成功」已删除。出口:
|
|
507
|
+
* completed — 观察到 full_compaction.complete / context.apply_compaction;
|
|
508
|
+
* cancelled — 观察到 full_compaction.cancel(API 失败/abort 与用户取消共用,
|
|
509
|
+
* 绝不当成功);
|
|
510
|
+
* stopped — 等待中 stop()/forceFinish(consumeTurn 产 interrupted 兜底);
|
|
511
|
+
* unknown — 距上次观察到任何新 compaction 记录(begin/terminal)超过
|
|
512
|
+
* compactIdleTimeoutMs(沉默超时,压缩可能仍在后台进行)。
|
|
513
|
+
* 观察到新记录即重置沉默时钟,并顺带重置 turn idle 时钟(lastEventAt +
|
|
514
|
+
* wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
|
|
521
515
|
*/
|
|
522
|
-
async
|
|
516
|
+
async waitForCompactionTerminal(reader, opts, baselineTerminalCount) {
|
|
523
517
|
const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
|
|
524
|
-
|
|
525
|
-
|
|
518
|
+
let lastActivityAt = Date.now();
|
|
519
|
+
let lastTotalCount = reader.call(this.sessionReader, sessionId, opts.cwd).length;
|
|
520
|
+
while (true) {
|
|
526
521
|
if (this.forceFinish || this.stopRequested)
|
|
527
|
-
return;
|
|
522
|
+
return 'stopped';
|
|
528
523
|
const records = reader.call(this.sessionReader, sessionId, opts.cwd);
|
|
529
|
-
if (records.length
|
|
530
|
-
|
|
531
|
-
|
|
524
|
+
if (records.length !== lastTotalCount) {
|
|
525
|
+
lastTotalCount = records.length;
|
|
526
|
+
lastActivityAt = Date.now();
|
|
527
|
+
// 压缩仍在活动(新 begin / 重试 / terminal):重置 turn idle 时钟,
|
|
528
|
+
// 让 consumeTurn 重新计算滚动截止时间。
|
|
529
|
+
this.lastEventAt = Date.now();
|
|
530
|
+
this.wakeWaiters();
|
|
531
|
+
}
|
|
532
|
+
const terminalRecords = records.filter(isCompactionTerminalLike);
|
|
533
|
+
if (terminalRecords.length > baselineTerminalCount) {
|
|
534
|
+
const newest = terminalRecords[terminalRecords.length - 1];
|
|
535
|
+
getLogger().info(`[${this.logTag}] compaction terminal observed (${newest.type}, ${terminalRecords.length} > baseline ${baselineTerminalCount})`);
|
|
536
|
+
return newest.type === 'full_compaction.cancel' ? 'cancelled' : 'completed';
|
|
537
|
+
}
|
|
538
|
+
if (Date.now() - lastActivityAt >= this.compactIdleTimeoutMs) {
|
|
539
|
+
getLogger().warn(`[${this.logTag}] no compaction terminal within ${this.compactIdleTimeoutMs}ms of last activity (baselineTerminal=${baselineTerminalCount}); reporting unknown`);
|
|
540
|
+
return 'unknown';
|
|
532
541
|
}
|
|
533
542
|
await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
|
|
534
543
|
}
|
|
535
|
-
getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
|
|
536
544
|
}
|
|
537
545
|
handleNotification(method, params) {
|
|
538
546
|
// Terminal-backed Bash 的通知走 runner 自产事件(见 bashToolCallIds),
|
|
@@ -127,7 +127,6 @@ export declare class KimiAcpTranslator {
|
|
|
127
127
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
128
128
|
* always-class option (kimi `approve_always`, opencode `allow_always`).
|
|
129
129
|
*/
|
|
130
|
-
private deriveAvailableDecisions;
|
|
131
130
|
/**
|
|
132
131
|
* ResultEvent.usage requires input_tokens/output_tokens: number, but the
|
|
133
132
|
* ACP wire has no such split — the live snapshot is context occupancy only.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { makeQuestionApprovalEvent } from '../../question-common.js';
|
|
32
32
|
import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
|
|
33
|
+
import { deriveAcpAvailableDecisions, truncateWithEllipsis, } from '../../common/acp/protocol-helpers.js';
|
|
33
34
|
import { getLogger } from '../../../logger/index.js';
|
|
34
35
|
/**
|
|
35
36
|
* Fixed snapshot item ids: ACP has exactly one text stream and one thinking
|
|
@@ -328,10 +329,12 @@ export class KimiAcpTranslator {
|
|
|
328
329
|
requestId,
|
|
329
330
|
kind: 'command',
|
|
330
331
|
command: params.toolCall?.title ?? undefined,
|
|
331
|
-
reason: params.toolCall?.rawInput
|
|
332
|
+
reason: params.toolCall?.rawInput
|
|
333
|
+
? truncateWithEllipsis(params.toolCall.rawInput, 200)
|
|
334
|
+
: undefined,
|
|
332
335
|
// §P4: 从服务端 options kind 派生——带 approve_always(或 allow_always)
|
|
333
336
|
// 才提供「本会话总是允许」(acceptForSession);否则与旧行为一致。
|
|
334
|
-
availableDecisions:
|
|
337
|
+
availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
|
|
335
338
|
};
|
|
336
339
|
return [
|
|
337
340
|
{
|
|
@@ -372,14 +375,6 @@ export class KimiAcpTranslator {
|
|
|
372
375
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
373
376
|
* always-class option (kimi `approve_always`, opencode `allow_always`).
|
|
374
377
|
*/
|
|
375
|
-
deriveAvailableDecisions(params) {
|
|
376
|
-
const decisions = ['accept', 'decline', 'cancel'];
|
|
377
|
-
const hasAlwaysOption = (params.options ?? []).some((opt) => opt.kind === 'approve_always' || opt.kind === 'allow_always');
|
|
378
|
-
if (hasAlwaysOption) {
|
|
379
|
-
decisions.push('acceptForSession');
|
|
380
|
-
}
|
|
381
|
-
return decisions;
|
|
382
|
-
}
|
|
383
378
|
// =========================================================================
|
|
384
379
|
// Private helpers
|
|
385
380
|
// =========================================================================
|
|
@@ -396,9 +391,6 @@ export class KimiAcpTranslator {
|
|
|
396
391
|
// =============================================================================
|
|
397
392
|
// Helpers
|
|
398
393
|
// =============================================================================
|
|
399
|
-
function truncate(text, maxLen) {
|
|
400
|
-
return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
|
|
401
|
-
}
|
|
402
394
|
/** elicitation schema property → UserQuestion(无法解析返回 null)。 */
|
|
403
395
|
function elicitationPropertyToQuestion(prop) {
|
|
404
396
|
const title = prop.title?.trim();
|