lark-remote 0.1.6 → 0.1.8

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 (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +97 -13
  9. package/dist/bridge/index.js +227 -87
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +294 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +7 -16
  20. package/dist/card/card-shared.js +7 -30
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. package/dist/session/common/catch-up.js +0 -31
@@ -5,14 +5,17 @@
5
5
  * Flow per run: acquire persistent connection → thread/start or thread/resume
6
6
  * → turn/start → consume notifications (agent message deltas, approval server
7
7
  * requests, …) until turn/completed → synthesize ResultEvent.
8
+ *
9
+ * Shares the ConnectionBasedRunner base with kimi/opencode: same notification
10
+ * queue + waitResolve pump, forceFinish/stopRequested semantics, turn idle
11
+ * watchdog, and ConnectionLostError respawn-and-retry-once.
8
12
  */
9
- import { ConnectionManager } from './connection-manager.js';
10
- import { ConnectionLostError } from './client.js';
13
+ import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
11
14
  import { CodexAppServerTranslator } from './translator.js';
15
+ import { RpcErrorCode } from '../../common/acp/protocol-types.js';
16
+ import { mapAnswersByIndex } from '../../question-common.js';
12
17
  import { getLogger } from '../../../logger/index.js';
13
- import { syntheticInitEvent } from '../../common/runner-utils.js';
14
- /** How long to wait for turn output notifications before failing. */
15
- const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
18
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
16
19
  /**
17
20
  * Build the protocol response body for a command/file approval decision.
18
21
  * Structured decisions (acceptWithExecpolicyAmendment) carry the payload the
@@ -32,194 +35,154 @@ function buildApprovalResponse(action, view) {
32
35
  }
33
36
  return { decision: action };
34
37
  }
35
- export class CodexAppServerRunner {
36
- kind;
37
- sessionReader;
38
- lifetime = 'workspace';
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
+ }
71
+ /**
72
+ * Map the client-side SandboxMode enum to the response-side SandboxPolicy
73
+ * object used by `thread/settings/update` (`sandboxPolicy`).
74
+ */
75
+ export function sandboxModeToSandboxPolicy(mode) {
76
+ switch (mode) {
77
+ case 'read-only':
78
+ return { type: 'readOnly', networkAccess: false };
79
+ case 'workspace-write':
80
+ return {
81
+ type: 'workspaceWrite',
82
+ writableRoots: [],
83
+ networkAccess: false,
84
+ excludeTmpdirEnvVar: false,
85
+ excludeSlashTmp: false,
86
+ };
87
+ case 'danger-full-access':
88
+ return { type: 'dangerFullAccess' };
89
+ }
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
+ };
101
+ export class CodexAppServerRunner extends ConnectionBasedRunner {
39
102
  connectionManager;
40
- currentClient = null;
41
103
  currentTranslator = null;
42
- currentThreadId = null;
43
- currentTurnId = null;
44
- lastEventAt = 0;
45
- _isRunning = false;
104
+ activeThreadId = null;
46
105
  model;
47
106
  modelProvider;
48
107
  reasoningEffort;
49
108
  sandboxConfig;
50
109
  approvalPolicyConfig;
51
- turnTimeoutMs;
52
110
  /** Pending approval requests: requestId (JSON-RPC id) → kind + view. */
53
111
  pendingApprovals = new Map();
54
- /** Notification queue consumed by the active run generator. */
55
- notificationQueue = [];
56
- waitResolve = null;
57
- forceFinish = false;
58
- /** stop() 在 turn/start 响应前到达时置位,turn/start 完成后补发 interrupt。 */
59
- stopRequested = false;
60
112
  constructor(opts) {
61
- this.kind = opts.kind;
62
- this.sessionReader = opts.sessionReader;
113
+ super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnTimeoutMs);
63
114
  this.model = opts.model;
64
115
  this.modelProvider = opts.modelProvider;
65
116
  this.reasoningEffort = opts.reasoningEffort;
66
117
  this.sandboxConfig = opts.sandbox;
67
118
  this.approvalPolicyConfig = opts.approvalPolicy;
68
- this.turnTimeoutMs = opts.turnTimeoutMs ?? TURN_IDLE_TIMEOUT_MS;
69
119
  const managerOpts = {
70
120
  binary: opts.binary ?? 'codex',
71
- args: opts.appServerArgs,
121
+ args: opts.appServerArgs ?? ['app-server', '--stdio'],
72
122
  env: opts.env,
73
123
  requestTimeoutMs: opts.requestTimeoutMs,
74
124
  idleTtlMs: opts.idleTtlMs,
125
+ // `experimentalApi: true` is required for `thread/settings/update`, which
126
+ // lets config.save push approval/sandbox changes to a live thread instead
127
+ // of waiting for the workspace-lifetime runner to be recreated.
128
+ initializeParams: {
129
+ clientInfo: {
130
+ name: 'lark-remote',
131
+ version: '1.0.0',
132
+ },
133
+ capabilities: {
134
+ experimentalApi: true,
135
+ },
136
+ },
75
137
  };
76
138
  this.connectionManager = new ConnectionManager(managerOpts);
77
139
  }
78
- get isRunning() {
79
- return this._isRunning;
140
+ get logTag() {
141
+ return 'codex-app-server-runner';
80
142
  }
81
- /**
82
- * Run a message in the current thread.
83
- *
84
- * 进程容错:若 app-server 子进程被外部误杀或自身异常退出(连接丢失),
85
- * 自动重拉进程并重试一次 setup(thread/start 或按 sessionId thread/resume
86
- * 继续处理),保证消费用户消息时进程挂了也能恢复。
87
- */
88
- async *run(message, opts) {
89
- if (this._isRunning) {
90
- throw new Error('CodexAppServerRunner is already running');
91
- }
92
- this._isRunning = true;
93
- this.resetRunState();
143
+ get turnTimeoutErrorMessage() {
144
+ return 'Codex app-server turn timed out';
145
+ }
146
+ get turnInterruptedErrorMessage() {
147
+ return 'Codex app-server turn interrupted';
148
+ }
149
+ currentSessionId() {
150
+ return this.activeThreadId;
151
+ }
152
+ shouldDeferStop() {
153
+ return !this.currentTurnId;
154
+ }
155
+ async cancelCurrentTurn() {
156
+ if (!this.currentClient || !this.activeThreadId || !this.currentTurnId)
157
+ return;
94
158
  try {
95
- try {
96
- await this.setupTurn(message, opts);
97
- }
98
- catch (err) {
99
- // 连接在 setup 期间丢失(进程被误杀/异常退出):重拉进程并重试一次。
100
- // thread/resume 会按持久化记录(rollout)恢复线程,继续原会话处理。
101
- if (err instanceof ConnectionLostError && !this.stopRequested) {
102
- getLogger().warn(`[codex-app-server-runner] connection lost during turn setup (${err.message}), respawning app-server and retrying once`);
103
- // 旧连接可能仍挂在 slot 里(close 事件尚未处理完):先强制释放,
104
- // 保证重试会拉起全新进程;并摘掉旧 client 的 hooks,避免其残留的
105
- // onClose 把错误结果塞进重试后的通知队列。
106
- await this.connectionManager.release(opts.cwd);
107
- const staleClient = this.currentClient;
108
- if (staleClient) {
109
- staleClient.setHooks({
110
- onNotification: () => { },
111
- onServerRequest: () => { },
112
- onClose: () => { },
113
- });
114
- }
115
- this.resetRunState();
116
- await this.setupTurn(message, opts);
117
- }
118
- else {
119
- throw err;
120
- }
121
- }
122
- if (this.stopRequested) {
123
- // 启动期 /stop:turn/start 已完成,补发 interrupt,避免 server 端 turn
124
- // 无人接管继续执行。
125
- getLogger().warn('[codex-app-server-runner] stop requested before turn/start resolved');
126
- try {
127
- await this.currentClient?.request('turn/interrupt', {
128
- threadId: this.currentThreadId,
129
- turnId: this.currentTurnId,
130
- });
131
- }
132
- catch (err) {
133
- getLogger().warn(`[codex-app-server-runner] deferred interrupt failed: ${err.message}`);
134
- }
135
- }
136
- // §9.22 守卫前提:桥的 pre-init result guard 和 run-state reducer 都以
137
- // system.init 作为「本轮真实开始」的标记,而 app-server 协议没有 init 事件。
138
- // 缺了它,成功的 result 会被当作 pre-init 丢弃,卡片终态停在 running,
139
- // 最后兜底成「输出流已结束,但未收到 result 事件」(2026-08-13 事故)。
140
- // 与 exec runner 的 syntheticInitEvent 模式一致:turn setup 成功即发 init,
141
- // 保证 turn_started / turn_diff / result 全部落在 init 之后。
142
- yield syntheticInitEvent(this.currentThreadId ?? opts.sessionId ?? '');
143
- yield* this.consumeTurn();
159
+ await this.currentClient.request('turn/interrupt', {
160
+ threadId: this.activeThreadId,
161
+ turnId: this.currentTurnId,
162
+ });
144
163
  }
145
164
  catch (err) {
146
- getLogger().error(`[codex-app-server-runner] run error: ${err.message}`);
147
- // 与 spawn 失败路径(spawning-runner §9.22)同理:error result 前必须补
148
- // init,否则守卫把错误结果丢弃,卡片只显示通用「输出流已结束」而非具体
149
- // 错误信息。
150
- yield syntheticInitEvent(this.currentThreadId ?? opts.sessionId ?? '');
151
- yield {
152
- type: 'result',
153
- subtype: 'error',
154
- // 优先上报本次 setup 创建/恢复的线程 id:thread/start 成功后 turn/start
155
- // 失败时 opts.sessionId 仍是旧值,漏报会导致下条消息再开一个孤儿线程
156
- // (review P3-10)。
157
- session_id: this.currentThreadId ?? opts.sessionId ?? '',
158
- errorMessage: err.message,
159
- };
160
- }
161
- finally {
162
- this._isRunning = false;
163
- this.currentTranslator = null;
164
- this.currentTurnId = null;
165
- this.stopRequested = false;
166
- this.currentThreadId = null;
167
- // 无论成功失败都要重新武装 idle TTL(无 slot 时是 no-op)。
168
- this.connectionManager.notifyIdle(opts.cwd);
165
+ getLogger().warn(`[${this.logTag}] interrupt failed: ${err.message}`);
169
166
  }
170
167
  }
171
- /**
172
- * Acquire the connection and set up the turn: thread/start(新会话)或
173
- * thread/resume(按 sessionId 恢复既有线程)→ turn/start。
174
- */
175
- async setupTurn(message, opts) {
176
- const client = await this.connectionManager.acquire(opts.cwd);
177
- this.connectionManager.notifyActivity(opts.cwd);
178
- this.currentClient = client;
179
- client.setHooks({
180
- onNotification: (method, params) => this.handleNotification(method, params),
181
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
182
- onClose: () => {
183
- getLogger().warn('[codex-app-server-runner] client connection closed');
184
- this.failTurn('Codex app-server connection closed');
185
- },
186
- });
187
- const threadParams = this.buildThreadParams(opts.cwd);
188
- let threadId;
189
- if (opts.sessionId) {
190
- const resumeParams = {
191
- threadId: opts.sessionId,
192
- ...threadParams,
193
- };
194
- await client.request('thread/resume', resumeParams);
195
- threadId = opts.sessionId;
196
- }
197
- else {
198
- const threadResult = await client.request('thread/start', threadParams);
199
- // 会话键假设(review P2-4):主线程 thread.id === session_meta.session_id,
200
- // 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
201
- // 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
202
- // 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
203
- // 桥只把主线程作为顶层会话,不在此链路内。
204
- threadId = threadResult.thread.id;
205
- }
206
- this.currentThreadId = threadId;
207
- const translator = new CodexAppServerTranslator();
208
- this.currentTranslator = translator;
209
- const turnParams = this.buildTurnParams(message, opts);
210
- const turnResult = await client.request('turn/start', turnParams);
211
- this.currentTurnId = turnResult.turn.id;
168
+ clearTurnState() {
169
+ this.currentTranslator = null;
170
+ this.activeThreadId = null;
171
+ }
172
+ async releaseConnection(cwd) {
173
+ await this.connectionManager.release(cwd);
174
+ }
175
+ notifyIdle(cwd) {
176
+ this.connectionManager.notifyIdle(cwd);
177
+ }
178
+ async disposeConnections() {
179
+ await this.connectionManager.disposeAll();
212
180
  }
213
181
  /**
214
182
  * Run a compact operation on the current thread.
215
183
  */
216
184
  async *runCompact(_message, opts) {
217
- if (this._isRunning) {
218
- throw new Error('CodexAppServerRunner is already running');
219
- }
220
- this._isRunning = true;
221
- this.resetRunState();
222
- try {
185
+ yield* this.executeTurn(opts, async () => {
223
186
  const client = await this.connectionManager.acquire(opts.cwd);
224
187
  this.connectionManager.notifyActivity(opts.cwd);
225
188
  this.currentClient = client;
@@ -231,7 +194,7 @@ export class CodexAppServerRunner {
231
194
  if (!opts.sessionId) {
232
195
  throw new Error('compact requires a sessionId');
233
196
  }
234
- this.currentThreadId = opts.sessionId;
197
+ this.activeThreadId = opts.sessionId;
235
198
  const translator = new CodexAppServerTranslator();
236
199
  translator.setOperationKind('compact');
237
200
  this.currentTranslator = translator;
@@ -246,55 +209,7 @@ export class CodexAppServerRunner {
246
209
  };
247
210
  await client.request('thread/resume', resumeParams);
248
211
  await client.request('thread/compact/start', { threadId: opts.sessionId });
249
- // 与 run() 同理:compact 也先发 init 再消费通知,避免 result 被守卫丢弃。
250
- yield syntheticInitEvent(this.currentThreadId ?? opts.sessionId ?? '');
251
- yield* this.consumeTurn();
252
- }
253
- catch (err) {
254
- getLogger().error(`[codex-app-server-runner] runCompact error: ${err.message}`);
255
- yield syntheticInitEvent(this.currentThreadId ?? opts.sessionId ?? '');
256
- yield {
257
- type: 'result',
258
- subtype: 'error',
259
- session_id: this.currentThreadId ?? opts.sessionId ?? '',
260
- errorMessage: err.message,
261
- };
262
- }
263
- finally {
264
- this._isRunning = false;
265
- this.currentTranslator = null;
266
- this.currentTurnId = null;
267
- this.stopRequested = false;
268
- this.currentThreadId = null;
269
- this.connectionManager.notifyIdle(opts.cwd);
270
- }
271
- }
272
- /**
273
- * Stop the current turn by interrupting it.
274
- */
275
- async stop(_opts) {
276
- if (!this._isRunning || !this.currentClient || !this.currentThreadId)
277
- return;
278
- if (!this.currentTurnId) {
279
- // turn/start 尚未响应:先标记,等 run() 在 turn/start 完成后补发 interrupt。
280
- this.stopRequested = true;
281
- this.forceFinish = true;
282
- this.wakeWaiters();
283
- return;
284
- }
285
- try {
286
- await this.currentClient.request('turn/interrupt', {
287
- threadId: this.currentThreadId,
288
- turnId: this.currentTurnId ?? '',
289
- });
290
- }
291
- catch (err) {
292
- getLogger().warn(`[codex-app-server-runner] stop error: ${err.message}`);
293
- }
294
- // Unblock the turn loop regardless of whether the server sends a
295
- // turn/completed notification afterwards.
296
- this.forceFinish = true;
297
- this.wakeWaiters();
212
+ });
298
213
  }
299
214
  /**
300
215
  * Respond to an approval server request. `response` is the bridge
@@ -306,7 +221,15 @@ export class CodexAppServerRunner {
306
221
  if (!client || !pending)
307
222
  return;
308
223
  const action = response?.action ?? 'decline';
309
- 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') {
310
233
  // 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
311
234
  // scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
312
235
  // 勾选的条目当作授予返回(2026-08-12 review 发现:勾选后点「拒绝」实
@@ -323,30 +246,7 @@ export class CodexAppServerRunner {
323
246
  client.respond(requestId, buildApprovalResponse(action, pending.view));
324
247
  }
325
248
  this.pendingApprovals.delete(requestId);
326
- getLogger().info(`[codex-app-server-runner] approval responded requestId=${requestId} kind=${pending.kind} action=${action}`);
327
- }
328
- /**
329
- * Dispose the runner: release the connection.
330
- */
331
- async dispose() {
332
- try {
333
- await this.connectionManager.disposeAll();
334
- }
335
- catch (err) {
336
- getLogger().warn(`[codex-app-server-runner] dispose error: ${err.message}`);
337
- }
338
- }
339
- getUsageAuthority() {
340
- return 'live';
341
- }
342
- killOrphan() {
343
- // No-op: the app-server connection is managed by ConnectionManager.
344
- }
345
- registerExitHandlers() {
346
- // No-op: connection manager handles cleanup on exit.
347
- }
348
- unregisterExitHandlers() {
349
- // No-op.
249
+ getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} kind=${pending.kind} action=${action}`);
350
250
  }
351
251
  getStatusInfo() {
352
252
  return {
@@ -361,6 +261,45 @@ export class CodexAppServerRunner {
361
261
  },
362
262
  };
363
263
  }
264
+ /**
265
+ * Unified approval-mode hot-update entry (called by the bridge via duck
266
+ * typing on active runs). Codex's `thread/settings/update` applies the new
267
+ * approval/sandbox policy to subsequent turns, even while the current turn
268
+ * is still active. This lets `/config` take effect without waiting for this
269
+ * workspace-lifetime runner to be recreated/evicted. Local fields are always
270
+ * updated so `/status` reflects the new config immediately; if there is no
271
+ * live thread, the next `thread/start`/`thread/resume` uses the new values
272
+ * anyway.
273
+ */
274
+ async updateApprovalMode(settings) {
275
+ if (settings.approvalPolicy !== undefined) {
276
+ this.approvalPolicyConfig = settings.approvalPolicy;
277
+ }
278
+ if (settings.sandbox !== undefined) {
279
+ this.sandboxConfig = settings.sandbox;
280
+ }
281
+ if (!this.currentClient || !this.activeThreadId) {
282
+ return;
283
+ }
284
+ const params = {
285
+ threadId: this.activeThreadId,
286
+ ...(settings.approvalPolicy !== undefined ? { approvalPolicy: settings.approvalPolicy } : {}),
287
+ ...(settings.sandbox !== undefined
288
+ ? { sandboxPolicy: sandboxModeToSandboxPolicy(settings.sandbox) }
289
+ : {}),
290
+ };
291
+ try {
292
+ await this.currentClient.request('thread/settings/update', params);
293
+ getLogger().info(`[${this.logTag}] thread/settings/update applied for thread=${this.activeThreadId}`);
294
+ }
295
+ catch (err) {
296
+ // Config save must not fail because a live thread rejected an experimental
297
+ // update (e.g. older Codex without thread/settings/update). The local
298
+ // snapshot is already updated, so /status and the next thread start/resume
299
+ // still use the new values.
300
+ getLogger().warn(`[${this.logTag}] thread/settings/update failed: ${err.message}`);
301
+ }
302
+ }
364
303
  // =========================================================================
365
304
  // Internal
366
305
  // =========================================================================
@@ -371,12 +310,15 @@ export class CodexAppServerRunner {
371
310
  ...(this.modelProvider ? { modelProvider: this.modelProvider } : {}),
372
311
  ...(this.approvalPolicyConfig ? { approvalPolicy: this.approvalPolicyConfig } : {}),
373
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,
374
316
  };
375
317
  return params;
376
318
  }
377
319
  buildTurnParams(message, opts) {
378
320
  const params = {
379
- threadId: this.currentThreadId ?? '',
321
+ threadId: this.activeThreadId ?? '',
380
322
  input: [{ type: 'text', text: message }],
381
323
  ...((opts.model ?? this.model) ? { model: opts.model ?? this.model } : {}),
382
324
  ...((opts.reasoningEffort ?? this.reasoningEffort)
@@ -386,79 +328,46 @@ export class CodexAppServerRunner {
386
328
  return params;
387
329
  }
388
330
  /**
389
- * Consume the notification queue until a result event arrives or the turn
390
- * times out / is interrupted.
391
- */
392
- async *consumeTurn() {
393
- while (true) {
394
- const timedOut = await this.waitForEvents(this.nextTurnIdleDeadline());
395
- if (timedOut) {
396
- await this.interruptCurrentTurn();
397
- getLogger().warn('[codex-app-server-runner] turn wait timed out');
398
- yield {
399
- type: 'result',
400
- subtype: 'error',
401
- session_id: this.currentThreadId ?? '',
402
- errorMessage: 'Codex app-server turn timed out',
403
- };
404
- return;
405
- }
406
- if (this.forceFinish) {
407
- yield {
408
- type: 'result',
409
- subtype: 'interrupted',
410
- session_id: this.currentThreadId ?? '',
411
- errorMessage: 'Codex app-server turn interrupted',
412
- };
413
- return;
414
- }
415
- while (this.notificationQueue.length > 0) {
416
- const ev = this.notificationQueue.shift();
417
- yield ev;
418
- if (ev.type === 'result') {
419
- return;
420
- }
421
- }
422
- }
423
- }
424
- /**
425
- * Compute the current idle deadline. 0 disables the idle timeout; otherwise
426
- * the deadline is based on the last received event, not the turn start time.
331
+ * Acquire the connection and set up the turn: thread/start(新会话)或
332
+ * thread/resume(按 sessionId 恢复既有线程)→ turn/start。
427
333
  */
428
- nextTurnIdleDeadline() {
429
- if (this.turnTimeoutMs <= 0)
430
- return Number.POSITIVE_INFINITY;
431
- return this.lastEventAt + this.turnTimeoutMs;
432
- }
433
- async interruptCurrentTurn() {
434
- if (!this.currentClient || !this.currentThreadId || !this.currentTurnId)
435
- return;
436
- try {
437
- await this.currentClient.request('turn/interrupt', {
438
- threadId: this.currentThreadId,
439
- turnId: this.currentTurnId,
440
- });
441
- }
442
- catch (err) {
443
- getLogger().warn(`[codex-app-server-runner] idle-timeout interrupt failed: ${err.message}`);
334
+ async setupTurn(message, opts) {
335
+ const client = await this.connectionManager.acquire(opts.cwd);
336
+ this.connectionManager.notifyActivity(opts.cwd);
337
+ this.currentClient = client;
338
+ client.setHooks({
339
+ onNotification: (method, params) => this.handleNotification(method, params),
340
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
341
+ onClose: () => {
342
+ getLogger().warn(`[${this.logTag}] client connection closed`);
343
+ this.failTurn('Codex app-server connection closed');
344
+ },
345
+ });
346
+ const threadParams = this.buildThreadParams(opts.cwd);
347
+ let threadId;
348
+ if (opts.sessionId) {
349
+ const resumeParams = {
350
+ threadId: opts.sessionId,
351
+ ...threadParams,
352
+ };
353
+ await client.request('thread/resume', resumeParams);
354
+ threadId = opts.sessionId;
444
355
  }
445
- }
446
- async waitForEvents(deadline) {
447
- while (this.notificationQueue.length === 0 && !this.forceFinish) {
448
- const remaining = deadline - Date.now();
449
- if (remaining <= 0)
450
- return true;
451
- await new Promise((resolve) => {
452
- this.waitResolve = resolve;
453
- setTimeout(() => {
454
- if (this.waitResolve === resolve) {
455
- this.waitResolve = null;
456
- resolve();
457
- }
458
- }, Math.min(remaining, 30_000));
459
- });
356
+ else {
357
+ const threadResult = await client.request('thread/start', threadParams);
358
+ // 会话键假设(review P2-4):主线程 thread.id === session_meta.session_id,
359
+ // 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
360
+ // 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
361
+ // 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
362
+ // 桥只把主线程作为顶层会话,不在此链路内。
363
+ threadId = threadResult.thread.id;
460
364
  }
461
- return false;
365
+ this.activeThreadId = threadId;
366
+ const translator = new CodexAppServerTranslator();
367
+ this.currentTranslator = translator;
368
+ const turnParams = this.buildTurnParams(message, opts);
369
+ const turnResult = await client.request('turn/start', turnParams);
370
+ this.currentTurnId = turnResult.turn.id;
462
371
  }
463
372
  handleNotification(method, params) {
464
373
  const events = this.currentTranslator?.handleNotification(method, params) ?? [];
@@ -470,48 +379,17 @@ export class CodexAppServerRunner {
470
379
  // 未处理/不支持的服务端请求必须显式响应(error 即"拒绝",turn 继续),
471
380
  // 否则服务端会一直等待响应(applyPatchApproval、item/tool/requestUserInput、
472
381
  // mcpServer/elicitation/request 等)。
473
- this.currentClient?.respondError(id, -32601, `Unsupported server request: ${method}`);
382
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
474
383
  return;
475
384
  }
476
385
  for (const ev of events) {
477
386
  if (ev.type === 'approval_requested') {
478
387
  this.pendingApprovals.set(ev.requestId, { kind: ev.kind, view: ev.view });
479
- getLogger().info(`[codex-app-server-runner] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
388
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
480
389
  }
481
390
  }
482
391
  this.pushEvents(events);
483
392
  }
484
- pushEvents(events) {
485
- if (events.length === 0)
486
- return;
487
- this.lastEventAt = Date.now();
488
- this.notificationQueue.push(...events);
489
- this.wakeWaiters();
490
- }
491
- wakeWaiters() {
492
- if (this.waitResolve) {
493
- const resolve = this.waitResolve;
494
- this.waitResolve = null;
495
- resolve();
496
- }
497
- }
498
- failTurn(message) {
499
- this.pushEvents([
500
- {
501
- type: 'result',
502
- subtype: 'error',
503
- session_id: this.currentThreadId ?? '',
504
- errorMessage: message,
505
- },
506
- ]);
507
- }
508
- resetRunState() {
509
- this.notificationQueue = [];
510
- this.waitResolve = null;
511
- this.lastEventAt = Date.now();
512
- this.forceFinish = false;
513
- this.pendingApprovals.clear();
514
- }
515
393
  buildGrantedPermissions(view) {
516
394
  const items = view.permissions?.items ?? [];
517
395
  const read = [];