@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.
Files changed (201) hide show
  1. package/README.md +141 -63
  2. package/assets/ccodex/goals/continuation.md +1 -0
  3. package/compatibility.json +5 -6
  4. package/config.example.toml +19 -4
  5. package/dist/claude/asyncQueue.d.ts +3 -0
  6. package/dist/claude/asyncQueue.js +13 -2
  7. package/dist/claude/asyncQueue.js.map +1 -1
  8. package/dist/claude/commandActions.d.ts +6 -0
  9. package/dist/claude/commandActions.js +57 -0
  10. package/dist/claude/commandActions.js.map +1 -0
  11. package/dist/claude/compactCommand.d.ts +2 -0
  12. package/dist/claude/compactCommand.js +7 -0
  13. package/dist/claude/compactCommand.js.map +1 -0
  14. package/dist/claude/goalTools.d.ts +15 -0
  15. package/dist/claude/goalTools.js +128 -0
  16. package/dist/claude/goalTools.js.map +1 -0
  17. package/dist/claude/hookMapper.d.ts +17 -4
  18. package/dist/claude/hookMapper.js +7 -5
  19. package/dist/claude/hookMapper.js.map +1 -1
  20. package/dist/claude/inputMapper.d.ts +1 -0
  21. package/dist/claude/inputMapper.js +1 -0
  22. package/dist/claude/inputMapper.js.map +1 -1
  23. package/dist/claude/rateLimits.d.ts +72 -0
  24. package/dist/claude/rateLimits.js +330 -0
  25. package/dist/claude/rateLimits.js.map +1 -0
  26. package/dist/claude/responseItemValidation.d.ts +2 -0
  27. package/dist/claude/responseItemValidation.js +40 -0
  28. package/dist/claude/responseItemValidation.js.map +1 -0
  29. package/dist/claude/resultClassifier.d.ts +8 -2
  30. package/dist/claude/resultClassifier.js +9 -1
  31. package/dist/claude/resultClassifier.js.map +1 -1
  32. package/dist/claude/service.d.ts +116 -29
  33. package/dist/claude/service.js +1337 -680
  34. package/dist/claude/service.js.map +1 -1
  35. package/dist/claude/session/backgroundOutput.d.ts +2 -0
  36. package/dist/claude/session/backgroundOutput.js +29 -0
  37. package/dist/claude/session/backgroundOutput.js.map +1 -0
  38. package/dist/claude/session/commands.d.ts +862 -0
  39. package/dist/claude/session/commands.js +2 -0
  40. package/dist/claude/session/commands.js.map +1 -0
  41. package/dist/claude/session/goalState.d.ts +49 -0
  42. package/dist/claude/session/goalState.js +407 -0
  43. package/dist/claude/session/goalState.js.map +1 -0
  44. package/dist/claude/session/mailbox.d.ts +52 -0
  45. package/dist/claude/session/mailbox.js +190 -0
  46. package/dist/claude/session/mailbox.js.map +1 -0
  47. package/dist/claude/session/outputAdapter.d.ts +11 -0
  48. package/dist/claude/session/outputAdapter.js +31 -0
  49. package/dist/claude/session/outputAdapter.js.map +1 -0
  50. package/dist/claude/session/permissionUpdates.d.ts +4 -0
  51. package/dist/claude/session/permissionUpdates.js +17 -0
  52. package/dist/claude/session/permissionUpdates.js.map +1 -0
  53. package/dist/claude/session/providerFacts.d.ts +47 -0
  54. package/dist/claude/session/providerFacts.js +84 -0
  55. package/dist/claude/session/providerFacts.js.map +1 -0
  56. package/dist/claude/session/providerRuntime.d.ts +1 -0
  57. package/dist/claude/session/providerRuntime.js +2 -0
  58. package/dist/claude/session/providerRuntime.js.map +1 -0
  59. package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
  60. package/dist/claude/session/providerRuntimeFactory.js +122 -0
  61. package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
  62. package/dist/claude/session/repository.d.ts +57 -0
  63. package/dist/claude/session/repository.js +161 -0
  64. package/dist/claude/session/repository.js.map +1 -0
  65. package/dist/claude/session/runtime.d.ts +46 -0
  66. package/dist/claude/session/runtime.js +140 -0
  67. package/dist/claude/session/runtime.js.map +1 -0
  68. package/dist/claude/session/scopeState.d.ts +61 -0
  69. package/dist/claude/session/scopeState.js +74 -0
  70. package/dist/claude/session/scopeState.js.map +1 -0
  71. package/dist/claude/session/session.d.ts +285 -0
  72. package/dist/claude/session/session.js +6778 -0
  73. package/dist/claude/session/session.js.map +1 -0
  74. package/dist/claude/session/shellRunner.d.ts +13 -0
  75. package/dist/claude/session/shellRunner.js +103 -0
  76. package/dist/claude/session/shellRunner.js.map +1 -0
  77. package/dist/claude/session/usage.d.ts +2 -0
  78. package/dist/claude/session/usage.js +10 -0
  79. package/dist/claude/session/usage.js.map +1 -0
  80. package/dist/claude/sessionRegistry.d.ts +26 -0
  81. package/dist/claude/sessionRegistry.js +118 -0
  82. package/dist/claude/sessionRegistry.js.map +1 -0
  83. package/dist/claude/shellSupervisor.d.ts +1 -0
  84. package/dist/claude/shellSupervisor.js +69 -0
  85. package/dist/claude/shellSupervisor.js.map +1 -0
  86. package/dist/claude/statusCommand.d.ts +21 -0
  87. package/dist/claude/statusCommand.js +97 -0
  88. package/dist/claude/statusCommand.js.map +1 -0
  89. package/dist/claude/toolMapper.d.ts +9 -1
  90. package/dist/claude/toolMapper.js +98 -18
  91. package/dist/claude/toolMapper.js.map +1 -1
  92. package/dist/claude/transcriptBrancher.d.ts +37 -0
  93. package/dist/claude/transcriptBrancher.js +103 -0
  94. package/dist/claude/transcriptBrancher.js.map +1 -0
  95. package/dist/cli/delegate.js +23 -1
  96. package/dist/cli/delegate.js.map +1 -1
  97. package/dist/cli/main.js +28 -12
  98. package/dist/cli/main.js.map +1 -1
  99. package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
  100. package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
  101. package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
  102. package/dist/compatibility/probe.js +11 -6
  103. package/dist/compatibility/probe.js.map +1 -1
  104. package/dist/config/config.d.ts +12 -0
  105. package/dist/config/config.js +103 -11
  106. package/dist/config/config.js.map +1 -1
  107. package/dist/daemon/daemon.js +91 -43
  108. package/dist/daemon/daemon.js.map +1 -1
  109. package/dist/daemon/ownership.d.ts +15 -0
  110. package/dist/daemon/ownership.js +185 -0
  111. package/dist/daemon/ownership.js.map +1 -0
  112. package/dist/daemon/probe.js +1 -1
  113. package/dist/daemon/supervisor.d.ts +3 -0
  114. package/dist/daemon/supervisor.js +37 -1
  115. package/dist/daemon/supervisor.js.map +1 -1
  116. package/dist/gateway/clientConnection.d.ts +7 -1
  117. package/dist/gateway/clientConnection.js +656 -60
  118. package/dist/gateway/clientConnection.js.map +1 -1
  119. package/dist/gateway/logicalThreadProjection.d.ts +8 -0
  120. package/dist/gateway/logicalThreadProjection.js +59 -0
  121. package/dist/gateway/logicalThreadProjection.js.map +1 -0
  122. package/dist/gateway/remoteRelay.js +0 -1
  123. package/dist/gateway/remoteRelay.js.map +1 -1
  124. package/dist/gateway/server.d.ts +1 -1
  125. package/dist/gateway/server.js +69 -11
  126. package/dist/gateway/server.js.map +1 -1
  127. package/dist/gateway/stockRpc.d.ts +5 -2
  128. package/dist/gateway/stockRpc.js +52 -6
  129. package/dist/gateway/stockRpc.js.map +1 -1
  130. package/dist/gateway/subscriptions.d.ts +19 -3
  131. package/dist/gateway/subscriptions.js +152 -13
  132. package/dist/gateway/subscriptions.js.map +1 -1
  133. package/dist/gateway/threadList.d.ts +7 -2
  134. package/dist/gateway/threadList.js +8 -4
  135. package/dist/gateway/threadList.js.map +1 -1
  136. package/dist/gateway/transientNotice.d.ts +15 -0
  137. package/dist/gateway/transientNotice.js +96 -1
  138. package/dist/gateway/transientNotice.js.map +1 -1
  139. package/dist/handoff/providerEpochs.d.ts +18 -0
  140. package/dist/handoff/providerEpochs.js +77 -0
  141. package/dist/handoff/providerEpochs.js.map +1 -0
  142. package/dist/handoff/service.d.ts +86 -6
  143. package/dist/handoff/service.js +1276 -168
  144. package/dist/handoff/service.js.map +1 -1
  145. package/dist/handoff/store.d.ts +189 -2
  146. package/dist/handoff/store.js +618 -7
  147. package/dist/handoff/store.js.map +1 -1
  148. package/dist/handoff/titleGeneration.d.ts +28 -0
  149. package/dist/handoff/titleGeneration.js +138 -0
  150. package/dist/handoff/titleGeneration.js.map +1 -0
  151. package/dist/management/doctor.d.ts +1 -1
  152. package/dist/management/doctor.js +32 -23
  153. package/dist/management/doctor.js.map +1 -1
  154. package/dist/management/lifecycle.js +14 -2
  155. package/dist/management/lifecycle.js.map +1 -1
  156. package/dist/management/remoteShim.d.ts +10 -0
  157. package/dist/management/remoteShim.js +65 -0
  158. package/dist/management/remoteShim.js.map +1 -0
  159. package/dist/management/setup.d.ts +5 -0
  160. package/dist/management/setup.js +120 -39
  161. package/dist/management/setup.js.map +1 -1
  162. package/dist/observability/metrics.d.ts +2 -0
  163. package/dist/observability/metrics.js +6 -0
  164. package/dist/observability/metrics.js.map +1 -1
  165. package/dist/protocol/errors.d.ts +1 -0
  166. package/dist/protocol/errors.js +4 -1
  167. package/dist/protocol/errors.js.map +1 -1
  168. package/dist/runtime/dependencies.js +3 -3
  169. package/dist/runtime/dependencies.js.map +1 -1
  170. package/dist/runtime/providerAvailability.d.ts +24 -0
  171. package/dist/runtime/providerAvailability.js +107 -0
  172. package/dist/runtime/providerAvailability.js.map +1 -0
  173. package/dist/state/stateCommand.d.ts +21 -0
  174. package/dist/state/stateCommand.js +198 -0
  175. package/dist/state/stateCommand.js.map +1 -0
  176. package/dist/state/stockStateTracker.d.ts +16 -0
  177. package/dist/state/stockStateTracker.js +137 -0
  178. package/dist/state/stockStateTracker.js.map +1 -0
  179. package/dist/store/HybridStore.d.ts +118 -2
  180. package/dist/store/HybridStore.js +23 -1
  181. package/dist/store/HybridStore.js.map +1 -1
  182. package/dist/store/memoryStore.d.ts +57 -7
  183. package/dist/store/memoryStore.js +337 -5
  184. package/dist/store/memoryStore.js.map +1 -1
  185. package/dist/store/sqliteStore.d.ts +30 -4
  186. package/dist/store/sqliteStore.js +395 -42
  187. package/dist/store/sqliteStore.js.map +1 -1
  188. package/install.sh +2 -2
  189. package/{LICENSES.md → legal/LICENSES.md} +2 -2
  190. package/{THIRD_PARTY_NOTICES.md → legal/THIRD_PARTY_NOTICES.md} +177 -148
  191. package/package.json +28 -18
  192. package/vendor/codex/continuation.md +51 -0
  193. package/BUILD_BENCHMARKS.md +0 -27
  194. package/RELEASING.md +0 -98
  195. package/SSH_VERIFY.md +0 -69
  196. package/dist/claude/interactions.d.ts +0 -23
  197. package/dist/claude/interactions.js +0 -77
  198. package/dist/claude/interactions.js.map +0 -1
  199. package/dist/claude/sessionRuntime.d.ts +0 -102
  200. package/dist/claude/sessionRuntime.js +0 -1361
  201. 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
- export function attachClientConnection(client, stockSocket, claudeModels, claude, handoffs, subscriptions, logger, cursors, metrics, recorder, remoteControl) {
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
- subscriptions.subscribe(threadId, connectionId, notificationSink, serverRequestSink);
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 (!claude.resolveServerRequest(internalServerRequestId, response)) {
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.shouldFork(forkParams)) {
176
- emitTransientNotice(params.threadId, "Compacting before fork. If the App reports ‘Codex request timed out’, ignore it—the new task will still appear in the sidebar.");
177
- const result = await handoffs.fork(forkParams, stockRpc, connectionId);
178
- if (claude.ownsThread(result.thread.id)) {
179
- subscribeClaude(result.thread.id);
180
- claude.announceThread(result.thread);
181
- }
182
- else {
183
- subscriptions.emit(result.thread.id, "thread/started", { thread: result.thread });
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
- sendResult(message.id, result);
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
- subscribeClaude(params.threadId);
217
- const result = await claude.resumeThread((message.params ?? {}));
218
- sendResult(message.id, result);
219
- claude.replayPendingRequests(params.threadId);
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
- sendResult(message.id, await claude.updateThreadSettings((message.params ?? {})));
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.announce();
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
- sendResult(message.id, claude.setGoal((message.params ?? {})));
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
- sendResult(message.id, claude.clearGoal(params.threadId));
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 prepared = await claude.prepareTurn((message.params ?? {}));
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
- prepared.announce();
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 ?? {}).threadId);
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
- if (message.method === "turn/start" && threadId) {
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
- trackForwardedRequest(message);
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?.threadId && "error" in message)
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
- metrics.connectionClosed();
444
- subscriptions.detach(connectionId);
445
- handoffs.detachConnection(connectionId);
446
- remoteControl?.detach(connectionId);
447
- stockRpc.close(new Error("Client connection closed."));
448
- if (stock.readyState === WebSocketState.OPEN || stock.readyState === WebSocketState.CONNECTING) {
449
- stock.close();
450
- }
451
- logger.info("connection.closed", { connectionId });
452
- recorder.connection("closed", connectionId, { code, reason: reason.toString("utf8") });
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