@gkorepanov/ccodex 0.3.0 → 0.3.4

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.

Potentially problematic release.


This version of @gkorepanov/ccodex might be problematic. Click here for more details.

Files changed (176) hide show
  1. package/README.md +27 -9
  2. package/RELEASING.md +14 -15
  3. package/SSH_VERIFY.md +11 -0
  4. package/archive/README.md +23 -0
  5. package/compatibility.json +1 -2
  6. package/config.example.toml +12 -4
  7. package/dist/claude/asyncQueue.d.ts +3 -0
  8. package/dist/claude/asyncQueue.js +13 -2
  9. package/dist/claude/asyncQueue.js.map +1 -1
  10. package/dist/claude/commandActions.d.ts +6 -0
  11. package/dist/claude/commandActions.js +57 -0
  12. package/dist/claude/commandActions.js.map +1 -0
  13. package/dist/claude/compactCommand.d.ts +2 -0
  14. package/dist/claude/compactCommand.js +7 -0
  15. package/dist/claude/compactCommand.js.map +1 -0
  16. package/dist/claude/goalTools.d.ts +15 -0
  17. package/dist/claude/goalTools.js +173 -0
  18. package/dist/claude/goalTools.js.map +1 -0
  19. package/dist/claude/hookMapper.d.ts +17 -4
  20. package/dist/claude/hookMapper.js +7 -5
  21. package/dist/claude/hookMapper.js.map +1 -1
  22. package/dist/claude/rateLimits.d.ts +72 -0
  23. package/dist/claude/rateLimits.js +330 -0
  24. package/dist/claude/rateLimits.js.map +1 -0
  25. package/dist/claude/responseItemValidation.d.ts +2 -0
  26. package/dist/claude/responseItemValidation.js +40 -0
  27. package/dist/claude/responseItemValidation.js.map +1 -0
  28. package/dist/claude/resultClassifier.d.ts +8 -2
  29. package/dist/claude/resultClassifier.js +1 -1
  30. package/dist/claude/resultClassifier.js.map +1 -1
  31. package/dist/claude/service.d.ts +108 -28
  32. package/dist/claude/service.js +1276 -668
  33. package/dist/claude/service.js.map +1 -1
  34. package/dist/claude/session/backgroundOutput.d.ts +2 -0
  35. package/dist/claude/session/backgroundOutput.js +29 -0
  36. package/dist/claude/session/backgroundOutput.js.map +1 -0
  37. package/dist/claude/session/commands.d.ts +855 -0
  38. package/dist/claude/session/commands.js +2 -0
  39. package/dist/claude/session/commands.js.map +1 -0
  40. package/dist/claude/session/goalState.d.ts +49 -0
  41. package/dist/claude/session/goalState.js +407 -0
  42. package/dist/claude/session/goalState.js.map +1 -0
  43. package/dist/claude/session/mailbox.d.ts +52 -0
  44. package/dist/claude/session/mailbox.js +190 -0
  45. package/dist/claude/session/mailbox.js.map +1 -0
  46. package/dist/claude/session/outputAdapter.d.ts +11 -0
  47. package/dist/claude/session/outputAdapter.js +31 -0
  48. package/dist/claude/session/outputAdapter.js.map +1 -0
  49. package/dist/claude/session/providerFacts.d.ts +47 -0
  50. package/dist/claude/session/providerFacts.js +84 -0
  51. package/dist/claude/session/providerFacts.js.map +1 -0
  52. package/dist/claude/session/providerRuntime.d.ts +1 -0
  53. package/dist/claude/session/providerRuntime.js +2 -0
  54. package/dist/claude/session/providerRuntime.js.map +1 -0
  55. package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
  56. package/dist/claude/session/providerRuntimeFactory.js +127 -0
  57. package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
  58. package/dist/claude/session/repository.d.ts +57 -0
  59. package/dist/claude/session/repository.js +161 -0
  60. package/dist/claude/session/repository.js.map +1 -0
  61. package/dist/claude/session/runtime.d.ts +46 -0
  62. package/dist/claude/session/runtime.js +140 -0
  63. package/dist/claude/session/runtime.js.map +1 -0
  64. package/dist/claude/session/scopeState.d.ts +61 -0
  65. package/dist/claude/session/scopeState.js +74 -0
  66. package/dist/claude/session/scopeState.js.map +1 -0
  67. package/dist/claude/session/session.d.ts +281 -0
  68. package/dist/claude/session/session.js +6664 -0
  69. package/dist/claude/session/session.js.map +1 -0
  70. package/dist/claude/session/shellRunner.d.ts +13 -0
  71. package/dist/claude/session/shellRunner.js +103 -0
  72. package/dist/claude/session/shellRunner.js.map +1 -0
  73. package/dist/claude/session/usage.d.ts +2 -0
  74. package/dist/claude/session/usage.js +10 -0
  75. package/dist/claude/session/usage.js.map +1 -0
  76. package/dist/claude/sessionRegistry.d.ts +26 -0
  77. package/dist/claude/sessionRegistry.js +118 -0
  78. package/dist/claude/sessionRegistry.js.map +1 -0
  79. package/dist/claude/shellSupervisor.d.ts +1 -0
  80. package/dist/claude/shellSupervisor.js +69 -0
  81. package/dist/claude/shellSupervisor.js.map +1 -0
  82. package/dist/claude/statusCommand.d.ts +21 -0
  83. package/dist/claude/statusCommand.js +97 -0
  84. package/dist/claude/statusCommand.js.map +1 -0
  85. package/dist/claude/toolMapper.d.ts +9 -1
  86. package/dist/claude/toolMapper.js +92 -16
  87. package/dist/claude/toolMapper.js.map +1 -1
  88. package/dist/claude/transcriptBrancher.d.ts +37 -0
  89. package/dist/claude/transcriptBrancher.js +103 -0
  90. package/dist/claude/transcriptBrancher.js.map +1 -0
  91. package/dist/cli/delegate.js +23 -1
  92. package/dist/cli/delegate.js.map +1 -1
  93. package/dist/cli/main.js +28 -12
  94. package/dist/cli/main.js.map +1 -1
  95. package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
  96. package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
  97. package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
  98. package/dist/compatibility/probe.js +11 -6
  99. package/dist/compatibility/probe.js.map +1 -1
  100. package/dist/config/config.d.ts +9 -0
  101. package/dist/config/config.js +80 -11
  102. package/dist/config/config.js.map +1 -1
  103. package/dist/daemon/daemon.js +91 -43
  104. package/dist/daemon/daemon.js.map +1 -1
  105. package/dist/daemon/ownership.d.ts +15 -0
  106. package/dist/daemon/ownership.js +185 -0
  107. package/dist/daemon/ownership.js.map +1 -0
  108. package/dist/daemon/supervisor.d.ts +3 -0
  109. package/dist/daemon/supervisor.js +37 -1
  110. package/dist/daemon/supervisor.js.map +1 -1
  111. package/dist/gateway/clientConnection.d.ts +7 -1
  112. package/dist/gateway/clientConnection.js +435 -48
  113. package/dist/gateway/clientConnection.js.map +1 -1
  114. package/dist/gateway/remoteRelay.js +0 -1
  115. package/dist/gateway/remoteRelay.js.map +1 -1
  116. package/dist/gateway/server.d.ts +1 -1
  117. package/dist/gateway/server.js +37 -9
  118. package/dist/gateway/server.js.map +1 -1
  119. package/dist/gateway/subscriptions.d.ts +9 -2
  120. package/dist/gateway/subscriptions.js +87 -9
  121. package/dist/gateway/subscriptions.js.map +1 -1
  122. package/dist/gateway/transientNotice.d.ts +2 -0
  123. package/dist/gateway/transientNotice.js +42 -1
  124. package/dist/gateway/transientNotice.js.map +1 -1
  125. package/dist/handoff/service.d.ts +32 -3
  126. package/dist/handoff/service.js +304 -26
  127. package/dist/handoff/service.js.map +1 -1
  128. package/dist/handoff/titleGeneration.d.ts +28 -0
  129. package/dist/handoff/titleGeneration.js +151 -0
  130. package/dist/handoff/titleGeneration.js.map +1 -0
  131. package/dist/management/doctor.d.ts +1 -1
  132. package/dist/management/doctor.js +32 -23
  133. package/dist/management/doctor.js.map +1 -1
  134. package/dist/management/lifecycle.js +14 -2
  135. package/dist/management/lifecycle.js.map +1 -1
  136. package/dist/management/remoteShim.d.ts +10 -0
  137. package/dist/management/remoteShim.js +65 -0
  138. package/dist/management/remoteShim.js.map +1 -0
  139. package/dist/management/setup.d.ts +4 -0
  140. package/dist/management/setup.js +108 -39
  141. package/dist/management/setup.js.map +1 -1
  142. package/dist/observability/metrics.d.ts +2 -0
  143. package/dist/observability/metrics.js +6 -0
  144. package/dist/observability/metrics.js.map +1 -1
  145. package/dist/protocol/errors.d.ts +1 -0
  146. package/dist/protocol/errors.js +4 -1
  147. package/dist/protocol/errors.js.map +1 -1
  148. package/dist/runtime/dependencies.js +2 -2
  149. package/dist/runtime/dependencies.js.map +1 -1
  150. package/dist/runtime/providerAvailability.d.ts +24 -0
  151. package/dist/runtime/providerAvailability.js +107 -0
  152. package/dist/runtime/providerAvailability.js.map +1 -0
  153. package/dist/state/stateCommand.d.ts +21 -0
  154. package/dist/state/stateCommand.js +198 -0
  155. package/dist/state/stateCommand.js.map +1 -0
  156. package/dist/state/stockStateTracker.d.ts +14 -0
  157. package/dist/state/stockStateTracker.js +120 -0
  158. package/dist/state/stockStateTracker.js.map +1 -0
  159. package/dist/store/HybridStore.d.ts +118 -2
  160. package/dist/store/HybridStore.js +23 -1
  161. package/dist/store/HybridStore.js.map +1 -1
  162. package/dist/store/memoryStore.d.ts +57 -7
  163. package/dist/store/memoryStore.js +337 -5
  164. package/dist/store/memoryStore.js.map +1 -1
  165. package/dist/store/sqliteStore.d.ts +30 -4
  166. package/dist/store/sqliteStore.js +395 -42
  167. package/dist/store/sqliteStore.js.map +1 -1
  168. package/install.sh +2 -2
  169. package/package.json +18 -10
  170. package/dist/claude/interactions.d.ts +0 -23
  171. package/dist/claude/interactions.js +0 -77
  172. package/dist/claude/interactions.js.map +0 -1
  173. package/dist/claude/sessionRuntime.d.ts +0 -102
  174. package/dist/claude/sessionRuntime.js +0 -1361
  175. package/dist/claude/sessionRuntime.js.map +0 -1
  176. /package/{BUILD_BENCHMARKS.md → archive/BUILD_BENCHMARKS.md} +0 -0
@@ -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,26 @@ 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 { 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
+ export function attachClientConnection(client, stockSocket, claudeModels, claude, handoffs, subscriptions, logger, cursors, metrics, recorder, remoteControl, features = DEFAULT_FEATURES, providerAvailability, sharedStockState) {
12
19
  const connectionId = uuidv7();
20
+ let resolveClosed;
21
+ let rejectClosed;
22
+ const closed = new Promise((resolve, reject) => {
23
+ resolveClosed = resolve;
24
+ rejectClosed = reject;
25
+ });
26
+ void closed.catch(() => undefined);
13
27
  const stock = connectStock(stockSocket);
14
28
  const stockRpc = new StockRpc(stock);
29
+ const stockState = sharedStockState ?? new StockStateTracker();
15
30
  const queued = [];
16
31
  const requestStarted = new Map();
17
32
  const forwardedRequests = new Map();
@@ -32,9 +47,23 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
32
47
  const params = message.params && typeof message.params === "object"
33
48
  ? message.params
34
49
  : undefined;
50
+ const systemEphemeral = (message.method === "thread/start" || message.method === "thread/fork")
51
+ && params?.ephemeral === true
52
+ && (params.threadSource === "system" || (message.method === "thread/start" && params.threadSource !== "user"));
53
+ const internalEphemeralThread = typeof params?.threadId === "string"
54
+ && handoffs.ownsSystemEphemeral(connectionId, params.threadId);
55
+ const foreground = !systemEphemeral && !internalEphemeralThread
56
+ && ["thread/start", "thread/resume", "thread/fork", "turn/start"].includes(message.method)
57
+ ? "codex"
58
+ : undefined;
35
59
  forwardedRequests.set(requestKey(message.id), {
36
60
  method: message.method,
37
61
  ...(typeof params?.threadId === "string" ? { threadId: params.threadId } : {}),
62
+ ...(systemEphemeral
63
+ ? { systemEphemeral: message.params }
64
+ : {}),
65
+ ...(foreground ? { foreground } : {}),
66
+ ...(["thread/delete", "thread/unsubscribe"].includes(message.method) ? { clearForeground: true } : {}),
38
67
  });
39
68
  };
40
69
  metrics.connectionOpened();
@@ -78,6 +107,111 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
78
107
  completeLatency(id, "claude");
79
108
  sendJson({ id, error: { code, message: error instanceof Error ? error.message : String(error) } });
80
109
  };
110
+ let foreground;
111
+ let foregroundGeneration = 0;
112
+ let unknownStatusDiagnosed = false;
113
+ const emitRateLimits = (response) => {
114
+ for (const notification of rateLimitNotifications(response))
115
+ sendJson(notification);
116
+ };
117
+ const emitStockRateLimits = (value) => {
118
+ if (!value || typeof value !== "object")
119
+ return;
120
+ const result = value;
121
+ const buckets = result.rateLimitsByLimitId ? Object.values(result.rateLimitsByLimitId) : [];
122
+ const snapshots = buckets.length > 0 ? buckets : result.rateLimits ? [result.rateLimits] : [];
123
+ for (const rateLimits of snapshots)
124
+ sendJson({ method: "account/rateLimits/updated", params: { rateLimits } });
125
+ };
126
+ const combinedStatus = async (threadId) => {
127
+ const availability = providerAvailability
128
+ ? await providerAvailability.refreshAll()
129
+ : {
130
+ claude: { provider: "claude", state: "ready" },
131
+ codex: { provider: "codex", state: "ready" },
132
+ };
133
+ const [claudeUsage, codexRateLimits] = await Promise.all([
134
+ availability.claude.state === "ready"
135
+ ? claude.readRateLimitStatus(threadId).catch((error) => ({
136
+ rateLimits: claude.cachedRateLimits(),
137
+ unavailableReason: error instanceof Error ? error.message : String(error),
138
+ }))
139
+ : undefined,
140
+ availability.codex.state === "ready"
141
+ ? stockRpc.request("account/rateLimits/read", undefined).catch(() => undefined)
142
+ : undefined,
143
+ ]);
144
+ return formatCCodexStatus({
145
+ claude: { availability: availability.claude, ...(claudeUsage ? { usage: claudeUsage } : {}) },
146
+ codex: {
147
+ availability: availability.codex,
148
+ ...(codexRateLimits ? { rateLimits: codexRateLimits } : {}),
149
+ },
150
+ });
151
+ };
152
+ const threadState = async (threadId) => {
153
+ if (claude.ownsThread(threadId))
154
+ return formatCCodexState(claude.stateSnapshot(threadId));
155
+ let response;
156
+ try {
157
+ response = await stockRpc.request("thread/read", {
158
+ threadId,
159
+ includeTurns: true,
160
+ });
161
+ }
162
+ catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ if (!message.includes("includeTurns is unavailable")
165
+ && !message.includes("ephemeral threads do not support includeTurns"))
166
+ throw error;
167
+ response = await stockRpc.request("thread/read", {
168
+ threadId,
169
+ includeTurns: false,
170
+ });
171
+ }
172
+ return formatCCodexState(stockState.snapshot(response.thread));
173
+ };
174
+ const publishForegroundRateLimits = (generation) => {
175
+ const selected = foreground;
176
+ if (!selected)
177
+ return;
178
+ if (selected.provider === "claude") {
179
+ void claude.readRateLimits(selected.threadId).then((response) => {
180
+ if (foregroundGeneration === generation && foreground?.provider === "claude")
181
+ emitRateLimits(response);
182
+ }).catch((error) => logger.warn("claude.rate-limits.publish-failed", { connectionId, error: String(error) }));
183
+ return;
184
+ }
185
+ void (async () => {
186
+ if (providerAvailability && (await providerAvailability.read("codex")).state !== "ready")
187
+ return;
188
+ const response = await stockRpc.request("account/rateLimits/read", undefined);
189
+ if (foregroundGeneration === generation && foreground?.provider === "codex")
190
+ emitStockRateLimits(response);
191
+ })().catch((error) => logger.warn("codex.rate-limits.publish-failed", { connectionId, error: String(error) }));
192
+ };
193
+ const selectForeground = (provider, threadId) => {
194
+ foreground = { provider, threadId };
195
+ unknownStatusDiagnosed = false;
196
+ foregroundGeneration += 1;
197
+ publishForegroundRateLimits(foregroundGeneration);
198
+ };
199
+ const clearForeground = (threadId) => {
200
+ if (foreground?.threadId !== threadId)
201
+ return;
202
+ foreground = undefined;
203
+ foregroundGeneration += 1;
204
+ };
205
+ const diagnoseUnknownStatus = (signal) => {
206
+ if (unknownStatusDiagnosed)
207
+ return;
208
+ unknownStatusDiagnosed = true;
209
+ logger.warn("provider.status.unknown", {
210
+ connectionId,
211
+ signal,
212
+ behavior: "preserving provider-labelled stock payload until a foreground operation succeeds",
213
+ });
214
+ };
81
215
  const notificationSink = (method, params) => {
82
216
  if (method === "error" && params && typeof params === "object") {
83
217
  const failure = params;
@@ -101,9 +235,19 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
101
235
  const serverRequestSink = (id, method, params) => {
102
236
  sendJson({ id: serverRequestIds.wireId(id), method, params });
103
237
  };
104
- subscriptions.attach(connectionId, notificationSink);
238
+ subscriptions.attach(connectionId, notificationSink, serverRequestSink);
239
+ claude.subscribeRateLimits(connectionId, (response) => {
240
+ if (foreground?.provider === "claude")
241
+ emitRateLimits(response);
242
+ });
243
+ const subscribedClaudeThreads = new Set();
105
244
  const subscribeClaude = (threadId) => {
106
245
  subscriptions.subscribe(threadId, connectionId, notificationSink, serverRequestSink);
246
+ subscribedClaudeThreads.add(threadId);
247
+ };
248
+ const muteClaudeDuringSnapshot = (threadId) => {
249
+ subscribeClaude(threadId);
250
+ subscriptions.mute(threadId, connectionId);
107
251
  };
108
252
  client.on("message", async (data, isBinary) => {
109
253
  recorder.frame(connectionId, "client_to_gateway", data, isBinary);
@@ -113,7 +257,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
113
257
  : undefined;
114
258
  if (message && isResponse(message) && internalServerRequestId) {
115
259
  const response = "result" in message ? message.result : { rpcError: message.error };
116
- if (!claude.resolveServerRequest(internalServerRequestId, response)) {
260
+ if (!await claude.resolveServerRequest(internalServerRequestId, response)) {
117
261
  logger.warn("claude.interaction.unknown-response", { connectionId, requestId: internalServerRequestId });
118
262
  }
119
263
  return;
@@ -129,6 +273,28 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
129
273
  sendResult(message.id, await mergedModelList((message.params ?? {}), stockRpc, claudeModels, logger, cursors));
130
274
  return;
131
275
  }
276
+ if (message.method === "account/rateLimits/read" && foreground?.provider === "claude") {
277
+ sendResult(message.id, await claude.readRateLimits(foreground.threadId));
278
+ return;
279
+ }
280
+ if (message.method === "account/usage/read" && foreground?.provider === "claude") {
281
+ // The captured App status flow does not request this method. If a
282
+ // client does, lifetime/streak Codex statistics have no Claude
283
+ // equivalent and must not be mislabelled as provider usage.
284
+ sendResult(message.id, {
285
+ summary: {
286
+ lifetimeTokens: null,
287
+ peakDailyTokens: null,
288
+ longestRunningTurnSec: null,
289
+ currentStreakDays: null,
290
+ longestStreakDays: null,
291
+ },
292
+ dailyUsageBuckets: null,
293
+ });
294
+ return;
295
+ }
296
+ if (message.method === "account/rateLimits/read" && !foreground)
297
+ diagnoseUnknownStatus("account/rateLimits/read");
132
298
  if (message.method === "thread/list") {
133
299
  sendResult(message.id, await mergedThreadList((message.params ?? {}), stockRpc, claude, cursors));
134
300
  return;
@@ -141,13 +307,77 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
141
307
  const params = (message.params ?? {});
142
308
  if (params.model && claude.ownsModel(params.model)) {
143
309
  const result = await claude.startThread(params);
310
+ handoffs.observeDurableThread(connectionId, "claude");
144
311
  sendResult(message.id, result);
312
+ selectForeground("claude", result.thread.id);
145
313
  subscribeClaude(result.thread.id);
146
- claude.announceThread(result.thread);
314
+ await claude.announceThread(result.thread);
147
315
  return;
148
316
  }
149
317
  }
150
318
  const params = (message.params ?? {});
319
+ if (params.threadId && message.method === "turn/start"
320
+ && claude.ownsThread(params.threadId) && !claude.isChildProjection(params.threadId)) {
321
+ const promptedCompact = claudeCompactCommand((message.params ?? {}).input);
322
+ if (promptedCompact) {
323
+ const prepared = await claude.preparePromptedCompact(params.threadId, promptedCompact);
324
+ sendResult(message.id, prepared.response);
325
+ selectForeground("claude", params.threadId);
326
+ await prepared.announce();
327
+ return;
328
+ }
329
+ }
330
+ if (params.threadId && message.method === "turn/start" && features.statusCommand) {
331
+ const turn = (message.params ?? {});
332
+ if (isCCodexStateCommand(turn.input)) {
333
+ if (claude.ownsThread(params.threadId) && !claude.isChildProjection(params.threadId)) {
334
+ const prepared = await claude.prepareStateTurn(turn, () => threadState(params.threadId));
335
+ sendResult(message.id, prepared.response);
336
+ selectForeground("claude", params.threadId);
337
+ await prepared.announce();
338
+ prepared.start();
339
+ }
340
+ else {
341
+ const notice = transientCommandNotice(params.threadId, turn.input, await threadState(params.threadId), turn.clientUserMessageId ?? null);
342
+ sendResult(message.id, notice.response);
343
+ selectForeground(claude.ownsThread(params.threadId) ? "claude" : "codex", params.threadId);
344
+ emitNotice(notice);
345
+ }
346
+ return;
347
+ }
348
+ if (isCCodexStatusCommand(turn.input)) {
349
+ if (claude.ownsThread(params.threadId) && !claude.isChildProjection(params.threadId)) {
350
+ const prepared = await claude.prepareStatusTurn(turn, () => combinedStatus(params.threadId));
351
+ sendResult(message.id, prepared.response);
352
+ selectForeground("claude", params.threadId);
353
+ await prepared.announce();
354
+ prepared.start();
355
+ }
356
+ else {
357
+ const notice = transientCommandNotice(params.threadId, turn.input, await combinedStatus(claude.ownsThread(params.threadId) ? params.threadId : undefined), turn.clientUserMessageId ?? null);
358
+ sendResult(message.id, notice.response);
359
+ selectForeground(claude.ownsThread(params.threadId) ? "claude" : "codex", params.threadId);
360
+ emitNotice(notice);
361
+ }
362
+ return;
363
+ }
364
+ }
365
+ if (params.threadId && message.method === "turn/start"
366
+ && !claude.ownsThread(params.threadId) && providerAvailability) {
367
+ let availability = await providerAvailability.read("codex");
368
+ if (availability.state !== "ready")
369
+ availability = await providerAvailability.refresh("codex");
370
+ if (availability.state !== "ready") {
371
+ const notice = transientSystemNotice(params.threadId, providerUnavailableMessage(availability), "error");
372
+ sendResult(message.id, notice.response);
373
+ selectForeground("codex", params.threadId);
374
+ emitNotice(notice);
375
+ return;
376
+ }
377
+ }
378
+ if (params.threadId && message.method === "turn/start") {
379
+ handoffs.observeDurableTurn(connectionId, (message.params ?? {}));
380
+ }
151
381
  if (params.threadId && message.method === "thread/settings/update") {
152
382
  const update = (message.params ?? {});
153
383
  const staged = handoffs.interceptSettings(update);
@@ -172,25 +402,45 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
172
402
  }
173
403
  if (params.threadId && message.method === "thread/fork") {
174
404
  const forkParams = (message.params ?? {});
405
+ if (handoffs.isSystemEphemeralFork(forkParams)) {
406
+ sendResult(message.id, await handoffs.forkSystemEphemeral(forkParams, stockRpc, connectionId));
407
+ return;
408
+ }
175
409
  if (handoffs.shouldFork(forkParams)) {
176
410
  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
411
  const result = await handoffs.fork(forkParams, stockRpc, connectionId);
412
+ sendResult(message.id, result);
413
+ selectForeground(claude.ownsThread(result.thread.id) ? "claude" : "codex", result.thread.id);
178
414
  if (claude.ownsThread(result.thread.id)) {
179
415
  subscribeClaude(result.thread.id);
180
- claude.announceThread(result.thread);
416
+ await claude.announceThread(result.thread);
181
417
  }
182
418
  else {
183
419
  subscriptions.emit(result.thread.id, "thread/started", { thread: result.thread });
184
420
  }
185
- sendResult(message.id, result);
186
421
  return;
187
422
  }
188
423
  }
424
+ if (params.threadId && message.method === "thread/delete"
425
+ && handoffs.ownsSystemEphemeral(connectionId, params.threadId)) {
426
+ const result = await stockRpc.request("thread/delete", message.params ?? {});
427
+ handoffs.releaseSystemEphemeral(connectionId, params.threadId);
428
+ sendResult(message.id, result);
429
+ return;
430
+ }
431
+ if (params.threadId && message.method === "thread/unsubscribe"
432
+ && handoffs.ownsSystemEphemeral(connectionId, params.threadId)) {
433
+ const result = await stockRpc.request("thread/unsubscribe", message.params ?? {});
434
+ handoffs.releaseSystemEphemeral(connectionId, params.threadId);
435
+ sendResult(message.id, result);
436
+ return;
437
+ }
189
438
  if (params.threadId && message.method === "thread/delete" && !claude.ownsThread(params.threadId)
190
439
  && (handoffs.pending(params.threadId) || handoffs.overlay(params.threadId))) {
191
440
  const result = await stockRpc.request("thread/delete", message.params ?? {});
192
441
  handoffs.clearThread(params.threadId);
193
442
  sendResult(message.id, result);
443
+ clearForeground(params.threadId);
194
444
  return;
195
445
  }
196
446
  if (params.threadId && handoffs.overlay(params.threadId)) {
@@ -200,6 +450,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
200
450
  }
201
451
  if (message.method === "thread/resume") {
202
452
  sendResult(message.id, await handoffs.resumeOverlay((message.params ?? {}), stockRpc));
453
+ selectForeground("codex", params.threadId);
203
454
  return;
204
455
  }
205
456
  if (message.method === "thread/turns/list") {
@@ -213,10 +464,29 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
213
464
  }
214
465
  if (params.threadId && claude.ownsThread(params.threadId)) {
215
466
  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);
467
+ muteClaudeDuringSnapshot(params.threadId);
468
+ try {
469
+ const prepared = await claude.prepareResume((message.params ?? {}));
470
+ const result = prepared.response;
471
+ const snapshotHighWatermark = claude.eventHighWatermark(params.threadId);
472
+ const tokenUsage = claude.latestTokenUsage(params.threadId);
473
+ sendResult(message.id, result);
474
+ selectForeground("claude", params.threadId);
475
+ subscriptions.unmute(params.threadId, connectionId);
476
+ if (tokenUsage && tokenUsage.sequence <= snapshotHighWatermark) {
477
+ notificationSink(tokenUsage.method, tokenUsage.params);
478
+ }
479
+ for (const event of claude.eventsAfter(params.threadId, snapshotHighWatermark)) {
480
+ notificationSink(event.method, event.params);
481
+ }
482
+ await prepared.notifyGoalSnapshot(notificationSink);
483
+ await claude.replayPendingRequests(params.threadId, connectionId);
484
+ }
485
+ catch (error) {
486
+ subscriptions.unmute(params.threadId, connectionId);
487
+ subscriptions.unsubscribe(params.threadId, connectionId);
488
+ throw error;
489
+ }
220
490
  return;
221
491
  }
222
492
  if (message.method === "thread/read") {
@@ -235,7 +505,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
235
505
  }
236
506
  if (message.method === "thread/unsubscribe") {
237
507
  subscriptions.unsubscribe(params.threadId, connectionId);
508
+ subscribedClaudeThreads.delete(params.threadId);
238
509
  sendResult(message.id, { status: "unsubscribed" });
510
+ clearForeground(params.threadId);
511
+ if (!subscriptions.hasSubscribers(params.threadId))
512
+ await claude.releaseEphemeralThread(params.threadId);
239
513
  return;
240
514
  }
241
515
  if (message.method === "thread/name/set") {
@@ -243,7 +517,7 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
243
517
  return;
244
518
  }
245
519
  if (message.method === "thread/metadata/update") {
246
- sendResult(message.id, claude.updateThreadMetadata((message.params ?? {})));
520
+ sendResult(message.id, await claude.updateThreadMetadata((message.params ?? {})));
247
521
  return;
248
522
  }
249
523
  if (message.method === "thread/settings/update") {
@@ -255,20 +529,22 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
255
529
  return;
256
530
  }
257
531
  if (message.method === "thread/unarchive") {
258
- sendResult(message.id, claude.unarchiveThread(params.threadId));
532
+ sendResult(message.id, await claude.unarchiveThread(params.threadId));
259
533
  return;
260
534
  }
261
535
  if (message.method === "thread/delete") {
262
536
  const result = await claude.deleteThread(params.threadId);
263
537
  handoffs.clearThread(params.threadId);
264
538
  sendResult(message.id, result);
539
+ clearForeground(params.threadId);
265
540
  return;
266
541
  }
267
542
  if (message.method === "thread/fork") {
268
543
  const result = await claude.forkThread((message.params ?? {}));
269
544
  sendResult(message.id, result);
545
+ selectForeground("claude", result.thread.id);
270
546
  subscribeClaude(result.thread.id);
271
- claude.announceThread(result.thread);
547
+ await claude.announceThread(result.thread);
272
548
  return;
273
549
  }
274
550
  if (message.method === "thread/rollback") {
@@ -287,38 +563,64 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
287
563
  sendResult(message.id, await claude.shellCommand((message.params ?? {})));
288
564
  return;
289
565
  }
566
+ if (message.method === "thread/backgroundTerminals/clean") {
567
+ sendResult(message.id, await claude.cleanBackgroundTerminals((message.params ?? {})));
568
+ return;
569
+ }
570
+ if (message.method === "thread/backgroundTerminals/list") {
571
+ sendResult(message.id, await claude.listBackgroundTerminals((message.params ?? {})));
572
+ return;
573
+ }
574
+ if (message.method === "thread/backgroundTerminals/terminate") {
575
+ sendResult(message.id, await claude.terminateBackgroundTerminal((message.params ?? {})));
576
+ return;
577
+ }
290
578
  if (message.method === "review/start") {
291
579
  const prepared = await claude.prepareReview((message.params ?? {}));
292
- sendResult(message.id, prepared.response);
293
580
  if (prepared.forkedThread) {
294
581
  subscribeClaude(prepared.forkedThread.id);
295
- claude.announceThread(prepared.forkedThread);
582
+ await claude.announceThread(prepared.forkedThread);
296
583
  }
297
- prepared.announce();
584
+ sendResult(message.id, prepared.response);
585
+ await prepared.announce();
298
586
  prepared.start();
299
587
  return;
300
588
  }
301
589
  if (message.method === "thread/goal/set") {
302
- sendResult(message.id, claude.setGoal((message.params ?? {})));
590
+ const prepared = await claude.prepareGoalSet((message.params ?? {}));
591
+ try {
592
+ sendResult(message.id, prepared.response);
593
+ }
594
+ finally {
595
+ await prepared.notify();
596
+ }
303
597
  return;
304
598
  }
305
599
  if (message.method === "thread/goal/get") {
306
- sendResult(message.id, claude.getGoal(params.threadId));
600
+ sendResult(message.id, await claude.getGoal(params.threadId));
307
601
  return;
308
602
  }
309
603
  if (message.method === "thread/goal/clear") {
310
- sendResult(message.id, claude.clearGoal(params.threadId));
604
+ const prepared = await claude.prepareGoalClear(params.threadId);
605
+ try {
606
+ sendResult(message.id, prepared.response);
607
+ }
608
+ finally {
609
+ await prepared.notify();
610
+ }
311
611
  return;
312
612
  }
313
613
  if (message.method === "turn/start") {
314
- const prepared = await claude.prepareTurn((message.params ?? {}));
614
+ const turn = (message.params ?? {});
615
+ const prepared = await claude.prepareTurn(turn);
315
616
  sendResult(message.id, prepared.response);
316
- prepared.announce();
617
+ selectForeground("claude", params.threadId);
618
+ await prepared.announce();
317
619
  prepared.start();
318
620
  return;
319
621
  }
320
622
  if (message.method === "turn/interrupt") {
321
- await claude.interruptTurn((message.params ?? {}).threadId);
623
+ await claude.interruptTurn((message.params ?? {}));
322
624
  sendResult(message.id, {});
323
625
  return;
324
626
  }
@@ -341,18 +643,28 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
341
643
  ? message.params
342
644
  : undefined;
343
645
  const threadId = typeof params?.threadId === "string" ? params.threadId : undefined;
344
- if (message.method === "turn/start" && threadId) {
646
+ const childProjection = Boolean(threadId && claude.isChildProjection(threadId));
647
+ if (message.method === "turn/start" && threadId && !childProjection) {
345
648
  const notice = transientSystemNotice(threadId, failure.message, "error");
346
649
  sendResult(message.id, notice.response);
347
650
  emitNotice(notice);
348
651
  return;
349
652
  }
350
- if (threadId)
653
+ if (threadId && !childProjection)
351
654
  emitSystemError(threadId, failure.message);
352
655
  sendError(message.id, failure.code, failure.message);
353
- if (threadId) {
656
+ if (threadId && !childProjection) {
354
657
  if (claude.ownsThread(threadId)) {
355
- claude.reportError(threadId, typeof params?.turnId === "string" ? params.turnId : undefined, failure.message, rpcCodexErrorInfo(failure.code));
658
+ await claude.reportError(threadId, typeof params?.turnId === "string" ? params.turnId : undefined, failure.message, rpcCodexErrorInfo(failure.code)).catch((diagnosticError) => {
659
+ logger.warn("claude.request.error-report-failed", {
660
+ connectionId,
661
+ method: message.method,
662
+ originalError: failure.message,
663
+ diagnosticError: diagnosticError instanceof Error
664
+ ? diagnosticError.message
665
+ : String(diagnosticError),
666
+ });
667
+ });
356
668
  }
357
669
  else {
358
670
  sendJson({
@@ -369,12 +681,20 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
369
681
  return;
370
682
  }
371
683
  }
372
- trackForwardedRequest(message);
684
+ let forwarded = message;
685
+ if (message && isRequest(message) && message.method === "turn/start") {
686
+ const params = handoffs.prepareTitleTurn(connectionId, (message.params ?? {}));
687
+ if (params !== message.params)
688
+ forwarded = { ...message, params };
689
+ }
690
+ if (forwarded)
691
+ stockState.observeRequest(connectionId, forwarded);
692
+ trackForwardedRequest(forwarded);
373
693
  if (stock.readyState === WebSocketState.OPEN) {
374
- stock.send(data, { binary: isBinary });
694
+ stock.send(forwarded === message ? data : JSON.stringify(forwarded), { binary: isBinary });
375
695
  }
376
696
  else {
377
- queued.push({ data, isBinary });
697
+ queued.push({ data: forwarded === message ? data : Buffer.from(JSON.stringify(forwarded)), isBinary });
378
698
  }
379
699
  });
380
700
  stock.once("open", () => {
@@ -384,6 +704,11 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
384
704
  });
385
705
  stock.on("message", (data, isBinary) => {
386
706
  const message = isBinary ? undefined : parseRpcMessage(data);
707
+ if (message) {
708
+ stockState.observeResponse(connectionId, message);
709
+ stockState.observeNotification(message);
710
+ }
711
+ let foregroundAfterForward;
387
712
  if (message && "method" in message && !("id" in message) && message.method === "remoteControl/status/changed" && remoteControl) {
388
713
  remoteControl.intercept(connectionId, notificationSink, message.params);
389
714
  return;
@@ -392,12 +717,52 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
392
717
  completeLatency(message.id, "stock");
393
718
  const forwarded = forwardedRequests.get(requestKey(message.id));
394
719
  forwardedRequests.delete(requestKey(message.id));
395
- if (forwarded?.threadId && "error" in message)
720
+ if (forwarded?.systemEphemeral && "result" in message) {
721
+ const result = message.result && typeof message.result === "object"
722
+ ? message.result
723
+ : undefined;
724
+ if (typeof result?.thread?.id === "string") {
725
+ handoffs.registerForwardedEphemeralCandidate(connectionId, result.thread.id, forwarded.systemEphemeral);
726
+ }
727
+ }
728
+ if (forwarded?.threadId && isUuid(forwarded.threadId) && "error" in message) {
396
729
  emitSystemError(forwarded.threadId, message.error.message);
730
+ }
731
+ if (forwarded && "result" in message) {
732
+ const result = message.result && typeof message.result === "object"
733
+ ? message.result
734
+ : undefined;
735
+ const resultThreadId = typeof result?.thread?.id === "string" ? result.thread.id : forwarded.threadId;
736
+ if (forwarded.foreground && resultThreadId) {
737
+ handoffs.observeDurableThread(connectionId, "stock");
738
+ foregroundAfterForward = () => selectForeground(forwarded.foreground, resultThreadId);
739
+ }
740
+ else if (forwarded.clearForeground && forwarded.threadId) {
741
+ foregroundAfterForward = () => clearForeground(forwarded.threadId);
742
+ }
743
+ }
397
744
  }
398
745
  if (message && stockRpc.handle(message))
399
746
  return;
400
747
  if (message && "method" in message) {
748
+ if (!("id" in message) && message.method === "account/rateLimits/updated" && foreground?.provider === "claude") {
749
+ publishForegroundRateLimits(foregroundGeneration);
750
+ return;
751
+ }
752
+ if (!("id" in message) && message.method === "account/rateLimits/updated" && !foreground) {
753
+ diagnoseUnknownStatus("account/rateLimits/updated");
754
+ }
755
+ if (handoffs.captureInternalStockMessage(connectionId, message)) {
756
+ if (isRequest(message))
757
+ stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
758
+ return;
759
+ }
760
+ const rewritten = handoffs.rewriteTitleMessages(message);
761
+ if (rewritten) {
762
+ for (const output of rewritten)
763
+ sendJson(output);
764
+ return;
765
+ }
401
766
  if (handoffs.suppressStockTargetMessage(connectionId, message)) {
402
767
  if (isRequest(message))
403
768
  stock.send(JSON.stringify({ id: message.id, result: { decision: "decline" } }));
@@ -417,17 +782,12 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
417
782
  if (typeof error?.message === "string")
418
783
  emitSystemError(threadId, error.message);
419
784
  }
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
785
  }
427
786
  if (client.readyState === WebSocketState.OPEN) {
428
787
  recorder.frame(connectionId, "gateway_to_client", data, isBinary);
429
788
  client.send(data, { binary: isBinary });
430
789
  }
790
+ foregroundAfterForward?.();
431
791
  });
432
792
  const closeClient = (code, reason) => {
433
793
  if (client.readyState === WebSocketState.OPEN)
@@ -439,17 +799,44 @@ export function attachClientConnection(client, stockSocket, claudeModels, claude
439
799
  });
440
800
  stock.on("close", () => closeClient(1011, "Stock app-server closed"));
441
801
  client.on("error", (error) => logger.warn("connection.client.error", { connectionId, error: error.message }));
802
+ let closeStarted = false;
442
803
  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") });
804
+ if (closeStarted)
805
+ return;
806
+ closeStarted = true;
807
+ void (async () => {
808
+ metrics.connectionClosed();
809
+ const releasedThreads = [...subscribedClaudeThreads];
810
+ subscribedClaudeThreads.clear();
811
+ subscriptions.detach(connectionId);
812
+ claude.unsubscribeRateLimits(connectionId);
813
+ const releases = await Promise.allSettled(releasedThreads.map(async (threadId) => {
814
+ if (!subscriptions.hasSubscribers(threadId))
815
+ await claude.releaseEphemeralThread(threadId);
816
+ }));
817
+ remoteControl?.detach(connectionId);
818
+ let detach;
819
+ try {
820
+ await handoffs.detachConnection(connectionId, stockRpc);
821
+ }
822
+ catch (error) {
823
+ detach = error;
824
+ }
825
+ stockState.detach(connectionId);
826
+ stockRpc.close(new Error("Client connection closed."));
827
+ if (stock.readyState === WebSocketState.OPEN || stock.readyState === WebSocketState.CONNECTING) {
828
+ stock.close();
829
+ }
830
+ logger.info("connection.closed", { connectionId });
831
+ recorder.connection("closed", connectionId, { code, reason: reason.toString("utf8") });
832
+ const errors = [
833
+ ...releases.flatMap((result) => result.status === "rejected" ? [result.reason] : []),
834
+ ...(detach === undefined ? [] : [detach]),
835
+ ];
836
+ if (errors.length > 0)
837
+ throw new AggregateError(errors, "Client connection cleanup failed.");
838
+ })().then(resolveClosed, rejectClosed);
453
839
  });
840
+ return { closed };
454
841
  }
455
842
  //# sourceMappingURL=clientConnection.js.map