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/router/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { WorkspaceStore } from '../workspace/index.js';
|
|
|
8
8
|
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
|
+
import { AGENT_KINDS } from '../runner/types.js';
|
|
11
12
|
import { getLogger } from '../logger/index.js';
|
|
12
13
|
import { stripPlaceholders } from '../inbound/placeholder.js';
|
|
13
14
|
import { clampInt } from '../common/clamp.js';
|
|
@@ -262,7 +263,7 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
262
263
|
},
|
|
263
264
|
};
|
|
264
265
|
// ===========================================================================
|
|
265
|
-
//
|
|
266
|
+
// 命令名单单一事实源
|
|
266
267
|
// 直返语义(cardAction 同步返回 toast 给飞书回调,index.ts 控制层据此不经
|
|
267
268
|
// 队列直接处理)与即时语义(绕串行队列 enqueueImmediate,§9.6)正交,两个
|
|
268
269
|
// 独立清单只共享审批命令名来源(APPROVAL_ACTION_SPECS keys)——历史上同一
|
|
@@ -339,7 +340,15 @@ export const SESSION_MUTATING_ACTION_CMDS = new Set([
|
|
|
339
340
|
'ws.use',
|
|
340
341
|
'config.save',
|
|
341
342
|
]);
|
|
342
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* 会产生压缩工作的卡片动作(单源)。这类条目占用 lane 时必须自报身份:
|
|
345
|
+
* 排队卡写明「正在执行: 🗜 Compact」而不是「前面还有 0 条」,并且期间禁用
|
|
346
|
+
* 「立即执行」——该按钮走 interruptCurrentRun,会连带中断正在跑的压缩
|
|
347
|
+
* (2026-10-09 线上:Compact 占 lane 期间新消息的排队卡自相矛盾,且按钮
|
|
348
|
+
* 一点就会杀掉压缩)。同时它们也不允许编辑(预览不是用户文本)。
|
|
349
|
+
*/
|
|
350
|
+
export const COMPACT_ACTION_CMDS = new Set(['compact', 'resume.compact']);
|
|
351
|
+
/** 单源:payload.offset → 钳位 offset(原先 9 处逐字副本)。 */
|
|
343
352
|
function payloadOffset(value) {
|
|
344
353
|
return Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
345
354
|
}
|
|
@@ -358,9 +367,18 @@ function searchQueryFrom(value) {
|
|
|
358
367
|
}
|
|
359
368
|
/** 卡片 cardAction payload 缺字段的统一报错文案(原先 12 处手写且已漂移出两种前缀)。 */
|
|
360
369
|
const CARD_PAYLOAD_MISSING = '⚠️ 卡片 payload 缺少必要信息';
|
|
370
|
+
/**
|
|
371
|
+
* 记住并同步刷新的 config 卡片数上限。一次 /config 会话里用户实际会交互的
|
|
372
|
+
* 卡片只有最近一两张,5 张足够覆盖;无上限会让长跑进程的 Set 随卡片数增长。
|
|
373
|
+
*/
|
|
374
|
+
const MAX_CONFIG_CARDS = 5;
|
|
361
375
|
export class CommandRouter {
|
|
362
|
-
/**
|
|
363
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Valid agent kinds — single source of truth for resume.use / resume.page /
|
|
378
|
+
* cmdResume. 名单本体在 `runner/types.ts` 的 AGENT_KINDS(与 AgentKind 类型
|
|
379
|
+
* 同源),这里只做别名,避免人手抄第二份。
|
|
380
|
+
*/
|
|
381
|
+
static VALID_AGENTS = AGENT_KINDS;
|
|
364
382
|
sessionStore;
|
|
365
383
|
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
366
384
|
bridge;
|
|
@@ -394,13 +412,28 @@ export class CommandRouter {
|
|
|
394
412
|
/** /config 卡片编辑暂存区(public:测试直接读取断言,替代 as unknown as)。 */
|
|
395
413
|
pendingConfig = null;
|
|
396
414
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
415
|
+
* 已见过的 /config 卡片 messageId(有界,最近的在最后)。
|
|
416
|
+
*
|
|
417
|
+
* 草稿(pendingConfig)是全局一份:/config 文本命令重开卡片时沿用同一份草稿
|
|
418
|
+
* (这是既有语义,不改)。但**视图**是每张卡一份 —— 只刷新被点击的那张,其余
|
|
419
|
+
* 卡片就停在旧视图上:用户在旧卡上看到的和点保存实际提交的是两套内容
|
|
420
|
+
* (2026-10-08 发现的缺陷)。这里记住所有交互过的 config 卡,任何一次编辑/保存
|
|
421
|
+
* 后把草稿刷到每张卡,保证「看到的就是会保存的」。
|
|
402
422
|
*/
|
|
403
|
-
|
|
423
|
+
configCardIds = new Set();
|
|
424
|
+
/**
|
|
425
|
+
* 排队条目身份 key 的唯一铸造计数器(order.exec 与卡片动作共用)。
|
|
426
|
+
*
|
|
427
|
+
* 一张飞书卡片可以被点击 N 次,卡片 messageId 与入队动作是 1:N;而排队
|
|
428
|
+
* 条目的身份必须与「一次入队动作」1:1(queuedTasks 的定位、queueCardMessages
|
|
429
|
+
* 的卡片更新、taskIndex 的取消判据全部按这个 key 做 lookup)。复用卡片 id
|
|
430
|
+
* 会让同 key 的多条任务互相顶替:位置显示命中最早那条兄弟、第一条开始时删掉
|
|
431
|
+
* 索引导致其余兄弟被判「已撤销」静默丢弃(2026-10-08 线上:连点 Compact
|
|
432
|
+
* 三次 → 三张排队卡都显示「第 1 位」、两张永久停在「等待中」,只压缩一次)。
|
|
433
|
+
*
|
|
434
|
+
* Date.now() 不够(两次同步调用可落在同一毫秒);计数器与时间无关,恒唯一。
|
|
435
|
+
*/
|
|
436
|
+
queueTaskKeyCounter = 0;
|
|
404
437
|
/**
|
|
405
438
|
* Per-user sort preference for /ws list (memory-only, not persisted).
|
|
406
439
|
* Default 'recent' (most recently used first), can toggle to 'alpha'.
|
|
@@ -466,7 +499,7 @@ export class CommandRouter {
|
|
|
466
499
|
/**
|
|
467
500
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
468
501
|
*
|
|
469
|
-
* 命令识别前置条件(2026-09-15
|
|
502
|
+
* 命令识别前置条件(2026-09-15修复,`inbound-unified-input-design.md` §5.6):
|
|
470
503
|
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
471
504
|
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
472
505
|
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
@@ -513,7 +546,7 @@ export class CommandRouter {
|
|
|
513
546
|
await this.bridge.executeBash(cmd, ctx);
|
|
514
547
|
return null;
|
|
515
548
|
}
|
|
516
|
-
// Forward to Claude (
|
|
549
|
+
// Forward to Claude (pass the enqueue-time cwd through so the
|
|
517
550
|
// run uses the same cwd as the serial queue lane, even if /cd ran while
|
|
518
551
|
// the message was queued)
|
|
519
552
|
// 剥离占位符后再转发:agent 只该看到用户文本(附图已转成 attachments 块,
|
|
@@ -538,7 +571,7 @@ export class CommandRouter {
|
|
|
538
571
|
* Validates payload and dispatches to the corresponding command.
|
|
539
572
|
*/
|
|
540
573
|
async handleCardAction(value, ctx) {
|
|
541
|
-
//
|
|
574
|
+
// 审批家族以 APPROVAL_ACTION_SPECS keys 为单一来源分发
|
|
542
575
|
// (原 7 个 case 与 spec keys 是同一份知识的两份拷贝,已两次漂移)。
|
|
543
576
|
if (APPROVAL_ACTION_CMDS.includes(value.cmd)) {
|
|
544
577
|
return this.handleApprovalAction(value, ctx);
|
|
@@ -584,7 +617,7 @@ export class CommandRouter {
|
|
|
584
617
|
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
585
618
|
return;
|
|
586
619
|
}
|
|
587
|
-
//
|
|
620
|
+
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
588
621
|
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
589
622
|
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
590
623
|
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
@@ -759,12 +792,21 @@ export class CommandRouter {
|
|
|
759
792
|
if (!targetTask) {
|
|
760
793
|
// The target already began (or was cancelled) before this handler ran. We
|
|
761
794
|
// must NOT claim the queue was cleared — nothing is removed in this branch
|
|
762
|
-
// and tasks queued behind the target are still waiting
|
|
795
|
+
// and tasks queued behind the target are still waiting.
|
|
763
796
|
await this.bridge.sendResult({
|
|
764
797
|
text: '该消息已不在队列中(可能已开始执行或被撤销),无法立即执行。其余排队消息保持原状。',
|
|
765
798
|
}, ctx);
|
|
766
799
|
return;
|
|
767
800
|
}
|
|
801
|
+
// Compact 占 lane 时拒绝「立即执行」。该动作会调 interruptCurrentRun,而它
|
|
802
|
+
// 遍历 activeRuns 时会一并中断正在跑的压缩(压缩也注册在 activeRuns 里)。
|
|
803
|
+
// 渲染层已把按钮置灰,这里是旧卡片的兜底:改动前渲染的卡仍带可点的按钮。
|
|
804
|
+
// 放在存在性检查之后:目标已失效时,上一条「不在队列中」的反馈更准确。
|
|
805
|
+
const laneOccupant = this.bridge.getQueueExecutingInfo(cwd);
|
|
806
|
+
if (laneOccupant?.kind === 'compact') {
|
|
807
|
+
await this.bridge.sendResult({ text: '⚠️ 正在压缩会话,消息将在压缩完成后按顺序执行。' }, ctx);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
768
810
|
// 1.5. If the user edited the message, the original closure (captured at
|
|
769
811
|
// enqueue time with stale content) must NOT run. Register a one-shot
|
|
770
812
|
// replacement closure on the task's EXISTING queue slot BEFORE any await:
|
|
@@ -777,7 +819,7 @@ export class CommandRouter {
|
|
|
777
819
|
// which would put the edited task behind tasks queued after it.
|
|
778
820
|
if (targetTask.editedMessage) {
|
|
779
821
|
const editedContent = targetTask.editedMessage;
|
|
780
|
-
|
|
822
|
+
///Step3: 替换闭包复用原任务的 binding(不重新快照)+ 恢复丢失的
|
|
781
823
|
// cwdOverride(lane cwd 同源)。否则编辑后重新执行的 run 会丢 cwd 与绑定。
|
|
782
824
|
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
783
825
|
await this.handle(editedContent, ctx, {
|
|
@@ -834,7 +876,7 @@ export class CommandRouter {
|
|
|
834
876
|
const latestTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
835
877
|
if (latestTask?.editedMessage) {
|
|
836
878
|
const editedContent = latestTask.editedMessage;
|
|
837
|
-
|
|
879
|
+
///Step3: 复用原任务 binding + 恢复 cwdOverride(同上)。
|
|
838
880
|
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
839
881
|
await this.handle(editedContent, ctx, {
|
|
840
882
|
cwdOverride: cwd,
|
|
@@ -1049,7 +1091,7 @@ export class CommandRouter {
|
|
|
1049
1091
|
// setTaskReplacement and the begin-path consumption are synchronous, so
|
|
1050
1092
|
// there is no interleaving window.
|
|
1051
1093
|
//
|
|
1052
|
-
|
|
1094
|
+
///Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
1053
1095
|
const inputTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
1054
1096
|
if (!inputTask) {
|
|
1055
1097
|
// Task already left the queue (began or was cancelled) before we could
|
|
@@ -1155,7 +1197,7 @@ export class CommandRouter {
|
|
|
1155
1197
|
async handleLsRefresh(value, ctx) {
|
|
1156
1198
|
const targetPath = value.path;
|
|
1157
1199
|
const offset = value.offset ?? 0;
|
|
1158
|
-
// TOCTOU guard
|
|
1200
|
+
// TOCTOU guard: existsSync → statSync can race (target
|
|
1159
1201
|
// deleted in between), throwing into enqueueImmediate's catch which only
|
|
1160
1202
|
// logs — violating the "card button clicks must give visible feedback"
|
|
1161
1203
|
// red line. Treat a vanished/non-dir path as "use current cwd" instead of
|
|
@@ -1185,7 +1227,7 @@ export class CommandRouter {
|
|
|
1185
1227
|
return { toast: { type: 'error', content: paging.error } };
|
|
1186
1228
|
}
|
|
1187
1229
|
const resolvedTarget = path.resolve(targetPath);
|
|
1188
|
-
// TOCTOU guard
|
|
1230
|
+
// TOCTOU guard: existsSync → statSync can race; without the
|
|
1189
1231
|
// try/catch the throw lands in enqueueImmediate's silent .catch and the
|
|
1190
1232
|
// paginated card never updates (no visible feedback for the click).
|
|
1191
1233
|
let isDir;
|
|
@@ -1208,7 +1250,7 @@ export class CommandRouter {
|
|
|
1208
1250
|
*
|
|
1209
1251
|
* offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
|
|
1210
1252
|
* handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
|
|
1211
|
-
* ls.browse / ls.switch 的 value 里刻意不带 q
|
|
1253
|
+
* ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选。
|
|
1212
1254
|
*/
|
|
1213
1255
|
async handleLsFilter(value, ctx) {
|
|
1214
1256
|
const targetPath = value.path;
|
|
@@ -1348,11 +1390,25 @@ export class CommandRouter {
|
|
|
1348
1390
|
this.orderStore.updateUsedAt(order.id);
|
|
1349
1391
|
return {
|
|
1350
1392
|
orderText: order.text,
|
|
1351
|
-
internalKey:
|
|
1393
|
+
internalKey: this.mintQueueTaskKey('order', order.id),
|
|
1352
1394
|
};
|
|
1353
1395
|
}
|
|
1354
1396
|
/**
|
|
1355
|
-
*
|
|
1397
|
+
* 铸造一个排队条目身份 key(唯一铸造点)。
|
|
1398
|
+
*
|
|
1399
|
+
* 调用方:order.exec 文本钩子(resolveOrderExecForQueue)与入口层
|
|
1400
|
+
* (index.ts)的非即时卡片动作入队。卡片 messageId 只能当「回复目标」
|
|
1401
|
+
* (`taskMeta.feishuReplyTo`),不得当队列 key —— 理由见
|
|
1402
|
+
* `queueTaskKeyCounter` 字段注释。
|
|
1403
|
+
*/
|
|
1404
|
+
mintQueueTaskKey(kind, id) {
|
|
1405
|
+
this.queueTaskKeyCounter += 1;
|
|
1406
|
+
return id === undefined
|
|
1407
|
+
? `queue-${kind}-${this.queueTaskKeyCounter}`
|
|
1408
|
+
: `${kind}-${id}-${this.queueTaskKeyCounter}`;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
1356
1412
|
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
1357
1413
|
*/
|
|
1358
1414
|
rebuildListCard(kind, offset, ctx, q) {
|
|
@@ -1361,7 +1417,7 @@ export class CommandRouter {
|
|
|
1361
1417
|
return this.cmdWs([], ctx, offset, q).card;
|
|
1362
1418
|
}
|
|
1363
1419
|
/**
|
|
1364
|
-
*
|
|
1420
|
+
* 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
1365
1421
|
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
1366
1422
|
*/
|
|
1367
1423
|
async refreshListCard(kind, value, ctx) {
|
|
@@ -1372,7 +1428,7 @@ export class CommandRouter {
|
|
|
1372
1428
|
return card;
|
|
1373
1429
|
}
|
|
1374
1430
|
/**
|
|
1375
|
-
*
|
|
1431
|
+
* 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
|
|
1376
1432
|
* 的公共骨架——校验 orderId → reload+find → mutate → 成功后 toast + raw 卡。
|
|
1377
1433
|
* mutate 返回 error 时仅回 error toast,不重渲染。
|
|
1378
1434
|
* 注意:本 helper 只服务「toast + card 进响应体」语义(飞书需用响应体原地
|
|
@@ -1742,6 +1798,44 @@ export class CommandRouter {
|
|
|
1742
1798
|
this.pendingConfig = structuredClone(this.config);
|
|
1743
1799
|
}
|
|
1744
1800
|
}
|
|
1801
|
+
/**
|
|
1802
|
+
* 记住一张 config 卡片 messageId(有界:超限丢最早的一张)。
|
|
1803
|
+
* 无 messageId 的调用(测试/文本路径)不登记。
|
|
1804
|
+
*/
|
|
1805
|
+
rememberConfigCard(messageId) {
|
|
1806
|
+
if (!messageId)
|
|
1807
|
+
return;
|
|
1808
|
+
this.configCardIds.delete(messageId);
|
|
1809
|
+
this.configCardIds.add(messageId);
|
|
1810
|
+
while (this.configCardIds.size > MAX_CONFIG_CARDS) {
|
|
1811
|
+
const oldest = this.configCardIds.values().next().value;
|
|
1812
|
+
if (oldest === undefined)
|
|
1813
|
+
break;
|
|
1814
|
+
this.configCardIds.delete(oldest);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* 把当前草稿刷到所有已知 config 卡片上(含本次点击的那张)。
|
|
1819
|
+
*
|
|
1820
|
+
* 被点击的那张走 `updateCardInPlace`(失败 fallback 发新卡,保证点击必有
|
|
1821
|
+
* 反馈);其余兄弟卡走 `patchCard`(纯 best-effort,失败即从集合剔除,不再
|
|
1822
|
+
* 重复打无效 PATCH,也不会因为卡片过期而反复补发新卡)。
|
|
1823
|
+
*/
|
|
1824
|
+
async refreshConfigCards(ctx) {
|
|
1825
|
+
this.rememberConfigCard(ctx.messageId);
|
|
1826
|
+
const card = this.buildConfigCard().card;
|
|
1827
|
+
if (!card)
|
|
1828
|
+
return;
|
|
1829
|
+
for (const messageId of [...this.configCardIds]) {
|
|
1830
|
+
if (messageId === ctx.messageId) {
|
|
1831
|
+
await this.bridge.updateCardInPlace(card, ctx);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const ok = await this.bridge.patchCard(messageId, card);
|
|
1835
|
+
if (!ok)
|
|
1836
|
+
this.configCardIds.delete(messageId);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1745
1839
|
/**
|
|
1746
1840
|
* 串行化 config.* 卡片回调(2026-07-04 修复 toggle 卡死 bug)。
|
|
1747
1841
|
*
|
|
@@ -1786,7 +1880,7 @@ export class CommandRouter {
|
|
|
1786
1880
|
try {
|
|
1787
1881
|
// 只改暂存区,不写盘;原地更新卡片而非发送新卡(2026-07-04)
|
|
1788
1882
|
this.setNestedValue(this.pendingConfig, key, !boolVal);
|
|
1789
|
-
await this.
|
|
1883
|
+
await this.refreshConfigCards(ctx);
|
|
1790
1884
|
}
|
|
1791
1885
|
catch (err) {
|
|
1792
1886
|
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
@@ -1810,7 +1904,7 @@ export class CommandRouter {
|
|
|
1810
1904
|
for (const patch of patches) {
|
|
1811
1905
|
this.setNestedValue(this.pendingConfig, patch.key, patch.value);
|
|
1812
1906
|
}
|
|
1813
|
-
await this.
|
|
1907
|
+
await this.refreshConfigCards(ctx);
|
|
1814
1908
|
}
|
|
1815
1909
|
catch (err) {
|
|
1816
1910
|
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
@@ -1841,7 +1935,7 @@ export class CommandRouter {
|
|
|
1841
1935
|
for (const patch of patches) {
|
|
1842
1936
|
this.setNestedValue(this.pendingConfig, patch.key, patch.value);
|
|
1843
1937
|
}
|
|
1844
|
-
await this.
|
|
1938
|
+
await this.refreshConfigCards(ctx);
|
|
1845
1939
|
}
|
|
1846
1940
|
catch (err) {
|
|
1847
1941
|
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
@@ -1863,7 +1957,7 @@ export class CommandRouter {
|
|
|
1863
1957
|
try {
|
|
1864
1958
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
1865
1959
|
this.config = setConfigValues(this.configPath, this.config, updates);
|
|
1866
|
-
//
|
|
1960
|
+
// 运行时传播(idleTimeout / clearRunners / syncAgentChoices /
|
|
1867
1961
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1868
1962
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1869
1963
|
this.pendingConfig = null;
|
|
@@ -1873,7 +1967,7 @@ export class CommandRouter {
|
|
|
1873
1967
|
try {
|
|
1874
1968
|
// host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
|
|
1875
1969
|
await this.prefetchConfigCardCatalogs();
|
|
1876
|
-
await this.
|
|
1970
|
+
await this.refreshConfigCards(ctx);
|
|
1877
1971
|
}
|
|
1878
1972
|
catch (refreshErr) {
|
|
1879
1973
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
@@ -1909,7 +2003,7 @@ export class CommandRouter {
|
|
|
1909
2003
|
return diffConfig(original, pending);
|
|
1910
2004
|
}
|
|
1911
2005
|
/**
|
|
1912
|
-
* config
|
|
2006
|
+
* config 写盘后的运行时传播(自 config.save 卡片路径抽出,卡片保存与
|
|
1913
2007
|
* `/config <key> <value>` 文本直写两条路径共用)。
|
|
1914
2008
|
*
|
|
1915
2009
|
* 1. bridge 运行时值同步:setConfig + idle watchdog timeout;
|
|
@@ -1917,7 +2011,7 @@ export class CommandRouter {
|
|
|
1917
2011
|
* 配置值,/status 与真实 run 自相矛盾);
|
|
1918
2012
|
* 3. defaultAgent 切换 → 旧 agent session 存 previousSessions、新 agent 恢复
|
|
1919
2013
|
* 或清空(等效 /new);
|
|
1920
|
-
* 4. 当前 agent 配置变更 → syncAgentChoices
|
|
2014
|
+
* 4. 当前 agent 配置变更 → syncAgentChoices 并原子写盘(禁裸同步写盘,
|
|
1921
2015
|
* 写一半崩溃会截断 config.yaml 导致 bridge 起不来)。
|
|
1922
2016
|
*
|
|
1923
2017
|
* @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
|
|
@@ -1991,7 +2085,7 @@ export class CommandRouter {
|
|
|
1991
2085
|
switchSessionId = '';
|
|
1992
2086
|
}
|
|
1993
2087
|
}
|
|
1994
|
-
// DSH preset 变更 = 下次 run 新建 session(§4.3
|
|
2088
|
+
// DSH preset 变更 = 下次 run 新建 session(§4.3):
|
|
1995
2089
|
// preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
|
|
1996
2090
|
// 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
|
|
1997
2091
|
// preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
|
|
@@ -2009,7 +2103,7 @@ export class CommandRouter {
|
|
|
2009
2103
|
switchNotice = `DSH preset 已变更,下次消息将新建会话`;
|
|
2010
2104
|
}
|
|
2011
2105
|
}
|
|
2012
|
-
// DSH host 变更 =
|
|
2106
|
+
// DSH host 变更 = 换服务:旧 host 的 sessionId 发往新 host 是错的。
|
|
2013
2107
|
// 视为 session 边界,仿 preset 分支停车 + 清空当前 sessionId,下次 run 新建会话。
|
|
2014
2108
|
const hasDshHostChange = Object.keys(updates).some((k) => k === 'agents.dsh.host');
|
|
2015
2109
|
if (hasDshHostChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
|
|
@@ -2029,11 +2123,11 @@ export class CommandRouter {
|
|
|
2029
2123
|
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
2030
2124
|
if (hasCurrentAgentUpdate) {
|
|
2031
2125
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
2032
|
-
//
|
|
2126
|
+
// agentChoices 同步写盘必须走原子写(tmp+rename),禁裸同步写盘
|
|
2033
2127
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
2034
2128
|
this.bridge.setConfig(this.config);
|
|
2035
2129
|
}
|
|
2036
|
-
//
|
|
2130
|
+
// live approval settings: active workspace-lifetime runners
|
|
2037
2131
|
// are not evicted by clearRunners() (would orphan the live subprocess).
|
|
2038
2132
|
// Push the new approval-mode settings to those runners via the unified
|
|
2039
2133
|
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
@@ -2446,7 +2540,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2446
2540
|
// Claude writes into JSONL (e.g. `/tmp` → `/private/tmp` on macOS).
|
|
2447
2541
|
// Without this, a hand-typed unresolved path never matches any JSONL's
|
|
2448
2542
|
// cwd field and the user's `/cd /tmp/foo` never finds their sessions.
|
|
2449
|
-
// TOCTOU: the directory may vanish between stat and realpath
|
|
2543
|
+
// TOCTOU: the directory may vanish between stat and realpath.
|
|
2450
2544
|
// Reject gracefully instead of letting realpathSync throw into the queue
|
|
2451
2545
|
// task's .catch (which only logs — the user gets no feedback). Mirrors
|
|
2452
2546
|
// handleLsSwitch's realpath guard so /cd and ls.switch behave identically.
|
|
@@ -2503,7 +2597,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2503
2597
|
*/
|
|
2504
2598
|
switchCwdAndNotify(userId, canonical, _ctx) {
|
|
2505
2599
|
this.sessionStore.setCwd(userId, canonical);
|
|
2506
|
-
// Auto-resume newest session if exists (
|
|
2600
|
+
// Auto-resume newest session if exists (读取失败不阻断切换,给可见提示)
|
|
2507
2601
|
let newestSession;
|
|
2508
2602
|
try {
|
|
2509
2603
|
newestSession = this.sessionReader.getNewestSession(canonical);
|
|
@@ -2634,7 +2728,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2634
2728
|
}
|
|
2635
2729
|
/**
|
|
2636
2730
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
2637
|
-
* card.
|
|
2731
|
+
* card. duck-typing — check if the runner has `runCompact`
|
|
2638
2732
|
* (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
|
|
2639
2733
|
*/
|
|
2640
2734
|
canCompactSession(agentKind, cwd) {
|
|
@@ -3111,7 +3205,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3111
3205
|
if (!fs.existsSync(wsPath))
|
|
3112
3206
|
return { text: `路径不存在: ${wsPath}` };
|
|
3113
3207
|
// Canonicalize via realpath so it matches Claude JSONL cwd (2026-06-21).
|
|
3114
|
-
// TOCTOU guard
|
|
3208
|
+
// TOCTOU guard: mirror cmdCd — if the path vanishes
|
|
3115
3209
|
// between existsSync and realpath, reply gracefully instead of
|
|
3116
3210
|
// throwing into the queue task's silent .catch.
|
|
3117
3211
|
let canonical;
|
|
@@ -3332,7 +3426,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3332
3426
|
}
|
|
3333
3427
|
}
|
|
3334
3428
|
/**
|
|
3335
|
-
*
|
|
3429
|
+
* degraded card when a session's export is non-empty but unparseable
|
|
3336
3430
|
* (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
|
|
3337
3431
|
* The session exists, so reuse listSessions metadata (title) instead of the
|
|
3338
3432
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
@@ -3427,7 +3521,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3427
3521
|
};
|
|
3428
3522
|
}
|
|
3429
3523
|
}
|
|
3430
|
-
//
|
|
3524
|
+
// 校验通过后才写入
|
|
3431
3525
|
this.sessionStore.setSessionId(ctx.userId, agentKind, sessionIdArg, cwd);
|
|
3432
3526
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionIdArg, cwd, {
|
|
3433
3527
|
agentKind,
|
|
@@ -3490,7 +3584,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3490
3584
|
}
|
|
3491
3585
|
}
|
|
3492
3586
|
catch (err) {
|
|
3493
|
-
//
|
|
3587
|
+
// 读取失败必须给用户可见反馈,而不是误导性的「没有 session 记录」
|
|
3494
3588
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3495
3589
|
return { text: `读取 ${agentDisplayName(agentKind)} session 列表失败: ${msg}` };
|
|
3496
3590
|
}
|
|
@@ -3562,7 +3656,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3562
3656
|
const prefetchedTitle = meta?.displayTitle;
|
|
3563
3657
|
// aiTitle 不能单独作为"预取标题"键:若 displayTitle 缺失(理论上 opencode
|
|
3564
3658
|
// info.title 非空但 export 无 user text),标题兜底是 summary,label 必须
|
|
3565
|
-
// 是中性"会话摘要"而不是"最近输入"(Review
|
|
3659
|
+
// 是中性"会话摘要"而不是"最近输入"(Review 边缘分支)。
|
|
3566
3660
|
const titleText = prefetchedTitle ?? (summaryIsPlaceholder ? '' : s.summary);
|
|
3567
3661
|
const titleLabel = prefetchedTitle
|
|
3568
3662
|
? meta?.aiTitle
|
|
@@ -3727,7 +3821,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3727
3821
|
elements.push({ tag: 'hr' });
|
|
3728
3822
|
}
|
|
3729
3823
|
}
|
|
3730
|
-
// Pagination bar
|
|
3824
|
+
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
3731
3825
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
3732
3826
|
if (totalPages > 1) {
|
|
3733
3827
|
elements.push(...paginationBar({
|
|
@@ -3763,14 +3857,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3763
3857
|
const value = args.slice(1).join(' ');
|
|
3764
3858
|
try {
|
|
3765
3859
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
3766
|
-
//
|
|
3860
|
+
// 直写前若 pendingConfig 存在未保存差异,提示用户被丢弃的条数,
|
|
3767
3861
|
// 避免静默清空导致用户在卡片上的修改无反馈丢失。差异必须在 setConfigValue
|
|
3768
3862
|
// 之前计算——直写本身也会改变 config,不应计入「被丢弃的未保存修改」。
|
|
3769
3863
|
const discarded = this.pendingConfig
|
|
3770
3864
|
? Object.keys(this.diffConfig(this.config, this.pendingConfig)).length
|
|
3771
3865
|
: 0;
|
|
3772
3866
|
this.config = setConfigValue(this.configPath, this.config, key, value);
|
|
3773
|
-
//
|
|
3867
|
+
// 直写路径与 config.save 卡片路径共用运行时传播(idleTimeout /
|
|
3774
3868
|
// clearRunners / syncAgentChoices / defaultAgent 切换)。key 经 mapAgentKey
|
|
3775
3869
|
// 归一化,pi./codex./opencode./kimi. 命中 agents.* 判定;claude. 与
|
|
3776
3870
|
// defaultAgent 不再漏判。
|
|
@@ -23,7 +23,7 @@ export async function dispatchOrderExecForQueue(args) {
|
|
|
23
23
|
await bridge.sendResult({ text: '指令不存在或已被删除' }, ctx);
|
|
24
24
|
return 'not-found';
|
|
25
25
|
}
|
|
26
|
-
// Step4
|
|
26
|
+
// Step4/入队时刻快照 agent+session,随任务闭包带到执行时刻,
|
|
27
27
|
// 避免排队期间 /new、/config 改写 live 状态导致语义漂移。唯一捕获点
|
|
28
28
|
// Bridge.currentBinding。
|
|
29
29
|
const binding = bridge.currentBinding(ctx.userId);
|
|
@@ -38,7 +38,7 @@ export async function dispatchOrderExecForQueue(args) {
|
|
|
38
38
|
// The internal key is NOT a valid Feishu message id, so the queue
|
|
39
39
|
// status card must reply to the real Feishu card message id instead.
|
|
40
40
|
feishuReplyTo: ctx.messageId,
|
|
41
|
-
|
|
41
|
+
///Step4:binding 随 taskMeta 存进 QueuedTask,供替换闭包
|
|
42
42
|
// (queue.edit/queue.immediate)复用,不重新快照。
|
|
43
43
|
binding,
|
|
44
44
|
},
|
|
@@ -28,7 +28,7 @@ export interface BashRunner {
|
|
|
28
28
|
export declare class BashProcessRunner implements BashRunner {
|
|
29
29
|
private currentProcess;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* 背压高水位/低水位(层④,参照 jsonl-stream.ts 的 pauseThreshold/
|
|
32
32
|
* resumeThreshold)。stdout/stderr 队列超过高水位时 pause 流(内存有界),
|
|
33
33
|
* 消费者 drain 到低水位后 resume。构造可注入以便测试用小水位。
|
|
34
34
|
*/
|
|
@@ -50,7 +50,7 @@ export declare class BashProcessRunner implements BashRunner {
|
|
|
50
50
|
cwd: string;
|
|
51
51
|
}): AsyncGenerator<BashOutputEvent>;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* called by the process-level exit dispatcher on SIGINT/SIGTERM/exit.
|
|
54
54
|
* Kills the whole process group (leader + background children) via
|
|
55
55
|
* Terminator, matching agent-runner cleanupOnExit semantics.
|
|
56
56
|
*/
|
|
@@ -17,7 +17,7 @@ import { registerExitCleanup, unregisterExitCleanup } from '../common/spawning-r
|
|
|
17
17
|
export class BashProcessRunner {
|
|
18
18
|
currentProcess = null;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 背压高水位/低水位(层④,参照 jsonl-stream.ts 的 pauseThreshold/
|
|
21
21
|
* resumeThreshold)。stdout/stderr 队列超过高水位时 pause 流(内存有界),
|
|
22
22
|
* 消费者 drain 到低水位后 resume。构造可注入以便测试用小水位。
|
|
23
23
|
*/
|
|
@@ -32,7 +32,7 @@ export class BashProcessRunner {
|
|
|
32
32
|
if (this.queueLowWater > this.queueHighWater) {
|
|
33
33
|
this.queueLowWater = this.queueHighWater;
|
|
34
34
|
}
|
|
35
|
-
// `!` bash
|
|
35
|
+
// `!` bash 无协议停止通道:posix 走组杀(与原 ProcessStopper 同实现),
|
|
36
36
|
// win32 优雅段显式跳过后树杀。
|
|
37
37
|
this.terminator = opts?.terminator ?? createTerminator({ graceMs: stopGraceMs, agent: 'bash' });
|
|
38
38
|
this.shell = opts?.shell ?? createShellBackend();
|
|
@@ -61,7 +61,7 @@ export class BashProcessRunner {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
64
|
-
// win32 Git Bash
|
|
64
|
+
// win32 Git Bash 缺失:同步抛出,转成明确错误输出而非悬死。
|
|
65
65
|
if (err instanceof ShellUnavailableError) {
|
|
66
66
|
getLogger().warn(`[bash-runner] shell unavailable: ${err.message}`);
|
|
67
67
|
yield { type: 'stderr', content: `${err.message}\n` };
|
|
@@ -76,7 +76,7 @@ export class BashProcessRunner {
|
|
|
76
76
|
const stdout = proc.stdout;
|
|
77
77
|
const stderr = proc.stderr;
|
|
78
78
|
getLogger().info(`[bash-runner] spawn pid=${proc.pid} command="${command.slice(0, 50)}..." cwd=${opts.cwd}`);
|
|
79
|
-
//
|
|
79
|
+
// register with the process-level exit dispatcher while the process
|
|
80
80
|
// is alive — bridge exit/restart (SIGINT/SIGTERM/exit) then kills the whole
|
|
81
81
|
// group via cleanupOnExit instead of orphaning `!` processes.
|
|
82
82
|
registerExitCleanup(this);
|
|
@@ -85,7 +85,7 @@ export class BashProcessRunner {
|
|
|
85
85
|
// for logging, one for queueing) -- both fired on the same chunk.
|
|
86
86
|
const stdoutQueue = [];
|
|
87
87
|
const stderrQueue = [];
|
|
88
|
-
//
|
|
88
|
+
// 背压状态(层④):队列超高水位 → pause 对应流;drain 到低水位 → resume。
|
|
89
89
|
let stdoutPaused = false;
|
|
90
90
|
let stderrPaused = false;
|
|
91
91
|
const maybePauseStdout = () => {
|
|
@@ -209,7 +209,7 @@ export class BashProcessRunner {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
/**
|
|
212
|
-
*
|
|
212
|
+
* called by the process-level exit dispatcher on SIGINT/SIGTERM/exit.
|
|
213
213
|
* Kills the whole process group (leader + background children) via
|
|
214
214
|
* Terminator, matching agent-runner cleanupOnExit semantics.
|
|
215
215
|
*/
|
|
@@ -127,7 +127,7 @@ export class ClaudeRunner {
|
|
|
127
127
|
await this.session.respondPermission(requestId, { behavior: 'deny' });
|
|
128
128
|
return;
|
|
129
129
|
case 'cancel': {
|
|
130
|
-
// 审批超时/取消:deny 送达后中断 turn
|
|
130
|
+
// 审批超时/取消:deny 送达后中断 turn(对齐「审批超时未响应,
|
|
131
131
|
// 已自动取消」终态;仅 deny 会让 claude 继续回合导致卡片 done)。
|
|
132
132
|
await this.session.respondPermission(requestId, {
|
|
133
133
|
behavior: 'deny',
|
|
@@ -53,7 +53,7 @@ export interface PermissionResult {
|
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
56
|
-
* Terminator/SpawnHeartbeat
|
|
56
|
+
* Terminator/SpawnHeartbeat/退出分发器机制(契约保持),
|
|
57
57
|
* 但 run() 覆盖为「长驻 + 按 turn 消费」:进程跨 turn 存活,turn 结束以
|
|
58
58
|
* stream-json result 事件(非 compact)为界,而不是进程退出。
|
|
59
59
|
*/
|
|
@@ -138,7 +138,7 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
138
138
|
private startStreamReader;
|
|
139
139
|
/**
|
|
140
140
|
* 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
|
|
141
|
-
* 消费者提前关闭生成器(bridge 循环体抛错)→
|
|
141
|
+
* 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(语义)。
|
|
142
142
|
*
|
|
143
143
|
* result = turn 终态(2026-08-16 真实 claude 实测:长驻模式下 result 之后
|
|
144
144
|
* 不再有 stdout 事件,后台任务完成是静默的;旧 -p 流程「等进程退出再收尾」
|