@gkorepanov/ccodex 0.4.3 → 0.4.5
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 +1 -0
- package/config/compatibility.json +1 -1
- package/dist/claude/rateLimits.d.ts +8 -1
- package/dist/claude/rateLimits.js +28 -6
- package/dist/claude/rateLimits.js.map +1 -1
- package/dist/claude/service.d.ts +5 -3
- package/dist/claude/service.js +43 -4
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/scopeState.d.ts +4 -1
- package/dist/claude/session/scopeState.js +3 -2
- package/dist/claude/session/scopeState.js.map +1 -1
- package/dist/claude/session/session.d.ts +4 -0
- package/dist/claude/session/session.js +24 -11
- package/dist/claude/session/session.js.map +1 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +6 -1
- package/dist/config/config.js.map +1 -1
- package/dist/gateway/clientConnection.d.ts +2 -1
- package/dist/gateway/clientConnection.js +527 -95
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/optimisticSideThreads.d.ts +26 -0
- package/dist/gateway/optimisticSideThreads.js +158 -0
- package/dist/gateway/optimisticSideThreads.js.map +1 -0
- package/dist/gateway/server.js +11 -3
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/stockSideThreads.d.ts +29 -1
- package/dist/gateway/stockSideThreads.js +223 -21
- package/dist/gateway/stockSideThreads.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +1 -0
- package/dist/gateway/subscriptions.js +4 -0
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/threadList.d.ts +1 -1
- package/dist/gateway/threadList.js +1 -1
- package/dist/gateway/threadList.js.map +1 -1
- package/dist/gateway/transientNotice.d.ts +4 -0
- package/dist/gateway/transientNotice.js +19 -0
- package/dist/gateway/transientNotice.js.map +1 -1
- package/dist/handoff/service.d.ts +3 -1
- package/dist/handoff/service.js +66 -13
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/store.d.ts +1 -0
- package/dist/handoff/store.js +6 -0
- package/dist/handoff/store.js.map +1 -1
- package/dist/state/stockStateTracker.d.ts +4 -0
- package/dist/state/stockStateTracker.js +41 -0
- package/dist/state/stockStateTracker.js.map +1 -1
- package/examples/config.toml +1 -0
- package/package.json +4 -4
|
@@ -7,8 +7,8 @@ import { mergedModelList } from "./modelList.js";
|
|
|
7
7
|
import { StockRpc } from "./stockRpc.js";
|
|
8
8
|
import { mergedLoadedList, mergedThreadList } from "./threadList.js";
|
|
9
9
|
import { ServerRequestIds } from "./serverRequestIds.js";
|
|
10
|
-
import { providerMigrationCompleted, providerMigrationFailed, providerMigrationNotice, transientCommandNotice, transientSystemNotice, } from "./transientNotice.js";
|
|
11
|
-
import { rateLimitNotifications } from "../claude/rateLimits.js";
|
|
10
|
+
import { providerMigrationCompleted, providerMigrationFailed, providerMigrationNotice, transientCommandNotice, transientSystemNotice, transientSystemItemNotice, } from "./transientNotice.js";
|
|
11
|
+
import { rateLimitNotifications, } from "../claude/rateLimits.js";
|
|
12
12
|
import { formatCCodexStatus, isCCodexStatusCommand } from "../claude/statusCommand.js";
|
|
13
13
|
import { claudeCompactCommand } from "../claude/compactCommand.js";
|
|
14
14
|
import { DEFAULT_FEATURES } from "../config/config.js";
|
|
@@ -16,9 +16,13 @@ import { providerUnavailableMessage } from "../runtime/providerAvailability.js";
|
|
|
16
16
|
import { formatCCodexState, isCCodexStateCommand } from "../state/stateCommand.js";
|
|
17
17
|
import { StockStateTracker } from "../state/stockStateTracker.js";
|
|
18
18
|
import { STOCK_SIDE_THREAD_SOURCE } from "./stockSideThreads.js";
|
|
19
|
+
import { projectRpcToPublicThread } from "./logicalThreadProjection.js";
|
|
19
20
|
function requestedFast(serviceTier) {
|
|
20
21
|
return serviceTier === "fast" || serviceTier === "priority";
|
|
21
22
|
}
|
|
23
|
+
function isUserSideFork(params) {
|
|
24
|
+
return params.ephemeral === true && params.excludeTurns === true && params.threadSource === "user";
|
|
25
|
+
}
|
|
22
26
|
function claudeModelNoticeName(model) {
|
|
23
27
|
const name = model.replace(/^claude:/u, "").replace(/^claude-/u, "").split("-")[0] ?? model;
|
|
24
28
|
return `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
@@ -33,7 +37,7 @@ function fastTransitionNotice(before, after, requestedTier) {
|
|
|
33
37
|
}
|
|
34
38
|
return undefined;
|
|
35
39
|
}
|
|
36
|
-
export function attachClientConnection(client, stockSocket, claudeModels, claude, handoffs, subscriptions, logger, cursors, metrics, recorder, remoteControl, features = DEFAULT_FEATURES, providerAvailability, sharedStockState, stockSideThreads) {
|
|
40
|
+
export function attachClientConnection(client, stockSocket, claudeModels, claude, handoffs, subscriptions, logger, cursors, metrics, recorder, remoteControl, features = DEFAULT_FEATURES, providerAvailability, sharedStockState, stockSideThreads, optimisticSideThreads) {
|
|
37
41
|
const connectionId = uuidv7();
|
|
38
42
|
let resolveClosed;
|
|
39
43
|
let rejectClosed;
|
|
@@ -50,6 +54,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
50
54
|
const requestStarted = new Map();
|
|
51
55
|
const forwardedRequests = new Map();
|
|
52
56
|
const recentSystemErrors = new Map();
|
|
57
|
+
const knownLogicalNames = new Map();
|
|
53
58
|
const serverRequestIds = new ServerRequestIds();
|
|
54
59
|
const requestKey = (id) => `${typeof id}:${id}`;
|
|
55
60
|
const completeLatency = (id, provider) => {
|
|
@@ -96,6 +101,25 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
96
101
|
recorder.frame(connectionId, "gateway_to_client", data, false);
|
|
97
102
|
client.send(data);
|
|
98
103
|
};
|
|
104
|
+
const syncLogicalName = (thread) => {
|
|
105
|
+
if (!handoffs.logical?.(thread.id))
|
|
106
|
+
return;
|
|
107
|
+
if (knownLogicalNames.has(thread.id) && knownLogicalNames.get(thread.id) === thread.name)
|
|
108
|
+
return;
|
|
109
|
+
knownLogicalNames.set(thread.id, thread.name);
|
|
110
|
+
sendJson({
|
|
111
|
+
method: "thread/name/updated",
|
|
112
|
+
params: { threadId: thread.id, ...(thread.name === null ? {} : { threadName: thread.name }) },
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const syncLogicalNameFromResult = (result) => {
|
|
116
|
+
const thread = result && typeof result === "object"
|
|
117
|
+
? result.thread
|
|
118
|
+
: undefined;
|
|
119
|
+
if (thread && typeof thread === "object" && typeof thread.id === "string") {
|
|
120
|
+
syncLogicalName(thread);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
99
123
|
const emitNotice = (notice) => {
|
|
100
124
|
for (const notification of notice.notifications)
|
|
101
125
|
sendJson(notification);
|
|
@@ -139,6 +163,27 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
139
163
|
for (const notification of rateLimitNotifications(response))
|
|
140
164
|
sendJson(notification);
|
|
141
165
|
};
|
|
166
|
+
const activeClaudeTurns = new Map();
|
|
167
|
+
const pendingRateLimitTransitions = new Map();
|
|
168
|
+
const rateLimitNoticeText = (transition) => {
|
|
169
|
+
const reset = transition.resetsAt === null
|
|
170
|
+
? ""
|
|
171
|
+
: `\n ↳ resets ${new Date(transition.resetsAt * 1_000).toISOString().replace("T", " ").slice(0, 16)} UTC`;
|
|
172
|
+
return transition.status === "rejected"
|
|
173
|
+
? `❌ Claude rate limit reached${reset}`
|
|
174
|
+
: `⚠️ Claude usage is nearing its limit${reset}`;
|
|
175
|
+
};
|
|
176
|
+
const emitRateLimitTransition = (threadId, transition) => {
|
|
177
|
+
const turnId = activeClaudeTurns.get(threadId);
|
|
178
|
+
if (!turnId) {
|
|
179
|
+
const pending = pendingRateLimitTransitions.get(threadId) ?? [];
|
|
180
|
+
pending.push(transition);
|
|
181
|
+
pendingRateLimitTransitions.set(threadId, pending);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const notification of transientSystemItemNotice(threadId, turnId, rateLimitNoticeText(transition)))
|
|
185
|
+
sendJson(notification);
|
|
186
|
+
};
|
|
142
187
|
const emitStockRateLimits = (value) => {
|
|
143
188
|
if (!value || typeof value !== "object")
|
|
144
189
|
return;
|
|
@@ -241,6 +286,28 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
241
286
|
});
|
|
242
287
|
};
|
|
243
288
|
const notificationSink = (method, params) => {
|
|
289
|
+
let startedTurn;
|
|
290
|
+
if (method === "turn/started" && params && typeof params === "object") {
|
|
291
|
+
const value = params;
|
|
292
|
+
if (typeof value.threadId === "string" && typeof value.turn?.id === "string") {
|
|
293
|
+
activeClaudeTurns.set(value.threadId, value.turn.id);
|
|
294
|
+
startedTurn = { threadId: value.threadId, turnId: value.turn.id };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (method === "turn/completed" && params && typeof params === "object") {
|
|
298
|
+
const value = params;
|
|
299
|
+
if (typeof value.threadId === "string" && typeof value.turn?.id === "string"
|
|
300
|
+
&& activeClaudeTurns.get(value.threadId) === value.turn.id) {
|
|
301
|
+
activeClaudeTurns.delete(value.threadId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (method === "thread/name/updated" && params && typeof params === "object") {
|
|
305
|
+
const update = params;
|
|
306
|
+
if (typeof update.threadId === "string"
|
|
307
|
+
&& (typeof update.threadName === "string" || update.threadName === null)) {
|
|
308
|
+
knownLogicalNames.set(update.threadId, update.threadName);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
244
311
|
if (method === "error" && params && typeof params === "object") {
|
|
245
312
|
const failure = params;
|
|
246
313
|
if (typeof failure.threadId === "string" && typeof failure.error?.message === "string") {
|
|
@@ -259,14 +326,23 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
259
326
|
}
|
|
260
327
|
}
|
|
261
328
|
sendJson({ method, params });
|
|
329
|
+
if (startedTurn) {
|
|
330
|
+
const pending = pendingRateLimitTransitions.get(startedTurn.threadId) ?? [];
|
|
331
|
+
pendingRateLimitTransitions.delete(startedTurn.threadId);
|
|
332
|
+
for (const transition of pending)
|
|
333
|
+
emitRateLimitTransition(startedTurn.threadId, transition);
|
|
334
|
+
}
|
|
262
335
|
};
|
|
263
336
|
const serverRequestSink = (id, method, params) => {
|
|
264
337
|
sendJson({ id: serverRequestIds.wireId(id), method, params });
|
|
265
338
|
};
|
|
266
339
|
subscriptions.attach(connectionId, notificationSink, serverRequestSink);
|
|
267
|
-
claude.subscribeRateLimits(connectionId, (response) => {
|
|
268
|
-
if (foreground?.provider
|
|
269
|
-
|
|
340
|
+
claude.subscribeRateLimits(connectionId, (response, transition) => {
|
|
341
|
+
if (foreground?.provider !== "claude")
|
|
342
|
+
return;
|
|
343
|
+
emitRateLimits(response);
|
|
344
|
+
if (transition)
|
|
345
|
+
emitRateLimitTransition(foreground.threadId, transition);
|
|
270
346
|
});
|
|
271
347
|
const subscribedClaudeThreads = new Set();
|
|
272
348
|
const subscribeClaude = (threadId) => {
|
|
@@ -281,6 +357,271 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
281
357
|
subscribeClaude(threadId);
|
|
282
358
|
subscriptions.mute(threadId, connectionId);
|
|
283
359
|
};
|
|
360
|
+
const projectSideResult = (publicThreadId, target, result) => projectRpcToPublicThread({ result }, { publicThreadId, backendThreadId: target.backendThreadId }).result;
|
|
361
|
+
const projectSideParams = (publicThreadId, target, params) => projectRpcToPublicThread({ params }, { publicThreadId, backendThreadId: target.backendThreadId }).params;
|
|
362
|
+
const optimisticStockRequest = (method, params) => stockSideThreads?.request(method, params) ?? stockRpc.request(method, params);
|
|
363
|
+
const reportOptimisticFailure = (threadId) => {
|
|
364
|
+
if (!connectionAlive)
|
|
365
|
+
return;
|
|
366
|
+
const failure = optimisticSideThreads?.claimFailure(threadId);
|
|
367
|
+
if (failure) {
|
|
368
|
+
emitTransientNotice(threadId, `Could not prepare side chat\n ↳ ${failure.message}`, "error");
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const openOptimisticSide = (requestId, snapshot, provider, prepare) => {
|
|
372
|
+
const threadId = snapshot.thread.id;
|
|
373
|
+
subscriptions.subscribe(threadId, connectionId, notificationSink, serverRequestSink);
|
|
374
|
+
const response = optimisticSideThreads.open(connectionId, snapshot, prepare, async (target) => {
|
|
375
|
+
if (target.provider === "claude") {
|
|
376
|
+
subscriptions.unaliasThread(target.backendThreadId);
|
|
377
|
+
await claude.deleteThread(target.backendThreadId);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
await stockSideThreads?.discardOptimistic(threadId);
|
|
381
|
+
}
|
|
382
|
+
}, reportOptimisticFailure);
|
|
383
|
+
sendResult(requestId, response);
|
|
384
|
+
sendJson({ method: "thread/started", params: { thread: response.thread } });
|
|
385
|
+
selectForeground(provider === "claude" ? "claude" : "codex", threadId);
|
|
386
|
+
};
|
|
387
|
+
const handleOptimisticRequest = async (request, publicThreadId) => {
|
|
388
|
+
const sides = optimisticSideThreads;
|
|
389
|
+
sides.attach(publicThreadId, connectionId);
|
|
390
|
+
subscriptions.subscribe(publicThreadId, connectionId, notificationSink, serverRequestSink);
|
|
391
|
+
const snapshot = sides.snapshot(publicThreadId);
|
|
392
|
+
const publicParams = (request.params ?? {});
|
|
393
|
+
if (request.method === "thread/read") {
|
|
394
|
+
if (sides.phase(publicThreadId) !== "ready") {
|
|
395
|
+
sendResult(request.id, { thread: snapshot.thread });
|
|
396
|
+
reportOptimisticFailure(publicThreadId);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (request.method === "thread/resume") {
|
|
401
|
+
if (sides.phase(publicThreadId) !== "ready") {
|
|
402
|
+
sendResult(request.id, {
|
|
403
|
+
...snapshot,
|
|
404
|
+
initialTurnsPage: null,
|
|
405
|
+
});
|
|
406
|
+
reportOptimisticFailure(publicThreadId);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (request.method === "thread/turns/list") {
|
|
411
|
+
if (sides.phase(publicThreadId) !== "ready") {
|
|
412
|
+
sendResult(request.id, { data: [], nextCursor: null, backwardsCursor: null });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (request.method === "thread/items/list") {
|
|
417
|
+
if (sides.phase(publicThreadId) !== "ready") {
|
|
418
|
+
sendResult(request.id, { data: [], nextCursor: null, backwardsCursor: null });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (request.method === "thread/goal/get" && sides.phase(publicThreadId) !== "ready") {
|
|
423
|
+
sendResult(request.id, { goal: null });
|
|
424
|
+
reportOptimisticFailure(publicThreadId);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (request.method === "thread/unsubscribe") {
|
|
428
|
+
subscriptions.unsubscribe(publicThreadId, connectionId);
|
|
429
|
+
await sides.delete(publicThreadId);
|
|
430
|
+
sendResult(request.id, { status: "unsubscribed" });
|
|
431
|
+
clearForeground(publicThreadId);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (request.method === "thread/delete") {
|
|
435
|
+
await sides.delete(publicThreadId);
|
|
436
|
+
subscriptions.unsubscribe(publicThreadId, connectionId);
|
|
437
|
+
sendResult(request.id, {});
|
|
438
|
+
clearForeground(publicThreadId);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (request.method === "thread/inject_items") {
|
|
442
|
+
const queued = sides.run(publicThreadId, async (target) => {
|
|
443
|
+
const params = { ...publicParams, threadId: target.backendThreadId };
|
|
444
|
+
try {
|
|
445
|
+
if (target.provider === "claude") {
|
|
446
|
+
await claude.injectItems(params);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
await optimisticStockRequest(request.method, params);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (value) {
|
|
453
|
+
const error = value instanceof Error ? value : new Error(String(value));
|
|
454
|
+
sides.fail(publicThreadId, error);
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
sendResult(request.id, {});
|
|
459
|
+
void queued.catch(() => reportOptimisticFailure(publicThreadId));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
await sides.run(publicThreadId, async (target) => {
|
|
464
|
+
const params = { ...publicParams, threadId: target.backendThreadId };
|
|
465
|
+
if (request.method === "thread/read") {
|
|
466
|
+
const result = target.provider === "claude"
|
|
467
|
+
? claude.readThread(target.backendThreadId, Boolean(publicParams.includeTurns))
|
|
468
|
+
: await optimisticStockRequest("thread/read", params);
|
|
469
|
+
sendResult(request.id, projectSideResult(publicThreadId, target, result));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (request.method === "thread/resume") {
|
|
473
|
+
if (target.provider === "claude") {
|
|
474
|
+
subscriptions.mute(publicThreadId, connectionId);
|
|
475
|
+
try {
|
|
476
|
+
const prepared = await claude.prepareResume(params);
|
|
477
|
+
const snapshotHighWatermark = claude.eventHighWatermark(target.backendThreadId);
|
|
478
|
+
const tokenUsage = claude.latestTokenUsage(target.backendThreadId);
|
|
479
|
+
sendResult(request.id, projectSideResult(publicThreadId, target, prepared.response));
|
|
480
|
+
subscriptions.unmute(publicThreadId, connectionId);
|
|
481
|
+
if (tokenUsage && tokenUsage.sequence <= snapshotHighWatermark) {
|
|
482
|
+
notificationSink(tokenUsage.method, projectSideParams(publicThreadId, target, tokenUsage.params));
|
|
483
|
+
}
|
|
484
|
+
for (const event of claude.eventsAfter(target.backendThreadId, snapshotHighWatermark)) {
|
|
485
|
+
notificationSink(event.method, projectSideParams(publicThreadId, target, event.params));
|
|
486
|
+
}
|
|
487
|
+
await prepared.notifyGoalSnapshot((method, eventParams) => notificationSink(method, projectSideParams(publicThreadId, target, eventParams)));
|
|
488
|
+
await claude.replayPendingRequests(target.backendThreadId, connectionId);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
subscriptions.unmute(publicThreadId, connectionId);
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
sendResult(request.id, projectSideResult(publicThreadId, target, await optimisticStockRequest("thread/resume", params)));
|
|
497
|
+
}
|
|
498
|
+
selectForeground(target.provider === "claude" ? "claude" : "codex", publicThreadId);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (request.method === "thread/turns/list") {
|
|
502
|
+
const result = target.provider === "claude"
|
|
503
|
+
? claude.turnsPage(params)
|
|
504
|
+
: await optimisticStockRequest(request.method, params);
|
|
505
|
+
sendResult(request.id, projectSideResult(publicThreadId, target, result));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (request.method === "thread/items/list") {
|
|
509
|
+
const result = target.provider === "claude"
|
|
510
|
+
? claude.listItems(params)
|
|
511
|
+
: await optimisticStockRequest(request.method, params);
|
|
512
|
+
sendResult(request.id, projectSideResult(publicThreadId, target, result));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (request.method === "thread/goal/get") {
|
|
516
|
+
sendResult(request.id, target.provider === "claude"
|
|
517
|
+
? await claude.getGoal(target.backendThreadId)
|
|
518
|
+
: await optimisticStockRequest(request.method, params));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (request.method === "thread/name/set") {
|
|
522
|
+
sendResult(request.id, target.provider === "claude"
|
|
523
|
+
? await claude.setThreadName(params)
|
|
524
|
+
: await optimisticStockRequest(request.method, params));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (request.method === "thread/settings/update") {
|
|
528
|
+
if (target.provider === "claude") {
|
|
529
|
+
const before = claude.currentThreadSettings(target.backendThreadId);
|
|
530
|
+
const result = await claude.updateThreadSettings(params);
|
|
531
|
+
sendResult(request.id, result);
|
|
532
|
+
emitFastTransition(publicThreadId, before, claude.currentThreadSettings(target.backendThreadId), params.serviceTier);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
sendResult(request.id, await optimisticStockRequest(request.method, params));
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (request.method === "turn/start") {
|
|
540
|
+
if (target.provider === "claude") {
|
|
541
|
+
const turn = params;
|
|
542
|
+
const before = claude.currentThreadSettings(target.backendThreadId);
|
|
543
|
+
const prepared = await claude.prepareTurn(turn);
|
|
544
|
+
sendResult(request.id, prepared.response);
|
|
545
|
+
selectForeground("claude", publicThreadId);
|
|
546
|
+
emitFastTransition(publicThreadId, before, claude.currentThreadSettings(target.backendThreadId), turn.serviceTier);
|
|
547
|
+
await prepared.announce();
|
|
548
|
+
prepared.start();
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
sendResult(request.id, await optimisticStockRequest(request.method, params));
|
|
552
|
+
selectForeground("codex", publicThreadId);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (request.method === "turn/interrupt") {
|
|
557
|
+
if (target.provider === "claude") {
|
|
558
|
+
await claude.interruptTurn(params);
|
|
559
|
+
sendResult(request.id, {});
|
|
560
|
+
}
|
|
561
|
+
else
|
|
562
|
+
sendResult(request.id, await optimisticStockRequest(request.method, params));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (request.method === "turn/steer") {
|
|
566
|
+
sendResult(request.id, target.provider === "claude"
|
|
567
|
+
? await claude.steerTurn(params)
|
|
568
|
+
: await optimisticStockRequest(request.method, params));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (request.method === "thread/compact/start") {
|
|
572
|
+
sendResult(request.id, target.provider === "claude"
|
|
573
|
+
? await claude.compactThread(target.backendThreadId)
|
|
574
|
+
: await optimisticStockRequest(request.method, params));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (request.method === "thread/backgroundTerminals/clean") {
|
|
578
|
+
sendResult(request.id, target.provider === "claude"
|
|
579
|
+
? await claude.cleanBackgroundTerminals(params)
|
|
580
|
+
: await optimisticStockRequest(request.method, params));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (request.method === "thread/backgroundTerminals/list") {
|
|
584
|
+
sendResult(request.id, target.provider === "claude"
|
|
585
|
+
? await claude.listBackgroundTerminals(params)
|
|
586
|
+
: await optimisticStockRequest(request.method, params));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (request.method === "thread/backgroundTerminals/terminate") {
|
|
590
|
+
sendResult(request.id, target.provider === "claude"
|
|
591
|
+
? await claude.terminateBackgroundTerminal(params)
|
|
592
|
+
: await optimisticStockRequest(request.method, params));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (request.method === "thread/fork") {
|
|
596
|
+
let result;
|
|
597
|
+
if (target.provider === "claude") {
|
|
598
|
+
result = await claude.forkThread(params, publicThreadId);
|
|
599
|
+
sendResult(request.id, result);
|
|
600
|
+
subscribeClaude(result.thread.id);
|
|
601
|
+
await claude.announceThread(result.thread);
|
|
602
|
+
subscriptions.unaliasThread(target.backendThreadId);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
result = projectSideResult(publicThreadId, target, await optimisticStockRequest(request.method, params));
|
|
606
|
+
sendResult(request.id, result);
|
|
607
|
+
await stockSideThreads?.discardOptimistic(publicThreadId);
|
|
608
|
+
}
|
|
609
|
+
sides.forgetPromoted(publicThreadId);
|
|
610
|
+
selectForeground(target.provider === "claude" ? "claude" : "codex", result.thread.id);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
throw new RpcError(-32601, `Method '${request.method}' is not implemented for optimistic side threads yet.`);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
reportOptimisticFailure(publicThreadId);
|
|
618
|
+
const failure = rpcError(error);
|
|
619
|
+
if (request.method === "thread/inject_items")
|
|
620
|
+
sendResult(request.id, {});
|
|
621
|
+
else
|
|
622
|
+
sendError(request.id, failure.code, failure.message);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
284
625
|
client.on("message", async (data, isBinary) => {
|
|
285
626
|
recorder.frame(connectionId, "client_to_gateway", data, isBinary);
|
|
286
627
|
const message = isBinary ? undefined : parseRpcMessage(data);
|
|
@@ -292,6 +633,9 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
292
633
|
if (handoffs.resolveStockServerRequest
|
|
293
634
|
&& await handoffs.resolveStockServerRequest(internalServerRequestId, response))
|
|
294
635
|
return;
|
|
636
|
+
if (stockSideThreads
|
|
637
|
+
&& await stockSideThreads.resolveServerRequest(internalServerRequestId, response))
|
|
638
|
+
return;
|
|
295
639
|
if (!await claude.resolveServerRequest(internalServerRequestId, response)) {
|
|
296
640
|
logger.warn("claude.interaction.unknown-response", { connectionId, requestId: internalServerRequestId });
|
|
297
641
|
}
|
|
@@ -332,7 +676,10 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
332
676
|
if (message.method === "account/rateLimits/read" && !foreground)
|
|
333
677
|
diagnoseUnknownStatus("account/rateLimits/read");
|
|
334
678
|
if (message.method === "thread/list") {
|
|
335
|
-
|
|
679
|
+
const result = await mergedThreadList((message.params ?? {}), stockRpc, claude, cursors, handoffs, stockSideThreads);
|
|
680
|
+
sendResult(message.id, result);
|
|
681
|
+
for (const thread of result.data)
|
|
682
|
+
syncLogicalName(thread);
|
|
336
683
|
return;
|
|
337
684
|
}
|
|
338
685
|
if (message.method === "thread/loaded/list") {
|
|
@@ -353,6 +700,10 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
353
700
|
}
|
|
354
701
|
}
|
|
355
702
|
const params = (message.params ?? {});
|
|
703
|
+
if (params.threadId && optimisticSideThreads?.owns(params.threadId)) {
|
|
704
|
+
await handleOptimisticRequest(message, params.threadId);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
356
707
|
if (params.threadId && message.method === "turn/start"
|
|
357
708
|
&& (claude.ownsThread(params.threadId)
|
|
358
709
|
|| handoffs.logical?.(params.threadId)?.epoch.provider === "claude")) {
|
|
@@ -528,6 +879,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
528
879
|
subscriptions.subscribe(params.threadId, connectionId, notificationSink, serverRequestSink);
|
|
529
880
|
const handled = await handoffs.requestLogical(message.method, (message.params ?? {}), stockRpc);
|
|
530
881
|
sendResult(message.id, handled.result);
|
|
882
|
+
syncLogicalNameFromResult(handled.result);
|
|
531
883
|
selectForeground(handled.provider === "claude" ? "claude" : "codex", params.threadId);
|
|
532
884
|
await handled.after?.();
|
|
533
885
|
return;
|
|
@@ -547,6 +899,72 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
547
899
|
sendResult(message.id, await handoffs.forkSystemEphemeral(forkParams, stockRpc, connectionId));
|
|
548
900
|
return;
|
|
549
901
|
}
|
|
902
|
+
const logicalSource = handoffs.logical?.(forkParams.threadId);
|
|
903
|
+
if (logicalSource && isUserSideFork(forkParams)) {
|
|
904
|
+
const targetThreadId = uuidv7();
|
|
905
|
+
const snapshot = handoffs.sideSnapshot(forkParams, targetThreadId);
|
|
906
|
+
if (snapshot && optimisticSideThreads) {
|
|
907
|
+
openOptimisticSide(message.id, snapshot, logicalSource.epoch.provider, async () => {
|
|
908
|
+
if (logicalSource.epoch.provider === "claude") {
|
|
909
|
+
const result = await claude.forkThread({
|
|
910
|
+
...forkParams,
|
|
911
|
+
threadId: logicalSource.epoch.backendThreadId,
|
|
912
|
+
}, forkParams.threadId);
|
|
913
|
+
subscriptions.aliasThread(result.thread.id, targetThreadId);
|
|
914
|
+
claude.cancelEphemeralRelease(result.thread.id);
|
|
915
|
+
return { provider: "claude", backendThreadId: result.thread.id };
|
|
916
|
+
}
|
|
917
|
+
if (!stockSideThreads)
|
|
918
|
+
throw new Error("Stock side-chat support is unavailable.");
|
|
919
|
+
const prepared = await stockSideThreads.prepareOptimisticSide({ ...forkParams, threadId: logicalSource.epoch.backendThreadId }, forkParams.threadId, targetThreadId);
|
|
920
|
+
return { provider: "stock", backendThreadId: prepared.backendThreadId };
|
|
921
|
+
});
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
let result;
|
|
925
|
+
if (logicalSource.epoch.provider === "claude") {
|
|
926
|
+
result = await claude.forkThread({
|
|
927
|
+
...forkParams,
|
|
928
|
+
threadId: logicalSource.epoch.backendThreadId,
|
|
929
|
+
}, forkParams.threadId);
|
|
930
|
+
sendResult(message.id, result);
|
|
931
|
+
selectForeground("claude", result.thread.id);
|
|
932
|
+
subscribeClaude(result.thread.id);
|
|
933
|
+
await claude.announceThread(result.thread);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (!stockSideThreads)
|
|
937
|
+
throw new Error("Stock side-chat support is unavailable.");
|
|
938
|
+
result = await stockSideThreads.forkSide(connectionId, { ...forkParams, threadId: logicalSource.epoch.backendThreadId }, forkParams.threadId, stockRpc);
|
|
939
|
+
sendResult(message.id, result);
|
|
940
|
+
selectForeground("codex", result.thread.id);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (!logicalSource && isUserSideFork(forkParams) && optimisticSideThreads) {
|
|
944
|
+
const targetThreadId = uuidv7();
|
|
945
|
+
const sourceProvider = claude.ownsThread(forkParams.threadId) ? "claude" : "stock";
|
|
946
|
+
const completedParams = sourceProvider === "stock"
|
|
947
|
+
? stockState.completeForkParams(forkParams)
|
|
948
|
+
: forkParams;
|
|
949
|
+
const snapshot = sourceProvider === "claude"
|
|
950
|
+
? claude.sideSnapshot(forkParams.threadId, targetThreadId, forkParams.threadId)
|
|
951
|
+
: stockState.sideSnapshot(completedParams, targetThreadId);
|
|
952
|
+
if (snapshot) {
|
|
953
|
+
openOptimisticSide(message.id, snapshot, sourceProvider, async () => {
|
|
954
|
+
if (sourceProvider === "claude") {
|
|
955
|
+
const result = await claude.forkThread(completedParams, forkParams.threadId);
|
|
956
|
+
subscriptions.aliasThread(result.thread.id, targetThreadId);
|
|
957
|
+
claude.cancelEphemeralRelease(result.thread.id);
|
|
958
|
+
return { provider: "claude", backendThreadId: result.thread.id };
|
|
959
|
+
}
|
|
960
|
+
if (!stockSideThreads)
|
|
961
|
+
throw new Error("Stock side-chat support is unavailable.");
|
|
962
|
+
const prepared = await stockSideThreads.prepareOptimisticSide(completedParams, forkParams.threadId, targetThreadId);
|
|
963
|
+
return { provider: "stock", backendThreadId: prepared.backendThreadId };
|
|
964
|
+
});
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
550
968
|
const logicalFork = handoffs.forkLogical;
|
|
551
969
|
const overlay = handoffs.overlay(params.threadId);
|
|
552
970
|
if ((logicalFork && handoffs.logical?.(params.threadId)) || overlay?.epochs) {
|
|
@@ -910,106 +1328,119 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
910
1328
|
stock.send(message.data, { binary: message.isBinary });
|
|
911
1329
|
}
|
|
912
1330
|
});
|
|
1331
|
+
const closeClient = (code, reason) => {
|
|
1332
|
+
if (client.readyState === WebSocketState.OPEN)
|
|
1333
|
+
client.close(code, reason);
|
|
1334
|
+
};
|
|
913
1335
|
stock.on("message", (data, isBinary) => {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (message && stockRpc.handle(message))
|
|
921
|
-
return;
|
|
922
|
-
if (message && stockSideThreads)
|
|
923
|
-
message = stockSideThreads.projectMessage(connectionId, message);
|
|
924
|
-
let foregroundAfterForward;
|
|
925
|
-
if (message && "method" in message && !("id" in message) && message.method === "remoteControl/status/changed" && remoteControl) {
|
|
926
|
-
remoteControl.intercept(connectionId, notificationSink, message.params);
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
if (message && isResponse(message)) {
|
|
930
|
-
completeLatency(message.id, "stock");
|
|
931
|
-
const forwarded = forwardedRequests.get(requestKey(message.id));
|
|
932
|
-
forwardedRequests.delete(requestKey(message.id));
|
|
933
|
-
if (forwarded?.systemEphemeral && "result" in message) {
|
|
934
|
-
const result = message.result && typeof message.result === "object"
|
|
935
|
-
? message.result
|
|
936
|
-
: undefined;
|
|
937
|
-
if (typeof result?.thread?.id === "string") {
|
|
938
|
-
handoffs.registerForwardedEphemeralCandidate(connectionId, result.thread.id, forwarded.systemEphemeral);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if (forwarded?.threadId && isUuid(forwarded.threadId) && "error" in message) {
|
|
942
|
-
emitSystemError(forwarded.threadId, message.error.message);
|
|
943
|
-
}
|
|
944
|
-
if (forwarded && "result" in message) {
|
|
945
|
-
const result = message.result && typeof message.result === "object"
|
|
946
|
-
? message.result
|
|
947
|
-
: undefined;
|
|
948
|
-
const resultThreadId = typeof result?.thread?.id === "string" ? result.thread.id : forwarded.threadId;
|
|
949
|
-
if (forwarded.foreground && resultThreadId) {
|
|
950
|
-
handoffs.observeDurableThread(connectionId, "stock");
|
|
951
|
-
foregroundAfterForward = () => selectForeground(forwarded.foreground, resultThreadId);
|
|
952
|
-
}
|
|
953
|
-
else if (forwarded.clearForeground && forwarded.threadId) {
|
|
954
|
-
foregroundAfterForward = () => clearForeground(forwarded.threadId);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
if (message && "method" in message) {
|
|
959
|
-
if (!("id" in message) && message.method === "account/rateLimits/updated" && foreground?.provider === "claude") {
|
|
960
|
-
publishForegroundRateLimits(foregroundGeneration);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (!("id" in message) && message.method === "account/rateLimits/updated" && !foreground) {
|
|
964
|
-
diagnoseUnknownStatus("account/rateLimits/updated");
|
|
965
|
-
}
|
|
966
|
-
if (handoffs.captureInternalStockMessage(connectionId, message)) {
|
|
967
|
-
if (isRequest(message))
|
|
968
|
-
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
969
|
-
return;
|
|
1336
|
+
try {
|
|
1337
|
+
const incoming = isBinary ? undefined : parseRpcMessage(data);
|
|
1338
|
+
let message = incoming;
|
|
1339
|
+
if (message) {
|
|
1340
|
+
stockState.observeResponse(connectionId, message);
|
|
1341
|
+
stockState.observeNotification(message);
|
|
970
1342
|
}
|
|
971
|
-
|
|
972
|
-
if (rewritten) {
|
|
973
|
-
for (const output of rewritten)
|
|
974
|
-
sendJson(output);
|
|
1343
|
+
if (message && stockRpc.handle(message))
|
|
975
1344
|
return;
|
|
1345
|
+
if (message && stockSideThreads) {
|
|
1346
|
+
const projection = stockSideThreads.projectMessage(connectionId, message);
|
|
1347
|
+
if (projection.kind === "drop")
|
|
1348
|
+
return;
|
|
1349
|
+
message = projection.message;
|
|
976
1350
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1351
|
+
let foregroundAfterForward;
|
|
1352
|
+
if (message && "method" in message && !("id" in message) && message.method === "remoteControl/status/changed" && remoteControl) {
|
|
1353
|
+
remoteControl.intercept(connectionId, notificationSink, message.params);
|
|
980
1354
|
return;
|
|
981
1355
|
}
|
|
982
|
-
if (
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1356
|
+
if (message && isResponse(message)) {
|
|
1357
|
+
completeLatency(message.id, "stock");
|
|
1358
|
+
const forwarded = forwardedRequests.get(requestKey(message.id));
|
|
1359
|
+
forwardedRequests.delete(requestKey(message.id));
|
|
1360
|
+
if (forwarded?.systemEphemeral && "result" in message) {
|
|
1361
|
+
const result = message.result && typeof message.result === "object"
|
|
1362
|
+
? message.result
|
|
1363
|
+
: undefined;
|
|
1364
|
+
if (typeof result?.thread?.id === "string") {
|
|
1365
|
+
handoffs.registerForwardedEphemeralCandidate(connectionId, result.thread.id, forwarded.systemEphemeral);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (forwarded?.threadId && isUuid(forwarded.threadId) && "error" in message) {
|
|
1369
|
+
emitSystemError(forwarded.threadId, message.error.message);
|
|
1370
|
+
}
|
|
1371
|
+
if (forwarded && "result" in message) {
|
|
1372
|
+
const result = message.result && typeof message.result === "object"
|
|
1373
|
+
? message.result
|
|
1374
|
+
: undefined;
|
|
1375
|
+
const resultThreadId = typeof result?.thread?.id === "string" ? result.thread.id : forwarded.threadId;
|
|
1376
|
+
if (forwarded.foreground && resultThreadId) {
|
|
1377
|
+
handoffs.observeDurableThread(connectionId, "stock");
|
|
1378
|
+
foregroundAfterForward = () => selectForeground(forwarded.foreground, resultThreadId);
|
|
1379
|
+
}
|
|
1380
|
+
else if (forwarded.clearForeground && forwarded.threadId) {
|
|
1381
|
+
foregroundAfterForward = () => clearForeground(forwarded.threadId);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
986
1384
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1385
|
+
if (message && "method" in message) {
|
|
1386
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && foreground?.provider === "claude") {
|
|
1387
|
+
publishForegroundRateLimits(foregroundGeneration);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && !foreground) {
|
|
1391
|
+
diagnoseUnknownStatus("account/rateLimits/updated");
|
|
1392
|
+
}
|
|
1393
|
+
if (handoffs.captureInternalStockMessage(connectionId, message)) {
|
|
1394
|
+
if (isRequest(message))
|
|
1395
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const rewritten = handoffs.rewriteTitleMessages(message);
|
|
1399
|
+
if (rewritten) {
|
|
1400
|
+
for (const output of rewritten)
|
|
1401
|
+
sendJson(output);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (handoffs.suppressStockTargetMessage(connectionId, message)) {
|
|
1405
|
+
if (isRequest(message))
|
|
1406
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (handoffs.ownsStockBackendMessage?.(message)) {
|
|
1410
|
+
if (isRequest(message))
|
|
1411
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const params = message.params && typeof message.params === "object" ? message.params : undefined;
|
|
1415
|
+
const nestedThread = params?.thread && typeof params.thread === "object"
|
|
1416
|
+
? params.thread.id
|
|
997
1417
|
: undefined;
|
|
998
|
-
|
|
999
|
-
|
|
1418
|
+
const threadId = typeof params?.threadId === "string"
|
|
1419
|
+
? params.threadId
|
|
1420
|
+
: typeof nestedThread === "string" ? nestedThread : undefined;
|
|
1421
|
+
if (message.method === "error" && threadId) {
|
|
1422
|
+
const error = params?.error && typeof params.error === "object"
|
|
1423
|
+
? params.error
|
|
1424
|
+
: undefined;
|
|
1425
|
+
if (typeof error?.message === "string")
|
|
1426
|
+
emitSystemError(threadId, error.message);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (client.readyState === WebSocketState.OPEN) {
|
|
1430
|
+
const outbound = message === incoming ? data : JSON.stringify(message);
|
|
1431
|
+
recorder.frame(connectionId, "gateway_to_client", outbound, isBinary && message === incoming);
|
|
1432
|
+
client.send(outbound, { binary: isBinary && message === incoming });
|
|
1000
1433
|
}
|
|
1434
|
+
foregroundAfterForward?.();
|
|
1001
1435
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
logger.error("connection.stock.message-failed", {
|
|
1438
|
+
connectionId,
|
|
1439
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error),
|
|
1440
|
+
});
|
|
1441
|
+
closeClient(1011, "CCodex failed to process a stock event");
|
|
1006
1442
|
}
|
|
1007
|
-
foregroundAfterForward?.();
|
|
1008
1443
|
});
|
|
1009
|
-
const closeClient = (code, reason) => {
|
|
1010
|
-
if (client.readyState === WebSocketState.OPEN)
|
|
1011
|
-
client.close(code, reason);
|
|
1012
|
-
};
|
|
1013
1444
|
stock.on("error", (error) => {
|
|
1014
1445
|
logger.error("connection.stock.error", { connectionId, error: error.message });
|
|
1015
1446
|
closeClient(1011, "Stock app-server connection failed");
|
|
@@ -1042,6 +1473,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
1042
1473
|
}
|
|
1043
1474
|
stockState.detach(connectionId);
|
|
1044
1475
|
stockSideThreads?.detachConnection(connectionId);
|
|
1476
|
+
optimisticSideThreads?.detachConnection(connectionId);
|
|
1045
1477
|
stockRpc.close(new Error("Client connection closed."));
|
|
1046
1478
|
if (stock.readyState === WebSocketState.OPEN || stock.readyState === WebSocketState.CONNECTING) {
|
|
1047
1479
|
stock.close();
|