lark-remote 0.4.4 → 0.5.0
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 +28 -0
- package/dist/binder.d.ts +11 -10
- package/dist/binder.js +17 -16
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.d.ts +1 -27
- package/dist/bridge/inbound-media.js +23 -21
- package/dist/bridge/index.d.ts +57 -57
- package/dist/bridge/index.js +136 -129
- package/dist/bridge/queue-manager.d.ts +45 -38
- package/dist/bridge/queue-manager.js +136 -136
- 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 +5 -5
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +22 -22
- 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 +5 -6
- package/dist/clone.d.ts +6 -9
- package/dist/clone.js +12 -8
- package/dist/common/clamp.d.ts +2 -0
- package/dist/common/clamp.js +5 -0
- package/dist/common/fs.d.ts +4 -3
- package/dist/common/fs.js +5 -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.d.ts +4 -5
- package/dist/config/dir.js +12 -10
- 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/config/wizard.js +2 -2
- package/dist/connector/index.d.ts +5 -7
- package/dist/connector/index.js +29 -16
- 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/inbound/receipt.d.ts +1 -3
- package/dist/inbound/receipt.js +2 -2
- package/dist/inbound/turn-assembler.d.ts +1 -3
- package/dist/inbound/turn-assembler.js +2 -2
- package/dist/index.js +38 -30
- package/dist/instance-lock.js +8 -7
- 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 +9 -5
- package/dist/platform/agent-stopper.d.ts +2 -2
- 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 +6 -6
- package/dist/platform/terminator-win32.js +3 -3
- 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/card-helpers.d.ts +1 -3
- package/dist/router/card-helpers.js +2 -2
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +3 -3
- package/dist/router/config/common/model-patch.d.ts +1 -2
- package/dist/router/config/common/model-patch.js +1 -2
- package/dist/router/config/opencode.js +2 -2
- package/dist/router/config/pi.js +1 -1
- package/dist/router/index.d.ts +30 -30
- package/dist/router/index.js +103 -84
- package/dist/router/order-exec-dispatch.d.ts +3 -10
- package/dist/router/order-exec-dispatch.js +5 -5
- package/dist/router/{utils.d.ts → usage-format.d.ts} +2 -4
- package/dist/router/{utils.js → usage-format.js} +3 -8
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.d.ts +1 -1
- package/dist/runner/claude/runner.js +2 -3
- package/dist/runner/claude/session.d.ts +4 -6
- package/dist/runner/claude/session.js +21 -23
- package/dist/runner/codex/app-server/runner.js +3 -3
- 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/{protocol-helpers.d.ts → acp-approval.d.ts} +4 -10
- package/dist/runner/common/acp/{protocol-helpers.js → acp-approval.js} +7 -6
- package/dist/runner/common/acp/base-acp-runner.d.ts +1 -1
- package/dist/runner/common/acp/base-acp-runner.js +2 -2
- 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 +5 -5
- package/dist/runner/common/connection-based-runner.js +5 -5
- 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 +13 -13
- package/dist/runner/common/jsonrpc/connection-manager.js +37 -37
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/{usage.d.ts → result-usage.d.ts} +1 -1
- package/dist/runner/common/{usage.js → result-usage.js} +2 -2
- package/dist/runner/common/spawning-runner.d.ts +23 -10
- package/dist/runner/common/spawning-runner.js +48 -22
- package/dist/runner/common/{runner-utils.d.ts → synthetic-events.d.ts} +4 -4
- package/dist/runner/common/{runner-utils.js → synthetic-events.js} +5 -5
- package/dist/runner/dsh/client.d.ts +1 -1
- package/dist/runner/dsh/client.js +1 -1
- package/dist/runner/dsh/runner.js +3 -3
- package/dist/runner/kimi/acp/runner.d.ts +4 -4
- package/dist/runner/kimi/acp/runner.js +10 -10
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +4 -4
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/opencode/acp/translator.js +2 -2
- package/dist/runner/pi/rpc/runner.d.ts +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -2
- package/dist/runner/pi/rpc/translator.js +4 -6
- package/dist/runner/registry.d.ts +3 -3
- package/dist/runner/registry.js +3 -3
- package/dist/runner/types.d.ts +1 -1
- package/dist/session/claude/session-index.d.ts +13 -1
- package/dist/session/claude/session-index.js +21 -6
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/session-reader.js +1 -1
- 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 +14 -14
- 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.d.ts +1 -1
- package/dist/session/dsh/sessions.js +3 -3
- package/dist/session/kimi/sessions.d.ts +2 -6
- package/dist/session/kimi/sessions.js +13 -19
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +15 -13
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/registry.d.ts +1 -1
- package/dist/session/registry.js +1 -1
- package/dist/session/session-store.d.ts +3 -4
- package/dist/session/session-store.js +3 -5
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/version-check.d.ts +5 -3
- package/dist/update/version-check.js +10 -8
- package/dist/workspace/index.d.ts +4 -4
- package/dist/workspace/index.js +5 -5
- package/package.json +1 -1
package/dist/bridge/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
|
|
|
9
9
|
import { buildSessionHistoryCard } from '../router/card-helpers.js';
|
|
10
10
|
import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
11
11
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
12
|
-
import { normalizeResultUsage } from '../runner/common/usage.js';
|
|
12
|
+
import { normalizeResultUsage } from '../runner/common/result-usage.js';
|
|
13
13
|
import { BashProcessRunner } from '../runner/index.js';
|
|
14
14
|
import { QueueManager, } from './queue-manager.js';
|
|
15
15
|
import { ApprovalCoordinator, decisionToApprovalAction } from './approval-coordinator.js';
|
|
@@ -18,13 +18,13 @@ import { InboundMediaHandler } from './inbound-media.js';
|
|
|
18
18
|
* Max events to read for the completion notification card (mirror router's
|
|
19
19
|
* AUTO_RESUME_MAX_EVENTS = 5). Without a cap, readSessionContent returns every
|
|
20
20
|
* event after the last user message and the card blows past Feishu's 28KB
|
|
21
|
-
* budget
|
|
21
|
+
* budget.
|
|
22
22
|
*/
|
|
23
23
|
const COMPLETION_NOTIFICATION_MAX_EVENTS = 5;
|
|
24
24
|
/** Approval request timeout (ms) — pending approval expires after this. */
|
|
25
25
|
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
26
|
/**
|
|
27
|
-
* runner
|
|
27
|
+
* runner 缓存的工作区数量上限:超过后创建新工作区槽位时
|
|
28
28
|
* 顺带回收「非 active 且进程已停」的死缓存,防止随访问过的工作区数量
|
|
29
29
|
* 无界增长。活进程/在途 run 一律保留(workspace-lifetime 长驻连接不能误杀)。
|
|
30
30
|
*/
|
|
@@ -50,8 +50,8 @@ function terminalReactionEmoji(terminal) {
|
|
|
50
50
|
return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* 单点化:`'runCompact' in runner` 鸭子探测
|
|
54
|
+
* codex app-server + kimi/opencode ACP runner 满足。
|
|
55
55
|
*/
|
|
56
56
|
function runnerHasRunCompact(runner) {
|
|
57
57
|
return ('runCompact' in runner && typeof runner.runCompact === 'function');
|
|
@@ -62,10 +62,10 @@ function runnerHasRunCompact(runner) {
|
|
|
62
62
|
*
|
|
63
63
|
* The router delegates non-command messages here via `forwardToClaude` and
|
|
64
64
|
* sends command results via `sendResult`. Serial processing is enforced per
|
|
65
|
-
*
|
|
66
|
-
* most one agent process runs per
|
|
65
|
+
* cwd by `enqueue`: each cwd has its own Promise chain, so at
|
|
66
|
+
* most one agent process runs per cwd at a time, while different
|
|
67
67
|
* workspaces run in parallel (see `activeRuns`, §9.6). This is a load-bearing
|
|
68
|
-
* invariant — concurrent spawns within one
|
|
68
|
+
* invariant — concurrent spawns within one cwd would corrupt session
|
|
69
69
|
* state. Most card actions go through `enqueueImmediate` (fire-and-forget,
|
|
70
70
|
* outside the serial chain); only run-forwarding is serialized.
|
|
71
71
|
*
|
|
@@ -98,8 +98,8 @@ export class Bridge {
|
|
|
98
98
|
*/
|
|
99
99
|
runners = new Map();
|
|
100
100
|
/**
|
|
101
|
-
* Runner 槽位在「活跃运行期间配置变更」时被标 stale
|
|
102
|
-
* 活跃
|
|
101
|
+
* Runner 槽位在「活跃运行期间配置变更」时被标 stale:clearRunners() 对
|
|
102
|
+
* 活跃 cwd 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
|
|
103
103
|
* finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
|
|
104
104
|
* key 为 `${cwd}\u0000${agentKind}`。
|
|
105
105
|
*/
|
|
@@ -107,7 +107,7 @@ export class Bridge {
|
|
|
107
107
|
/** Queue manager for per-workspace serial processing queue. */
|
|
108
108
|
/** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
|
|
109
109
|
queueManager;
|
|
110
|
-
/** Active runs per
|
|
110
|
+
/** Active runs per cwd (cwd). Multiple workspaces can run in parallel. */
|
|
111
111
|
activeRuns = new Map();
|
|
112
112
|
/** Approval coordinators keyed by runId. */
|
|
113
113
|
approvalCoordinators = new Map();
|
|
@@ -115,11 +115,11 @@ export class Bridge {
|
|
|
115
115
|
* 最近一次已完成的、runner 有 runCompact 的 run(按 cwd)。run 结束后
|
|
116
116
|
* activeRuns 已清空,Compact 需要它来校验 runId 并提供 sessionId。
|
|
117
117
|
*/
|
|
118
|
-
|
|
118
|
+
lastCompactableRun = new Map();
|
|
119
119
|
/**
|
|
120
120
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
121
121
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
122
|
-
*
|
|
122
|
+
* cwd, so they live outside `activeRuns`. Tracked so `/stop` can reach
|
|
123
123
|
* the per-run BashRunner instance via interruptCurrentRun.
|
|
124
124
|
*/
|
|
125
125
|
activeBashRuns = new Map();
|
|
@@ -131,29 +131,29 @@ export class Bridge {
|
|
|
131
131
|
this.idleTimeoutMs = deps.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
132
132
|
this.agentRegistry = deps.agentRegistry;
|
|
133
133
|
this.sessionReaderRegistry = deps.sessionReaderRegistry;
|
|
134
|
-
this.queueManager = new QueueManager((
|
|
134
|
+
this.queueManager = new QueueManager((cwd) => this.activeRuns.has(cwd), (chatId, card, opts) => this.connector.sendWithRetry(chatId, { card }, { replyTo: opts?.replyTo }), (messageId, card) => this.connector.updateCard(messageId, card));
|
|
135
135
|
this.mediaHandler = new InboundMediaHandler({
|
|
136
136
|
resolveCwd: (userId) => this.resolveCwd(userId),
|
|
137
137
|
getConfig: () => this.config,
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
-
* Get or create a runner for the given
|
|
141
|
+
* Get or create a runner for the given cwd, keyed by current defaultAgent.
|
|
142
142
|
*
|
|
143
143
|
* Fix 4 (2026-07-18): Cache key includes agentKind, so switching defaultAgent
|
|
144
144
|
* creates a NEW runner under a different key without evicting the old one.
|
|
145
145
|
* This eliminates the need for clearRunners() on agent switch.
|
|
146
146
|
*/
|
|
147
|
-
getRunner(
|
|
148
|
-
// Fix 4: Check for runner under (
|
|
149
|
-
const workspaceMap = this.runners.get(
|
|
147
|
+
getRunner(cwd, kind = this.config.defaultAgent) {
|
|
148
|
+
// Fix 4: Check for runner under (cwd, kind) key
|
|
149
|
+
const workspaceMap = this.runners.get(cwd);
|
|
150
150
|
if (workspaceMap?.has(kind)) {
|
|
151
151
|
return workspaceMap.get(kind);
|
|
152
152
|
}
|
|
153
|
-
//
|
|
153
|
+
// 缓存超过上限且是新工作区时才回收死槽位——刚创建未 run
|
|
154
154
|
// 的 runner 同样是 isRunning=false,小缓存下立即回收会破坏「创建即注册」
|
|
155
155
|
// 的既有语义(exit-dispatcher 计数回归)。
|
|
156
|
-
if (this.runners.size >= MAX_CACHED_RUNNER_WORKSPACES && !this.runners.has(
|
|
156
|
+
if (this.runners.size >= MAX_CACHED_RUNNER_WORKSPACES && !this.runners.has(cwd)) {
|
|
157
157
|
for (const [cwd, slotMap] of [...this.runners.entries()]) {
|
|
158
158
|
if (this.activeRuns.has(cwd))
|
|
159
159
|
continue;
|
|
@@ -164,8 +164,8 @@ export class Bridge {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
// Create new runner for this (
|
|
168
|
-
const runner = this.agentRegistry.get(kind,
|
|
167
|
+
// Create new runner for this (cwd, kind) slot
|
|
168
|
+
const runner = this.agentRegistry.get(kind, cwd);
|
|
169
169
|
// Call lifecycle methods for agent runners created via factory
|
|
170
170
|
if (runner && typeof runner.killOrphan === 'function') {
|
|
171
171
|
runner.killOrphan();
|
|
@@ -173,9 +173,9 @@ export class Bridge {
|
|
|
173
173
|
if (runner && typeof runner.registerExitHandlers === 'function') {
|
|
174
174
|
runner.registerExitHandlers();
|
|
175
175
|
}
|
|
176
|
-
// Store in nested map: Map<
|
|
176
|
+
// Store in nested map: Map<cwd, Map<kind, Runner>>
|
|
177
177
|
if (!workspaceMap) {
|
|
178
|
-
this.runners.set(
|
|
178
|
+
this.runners.set(cwd, new Map([[kind, runner]]));
|
|
179
179
|
}
|
|
180
180
|
else {
|
|
181
181
|
workspaceMap.set(kind, runner);
|
|
@@ -241,25 +241,29 @@ export class Bridge {
|
|
|
241
241
|
return undefined;
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
/** Get the current runner for a
|
|
245
|
-
getCurrentRunner(
|
|
246
|
-
return this.getRunner(
|
|
244
|
+
/** Get the current runner for a cwd. Used by router for agent-specific commands (.2). */
|
|
245
|
+
getCurrentRunner(cwd) {
|
|
246
|
+
return this.getRunner(cwd);
|
|
247
247
|
}
|
|
248
248
|
/**
|
|
249
249
|
* Whether the runner for the given agent kind has runCompact capability.
|
|
250
|
-
* Used by router for compact button gating (
|
|
250
|
+
* Used by router for compact button gating (duck-typing
|
|
251
251
|
* instead of hardcoding agentKind === 'codex').
|
|
252
252
|
*/
|
|
253
|
-
hasRunCompact(
|
|
254
|
-
const runner = this.getRunner(
|
|
253
|
+
hasRunCompact(cwd, kind = this.config.defaultAgent) {
|
|
254
|
+
const runner = this.getRunner(cwd, kind);
|
|
255
255
|
return runnerHasRunCompact(runner);
|
|
256
256
|
}
|
|
257
257
|
/** 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
|
|
258
|
-
*
|
|
259
|
-
* 执行时刻,避免 /new、/config 在排队期间改写 live
|
|
258
|
+
* 唯一捕获点:排队消息在入队时刻把 agent+session 钉进 AgentBinding,随任务闭包带到执行时刻
|
|
259
|
+
* 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移。 */
|
|
260
260
|
currentBinding(userId) {
|
|
261
261
|
const agent = this.config.defaultAgent;
|
|
262
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
agent,
|
|
264
|
+
sessionId: this.sessionStore.getSessionId(userId, agent),
|
|
265
|
+
sessionEpoch: this.sessionStore.getSessionEpoch(userId, agent),
|
|
266
|
+
};
|
|
263
267
|
}
|
|
264
268
|
/**
|
|
265
269
|
* Clear all cached runner instances. Call after config changes that affect runner config.
|
|
@@ -274,7 +278,7 @@ export class Bridge {
|
|
|
274
278
|
// ends, and the next run picks up the new config/agent via getRunner.
|
|
275
279
|
for (const cwd of [...this.runners.keys()]) {
|
|
276
280
|
if (this.activeRuns.has(cwd)) {
|
|
277
|
-
//
|
|
281
|
+
// 活跃 cwd 不能立即 evict(会误杀长驻连接),但必须标记 stale,
|
|
278
282
|
// 否则 finalizeRun 对 workspace-lifetime runner 也不 evict → 下一轮复用旧配置实例。
|
|
279
283
|
// 这里只标待淘汰,当前 run 结束后由 finalizeRun 安全 evict。
|
|
280
284
|
for (const kind of [...this.runners.get(cwd).keys()]) {
|
|
@@ -290,7 +294,7 @@ export class Bridge {
|
|
|
290
294
|
}
|
|
291
295
|
/**
|
|
292
296
|
* Evict a (cwd, agentKind) runner cache slot. Unregisters the runner from the
|
|
293
|
-
* process-level exit dispatcher first (
|
|
297
|
+
* process-level exit dispatcher first (without removal the singleton
|
|
294
298
|
* Set<Runner> retains every historical runner instance, so memory still grows
|
|
295
299
|
* with run count), then deletes the slot. Every runner-discarding path
|
|
296
300
|
* (clearRunners / finalizeRun / interruptCurrentRun) must go through this.
|
|
@@ -365,48 +369,48 @@ export class Bridge {
|
|
|
365
369
|
* Enqueue a task into the workspace-level serial bridge queue.
|
|
366
370
|
* Delegates to QueueManager for actual implementation.
|
|
367
371
|
*/
|
|
368
|
-
enqueue(
|
|
369
|
-
this.queueManager.enqueue(
|
|
372
|
+
enqueue(cwd, task, opts) {
|
|
373
|
+
this.queueManager.enqueue(cwd, task, opts);
|
|
370
374
|
}
|
|
371
375
|
/**
|
|
372
376
|
* Execute a task immediately without going through the queue.
|
|
373
377
|
* Used for / commands that should respond immediately.
|
|
374
378
|
*/
|
|
375
|
-
enqueueImmediate(
|
|
376
|
-
this.queueManager.enqueueImmediate(
|
|
379
|
+
enqueueImmediate(cwd, task) {
|
|
380
|
+
this.queueManager.enqueueImmediate(cwd, task);
|
|
377
381
|
}
|
|
378
|
-
/** Get queue info for a
|
|
379
|
-
getQueueInfo(
|
|
380
|
-
return this.queueManager.getQueueInfo(
|
|
382
|
+
/** Get queue info for a cwd. */
|
|
383
|
+
getQueueInfo(cwd) {
|
|
384
|
+
return this.queueManager.getQueueInfo(cwd);
|
|
381
385
|
}
|
|
382
386
|
/** Remove a task from the queue by messageId. Returns true if found and removed. */
|
|
383
|
-
removeFromQueue(
|
|
384
|
-
return this.queueManager.removeFromQueue(
|
|
387
|
+
removeFromQueue(cwd, messageId) {
|
|
388
|
+
return this.queueManager.removeFromQueue(cwd, messageId);
|
|
385
389
|
}
|
|
386
390
|
/**
|
|
387
391
|
* One-shot: replace the execution closure of an existing queued task in
|
|
388
392
|
* place, preserving its queue position. Used by queue.immediate after the
|
|
389
393
|
* user edits a queued message (see router handleQueueImmediate).
|
|
390
394
|
*/
|
|
391
|
-
setTaskReplacement(
|
|
392
|
-
this.queueManager.setTaskReplacement(
|
|
395
|
+
setTaskReplacement(cwd, messageId, task) {
|
|
396
|
+
this.queueManager.setTaskReplacement(cwd, messageId, task);
|
|
393
397
|
}
|
|
394
398
|
/** Update queue card to "cancelled" status when user clicks 撤销. */
|
|
395
|
-
async updateQueueCardToCancelled(
|
|
396
|
-
await this.queueManager.updateQueueCardToCancelled(
|
|
399
|
+
async updateQueueCardToCancelled(cwd, messageId) {
|
|
400
|
+
await this.queueManager.updateQueueCardToCancelled(cwd, messageId);
|
|
397
401
|
}
|
|
398
402
|
/** Mark a queued task's card as executing immediately (grey out buttons).
|
|
399
403
|
* Called by handleQueueImmediate so the user sees feedback right away
|
|
400
404
|
* instead of waiting for the queue callback. Idempotent: the queue
|
|
401
405
|
* callback's own updateQueueCardToExecuting call becomes a no-op because
|
|
402
406
|
* updateQueueCardToExecuting deletes the card-message mapping in finally. */
|
|
403
|
-
async markQueueCardExecuting(
|
|
404
|
-
const task = this.getQueuedTask(
|
|
405
|
-
await this.queueManager.updateQueueCardToExecuting(
|
|
407
|
+
async markQueueCardExecuting(cwd, messageId) {
|
|
408
|
+
const task = this.getQueuedTask(cwd, messageId);
|
|
409
|
+
await this.queueManager.updateQueueCardToExecuting(cwd, messageId, task?.messagePreview ?? '', false);
|
|
406
410
|
}
|
|
407
411
|
/** Get task metadata from queue. */
|
|
408
|
-
getQueuedTask(
|
|
409
|
-
return this.queueManager.getQueuedTask(
|
|
412
|
+
getQueuedTask(cwd, messageId) {
|
|
413
|
+
return this.queueManager.getQueuedTask(cwd, messageId);
|
|
410
414
|
}
|
|
411
415
|
/**
|
|
412
416
|
* Whether a task with the given messageId has begun executing and has not
|
|
@@ -418,9 +422,9 @@ export class Bridge {
|
|
|
418
422
|
hasTaskBegan(messageId) {
|
|
419
423
|
return this.queueManager.hasBegan(messageId);
|
|
420
424
|
}
|
|
421
|
-
/** Get all queued tasks for a
|
|
422
|
-
getQueuedTasks(
|
|
423
|
-
return this.queueManager.getQueuedTasks(
|
|
425
|
+
/** Get all queued tasks for a cwd. Returns a copy to prevent aliasing bugs. */
|
|
426
|
+
getQueuedTasks(cwd) {
|
|
427
|
+
return this.queueManager.getQueuedTasks(cwd);
|
|
424
428
|
}
|
|
425
429
|
/**
|
|
426
430
|
* Update the messagePreview for a queued task and build the updated queue
|
|
@@ -429,18 +433,18 @@ export class Bridge {
|
|
|
429
433
|
* queued. Returning the card via the callback response avoids the
|
|
430
434
|
* PATCH-vs-callback-response race that left the card stuck in edit state.
|
|
431
435
|
*/
|
|
432
|
-
async updateMessagePreview(
|
|
433
|
-
const updated = this.queueManager.updateQueuedTaskMessage(
|
|
436
|
+
async updateMessagePreview(cwd, messageId, newMessage) {
|
|
437
|
+
const updated = this.queueManager.updateQueuedTaskMessage(cwd, messageId, newMessage);
|
|
434
438
|
if (!updated)
|
|
435
439
|
return null;
|
|
436
|
-
return this.queueManager.buildQueueCardForEdit(
|
|
440
|
+
return this.queueManager.buildQueueCardForEdit(cwd, messageId, newMessage);
|
|
437
441
|
}
|
|
438
|
-
/** Whether a claude run is currently in progress in the given
|
|
439
|
-
isBusyFor(
|
|
440
|
-
return this.activeRuns.has(
|
|
442
|
+
/** Whether a claude run is currently in progress in the given cwd. */
|
|
443
|
+
isBusyFor(cwd) {
|
|
444
|
+
return this.activeRuns.has(cwd);
|
|
441
445
|
}
|
|
442
|
-
getActiveRunFor(
|
|
443
|
-
const active = this.activeRuns.get(
|
|
446
|
+
getActiveRunFor(cwd) {
|
|
447
|
+
const active = this.activeRuns.get(cwd);
|
|
444
448
|
if (!active)
|
|
445
449
|
return undefined;
|
|
446
450
|
const state = active.session.currentState;
|
|
@@ -457,7 +461,7 @@ export class Bridge {
|
|
|
457
461
|
cacheCreationTokens: state.cacheCreationTokens,
|
|
458
462
|
};
|
|
459
463
|
}
|
|
460
|
-
/** Whether any claude run is currently in progress (any
|
|
464
|
+
/** Whether any claude run is currently in progress (any cwd). */
|
|
461
465
|
get isBusy() {
|
|
462
466
|
return this.activeRuns.size > 0;
|
|
463
467
|
}
|
|
@@ -515,7 +519,7 @@ export class Bridge {
|
|
|
515
519
|
async interruptCurrentRun(input) {
|
|
516
520
|
// Find the active claude run matching this user/chat
|
|
517
521
|
for (const [cwd, active] of this.activeRuns) {
|
|
518
|
-
if (input.
|
|
522
|
+
if (input.cwd !== undefined && cwd !== input.cwd)
|
|
519
523
|
continue;
|
|
520
524
|
if (active.userId === input.userId && active.chatId === input.chatId) {
|
|
521
525
|
if (input.runId && active.runId !== input.runId)
|
|
@@ -531,7 +535,7 @@ export class Bridge {
|
|
|
531
535
|
// below may outlive the task's settle, letting the serial chain advance
|
|
532
536
|
// to a successor that begins executing before we resume. An
|
|
533
537
|
// unconditional reset would then zero the successor's count and mark
|
|
534
|
-
// its slot interrupted, hiding it from queue cards
|
|
538
|
+
// its slot interrupted, hiding it from queue cards.
|
|
535
539
|
const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
|
|
536
540
|
await Promise.allSettled([
|
|
537
541
|
// 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
|
|
@@ -542,13 +546,13 @@ export class Bridge {
|
|
|
542
546
|
]);
|
|
543
547
|
// The chain may have advanced while the stop was in flight: a new run
|
|
544
548
|
// (from a task that began during the stop window) may now occupy this
|
|
545
|
-
//
|
|
549
|
+
// cwd. Only delete the entry we actually stopped.
|
|
546
550
|
if (this.activeRuns.get(cwd) === active) {
|
|
547
551
|
this.activeRuns.delete(cwd);
|
|
548
552
|
}
|
|
549
553
|
// Fix 4: Clean up this specific (cwd, agentKind) runner slot
|
|
550
|
-
// (
|
|
551
|
-
//
|
|
554
|
+
// (eviction must also unregister the runner from the exit dispatcher)
|
|
555
|
+
// evict by the run's captured agentKind, not the live
|
|
552
556
|
// defaultAgent (may have changed mid-run via /config).
|
|
553
557
|
this.evictRunnerSlot(cwd, active.agentKind);
|
|
554
558
|
// Reset queue executing count: the task was interrupted externally, so
|
|
@@ -570,7 +574,7 @@ export class Bridge {
|
|
|
570
574
|
// Then check active bash runs (! commands). These bypass the serial queue
|
|
571
575
|
// and are tracked separately; stop the per-run BashRunner directly.
|
|
572
576
|
for (const [runId, b] of this.activeBashRuns) {
|
|
573
|
-
if (input.
|
|
577
|
+
if (input.cwd !== undefined && b.cwd !== input.cwd)
|
|
574
578
|
continue;
|
|
575
579
|
if (b.userId === input.userId && b.chatId === input.chatId) {
|
|
576
580
|
if (input.runId && runId !== input.runId)
|
|
@@ -679,7 +683,7 @@ export class Bridge {
|
|
|
679
683
|
}
|
|
680
684
|
/**
|
|
681
685
|
* Resolve the working directory for a user.
|
|
682
|
-
* First checks sessionStore (set by /cd), then falls back to first
|
|
686
|
+
* First checks sessionStore (set by /cd), then falls back to first cwd.
|
|
683
687
|
* Returns undefined if no cwd is available.
|
|
684
688
|
*/
|
|
685
689
|
resolveCwd(userId) {
|
|
@@ -691,7 +695,7 @@ export class Bridge {
|
|
|
691
695
|
// NOTE: fallback uses insertion order, not sort preference — by design
|
|
692
696
|
// (cwd fallback stays insertion-order for now)
|
|
693
697
|
cwd = workspaces[0].path;
|
|
694
|
-
getLogger().info(`[lark-remote] resolveCwd fallback cwd=${cwd} (first saved
|
|
698
|
+
getLogger().info(`[lark-remote] resolveCwd fallback cwd=${cwd} (first saved cwd)`);
|
|
695
699
|
}
|
|
696
700
|
}
|
|
697
701
|
return cwd;
|
|
@@ -705,9 +709,9 @@ export class Bridge {
|
|
|
705
709
|
* updates the same run card, and terminal events finalize that card.
|
|
706
710
|
*/
|
|
707
711
|
async forwardToClaude(message, ctx, opts) {
|
|
708
|
-
//
|
|
712
|
+
// agentKind 一律来自绑定。无 binding 时保持现状(live defaultAgent)。
|
|
709
713
|
const agentKind = opts?.binding?.agent ?? this.config.defaultAgent;
|
|
710
|
-
//
|
|
714
|
+
// lane 与执行 cwd 同源。消息入队时(index.ts 闭包)捕获的 cwd
|
|
711
715
|
// 作为 cwdOverride 显式传入;执行时若 sessionStore cwd 已被 /cd 等命令改写,
|
|
712
716
|
// 以 lane 为准 —— 否则旧 lane 的排队消息会被 busy-drop 静默丢失。空串视为
|
|
713
717
|
// 未提供(与入队前无 cwd 的兜底语义一致)。
|
|
@@ -720,21 +724,24 @@ export class Bridge {
|
|
|
720
724
|
const entry = this.sessionStore.get(ctx.userId);
|
|
721
725
|
getLogger().debug(`[lark-remote] forward entry userId=${ctx.userId} chatId=${ctx.chatId} cwd=${cwd} ` +
|
|
722
726
|
`agent=${agentKind} sessionId=${opts?.binding?.sessionId ?? entry?.sessions?.get(agentKind) ?? '(none)'} message=${message.slice(0, 100)}`);
|
|
723
|
-
// Check if current
|
|
727
|
+
// Check if current cwd already has a run in progress
|
|
724
728
|
if (this.activeRuns.has(cwd)) {
|
|
725
|
-
getLogger().warn(`[lark-remote]
|
|
726
|
-
await this.sendResult({ text: '
|
|
729
|
+
getLogger().warn(`[lark-remote] cwd busy, dropping message userId=${ctx.userId} cwd=${cwd}`);
|
|
730
|
+
await this.sendResult({ text: '此工作目录正在处理中,请 /stop 后重试' }, ctx);
|
|
727
731
|
return;
|
|
728
732
|
}
|
|
729
733
|
// Note: Multiple workspaces can have runs in parallel. If runner is busy,
|
|
730
734
|
// runner.run() will throw and we'll catch it below.
|
|
731
|
-
//
|
|
735
|
+
// sessionId 钉死语义——binding 有 sessionId 时用它(入队时刻快照),
|
|
732
736
|
// 否则跟随 live store(无 binding 或入队时无 session 的正常路径)。
|
|
733
737
|
const sessionId = opts?.binding?.sessionId ?? this.sessionStore.getSessionId(ctx.userId, agentKind);
|
|
734
|
-
// 会话代际快照(2026-08-09
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
+
// 会话代际快照(2026-08-09,2026-10-07 前移):/new、/cd、/resume 会 bump
|
|
739
|
+
// epoch,该 run 后续 system.init 的 sessionId 写回即判 stale 跳过。
|
|
740
|
+
// 快照点优先取 binding.sessionEpoch(入队/commit 时刻,与 sessionId 同刻):
|
|
741
|
+
// 排队消息在「commit 后、开跑前」被 reset 时,执行起点捕获会漏判(epoch 已是
|
|
742
|
+
// bump 后的值)→ 旧 sessionId 被写回复活 reset。无 binding 时回退执行起点捕获,
|
|
743
|
+
// 保持非排队路径(如 /compact)的既有语义。
|
|
744
|
+
const sessionEpochAtStart = opts?.binding?.sessionEpoch ?? this.sessionStore.getSessionEpoch(ctx.userId, agentKind);
|
|
738
745
|
// Resolve the runner BEFORE creating the run session so the exact instance
|
|
739
746
|
// that will run is captured into activeRun. interruptCurrentRun stops THAT
|
|
740
747
|
// instance directly -- never a fresh runner re-resolved from a cache that
|
|
@@ -754,8 +761,8 @@ export class Bridge {
|
|
|
754
761
|
createRunSession(ctx, cwd, runner, agentKind = this.config.defaultAgent) {
|
|
755
762
|
const runId = randomUUID();
|
|
756
763
|
getLogger().debug(`[lark-remote] creating runCard runId=${runId}`);
|
|
757
|
-
//
|
|
758
|
-
// 缓存键 (cwd, agentKind) 和 eviction
|
|
764
|
+
// agentKind 来自入队绑定(forwardToClaude 解析),不再读 live config。
|
|
765
|
+
// 缓存键 (cwd, agentKind) 和 eviction 都用这个值。
|
|
759
766
|
const cardSession = new RunCardSession({
|
|
760
767
|
connector: this.connector,
|
|
761
768
|
chatId: ctx.chatId,
|
|
@@ -783,7 +790,7 @@ export class Bridge {
|
|
|
783
790
|
return { runId, cardSession, activeRun };
|
|
784
791
|
}
|
|
785
792
|
/**
|
|
786
|
-
* §9.12 空闲看门狗:单 interval +
|
|
793
|
+
* §9.12 空闲看门狗:单 interval + 截止时间判定,普通 run 与 compact
|
|
787
794
|
* 共用同一套兜底。事件到达只 `touch()` 刷新时间戳(无 timer 操作);静默超过
|
|
788
795
|
* `idleTimeoutMs` 才 finish('idle_timeout') + runner.stop(),让该工作区的串行
|
|
789
796
|
* 队列继续推进(否则一个挂死的 run 永久堵住后续消息)。
|
|
@@ -832,7 +839,7 @@ export class Bridge {
|
|
|
832
839
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
833
840
|
*/
|
|
834
841
|
async runAgentStreamToEnd(runner, cardSession, sessionId, sessionEpochAtStart, ctx, cwd, runId, message, agentKind = this.config.defaultAgent) {
|
|
835
|
-
// Runner 声明的 usage
|
|
842
|
+
// Runner 声明的 usage 权威来源:codex app-server = 'live'
|
|
836
843
|
// (turn/started 的 tokenUsage.last 是本 turn 增量)。其余 runner 未声明。
|
|
837
844
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
838
845
|
// §9.12 空闲看门狗:判定口径见 startIdleWatchdog;武装点在 cardSession.start() 之后。
|
|
@@ -872,7 +879,7 @@ export class Bridge {
|
|
|
872
879
|
// Reasoning tokens (pi usage.reasoning) from the live result event.
|
|
873
880
|
let finalReasoningTokens;
|
|
874
881
|
// 统一的 usage meta 构造(finalize 各分支共享,本方法内原先 5 处手写 11-15
|
|
875
|
-
// 字段对象;第 6 处
|
|
882
|
+
// 字段对象;第 6 处 streamCompact 形状不同未收敛)。
|
|
876
883
|
// catch 路径需覆盖 flow 字段时在展开后覆写即可。catch error 路径亦复用此
|
|
877
884
|
// 闭包,相比旧手写对象补齐了累计 cache 字段(cumulativeCacheReadTokens/
|
|
878
885
|
// cumulativeCacheCreationTokens),属有意的口径对齐(向 done 路径看齐)。
|
|
@@ -907,7 +914,7 @@ export class Bridge {
|
|
|
907
914
|
// Card session did not enter running state - still try to send static fallback
|
|
908
915
|
getLogger().info(`[lark-remote] cardSession not running, using static fallback`);
|
|
909
916
|
}
|
|
910
|
-
//
|
|
917
|
+
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
911
918
|
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
912
919
|
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
913
920
|
watchdog = this.startIdleWatchdog({
|
|
@@ -948,20 +955,20 @@ export class Bridge {
|
|
|
948
955
|
getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.session_id}`);
|
|
949
956
|
}
|
|
950
957
|
else {
|
|
951
|
-
//
|
|
952
|
-
//
|
|
958
|
+
// Use session's real directory from event.cwd (not runner's cwd parameter)
|
|
959
|
+
// guard empty string too -- `??` only catches null/undefined, but a
|
|
953
960
|
// translator (or older build) may emit cwd="", which must NOT overwrite
|
|
954
961
|
// the good runner cwd.
|
|
955
962
|
const hasCwd = !!this.sessionStore.getCwd(ctx.userId);
|
|
956
963
|
const realCwd = event.cwd && event.cwd.length > 0 ? event.cwd : cwd;
|
|
957
964
|
if (hasCwd && event.cwd && event.cwd !== cwd) {
|
|
958
965
|
// Session has its own cwd (e.g. EnterWorktree relocate). Keep the
|
|
959
|
-
//
|
|
960
|
-
getLogger().info(`[lark-remote] system.init:
|
|
966
|
+
// cwd unchanged; record the session's actual cwd in sessionCwds.
|
|
967
|
+
getLogger().info(`[lark-remote] system.init: cwd=${cwd}, session cwd=${event.cwd}`);
|
|
961
968
|
this.sessionStore.setSessionIdAndSessionCwd(ctx.userId, agentKind, event.session_id, realCwd);
|
|
962
969
|
}
|
|
963
970
|
else {
|
|
964
|
-
// First use (no
|
|
971
|
+
// First use (no cwd yet) or event.cwd matches runner cwd:
|
|
965
972
|
// set sessionId + cwd as before; on first use also bootstrap sessionCwds.
|
|
966
973
|
const bootstrapSessionCwd = !hasCwd && event.cwd && event.cwd.length > 0 ? event.cwd : undefined;
|
|
967
974
|
this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.session_id, realCwd, bootstrapSessionCwd);
|
|
@@ -1021,7 +1028,7 @@ export class Bridge {
|
|
|
1021
1028
|
: `[lark-remote] result event received runId=${runId}`);
|
|
1022
1029
|
// app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
|
|
1023
1030
|
// 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
|
|
1024
|
-
//
|
|
1031
|
+
// 里兜底上报。此处仅当 runner 声明 live usage(app-server)
|
|
1025
1032
|
// 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
|
|
1026
1033
|
// 此列(它们各自有 system.init / turn_started 写回)。
|
|
1027
1034
|
if (usageAuthority === 'live' &&
|
|
@@ -1114,7 +1121,7 @@ export class Bridge {
|
|
|
1114
1121
|
const coordinator = this.approvalCoordinators.get(runId);
|
|
1115
1122
|
coordinator?.updateView(event.requestId, event.view);
|
|
1116
1123
|
}
|
|
1117
|
-
// §9.12
|
|
1124
|
+
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1118
1125
|
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1119
1126
|
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1120
1127
|
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
@@ -1122,9 +1129,9 @@ export class Bridge {
|
|
|
1122
1129
|
await cardSession.push(event);
|
|
1123
1130
|
}
|
|
1124
1131
|
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${sawResult}`);
|
|
1125
|
-
// §9.12
|
|
1132
|
+
// §9.12 红线:for-await 自然结束 = CLI 已退出,
|
|
1126
1133
|
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1127
|
-
// 收尾处理跨过死线被误标 idle_timeout
|
|
1134
|
+
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(随本修复一并关闭)。
|
|
1128
1135
|
watchdog?.stop();
|
|
1129
1136
|
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1130
1137
|
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
@@ -1203,7 +1210,7 @@ export class Bridge {
|
|
|
1203
1210
|
}
|
|
1204
1211
|
catch (err) {
|
|
1205
1212
|
getLogger().error(`[lark-remote] claude run error runId=${runId}:`, err);
|
|
1206
|
-
//
|
|
1213
|
+
// 双保险:run() 的 finally 已负责杀子进程,但 catch 路径再补一次
|
|
1207
1214
|
// runner.stop()(与 bash 路径对齐),覆盖任何 finally 之外的残留窗口。
|
|
1208
1215
|
try {
|
|
1209
1216
|
await runner.stop();
|
|
@@ -1292,10 +1299,10 @@ export class Bridge {
|
|
|
1292
1299
|
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1293
1300
|
}
|
|
1294
1301
|
finally {
|
|
1295
|
-
//
|
|
1302
|
+
// cleanup must survive mid-finalize errors. The only unguarded
|
|
1296
1303
|
// expression in finalizeRun is the renderRunCard(...) argument evaluated
|
|
1297
1304
|
// at the sendResult call site; if it throws, the run must still release
|
|
1298
|
-
// its activeRuns slot — otherwise the
|
|
1305
|
+
// its activeRuns slot — otherwise the cwd is permanently busy
|
|
1299
1306
|
// (every later message busy-dropped until a manual /stop).
|
|
1300
1307
|
if (this.activeRuns.get(cwd) === activeRun) {
|
|
1301
1308
|
this.activeRuns.delete(cwd);
|
|
@@ -1305,7 +1312,7 @@ export class Bridge {
|
|
|
1305
1312
|
// evicted+disposed by clearRunners (/config) and interruptCurrentRun
|
|
1306
1313
|
// (/stop). Spawn-per-message runners are evicted as before to prevent
|
|
1307
1314
|
// stale process references accumulating.
|
|
1308
|
-
//
|
|
1315
|
+
// 配置变更期间(clearRunners 标了 stale)的 workspace-lifetime
|
|
1309
1316
|
// runner 也必须在 run 结束后 evict,否则下一轮复用旧配置实例。
|
|
1310
1317
|
const slotKey = `${cwd}\u0000${activeRun.agentKind}`;
|
|
1311
1318
|
if (activeRun.runner.lifetime !== 'workspace' || this.staleRunnerSlots.has(slotKey)) {
|
|
@@ -1315,7 +1322,7 @@ export class Bridge {
|
|
|
1315
1322
|
// Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
|
|
1316
1323
|
// 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
|
|
1317
1324
|
// interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
|
|
1318
|
-
//
|
|
1325
|
+
// 写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
|
|
1319
1326
|
// (codex app-server + kimi acp 均满足)。
|
|
1320
1327
|
const runTerminal = cardSession.currentState.terminal;
|
|
1321
1328
|
const runnerHasCompact = runnerHasRunCompact(activeRun.runner);
|
|
@@ -1328,7 +1335,7 @@ export class Bridge {
|
|
|
1328
1335
|
const compactSessionId = cardSession.currentState.sessionId ??
|
|
1329
1336
|
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
1330
1337
|
if (compactSessionId) {
|
|
1331
|
-
this.
|
|
1338
|
+
this.lastCompactableRun.set(cwd, {
|
|
1332
1339
|
runId: activeRun.runId,
|
|
1333
1340
|
sessionId: compactSessionId,
|
|
1334
1341
|
agentKind: activeRun.agentKind,
|
|
@@ -1347,7 +1354,7 @@ export class Bridge {
|
|
|
1347
1354
|
*/
|
|
1348
1355
|
async sendCompletionNotificationCard(sessionId, cwd, ctx, agentKind = this.config.defaultAgent) {
|
|
1349
1356
|
try {
|
|
1350
|
-
//
|
|
1357
|
+
// cap the events read for the notification card — without
|
|
1351
1358
|
// maxEvents claude returns every event after the last user message and
|
|
1352
1359
|
// the card blows past Feishu's 28KB budget.
|
|
1353
1360
|
const content = this.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd, {
|
|
@@ -1367,7 +1374,7 @@ export class Bridge {
|
|
|
1367
1374
|
actions: [resumeUseButton(sessionId, agentKind)],
|
|
1368
1375
|
headerTemplate: 'green',
|
|
1369
1376
|
});
|
|
1370
|
-
//
|
|
1377
|
+
// send through sendResult so enforceCardBudget guards the card
|
|
1371
1378
|
// (previously sent via connector directly, bypassing the budget — long
|
|
1372
1379
|
// sessions produced >28KB cards that Feishu silently rejected).
|
|
1373
1380
|
const sent = await this.sendResult({ card }, ctx);
|
|
@@ -1392,7 +1399,7 @@ export class Bridge {
|
|
|
1392
1399
|
withCoordinator(runId, fn) {
|
|
1393
1400
|
const coordinator = this.approvalCoordinators.get(runId);
|
|
1394
1401
|
if (!coordinator) {
|
|
1395
|
-
//
|
|
1402
|
+
// run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
|
|
1396
1403
|
// 不能静默。router 会把该异常转成 error toast。
|
|
1397
1404
|
getLogger().warn(`[lark-remote] no approval coordinator for runId=${runId.slice(0, 8)}`);
|
|
1398
1405
|
throw new Error('任务已结束,审批无法响应');
|
|
@@ -1456,7 +1463,7 @@ export class Bridge {
|
|
|
1456
1463
|
return this.withCoordinator(opts.runId, (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1457
1464
|
}
|
|
1458
1465
|
/**
|
|
1459
|
-
* Claude AskUserQuestion 自定义答案(Other
|
|
1466
|
+
* Claude AskUserQuestion 自定义答案(Other):自由文本直接
|
|
1460
1467
|
* 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
|
|
1461
1468
|
*/
|
|
1462
1469
|
async handleApprovalAnswerCustom(opts) {
|
|
@@ -1475,14 +1482,14 @@ export class Bridge {
|
|
|
1475
1482
|
return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1476
1483
|
}
|
|
1477
1484
|
/**
|
|
1478
|
-
* Handle
|
|
1485
|
+
* Handle compact card action — trigger a compaction request for any
|
|
1479
1486
|
* runCompact-capable runner(codex/kimi/opencode/pi/claude,按
|
|
1480
|
-
*
|
|
1487
|
+
* lastCompactableRun 记录的 agentKind 路由)。Validates the run exists
|
|
1481
1488
|
* and is in a terminal state, then calls the runner's runCompact().
|
|
1482
1489
|
*/
|
|
1483
|
-
async
|
|
1490
|
+
async handleCompactAction(value, ctx) {
|
|
1484
1491
|
const log = getLogger();
|
|
1485
|
-
log.info(`[lark-remote]
|
|
1492
|
+
log.info(`[lark-remote] handleCompactAction userId=${ctx.userId} runId=${value.runId?.slice(0, 8)}...`);
|
|
1486
1493
|
const { runId } = value;
|
|
1487
1494
|
if (!runId) {
|
|
1488
1495
|
await this.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 runId' }, ctx);
|
|
@@ -1493,8 +1500,8 @@ export class Bridge {
|
|
|
1493
1500
|
await this.sendResult({ text: '⚠️ 未设置工作目录' }, ctx);
|
|
1494
1501
|
return;
|
|
1495
1502
|
}
|
|
1496
|
-
// 校验:run 结束后 activeRuns 已清空,用
|
|
1497
|
-
const last = this.
|
|
1503
|
+
// 校验:run 结束后 activeRuns 已清空,用 lastCompactableRun 对照 runId。
|
|
1504
|
+
const last = this.lastCompactableRun.get(cwd);
|
|
1498
1505
|
if (!last || last.runId !== runId) {
|
|
1499
1506
|
await this.sendResult({ text: '⚠️ 该任务已结束或不属于当前会话' }, ctx);
|
|
1500
1507
|
return;
|
|
@@ -1513,8 +1520,8 @@ export class Bridge {
|
|
|
1513
1520
|
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
1514
1521
|
return;
|
|
1515
1522
|
}
|
|
1516
|
-
log.info(`[lark-remote]
|
|
1517
|
-
await this.
|
|
1523
|
+
log.info(`[lark-remote] handleCompactAction executing runCompact for runId=${runId} cwd=${cwd}`);
|
|
1524
|
+
await this.streamCompact({
|
|
1518
1525
|
sessionId: last.sessionId,
|
|
1519
1526
|
cwd,
|
|
1520
1527
|
agentKind: last.agentKind,
|
|
@@ -1556,16 +1563,16 @@ export class Bridge {
|
|
|
1556
1563
|
* Stream a compaction to a card: start a RunCardSession with
|
|
1557
1564
|
* operationKind='compaction' (no recursive Compact button), consume
|
|
1558
1565
|
* runner.runCompact() events, read authoritative jsonl usage, and finish
|
|
1559
|
-
* the card. Shared by
|
|
1566
|
+
* the card. Shared by handleCompactAction (run card button) and
|
|
1560
1567
|
* handleResumeCompact (resume cards) so both flows stay in sync.
|
|
1561
1568
|
*
|
|
1562
1569
|
* Precondition: callers have already validated that `runner` implements
|
|
1563
1570
|
* runCompact and that the session exists in `cwd`.
|
|
1564
1571
|
*/
|
|
1565
|
-
async
|
|
1572
|
+
async streamCompact(opts) {
|
|
1566
1573
|
const log = getLogger();
|
|
1567
1574
|
const { sessionId, cwd, agentKind, runner, ctx } = opts;
|
|
1568
|
-
log.info(`[lark-remote]
|
|
1575
|
+
log.info(`[lark-remote] streamCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
1569
1576
|
// 防御性能力检查:调用方已校验(runId / resume 两条路径),此处双保险并
|
|
1570
1577
|
// 让 TS 收窄 runner 类型('runCompact' in runner 之后的 cast 才合法)。
|
|
1571
1578
|
if (!('runCompact' in runner) ||
|
|
@@ -1577,7 +1584,7 @@ export class Bridge {
|
|
|
1577
1584
|
// 记录,令 /stop 只停到最后注册的那个。
|
|
1578
1585
|
const busyWith = this.activeRuns.get(cwd);
|
|
1579
1586
|
if (busyWith) {
|
|
1580
|
-
log.info(`[lark-remote] compact refused:
|
|
1587
|
+
log.info(`[lark-remote] compact refused: cwd busy cwd=${cwd} runId=${busyWith.runId}`);
|
|
1581
1588
|
await this.sendResult({ text: '⚠️ 该工作区正在运行任务,请等待结束或先 /stop' }, ctx);
|
|
1582
1589
|
return;
|
|
1583
1590
|
}
|
|
@@ -1592,7 +1599,7 @@ export class Bridge {
|
|
|
1592
1599
|
agentKind,
|
|
1593
1600
|
},
|
|
1594
1601
|
});
|
|
1595
|
-
//
|
|
1602
|
+
// 压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
1596
1603
|
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
1597
1604
|
// 压缩都只能等 runner 级超时,用户既停不掉也看不出它还在跑。
|
|
1598
1605
|
const activeRun = {
|
|
@@ -1675,10 +1682,10 @@ export class Bridge {
|
|
|
1675
1682
|
cumulativeCacheReadTokens: finalUsage?.cumulativeCacheReadTokens,
|
|
1676
1683
|
cumulativeCacheCreationTokens: finalUsage?.cumulativeCacheCreationTokens,
|
|
1677
1684
|
});
|
|
1678
|
-
log.info(`[lark-remote]
|
|
1685
|
+
log.info(`[lark-remote] streamCompact finished sessionId=${sessionId} sawResult=${sawResult} subtype=${resultSubtype ?? 'none'}`);
|
|
1679
1686
|
}
|
|
1680
1687
|
catch (err) {
|
|
1681
|
-
log.error(`[lark-remote]
|
|
1688
|
+
log.error(`[lark-remote] streamCompact failed: ${errorMessage(err)}`);
|
|
1682
1689
|
// 终态守卫(与 runAgentStreamToEnd 同口径):running/finalizing 才转 error,
|
|
1683
1690
|
// 已终态(interrupted/idle_timeout)保留首终态,不被迟到的异常改写。
|
|
1684
1691
|
const catchTerminal = cardSession.currentState.terminal;
|
|
@@ -1705,7 +1712,7 @@ export class Bridge {
|
|
|
1705
1712
|
* Handle resume.compact card action — compact a session directly from a
|
|
1706
1713
|
* resume card (auto-resume / `/resume <id>`), without a runId.
|
|
1707
1714
|
*
|
|
1708
|
-
* Unlike
|
|
1715
|
+
* Unlike handleCompactAction (which validates against the last finished run's
|
|
1709
1716
|
* runId), the resume card carries the sessionId + agent it was rendered for.
|
|
1710
1717
|
* Validation: the session must exist in the current cwd (same rule as
|
|
1711
1718
|
* cmdResume / resume.use) and the runner must implement runCompact.
|
|
@@ -1749,7 +1756,7 @@ export class Bridge {
|
|
|
1749
1756
|
await this.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
|
|
1750
1757
|
return;
|
|
1751
1758
|
}
|
|
1752
|
-
// §5.3: kimi 压缩在途检测(与
|
|
1759
|
+
// §5.3: kimi 压缩在途检测(与 handleCompactAction 同一入口)。
|
|
1753
1760
|
if (await this.rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx))
|
|
1754
1761
|
return;
|
|
1755
1762
|
// Check that the runner has runCompact(codex/kimi/opencode/pi/claude 鸭子探测)。
|
|
@@ -1759,7 +1766,7 @@ export class Bridge {
|
|
|
1759
1766
|
return;
|
|
1760
1767
|
}
|
|
1761
1768
|
log.info(`[lark-remote] handleResumeCompact executing runCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
1762
|
-
await this.
|
|
1769
|
+
await this.streamCompact({ sessionId, cwd, agentKind, runner, ctx });
|
|
1763
1770
|
}
|
|
1764
1771
|
/**
|
|
1765
1772
|
* Execute a bash command and stream output to a card.
|
|
@@ -1812,7 +1819,7 @@ export class Bridge {
|
|
|
1812
1819
|
});
|
|
1813
1820
|
// Track this bash run independently from claude's `activeRuns`. Bash runs
|
|
1814
1821
|
// bypass the serial queue and may run in parallel with a claude run in the
|
|
1815
|
-
// same
|
|
1822
|
+
// same cwd, so they must NOT share activeRuns (which is keyed by cwd
|
|
1816
1823
|
// and would let bash/claude overwrite each other). Keyed by runId so
|
|
1817
1824
|
// multiple `!` commands can run concurrently. Tracked so /stop can reach
|
|
1818
1825
|
// the bashRunner (which is otherwise a local) via interruptCurrentRun.
|
|
@@ -1838,7 +1845,7 @@ export class Bridge {
|
|
|
1838
1845
|
}
|
|
1839
1846
|
for await (const event of bashRunner.run(command, { cwd })) {
|
|
1840
1847
|
if (event.type === 'stdout') {
|
|
1841
|
-
//
|
|
1848
|
+
// 层④:bridge 本地 output 也必须 store-time 截断——仅 session state
|
|
1842
1849
|
// 有界并不会让 `output +=` 自然有界(review 第④条表述不成立),!yes 洪峰
|
|
1843
1850
|
// 下本地字符串仍全量驻留。capBashOutput 增量调用 O(CAP+chunk),不会带回
|
|
1844
1851
|
// O(n²) 截断成本(output 恒 ≤ CAP,拼接后 ≤ CAP+chunk 再 slice 尾部)。
|
|
@@ -1877,7 +1884,7 @@ export class Bridge {
|
|
|
1877
1884
|
const settleResult = await cardSession.settle();
|
|
1878
1885
|
if (settleResult === 'unsent') {
|
|
1879
1886
|
// Stream never established a card — send a static terminal card as fallback.
|
|
1880
|
-
// Use 2.0 schema to match the streaming card (
|
|
1887
|
+
// Use 2.0 schema to match the streaming card (avoid 2.0→1.x downgrade).
|
|
1881
1888
|
try {
|
|
1882
1889
|
await this.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
|
|
1883
1890
|
}
|