lark-remote 0.4.5 → 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 +18 -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 +9 -9
- package/dist/bridge/index.js +41 -41
- package/dist/bridge/queue-manager.d.ts +12 -12
- package/dist/bridge/queue-manager.js +19 -19
- 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/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 +9 -9
- 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 +9 -9
- package/dist/router/index.js +34 -34
- 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.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/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 +1 -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
|
@@ -262,7 +262,7 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
262
262
|
},
|
|
263
263
|
};
|
|
264
264
|
// ===========================================================================
|
|
265
|
-
//
|
|
265
|
+
// 命令名单单一事实源
|
|
266
266
|
// 直返语义(cardAction 同步返回 toast 给飞书回调,index.ts 控制层据此不经
|
|
267
267
|
// 队列直接处理)与即时语义(绕串行队列 enqueueImmediate,§9.6)正交,两个
|
|
268
268
|
// 独立清单只共享审批命令名来源(APPROVAL_ACTION_SPECS keys)——历史上同一
|
|
@@ -339,7 +339,7 @@ export const SESSION_MUTATING_ACTION_CMDS = new Set([
|
|
|
339
339
|
'ws.use',
|
|
340
340
|
'config.save',
|
|
341
341
|
]);
|
|
342
|
-
/**
|
|
342
|
+
/** 单源:payload.offset → 钳位 offset(原先 9 处逐字副本)。 */
|
|
343
343
|
function payloadOffset(value) {
|
|
344
344
|
return Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
345
345
|
}
|
|
@@ -466,7 +466,7 @@ export class CommandRouter {
|
|
|
466
466
|
/**
|
|
467
467
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
468
468
|
*
|
|
469
|
-
* 命令识别前置条件(2026-09-15
|
|
469
|
+
* 命令识别前置条件(2026-09-15修复,`inbound-unified-input-design.md` §5.6):
|
|
470
470
|
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
471
471
|
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
472
472
|
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
@@ -513,7 +513,7 @@ export class CommandRouter {
|
|
|
513
513
|
await this.bridge.executeBash(cmd, ctx);
|
|
514
514
|
return null;
|
|
515
515
|
}
|
|
516
|
-
// Forward to Claude (
|
|
516
|
+
// Forward to Claude (pass the enqueue-time cwd through so the
|
|
517
517
|
// run uses the same cwd as the serial queue lane, even if /cd ran while
|
|
518
518
|
// the message was queued)
|
|
519
519
|
// 剥离占位符后再转发:agent 只该看到用户文本(附图已转成 attachments 块,
|
|
@@ -538,7 +538,7 @@ export class CommandRouter {
|
|
|
538
538
|
* Validates payload and dispatches to the corresponding command.
|
|
539
539
|
*/
|
|
540
540
|
async handleCardAction(value, ctx) {
|
|
541
|
-
//
|
|
541
|
+
// 审批家族以 APPROVAL_ACTION_SPECS keys 为单一来源分发
|
|
542
542
|
// (原 7 个 case 与 spec keys 是同一份知识的两份拷贝,已两次漂移)。
|
|
543
543
|
if (APPROVAL_ACTION_CMDS.includes(value.cmd)) {
|
|
544
544
|
return this.handleApprovalAction(value, ctx);
|
|
@@ -584,7 +584,7 @@ export class CommandRouter {
|
|
|
584
584
|
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
585
585
|
return;
|
|
586
586
|
}
|
|
587
|
-
//
|
|
587
|
+
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
588
588
|
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
589
589
|
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
590
590
|
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
@@ -759,7 +759,7 @@ export class CommandRouter {
|
|
|
759
759
|
if (!targetTask) {
|
|
760
760
|
// The target already began (or was cancelled) before this handler ran. We
|
|
761
761
|
// must NOT claim the queue was cleared — nothing is removed in this branch
|
|
762
|
-
// and tasks queued behind the target are still waiting
|
|
762
|
+
// and tasks queued behind the target are still waiting.
|
|
763
763
|
await this.bridge.sendResult({
|
|
764
764
|
text: '该消息已不在队列中(可能已开始执行或被撤销),无法立即执行。其余排队消息保持原状。',
|
|
765
765
|
}, ctx);
|
|
@@ -777,7 +777,7 @@ export class CommandRouter {
|
|
|
777
777
|
// which would put the edited task behind tasks queued after it.
|
|
778
778
|
if (targetTask.editedMessage) {
|
|
779
779
|
const editedContent = targetTask.editedMessage;
|
|
780
|
-
|
|
780
|
+
///Step3: 替换闭包复用原任务的 binding(不重新快照)+ 恢复丢失的
|
|
781
781
|
// cwdOverride(lane cwd 同源)。否则编辑后重新执行的 run 会丢 cwd 与绑定。
|
|
782
782
|
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
783
783
|
await this.handle(editedContent, ctx, {
|
|
@@ -834,7 +834,7 @@ export class CommandRouter {
|
|
|
834
834
|
const latestTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
835
835
|
if (latestTask?.editedMessage) {
|
|
836
836
|
const editedContent = latestTask.editedMessage;
|
|
837
|
-
|
|
837
|
+
///Step3: 复用原任务 binding + 恢复 cwdOverride(同上)。
|
|
838
838
|
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
839
839
|
await this.handle(editedContent, ctx, {
|
|
840
840
|
cwdOverride: cwd,
|
|
@@ -1049,7 +1049,7 @@ export class CommandRouter {
|
|
|
1049
1049
|
// setTaskReplacement and the begin-path consumption are synchronous, so
|
|
1050
1050
|
// there is no interleaving window.
|
|
1051
1051
|
//
|
|
1052
|
-
|
|
1052
|
+
///Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
1053
1053
|
const inputTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
1054
1054
|
if (!inputTask) {
|
|
1055
1055
|
// Task already left the queue (began or was cancelled) before we could
|
|
@@ -1155,7 +1155,7 @@ export class CommandRouter {
|
|
|
1155
1155
|
async handleLsRefresh(value, ctx) {
|
|
1156
1156
|
const targetPath = value.path;
|
|
1157
1157
|
const offset = value.offset ?? 0;
|
|
1158
|
-
// TOCTOU guard
|
|
1158
|
+
// TOCTOU guard: existsSync → statSync can race (target
|
|
1159
1159
|
// deleted in between), throwing into enqueueImmediate's catch which only
|
|
1160
1160
|
// logs — violating the "card button clicks must give visible feedback"
|
|
1161
1161
|
// red line. Treat a vanished/non-dir path as "use current cwd" instead of
|
|
@@ -1185,7 +1185,7 @@ export class CommandRouter {
|
|
|
1185
1185
|
return { toast: { type: 'error', content: paging.error } };
|
|
1186
1186
|
}
|
|
1187
1187
|
const resolvedTarget = path.resolve(targetPath);
|
|
1188
|
-
// TOCTOU guard
|
|
1188
|
+
// TOCTOU guard: existsSync → statSync can race; without the
|
|
1189
1189
|
// try/catch the throw lands in enqueueImmediate's silent .catch and the
|
|
1190
1190
|
// paginated card never updates (no visible feedback for the click).
|
|
1191
1191
|
let isDir;
|
|
@@ -1208,7 +1208,7 @@ export class CommandRouter {
|
|
|
1208
1208
|
*
|
|
1209
1209
|
* offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
|
|
1210
1210
|
* handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
|
|
1211
|
-
* ls.browse / ls.switch 的 value 里刻意不带 q
|
|
1211
|
+
* ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选。
|
|
1212
1212
|
*/
|
|
1213
1213
|
async handleLsFilter(value, ctx) {
|
|
1214
1214
|
const targetPath = value.path;
|
|
@@ -1352,7 +1352,7 @@ export class CommandRouter {
|
|
|
1352
1352
|
};
|
|
1353
1353
|
}
|
|
1354
1354
|
/**
|
|
1355
|
-
*
|
|
1355
|
+
* 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
1356
1356
|
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
1357
1357
|
*/
|
|
1358
1358
|
rebuildListCard(kind, offset, ctx, q) {
|
|
@@ -1361,7 +1361,7 @@ export class CommandRouter {
|
|
|
1361
1361
|
return this.cmdWs([], ctx, offset, q).card;
|
|
1362
1362
|
}
|
|
1363
1363
|
/**
|
|
1364
|
-
*
|
|
1364
|
+
* 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
1365
1365
|
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
1366
1366
|
*/
|
|
1367
1367
|
async refreshListCard(kind, value, ctx) {
|
|
@@ -1372,7 +1372,7 @@ export class CommandRouter {
|
|
|
1372
1372
|
return card;
|
|
1373
1373
|
}
|
|
1374
1374
|
/**
|
|
1375
|
-
*
|
|
1375
|
+
* 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
|
|
1376
1376
|
* 的公共骨架——校验 orderId → reload+find → mutate → 成功后 toast + raw 卡。
|
|
1377
1377
|
* mutate 返回 error 时仅回 error toast,不重渲染。
|
|
1378
1378
|
* 注意:本 helper 只服务「toast + card 进响应体」语义(飞书需用响应体原地
|
|
@@ -1863,7 +1863,7 @@ export class CommandRouter {
|
|
|
1863
1863
|
try {
|
|
1864
1864
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
1865
1865
|
this.config = setConfigValues(this.configPath, this.config, updates);
|
|
1866
|
-
//
|
|
1866
|
+
// 运行时传播(idleTimeout / clearRunners / syncAgentChoices /
|
|
1867
1867
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1868
1868
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1869
1869
|
this.pendingConfig = null;
|
|
@@ -1909,7 +1909,7 @@ export class CommandRouter {
|
|
|
1909
1909
|
return diffConfig(original, pending);
|
|
1910
1910
|
}
|
|
1911
1911
|
/**
|
|
1912
|
-
* config
|
|
1912
|
+
* config 写盘后的运行时传播(自 config.save 卡片路径抽出,卡片保存与
|
|
1913
1913
|
* `/config <key> <value>` 文本直写两条路径共用)。
|
|
1914
1914
|
*
|
|
1915
1915
|
* 1. bridge 运行时值同步:setConfig + idle watchdog timeout;
|
|
@@ -1917,7 +1917,7 @@ export class CommandRouter {
|
|
|
1917
1917
|
* 配置值,/status 与真实 run 自相矛盾);
|
|
1918
1918
|
* 3. defaultAgent 切换 → 旧 agent session 存 previousSessions、新 agent 恢复
|
|
1919
1919
|
* 或清空(等效 /new);
|
|
1920
|
-
* 4. 当前 agent 配置变更 → syncAgentChoices
|
|
1920
|
+
* 4. 当前 agent 配置变更 → syncAgentChoices 并原子写盘(禁裸同步写盘,
|
|
1921
1921
|
* 写一半崩溃会截断 config.yaml 导致 bridge 起不来)。
|
|
1922
1922
|
*
|
|
1923
1923
|
* @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
|
|
@@ -1991,7 +1991,7 @@ export class CommandRouter {
|
|
|
1991
1991
|
switchSessionId = '';
|
|
1992
1992
|
}
|
|
1993
1993
|
}
|
|
1994
|
-
// DSH preset 变更 = 下次 run 新建 session(§4.3
|
|
1994
|
+
// DSH preset 变更 = 下次 run 新建 session(§4.3):
|
|
1995
1995
|
// preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
|
|
1996
1996
|
// 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
|
|
1997
1997
|
// preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
|
|
@@ -2009,7 +2009,7 @@ export class CommandRouter {
|
|
|
2009
2009
|
switchNotice = `DSH preset 已变更,下次消息将新建会话`;
|
|
2010
2010
|
}
|
|
2011
2011
|
}
|
|
2012
|
-
// DSH host 变更 =
|
|
2012
|
+
// DSH host 变更 = 换服务:旧 host 的 sessionId 发往新 host 是错的。
|
|
2013
2013
|
// 视为 session 边界,仿 preset 分支停车 + 清空当前 sessionId,下次 run 新建会话。
|
|
2014
2014
|
const hasDshHostChange = Object.keys(updates).some((k) => k === 'agents.dsh.host');
|
|
2015
2015
|
if (hasDshHostChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
|
|
@@ -2029,11 +2029,11 @@ export class CommandRouter {
|
|
|
2029
2029
|
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
2030
2030
|
if (hasCurrentAgentUpdate) {
|
|
2031
2031
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
2032
|
-
//
|
|
2032
|
+
// agentChoices 同步写盘必须走原子写(tmp+rename),禁裸同步写盘
|
|
2033
2033
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
2034
2034
|
this.bridge.setConfig(this.config);
|
|
2035
2035
|
}
|
|
2036
|
-
//
|
|
2036
|
+
// live approval settings: active workspace-lifetime runners
|
|
2037
2037
|
// are not evicted by clearRunners() (would orphan the live subprocess).
|
|
2038
2038
|
// Push the new approval-mode settings to those runners via the unified
|
|
2039
2039
|
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
@@ -2446,7 +2446,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2446
2446
|
// Claude writes into JSONL (e.g. `/tmp` → `/private/tmp` on macOS).
|
|
2447
2447
|
// Without this, a hand-typed unresolved path never matches any JSONL's
|
|
2448
2448
|
// cwd field and the user's `/cd /tmp/foo` never finds their sessions.
|
|
2449
|
-
// TOCTOU: the directory may vanish between stat and realpath
|
|
2449
|
+
// TOCTOU: the directory may vanish between stat and realpath.
|
|
2450
2450
|
// Reject gracefully instead of letting realpathSync throw into the queue
|
|
2451
2451
|
// task's .catch (which only logs — the user gets no feedback). Mirrors
|
|
2452
2452
|
// handleLsSwitch's realpath guard so /cd and ls.switch behave identically.
|
|
@@ -2503,7 +2503,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2503
2503
|
*/
|
|
2504
2504
|
switchCwdAndNotify(userId, canonical, _ctx) {
|
|
2505
2505
|
this.sessionStore.setCwd(userId, canonical);
|
|
2506
|
-
// Auto-resume newest session if exists (
|
|
2506
|
+
// Auto-resume newest session if exists (读取失败不阻断切换,给可见提示)
|
|
2507
2507
|
let newestSession;
|
|
2508
2508
|
try {
|
|
2509
2509
|
newestSession = this.sessionReader.getNewestSession(canonical);
|
|
@@ -2634,7 +2634,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2634
2634
|
}
|
|
2635
2635
|
/**
|
|
2636
2636
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
2637
|
-
* card.
|
|
2637
|
+
* card. duck-typing — check if the runner has `runCompact`
|
|
2638
2638
|
* (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
|
|
2639
2639
|
*/
|
|
2640
2640
|
canCompactSession(agentKind, cwd) {
|
|
@@ -3111,7 +3111,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3111
3111
|
if (!fs.existsSync(wsPath))
|
|
3112
3112
|
return { text: `路径不存在: ${wsPath}` };
|
|
3113
3113
|
// Canonicalize via realpath so it matches Claude JSONL cwd (2026-06-21).
|
|
3114
|
-
// TOCTOU guard
|
|
3114
|
+
// TOCTOU guard: mirror cmdCd — if the path vanishes
|
|
3115
3115
|
// between existsSync and realpath, reply gracefully instead of
|
|
3116
3116
|
// throwing into the queue task's silent .catch.
|
|
3117
3117
|
let canonical;
|
|
@@ -3332,7 +3332,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3332
3332
|
}
|
|
3333
3333
|
}
|
|
3334
3334
|
/**
|
|
3335
|
-
*
|
|
3335
|
+
* degraded card when a session's export is non-empty but unparseable
|
|
3336
3336
|
* (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
|
|
3337
3337
|
* The session exists, so reuse listSessions metadata (title) instead of the
|
|
3338
3338
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
@@ -3427,7 +3427,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3427
3427
|
};
|
|
3428
3428
|
}
|
|
3429
3429
|
}
|
|
3430
|
-
//
|
|
3430
|
+
// 校验通过后才写入
|
|
3431
3431
|
this.sessionStore.setSessionId(ctx.userId, agentKind, sessionIdArg, cwd);
|
|
3432
3432
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionIdArg, cwd, {
|
|
3433
3433
|
agentKind,
|
|
@@ -3490,7 +3490,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3490
3490
|
}
|
|
3491
3491
|
}
|
|
3492
3492
|
catch (err) {
|
|
3493
|
-
//
|
|
3493
|
+
// 读取失败必须给用户可见反馈,而不是误导性的「没有 session 记录」
|
|
3494
3494
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3495
3495
|
return { text: `读取 ${agentDisplayName(agentKind)} session 列表失败: ${msg}` };
|
|
3496
3496
|
}
|
|
@@ -3562,7 +3562,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3562
3562
|
const prefetchedTitle = meta?.displayTitle;
|
|
3563
3563
|
// aiTitle 不能单独作为"预取标题"键:若 displayTitle 缺失(理论上 opencode
|
|
3564
3564
|
// info.title 非空但 export 无 user text),标题兜底是 summary,label 必须
|
|
3565
|
-
// 是中性"会话摘要"而不是"最近输入"(Review
|
|
3565
|
+
// 是中性"会话摘要"而不是"最近输入"(Review 边缘分支)。
|
|
3566
3566
|
const titleText = prefetchedTitle ?? (summaryIsPlaceholder ? '' : s.summary);
|
|
3567
3567
|
const titleLabel = prefetchedTitle
|
|
3568
3568
|
? meta?.aiTitle
|
|
@@ -3727,7 +3727,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3727
3727
|
elements.push({ tag: 'hr' });
|
|
3728
3728
|
}
|
|
3729
3729
|
}
|
|
3730
|
-
// Pagination bar
|
|
3730
|
+
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
3731
3731
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
3732
3732
|
if (totalPages > 1) {
|
|
3733
3733
|
elements.push(...paginationBar({
|
|
@@ -3763,14 +3763,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3763
3763
|
const value = args.slice(1).join(' ');
|
|
3764
3764
|
try {
|
|
3765
3765
|
const oldDefaultAgent = this.config.defaultAgent;
|
|
3766
|
-
//
|
|
3766
|
+
// 直写前若 pendingConfig 存在未保存差异,提示用户被丢弃的条数,
|
|
3767
3767
|
// 避免静默清空导致用户在卡片上的修改无反馈丢失。差异必须在 setConfigValue
|
|
3768
3768
|
// 之前计算——直写本身也会改变 config,不应计入「被丢弃的未保存修改」。
|
|
3769
3769
|
const discarded = this.pendingConfig
|
|
3770
3770
|
? Object.keys(this.diffConfig(this.config, this.pendingConfig)).length
|
|
3771
3771
|
: 0;
|
|
3772
3772
|
this.config = setConfigValue(this.configPath, this.config, key, value);
|
|
3773
|
-
//
|
|
3773
|
+
// 直写路径与 config.save 卡片路径共用运行时传播(idleTimeout /
|
|
3774
3774
|
// clearRunners / syncAgentChoices / defaultAgent 切换)。key 经 mapAgentKey
|
|
3775
3775
|
// 归一化,pi./codex./opencode./kimi. 命中 agents.* 判定;claude. 与
|
|
3776
3776
|
// 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 流程「等进程退出再收尾」
|
|
@@ -43,7 +43,7 @@ const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
|
43
43
|
const COMPACTION_RESULT_SUBTYPES = new Set(['compact', 'compaction']);
|
|
44
44
|
/**
|
|
45
45
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
46
|
-
* Terminator/SpawnHeartbeat
|
|
46
|
+
* Terminator/SpawnHeartbeat/退出分发器机制(契约保持),
|
|
47
47
|
* 但 run() 覆盖为「长驻 + 按 turn 消费」:进程跨 turn 存活,turn 结束以
|
|
48
48
|
* stream-json result 事件(非 compact)为界,而不是进程退出。
|
|
49
49
|
*/
|
|
@@ -127,7 +127,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
127
127
|
// 或 --resume 目标会话)。比较进程当前 session(init 报告值)而非 spawn
|
|
128
128
|
// 请求值:bridge 会把 init 的 session_id 写回 SessionStore,第二条消息
|
|
129
129
|
// 带着它来;若拿 spawn 请求值('')比较,每条消息都会误判切换、杀进程
|
|
130
|
-
// 重启,长驻设计失效(2026-08-16
|
|
130
|
+
// 重启,长驻设计失效(2026-08-16)。
|
|
131
131
|
if (this.isRunning && this.sessionId !== (opts.sessionId ?? '')) {
|
|
132
132
|
getLogger().info(`[${this.logTag}] session mismatch: process=${this.sessionId || '(fresh)'} ` +
|
|
133
133
|
`requested=${opts.sessionId ?? '(fresh)'}, recycling process`);
|
|
@@ -174,7 +174,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
174
174
|
getLogger().warn(`[${this.logTag}] respondPermission skipped: process not running requestId=${requestId}`);
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
|
-
//
|
|
177
|
+
// requestId 不在待审批表(已 control_cancel / 进程回收)时
|
|
178
178
|
// 丢弃响应,不能以空 updatedInput allow(claude 会拿空输入执行工具)。
|
|
179
179
|
if (!this.pendingToolInputs.has(requestId)) {
|
|
180
180
|
getLogger().warn(`[${this.logTag}] respondPermission dropped: unknown requestId=${requestId}`);
|
|
@@ -204,7 +204,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
204
204
|
async respondAskUserQuestion(requestId, answers) {
|
|
205
205
|
if (!this.isRunning)
|
|
206
206
|
return;
|
|
207
|
-
//
|
|
207
|
+
// 未知 requestId 时不能回写缺原始 questions 的 answers
|
|
208
208
|
// (官方要求 updatedInput 必须包含原始 questions)。
|
|
209
209
|
if (!this.pendingToolInputs.has(requestId)) {
|
|
210
210
|
getLogger().warn(`[${this.logTag}] respondAskUserQuestion dropped: unknown requestId=${requestId}`);
|
|
@@ -285,7 +285,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
285
285
|
*/
|
|
286
286
|
async startProcess(opts) {
|
|
287
287
|
if (this.isRunning) {
|
|
288
|
-
//
|
|
288
|
+
// 复用进程(跨 turn 长驻)时清掉上一轮的空闲回收 timer——
|
|
289
289
|
// 否则长 turn 中它会空转触发一次(回调因 turnActive 空转返回,无害但
|
|
290
290
|
// 多余);新一轮结束后的 armIdleTimer 会重新武装。
|
|
291
291
|
this.clearIdleTimer();
|
|
@@ -381,7 +381,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
381
381
|
// =========================================================================
|
|
382
382
|
/**
|
|
383
383
|
* 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
|
|
384
|
-
* 消费者提前关闭生成器(bridge 循环体抛错)→
|
|
384
|
+
* 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(语义)。
|
|
385
385
|
*
|
|
386
386
|
* result = turn 终态(2026-08-16 真实 claude 实测:长驻模式下 result 之后
|
|
387
387
|
* 不再有 stdout 事件,后台任务完成是静默的;旧 -p 流程「等进程退出再收尾」
|
|
@@ -554,7 +554,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
554
554
|
async translateEvent(raw) {
|
|
555
555
|
const event = raw;
|
|
556
556
|
const type = event.type;
|
|
557
|
-
//
|
|
557
|
+
// --resume 会先重放上一轮历史事件(旧 result 之外还可能有
|
|
558
558
|
// 旧 assistant/user 内容)。init 之前除 init 本身外一律丢弃,避免历史
|
|
559
559
|
// 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
|
|
560
560
|
if (type !== 'system' && !this.sawInit)
|
|
@@ -678,7 +678,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
678
678
|
const request = (raw.request ?? {});
|
|
679
679
|
const subtype = request.subtype;
|
|
680
680
|
if (subtype !== 'can_use_tool') {
|
|
681
|
-
//
|
|
681
|
+
// 未知 subtype 不能静默丢弃——claude 在等 control_response,
|
|
682
682
|
// 不回应 turn 会永久挂起(无审批卡、无超时)。deny 兜底让 claude 继续。
|
|
683
683
|
getLogger().warn(`[${this.logTag}] unknown control request subtype=${String(subtype)} requestId=${requestId}`);
|
|
684
684
|
void this.writeControlResponse(requestId, {
|
|
@@ -692,7 +692,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
692
692
|
const toolName = String(request.tool_name ?? '');
|
|
693
693
|
const input = (request.input ?? {});
|
|
694
694
|
this.pendingToolInputs.set(requestId, input);
|
|
695
|
-
//
|
|
695
|
+
// 允许所有只放行工具权限;AskUserQuestion 不能被空 answers
|
|
696
696
|
// 自动放行(claude 会把空答案当作已作答),必须继续上抛卡片让用户回答。
|
|
697
697
|
if (this.autoApprove && toolName !== 'AskUserQuestion') {
|
|
698
698
|
getLogger().info(`[${this.logTag}] auto-approving requestId=${requestId} tool=${toolName}`);
|
|
@@ -702,7 +702,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
702
702
|
}).catch((err) => {
|
|
703
703
|
getLogger().warn(`[${this.logTag}] auto-approve write failed requestId=${requestId}: ${err.message}`);
|
|
704
704
|
});
|
|
705
|
-
//
|
|
705
|
+
// 自动放行不经过协调器/respondPermission,条目不会随
|
|
706
706
|
// 响应被删除——立即释放,否则长会话(允许所有)下 Map 无界增长。
|
|
707
707
|
this.pendingToolInputs.delete(requestId);
|
|
708
708
|
return [];
|
|
@@ -717,7 +717,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
717
717
|
behavior: 'deny',
|
|
718
718
|
message: '无法解析 AskUserQuestion 选项',
|
|
719
719
|
}).catch(() => { });
|
|
720
|
-
//
|
|
720
|
+
// 解析失败 deny 后同样释放条目(不经过 respondPermission)。
|
|
721
721
|
this.pendingToolInputs.delete(requestId);
|
|
722
722
|
return [];
|
|
723
723
|
}
|
|
@@ -760,7 +760,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
760
760
|
];
|
|
761
761
|
}
|
|
762
762
|
// 非命令工具的兜底防御:input 无 command 字段时 JSON 序列化,但空对象
|
|
763
|
-
// 序列化成 `{}`
|
|
763
|
+
// 序列化成 `{}` 对用户无意义——内容为空时回退展示工具名。
|
|
764
764
|
const rawCommand = typeof input.command === 'string' ? input.command : JSON.stringify(input);
|
|
765
765
|
const command = rawCommand && rawCommand !== '{}' && rawCommand !== 'null'
|
|
766
766
|
? rawCommand
|
|
@@ -817,7 +817,7 @@ function parseUserQuestions(input) {
|
|
|
817
817
|
const question = typeof q.question === 'string' ? q.question : '';
|
|
818
818
|
if (!question)
|
|
819
819
|
continue;
|
|
820
|
-
//
|
|
820
|
+
// answers 字典以问题文本为 key,重复文本会互相覆盖——
|
|
821
821
|
// 视为解析失败(调用方 deny 兜底),不把坏问题上抛给用户。
|
|
822
822
|
if (seenQuestions.has(question))
|
|
823
823
|
return [];
|
|
@@ -839,7 +839,7 @@ function parseUserQuestions(input) {
|
|
|
839
839
|
});
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
|
-
//
|
|
842
|
+
// 零选项问题无法作答(多选连自定义答案都没有),视为
|
|
843
843
|
// 解析失败(调用方 deny 兜底),不把坏问题上抛给用户。
|
|
844
844
|
if (options.length === 0)
|
|
845
845
|
return [];
|
|
@@ -136,7 +136,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
136
136
|
},
|
|
137
137
|
};
|
|
138
138
|
this.connectionManager = new ConnectionManager(managerOpts);
|
|
139
|
-
//
|
|
139
|
+
// 协议停止通道:codex 的 cancel 是 `turn/interrupt` 请求,已由
|
|
140
140
|
// cancelCurrentTurn 实现;登记后 win32 优雅停止才有通道可用(否则一路树杀)。
|
|
141
141
|
this.connectionManager.stopper = ({ client }) => this.buildCooperativeStop(client);
|
|
142
142
|
}
|
|
@@ -235,7 +235,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
235
235
|
else if (pending.kind === 'permissions') {
|
|
236
236
|
// 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
|
|
237
237
|
// scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
|
|
238
|
-
// 勾选的条目当作授予返回(2026-08-12
|
|
238
|
+
// 勾选的条目当作授予返回(2026-08-12 :勾选后点「拒绝」实
|
|
239
239
|
// 际授予了所选权限)。accept/acceptForSession 才带用户勾选的条目。
|
|
240
240
|
const denied = action === 'decline' || action === 'cancel';
|
|
241
241
|
client.respond(requestId, {
|
|
@@ -358,7 +358,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
358
358
|
}
|
|
359
359
|
else {
|
|
360
360
|
const threadResult = await client.request('thread/start', threadParams);
|
|
361
|
-
//
|
|
361
|
+
// 会话键假设:主线程 thread.id === session_meta.session_id,
|
|
362
362
|
// 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
|
|
363
363
|
// 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
|
|
364
364
|
// 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
|
|
@@ -26,7 +26,7 @@ export declare class CodexAppServerTranslator {
|
|
|
26
26
|
/** Full command output per item id (item-scoped snapshot). */
|
|
27
27
|
private toolOutputByItem;
|
|
28
28
|
/**
|
|
29
|
-
* Tool identity per item id (信息保真
|
|
29
|
+
* Tool identity per item id (信息保真): recorded at item/started, spread
|
|
30
30
|
* into every toolOutput turn_diff, released at item/completed.
|
|
31
31
|
*/
|
|
32
32
|
private toolItemMeta;
|
|
@@ -85,7 +85,7 @@ export declare class CodexAppServerTranslator {
|
|
|
85
85
|
private handlePlanUpdated;
|
|
86
86
|
private handleTokenUsageUpdated;
|
|
87
87
|
/**
|
|
88
|
-
* warning 通知 → notice 事件(信息保真
|
|
88
|
+
* warning 通知 → notice 事件(信息保真 运行期告警不再静默丢弃)。
|
|
89
89
|
*/
|
|
90
90
|
private handleWarning;
|
|
91
91
|
/**
|
|
@@ -27,7 +27,7 @@ export class CodexAppServerTranslator {
|
|
|
27
27
|
/** Full command output per item id (item-scoped snapshot). */
|
|
28
28
|
toolOutputByItem = new Map();
|
|
29
29
|
/**
|
|
30
|
-
* Tool identity per item id (信息保真
|
|
30
|
+
* Tool identity per item id (信息保真): recorded at item/started, spread
|
|
31
31
|
* into every toolOutput turn_diff, released at item/completed.
|
|
32
32
|
*/
|
|
33
33
|
toolItemMeta = new Map();
|
|
@@ -326,7 +326,7 @@ export class CodexAppServerTranslator {
|
|
|
326
326
|
return [];
|
|
327
327
|
}
|
|
328
328
|
this.toolOutputByItem.set(item.id, authoritative);
|
|
329
|
-
// 信息保真
|
|
329
|
+
// 信息保真 完成事件同样携带身份,并在发出后释放 meta。
|
|
330
330
|
const meta = this.toolItemMeta.get(item.id);
|
|
331
331
|
this.toolItemMeta.delete(item.id);
|
|
332
332
|
return [
|
|
@@ -383,7 +383,7 @@ export class CodexAppServerTranslator {
|
|
|
383
383
|
];
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
|
-
// 信息保真
|
|
386
|
+
// 信息保真 webSearch/mcpToolCall/dynamicToolCall 等无专属 completed 分支的
|
|
387
387
|
// 工具 item 也必须收尾——否则 run-state 侧工具块永挂 running(无 completedAt/终态),
|
|
388
388
|
// toolItemMeta 泄漏到下个 turn 的 clear() 才被释放。
|
|
389
389
|
if (this.startedToolItems.has(item.id)) {
|
|
@@ -466,7 +466,7 @@ export class CodexAppServerTranslator {
|
|
|
466
466
|
return [];
|
|
467
467
|
}
|
|
468
468
|
/**
|
|
469
|
-
* warning 通知 → notice 事件(信息保真
|
|
469
|
+
* warning 通知 → notice 事件(信息保真 运行期告警不再静默丢弃)。
|
|
470
470
|
*/
|
|
471
471
|
handleWarning(params) {
|
|
472
472
|
return [
|
|
@@ -653,7 +653,7 @@ export class CodexAppServerTranslator {
|
|
|
653
653
|
// item/started 锚定工具块位置(真实时序):命令开始时即创建工具块,
|
|
654
654
|
// 而不是等到首个输出 delta——否则后续 reasoning item 会插到 command 之前。
|
|
655
655
|
this.startedToolItems.add(item.id);
|
|
656
|
-
// 信息保真
|
|
656
|
+
// 信息保真 记录工具身份,随 turn_diff 透传(run-state 首见时写入)。
|
|
657
657
|
this.toolItemMeta.set(item.id, {
|
|
658
658
|
toolName: 'Bash',
|
|
659
659
|
toolInput: { command: item.command, ...(item.cwd ? { cwd: item.cwd } : {}) },
|