lark-remote 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +423 -20
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +78 -7
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +210 -2
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +43 -0
- package/dist/config/index.js +25 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +32 -0
- package/dist/index.js +95 -23
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/index.d.ts +0 -10
- package/dist/router/config/index.js +0 -17
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +61 -1
- package/dist/router/index.js +323 -48
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +46 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/package.json +1 -14
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAppServerRunner: workspace-lifetime runner using the Codex App Server
|
|
3
|
+
* protocol (`codex app-server`, JSON-RPC over stdio).
|
|
4
|
+
*
|
|
5
|
+
* Flow per run: acquire persistent connection → thread/start or thread/resume
|
|
6
|
+
* → turn/start → consume notifications (agent message deltas, approval server
|
|
7
|
+
* requests, …) until turn/completed → synthesize ResultEvent.
|
|
8
|
+
*/
|
|
9
|
+
import { ConnectionManager } from './connection-manager.js';
|
|
10
|
+
import { ConnectionLostError } from './client.js';
|
|
11
|
+
import { CodexAppServerTranslator } from './translator.js';
|
|
12
|
+
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;
|
|
16
|
+
/**
|
|
17
|
+
* Build the protocol response body for a command/file approval decision.
|
|
18
|
+
* Structured decisions (acceptWithExecpolicyAmendment) carry the payload the
|
|
19
|
+
* server originally offered; if it is missing, fall back to plain accept.
|
|
20
|
+
*/
|
|
21
|
+
function buildApprovalResponse(action, view) {
|
|
22
|
+
if (action === 'accept_for_session') {
|
|
23
|
+
return { decision: 'acceptForSession' };
|
|
24
|
+
}
|
|
25
|
+
if (action === 'accept_with_execpolicy_amendment') {
|
|
26
|
+
const payload = view.decisionPayloads?.['acceptWithExecpolicyAmendment'];
|
|
27
|
+
if (payload) {
|
|
28
|
+
return { decision: { acceptWithExecpolicyAmendment: payload } };
|
|
29
|
+
}
|
|
30
|
+
getLogger().warn('[codex-app-server-runner] acceptWithExecpolicyAmendment payload missing, falling back to accept');
|
|
31
|
+
return { decision: 'accept' };
|
|
32
|
+
}
|
|
33
|
+
return { decision: action };
|
|
34
|
+
}
|
|
35
|
+
export class CodexAppServerRunner {
|
|
36
|
+
kind;
|
|
37
|
+
sessionReader;
|
|
38
|
+
lifetime = 'workspace';
|
|
39
|
+
connectionManager;
|
|
40
|
+
currentClient = null;
|
|
41
|
+
currentTranslator = null;
|
|
42
|
+
currentThreadId = null;
|
|
43
|
+
currentTurnId = null;
|
|
44
|
+
lastEventAt = 0;
|
|
45
|
+
_isRunning = false;
|
|
46
|
+
model;
|
|
47
|
+
modelProvider;
|
|
48
|
+
reasoningEffort;
|
|
49
|
+
sandboxConfig;
|
|
50
|
+
approvalPolicyConfig;
|
|
51
|
+
turnTimeoutMs;
|
|
52
|
+
/** Pending approval requests: requestId (JSON-RPC id) → kind + view. */
|
|
53
|
+
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
|
+
constructor(opts) {
|
|
61
|
+
this.kind = opts.kind;
|
|
62
|
+
this.sessionReader = opts.sessionReader;
|
|
63
|
+
this.model = opts.model;
|
|
64
|
+
this.modelProvider = opts.modelProvider;
|
|
65
|
+
this.reasoningEffort = opts.reasoningEffort;
|
|
66
|
+
this.sandboxConfig = opts.sandbox;
|
|
67
|
+
this.approvalPolicyConfig = opts.approvalPolicy;
|
|
68
|
+
this.turnTimeoutMs = opts.turnTimeoutMs ?? TURN_IDLE_TIMEOUT_MS;
|
|
69
|
+
const managerOpts = {
|
|
70
|
+
binary: opts.binary ?? 'codex',
|
|
71
|
+
args: opts.appServerArgs,
|
|
72
|
+
env: opts.env,
|
|
73
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
74
|
+
idleTtlMs: opts.idleTtlMs,
|
|
75
|
+
};
|
|
76
|
+
this.connectionManager = new ConnectionManager(managerOpts);
|
|
77
|
+
}
|
|
78
|
+
get isRunning() {
|
|
79
|
+
return this._isRunning;
|
|
80
|
+
}
|
|
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();
|
|
94
|
+
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();
|
|
144
|
+
}
|
|
145
|
+
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);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
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;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Run a compact operation on the current thread.
|
|
215
|
+
*/
|
|
216
|
+
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 {
|
|
223
|
+
const client = await this.connectionManager.acquire(opts.cwd);
|
|
224
|
+
this.connectionManager.notifyActivity(opts.cwd);
|
|
225
|
+
this.currentClient = client;
|
|
226
|
+
client.setHooks({
|
|
227
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
228
|
+
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
229
|
+
onClose: () => this.failTurn('Codex app-server connection closed'),
|
|
230
|
+
});
|
|
231
|
+
if (!opts.sessionId) {
|
|
232
|
+
throw new Error('compact requires a sessionId');
|
|
233
|
+
}
|
|
234
|
+
this.currentThreadId = opts.sessionId;
|
|
235
|
+
const translator = new CodexAppServerTranslator();
|
|
236
|
+
translator.setOperationKind('compact');
|
|
237
|
+
this.currentTranslator = translator;
|
|
238
|
+
// 冷连接兜底:thread/compact/start 要求线程已加载。真实协议里对未加载
|
|
239
|
+
// 的线程直接 compact 返回 -32600 "thread not found"(codex-cli 0.147.0
|
|
240
|
+
// 实测),只有先 thread/resume(或 thread/start)把线程载入内存才能
|
|
241
|
+
// 压缩。连接池复用期间线程本就在内存,resume 幂等无害;连接被
|
|
242
|
+
// /stop、idle TTL 或进程重建后,这一步是 Compact 能工作的前提。
|
|
243
|
+
const resumeParams = {
|
|
244
|
+
threadId: opts.sessionId,
|
|
245
|
+
...this.buildThreadParams(opts.cwd),
|
|
246
|
+
};
|
|
247
|
+
await client.request('thread/resume', resumeParams);
|
|
248
|
+
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();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Respond to an approval server request. `response` is the bridge
|
|
301
|
+
* ApprovalAction (`{ action: 'accept' | 'accept_for_session' | 'decline' | 'cancel' }`).
|
|
302
|
+
*/
|
|
303
|
+
async respondApproval(requestId, response) {
|
|
304
|
+
const client = this.currentClient;
|
|
305
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
306
|
+
if (!client || !pending)
|
|
307
|
+
return;
|
|
308
|
+
const action = response?.action ?? 'decline';
|
|
309
|
+
if (pending.kind === 'permissions') {
|
|
310
|
+
// 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
|
|
311
|
+
// scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
|
|
312
|
+
// 勾选的条目当作授予返回(2026-08-12 review 发现:勾选后点「拒绝」实
|
|
313
|
+
// 际授予了所选权限)。accept/acceptForSession 才带用户勾选的条目。
|
|
314
|
+
const denied = action === 'decline' || action === 'cancel';
|
|
315
|
+
client.respond(requestId, {
|
|
316
|
+
permissions: denied
|
|
317
|
+
? this.denyAllPermissions()
|
|
318
|
+
: this.buildGrantedPermissions(pending.view),
|
|
319
|
+
scope: action === 'accept_for_session' ? 'session' : 'turn',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
client.respond(requestId, buildApprovalResponse(action, pending.view));
|
|
324
|
+
}
|
|
325
|
+
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.
|
|
350
|
+
}
|
|
351
|
+
getStatusInfo() {
|
|
352
|
+
return {
|
|
353
|
+
kind: this.kind,
|
|
354
|
+
model: this.model ?? '(app-server)',
|
|
355
|
+
provider: this.modelProvider,
|
|
356
|
+
reasoning: this.reasoningEffort,
|
|
357
|
+
extras: {
|
|
358
|
+
mode: 'app-server',
|
|
359
|
+
...(this.sandboxConfig ? { sandbox: this.sandboxConfig } : {}),
|
|
360
|
+
...(this.approvalPolicyConfig ? { approvalPolicy: String(this.approvalPolicyConfig) } : {}),
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// =========================================================================
|
|
365
|
+
// Internal
|
|
366
|
+
// =========================================================================
|
|
367
|
+
buildThreadParams(cwd) {
|
|
368
|
+
const params = {
|
|
369
|
+
cwd,
|
|
370
|
+
...(this.model ? { model: this.model } : {}),
|
|
371
|
+
...(this.modelProvider ? { modelProvider: this.modelProvider } : {}),
|
|
372
|
+
...(this.approvalPolicyConfig ? { approvalPolicy: this.approvalPolicyConfig } : {}),
|
|
373
|
+
...(this.sandboxConfig ? { sandbox: this.sandboxConfig } : {}),
|
|
374
|
+
};
|
|
375
|
+
return params;
|
|
376
|
+
}
|
|
377
|
+
buildTurnParams(message, opts) {
|
|
378
|
+
const params = {
|
|
379
|
+
threadId: this.currentThreadId ?? '',
|
|
380
|
+
input: [{ type: 'text', text: message }],
|
|
381
|
+
...((opts.model ?? this.model) ? { model: opts.model ?? this.model } : {}),
|
|
382
|
+
...((opts.reasoningEffort ?? this.reasoningEffort)
|
|
383
|
+
? { effort: opts.reasoningEffort ?? this.reasoningEffort }
|
|
384
|
+
: {}),
|
|
385
|
+
};
|
|
386
|
+
return params;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
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.
|
|
427
|
+
*/
|
|
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}`);
|
|
444
|
+
}
|
|
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
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
handleNotification(method, params) {
|
|
464
|
+
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
465
|
+
this.pushEvents(events);
|
|
466
|
+
}
|
|
467
|
+
handleServerRequest(id, method, params) {
|
|
468
|
+
const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
|
|
469
|
+
if (events.length === 0) {
|
|
470
|
+
// 未处理/不支持的服务端请求必须显式响应(error 即"拒绝",turn 继续),
|
|
471
|
+
// 否则服务端会一直等待响应(applyPatchApproval、item/tool/requestUserInput、
|
|
472
|
+
// mcpServer/elicitation/request 等)。
|
|
473
|
+
this.currentClient?.respondError(id, -32601, `Unsupported server request: ${method}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
for (const ev of events) {
|
|
477
|
+
if (ev.type === 'approval_requested') {
|
|
478
|
+
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}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
this.pushEvents(events);
|
|
483
|
+
}
|
|
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
|
+
buildGrantedPermissions(view) {
|
|
516
|
+
const items = view.permissions?.items ?? [];
|
|
517
|
+
const read = [];
|
|
518
|
+
const write = [];
|
|
519
|
+
let networkEnabled = false;
|
|
520
|
+
for (const item of items) {
|
|
521
|
+
if (!item.selected)
|
|
522
|
+
continue;
|
|
523
|
+
if (item.target.kind === 'fsRead') {
|
|
524
|
+
read.push(item.target.path);
|
|
525
|
+
}
|
|
526
|
+
else if (item.target.kind === 'fsWrite') {
|
|
527
|
+
write.push(item.target.path);
|
|
528
|
+
}
|
|
529
|
+
else if (item.target.kind === 'network') {
|
|
530
|
+
networkEnabled = true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// 真实 GrantedPermissionProfile 形状(v1 schema):fileSystem 用
|
|
534
|
+
// entries[{path, access}](read/write 为 legacy 字符串数组,即将移除),
|
|
535
|
+
// network 是 { enabled } 布尔开关,不是 host 列表。
|
|
536
|
+
return {
|
|
537
|
+
fileSystem: {
|
|
538
|
+
entries: [
|
|
539
|
+
...read.map((path) => ({ path, access: 'read' })),
|
|
540
|
+
...write.map((path) => ({ path, access: 'write' })),
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
network: { enabled: networkEnabled },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** 拒绝权限审批:空 fileSystem + 关闭网络(真实 GrantedPermissionProfile)。 */
|
|
547
|
+
denyAllPermissions() {
|
|
548
|
+
return {
|
|
549
|
+
fileSystem: { entries: [] },
|
|
550
|
+
network: { enabled: false },
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAppServerTranslator: translates protocol notifications and server
|
|
3
|
+
* requests into AgentEvents.
|
|
4
|
+
*
|
|
5
|
+
* The translator is stateful per turn — it tracks the current turn's state,
|
|
6
|
+
* accumulates deltas, and synthesizes the final ResultEvent when the turn
|
|
7
|
+
* completes. Method names and parameter shapes follow the real Codex
|
|
8
|
+
* app-server v2 protocol (see protocol-types.ts).
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentEvent } from '../../types.js';
|
|
11
|
+
import type { ApprovalRequestedEvent, ApprovalResolvedEvent } from '../../types.js';
|
|
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
|
+
export type TranslatorEvent = AgentEvent | TurnStartedEvent | TurnDiffEvent | ApprovalRequestedEvent | ApprovalResolvedEvent;
|
|
34
|
+
export declare class CodexAppServerTranslator {
|
|
35
|
+
private turnId;
|
|
36
|
+
private threadId;
|
|
37
|
+
private turnStatus;
|
|
38
|
+
/** Full agentMessage text per item id (item-scoped snapshot). */
|
|
39
|
+
private textByItem;
|
|
40
|
+
/**
|
|
41
|
+
* Reasoning content per item id: paragraphs keyed by `contentIndex`
|
|
42
|
+
* (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
|
|
43
|
+
* item is isolated so interleaved items keep their own chronology.
|
|
44
|
+
*/
|
|
45
|
+
private reasoningByItem;
|
|
46
|
+
/** Full command output per item id (item-scoped snapshot). */
|
|
47
|
+
private toolOutputByItem;
|
|
48
|
+
/** Full plan text per item id (item-scoped snapshot). */
|
|
49
|
+
private planByItem;
|
|
50
|
+
/** commandExecution items that emitted item/started (tool block position anchor). */
|
|
51
|
+
private startedToolItems;
|
|
52
|
+
/** fileChange items seen this turn, keyed by item id (item/started). */
|
|
53
|
+
private fileChangeItems;
|
|
54
|
+
/** File approvals whose item/started has not arrived yet (out-of-order flow). */
|
|
55
|
+
private pendingFileApprovalByItemId;
|
|
56
|
+
private usage;
|
|
57
|
+
private operationKind;
|
|
58
|
+
/**
|
|
59
|
+
* Handle a notification from the server and return events.
|
|
60
|
+
*/
|
|
61
|
+
handleNotification(method: string, params: unknown): TranslatorEvent[];
|
|
62
|
+
/**
|
|
63
|
+
* Handle a server request (incoming JSON-RPC request from server, e.g. approval).
|
|
64
|
+
*/
|
|
65
|
+
handleServerRequest(id: number | string, method: string, params: unknown): TranslatorEvent[];
|
|
66
|
+
/**
|
|
67
|
+
* Synthesize a ResultEvent from the current turn state.
|
|
68
|
+
* Called when the turn completes.
|
|
69
|
+
*/
|
|
70
|
+
handleTurnCompleted(params?: TurnCompletedNotification['params']): TranslatorEvent[];
|
|
71
|
+
/**
|
|
72
|
+
* Get the current usage stats.
|
|
73
|
+
*/
|
|
74
|
+
getUsage(): {
|
|
75
|
+
inputTokens: number;
|
|
76
|
+
outputTokens: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Get the current turn status.
|
|
80
|
+
*/
|
|
81
|
+
getTurnStatus(): string;
|
|
82
|
+
/**
|
|
83
|
+
* Set the operation kind (exec vs compact).
|
|
84
|
+
*/
|
|
85
|
+
setOperationKind(kind: 'exec' | 'compact'): void;
|
|
86
|
+
private handleTurnStarted;
|
|
87
|
+
private handleAgentMessageDelta;
|
|
88
|
+
private handleReasoningDelta;
|
|
89
|
+
private handleReasoningPartAdded;
|
|
90
|
+
private handleItemCompleted;
|
|
91
|
+
private handleReasoningTextDelta;
|
|
92
|
+
private handleCommandOutputDelta;
|
|
93
|
+
private handlePlanUpdated;
|
|
94
|
+
private handleTokenUsageUpdated;
|
|
95
|
+
private handleError;
|
|
96
|
+
private handleThreadCompacted;
|
|
97
|
+
private handleServerRequestResolved;
|
|
98
|
+
private handleCommandApproval;
|
|
99
|
+
private handleFileApproval;
|
|
100
|
+
private handleItemStarted;
|
|
101
|
+
private handlePermissionsApproval;
|
|
102
|
+
private buildPermissionItems;
|
|
103
|
+
/** Get (or lazily create) the reasoning accumulator for one item. */
|
|
104
|
+
private reasoningEntry;
|
|
105
|
+
/** Compose the display snapshot for one reasoning item (content + summary). */
|
|
106
|
+
private reasoningSnapshotFor;
|
|
107
|
+
/** Receive-time stamp, aligned with the exec JSONL parse path. */
|
|
108
|
+
private now;
|
|
109
|
+
}
|