@walkhi/code-relax 0.1.0-beta.34 → 0.1.0-beta.35

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.
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.34"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.35"}
@@ -35,6 +35,7 @@ export class ManagedAppServer extends EventEmitter {
35
35
  this.recordPath = path.join(stateRoot, 'managed-app-server.json');
36
36
  this.client = client;
37
37
  this.state = { state: 'absent', ready: false, error: null };
38
+ this.client.diagnosticIdentity = () => ({ kernelPid: this.state.pid ?? null, kernelCreated: this.state.created ?? null });
38
39
  this.restartStatus = null;
39
40
  this.stopped = true;
40
41
  this.restarting = false;
@@ -13,7 +13,7 @@ const writerConflictMessages = ['already has an active writer', 'already has a l
13
13
 
14
14
  export class ThreadWriterConflictError extends Error {
15
15
  constructor(threadId, message) {
16
- super('该对话正在另一个 Codex 实例中使用。');
16
+ super('该对话存在写入冲突,请稍后重试。');
17
17
  this.name = 'ThreadWriterConflictError';
18
18
  this.code = 'thread_writer_owned_elsewhere';
19
19
  this.threadId = threadId;
@@ -37,6 +37,7 @@ export class SharedAppServer extends EventEmitter {
37
37
  this.serverVersion = '';
38
38
  this.epoch = randomUUID();
39
39
  this.sequence = 0;
40
+ this.connectionId = null;
40
41
  this.pending = new Map();
41
42
  this.serverRequests = new serverRequests.Store(randomUUID(), id => {
42
43
  this.emit('requests-changed', id);
@@ -64,6 +65,7 @@ export class SharedAppServer extends EventEmitter {
64
65
  async open() {
65
66
  const socket = await this.openSocket();
66
67
  this.socket = socket;
68
+ this.connectionId = randomUUID();
67
69
  this.ready = false;
68
70
  this.serverVersion = '';
69
71
  this.serverRequests.reset(randomUUID());
@@ -78,12 +80,16 @@ export class SharedAppServer extends EventEmitter {
78
80
  clearTimeout(pending.timer);
79
81
  if (message.error) {
80
82
  const text = String(message.error.message || 'app-server 请求失败');
83
+ this.traceLifecycle('error', pending, { rpcCode: message.error.code,
84
+ writerError: writerConflictMessages.find(value => text.toLowerCase().includes(value)) || null });
81
85
  pending.reject(writerConflictMessages.some(value => text.toLowerCase().includes(value))
82
86
  ? new ThreadWriterConflictError(pending.threadId, text)
83
87
  : Object.assign(new Error(text), { rpcCode: message.error.code, details: message.error.data,
84
88
  ...(/^invalid cursor:/i.test(text) ? { code: 'history_cursor_expired' } : {}) }));
85
89
  }
86
90
  else {
91
+ this.traceLifecycle('complete', pending, { status: message.result?.status,
92
+ resultThreadId: message.result?.thread?.id });
87
93
  try { pending.resolve(pending.transform(message.result)); }
88
94
  catch (error) { pending.reject(error); }
89
95
  }
@@ -93,6 +99,7 @@ export class SharedAppServer extends EventEmitter {
93
99
  this.serverVersion = '';
94
100
  for (const pending of this.pending.values()) {
95
101
  clearTimeout(pending.timer);
102
+ this.traceLifecycle('disconnected', pending);
96
103
  pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
97
104
  }
98
105
  this.pending.clear();
@@ -121,24 +128,44 @@ export class SharedAppServer extends EventEmitter {
121
128
 
122
129
  request(method, params, transform = value => value) {
123
130
  const id = ++this.sequence;
131
+ const diagnostic = { method, requestId: id, threadId: params?.threadId, connectionId: this.connectionId,
132
+ ...(method === 'thread/settings/update' && params?.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}) };
133
+ this.traceLifecycle('begin', diagnostic);
124
134
  return new Promise((resolve, reject) => {
125
135
  const timer = setTimeout(() => {
126
136
  const error = Object.assign(new Error(`${method} 超时;不会自动重发,请先检查对话。`),
127
137
  { code: 'app_server_request_timeout' });
138
+ this.traceLifecycle('timeout', diagnostic);
128
139
  if ((method === 'turn/start' || method === 'turn/steer') && this.pending.has(id)) {
129
140
  messageDelivery.retainLateResult(this.pending.get(id), error);
130
141
  } else { this.pending.delete(id); reject(error); }
131
142
  }, 30000);
132
- this.pending.set(id, { resolve, reject, timer, transform, threadId: params?.threadId });
143
+ this.pending.set(id, { resolve, reject, timer, transform, ...diagnostic });
133
144
  this.socket.send(JSON.stringify({ id, method, params }), error => {
134
145
  const pending = this.pending.get(id);
135
146
  if (!error || !pending) return;
147
+ this.traceLifecycle('send-error', diagnostic);
136
148
  this.pending.delete(id);
137
149
  clearTimeout(timer); pending.reject(error);
138
150
  });
139
151
  });
140
152
  }
141
153
 
154
+ traceLifecycle(phase, request, extra = {}) {
155
+ if (!['thread/start', 'thread/resume', 'thread/unsubscribe', 'thread/settings/update', 'thread/settings/updated'].includes(request.method)) return;
156
+ const threadId = value => typeof value === 'string' && /^[\w-]{1,80}$/.test(value) ? value : null;
157
+ const identity = this.diagnosticIdentity?.() || {};
158
+ console.info('[thread-lifecycle]', JSON.stringify({ time: new Date().toISOString(), bridgePid: process.pid,
159
+ kernelPid: identity.kernelPid ?? null, kernelCreated: identity.kernelCreated ?? null,
160
+ connectionId: request.connectionId, serverVersion: this.serverVersion, phase,
161
+ method: request.method, requestId: request.requestId, threadId: threadId(request.threadId),
162
+ ...([null, 'default', 'priority', 'fast'].includes(request.serviceTier) ? { serviceTier: request.serviceTier } : {}),
163
+ ...(extra.resultThreadId ? { resultThreadId: threadId(extra.resultThreadId) } : {}),
164
+ ...(typeof extra.status === 'string' && /^[\w-]{1,40}$/.test(extra.status) ? { status: extra.status } : {}),
165
+ ...(typeof extra.rpcCode === 'number' ? { rpcCode: extra.rpcCode } : {}),
166
+ ...(extra.writerError ? { writerError: extra.writerError } : {}) }));
167
+ }
168
+
142
169
  async projects() {
143
170
  await this.connect();
144
171
  return events.readProjects((method, params) => this.request(method, params));
@@ -190,6 +217,7 @@ export class SharedAppServer extends EventEmitter {
190
217
  const id = params.threadId;
191
218
  if (method === 'thread/settings/updated') {
192
219
  const settings = params.threadSettings;
220
+ this.traceLifecycle('notification', { method, threadId: id, connectionId: this.connectionId, serviceTier: settings.serviceTier ?? null });
193
221
  this.contexts.set(id, { ...this.contexts.get(id), model: settings.model, thinking: settings.effort, serviceTier: settings.serviceTier ?? null,
194
222
  cwd: settings.cwd, ...events.permissionState(settings) });
195
223
  this.settingsNotifications.set(id, settings);
@@ -315,7 +343,7 @@ export class SharedAppServer extends EventEmitter {
315
343
  } catch (error) {
316
344
  snapshot?.close();
317
345
  if (!isThreadWriterConflict(error)) throw error;
318
- record = await this.readOnlyRecord(threadId, pageOptions, '该对话正在另一个 Codex 实例中使用。');
346
+ record = await this.readOnlyRecord(threadId, pageOptions, '该对话存在写入冲突,请稍后重试。');
319
347
  result = null;
320
348
  }
321
349
  if (result) try {
@@ -390,11 +418,18 @@ export class SharedAppServer extends EventEmitter {
390
418
  slot.tail = slot.tail.then(async () => {
391
419
  if (this.subscriptions.get(threadId) !== slot) return;
392
420
  if (slot.users || slot.readers || this.threads.get(threadId)?.tokens.size || this.keepThreadLoaded(threadId, this.threads.get(threadId))) return;
393
- this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
394
- this.subscriptions.delete(threadId); clearTimeout(slot.expiry);
395
421
  if (this.ready && !record?.readOnly) await this.unsubscribeThread(threadId);
422
+ // Keep this queue registered until unsubscribe completes. A returning reader
423
+ // must enqueue its resume behind it, rather than creating a second queue.
424
+ if (this.subscriptions.get(threadId) !== slot) return;
425
+ this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
426
+ if (!slot.users && !slot.readers) this.subscriptions.delete(threadId);
427
+ clearTimeout(slot.expiry);
396
428
  this.serverRequests.forget(threadId);
397
- }).catch(error => this.emit('subscription-error', error));
429
+ }).catch(error => {
430
+ if (this.subscriptions.get(threadId) === slot && this.threads.get(threadId) === record && record) record.needsReload = true;
431
+ this.emit('subscription-error', error);
432
+ });
398
433
  }
399
434
 
400
435
  keepThreadLoaded(threadId, record) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.34",
3
+ "version": "0.1.0-beta.35",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -41,8 +41,8 @@
41
41
  "tag": "beta"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.34",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.34",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.34"
44
+ "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.35",
45
+ "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.35",
46
+ "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.35"
47
47
  }
48
48
  }