@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.12
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.md +44 -54
- package/dist/bin/codex-remote.mjs +34 -60
- package/dist/bin/migrate-relay-db.mjs +104 -0
- package/dist/bin/self-relay-server.mjs +17 -10
- package/dist/shared/app-server-events.cjs +15 -3
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +6 -11
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +51 -8
- package/dist/src/managed-app-server.mjs +21 -10
- package/dist/src/onboarding.mjs +3 -3
- package/dist/src/platform/windows/codex-runtime.mjs +138 -4
- package/dist/src/platform/windows/desktop-host.mjs +1 -2
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
- package/dist/src/platform/windows/resident-start.js +8 -0
- package/dist/src/platform/windows/resident-startup.ps1 +6 -3
- package/dist/src/platform/windows/service-host.mjs +0 -9
- package/dist/src/self-relay/admin-page.mjs +1 -1
- package/dist/src/self-relay/admin-state.mjs +8 -6
- package/dist/src/self-relay/connector.mjs +84 -12
- package/dist/src/self-relay/email-accounts.mjs +125 -0
- package/dist/src/self-relay/lifecycle.mjs +52 -100
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +155 -38
- package/dist/src/server.mjs +736 -610
- package/dist/src/service-doctor.mjs +1 -1
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/shared-app-server.mjs +274 -272
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +2 -1
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -50
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -778
- package/dist/web/chat.js +0 -3815
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community.css +0 -82
- package/dist/web/community.html +0 -36
- package/dist/web/composer-controller.js +0 -97
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -288
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -289
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -77
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- package/dist/web/vendor/lucide.LICENSE.txt +0 -17
|
@@ -4,36 +4,37 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import events from '../shared/app-server-events.cjs';
|
|
5
5
|
import serverRequests from '../shared/server-requests.cjs';
|
|
6
6
|
|
|
7
|
-
const syncTokenTtlMs = 30_000;
|
|
8
|
-
const syncBufferMaxBytes = 8 * 1024 * 1024;
|
|
9
|
-
const historyPageMaxTurns = 8;
|
|
10
|
-
const historyPageMaxBytes = 500 * 1024;
|
|
11
|
-
const turnItemPageSize = 50;
|
|
12
|
-
const writerConflictMessages = ['already has an active writer', 'already has a live local writer'];
|
|
13
|
-
|
|
14
|
-
export class ThreadWriterConflictError extends Error {
|
|
15
|
-
constructor(threadId, message) {
|
|
16
|
-
super('该对话正在另一个 Codex 实例中使用。');
|
|
17
|
-
this.name = 'ThreadWriterConflictError';
|
|
18
|
-
this.code = 'thread_writer_owned_elsewhere';
|
|
19
|
-
this.threadId = threadId;
|
|
20
|
-
this.readOnly = true;
|
|
21
|
-
this.details = message;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function isThreadWriterConflict(error) {
|
|
26
|
-
const message = String(error?.message || error).toLowerCase();
|
|
27
|
-
return error instanceof ThreadWriterConflictError || writerConflictMessages.some(text => message.includes(text));
|
|
28
|
-
}
|
|
7
|
+
const syncTokenTtlMs = 30_000;
|
|
8
|
+
const syncBufferMaxBytes = 8 * 1024 * 1024;
|
|
9
|
+
const historyPageMaxTurns = 8;
|
|
10
|
+
const historyPageMaxBytes = 500 * 1024;
|
|
11
|
+
const turnItemPageSize = 50;
|
|
12
|
+
const writerConflictMessages = ['already has an active writer', 'already has a live local writer'];
|
|
13
|
+
|
|
14
|
+
export class ThreadWriterConflictError extends Error {
|
|
15
|
+
constructor(threadId, message) {
|
|
16
|
+
super('该对话正在另一个 Codex 实例中使用。');
|
|
17
|
+
this.name = 'ThreadWriterConflictError';
|
|
18
|
+
this.code = 'thread_writer_owned_elsewhere';
|
|
19
|
+
this.threadId = threadId;
|
|
20
|
+
this.readOnly = true;
|
|
21
|
+
this.details = message;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isThreadWriterConflict(error) {
|
|
26
|
+
const message = String(error?.message || error).toLowerCase();
|
|
27
|
+
return error instanceof ThreadWriterConflictError || writerConflictMessages.some(text => message.includes(text));
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
// Connect to the Desktop's existing execution service; never spawn or adopt a task.
|
|
31
31
|
export class SharedAppServer extends EventEmitter {
|
|
32
32
|
constructor(url) {
|
|
33
33
|
super();
|
|
34
34
|
const address = new URL(url);
|
|
35
|
-
if (address.protocol !== 'ws:' || address.hostname !== '127.0.0.1') throw new Error('Codex 内核必须使用本机回环 WebSocket。');
|
|
35
|
+
if (address.protocol !== 'ws:' || address.hostname !== '127.0.0.1') throw new Error('Codex 内核必须使用本机回环 WebSocket。');
|
|
36
36
|
this.url = url;
|
|
37
|
+
this.serverVersion = '';
|
|
37
38
|
this.epoch = randomUUID();
|
|
38
39
|
this.sequence = 0;
|
|
39
40
|
this.pending = new Map();
|
|
@@ -41,10 +42,10 @@ export class SharedAppServer extends EventEmitter {
|
|
|
41
42
|
this.threads = new Map();
|
|
42
43
|
this.contexts = new Map();
|
|
43
44
|
this.settingsNotifications = new Map();
|
|
44
|
-
this.permissionOptions = new events.PermissionOptionsCache();
|
|
45
|
-
this.subscriptions = new Map();
|
|
46
|
-
this.operationGroups = new Map();
|
|
47
|
-
this.usageCache = new events.UsageCache(value => this.emit('usage', value));
|
|
45
|
+
this.permissionOptions = new events.PermissionOptionsCache();
|
|
46
|
+
this.subscriptions = new Map();
|
|
47
|
+
this.operationGroups = new Map();
|
|
48
|
+
this.usageCache = new events.UsageCache(value => this.emit('usage', value));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
async connect() {
|
|
@@ -58,6 +59,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
58
59
|
const socket = new WebSocket(this.url, { handshakeTimeout: 5000 });
|
|
59
60
|
this.socket = socket;
|
|
60
61
|
this.ready = false;
|
|
62
|
+
this.serverVersion = '';
|
|
61
63
|
this.serverRequests.reset(randomUUID());
|
|
62
64
|
socket.on('message', bytes => {
|
|
63
65
|
let message;
|
|
@@ -68,24 +70,25 @@ export class SharedAppServer extends EventEmitter {
|
|
|
68
70
|
if (!pending) return;
|
|
69
71
|
this.pending.delete(message.id);
|
|
70
72
|
clearTimeout(pending.timer);
|
|
71
|
-
if (message.error) {
|
|
72
|
-
const text = String(message.error.message || 'app-server 请求失败');
|
|
73
|
-
pending.reject(writerConflictMessages.some(value => text.toLowerCase().includes(value))
|
|
74
|
-
? new ThreadWriterConflictError(pending.threadId, text)
|
|
75
|
-
: Object.assign(new Error(text), { rpcCode: message.error.code, details: message.error.data }));
|
|
76
|
-
}
|
|
73
|
+
if (message.error) {
|
|
74
|
+
const text = String(message.error.message || 'app-server 请求失败');
|
|
75
|
+
pending.reject(writerConflictMessages.some(value => text.toLowerCase().includes(value))
|
|
76
|
+
? new ThreadWriterConflictError(pending.threadId, text)
|
|
77
|
+
: Object.assign(new Error(text), { rpcCode: message.error.code, details: message.error.data }));
|
|
78
|
+
}
|
|
77
79
|
else pending.resolve(message.result);
|
|
78
80
|
});
|
|
79
81
|
socket.on('close', () => {
|
|
80
82
|
this.ready = false;
|
|
83
|
+
this.serverVersion = '';
|
|
81
84
|
for (const pending of this.pending.values()) {
|
|
82
85
|
clearTimeout(pending.timer);
|
|
83
|
-
pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
|
|
86
|
+
pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
|
|
84
87
|
}
|
|
85
|
-
this.pending.clear();
|
|
86
|
-
this.serverRequests.reset(randomUUID());
|
|
87
|
-
this.threads.clear();
|
|
88
|
-
this.operationGroups.clear();
|
|
88
|
+
this.pending.clear();
|
|
89
|
+
this.serverRequests.reset(randomUUID());
|
|
90
|
+
this.threads.clear();
|
|
91
|
+
this.operationGroups.clear();
|
|
89
92
|
for (const slot of this.subscriptions.values()) clearTimeout(slot.expiry);
|
|
90
93
|
this.subscriptions.clear();
|
|
91
94
|
this.contexts.clear();
|
|
@@ -97,7 +100,10 @@ export class SharedAppServer extends EventEmitter {
|
|
|
97
100
|
socket.on('error', () => {}); // open/request reports the connection error to the caller.
|
|
98
101
|
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
|
|
99
102
|
try {
|
|
100
|
-
await this.request('initialize', { clientInfo: { name: 'codex_remote_shared', version: '1.0.0' }, capabilities: { experimentalApi: true } });
|
|
103
|
+
const initialized = await this.request('initialize', { clientInfo: { name: 'codex_remote_shared', version: '1.0.0' }, capabilities: { experimentalApi: true } });
|
|
104
|
+
// The first user-agent product reports the running server build, not the
|
|
105
|
+
// selected executable that may only take effect after a future restart.
|
|
106
|
+
this.serverVersion = String(initialized?.userAgent || '').match(/^[^\s/]+\/([^\s]+)/)?.[1] || '';
|
|
101
107
|
socket.send(JSON.stringify({ method: 'initialized' }));
|
|
102
108
|
this.ready = true;
|
|
103
109
|
this.usageCache.read(() => this.request('account/rateLimits/read')).catch(error => this.emit('usage-error', error));
|
|
@@ -111,7 +117,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
111
117
|
this.pending.delete(id);
|
|
112
118
|
reject(new Error(`${method} 超时;不会自动重发,请先检查对话。`));
|
|
113
119
|
}, 30000);
|
|
114
|
-
this.pending.set(id, { resolve, reject, timer, threadId: params?.threadId });
|
|
120
|
+
this.pending.set(id, { resolve, reject, timer, threadId: params?.threadId });
|
|
115
121
|
this.socket.send(JSON.stringify({ id, method, params }), error => {
|
|
116
122
|
if (!error || !this.pending.delete(id)) return;
|
|
117
123
|
clearTimeout(timer); reject(error);
|
|
@@ -124,26 +130,26 @@ export class SharedAppServer extends EventEmitter {
|
|
|
124
130
|
return events.readProjects((method, params) => this.request(method, params));
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
async send(args) {
|
|
128
|
-
await this.connect();
|
|
129
|
-
this.requireWritable(args.threadId);
|
|
133
|
+
async send(args) {
|
|
134
|
+
await this.connect();
|
|
135
|
+
this.requireWritable(args.threadId);
|
|
130
136
|
// Reading the live thread checks this service owns it without resume changing settings.
|
|
131
|
-
const { thread } = await this.request('thread/read', { threadId: args.threadId, includeTurns: false });
|
|
132
|
-
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
133
|
-
if (status === 'active') {
|
|
134
|
-
const page = await this.request('thread/turns/list', {
|
|
135
|
-
threadId: args.threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
136
|
-
});
|
|
137
|
-
const active = page.data?.[0];
|
|
138
|
-
if (active?.status !== 'inProgress') throw new Error('活动轮次已变化,请刷新后再发送。');
|
|
137
|
+
const { thread } = await this.request('thread/read', { threadId: args.threadId, includeTurns: false });
|
|
138
|
+
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
139
|
+
if (status === 'active') {
|
|
140
|
+
const page = await this.request('thread/turns/list', {
|
|
141
|
+
threadId: args.threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
142
|
+
});
|
|
143
|
+
const active = page.data?.[0];
|
|
144
|
+
if (active?.status !== 'inProgress') throw new Error('活动轮次已变化,请刷新后再发送。');
|
|
139
145
|
return this.request('turn/steer', { threadId: args.threadId, expectedTurnId: active.id, input: args.input, ...(args.clientUserMessageId ? { clientUserMessageId: args.clientUserMessageId } : {}) });
|
|
140
146
|
}
|
|
141
147
|
return this.request('turn/start', args);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
respondApproval(threadId, body) {
|
|
145
|
-
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('Codex 内核未就绪');
|
|
146
|
-
this.requireWritable(threadId);
|
|
150
|
+
respondApproval(threadId, body) {
|
|
151
|
+
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('Codex 内核未就绪');
|
|
152
|
+
this.requireWritable(threadId);
|
|
147
153
|
return this.serverRequests.respond(threadId, body, message => new Promise((resolve, reject) => {
|
|
148
154
|
this.socket.send(JSON.stringify(message), error => error ? reject(error) : resolve());
|
|
149
155
|
}));
|
|
@@ -166,20 +172,20 @@ export class SharedAppServer extends EventEmitter {
|
|
|
166
172
|
if (method === 'thread/tokenUsage/updated') {
|
|
167
173
|
this.contexts.set(id, { ...this.contexts.get(id), tokenUsage: params.tokenUsage });
|
|
168
174
|
return;
|
|
169
|
-
}
|
|
170
|
-
const record = this.threads.get(id);
|
|
171
|
-
if (!record) return;
|
|
172
|
-
const current = params.turn?.id ? record.thread.turns?.find(turn => turn.id === params.turn.id) : null;
|
|
173
|
-
const groupedHistory = current?.items?.some(item => item?.type === 'lazyOperationGroup');
|
|
174
|
-
const appliedParams = groupedHistory && method === 'turn/completed' && Array.isArray(params.turn?.items)
|
|
175
|
-
? { ...params, turn: { ...params.turn,
|
|
176
|
-
items: params.turn.items.filter(item => ['userMessage', 'agentMessage'].includes(item?.type)) } }
|
|
177
|
-
: params;
|
|
178
|
-
const turn = events.applyNotification(record.thread, method, appliedParams);
|
|
175
|
+
}
|
|
176
|
+
const record = this.threads.get(id);
|
|
177
|
+
if (!record) return;
|
|
178
|
+
const current = params.turn?.id ? record.thread.turns?.find(turn => turn.id === params.turn.id) : null;
|
|
179
|
+
const groupedHistory = current?.items?.some(item => item?.type === 'lazyOperationGroup');
|
|
180
|
+
const appliedParams = groupedHistory && method === 'turn/completed' && Array.isArray(params.turn?.items)
|
|
181
|
+
? { ...params, turn: { ...params.turn,
|
|
182
|
+
items: params.turn.items.filter(item => ['userMessage', 'agentMessage'].includes(item?.type)) } }
|
|
183
|
+
: params;
|
|
184
|
+
const turn = events.applyNotification(record.thread, method, appliedParams);
|
|
179
185
|
if (turn) {
|
|
180
186
|
const sequence = ++record.sequence;
|
|
181
187
|
record.changes.set(turn.id, { sequence, turn });
|
|
182
|
-
record.changeBytes += Buffer.byteLength(JSON.stringify({ method, params: appliedParams }));
|
|
188
|
+
record.changeBytes += Buffer.byteLength(JSON.stringify({ method, params: appliedParams }));
|
|
183
189
|
if (record.changeBytes > syncBufferMaxBytes) {
|
|
184
190
|
record.recoverAfter = sequence;
|
|
185
191
|
record.changes.clear(); record.changeBytes = 0;
|
|
@@ -190,49 +196,49 @@ export class SharedAppServer extends EventEmitter {
|
|
|
190
196
|
if (method === 'turn/completed') record.thread.turns = record.thread.turns.slice(-10);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
async timeline(threadId, pageOptions = {}) {
|
|
199
|
+
async timeline(threadId, pageOptions = {}) {
|
|
194
200
|
await this.connect();
|
|
195
201
|
const slot = this.slot(threadId);
|
|
196
202
|
const task = slot.tail.then(async () => {
|
|
197
|
-
let record = this.threads.get(threadId);
|
|
198
|
-
if (record?.readOnly) {
|
|
199
|
-
this.threads.delete(threadId);
|
|
200
|
-
this.contexts.delete(threadId);
|
|
201
|
-
record = null;
|
|
202
|
-
}
|
|
203
|
-
if (!record) {
|
|
204
|
-
let result;
|
|
205
|
-
try {
|
|
206
|
-
result = await this.request('thread/resume', { threadId, excludeTurns: true,
|
|
207
|
-
initialTurnsPage: { limit: 1, sortDirection: 'desc', itemsView: 'summary' } });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
if (!isThreadWriterConflict(error)) throw error;
|
|
210
|
-
const [read, page] = await Promise.all([
|
|
211
|
-
this.request('thread/read', { threadId, includeTurns: false }),
|
|
212
|
-
this.request('thread/turns/list', {
|
|
213
|
-
threadId, limit: historyPageMaxTurns, sortDirection: 'desc', itemsView: 'summary',
|
|
214
|
-
}),
|
|
215
|
-
]);
|
|
216
|
-
const compact = await Promise.all([...(page.data || [])].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
217
|
-
const turns = this.fitTurns({ thread: read.thread }, compact, pageOptions);
|
|
218
|
-
const thread = { ...read.thread, turns, readOnly: true,
|
|
219
|
-
readOnlyReason: '该对话正在另一个 Codex 实例中使用。' };
|
|
220
|
-
record = { thread, nextCursor: page.nextCursor ?? null, sequence: 0, recoverAfter: 0,
|
|
221
|
-
changes: new Map(), changeBytes: 0, tokens: new Map(), readOnly: true };
|
|
222
|
-
this.threads.set(threadId, record);
|
|
223
|
-
result = null;
|
|
224
|
-
}
|
|
225
|
-
if (result) {
|
|
226
|
-
if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('Codex 内核未返回分页历史。');
|
|
227
|
-
const compact = await Promise.all([...result.initialTurnsPage.data].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
228
|
-
const thread = { ...result.thread, turns: compact };
|
|
229
|
-
record = { thread, nextCursor: result.initialTurnsPage.nextCursor ?? null, sequence: 0,
|
|
230
|
-
recoverAfter: 0, changes: new Map(), changeBytes: 0, tokens: new Map() };
|
|
231
|
-
this.threads.set(threadId, record);
|
|
232
|
-
this.contexts.set(threadId, { ...this.contexts.get(threadId), model: result.model, thinking: result.reasoningEffort, serviceTier: result.serviceTier ?? null,
|
|
233
|
-
cwd: result.cwd, ...events.permissionState(result) });
|
|
234
|
-
await this.extendHistoryRecord(threadId, record, pageOptions);
|
|
235
|
-
}
|
|
203
|
+
let record = this.threads.get(threadId);
|
|
204
|
+
if (record?.readOnly) {
|
|
205
|
+
this.threads.delete(threadId);
|
|
206
|
+
this.contexts.delete(threadId);
|
|
207
|
+
record = null;
|
|
208
|
+
}
|
|
209
|
+
if (!record) {
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await this.request('thread/resume', { threadId, excludeTurns: true,
|
|
213
|
+
initialTurnsPage: { limit: 1, sortDirection: 'desc', itemsView: 'summary' } });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!isThreadWriterConflict(error)) throw error;
|
|
216
|
+
const [read, page] = await Promise.all([
|
|
217
|
+
this.request('thread/read', { threadId, includeTurns: false }),
|
|
218
|
+
this.request('thread/turns/list', {
|
|
219
|
+
threadId, limit: historyPageMaxTurns, sortDirection: 'desc', itemsView: 'summary',
|
|
220
|
+
}),
|
|
221
|
+
]);
|
|
222
|
+
const compact = await Promise.all([...(page.data || [])].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
223
|
+
const turns = this.fitTurns({ thread: read.thread }, compact, pageOptions);
|
|
224
|
+
const thread = { ...read.thread, turns, readOnly: true,
|
|
225
|
+
readOnlyReason: '该对话正在另一个 Codex 实例中使用。' };
|
|
226
|
+
record = { thread, nextCursor: page.nextCursor ?? null, sequence: 0, recoverAfter: 0,
|
|
227
|
+
changes: new Map(), changeBytes: 0, tokens: new Map(), readOnly: true };
|
|
228
|
+
this.threads.set(threadId, record);
|
|
229
|
+
result = null;
|
|
230
|
+
}
|
|
231
|
+
if (result) {
|
|
232
|
+
if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('Codex 内核未返回分页历史。');
|
|
233
|
+
const compact = await Promise.all([...result.initialTurnsPage.data].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
234
|
+
const thread = { ...result.thread, turns: compact };
|
|
235
|
+
record = { thread, nextCursor: result.initialTurnsPage.nextCursor ?? null, sequence: 0,
|
|
236
|
+
recoverAfter: 0, changes: new Map(), changeBytes: 0, tokens: new Map() };
|
|
237
|
+
this.threads.set(threadId, record);
|
|
238
|
+
this.contexts.set(threadId, { ...this.contexts.get(threadId), model: result.model, thinking: result.reasoningEffort, serviceTier: result.serviceTier ?? null,
|
|
239
|
+
cwd: result.cwd, ...events.permissionState(result) });
|
|
240
|
+
await this.extendHistoryRecord(threadId, record, pageOptions);
|
|
241
|
+
}
|
|
236
242
|
}
|
|
237
243
|
const id = randomUUID(), expires = Date.now() + syncTokenTtlMs;
|
|
238
244
|
record.tokens.set(id, { sequence: record.sequence, expires });
|
|
@@ -271,11 +277,11 @@ export class SharedAppServer extends EventEmitter {
|
|
|
271
277
|
if (record?.tokens.size) { this.scheduleCleanup(threadId, slot); return; }
|
|
272
278
|
slot.tail = slot.tail.then(async () => {
|
|
273
279
|
if (slot.users || this.threads.get(threadId)?.tokens.size) return;
|
|
274
|
-
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
275
|
-
this.operationGroups.delete(threadId);
|
|
280
|
+
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
281
|
+
this.operationGroups.delete(threadId);
|
|
276
282
|
this.subscriptions.delete(threadId); clearTimeout(slot.expiry);
|
|
277
|
-
if (this.ready && !record?.readOnly) await this.request('thread/unsubscribe', { threadId });
|
|
278
|
-
this.serverRequests.forget(threadId);
|
|
283
|
+
if (this.ready && !record?.readOnly) await this.request('thread/unsubscribe', { threadId });
|
|
284
|
+
this.serverRequests.forget(threadId);
|
|
279
285
|
}).catch(error => this.emit('subscription-error', error));
|
|
280
286
|
}
|
|
281
287
|
|
|
@@ -313,140 +319,145 @@ export class SharedAppServer extends EventEmitter {
|
|
|
313
319
|
snapshot, catchup, release };
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
pageSettings(options = {}) {
|
|
317
|
-
return {
|
|
318
|
-
maxTurns: Math.max(1, Math.min(historyPageMaxTurns, Number(options.maxTurns) || historyPageMaxTurns)),
|
|
319
|
-
maxBytes: Math.max(1, Number(options.maxBytes) || historyPageMaxBytes),
|
|
320
|
-
measure: typeof options.measure === 'function' ? options.measure : value => Buffer.byteLength(JSON.stringify(value)),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async compactTurn(threadId, turn) {
|
|
325
|
-
const compact = events.compactTurn(turn);
|
|
326
|
-
if (!turn?.id) return compact;
|
|
327
|
-
const batches = [];
|
|
328
|
-
let cursor;
|
|
329
|
-
do {
|
|
330
|
-
const page = await this.request('thread/items/list', {
|
|
331
|
-
threadId, turnId: turn.id, ...(cursor ? { cursor } : {}),
|
|
332
|
-
limit: 100, sortDirection: 'desc',
|
|
333
|
-
});
|
|
334
|
-
if (!Array.isArray(page.data)) throw new Error('Codex 内核未返回单轮条目索引。');
|
|
335
|
-
batches.push(page.data.map(entry => events.compactItem(entry.item)));
|
|
336
|
-
cursor = page.nextCursor ?? null;
|
|
337
|
-
} while (cursor);
|
|
338
|
-
const items = batches.reverse().flatMap(batch => batch.reverse());
|
|
339
|
-
const groups = new Map(), skeleton = [];
|
|
340
|
-
let operationItems = [];
|
|
341
|
-
const flushOperations = () => {
|
|
342
|
-
if (!operationItems.length) return;
|
|
343
|
-
const visibleItems = operationItems.filter(item => item?.type !== 'reasoning');
|
|
344
|
-
operationItems = [];
|
|
345
|
-
if (!visibleItems.length) return;
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
322
|
+
pageSettings(options = {}) {
|
|
323
|
+
return {
|
|
324
|
+
maxTurns: Math.max(1, Math.min(historyPageMaxTurns, Number(options.maxTurns) || historyPageMaxTurns)),
|
|
325
|
+
maxBytes: Math.max(1, Number(options.maxBytes) || historyPageMaxBytes),
|
|
326
|
+
measure: typeof options.measure === 'function' ? options.measure : value => Buffer.byteLength(JSON.stringify(value)),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async compactTurn(threadId, turn) {
|
|
331
|
+
const compact = events.compactTurn(turn);
|
|
332
|
+
if (!turn?.id) return compact;
|
|
333
|
+
const batches = [];
|
|
334
|
+
let cursor;
|
|
335
|
+
do {
|
|
336
|
+
const page = await this.request('thread/items/list', {
|
|
337
|
+
threadId, turnId: turn.id, ...(cursor ? { cursor } : {}),
|
|
338
|
+
limit: 100, sortDirection: 'desc',
|
|
339
|
+
});
|
|
340
|
+
if (!Array.isArray(page.data)) throw new Error('Codex 内核未返回单轮条目索引。');
|
|
341
|
+
batches.push(page.data.map(entry => events.compactItem(entry.item)));
|
|
342
|
+
cursor = page.nextCursor ?? null;
|
|
343
|
+
} while (cursor);
|
|
344
|
+
const items = batches.reverse().flatMap(batch => batch.reverse());
|
|
345
|
+
const groups = new Map(), skeleton = [];
|
|
346
|
+
let operationItems = [];
|
|
347
|
+
const flushOperations = () => {
|
|
348
|
+
if (!operationItems.length) return;
|
|
349
|
+
const visibleItems = operationItems.filter(item => item?.type !== 'reasoning');
|
|
350
|
+
operationItems = [];
|
|
351
|
+
if (!visibleItems.length) return;
|
|
352
|
+
const subAgentItems = visibleItems.filter(isSubAgentItem);
|
|
353
|
+
const detailItems = visibleItems.filter(item => !isSubAgentItem(item));
|
|
354
|
+
if (detailItems.length) {
|
|
355
|
+
const groupId = `g${groups.size + 1}`;
|
|
356
|
+
groups.set(groupId, detailItems);
|
|
357
|
+
skeleton.push({ type: 'lazyOperationGroup', id: `${turn.id}:${groupId}`,
|
|
358
|
+
turnId: turn.id, groupId,
|
|
359
|
+
operationTypes: [...new Set(detailItems.map(item => item.type).filter(Boolean))] });
|
|
360
|
+
}
|
|
361
|
+
skeleton.push(...subAgentItems);
|
|
362
|
+
};
|
|
363
|
+
for (const item of items) {
|
|
364
|
+
if (['userMessage', 'agentMessage', 'imageGeneration', 'imageView'].includes(item?.type)) {
|
|
365
|
+
flushOperations();
|
|
366
|
+
skeleton.push(item);
|
|
367
|
+
} else operationItems.push(item);
|
|
368
|
+
}
|
|
369
|
+
flushOperations();
|
|
370
|
+
let threadGroups = this.operationGroups.get(threadId);
|
|
371
|
+
if (!threadGroups) { threadGroups = new Map(); this.operationGroups.set(threadId, threadGroups); }
|
|
372
|
+
threadGroups.set(turn.id, groups);
|
|
373
|
+
return { ...compact, items: skeleton };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async operationGroupItems(threadId, turnId, groupId, offset) {
|
|
377
|
+
await this.connect();
|
|
378
|
+
const items = this.operationGroups.get(threadId)?.get(turnId)?.get(groupId);
|
|
379
|
+
if (!items) throw new Error('操作组已失效,请重新打开对话。');
|
|
380
|
+
const start = Math.max(0, Number(offset) || 0);
|
|
381
|
+
const pageItems = items.slice(start, start + turnItemPageSize);
|
|
382
|
+
const nextOffset = start + pageItems.length < items.length ? start + pageItems.length : null;
|
|
383
|
+
return { turnId, groupId, items: pageItems, nextOffset, total: items.length };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async turnContext(threadId, turnId) {
|
|
387
|
+
await this.connect();
|
|
388
|
+
if (!threadId || !turnId) throw new Error('缺少图片所属的对话或轮次。');
|
|
389
|
+
return this.compactTurn(threadId, { id: turnId });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fitTurns(base, turns, options = {}) {
|
|
393
|
+
const settings = this.pageSettings(options), accepted = [];
|
|
394
|
+
let firstOversized = false;
|
|
395
|
+
for (let index = turns.length - 1; index >= 0 && accepted.length < settings.maxTurns; index--) {
|
|
396
|
+
const candidate = [turns[index], ...accepted];
|
|
397
|
+
const candidateBytes = settings.measure({ ...base, thread: { ...base.thread, turns: candidate } });
|
|
398
|
+
if (accepted.length >= 2 && candidateBytes > settings.maxBytes) break;
|
|
399
|
+
accepted.unshift(turns[index]);
|
|
400
|
+
if (accepted.length === 1) firstOversized = candidateBytes > settings.maxBytes;
|
|
401
|
+
if (firstOversized || (accepted.length >= 2 && candidateBytes > settings.maxBytes)) break;
|
|
402
|
+
}
|
|
403
|
+
return accepted;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async extendHistoryRecord(threadId, record, options = {}) {
|
|
407
|
+
const settings = this.pageSettings(options);
|
|
408
|
+
if (!record.nextCursor || !record.thread.turns.length) return;
|
|
409
|
+
const firstOversized = settings.measure({ thread: record.thread, nextCursor: record.nextCursor }) > settings.maxBytes;
|
|
410
|
+
if (firstOversized) return;
|
|
411
|
+
while (record.nextCursor && record.thread.turns.length < settings.maxTurns) {
|
|
412
|
+
const cursor = record.nextCursor;
|
|
413
|
+
const page = await this.request('thread/turns/list', {
|
|
414
|
+
threadId, cursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
415
|
+
});
|
|
416
|
+
if (!Array.isArray(page.data) || !page.data.length) {
|
|
417
|
+
record.nextCursor = page.nextCursor ?? null;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
const candidate = [await this.compactTurn(threadId, page.data[0]), ...record.thread.turns];
|
|
421
|
+
const value = { thread: { ...record.thread, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
422
|
+
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
423
|
+
record.thread.turns = candidate;
|
|
424
|
+
record.nextCursor = page.nextCursor ?? null;
|
|
425
|
+
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async history(threadId, cursor, options = {}) {
|
|
430
|
+
await this.connect();
|
|
431
|
+
if (!cursor) throw new Error('缺少历史分页游标。');
|
|
432
|
+
const settings = this.pageSettings(options), turns = [];
|
|
433
|
+
let firstOversized = false;
|
|
434
|
+
let nextCursor = cursor;
|
|
435
|
+
while (nextCursor && turns.length < settings.maxTurns) {
|
|
436
|
+
const pageCursor = nextCursor;
|
|
437
|
+
const page = await this.request('thread/turns/list', {
|
|
438
|
+
threadId, cursor: pageCursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
439
|
+
});
|
|
440
|
+
if (!Array.isArray(page.data) || !page.data.length) {
|
|
441
|
+
nextCursor = page.nextCursor ?? null;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const compact = await this.compactTurn(threadId, page.data[0]);
|
|
445
|
+
const candidate = [compact, ...turns];
|
|
446
|
+
const value = { thread: { id: threadId, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
447
|
+
const candidateBytes = settings.measure(value);
|
|
448
|
+
if (turns.length >= 2 && candidateBytes > settings.maxBytes) break;
|
|
449
|
+
turns.unshift(compact);
|
|
450
|
+
nextCursor = page.nextCursor ?? null;
|
|
451
|
+
if (turns.length === 1) firstOversized = candidateBytes > settings.maxBytes;
|
|
452
|
+
if (firstOversized || (turns.length >= 2 && candidateBytes > settings.maxBytes)) break;
|
|
453
|
+
}
|
|
454
|
+
return { thread: { id: threadId, turns }, nextCursor };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async interrupt(threadId, turnId) {
|
|
458
|
+
await this.connect();
|
|
459
|
+
this.requireWritable(threadId);
|
|
460
|
+
return this.request('turn/interrupt', { threadId, turnId });
|
|
450
461
|
}
|
|
451
462
|
|
|
452
463
|
async models() {
|
|
@@ -465,16 +476,16 @@ export class SharedAppServer extends EventEmitter {
|
|
|
465
476
|
return this.usageCache.read(() => this.request('account/rateLimits/read'));
|
|
466
477
|
}
|
|
467
478
|
|
|
468
|
-
async settings(threadId, model, effort, serviceTier) {
|
|
469
|
-
await this.connect();
|
|
470
|
-
this.requireWritable(threadId);
|
|
479
|
+
async settings(threadId, model, effort, serviceTier) {
|
|
480
|
+
await this.connect();
|
|
481
|
+
this.requireWritable(threadId);
|
|
471
482
|
await events.updateSettings((method, params) => this.request(method, params), this.contexts, this.settingsNotifications, threadId, model, effort, serviceTier);
|
|
472
483
|
return this.context(threadId);
|
|
473
484
|
}
|
|
474
485
|
|
|
475
|
-
async permissions(threadId, mode) {
|
|
476
|
-
await this.connect();
|
|
477
|
-
this.requireWritable(threadId);
|
|
486
|
+
async permissions(threadId, mode) {
|
|
487
|
+
await this.connect();
|
|
488
|
+
this.requireWritable(threadId);
|
|
478
489
|
await events.updatePermissions((method, params) => this.request(method, params), this.contexts, this.settingsNotifications, this.permissionOptions, threadId, mode);
|
|
479
490
|
return this.context(threadId);
|
|
480
491
|
}
|
|
@@ -485,30 +496,21 @@ export class SharedAppServer extends EventEmitter {
|
|
|
485
496
|
return this.context(threadId);
|
|
486
497
|
}
|
|
487
498
|
|
|
488
|
-
context(threadId) {
|
|
499
|
+
context(threadId) {
|
|
489
500
|
const context = this.permissionOptions.context(threadId, this.contexts.get(threadId) || {}), usage = context.tokenUsage;
|
|
490
501
|
const usedTokens = usage?.last?.totalTokens, contextWindow = usage?.modelContextWindow;
|
|
491
502
|
return { ...context, available: Boolean(contextWindow), usedTokens, contextWindow,
|
|
492
503
|
usedPercent: contextWindow ? 100 * usedTokens / contextWindow : 0 };
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
requireWritable(threadId) {
|
|
496
|
-
const record = this.threads.get(threadId);
|
|
497
|
-
if (record?.readOnly) throw new ThreadWriterConflictError(threadId, record.thread.readOnlyReason);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
close() { this.socket?.close(); }
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (types.has('fileChange')) parts.push('编辑了文件');
|
|
507
|
-
if (types.has('commandExecution')) parts.push('运行了命令');
|
|
508
|
-
if (types.has('mcpToolCall') || types.has('dynamicToolCall')) parts.push('调用了工具');
|
|
509
|
-
if (types.has('webSearch')) parts.push('搜索了网页');
|
|
510
|
-
if (types.has('imageView')) parts.push('查看了图像');
|
|
511
|
-
if (types.has('collabAgentToolCall') || types.has('subAgentActivity')) parts.push('处理了子任务');
|
|
512
|
-
const available = hasMore || items.some(item => item && !['userMessage', 'agentMessage'].includes(item.type));
|
|
513
|
-
return { available, title: parts.length ? `${parts.join(' · ')}${hasMore ? '等' : ''}` : '执行过程' };
|
|
514
|
-
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
requireWritable(threadId) {
|
|
507
|
+
const record = this.threads.get(threadId);
|
|
508
|
+
if (record?.readOnly) throw new ThreadWriterConflictError(threadId, record.thread.readOnlyReason);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
close() { this.socket?.close(); }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isSubAgentItem(item) {
|
|
515
|
+
return item?.type === 'collabAgentToolCall' || item?.type === 'subAgentActivity';
|
|
516
|
+
}
|