lark-remote 0.3.0 → 0.4.1
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 +35 -0
- package/README.en.md +6 -6
- package/README.md +8 -7
- package/dist/binder.d.ts +1 -1
- package/dist/binder.js +1 -1
- package/dist/bridge/final-usage.js +2 -2
- package/dist/bridge/inbound-media.d.ts +13 -20
- package/dist/bridge/inbound-media.js +107 -91
- package/dist/bridge/index.d.ts +6 -6
- package/dist/bridge/index.js +81 -87
- package/dist/cli.js +5 -1
- package/dist/clone.d.ts +1 -1
- package/dist/common/fs.js +1 -1
- package/dist/config/dir.d.ts +9 -0
- package/dist/config/dir.js +69 -0
- package/dist/config/index.d.ts +22 -3
- package/dist/config/index.js +26 -4
- package/dist/connector/index.d.ts +28 -1
- package/dist/connector/index.js +155 -31
- package/dist/error-classification.d.ts +7 -0
- package/dist/error-classification.js +37 -1
- package/dist/inbound/event.d.ts +41 -0
- package/dist/inbound/event.js +11 -0
- package/dist/inbound/placeholder.d.ts +26 -0
- package/dist/inbound/placeholder.js +149 -0
- package/dist/inbound/prompt.d.ts +20 -0
- package/dist/inbound/prompt.js +47 -0
- package/dist/inbound/receipt.d.ts +28 -0
- package/dist/inbound/receipt.js +62 -0
- package/dist/inbound/turn-assembler.d.ts +63 -0
- package/dist/inbound/turn-assembler.js +233 -0
- package/dist/inbound/turn.d.ts +48 -0
- package/dist/inbound/turn.js +10 -0
- package/dist/index.js +108 -73
- package/dist/platform/command.js +1 -1
- package/dist/platform/sleep-blocker.d.ts +17 -0
- package/dist/platform/sleep-blocker.js +73 -0
- package/dist/platform/terminator-win32.js +1 -1
- package/dist/restart.d.ts +4 -4
- package/dist/restart.js +5 -5
- package/dist/router/config/kimi.js +4 -3
- package/dist/router/config/opencode.d.ts +1 -1
- package/dist/router/config/opencode.js +11 -0
- package/dist/router/index.d.ts +17 -0
- package/dist/router/index.js +53 -15
- package/dist/runner/common/acp/base-acp-runner.js +7 -2
- package/dist/runner/common/acp/base-translator.d.ts +19 -0
- package/dist/runner/common/acp/base-translator.js +82 -0
- package/dist/runner/common/acp/protocol-types.d.ts +16 -2
- package/dist/runner/common/acp/protocol-types.js +3 -0
- package/dist/runner/common/connection-based-runner.js +6 -2
- package/dist/runner/kimi/acp/runner.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +1 -1
- package/dist/runner/kimi/acp/translator.js +60 -4
- package/dist/runner/opencode/acp/runner.d.ts +1 -1
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -100,7 +100,7 @@ export function createWin32Terminator(deps) {
|
|
|
100
100
|
// M0 真机验证项(§9.2 矩阵):本方法在 process.on('exit') 里调用,而
|
|
101
101
|
// killTree 依赖 libuv uv_spawn 在 exit handler 内同步发起 taskkill——
|
|
102
102
|
// 大概率可用(uv_spawn 本身同步),但与 posix 路径(同步 process.kill)
|
|
103
|
-
// 可靠性不等价;失败表现为 win32 上
|
|
103
|
+
// 可靠性不等价;失败表现为 win32 上 lark-remote 退出后 agent 变孤儿。
|
|
104
104
|
if (!isAlive(proc))
|
|
105
105
|
return;
|
|
106
106
|
killTree(proc.pid);
|
package/dist/restart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Self-restart without an external watchdog/cron: the only process that can
|
|
3
|
-
* reliably start the successor is the dying
|
|
3
|
+
* reliably start the successor is the dying lark-remote itself. The old process
|
|
4
4
|
* spawns a detached replacement (same executable + argv, hence same
|
|
5
5
|
* --config-dir), then exits cleanly. The child learns the old pid via env
|
|
6
6
|
* and waits for it to die before acquiring the instance lock, so the lock
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
/** Env var telling a replacement child which parent pid to wait for before acquiring the instance lock. */
|
|
10
10
|
export declare const RESTART_WAIT_PID_ENV = "LARK_REMOTE_RESTART_WAIT_PID";
|
|
11
11
|
/**
|
|
12
|
-
* Spawn a detached
|
|
12
|
+
* Spawn a detached lark-remote process (shared by /restart replacement and /clone
|
|
13
13
|
* new-instance) whose early output — before the child's own file logger
|
|
14
14
|
* initializes — lands on logFilePath. Returns the child pid, or null when
|
|
15
15
|
* spawn failed synchronously / no pid; filesystem errors (unwritable log
|
|
@@ -26,7 +26,7 @@ export declare function spawnDetachedBridge(args: string[], logFilePath: string,
|
|
|
26
26
|
env?: NodeJS.ProcessEnv;
|
|
27
27
|
}): number | null;
|
|
28
28
|
/**
|
|
29
|
-
* Spawn a detached replacement
|
|
29
|
+
* Spawn a detached replacement lark-remote with the same executable and argv
|
|
30
30
|
* (including --config-dir) and return its pid. The caller is expected to
|
|
31
31
|
* exit right after. stdio is redirected to restart-child.log so early
|
|
32
32
|
* startup failures (before the file logger initializes) are not lost.
|
|
@@ -34,7 +34,7 @@ export declare function spawnDetachedBridge(args: string[], logFilePath: string,
|
|
|
34
34
|
export declare function spawnReplacementBridge(logsDir: string): number;
|
|
35
35
|
/**
|
|
36
36
|
* On startup, if this process was spawned as a restart replacement, wait for
|
|
37
|
-
* the previous
|
|
37
|
+
* the previous lark-remote process to exit (and release the instance lock) before
|
|
38
38
|
* continuing. No-op for normal starts. On timeout, proceed anyway — lock
|
|
39
39
|
* acquisition remains the authority.
|
|
40
40
|
*/
|
package/dist/restart.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getLogger } from './logger/index.js';
|
|
|
5
5
|
import { sleep } from './common/sleep.js';
|
|
6
6
|
/**
|
|
7
7
|
* Self-restart without an external watchdog/cron: the only process that can
|
|
8
|
-
* reliably start the successor is the dying
|
|
8
|
+
* reliably start the successor is the dying lark-remote itself. The old process
|
|
9
9
|
* spawns a detached replacement (same executable + argv, hence same
|
|
10
10
|
* --config-dir), then exits cleanly. The child learns the old pid via env
|
|
11
11
|
* and waits for it to die before acquiring the instance lock, so the lock
|
|
@@ -16,7 +16,7 @@ export const RESTART_WAIT_PID_ENV = 'LARK_REMOTE_RESTART_WAIT_PID';
|
|
|
16
16
|
const WAIT_TIMEOUT_MS = 20_000;
|
|
17
17
|
const POLL_MS = 100;
|
|
18
18
|
/**
|
|
19
|
-
* Spawn a detached
|
|
19
|
+
* Spawn a detached lark-remote process (shared by /restart replacement and /clone
|
|
20
20
|
* new-instance) whose early output — before the child's own file logger
|
|
21
21
|
* initializes — lands on logFilePath. Returns the child pid, or null when
|
|
22
22
|
* spawn failed synchronously / no pid; filesystem errors (unwritable log
|
|
@@ -51,7 +51,7 @@ export function spawnDetachedBridge(args, logFilePath, opts) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* Spawn a detached replacement
|
|
54
|
+
* Spawn a detached replacement lark-remote with the same executable and argv
|
|
55
55
|
* (including --config-dir) and return its pid. The caller is expected to
|
|
56
56
|
* exit right after. stdio is redirected to restart-child.log so early
|
|
57
57
|
* startup failures (before the file logger initializes) are not lost.
|
|
@@ -61,13 +61,13 @@ export function spawnReplacementBridge(logsDir) {
|
|
|
61
61
|
env: { ...process.env, [RESTART_WAIT_PID_ENV]: String(process.pid) },
|
|
62
62
|
});
|
|
63
63
|
if (pid === null) {
|
|
64
|
-
throw new Error('spawn replacement
|
|
64
|
+
throw new Error('spawn replacement lark-remote failed: no pid');
|
|
65
65
|
}
|
|
66
66
|
return pid;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* On startup, if this process was spawned as a restart replacement, wait for
|
|
70
|
-
* the previous
|
|
70
|
+
* the previous lark-remote process to exit (and release the instance lock) before
|
|
71
71
|
* continuing. No-op for normal starts. On timeout, proceed anyway — lock
|
|
72
72
|
* acquisition remains the authority.
|
|
73
73
|
*/
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { loadKimiConfig, KIMI_THINKING_EFFORTS, FALLBACK_EFFORTS, } from '../../config/kimi-config.js';
|
|
8
8
|
import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
|
|
9
9
|
const PERMISSION_MODE_OPTIONS = [
|
|
10
|
-
|
|
11
|
-
{ text: '
|
|
12
|
-
{ text: '
|
|
10
|
+
// kimi 0.40.0(2026-09-02)起官方语义:manual=始终询问 / yolo=必要时询问 / auto=完全自动
|
|
11
|
+
{ text: 'manual(始终询问)', value: 'manual' },
|
|
12
|
+
{ text: 'auto(完全自动)', value: 'auto' },
|
|
13
|
+
{ text: 'yolo(必要时询问)', value: 'yolo' },
|
|
13
14
|
];
|
|
14
15
|
export class KimiConfigBuilder {
|
|
15
16
|
buildFields(displayConfig) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OpenCode config builder — builds fields and handles field changes for OpenCode agent.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfigCardBuilder, ConfigField } from './types.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type AppConfig } from '../../config/index.js';
|
|
6
6
|
export declare class OpencodeConfigBuilder implements AgentConfigCardBuilder {
|
|
7
7
|
buildFields(displayConfig: AppConfig): ConfigField[];
|
|
8
8
|
handleFieldChange(key: string, value: unknown, config: AppConfig): Array<{
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* OpenCode config builder — builds fields and handles field changes for OpenCode agent.
|
|
3
3
|
*/
|
|
4
4
|
import { loadOpencodeConfig } from '../../config/opencode-config.js';
|
|
5
|
+
import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
|
|
5
6
|
import { resetModelPatch } from './common/model-patch.js';
|
|
6
7
|
const MODE_OPTIONS = [
|
|
7
8
|
{ text: 'build(默认,逐项审批)', value: 'build' },
|
|
@@ -38,6 +39,16 @@ export class OpencodeConfigBuilder {
|
|
|
38
39
|
options: MODE_OPTIONS,
|
|
39
40
|
currentValue: currentMode,
|
|
40
41
|
});
|
|
42
|
+
// Turn 空闲超时(纯 ACP 对齐 kimi:卡片暴露 turnIdleTimeoutMinutes,
|
|
43
|
+
// binary/requestTimeoutMs/idleTtlMs 为 YAML-only 工程参数)
|
|
44
|
+
const currentTurnIdleTimeoutMinutes = displayConfig.agents?.opencode?.acp?.turnIdleTimeoutMinutes ??
|
|
45
|
+
DEFAULT_TURN_IDLE_TIMEOUT_MINUTES;
|
|
46
|
+
fields.push({
|
|
47
|
+
key: 'agents.opencode.acp.turnIdleTimeoutMinutes',
|
|
48
|
+
label: 'Turn 空闲超时(分钟, 0关闭)',
|
|
49
|
+
type: 'input',
|
|
50
|
+
currentValue: String(currentTurnIdleTimeoutMinutes),
|
|
51
|
+
});
|
|
41
52
|
return fields;
|
|
42
53
|
}
|
|
43
54
|
handleFieldChange(key, value, config) {
|
package/dist/router/index.d.ts
CHANGED
|
@@ -121,6 +121,11 @@ export declare class CommandRouter {
|
|
|
121
121
|
* returns its pid. Injected from index.ts; /restart is unavailable without it.
|
|
122
122
|
*/
|
|
123
123
|
private restartSpawner?;
|
|
124
|
+
/**
|
|
125
|
+
* 冲刷装配器窗口内未提交的入站 turn(/exit、/restart 干净退出前调用,
|
|
126
|
+
* 避免静默期窗口内的内容丢失)。index.ts 注入。
|
|
127
|
+
*/
|
|
128
|
+
private flushInbound?;
|
|
124
129
|
private idleTimeoutMs;
|
|
125
130
|
/** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
|
|
126
131
|
private devMode;
|
|
@@ -197,12 +202,24 @@ export declare class CommandRouter {
|
|
|
197
202
|
* The reader is fetched from the registry each time to reflect config changes.
|
|
198
203
|
*/
|
|
199
204
|
private get sessionReader();
|
|
205
|
+
/**
|
|
206
|
+
* 注入入站装配器的冲刷回调(/exit、/restart 干净退出前调用,
|
|
207
|
+
* 避免静默期窗口内未提交的 turn 丢失)。index.ts 在构造后接线。
|
|
208
|
+
*/
|
|
209
|
+
setInboundFlusher(flush: () => Promise<void>): void;
|
|
200
210
|
/**
|
|
201
211
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
212
|
+
*
|
|
213
|
+
* 命令识别前置条件(2026-09-15 P0 修复):
|
|
214
|
+
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
215
|
+
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
216
|
+
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
217
|
+
* 三条同时成立时生效,其余一律按文本转发。
|
|
202
218
|
*/
|
|
203
219
|
handle(message: string, ctx: CommandContext, opts?: {
|
|
204
220
|
cwdOverride?: string;
|
|
205
221
|
binding?: AgentBinding;
|
|
222
|
+
allowCommandPrefix?: boolean;
|
|
206
223
|
}): Promise<CommandResult | null>;
|
|
207
224
|
/**
|
|
208
225
|
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
package/dist/router/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { OrderStore } from '../order/index.js';
|
|
|
9
9
|
import { resolveAlias } from '../order/alias-resolve.js';
|
|
10
10
|
import { diffConfig, setNestedValue } from './config-diff.js';
|
|
11
11
|
import { getLogger } from '../logger/index.js';
|
|
12
|
+
import { stripPlaceholders } from '../inbound/placeholder.js';
|
|
12
13
|
import { activeRunUsage, clampInt } from './utils.js';
|
|
13
14
|
import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
|
|
14
15
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
@@ -342,6 +343,11 @@ export class CommandRouter {
|
|
|
342
343
|
* returns its pid. Injected from index.ts; /restart is unavailable without it.
|
|
343
344
|
*/
|
|
344
345
|
restartSpawner;
|
|
346
|
+
/**
|
|
347
|
+
* 冲刷装配器窗口内未提交的入站 turn(/exit、/restart 干净退出前调用,
|
|
348
|
+
* 避免静默期窗口内的内容丢失)。index.ts 注入。
|
|
349
|
+
*/
|
|
350
|
+
flushInbound;
|
|
345
351
|
idleTimeoutMs;
|
|
346
352
|
/** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
|
|
347
353
|
devMode;
|
|
@@ -416,13 +422,33 @@ export class CommandRouter {
|
|
|
416
422
|
get sessionReader() {
|
|
417
423
|
return this.sessionReaderRegistry.get(this.config.defaultAgent);
|
|
418
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* 注入入站装配器的冲刷回调(/exit、/restart 干净退出前调用,
|
|
427
|
+
* 避免静默期窗口内未提交的 turn 丢失)。index.ts 在构造后接线。
|
|
428
|
+
*/
|
|
429
|
+
setInboundFlusher(flush) {
|
|
430
|
+
this.flushInbound = flush;
|
|
431
|
+
}
|
|
419
432
|
/**
|
|
420
433
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
434
|
+
*
|
|
435
|
+
* 命令识别前置条件(2026-09-15 P0 修复):
|
|
436
|
+
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
437
|
+
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
438
|
+
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
439
|
+
* 三条同时成立时生效,其余一律按文本转发。
|
|
421
440
|
*/
|
|
422
441
|
async handle(message, ctx, opts) {
|
|
423
442
|
const trimmed = message.trim();
|
|
424
|
-
|
|
425
|
-
|
|
443
|
+
// 双保险:任何调用点都必须先剥离结构占位符,再判命令前缀
|
|
444
|
+
//(index.ts 传 allowCommandPrefix = rawContentType === 'text',但那层不是唯一入口)。
|
|
445
|
+
const guard = stripPlaceholders(trimmed);
|
|
446
|
+
// 默认 true:order.exec 等既有内部调用点传的是用户已确认的指令文本,
|
|
447
|
+
// 且剥离后无占位符 —— 默认放行不改变它们的语义(见 command-guard.test.ts)。
|
|
448
|
+
const allowCommandPrefix = opts?.allowCommandPrefix ?? true;
|
|
449
|
+
const commandEligible = allowCommandPrefix && guard.kinds.length === 0 && guard.clean !== '';
|
|
450
|
+
const startsWithBang = commandEligible && guard.clean.startsWith('!');
|
|
451
|
+
const startsWithSlash = commandEligible && guard.clean.startsWith('/');
|
|
426
452
|
getLogger().info(`[router] handle message="${message.slice(0, 50)}..." trimmed="${trimmed.slice(0, 50)}..." startsWithSlash=${startsWithSlash} startsWithBang=${startsWithBang}`);
|
|
427
453
|
// 复制分身流程活跃期:一切消息(含命令)交给 clone 状态机处理,
|
|
428
454
|
// 不转发 coding agent、不进命令分发(index.ts 已在入队前拦截,这里
|
|
@@ -432,20 +458,20 @@ export class CommandRouter {
|
|
|
432
458
|
return null;
|
|
433
459
|
}
|
|
434
460
|
if (startsWithSlash) {
|
|
435
|
-
const result = await this.executeCommand(
|
|
461
|
+
const result = await this.executeCommand(guard.clean, ctx);
|
|
436
462
|
if (result) {
|
|
437
463
|
await this.bridge.sendResult(result, ctx);
|
|
438
464
|
}
|
|
439
465
|
if (this.pendingExit) {
|
|
440
|
-
//
|
|
441
|
-
await this.
|
|
466
|
+
// 干净退出前冲刷装配器窗口内未提交的入站 turn,避免静默期窗口内的内容丢失。
|
|
467
|
+
await this.flushInbound?.();
|
|
442
468
|
this.exitHandler();
|
|
443
469
|
}
|
|
444
470
|
return result;
|
|
445
471
|
}
|
|
446
472
|
// Handle bang commands (!command)
|
|
447
|
-
if (
|
|
448
|
-
const cmd =
|
|
473
|
+
if (startsWithBang) {
|
|
474
|
+
const cmd = guard.clean.slice(1).trim();
|
|
449
475
|
if (!cmd) {
|
|
450
476
|
await this.bridge.sendResult({ text: '请输入要执行的命令,例如 !ls' }, ctx);
|
|
451
477
|
return null;
|
|
@@ -456,7 +482,9 @@ export class CommandRouter {
|
|
|
456
482
|
// Forward to Claude (P1-14: pass the enqueue-time workspace through so the
|
|
457
483
|
// run uses the same cwd as the serial queue lane, even if /cd ran while
|
|
458
484
|
// the message was queued)
|
|
459
|
-
|
|
485
|
+
// 剥离占位符后再转发:agent 只该看到用户文本(附图已转成 attachments 块,
|
|
486
|
+
// 那是装配器生成的协议块,stripPlaceholders 会原样保留)。
|
|
487
|
+
await this.bridge.forwardToClaude(guard.clean, ctx, opts);
|
|
460
488
|
return null;
|
|
461
489
|
}
|
|
462
490
|
/**
|
|
@@ -607,7 +635,7 @@ export class CommandRouter {
|
|
|
607
635
|
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
608
636
|
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
609
637
|
if (this.pendingExit) {
|
|
610
|
-
await this.
|
|
638
|
+
await this.flushInbound?.();
|
|
611
639
|
this.exitHandler();
|
|
612
640
|
}
|
|
613
641
|
return;
|
|
@@ -2030,8 +2058,8 @@ export class CommandRouter {
|
|
|
2030
2058
|
{ cmd: 'ws', label: '/ws', desc: 'workspace 管理(save|use|remove)' },
|
|
2031
2059
|
{ cmd: 'config', label: '/config /c', desc: '查看和修改配置(卡片交互)' },
|
|
2032
2060
|
{ cmd: 'active', label: '/active', desc: '查看所有正在进行中的 session' },
|
|
2033
|
-
{ cmd: 'exit', label: '/exit /e', desc: '退出
|
|
2034
|
-
{ cmd: 'restart', label: '/restart', desc: '重启
|
|
2061
|
+
{ cmd: 'exit', label: '/exit /e', desc: '退出 lark-remote' },
|
|
2062
|
+
{ cmd: 'restart', label: '/restart', desc: '重启 lark-remote(新进程,config 不变)' },
|
|
2035
2063
|
{ cmd: 'update', label: '/update /u', desc: '检查并升级到最新版本' },
|
|
2036
2064
|
{
|
|
2037
2065
|
cmd: 'clone',
|
|
@@ -2140,6 +2168,16 @@ export class CommandRouter {
|
|
|
2140
2168
|
content: '**Agent**\n通过 `/config` → 默认 Agent 切换:Claude / Codex / Opencode / Pi / Kimi',
|
|
2141
2169
|
},
|
|
2142
2170
|
});
|
|
2171
|
+
// 系统休眠说明
|
|
2172
|
+
bodyElements.push({ tag: 'hr' });
|
|
2173
|
+
bodyElements.push({
|
|
2174
|
+
tag: 'div',
|
|
2175
|
+
text: {
|
|
2176
|
+
tag: 'lark_md',
|
|
2177
|
+
content: '**系统休眠**\nlark-remote 运行期间会阻止系统自动休眠,以免远程连接不上,' +
|
|
2178
|
+
'不影响显示器睡眠',
|
|
2179
|
+
},
|
|
2180
|
+
});
|
|
2143
2181
|
return {
|
|
2144
2182
|
card: {
|
|
2145
2183
|
schema: '2.0',
|
|
@@ -2228,7 +2266,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2228
2266
|
async cmdExit() {
|
|
2229
2267
|
// Signal handle() to invoke exitHandler after the reply is sent.
|
|
2230
2268
|
this.pendingExit = true;
|
|
2231
|
-
return { text: '
|
|
2269
|
+
return { text: 'lark-remote 正在退出...' };
|
|
2232
2270
|
}
|
|
2233
2271
|
cmdRestart() {
|
|
2234
2272
|
if (!this.restartSpawner) {
|
|
@@ -2240,7 +2278,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2240
2278
|
// 链路——spawn 失败则旧进程保持存活,不会两头落空。
|
|
2241
2279
|
const pid = this.restartSpawner();
|
|
2242
2280
|
this.pendingExit = true;
|
|
2243
|
-
return { text: `♻️
|
|
2281
|
+
return { text: `♻️ lark-remote 重启中(新进程 pid ${pid}),启动通知稍后送达…` };
|
|
2244
2282
|
}
|
|
2245
2283
|
catch (err) {
|
|
2246
2284
|
getLogger().error('[router] restart spawn failed:', err);
|
|
@@ -3413,8 +3451,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3413
3451
|
return { card };
|
|
3414
3452
|
}
|
|
3415
3453
|
cmdActive(_args, _ctx, offset = 0) {
|
|
3416
|
-
// List all active runs from
|
|
3417
|
-
// New semantics (2026-07-20): only shows runs started by THIS
|
|
3454
|
+
// List all active runs from lark-remote memory (not from file system scan)
|
|
3455
|
+
// New semantics (2026-07-20): only shows runs started by THIS lark-remote process
|
|
3418
3456
|
const activeRuns = this.bridge.getActiveRuns();
|
|
3419
3457
|
const activeBashRuns = this.bridge.getActiveBashRuns();
|
|
3420
3458
|
if (activeRuns.length === 0 && activeBashRuns.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { respondAcpApproval, sendAcpSetMode } from './protocol-helpers.js';
|
|
1
|
+
import { respondAcpApproval, sendAcpSetMode, truncateWithEllipsis, } from './protocol-helpers.js';
|
|
2
2
|
import { getLogger } from '../../../logger/index.js';
|
|
3
3
|
import { ConnectionBasedRunner } from '../connection-based-runner.js';
|
|
4
4
|
export class BaseAcpRunner extends ConnectionBasedRunner {
|
|
@@ -87,7 +87,12 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
|
|
|
87
87
|
options,
|
|
88
88
|
...(proto ? { proto } : {}),
|
|
89
89
|
});
|
|
90
|
-
|
|
90
|
+
// 命令预览进日志:审批卡在飞书侧,日志是本地唯一的事后追溯通道
|
|
91
|
+
// (kimi 的 request_permission 不带 rawInput,曾只剩 kind=command 可查)。
|
|
92
|
+
const commandPreview = ev.view.command
|
|
93
|
+
? ` command=${truncateWithEllipsis(ev.view.command, 120)}`
|
|
94
|
+
: '';
|
|
95
|
+
getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}${commandPreview}`);
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
}
|
|
@@ -59,6 +59,16 @@ export declare abstract class BaseAcpTranslator {
|
|
|
59
59
|
protected liveCostUsd?: number;
|
|
60
60
|
/** Current turn id, set by produceTurnStarted. */
|
|
61
61
|
protected currentTurnId: string;
|
|
62
|
+
/**
|
|
63
|
+
* Latest streaming-args text per tool call (keyed by wire toolCallId).
|
|
64
|
+
* kimi ACP sends the permission request BEFORE tool.call.started (verified
|
|
65
|
+
* live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
|
|
66
|
+
* carries only title + a truncated summary in content, NO rawInput — the
|
|
67
|
+
* full args only ever reach the client as accumulated text in the
|
|
68
|
+
* tool_call/tool_call_update streaming deltas. Tracking them here lets the
|
|
69
|
+
* approval handler correlate by toolCallId and recover the real command.
|
|
70
|
+
*/
|
|
71
|
+
private toolCallArgsByCallId;
|
|
62
72
|
/**
|
|
63
73
|
* Handle a notification from the ACP server and return translated events.
|
|
64
74
|
* Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
|
|
@@ -115,6 +125,15 @@ export declare abstract class BaseAcpTranslator {
|
|
|
115
125
|
* tool_call_update → user/tool_result. status:'failed' → is_error:true.
|
|
116
126
|
*/
|
|
117
127
|
protected handleToolCallUpdate(event: ToolCallUpdateEvent, _sessionId: string): AcpTranslatorEvent[];
|
|
128
|
+
/**
|
|
129
|
+
* Latest tracked streaming-args text for a tool call (undefined when never
|
|
130
|
+
* seen or already terminated). Approval handlers correlate
|
|
131
|
+
* request_permission's toolCall.toolCallId against this to recover the real
|
|
132
|
+
* command (see toolCallArgsByCallId).
|
|
133
|
+
*/
|
|
134
|
+
protected getTrackedToolCallArgs(toolCallId: string): string | undefined;
|
|
135
|
+
/** Record latest args text, bounded so a long session cannot grow it forever. */
|
|
136
|
+
private trackToolCallArgs;
|
|
118
137
|
/**
|
|
119
138
|
* tool_result content extraction. Default: rawOutput passthrough (kimi
|
|
120
139
|
* semantics). opencode overrides — its rawOutput is an OBJECT
|
|
@@ -11,6 +11,36 @@ const KIND_TO_TOOL = {
|
|
|
11
11
|
search: 'Grep',
|
|
12
12
|
fetch: 'WebFetch',
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Join the text payloads of a tool_call(/_update) content array
|
|
16
|
+
* (`[{type:'content', content:{type:'text', text}}]` entries). Returns
|
|
17
|
+
* undefined when no text entry exists (diff/terminal-only content).
|
|
18
|
+
*/
|
|
19
|
+
function extractToolCallContentText(content) {
|
|
20
|
+
if (!Array.isArray(content))
|
|
21
|
+
return undefined;
|
|
22
|
+
let text = '';
|
|
23
|
+
let found = false;
|
|
24
|
+
for (const entry of content) {
|
|
25
|
+
const e = entry;
|
|
26
|
+
if (e?.type === 'content' && e.content?.type === 'text' && typeof e.content.text === 'string') {
|
|
27
|
+
text += e.content.text;
|
|
28
|
+
found = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found ? text : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Best-effort stringify for tracked rawInput objects (never throws). */
|
|
34
|
+
function stringifyUnknown(value) {
|
|
35
|
+
if (typeof value === 'string')
|
|
36
|
+
return value;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(value) ?? String(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
14
44
|
// =============================================================================
|
|
15
45
|
// Translator base class
|
|
16
46
|
// =============================================================================
|
|
@@ -25,6 +55,16 @@ export class BaseAcpTranslator {
|
|
|
25
55
|
liveCostUsd;
|
|
26
56
|
/** Current turn id, set by produceTurnStarted. */
|
|
27
57
|
currentTurnId = '';
|
|
58
|
+
/**
|
|
59
|
+
* Latest streaming-args text per tool call (keyed by wire toolCallId).
|
|
60
|
+
* kimi ACP sends the permission request BEFORE tool.call.started (verified
|
|
61
|
+
* live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
|
|
62
|
+
* carries only title + a truncated summary in content, NO rawInput — the
|
|
63
|
+
* full args only ever reach the client as accumulated text in the
|
|
64
|
+
* tool_call/tool_call_update streaming deltas. Tracking them here lets the
|
|
65
|
+
* approval handler correlate by toolCallId and recover the real command.
|
|
66
|
+
*/
|
|
67
|
+
toolCallArgsByCallId = new Map();
|
|
28
68
|
/**
|
|
29
69
|
* Handle a notification from the ACP server and return translated events.
|
|
30
70
|
* Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
|
|
@@ -191,6 +231,13 @@ export class BaseAcpTranslator {
|
|
|
191
231
|
// Keep raw string as-is
|
|
192
232
|
}
|
|
193
233
|
}
|
|
234
|
+
const argsText = extractToolCallContentText(event.content);
|
|
235
|
+
if (argsText !== undefined) {
|
|
236
|
+
this.trackToolCallArgs(event.toolCallId, argsText);
|
|
237
|
+
}
|
|
238
|
+
else if (event.rawInput !== undefined && event.rawInput !== null) {
|
|
239
|
+
this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
|
|
240
|
+
}
|
|
194
241
|
const mapped = event.kind ? KIND_TO_TOOL[event.kind] : undefined;
|
|
195
242
|
const name = mapped ?? event.title;
|
|
196
243
|
const summary = mapped && event.title && event.title !== name ? event.title : undefined;
|
|
@@ -218,6 +265,22 @@ export class BaseAcpTranslator {
|
|
|
218
265
|
handleToolCallUpdate(event, _sessionId) {
|
|
219
266
|
const isError = event.status === 'failed';
|
|
220
267
|
const content = this.extractToolResultContent(event);
|
|
268
|
+
// Terminal updates end the args-streaming phase — drop the tracked entry.
|
|
269
|
+
// In-progress delta text is cumulative (events-map.ts
|
|
270
|
+
// toolCallDeltaToSessionUpdate: content.text = accumulator.args), so the
|
|
271
|
+
// latest text replaces the previous one rather than appending.
|
|
272
|
+
if (event.status === 'completed' || event.status === 'failed') {
|
|
273
|
+
this.toolCallArgsByCallId.delete(event.toolCallId);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const argsText = extractToolCallContentText(event.content);
|
|
277
|
+
if (argsText !== undefined) {
|
|
278
|
+
this.trackToolCallArgs(event.toolCallId, argsText);
|
|
279
|
+
}
|
|
280
|
+
else if (event.rawInput !== undefined && event.rawInput !== null) {
|
|
281
|
+
this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
221
284
|
return [
|
|
222
285
|
{
|
|
223
286
|
type: 'user',
|
|
@@ -235,6 +298,25 @@ export class BaseAcpTranslator {
|
|
|
235
298
|
},
|
|
236
299
|
];
|
|
237
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Latest tracked streaming-args text for a tool call (undefined when never
|
|
303
|
+
* seen or already terminated). Approval handlers correlate
|
|
304
|
+
* request_permission's toolCall.toolCallId against this to recover the real
|
|
305
|
+
* command (see toolCallArgsByCallId).
|
|
306
|
+
*/
|
|
307
|
+
getTrackedToolCallArgs(toolCallId) {
|
|
308
|
+
return this.toolCallArgsByCallId.get(toolCallId);
|
|
309
|
+
}
|
|
310
|
+
/** Record latest args text, bounded so a long session cannot grow it forever. */
|
|
311
|
+
trackToolCallArgs(toolCallId, argsText) {
|
|
312
|
+
this.toolCallArgsByCallId.delete(toolCallId);
|
|
313
|
+
this.toolCallArgsByCallId.set(toolCallId, argsText);
|
|
314
|
+
if (this.toolCallArgsByCallId.size > 100) {
|
|
315
|
+
const oldest = this.toolCallArgsByCallId.keys().next().value;
|
|
316
|
+
if (oldest !== undefined)
|
|
317
|
+
this.toolCallArgsByCallId.delete(oldest);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
238
320
|
/**
|
|
239
321
|
* tool_result content extraction. Default: rawOutput passthrough (kimi
|
|
240
322
|
* semantics). opencode overrides — its rawOutput is an OBJECT
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* 以 `turn_diff` 快照发出——禁止把 delta 直接当快照或把全文当增量重复追加。
|
|
23
23
|
* 4. `tool_call` 事件可能**没有 rawInput**(工具 lazy-create,参数在后续
|
|
24
24
|
* tool_call_update 才补齐);translator 必须归一化为 `{}`,不得因缺失崩溃。
|
|
25
|
+
* 5. `session/request_permission` 的 toolCall **不一定带 rawInput**(kimi 实测:
|
|
26
|
+
* 审批门先于 tool.call.started,只有 title + content 截断摘要);完整命令
|
|
27
|
+
* 需按 toolCallId 关联审批前的流式 args 文本恢复(BaseAcpTranslator 追踪)。
|
|
25
28
|
*/
|
|
26
29
|
export type { JsonRpcRequest, JsonRpcNotification, JsonRpcSuccessResponse, JsonRpcErrorResponse, } from '../jsonrpc/types.js';
|
|
27
30
|
/** Standard JSON-RPC error codes. */
|
|
@@ -226,9 +229,20 @@ export interface PermissionOption {
|
|
|
226
229
|
}
|
|
227
230
|
export interface RequestPermissionParams {
|
|
228
231
|
sessionId: string;
|
|
232
|
+
/**
|
|
233
|
+
* Approval target. Divergence by server (2026-09-13 live probes):
|
|
234
|
+
* - kimi 0.42: {toolCallId, title: toolName, content: [truncated summary
|
|
235
|
+
* text]} — NO rawInput; the approval gate fires BEFORE tool.call.started,
|
|
236
|
+
* so the full args only exist in the pre-approval tool_call/_update
|
|
237
|
+
* streaming deltas (correlate by toolCallId, see BaseAcpTranslator).
|
|
238
|
+
* - opencode: {toolCallId, title: full command for bash/shell, rawInput:
|
|
239
|
+
* full input object} — title/rawInput are directly usable.
|
|
240
|
+
*/
|
|
229
241
|
toolCall?: {
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
toolCallId?: string;
|
|
243
|
+
title?: string;
|
|
244
|
+
rawInput?: unknown;
|
|
245
|
+
content?: unknown[];
|
|
232
246
|
[key: string]: unknown;
|
|
233
247
|
};
|
|
234
248
|
options: PermissionOption[];
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* 以 `turn_diff` 快照发出——禁止把 delta 直接当快照或把全文当增量重复追加。
|
|
23
23
|
* 4. `tool_call` 事件可能**没有 rawInput**(工具 lazy-create,参数在后续
|
|
24
24
|
* tool_call_update 才补齐);translator 必须归一化为 `{}`,不得因缺失崩溃。
|
|
25
|
+
* 5. `session/request_permission` 的 toolCall **不一定带 rawInput**(kimi 实测:
|
|
26
|
+
* 审批门先于 tool.call.started,只有 title + content 截断摘要);完整命令
|
|
27
|
+
* 需按 toolCallId 关联审批前的流式 args 文本恢复(BaseAcpTranslator 追踪)。
|
|
25
28
|
*/
|
|
26
29
|
/** Standard JSON-RPC error codes. */
|
|
27
30
|
export const RpcErrorCode = {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ConnectionLostError } from './jsonrpc/client.js';
|
|
2
2
|
import { getLogger } from '../../logger/index.js';
|
|
3
3
|
import { syntheticInitEvent } from './runner-utils.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
|
|
5
|
+
/** How long to wait for turn output notifications before failing.
|
|
6
|
+
* 30 分钟(单源于 config 的 DEFAULT_TURN_IDLE_TIMEOUT_MINUTES):ACP/app-server
|
|
7
|
+
* 类 agent 常因长工具调用(pip/npm install 等)或等待 SubAgent 长时间静默,
|
|
8
|
+
* 10 分钟会误杀正常进行中的 run。未接配置的 runner(如 opencode)直接吃此默认。 */
|
|
9
|
+
const TURN_IDLE_TIMEOUT_MS = DEFAULT_TURN_IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
|
6
10
|
export class ConnectionBasedRunner {
|
|
7
11
|
kind;
|
|
8
12
|
sessionReader;
|
|
@@ -36,7 +36,7 @@ export interface KimiAcpRunnerOptions {
|
|
|
36
36
|
requestTimeoutMs?: number;
|
|
37
37
|
/** Idle TTL for connection manager. */
|
|
38
38
|
idleTtlMs?: number;
|
|
39
|
-
/** How long to wait for turn output before failing. Defaults to
|
|
39
|
+
/** How long to wait for turn output before failing. Defaults to 30 min. */
|
|
40
40
|
turnIdleTimeoutMs?: number;
|
|
41
41
|
/** §5.2: compaction 等待的沉默超时——距上次观察到任何新 compaction 记录
|
|
42
42
|
* (begin/terminal)超过该时长仍未终态才放弃(outcome unknown,卡片 error,
|
|
@@ -27,7 +27,7 @@ import { mapAnswersByIndex } from '../../question-common.js';
|
|
|
27
27
|
import { getLogger } from '../../../logger/index.js';
|
|
28
28
|
import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
|
|
29
29
|
import { ProcessStopper } from '../../common/process-stopper.js';
|
|
30
|
-
/** §5.2: compaction
|
|
30
|
+
/** §5.2: compaction 等待的默认沉默超时(10 分钟,独立于 turn idle 默认 30 分钟)。 */
|
|
31
31
|
const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
32
32
|
/** Poll interval for the wire.jsonl compaction record. */
|
|
33
33
|
const COMPACT_POLL_INTERVAL_MS = 1_000;
|