@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.21
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 +55 -71
- package/dist/bin/codex-remote.mjs +150 -126
- package/dist/bin/manage-relay-admin.mjs +20 -0
- package/dist/bin/migrate-relay-db.mjs +107 -0
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +2 -2
- package/dist/bin/self-relay-server.mjs +18 -10
- package/dist/shared/app-server-events.cjs +42 -15
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +7 -6
- package/dist/src/desktop-launcher.mjs +16 -21
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +52 -9
- package/dist/src/managed-app-server.mjs +44 -13
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +9 -11
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/background-process.mjs +1 -1
- package/dist/src/platform/windows/codex-runtime.mjs +251 -0
- package/dist/src/platform/windows/desktop-host.mjs +17 -3
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-start.js +20 -0
- package/dist/src/platform/windows/resident-startup.ps1 +76 -20
- package/dist/src/platform/windows/service-host.mjs +4 -16
- package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/platform/windows/user-environment.mjs +1 -1
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +79 -0
- package/dist/src/self-relay/admin-server.mjs +66 -0
- package/dist/src/self-relay/admin-state.mjs +82 -19
- package/dist/src/self-relay/admin-style.mjs +9 -0
- package/dist/src/self-relay/admin-usage-page.mjs +28 -0
- package/dist/src/self-relay/connector.mjs +243 -30
- package/dist/src/self-relay/demo.mjs +3 -4
- package/dist/src/self-relay/email-accounts.mjs +141 -0
- package/dist/src/self-relay/lifecycle.mjs +99 -142
- 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 +335 -59
- package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
- package/dist/src/self-relay/usage-meter.mjs +125 -0
- package/dist/src/server.mjs +731 -1439
- package/dist/src/service-control.mjs +4 -4
- package/dist/src/service-doctor.mjs +27 -29
- package/dist/src/service-health.mjs +2 -2
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/service-lifecycle.mjs +19 -19
- package/dist/src/shared-app-server.mjs +277 -270
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +11 -9
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +17 -18
- package/tools/postinstall.mjs +69 -15
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -358
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -20
- package/dist/src/thread-catalog.mjs +0 -47
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -57
- package/dist/web/chat-transport.js +0 -202
- package/dist/web/chat.css +0 -782
- package/dist/web/chat.js +0 -3830
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community-view.js +0 -20
- package/dist/web/community.css +0 -77
- package/dist/web/community.html +0 -37
- package/dist/web/composer-controller.js +0 -101
- 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 -325
- 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 -296
- 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 -81
- 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('
|
|
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('
|
|
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,24 +130,29 @@ 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
137
|
const { thread } = await this.request('thread/read', { threadId: args.threadId, includeTurns: false });
|
|
132
138
|
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
133
|
-
if (status === 'active') {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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('活动轮次已变化,请刷新后再发送。');
|
|
145
|
+
const { turnTrigger, ...source } = events.mobileTurnSource(args.clientUserMessageId);
|
|
146
|
+
const result = await this.request('turn/steer', { threadId: args.threadId, expectedTurnId: active.id, input: args.input, ...source, ...(args.clientUserMessageId ? { clientUserMessageId: args.clientUserMessageId } : {}) });
|
|
147
|
+
return { result, usage: { kind: 'steer', turnId: active.id } };
|
|
148
|
+
}
|
|
149
|
+
const result = await this.request('turn/start', args);
|
|
150
|
+
return { result, usage: { kind: 'start', turnId: result?.turn?.id || null } };
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
respondApproval(threadId, body) {
|
|
143
|
-
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('
|
|
144
|
-
this.requireWritable(threadId);
|
|
153
|
+
respondApproval(threadId, body) {
|
|
154
|
+
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('Codex 内核未就绪');
|
|
155
|
+
this.requireWritable(threadId);
|
|
145
156
|
return this.serverRequests.respond(threadId, body, message => new Promise((resolve, reject) => {
|
|
146
157
|
this.socket.send(JSON.stringify(message), error => error ? reject(error) : resolve());
|
|
147
158
|
}));
|
|
@@ -164,20 +175,20 @@ export class SharedAppServer extends EventEmitter {
|
|
|
164
175
|
if (method === 'thread/tokenUsage/updated') {
|
|
165
176
|
this.contexts.set(id, { ...this.contexts.get(id), tokenUsage: params.tokenUsage });
|
|
166
177
|
return;
|
|
167
|
-
}
|
|
168
|
-
const record = this.threads.get(id);
|
|
169
|
-
if (!record) return;
|
|
170
|
-
const current = params.turn?.id ? record.thread.turns?.find(turn => turn.id === params.turn.id) : null;
|
|
171
|
-
const groupedHistory = current?.items?.some(item => item?.type === 'lazyOperationGroup');
|
|
172
|
-
const appliedParams = groupedHistory && method === 'turn/completed' && Array.isArray(params.turn?.items)
|
|
173
|
-
? { ...params, turn: { ...params.turn,
|
|
174
|
-
items: params.turn.items.filter(item => ['userMessage', 'agentMessage'].includes(item?.type)) } }
|
|
175
|
-
: params;
|
|
176
|
-
const turn = events.applyNotification(record.thread, method, appliedParams);
|
|
178
|
+
}
|
|
179
|
+
const record = this.threads.get(id);
|
|
180
|
+
if (!record) return;
|
|
181
|
+
const current = params.turn?.id ? record.thread.turns?.find(turn => turn.id === params.turn.id) : null;
|
|
182
|
+
const groupedHistory = current?.items?.some(item => item?.type === 'lazyOperationGroup');
|
|
183
|
+
const appliedParams = groupedHistory && method === 'turn/completed' && Array.isArray(params.turn?.items)
|
|
184
|
+
? { ...params, turn: { ...params.turn,
|
|
185
|
+
items: params.turn.items.filter(item => ['userMessage', 'agentMessage'].includes(item?.type)) } }
|
|
186
|
+
: params;
|
|
187
|
+
const turn = events.applyNotification(record.thread, method, appliedParams);
|
|
177
188
|
if (turn) {
|
|
178
189
|
const sequence = ++record.sequence;
|
|
179
190
|
record.changes.set(turn.id, { sequence, turn });
|
|
180
|
-
record.changeBytes += Buffer.byteLength(JSON.stringify({ method, params: appliedParams }));
|
|
191
|
+
record.changeBytes += Buffer.byteLength(JSON.stringify({ method, params: appliedParams }));
|
|
181
192
|
if (record.changeBytes > syncBufferMaxBytes) {
|
|
182
193
|
record.recoverAfter = sequence;
|
|
183
194
|
record.changes.clear(); record.changeBytes = 0;
|
|
@@ -188,49 +199,49 @@ export class SharedAppServer extends EventEmitter {
|
|
|
188
199
|
if (method === 'turn/completed') record.thread.turns = record.thread.turns.slice(-10);
|
|
189
200
|
}
|
|
190
201
|
|
|
191
|
-
async timeline(threadId, pageOptions = {}) {
|
|
202
|
+
async timeline(threadId, pageOptions = {}) {
|
|
192
203
|
await this.connect();
|
|
193
204
|
const slot = this.slot(threadId);
|
|
194
205
|
const task = slot.tail.then(async () => {
|
|
195
|
-
let record = this.threads.get(threadId);
|
|
196
|
-
if (record?.readOnly) {
|
|
197
|
-
this.threads.delete(threadId);
|
|
198
|
-
this.contexts.delete(threadId);
|
|
199
|
-
record = null;
|
|
200
|
-
}
|
|
201
|
-
if (!record) {
|
|
202
|
-
let result;
|
|
203
|
-
try {
|
|
204
|
-
result = await this.request('thread/resume', { threadId, excludeTurns: true,
|
|
205
|
-
initialTurnsPage: { limit: 1, sortDirection: 'desc', itemsView: 'summary' } });
|
|
206
|
-
} catch (error) {
|
|
207
|
-
if (!isThreadWriterConflict(error)) throw error;
|
|
208
|
-
const [read, page] = await Promise.all([
|
|
209
|
-
this.request('thread/read', { threadId, includeTurns: false }),
|
|
210
|
-
this.request('thread/turns/list', {
|
|
211
|
-
threadId, limit: historyPageMaxTurns, sortDirection: 'desc', itemsView: 'summary',
|
|
212
|
-
}),
|
|
213
|
-
]);
|
|
214
|
-
const compact = await Promise.all([...(page.data || [])].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
215
|
-
const turns = this.fitTurns({ thread: read.thread }, compact, pageOptions);
|
|
216
|
-
const thread = { ...read.thread, turns, readOnly: true,
|
|
217
|
-
readOnlyReason: '该对话正在另一个 Codex 实例中使用。' };
|
|
218
|
-
record = { thread, nextCursor: page.nextCursor ?? null, sequence: 0, recoverAfter: 0,
|
|
219
|
-
changes: new Map(), changeBytes: 0, tokens: new Map(), readOnly: true };
|
|
220
|
-
this.threads.set(threadId, record);
|
|
221
|
-
result = null;
|
|
222
|
-
}
|
|
223
|
-
if (result) {
|
|
224
|
-
if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('
|
|
225
|
-
const compact = await Promise.all([...result.initialTurnsPage.data].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
226
|
-
const thread = { ...result.thread, turns: compact };
|
|
227
|
-
record = { thread, nextCursor: result.initialTurnsPage.nextCursor ?? null, sequence: 0,
|
|
228
|
-
recoverAfter: 0, changes: new Map(), changeBytes: 0, tokens: new Map() };
|
|
229
|
-
this.threads.set(threadId, record);
|
|
230
|
-
this.contexts.set(threadId, { ...this.contexts.get(threadId), model: result.model, thinking: result.reasoningEffort, serviceTier: result.serviceTier ?? null,
|
|
231
|
-
cwd: result.cwd, ...events.permissionState(result) });
|
|
232
|
-
await this.extendHistoryRecord(threadId, record, pageOptions);
|
|
233
|
-
}
|
|
206
|
+
let record = this.threads.get(threadId);
|
|
207
|
+
if (record?.readOnly) {
|
|
208
|
+
this.threads.delete(threadId);
|
|
209
|
+
this.contexts.delete(threadId);
|
|
210
|
+
record = null;
|
|
211
|
+
}
|
|
212
|
+
if (!record) {
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await this.request('thread/resume', { threadId, excludeTurns: true,
|
|
216
|
+
initialTurnsPage: { limit: 1, sortDirection: 'desc', itemsView: 'summary' } });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (!isThreadWriterConflict(error)) throw error;
|
|
219
|
+
const [read, page] = await Promise.all([
|
|
220
|
+
this.request('thread/read', { threadId, includeTurns: false }),
|
|
221
|
+
this.request('thread/turns/list', {
|
|
222
|
+
threadId, limit: historyPageMaxTurns, sortDirection: 'desc', itemsView: 'summary',
|
|
223
|
+
}),
|
|
224
|
+
]);
|
|
225
|
+
const compact = await Promise.all([...(page.data || [])].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
226
|
+
const turns = this.fitTurns({ thread: read.thread }, compact, pageOptions);
|
|
227
|
+
const thread = { ...read.thread, turns, readOnly: true,
|
|
228
|
+
readOnlyReason: '该对话正在另一个 Codex 实例中使用。' };
|
|
229
|
+
record = { thread, nextCursor: page.nextCursor ?? null, sequence: 0, recoverAfter: 0,
|
|
230
|
+
changes: new Map(), changeBytes: 0, tokens: new Map(), readOnly: true };
|
|
231
|
+
this.threads.set(threadId, record);
|
|
232
|
+
result = null;
|
|
233
|
+
}
|
|
234
|
+
if (result) {
|
|
235
|
+
if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('Codex 内核未返回分页历史。');
|
|
236
|
+
const compact = await Promise.all([...result.initialTurnsPage.data].reverse().map(turn => this.compactTurn(threadId, turn)));
|
|
237
|
+
const thread = { ...result.thread, turns: compact };
|
|
238
|
+
record = { thread, nextCursor: result.initialTurnsPage.nextCursor ?? null, sequence: 0,
|
|
239
|
+
recoverAfter: 0, changes: new Map(), changeBytes: 0, tokens: new Map() };
|
|
240
|
+
this.threads.set(threadId, record);
|
|
241
|
+
this.contexts.set(threadId, { ...this.contexts.get(threadId), model: result.model, thinking: result.reasoningEffort, serviceTier: result.serviceTier ?? null,
|
|
242
|
+
cwd: result.cwd, ...events.permissionState(result) });
|
|
243
|
+
await this.extendHistoryRecord(threadId, record, pageOptions);
|
|
244
|
+
}
|
|
234
245
|
}
|
|
235
246
|
const id = randomUUID(), expires = Date.now() + syncTokenTtlMs;
|
|
236
247
|
record.tokens.set(id, { sequence: record.sequence, expires });
|
|
@@ -269,11 +280,11 @@ export class SharedAppServer extends EventEmitter {
|
|
|
269
280
|
if (record?.tokens.size) { this.scheduleCleanup(threadId, slot); return; }
|
|
270
281
|
slot.tail = slot.tail.then(async () => {
|
|
271
282
|
if (slot.users || this.threads.get(threadId)?.tokens.size) return;
|
|
272
|
-
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
273
|
-
this.operationGroups.delete(threadId);
|
|
283
|
+
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
284
|
+
this.operationGroups.delete(threadId);
|
|
274
285
|
this.subscriptions.delete(threadId); clearTimeout(slot.expiry);
|
|
275
|
-
if (this.ready && !record?.readOnly) await this.request('thread/unsubscribe', { threadId });
|
|
276
|
-
this.serverRequests.forget(threadId);
|
|
286
|
+
if (this.ready && !record?.readOnly) await this.request('thread/unsubscribe', { threadId });
|
|
287
|
+
this.serverRequests.forget(threadId);
|
|
277
288
|
}).catch(error => this.emit('subscription-error', error));
|
|
278
289
|
}
|
|
279
290
|
|
|
@@ -311,140 +322,145 @@ export class SharedAppServer extends EventEmitter {
|
|
|
311
322
|
snapshot, catchup, release };
|
|
312
323
|
}
|
|
313
324
|
|
|
314
|
-
pageSettings(options = {}) {
|
|
315
|
-
return {
|
|
316
|
-
maxTurns: Math.max(1, Math.min(historyPageMaxTurns, Number(options.maxTurns) || historyPageMaxTurns)),
|
|
317
|
-
maxBytes: Math.max(1, Number(options.maxBytes) || historyPageMaxBytes),
|
|
318
|
-
measure: typeof options.measure === 'function' ? options.measure : value => Buffer.byteLength(JSON.stringify(value)),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async compactTurn(threadId, turn) {
|
|
323
|
-
const compact = events.compactTurn(turn);
|
|
324
|
-
if (!turn?.id) return compact;
|
|
325
|
-
const batches = [];
|
|
326
|
-
let cursor;
|
|
327
|
-
do {
|
|
328
|
-
const page = await this.request('thread/items/list', {
|
|
329
|
-
threadId, turnId: turn.id, ...(cursor ? { cursor } : {}),
|
|
330
|
-
limit: 100, sortDirection: 'desc',
|
|
331
|
-
});
|
|
332
|
-
if (!Array.isArray(page.data)) throw new Error('
|
|
333
|
-
batches.push(page.data.map(entry => events.compactItem(entry.item)));
|
|
334
|
-
cursor = page.nextCursor ?? null;
|
|
335
|
-
} while (cursor);
|
|
336
|
-
const items = batches.reverse().flatMap(batch => batch.reverse());
|
|
337
|
-
const groups = new Map(), skeleton = [];
|
|
338
|
-
let operationItems = [];
|
|
339
|
-
const flushOperations = () => {
|
|
340
|
-
if (!operationItems.length) return;
|
|
341
|
-
const visibleItems = operationItems.filter(item => item?.type !== 'reasoning');
|
|
342
|
-
operationItems = [];
|
|
343
|
-
if (!visibleItems.length) return;
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
if (firstOversized) return;
|
|
398
|
-
while (record.nextCursor && record.thread.turns.length < settings.maxTurns) {
|
|
399
|
-
const cursor = record.nextCursor;
|
|
400
|
-
const page = await this.request('thread/turns/list', {
|
|
401
|
-
threadId, cursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
402
|
-
});
|
|
403
|
-
if (!Array.isArray(page.data) || !page.data.length) {
|
|
404
|
-
record.nextCursor = page.nextCursor ?? null;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
const candidate = [await this.compactTurn(threadId, page.data[0]), ...record.thread.turns];
|
|
408
|
-
const value = { thread: { ...record.thread, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
409
|
-
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
410
|
-
record.thread.turns = candidate;
|
|
411
|
-
record.nextCursor = page.nextCursor ?? null;
|
|
412
|
-
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async history(threadId, cursor, options = {}) {
|
|
417
|
-
await this.connect();
|
|
418
|
-
if (!cursor) throw new Error('缺少历史分页游标。');
|
|
419
|
-
const settings = this.pageSettings(options), turns = [];
|
|
420
|
-
let firstOversized = false;
|
|
421
|
-
let nextCursor = cursor;
|
|
422
|
-
while (nextCursor && turns.length < settings.maxTurns) {
|
|
423
|
-
const pageCursor = nextCursor;
|
|
424
|
-
const page = await this.request('thread/turns/list', {
|
|
425
|
-
threadId, cursor: pageCursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
426
|
-
});
|
|
427
|
-
if (!Array.isArray(page.data) || !page.data.length) {
|
|
428
|
-
nextCursor = page.nextCursor ?? null;
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
const compact = await this.compactTurn(threadId, page.data[0]);
|
|
432
|
-
const candidate = [compact, ...turns];
|
|
433
|
-
const value = { thread: { id: threadId, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
434
|
-
const candidateBytes = settings.measure(value);
|
|
435
|
-
if (turns.length >= 2 && candidateBytes > settings.maxBytes) break;
|
|
436
|
-
turns.unshift(compact);
|
|
437
|
-
nextCursor = page.nextCursor ?? null;
|
|
438
|
-
if (turns.length === 1) firstOversized = candidateBytes > settings.maxBytes;
|
|
439
|
-
if (firstOversized || (turns.length >= 2 && candidateBytes > settings.maxBytes)) break;
|
|
440
|
-
}
|
|
441
|
-
return { thread: { id: threadId, turns }, nextCursor };
|
|
442
|
-
}
|
|
325
|
+
pageSettings(options = {}) {
|
|
326
|
+
return {
|
|
327
|
+
maxTurns: Math.max(1, Math.min(historyPageMaxTurns, Number(options.maxTurns) || historyPageMaxTurns)),
|
|
328
|
+
maxBytes: Math.max(1, Number(options.maxBytes) || historyPageMaxBytes),
|
|
329
|
+
measure: typeof options.measure === 'function' ? options.measure : value => Buffer.byteLength(JSON.stringify(value)),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async compactTurn(threadId, turn) {
|
|
334
|
+
const compact = events.compactTurn(turn);
|
|
335
|
+
if (!turn?.id) return compact;
|
|
336
|
+
const batches = [];
|
|
337
|
+
let cursor;
|
|
338
|
+
do {
|
|
339
|
+
const page = await this.request('thread/items/list', {
|
|
340
|
+
threadId, turnId: turn.id, ...(cursor ? { cursor } : {}),
|
|
341
|
+
limit: 100, sortDirection: 'desc',
|
|
342
|
+
});
|
|
343
|
+
if (!Array.isArray(page.data)) throw new Error('Codex 内核未返回单轮条目索引。');
|
|
344
|
+
batches.push(page.data.map(entry => events.compactItem(entry.item)));
|
|
345
|
+
cursor = page.nextCursor ?? null;
|
|
346
|
+
} while (cursor);
|
|
347
|
+
const items = batches.reverse().flatMap(batch => batch.reverse());
|
|
348
|
+
const groups = new Map(), skeleton = [];
|
|
349
|
+
let operationItems = [];
|
|
350
|
+
const flushOperations = () => {
|
|
351
|
+
if (!operationItems.length) return;
|
|
352
|
+
const visibleItems = operationItems.filter(item => item?.type !== 'reasoning');
|
|
353
|
+
operationItems = [];
|
|
354
|
+
if (!visibleItems.length) return;
|
|
355
|
+
const subAgentItems = visibleItems.filter(isSubAgentItem);
|
|
356
|
+
const detailItems = visibleItems.filter(item => !isSubAgentItem(item));
|
|
357
|
+
if (detailItems.length) {
|
|
358
|
+
const groupId = `g${groups.size + 1}`;
|
|
359
|
+
groups.set(groupId, detailItems);
|
|
360
|
+
skeleton.push({ type: 'lazyOperationGroup', id: `${turn.id}:${groupId}`,
|
|
361
|
+
turnId: turn.id, groupId,
|
|
362
|
+
operationTypes: [...new Set(detailItems.map(item => item.type).filter(Boolean))] });
|
|
363
|
+
}
|
|
364
|
+
skeleton.push(...subAgentItems);
|
|
365
|
+
};
|
|
366
|
+
for (const item of items) {
|
|
367
|
+
if (['userMessage', 'agentMessage', 'imageGeneration', 'imageView'].includes(item?.type)) {
|
|
368
|
+
flushOperations();
|
|
369
|
+
skeleton.push(item);
|
|
370
|
+
} else operationItems.push(item);
|
|
371
|
+
}
|
|
372
|
+
flushOperations();
|
|
373
|
+
let threadGroups = this.operationGroups.get(threadId);
|
|
374
|
+
if (!threadGroups) { threadGroups = new Map(); this.operationGroups.set(threadId, threadGroups); }
|
|
375
|
+
threadGroups.set(turn.id, groups);
|
|
376
|
+
return { ...compact, items: skeleton };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async operationGroupItems(threadId, turnId, groupId, offset) {
|
|
380
|
+
await this.connect();
|
|
381
|
+
const items = this.operationGroups.get(threadId)?.get(turnId)?.get(groupId);
|
|
382
|
+
if (!items) throw new Error('操作组已失效,请重新打开对话。');
|
|
383
|
+
const start = Math.max(0, Number(offset) || 0);
|
|
384
|
+
const pageItems = items.slice(start, start + turnItemPageSize);
|
|
385
|
+
const nextOffset = start + pageItems.length < items.length ? start + pageItems.length : null;
|
|
386
|
+
return { turnId, groupId, items: pageItems, nextOffset, total: items.length };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async turnContext(threadId, turnId) {
|
|
390
|
+
await this.connect();
|
|
391
|
+
if (!threadId || !turnId) throw new Error('缺少图片所属的对话或轮次。');
|
|
392
|
+
return this.compactTurn(threadId, { id: turnId });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fitTurns(base, turns, options = {}) {
|
|
396
|
+
const settings = this.pageSettings(options), accepted = [];
|
|
397
|
+
let firstOversized = false;
|
|
398
|
+
for (let index = turns.length - 1; index >= 0 && accepted.length < settings.maxTurns; index--) {
|
|
399
|
+
const candidate = [turns[index], ...accepted];
|
|
400
|
+
const candidateBytes = settings.measure({ ...base, thread: { ...base.thread, turns: candidate } });
|
|
401
|
+
if (accepted.length >= 2 && candidateBytes > settings.maxBytes) break;
|
|
402
|
+
accepted.unshift(turns[index]);
|
|
403
|
+
if (accepted.length === 1) firstOversized = candidateBytes > settings.maxBytes;
|
|
404
|
+
if (firstOversized || (accepted.length >= 2 && candidateBytes > settings.maxBytes)) break;
|
|
405
|
+
}
|
|
406
|
+
return accepted;
|
|
407
|
+
}
|
|
443
408
|
|
|
444
|
-
async
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
409
|
+
async extendHistoryRecord(threadId, record, options = {}) {
|
|
410
|
+
const settings = this.pageSettings(options);
|
|
411
|
+
if (!record.nextCursor || !record.thread.turns.length) return;
|
|
412
|
+
const firstOversized = settings.measure({ thread: record.thread, nextCursor: record.nextCursor }) > settings.maxBytes;
|
|
413
|
+
if (firstOversized) return;
|
|
414
|
+
while (record.nextCursor && record.thread.turns.length < settings.maxTurns) {
|
|
415
|
+
const cursor = record.nextCursor;
|
|
416
|
+
const page = await this.request('thread/turns/list', {
|
|
417
|
+
threadId, cursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
418
|
+
});
|
|
419
|
+
if (!Array.isArray(page.data) || !page.data.length) {
|
|
420
|
+
record.nextCursor = page.nextCursor ?? null;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
const candidate = [await this.compactTurn(threadId, page.data[0]), ...record.thread.turns];
|
|
424
|
+
const value = { thread: { ...record.thread, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
425
|
+
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
426
|
+
record.thread.turns = candidate;
|
|
427
|
+
record.nextCursor = page.nextCursor ?? null;
|
|
428
|
+
if (record.thread.turns.length >= 2 && settings.measure(value) > settings.maxBytes) break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async history(threadId, cursor, options = {}) {
|
|
433
|
+
await this.connect();
|
|
434
|
+
if (!cursor) throw new Error('缺少历史分页游标。');
|
|
435
|
+
const settings = this.pageSettings(options), turns = [];
|
|
436
|
+
let firstOversized = false;
|
|
437
|
+
let nextCursor = cursor;
|
|
438
|
+
while (nextCursor && turns.length < settings.maxTurns) {
|
|
439
|
+
const pageCursor = nextCursor;
|
|
440
|
+
const page = await this.request('thread/turns/list', {
|
|
441
|
+
threadId, cursor: pageCursor, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
442
|
+
});
|
|
443
|
+
if (!Array.isArray(page.data) || !page.data.length) {
|
|
444
|
+
nextCursor = page.nextCursor ?? null;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
const compact = await this.compactTurn(threadId, page.data[0]);
|
|
448
|
+
const candidate = [compact, ...turns];
|
|
449
|
+
const value = { thread: { id: threadId, turns: candidate }, nextCursor: page.nextCursor ?? null };
|
|
450
|
+
const candidateBytes = settings.measure(value);
|
|
451
|
+
if (turns.length >= 2 && candidateBytes > settings.maxBytes) break;
|
|
452
|
+
turns.unshift(compact);
|
|
453
|
+
nextCursor = page.nextCursor ?? null;
|
|
454
|
+
if (turns.length === 1) firstOversized = candidateBytes > settings.maxBytes;
|
|
455
|
+
if (firstOversized || (turns.length >= 2 && candidateBytes > settings.maxBytes)) break;
|
|
456
|
+
}
|
|
457
|
+
return { thread: { id: threadId, turns }, nextCursor };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async interrupt(threadId, turnId) {
|
|
461
|
+
await this.connect();
|
|
462
|
+
this.requireWritable(threadId);
|
|
463
|
+
return this.request('turn/interrupt', { threadId, turnId });
|
|
448
464
|
}
|
|
449
465
|
|
|
450
466
|
async models() {
|
|
@@ -463,16 +479,16 @@ export class SharedAppServer extends EventEmitter {
|
|
|
463
479
|
return this.usageCache.read(() => this.request('account/rateLimits/read'));
|
|
464
480
|
}
|
|
465
481
|
|
|
466
|
-
async settings(threadId, model, effort, serviceTier) {
|
|
467
|
-
await this.connect();
|
|
468
|
-
this.requireWritable(threadId);
|
|
482
|
+
async settings(threadId, model, effort, serviceTier) {
|
|
483
|
+
await this.connect();
|
|
484
|
+
this.requireWritable(threadId);
|
|
469
485
|
await events.updateSettings((method, params) => this.request(method, params), this.contexts, this.settingsNotifications, threadId, model, effort, serviceTier);
|
|
470
486
|
return this.context(threadId);
|
|
471
487
|
}
|
|
472
488
|
|
|
473
|
-
async permissions(threadId, mode) {
|
|
474
|
-
await this.connect();
|
|
475
|
-
this.requireWritable(threadId);
|
|
489
|
+
async permissions(threadId, mode) {
|
|
490
|
+
await this.connect();
|
|
491
|
+
this.requireWritable(threadId);
|
|
476
492
|
await events.updatePermissions((method, params) => this.request(method, params), this.contexts, this.settingsNotifications, this.permissionOptions, threadId, mode);
|
|
477
493
|
return this.context(threadId);
|
|
478
494
|
}
|
|
@@ -483,30 +499,21 @@ export class SharedAppServer extends EventEmitter {
|
|
|
483
499
|
return this.context(threadId);
|
|
484
500
|
}
|
|
485
501
|
|
|
486
|
-
context(threadId) {
|
|
502
|
+
context(threadId) {
|
|
487
503
|
const context = this.permissionOptions.context(threadId, this.contexts.get(threadId) || {}), usage = context.tokenUsage;
|
|
488
504
|
const usedTokens = usage?.last?.totalTokens, contextWindow = usage?.modelContextWindow;
|
|
489
505
|
return { ...context, available: Boolean(contextWindow), usedTokens, contextWindow,
|
|
490
506
|
usedPercent: contextWindow ? 100 * usedTokens / contextWindow : 0 };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
requireWritable(threadId) {
|
|
494
|
-
const record = this.threads.get(threadId);
|
|
495
|
-
if (record?.readOnly) throw new ThreadWriterConflictError(threadId, record.thread.readOnlyReason);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
close() { this.socket?.close(); }
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (types.has('fileChange')) parts.push('编辑了文件');
|
|
505
|
-
if (types.has('commandExecution')) parts.push('运行了命令');
|
|
506
|
-
if (types.has('mcpToolCall') || types.has('dynamicToolCall')) parts.push('调用了工具');
|
|
507
|
-
if (types.has('webSearch')) parts.push('搜索了网页');
|
|
508
|
-
if (types.has('imageView')) parts.push('查看了图像');
|
|
509
|
-
if (types.has('collabAgentToolCall') || types.has('subAgentActivity')) parts.push('处理了子任务');
|
|
510
|
-
const available = hasMore || items.some(item => item && !['userMessage', 'agentMessage'].includes(item.type));
|
|
511
|
-
return { available, title: parts.length ? `${parts.join(' · ')}${hasMore ? '等' : ''}` : '执行过程' };
|
|
512
|
-
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
requireWritable(threadId) {
|
|
510
|
+
const record = this.threads.get(threadId);
|
|
511
|
+
if (record?.readOnly) throw new ThreadWriterConflictError(threadId, record.thread.readOnlyReason);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
close() { this.socket?.close(); }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isSubAgentItem(item) {
|
|
518
|
+
return item?.type === 'collabAgentToolCall' || item?.type === 'subAgentActivity';
|
|
519
|
+
}
|