@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.3
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/README.en.md +4 -4
- package/README.md +14 -5
- package/data/build/dist/client/account-bar.js +57 -3
- package/data/build/dist/client/bundle.js +36246 -268
- package/data/build/dist/client/cli-slots.js +60 -18
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
- package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
- package/data/build/dist/client/features/file-management-panel.js +48 -0
- package/data/build/dist/client/features/file-management.d.ts +4 -0
- package/data/build/dist/client/features/file-management.js +50 -0
- package/data/build/dist/client/features/index.d.ts +10 -1
- package/data/build/dist/client/features/index.js +12 -1
- package/data/build/dist/client/features/login-protection-panel.js +1 -1
- package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
- package/data/build/dist/client/features/login-protection-session.js +18 -0
- package/data/build/dist/client/features/peer-host.d.ts +4 -0
- package/data/build/dist/client/features/peer-host.js +89 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
- package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
- package/data/build/dist/client/features/reverse-proxy.js +75 -5
- package/data/build/dist/client/features/types.d.ts +9 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
- package/data/build/dist/client/file-management-dom.d.ts +12 -0
- package/data/build/dist/client/file-management-dom.js +527 -0
- package/data/build/dist/client/git-management.js +24 -5
- package/data/build/dist/client/host-navigation.d.ts +29 -0
- package/data/build/dist/client/host-navigation.js +29 -0
- package/data/build/dist/client/host-router.d.ts +3 -0
- package/data/build/dist/client/host-router.js +2 -0
- package/data/build/dist/client/index.d.ts +6 -1
- package/data/build/dist/client/index.js +30 -10
- package/data/build/dist/client/locale.js +36 -7
- package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
- package/data/build/dist/client/peer-host-connection-button.js +69 -0
- package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
- package/data/build/dist/client/peer-host-management-api.js +33 -0
- package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
- package/data/build/dist/client/peer-host-management-panel.js +181 -0
- package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
- package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
- package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
- package/data/build/dist/client/peer-host-scoped-client.js +316 -0
- package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
- package/data/build/dist/client/peer-host-session-controller.js +95 -0
- package/data/build/dist/client/quick-phrase-slot.js +14 -0
- package/data/build/dist/client/remote-web-context.js +26 -4
- package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
- package/data/build/dist/client/session-changed-files-view.js +337 -0
- package/data/build/dist/client/settings-bridge.d.ts +14 -4
- package/data/build/dist/client/settings-bridge.js +59 -12
- package/data/build/dist/client/settings-section.js +17 -4
- package/data/build/dist/client/subscription-slot.js +21 -8
- package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
- package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
- package/data/build/dist/dsh-capabilities/matrix.js +9 -0
- package/data/build/dist/dsh-capabilities/routes.js +35 -0
- package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
- package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/host-router.d.ts +26 -0
- package/data/build/dist/features/host-router.js +110 -0
- package/data/build/dist/features/registry.js +3 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
- package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
- package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
- package/data/build/dist/host/cli-adapters/feature.js +94 -10
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
- package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
- package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
- package/data/build/dist/host/cli-adapters/registry.js +41 -7
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
- package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
- package/data/build/dist/host/dsh-device-runtime.js +55 -29
- package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
- package/data/build/dist/host/dsh-runtime-version.js +71 -2
- package/data/build/dist/host/features/auth.js +10 -2
- package/data/build/dist/host/features/file-management.d.ts +5 -0
- package/data/build/dist/host/features/file-management.js +267 -0
- package/data/build/dist/host/features/git-management.js +24 -2
- package/data/build/dist/host/features/index.d.ts +2 -0
- package/data/build/dist/host/features/index.js +6 -0
- package/data/build/dist/host/features/peer-host.d.ts +22 -0
- package/data/build/dist/host/features/peer-host.js +322 -0
- package/data/build/dist/host/features/types.d.ts +4 -0
- package/data/build/dist/host/host-router.d.ts +3 -0
- package/data/build/dist/host/host-router.js +2 -0
- package/data/build/dist/host/index-injection.d.ts +17 -0
- package/data/build/dist/host/index-injection.js +24 -0
- package/data/build/dist/host/index.d.ts +4 -1
- package/data/build/dist/host/index.js +41 -7
- package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
- package/data/build/dist/host/lan-access-dsh.js +145 -26
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
- package/data/build/dist/host/native-session-bridge.d.ts +18 -2
- package/data/build/dist/host/native-session-bridge.js +236 -8
- package/data/build/dist/host/remote-web-runtime.js +26 -1
- package/data/build/dist/host/rpc.js +8 -3
- package/data/build/dist/host/session-changes.d.ts +5 -0
- package/data/build/dist/host/session-changes.js +163 -0
- package/data/build/dist/host/settings.js +6 -0
- package/data/build/dist/index.d.ts +1 -1
- package/data/build/dist/index.js +1 -1
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
- package/data/build/dist/shared/contracts/config.d.ts +15 -0
- package/data/build/dist/shared/contracts/config.js +10 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +14 -0
- package/data/build/dist/shared/contracts/errors.js +15 -0
- package/data/build/dist/shared/contracts/feature.d.ts +2 -0
- package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
- package/data/build/dist/shared/contracts/file-management.js +3 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
- package/data/build/dist/shared/contracts/peer-host.js +19 -0
- package/data/build/dist/shared/contracts/version.d.ts +1 -1
- package/data/build/dist/shared/contracts/version.js +1 -1
- package/data/build/dist/shared/index.d.ts +4 -3
- package/data/build/dist/shared/index.js +2 -2
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +23 -1
- package/data/build/dist/transport/dsh-transport.d.ts +2 -0
- package/data/build/dist/transport/dsh-transport.js +11 -0
- package/data/build/dist/transport/webrtc-client.d.ts +2 -0
- package/data/build/dist/transport/webrtc-client.js +2 -1
- package/data/build/dist/transport/webrtc-host.d.ts +2 -0
- package/data/build/dist/transport/webrtc-host.js +6 -3
- package/data/build/dist/typert.host.d.ts +1 -1
- package/data/build/dist/typert.host.js +2 -1
- package/dsh.bundle.patch +10 -0
- package/package.json +19 -11
- package/profile/README.md +2 -2
- package/profile/package.json +2 -2
- package/profile/version.json +1 -1
- package/version.json +1 -1
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { JsonRpcProcess } from './json-rpc-process.js';
|
|
4
|
+
import { basename, join, resolve } from 'node:path';
|
|
5
|
+
import { JsonRpcProcess, JsonRpcRequestError } from './json-rpc-process.js';
|
|
5
6
|
import { detectBinary, emptyCatalog, isRecord, textValue, usageChunk } from './rpc-driver-utils.js';
|
|
6
7
|
import { CODEX_CATALOG, isProviderDefaultModel } from './model-catalog.js';
|
|
7
8
|
import { probeStoredSession, readFirstJsonRecord } from './session-probe.js';
|
|
8
9
|
import { firstToolText, normalizeToolStatus, serializeToolValue } from './tool-observation.js';
|
|
9
10
|
import { isQuestionEvent, questionAnswersRecord, readAgentQuestions } from './interaction-events.js';
|
|
11
|
+
// 只有 Provider 已明确报告超过窗口时才主动压缩;接近上限仍交给 Codex
|
|
12
|
+
// 自身的 auto-compact,避免对正常的高占用回合重复发起压缩。
|
|
13
|
+
const CODEX_COMPACTION_THRESHOLD = 1;
|
|
14
|
+
const CODEX_COMPACTION_TIMEOUT_MS = 60_000;
|
|
10
15
|
/** Codex app-server 的 JSON-RPC 驱动,Host 只暴露统一文本流,不暴露线程和 token。 */
|
|
11
16
|
export class CodexAppServerDriver {
|
|
12
17
|
descriptor = { id: 'codex', name: 'Codex', protocol: 'json-rpc', capabilities: ['models', 'stream', 'resume', 'interrupt', 'tool-events', 'reasoning', 'usage', 'permission', 'questions', 'steer'] };
|
|
18
|
+
// Codex 一个 Provider turn 可能同时包含多个工具调用。只有 assistant item
|
|
19
|
+
// 切换后才分段,不能在每个工具完成后注入下一 step。
|
|
13
20
|
supportsSegmentedTurns = true;
|
|
14
21
|
binaries;
|
|
15
22
|
runSpawnSync;
|
|
@@ -72,11 +79,12 @@ export class CodexAppServerDriver {
|
|
|
72
79
|
const command = this.cachedBinary ?? (await this.detect()).command;
|
|
73
80
|
if (command === null)
|
|
74
81
|
throw new Error('Codex 未安装');
|
|
75
|
-
if (input.splitToolSteps) {
|
|
82
|
+
if (input.splitToolSteps && this.supportsSegmentedTurns) {
|
|
76
83
|
yield* this.executeSegmentedTurn(input, command);
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
86
|
const session = await this.getSession(input, command);
|
|
87
|
+
prepareCodexSession(session, input);
|
|
80
88
|
const rpc = session.rpc;
|
|
81
89
|
try {
|
|
82
90
|
if (session.threadId === '') {
|
|
@@ -86,13 +94,28 @@ export class CodexAppServerDriver {
|
|
|
86
94
|
session.threadId = readId(thread) ?? input.providerSessionId ?? input.sessionId;
|
|
87
95
|
session.providerSessionId = session.threadId;
|
|
88
96
|
}
|
|
97
|
+
await this.ensureContextCapacity(session, input);
|
|
98
|
+
yield* drainCompactionEvents(session);
|
|
89
99
|
yield { type: 'session-binding', providerSessionId: session.providerSessionId };
|
|
90
100
|
const eventQueue = createCodexTurnEventQueue();
|
|
91
101
|
let activeTurnId = null;
|
|
92
102
|
let turnStartResolved = false;
|
|
93
103
|
const notificationsBeforeTurnStart = [];
|
|
94
104
|
let terminalReason = null;
|
|
105
|
+
let sawMeaningfulEvent = false;
|
|
95
106
|
const acceptNotification = (message, allowUnidentifiedTool) => {
|
|
107
|
+
// Codex 自动压缩可能在独立的内部 turn 中运行。它不属于当前用户 turn,
|
|
108
|
+
// 但压缩 item 和 thread/compacted 必须继续进入公共投影层,否则压缩虽已发生,
|
|
109
|
+
// 原生会话不会显示任何 compaction 活动。
|
|
110
|
+
if (isCodexCompactionNotification(message, session.threadId)) {
|
|
111
|
+
const notificationTurnId = readTurnId(message);
|
|
112
|
+
// 显式 thread/compact/start 由 waitForCompaction 独占消费通知并生成
|
|
113
|
+
// start/summary/end,当前 turn 队列不能再次投影同一组事件。
|
|
114
|
+
if (!session.suppressCompactionNotifications
|
|
115
|
+
&& (notificationTurnId === null || !session.suppressedCompactionTurnIds.has(notificationTurnId)))
|
|
116
|
+
eventQueue.push(message);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
96
119
|
if (!isCodexNotificationForTurn(message, session.threadId, activeTurnId, allowUnidentifiedTool))
|
|
97
120
|
return;
|
|
98
121
|
const turnId = readTurnId(message);
|
|
@@ -129,20 +152,18 @@ export class CodexAppServerDriver {
|
|
|
129
152
|
else
|
|
130
153
|
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
131
154
|
try {
|
|
132
|
-
const response = await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}, { signal: input.signal, killOnAbort: false });
|
|
155
|
+
const response = await this.startTurn(session, input);
|
|
156
|
+
// turn/start 可能因上下文超限内部触发 compact;压缩事件在重试成功后
|
|
157
|
+
// 仍需先交给公共投影器,不能留在驱动的暂存队列里。
|
|
158
|
+
yield* drainCompactionEvents(session);
|
|
137
159
|
const responseTurnId = readTurnId(response);
|
|
138
160
|
if (responseTurnId !== null) {
|
|
139
161
|
activeTurnId = responseTurnId;
|
|
140
162
|
session.turnId = responseTurnId;
|
|
141
163
|
}
|
|
142
164
|
turnStartResolved = true;
|
|
143
|
-
// 响应前缓存的通知可能来自 thread/resume 的旧历史。没有 turnId
|
|
144
|
-
//
|
|
145
|
-
// 原有规则处理。
|
|
165
|
+
// 响应前缓存的通知可能来自 thread/resume 的旧历史。没有 turnId 的事件
|
|
166
|
+
// 无法证明属于本次回合,因此只能丢弃;有明确 turnId 的事件仍按原有规则处理。
|
|
146
167
|
for (const message of notificationsBeforeTurnStart.splice(0))
|
|
147
168
|
acceptNotification(message, false);
|
|
148
169
|
// 某些 app-server 会直接在 turn/start 响应中返回终态。父仓库把它
|
|
@@ -151,9 +172,13 @@ export class CodexAppServerDriver {
|
|
|
151
172
|
if (responseTerminal !== null)
|
|
152
173
|
onNotification(responseTerminal);
|
|
153
174
|
for await (const message of eventQueue.iterable) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
175
|
+
const rawChunk = codexMessageToChunk(message);
|
|
176
|
+
if (rawChunk === null)
|
|
177
|
+
continue;
|
|
178
|
+
if (rawChunk.type !== 'finish')
|
|
179
|
+
sawMeaningfulEvent = true;
|
|
180
|
+
const chunk = stabilizeCodexEvent(session, rawChunk);
|
|
181
|
+
yield chunk;
|
|
157
182
|
}
|
|
158
183
|
if (input.signal?.aborted)
|
|
159
184
|
await this.interrupt(input.sessionId);
|
|
@@ -168,18 +193,28 @@ export class CodexAppServerDriver {
|
|
|
168
193
|
removeNotificationListener();
|
|
169
194
|
eventQueue.close();
|
|
170
195
|
}
|
|
171
|
-
|
|
196
|
+
// 终态 chunk 之后调用方不会再拉动本迭代器,turnId 必须在产出 finish
|
|
197
|
+
// 前复位,否则下一轮 steer/interrupt 会指向已经结束的 turn。
|
|
198
|
+
session.turnId = null;
|
|
199
|
+
if (!input.signal?.aborted && terminalReason !== 'cancel' && !sawMeaningfulEvent) {
|
|
200
|
+
yield { type: 'text-delta', text: 'CODINGNS_PROVIDER_EMPTY_RESPONSE: Codex Provider 未返回任何有效事件。' };
|
|
201
|
+
yield { type: 'finish', reason: 'error' };
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
yield { type: 'finish', reason: input.signal?.aborted ? 'cancel' : terminalReason ?? 'stop' };
|
|
205
|
+
}
|
|
172
206
|
}
|
|
173
207
|
finally { /* app-server 在会话结束前保持连接。 */ }
|
|
174
208
|
}
|
|
175
209
|
/**
|
|
176
|
-
* 将一个 Codex turn
|
|
210
|
+
* 将一个 Codex turn 按 assistant item 切成多个 DSH step。
|
|
177
211
|
*
|
|
178
212
|
* DSH 的 step 边界只能由 Agent Loop 提交;这里仅提前结束本次 llm/stream,
|
|
179
213
|
* 保留同一个 Codex turn 的通知队列,下一次 llm/stream 再继续消费它。
|
|
180
214
|
*/
|
|
181
215
|
async *executeSegmentedTurn(input, command) {
|
|
182
216
|
const session = await this.getSession(input, command);
|
|
217
|
+
prepareCodexSession(session, input);
|
|
183
218
|
const rpc = session.rpc;
|
|
184
219
|
let active;
|
|
185
220
|
try {
|
|
@@ -190,13 +225,31 @@ export class CodexAppServerDriver {
|
|
|
190
225
|
session.threadId = readId(thread) ?? input.providerSessionId ?? input.sessionId;
|
|
191
226
|
session.providerSessionId = session.threadId;
|
|
192
227
|
}
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
// 只有 Host 显式声明续段时才复用挂起的 Provider 运行。新用户回合、注入
|
|
229
|
+
// 失败或取消后的下一次执行必须丢弃旧段:旧段要么已经跑完,要么其队列
|
|
230
|
+
// 已关闭,复用只会立刻产出 stop,把新消息变成空回合。
|
|
231
|
+
const suspended = input.resumeSegmentedTurn === true
|
|
232
|
+
&& session.segmentedTurn !== undefined
|
|
233
|
+
&& !session.segmentedTurn.done
|
|
234
|
+
? session.segmentedTurn
|
|
235
|
+
: undefined;
|
|
236
|
+
if (suspended === undefined && session.segmentedTurn !== undefined) {
|
|
237
|
+
this.closeSegmentedTurn(session, session.segmentedTurn);
|
|
238
|
+
}
|
|
239
|
+
// 分段 step 仍属于同一个 Provider turn;恢复它时不能插入 compact turn。
|
|
240
|
+
const hasSuspendedTurn = suspended !== undefined;
|
|
241
|
+
if (!hasSuspendedTurn)
|
|
242
|
+
await this.ensureContextCapacity(session, input);
|
|
243
|
+
if (!hasSuspendedTurn)
|
|
244
|
+
yield* drainCompactionEvents(session);
|
|
245
|
+
active = suspended ?? await this.startSegmentedTurn(session, input);
|
|
246
|
+
yield* drainCompactionEvents(session);
|
|
247
|
+
const isNew = suspended === undefined;
|
|
195
248
|
if (isNew)
|
|
196
249
|
session.segmentedTurn = active;
|
|
197
250
|
if (isNew)
|
|
198
251
|
yield { type: 'session-binding', providerSessionId: session.providerSessionId };
|
|
199
|
-
for await (const chunk of this.consumeSegment(active, input))
|
|
252
|
+
for await (const chunk of this.consumeSegment(session, active, input))
|
|
200
253
|
yield chunk;
|
|
201
254
|
if (active.done && session.segmentedTurn === active)
|
|
202
255
|
session.segmentedTurn = undefined;
|
|
@@ -219,9 +272,21 @@ export class CodexAppServerDriver {
|
|
|
219
272
|
removeNotificationListener: () => undefined,
|
|
220
273
|
terminalReason: null,
|
|
221
274
|
done: false,
|
|
275
|
+
pendingChunk: undefined,
|
|
276
|
+
currentAssistantMessageId: undefined,
|
|
277
|
+
sawCompletedTool: false,
|
|
222
278
|
removeAbortListener: () => undefined,
|
|
223
279
|
};
|
|
224
280
|
const acceptNotification = (message, allowUnidentifiedTool) => {
|
|
281
|
+
if (isCodexCompactionNotification(message, session.threadId)) {
|
|
282
|
+
const notificationTurnId = readTurnId(message);
|
|
283
|
+
// 显式 thread/compact/start 由 waitForCompaction 独占消费通知并生成
|
|
284
|
+
// start/summary/end,当前分段 turn 不能再次投影同一组事件。
|
|
285
|
+
if (!session.suppressCompactionNotifications
|
|
286
|
+
&& (notificationTurnId === null || !session.suppressedCompactionTurnIds.has(notificationTurnId)))
|
|
287
|
+
eventQueue.push(message);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
225
290
|
if (!isCodexNotificationForTurn(message, session.threadId, activeTurnId, allowUnidentifiedTool))
|
|
226
291
|
return;
|
|
227
292
|
const turnId = readTurnId(message);
|
|
@@ -254,11 +319,7 @@ export class CodexAppServerDriver {
|
|
|
254
319
|
else
|
|
255
320
|
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
256
321
|
try {
|
|
257
|
-
const response = await
|
|
258
|
-
threadId: session.threadId,
|
|
259
|
-
input: [{ type: 'text', text: input.prompt }],
|
|
260
|
-
...(input.effortId ? { effort: input.effortId } : {}),
|
|
261
|
-
}, { signal: input.signal, killOnAbort: false });
|
|
322
|
+
const response = await this.startTurn(session, input);
|
|
262
323
|
const responseTurnId = readTurnId(response);
|
|
263
324
|
if (responseTurnId !== null) {
|
|
264
325
|
activeTurnId = responseTurnId;
|
|
@@ -277,28 +338,56 @@ export class CodexAppServerDriver {
|
|
|
277
338
|
throw error;
|
|
278
339
|
}
|
|
279
340
|
}
|
|
280
|
-
async *consumeSegment(active, input) {
|
|
341
|
+
async *consumeSegment(session, active, input) {
|
|
281
342
|
while (true) {
|
|
282
|
-
|
|
283
|
-
if (
|
|
343
|
+
let chunk;
|
|
344
|
+
if (active.pendingChunk !== undefined) {
|
|
345
|
+
chunk = active.pendingChunk;
|
|
346
|
+
active.pendingChunk = undefined;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const next = await active.queue.next();
|
|
350
|
+
if (next.done) {
|
|
351
|
+
if (input.signal?.aborted)
|
|
352
|
+
throw new Error('请求已取消');
|
|
353
|
+
active.done = true;
|
|
354
|
+
session.turnId = null;
|
|
355
|
+
// finish 是调用方看到的最后一个 chunk,之后不会再拉动本迭代器。
|
|
356
|
+
// 悬挂段必须在产出终态前解除,否则它会被当成下一轮的可续段复用。
|
|
357
|
+
if (session.segmentedTurn === active)
|
|
358
|
+
session.segmentedTurn = undefined;
|
|
359
|
+
active.removeAbortListener();
|
|
360
|
+
active.removeNotificationListener();
|
|
361
|
+
yield { type: 'finish', reason: active.terminalReason ?? 'stop' };
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
284
364
|
if (input.signal?.aborted)
|
|
285
365
|
throw new Error('请求已取消');
|
|
286
|
-
|
|
287
|
-
active.removeAbortListener();
|
|
288
|
-
active.removeNotificationListener();
|
|
289
|
-
yield { type: 'finish', reason: active.terminalReason ?? 'stop' };
|
|
290
|
-
return;
|
|
366
|
+
chunk = codexMessageToChunk(next.value);
|
|
291
367
|
}
|
|
292
|
-
if (input.signal?.aborted)
|
|
293
|
-
throw new Error('请求已取消');
|
|
294
|
-
const chunk = codexMessageToChunk(next.value);
|
|
295
368
|
if (chunk === null)
|
|
296
369
|
continue;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
370
|
+
// 一个 assistant item 可能在多个工具调用之间切换。把新 item 的首个
|
|
371
|
+
// 正文留给下一次 llm/stream,当前流只返回边界,确保 DSH 先创建新 step。
|
|
372
|
+
if ((chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')
|
|
373
|
+
&& chunk.messageId !== undefined) {
|
|
374
|
+
const previousMessageId = active.currentAssistantMessageId;
|
|
375
|
+
if (previousMessageId !== undefined
|
|
376
|
+
&& previousMessageId !== chunk.messageId
|
|
377
|
+
&& active.sawCompletedTool) {
|
|
378
|
+
active.pendingChunk = chunk;
|
|
379
|
+
active.currentAssistantMessageId = chunk.messageId;
|
|
380
|
+
active.sawCompletedTool = false;
|
|
381
|
+
yield { type: 'step-boundary' };
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
active.currentAssistantMessageId = chunk.messageId;
|
|
301
385
|
}
|
|
386
|
+
const stabilizedChunk = stabilizeCodexEvent(session, chunk);
|
|
387
|
+
if (stabilizedChunk.type === 'tool-event' && (stabilizedChunk.status === 'completed' || stabilizedChunk.status === 'failed')) {
|
|
388
|
+
active.sawCompletedTool = true;
|
|
389
|
+
}
|
|
390
|
+
yield stabilizedChunk;
|
|
302
391
|
}
|
|
303
392
|
}
|
|
304
393
|
closeSegmentedTurn(session, active) {
|
|
@@ -309,6 +398,144 @@ export class CodexAppServerDriver {
|
|
|
309
398
|
if (session.segmentedTurn === active)
|
|
310
399
|
session.segmentedTurn = undefined;
|
|
311
400
|
}
|
|
401
|
+
/** 在下一轮开始前主动压缩,避免把已满的线程直接交给 turn/start。 */
|
|
402
|
+
async ensureContextCapacity(session, input) {
|
|
403
|
+
if (session.contextWindow === undefined || session.contextTokens === undefined)
|
|
404
|
+
return;
|
|
405
|
+
// 达到窗口上限时下一轮还会追加用户输入,必须在 turn/start 前压缩;
|
|
406
|
+
// 只用严格大于会把恰好满窗的会话直接送进超限错误路径。
|
|
407
|
+
if (session.contextTokens < session.contextWindow * CODEX_COMPACTION_THRESHOLD)
|
|
408
|
+
return;
|
|
409
|
+
await this.compactThread(session, input);
|
|
410
|
+
}
|
|
411
|
+
/** turn/start 遇到上下文超限时压缩一次并重试,兼容关闭自动压缩的 Codex 配置。 */
|
|
412
|
+
async startTurn(session, input) {
|
|
413
|
+
try {
|
|
414
|
+
return await session.rpc.request('turn/start', codexTurnStartParams(input, session.threadId), { signal: input.signal, killOnAbort: false });
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (!isContextWindowError(error))
|
|
418
|
+
throw error;
|
|
419
|
+
await this.compactThread(session, input);
|
|
420
|
+
return session.rpc.request('turn/start', codexTurnStartParams(input, session.threadId), { signal: input.signal, killOnAbort: false });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** thread/compact/start 立即返回,真正完成由 contextCompaction turn 的终态通知表示。 */
|
|
424
|
+
async compactThread(session, input) {
|
|
425
|
+
if (session.compacting !== undefined)
|
|
426
|
+
return session.compacting;
|
|
427
|
+
// 在创建异步等待器前同步设置,避免多个 RPC 监听器交错处理同一批通知时
|
|
428
|
+
// 另一个监听器重复投影显式压缩事件。
|
|
429
|
+
session.suppressCompactionNotifications = true;
|
|
430
|
+
const compacting = this.waitForCompaction(session, input);
|
|
431
|
+
session.compacting = compacting;
|
|
432
|
+
try {
|
|
433
|
+
await compacting;
|
|
434
|
+
session.contextTokens = 0;
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
if (session.compacting === compacting)
|
|
438
|
+
session.compacting = undefined;
|
|
439
|
+
session.suppressCompactionNotifications = false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async waitForCompaction(session, input) {
|
|
443
|
+
const compactionId = `codex-compaction-${randomUUID()}`;
|
|
444
|
+
session.pendingCompactionEvents.push({ type: 'context-compaction', phase: 'start', compactionId });
|
|
445
|
+
let compactTurnId = null;
|
|
446
|
+
let sawCompaction = false;
|
|
447
|
+
let summary;
|
|
448
|
+
let shadowedTokenCount;
|
|
449
|
+
let shadowedItemCount;
|
|
450
|
+
let resolveDone;
|
|
451
|
+
let rejectDone;
|
|
452
|
+
const done = new Promise((resolve, reject) => { resolveDone = resolve; rejectDone = reject; });
|
|
453
|
+
const onNotification = (message) => {
|
|
454
|
+
const params = isRecord(message.params) ? message.params : null;
|
|
455
|
+
if (readScopedId(params, ['threadId', 'thread_id'], 'thread') !== session.threadId)
|
|
456
|
+
return;
|
|
457
|
+
if (message.method === 'thread/compacted') {
|
|
458
|
+
const details = compactionDetails(params);
|
|
459
|
+
summary = details.summary ?? summary;
|
|
460
|
+
shadowedTokenCount = details.shadowedTokenCount ?? shadowedTokenCount;
|
|
461
|
+
shadowedItemCount = details.shadowedItemCount ?? shadowedItemCount;
|
|
462
|
+
resolveDone?.();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (message.method === 'item/started') {
|
|
466
|
+
const item = isRecord(params?.item) ? params.item : null;
|
|
467
|
+
if (isCodexCompactionItemType(item?.type)) {
|
|
468
|
+
sawCompaction = true;
|
|
469
|
+
compactTurnId = readTurnId(message);
|
|
470
|
+
if (compactTurnId !== null)
|
|
471
|
+
session.suppressedCompactionTurnIds.add(compactTurnId);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (message.method === 'item/completed') {
|
|
476
|
+
const item = isRecord(params?.item) ? params.item : null;
|
|
477
|
+
if (isCodexCompactionItemType(item?.type)) {
|
|
478
|
+
const itemTurnId = readTurnId(message);
|
|
479
|
+
if (itemTurnId !== null)
|
|
480
|
+
session.suppressedCompactionTurnIds.add(itemTurnId);
|
|
481
|
+
const details = compactionDetails(item);
|
|
482
|
+
summary = details.summary ?? summary;
|
|
483
|
+
shadowedTokenCount = details.shadowedTokenCount ?? shadowedTokenCount;
|
|
484
|
+
shadowedItemCount = details.shadowedItemCount ?? shadowedItemCount;
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (message.method !== 'turn/completed')
|
|
489
|
+
return;
|
|
490
|
+
const turnId = readTurnId(message);
|
|
491
|
+
if (sawCompaction && compactTurnId !== null && turnId !== compactTurnId)
|
|
492
|
+
return;
|
|
493
|
+
if (!sawCompaction && compactTurnId === null)
|
|
494
|
+
return;
|
|
495
|
+
const turn = isRecord(params?.turn) ? params.turn : null;
|
|
496
|
+
if (turn?.status === 'failed' || turn?.status === 'interrupted' || turn?.status === 'cancelled') {
|
|
497
|
+
rejectDone?.(new Error('Codex 上下文压缩失败'));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
resolveDone?.();
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const removeListener = session.rpc.addNotificationListener(onNotification);
|
|
504
|
+
const timer = setTimeout(() => rejectDone?.(new Error('Codex 上下文压缩超时')), CODEX_COMPACTION_TIMEOUT_MS);
|
|
505
|
+
timer.unref?.();
|
|
506
|
+
try {
|
|
507
|
+
await session.rpc.request('thread/compact/start', { threadId: session.threadId }, { signal: input.signal, killOnAbort: false });
|
|
508
|
+
await done;
|
|
509
|
+
if (summary !== undefined || shadowedTokenCount !== undefined || shadowedItemCount !== undefined) {
|
|
510
|
+
// Codex 的 app-server 通知通常不携带被压缩 token 数。DSH 的
|
|
511
|
+
// contextPressure 必须用该数扣除已被替换的 surface;这里使用压缩前
|
|
512
|
+
// 最后一次 Provider prompt 规模作为保守锚点,不能把缺失值写成 0。
|
|
513
|
+
const resolvedShadowedTokenCount = shadowedTokenCount ?? session.contextTokens;
|
|
514
|
+
session.pendingCompactionEvents.push({
|
|
515
|
+
type: 'context-compaction',
|
|
516
|
+
phase: 'summary',
|
|
517
|
+
compactionId,
|
|
518
|
+
...(summary === undefined ? {} : { summary }),
|
|
519
|
+
...(resolvedShadowedTokenCount === undefined ? {} : { shadowedTokenCount: resolvedShadowedTokenCount }),
|
|
520
|
+
...(shadowedItemCount === undefined ? {} : { shadowedItemCount }),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
session.pendingCompactionEvents.push({ type: 'context-compaction', phase: 'end', compactionId });
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
session.pendingCompactionEvents.push({
|
|
527
|
+
type: 'context-compaction',
|
|
528
|
+
phase: 'end',
|
|
529
|
+
compactionId,
|
|
530
|
+
error: error instanceof Error ? error.message : String(error),
|
|
531
|
+
});
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
removeListener();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
312
539
|
/** 将新输入 steer 到当前 Codex turn。 */
|
|
313
540
|
async steer(sessionId, prompt) {
|
|
314
541
|
const session = this.sessions.get(sessionId);
|
|
@@ -329,17 +556,35 @@ export class CodexAppServerDriver {
|
|
|
329
556
|
}
|
|
330
557
|
catch { /* app-server 可能已经发出 turn/completed */ }
|
|
331
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* 丢弃等待下一个 DSH step 的分段运行(取消、注入失败或切换适配器时)。
|
|
561
|
+
* 队列关闭后旧回合的通知不会再被投影进下一轮,续段标记同时解除。
|
|
562
|
+
*/
|
|
563
|
+
discardSegmentedTurn(sessionId) {
|
|
564
|
+
const session = this.sessions.get(sessionId);
|
|
565
|
+
if (session?.segmentedTurn === undefined)
|
|
566
|
+
return;
|
|
567
|
+
this.closeSegmentedTurn(session, session.segmentedTurn);
|
|
568
|
+
}
|
|
332
569
|
/** 回传原生权限请求;审批值只在 Host 进程中流转。 */
|
|
333
570
|
respondPermission(sessionId, response) {
|
|
334
571
|
const session = this.sessions.get(sessionId);
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
572
|
+
const pending = session?.pendingPermissions.get(response.requestId);
|
|
573
|
+
if (pending === undefined || session === undefined)
|
|
337
574
|
throw new Error('Codex 权限请求不存在');
|
|
338
575
|
session.pendingPermissions.delete(response.requestId);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
576
|
+
if (pending.response === 'file-change') {
|
|
577
|
+
pending.resolve({ decision: response.approved ? 'accept' : 'decline' });
|
|
578
|
+
}
|
|
579
|
+
else if (pending.response === 'command') {
|
|
580
|
+
pending.resolve({ decision: response.approved ? 'accept' : 'decline' });
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
pending.resolve({
|
|
584
|
+
approved: response.approved,
|
|
585
|
+
...(response.reason?.trim() ? { reason: response.reason.trim().slice(0, 512) } : {}),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
343
588
|
}
|
|
344
589
|
/** 回传 requestUserInput 的结构化回答。 */
|
|
345
590
|
respondQuestion(sessionId, response) {
|
|
@@ -354,8 +599,8 @@ export class CodexAppServerDriver {
|
|
|
354
599
|
for (const session of this.sessions.values()) {
|
|
355
600
|
if (session.segmentedTurn !== undefined)
|
|
356
601
|
this.closeSegmentedTurn(session, session.segmentedTurn);
|
|
357
|
-
for (const
|
|
358
|
-
resolve({ approved: false });
|
|
602
|
+
for (const pending of session.pendingPermissions.values())
|
|
603
|
+
pending.resolve({ approved: false });
|
|
359
604
|
session.pendingPermissions.clear();
|
|
360
605
|
for (const resolve of session.pendingQuestions.values())
|
|
361
606
|
resolve({ answers: {} });
|
|
@@ -377,16 +622,23 @@ export class CodexAppServerDriver {
|
|
|
377
622
|
const session = {
|
|
378
623
|
rpc,
|
|
379
624
|
cwd: input.cwd,
|
|
625
|
+
modelId: input.modelId,
|
|
626
|
+
contextWindow: undefined,
|
|
627
|
+
contextTokens: undefined,
|
|
380
628
|
threadId: '',
|
|
381
629
|
turnId: null,
|
|
382
630
|
providerSessionId: input.providerSessionId ?? input.sessionId,
|
|
383
631
|
pendingPermissions: new Map(),
|
|
384
632
|
pendingQuestions: new Map(),
|
|
385
633
|
segmentedTurn: undefined,
|
|
634
|
+
compacting: undefined,
|
|
635
|
+
suppressCompactionNotifications: false,
|
|
636
|
+
suppressedCompactionTurnIds: new Set(),
|
|
637
|
+
pendingCompactionEvents: [],
|
|
386
638
|
};
|
|
387
639
|
this.processes.add(rpc);
|
|
388
640
|
this.sessions.set(input.sessionId, session);
|
|
389
|
-
await rpc.request('initialize', { clientInfo: { name: 'codingns4dsh', version: '0.1.1' }, capabilities: {} }, { signal: input.signal, killOnAbort: false });
|
|
641
|
+
await rpc.request('initialize', { clientInfo: { name: 'codingns4dsh', version: '0.1.1' }, capabilities: { experimentalApi: true } }, { signal: input.signal, killOnAbort: false });
|
|
390
642
|
rpc.notify('initialized', {});
|
|
391
643
|
rpc.setServerRequestHandler((request) => {
|
|
392
644
|
const requestId = readRequestId(request);
|
|
@@ -399,11 +651,60 @@ export class CodexAppServerDriver {
|
|
|
399
651
|
if (isQuestionEvent(`${method} ${type}`)) {
|
|
400
652
|
return new Promise((resolve) => session.pendingQuestions.set(requestId, resolve));
|
|
401
653
|
}
|
|
402
|
-
return new Promise((resolve) => session.pendingPermissions.set(requestId,
|
|
654
|
+
return new Promise((resolve) => session.pendingPermissions.set(requestId, {
|
|
655
|
+
resolve,
|
|
656
|
+
response: permissionResponseKind(method, params),
|
|
657
|
+
}));
|
|
403
658
|
});
|
|
404
659
|
return session;
|
|
405
660
|
}
|
|
406
661
|
}
|
|
662
|
+
function stabilizeCodexUsage(session, event) {
|
|
663
|
+
if (event.type !== 'usage')
|
|
664
|
+
return event;
|
|
665
|
+
if (event.contextTokens !== undefined)
|
|
666
|
+
session.contextTokens = event.contextTokens;
|
|
667
|
+
if (event.contextWindow === undefined)
|
|
668
|
+
return event;
|
|
669
|
+
if (session.contextWindow === undefined) {
|
|
670
|
+
session.contextWindow = event.contextWindow;
|
|
671
|
+
return event;
|
|
672
|
+
}
|
|
673
|
+
if (session.contextWindow === event.contextWindow)
|
|
674
|
+
return event;
|
|
675
|
+
// 同一 Codex thread/model 的 usage 通知可能带有全局默认窗口或迟到旧值。
|
|
676
|
+
// 它不能覆盖首个已确认窗口,否则 DSH 会把 256K 错显示成 1M,并跳过压缩。
|
|
677
|
+
const contextTokens = event.contextTokens ?? event.inputTokens;
|
|
678
|
+
return {
|
|
679
|
+
...event,
|
|
680
|
+
contextWindow: session.contextWindow,
|
|
681
|
+
contextTokens,
|
|
682
|
+
contextUsageRatio: Number(Math.min(1, contextTokens / session.contextWindow).toFixed(6)),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function stabilizeCodexEvent(session, event) {
|
|
686
|
+
if (event.type !== 'context-compaction')
|
|
687
|
+
return stabilizeCodexUsage(session, event);
|
|
688
|
+
// 自动压缩的 item 通知没有独立 usage。压缩结束后清零本地状态,避免下一轮
|
|
689
|
+
// 把压缩前的旧 prompt 规模误认为当前上下文并再次发起压缩。
|
|
690
|
+
if (event.phase === 'end') {
|
|
691
|
+
session.contextTokens = 0;
|
|
692
|
+
return event;
|
|
693
|
+
}
|
|
694
|
+
if (event.phase !== 'summary' || event.shadowedTokenCount !== undefined || session.contextTokens === undefined)
|
|
695
|
+
return event;
|
|
696
|
+
return { ...event, shadowedTokenCount: session.contextTokens };
|
|
697
|
+
}
|
|
698
|
+
function prepareCodexSession(session, input) {
|
|
699
|
+
// 未提供 modelId 表示沿用当前 Codex thread 的模型,不能因为配置字段缺省
|
|
700
|
+
// 就把已经确认的上下文容量清空。
|
|
701
|
+
if (input.modelId === undefined || session.modelId === input.modelId)
|
|
702
|
+
return;
|
|
703
|
+
// 模型切换意味着上下文容量可能改变;只有这类明确路由变化才允许重置
|
|
704
|
+
// 稳定窗口,同一模型的迟到 usage 不能触发重置。
|
|
705
|
+
session.modelId = input.modelId;
|
|
706
|
+
session.contextWindow = undefined;
|
|
707
|
+
}
|
|
407
708
|
function parseCodexCatalog(value) {
|
|
408
709
|
const root = isRecord(value) && isRecord(value.data) ? value.data : value;
|
|
409
710
|
const models = Array.isArray(root)
|
|
@@ -449,6 +750,39 @@ function codexMessageToChunk(message) {
|
|
|
449
750
|
const method = typeof message.method === 'string' ? message.method : '';
|
|
450
751
|
const type = typeof item.type === 'string' ? item.type : '';
|
|
451
752
|
const text = textValue(params.delta ?? params.text ?? params.content ?? params.message);
|
|
753
|
+
if (isCodexCompactionItemType(type)) {
|
|
754
|
+
const details = compactionDetails(item);
|
|
755
|
+
const compactionId = firstToolText(item.id, params.itemId);
|
|
756
|
+
const phase = method === 'thread/compacted'
|
|
757
|
+
? 'end'
|
|
758
|
+
: method.includes('started') || method.includes('start')
|
|
759
|
+
? 'start'
|
|
760
|
+
: method.includes('completed') || method.includes('compacted') || method.includes('summary')
|
|
761
|
+
? 'summary'
|
|
762
|
+
: 'end';
|
|
763
|
+
return {
|
|
764
|
+
type: 'context-compaction',
|
|
765
|
+
phase,
|
|
766
|
+
...(compactionId === undefined ? {} : { compactionId }),
|
|
767
|
+
...(details.summary === undefined ? {} : { summary: details.summary }),
|
|
768
|
+
...(details.shadowedTokenCount === undefined ? {} : { shadowedTokenCount: details.shadowedTokenCount }),
|
|
769
|
+
...(details.shadowedItemCount === undefined ? {} : { shadowedItemCount: details.shadowedItemCount }),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (method === 'thread/compacted') {
|
|
773
|
+
const details = compactionDetails(params);
|
|
774
|
+
return {
|
|
775
|
+
type: 'context-compaction',
|
|
776
|
+
phase: 'end',
|
|
777
|
+
...(details.summary === undefined ? {} : { summary: details.summary }),
|
|
778
|
+
...(details.shadowedTokenCount === undefined ? {} : { shadowedTokenCount: details.shadowedTokenCount }),
|
|
779
|
+
...(details.shadowedItemCount === undefined ? {} : { shadowedItemCount: details.shadowedItemCount }),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
if (method === 'turn/completed' && isContextCompactionTurn(params.turn)) {
|
|
783
|
+
const compactionId = readTurnId(message);
|
|
784
|
+
return { type: 'context-compaction', phase: 'end', ...(compactionId === null ? {} : { compactionId }) };
|
|
785
|
+
}
|
|
452
786
|
if (isQuestionEvent(`${method} ${type}`)) {
|
|
453
787
|
const requestId = readRequestId(message) ?? readRequestId(params);
|
|
454
788
|
const questions = readAgentQuestions(params.questions ?? item.questions ?? params);
|
|
@@ -457,9 +791,17 @@ function codexMessageToChunk(message) {
|
|
|
457
791
|
}
|
|
458
792
|
if (method.includes('permission') || method.includes('Approval') || type.includes('permission') || type.includes('approval')) {
|
|
459
793
|
const requestId = readRequestId(message) ?? readRequestId(params);
|
|
460
|
-
const
|
|
794
|
+
const permission = codexPermissionDetails(method, params, item);
|
|
795
|
+
const detail = permission.detail ?? text ?? undefined;
|
|
461
796
|
if (requestId !== null)
|
|
462
|
-
return {
|
|
797
|
+
return {
|
|
798
|
+
type: 'permission-request',
|
|
799
|
+
requestId,
|
|
800
|
+
kind: permission.kind,
|
|
801
|
+
...(permission.toolName === undefined ? {} : { toolName: permission.toolName }),
|
|
802
|
+
...(permission.callId === undefined ? {} : { callId: permission.callId }),
|
|
803
|
+
...(detail === undefined ? {} : { detail }),
|
|
804
|
+
};
|
|
463
805
|
}
|
|
464
806
|
const messageId = firstToolText(params.itemId, item.id);
|
|
465
807
|
if (method.includes('agentMessage') || method.includes('message') && (type.includes('text') || type === '')) {
|
|
@@ -469,10 +811,10 @@ function codexMessageToChunk(message) {
|
|
|
469
811
|
return text ? { type: 'reasoning-delta', text, ...(messageId === undefined ? {} : { messageId }) } : null;
|
|
470
812
|
}
|
|
471
813
|
if (isCodexToolEvent(method, type)) {
|
|
472
|
-
const name = firstToolText(item.name, item.toolName, item.tool, item.command !== undefined ? 'command_execution' : undefined, type);
|
|
814
|
+
const name = firstToolText(isFileChangeType(type) ? 'edit_file' : undefined, item.name, item.toolName, item.tool, item.command !== undefined ? 'command_execution' : undefined, type);
|
|
473
815
|
const callId = firstToolText(item.callId, item.call_id, item.toolCallId, item.id, params.itemId);
|
|
474
816
|
const agentId = firstToolText(item.agentId, item.agent_id, type.includes('agent') || type.includes('collab') ? item.id : undefined);
|
|
475
|
-
const input = serializeToolValue(item.arguments ?? item.input ?? item.command);
|
|
817
|
+
const input = serializeToolValue(isFileChangeType(type) ? fileChangeToolInput(item) : item.arguments ?? item.input ?? item.command);
|
|
476
818
|
const rawOutput = item.result ?? item.output ?? item.aggregated_output;
|
|
477
819
|
const explicitError = serializeToolValue(item.error);
|
|
478
820
|
const output = serializeToolValue(rawOutput);
|
|
@@ -496,7 +838,71 @@ function codexMessageToChunk(message) {
|
|
|
496
838
|
}
|
|
497
839
|
return codexUsageChunk(params);
|
|
498
840
|
}
|
|
499
|
-
|
|
841
|
+
function* drainCompactionEvents(session) {
|
|
842
|
+
while (session.pendingCompactionEvents.length > 0) {
|
|
843
|
+
const event = session.pendingCompactionEvents.shift();
|
|
844
|
+
if (event !== undefined)
|
|
845
|
+
yield event;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function compactionDetails(value) {
|
|
849
|
+
if (!isRecord(value))
|
|
850
|
+
return {};
|
|
851
|
+
const nested = isRecord(value.compaction) ? value.compaction : isRecord(value.item) ? value.item : value;
|
|
852
|
+
const summary = textValue(nested.summary ?? nested.summaryText ?? nested.compactionSummary);
|
|
853
|
+
const shadowedTokenCount = optionalToken(nested.shadowedTokenCount ?? nested.shadowed_tokens ?? nested.compactedTokens);
|
|
854
|
+
const shadowedItemCount = optionalToken(nested.shadowedItemCount ?? nested.shadowed_items ?? nested.compactedItems);
|
|
855
|
+
return {
|
|
856
|
+
...(summary === null || summary.trim() === '' ? {} : { summary: summary.trim() }),
|
|
857
|
+
...(shadowedTokenCount === undefined ? {} : { shadowedTokenCount }),
|
|
858
|
+
...(shadowedItemCount === undefined ? {} : { shadowedItemCount }),
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function isContextCompactionTurn(value) {
|
|
862
|
+
if (!isRecord(value))
|
|
863
|
+
return false;
|
|
864
|
+
const kind = String(value.type ?? value.kind ?? value.name ?? '').toLowerCase();
|
|
865
|
+
return kind.includes('contextcompaction') || kind.includes('compaction');
|
|
866
|
+
}
|
|
867
|
+
function codexPermissionDetails(method, params, item) {
|
|
868
|
+
const fileChange = method.includes('fileChange') || method.includes('file_change') || String(item.type ?? '').toLowerCase() === 'filechange';
|
|
869
|
+
const command = typeof params.command === 'string' ? params.command : typeof item.command === 'string' ? item.command : undefined;
|
|
870
|
+
const reason = typeof params.reason === 'string' ? params.reason : typeof params.description === 'string' ? params.description : undefined;
|
|
871
|
+
const grantRoot = typeof params.grantRoot === 'string' ? params.grantRoot : undefined;
|
|
872
|
+
const paths = permissionPaths(params.fileChanges ?? params.file_changes ?? item.changes);
|
|
873
|
+
const callId = firstToolText(params.callId, params.call_id, params.itemId, item.id);
|
|
874
|
+
const detail = [reason, command, paths.length > 0 ? `文件: ${paths.join(', ')}` : undefined, grantRoot ? `允许写入: ${grantRoot}` : undefined]
|
|
875
|
+
.filter((value) => value !== undefined && value.trim() !== '')
|
|
876
|
+
.join(';');
|
|
877
|
+
return {
|
|
878
|
+
kind: typeof params.kind === 'string' ? params.kind : fileChange ? 'file_change' : 'command',
|
|
879
|
+
...(fileChange ? { toolName: 'edit' } : {}),
|
|
880
|
+
...(callId === undefined ? {} : { callId }),
|
|
881
|
+
...(detail === '' ? {} : { detail }),
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function permissionPaths(value) {
|
|
885
|
+
if (Array.isArray(value))
|
|
886
|
+
return value.flatMap((entry) => permissionPaths(entry));
|
|
887
|
+
if (!isRecord(value))
|
|
888
|
+
return [];
|
|
889
|
+
const path = firstToolText(value.path, value.filePath, value.file_path);
|
|
890
|
+
return path === undefined ? [] : [path];
|
|
891
|
+
}
|
|
892
|
+
function isFileChangeType(type) {
|
|
893
|
+
return type.replace(/[_-]/gu, '').toLowerCase() === 'filechange';
|
|
894
|
+
}
|
|
895
|
+
function fileChangeToolInput(item) {
|
|
896
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
897
|
+
return {
|
|
898
|
+
changes: changes.map((change) => isRecord(change) ? {
|
|
899
|
+
file_path: firstToolText(change.path, change.filePath, change.file_path) ?? '',
|
|
900
|
+
...(typeof change.kind === 'string' ? { kind: change.kind } : {}),
|
|
901
|
+
...(typeof change.diff === 'string' ? { diff: change.diff } : typeof change.patch === 'string' ? { diff: change.patch } : {}),
|
|
902
|
+
} : change),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
/** Codex app-server 的 inputTokens 是含缓存读取的完整输入,cachedInputTokens 是其中已命中的子集。 */
|
|
500
906
|
function codexUsageChunk(params) {
|
|
501
907
|
const tokenUsage = isRecord(params.tokenUsage) ? params.tokenUsage : isRecord(params.token_usage) ? params.token_usage : null;
|
|
502
908
|
if (tokenUsage === null)
|
|
@@ -510,11 +916,10 @@ function codexUsageChunk(params) {
|
|
|
510
916
|
: isRecord(tokenUsage.total)
|
|
511
917
|
? tokenUsage.total
|
|
512
918
|
: tokenUsage;
|
|
513
|
-
const
|
|
919
|
+
const inputTokens = optionalToken(latest.inputTokens ?? latest.input_tokens);
|
|
514
920
|
const cacheReadTokens = optionalToken(latest.cachedInputTokens ?? latest.cached_input_tokens);
|
|
515
921
|
const usage = usageChunk({
|
|
516
|
-
inputTokens:
|
|
517
|
-
...(uncachedInputTokens === undefined ? {} : { uncachedInputTokens }),
|
|
922
|
+
inputTokens: inputTokens ?? 0,
|
|
518
923
|
...(latest.outputTokens === undefined && latest.output_tokens === undefined ? {} : { outputTokens: latest.outputTokens ?? latest.output_tokens }),
|
|
519
924
|
...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
|
|
520
925
|
...(latest.totalTokens === undefined && latest.total_tokens === undefined ? {} : { totalTokens: latest.totalTokens ?? latest.total_tokens }),
|
|
@@ -534,7 +939,7 @@ function codexUsageChunk(params) {
|
|
|
534
939
|
?? params.contextWindow
|
|
535
940
|
?? params.context_window);
|
|
536
941
|
const contextWindow = contextWindowValue !== undefined && contextWindowValue > 0 ? contextWindowValue : undefined;
|
|
537
|
-
const contextTokens = usage.inputTokens
|
|
942
|
+
const contextTokens = usage.inputTokens;
|
|
538
943
|
return {
|
|
539
944
|
...usage,
|
|
540
945
|
...(contextWindow === undefined ? {} : {
|
|
@@ -616,12 +1021,46 @@ function isCodexNotificationForTurn(message, threadId, turnId, allowUnidentified
|
|
|
616
1021
|
const notificationTurnId = readTurnId(message);
|
|
617
1022
|
if (notificationThreadId !== null && notificationThreadId !== threadId)
|
|
618
1023
|
return false;
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
if (
|
|
1024
|
+
// 权限/问题审批是 Codex 发起的 JSON-RPC 请求,必须先交给交互层,不能因
|
|
1025
|
+
// 它的 turnId 尚未出现在 turn/start 响应中而丢弃。
|
|
1026
|
+
if (message.id !== undefined && message.id !== null)
|
|
1027
|
+
return true;
|
|
1028
|
+
// 响应返回前 activeTurnId 为空;带 turnId 的旧通知不能提前关闭新回合队列。
|
|
1029
|
+
if (turnId === null && notificationTurnId !== null)
|
|
1030
|
+
return false;
|
|
1031
|
+
// turn/start 响应前的无 turnId 通知无法证明属于本次回合,尤其不能让迟到的
|
|
1032
|
+
// 无标识 turn/completed 直接关闭新回合;响应后再交给当前开放的 DSH step。
|
|
1033
|
+
if (notificationTurnId === null && !allowUnidentifiedTool)
|
|
622
1034
|
return false;
|
|
623
1035
|
return turnId === null || notificationTurnId === null || notificationTurnId === turnId;
|
|
624
1036
|
}
|
|
1037
|
+
function isCodexCompactionNotification(message, threadId) {
|
|
1038
|
+
const params = isRecord(message.params) ? message.params : null;
|
|
1039
|
+
if (readScopedId(params, ['threadId', 'thread_id'], 'thread') !== null
|
|
1040
|
+
&& readScopedId(params, ['threadId', 'thread_id'], 'thread') !== threadId)
|
|
1041
|
+
return false;
|
|
1042
|
+
if (message.method === 'thread/compacted')
|
|
1043
|
+
return true;
|
|
1044
|
+
if (message.method !== 'item/started' && message.method !== 'item/completed')
|
|
1045
|
+
return false;
|
|
1046
|
+
const item = isRecord(params?.item) ? params.item : null;
|
|
1047
|
+
return isCodexCompactionItemType(item?.type);
|
|
1048
|
+
}
|
|
1049
|
+
function isCodexCompactionItemType(value) {
|
|
1050
|
+
return value === 'contextCompaction' || value === 'context_compaction' || value === 'context-compaction';
|
|
1051
|
+
}
|
|
1052
|
+
function isContextWindowError(error) {
|
|
1053
|
+
if (error instanceof JsonRpcRequestError)
|
|
1054
|
+
return hasContextWindowError(error.data);
|
|
1055
|
+
return hasContextWindowError(error);
|
|
1056
|
+
}
|
|
1057
|
+
function hasContextWindowError(value) {
|
|
1058
|
+
if (typeof value === 'string')
|
|
1059
|
+
return /context[_-]?window[_-]?exceeded|context window exceeded/iu.test(value);
|
|
1060
|
+
if (!isRecord(value))
|
|
1061
|
+
return false;
|
|
1062
|
+
return Object.entries(value).some(([key, child]) => /context.?window.?exceeded/iu.test(key) || hasContextWindowError(child));
|
|
1063
|
+
}
|
|
625
1064
|
function readCodexTerminalReason(message) {
|
|
626
1065
|
if (message.method === 'error') {
|
|
627
1066
|
const params = isRecord(message.params) ? message.params : null;
|
|
@@ -631,7 +1070,7 @@ function readCodexTerminalReason(message) {
|
|
|
631
1070
|
return null;
|
|
632
1071
|
const params = isRecord(message.params) ? message.params : null;
|
|
633
1072
|
const turn = isRecord(params?.turn) ? params.turn : null;
|
|
634
|
-
if (turn?.status === 'failed')
|
|
1073
|
+
if (turn?.status === 'failed' || hasContextWindowError(turn?.error))
|
|
635
1074
|
return 'error';
|
|
636
1075
|
if (turn?.status === 'interrupted' || turn?.status === 'cancelled')
|
|
637
1076
|
return 'cancel';
|
|
@@ -661,16 +1100,33 @@ function isCodexToolEvent(method, type) {
|
|
|
661
1100
|
const normalized = type.replace(/[_-]/gu, '').toLowerCase();
|
|
662
1101
|
return ['commandexecution', 'filechange', 'mcptoolcall', 'functioncall', 'customtoolcall', 'dynamictoolcall'].includes(normalized);
|
|
663
1102
|
}
|
|
664
|
-
function
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1103
|
+
function codexTurnStartParams(input, threadId) {
|
|
1104
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
1105
|
+
return {
|
|
1106
|
+
threadId,
|
|
1107
|
+
input: [{ type: 'text', text: input.prompt }],
|
|
1108
|
+
cwd,
|
|
1109
|
+
// DSH 的 workspace-write 只约束自身工具沙箱,不会自动传递给 Codex
|
|
1110
|
+
// app-server。显式声明当前工作区,避免 Codex 将文件编辑误判为只读越权。
|
|
1111
|
+
sandboxPolicy: {
|
|
1112
|
+
type: 'workspaceWrite',
|
|
1113
|
+
writableRoots: [cwd],
|
|
1114
|
+
networkAccess: false,
|
|
1115
|
+
excludeTmpdirEnvVar: false,
|
|
1116
|
+
excludeSlashTmp: false,
|
|
1117
|
+
},
|
|
1118
|
+
approvalPolicy: 'on-request',
|
|
1119
|
+
...(input.effortId ? { effort: input.effortId } : {}),
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function permissionResponseKind(method, params) {
|
|
1123
|
+
if (method.includes('fileChange') || method.includes('file_change'))
|
|
1124
|
+
return 'file-change';
|
|
1125
|
+
// 新版 Codex 带 threadId/turnId/itemId,并要求 decision;旧版测试和旧
|
|
1126
|
+
// app-server 使用 approved 字段,按请求形状保持向后兼容。
|
|
1127
|
+
if (method.includes('commandExecution') && (params.threadId !== undefined || params.turnId !== undefined || params.itemId !== undefined))
|
|
1128
|
+
return 'command';
|
|
1129
|
+
return 'legacy';
|
|
674
1130
|
}
|
|
675
1131
|
function readId(value) {
|
|
676
1132
|
if (!isRecord(value))
|