lark-remote 0.1.7 → 0.1.9

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bridge/approval-coordinator.d.ts +30 -1
  3. package/dist/bridge/approval-coordinator.js +93 -5
  4. package/dist/bridge/index.d.ts +36 -6
  5. package/dist/bridge/index.js +56 -10
  6. package/dist/card/approval-render.js +287 -48
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +40 -16
  14. package/dist/order/alias-resolve.d.ts +6 -2
  15. package/dist/order/index.d.ts +9 -2
  16. package/dist/order/index.js +33 -0
  17. package/dist/router/card-action-payload.d.ts +30 -0
  18. package/dist/router/card-action-payload.js +39 -0
  19. package/dist/router/config/claude.js +8 -3
  20. package/dist/router/config/dsh.d.ts +51 -0
  21. package/dist/router/config/dsh.js +179 -0
  22. package/dist/router/config/index.d.ts +1 -1
  23. package/dist/router/config/index.js +4 -1
  24. package/dist/router/config/types.d.ts +6 -0
  25. package/dist/router/index.d.ts +39 -4
  26. package/dist/router/index.js +471 -90
  27. package/dist/router/utils.js +12 -2
  28. package/dist/runner/agent-choices-common.js +3 -0
  29. package/dist/runner/claude/runner.js +22 -0
  30. package/dist/runner/claude/session.d.ts +26 -0
  31. package/dist/runner/claude/session.js +105 -9
  32. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  33. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  34. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  35. package/dist/runner/codex/app-server/runner.js +56 -1
  36. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  37. package/dist/runner/codex/app-server/translator.js +45 -0
  38. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  39. package/dist/runner/common/acp/protocol-types.js +2 -0
  40. package/dist/runner/common/connection-based-runner.js +10 -5
  41. package/dist/runner/dsh/client.d.ts +95 -0
  42. package/dist/runner/dsh/client.js +418 -0
  43. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  44. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  45. package/dist/runner/dsh/index.d.ts +3 -0
  46. package/dist/runner/dsh/index.js +4 -0
  47. package/dist/runner/dsh/runner.d.ts +57 -0
  48. package/dist/runner/dsh/runner.js +211 -0
  49. package/dist/runner/dsh/translator.d.ts +28 -0
  50. package/dist/runner/dsh/translator.js +98 -0
  51. package/dist/runner/dsh/types.d.ts +155 -0
  52. package/dist/runner/dsh/types.js +14 -0
  53. package/dist/runner/index.d.ts +1 -0
  54. package/dist/runner/index.js +1 -0
  55. package/dist/runner/kimi/acp/runner.js +84 -1
  56. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  57. package/dist/runner/kimi/acp/translator.js +108 -3
  58. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  59. package/dist/runner/pi/rpc/runner.js +8 -12
  60. package/dist/runner/probe.js +6 -1
  61. package/dist/runner/question-common.d.ts +26 -0
  62. package/dist/runner/question-common.js +42 -0
  63. package/dist/runner/types.d.ts +94 -10
  64. package/dist/session/claude/sessions.js +37 -15
  65. package/dist/session/dsh/index.d.ts +1 -0
  66. package/dist/session/dsh/index.js +2 -0
  67. package/dist/session/dsh/sessions.d.ts +44 -0
  68. package/dist/session/dsh/sessions.js +252 -0
  69. package/dist/session/index.d.ts +1 -0
  70. package/dist/session/index.js +1 -0
  71. package/dist/workspace/index.d.ts +12 -0
  72. package/dist/workspace/index.js +28 -2
  73. package/package.json +2 -1
  74. package/dist/order/alias-store.d.ts +0 -38
  75. package/dist/order/alias-store.js +0 -106
@@ -80,6 +80,16 @@ export function formatUsageStats(usage, options) {
80
80
  const cumTotalSuffix = usage?.cumulativeTotalTokens !== undefined
81
81
  ? ` · 累计 ${formatTokenK(usage.cumulativeTotalTokens)}`
82
82
  : '';
83
+ // 一致性护栏:本 run ≤ 累计 是硬不变量(累计 = 所有 run 之和,必 ≥ 本 run)。
84
+ // 违反说明来源 scope 混用(如 DeepSeek live result usage 是累积/超大 scope,
85
+ // 大于 jsonl 去重聚合的 session 累计)。只标记不修正——数字保持原样便于排查,
86
+ // 由数据源修复。两侧都有值时校验(避免旧数据/缺失字段误报)。
87
+ // 两个分支(real input/output / 10% 估计)共用,保证"本 run > 累计"在任意
88
+ // 路径都显式暴露。
89
+ const withCumGuard = (totalTokens) => cumTotalSuffix +
90
+ (usage?.cumulativeTotalTokens !== undefined && totalTokens > usage.cumulativeTotalTokens
91
+ ? ' · ⚠️ 累计异常'
92
+ : '');
83
93
  // 计算累计 cache 命中率
84
94
  const cumInput = usage?.cumulativeInputTokens;
85
95
  const cumCacheRead = usage?.cumulativeCacheReadTokens;
@@ -110,7 +120,7 @@ export function formatUsageStats(usage, options) {
110
120
  lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
111
121
  }
112
122
  lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
113
- lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
123
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
114
124
  }
115
125
  else {
116
126
  // Estimate path: when real input/output are absent,
@@ -127,7 +137,7 @@ export function formatUsageStats(usage, options) {
127
137
  lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
128
138
  }
129
139
  lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
130
- lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
140
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
131
141
  }
132
142
  }
133
143
  return lines.join('\n');
@@ -23,6 +23,9 @@ export const AGENT_CHOICE_FIELDS = {
23
23
  { configKey: 'model', choicesKey: 'model' },
24
24
  { configKey: 'thinkingEffort', choicesKey: 'thinkingEffort' },
25
25
  ],
26
+ // DSH has no model/provider choice fields (host endpoint is connection
27
+ // config, not a per-run choice) — empty to satisfy the Record type.
28
+ dsh: [],
26
29
  };
27
30
  /** 返回指定 agent 的字段映射;claude 或未知 agent 返回 undefined。 */
28
31
  export function choiceFieldsFor(agent) {
@@ -102,6 +102,28 @@ export class ClaudeRunner {
102
102
  this.session.setAutoApprove(true);
103
103
  await this.session.respondPermission(requestId, { behavior: 'allow' });
104
104
  return;
105
+ case 'accept_with_feedback': {
106
+ // 计划审批「批准并采纳修改」:allow + updatedInput.plan(改写过的新计划),
107
+ // 触发 Claude 侧 planWasEdited → tool_result 回显 edited plan 并写回文件。
108
+ const plan = response.plan;
109
+ if (!plan || !plan.trim()) {
110
+ throw new Error('approval accept_with_feedback missing plan payload');
111
+ }
112
+ await this.session.respondPermission(requestId, {
113
+ behavior: 'allow',
114
+ updatedInput: { plan },
115
+ });
116
+ return;
117
+ }
118
+ case 'decline_with_feedback': {
119
+ // 计划审批「拒绝并附意见」:deny + message,Claude 留在 plan 模式修订。
120
+ const message = response.message?.trim();
121
+ if (!message) {
122
+ throw new Error('approval decline_with_feedback missing message payload');
123
+ }
124
+ await this.session.respondPermission(requestId, { behavior: 'deny', message });
125
+ return;
126
+ }
105
127
  case 'decline':
106
128
  await this.session.respondPermission(requestId, { behavior: 'deny' });
107
129
  return;
@@ -46,6 +46,11 @@ export interface ClaudeSessionOptions {
46
46
  export interface PermissionResult {
47
47
  behavior: 'allow' | 'deny';
48
48
  message?: string;
49
+ /**
50
+ * allow 时显式覆盖 updatedInput(默认回写 pendingToolInputs 原始 input)。
51
+ * 计划审批「批准并采纳修改意见」用它携带改写的 plan。
52
+ */
53
+ updatedInput?: unknown;
49
54
  }
50
55
  /**
51
56
  * ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
@@ -74,6 +79,12 @@ export declare class ClaudeSession extends SpawningRunner {
74
79
  private waitResolve;
75
80
  /** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
76
81
  private pendingToolInputs;
82
+ /**
83
+ * 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
84
+ * 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
85
+ * 会话进程生命周期内有效(长驻进程 = 一个会话)。
86
+ */
87
+ private planFilePath;
77
88
  /** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
78
89
  private autoApprove;
79
90
  /** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
@@ -160,6 +171,21 @@ export declare class ClaudeSession extends SpawningRunner {
160
171
  */
161
172
  private writeStdin;
162
173
  private translateEvent;
174
+ /**
175
+ * 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
176
+ * ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
177
+ * 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
178
+ * 退化场景——渲染层可展示实际落盘位置)。
179
+ */
180
+ private trackPlanFilePath;
181
+ /**
182
+ * ExitPlanMode 计划解析,优先级:
183
+ * 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
184
+ * 2. input.planFilePath 指向的文件;
185
+ * 3. 会话内 Write/Edit 跟踪到的计划文件。
186
+ * 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
187
+ */
188
+ private resolvePlanContent;
163
189
  private translateControlRequest;
164
190
  /** 统一方案:所有 runner 自己生成 timestamp(缺失时补)。 */
165
191
  private withTimestamp;
@@ -27,13 +27,17 @@
27
27
  * 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
28
28
  * 时经 ProcessStopper 组杀,下条消息按 SessionStore 的 sessionId --resume。
29
29
  */
30
+ import { readFile } from 'node:fs/promises';
30
31
  import { silentlyUnlink } from '../../common/fs.js';
31
32
  import { getLogger } from '../../logger/index.js';
32
33
  import { SpawningRunner } from '../common/spawning-runner.js';
33
34
  import { pipeAllStdio } from '../common/runner-utils.js';
34
35
  import { authErrorEvent, syntheticInitEvent } from '../common/runner-utils.js';
36
+ import { makeQuestionApprovalEvent } from '../question-common.js';
35
37
  /** 拒绝权限时的默认提示(进入 claude 上下文,中文用户可读)。 */
36
38
  const DENY_MESSAGE = '用户拒绝了此工具调用,请停止并等待用户指令。';
39
+ /** ExitPlanMode 计划文件读取上限(超出截断,防超大计划拖垮事件翻译)。 */
40
+ const MAX_PLAN_CHARS = 60_000;
37
41
  /** 会话级空闲回收默认 TTL(对齐 codex appServer.idleTtlMs 默认 30 分钟)。 */
38
42
  const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
39
43
  /** result 事件里表示「turn 中途压缩」的 subtype(不是 turn 结束)。 */
@@ -65,6 +69,12 @@ export class ClaudeSession extends SpawningRunner {
65
69
  waitResolve = null;
66
70
  /** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
67
71
  pendingToolInputs = new Map();
72
+ /**
73
+ * 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
74
+ * 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
75
+ * 会话进程生命周期内有效(长驻进程 = 一个会话)。
76
+ */
77
+ planFilePath = '';
68
78
  /** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
69
79
  autoApprove = false;
70
80
  /** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
@@ -181,7 +191,9 @@ export class ClaudeSession extends SpawningRunner {
181
191
  if (result.behavior === 'allow') {
182
192
  await this.writeControlResponse(requestId, {
183
193
  behavior: 'allow',
184
- updatedInput: input ?? {},
194
+ // 显式 updatedInput 优先(计划审批「批准并采纳修改」携带改写 plan);
195
+ // 缺省回写原始 input,保证其他工具审批行为不变。
196
+ updatedInput: result.updatedInput !== undefined ? result.updatedInput : (input ?? {}),
185
197
  });
186
198
  }
187
199
  else {
@@ -343,7 +355,7 @@ export class ClaudeSession extends SpawningRunner {
343
355
  for await (const raw of stream) {
344
356
  if (generation !== this.processGeneration)
345
357
  return; // 旧进程残留事件丢弃
346
- const events = this.translateEvent(raw);
358
+ const events = await this.translateEvent(raw);
347
359
  if (events.length > 0) {
348
360
  this.eventQueue.push(...events);
349
361
  this.wakeWaiters();
@@ -529,7 +541,7 @@ export class ClaudeSession extends SpawningRunner {
529
541
  // =========================================================================
530
542
  // Internal: event translation
531
543
  // =========================================================================
532
- translateEvent(raw) {
544
+ async translateEvent(raw) {
533
545
  const event = raw;
534
546
  const type = event.type;
535
547
  // review P3-3:--resume 会先重放上一轮历史事件(旧 result 之外还可能有
@@ -537,6 +549,10 @@ export class ClaudeSession extends SpawningRunner {
537
549
  // 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
538
550
  if (type !== 'system' && !this.sawInit)
539
551
  return [];
552
+ // 跟踪会话内计划文件写入(ExitPlanMode 审批卡的 plan 文件兜底)。
553
+ if (type === 'assistant') {
554
+ this.trackPlanFilePath(event);
555
+ }
540
556
  if (type === 'control_request') {
541
557
  return this.translateControlRequest(event);
542
558
  }
@@ -587,7 +603,63 @@ export class ClaudeSession extends SpawningRunner {
587
603
  // 下游 reducer 忽略,保留诊断价值。
588
604
  return [this.withTimestamp(event)];
589
605
  }
590
- translateControlRequest(raw) {
606
+ /**
607
+ * 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
608
+ * ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
609
+ * 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
610
+ * 退化场景——渲染层可展示实际落盘位置)。
611
+ */
612
+ trackPlanFilePath(raw) {
613
+ const content = raw.message?.content;
614
+ if (!Array.isArray(content))
615
+ return;
616
+ for (const block of content) {
617
+ const b = block;
618
+ if (b?.type !== 'tool_use')
619
+ continue;
620
+ if (b.name !== 'Write' && b.name !== 'Edit')
621
+ continue;
622
+ const fp = b.input?.file_path;
623
+ if (typeof fp === 'string' && fp.includes('/plans/')) {
624
+ this.planFilePath = fp;
625
+ }
626
+ }
627
+ }
628
+ /**
629
+ * ExitPlanMode 计划解析,优先级:
630
+ * 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
631
+ * 2. input.planFilePath 指向的文件;
632
+ * 3. 会话内 Write/Edit 跟踪到的计划文件。
633
+ * 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
634
+ */
635
+ async resolvePlanContent(input, trackedPath) {
636
+ const direct = typeof input.plan === 'string' && input.plan.trim() ? input.plan : undefined;
637
+ if (direct !== undefined) {
638
+ // 计划已随 input 注入时仍携带文件路径(input.planFilePath 或会话内跟踪),
639
+ // 供卡片展示「计划落盘位置」。
640
+ const filePath = typeof input.planFilePath === 'string' && input.planFilePath
641
+ ? input.planFilePath
642
+ : trackedPath;
643
+ return filePath ? { plan: direct, planFilePath: filePath } : { plan: direct };
644
+ }
645
+ const candidate = typeof input.planFilePath === 'string' && input.planFilePath
646
+ ? input.planFilePath
647
+ : trackedPath;
648
+ if (!candidate)
649
+ return {};
650
+ try {
651
+ const content = await readFile(candidate, 'utf8');
652
+ const trimmed = content.trim();
653
+ if (trimmed) {
654
+ return { plan: trimmed.slice(0, MAX_PLAN_CHARS), planFilePath: candidate };
655
+ }
656
+ }
657
+ catch {
658
+ // 计划文件缺失/不可读:fall through,渲染层兜底。
659
+ }
660
+ return {};
661
+ }
662
+ async translateControlRequest(raw) {
591
663
  const requestId = raw.request_id;
592
664
  if (requestId === null || requestId === undefined) {
593
665
  getLogger().warn(`[${this.logTag}] control_request missing request_id, dropping`);
@@ -639,17 +711,36 @@ export class ClaudeSession extends SpawningRunner {
639
711
  this.pendingToolInputs.delete(requestId);
640
712
  return [];
641
713
  }
714
+ return [makeQuestionApprovalEvent(requestId, questions, this.sessionId)];
715
+ }
716
+ // ExitPlanMode:计划已写入计划文件(或经 input.plan 注入)。没有 command
717
+ // 语义,塞进 command 槽位会显示无意义 `{}` → 用独立 tool kind,把计划
718
+ // 全文 + 文件路径带给渲染层,决策对齐 TUI 四种交互。
719
+ if (toolName === 'ExitPlanMode') {
720
+ const resolved = await this.resolvePlanContent(input, this.planFilePath);
642
721
  const view = {
643
722
  requestId,
644
- kind: 'question',
645
- questions,
646
- availableDecisions: [],
723
+ kind: 'tool',
724
+ toolName,
725
+ reason: typeof request.description === 'string' ? request.description : undefined,
726
+ ...(resolved.plan !== undefined ? { plan: resolved.plan } : {}),
727
+ ...(resolved.planFilePath !== undefined ? { planFilePath: resolved.planFilePath } : {}),
728
+ // 对齐 TUI:批准并执行 / 批准并自动放行(acceptAll)/ 拒绝并附意见 /
729
+ // 批准并采纳修改 / 拒绝。
730
+ availableDecisions: [
731
+ 'accept',
732
+ 'acceptAll',
733
+ 'declineWithFeedback',
734
+ 'acceptWithFeedback',
735
+ 'decline',
736
+ ],
647
737
  };
738
+ getLogger().info(`[${this.logTag}] permission request requestId=${requestId} tool=${toolName}`);
648
739
  return [
649
740
  {
650
741
  type: 'approval_requested',
651
742
  requestId,
652
- kind: 'question',
743
+ kind: 'tool',
653
744
  threadId: this.sessionId,
654
745
  turnId: '',
655
746
  itemId: '',
@@ -658,7 +749,12 @@ export class ClaudeSession extends SpawningRunner {
658
749
  },
659
750
  ];
660
751
  }
661
- const command = typeof input.command === 'string' ? input.command : JSON.stringify(input ?? {});
752
+ // 非命令工具的兜底防御:input 无 command 字段时 JSON 序列化,但空对象
753
+ // 序列化成 `{}` 对用户无意义——内容为空时回退展示工具名(review P3-5)。
754
+ const rawCommand = typeof input.command === 'string' ? input.command : JSON.stringify(input);
755
+ const command = rawCommand && rawCommand !== '{}' && rawCommand !== 'null'
756
+ ? rawCommand
757
+ : toolName || '(unknown tool)';
662
758
  const view = {
663
759
  requestId,
664
760
  kind: 'command',
@@ -83,6 +83,8 @@ export interface ThreadResumeParams {
83
83
  modelProvider?: string | null;
84
84
  approvalPolicy?: AskForApproval;
85
85
  sandbox?: SandboxMode;
86
+ /** Configuration overrides for the resumed thread (codex v2 wire field). */
87
+ config?: Record<string, unknown> | null;
86
88
  }
87
89
  export interface ThreadResumeResponse {
88
90
  thread: Thread;
@@ -432,6 +434,7 @@ export declare const ServerRequestMethod: {
432
434
  readonly COMMAND_EXECUTION_APPROVAL: "item/commandExecution/requestApproval";
433
435
  readonly FILE_CHANGE_APPROVAL: "item/fileChange/requestApproval";
434
436
  readonly PERMISSIONS_APPROVAL: "item/permissions/requestApproval";
437
+ readonly REQUEST_USER_INPUT: "item/tool/requestUserInput";
435
438
  };
436
439
  export interface NetworkApprovalContext {
437
440
  host: string;
@@ -515,5 +518,37 @@ export interface PermissionsRequestApprovalResponse {
515
518
  permissions: GrantedPermissionProfile;
516
519
  scope?: 'turn' | 'session';
517
520
  }
521
+ /**
522
+ * Codex AskUserQuestion(request_user_input)协议形状。
523
+ * 来源:codex-rs/codex-rs/protocol/src/request_user_input.rs +
524
+ * app-server-protocol v2/item.rs ToolRequestUserInput*(EXPERIMENTAL)。
525
+ * isOther=true → 渲染层提供自定义答案输入;isSecret 仅影响展示(按普通输入
526
+ * 处理);options 为 null/空 → 自由文本题。
527
+ */
528
+ export interface ToolRequestUserInputOption {
529
+ label: string;
530
+ description: string;
531
+ }
532
+ export interface ToolRequestUserInputQuestion {
533
+ id: string;
534
+ header: string;
535
+ question: string;
536
+ isOther: boolean;
537
+ isSecret: boolean;
538
+ options: ToolRequestUserInputOption[] | null;
539
+ }
540
+ export interface ToolRequestUserInputParams {
541
+ threadId: string;
542
+ turnId: string;
543
+ itemId: string;
544
+ questions: ToolRequestUserInputQuestion[];
545
+ autoResolutionMs?: number | null;
546
+ }
547
+ export interface ToolRequestUserInputAnswer {
548
+ answers: string[];
549
+ }
550
+ export interface ToolRequestUserInputResponse {
551
+ answers: Record<string, ToolRequestUserInputAnswer>;
552
+ }
518
553
  /** Unsupported / ignored server request methods. */
519
554
  export declare const UNSUPPORTED_SERVER_REQUEST_METHODS: Set<string>;
@@ -41,10 +41,10 @@ export const ServerRequestMethod = {
41
41
  COMMAND_EXECUTION_APPROVAL: 'item/commandExecution/requestApproval',
42
42
  FILE_CHANGE_APPROVAL: 'item/fileChange/requestApproval',
43
43
  PERMISSIONS_APPROVAL: 'item/permissions/requestApproval',
44
+ REQUEST_USER_INPUT: 'item/tool/requestUserInput',
44
45
  };
45
46
  /** Unsupported / ignored server request methods. */
46
47
  export const UNSUPPORTED_SERVER_REQUEST_METHODS = new Set([
47
- 'item/tool/requestUserInput',
48
48
  'mcpServer/elicitation/request',
49
49
  'item/tool/call',
50
50
  'account/chatgptAuthTokens/refresh',
@@ -43,6 +43,16 @@ export interface CodexAppServerRunnerOptions {
43
43
  * object used by `thread/settings/update` (`sandboxPolicy`).
44
44
  */
45
45
  export declare function sandboxModeToSandboxPolicy(mode: SandboxMode): SandboxPolicy;
46
+ /**
47
+ * Codex 官方 feature override(thread/start·resume 的 `config` 字段):
48
+ * 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
49
+ * config.toml。协议键与 openai/codex 的 feature registry 一致
50
+ * (key `default_mode_request_user_input`),app-server 官方测试
51
+ * (turn_start.rs)验证同一路径。
52
+ */
53
+ export declare const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT: {
54
+ readonly 'features.default_mode_request_user_input': true;
55
+ };
46
56
  export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcClient<InitializeResult>, TranslatorEvent> {
47
57
  private connectionManager;
48
58
  private currentTranslator;
@@ -13,6 +13,7 @@
13
13
  import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
14
14
  import { CodexAppServerTranslator } from './translator.js';
15
15
  import { RpcErrorCode } from '../../common/acp/protocol-types.js';
16
+ import { mapAnswersByIndex } from '../../question-common.js';
16
17
  import { getLogger } from '../../../logger/index.js';
17
18
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
18
19
  /**
@@ -34,6 +35,39 @@ function buildApprovalResponse(action, view) {
34
35
  }
35
36
  return { decision: action };
36
37
  }
38
+ /**
39
+ * 构造 request_user_input 响应。answer → 按问题 id 回编;decline/cancel →
40
+ * 空 answers。answers 值与问题 options 无强校验(Codex 接受任意字符串,
41
+ * 包括自由文本题的自定义答案)。
42
+ */
43
+ function buildRequestUserInputResponse(action, questions, response) {
44
+ if (action !== 'answer') {
45
+ return { answers: {} };
46
+ }
47
+ const answers = response.answers;
48
+ if (!answers) {
49
+ return { answers: {} };
50
+ }
51
+ const notes = response.notes;
52
+ const byIndex = mapAnswersByIndex(questions ?? [], answers);
53
+ const result = {};
54
+ (questions ?? []).forEach((q, i) => {
55
+ const value = byIndex[i];
56
+ if (!q.id || value === undefined)
57
+ return;
58
+ const values = Array.isArray(value) ? value : [value];
59
+ const cleaned = values.filter((v) => typeof v === 'string' && v.length > 0);
60
+ // Codex user_note:补充说明编码为 "user_note: <text>" 条目(2026-08-18
61
+ // live 验证 deepseek-v4-flash 明确理解并遵守该格式)。
62
+ const note = notes?.[q.question]?.trim();
63
+ if (note)
64
+ cleaned.push(`user_note: ${note}`);
65
+ if (cleaned.length === 0)
66
+ return;
67
+ result[q.id] = { answers: cleaned };
68
+ });
69
+ return { answers: result };
70
+ }
37
71
  /**
38
72
  * Map the client-side SandboxMode enum to the response-side SandboxPolicy
39
73
  * object used by `thread/settings/update` (`sandboxPolicy`).
@@ -54,6 +88,16 @@ export function sandboxModeToSandboxPolicy(mode) {
54
88
  return { type: 'dangerFullAccess' };
55
89
  }
56
90
  }
91
+ /**
92
+ * Codex 官方 feature override(thread/start·resume 的 `config` 字段):
93
+ * 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
94
+ * config.toml。协议键与 openai/codex 的 feature registry 一致
95
+ * (key `default_mode_request_user_input`),app-server 官方测试
96
+ * (turn_start.rs)验证同一路径。
97
+ */
98
+ export const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT = {
99
+ 'features.default_mode_request_user_input': true,
100
+ };
57
101
  export class CodexAppServerRunner extends ConnectionBasedRunner {
58
102
  connectionManager;
59
103
  currentTranslator = null;
@@ -177,7 +221,15 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
177
221
  if (!client || !pending)
178
222
  return;
179
223
  const action = response?.action ?? 'decline';
180
- if (pending.kind === 'permissions') {
224
+ if (pending.kind === 'question') {
225
+ // AskUserQuestion 回编:文本 key 答案按问题顺序展开后映射回问题 id
226
+ // ({answers:{id:{answers:[值]}}});跳过/拒绝/取消 → 空 answers
227
+ // (Codex 视为 unanswered,turn 继续,对齐 cc-connect deny 语义)。
228
+ const questions = pending.view.questions ?? [];
229
+ const answerPayload = buildRequestUserInputResponse(action, questions, response);
230
+ client.respond(requestId, answerPayload);
231
+ }
232
+ else if (pending.kind === 'permissions') {
181
233
  // 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
182
234
  // scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
183
235
  // 勾选的条目当作授予返回(2026-08-12 review 发现:勾选后点「拒绝」实
@@ -258,6 +310,9 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
258
310
  ...(this.modelProvider ? { modelProvider: this.modelProvider } : {}),
259
311
  ...(this.approvalPolicyConfig ? { approvalPolicy: this.approvalPolicyConfig } : {}),
260
312
  ...(this.sandboxConfig ? { sandbox: this.sandboxConfig } : {}),
313
+ // Default 协作模式下默认启用 request_user_input(Ask User Question),
314
+ // 协议级 override,不依赖 codex config.toml。
315
+ config: THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT,
261
316
  };
262
317
  return params;
263
318
  }
@@ -43,6 +43,13 @@ export declare class CodexAppServerTranslator {
43
43
  * Handle a server request (incoming JSON-RPC request from server, e.g. approval).
44
44
  */
45
45
  handleServerRequest(id: number | string, method: string, params: unknown): TranslatorEvent[];
46
+ /**
47
+ * Codex AskUserQuestion:item/tool/requestUserInput → question 审批事件。
48
+ * 问题 id 保留在 UserQuestion.id(回编时文本 → id 映射),options 为
49
+ * null/空 → 自由文本题(空数组驱动卡片渲染输入框);autoResolutionMs
50
+ * 透传为 per-request 超时(coordinator 优先使用)。
51
+ */
52
+ private handleRequestUserInput;
46
53
  /**
47
54
  * Synthesize a ResultEvent from the current turn state.
48
55
  * Called when the turn completes.
@@ -7,6 +7,7 @@
7
7
  * completes. Method names and parameter shapes follow the real Codex
8
8
  * app-server v2 protocol (see protocol-types.ts).
9
9
  */
10
+ import { makeQuestionApprovalEvent } from '../../question-common.js';
10
11
  import { NotificationMethod, ServerRequestMethod, UNSUPPORTED_SERVER_REQUEST_METHODS, } from './protocol-types.js';
11
12
  // =============================================================================
12
13
  // Translator
@@ -86,10 +87,33 @@ export class CodexAppServerTranslator {
86
87
  return this.handleFileApproval(id, params);
87
88
  case ServerRequestMethod.PERMISSIONS_APPROVAL:
88
89
  return this.handlePermissionsApproval(id, params);
90
+ case ServerRequestMethod.REQUEST_USER_INPUT:
91
+ return this.handleRequestUserInput(id, params);
89
92
  default:
90
93
  return [];
91
94
  }
92
95
  }
96
+ /**
97
+ * Codex AskUserQuestion:item/tool/requestUserInput → question 审批事件。
98
+ * 问题 id 保留在 UserQuestion.id(回编时文本 → id 映射),options 为
99
+ * null/空 → 自由文本题(空数组驱动卡片渲染输入框);autoResolutionMs
100
+ * 透传为 per-request 超时(coordinator 优先使用)。
101
+ */
102
+ handleRequestUserInput(requestId, params) {
103
+ const questions = params.questions
104
+ .map((q) => requestUserInputQuestionToUserQuestion(q))
105
+ .filter((q) => q !== null);
106
+ if (questions.length === 0) {
107
+ // 无有效问题:不渲染空提问卡,返回 [] 由 runner 以 method-not-found
108
+ // 拒绝(服务端视作请求失败,不会悬挂)。
109
+ return [];
110
+ }
111
+ return [
112
+ makeQuestionApprovalEvent(requestId, questions, params.threadId, {
113
+ timeoutMs: params.autoResolutionMs ?? undefined,
114
+ }),
115
+ ];
116
+ }
93
117
  /**
94
118
  * Synthesize a ResultEvent from the current turn state.
95
119
  * Called when the turn completes.
@@ -713,4 +737,25 @@ function normalizeCommandDecisions(list) {
713
737
  ...(Object.keys(payloads).length > 0 ? { payloads } : {}),
714
738
  };
715
739
  }
740
+ /** Codex wire question → 统一 UserQuestion(options null/空 → 自由文本题)。 */
741
+ function requestUserInputQuestionToUserQuestion(q) {
742
+ const question = q.question?.trim();
743
+ if (!question)
744
+ return null;
745
+ const options = (q.options ?? []).map((opt) => ({
746
+ label: opt.label,
747
+ ...(opt.description ? { description: opt.description } : {}),
748
+ }));
749
+ return {
750
+ id: q.id,
751
+ header: q.header || undefined,
752
+ question,
753
+ isOther: q.isOther,
754
+ isSecret: q.isSecret,
755
+ options,
756
+ // Codex user_note:选项题渲染「补充说明(可选)」输入(live 验证模型理解
757
+ // "user_note:" 条目);自由文本题(options 空)不需要。
758
+ ...(options.length > 0 ? { allowNote: true } : {}),
759
+ };
760
+ }
716
761
  //# sourceMappingURL=translator.js.map
@@ -207,6 +207,8 @@ export interface SessionUpdateNotification {
207
207
  }
208
208
  export declare const ServerRequestMethod: {
209
209
  readonly REQUEST_PERMISSION: "session/request_permission";
210
+ /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
211
+ readonly ELICITATION_CREATE: "elicitation/create";
210
212
  };
211
213
  export interface PermissionOption {
212
214
  /** Opaque option identifier — echo back in response outcome.optionId. */
@@ -236,3 +238,34 @@ export interface RequestPermissionResponse {
236
238
  optionId?: string;
237
239
  };
238
240
  }
241
+ export interface ElicitationEnumOption {
242
+ const: string;
243
+ title?: string;
244
+ description?: string;
245
+ }
246
+ export interface ElicitationPropertySchema {
247
+ type: 'string' | 'array';
248
+ title?: string;
249
+ description?: string;
250
+ minItems?: number;
251
+ oneOf?: ElicitationEnumOption[];
252
+ items?: {
253
+ anyOf?: ElicitationEnumOption[];
254
+ };
255
+ }
256
+ export interface ElicitationCreateParams {
257
+ sessionId: string;
258
+ toolCallId?: string;
259
+ mode: 'form';
260
+ message?: string;
261
+ requestedSchema: {
262
+ type: 'object';
263
+ required?: string[];
264
+ properties: Record<string, ElicitationPropertySchema>;
265
+ };
266
+ }
267
+ export interface ElicitationCreateResponse {
268
+ action: 'accept' | 'decline' | 'cancel';
269
+ /** accept 时的答案,key 为 q0..qn(按问题顺序)。 */
270
+ content?: Record<string, string | string[]>;
271
+ }
@@ -71,5 +71,7 @@ export const SessionEventType = {
71
71
  // =============================================================================
72
72
  export const ServerRequestMethod = {
73
73
  REQUEST_PERMISSION: 'session/request_permission',
74
+ /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
75
+ ELICITATION_CREATE: 'elicitation/create',
74
76
  };
75
77
  //# sourceMappingURL=protocol-types.js.map
@@ -104,18 +104,23 @@ export class ConnectionBasedRunner {
104
104
  * unblock; otherwise cancel now via cancelCurrentTurn().
105
105
  */
106
106
  async stop(_opts) {
107
- if (!this._isRunning || !this.currentClient || !this.currentSessionId())
107
+ if (!this._isRunning)
108
108
  return;
109
+ // CC-01: 启动阶段(client/session 未建立)也必须记录停止意图,否则 /stop 被吞、
110
+ // turn 照常执行。executeTurn 在 setup 完成后检查 stopRequested 补发 cancel。
111
+ this.stopRequested = true;
112
+ this.forceFinish = true;
113
+ this.wakeWaiters();
114
+ if (!this.currentClient || !this.currentSessionId()) {
115
+ getLogger().warn(`[${this.logTag}] stop during setup (client/session not ready); will cancel after setup`);
116
+ return;
117
+ }
109
118
  if (this.shouldDeferStop()) {
110
119
  // turn/start 或 prompt 尚未响应:先标记,等 run() 完成后再补发 cancel。
111
- this.stopRequested = true;
112
- this.forceFinish = true;
113
- this.wakeWaiters();
114
120
  return;
115
121
  }
116
122
  // turn/start 或 prompt 已 resolved:立即 cancel;同时置 stopRequested 抑制
117
123
  // 尚未结算的 prompt 结果翻译(opencode 语义),对已结算方无害。
118
- this.stopRequested = true;
119
124
  try {
120
125
  await this.cancelCurrentTurn();
121
126
  }