@gkorepanov/ccodex 0.3.0 → 0.4.0
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 +141 -63
- package/assets/ccodex/goals/continuation.md +1 -0
- package/compatibility.json +5 -6
- package/config.example.toml +19 -4
- package/dist/claude/asyncQueue.d.ts +3 -0
- package/dist/claude/asyncQueue.js +13 -2
- package/dist/claude/asyncQueue.js.map +1 -1
- package/dist/claude/commandActions.d.ts +6 -0
- package/dist/claude/commandActions.js +57 -0
- package/dist/claude/commandActions.js.map +1 -0
- package/dist/claude/compactCommand.d.ts +2 -0
- package/dist/claude/compactCommand.js +7 -0
- package/dist/claude/compactCommand.js.map +1 -0
- package/dist/claude/goalTools.d.ts +15 -0
- package/dist/claude/goalTools.js +128 -0
- package/dist/claude/goalTools.js.map +1 -0
- package/dist/claude/hookMapper.d.ts +17 -4
- package/dist/claude/hookMapper.js +7 -5
- package/dist/claude/hookMapper.js.map +1 -1
- package/dist/claude/inputMapper.d.ts +1 -0
- package/dist/claude/inputMapper.js +1 -0
- package/dist/claude/inputMapper.js.map +1 -1
- package/dist/claude/rateLimits.d.ts +72 -0
- package/dist/claude/rateLimits.js +330 -0
- package/dist/claude/rateLimits.js.map +1 -0
- package/dist/claude/responseItemValidation.d.ts +2 -0
- package/dist/claude/responseItemValidation.js +40 -0
- package/dist/claude/responseItemValidation.js.map +1 -0
- package/dist/claude/resultClassifier.d.ts +8 -2
- package/dist/claude/resultClassifier.js +9 -1
- package/dist/claude/resultClassifier.js.map +1 -1
- package/dist/claude/service.d.ts +116 -29
- package/dist/claude/service.js +1337 -680
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/backgroundOutput.d.ts +2 -0
- package/dist/claude/session/backgroundOutput.js +29 -0
- package/dist/claude/session/backgroundOutput.js.map +1 -0
- package/dist/claude/session/commands.d.ts +862 -0
- package/dist/claude/session/commands.js +2 -0
- package/dist/claude/session/commands.js.map +1 -0
- package/dist/claude/session/goalState.d.ts +49 -0
- package/dist/claude/session/goalState.js +407 -0
- package/dist/claude/session/goalState.js.map +1 -0
- package/dist/claude/session/mailbox.d.ts +52 -0
- package/dist/claude/session/mailbox.js +190 -0
- package/dist/claude/session/mailbox.js.map +1 -0
- package/dist/claude/session/outputAdapter.d.ts +11 -0
- package/dist/claude/session/outputAdapter.js +31 -0
- package/dist/claude/session/outputAdapter.js.map +1 -0
- package/dist/claude/session/permissionUpdates.d.ts +4 -0
- package/dist/claude/session/permissionUpdates.js +17 -0
- package/dist/claude/session/permissionUpdates.js.map +1 -0
- package/dist/claude/session/providerFacts.d.ts +47 -0
- package/dist/claude/session/providerFacts.js +84 -0
- package/dist/claude/session/providerFacts.js.map +1 -0
- package/dist/claude/session/providerRuntime.d.ts +1 -0
- package/dist/claude/session/providerRuntime.js +2 -0
- package/dist/claude/session/providerRuntime.js.map +1 -0
- package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
- package/dist/claude/session/providerRuntimeFactory.js +122 -0
- package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
- package/dist/claude/session/repository.d.ts +57 -0
- package/dist/claude/session/repository.js +161 -0
- package/dist/claude/session/repository.js.map +1 -0
- package/dist/claude/session/runtime.d.ts +46 -0
- package/dist/claude/session/runtime.js +140 -0
- package/dist/claude/session/runtime.js.map +1 -0
- package/dist/claude/session/scopeState.d.ts +61 -0
- package/dist/claude/session/scopeState.js +74 -0
- package/dist/claude/session/scopeState.js.map +1 -0
- package/dist/claude/session/session.d.ts +285 -0
- package/dist/claude/session/session.js +6778 -0
- package/dist/claude/session/session.js.map +1 -0
- package/dist/claude/session/shellRunner.d.ts +13 -0
- package/dist/claude/session/shellRunner.js +103 -0
- package/dist/claude/session/shellRunner.js.map +1 -0
- package/dist/claude/session/usage.d.ts +2 -0
- package/dist/claude/session/usage.js +10 -0
- package/dist/claude/session/usage.js.map +1 -0
- package/dist/claude/sessionRegistry.d.ts +26 -0
- package/dist/claude/sessionRegistry.js +118 -0
- package/dist/claude/sessionRegistry.js.map +1 -0
- package/dist/claude/shellSupervisor.d.ts +1 -0
- package/dist/claude/shellSupervisor.js +69 -0
- package/dist/claude/shellSupervisor.js.map +1 -0
- package/dist/claude/statusCommand.d.ts +21 -0
- package/dist/claude/statusCommand.js +97 -0
- package/dist/claude/statusCommand.js.map +1 -0
- package/dist/claude/toolMapper.d.ts +9 -1
- package/dist/claude/toolMapper.js +98 -18
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/claude/transcriptBrancher.d.ts +37 -0
- package/dist/claude/transcriptBrancher.js +103 -0
- package/dist/claude/transcriptBrancher.js.map +1 -0
- package/dist/cli/delegate.js +23 -1
- package/dist/cli/delegate.js.map +1 -1
- package/dist/cli/main.js +28 -12
- package/dist/cli/main.js.map +1 -1
- package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
- package/dist/compatibility/probe.js +11 -6
- package/dist/compatibility/probe.js.map +1 -1
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +103 -11
- package/dist/config/config.js.map +1 -1
- package/dist/daemon/daemon.js +91 -43
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ownership.d.ts +15 -0
- package/dist/daemon/ownership.js +185 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/probe.js +1 -1
- package/dist/daemon/supervisor.d.ts +3 -0
- package/dist/daemon/supervisor.js +37 -1
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/gateway/clientConnection.d.ts +7 -1
- package/dist/gateway/clientConnection.js +656 -60
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/logicalThreadProjection.d.ts +8 -0
- package/dist/gateway/logicalThreadProjection.js +59 -0
- package/dist/gateway/logicalThreadProjection.js.map +1 -0
- package/dist/gateway/remoteRelay.js +0 -1
- package/dist/gateway/remoteRelay.js.map +1 -1
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +69 -11
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/stockRpc.d.ts +5 -2
- package/dist/gateway/stockRpc.js +52 -6
- package/dist/gateway/stockRpc.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +19 -3
- package/dist/gateway/subscriptions.js +152 -13
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/threadList.d.ts +7 -2
- package/dist/gateway/threadList.js +8 -4
- package/dist/gateway/threadList.js.map +1 -1
- package/dist/gateway/transientNotice.d.ts +15 -0
- package/dist/gateway/transientNotice.js +96 -1
- package/dist/gateway/transientNotice.js.map +1 -1
- package/dist/handoff/providerEpochs.d.ts +18 -0
- package/dist/handoff/providerEpochs.js +77 -0
- package/dist/handoff/providerEpochs.js.map +1 -0
- package/dist/handoff/service.d.ts +86 -6
- package/dist/handoff/service.js +1276 -168
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/store.d.ts +189 -2
- package/dist/handoff/store.js +618 -7
- package/dist/handoff/store.js.map +1 -1
- package/dist/handoff/titleGeneration.d.ts +28 -0
- package/dist/handoff/titleGeneration.js +138 -0
- package/dist/handoff/titleGeneration.js.map +1 -0
- package/dist/management/doctor.d.ts +1 -1
- package/dist/management/doctor.js +32 -23
- package/dist/management/doctor.js.map +1 -1
- package/dist/management/lifecycle.js +14 -2
- package/dist/management/lifecycle.js.map +1 -1
- package/dist/management/remoteShim.d.ts +10 -0
- package/dist/management/remoteShim.js +65 -0
- package/dist/management/remoteShim.js.map +1 -0
- package/dist/management/setup.d.ts +5 -0
- package/dist/management/setup.js +120 -39
- package/dist/management/setup.js.map +1 -1
- package/dist/observability/metrics.d.ts +2 -0
- package/dist/observability/metrics.js +6 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.js +4 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/runtime/dependencies.js +3 -3
- package/dist/runtime/dependencies.js.map +1 -1
- package/dist/runtime/providerAvailability.d.ts +24 -0
- package/dist/runtime/providerAvailability.js +107 -0
- package/dist/runtime/providerAvailability.js.map +1 -0
- package/dist/state/stateCommand.d.ts +21 -0
- package/dist/state/stateCommand.js +198 -0
- package/dist/state/stateCommand.js.map +1 -0
- package/dist/state/stockStateTracker.d.ts +16 -0
- package/dist/state/stockStateTracker.js +137 -0
- package/dist/state/stockStateTracker.js.map +1 -0
- package/dist/store/HybridStore.d.ts +118 -2
- package/dist/store/HybridStore.js +23 -1
- package/dist/store/HybridStore.js.map +1 -1
- package/dist/store/memoryStore.d.ts +57 -7
- package/dist/store/memoryStore.js +337 -5
- package/dist/store/memoryStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +30 -4
- package/dist/store/sqliteStore.js +395 -42
- package/dist/store/sqliteStore.js.map +1 -1
- package/install.sh +2 -2
- package/{LICENSES.md → legal/LICENSES.md} +2 -2
- package/{THIRD_PARTY_NOTICES.md → legal/THIRD_PARTY_NOTICES.md} +177 -148
- package/package.json +28 -18
- package/vendor/codex/continuation.md +51 -0
- package/BUILD_BENCHMARKS.md +0 -27
- package/RELEASING.md +0 -98
- package/SSH_VERIFY.md +0 -69
- package/dist/claude/interactions.d.ts +0 -23
- package/dist/claude/interactions.js +0 -77
- package/dist/claude/interactions.js.map +0 -1
- package/dist/claude/sessionRuntime.d.ts +0 -102
- package/dist/claude/sessionRuntime.js +0 -1361
- package/dist/claude/sessionRuntime.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v7 as uuidv7 } from "uuid";
|
|
1
|
+
import { validate as isUuid, v7 as uuidv7 } from "uuid";
|
|
2
2
|
import { WebSocket as WebSocketState } from "ws";
|
|
3
3
|
import { connectStock } from "../codex/stockConnection.js";
|
|
4
4
|
import { isRequest, isResponse, parseRpcMessage } from "../protocol/envelopes.js";
|
|
@@ -7,11 +7,44 @@ 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 { transientSystemNotice } from "./transientNotice.js";
|
|
11
|
-
|
|
10
|
+
import { providerMigrationCompleted, providerMigrationFailed, providerMigrationNotice, transientCommandNotice, transientSystemNotice, } from "./transientNotice.js";
|
|
11
|
+
import { rateLimitNotifications } from "../claude/rateLimits.js";
|
|
12
|
+
import { formatCCodexStatus, isCCodexStatusCommand } from "../claude/statusCommand.js";
|
|
13
|
+
import { claudeCompactCommand } from "../claude/compactCommand.js";
|
|
14
|
+
import { DEFAULT_FEATURES } from "../config/config.js";
|
|
15
|
+
import { providerUnavailableMessage } from "../runtime/providerAvailability.js";
|
|
16
|
+
import { formatCCodexState, isCCodexStateCommand } from "../state/stateCommand.js";
|
|
17
|
+
import { StockStateTracker } from "../state/stockStateTracker.js";
|
|
18
|
+
function requestedFast(serviceTier) {
|
|
19
|
+
return serviceTier === "fast" || serviceTier === "priority";
|
|
20
|
+
}
|
|
21
|
+
function claudeModelNoticeName(model) {
|
|
22
|
+
const name = model.replace(/^claude:/u, "").replace(/^claude-/u, "").split("-")[0] ?? model;
|
|
23
|
+
return `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
24
|
+
}
|
|
25
|
+
function fastTransitionNotice(before, after, requestedTier) {
|
|
26
|
+
const effectiveRequest = requestedTier === undefined ? before?.serviceTier : requestedTier;
|
|
27
|
+
if (before && !requestedFast(before.serviceTier) && requestedFast(after.serviceTier)) {
|
|
28
|
+
return "Fast mode is on — usage limits may be consumed faster.";
|
|
29
|
+
}
|
|
30
|
+
if (requestedFast(effectiveRequest) && !requestedFast(after.serviceTier)) {
|
|
31
|
+
return `Fast was requested, but ${claudeModelNoticeName(after.model)} does not support it — continuing in Standard.`;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function attachClientConnection(client, stockSocket, claudeModels, claude, handoffs, subscriptions, logger, cursors, metrics, recorder, remoteControl, features = DEFAULT_FEATURES, providerAvailability, sharedStockState) {
|
|
12
36
|
const connectionId = uuidv7();
|
|
37
|
+
let resolveClosed;
|
|
38
|
+
let rejectClosed;
|
|
39
|
+
const closed = new Promise((resolve, reject) => {
|
|
40
|
+
resolveClosed = resolve;
|
|
41
|
+
rejectClosed = reject;
|
|
42
|
+
});
|
|
43
|
+
void closed.catch(() => undefined);
|
|
44
|
+
let connectionAlive = true;
|
|
13
45
|
const stock = connectStock(stockSocket);
|
|
14
46
|
const stockRpc = new StockRpc(stock);
|
|
47
|
+
const stockState = sharedStockState ?? new StockStateTracker();
|
|
15
48
|
const queued = [];
|
|
16
49
|
const requestStarted = new Map();
|
|
17
50
|
const forwardedRequests = new Map();
|
|
@@ -32,9 +65,23 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
32
65
|
const params = message.params && typeof message.params === "object"
|
|
33
66
|
? message.params
|
|
34
67
|
: undefined;
|
|
68
|
+
const systemEphemeral = (message.method === "thread/start" || message.method === "thread/fork")
|
|
69
|
+
&& params?.ephemeral === true
|
|
70
|
+
&& (params.threadSource === "system" || (message.method === "thread/start" && params.threadSource !== "user"));
|
|
71
|
+
const internalEphemeralThread = typeof params?.threadId === "string"
|
|
72
|
+
&& handoffs.ownsSystemEphemeral(connectionId, params.threadId);
|
|
73
|
+
const foreground = !systemEphemeral && !internalEphemeralThread
|
|
74
|
+
&& ["thread/start", "thread/resume", "thread/fork", "turn/start"].includes(message.method)
|
|
75
|
+
? "codex"
|
|
76
|
+
: undefined;
|
|
35
77
|
forwardedRequests.set(requestKey(message.id), {
|
|
36
78
|
method: message.method,
|
|
37
79
|
...(typeof params?.threadId === "string" ? { threadId: params.threadId } : {}),
|
|
80
|
+
...(systemEphemeral
|
|
81
|
+
? { systemEphemeral: message.params }
|
|
82
|
+
: {}),
|
|
83
|
+
...(foreground ? { foreground } : {}),
|
|
84
|
+
...(["thread/delete", "thread/unsubscribe"].includes(message.method) ? { clearForeground: true } : {}),
|
|
38
85
|
});
|
|
39
86
|
};
|
|
40
87
|
metrics.connectionOpened();
|
|
@@ -56,6 +103,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
56
103
|
emitNotice(notice);
|
|
57
104
|
return notice;
|
|
58
105
|
};
|
|
106
|
+
const emitFastTransition = (threadId, before, after, requestedTier) => {
|
|
107
|
+
const text = fastTransitionNotice(before, after, requestedTier);
|
|
108
|
+
if (text)
|
|
109
|
+
emitTransientNotice(threadId, text);
|
|
110
|
+
};
|
|
59
111
|
const emitSystemError = (threadId, message) => {
|
|
60
112
|
const now = Date.now();
|
|
61
113
|
const key = `${threadId}\u0000${message}`;
|
|
@@ -78,6 +130,114 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
78
130
|
completeLatency(id, "claude");
|
|
79
131
|
sendJson({ id, error: { code, message: error instanceof Error ? error.message : String(error) } });
|
|
80
132
|
};
|
|
133
|
+
let foreground;
|
|
134
|
+
let foregroundGeneration = 0;
|
|
135
|
+
let unknownStatusDiagnosed = false;
|
|
136
|
+
const emitRateLimits = (response) => {
|
|
137
|
+
for (const notification of rateLimitNotifications(response))
|
|
138
|
+
sendJson(notification);
|
|
139
|
+
};
|
|
140
|
+
const emitStockRateLimits = (value) => {
|
|
141
|
+
if (!value || typeof value !== "object")
|
|
142
|
+
return;
|
|
143
|
+
const result = value;
|
|
144
|
+
const buckets = result.rateLimitsByLimitId ? Object.values(result.rateLimitsByLimitId) : [];
|
|
145
|
+
const snapshots = buckets.length > 0 ? buckets : result.rateLimits ? [result.rateLimits] : [];
|
|
146
|
+
for (const rateLimits of snapshots)
|
|
147
|
+
sendJson({ method: "account/rateLimits/updated", params: { rateLimits } });
|
|
148
|
+
};
|
|
149
|
+
const combinedStatus = async (threadId) => {
|
|
150
|
+
const availability = providerAvailability
|
|
151
|
+
? await providerAvailability.refreshAll()
|
|
152
|
+
: {
|
|
153
|
+
claude: { provider: "claude", state: "ready" },
|
|
154
|
+
codex: { provider: "codex", state: "ready" },
|
|
155
|
+
};
|
|
156
|
+
const [claudeUsage, codexRateLimits] = await Promise.all([
|
|
157
|
+
availability.claude.state === "ready"
|
|
158
|
+
? claude.readRateLimitStatus(threadId).catch((error) => ({
|
|
159
|
+
rateLimits: claude.cachedRateLimits(),
|
|
160
|
+
unavailableReason: error instanceof Error ? error.message : String(error),
|
|
161
|
+
}))
|
|
162
|
+
: undefined,
|
|
163
|
+
availability.codex.state === "ready"
|
|
164
|
+
? stockRpc.request("account/rateLimits/read", undefined).catch(() => undefined)
|
|
165
|
+
: undefined,
|
|
166
|
+
]);
|
|
167
|
+
return formatCCodexStatus({
|
|
168
|
+
claude: { availability: availability.claude, ...(claudeUsage ? { usage: claudeUsage } : {}) },
|
|
169
|
+
codex: {
|
|
170
|
+
availability: availability.codex,
|
|
171
|
+
...(codexRateLimits ? { rateLimits: codexRateLimits } : {}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const threadState = async (threadId) => {
|
|
176
|
+
const logical = handoffs.logical?.(threadId);
|
|
177
|
+
if (logical?.epoch.provider === "claude") {
|
|
178
|
+
return formatCCodexState(claude.stateSnapshot(logical.epoch.backendThreadId));
|
|
179
|
+
}
|
|
180
|
+
if (claude.ownsThread(threadId))
|
|
181
|
+
return formatCCodexState(claude.stateSnapshot(threadId));
|
|
182
|
+
let response;
|
|
183
|
+
try {
|
|
184
|
+
response = logical
|
|
185
|
+
? (await handoffs.requestLogical("thread/read", { threadId, includeTurns: true }, stockRpc)).result
|
|
186
|
+
: await stockRpc.request("thread/read", { threadId, includeTurns: true });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
if (!message.includes("includeTurns is unavailable")
|
|
191
|
+
&& !message.includes("ephemeral threads do not support includeTurns"))
|
|
192
|
+
throw error;
|
|
193
|
+
response = logical
|
|
194
|
+
? (await handoffs.requestLogical("thread/read", { threadId, includeTurns: false }, stockRpc)).result
|
|
195
|
+
: await stockRpc.request("thread/read", { threadId, includeTurns: false });
|
|
196
|
+
}
|
|
197
|
+
return formatCCodexState(stockState.snapshot(response.thread));
|
|
198
|
+
};
|
|
199
|
+
const publishForegroundRateLimits = (generation) => {
|
|
200
|
+
const selected = foreground;
|
|
201
|
+
if (!selected)
|
|
202
|
+
return;
|
|
203
|
+
if (selected.provider === "claude") {
|
|
204
|
+
const backend = handoffs.logical?.(selected.threadId)?.epoch.backendThreadId ?? selected.threadId;
|
|
205
|
+
void claude.readRateLimits(backend).then((response) => {
|
|
206
|
+
if (foregroundGeneration === generation && foreground?.provider === "claude")
|
|
207
|
+
emitRateLimits(response);
|
|
208
|
+
}).catch((error) => logger.warn("claude.rate-limits.publish-failed", { connectionId, error: String(error) }));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
void (async () => {
|
|
212
|
+
if (providerAvailability && (await providerAvailability.read("codex")).state !== "ready")
|
|
213
|
+
return;
|
|
214
|
+
const response = await stockRpc.request("account/rateLimits/read", undefined);
|
|
215
|
+
if (foregroundGeneration === generation && foreground?.provider === "codex")
|
|
216
|
+
emitStockRateLimits(response);
|
|
217
|
+
})().catch((error) => logger.warn("codex.rate-limits.publish-failed", { connectionId, error: String(error) }));
|
|
218
|
+
};
|
|
219
|
+
const selectForeground = (provider, threadId) => {
|
|
220
|
+
foreground = { provider, threadId };
|
|
221
|
+
unknownStatusDiagnosed = false;
|
|
222
|
+
foregroundGeneration += 1;
|
|
223
|
+
publishForegroundRateLimits(foregroundGeneration);
|
|
224
|
+
};
|
|
225
|
+
const clearForeground = (threadId) => {
|
|
226
|
+
if (foreground?.threadId !== threadId)
|
|
227
|
+
return;
|
|
228
|
+
foreground = undefined;
|
|
229
|
+
foregroundGeneration += 1;
|
|
230
|
+
};
|
|
231
|
+
const diagnoseUnknownStatus = (signal) => {
|
|
232
|
+
if (unknownStatusDiagnosed)
|
|
233
|
+
return;
|
|
234
|
+
unknownStatusDiagnosed = true;
|
|
235
|
+
logger.warn("provider.status.unknown", {
|
|
236
|
+
connectionId,
|
|
237
|
+
signal,
|
|
238
|
+
behavior: "preserving provider-labelled stock payload until a foreground operation succeeds",
|
|
239
|
+
});
|
|
240
|
+
};
|
|
81
241
|
const notificationSink = (method, params) => {
|
|
82
242
|
if (method === "error" && params && typeof params === "object") {
|
|
83
243
|
const failure = params;
|
|
@@ -101,9 +261,23 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
101
261
|
const serverRequestSink = (id, method, params) => {
|
|
102
262
|
sendJson({ id: serverRequestIds.wireId(id), method, params });
|
|
103
263
|
};
|
|
104
|
-
subscriptions.attach(connectionId, notificationSink);
|
|
264
|
+
subscriptions.attach(connectionId, notificationSink, serverRequestSink);
|
|
265
|
+
claude.subscribeRateLimits(connectionId, (response) => {
|
|
266
|
+
if (foreground?.provider === "claude")
|
|
267
|
+
emitRateLimits(response);
|
|
268
|
+
});
|
|
269
|
+
const subscribedClaudeThreads = new Set();
|
|
105
270
|
const subscribeClaude = (threadId) => {
|
|
106
|
-
|
|
271
|
+
if (!connectionAlive
|
|
272
|
+
|| !subscriptions.subscribe(threadId, connectionId, notificationSink, serverRequestSink))
|
|
273
|
+
return false;
|
|
274
|
+
subscribedClaudeThreads.add(threadId);
|
|
275
|
+
claude.cancelEphemeralRelease(threadId);
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
const muteClaudeDuringSnapshot = (threadId) => {
|
|
279
|
+
subscribeClaude(threadId);
|
|
280
|
+
subscriptions.mute(threadId, connectionId);
|
|
107
281
|
};
|
|
108
282
|
client.on("message", async (data, isBinary) => {
|
|
109
283
|
recorder.frame(connectionId, "client_to_gateway", data, isBinary);
|
|
@@ -113,7 +287,10 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
113
287
|
: undefined;
|
|
114
288
|
if (message && isResponse(message) && internalServerRequestId) {
|
|
115
289
|
const response = "result" in message ? message.result : { rpcError: message.error };
|
|
116
|
-
if (
|
|
290
|
+
if (handoffs.resolveStockServerRequest
|
|
291
|
+
&& await handoffs.resolveStockServerRequest(internalServerRequestId, response))
|
|
292
|
+
return;
|
|
293
|
+
if (!await claude.resolveServerRequest(internalServerRequestId, response)) {
|
|
117
294
|
logger.warn("claude.interaction.unknown-response", { connectionId, requestId: internalServerRequestId });
|
|
118
295
|
}
|
|
119
296
|
return;
|
|
@@ -129,25 +306,119 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
129
306
|
sendResult(message.id, await mergedModelList((message.params ?? {}), stockRpc, claudeModels, logger, cursors));
|
|
130
307
|
return;
|
|
131
308
|
}
|
|
309
|
+
if (message.method === "account/rateLimits/read" && foreground?.provider === "claude") {
|
|
310
|
+
const backend = handoffs.logical?.(foreground.threadId)?.epoch.backendThreadId ?? foreground.threadId;
|
|
311
|
+
sendResult(message.id, await claude.readRateLimits(backend));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (message.method === "account/usage/read" && foreground?.provider === "claude") {
|
|
315
|
+
// The captured App status flow does not request this method. If a
|
|
316
|
+
// client does, lifetime/streak Codex statistics have no Claude
|
|
317
|
+
// equivalent and must not be mislabelled as provider usage.
|
|
318
|
+
sendResult(message.id, {
|
|
319
|
+
summary: {
|
|
320
|
+
lifetimeTokens: null,
|
|
321
|
+
peakDailyTokens: null,
|
|
322
|
+
longestRunningTurnSec: null,
|
|
323
|
+
currentStreakDays: null,
|
|
324
|
+
longestStreakDays: null,
|
|
325
|
+
},
|
|
326
|
+
dailyUsageBuckets: null,
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (message.method === "account/rateLimits/read" && !foreground)
|
|
331
|
+
diagnoseUnknownStatus("account/rateLimits/read");
|
|
132
332
|
if (message.method === "thread/list") {
|
|
133
|
-
sendResult(message.id, await mergedThreadList((message.params ?? {}), stockRpc, claude, cursors));
|
|
333
|
+
sendResult(message.id, await mergedThreadList((message.params ?? {}), stockRpc, claude, cursors, handoffs));
|
|
134
334
|
return;
|
|
135
335
|
}
|
|
136
336
|
if (message.method === "thread/loaded/list") {
|
|
137
|
-
sendResult(message.id, await mergedLoadedList((message.params ?? {}), stockRpc, claude, cursors));
|
|
337
|
+
sendResult(message.id, await mergedLoadedList((message.params ?? {}), stockRpc, claude, cursors, handoffs));
|
|
138
338
|
return;
|
|
139
339
|
}
|
|
140
340
|
if (message.method === "thread/start") {
|
|
141
341
|
const params = (message.params ?? {});
|
|
142
342
|
if (params.model && claude.ownsModel(params.model)) {
|
|
143
343
|
const result = await claude.startThread(params);
|
|
344
|
+
handoffs.observeDurableThread(connectionId, "claude");
|
|
144
345
|
sendResult(message.id, result);
|
|
346
|
+
selectForeground("claude", result.thread.id);
|
|
145
347
|
subscribeClaude(result.thread.id);
|
|
146
|
-
claude.announceThread(result.thread);
|
|
348
|
+
await claude.announceThread(result.thread);
|
|
349
|
+
emitFastTransition(result.thread.id, undefined, claude.currentThreadSettings(result.thread.id), params.serviceTier);
|
|
147
350
|
return;
|
|
148
351
|
}
|
|
149
352
|
}
|
|
150
353
|
const params = (message.params ?? {});
|
|
354
|
+
if (params.threadId && message.method === "turn/start"
|
|
355
|
+
&& (claude.ownsThread(params.threadId)
|
|
356
|
+
|| handoffs.logical?.(params.threadId)?.epoch.provider === "claude")) {
|
|
357
|
+
const backend = handoffs.logical?.(params.threadId)?.epoch.backendThreadId ?? params.threadId;
|
|
358
|
+
if (!claude.isChildProjection(backend)) {
|
|
359
|
+
const promptedCompact = claudeCompactCommand((message.params ?? {}).input);
|
|
360
|
+
if (promptedCompact) {
|
|
361
|
+
const prepared = await claude.preparePromptedCompact(backend, promptedCompact);
|
|
362
|
+
sendResult(message.id, prepared.response);
|
|
363
|
+
selectForeground("claude", params.threadId);
|
|
364
|
+
await prepared.announce();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (params.threadId && message.method === "turn/start" && features.statusCommand) {
|
|
370
|
+
const turn = (message.params ?? {});
|
|
371
|
+
if (isCCodexStateCommand(turn.input)) {
|
|
372
|
+
if (claude.ownsThread(params.threadId) && !claude.isChildProjection(params.threadId)) {
|
|
373
|
+
const prepared = await claude.prepareStateTurn(turn, () => threadState(params.threadId));
|
|
374
|
+
sendResult(message.id, prepared.response);
|
|
375
|
+
selectForeground("claude", params.threadId);
|
|
376
|
+
await prepared.announce();
|
|
377
|
+
prepared.start();
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
const notice = transientCommandNotice(params.threadId, turn.input, await threadState(params.threadId), turn.clientUserMessageId ?? null);
|
|
381
|
+
sendResult(message.id, notice.response);
|
|
382
|
+
selectForeground(claude.ownsThread(params.threadId) ? "claude" : "codex", params.threadId);
|
|
383
|
+
emitNotice(notice);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (isCCodexStatusCommand(turn.input)) {
|
|
388
|
+
if (claude.ownsThread(params.threadId) && !claude.isChildProjection(params.threadId)) {
|
|
389
|
+
const prepared = await claude.prepareStatusTurn(turn, () => combinedStatus(params.threadId));
|
|
390
|
+
sendResult(message.id, prepared.response);
|
|
391
|
+
selectForeground("claude", params.threadId);
|
|
392
|
+
await prepared.announce();
|
|
393
|
+
prepared.start();
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const notice = transientCommandNotice(params.threadId, turn.input, await combinedStatus(claude.ownsThread(params.threadId) ? params.threadId : undefined), turn.clientUserMessageId ?? null);
|
|
397
|
+
sendResult(message.id, notice.response);
|
|
398
|
+
selectForeground(claude.ownsThread(params.threadId) ? "claude" : "codex", params.threadId);
|
|
399
|
+
emitNotice(notice);
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (params.threadId && message.method === "turn/start"
|
|
405
|
+
&& (handoffs.logical?.(params.threadId)?.epoch.provider === "stock"
|
|
406
|
+
|| (!handoffs.logical?.(params.threadId) && !claude.ownsThread(params.threadId)))
|
|
407
|
+
&& providerAvailability) {
|
|
408
|
+
let availability = await providerAvailability.read("codex");
|
|
409
|
+
if (availability.state !== "ready")
|
|
410
|
+
availability = await providerAvailability.refresh("codex");
|
|
411
|
+
if (availability.state !== "ready") {
|
|
412
|
+
const notice = transientSystemNotice(params.threadId, providerUnavailableMessage(availability), "error");
|
|
413
|
+
sendResult(message.id, notice.response);
|
|
414
|
+
selectForeground("codex", params.threadId);
|
|
415
|
+
emitNotice(notice);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (params.threadId && message.method === "turn/start") {
|
|
420
|
+
handoffs.observeDurableTurn(connectionId, (message.params ?? {}));
|
|
421
|
+
}
|
|
151
422
|
if (params.threadId && message.method === "thread/settings/update") {
|
|
152
423
|
const update = (message.params ?? {});
|
|
153
424
|
const staged = handoffs.interceptSettings(update);
|
|
@@ -161,6 +432,104 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
161
432
|
return;
|
|
162
433
|
}
|
|
163
434
|
}
|
|
435
|
+
if (params.threadId && message.method === "turn/start") {
|
|
436
|
+
const turn = (message.params ?? {});
|
|
437
|
+
const pending = handoffs.stageTurnSwitch
|
|
438
|
+
? handoffs.stageTurnSwitch(turn)
|
|
439
|
+
: handoffs.pending(params.threadId);
|
|
440
|
+
if (pending) {
|
|
441
|
+
subscriptions.subscribe(params.threadId, connectionId, notificationSink, serverRequestSink);
|
|
442
|
+
const migration = providerMigrationNotice(params.threadId);
|
|
443
|
+
sendResult(message.id, migration.response);
|
|
444
|
+
for (const notification of migration.notifications) {
|
|
445
|
+
subscriptions.emitPublic(params.threadId, notification.method, notification.params);
|
|
446
|
+
}
|
|
447
|
+
const execute = async () => {
|
|
448
|
+
const switchProviderTurn = handoffs.switchProviderTurn;
|
|
449
|
+
let compacted = false;
|
|
450
|
+
const completeCompaction = () => {
|
|
451
|
+
if (compacted)
|
|
452
|
+
return;
|
|
453
|
+
compacted = true;
|
|
454
|
+
for (const notification of providerMigrationCompleted(params.threadId, migration)) {
|
|
455
|
+
subscriptions.emitPublic(params.threadId, notification.method, notification.params);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
if (switchProviderTurn) {
|
|
459
|
+
await switchProviderTurn.call(handoffs, turn, migration.response.turn, stockRpc, connectionId, completeCompaction);
|
|
460
|
+
selectForeground(pending.targetProvider === "claude" ? "claude" : "codex", params.threadId);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (pending.sourceProvider === "claude") {
|
|
464
|
+
const hidden = await claude.forkThread({
|
|
465
|
+
threadId: params.threadId, ephemeral: true, threadSource: "subAgent",
|
|
466
|
+
});
|
|
467
|
+
await claude.compactThread(hidden.thread.id);
|
|
468
|
+
const target = await stockRpc.request("thread/start", {
|
|
469
|
+
model: pending.targetModel,
|
|
470
|
+
cwd: turn.cwd,
|
|
471
|
+
approvalPolicy: turn.approvalPolicy,
|
|
472
|
+
permissions: turn.permissions,
|
|
473
|
+
});
|
|
474
|
+
completeCompaction();
|
|
475
|
+
await stockRpc.request("turn/start", { ...turn, threadId: target.thread.id });
|
|
476
|
+
selectForeground("codex", params.threadId);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const hidden = await stockRpc.request("thread/fork", {
|
|
480
|
+
threadId: params.threadId, ephemeral: true, excludeTurns: true,
|
|
481
|
+
});
|
|
482
|
+
await stockRpc.request("turn/start", {
|
|
483
|
+
threadId: hidden.thread.id,
|
|
484
|
+
input: [{ type: "text", text: "Compact this task for a provider handoff.", text_elements: [] }],
|
|
485
|
+
});
|
|
486
|
+
const target = await claude.startThread({
|
|
487
|
+
model: pending.targetModel,
|
|
488
|
+
...(turn.cwd !== undefined ? { cwd: turn.cwd } : {}),
|
|
489
|
+
...(turn.approvalPolicy !== undefined ? { approvalPolicy: turn.approvalPolicy } : {}),
|
|
490
|
+
...(turn.permissions !== undefined ? { permissions: turn.permissions } : {}),
|
|
491
|
+
});
|
|
492
|
+
const prepared = await claude.prepareTurn({ ...turn, threadId: target.thread.id });
|
|
493
|
+
completeCompaction();
|
|
494
|
+
await prepared.announce();
|
|
495
|
+
prepared.start();
|
|
496
|
+
selectForeground("claude", params.threadId);
|
|
497
|
+
};
|
|
498
|
+
void execute().catch((error) => {
|
|
499
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
500
|
+
for (const notification of providerMigrationFailed(params.threadId, migration, message))
|
|
501
|
+
subscriptions.emitPublic(params.threadId, notification.method, notification.params);
|
|
502
|
+
for (const notification of transientSystemNotice(params.threadId, `Provider switch failed; your message was not sent\n ↳ ${message}`, "error").notifications)
|
|
503
|
+
subscriptions.emitPublic(params.threadId, notification.method, notification.params);
|
|
504
|
+
const source = handoffs.logical?.(params.threadId);
|
|
505
|
+
if (source)
|
|
506
|
+
subscriptions.emitPublic(params.threadId, "thread/settings/updated", {
|
|
507
|
+
threadId: params.threadId,
|
|
508
|
+
threadSettings: {
|
|
509
|
+
...source.epoch.settings,
|
|
510
|
+
model: source.epoch.model,
|
|
511
|
+
modelProvider: source.epoch.provider === "claude" ? "claude" : "openai",
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (params.threadId && handoffs.logical?.(params.threadId)
|
|
519
|
+
&& message.method !== "thread/fork" && message.method !== "thread/rollback") {
|
|
520
|
+
if (message.method === "thread/unsubscribe") {
|
|
521
|
+
subscriptions.unsubscribe(params.threadId, connectionId);
|
|
522
|
+
sendResult(message.id, { status: "unsubscribed" });
|
|
523
|
+
clearForeground(params.threadId);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
subscriptions.subscribe(params.threadId, connectionId, notificationSink, serverRequestSink);
|
|
527
|
+
const handled = await handoffs.requestLogical(message.method, (message.params ?? {}), stockRpc);
|
|
528
|
+
sendResult(message.id, handled.result);
|
|
529
|
+
selectForeground(handled.provider === "claude" ? "claude" : "codex", params.threadId);
|
|
530
|
+
await handled.after?.();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
164
533
|
if (params.threadId && message.method === "turn/start") {
|
|
165
534
|
const target = handoffs.blockedTurnTarget((message.params ?? {}));
|
|
166
535
|
if (target) {
|
|
@@ -172,34 +541,92 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
172
541
|
}
|
|
173
542
|
if (params.threadId && message.method === "thread/fork") {
|
|
174
543
|
const forkParams = (message.params ?? {});
|
|
175
|
-
if (handoffs.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
544
|
+
if (handoffs.isSystemEphemeralFork(forkParams)) {
|
|
545
|
+
sendResult(message.id, await handoffs.forkSystemEphemeral(forkParams, stockRpc, connectionId));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const logicalFork = handoffs.forkLogical;
|
|
549
|
+
const overlay = handoffs.overlay(params.threadId);
|
|
550
|
+
if ((logicalFork && handoffs.logical?.(params.threadId)) || overlay?.epochs) {
|
|
551
|
+
if (logicalFork) {
|
|
552
|
+
sendResult(message.id, await logicalFork.call(handoffs, forkParams, stockRpc));
|
|
553
|
+
return;
|
|
184
554
|
}
|
|
185
|
-
|
|
555
|
+
const turns = overlay.epochs.flatMap((epoch) => epoch.publicTurnIds.map((id) => ({ id, items: [], status: "completed" })));
|
|
556
|
+
sendResult(message.id, { thread: { id: uuidv7(), forkedFromId: params.threadId, turns } });
|
|
186
557
|
return;
|
|
187
558
|
}
|
|
559
|
+
if (forkParams.model) {
|
|
560
|
+
const sourceProvider = claude.ownsThread(params.threadId) ? "claude" : "stock";
|
|
561
|
+
const targetProvider = claude.ownsModel(forkParams.model) ? "claude" : "stock";
|
|
562
|
+
if (sourceProvider !== targetProvider) {
|
|
563
|
+
throw new RpcError(-32602, "Fork is same-provider only. Change the model and send a message to switch provider.");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (params.threadId && message.method === "thread/delete"
|
|
568
|
+
&& handoffs.ownsSystemEphemeral(connectionId, params.threadId)) {
|
|
569
|
+
const result = await stockRpc.request("thread/delete", message.params ?? {});
|
|
570
|
+
handoffs.releaseSystemEphemeral(connectionId, params.threadId);
|
|
571
|
+
sendResult(message.id, result);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (params.threadId && message.method === "thread/unsubscribe"
|
|
575
|
+
&& handoffs.ownsSystemEphemeral(connectionId, params.threadId)) {
|
|
576
|
+
const result = await stockRpc.request("thread/unsubscribe", message.params ?? {});
|
|
577
|
+
handoffs.releaseSystemEphemeral(connectionId, params.threadId);
|
|
578
|
+
sendResult(message.id, result);
|
|
579
|
+
return;
|
|
188
580
|
}
|
|
189
581
|
if (params.threadId && message.method === "thread/delete" && !claude.ownsThread(params.threadId)
|
|
190
582
|
&& (handoffs.pending(params.threadId) || handoffs.overlay(params.threadId))) {
|
|
191
583
|
const result = await stockRpc.request("thread/delete", message.params ?? {});
|
|
192
584
|
handoffs.clearThread(params.threadId);
|
|
193
585
|
sendResult(message.id, result);
|
|
586
|
+
clearForeground(params.threadId);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (params.threadId && message.method === "thread/rollback" && handoffs.logical?.(params.threadId)) {
|
|
590
|
+
sendResult(message.id, await handoffs.rollbackLogicalFork((message.params ?? {}), stockRpc, connectionId));
|
|
194
591
|
return;
|
|
195
592
|
}
|
|
196
593
|
if (params.threadId && handoffs.overlay(params.threadId)) {
|
|
594
|
+
if (message.method === "thread/rollback") {
|
|
595
|
+
const rollback = (message.params ?? {});
|
|
596
|
+
const logicalRollback = handoffs.rollbackLogicalFork;
|
|
597
|
+
if (logicalRollback) {
|
|
598
|
+
sendResult(message.id, await logicalRollback.call(handoffs, rollback, stockRpc, connectionId));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const overlay = handoffs.overlay(params.threadId);
|
|
602
|
+
if (overlay.epochs) {
|
|
603
|
+
const turns = overlay.epochs.flatMap((epoch) => epoch.publicTurnIds.map((turnId) => ({ epoch, turnId })));
|
|
604
|
+
const selected = turns.at(turns.length - rollback.numTurns - 1);
|
|
605
|
+
if (!selected)
|
|
606
|
+
throw new RpcError(-32602, "Fork rollback removed every provider turn.");
|
|
607
|
+
if (selected.epoch.provider === "claude") {
|
|
608
|
+
await claude.forkThread({
|
|
609
|
+
threadId: selected.epoch.backendThreadId,
|
|
610
|
+
lastTurnId: selected.turnId,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
await stockRpc.request("thread/fork", {
|
|
615
|
+
threadId: selected.epoch.backendThreadId,
|
|
616
|
+
lastTurnId: selected.turnId,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
sendResult(message.id, {});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
197
623
|
if (message.method === "thread/read") {
|
|
198
624
|
sendResult(message.id, await handoffs.readOverlay((message.params ?? {}), stockRpc));
|
|
199
625
|
return;
|
|
200
626
|
}
|
|
201
627
|
if (message.method === "thread/resume") {
|
|
202
628
|
sendResult(message.id, await handoffs.resumeOverlay((message.params ?? {}), stockRpc));
|
|
629
|
+
selectForeground("codex", params.threadId);
|
|
203
630
|
return;
|
|
204
631
|
}
|
|
205
632
|
if (message.method === "thread/turns/list") {
|
|
@@ -213,15 +640,40 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
213
640
|
}
|
|
214
641
|
if (params.threadId && claude.ownsThread(params.threadId)) {
|
|
215
642
|
if (message.method === "thread/resume") {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
643
|
+
muteClaudeDuringSnapshot(params.threadId);
|
|
644
|
+
try {
|
|
645
|
+
const prepared = await claude.prepareResume((message.params ?? {}));
|
|
646
|
+
const result = prepared.response;
|
|
647
|
+
const snapshotHighWatermark = claude.eventHighWatermark(params.threadId);
|
|
648
|
+
const tokenUsage = claude.latestTokenUsage(params.threadId);
|
|
649
|
+
sendResult(message.id, result);
|
|
650
|
+
selectForeground("claude", params.threadId);
|
|
651
|
+
subscriptions.unmute(params.threadId, connectionId);
|
|
652
|
+
if (tokenUsage && tokenUsage.sequence <= snapshotHighWatermark) {
|
|
653
|
+
notificationSink(tokenUsage.method, tokenUsage.params);
|
|
654
|
+
}
|
|
655
|
+
for (const event of claude.eventsAfter(params.threadId, snapshotHighWatermark)) {
|
|
656
|
+
notificationSink(event.method, event.params);
|
|
657
|
+
}
|
|
658
|
+
await prepared.notifyGoalSnapshot(notificationSink);
|
|
659
|
+
await claude.replayPendingRequests(params.threadId, connectionId);
|
|
660
|
+
const failedFork = handoffs.claimFailedFork(params.threadId);
|
|
661
|
+
if (failedFork)
|
|
662
|
+
emitTransientNotice(params.threadId, `Cross-provider fork failed while the App was disconnected: ${failedFork}`, "error");
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
subscriptions.unmute(params.threadId, connectionId);
|
|
666
|
+
subscriptions.unsubscribe(params.threadId, connectionId);
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
220
669
|
return;
|
|
221
670
|
}
|
|
222
671
|
if (message.method === "thread/read") {
|
|
223
672
|
const read = (message.params ?? {});
|
|
224
673
|
sendResult(message.id, claude.readThread(read.threadId, read.includeTurns ?? false));
|
|
674
|
+
const failedFork = handoffs.claimFailedFork(params.threadId);
|
|
675
|
+
if (failedFork)
|
|
676
|
+
emitTransientNotice(params.threadId, `Cross-provider fork failed while the App was disconnected: ${failedFork}`, "error");
|
|
225
677
|
return;
|
|
226
678
|
}
|
|
227
679
|
if (message.method === "thread/turns/list") {
|
|
@@ -235,7 +687,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
235
687
|
}
|
|
236
688
|
if (message.method === "thread/unsubscribe") {
|
|
237
689
|
subscriptions.unsubscribe(params.threadId, connectionId);
|
|
690
|
+
subscribedClaudeThreads.delete(params.threadId);
|
|
238
691
|
sendResult(message.id, { status: "unsubscribed" });
|
|
692
|
+
clearForeground(params.threadId);
|
|
693
|
+
if (!subscriptions.hasSubscribers(params.threadId))
|
|
694
|
+
await claude.releaseEphemeralThread(params.threadId);
|
|
239
695
|
return;
|
|
240
696
|
}
|
|
241
697
|
if (message.method === "thread/name/set") {
|
|
@@ -243,11 +699,16 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
243
699
|
return;
|
|
244
700
|
}
|
|
245
701
|
if (message.method === "thread/metadata/update") {
|
|
246
|
-
sendResult(message.id, claude.updateThreadMetadata((message.params ?? {})));
|
|
702
|
+
sendResult(message.id, await claude.updateThreadMetadata((message.params ?? {})));
|
|
247
703
|
return;
|
|
248
704
|
}
|
|
249
705
|
if (message.method === "thread/settings/update") {
|
|
250
|
-
|
|
706
|
+
const update = (message.params ?? {});
|
|
707
|
+
const before = claude.currentThreadSettings(params.threadId);
|
|
708
|
+
const result = await claude.updateThreadSettings(update);
|
|
709
|
+
const after = claude.currentThreadSettings(params.threadId);
|
|
710
|
+
sendResult(message.id, result);
|
|
711
|
+
emitFastTransition(params.threadId, before, after, update.serviceTier);
|
|
251
712
|
return;
|
|
252
713
|
}
|
|
253
714
|
if (message.method === "thread/archive") {
|
|
@@ -255,20 +716,22 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
255
716
|
return;
|
|
256
717
|
}
|
|
257
718
|
if (message.method === "thread/unarchive") {
|
|
258
|
-
sendResult(message.id, claude.unarchiveThread(params.threadId));
|
|
719
|
+
sendResult(message.id, await claude.unarchiveThread(params.threadId));
|
|
259
720
|
return;
|
|
260
721
|
}
|
|
261
722
|
if (message.method === "thread/delete") {
|
|
262
723
|
const result = await claude.deleteThread(params.threadId);
|
|
263
724
|
handoffs.clearThread(params.threadId);
|
|
264
725
|
sendResult(message.id, result);
|
|
726
|
+
clearForeground(params.threadId);
|
|
265
727
|
return;
|
|
266
728
|
}
|
|
267
729
|
if (message.method === "thread/fork") {
|
|
268
730
|
const result = await claude.forkThread((message.params ?? {}));
|
|
269
731
|
sendResult(message.id, result);
|
|
732
|
+
selectForeground("claude", result.thread.id);
|
|
270
733
|
subscribeClaude(result.thread.id);
|
|
271
|
-
claude.announceThread(result.thread);
|
|
734
|
+
await claude.announceThread(result.thread);
|
|
272
735
|
return;
|
|
273
736
|
}
|
|
274
737
|
if (message.method === "thread/rollback") {
|
|
@@ -287,38 +750,67 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
287
750
|
sendResult(message.id, await claude.shellCommand((message.params ?? {})));
|
|
288
751
|
return;
|
|
289
752
|
}
|
|
753
|
+
if (message.method === "thread/backgroundTerminals/clean") {
|
|
754
|
+
sendResult(message.id, await claude.cleanBackgroundTerminals((message.params ?? {})));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (message.method === "thread/backgroundTerminals/list") {
|
|
758
|
+
sendResult(message.id, await claude.listBackgroundTerminals((message.params ?? {})));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (message.method === "thread/backgroundTerminals/terminate") {
|
|
762
|
+
sendResult(message.id, await claude.terminateBackgroundTerminal((message.params ?? {})));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
290
765
|
if (message.method === "review/start") {
|
|
291
766
|
const prepared = await claude.prepareReview((message.params ?? {}));
|
|
292
|
-
sendResult(message.id, prepared.response);
|
|
293
767
|
if (prepared.forkedThread) {
|
|
294
768
|
subscribeClaude(prepared.forkedThread.id);
|
|
295
|
-
claude.announceThread(prepared.forkedThread);
|
|
769
|
+
await claude.announceThread(prepared.forkedThread);
|
|
296
770
|
}
|
|
297
|
-
prepared.
|
|
771
|
+
sendResult(message.id, prepared.response);
|
|
772
|
+
await prepared.announce();
|
|
298
773
|
prepared.start();
|
|
299
774
|
return;
|
|
300
775
|
}
|
|
301
776
|
if (message.method === "thread/goal/set") {
|
|
302
|
-
|
|
777
|
+
const prepared = await claude.prepareGoalSet((message.params ?? {}));
|
|
778
|
+
try {
|
|
779
|
+
sendResult(message.id, prepared.response);
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
await prepared.notify();
|
|
783
|
+
}
|
|
303
784
|
return;
|
|
304
785
|
}
|
|
305
786
|
if (message.method === "thread/goal/get") {
|
|
306
|
-
sendResult(message.id, claude.getGoal(params.threadId));
|
|
787
|
+
sendResult(message.id, await claude.getGoal(params.threadId));
|
|
307
788
|
return;
|
|
308
789
|
}
|
|
309
790
|
if (message.method === "thread/goal/clear") {
|
|
310
|
-
|
|
791
|
+
const prepared = await claude.prepareGoalClear(params.threadId);
|
|
792
|
+
try {
|
|
793
|
+
sendResult(message.id, prepared.response);
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
await prepared.notify();
|
|
797
|
+
}
|
|
311
798
|
return;
|
|
312
799
|
}
|
|
313
800
|
if (message.method === "turn/start") {
|
|
314
|
-
const
|
|
801
|
+
const turn = (message.params ?? {});
|
|
802
|
+
const before = claude.currentThreadSettings(params.threadId);
|
|
803
|
+
const prepared = await claude.prepareTurn(turn);
|
|
804
|
+
const after = claude.currentThreadSettings(params.threadId);
|
|
315
805
|
sendResult(message.id, prepared.response);
|
|
316
|
-
|
|
806
|
+
selectForeground("claude", params.threadId);
|
|
807
|
+
emitFastTransition(params.threadId, before, after, turn.serviceTier);
|
|
808
|
+
await prepared.announce();
|
|
317
809
|
prepared.start();
|
|
318
810
|
return;
|
|
319
811
|
}
|
|
320
812
|
if (message.method === "turn/interrupt") {
|
|
321
|
-
await claude.interruptTurn((message.params ?? {})
|
|
813
|
+
await claude.interruptTurn((message.params ?? {}));
|
|
322
814
|
sendResult(message.id, {});
|
|
323
815
|
return;
|
|
324
816
|
}
|
|
@@ -328,6 +820,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
328
820
|
}
|
|
329
821
|
throw new RpcError(-32601, `Method '${message.method}' is not implemented for Claude threads yet.`);
|
|
330
822
|
}
|
|
823
|
+
if (params.threadId && (message.method === "thread/read" || message.method === "thread/resume")) {
|
|
824
|
+
const failedFork = handoffs.claimFailedFork(params.threadId);
|
|
825
|
+
if (failedFork)
|
|
826
|
+
emitTransientNotice(params.threadId, `Cross-provider fork failed while the App was disconnected: ${failedFork}`, "error");
|
|
827
|
+
}
|
|
331
828
|
}
|
|
332
829
|
catch (error) {
|
|
333
830
|
const failure = rpcError(error);
|
|
@@ -341,18 +838,28 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
341
838
|
? message.params
|
|
342
839
|
: undefined;
|
|
343
840
|
const threadId = typeof params?.threadId === "string" ? params.threadId : undefined;
|
|
344
|
-
|
|
841
|
+
const childProjection = Boolean(threadId && claude.isChildProjection(threadId));
|
|
842
|
+
if (message.method === "turn/start" && threadId && !childProjection) {
|
|
345
843
|
const notice = transientSystemNotice(threadId, failure.message, "error");
|
|
346
844
|
sendResult(message.id, notice.response);
|
|
347
845
|
emitNotice(notice);
|
|
348
846
|
return;
|
|
349
847
|
}
|
|
350
|
-
if (threadId)
|
|
848
|
+
if (threadId && !childProjection)
|
|
351
849
|
emitSystemError(threadId, failure.message);
|
|
352
850
|
sendError(message.id, failure.code, failure.message);
|
|
353
|
-
if (threadId) {
|
|
851
|
+
if (threadId && !childProjection) {
|
|
354
852
|
if (claude.ownsThread(threadId)) {
|
|
355
|
-
claude.reportError(threadId, typeof params?.turnId === "string" ? params.turnId : undefined, failure.message, rpcCodexErrorInfo(failure.code))
|
|
853
|
+
await claude.reportError(threadId, typeof params?.turnId === "string" ? params.turnId : undefined, failure.message, rpcCodexErrorInfo(failure.code)).catch((diagnosticError) => {
|
|
854
|
+
logger.warn("claude.request.error-report-failed", {
|
|
855
|
+
connectionId,
|
|
856
|
+
method: message.method,
|
|
857
|
+
originalError: failure.message,
|
|
858
|
+
diagnosticError: diagnosticError instanceof Error
|
|
859
|
+
? diagnosticError.message
|
|
860
|
+
: String(diagnosticError),
|
|
861
|
+
});
|
|
862
|
+
});
|
|
356
863
|
}
|
|
357
864
|
else {
|
|
358
865
|
sendJson({
|
|
@@ -369,12 +876,28 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
369
876
|
return;
|
|
370
877
|
}
|
|
371
878
|
}
|
|
372
|
-
|
|
879
|
+
let forwarded = message;
|
|
880
|
+
if (message && isRequest(message) && message.method === "turn/start") {
|
|
881
|
+
const params = handoffs.prepareTitleTurn(connectionId, (message.params ?? {}));
|
|
882
|
+
if (params !== message.params)
|
|
883
|
+
forwarded = { ...message, params };
|
|
884
|
+
}
|
|
885
|
+
if (message && isRequest(message) && message.method === "thread/fork") {
|
|
886
|
+
const params = (message.params ?? {});
|
|
887
|
+
if (params.ephemeral !== true && params.threadSource !== "system") {
|
|
888
|
+
const completed = stockState.completeForkParams(params);
|
|
889
|
+
if (completed !== params)
|
|
890
|
+
forwarded = { ...message, params: completed };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (forwarded)
|
|
894
|
+
stockState.observeRequest(connectionId, forwarded);
|
|
895
|
+
trackForwardedRequest(forwarded);
|
|
373
896
|
if (stock.readyState === WebSocketState.OPEN) {
|
|
374
|
-
stock.send(data, { binary: isBinary });
|
|
897
|
+
stock.send(forwarded === message ? data : JSON.stringify(forwarded), { binary: isBinary });
|
|
375
898
|
}
|
|
376
899
|
else {
|
|
377
|
-
queued.push({ data, isBinary });
|
|
900
|
+
queued.push({ data: forwarded === message ? data : Buffer.from(JSON.stringify(forwarded)), isBinary });
|
|
378
901
|
}
|
|
379
902
|
});
|
|
380
903
|
stock.once("open", () => {
|
|
@@ -384,6 +907,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
384
907
|
});
|
|
385
908
|
stock.on("message", (data, isBinary) => {
|
|
386
909
|
const message = isBinary ? undefined : parseRpcMessage(data);
|
|
910
|
+
if (message) {
|
|
911
|
+
stockState.observeResponse(connectionId, message);
|
|
912
|
+
stockState.observeNotification(message);
|
|
913
|
+
}
|
|
914
|
+
let foregroundAfterForward;
|
|
387
915
|
if (message && "method" in message && !("id" in message) && message.method === "remoteControl/status/changed" && remoteControl) {
|
|
388
916
|
remoteControl.intercept(connectionId, notificationSink, message.params);
|
|
389
917
|
return;
|
|
@@ -392,17 +920,62 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
392
920
|
completeLatency(message.id, "stock");
|
|
393
921
|
const forwarded = forwardedRequests.get(requestKey(message.id));
|
|
394
922
|
forwardedRequests.delete(requestKey(message.id));
|
|
395
|
-
if (forwarded?.
|
|
923
|
+
if (forwarded?.systemEphemeral && "result" in message) {
|
|
924
|
+
const result = message.result && typeof message.result === "object"
|
|
925
|
+
? message.result
|
|
926
|
+
: undefined;
|
|
927
|
+
if (typeof result?.thread?.id === "string") {
|
|
928
|
+
handoffs.registerForwardedEphemeralCandidate(connectionId, result.thread.id, forwarded.systemEphemeral);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (forwarded?.threadId && isUuid(forwarded.threadId) && "error" in message) {
|
|
396
932
|
emitSystemError(forwarded.threadId, message.error.message);
|
|
933
|
+
}
|
|
934
|
+
if (forwarded && "result" in message) {
|
|
935
|
+
const result = message.result && typeof message.result === "object"
|
|
936
|
+
? message.result
|
|
937
|
+
: undefined;
|
|
938
|
+
const resultThreadId = typeof result?.thread?.id === "string" ? result.thread.id : forwarded.threadId;
|
|
939
|
+
if (forwarded.foreground && resultThreadId) {
|
|
940
|
+
handoffs.observeDurableThread(connectionId, "stock");
|
|
941
|
+
foregroundAfterForward = () => selectForeground(forwarded.foreground, resultThreadId);
|
|
942
|
+
}
|
|
943
|
+
else if (forwarded.clearForeground && forwarded.threadId) {
|
|
944
|
+
foregroundAfterForward = () => clearForeground(forwarded.threadId);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
397
947
|
}
|
|
398
948
|
if (message && stockRpc.handle(message))
|
|
399
949
|
return;
|
|
400
950
|
if (message && "method" in message) {
|
|
951
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && foreground?.provider === "claude") {
|
|
952
|
+
publishForegroundRateLimits(foregroundGeneration);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (!("id" in message) && message.method === "account/rateLimits/updated" && !foreground) {
|
|
956
|
+
diagnoseUnknownStatus("account/rateLimits/updated");
|
|
957
|
+
}
|
|
958
|
+
if (handoffs.captureInternalStockMessage(connectionId, message)) {
|
|
959
|
+
if (isRequest(message))
|
|
960
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const rewritten = handoffs.rewriteTitleMessages(message);
|
|
964
|
+
if (rewritten) {
|
|
965
|
+
for (const output of rewritten)
|
|
966
|
+
sendJson(output);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
401
969
|
if (handoffs.suppressStockTargetMessage(connectionId, message)) {
|
|
402
970
|
if (isRequest(message))
|
|
403
971
|
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
404
972
|
return;
|
|
405
973
|
}
|
|
974
|
+
if (handoffs.ownsStockBackendMessage?.(message)) {
|
|
975
|
+
if (isRequest(message))
|
|
976
|
+
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
406
979
|
const params = message.params && typeof message.params === "object" ? message.params : undefined;
|
|
407
980
|
const nestedThread = params?.thread && typeof params.thread === "object"
|
|
408
981
|
? params.thread.id
|
|
@@ -417,17 +990,12 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
417
990
|
if (typeof error?.message === "string")
|
|
418
991
|
emitSystemError(threadId, error.message);
|
|
419
992
|
}
|
|
420
|
-
if (threadId && handoffs.ownsInternalStockThread(threadId)) {
|
|
421
|
-
handoffs.recordInternalStockMessage(message);
|
|
422
|
-
if (isRequest(message))
|
|
423
|
-
stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
993
|
}
|
|
427
994
|
if (client.readyState === WebSocketState.OPEN) {
|
|
428
995
|
recorder.frame(connectionId, "gateway_to_client", data, isBinary);
|
|
429
996
|
client.send(data, { binary: isBinary });
|
|
430
997
|
}
|
|
998
|
+
foregroundAfterForward?.();
|
|
431
999
|
});
|
|
432
1000
|
const closeClient = (code, reason) => {
|
|
433
1001
|
if (client.readyState === WebSocketState.OPEN)
|
|
@@ -439,17 +1007,45 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
|
|
|
439
1007
|
});
|
|
440
1008
|
stock.on("close", () => closeClient(1011, "Stock app-server closed"));
|
|
441
1009
|
client.on("error", (error) => logger.warn("connection.client.error", { connectionId, error: error.message }));
|
|
1010
|
+
let closeStarted = false;
|
|
442
1011
|
client.on("close", (code, reason) => {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1012
|
+
if (closeStarted)
|
|
1013
|
+
return;
|
|
1014
|
+
closeStarted = true;
|
|
1015
|
+
connectionAlive = false;
|
|
1016
|
+
void (async () => {
|
|
1017
|
+
metrics.connectionClosed();
|
|
1018
|
+
const releasedThreads = [...subscribedClaudeThreads];
|
|
1019
|
+
subscribedClaudeThreads.clear();
|
|
1020
|
+
subscriptions.detach(connectionId);
|
|
1021
|
+
claude.unsubscribeRateLimits(connectionId);
|
|
1022
|
+
const releases = await Promise.allSettled(releasedThreads.map(async (threadId) => {
|
|
1023
|
+
if (!subscriptions.hasSubscribers(threadId))
|
|
1024
|
+
claude.scheduleEphemeralRelease(threadId);
|
|
1025
|
+
}));
|
|
1026
|
+
remoteControl?.detach(connectionId);
|
|
1027
|
+
let detach;
|
|
1028
|
+
try {
|
|
1029
|
+
await handoffs.detachConnection(connectionId, stockRpc);
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
detach = error;
|
|
1033
|
+
}
|
|
1034
|
+
stockState.detach(connectionId);
|
|
1035
|
+
stockRpc.close(new Error("Client connection closed."));
|
|
1036
|
+
if (stock.readyState === WebSocketState.OPEN || stock.readyState === WebSocketState.CONNECTING) {
|
|
1037
|
+
stock.close();
|
|
1038
|
+
}
|
|
1039
|
+
logger.info("connection.closed", { connectionId });
|
|
1040
|
+
recorder.connection("closed", connectionId, { code, reason: reason.toString("utf8") });
|
|
1041
|
+
const errors = [
|
|
1042
|
+
...releases.flatMap((result) => result.status === "rejected" ? [result.reason] : []),
|
|
1043
|
+
...(detach === undefined ? [] : [detach]),
|
|
1044
|
+
];
|
|
1045
|
+
if (errors.length > 0)
|
|
1046
|
+
throw new AggregateError(errors, "Client connection cleanup failed.");
|
|
1047
|
+
})().then(resolveClosed, rejectClosed);
|
|
453
1048
|
});
|
|
1049
|
+
return { closed };
|
|
454
1050
|
}
|
|
455
1051
|
//# sourceMappingURL=clientConnection.js.map
|