@zakstam/codex-local-component 0.2.1 → 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 (181) hide show
  1. package/README.md +26 -4
  2. package/dist/app-server/client.d.ts +51 -1
  3. package/dist/app-server/client.d.ts.map +1 -1
  4. package/dist/app-server/client.js +77 -3
  5. package/dist/app-server/client.js.map +1 -1
  6. package/dist/app-server/index.d.ts +1 -1
  7. package/dist/app-server/index.d.ts.map +1 -1
  8. package/dist/app-server/index.js +1 -1
  9. package/dist/app-server/index.js.map +1 -1
  10. package/dist/client/index.d.ts +4 -2
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +3 -1
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/reasoning.d.ts +10 -0
  15. package/dist/client/reasoning.d.ts.map +1 -0
  16. package/dist/client/reasoning.js +4 -0
  17. package/dist/client/reasoning.js.map +1 -0
  18. package/dist/client/serverRequests.d.ts +14 -0
  19. package/dist/client/serverRequests.d.ts.map +1 -0
  20. package/dist/client/serverRequests.js +10 -0
  21. package/dist/client/serverRequests.js.map +1 -0
  22. package/dist/client/threads.d.ts +39 -3
  23. package/dist/client/threads.d.ts.map +1 -1
  24. package/dist/client/threads.js +18 -0
  25. package/dist/client/threads.js.map +1 -1
  26. package/dist/client/types.d.ts +23 -1
  27. package/dist/client/types.d.ts.map +1 -1
  28. package/dist/component/approvals.d.ts +1 -1
  29. package/dist/component/index.d.ts +2 -0
  30. package/dist/component/index.d.ts.map +1 -1
  31. package/dist/component/index.js +2 -0
  32. package/dist/component/index.js.map +1 -1
  33. package/dist/component/ingest/applyApprovals.d.ts +5 -0
  34. package/dist/component/ingest/applyApprovals.d.ts.map +1 -0
  35. package/dist/component/ingest/applyApprovals.js +59 -0
  36. package/dist/component/ingest/applyApprovals.js.map +1 -0
  37. package/dist/component/ingest/applyMessages.d.ts +4 -0
  38. package/dist/component/ingest/applyMessages.d.ts.map +1 -0
  39. package/dist/component/ingest/applyMessages.js +149 -0
  40. package/dist/component/ingest/applyMessages.js.map +1 -0
  41. package/dist/component/ingest/applyStreams.d.ts +7 -0
  42. package/dist/component/ingest/applyStreams.d.ts.map +1 -0
  43. package/dist/component/ingest/applyStreams.js +189 -0
  44. package/dist/component/ingest/applyStreams.js.map +1 -0
  45. package/dist/component/ingest/applyTurns.d.ts +6 -0
  46. package/dist/component/ingest/applyTurns.d.ts.map +1 -0
  47. package/dist/component/ingest/applyTurns.js +94 -0
  48. package/dist/component/ingest/applyTurns.js.map +1 -0
  49. package/dist/component/ingest/checkpoints.d.ts +12 -0
  50. package/dist/component/ingest/checkpoints.d.ts.map +1 -0
  51. package/dist/component/ingest/checkpoints.js +63 -0
  52. package/dist/component/ingest/checkpoints.js.map +1 -0
  53. package/dist/component/ingest/index.d.ts +10 -0
  54. package/dist/component/ingest/index.d.ts.map +1 -0
  55. package/dist/component/ingest/index.js +76 -0
  56. package/dist/component/ingest/index.js.map +1 -0
  57. package/dist/component/ingest/normalize.d.ts +5 -0
  58. package/dist/component/ingest/normalize.d.ts.map +1 -0
  59. package/dist/component/ingest/normalize.js +35 -0
  60. package/dist/component/ingest/normalize.js.map +1 -0
  61. package/dist/component/ingest/postIngest.d.ts +4 -0
  62. package/dist/component/ingest/postIngest.d.ts.map +1 -0
  63. package/dist/component/ingest/postIngest.js +31 -0
  64. package/dist/component/ingest/postIngest.js.map +1 -0
  65. package/dist/component/ingest/sessionGuard.d.ts +9 -0
  66. package/dist/component/ingest/sessionGuard.d.ts.map +1 -0
  67. package/dist/component/ingest/sessionGuard.js +102 -0
  68. package/dist/component/ingest/sessionGuard.js.map +1 -0
  69. package/dist/component/ingest/stateCache.d.ts +19 -0
  70. package/dist/component/ingest/stateCache.d.ts.map +1 -0
  71. package/dist/component/ingest/stateCache.js +121 -0
  72. package/dist/component/ingest/stateCache.js.map +1 -0
  73. package/dist/component/ingest/types.d.ts +129 -0
  74. package/dist/component/ingest/types.d.ts.map +1 -0
  75. package/dist/component/ingest/types.js +2 -0
  76. package/dist/component/ingest/types.js.map +1 -0
  77. package/dist/component/reasoning.d.ts +37 -0
  78. package/dist/component/reasoning.d.ts.map +1 -0
  79. package/dist/component/reasoning.js +48 -0
  80. package/dist/component/reasoning.js.map +1 -0
  81. package/dist/component/schema.d.ts +87 -11
  82. package/dist/component/schema.d.ts.map +1 -1
  83. package/dist/component/schema.js +47 -0
  84. package/dist/component/schema.js.map +1 -1
  85. package/dist/component/serverRequests.d.ts +53 -0
  86. package/dist/component/serverRequests.d.ts.map +1 -0
  87. package/dist/component/serverRequests.js +187 -0
  88. package/dist/component/serverRequests.js.map +1 -0
  89. package/dist/component/streamStats.d.ts +10 -0
  90. package/dist/component/streamStats.d.ts.map +1 -1
  91. package/dist/component/streamStats.js +34 -0
  92. package/dist/component/streamStats.js.map +1 -1
  93. package/dist/component/sync.d.ts +4 -67
  94. package/dist/component/sync.d.ts.map +1 -1
  95. package/dist/component/syncHelpers.d.ts +12 -35
  96. package/dist/component/syncHelpers.d.ts.map +1 -1
  97. package/dist/component/syncHelpers.js +11 -228
  98. package/dist/component/syncHelpers.js.map +1 -1
  99. package/dist/component/syncIngest.d.ts +2 -72
  100. package/dist/component/syncIngest.d.ts.map +1 -1
  101. package/dist/component/syncIngest.js +9 -726
  102. package/dist/component/syncIngest.js.map +1 -1
  103. package/dist/component/syncRuntime.d.ts +7 -1
  104. package/dist/component/syncRuntime.d.ts.map +1 -1
  105. package/dist/component/syncRuntime.js +8 -2
  106. package/dist/component/syncRuntime.js.map +1 -1
  107. package/dist/component/types.d.ts +5 -1
  108. package/dist/component/types.d.ts.map +1 -1
  109. package/dist/component/types.js +2 -0
  110. package/dist/component/types.js.map +1 -1
  111. package/dist/host/convex-entry.d.ts +3 -0
  112. package/dist/host/convex-entry.d.ts.map +1 -0
  113. package/dist/host/convex-entry.js +3 -0
  114. package/dist/host/convex-entry.js.map +1 -0
  115. package/dist/host/convex.d.ts +17 -0
  116. package/dist/host/convex.d.ts.map +1 -1
  117. package/dist/host/convex.js +9 -0
  118. package/dist/host/convex.js.map +1 -1
  119. package/dist/host/convexSlice.d.ts +504 -0
  120. package/dist/host/convexSlice.d.ts.map +1 -0
  121. package/dist/host/convexSlice.js +315 -0
  122. package/dist/host/convexSlice.js.map +1 -0
  123. package/dist/host/index.d.ts +3 -2
  124. package/dist/host/index.d.ts.map +1 -1
  125. package/dist/host/index.js +2 -1
  126. package/dist/host/index.js.map +1 -1
  127. package/dist/host/runtime.d.ts +126 -2
  128. package/dist/host/runtime.d.ts.map +1 -1
  129. package/dist/host/runtime.js +444 -53
  130. package/dist/host/runtime.js.map +1 -1
  131. package/dist/local-adapter/bridge.d.ts +3 -2
  132. package/dist/local-adapter/bridge.d.ts.map +1 -1
  133. package/dist/local-adapter/bridge.js.map +1 -1
  134. package/dist/mapping.d.ts +29 -0
  135. package/dist/mapping.d.ts.map +1 -1
  136. package/dist/mapping.js +136 -46
  137. package/dist/mapping.js.map +1 -1
  138. package/dist/protocol/classifier.d.ts +2 -12
  139. package/dist/protocol/classifier.d.ts.map +1 -1
  140. package/dist/protocol/classifier.js +1 -104
  141. package/dist/protocol/classifier.js.map +1 -1
  142. package/dist/protocol/events.d.ts +72 -0
  143. package/dist/protocol/events.d.ts.map +1 -0
  144. package/dist/protocol/events.js +533 -0
  145. package/dist/protocol/events.js.map +1 -0
  146. package/dist/protocol/generated.d.ts +16 -2
  147. package/dist/protocol/generated.d.ts.map +1 -1
  148. package/dist/protocol/index.d.ts +3 -0
  149. package/dist/protocol/index.d.ts.map +1 -1
  150. package/dist/protocol/index.js +3 -0
  151. package/dist/protocol/index.js.map +1 -1
  152. package/dist/protocol/outbound.d.ts +14 -0
  153. package/dist/protocol/outbound.d.ts.map +1 -0
  154. package/dist/protocol/outbound.js +2 -0
  155. package/dist/protocol/outbound.js.map +1 -0
  156. package/dist/protocol/parser.d.ts +3 -2
  157. package/dist/protocol/parser.d.ts.map +1 -1
  158. package/dist/protocol/parser.js +99 -3
  159. package/dist/protocol/parser.js.map +1 -1
  160. package/dist/protocol/schemas/CommandExecutionRequestApprovalResponse.json +72 -0
  161. package/dist/protocol/schemas/DynamicToolCallResponse.json +66 -0
  162. package/dist/protocol/schemas/FileChangeRequestApprovalResponse.json +47 -0
  163. package/dist/protocol/schemas/ToolRequestUserInputResponse.json +34 -0
  164. package/dist/react/index.d.ts +3 -1
  165. package/dist/react/index.d.ts.map +1 -1
  166. package/dist/react/index.js +2 -0
  167. package/dist/react/index.js.map +1 -1
  168. package/dist/react/types.d.ts +13 -1
  169. package/dist/react/types.d.ts.map +1 -1
  170. package/dist/react/useCodexReasoning.d.ts +7 -0
  171. package/dist/react/useCodexReasoning.d.ts.map +1 -0
  172. package/dist/react/useCodexReasoning.js +16 -0
  173. package/dist/react/useCodexReasoning.js.map +1 -0
  174. package/dist/react/useCodexStreamOverlay.d.ts.map +1 -1
  175. package/dist/react/useCodexStreamOverlay.js +68 -23
  176. package/dist/react/useCodexStreamOverlay.js.map +1 -1
  177. package/dist/react/useCodexStreamingReasoning.d.ts +12 -0
  178. package/dist/react/useCodexStreamingReasoning.d.ts.map +1 -0
  179. package/dist/react/useCodexStreamingReasoning.js +21 -0
  180. package/dist/react/useCodexStreamingReasoning.js.map +1 -0
  181. package/package.json +5 -1
@@ -1,6 +1,76 @@
1
- import { buildInitializeRequest, buildInitializedNotification, buildThreadStartRequest, buildTurnInterruptRequest, buildTurnStartTextRequest, isUuidLikeThreadId, } from "../app-server/client.js";
1
+ import { buildAccountLoginCancelRequest, buildAccountLoginStartRequest, buildAccountLogoutRequest, buildAccountRateLimitsReadRequest, buildAccountReadRequest, buildChatgptAuthTokensRefreshResponse, buildCommandExecutionApprovalResponse, buildDynamicToolCallResponse, buildFileChangeApprovalResponse, buildThreadArchiveRequest, buildThreadForkRequest, buildThreadListRequest, buildThreadLoadedListRequest, buildThreadReadRequest, buildThreadResumeRequest, buildThreadRollbackRequest, buildInitializeRequestWithCapabilities, buildInitializedNotification, buildThreadStartRequest, buildThreadUnarchiveRequest, buildToolRequestUserInputResponse, buildTurnInterruptRequest, buildTurnStartTextRequest, isUuidLikeThreadId, } from "../app-server/client.js";
2
2
  import { CodexLocalBridge } from "../local-adapter/bridge.js";
3
3
  const MAX_BATCH_SIZE = 32;
4
+ const MANAGED_SERVER_REQUEST_METHODS = new Set([
5
+ "item/commandExecution/requestApproval",
6
+ "item/fileChange/requestApproval",
7
+ "item/tool/requestUserInput",
8
+ "item/tool/call",
9
+ ]);
10
+ const TURN_SCOPED_EVENT_PREFIXES = ["turn/", "item/"];
11
+ function toRequestKey(requestId) {
12
+ return `${typeof requestId}:${String(requestId)}`;
13
+ }
14
+ function asObject(value) {
15
+ return typeof value === "object" && value !== null ? value : null;
16
+ }
17
+ function parseManagedServerRequestFromEvent(event) {
18
+ if (!MANAGED_SERVER_REQUEST_METHODS.has(event.kind)) {
19
+ return null;
20
+ }
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(event.payloadJson);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ const message = asObject(parsed);
29
+ if (!message || typeof message.method !== "string") {
30
+ return null;
31
+ }
32
+ if (!MANAGED_SERVER_REQUEST_METHODS.has(message.method)) {
33
+ return null;
34
+ }
35
+ if (typeof message.id !== "number" && typeof message.id !== "string") {
36
+ return null;
37
+ }
38
+ const params = asObject(message.params);
39
+ if (!params) {
40
+ return null;
41
+ }
42
+ if (typeof params.threadId !== "string" || typeof params.turnId !== "string") {
43
+ return null;
44
+ }
45
+ const method = message.method;
46
+ const itemId = typeof params.itemId === "string"
47
+ ? params.itemId
48
+ : method === "item/tool/call" && typeof params.callId === "string"
49
+ ? params.callId
50
+ : null;
51
+ if (!itemId) {
52
+ return null;
53
+ }
54
+ const reason = typeof params.reason === "string" ? params.reason : undefined;
55
+ const questionsRaw = params.questions;
56
+ const questions = method === "item/tool/requestUserInput" && Array.isArray(questionsRaw)
57
+ ? questionsRaw
58
+ : undefined;
59
+ return {
60
+ requestId: message.id,
61
+ method,
62
+ threadId: params.threadId,
63
+ turnId: params.turnId,
64
+ itemId,
65
+ payloadJson: event.payloadJson,
66
+ createdAt: event.createdAt,
67
+ ...(reason ? { reason } : {}),
68
+ ...(questions ? { questions } : {}),
69
+ };
70
+ }
71
+ function isTurnScopedEvent(kind) {
72
+ return kind === "error" || TURN_SCOPED_EVENT_PREFIXES.some((prefix) => kind.startsWith(prefix));
73
+ }
4
74
  function randomSessionId() {
5
75
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
6
76
  return crypto.randomUUID();
@@ -10,18 +80,20 @@ function randomSessionId() {
10
80
  function isServerNotification(message) {
11
81
  return "method" in message;
12
82
  }
83
+ function isChatgptAuthTokensRefreshRequest(message) {
84
+ if (!("method" in message) ||
85
+ message.method !== "account/chatgptAuthTokens/refresh" ||
86
+ !("id" in message) ||
87
+ (typeof message.id !== "number" && typeof message.id !== "string") ||
88
+ !("params" in message)) {
89
+ return false;
90
+ }
91
+ const params = asObject(message.params);
92
+ return !!params && typeof params.reason === "string";
93
+ }
13
94
  function isResponse(message) {
14
95
  return "id" in message && !isServerNotification(message);
15
96
  }
16
- function normalizeIngestKind(kind) {
17
- if (kind === "codex/event/task_started") {
18
- return "turn/started";
19
- }
20
- if (kind === "codex/event/task_complete") {
21
- return "turn/completed";
22
- }
23
- return kind;
24
- }
25
97
  export function createCodexHostRuntime(args) {
26
98
  let bridge = null;
27
99
  let actor = null;
@@ -38,7 +110,25 @@ export function createCodexHostRuntime(args) {
38
110
  let flushTimer = null;
39
111
  let flushTail = Promise.resolve();
40
112
  let ingestFlushMs = 250;
113
+ const pendingServerRequests = new Map();
114
+ const pendingAuthTokensRefreshRequests = new Map();
115
+ const enqueuedByKind = new Map();
116
+ const skippedByKind = new Map();
117
+ let enqueuedEventCount = 0;
118
+ let skippedEventCount = 0;
41
119
  const pendingRequests = new Map();
120
+ const incrementCount = (counts, kind) => {
121
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
122
+ };
123
+ const snapshotKindCounts = (counts) => Array.from(counts.entries())
124
+ .sort((left, right) => left[0].localeCompare(right[0]))
125
+ .map(([kind, count]) => ({ kind, count }));
126
+ const resetIngestMetrics = () => {
127
+ enqueuedByKind.clear();
128
+ skippedByKind.clear();
129
+ enqueuedEventCount = 0;
130
+ skippedEventCount = 0;
131
+ };
42
132
  const emitState = (lastError = null) => {
43
133
  args.handlers?.onState?.({
44
134
  running: !!bridge,
@@ -46,6 +136,13 @@ export function createCodexHostRuntime(args) {
46
136
  externalThreadId,
47
137
  turnId,
48
138
  turnInFlight,
139
+ pendingServerRequestCount: pendingServerRequests.size,
140
+ ingestMetrics: {
141
+ enqueuedEventCount,
142
+ skippedEventCount,
143
+ enqueuedByKind: snapshotKindCounts(enqueuedByKind),
144
+ skippedByKind: snapshotKindCounts(skippedByKind),
145
+ },
49
146
  lastError,
50
147
  });
51
148
  };
@@ -60,15 +157,123 @@ export function createCodexHostRuntime(args) {
60
157
  nextRequestId += 1;
61
158
  return id;
62
159
  };
63
- const sendMessage = (message, trackedMethod) => {
160
+ const assertRuntimeReady = () => {
64
161
  if (!bridge) {
65
162
  throw new Error("Bridge not started");
66
163
  }
67
- bridge.send(message);
164
+ return bridge;
165
+ };
166
+ const methodForRequest = (message) => message.method;
167
+ const sendMessage = (message, trackedMethod) => {
168
+ const runtimeBridge = assertRuntimeReady();
169
+ runtimeBridge.send(message);
68
170
  if ("id" in message && typeof message.id === "number" && trackedMethod) {
69
171
  pendingRequests.set(message.id, { method: trackedMethod });
70
172
  }
71
173
  };
174
+ const sendRequest = (message) => {
175
+ const runtimeBridge = assertRuntimeReady();
176
+ if (typeof message.id !== "number") {
177
+ throw new Error("Runtime requires numeric request ids.");
178
+ }
179
+ const messageId = message.id;
180
+ return new Promise((resolve, reject) => {
181
+ pendingRequests.set(messageId, { method: methodForRequest(message), resolve, reject });
182
+ runtimeBridge.send(message);
183
+ });
184
+ };
185
+ const registerPendingServerRequest = async (request) => {
186
+ pendingServerRequests.set(toRequestKey(request.requestId), request);
187
+ if (actor) {
188
+ await args.persistence.upsertPendingServerRequest({ actor, request });
189
+ }
190
+ emitState();
191
+ };
192
+ const resolvePendingServerRequest = async (argsForResolve) => {
193
+ const key = toRequestKey(argsForResolve.requestId);
194
+ const pending = pendingServerRequests.get(key);
195
+ if (!pending) {
196
+ return;
197
+ }
198
+ pendingServerRequests.delete(key);
199
+ if (actor) {
200
+ await args.persistence.resolvePendingServerRequest({
201
+ actor,
202
+ threadId: pending.threadId,
203
+ requestId: pending.requestId,
204
+ status: argsForResolve.status,
205
+ resolvedAt: Date.now(),
206
+ ...(argsForResolve.responseJson ? { responseJson: argsForResolve.responseJson } : {}),
207
+ });
208
+ }
209
+ emitState();
210
+ };
211
+ const expireTurnServerRequests = async (turn) => {
212
+ if (!turn.turnId) {
213
+ return;
214
+ }
215
+ const idsToExpire = [];
216
+ for (const request of pendingServerRequests.values()) {
217
+ if (request.threadId === turn.threadId && request.turnId === turn.turnId) {
218
+ idsToExpire.push(request.requestId);
219
+ }
220
+ }
221
+ for (const requestId of idsToExpire) {
222
+ await resolvePendingServerRequest({ requestId, status: "expired" });
223
+ }
224
+ };
225
+ const getPendingServerRequest = (requestId) => {
226
+ const pending = pendingServerRequests.get(toRequestKey(requestId));
227
+ if (!pending) {
228
+ throw new Error(`No pending server request found for id ${String(requestId)}`);
229
+ }
230
+ return pending;
231
+ };
232
+ const sendServerRequestResponse = async (requestId, responseMessage) => {
233
+ getPendingServerRequest(requestId);
234
+ sendMessage(responseMessage);
235
+ await resolvePendingServerRequest({
236
+ requestId,
237
+ status: "answered",
238
+ responseJson: JSON.stringify(responseMessage),
239
+ });
240
+ };
241
+ const registerPendingAuthTokensRefreshRequest = (request) => {
242
+ pendingAuthTokensRefreshRequests.set(toRequestKey(request.requestId), request);
243
+ };
244
+ const getPendingAuthTokensRefreshRequest = (requestId) => {
245
+ const pending = pendingAuthTokensRefreshRequests.get(toRequestKey(requestId));
246
+ if (!pending) {
247
+ throw new Error(`No pending auth token refresh request found for id ${String(requestId)}`);
248
+ }
249
+ return pending;
250
+ };
251
+ const resolvePendingAuthTokensRefreshRequest = (requestId) => {
252
+ pendingAuthTokensRefreshRequests.delete(toRequestKey(requestId));
253
+ };
254
+ const throwIfTurnMutationLocked = () => {
255
+ if (turnInFlight && !turnSettled) {
256
+ throw new Error("Cannot change thread lifecycle while a turn is in flight.");
257
+ }
258
+ };
259
+ const setRuntimeThreadFromResponse = (message, method) => {
260
+ if (message.error || !message.result || typeof message.result !== "object") {
261
+ return;
262
+ }
263
+ if (!("thread" in message.result) || typeof message.result.thread !== "object" || message.result.thread === null) {
264
+ return;
265
+ }
266
+ if (!("id" in message.result.thread) || typeof message.result.thread.id !== "string") {
267
+ return;
268
+ }
269
+ if (method === "thread/start" || method === "thread/resume" || method === "thread/fork") {
270
+ runtimeThreadId = message.result.thread.id;
271
+ if (!externalThreadId) {
272
+ externalThreadId = message.result.thread.id;
273
+ }
274
+ emitState();
275
+ }
276
+ };
72
277
  const flushQueue = async () => {
73
278
  if (!actor || !sessionId || !threadId) {
74
279
  return;
@@ -94,6 +299,28 @@ export function createCodexHostRuntime(args) {
94
299
  flushTail = next.catch(() => undefined);
95
300
  await next;
96
301
  };
302
+ const toIngestDelta = (event, persistedThreadId) => {
303
+ const resolvedTurnId = event.turnId ?? turnId;
304
+ if (!resolvedTurnId || !isTurnScopedEvent(event.kind)) {
305
+ return null;
306
+ }
307
+ const resolvedStreamId = event.streamId;
308
+ if (!resolvedStreamId) {
309
+ throw new Error(`Protocol event missing streamId for turn-scoped kind: ${event.kind}`);
310
+ }
311
+ return {
312
+ type: "stream_delta",
313
+ eventId: event.eventId,
314
+ kind: event.kind,
315
+ payloadJson: event.payloadJson,
316
+ cursorStart: event.cursorStart,
317
+ cursorEnd: event.cursorEnd,
318
+ createdAt: event.createdAt,
319
+ threadId: persistedThreadId,
320
+ turnId: resolvedTurnId,
321
+ streamId: resolvedStreamId,
322
+ };
323
+ };
97
324
  const enqueueIngestDelta = async (delta, forceFlush) => {
98
325
  ingestQueue.push(delta);
99
326
  if (forceFlush || ingestQueue.length >= MAX_BATCH_SIZE) {
@@ -116,6 +343,7 @@ export function createCodexHostRuntime(args) {
116
343
  sessionId = startArgs.sessionId ? `${startArgs.sessionId}-${randomSessionId()}` : randomSessionId();
117
344
  externalThreadId = startArgs.externalThreadId ?? null;
118
345
  ingestFlushMs = startArgs.ingestFlushMs ?? 250;
346
+ resetIngestMetrics();
119
347
  const bridgeConfig = {};
120
348
  if (args.bridge?.codexBin !== undefined) {
121
349
  bridgeConfig.codexBin = args.bridge.codexBin;
@@ -124,7 +352,7 @@ export function createCodexHostRuntime(args) {
124
352
  if (resolvedCwd !== undefined) {
125
353
  bridgeConfig.cwd = resolvedCwd;
126
354
  }
127
- bridge = new CodexLocalBridge(bridgeConfig, {
355
+ const bridgeHandlers = {
128
356
  onEvent: async (event) => {
129
357
  if (!actor || !sessionId) {
130
358
  return;
@@ -167,11 +395,15 @@ export function createCodexHostRuntime(args) {
167
395
  interruptRequested = false;
168
396
  }
169
397
  }
170
- if (event.kind === "turn/completed" || event.kind === "codex/event/turn_aborted") {
398
+ if (event.kind === "turn/completed") {
171
399
  turnId = null;
172
400
  turnInFlight = false;
173
401
  turnSettled = true;
174
402
  emitState();
403
+ await expireTurnServerRequests({
404
+ threadId: threadId ?? event.threadId,
405
+ ...(event.turnId ? { turnId: event.turnId } : {}),
406
+ });
175
407
  }
176
408
  if (event.kind === "error") {
177
409
  turnInFlight = false;
@@ -180,53 +412,51 @@ export function createCodexHostRuntime(args) {
180
412
  turnId = null;
181
413
  }
182
414
  emitState();
415
+ await expireTurnServerRequests({
416
+ threadId: threadId ?? event.threadId,
417
+ ...(event.turnId ? { turnId: event.turnId } : {}),
418
+ });
419
+ }
420
+ const pendingServerRequest = parseManagedServerRequestFromEvent(event);
421
+ if (pendingServerRequest) {
422
+ const persistedThreadId = threadId;
423
+ if (persistedThreadId) {
424
+ await registerPendingServerRequest({
425
+ ...pendingServerRequest,
426
+ threadId: persistedThreadId,
427
+ });
428
+ }
183
429
  }
184
430
  args.handlers?.onEvent?.(event);
185
431
  const persistedThreadId = threadId;
186
432
  if (!persistedThreadId) {
187
433
  return;
188
434
  }
189
- const delta = event.streamId && event.turnId
190
- ? {
191
- type: "stream_delta",
192
- eventId: event.eventId,
193
- kind: normalizeIngestKind(event.kind),
194
- payloadJson: event.payloadJson,
195
- cursorStart: event.cursorStart,
196
- cursorEnd: event.cursorEnd,
197
- createdAt: event.createdAt,
198
- threadId: persistedThreadId,
199
- turnId: event.turnId,
200
- streamId: event.streamId,
201
- }
202
- : {
203
- type: "lifecycle_event",
204
- eventId: event.eventId,
205
- kind: normalizeIngestKind(event.kind),
206
- payloadJson: event.payloadJson,
207
- createdAt: event.createdAt,
208
- threadId: persistedThreadId,
209
- ...(event.turnId ? { turnId: event.turnId } : {}),
210
- };
435
+ const delta = toIngestDelta(event, persistedThreadId);
436
+ if (!delta) {
437
+ skippedEventCount += 1;
438
+ incrementCount(skippedByKind, event.kind);
439
+ return;
440
+ }
441
+ enqueuedEventCount += 1;
442
+ incrementCount(enqueuedByKind, event.kind);
211
443
  const forceFlush = event.kind === "turn/completed" ||
212
- event.kind === "codex/event/turn_aborted" ||
213
444
  event.kind === "error";
214
445
  await enqueueIngestDelta(delta, forceFlush);
215
446
  },
216
447
  onGlobalMessage: async (message) => {
448
+ if (isChatgptAuthTokensRefreshRequest(message)) {
449
+ registerPendingAuthTokensRefreshRequest({
450
+ requestId: message.id,
451
+ params: message.params,
452
+ createdAt: Date.now(),
453
+ });
454
+ }
217
455
  if (isResponse(message) && typeof message.id === "number") {
218
456
  const pending = pendingRequests.get(message.id);
219
457
  pendingRequests.delete(message.id);
220
- if (pending?.method === "thread/start" &&
221
- !message.error &&
222
- message.result &&
223
- typeof message.result === "object" &&
224
- "thread" in message.result &&
225
- typeof message.result.thread === "object" &&
226
- message.result.thread !== null &&
227
- "id" in message.result.thread &&
228
- typeof message.result.thread.id === "string") {
229
- runtimeThreadId = message.result.thread.id;
458
+ if (pending) {
459
+ setRuntimeThreadFromResponse(message, pending.method);
230
460
  }
231
461
  if (message.error && pending?.method === "turn/start") {
232
462
  turnInFlight = false;
@@ -234,6 +464,15 @@ export function createCodexHostRuntime(args) {
234
464
  turnId = null;
235
465
  emitState();
236
466
  }
467
+ if (pending?.resolve) {
468
+ if (message.error) {
469
+ const code = typeof message.error.code === "number" ? String(message.error.code) : "UNKNOWN";
470
+ pending.reject?.(new Error(`[${code}] ${message.error.message}`));
471
+ }
472
+ else {
473
+ pending.resolve(message);
474
+ }
475
+ }
237
476
  }
238
477
  args.handlers?.onGlobalMessage?.(message);
239
478
  },
@@ -245,23 +484,53 @@ export function createCodexHostRuntime(args) {
245
484
  onProcessExit: (code) => {
246
485
  emitState(`codex exited with code ${String(code)}`);
247
486
  },
248
- });
487
+ };
488
+ bridge = args.bridgeFactory
489
+ ? args.bridgeFactory(bridgeConfig, bridgeHandlers)
490
+ : new CodexLocalBridge(bridgeConfig, bridgeHandlers);
249
491
  bridge.start();
250
- sendMessage(buildInitializeRequest(requestId(), {
492
+ sendMessage(buildInitializeRequestWithCapabilities(requestId(), {
251
493
  name: "codex_local_host_runtime",
252
494
  title: "Codex Local Host Runtime",
253
495
  version: "0.1.0",
496
+ }, {
497
+ experimentalApi: Array.isArray(startArgs.dynamicTools) && startArgs.dynamicTools.length > 0,
254
498
  }), "initialize");
255
499
  sendMessage(buildInitializedNotification());
256
- sendMessage(buildThreadStartRequest(requestId(), {
257
- ...(startArgs.model ? { model: startArgs.model } : {}),
258
- ...(startArgs.cwd ? { cwd: startArgs.cwd } : {}),
259
- }), "thread/start");
500
+ const strategy = startArgs.threadStrategy ?? "start";
501
+ if ((strategy === "resume" || strategy === "fork") && !startArgs.runtimeThreadId) {
502
+ throw new Error(`runtimeThreadId is required when threadStrategy=\"${strategy}\".`);
503
+ }
504
+ if (strategy === "start") {
505
+ sendMessage(buildThreadStartRequest(requestId(), {
506
+ ...(startArgs.model ? { model: startArgs.model } : {}),
507
+ ...(startArgs.cwd ? { cwd: startArgs.cwd } : {}),
508
+ ...(startArgs.dynamicTools ? { dynamicTools: startArgs.dynamicTools } : {}),
509
+ }), "thread/start");
510
+ }
511
+ else if (strategy === "resume") {
512
+ sendMessage(buildThreadResumeRequest(requestId(), {
513
+ threadId: startArgs.runtimeThreadId,
514
+ ...(startArgs.model ? { model: startArgs.model } : {}),
515
+ ...(startArgs.cwd ? { cwd: startArgs.cwd } : {}),
516
+ ...(startArgs.dynamicTools ? { dynamicTools: startArgs.dynamicTools } : {}),
517
+ }), "thread/resume");
518
+ }
519
+ else {
520
+ sendMessage(buildThreadForkRequest(requestId(), {
521
+ threadId: startArgs.runtimeThreadId,
522
+ ...(startArgs.model ? { model: startArgs.model } : {}),
523
+ ...(startArgs.cwd ? { cwd: startArgs.cwd } : {}),
524
+ }), "thread/fork");
525
+ }
260
526
  emitState();
261
527
  };
262
528
  const stop = async () => {
263
529
  clearFlushTimer();
264
530
  await flushQueue();
531
+ for (const [, pending] of pendingRequests) {
532
+ pending.reject?.(new Error("Bridge stopped before request completed."));
533
+ }
265
534
  bridge?.stop();
266
535
  bridge = null;
267
536
  actor = null;
@@ -274,6 +543,9 @@ export function createCodexHostRuntime(args) {
274
543
  turnSettled = false;
275
544
  interruptRequested = false;
276
545
  pendingRequests.clear();
546
+ pendingServerRequests.clear();
547
+ pendingAuthTokensRefreshRequests.clear();
548
+ resetIngestMetrics();
277
549
  emitState();
278
550
  };
279
551
  const sendTurn = (text) => {
@@ -301,12 +573,112 @@ export function createCodexHostRuntime(args) {
301
573
  }
302
574
  sendMessage(buildTurnInterruptRequest(requestId(), { threadId: runtimeThreadId, turnId }), "turn/interrupt");
303
575
  };
576
+ const resumeThread = async (nextRuntimeThreadId, params) => {
577
+ throwIfTurnMutationLocked();
578
+ return sendRequest(buildThreadResumeRequest(requestId(), {
579
+ threadId: nextRuntimeThreadId,
580
+ ...(params ?? {}),
581
+ }));
582
+ };
583
+ const forkThread = async (sourceRuntimeThreadId, params) => {
584
+ throwIfTurnMutationLocked();
585
+ return sendRequest(buildThreadForkRequest(requestId(), {
586
+ threadId: sourceRuntimeThreadId,
587
+ ...(params ?? {}),
588
+ }));
589
+ };
590
+ const archiveThread = async (targetRuntimeThreadId) => {
591
+ throwIfTurnMutationLocked();
592
+ return sendRequest(buildThreadArchiveRequest(requestId(), {
593
+ threadId: targetRuntimeThreadId,
594
+ }));
595
+ };
596
+ const unarchiveThread = async (targetRuntimeThreadId) => {
597
+ throwIfTurnMutationLocked();
598
+ return sendRequest(buildThreadUnarchiveRequest(requestId(), {
599
+ threadId: targetRuntimeThreadId,
600
+ }));
601
+ };
602
+ const rollbackThread = async (targetRuntimeThreadId, numTurns) => {
603
+ throwIfTurnMutationLocked();
604
+ return sendRequest(buildThreadRollbackRequest(requestId(), {
605
+ threadId: targetRuntimeThreadId,
606
+ numTurns,
607
+ }));
608
+ };
609
+ const readThread = async (targetRuntimeThreadId, includeTurns = false) => sendRequest(buildThreadReadRequest(requestId(), {
610
+ threadId: targetRuntimeThreadId,
611
+ includeTurns,
612
+ }));
613
+ const readAccount = async (params) => sendRequest(buildAccountReadRequest(requestId(), params));
614
+ const loginAccount = async (params) => sendRequest(buildAccountLoginStartRequest(requestId(), params));
615
+ const cancelAccountLogin = async (params) => sendRequest(buildAccountLoginCancelRequest(requestId(), params));
616
+ const logoutAccount = async () => sendRequest(buildAccountLogoutRequest(requestId()));
617
+ const readAccountRateLimits = async () => sendRequest(buildAccountRateLimitsReadRequest(requestId()));
618
+ const listThreads = async (params) => sendRequest(buildThreadListRequest(requestId(), params));
619
+ const listLoadedThreads = async (params) => sendRequest(buildThreadLoadedListRequest(requestId(), params));
620
+ const listPendingServerRequests = async (threadIdFilter) => {
621
+ if (actor) {
622
+ return args.persistence.listPendingServerRequests({
623
+ actor,
624
+ ...(threadIdFilter ? { threadId: threadIdFilter } : {}),
625
+ });
626
+ }
627
+ return [];
628
+ };
629
+ const respondCommandApproval = async (argsForDecision) => {
630
+ const pending = getPendingServerRequest(argsForDecision.requestId);
631
+ if (pending.method !== "item/commandExecution/requestApproval") {
632
+ throw new Error(`Server request ${String(argsForDecision.requestId)} is ${pending.method}, expected item/commandExecution/requestApproval`);
633
+ }
634
+ await sendServerRequestResponse(argsForDecision.requestId, buildCommandExecutionApprovalResponse(argsForDecision.requestId, argsForDecision.decision));
635
+ };
636
+ const respondFileChangeApproval = async (argsForDecision) => {
637
+ const pending = getPendingServerRequest(argsForDecision.requestId);
638
+ if (pending.method !== "item/fileChange/requestApproval") {
639
+ throw new Error(`Server request ${String(argsForDecision.requestId)} is ${pending.method}, expected item/fileChange/requestApproval`);
640
+ }
641
+ await sendServerRequestResponse(argsForDecision.requestId, buildFileChangeApprovalResponse(argsForDecision.requestId, argsForDecision.decision));
642
+ };
643
+ const respondToolUserInput = async (argsForAnswer) => {
644
+ const pending = getPendingServerRequest(argsForAnswer.requestId);
645
+ if (pending.method !== "item/tool/requestUserInput") {
646
+ throw new Error(`Server request ${String(argsForAnswer.requestId)} is ${pending.method}, expected item/tool/requestUserInput`);
647
+ }
648
+ await sendServerRequestResponse(argsForAnswer.requestId, buildToolRequestUserInputResponse(argsForAnswer.requestId, argsForAnswer.answers));
649
+ };
650
+ const respondDynamicToolCall = async (argsForResult) => {
651
+ const pending = getPendingServerRequest(argsForResult.requestId);
652
+ if (pending.method !== "item/tool/call") {
653
+ throw new Error(`Server request ${String(argsForResult.requestId)} is ${pending.method}, expected item/tool/call`);
654
+ }
655
+ await sendServerRequestResponse(argsForResult.requestId, buildDynamicToolCallResponse(argsForResult.requestId, {
656
+ success: argsForResult.success,
657
+ contentItems: argsForResult.contentItems,
658
+ }));
659
+ };
660
+ const respondChatgptAuthTokensRefresh = async (argsForTokens) => {
661
+ getPendingAuthTokensRefreshRequest(argsForTokens.requestId);
662
+ const responseMessage = buildChatgptAuthTokensRefreshResponse(argsForTokens.requestId, {
663
+ idToken: argsForTokens.idToken,
664
+ accessToken: argsForTokens.accessToken,
665
+ });
666
+ sendMessage(responseMessage);
667
+ resolvePendingAuthTokensRefreshRequest(argsForTokens.requestId);
668
+ };
304
669
  const getState = () => ({
305
670
  running: !!bridge,
306
671
  threadId,
307
672
  externalThreadId,
308
673
  turnId,
309
674
  turnInFlight,
675
+ pendingServerRequestCount: pendingServerRequests.size,
676
+ ingestMetrics: {
677
+ enqueuedEventCount,
678
+ skippedEventCount,
679
+ enqueuedByKind: snapshotKindCounts(enqueuedByKind),
680
+ skippedByKind: snapshotKindCounts(skippedByKind),
681
+ },
310
682
  lastError: null,
311
683
  });
312
684
  return {
@@ -314,6 +686,25 @@ export function createCodexHostRuntime(args) {
314
686
  stop,
315
687
  sendTurn,
316
688
  interrupt,
689
+ resumeThread,
690
+ forkThread,
691
+ archiveThread,
692
+ unarchiveThread,
693
+ rollbackThread,
694
+ readThread,
695
+ readAccount,
696
+ loginAccount,
697
+ cancelAccountLogin,
698
+ logoutAccount,
699
+ readAccountRateLimits,
700
+ listThreads,
701
+ listLoadedThreads,
702
+ listPendingServerRequests,
703
+ respondCommandApproval,
704
+ respondFileChangeApproval,
705
+ respondToolUserInput,
706
+ respondDynamicToolCall,
707
+ respondChatgptAuthTokensRefresh,
317
708
  getState,
318
709
  };
319
710
  }