@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.
Files changed (120) hide show
  1. package/README.md +55 -71
  2. package/dist/bin/codex-remote.mjs +150 -126
  3. package/dist/bin/manage-relay-admin.mjs +20 -0
  4. package/dist/bin/migrate-relay-db.mjs +107 -0
  5. package/dist/bin/self-relay-admin.mjs +8 -0
  6. package/dist/bin/self-relay-demo.mjs +2 -2
  7. package/dist/bin/self-relay-server.mjs +18 -10
  8. package/dist/shared/app-server-events.cjs +42 -15
  9. package/dist/shared/file-mentions.cjs +35 -0
  10. package/dist/shared/server-requests.cjs +86 -58
  11. package/dist/skills/code-relax/SKILL.md +7 -6
  12. package/dist/src/desktop-launcher.mjs +16 -21
  13. package/dist/src/file-upload-session.mjs +124 -0
  14. package/dist/src/lan-socket-server.mjs +52 -9
  15. package/dist/src/managed-app-server.mjs +44 -13
  16. package/dist/src/message-images.mjs +0 -18
  17. package/dist/src/onboarding.mjs +9 -11
  18. package/dist/src/platform/README.md +1 -1
  19. package/dist/src/platform/windows/background-process.mjs +1 -1
  20. package/dist/src/platform/windows/codex-runtime.mjs +251 -0
  21. package/dist/src/platform/windows/desktop-host.mjs +17 -3
  22. package/dist/src/platform/windows/desktop-info.ps1 +2 -4
  23. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  24. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
  25. package/dist/src/platform/windows/launch-worker.mjs +42 -0
  26. package/dist/src/platform/windows/resident-start.js +20 -0
  27. package/dist/src/platform/windows/resident-startup.ps1 +76 -20
  28. package/dist/src/platform/windows/service-host.mjs +4 -16
  29. package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
  30. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  31. package/dist/src/platform/windows/user-environment.mjs +1 -1
  32. package/dist/src/runtime-discovery.mjs +43 -0
  33. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  34. package/dist/src/self-relay/admin-page.mjs +79 -0
  35. package/dist/src/self-relay/admin-server.mjs +66 -0
  36. package/dist/src/self-relay/admin-state.mjs +82 -19
  37. package/dist/src/self-relay/admin-style.mjs +9 -0
  38. package/dist/src/self-relay/admin-usage-page.mjs +28 -0
  39. package/dist/src/self-relay/connector.mjs +243 -30
  40. package/dist/src/self-relay/demo.mjs +3 -4
  41. package/dist/src/self-relay/email-accounts.mjs +141 -0
  42. package/dist/src/self-relay/lifecycle.mjs +99 -142
  43. package/dist/src/self-relay/mysql-state.mjs +104 -0
  44. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  45. package/dist/src/self-relay/server.mjs +335 -59
  46. package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
  47. package/dist/src/self-relay/usage-meter.mjs +125 -0
  48. package/dist/src/server.mjs +731 -1439
  49. package/dist/src/service-control.mjs +4 -4
  50. package/dist/src/service-doctor.mjs +27 -29
  51. package/dist/src/service-health.mjs +2 -2
  52. package/dist/src/service-install.mjs +1 -4
  53. package/dist/src/service-lifecycle.mjs +19 -19
  54. package/dist/src/shared-app-server.mjs +277 -270
  55. package/dist/src/shared-catalog.mjs +1 -11
  56. package/dist/src/shared-recovery.mjs +11 -9
  57. package/dist/src/shared-thread-stream.mjs +1 -1
  58. package/dist/src/skill-install.mjs +0 -13
  59. package/package.json +17 -18
  60. package/tools/postinstall.mjs +69 -15
  61. package/dist/src/app-server-client.mjs +0 -468
  62. package/dist/src/app-server-tasks.mjs +0 -358
  63. package/dist/src/debug-ui.mjs +0 -131
  64. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  65. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  66. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  67. package/dist/src/platform/windows/launch-worker.ps1 +0 -20
  68. package/dist/src/thread-catalog.mjs +0 -47
  69. package/dist/tools/find-desktop-pipe.mjs +0 -10
  70. package/dist/web/activity-view.js +0 -283
  71. package/dist/web/approval-ui.js +0 -188
  72. package/dist/web/capabilities.js +0 -57
  73. package/dist/web/chat-transport.js +0 -202
  74. package/dist/web/chat.css +0 -782
  75. package/dist/web/chat.js +0 -3830
  76. package/dist/web/client-platform.js +0 -11
  77. package/dist/web/community-view.js +0 -20
  78. package/dist/web/community.css +0 -77
  79. package/dist/web/community.html +0 -37
  80. package/dist/web/composer-controller.js +0 -101
  81. package/dist/web/conversation-controller.js +0 -99
  82. package/dist/web/disclosure-state-controller.js +0 -95
  83. package/dist/web/draft-controller.js +0 -103
  84. package/dist/web/harmony-platform.js +0 -52
  85. package/dist/web/highlight-powershell.min.js +0 -39
  86. package/dist/web/highlight.min.js +0 -1262
  87. package/dist/web/history-cache.js +0 -240
  88. package/dist/web/history-controller.js +0 -167
  89. package/dist/web/index.html +0 -325
  90. package/dist/web/lan-session.js +0 -122
  91. package/dist/web/link-action-controller.js +0 -212
  92. package/dist/web/message-send-controller.js +0 -177
  93. package/dist/web/message-view.js +0 -98
  94. package/dist/web/p2p-data-channel.js +0 -50
  95. package/dist/web/p2p-diagnostics.js +0 -28
  96. package/dist/web/p2p-probe.js +0 -123
  97. package/dist/web/page-resume.js +0 -28
  98. package/dist/web/pending-message-store.js +0 -108
  99. package/dist/web/queue-controller.js +0 -82
  100. package/dist/web/resources.json +0 -1
  101. package/dist/web/self-relay-session.js +0 -129
  102. package/dist/web/server-requests.js +0 -67
  103. package/dist/web/station-connection-controller.js +0 -75
  104. package/dist/web/task-list-view.js +0 -296
  105. package/dist/web/thread-attention-controller.js +0 -124
  106. package/dist/web/thread-context-controller.js +0 -30
  107. package/dist/web/thread-list-controller.js +0 -61
  108. package/dist/web/thread-list-sync.js +0 -86
  109. package/dist/web/thread-title-controller.js +0 -57
  110. package/dist/web/timeline-formatters.js +0 -249
  111. package/dist/web/timeline-reducer.js +0 -81
  112. package/dist/web/timeline-renderer.js +0 -161
  113. package/dist/web/timeline-scroll-controller.js +0 -50
  114. package/dist/web/timeline-sync.js +0 -67
  115. package/dist/web/usage-controller.js +0 -258
  116. package/dist/web/vendor/README.md +0 -14
  117. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  118. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  119. package/dist/web/vendor/highlight.min.js +0 -1262
  120. 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('完整控制必须使用本机回环 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('完整控制连接断开;发送结果可能未确认,请检查对话后再操作。'));
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 live = await this.request('thread/read', { threadId: args.threadId, includeTurns: true });
135
- const active = live.thread.turns?.findLast(turn => turn.status === 'inProgress');
136
- if (!active) throw new Error('活动轮次已变化,请刷新后再发送。');
137
- return this.request('turn/steer', { threadId: args.threadId, expectedTurnId: active.id, input: args.input, ...(args.clientUserMessageId ? { clientUserMessageId: args.clientUserMessageId } : {}) });
138
- }
139
- return this.request('turn/start', args);
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 groupId = `g${groups.size + 1}`;
345
- const outline = operationOutline(visibleItems, false);
346
- groups.set(groupId, visibleItems);
347
- skeleton.push({ type: 'lazyOperationGroup', id: `${turn.id}:${groupId}`,
348
- turnId: turn.id, groupId, title: outline.title, count: visibleItems.length });
349
- };
350
- for (const item of items) {
351
- if (['userMessage', 'agentMessage', 'imageGeneration'].includes(item?.type)) {
352
- flushOperations();
353
- skeleton.push(item);
354
- } else operationItems.push(item);
355
- }
356
- flushOperations();
357
- let threadGroups = this.operationGroups.get(threadId);
358
- if (!threadGroups) { threadGroups = new Map(); this.operationGroups.set(threadId, threadGroups); }
359
- threadGroups.set(turn.id, groups);
360
- return { ...compact, items: skeleton };
361
- }
362
-
363
- async operationGroupItems(threadId, turnId, groupId, offset) {
364
- await this.connect();
365
- const items = this.operationGroups.get(threadId)?.get(turnId)?.get(groupId);
366
- if (!items) throw new Error('操作组已失效,请重新打开对话。');
367
- const start = Math.max(0, Number(offset) || 0);
368
- const pageItems = items.slice(start, start + turnItemPageSize);
369
- const nextOffset = start + pageItems.length < items.length ? start + pageItems.length : null;
370
- return { turnId, groupId, items: pageItems, nextOffset, total: items.length };
371
- }
372
-
373
- async turnContext(threadId, turnId) {
374
- await this.connect();
375
- if (!threadId || !turnId) throw new Error('缺少图片所属的对话或轮次。');
376
- return this.compactTurn(threadId, { id: turnId });
377
- }
378
-
379
- fitTurns(base, turns, options = {}) {
380
- const settings = this.pageSettings(options), accepted = [];
381
- let firstOversized = false;
382
- for (let index = turns.length - 1; index >= 0 && accepted.length < settings.maxTurns; index--) {
383
- const candidate = [turns[index], ...accepted];
384
- const candidateBytes = settings.measure({ ...base, thread: { ...base.thread, turns: candidate } });
385
- if (accepted.length >= 2 && candidateBytes > settings.maxBytes) break;
386
- accepted.unshift(turns[index]);
387
- if (accepted.length === 1) firstOversized = candidateBytes > settings.maxBytes;
388
- if (firstOversized || (accepted.length >= 2 && candidateBytes > settings.maxBytes)) break;
389
- }
390
- return accepted;
391
- }
392
-
393
- async extendHistoryRecord(threadId, record, options = {}) {
394
- const settings = this.pageSettings(options);
395
- if (!record.nextCursor || !record.thread.turns.length) return;
396
- const firstOversized = settings.measure({ thread: record.thread, nextCursor: record.nextCursor }) > settings.maxBytes;
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 interrupt(threadId, turnId) {
445
- await this.connect();
446
- this.requireWritable(threadId);
447
- return this.request('turn/interrupt', { threadId, turnId });
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 operationOutline(items, hasMore) {
502
- const types = new Set(items.map(item => item?.type));
503
- const parts = [];
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
+ }