lark-remote 0.1.6 → 0.1.7

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 (188) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +57 -34
  5. package/dist/bridge/approval-coordinator.js +191 -40
  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 +78 -7
  9. package/dist/bridge/index.js +202 -79
  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 +152 -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 +1 -12
  20. package/dist/card/card-shared.js +1 -26
  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.js +99 -232
  24. package/dist/card/run-state.d.ts +13 -0
  25. package/dist/card/run-state.js +25 -16
  26. package/dist/card/text-truncate.d.ts +13 -12
  27. package/dist/card/text-truncate.js +13 -63
  28. package/dist/common/fs.d.ts +8 -0
  29. package/dist/common/fs.js +17 -0
  30. package/dist/common/guards.d.ts +0 -20
  31. package/dist/common/guards.js +0 -43
  32. package/dist/common/sleep.d.ts +2 -0
  33. package/dist/common/sleep.js +5 -0
  34. package/dist/common/truncate.d.ts +39 -0
  35. package/dist/common/truncate.js +92 -0
  36. package/dist/config/codex-config.d.ts +1 -1
  37. package/dist/config/codex-config.js +1 -1
  38. package/dist/config/index.d.ts +88 -23
  39. package/dist/config/index.js +105 -34
  40. package/dist/connector/index.d.ts +69 -2
  41. package/dist/connector/index.js +107 -1
  42. package/dist/index.js +123 -105
  43. package/dist/instance-lock.js +4 -12
  44. package/dist/order/alias-resolve.d.ts +13 -0
  45. package/dist/order/alias-resolve.js +22 -0
  46. package/dist/order/alias-store.d.ts +38 -0
  47. package/dist/order/alias-store.js +106 -0
  48. package/dist/persistence/atomic-write.d.ts +8 -2
  49. package/dist/persistence/atomic-write.js +23 -7
  50. package/dist/restart.js +2 -1
  51. package/dist/router/card-helpers.d.ts +63 -0
  52. package/dist/router/card-helpers.js +146 -0
  53. package/dist/router/config/claude.js +25 -1
  54. package/dist/router/config/codex.js +29 -50
  55. package/dist/router/config/common/model-patch.d.ts +22 -0
  56. package/dist/router/config/common/model-patch.js +26 -0
  57. package/dist/router/config/common/render.js +5 -4
  58. package/dist/router/config/index.d.ts +12 -0
  59. package/dist/router/config/index.js +25 -1
  60. package/dist/router/config/kimi.d.ts +4 -1
  61. package/dist/router/config/kimi.js +27 -0
  62. package/dist/router/config/opencode.js +18 -5
  63. package/dist/router/config/pi.js +4 -5
  64. package/dist/router/config/types.d.ts +11 -2
  65. package/dist/router/index.d.ts +56 -21
  66. package/dist/router/index.js +373 -548
  67. package/dist/router/utils.d.ts +2 -0
  68. package/dist/router/utils.js +4 -0
  69. package/dist/runner/agent-choices-common.d.ts +16 -0
  70. package/dist/runner/agent-choices-common.js +31 -0
  71. package/dist/runner/claude/runner.d.ts +47 -10
  72. package/dist/runner/claude/runner.js +117 -46
  73. package/dist/runner/claude/session.d.ts +166 -0
  74. package/dist/runner/claude/session.js +749 -0
  75. package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
  76. package/dist/runner/codex/app-server/protocol-types.js +1 -12
  77. package/dist/runner/codex/app-server/runner.d.ts +42 -59
  78. package/dist/runner/codex/app-server/runner.js +160 -337
  79. package/dist/runner/codex/app-server/translator.d.ts +1 -21
  80. package/dist/runner/codex/app-server/translator.js +0 -18
  81. package/dist/runner/codex/index.d.ts +0 -2
  82. package/dist/runner/codex/index.js +0 -2
  83. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  84. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  85. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  86. package/dist/runner/common/acp/protocol-types.js +75 -0
  87. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  88. package/dist/runner/common/connection-based-runner.js +261 -0
  89. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  90. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  91. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  92. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  93. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  95. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  96. package/dist/runner/common/jsonrpc/types.js +9 -0
  97. package/dist/runner/common/process-stopper.d.ts +2 -1
  98. package/dist/runner/common/process-stopper.js +1 -0
  99. package/dist/runner/common/runner-utils.d.ts +6 -13
  100. package/dist/runner/common/runner-utils.js +6 -47
  101. package/dist/runner/common/spawning-runner.d.ts +24 -70
  102. package/dist/runner/common/spawning-runner.js +98 -152
  103. package/dist/runner/index.d.ts +4 -4
  104. package/dist/runner/index.js +4 -4
  105. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  106. package/dist/runner/kimi/acp/runner.js +448 -0
  107. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  108. package/dist/runner/kimi/acp/translator.js +336 -0
  109. package/dist/runner/kimi/index.d.ts +1 -1
  110. package/dist/runner/kimi/index.js +1 -1
  111. package/dist/runner/opencode/acp/index.d.ts +1 -0
  112. package/dist/runner/opencode/acp/index.js +2 -0
  113. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  114. package/dist/runner/opencode/acp/runner.js +429 -0
  115. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  116. package/dist/runner/opencode/acp/translator.js +345 -0
  117. package/dist/runner/opencode/index.d.ts +1 -1
  118. package/dist/runner/opencode/index.js +1 -1
  119. package/dist/runner/pi/index.d.ts +1 -1
  120. package/dist/runner/pi/index.js +1 -1
  121. package/dist/runner/pi/rpc/client.d.ts +52 -0
  122. package/dist/runner/pi/rpc/client.js +114 -0
  123. package/dist/runner/pi/rpc/index.d.ts +4 -0
  124. package/dist/runner/pi/rpc/index.js +4 -0
  125. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  126. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  127. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  128. package/dist/runner/pi/rpc/runner.js +199 -0
  129. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  130. package/dist/runner/pi/rpc/translator.js +258 -0
  131. package/dist/runner/registry.d.ts +7 -8
  132. package/dist/runner/registry.js +3 -4
  133. package/dist/runner/resolve-agent-choices.js +18 -59
  134. package/dist/runner/sync-agent-choices.js +16 -67
  135. package/dist/runner/types.d.ts +43 -13
  136. package/dist/session/claude/session-index.js +2 -3
  137. package/dist/session/claude/session-reader.js +3 -6
  138. package/dist/session/claude/sessions.js +6 -13
  139. package/dist/session/codex/rollout-reader.d.ts +2 -2
  140. package/dist/session/codex/rollout-reader.js +14 -17
  141. package/dist/session/codex/sessions.d.ts +1 -1
  142. package/dist/session/codex/sessions.js +2 -2
  143. package/dist/session/common/content-blocks.d.ts +7 -0
  144. package/dist/session/common/content-blocks.js +13 -9
  145. package/dist/session/common/jsonl.d.ts +8 -25
  146. package/dist/session/common/jsonl.js +126 -261
  147. package/dist/session/common/pagination.d.ts +28 -0
  148. package/dist/session/common/pagination.js +33 -0
  149. package/dist/session/kimi/sessions.d.ts +34 -1
  150. package/dist/session/kimi/sessions.js +208 -98
  151. package/dist/session/opencode/sessions.d.ts +17 -3
  152. package/dist/session/opencode/sessions.js +29 -29
  153. package/dist/session/pi/sessions.d.ts +5 -1
  154. package/dist/session/pi/sessions.js +12 -16
  155. package/dist/session/session-store.d.ts +11 -1
  156. package/dist/session/session-store.js +75 -178
  157. package/dist/update/index.d.ts +2 -2
  158. package/dist/update/index.js +2 -2
  159. package/dist/update/install.d.ts +20 -2
  160. package/dist/update/install.js +51 -7
  161. package/package.json +1 -1
  162. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  163. package/dist/runner/codex/argv.d.ts +0 -33
  164. package/dist/runner/codex/argv.js +0 -40
  165. package/dist/runner/codex/factory.d.ts +0 -29
  166. package/dist/runner/codex/factory.js +0 -39
  167. package/dist/runner/codex/jsonl.d.ts +0 -37
  168. package/dist/runner/codex/jsonl.js +0 -221
  169. package/dist/runner/codex/runner.d.ts +0 -66
  170. package/dist/runner/codex/runner.js +0 -129
  171. package/dist/runner/common/exec-translator.d.ts +0 -73
  172. package/dist/runner/common/exec-translator.js +0 -105
  173. package/dist/runner/kimi/jsonl.d.ts +0 -28
  174. package/dist/runner/kimi/jsonl.js +0 -143
  175. package/dist/runner/kimi/runner.d.ts +0 -49
  176. package/dist/runner/kimi/runner.js +0 -105
  177. package/dist/runner/opencode/argv.d.ts +0 -32
  178. package/dist/runner/opencode/argv.js +0 -35
  179. package/dist/runner/opencode/jsonl.d.ts +0 -68
  180. package/dist/runner/opencode/jsonl.js +0 -295
  181. package/dist/runner/opencode/runner.d.ts +0 -61
  182. package/dist/runner/opencode/runner.js +0 -93
  183. package/dist/runner/pi/jsonl.d.ts +0 -128
  184. package/dist/runner/pi/jsonl.js +0 -249
  185. package/dist/runner/pi/runner.d.ts +0 -36
  186. package/dist/runner/pi/runner.js +0 -76
  187. package/dist/session/common/catch-up.d.ts +0 -12
  188. package/dist/session/common/catch-up.js +0 -31
@@ -5,14 +5,16 @@
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';
12
16
  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;
17
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
16
18
  /**
17
19
  * Build the protocol response body for a command/file approval decision.
18
20
  * Structured decisions (acceptWithExecpolicyAmendment) carry the payload the
@@ -32,194 +34,111 @@ function buildApprovalResponse(action, view) {
32
34
  }
33
35
  return { decision: action };
34
36
  }
35
- export class CodexAppServerRunner {
36
- kind;
37
- sessionReader;
38
- lifetime = 'workspace';
37
+ /**
38
+ * Map the client-side SandboxMode enum to the response-side SandboxPolicy
39
+ * object used by `thread/settings/update` (`sandboxPolicy`).
40
+ */
41
+ export function sandboxModeToSandboxPolicy(mode) {
42
+ switch (mode) {
43
+ case 'read-only':
44
+ return { type: 'readOnly', networkAccess: false };
45
+ case 'workspace-write':
46
+ return {
47
+ type: 'workspaceWrite',
48
+ writableRoots: [],
49
+ networkAccess: false,
50
+ excludeTmpdirEnvVar: false,
51
+ excludeSlashTmp: false,
52
+ };
53
+ case 'danger-full-access':
54
+ return { type: 'dangerFullAccess' };
55
+ }
56
+ }
57
+ export class CodexAppServerRunner extends ConnectionBasedRunner {
39
58
  connectionManager;
40
- currentClient = null;
41
59
  currentTranslator = null;
42
- currentThreadId = null;
43
- currentTurnId = null;
44
- lastEventAt = 0;
45
- _isRunning = false;
60
+ activeThreadId = null;
46
61
  model;
47
62
  modelProvider;
48
63
  reasoningEffort;
49
64
  sandboxConfig;
50
65
  approvalPolicyConfig;
51
- turnTimeoutMs;
52
66
  /** Pending approval requests: requestId (JSON-RPC id) → kind + view. */
53
67
  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
68
  constructor(opts) {
61
- this.kind = opts.kind;
62
- this.sessionReader = opts.sessionReader;
69
+ super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnTimeoutMs);
63
70
  this.model = opts.model;
64
71
  this.modelProvider = opts.modelProvider;
65
72
  this.reasoningEffort = opts.reasoningEffort;
66
73
  this.sandboxConfig = opts.sandbox;
67
74
  this.approvalPolicyConfig = opts.approvalPolicy;
68
- this.turnTimeoutMs = opts.turnTimeoutMs ?? TURN_IDLE_TIMEOUT_MS;
69
75
  const managerOpts = {
70
76
  binary: opts.binary ?? 'codex',
71
- args: opts.appServerArgs,
77
+ args: opts.appServerArgs ?? ['app-server', '--stdio'],
72
78
  env: opts.env,
73
79
  requestTimeoutMs: opts.requestTimeoutMs,
74
80
  idleTtlMs: opts.idleTtlMs,
81
+ // `experimentalApi: true` is required for `thread/settings/update`, which
82
+ // lets config.save push approval/sandbox changes to a live thread instead
83
+ // of waiting for the workspace-lifetime runner to be recreated.
84
+ initializeParams: {
85
+ clientInfo: {
86
+ name: 'lark-remote',
87
+ version: '1.0.0',
88
+ },
89
+ capabilities: {
90
+ experimentalApi: true,
91
+ },
92
+ },
75
93
  };
76
94
  this.connectionManager = new ConnectionManager(managerOpts);
77
95
  }
78
- get isRunning() {
79
- return this._isRunning;
96
+ get logTag() {
97
+ return 'codex-app-server-runner';
80
98
  }
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();
99
+ get turnTimeoutErrorMessage() {
100
+ return 'Codex app-server turn timed out';
101
+ }
102
+ get turnInterruptedErrorMessage() {
103
+ return 'Codex app-server turn interrupted';
104
+ }
105
+ currentSessionId() {
106
+ return this.activeThreadId;
107
+ }
108
+ shouldDeferStop() {
109
+ return !this.currentTurnId;
110
+ }
111
+ async cancelCurrentTurn() {
112
+ if (!this.currentClient || !this.activeThreadId || !this.currentTurnId)
113
+ return;
94
114
  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();
115
+ await this.currentClient.request('turn/interrupt', {
116
+ threadId: this.activeThreadId,
117
+ turnId: this.currentTurnId,
118
+ });
144
119
  }
145
120
  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);
121
+ getLogger().warn(`[${this.logTag}] interrupt failed: ${err.message}`);
169
122
  }
170
123
  }
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;
124
+ clearTurnState() {
125
+ this.currentTranslator = null;
126
+ this.activeThreadId = null;
127
+ }
128
+ async releaseConnection(cwd) {
129
+ await this.connectionManager.release(cwd);
130
+ }
131
+ notifyIdle(cwd) {
132
+ this.connectionManager.notifyIdle(cwd);
133
+ }
134
+ async disposeConnections() {
135
+ await this.connectionManager.disposeAll();
212
136
  }
213
137
  /**
214
138
  * Run a compact operation on the current thread.
215
139
  */
216
140
  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 {
141
+ yield* this.executeTurn(opts, async () => {
223
142
  const client = await this.connectionManager.acquire(opts.cwd);
224
143
  this.connectionManager.notifyActivity(opts.cwd);
225
144
  this.currentClient = client;
@@ -231,7 +150,7 @@ export class CodexAppServerRunner {
231
150
  if (!opts.sessionId) {
232
151
  throw new Error('compact requires a sessionId');
233
152
  }
234
- this.currentThreadId = opts.sessionId;
153
+ this.activeThreadId = opts.sessionId;
235
154
  const translator = new CodexAppServerTranslator();
236
155
  translator.setOperationKind('compact');
237
156
  this.currentTranslator = translator;
@@ -246,55 +165,7 @@ export class CodexAppServerRunner {
246
165
  };
247
166
  await client.request('thread/resume', resumeParams);
248
167
  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();
168
+ });
298
169
  }
299
170
  /**
300
171
  * Respond to an approval server request. `response` is the bridge
@@ -323,30 +194,7 @@ export class CodexAppServerRunner {
323
194
  client.respond(requestId, buildApprovalResponse(action, pending.view));
324
195
  }
325
196
  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.
197
+ getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} kind=${pending.kind} action=${action}`);
350
198
  }
351
199
  getStatusInfo() {
352
200
  return {
@@ -361,6 +209,45 @@ export class CodexAppServerRunner {
361
209
  },
362
210
  };
363
211
  }
212
+ /**
213
+ * Unified approval-mode hot-update entry (called by the bridge via duck
214
+ * typing on active runs). Codex's `thread/settings/update` applies the new
215
+ * approval/sandbox policy to subsequent turns, even while the current turn
216
+ * is still active. This lets `/config` take effect without waiting for this
217
+ * workspace-lifetime runner to be recreated/evicted. Local fields are always
218
+ * updated so `/status` reflects the new config immediately; if there is no
219
+ * live thread, the next `thread/start`/`thread/resume` uses the new values
220
+ * anyway.
221
+ */
222
+ async updateApprovalMode(settings) {
223
+ if (settings.approvalPolicy !== undefined) {
224
+ this.approvalPolicyConfig = settings.approvalPolicy;
225
+ }
226
+ if (settings.sandbox !== undefined) {
227
+ this.sandboxConfig = settings.sandbox;
228
+ }
229
+ if (!this.currentClient || !this.activeThreadId) {
230
+ return;
231
+ }
232
+ const params = {
233
+ threadId: this.activeThreadId,
234
+ ...(settings.approvalPolicy !== undefined ? { approvalPolicy: settings.approvalPolicy } : {}),
235
+ ...(settings.sandbox !== undefined
236
+ ? { sandboxPolicy: sandboxModeToSandboxPolicy(settings.sandbox) }
237
+ : {}),
238
+ };
239
+ try {
240
+ await this.currentClient.request('thread/settings/update', params);
241
+ getLogger().info(`[${this.logTag}] thread/settings/update applied for thread=${this.activeThreadId}`);
242
+ }
243
+ catch (err) {
244
+ // Config save must not fail because a live thread rejected an experimental
245
+ // update (e.g. older Codex without thread/settings/update). The local
246
+ // snapshot is already updated, so /status and the next thread start/resume
247
+ // still use the new values.
248
+ getLogger().warn(`[${this.logTag}] thread/settings/update failed: ${err.message}`);
249
+ }
250
+ }
364
251
  // =========================================================================
365
252
  // Internal
366
253
  // =========================================================================
@@ -376,7 +263,7 @@ export class CodexAppServerRunner {
376
263
  }
377
264
  buildTurnParams(message, opts) {
378
265
  const params = {
379
- threadId: this.currentThreadId ?? '',
266
+ threadId: this.activeThreadId ?? '',
380
267
  input: [{ type: 'text', text: message }],
381
268
  ...((opts.model ?? this.model) ? { model: opts.model ?? this.model } : {}),
382
269
  ...((opts.reasoningEffort ?? this.reasoningEffort)
@@ -386,79 +273,46 @@ export class CodexAppServerRunner {
386
273
  return params;
387
274
  }
388
275
  /**
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.
276
+ * Acquire the connection and set up the turn: thread/start(新会话)或
277
+ * thread/resume(按 sessionId 恢复既有线程)→ turn/start。
427
278
  */
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}`);
279
+ async setupTurn(message, opts) {
280
+ const client = await this.connectionManager.acquire(opts.cwd);
281
+ this.connectionManager.notifyActivity(opts.cwd);
282
+ this.currentClient = client;
283
+ client.setHooks({
284
+ onNotification: (method, params) => this.handleNotification(method, params),
285
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
286
+ onClose: () => {
287
+ getLogger().warn(`[${this.logTag}] client connection closed`);
288
+ this.failTurn('Codex app-server connection closed');
289
+ },
290
+ });
291
+ const threadParams = this.buildThreadParams(opts.cwd);
292
+ let threadId;
293
+ if (opts.sessionId) {
294
+ const resumeParams = {
295
+ threadId: opts.sessionId,
296
+ ...threadParams,
297
+ };
298
+ await client.request('thread/resume', resumeParams);
299
+ threadId = opts.sessionId;
444
300
  }
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
- });
301
+ else {
302
+ const threadResult = await client.request('thread/start', threadParams);
303
+ // 会话键假设(review P2-4):主线程 thread.id === session_meta.session_id,
304
+ // 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
305
+ // 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
306
+ // 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
307
+ // 桥只把主线程作为顶层会话,不在此链路内。
308
+ threadId = threadResult.thread.id;
460
309
  }
461
- return false;
310
+ this.activeThreadId = threadId;
311
+ const translator = new CodexAppServerTranslator();
312
+ this.currentTranslator = translator;
313
+ const turnParams = this.buildTurnParams(message, opts);
314
+ const turnResult = await client.request('turn/start', turnParams);
315
+ this.currentTurnId = turnResult.turn.id;
462
316
  }
463
317
  handleNotification(method, params) {
464
318
  const events = this.currentTranslator?.handleNotification(method, params) ?? [];
@@ -470,48 +324,17 @@ export class CodexAppServerRunner {
470
324
  // 未处理/不支持的服务端请求必须显式响应(error 即"拒绝",turn 继续),
471
325
  // 否则服务端会一直等待响应(applyPatchApproval、item/tool/requestUserInput、
472
326
  // mcpServer/elicitation/request 等)。
473
- this.currentClient?.respondError(id, -32601, `Unsupported server request: ${method}`);
327
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
474
328
  return;
475
329
  }
476
330
  for (const ev of events) {
477
331
  if (ev.type === 'approval_requested') {
478
332
  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}`);
333
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
480
334
  }
481
335
  }
482
336
  this.pushEvents(events);
483
337
  }
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
338
  buildGrantedPermissions(view) {
516
339
  const items = view.permissions?.items ?? [];
517
340
  const read = [];
@@ -7,29 +7,9 @@
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 type { AgentEvent } from '../../types.js';
10
+ import type { AgentEvent, TurnStartedEvent, TurnDiffEvent } from '../../types.js';
11
11
  import type { ApprovalRequestedEvent, ApprovalResolvedEvent } from '../../types.js';
12
12
  import { type TurnCompletedNotification } from './protocol-types.js';
13
- export interface TurnStartedEvent {
14
- type: 'turn_started';
15
- threadId: string;
16
- turnId: string;
17
- operationKind: 'turn' | 'compaction';
18
- }
19
- export interface TurnDiffEvent {
20
- type: 'turn_diff';
21
- text?: string;
22
- reasoning?: string;
23
- toolOutput?: string;
24
- plan?: string;
25
- fileChanges?: Array<{
26
- path: string;
27
- kind: 'add' | 'update' | 'delete';
28
- diff?: string;
29
- }>;
30
- threadId: string;
31
- turnId: string;
32
- }
33
13
  export type TranslatorEvent = AgentEvent | TurnStartedEvent | TurnDiffEvent | ApprovalRequestedEvent | ApprovalResolvedEvent;
34
14
  export declare class CodexAppServerTranslator {
35
15
  private turnId;