lark-remote 0.4.5 → 0.5.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 +34 -0
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.js +10 -7
- package/dist/bridge/index.d.ts +55 -17
- package/dist/bridge/index.js +145 -74
- package/dist/bridge/queue-manager.d.ts +71 -20
- package/dist/bridge/queue-manager.js +139 -57
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +1 -1
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +14 -14
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/cli.js +2 -1
- package/dist/clone.d.ts +5 -3
- package/dist/clone.js +8 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.js +5 -2
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/connector/index.d.ts +5 -5
- package/dist/connector/index.js +24 -11
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/index.js +23 -12
- package/dist/instance-lock.js +4 -3
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +6 -2
- package/dist/platform/agent-stopper.d.ts +1 -1
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +2 -2
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +2 -2
- package/dist/router/config/opencode.js +1 -1
- package/dist/router/index.d.ts +65 -16
- package/dist/router/index.js +141 -47
- package/dist/router/order-exec-dispatch.js +2 -2
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -2
- package/dist/runner/claude/session.js +14 -14
- package/dist/runner/codex/app-server/runner.d.ts +28 -0
- package/dist/runner/codex/app-server/runner.js +57 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/acp-approval.d.ts +2 -2
- package/dist/runner/common/acp/acp-approval.js +3 -3
- package/dist/runner/common/acp/base-acp-runner.js +1 -1
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +1 -1
- package/dist/runner/common/connection-based-runner.js +2 -2
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/spawning-runner.d.ts +21 -8
- package/dist/runner/common/spawning-runner.js +41 -15
- package/dist/runner/dsh/runner.js +2 -2
- package/dist/runner/kimi/acp/runner.d.ts +3 -3
- package/dist/runner/kimi/acp/runner.js +9 -9
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +4 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +3 -3
- package/dist/runner/opencode/acp/translator.js +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -1
- package/dist/runner/pi/rpc/translator.js +4 -4
- package/dist/runner/types.d.ts +11 -2
- package/dist/runner/types.js +12 -1
- package/dist/session/claude/session-index.d.ts +12 -0
- package/dist/session/claude/session-index.js +20 -5
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +12 -12
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.js +2 -2
- package/dist/session/kimi/sessions.d.ts +2 -2
- package/dist/session/kimi/sessions.js +11 -11
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +12 -10
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/package.json +1 -1
package/dist/bridge/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
|
11
11
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
12
12
|
import { normalizeResultUsage } from '../runner/common/result-usage.js';
|
|
13
13
|
import { BashProcessRunner } from '../runner/index.js';
|
|
14
|
+
import { AGENT_KINDS } from '../runner/types.js';
|
|
14
15
|
import { QueueManager, } from './queue-manager.js';
|
|
15
16
|
import { ApprovalCoordinator, decisionToApprovalAction } from './approval-coordinator.js';
|
|
16
17
|
import { InboundMediaHandler } from './inbound-media.js';
|
|
@@ -18,13 +19,13 @@ import { InboundMediaHandler } from './inbound-media.js';
|
|
|
18
19
|
* Max events to read for the completion notification card (mirror router's
|
|
19
20
|
* AUTO_RESUME_MAX_EVENTS = 5). Without a cap, readSessionContent returns every
|
|
20
21
|
* event after the last user message and the card blows past Feishu's 28KB
|
|
21
|
-
* budget
|
|
22
|
+
* budget.
|
|
22
23
|
*/
|
|
23
24
|
const COMPLETION_NOTIFICATION_MAX_EVENTS = 5;
|
|
24
25
|
/** Approval request timeout (ms) — pending approval expires after this. */
|
|
25
26
|
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
27
|
/**
|
|
27
|
-
* runner
|
|
28
|
+
* runner 缓存的工作区数量上限:超过后创建新工作区槽位时
|
|
28
29
|
* 顺带回收「非 active 且进程已停」的死缓存,防止随访问过的工作区数量
|
|
29
30
|
* 无界增长。活进程/在途 run 一律保留(workspace-lifetime 长驻连接不能误杀)。
|
|
30
31
|
*/
|
|
@@ -50,12 +51,18 @@ function terminalReactionEmoji(terminal) {
|
|
|
50
51
|
return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* 单点化:`'runCompact' in runner` 鸭子探测
|
|
55
|
+
* codex app-server + kimi/opencode ACP runner 满足。
|
|
55
56
|
*/
|
|
56
57
|
function runnerHasRunCompact(runner) {
|
|
57
58
|
return ('runCompact' in runner && typeof runner.runCompact === 'function');
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* 每个 cwd 保留的「可压缩 run」条数上限。Compact 按钮挂在终态 run 卡片上,
|
|
62
|
+
* 卡片会被后续消息不断顶上去;无上限会让长跑进程随 run 数无界增长,而用户
|
|
63
|
+
* 实际能点到的只有最近几张卡,20 条足以覆盖。
|
|
64
|
+
*/
|
|
65
|
+
const MAX_COMPACTABLE_RUNS_PER_CWD = 20;
|
|
59
66
|
/**
|
|
60
67
|
* Bridge owns the per-workspace serial processing queue (§9.6) and the
|
|
61
68
|
* single-card streaming forwarding loop (§9.12 watchdog + session sync).
|
|
@@ -98,7 +105,7 @@ export class Bridge {
|
|
|
98
105
|
*/
|
|
99
106
|
runners = new Map();
|
|
100
107
|
/**
|
|
101
|
-
* Runner 槽位在「活跃运行期间配置变更」时被标 stale
|
|
108
|
+
* Runner 槽位在「活跃运行期间配置变更」时被标 stale:clearRunners() 对
|
|
102
109
|
* 活跃 cwd 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
|
|
103
110
|
* finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
|
|
104
111
|
* key 为 `${cwd}\u0000${agentKind}`。
|
|
@@ -112,10 +119,16 @@ export class Bridge {
|
|
|
112
119
|
/** Approval coordinators keyed by runId. */
|
|
113
120
|
approvalCoordinators = new Map();
|
|
114
121
|
/**
|
|
115
|
-
*
|
|
116
|
-
* activeRuns
|
|
122
|
+
* 最近若干次已完成的、runner 有 runCompact 的 run(cwd → runId → 记录)。
|
|
123
|
+
* run 结束后 activeRuns 已清空,而终态卡上的 Compact 按钮只带 runId,
|
|
124
|
+
* 需要它反查 sessionId + agentKind。
|
|
125
|
+
*
|
|
126
|
+
* 为什么按 runId 存而不是「每 cwd 只留最新一条」:compact 卡片动作走串行
|
|
127
|
+
* 队列(会被正在跑的 run 挡住),点击到执行之间可能夹着别的 run 收尾。
|
|
128
|
+
* 单槽会被覆盖 —— 旧卡片的压缩必然报「该任务已结束或不属于当前会话」,
|
|
129
|
+
* 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
|
|
117
130
|
*/
|
|
118
|
-
|
|
131
|
+
compactableRuns = new Map();
|
|
119
132
|
/**
|
|
120
133
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
121
134
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
@@ -150,7 +163,7 @@ export class Bridge {
|
|
|
150
163
|
if (workspaceMap?.has(kind)) {
|
|
151
164
|
return workspaceMap.get(kind);
|
|
152
165
|
}
|
|
153
|
-
//
|
|
166
|
+
// 缓存超过上限且是新工作区时才回收死槽位——刚创建未 run
|
|
154
167
|
// 的 runner 同样是 isRunning=false,小缓存下立即回收会破坏「创建即注册」
|
|
155
168
|
// 的既有语义(exit-dispatcher 计数回归)。
|
|
156
169
|
if (this.runners.size >= MAX_CACHED_RUNNER_WORKSPACES && !this.runners.has(cwd)) {
|
|
@@ -241,13 +254,13 @@ export class Bridge {
|
|
|
241
254
|
return undefined;
|
|
242
255
|
}
|
|
243
256
|
}
|
|
244
|
-
/** Get the current runner for a cwd. Used by router for agent-specific commands (
|
|
257
|
+
/** Get the current runner for a cwd. Used by router for agent-specific commands (.2). */
|
|
245
258
|
getCurrentRunner(cwd) {
|
|
246
259
|
return this.getRunner(cwd);
|
|
247
260
|
}
|
|
248
261
|
/**
|
|
249
262
|
* Whether the runner for the given agent kind has runCompact capability.
|
|
250
|
-
* Used by router for compact button gating (
|
|
263
|
+
* Used by router for compact button gating (duck-typing
|
|
251
264
|
* instead of hardcoding agentKind === 'codex').
|
|
252
265
|
*/
|
|
253
266
|
hasRunCompact(cwd, kind = this.config.defaultAgent) {
|
|
@@ -255,8 +268,8 @@ export class Bridge {
|
|
|
255
268
|
return runnerHasRunCompact(runner);
|
|
256
269
|
}
|
|
257
270
|
/** 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
|
|
258
|
-
*
|
|
259
|
-
* 执行时刻,避免 /new、/config 在排队期间改写 live
|
|
271
|
+
* 唯一捕获点:排队消息在入队时刻把 agent+session 钉进 AgentBinding,随任务闭包带到执行时刻
|
|
272
|
+
* 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移。 */
|
|
260
273
|
currentBinding(userId) {
|
|
261
274
|
const agent = this.config.defaultAgent;
|
|
262
275
|
return {
|
|
@@ -278,7 +291,7 @@ export class Bridge {
|
|
|
278
291
|
// ends, and the next run picks up the new config/agent via getRunner.
|
|
279
292
|
for (const cwd of [...this.runners.keys()]) {
|
|
280
293
|
if (this.activeRuns.has(cwd)) {
|
|
281
|
-
//
|
|
294
|
+
// 活跃 cwd 不能立即 evict(会误杀长驻连接),但必须标记 stale,
|
|
282
295
|
// 否则 finalizeRun 对 workspace-lifetime runner 也不 evict → 下一轮复用旧配置实例。
|
|
283
296
|
// 这里只标待淘汰,当前 run 结束后由 finalizeRun 安全 evict。
|
|
284
297
|
for (const kind of [...this.runners.get(cwd).keys()]) {
|
|
@@ -294,7 +307,7 @@ export class Bridge {
|
|
|
294
307
|
}
|
|
295
308
|
/**
|
|
296
309
|
* Evict a (cwd, agentKind) runner cache slot. Unregisters the runner from the
|
|
297
|
-
* process-level exit dispatcher first (
|
|
310
|
+
* process-level exit dispatcher first (without removal the singleton
|
|
298
311
|
* Set<Runner> retains every historical runner instance, so memory still grows
|
|
299
312
|
* with run count), then deletes the slot. Every runner-discarding path
|
|
300
313
|
* (clearRunners / finalizeRun / interruptCurrentRun) must go through this.
|
|
@@ -383,6 +396,13 @@ export class Bridge {
|
|
|
383
396
|
getQueueInfo(cwd) {
|
|
384
397
|
return this.queueManager.getQueueInfo(cwd);
|
|
385
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* lane 当前占用者的展示信息(排队卡与「立即执行」守卫共用)。
|
|
401
|
+
* 典型用途:占用者是 Compact 时拒绝 queue.immediate,避免中断压缩。
|
|
402
|
+
*/
|
|
403
|
+
getQueueExecutingInfo(cwd) {
|
|
404
|
+
return this.queueManager.getExecutingInfo(cwd);
|
|
405
|
+
}
|
|
386
406
|
/** Remove a task from the queue by messageId. Returns true if found and removed. */
|
|
387
407
|
removeFromQueue(cwd, messageId) {
|
|
388
408
|
return this.queueManager.removeFromQueue(cwd, messageId);
|
|
@@ -535,7 +555,7 @@ export class Bridge {
|
|
|
535
555
|
// below may outlive the task's settle, letting the serial chain advance
|
|
536
556
|
// to a successor that begins executing before we resume. An
|
|
537
557
|
// unconditional reset would then zero the successor's count and mark
|
|
538
|
-
// its slot interrupted, hiding it from queue cards
|
|
558
|
+
// its slot interrupted, hiding it from queue cards.
|
|
539
559
|
const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
|
|
540
560
|
await Promise.allSettled([
|
|
541
561
|
// 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
|
|
@@ -551,8 +571,8 @@ export class Bridge {
|
|
|
551
571
|
this.activeRuns.delete(cwd);
|
|
552
572
|
}
|
|
553
573
|
// Fix 4: Clean up this specific (cwd, agentKind) runner slot
|
|
554
|
-
// (
|
|
555
|
-
//
|
|
574
|
+
// (eviction must also unregister the runner from the exit dispatcher)
|
|
575
|
+
// evict by the run's captured agentKind, not the live
|
|
556
576
|
// defaultAgent (may have changed mid-run via /config).
|
|
557
577
|
this.evictRunnerSlot(cwd, active.agentKind);
|
|
558
578
|
// Reset queue executing count: the task was interrupted externally, so
|
|
@@ -663,6 +683,26 @@ export class Bridge {
|
|
|
663
683
|
return false;
|
|
664
684
|
}
|
|
665
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Best-effort in-place refresh of a KNOWN card (no fallback send).
|
|
688
|
+
*
|
|
689
|
+
* `updateCardInPlace` falls back to sending a new card when the PATCH fails,
|
|
690
|
+
* which is right for the card the user just clicked (a silent failure would
|
|
691
|
+
* leave the click with no feedback) but wrong when refreshing *sibling*
|
|
692
|
+
* cards: an expired/withdrawn card would spawn a duplicate card on every
|
|
693
|
+
* edit. Callers that fan out to several cards use this instead and drop the
|
|
694
|
+
* card id from their set on `false`.
|
|
695
|
+
*/
|
|
696
|
+
async patchCard(messageId, card) {
|
|
697
|
+
try {
|
|
698
|
+
await this.connector.updateCard(messageId, card);
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
getLogger().debug(`[lark-remote] patchCard failed messageId=${messageId}: ${errorMessage(err)}`);
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
666
706
|
/** Send a file to the user via Feishu. */
|
|
667
707
|
async sendFile(filePath, ctx) {
|
|
668
708
|
try {
|
|
@@ -709,9 +749,9 @@ export class Bridge {
|
|
|
709
749
|
* updates the same run card, and terminal events finalize that card.
|
|
710
750
|
*/
|
|
711
751
|
async forwardToClaude(message, ctx, opts) {
|
|
712
|
-
//
|
|
752
|
+
// agentKind 一律来自绑定。无 binding 时保持现状(live defaultAgent)。
|
|
713
753
|
const agentKind = opts?.binding?.agent ?? this.config.defaultAgent;
|
|
714
|
-
//
|
|
754
|
+
// lane 与执行 cwd 同源。消息入队时(index.ts 闭包)捕获的 cwd
|
|
715
755
|
// 作为 cwdOverride 显式传入;执行时若 sessionStore cwd 已被 /cd 等命令改写,
|
|
716
756
|
// 以 lane 为准 —— 否则旧 lane 的排队消息会被 busy-drop 静默丢失。空串视为
|
|
717
757
|
// 未提供(与入队前无 cwd 的兜底语义一致)。
|
|
@@ -732,12 +772,12 @@ export class Bridge {
|
|
|
732
772
|
}
|
|
733
773
|
// Note: Multiple workspaces can have runs in parallel. If runner is busy,
|
|
734
774
|
// runner.run() will throw and we'll catch it below.
|
|
735
|
-
//
|
|
775
|
+
// sessionId 钉死语义——binding 有 sessionId 时用它(入队时刻快照),
|
|
736
776
|
// 否则跟随 live store(无 binding 或入队时无 session 的正常路径)。
|
|
737
777
|
const sessionId = opts?.binding?.sessionId ?? this.sessionStore.getSessionId(ctx.userId, agentKind);
|
|
738
778
|
// 会话代际快照(2026-08-09,2026-10-07 前移):/new、/cd、/resume 会 bump
|
|
739
779
|
// epoch,该 run 后续 system.init 的 sessionId 写回即判 stale 跳过。
|
|
740
|
-
// 快照点优先取 binding.sessionEpoch(入队/commit
|
|
780
|
+
// 快照点优先取 binding.sessionEpoch(入队/commit 时刻,与 sessionId 同刻):
|
|
741
781
|
// 排队消息在「commit 后、开跑前」被 reset 时,执行起点捕获会漏判(epoch 已是
|
|
742
782
|
// bump 后的值)→ 旧 sessionId 被写回复活 reset。无 binding 时回退执行起点捕获,
|
|
743
783
|
// 保持非排队路径(如 /compact)的既有语义。
|
|
@@ -761,8 +801,8 @@ export class Bridge {
|
|
|
761
801
|
createRunSession(ctx, cwd, runner, agentKind = this.config.defaultAgent) {
|
|
762
802
|
const runId = randomUUID();
|
|
763
803
|
getLogger().debug(`[lark-remote] creating runCard runId=${runId}`);
|
|
764
|
-
//
|
|
765
|
-
// 缓存键 (cwd, agentKind) 和 eviction
|
|
804
|
+
// agentKind 来自入队绑定(forwardToClaude 解析),不再读 live config。
|
|
805
|
+
// 缓存键 (cwd, agentKind) 和 eviction 都用这个值。
|
|
766
806
|
const cardSession = new RunCardSession({
|
|
767
807
|
connector: this.connector,
|
|
768
808
|
chatId: ctx.chatId,
|
|
@@ -790,7 +830,7 @@ export class Bridge {
|
|
|
790
830
|
return { runId, cardSession, activeRun };
|
|
791
831
|
}
|
|
792
832
|
/**
|
|
793
|
-
* §9.12 空闲看门狗:单 interval +
|
|
833
|
+
* §9.12 空闲看门狗:单 interval + 截止时间判定,普通 run 与 compact
|
|
794
834
|
* 共用同一套兜底。事件到达只 `touch()` 刷新时间戳(无 timer 操作);静默超过
|
|
795
835
|
* `idleTimeoutMs` 才 finish('idle_timeout') + runner.stop(),让该工作区的串行
|
|
796
836
|
* 队列继续推进(否则一个挂死的 run 永久堵住后续消息)。
|
|
@@ -839,7 +879,7 @@ export class Bridge {
|
|
|
839
879
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
840
880
|
*/
|
|
841
881
|
async runAgentStreamToEnd(runner, cardSession, sessionId, sessionEpochAtStart, ctx, cwd, runId, message, agentKind = this.config.defaultAgent) {
|
|
842
|
-
// Runner 声明的 usage
|
|
882
|
+
// Runner 声明的 usage 权威来源:codex app-server = 'live'
|
|
843
883
|
// (turn/started 的 tokenUsage.last 是本 turn 增量)。其余 runner 未声明。
|
|
844
884
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
845
885
|
// §9.12 空闲看门狗:判定口径见 startIdleWatchdog;武装点在 cardSession.start() 之后。
|
|
@@ -914,7 +954,7 @@ export class Bridge {
|
|
|
914
954
|
// Card session did not enter running state - still try to send static fallback
|
|
915
955
|
getLogger().info(`[lark-remote] cardSession not running, using static fallback`);
|
|
916
956
|
}
|
|
917
|
-
//
|
|
957
|
+
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
918
958
|
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
919
959
|
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
920
960
|
watchdog = this.startIdleWatchdog({
|
|
@@ -955,8 +995,8 @@ export class Bridge {
|
|
|
955
995
|
getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.session_id}`);
|
|
956
996
|
}
|
|
957
997
|
else {
|
|
958
|
-
//
|
|
959
|
-
//
|
|
998
|
+
// Use session's real directory from event.cwd (not runner's cwd parameter)
|
|
999
|
+
// guard empty string too -- `??` only catches null/undefined, but a
|
|
960
1000
|
// translator (or older build) may emit cwd="", which must NOT overwrite
|
|
961
1001
|
// the good runner cwd.
|
|
962
1002
|
const hasCwd = !!this.sessionStore.getCwd(ctx.userId);
|
|
@@ -1028,7 +1068,7 @@ export class Bridge {
|
|
|
1028
1068
|
: `[lark-remote] result event received runId=${runId}`);
|
|
1029
1069
|
// app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
|
|
1030
1070
|
// 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
|
|
1031
|
-
//
|
|
1071
|
+
// 里兜底上报。此处仅当 runner 声明 live usage(app-server)
|
|
1032
1072
|
// 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
|
|
1033
1073
|
// 此列(它们各自有 system.init / turn_started 写回)。
|
|
1034
1074
|
if (usageAuthority === 'live' &&
|
|
@@ -1121,7 +1161,7 @@ export class Bridge {
|
|
|
1121
1161
|
const coordinator = this.approvalCoordinators.get(runId);
|
|
1122
1162
|
coordinator?.updateView(event.requestId, event.view);
|
|
1123
1163
|
}
|
|
1124
|
-
// §9.12
|
|
1164
|
+
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1125
1165
|
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1126
1166
|
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1127
1167
|
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
@@ -1129,9 +1169,9 @@ export class Bridge {
|
|
|
1129
1169
|
await cardSession.push(event);
|
|
1130
1170
|
}
|
|
1131
1171
|
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${sawResult}`);
|
|
1132
|
-
// §9.12
|
|
1172
|
+
// §9.12 红线:for-await 自然结束 = CLI 已退出,
|
|
1133
1173
|
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1134
|
-
// 收尾处理跨过死线被误标 idle_timeout
|
|
1174
|
+
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(随本修复一并关闭)。
|
|
1135
1175
|
watchdog?.stop();
|
|
1136
1176
|
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1137
1177
|
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
@@ -1210,7 +1250,7 @@ export class Bridge {
|
|
|
1210
1250
|
}
|
|
1211
1251
|
catch (err) {
|
|
1212
1252
|
getLogger().error(`[lark-remote] claude run error runId=${runId}:`, err);
|
|
1213
|
-
//
|
|
1253
|
+
// 双保险:run() 的 finally 已负责杀子进程,但 catch 路径再补一次
|
|
1214
1254
|
// runner.stop()(与 bash 路径对齐),覆盖任何 finally 之外的残留窗口。
|
|
1215
1255
|
try {
|
|
1216
1256
|
await runner.stop();
|
|
@@ -1299,7 +1339,7 @@ export class Bridge {
|
|
|
1299
1339
|
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1300
1340
|
}
|
|
1301
1341
|
finally {
|
|
1302
|
-
//
|
|
1342
|
+
// cleanup must survive mid-finalize errors. The only unguarded
|
|
1303
1343
|
// expression in finalizeRun is the renderRunCard(...) argument evaluated
|
|
1304
1344
|
// at the sendResult call site; if it throws, the run must still release
|
|
1305
1345
|
// its activeRuns slot — otherwise the cwd is permanently busy
|
|
@@ -1312,7 +1352,7 @@ export class Bridge {
|
|
|
1312
1352
|
// evicted+disposed by clearRunners (/config) and interruptCurrentRun
|
|
1313
1353
|
// (/stop). Spawn-per-message runners are evicted as before to prevent
|
|
1314
1354
|
// stale process references accumulating.
|
|
1315
|
-
//
|
|
1355
|
+
// 配置变更期间(clearRunners 标了 stale)的 workspace-lifetime
|
|
1316
1356
|
// runner 也必须在 run 结束后 evict,否则下一轮复用旧配置实例。
|
|
1317
1357
|
const slotKey = `${cwd}\u0000${activeRun.agentKind}`;
|
|
1318
1358
|
if (activeRun.runner.lifetime !== 'workspace' || this.staleRunnerSlots.has(slotKey)) {
|
|
@@ -1322,7 +1362,7 @@ export class Bridge {
|
|
|
1322
1362
|
// Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
|
|
1323
1363
|
// 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
|
|
1324
1364
|
// interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
|
|
1325
|
-
//
|
|
1365
|
+
// 写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
|
|
1326
1366
|
// (codex app-server + kimi acp 均满足)。
|
|
1327
1367
|
const runTerminal = cardSession.currentState.terminal;
|
|
1328
1368
|
const runnerHasCompact = runnerHasRunCompact(activeRun.runner);
|
|
@@ -1335,11 +1375,7 @@ export class Bridge {
|
|
|
1335
1375
|
const compactSessionId = cardSession.currentState.sessionId ??
|
|
1336
1376
|
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
1337
1377
|
if (compactSessionId) {
|
|
1338
|
-
this.
|
|
1339
|
-
runId: activeRun.runId,
|
|
1340
|
-
sessionId: compactSessionId,
|
|
1341
|
-
agentKind: activeRun.agentKind,
|
|
1342
|
-
});
|
|
1378
|
+
this.recordCompactableRun(cwd, activeRun.runId, compactSessionId, activeRun.agentKind);
|
|
1343
1379
|
}
|
|
1344
1380
|
}
|
|
1345
1381
|
getLogger().info(`[lark-remote] activeRuns.delete cwd=${cwd} runId=${runId}`);
|
|
@@ -1354,7 +1390,7 @@ export class Bridge {
|
|
|
1354
1390
|
*/
|
|
1355
1391
|
async sendCompletionNotificationCard(sessionId, cwd, ctx, agentKind = this.config.defaultAgent) {
|
|
1356
1392
|
try {
|
|
1357
|
-
//
|
|
1393
|
+
// cap the events read for the notification card — without
|
|
1358
1394
|
// maxEvents claude returns every event after the last user message and
|
|
1359
1395
|
// the card blows past Feishu's 28KB budget.
|
|
1360
1396
|
const content = this.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd, {
|
|
@@ -1374,7 +1410,7 @@ export class Bridge {
|
|
|
1374
1410
|
actions: [resumeUseButton(sessionId, agentKind)],
|
|
1375
1411
|
headerTemplate: 'green',
|
|
1376
1412
|
});
|
|
1377
|
-
//
|
|
1413
|
+
// send through sendResult so enforceCardBudget guards the card
|
|
1378
1414
|
// (previously sent via connector directly, bypassing the budget — long
|
|
1379
1415
|
// sessions produced >28KB cards that Feishu silently rejected).
|
|
1380
1416
|
const sent = await this.sendResult({ card }, ctx);
|
|
@@ -1399,7 +1435,7 @@ export class Bridge {
|
|
|
1399
1435
|
withCoordinator(runId, fn) {
|
|
1400
1436
|
const coordinator = this.approvalCoordinators.get(runId);
|
|
1401
1437
|
if (!coordinator) {
|
|
1402
|
-
//
|
|
1438
|
+
// run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
|
|
1403
1439
|
// 不能静默。router 会把该异常转成 error toast。
|
|
1404
1440
|
getLogger().warn(`[lark-remote] no approval coordinator for runId=${runId.slice(0, 8)}`);
|
|
1405
1441
|
throw new Error('任务已结束,审批无法响应');
|
|
@@ -1463,7 +1499,7 @@ export class Bridge {
|
|
|
1463
1499
|
return this.withCoordinator(opts.runId, (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1464
1500
|
}
|
|
1465
1501
|
/**
|
|
1466
|
-
* Claude AskUserQuestion 自定义答案(Other
|
|
1502
|
+
* Claude AskUserQuestion 自定义答案(Other):自由文本直接
|
|
1467
1503
|
* 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
|
|
1468
1504
|
*/
|
|
1469
1505
|
async handleApprovalAnswerCustom(opts) {
|
|
@@ -1481,11 +1517,48 @@ export class Bridge {
|
|
|
1481
1517
|
log.info(`[lark-remote] handleApprovalAnswerNote runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
|
|
1482
1518
|
return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1483
1519
|
}
|
|
1520
|
+
/**
|
|
1521
|
+
* 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
|
|
1522
|
+
* sessionId + agentKind。runId 是按钮自带的唯一键,同 cwd 内唯一。
|
|
1523
|
+
* 超出上限按插入序淘汰最旧的一条(Map 保持插入序;重复写同一 runId
|
|
1524
|
+
* 先 delete 再 set 以刷新位置)。
|
|
1525
|
+
*/
|
|
1526
|
+
recordCompactableRun(cwd, runId, sessionId, agentKind) {
|
|
1527
|
+
let byRunId = this.compactableRuns.get(cwd);
|
|
1528
|
+
if (!byRunId) {
|
|
1529
|
+
byRunId = new Map();
|
|
1530
|
+
this.compactableRuns.set(cwd, byRunId);
|
|
1531
|
+
}
|
|
1532
|
+
byRunId.delete(runId);
|
|
1533
|
+
byRunId.set(runId, { sessionId, agentKind, cwd });
|
|
1534
|
+
while (byRunId.size > MAX_COMPACTABLE_RUNS_PER_CWD) {
|
|
1535
|
+
const oldest = byRunId.keys().next().value;
|
|
1536
|
+
if (oldest === undefined)
|
|
1537
|
+
break;
|
|
1538
|
+
byRunId.delete(oldest);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
|
|
1543
|
+
*
|
|
1544
|
+
* 为什么不带 cwd 过滤:按钮属于「那一次 run」,与用户此刻在哪个工作目录、
|
|
1545
|
+
* 排队期间有没有 /cd 无关。`/stop` 的 interruptCurrentRun 同样是全局按 runId
|
|
1546
|
+
* 找(从不查当前 cwd),compact 与它保持一致。用当前 cwd 收窄会让「切了工作区
|
|
1547
|
+
* 再点旧卡片」和「排队期间 /cd」两种情况误报「该任务已结束或不属于当前会话」。
|
|
1548
|
+
*/
|
|
1549
|
+
findCompactableRun(runId) {
|
|
1550
|
+
for (const byRunId of this.compactableRuns.values()) {
|
|
1551
|
+
const record = byRunId.get(runId);
|
|
1552
|
+
if (record)
|
|
1553
|
+
return record;
|
|
1554
|
+
}
|
|
1555
|
+
return undefined;
|
|
1556
|
+
}
|
|
1484
1557
|
/**
|
|
1485
1558
|
* Handle compact card action — trigger a compaction request for any
|
|
1486
|
-
* runCompact-capable runner(codex/kimi/opencode/pi/claude
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1559
|
+
* runCompact-capable runner(codex/kimi/opencode/pi/claude)。按钮带的是
|
|
1560
|
+
* 那次 run 的 runId,据此反查该 run 实际使用的 sessionId + agentKind
|
|
1561
|
+
* + cwd(compactableRuns,按 runId 记录),再调 runner 的 runCompact()。
|
|
1489
1562
|
*/
|
|
1490
1563
|
async handleCompactAction(value, ctx) {
|
|
1491
1564
|
const log = getLogger();
|
|
@@ -1495,36 +1568,32 @@ export class Bridge {
|
|
|
1495
1568
|
await this.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 runId' }, ctx);
|
|
1496
1569
|
return;
|
|
1497
1570
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
// 校验:run 结束后 activeRuns 已清空,用 lastCompactableRun 对照 runId。
|
|
1504
|
-
const last = this.lastCompactableRun.get(cwd);
|
|
1505
|
-
if (!last || last.runId !== runId) {
|
|
1571
|
+
// 校验:run 结束后 activeRuns 已清空,只能靠按钮自带的 runId 反查记录。
|
|
1572
|
+
// 按 runId 存(不按「最新的那一条」),点击到执行之间夹了新 run 收尾也不失效;
|
|
1573
|
+
// 查找也不按「当前 cwd」收窄(见 findCompactableRun)。
|
|
1574
|
+
const target = this.findCompactableRun(runId);
|
|
1575
|
+
if (!target) {
|
|
1506
1576
|
await this.sendResult({ text: '⚠️ 该任务已结束或不属于当前会话' }, ctx);
|
|
1507
1577
|
return;
|
|
1508
1578
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
return;
|
|
1512
|
-
}
|
|
1579
|
+
// 压缩必须用那次 run 自己的 cwd(session 文件 + runner 实例都按 cwd 解析)。
|
|
1580
|
+
const cwd = target.cwd;
|
|
1513
1581
|
// §5.3: kimi 压缩在途检测(wire 状态机)。第二次点击在本地毫秒级如实回答,
|
|
1514
1582
|
// 不开卡、不发 session/prompt——否则 kimi 服务端拒绝并产生「秒回」假完成。
|
|
1515
|
-
if (await this.rejectIfCompactionInFlight(
|
|
1583
|
+
if (await this.rejectIfCompactionInFlight(target.sessionId, cwd, target.agentKind, ctx)) {
|
|
1516
1584
|
return;
|
|
1585
|
+
}
|
|
1517
1586
|
// Check that the runner has runCompact method
|
|
1518
|
-
const runner = this.getRunner(cwd,
|
|
1587
|
+
const runner = this.getRunner(cwd, target.agentKind);
|
|
1519
1588
|
if (!runnerHasRunCompact(runner)) {
|
|
1520
1589
|
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
1521
1590
|
return;
|
|
1522
1591
|
}
|
|
1523
1592
|
log.info(`[lark-remote] handleCompactAction executing runCompact for runId=${runId} cwd=${cwd}`);
|
|
1524
1593
|
await this.streamCompact({
|
|
1525
|
-
sessionId:
|
|
1594
|
+
sessionId: target.sessionId,
|
|
1526
1595
|
cwd,
|
|
1527
|
-
agentKind:
|
|
1596
|
+
agentKind: target.agentKind,
|
|
1528
1597
|
runner,
|
|
1529
1598
|
ctx,
|
|
1530
1599
|
});
|
|
@@ -1599,7 +1668,7 @@ export class Bridge {
|
|
|
1599
1668
|
agentKind,
|
|
1600
1669
|
},
|
|
1601
1670
|
});
|
|
1602
|
-
//
|
|
1671
|
+
// 压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
1603
1672
|
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
1604
1673
|
// 压缩都只能等 runner 级超时,用户既停不掉也看不出它还在跑。
|
|
1605
1674
|
const activeRun = {
|
|
@@ -1731,9 +1800,10 @@ export class Bridge {
|
|
|
1731
1800
|
return;
|
|
1732
1801
|
}
|
|
1733
1802
|
// Resolve agent kind the same way resume.use does: card value wins,
|
|
1734
|
-
// otherwise fall back to defaultAgent.
|
|
1735
|
-
|
|
1736
|
-
|
|
1803
|
+
// otherwise fall back to defaultAgent. 名单来自单源 AGENT_KINDS —— 手抄一份
|
|
1804
|
+
// 必然再次漂移(这份曾经漏 dsh:带 dsh 的卡片会静默退回 defaultAgent,
|
|
1805
|
+
// 用错 reader 报「未找到 session」)。
|
|
1806
|
+
const agentKind = value.agent && AGENT_KINDS.includes(value.agent)
|
|
1737
1807
|
? value.agent
|
|
1738
1808
|
: this.config.defaultAgent;
|
|
1739
1809
|
// Session must exist in the current cwd (same rule as cmdResume /
|
|
@@ -1845,10 +1915,11 @@ export class Bridge {
|
|
|
1845
1915
|
}
|
|
1846
1916
|
for await (const event of bashRunner.run(command, { cwd })) {
|
|
1847
1917
|
if (event.type === 'stdout') {
|
|
1848
|
-
//
|
|
1849
|
-
// 有界并不会让 `output +=`
|
|
1850
|
-
//
|
|
1851
|
-
// O(n²) 截断成本(output 恒 ≤ CAP,拼接后
|
|
1918
|
+
// 层④:bridge 本地 output 也必须 store-time 截断——仅 session state
|
|
1919
|
+
// 有界并不会让 `output +=` 自然有界(「store 有界则本地拼接自然有界」
|
|
1920
|
+
// 不成立),!yes 洪峰下本地字符串仍全量驻留。capBashOutput 增量调用
|
|
1921
|
+
// O(CAP+chunk),不会带回 O(n²) 截断成本(output 恒 ≤ CAP,拼接后
|
|
1922
|
+
// ≤ CAP+chunk 再 slice 尾部)。
|
|
1852
1923
|
output = capBashOutput(output + event.content);
|
|
1853
1924
|
await cardSession.update({ output, stderr });
|
|
1854
1925
|
}
|
|
@@ -1884,7 +1955,7 @@ export class Bridge {
|
|
|
1884
1955
|
const settleResult = await cardSession.settle();
|
|
1885
1956
|
if (settleResult === 'unsent') {
|
|
1886
1957
|
// Stream never established a card — send a static terminal card as fallback.
|
|
1887
|
-
// Use 2.0 schema to match the streaming card (
|
|
1958
|
+
// Use 2.0 schema to match the streaming card (avoid 2.0→1.x downgrade).
|
|
1888
1959
|
try {
|
|
1889
1960
|
await this.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
|
|
1890
1961
|
}
|