@gkorepanov/ccodex 0.4.4 → 0.4.6
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 +4 -3
- package/dist/claude/service.js +23 -3
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/session.js +1 -8
- 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 +528 -101
- 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/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 +4 -2
- package/dist/handoff/service.js +88 -22
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/store.d.ts +6 -4
- package/dist/handoff/store.js +43 -19
- 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) {
|
|
@@ -589,17 +1007,12 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
589
1007
|
return;
|
|
590
1008
|
}
|
|
591
1009
|
if (params.threadId && message.method === "thread/rollback" && handoffs.logical?.(params.threadId)) {
|
|
592
|
-
sendResult(message.id, await handoffs.
|
|
1010
|
+
sendResult(message.id, await handoffs.rollbackLogicalThread((message.params ?? {}), stockRpc, connectionId));
|
|
593
1011
|
return;
|
|
594
1012
|
}
|
|
595
1013
|
if (params.threadId && handoffs.overlay(params.threadId)) {
|
|
596
1014
|
if (message.method === "thread/rollback") {
|
|
597
1015
|
const rollback = (message.params ?? {});
|
|
598
|
-
const logicalRollback = handoffs.rollbackLogicalFork;
|
|
599
|
-
if (logicalRollback) {
|
|
600
|
-
sendResult(message.id, await logicalRollback.call(handoffs, rollback, stockRpc, connectionId));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
1016
|
const overlay = handoffs.overlay(params.threadId);
|
|
604
1017
|
if (overlay.epochs) {
|
|
605
1018
|
const turns = overlay.epochs.flatMap((epoch) => epoch.publicTurnIds.map((turnId) => ({ epoch, turnId })));
|
|
@@ -910,106 +1323,119 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
910
1323
|
stock.send(message.data, { binary: message.isBinary });
|
|
911
1324
|
}
|
|
912
1325
|
});
|
|
1326
|
+
const closeClient = (code, reason) => {
|
|
1327
|
+
if (client.readyState === WebSocketState.OPEN)
|
|
1328
|
+
client.close(code, reason);
|
|
1329
|
+
};
|
|
913
1330
|
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;
|
|
1331
|
+
try {
|
|
1332
|
+
const incoming = isBinary ? undefined : parseRpcMessage(data);
|
|
1333
|
+
let message = incoming;
|
|
1334
|
+
if (message) {
|
|
1335
|
+
stockState.observeResponse(connectionId, message);
|
|
1336
|
+
stockState.observeNotification(message);
|
|
970
1337
|
}
|
|
971
|
-
|
|
972
|
-
if (rewritten) {
|
|
973
|
-
for (const output of rewritten)
|
|
974
|
-
sendJson(output);
|
|
1338
|
+
if (message && stockRpc.handle(message))
|
|
975
1339
|
return;
|
|
1340
|
+
if (message && stockSideThreads) {
|
|
1341
|
+
const projection = stockSideThreads.projectMessage(connectionId, message);
|
|
1342
|
+
if (projection.kind === "drop")
|
|
1343
|
+
return;
|
|
1344
|
+
message = projection.message;
|
|
976
1345
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1346
|
+
let foregroundAfterForward;
|
|
1347
|
+
if (message && "method" in message && !("id" in message) && message.method === "remoteControl/status/changed" && remoteControl) {
|
|
1348
|
+
remoteControl.intercept(connectionId, notificationSink, message.params);
|
|
980
1349
|
return;
|
|
981
1350
|
}
|
|
982
|
-
if (
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1351
|
+
if (message && isResponse(message)) {
|
|
1352
|
+
completeLatency(message.id, "stock");
|
|
1353
|
+
const forwarded = forwardedRequests.get(requestKey(message.id));
|
|
1354
|
+
forwardedRequests.delete(requestKey(message.id));
|
|
1355
|
+
if (forwarded?.systemEphemeral && "result" in message) {
|
|
1356
|
+
const result = message.result && typeof message.result === "object"
|
|
1357
|
+
? message.result
|
|
1358
|
+
: undefined;
|
|
1359
|
+
if (typeof result?.thread?.id === "string") {
|
|
1360
|
+
handoffs.registerForwardedEphemeralCandidate(connectionId, result.thread.id, forwarded.systemEphemeral);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (forwarded?.threadId && isUuid(forwarded.threadId) && "error" in message) {
|
|
1364
|
+
emitSystemError(forwarded.threadId, message.error.message);
|
|
1365
|
+
}
|
|
1366
|
+
if (forwarded && "result" in message) {
|
|
1367
|
+
const result = message.result && typeof message.result === "object"
|
|
1368
|
+
? message.result
|
|
1369
|
+
: undefined;
|
|
1370
|
+
const resultThreadId = typeof result?.thread?.id === "string" ? result.thread.id : forwarded.threadId;
|
|
1371
|
+
if (forwarded.foreground && resultThreadId) {
|
|
1372
|
+
handoffs.observeDurableThread(connectionId, "stock");
|
|
1373
|
+
foregroundAfterForward = () => selectForeground(forwarded.foreground, resultThreadId);
|
|
1374
|
+
}
|
|
1375
|
+
else if (forwarded.clearForeground && forwarded.threadId) {
|
|
1376
|
+
foregroundAfterForward = () => clearForeground(forwarded.threadId);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
986
1379
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1380
|
+
if (message && "method" in message) {
|
|
1381
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && foreground?.provider === "claude") {
|
|
1382
|
+
publishForegroundRateLimits(foregroundGeneration);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && !foreground) {
|
|
1386
|
+
diagnoseUnknownStatus("account/rateLimits/updated");
|
|
1387
|
+
}
|
|
1388
|
+
if (handoffs.captureInternalStockMessage(connectionId, message)) {
|
|
1389
|
+
if (isRequest(message))
|
|
1390
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const rewritten = handoffs.rewriteTitleMessages(message);
|
|
1394
|
+
if (rewritten) {
|
|
1395
|
+
for (const output of rewritten)
|
|
1396
|
+
sendJson(output);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (handoffs.suppressStockTargetMessage(connectionId, message)) {
|
|
1400
|
+
if (isRequest(message))
|
|
1401
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (handoffs.ownsStockBackendMessage?.(message)) {
|
|
1405
|
+
if (isRequest(message))
|
|
1406
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const params = message.params && typeof message.params === "object" ? message.params : undefined;
|
|
1410
|
+
const nestedThread = params?.thread && typeof params.thread === "object"
|
|
1411
|
+
? params.thread.id
|
|
997
1412
|
: undefined;
|
|
998
|
-
|
|
999
|
-
|
|
1413
|
+
const threadId = typeof params?.threadId === "string"
|
|
1414
|
+
? params.threadId
|
|
1415
|
+
: typeof nestedThread === "string" ? nestedThread : undefined;
|
|
1416
|
+
if (message.method === "error" && threadId) {
|
|
1417
|
+
const error = params?.error && typeof params.error === "object"
|
|
1418
|
+
? params.error
|
|
1419
|
+
: undefined;
|
|
1420
|
+
if (typeof error?.message === "string")
|
|
1421
|
+
emitSystemError(threadId, error.message);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (client.readyState === WebSocketState.OPEN) {
|
|
1425
|
+
const outbound = message === incoming ? data : JSON.stringify(message);
|
|
1426
|
+
recorder.frame(connectionId, "gateway_to_client", outbound, isBinary && message === incoming);
|
|
1427
|
+
client.send(outbound, { binary: isBinary && message === incoming });
|
|
1000
1428
|
}
|
|
1429
|
+
foregroundAfterForward?.();
|
|
1001
1430
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
logger.error("connection.stock.message-failed", {
|
|
1433
|
+
connectionId,
|
|
1434
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error),
|
|
1435
|
+
});
|
|
1436
|
+
closeClient(1011, "CCodex failed to process a stock event");
|
|
1006
1437
|
}
|
|
1007
|
-
foregroundAfterForward?.();
|
|
1008
1438
|
});
|
|
1009
|
-
const closeClient = (code, reason) => {
|
|
1010
|
-
if (client.readyState === WebSocketState.OPEN)
|
|
1011
|
-
client.close(code, reason);
|
|
1012
|
-
};
|
|
1013
1439
|
stock.on("error", (error) => {
|
|
1014
1440
|
logger.error("connection.stock.error", { connectionId, error: error.message });
|
|
1015
1441
|
closeClient(1011, "Stock app-server connection failed");
|
|
@@ -1042,6 +1468,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
1042
1468
|
}
|
|
1043
1469
|
stockState.detach(connectionId);
|
|
1044
1470
|
stockSideThreads?.detachConnection(connectionId);
|
|
1471
|
+
optimisticSideThreads?.detachConnection(connectionId);
|
|
1045
1472
|
stockRpc.close(new Error("Client connection closed."));
|
|
1046
1473
|
if (stock.readyState === WebSocketState.OPEN || stock.readyState === WebSocketState.CONNECTING) {
|
|
1047
1474
|
stock.close();
|