@blue-soda/dsh-remote 0.5.6 → 0.5.8

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 (232) hide show
  1. package/README.md +14 -3
  2. package/cordis.patch.yml +30 -30
  3. package/dist/client.github.js +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/index.js +1216 -396
  6. package/dist/index.js.map +4 -4
  7. package/dsh-plugin.json +41 -41
  8. package/package.json +165 -165
  9. package/dist/acp.d.ts +0 -75
  10. package/dist/acp.d.ts.map +0 -1
  11. package/dist/acp.js +0 -61
  12. package/dist/acp.js.map +0 -1
  13. package/dist/api-proxy-switch.d.ts +0 -33
  14. package/dist/api-proxy-switch.d.ts.map +0 -1
  15. package/dist/api-proxy-switch.js +0 -109
  16. package/dist/api-proxy-switch.js.map +0 -1
  17. package/dist/atomic-file.d.ts +0 -16
  18. package/dist/atomic-file.d.ts.map +0 -1
  19. package/dist/atomic-file.js +0 -81
  20. package/dist/atomic-file.js.map +0 -1
  21. package/dist/bin/doctor.d.ts +0 -3
  22. package/dist/bin/doctor.d.ts.map +0 -1
  23. package/dist/bin/doctor.js +0 -13
  24. package/dist/bin/doctor.js.map +0 -1
  25. package/dist/cli.d.ts +0 -32
  26. package/dist/cli.d.ts.map +0 -1
  27. package/dist/cli.js +0 -289
  28. package/dist/cli.js.map +0 -1
  29. package/dist/client-runtime.d.ts +0 -375
  30. package/dist/client-runtime.d.ts.map +0 -1
  31. package/dist/client-runtime.js +0 -1768
  32. package/dist/client-runtime.js.map +0 -1
  33. package/dist/client-secure-transport.d.ts +0 -23
  34. package/dist/client-secure-transport.d.ts.map +0 -1
  35. package/dist/client-secure-transport.js +0 -144
  36. package/dist/client-secure-transport.js.map +0 -1
  37. package/dist/client-target-store.d.ts +0 -40
  38. package/dist/client-target-store.d.ts.map +0 -1
  39. package/dist/client-target-store.js +0 -79
  40. package/dist/client-target-store.js.map +0 -1
  41. package/dist/client.d.ts +0 -13
  42. package/dist/client.d.ts.map +0 -1
  43. package/dist/codex/app-server.d.ts +0 -71
  44. package/dist/codex/app-server.d.ts.map +0 -1
  45. package/dist/codex/app-server.js +0 -268
  46. package/dist/codex/app-server.js.map +0 -1
  47. package/dist/codex/codex-image-codec.d.ts +0 -7
  48. package/dist/codex/codex-image-codec.d.ts.map +0 -1
  49. package/dist/codex/codex-image-codec.js +0 -288
  50. package/dist/codex/codex-image-codec.js.map +0 -1
  51. package/dist/codex/domain.d.ts +0 -102
  52. package/dist/codex/domain.d.ts.map +0 -1
  53. package/dist/codex/domain.js +0 -1323
  54. package/dist/codex/domain.js.map +0 -1
  55. package/dist/codex/method-policy.d.ts +0 -338
  56. package/dist/codex/method-policy.d.ts.map +0 -1
  57. package/dist/codex/method-policy.js +0 -141
  58. package/dist/codex/method-policy.js.map +0 -1
  59. package/dist/codex/peer-bridge.d.ts +0 -57
  60. package/dist/codex/peer-bridge.d.ts.map +0 -1
  61. package/dist/codex/peer-bridge.js +0 -299
  62. package/dist/codex/peer-bridge.js.map +0 -1
  63. package/dist/codex/permissions.d.ts +0 -4
  64. package/dist/codex/permissions.d.ts.map +0 -1
  65. package/dist/codex/permissions.js +0 -21
  66. package/dist/codex/permissions.js.map +0 -1
  67. package/dist/codex/session-id.d.ts +0 -10
  68. package/dist/codex/session-id.d.ts.map +0 -1
  69. package/dist/codex/session-id.js +0 -15
  70. package/dist/codex/session-id.js.map +0 -1
  71. package/dist/codex/virtual-harness.d.ts +0 -209
  72. package/dist/codex/virtual-harness.d.ts.map +0 -1
  73. package/dist/codex/virtual-harness.js +0 -2650
  74. package/dist/codex/virtual-harness.js.map +0 -1
  75. package/dist/codex-workspace-bridge.d.ts +0 -108
  76. package/dist/codex-workspace-bridge.d.ts.map +0 -1
  77. package/dist/codex-workspace-bridge.js +0 -502
  78. package/dist/codex-workspace-bridge.js.map +0 -1
  79. package/dist/config.d.ts +0 -99
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js +0 -127
  82. package/dist/config.js.map +0 -1
  83. package/dist/connection-controller.d.ts +0 -44
  84. package/dist/connection-controller.d.ts.map +0 -1
  85. package/dist/connection-controller.js +0 -185
  86. package/dist/connection-controller.js.map +0 -1
  87. package/dist/control-route.d.ts +0 -34
  88. package/dist/control-route.d.ts.map +0 -1
  89. package/dist/control-route.js +0 -136
  90. package/dist/control-route.js.map +0 -1
  91. package/dist/control-runtime.d.ts +0 -138
  92. package/dist/control-runtime.d.ts.map +0 -1
  93. package/dist/control-runtime.js +0 -543
  94. package/dist/control-runtime.js.map +0 -1
  95. package/dist/control-stream.d.ts +0 -67
  96. package/dist/control-stream.d.ts.map +0 -1
  97. package/dist/control-stream.js +0 -179
  98. package/dist/control-stream.js.map +0 -1
  99. package/dist/defaults.d.ts +0 -9
  100. package/dist/defaults.d.ts.map +0 -1
  101. package/dist/defaults.js +0 -9
  102. package/dist/defaults.js.map +0 -1
  103. package/dist/file-viewer-bridge.d.ts +0 -30
  104. package/dist/file-viewer-bridge.d.ts.map +0 -1
  105. package/dist/file-viewer-bridge.js +0 -128
  106. package/dist/file-viewer-bridge.js.map +0 -1
  107. package/dist/file-viewer-contract.d.ts +0 -5
  108. package/dist/file-viewer-contract.d.ts.map +0 -1
  109. package/dist/file-viewer-contract.js +0 -4
  110. package/dist/file-viewer-contract.js.map +0 -1
  111. package/dist/harness-api-bridge.d.ts +0 -81
  112. package/dist/harness-api-bridge.d.ts.map +0 -1
  113. package/dist/harness-api-bridge.js +0 -862
  114. package/dist/harness-api-bridge.js.map +0 -1
  115. package/dist/harness-api-history.d.ts +0 -3
  116. package/dist/harness-api-history.d.ts.map +0 -1
  117. package/dist/harness-api-history.js +0 -59
  118. package/dist/harness-api-history.js.map +0 -1
  119. package/dist/harness-remote-bridge.d.ts +0 -60
  120. package/dist/harness-remote-bridge.d.ts.map +0 -1
  121. package/dist/harness-remote-bridge.js +0 -540
  122. package/dist/harness-remote-bridge.js.map +0 -1
  123. package/dist/harness-version.d.ts +0 -23
  124. package/dist/harness-version.d.ts.map +0 -1
  125. package/dist/harness-version.js +0 -95
  126. package/dist/harness-version.js.map +0 -1
  127. package/dist/identity-store.d.ts +0 -45
  128. package/dist/identity-store.d.ts.map +0 -1
  129. package/dist/identity-store.js +0 -186
  130. package/dist/identity-store.js.map +0 -1
  131. package/dist/ids.d.ts +0 -2
  132. package/dist/ids.d.ts.map +0 -1
  133. package/dist/ids.js +0 -17
  134. package/dist/ids.js.map +0 -1
  135. package/dist/index.d.ts +0 -78
  136. package/dist/index.d.ts.map +0 -1
  137. package/dist/logging.d.ts +0 -18
  138. package/dist/logging.d.ts.map +0 -1
  139. package/dist/logging.js +0 -30
  140. package/dist/logging.js.map +0 -1
  141. package/dist/loopback-host.d.ts +0 -20
  142. package/dist/loopback-host.d.ts.map +0 -1
  143. package/dist/loopback-host.js +0 -245
  144. package/dist/loopback-host.js.map +0 -1
  145. package/dist/loopback-preview.d.ts +0 -19
  146. package/dist/loopback-preview.d.ts.map +0 -1
  147. package/dist/loopback-preview.js +0 -232
  148. package/dist/loopback-preview.js.map +0 -1
  149. package/dist/native-rtc-helper.d.ts +0 -14
  150. package/dist/native-rtc-helper.d.ts.map +0 -1
  151. package/dist/native-rtc-helper.js +0 -575
  152. package/dist/native-rtc-helper.js.map +0 -1
  153. package/dist/remote-api-proxy.d.ts +0 -17
  154. package/dist/remote-api-proxy.d.ts.map +0 -1
  155. package/dist/remote-api-proxy.js +0 -451
  156. package/dist/remote-api-proxy.js.map +0 -1
  157. package/dist/remote-directory-browser.d.ts +0 -16
  158. package/dist/remote-directory-browser.d.ts.map +0 -1
  159. package/dist/remote-directory-browser.js +0 -48
  160. package/dist/remote-directory-browser.js.map +0 -1
  161. package/dist/remote-file-content-provider.d.ts +0 -48
  162. package/dist/remote-file-content-provider.d.ts.map +0 -1
  163. package/dist/remote-file-content-provider.js +0 -93
  164. package/dist/remote-file-content-provider.js.map +0 -1
  165. package/dist/remote-typert-gateway.d.ts +0 -20
  166. package/dist/remote-typert-gateway.d.ts.map +0 -1
  167. package/dist/remote-typert-gateway.js +0 -334
  168. package/dist/remote-typert-gateway.js.map +0 -1
  169. package/dist/rpc-binary-attachments.d.ts +0 -33
  170. package/dist/rpc-binary-attachments.d.ts.map +0 -1
  171. package/dist/rpc-binary-attachments.js +0 -142
  172. package/dist/rpc-binary-attachments.js.map +0 -1
  173. package/dist/rpc-router.d.ts +0 -31
  174. package/dist/rpc-router.d.ts.map +0 -1
  175. package/dist/rpc-router.js +0 -201
  176. package/dist/rpc-router.js.map +0 -1
  177. package/dist/safe-error.d.ts +0 -8
  178. package/dist/safe-error.d.ts.map +0 -1
  179. package/dist/safe-error.js +0 -20
  180. package/dist/safe-error.js.map +0 -1
  181. package/dist/server-api.d.ts +0 -109
  182. package/dist/server-api.d.ts.map +0 -1
  183. package/dist/server-api.js +0 -520
  184. package/dist/server-api.js.map +0 -1
  185. package/dist/server-connection.d.ts +0 -79
  186. package/dist/server-connection.d.ts.map +0 -1
  187. package/dist/server-connection.js +0 -1023
  188. package/dist/server-connection.js.map +0 -1
  189. package/dist/server-credentials.d.ts +0 -55
  190. package/dist/server-credentials.d.ts.map +0 -1
  191. package/dist/server-credentials.js +0 -108
  192. package/dist/server-credentials.js.map +0 -1
  193. package/dist/service.d.ts +0 -151
  194. package/dist/service.d.ts.map +0 -1
  195. package/dist/service.js +0 -448
  196. package/dist/service.js.map +0 -1
  197. package/dist/session-format-compat.d.ts +0 -3
  198. package/dist/session-format-compat.d.ts.map +0 -1
  199. package/dist/session-format-compat.js +0 -183
  200. package/dist/session-format-compat.js.map +0 -1
  201. package/dist/status-stream.d.ts +0 -48
  202. package/dist/status-stream.d.ts.map +0 -1
  203. package/dist/status-stream.js +0 -133
  204. package/dist/status-stream.js.map +0 -1
  205. package/dist/terminal-policy.d.ts +0 -21
  206. package/dist/terminal-policy.d.ts.map +0 -1
  207. package/dist/terminal-policy.js +0 -68
  208. package/dist/terminal-policy.js.map +0 -1
  209. package/dist/tui-command.d.ts +0 -13
  210. package/dist/tui-command.d.ts.map +0 -1
  211. package/dist/tui-command.js +0 -392
  212. package/dist/tui-command.js.map +0 -1
  213. package/dist/typert-gateway-contract.d.ts +0 -59
  214. package/dist/typert-gateway-contract.d.ts.map +0 -1
  215. package/dist/typert-gateway-contract.js +0 -2
  216. package/dist/typert-gateway-contract.js.map +0 -1
  217. package/dist/typert-gateway-switch.d.ts +0 -85
  218. package/dist/typert-gateway-switch.d.ts.map +0 -1
  219. package/dist/typert-gateway-switch.js +0 -448
  220. package/dist/typert-gateway-switch.js.map +0 -1
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.d.ts.map +0 -1
  223. package/dist/types.js +0 -2
  224. package/dist/types.js.map +0 -1
  225. package/dist/version.d.ts +0 -12
  226. package/dist/version.d.ts.map +0 -1
  227. package/dist/version.js +0 -12
  228. package/dist/version.js.map +0 -1
  229. package/dist/werift-rtc.d.ts +0 -121
  230. package/dist/werift-rtc.d.ts.map +0 -1
  231. package/dist/werift-rtc.js +0 -490
  232. package/dist/werift-rtc.js.map +0 -1
@@ -1,2650 +0,0 @@
1
- import { CodexRemoteClient, deriveCodexCwdWorkspaces, projectCodexThread, } from '@dsh-remote/client-core';
2
- import { codexPermissionPresetFromResponse } from './permissions.js';
3
- import { decodeByteValue } from '../rpc-binary-attachments.js';
4
- import { CODEX_HISTORY_MAX_MESSAGES } from './method-policy.js';
5
- import { collectImageBlocks, contentBlocks, isCanonicalBase64, toolOutputImageBlocks, } from './codex-image-codec.js';
6
- const CODEX_SESSION_PREFIX = 'codex:';
7
- const CODEX_WORKSPACE_PREFIX = 'codex-workspace:';
8
- const CODEX_PROVIDER = 'codex';
9
- const CODEX_MODEL = 'codex';
10
- const CODEX_PAGE_LIMIT = 100;
11
- const MAX_CODEX_PAGES = 32;
12
- const MAX_LIVE_TOOL_OUTPUT = 128 * 1024;
13
- const DEFAULT_HISTORY_MESSAGES = 50;
14
- const SESSION_SEARCH_RESULT_LIMIT = 20;
15
- const SESSION_SEARCH_SNIPPET_CODE_POINTS = 240;
16
- const CODEX_DEFAULT_PERMISSION = 'workspace-write';
17
- const MAX_CODEX_PROMPT_PARTS = 16;
18
- const MAX_CODEX_PROMPT_TEXT = 256 * 1024;
19
- const MAX_CODEX_IMAGE_BASE64 = 288 * 1024 * 1024;
20
- const CODEX_IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
21
- const CODEX_IMAGE_ATTACHMENT_PREFIX = 'codex-image:';
22
- const HOST_WORKSPACE_ENDPOINTS = new Set([
23
- 'workspaceFiles/list', 'workspaceFiles/stat', 'workspaceFiles/read', 'workspaceFiles/readBytes',
24
- 'workspaceFiles/readAll', 'workspaceFiles/readRelated', 'terminal/environment', 'terminal/shells',
25
- 'terminal/list', 'terminal/create', 'terminal/write', 'terminal/resize', 'terminal/rename', 'terminal/close',
26
- ]);
27
- const HOST_WORKSPACE_STREAM_ENDPOINTS = new Set(['workspaceFiles/changes', 'terminal/follow', 'terminal/retain']);
28
- function isHostWorkspaceEndpoint(endpoint) {
29
- return HOST_WORKSPACE_ENDPOINTS.has(endpoint);
30
- }
31
- function isHostWorkspaceStreamEndpoint(endpoint) {
32
- return HOST_WORKSPACE_STREAM_ENDPOINTS.has(endpoint);
33
- }
34
- export function codexProjectWorkspaceId(projectId) {
35
- return `${CODEX_WORKSPACE_PREFIX}project:${projectId}`;
36
- }
37
- /** Discover the CodeX projects visible through the Host App Server. */
38
- export async function discoverCodexVirtualWorkspaces(client, signal) {
39
- return (await loadCatalog(client, signal)).workspaces;
40
- }
41
- /**
42
- * A plugin-owned virtual Harness target. It projects CodeX Thread/Turn data at
43
- * the existing ApiProxy/Typert carrier boundary, so every UI layer above the
44
- * official Workspace and Session controllers remains native DSH.
45
- */
46
- export class CodexVirtualHarness {
47
- client;
48
- host;
49
- sessionGeneration;
50
- hostCarrier;
51
- api;
52
- catalog;
53
- workspaceStreams = new Set();
54
- controlStreams = new Set();
55
- eventStreams = new Map();
56
- rcMuxStreams = new Set();
57
- rcHostStreams = new Set();
58
- follows = new Set();
59
- pendingRequestIds = new Map();
60
- pendingApprovals = new Map();
61
- pendingThreads = new Map();
62
- blankThreads = new Set();
63
- selectedModels = new Map();
64
- selectedPermissions = new Map();
65
- imageAttachments = new Map();
66
- modelDirectory;
67
- modelDirectoryPromise;
68
- lastProjectionSeq = 0;
69
- commandSeq = 0;
70
- selectedWorkspaceId;
71
- closed = false;
72
- /**
73
- * The local shell's carriers. Once this Harness answers `/api` for a remote Codex
74
- * workspace, every endpoint outside the CodeX domain belongs to the shell that
75
- * owns the window — its settings bootstrap, plugin registry and account reads.
76
- * @param carrier - local carriers, or undefined to fall back to the remote Host.
77
- */
78
- setLocalCarrier(carrier) { this.localCarrier = carrier; }
79
- localCarrier;
80
- constructor(client, host, sessionGeneration = 'legacy', hostCarrier) {
81
- this.client = client;
82
- this.host = host;
83
- this.sessionGeneration = sessionGeneration;
84
- this.hostCarrier = hostCarrier;
85
- this.api = this.createApiProxy();
86
- }
87
- static remote(core, host, sessionGeneration = 'legacy', hostCarrier) {
88
- return new CodexVirtualHarness(new CodexRemoteClient(core), host, sessionGeneration, hostCarrier);
89
- }
90
- async workspaces(signal) {
91
- return (await this.refreshCatalog(signal)).workspaces;
92
- }
93
- async selectWorkspace(workspaceId, signal) {
94
- const catalog = await loadCatalog(this.client, signal);
95
- const workspace = catalog.workspaces.find(item => item.workspaceId === workspaceId);
96
- if (workspace === undefined)
97
- throw new Error('The selected CodeX workspace is no longer available.');
98
- this.selectedWorkspaceId = workspace.workspaceId;
99
- this.catalog = catalog;
100
- return workspace;
101
- }
102
- async preferredSessionId(signal) {
103
- const catalog = this.catalog;
104
- const selected = catalog?.workspaces.find(workspace => workspace.workspaceId === this.selectedWorkspaceId);
105
- const selectedSessionIds = new Set(selected?.sessionIds ?? []);
106
- const sessions = (catalog?.sessions ?? []).filter(session => selectedSessionIds.has(session.id));
107
- const idleNamed = sessions.filter(session => session.status !== 'running'
108
- && session.status !== 'waiting'
109
- && displayTitle(session) !== null);
110
- const idle = sessions.filter(session => session.status !== 'running' && session.status !== 'waiting');
111
- // The most recently updated Thread is often the one still owned by the
112
- // interactive Codex process. Prefer the previous named Thread when one is
113
- // available, then fall back through the complete catalog.
114
- const preferredNamed = idleNamed.length > 1 ? [...idleNamed.slice(1), idleNamed[0]] : idleNamed;
115
- const candidates = [...new Set([...preferredNamed, ...idle, ...sessions].map(session => session.id))];
116
- for (const sessionId of candidates) {
117
- const threadId = nativeThreadId(sessionId);
118
- try {
119
- const thread = await this.fetchThread(threadId, signal);
120
- return sessionId;
121
- }
122
- catch {
123
- // A currently active or concurrently changing CodeX Thread may reject
124
- // a full history read. Keep looking so the native UI does not remain
125
- // on an ungrouped placeholder Session after entering the Workspace.
126
- }
127
- }
128
- return undefined;
129
- }
130
- async invoke(request) {
131
- const result = await this.dispatch(`${request.namespace}/${request.method}`, { args: request.args }, request.signal ?? new AbortController().signal);
132
- if (result.ok)
133
- return result.value;
134
- throw Object.assign(new Error(result.error.message), {
135
- isDSHRemoteError: true,
136
- code: result.error.code,
137
- details: result.error.details,
138
- });
139
- }
140
- async dispatch(endpoint, payload, signal) {
141
- try {
142
- if (isHostWorkspaceEndpoint(endpoint)) {
143
- if (this.hostCarrier === undefined)
144
- return fail('method-not-found', `CodeX virtual Harness does not implement ${endpoint}.`);
145
- return normalizeByteResult(endpoint, await this.hostCarrier.dispatch(endpoint, payload, signal));
146
- }
147
- const args = carrierArgs(payload);
148
- switch (endpoint) {
149
- case '$events/result': return business(await this.answerRemoteEvent(args, signal));
150
- case 'workspace/list': {
151
- const catalog = await this.refreshCatalog(signal);
152
- return business(success({
153
- items: nativeVisibleWorkspaces(catalog, this.selectedWorkspaceId).map(nativeWorkspace),
154
- archivedSessionIds: catalog.sessions.filter(item => item.archived).map(item => item.id),
155
- // `WorkspaceBaseline` declares this field, and the client reads its length
156
- // directly: omitting it throws instead of rendering an empty pin list.
157
- pinnedSessionIds: [],
158
- }));
159
- }
160
- case 'workspace/create': return business(await this.createWorkspace(requestArg(args), signal));
161
- case 'workspace/rename': return business(await this.renameWorkspace(requestArg(args)));
162
- case 'workspace/delete': return business(failure('workspace-read-only', 'CodeX virtual Workspaces cannot be deleted.'));
163
- case 'workspace/insertBefore': return business({
164
- workspaceIds: nativeVisibleWorkspaces(await this.currentCatalog(signal), this.selectedWorkspaceId).map(item => item.workspaceId),
165
- });
166
- case 'workspace/insertSessionBefore': return business(await this.workspaceForSession(requestArg(args), signal));
167
- case 'workspace/archiveSession': return business(await this.archiveSession(requestArg(args), signal));
168
- case 'session/list': return business(success({ items: await this.sessionSummaries(signal) }));
169
- case 'session/search': return business(await this.searchSessions(requestArg(args), signal));
170
- case 'session/create': return business(await this.createSession(requestArg(args), signal));
171
- case 'session/fork': return business(await this.forkSession(requestArg(args), signal));
172
- case 'session/history': return business(await this.sessionHistory(requestArg(args), signal));
173
- case 'session/page': return business(await this.sessionPage(requestArg(args), signal));
174
- case 'session/prompt': return business(await this.prompt(requestArg(args), signal));
175
- case 'session/cancel': return business(await this.cancel(requestArg(args), signal));
176
- case 'session/rename': return business(await this.renameSession(requestArg(args), signal));
177
- case 'session/updateQueue': return business(failure('queue-item-not-found', 'CodeX does not expose a DSH inbox queue.'));
178
- case 'session/attachment': return business(await this.attachment(requestArg(args), signal));
179
- case 'session/modelCatalog': return business(success(modelCatalog(await this.models(signal))));
180
- case 'session/models': {
181
- const sessionId = requiredString(requestArg(args).sessionId, 'sessionId');
182
- nativeThreadId(sessionId);
183
- const directory = await this.models(signal);
184
- return business(success({
185
- current: this.modelSelection(sessionId, directory),
186
- routable: true,
187
- groups: directory.groups,
188
- failures: [],
189
- }));
190
- }
191
- case 'session/selectModel': return business(await this.selectModel(requestArg(args), signal));
192
- case 'session/canOpenWorkspacePath': return business(this.selectedWorkspaceId !== undefined);
193
- case 'session/openWorkspacePath': return business(failure('bad-request', 'Opening Host paths is unavailable in CodeX mode.'));
194
- case 'host/describe': return business(success(await this.describeHost(signal)));
195
- case 'host/listDirectory': return business(await this.listDirectory(requestArg(args), signal));
196
- case 'directoryPicker/list': return business(await this.listDirectory(requestArg(args), signal));
197
- case 'skills/list': return business(success({ items: [] }));
198
- case 'commands/list': return ok(this.commandList(args));
199
- case 'commands/execute': return ok(await this.executeCommand(args, signal));
200
- default: {
201
- // Selecting this Harness as the Codex target makes it answer every `/api`
202
- // endpoint, but it only owns the CodeX domain. The rest belongs to the local
203
- // shell: its settings bootstrap, plugin registry and account reads describe
204
- // *this* installation. Answering "method-not-found" broke the native start,
205
- // and answering from the remote Host showed the remote Host's welcome flow
206
- // and account instead of the local ones.
207
- if (this.localCarrier?.dispatch !== undefined)
208
- return await this.localCarrier.dispatch(endpoint, payload, signal);
209
- if (this.hostCarrier !== undefined)
210
- return await this.hostCarrier.dispatch(endpoint, payload, signal);
211
- return fail('method-not-found', `CodeX virtual Harness does not implement ${endpoint}.`);
212
- }
213
- }
214
- }
215
- catch (error) {
216
- return failFrom(error);
217
- }
218
- }
219
- async open(endpoint, payload, signal) {
220
- if (isHostWorkspaceStreamEndpoint(endpoint)) {
221
- if (this.hostCarrier === undefined) {
222
- throw Object.assign(new Error(`CodeX virtual Harness does not implement stream ${endpoint}.`), {
223
- isDSHRemoteError: true,
224
- code: 'method-not-found',
225
- details: {},
226
- });
227
- }
228
- return this.hostCarrier.open(endpoint, payload, signal);
229
- }
230
- const args = carrierArgs(payload);
231
- if (endpoint === 'workspace/follow')
232
- return this.workspaceFollow(signal);
233
- if (endpoint === 'session/control')
234
- return this.sessionControl(signal);
235
- if (endpoint === 'session/follow')
236
- return this.sessionFollow(requestArg(args), signal);
237
- if (endpoint === '$events')
238
- return this.remoteEvents(signal);
239
- // Same delegation as the unary path: a stream the CodeX domain does not own is the
240
- // local shell's (the plugin registry's own events stream, for instance), and
241
- // refusing it failed those subscriptions.
242
- if (this.localCarrier?.open !== undefined)
243
- return this.localCarrier.open(endpoint, payload, signal);
244
- if (this.hostCarrier !== undefined)
245
- return this.hostCarrier.open(endpoint, payload, signal);
246
- throw Object.assign(new Error(`CodeX virtual Harness does not implement stream ${endpoint}.`), {
247
- isDSHRemoteError: true,
248
- code: 'method-not-found',
249
- details: {},
250
- });
251
- }
252
- async close() {
253
- if (this.closed)
254
- return;
255
- this.closed = true;
256
- for (const stream of this.workspaceStreams)
257
- stream.close();
258
- for (const stream of this.controlStreams)
259
- stream.close();
260
- for (const stream of this.eventStreams.values())
261
- stream.close();
262
- for (const stream of this.rcMuxStreams)
263
- stream.close();
264
- for (const stream of this.rcHostStreams)
265
- stream.close();
266
- this.workspaceStreams.clear();
267
- this.controlStreams.clear();
268
- this.eventStreams.clear();
269
- this.rcMuxStreams.clear();
270
- this.rcHostStreams.clear();
271
- const follows = [...this.follows];
272
- this.follows.clear();
273
- for (const follow of follows) {
274
- follow.queue.close();
275
- await follow.close?.().catch(() => undefined);
276
- }
277
- this.pendingApprovals.clear();
278
- this.pendingRequestIds.clear();
279
- this.pendingThreads.clear();
280
- this.blankThreads.clear();
281
- this.selectedModels.clear();
282
- this.selectedPermissions.clear();
283
- this.imageAttachments.clear();
284
- }
285
- async refreshCatalog(signal) {
286
- const catalog = await loadCatalog(this.client, signal, this.pendingThreads);
287
- if (this.selectedWorkspaceId !== undefined
288
- && !catalog.workspaces.some(workspace => workspace.workspaceId === this.selectedWorkspaceId)) {
289
- this.selectedWorkspaceId = undefined;
290
- }
291
- this.catalog = catalog;
292
- return catalog;
293
- }
294
- async currentCatalog(signal) {
295
- return this.catalog ?? this.refreshCatalog(signal);
296
- }
297
- models(signal) {
298
- if (this.modelDirectory !== undefined)
299
- return Promise.resolve(this.modelDirectory);
300
- this.modelDirectoryPromise ??= loadModelDirectory(this.client, signal)
301
- .then(directory => {
302
- this.modelDirectory = directory;
303
- return directory;
304
- })
305
- .finally(() => { this.modelDirectoryPromise = undefined; });
306
- return this.modelDirectoryPromise;
307
- }
308
- modelSelection(sessionId, directory) {
309
- return this.selectedModels.get(sessionId) ?? directory?.default ?? modelSelection();
310
- }
311
- permissionSelection(sessionId) {
312
- return this.selectedPermissions.get(sessionId);
313
- }
314
- setPermissionSelection(sessionId, preset) {
315
- if (preset === undefined)
316
- this.selectedPermissions.delete(sessionId);
317
- else
318
- this.selectedPermissions.set(sessionId, preset);
319
- this.publishProjection(sessionId, 'permissions', codexPermissionsProjection(preset), this.nextProjectionSeq());
320
- }
321
- commandList(args) {
322
- nativeThreadId(requiredString(args.agentId, 'agentId'));
323
- return [{
324
- name: 'permission',
325
- description: 'Switch the CodeX Remote sandbox mode and approval policy',
326
- input: { hint: '<preset>' },
327
- }];
328
- }
329
- async executeCommand(args, signal) {
330
- const sessionId = requiredString(args.agentId, 'agentId');
331
- const threadId = nativeThreadId(sessionId);
332
- const line = requiredString(args.line, 'command line').trim();
333
- if (!Array.isArray(args.images))
334
- throw new Error('The CodeX command image list is required.');
335
- const match = /^\/permission(?:\s+([\s\S]*))?$/u.exec(line);
336
- if (match === null)
337
- return undefined;
338
- const commandId = `codex-permission:${++this.commandSeq}`;
339
- if (args.images.length > 0)
340
- return {
341
- commandId,
342
- result: { kind: 'error', text: 'CodeX Remote permission commands do not accept attachments.' },
343
- };
344
- const requested = (match[1] ?? '').trim();
345
- if (requested === '') {
346
- await this.readHistoryPage(threadId, { maxMessages: 1 }, signal);
347
- return {
348
- commandId,
349
- result: { kind: 'success', text: `current preset ${this.permissionSelection(sessionId) ?? 'Host settings (not reported)'}` },
350
- };
351
- }
352
- if (!isCodexPermissionPreset(requested))
353
- return {
354
- commandId,
355
- result: { kind: 'error', text: `unknown preset "${requested}" (available: workspace-write, danger-full-access)` },
356
- };
357
- const result = await this.client.request('thread/resume', { threadId, permissionPreset: requested }, signal);
358
- const effective = codexPermissionPresetFromResponse(result);
359
- if (effective === undefined)
360
- throw new Error('The Host did not confirm the CodeX permission preset.');
361
- this.setPermissionSelection(sessionId, effective);
362
- if (effective !== requested)
363
- throw new Error('The Host did not apply the requested CodeX permission preset.');
364
- return { commandId, result: { kind: 'success', text: `preset ${effective}` } };
365
- }
366
- sessionTitle(sessionId) {
367
- const session = this.catalog?.sessions.find(item => item.id === sessionId);
368
- return session === undefined ? null : displayTitle(session);
369
- }
370
- async selectModel(request, signal) {
371
- const sessionId = requiredString(request.sessionId, 'sessionId');
372
- nativeThreadId(sessionId);
373
- const provider = requiredString(request.provider, 'provider');
374
- const model = requiredString(request.model, 'model');
375
- const reasoningEffort = string(request.reasoningEffort);
376
- const directory = await this.models(signal);
377
- const available = directory.models.get(model);
378
- if (provider !== CODEX_PROVIDER || available === undefined) {
379
- return failure('model-unavailable', 'The selected CodeX model is unavailable on this Host.');
380
- }
381
- const supportedEfforts = available.reasoning?.efforts.map(effort => effort.id) ?? [];
382
- if (reasoningEffort !== undefined && !supportedEfforts.includes(reasoningEffort)) {
383
- return failure('model-unavailable', 'The selected reasoning effort is unavailable for this CodeX model.');
384
- }
385
- const selected = {
386
- provider,
387
- model,
388
- ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
389
- };
390
- this.selectedModels.set(sessionId, selected);
391
- const seq = Date.now();
392
- this.publishProjection(sessionId, 'modelSelection', modelSelectionProjection(selected), seq);
393
- return success({ selected });
394
- }
395
- publishProjection(sessionId, key, value, seq) {
396
- for (const queue of this.controlStreams)
397
- queue.push({ type: 'projection', sessionId, key, value, seq });
398
- this.broadcastRcMux({ type: 'session/projection', sessionId, key, value, seq });
399
- }
400
- nextProjectionSeq() {
401
- const seq = Math.max(Date.now(), this.lastProjectionSeq + 1);
402
- this.lastProjectionSeq = seq;
403
- return seq;
404
- }
405
- updateThreadName(threadId, name) {
406
- const pending = this.pendingThreads.get(threadId);
407
- if (pending !== undefined)
408
- this.pendingThreads.set(threadId, { ...pending, name });
409
- if (this.catalog === undefined)
410
- return;
411
- const threads = this.catalog.threads.map(thread => string(thread.id) === threadId ? { ...thread, name } : thread);
412
- const sessions = this.catalog.sessions.map(session => {
413
- if (session.nativeId !== threadId)
414
- return session;
415
- const { title: _title, ...rest } = session;
416
- return name.trim() === '' ? rest : { ...rest, title: name };
417
- });
418
- this.catalog = { ...this.catalog, threads, sessions };
419
- }
420
- async sessionSummaries(signal) {
421
- const catalog = await this.refreshCatalog(signal);
422
- const directory = await this.models(signal).catch(() => undefined);
423
- return catalog.sessions.map(session => {
424
- const blank = this.blankThreads.has(session.nativeId)
425
- || this.pendingThreads.has(session.nativeId);
426
- return this.sessionSummary(session, {
427
- blank,
428
- modelSelection: this.modelSelection(session.id, directory),
429
- lastPromptAt: session.updatedAt || null,
430
- });
431
- });
432
- }
433
- sessionSummary(session, options) {
434
- return {
435
- sessionId: session.id,
436
- updatedAt: session.updatedAt,
437
- running: session.status === 'running' || session.status === 'waiting',
438
- blank: options.blank,
439
- ...(session.cwd === undefined ? {} : { cwd: session.cwd }),
440
- projections: {
441
- // `SessionProjectionHints` is a discriminated union; without its kind the
442
- // client's exhaustive switch throws and takes the whole session list down.
443
- kind: 'sequenced',
444
- asOfSeq: options.asOfSeq ?? 0,
445
- values: {
446
- title: displayTitle(session),
447
- sessionListMetadata: { blank: options.blank, lastPromptAt: options.lastPromptAt },
448
- modelSelection: modelSelectionProjection(options.modelSelection),
449
- permissions: codexPermissionsProjection(this.permissionSelection(session.id)),
450
- imageLimits: codexImageLimitsProjection(),
451
- },
452
- },
453
- };
454
- }
455
- async searchSessions(request, signal) {
456
- const query = requiredString(request.query, 'query').trim();
457
- if (query.length === 0 || query.length > 500 || query.includes('\0')) {
458
- return failure('bad-request', 'The CodeX Session search query is invalid.');
459
- }
460
- const catalog = await this.currentCatalog(signal);
461
- const needle = query.toLocaleLowerCase();
462
- const matches = catalog.threads.flatMap(thread => {
463
- const session = projectCodexThread(thread);
464
- if (session === undefined)
465
- return [];
466
- const candidates = [session.title, session.preview, session.cwd, session.nativeId]
467
- .filter((value) => value !== undefined && value.length > 0);
468
- const snippet = candidates.find(value => value.toLocaleLowerCase().includes(needle));
469
- if (snippet === undefined)
470
- return [];
471
- return [{ sessionId: session.id, snippet: truncateCodePoints(snippet, SESSION_SEARCH_SNIPPET_CODE_POINTS) }];
472
- });
473
- return success({
474
- items: matches.slice(0, SESSION_SEARCH_RESULT_LIMIT),
475
- hasMore: matches.length > SESSION_SEARCH_RESULT_LIMIT,
476
- });
477
- }
478
- async workspaceFollow(signal) {
479
- const queue = new AsyncValueQueue(signal);
480
- this.workspaceStreams.add(queue);
481
- const catalog = await this.refreshCatalog(signal);
482
- queue.push({
483
- type: 'baseline',
484
- value: {
485
- items: nativeVisibleWorkspaces(catalog, this.selectedWorkspaceId).map(nativeWorkspace),
486
- archivedSessionIds: catalog.sessions.filter(item => item.archived).map(item => item.id),
487
- // Required by `WorkspaceBaseline`; CodeX virtual Workspaces hold no pins.
488
- pinnedSessionIds: [],
489
- },
490
- });
491
- return queue.iterate(() => this.workspaceStreams.delete(queue));
492
- }
493
- async sessionControl(signal) {
494
- const queue = new AsyncValueQueue(signal);
495
- this.controlStreams.add(queue);
496
- queue.push({ type: 'baseline', value: { queues: {}, jobs: {}, projections: {} } });
497
- return queue.iterate(() => this.controlStreams.delete(queue));
498
- }
499
- async remoteEvents(signal) {
500
- const queue = new AsyncValueQueue(signal);
501
- const clientId = `codex-events:${this.host.deviceId}:${Date.now().toString(36)}`;
502
- this.eventStreams.set(clientId, queue);
503
- const home = (await this.currentCatalog(signal)).workspaces[0]?.path ?? '/';
504
- queue.push({ type: 'ready', clientId, host: { home } });
505
- return queue.iterate(() => this.eventStreams.delete(clientId));
506
- }
507
- async sessionFollow(request, signal) {
508
- const sessionId = sessionIdFromAddress(record(request.address));
509
- const threadId = nativeThreadId(sessionId);
510
- const history = await this.readHistoryPage(threadId, {
511
- maxMessages: optionalPositiveInteger(request.maxMessages),
512
- }, signal);
513
- const directory = await this.models(signal).catch(() => undefined);
514
- const queue = new AsyncValueQueue(signal);
515
- const follow = {
516
- sessionId,
517
- threadId,
518
- queue,
519
- nextSeq: history.cursor + 1,
520
- turn: history.nextTurn,
521
- stepOpen: history.activeTurnId !== undefined,
522
- startedItems: new Set(),
523
- completedItems: new Set(),
524
- streamedBlocks: new Map(),
525
- nextBlockIndex: 0,
526
- streamActive: false,
527
- assistantStreamRevision: 0,
528
- liveItems: new Map(),
529
- liveToolOutput: new Map(),
530
- liveToolResultSeq: new Map(),
531
- pendingToolImages: [],
532
- requestId: this.pendingRequestIds.get(sessionId),
533
- ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
534
- };
535
- this.follows.add(follow);
536
- queue.push({
537
- type: 'snapshot',
538
- header: history.header,
539
- cursor: history.cursor,
540
- records: history.records,
541
- hasMore: history.hasMore,
542
- projections: {
543
- // Same union as the summary path: the kind is what the client switches on.
544
- kind: 'sequenced',
545
- asOfSeq: history.cursor,
546
- values: {
547
- title: this.sessionTitle(sessionId),
548
- sessionListMetadata: { blank: history.cursor < 0, lastPromptAt: null },
549
- modelSelection: modelSelectionProjection(this.modelSelection(sessionId, directory)),
550
- permissions: codexPermissionsProjection(this.permissionSelection(sessionId)),
551
- imageLimits: codexImageLimitsProjection(),
552
- },
553
- },
554
- ...(this.sessionGeneration === 'v3' ? { assistantStream: { revision: 0 } } : {}),
555
- });
556
- try {
557
- const stream = await this.client.subscribe(threadId, frame => this.acceptCodexFrame(follow, frame), signal, () => this.closeFollowAfterRemoteStreamClosed(follow));
558
- follow.close = () => stream.close();
559
- }
560
- catch (error) {
561
- this.follows.delete(follow);
562
- queue.close();
563
- throw error;
564
- }
565
- return queue.iterate(() => {
566
- this.follows.delete(follow);
567
- void follow.close?.().catch(() => undefined);
568
- });
569
- }
570
- acceptCodexFrame(follow, frame) {
571
- const params = record(frame.params);
572
- if (frame.method === 'thread/name/updated') {
573
- const name = string(params.threadName);
574
- if (name === undefined)
575
- return;
576
- const seq = this.nextProjectionSeq();
577
- this.updateThreadName(follow.threadId, name);
578
- const title = name.trim() === '' ? null : name.slice(0, 256);
579
- this.publishProjection(follow.sessionId, 'title', title, seq);
580
- return;
581
- }
582
- if (frame.method === 'thread/settings/updated') {
583
- const preset = codexPermissionPresetFromResponse(params);
584
- this.setPermissionSelection(follow.sessionId, preset);
585
- return;
586
- }
587
- if (frame.method === 'turn/started') {
588
- const turn = record(params.turn);
589
- this.endAssistantAttempt(follow, { kind: 'abandoned' });
590
- this.resetLiveTurn(follow);
591
- follow.turn += 1;
592
- follow.activeTurnId = string(turn.id);
593
- follow.requestId = this.pendingRequestIds.get(follow.sessionId);
594
- follow.stepOpen = true;
595
- this.pushEvent(follow, 'turn/start', { turn: follow.turn });
596
- this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
597
- this.emitRemoteEvent('api-session/status', [follow.sessionId, true]);
598
- for (const item of array(turn.items))
599
- this.acceptCompletedItem(follow, record(item), false);
600
- return;
601
- }
602
- if (frame.method === 'item/started' || frame.method === 'item/completed') {
603
- const item = record(params.item);
604
- const itemId = string(item.id);
605
- if (frame.method === 'item/started' && itemId !== undefined) {
606
- follow.startedItems.add(itemId);
607
- follow.liveItems.set(itemId, item);
608
- if (isToolItemType(string(item.type)))
609
- this.ensureToolStarted(follow, item);
610
- }
611
- if (frame.method === 'item/completed')
612
- this.acceptCompletedItem(follow, item, true);
613
- return;
614
- }
615
- if (frame.method === 'item/agentMessage/delta') {
616
- const itemId = string(params.itemId) ?? `assistant:${follow.turn}`;
617
- const delta = string(params.delta);
618
- if (delta === undefined)
619
- return;
620
- this.appendStreamDelta(follow, `agent:${itemId}`, itemId, 'text', delta);
621
- return;
622
- }
623
- if (frame.method === 'item/reasoning/summaryPartAdded') {
624
- const itemId = string(params.itemId);
625
- if (itemId !== undefined)
626
- this.ensureStreamBlock(follow, `reasoning-summary:${itemId}:${integer(params.summaryIndex) ?? 0}`, itemId, 'reasoning');
627
- return;
628
- }
629
- if (frame.method === 'item/reasoning/summaryTextDelta'
630
- || frame.method === 'item/reasoning/textDelta'
631
- || frame.method === 'item/plan/delta') {
632
- const itemId = string(params.itemId);
633
- const delta = string(params.delta);
634
- if (itemId === undefined || delta === undefined)
635
- return;
636
- const key = frame.method === 'item/plan/delta'
637
- ? `plan:${itemId}`
638
- : frame.method === 'item/reasoning/summaryTextDelta'
639
- ? `reasoning-summary:${itemId}:${integer(params.summaryIndex) ?? 0}`
640
- : `reasoning-content:${itemId}:${integer(params.contentIndex) ?? 0}`;
641
- this.appendStreamDelta(follow, key, itemId, 'reasoning', delta);
642
- return;
643
- }
644
- if (frame.method === 'turn/plan/updated') {
645
- const todos = array(params.plan).flatMap(value => {
646
- const item = record(value);
647
- const content = string(item.step);
648
- if (content === undefined)
649
- return [];
650
- const status = item.status === 'inProgress'
651
- ? 'in_progress'
652
- : item.status === 'completed' ? 'completed' : 'pending';
653
- return [{ content, status }];
654
- });
655
- this.pushEvent(follow, 'todo/write', { todos });
656
- return;
657
- }
658
- if (frame.method === 'item/commandExecution/outputDelta'
659
- || frame.method === 'item/fileChange/outputDelta'
660
- || frame.method === 'item/mcpToolCall/progress') {
661
- const itemId = string(params.itemId);
662
- const delta = frame.method === 'item/mcpToolCall/progress'
663
- ? string(params.message)
664
- : string(params.delta);
665
- if (itemId === undefined || delta === undefined)
666
- return;
667
- const type = frame.method === 'item/commandExecution/outputDelta'
668
- ? 'commandExecution'
669
- : frame.method === 'item/fileChange/outputDelta' ? 'fileChange' : 'mcpToolCall';
670
- const item = follow.liveItems.get(itemId) ?? { id: itemId, type, status: 'inProgress' };
671
- follow.liveItems.set(itemId, item);
672
- this.ensureToolStarted(follow, item);
673
- const separator = frame.method === 'item/mcpToolCall/progress' && follow.liveToolOutput.has(itemId) ? '\n' : '';
674
- const output = appendBoundedText(follow.liveToolOutput.get(itemId) ?? '', `${separator}${delta}`);
675
- follow.liveToolOutput.set(itemId, output);
676
- this.emitToolResult(follow, item, output);
677
- return;
678
- }
679
- if (frame.method === 'item/fileChange/patchUpdated') {
680
- const itemId = string(params.itemId);
681
- if (itemId === undefined)
682
- return;
683
- const previous = follow.liveItems.get(itemId) ?? { id: itemId, type: 'fileChange', status: 'inProgress' };
684
- const item = { ...previous, changes: sanitizedFileChanges(params.changes) };
685
- follow.liveItems.set(itemId, item);
686
- this.ensureToolStarted(follow, item);
687
- this.emitToolResult(follow, item, fileChangeSummary(item));
688
- return;
689
- }
690
- if (frame.method === 'thread/status/changed') {
691
- const status = record(params.status);
692
- const running = status.type === 'active';
693
- this.emitRemoteEvent('api-session/status', [follow.sessionId, running]);
694
- if (status.type === 'systemError') {
695
- this.broadcastRcHost({
696
- type: 'host/agent-error',
697
- sessionId: follow.sessionId,
698
- message: 'CodeX reported a thread system error.',
699
- });
700
- }
701
- return;
702
- }
703
- if (frame.method === 'model/rerouted') {
704
- const model = string(params.toModel);
705
- if (model === undefined)
706
- return;
707
- const previous = this.selectedModels.get(follow.sessionId);
708
- const selected = {
709
- provider: CODEX_PROVIDER,
710
- model,
711
- ...(previous?.reasoningEffort === undefined ? {} : { reasoningEffort: previous.reasoningEffort }),
712
- };
713
- this.selectedModels.set(follow.sessionId, selected);
714
- const seq = this.pushEvent(follow, 'request/context', { provider: CODEX_PROVIDER, model });
715
- this.publishProjection(follow.sessionId, 'modelSelection', modelSelectionProjection(selected), seq);
716
- return;
717
- }
718
- if (frame.method === 'turn/completed') {
719
- const turn = record(params.turn);
720
- for (const item of array(turn.items))
721
- this.acceptCompletedItem(follow, record(item), true);
722
- if (follow.stepOpen) {
723
- this.closeAllStreamBlocks(follow);
724
- if (follow.streamActive)
725
- this.finishStream(follow);
726
- this.endAssistantAttempt(follow, { kind: 'abandoned' });
727
- this.pushEvent(follow, 'step/end', { turn: follow.turn, step: 1 });
728
- follow.stepOpen = false;
729
- }
730
- this.pushEvent(follow, 'turn/end', {
731
- turn: follow.turn,
732
- reason: turn.status === 'failed' || turn.error !== undefined && turn.error !== null
733
- ? { kind: 'error', error: { message: 'CodeX turn failed.', code: 'codex-turn-failed' } }
734
- : { kind: 'completed' },
735
- });
736
- this.emitRemoteEvent('api-session/status', [follow.sessionId, false]);
737
- this.emitRemoteEvent('api-session/activity', [follow.sessionId, Date.now()]);
738
- follow.activeTurnId = undefined;
739
- this.pendingRequestIds.delete(follow.sessionId);
740
- void this.refreshAndPublishWorkspaces();
741
- return;
742
- }
743
- if (frame.method === 'item/commandExecution/requestApproval'
744
- || frame.method === 'item/fileChange/requestApproval') {
745
- const requestHandle = string(params.requestHandle);
746
- if (requestHandle === undefined)
747
- return;
748
- const command = commandText(params.command);
749
- this.pendingApprovals.set(requestHandle, { requestHandle, sessionId: follow.sessionId });
750
- this.emitApproval({
751
- eventId: requestHandle,
752
- agentId: follow.sessionId,
753
- request: {
754
- toolName: frame.method.includes('fileChange') ? 'CodeX file change' : 'CodeX command',
755
- ...(command === undefined ? {} : { reason: command }),
756
- },
757
- });
758
- }
759
- }
760
- acceptCompletedItem(follow, item, settleAssistant) {
761
- const itemId = string(item.id) ?? `item:${follow.nextSeq}`;
762
- if (follow.completedItems.has(itemId))
763
- return;
764
- const type = string(item.type);
765
- if (type === 'agentMessage' && !settleAssistant)
766
- return;
767
- follow.completedItems.add(itemId);
768
- this.closeItemStreamBlocks(follow, itemId, itemText(item));
769
- if (type === 'agentMessage' && follow.streamActive && follow.streamedBlocks.size === 0)
770
- this.finishStream(follow);
771
- const supplementalImages = type === 'agentMessage' ? follow.pendingToolImages : [];
772
- let assistantMessageSeq;
773
- for (const event of itemEvents(item, follow.turn, 1, follow.requestId, this.modelSelection(follow.sessionId), follow.sessionId, supplementalImages)) {
774
- if (event.type === 'tool/call' && follow.startedItems.has(itemId))
775
- continue;
776
- if (event.type === 'tool/result')
777
- this.pushToolResult(follow, itemId, event);
778
- else {
779
- const seq = this.pushEvent(follow, event.type, event.data, event.view);
780
- if (event.type === 'assistant/message')
781
- assistantMessageSeq = seq;
782
- }
783
- }
784
- if (type === 'agentMessage' && assistantMessageSeq !== undefined) {
785
- this.endAssistantAttempt(follow, {
786
- kind: 'committed',
787
- eventType: 'assistant/message',
788
- seq: assistantMessageSeq,
789
- });
790
- }
791
- if (type === 'agentMessage' && supplementalImages.length > 0)
792
- follow.pendingToolImages = [];
793
- else if (type !== undefined && isToolItemType(type)) {
794
- follow.pendingToolImages.push(...toolOutputImageBlocks(item));
795
- }
796
- follow.liveItems.delete(itemId);
797
- follow.liveToolOutput.delete(itemId);
798
- follow.liveToolResultSeq.delete(itemId);
799
- follow.liveToolResultSeq.delete(`${itemId}:call`);
800
- }
801
- pushEvent(follow, type, data, view, placement) {
802
- const seq = follow.nextSeq++;
803
- const event = {
804
- type,
805
- seq,
806
- time: Date.now(),
807
- data: adaptEventData(type, type === 'assistant/message' && this.sessionGeneration === 'v3' && follow.assistantAttempt !== undefined
808
- ? { ...record(data), stream: follow.assistantAttempt.stream }
809
- : data, this.sessionGeneration),
810
- ...(isSurfaceEvent(type)
811
- ? placement === undefined
812
- ? { surfaceOp: 'append' }
813
- : {
814
- surfaceOp: this.sessionGeneration === 'v3'
815
- ? { op: 'replace', startSeq: placement.replaceSeq, endSeq: placement.replaceSeq }
816
- : { op: 'replace', start: placement.replaceSeq, end: placement.replaceSeq },
817
- sourceEventSeqs: [placement.replaceSeq],
818
- }
819
- : {}),
820
- };
821
- this.cacheImageBlocks(follow.sessionId, event.data);
822
- if (!follow.rcOnly) {
823
- follow.queue.push({
824
- type: 'event',
825
- event,
826
- ...(view === undefined ? {} : { view }),
827
- });
828
- }
829
- this.broadcastRcMux({
830
- type: 'session/event',
831
- sessionId: follow.sessionId,
832
- event,
833
- ...(view === undefined ? {} : { view }),
834
- });
835
- return seq;
836
- }
837
- cacheImageBlocks(sessionId, value) {
838
- for (const block of collectImageBlocks(value)) {
839
- const attachment = record(block.attachment);
840
- const attachmentId = string(attachment.attachmentId);
841
- const data = string(block.data);
842
- if (attachmentId === undefined || data === undefined || !attachmentId.startsWith(CODEX_IMAGE_ATTACHMENT_PREFIX))
843
- continue;
844
- this.imageAttachments.set(attachmentId, { sessionId, attachment, data });
845
- }
846
- }
847
- ensureStreamBlock(follow, key, itemId, kind) {
848
- const current = follow.streamedBlocks.get(key);
849
- if (current !== undefined)
850
- return current;
851
- const block = { itemId, index: follow.nextBlockIndex++, kind, text: '' };
852
- follow.streamedBlocks.set(key, block);
853
- follow.streamActive = true;
854
- this.pushAssistantChunk(follow, { type: 'block-start', index: block.index, blockType: kind });
855
- return block;
856
- }
857
- appendStreamDelta(follow, key, itemId, kind, delta) {
858
- const block = this.ensureStreamBlock(follow, key, itemId, kind);
859
- block.text = appendBoundedText(block.text, delta);
860
- this.pushAssistantChunk(follow, {
861
- type: kind === 'reasoning' ? 'reasoning-delta' : 'text-delta', index: block.index, text: delta,
862
- });
863
- }
864
- closeItemStreamBlocks(follow, itemId, fallback) {
865
- const matches = [...follow.streamedBlocks].filter(([, block]) => block.itemId === itemId);
866
- for (const [key, block] of matches) {
867
- const text = block.text || fallback || '';
868
- this.pushAssistantChunk(follow, {
869
- type: 'block-end', index: block.index, block: { type: block.kind, text },
870
- });
871
- follow.streamedBlocks.delete(key);
872
- }
873
- }
874
- closeAllStreamBlocks(follow) {
875
- for (const block of follow.streamedBlocks.values()) {
876
- this.pushAssistantChunk(follow, {
877
- type: 'block-end', index: block.index, block: { type: block.kind, text: block.text },
878
- });
879
- }
880
- follow.streamedBlocks.clear();
881
- }
882
- finishStream(follow) {
883
- this.pushAssistantChunk(follow, { type: 'finish', reason: { kind: 'stop' } });
884
- follow.streamActive = false;
885
- }
886
- pushAssistantChunk(follow, chunk) {
887
- if (this.sessionGeneration !== 'v3' || follow.rcOnly === true) {
888
- this.pushEvent(follow, 'assistant/chunk', { turn: follow.turn, step: 1, chunk });
889
- return;
890
- }
891
- let attempt = follow.assistantAttempt;
892
- if (attempt === undefined) {
893
- attempt = {
894
- attemptId: `codex-attempt:${follow.activeTurnId ?? follow.turn}:${follow.nextSeq}`,
895
- startedAfterSeq: follow.nextSeq - 1,
896
- nextIndex: 0,
897
- stream: [],
898
- };
899
- follow.assistantAttempt = attempt;
900
- follow.assistantStreamRevision += 1;
901
- follow.queue.push({
902
- type: 'assistant-stream',
903
- frame: {
904
- type: 'start',
905
- attemptId: attempt.attemptId,
906
- revision: follow.assistantStreamRevision,
907
- startedAfterSeq: attempt.startedAfterSeq,
908
- turn: follow.turn,
909
- step: 1,
910
- },
911
- });
912
- }
913
- const time = Date.now();
914
- const index = attempt.nextIndex++;
915
- attempt.stream.push({ time, chunk });
916
- follow.assistantStreamRevision += 1;
917
- follow.queue.push({
918
- type: 'assistant-stream',
919
- frame: {
920
- type: 'chunk',
921
- attemptId: attempt.attemptId,
922
- revision: follow.assistantStreamRevision,
923
- index,
924
- time,
925
- chunk,
926
- },
927
- });
928
- }
929
- endAssistantAttempt(follow, outcome) {
930
- const attempt = follow.assistantAttempt;
931
- if (attempt === undefined)
932
- return;
933
- follow.assistantAttempt = undefined;
934
- follow.assistantStreamRevision += 1;
935
- if (this.sessionGeneration === 'v3' && follow.rcOnly !== true) {
936
- follow.queue.push({
937
- type: 'assistant-stream',
938
- frame: {
939
- type: 'end',
940
- attemptId: attempt.attemptId,
941
- revision: follow.assistantStreamRevision,
942
- index: attempt.nextIndex,
943
- outcome,
944
- },
945
- });
946
- }
947
- }
948
- ensureToolStarted(follow, item) {
949
- const itemId = string(item.id);
950
- if (itemId === undefined)
951
- return;
952
- if (!follow.startedItems.has(itemId))
953
- follow.startedItems.add(itemId);
954
- if (follow.liveItems.get(itemId) !== item)
955
- follow.liveItems.set(itemId, item);
956
- const call = itemEvents(item, follow.turn, 1, follow.requestId, this.modelSelection(follow.sessionId), follow.sessionId).find(event => event.type === 'tool/call');
957
- if (call !== undefined && !follow.liveToolResultSeq.has(`${itemId}:call`)) {
958
- this.pushEvent(follow, call.type, call.data, call.view);
959
- follow.liveToolResultSeq.set(`${itemId}:call`, -1);
960
- }
961
- }
962
- emitToolResult(follow, item, resultText) {
963
- const itemId = string(item.id);
964
- if (itemId === undefined)
965
- return;
966
- const result = toolResultEvent(item, follow.turn, 1, itemId, resultText);
967
- this.pushToolResult(follow, itemId, result);
968
- }
969
- pushToolResult(follow, itemId, event) {
970
- const previous = follow.liveToolResultSeq.get(itemId);
971
- const projected = previous === undefined ? event : toolResultContentReplacement(event);
972
- const seq = this.pushEvent(follow, projected.type, projected.data, projected.view, previous === undefined ? undefined : { replaceSeq: previous });
973
- follow.liveToolResultSeq.set(itemId, seq);
974
- }
975
- resetLiveTurn(follow) {
976
- follow.startedItems.clear();
977
- follow.completedItems.clear();
978
- follow.streamedBlocks.clear();
979
- follow.nextBlockIndex = 0;
980
- follow.streamActive = false;
981
- follow.assistantAttempt = undefined;
982
- follow.liveItems.clear();
983
- follow.liveToolOutput.clear();
984
- follow.liveToolResultSeq.clear();
985
- follow.pendingToolImages = [];
986
- }
987
- emitRemoteEvent(event, args) {
988
- for (const queue of this.eventStreams.values())
989
- queue.push({ type: 'emit', event, args });
990
- const sessionId = typeof args[0] === 'string' ? args[0] : undefined;
991
- if (event === 'api-session/status' && sessionId !== undefined && typeof args[1] === 'boolean') {
992
- this.broadcastRcHost({ type: 'host/session-status', sessionId, running: args[1] });
993
- }
994
- else if (event === 'api-session/removed' && sessionId !== undefined) {
995
- this.broadcastRcHost({ type: 'host/session-removed', sessionId });
996
- }
997
- else if (event === 'api-session/added' && isRecord(args[0])) {
998
- const summary = args[0];
999
- this.broadcastRcHost({
1000
- type: 'host/session-added',
1001
- sessionId: summary.sessionId,
1002
- blank: summary.blank === true,
1003
- ...(typeof summary.cwd === 'string' ? { cwd: summary.cwd } : {}),
1004
- });
1005
- }
1006
- }
1007
- emitApproval(input) {
1008
- for (const queue of this.eventStreams.values())
1009
- queue.push({
1010
- type: 'waterfall',
1011
- event: 'approval/request',
1012
- eventId: input.eventId,
1013
- agentId: input.agentId,
1014
- request: input.request,
1015
- });
1016
- this.broadcastRcMux({
1017
- type: 'approval/requested',
1018
- sessionId: input.agentId,
1019
- approvalId: input.eventId,
1020
- toolName: string(input.request.toolName) ?? 'CodeX',
1021
- ...(typeof input.request.reason === 'string' ? { reason: input.request.reason } : {}),
1022
- }, input.eventId);
1023
- }
1024
- async answerRemoteEvent(args, signal) {
1025
- const eventId = string(args.eventId);
1026
- const outcome = record(args.outcome);
1027
- if (eventId === undefined)
1028
- throw new Error('The CodeX approval result is missing its event id.');
1029
- const pending = this.pendingApprovals.get(eventId);
1030
- if (pending === undefined)
1031
- return undefined;
1032
- this.pendingApprovals.delete(eventId);
1033
- const decision = outcome.kind === 'result' && outcome.value === 'allowed-once'
1034
- ? 'accept'
1035
- : outcome.kind === 'result' && outcome.value === 'cancelled'
1036
- ? 'cancel'
1037
- : 'decline';
1038
- await this.client.respond(pending.requestHandle, decision, signal);
1039
- return undefined;
1040
- }
1041
- async createWorkspace(request, signal) {
1042
- const path = string(request.path);
1043
- if (path === undefined || path.trim() === '')
1044
- return failure('bad-request', 'A CodeX working directory is required.');
1045
- const catalog = await this.currentCatalog(signal);
1046
- const existing = catalog.workspaces.find(item => item.path === path);
1047
- if (existing !== undefined)
1048
- return success({ workspace: nativeWorkspace(existing), created: false });
1049
- return failure('workspace-not-found', 'The selected directory is not visible to CodeX Remote.');
1050
- }
1051
- async renameWorkspace(request) {
1052
- return failure('workspace-read-only', `CodeX virtual Workspace ${string(request.workspaceId) ?? ''} cannot be renamed.`);
1053
- }
1054
- async workspaceForSession(request, signal) {
1055
- const sessionId = string(request.sessionId);
1056
- const catalog = await this.currentCatalog(signal);
1057
- const workspace = catalog.workspaces.find(item => sessionId !== undefined && item.sessionIds.includes(sessionId));
1058
- return workspace === undefined
1059
- ? failure('workspace-not-found', 'The CodeX virtual Workspace was not found.')
1060
- : success({ workspace: nativeWorkspace(workspace) });
1061
- }
1062
- selectedWorkspace(catalog) {
1063
- return this.selectedWorkspaceId === undefined
1064
- ? undefined
1065
- : catalog.workspaces.find(item => item.workspaceId === this.selectedWorkspaceId);
1066
- }
1067
- async describeHost(signal) {
1068
- const catalog = await this.currentCatalog(signal);
1069
- const workspace = this.selectedWorkspace(catalog);
1070
- const models = await this.models(signal).catch(() => undefined);
1071
- return {
1072
- version: 'CodeX Remote',
1073
- cwd: workspace?.path ?? '',
1074
- home: workspace?.path ?? '',
1075
- provider: 'CodeX',
1076
- ...(models === undefined ? {} : { model: models.default.model }),
1077
- attachedSessions: catalog.sessions.length,
1078
- canOpenPath: workspace !== undefined,
1079
- };
1080
- }
1081
- async listDirectory(request, signal) {
1082
- const catalog = await this.currentCatalog(signal);
1083
- const workspace = this.selectedWorkspace(catalog);
1084
- if (workspace === undefined)
1085
- return failure('workspace-not-found', 'The CodeX virtual Workspace was not found.');
1086
- const path = string(request.path) ?? workspace.path;
1087
- if (!containsPath(workspace.path, path)) {
1088
- return failure('workspace-not-found', 'The selected directory is outside the current CodeX virtual Workspace.');
1089
- }
1090
- const listing = record(await this.client.request('dsh/directoryList', { path }, signal));
1091
- if (!isCodexDirectoryListing(listing)) {
1092
- return failure('internal', 'CodeX Remote returned an invalid directory listing.');
1093
- }
1094
- const listedPaths = [
1095
- listing.path,
1096
- listing.home,
1097
- ...listing.crumbs.map(item => item.path),
1098
- ...listing.entries.map(item => item.path),
1099
- ];
1100
- if (listedPaths.some(path => !containsPath(workspace.path, path))) {
1101
- return failure('internal', 'CodeX Remote returned a directory outside the current Workspace.');
1102
- }
1103
- return success(listing);
1104
- }
1105
- async archiveSession(request, signal) {
1106
- const sessionId = requiredString(request.sessionId, 'sessionId');
1107
- await this.client.request('thread/archive', { threadId: nativeThreadId(sessionId) }, signal);
1108
- const catalog = await this.refreshCatalog(signal);
1109
- this.publishWorkspaceBaseline(catalog);
1110
- return success({ archivedSessionIds: [sessionId] });
1111
- }
1112
- async createSession(request, signal) {
1113
- const catalog = await this.currentCatalog(signal);
1114
- const workspaceId = string(request.workspaceId) ?? this.selectedWorkspaceId;
1115
- const workspace = workspaceId === undefined ? undefined : catalog.workspaces.find(item => item.workspaceId === workspaceId);
1116
- const cwd = string(request.cwd)
1117
- ?? workspace?.path;
1118
- if (cwd === undefined)
1119
- return failure('workspace-not-found', 'The CodeX virtual Workspace was not found.');
1120
- if (workspace !== undefined && !containsPath(workspace.path, cwd)) {
1121
- return failure('workspace-not-found', 'The selected directory is outside the current CodeX virtual Workspace.');
1122
- }
1123
- const directory = await this.models(signal);
1124
- const selection = directory.default;
1125
- const requestedPreset = CODEX_DEFAULT_PERMISSION;
1126
- const result = record(await this.client.request('thread/start', {
1127
- cwd,
1128
- model: selection.model,
1129
- permissionPreset: requestedPreset,
1130
- }, signal));
1131
- const permissionPreset = codexPermissionPresetFromResponse(result) ?? requestedPreset;
1132
- const thread = { ...record(result.thread), cwd, turns: array(record(result.thread).turns) };
1133
- const projected = projectCodexThread(thread);
1134
- if (projected === undefined)
1135
- return failure('internal', 'CodeX returned an invalid Thread.');
1136
- this.pendingThreads.set(projected.nativeId, thread);
1137
- this.blankThreads.add(projected.nativeId);
1138
- this.selectedModels.set(projected.id, selection);
1139
- this.selectedPermissions.set(projected.id, permissionPreset);
1140
- await this.refreshAndPublishWorkspaces();
1141
- const seq = this.nextProjectionSeq();
1142
- this.emitRemoteEvent('api-session/added', [this.sessionSummary(projected, {
1143
- blank: true,
1144
- modelSelection: selection,
1145
- lastPromptAt: null,
1146
- asOfSeq: seq,
1147
- })]);
1148
- this.publishProjection(projected.id, 'title', displayTitle(projected), seq);
1149
- return success({ sessionId: projected.id });
1150
- }
1151
- async forkSession(request, signal) {
1152
- const sessionId = requiredString(request.sessionId, 'sessionId');
1153
- const result = record(await this.client.request('thread/fork', {
1154
- threadId: nativeThreadId(sessionId),
1155
- }, signal));
1156
- const permissionPreset = codexPermissionPresetFromResponse(result);
1157
- const projected = projectCodexThread(record(result.thread));
1158
- if (projected === undefined)
1159
- return failure('internal', 'CodeX returned an invalid forked Thread.');
1160
- this.selectedModels.set(projected.id, this.modelSelection(sessionId, await this.models(signal)));
1161
- this.setPermissionSelection(projected.id, permissionPreset);
1162
- await this.refreshAndPublishWorkspaces();
1163
- const seq = this.nextProjectionSeq();
1164
- this.publishProjection(projected.id, 'title', displayTitle(projected), seq);
1165
- return success({ sessionId: projected.id });
1166
- }
1167
- async prompt(request, signal) {
1168
- const sessionId = requiredString(request.sessionId, 'sessionId');
1169
- const content = array(request.content);
1170
- const input = codexPromptInput(content);
1171
- if (input === undefined)
1172
- return failure('attachment-error', 'CodeX virtual Sessions accept text and pasted PNG, JPEG, WebP, or GIF images only.');
1173
- const threadId = nativeThreadId(sessionId);
1174
- const selection = this.modelSelection(sessionId, await this.models(signal));
1175
- const isFreshBlankThread = this.blankThreads.has(threadId) || this.pendingThreads.has(threadId);
1176
- await this.ensureRcFollow(sessionId);
1177
- if (!isFreshBlankThread) {
1178
- const resumeResult = await this.client.request('thread/resume', {
1179
- threadId,
1180
- model: selection.model,
1181
- }, signal);
1182
- const serverPreset = codexPermissionPresetFromResponse(resumeResult);
1183
- if (serverPreset !== undefined) {
1184
- this.setPermissionSelection(sessionId, serverPreset);
1185
- }
1186
- }
1187
- this.pendingRequestIds.set(sessionId, string(request.requestId) ?? '');
1188
- const mode = request.mode === 'steer' ? 'turn/steer' : 'turn/start';
1189
- if (mode === 'turn/steer') {
1190
- const active = this.activeTurnId(threadId);
1191
- if (active === undefined)
1192
- return failure('steer-unavailable', 'The CodeX Thread has no active turn to steer.');
1193
- await this.client.request(mode, { threadId, expectedTurnId: active, input }, signal);
1194
- }
1195
- else {
1196
- await this.client.request(mode, {
1197
- threadId,
1198
- input,
1199
- ...codexModelParams(selection),
1200
- }, signal);
1201
- }
1202
- this.blankThreads.delete(threadId);
1203
- return success({ accepted: true });
1204
- }
1205
- async attachment(request, signal) {
1206
- const sessionId = requiredString(request.sessionId, 'sessionId');
1207
- const attachmentId = requiredString(request.attachmentId, 'attachmentId');
1208
- if (!attachmentId.startsWith(CODEX_IMAGE_ATTACHMENT_PREFIX)) {
1209
- return failure('attachment-error', 'The CodeX image is not available in this virtual Session.', { reason: 'not-referenced' });
1210
- }
1211
- nativeThreadId(sessionId);
1212
- const cached = this.imageAttachments.get(attachmentId);
1213
- if (cached?.sessionId === sessionId)
1214
- return success({ attachment: cached.attachment, data: cached.data });
1215
- await this.hydrateImageAttachments(sessionId, signal);
1216
- const hydrated = this.imageAttachments.get(attachmentId);
1217
- return hydrated?.sessionId === sessionId
1218
- ? success({ attachment: hydrated.attachment, data: hydrated.data })
1219
- : failure('attachment-error', 'The CodeX image is not available in this virtual Session.', { reason: 'not-referenced' });
1220
- }
1221
- async hydrateImageAttachments(sessionId, signal) {
1222
- const threadId = nativeThreadId(sessionId);
1223
- const result = record(await this.client.request('thread/read', { threadId, includeTurns: true }, signal));
1224
- const thread = record(result.thread);
1225
- if (string(thread.id) !== threadId)
1226
- throw new Error('CodeX returned an invalid Thread history.');
1227
- this.cacheHistoryImages(projectCodexNativeHistory(thread, sessionId));
1228
- }
1229
- async cancel(request, signal) {
1230
- const sessionId = requiredString(request.sessionId, 'sessionId');
1231
- const threadId = nativeThreadId(sessionId);
1232
- const turnId = this.activeTurnId(threadId);
1233
- if (turnId !== undefined)
1234
- await this.client.request('turn/interrupt', { threadId, turnId }, signal);
1235
- return success({ accepted: true });
1236
- }
1237
- async renameSession(request, signal) {
1238
- const sessionId = requiredString(request.sessionId, 'sessionId');
1239
- const title = requiredString(request.title, 'title');
1240
- // Codex 0.160.0 refuses an empty name upstream ("Codex App Server rejected the
1241
- // request"), so clearing a title stays a local projection change.
1242
- if (title.trim() !== '') {
1243
- await this.client.request('thread/name/set', { threadId: nativeThreadId(sessionId), name: title }, signal);
1244
- }
1245
- const seq = this.nextProjectionSeq();
1246
- this.updateThreadName(nativeThreadId(sessionId), title);
1247
- this.publishProjection(sessionId, 'title', title, seq);
1248
- await this.refreshAndPublishWorkspaces();
1249
- return success({ title, seq });
1250
- }
1251
- async sessionPage(request, signal) {
1252
- const sessionId = sessionIdFromAddress(record(request.address));
1253
- const page = await this.readHistoryPage(nativeThreadId(sessionId), {
1254
- beforeSeq: optionalNonNegativeInteger(request.beforeSeq),
1255
- throughSeq: optionalInteger(request.throughSeq),
1256
- maxMessages: optionalPositiveInteger(request.maxMessages),
1257
- }, signal);
1258
- return success({ records: page.records, hasMore: page.hasMore });
1259
- }
1260
- activeTurnId(threadId) {
1261
- for (const follow of this.follows)
1262
- if (follow.threadId === threadId && follow.stepOpen)
1263
- return follow.activeTurnId;
1264
- return undefined;
1265
- }
1266
- async fetchThread(threadId, signal) {
1267
- const result = record(await this.client.request('thread/read', { threadId, includeTurns: false }, signal));
1268
- const thread = record(result.thread);
1269
- if (string(thread.id) !== threadId)
1270
- throw new Error('CodeX returned an invalid Thread history.');
1271
- return thread;
1272
- }
1273
- async readHistoryPage(threadId, page, signal) {
1274
- // The Host's call policy caps a history page at CODEX_HISTORY_MAX_MESSAGES, and the
1275
- // native UI asks for a far larger page. Forwarding that request was refused outright
1276
- // ("dsh/sessionHistory -> maxMessages: too_big") and the session history never loaded,
1277
- // so every read clamps to the same constant the policy uses.
1278
- const maxMessages = page.maxMessages === undefined
1279
- ? undefined
1280
- : Math.min(CODEX_HISTORY_MAX_MESSAGES, Math.max(1, page.maxMessages));
1281
- let value;
1282
- try {
1283
- value = record(await this.client.request('dsh/sessionHistory', {
1284
- threadId,
1285
- ...(page.beforeSeq === undefined ? {} : { beforeSeq: page.beforeSeq }),
1286
- ...(page.throughSeq === undefined ? {} : { throughSeq: page.throughSeq }),
1287
- ...(maxMessages === undefined ? {} : { maxMessages }),
1288
- }, signal));
1289
- }
1290
- catch (error) {
1291
- if (!isLegacySessionHistoryUnsupported(error))
1292
- throw error;
1293
- const result = record(await this.client.request('thread/read', { threadId, includeTurns: true }, signal));
1294
- const thread = record(result.thread);
1295
- if (string(thread.id) !== threadId)
1296
- throw new Error('CodeX returned an invalid Thread history.');
1297
- value = paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`, this.sessionGeneration), {
1298
- beforeSeq: page.beforeSeq,
1299
- throughSeq: page.throughSeq,
1300
- maxMessages,
1301
- });
1302
- }
1303
- value = adaptCodexHistoryPage(value, this.sessionGeneration);
1304
- if (!isCodexNativeHistoryPage(value, `codex:${threadId}`)) {
1305
- throw new Error('CodeX Remote returned an invalid paginated History.');
1306
- }
1307
- if (Object.hasOwn(value, 'permissionPreset')) {
1308
- const preset = value.permissionPreset;
1309
- this.setPermissionSelection(`codex:${threadId}`, preset === 'workspace-write' || preset === 'danger-full-access' ? preset : undefined);
1310
- }
1311
- this.cacheHistoryImages(value);
1312
- return value;
1313
- }
1314
- cacheHistoryImages(history) {
1315
- const sessionId = history.header.id;
1316
- const entries = 'entries' in history ? history.entries : history.records;
1317
- for (const entry of entries)
1318
- this.cacheImageBlocks(sessionId, entry.event.data);
1319
- }
1320
- async sessionHistory(request, signal) {
1321
- const sessionId = requiredString(request.sessionId, 'sessionId');
1322
- const beforeSeq = optionalNonNegativeInteger(request.beforeSeq);
1323
- const history = await this.readHistoryPage(nativeThreadId(sessionId), {
1324
- beforeSeq,
1325
- maxMessages: optionalPositiveInteger(request.maxMessages),
1326
- }, signal);
1327
- const directory = await this.models(signal).catch(() => undefined);
1328
- // rc.2 has one global mux rather than the alpha session/follow stream. A
1329
- // history read is the reliable signal that the native client has opened a
1330
- // Session, so attach the CodeX live stream here before returning its tail.
1331
- if (beforeSeq === undefined)
1332
- await this.ensureRcFollow(sessionId, history);
1333
- return success({
1334
- events: history.records.map(entry => ({
1335
- event: entry.event,
1336
- ...(entry.view === undefined ? {} : { view: entry.view }),
1337
- })),
1338
- hasMore: history.hasMore,
1339
- ...(beforeSeq !== undefined ? {} : {
1340
- projections: {
1341
- // Same discriminated union as the other projection sites.
1342
- kind: 'sequenced',
1343
- asOfSeq: history.cursor,
1344
- values: {
1345
- title: this.sessionTitle(sessionId),
1346
- sessionListMetadata: { blank: history.cursor < 0, lastPromptAt: null },
1347
- modelSelection: modelSelectionProjection(this.modelSelection(sessionId, directory)),
1348
- permissions: codexPermissionsProjection(this.permissionSelection(sessionId)),
1349
- imageLimits: codexImageLimitsProjection(),
1350
- },
1351
- },
1352
- }),
1353
- });
1354
- }
1355
- async ensureRcFollow(sessionId, seedHistory) {
1356
- if (this.followsHas(sessionId))
1357
- return;
1358
- const threadId = nativeThreadId(sessionId);
1359
- // The Host bounds CodeX streams per connection. rc.2 only renders one
1360
- // active conversation, so release older rc-only follows when navigation
1361
- // moves to another Session. Alpha follows own their lifecycle separately.
1362
- const stale = [...this.follows].filter(follow => follow.rcOnly && follow.sessionId !== sessionId);
1363
- for (const follow of stale) {
1364
- this.follows.delete(follow);
1365
- follow.queue.close();
1366
- await follow.close?.().catch(() => undefined);
1367
- }
1368
- const history = seedHistory ?? await this.readHistoryPage(threadId, {}, undefined);
1369
- const controller = new AbortController();
1370
- const follow = {
1371
- sessionId,
1372
- threadId,
1373
- queue: new AsyncValueQueue(controller.signal),
1374
- nextSeq: history.cursor + 1,
1375
- turn: history.nextTurn,
1376
- stepOpen: history.activeTurnId !== undefined,
1377
- startedItems: new Set(),
1378
- completedItems: new Set(),
1379
- streamedBlocks: new Map(),
1380
- nextBlockIndex: 0,
1381
- streamActive: false,
1382
- assistantStreamRevision: 0,
1383
- liveItems: new Map(),
1384
- liveToolOutput: new Map(),
1385
- liveToolResultSeq: new Map(),
1386
- pendingToolImages: [],
1387
- requestId: this.pendingRequestIds.get(sessionId),
1388
- rcOnly: true,
1389
- ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
1390
- };
1391
- this.follows.add(follow);
1392
- try {
1393
- const stream = await this.client.subscribe(threadId, frame => this.acceptCodexFrame(follow, frame), controller.signal, () => this.closeFollowAfterRemoteStreamClosed(follow));
1394
- follow.close = async () => {
1395
- controller.abort();
1396
- await stream.close();
1397
- };
1398
- }
1399
- catch (error) {
1400
- this.follows.delete(follow);
1401
- controller.abort();
1402
- follow.queue.close();
1403
- throw error;
1404
- }
1405
- }
1406
- followsHas(sessionId) {
1407
- for (const follow of this.follows)
1408
- if (follow.sessionId === sessionId)
1409
- return true;
1410
- return false;
1411
- }
1412
- closeFollowAfterRemoteStreamClosed(follow) {
1413
- this.follows.delete(follow);
1414
- follow.queue.close();
1415
- this.emitRemoteEvent('api-session/status', [follow.sessionId, false]);
1416
- }
1417
- async refreshAndPublishWorkspaces() {
1418
- const catalog = await this.refreshCatalog().catch(() => undefined);
1419
- if (catalog !== undefined)
1420
- this.publishWorkspaceBaseline(catalog);
1421
- }
1422
- publishWorkspaceBaseline(catalog) {
1423
- for (const queue of this.workspaceStreams) {
1424
- for (const workspace of nativeVisibleWorkspaces(catalog, this.selectedWorkspaceId)) {
1425
- queue.push({ type: 'upsert', workspace: nativeWorkspace(workspace) });
1426
- }
1427
- queue.push({ type: 'archived', archivedSessionIds: catalog.sessions.filter(item => item.archived).map(item => item.id) });
1428
- }
1429
- }
1430
- createApiProxy() {
1431
- const call = (endpoint) => async (request, signal) => {
1432
- const payload = endpoint === 'session.prompt' && isRecord(request.payload)
1433
- ? { ...request.payload, requestId: String(request.rpcId) }
1434
- : request.payload;
1435
- const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
1436
- return {
1437
- rpcId: request.rpcId,
1438
- result: (result.ok
1439
- ? success(result.value)
1440
- : failure(result.error.code, result.error.message, result.error.details)),
1441
- };
1442
- };
1443
- return {
1444
- sessions: {
1445
- list: call('session.list'),
1446
- search: call('session.search'),
1447
- create: call('session.create'),
1448
- history: call('session.history'),
1449
- models: call('session.models'),
1450
- selectModel: call('session.selectModel'),
1451
- rename: call('session.rename'),
1452
- fork: call('session.fork'),
1453
- prompt: call('session.prompt'),
1454
- attachment: call('session.attachment'),
1455
- updateQueue: call('session.updateQueue'),
1456
- cancel: call('session.cancel'),
1457
- },
1458
- workspace: {
1459
- list: call('workspace.list'),
1460
- create: call('workspace.create'),
1461
- rename: call('workspace.rename'),
1462
- delete: call('workspace.delete'),
1463
- insertBefore: call('workspace.insertBefore'),
1464
- insertSessionBefore: call('workspace.insertSessionBefore'),
1465
- archiveSession: call('workspace.archiveSession'),
1466
- },
1467
- subagents: {},
1468
- host: {
1469
- describe: call('host.describe'),
1470
- listDirectory: call('host.listDirectory'),
1471
- },
1472
- skills: { list: call('skills.list') },
1473
- agentPresets: {},
1474
- goals: {},
1475
- settings: {},
1476
- credentials: {},
1477
- llm: {},
1478
- events: {
1479
- mux: ((request, signal) => this.rcMux(request, signal)),
1480
- host: ((request, signal) => this.rcHost(request, signal)),
1481
- },
1482
- downloads: {},
1483
- respond: async (message) => {
1484
- const pending = this.pendingApprovals.get(String(message.rpcId));
1485
- if (pending === undefined)
1486
- return { accepted: false, reason: 'not-pending' };
1487
- const result = message.result;
1488
- const outcome = result.ok ? result.value : undefined;
1489
- const decision = outcome === 'allowed-once' ? 'accept' : outcome === 'cancelled' ? 'cancel' : 'decline';
1490
- await this.client.respond(pending.requestHandle, decision);
1491
- this.pendingApprovals.delete(pending.requestHandle);
1492
- return { accepted: true };
1493
- },
1494
- };
1495
- }
1496
- async *rcMux(request, signal) {
1497
- const queue = new AsyncValueQueue(signal);
1498
- this.rcMuxStreams.add(queue);
1499
- const catalog = await this.currentCatalog(signal);
1500
- for (const session of catalog.sessions)
1501
- queue.push({
1502
- rpcId: `${String(request.rpcId)}:${session.id}:subscribed`,
1503
- payload: { type: 'session/subscribed', sessionId: session.id, lastSeq: -1 },
1504
- });
1505
- try {
1506
- yield* queue;
1507
- }
1508
- finally {
1509
- this.rcMuxStreams.delete(queue);
1510
- queue.close();
1511
- }
1512
- }
1513
- async *rcHost(_request, signal) {
1514
- const queue = new AsyncValueQueue(signal);
1515
- this.rcHostStreams.add(queue);
1516
- try {
1517
- yield* queue;
1518
- }
1519
- finally {
1520
- this.rcHostStreams.delete(queue);
1521
- queue.close();
1522
- }
1523
- }
1524
- broadcastRcMux(payload, rpcId = `codex-mux:${Date.now()}:${Math.random()}`) {
1525
- for (const queue of this.rcMuxStreams)
1526
- queue.push({ rpcId, payload });
1527
- }
1528
- broadcastRcHost(payload) {
1529
- const frame = { rpcId: `codex-host:${Date.now()}:${Math.random()}`, payload };
1530
- for (const queue of this.rcHostStreams)
1531
- queue.push(frame);
1532
- }
1533
- }
1534
- async function loadCatalog(client, signal, pendingThreads) {
1535
- let projects = await loadCodexProjects(client, signal);
1536
- const threads = [];
1537
- let cursor;
1538
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
1539
- const result = record(await client.request('thread/list', {
1540
- limit: CODEX_PAGE_LIMIT,
1541
- sortKey: 'updated_at',
1542
- sortDirection: 'desc',
1543
- archived: false,
1544
- ...(cursor === undefined ? {} : { cursor }),
1545
- }, signal));
1546
- for (const value of array(result.data)) {
1547
- const thread = record(value);
1548
- if (string(thread.id) !== undefined)
1549
- threads.push(thread);
1550
- }
1551
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
1552
- if (cursor === undefined)
1553
- break;
1554
- }
1555
- if (pendingThreads !== undefined) {
1556
- const listedIds = new Set(threads.map(thread => string(thread.id)).filter((id) => id !== undefined));
1557
- for (const [threadId, thread] of pendingThreads) {
1558
- if (listedIds.has(threadId))
1559
- pendingThreads.delete(threadId);
1560
- else
1561
- threads.unshift(thread);
1562
- }
1563
- }
1564
- if (projects.length === 0) {
1565
- projects = deriveCodexCwdWorkspaces(threads).map(workspace => ({
1566
- id: workspace.id,
1567
- name: workspace.name,
1568
- roots: [{ path: workspace.path }],
1569
- position: workspace.position,
1570
- createdAt: workspace.createdAt,
1571
- updatedAt: workspace.updatedAt,
1572
- }));
1573
- }
1574
- const projected = threads.map(thread => {
1575
- const session = projectCodexThread(thread);
1576
- return session === undefined ? undefined : { thread, session };
1577
- }).filter((value) => value !== undefined);
1578
- const projectWorkspaces = projects.map(projectWorkspace);
1579
- const workspaceByProjectId = new Map(projects.map((project, index) => [project.id, projectWorkspaces[index]]));
1580
- const visibleSessionIds = new Set();
1581
- const visibleThreads = [];
1582
- for (const { thread, session } of projected) {
1583
- const projectWorkspaceForSession = findProjectWorkspace(thread, session, projects, workspaceByProjectId);
1584
- if (projectWorkspaceForSession !== undefined) {
1585
- addSessionToWorkspace(projectWorkspaceForSession, session);
1586
- visibleSessionIds.add(session.id);
1587
- visibleThreads.push(thread);
1588
- }
1589
- }
1590
- const sessions = projected
1591
- .map(value => value.session)
1592
- .filter(session => visibleSessionIds.has(session.id));
1593
- return {
1594
- threads: visibleThreads,
1595
- sessions,
1596
- projects,
1597
- workspaces: projectWorkspaces,
1598
- };
1599
- }
1600
- async function loadCodexProjects(client, signal) {
1601
- const projects = [];
1602
- let cursor;
1603
- try {
1604
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
1605
- const result = record(await client.request('project/list', {
1606
- limit: CODEX_PAGE_LIMIT,
1607
- ...(cursor === undefined ? {} : { cursor }),
1608
- }, signal));
1609
- for (const value of array(result.data)) {
1610
- const project = projectCodexProject(value, projects.length);
1611
- if (project !== undefined && !projects.some(item => item.id === project.id))
1612
- projects.push(project);
1613
- }
1614
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
1615
- if (cursor === undefined)
1616
- break;
1617
- }
1618
- }
1619
- catch (error) {
1620
- if (isProjectListUnsupported(error))
1621
- return [];
1622
- throw error;
1623
- }
1624
- return projects.sort((left, right) => left.position - right.position || left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
1625
- }
1626
- async function loadModelDirectory(client, signal) {
1627
- const models = new Map();
1628
- let defaultModelId;
1629
- let cursor;
1630
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
1631
- const result = record(await client.request('model/list', {
1632
- limit: CODEX_PAGE_LIMIT,
1633
- includeHidden: false,
1634
- ...(cursor === undefined ? {} : { cursor }),
1635
- }, signal));
1636
- for (const value of array(result.data)) {
1637
- const source = record(value);
1638
- if (source.hidden === true)
1639
- continue;
1640
- const id = string(source.model) ?? string(source.id);
1641
- if (id === undefined || models.has(id))
1642
- continue;
1643
- const efforts = array(source.supportedReasoningEfforts).map(value => {
1644
- const effort = record(value);
1645
- const effortId = string(effort.reasoningEffort);
1646
- if (effortId === undefined)
1647
- return undefined;
1648
- const description = string(effort.description);
1649
- return {
1650
- id: effortId,
1651
- name: reasoningEffortName(effortId),
1652
- ...(description === undefined ? {} : { description }),
1653
- };
1654
- }).filter((value) => value !== undefined);
1655
- const defaultEffort = string(source.defaultReasoningEffort);
1656
- const description = string(source.description);
1657
- const model = {
1658
- id,
1659
- name: string(source.displayName) ?? id,
1660
- ...(description === undefined ? {} : { description }),
1661
- ...(efforts.length === 0 ? {} : {
1662
- reasoning: {
1663
- efforts,
1664
- ...(defaultEffort === undefined || !efforts.some(effort => effort.id === defaultEffort)
1665
- ? {}
1666
- : { defaultEffort }),
1667
- },
1668
- }),
1669
- };
1670
- models.set(id, model);
1671
- if (source.isDefault === true)
1672
- defaultModelId = id;
1673
- }
1674
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
1675
- if (cursor === undefined)
1676
- break;
1677
- }
1678
- const defaultModel = models.get(defaultModelId ?? '') ?? models.values().next().value;
1679
- if (defaultModel === undefined)
1680
- throw new Error('CodeX did not advertise any available models.');
1681
- const defaultEffort = defaultModel.reasoning?.defaultEffort;
1682
- return {
1683
- default: {
1684
- provider: CODEX_PROVIDER,
1685
- model: defaultModel.id,
1686
- ...(defaultEffort === undefined ? {} : { reasoningEffort: defaultEffort }),
1687
- },
1688
- groups: [{ id: CODEX_PROVIDER, name: 'CodeX', models: [...models.values()] }],
1689
- models,
1690
- };
1691
- }
1692
- export function projectCodexNativeHistory(thread, sessionId, sessionGeneration = 'legacy') {
1693
- const entries = [];
1694
- let seq = 0;
1695
- let turnNumber = 0;
1696
- const active = activeTurnId(thread.turns);
1697
- const append = (type, data, time = Date.now(), view, sourceEventSeqs) => {
1698
- const eventSeq = seq++;
1699
- entries.push({
1700
- type: 'event',
1701
- event: {
1702
- type,
1703
- seq: eventSeq,
1704
- time,
1705
- data: adaptEventData(type, data, sessionGeneration),
1706
- ...(sourceEventSeqs === undefined ? {} : { sourceEventSeqs }),
1707
- ...(isSurfaceEvent(type) ? { surfaceOp: 'append' } : {}),
1708
- },
1709
- ...(view === undefined ? {} : { view }),
1710
- });
1711
- return eventSeq;
1712
- };
1713
- for (const rawTurn of array(thread.turns)) {
1714
- const turn = record(rawTurn);
1715
- const turnItems = array(turn.items).map(record);
1716
- const toolImages = turnItems.flatMap(toolOutputImageBlocks);
1717
- let imageOwner = -1;
1718
- for (let index = 0; index < turnItems.length; index += 1) {
1719
- if (turnItems[index].type === 'agentMessage')
1720
- imageOwner = index;
1721
- }
1722
- turnNumber += 1;
1723
- const turnActive = isActiveCodexTurn(turn);
1724
- const time = normalizeTime(turn.createdAt) || normalizeTime(turn.startedAt) || normalizeTime(thread.createdAt) || Date.now();
1725
- append('turn/start', { turn: turnNumber }, time);
1726
- append('step/start', { turn: turnNumber, step: 1 }, time);
1727
- for (let itemIndex = 0; itemIndex < turnItems.length; itemIndex += 1) {
1728
- const item = turnItems[itemIndex];
1729
- let toolCallSeq;
1730
- for (const event of itemEvents(item, turnNumber, 1, undefined, modelSelection(), sessionId, itemIndex === imageOwner ? toolImages : [])) {
1731
- const eventSeq = append(event.type, event.data, normalizeTime(item.createdAt) || time, event.view, event.type === 'tool/result' && toolCallSeq !== undefined ? [toolCallSeq] : undefined);
1732
- if (event.type === 'tool/call')
1733
- toolCallSeq = eventSeq;
1734
- }
1735
- }
1736
- if (turnActive)
1737
- continue;
1738
- append('step/end', { turn: turnNumber, step: 1 }, normalizeTime(turn.updatedAt) || normalizeTime(turn.completedAt) || time);
1739
- append('turn/end', {
1740
- turn: turnNumber,
1741
- reason: turn.status === 'failed' || turn.error !== undefined && turn.error !== null
1742
- ? { kind: 'error', error: { message: 'CodeX turn failed.', code: 'codex-turn-failed' } }
1743
- : { kind: 'completed' },
1744
- }, normalizeTime(turn.updatedAt) || normalizeTime(turn.completedAt) || time);
1745
- }
1746
- const projected = projectCodexThread(thread);
1747
- return {
1748
- header: {
1749
- version: sessionGeneration === 'v3' ? 3 : 1,
1750
- id: sessionId,
1751
- createdAt: projected?.createdAt ?? Date.now(),
1752
- ...(projected?.cwd === undefined ? {} : { cwd: projected.cwd }),
1753
- ...(sessionGeneration === 'v3' ? { isSeeded: false } : {}),
1754
- },
1755
- entries,
1756
- lastSeq: seq - 1,
1757
- nextTurn: turnNumber,
1758
- ...(active === undefined ? {} : { activeTurnId: active }),
1759
- };
1760
- }
1761
- export function paginateCodexNativeHistory(history, request) {
1762
- const throughSeq = Math.min(request.throughSeq ?? history.lastSeq, history.lastSeq);
1763
- const endSeq = Math.min(throughSeq, request.beforeSeq === undefined ? throughSeq : request.beforeSeq - 1);
1764
- const window = endSeq < 0 ? [] : history.entries.filter(entry => entry.event.seq <= endSeq);
1765
- const maxMessages = request.maxMessages ?? DEFAULT_HISTORY_MESSAGES;
1766
- let messages = 0;
1767
- let cut = 0;
1768
- for (let index = window.length - 1; index >= 0; index -= 1) {
1769
- const event = window[index].event;
1770
- if (!isSurfaceEvent(event.type) || event.surfaceOp !== 'append')
1771
- continue;
1772
- messages += 1;
1773
- if (messages >= maxMessages) {
1774
- cut = Math.min(event.seq, ...(event.sourceEventSeqs ?? []));
1775
- break;
1776
- }
1777
- }
1778
- return {
1779
- header: history.header,
1780
- cursor: history.lastSeq,
1781
- nextTurn: history.nextTurn,
1782
- records: window.filter(entry => entry.event.seq >= cut),
1783
- hasMore: window.some(entry => entry.event.seq < cut),
1784
- ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
1785
- };
1786
- }
1787
- function adaptEventData(type, data, sessionGeneration) {
1788
- if (sessionGeneration !== 'v3' || type !== 'assistant/message')
1789
- return data;
1790
- const value = record(data);
1791
- return Array.isArray(value.stream) ? value : { ...value, stream: [] };
1792
- }
1793
- function adaptCodexHistoryPage(value, sessionGeneration) {
1794
- if (sessionGeneration !== 'v3')
1795
- return value;
1796
- const page = record(value);
1797
- const sourceHeader = record(page.header);
1798
- const { seedLength: _seedLength, ...header } = sourceHeader;
1799
- const records = Array.isArray(page.records)
1800
- ? page.records.map(raw => {
1801
- const entry = record(raw);
1802
- const sourceEvent = record(entry.event);
1803
- const sourceSurfaceOp = record(sourceEvent.surfaceOp);
1804
- const surfaceOp = sourceEvent.surfaceOp === 'append'
1805
- ? 'append'
1806
- : sourceSurfaceOp.op === 'replace'
1807
- && integer(sourceSurfaceOp.startSeq) !== undefined
1808
- && integer(sourceSurfaceOp.endSeq) !== undefined
1809
- ? sourceSurfaceOp
1810
- : sourceSurfaceOp.op === 'replace'
1811
- && integer(sourceSurfaceOp.start) !== undefined
1812
- && integer(sourceSurfaceOp.end) !== undefined
1813
- ? {
1814
- op: 'replace',
1815
- startSeq: sourceSurfaceOp.start,
1816
- endSeq: sourceSurfaceOp.end,
1817
- }
1818
- : sourceEvent.surfaceOp;
1819
- return {
1820
- ...entry,
1821
- event: {
1822
- ...sourceEvent,
1823
- data: adaptEventData(string(sourceEvent.type) ?? '', sourceEvent.data, sessionGeneration),
1824
- ...(surfaceOp === undefined ? {} : { surfaceOp }),
1825
- },
1826
- };
1827
- })
1828
- : page.records;
1829
- return {
1830
- ...page,
1831
- header: { ...header, version: 3, isSeeded: sourceHeader.isSeeded === true },
1832
- records,
1833
- };
1834
- }
1835
- function itemEvents(item, turn, step, requestId, selection = modelSelection(), sessionId = CODEX_SESSION_PREFIX, supplementalImages = []) {
1836
- const type = string(item.type);
1837
- const id = string(item.id) ?? `${turn}:${step}:${hashString(JSON.stringify(item))}`;
1838
- const text = itemText(item);
1839
- if (type === 'userMessage')
1840
- return [{
1841
- type: 'user/message',
1842
- data: {
1843
- id,
1844
- role: 'user',
1845
- content: messageContent(item, `${sessionId}:${id}`, 'text', text),
1846
- source: requestId === undefined || requestId === '' ? { kind: 'user' } : { kind: 'user', rpcId: requestId },
1847
- },
1848
- }];
1849
- if (type === 'agentMessage' || type === 'reasoning' || type === 'plan')
1850
- return [{
1851
- type: 'assistant/message',
1852
- data: {
1853
- turn,
1854
- step,
1855
- message: {
1856
- id,
1857
- role: 'assistant',
1858
- content: messageContent(item, `${sessionId}:${id}`, type === 'reasoning' ? 'reasoning' : 'text', text, type === 'agentMessage' ? supplementalImages : []),
1859
- source: { kind: 'model', provider: selection.provider, model: selection.model },
1860
- },
1861
- },
1862
- }];
1863
- if (type !== undefined && isToolItemType(type)) {
1864
- const name = toolEventName(type);
1865
- const args = toolArguments(item);
1866
- return [
1867
- {
1868
- type: 'tool/call',
1869
- data: { turn, step, callId: id, name, arguments: JSON.stringify(args) },
1870
- view: { for: 'call', view: toolCallView(item, type, args) },
1871
- },
1872
- toolResultEvent(item, turn, step, id, toolResultText(item, type, text)),
1873
- ];
1874
- }
1875
- if (type === 'error')
1876
- return [{
1877
- type: 'assistant/message',
1878
- data: {
1879
- turn,
1880
- step,
1881
- message: {
1882
- id,
1883
- role: 'assistant',
1884
- content: [{ type: 'text', text: text ?? 'CodeX reported an error.' }],
1885
- source: { kind: 'model', provider: selection.provider, model: selection.model },
1886
- },
1887
- },
1888
- }];
1889
- return [];
1890
- }
1891
- function messageContent(item, attachmentSeed, fallbackType, fallbackText, supplementalImages = []) {
1892
- const source = item.content ?? item.input;
1893
- const blocks = contentBlocks(source, attachmentSeed);
1894
- const images = contentBlocks(supplementalImages, `${attachmentSeed}:tool-output`);
1895
- if (blocks.length > 0 || images.length > 0) {
1896
- return [...(blocks.length > 0 ? blocks : [{ type: fallbackType, text: fallbackText ?? '' }]), ...images];
1897
- }
1898
- return [{ type: fallbackType, text: fallbackText ?? '' }];
1899
- }
1900
- function toolResultEvent(item, turn, step, id, resultText) {
1901
- const type = string(item.type) ?? 'unknown';
1902
- return {
1903
- type: 'tool/result',
1904
- data: {
1905
- turn,
1906
- step,
1907
- message: {
1908
- id: `${id}:result`,
1909
- role: 'user',
1910
- content: [{
1911
- type: 'tool-result',
1912
- toolCallId: id,
1913
- content: [{ type: 'text', text: resultText }],
1914
- ...(item.status === 'failed' || item.success === false ? { isError: true } : {}),
1915
- }],
1916
- source: { kind: 'tool', callId: id },
1917
- },
1918
- },
1919
- view: { for: 'result', view: toolResultView(item, type, resultText) },
1920
- };
1921
- }
1922
- function toolResultContentReplacement(event) {
1923
- const data = record(event.data);
1924
- const message = record(data.message);
1925
- const content = array(message.content);
1926
- const result = record(content[0]);
1927
- if (result.isError === undefined)
1928
- return event;
1929
- const { isError: _isError, ...stableResult } = result;
1930
- return {
1931
- ...event,
1932
- data: {
1933
- ...data,
1934
- message: { ...message, content: [stableResult] },
1935
- },
1936
- };
1937
- }
1938
- function isToolItemType(type) {
1939
- return type === 'commandExecution'
1940
- || type === 'mcpToolCall'
1941
- || type === 'dynamicToolCall'
1942
- || type === 'fileChange'
1943
- || type === 'functionCallOutput'
1944
- || type === 'hookPrompt'
1945
- || type === 'collabAgentToolCall'
1946
- || type === 'subAgentActivity'
1947
- || type === 'webSearch'
1948
- || type === 'imageView'
1949
- || type === 'imageGeneration'
1950
- || type === 'sleep'
1951
- || type === 'enteredReviewMode'
1952
- || type === 'exitedReviewMode'
1953
- || type === 'contextCompaction';
1954
- }
1955
- function toolEventName(type) {
1956
- if (type === 'fileChange')
1957
- return 'codex.fileChange';
1958
- if (type === 'commandExecution')
1959
- return 'codex.command';
1960
- if (type === 'webSearch')
1961
- return 'codex.webSearch';
1962
- if (type === 'collabAgentToolCall' || type === 'subAgentActivity')
1963
- return 'codex.subagent';
1964
- if (type === 'imageView' || type === 'imageGeneration')
1965
- return 'codex.image';
1966
- if (type === 'enteredReviewMode' || type === 'exitedReviewMode')
1967
- return 'codex.reviewMode';
1968
- if (type === 'contextCompaction')
1969
- return 'codex.compaction';
1970
- return `codex.${type}`;
1971
- }
1972
- function toolResultText(item, type, text) {
1973
- if (type === 'fileChange')
1974
- return fileChangeSummary(item);
1975
- if (type === 'webSearch') {
1976
- const count = array(item.results).length;
1977
- return count === 0 ? 'Web search completed.' : `Web search returned ${count} result${count === 1 ? '' : 's'}.`;
1978
- }
1979
- if (type === 'imageView')
1980
- return `Viewed image: ${string(item.path) ?? 'image'}`;
1981
- if (type === 'imageGeneration') {
1982
- const savedPath = string(item.savedPath);
1983
- return savedPath === undefined ? `Image generation ${string(item.status) ?? 'completed'}.` : `Generated image: ${savedPath}`;
1984
- }
1985
- if (type === 'contextCompaction')
1986
- return 'CodeX compacted the conversation context.';
1987
- if (type === 'enteredReviewMode')
1988
- return `Entered review mode${string(item.review) ? `: ${string(item.review)}` : '.'}`;
1989
- if (type === 'exitedReviewMode')
1990
- return `Exited review mode${string(item.review) ? `: ${string(item.review)}` : '.'}`;
1991
- if (type === 'sleep')
1992
- return `Waited ${integer(item.durationMs) ?? 0} ms.`;
1993
- if (type === 'subAgentActivity') {
1994
- return [string(record(item.kind).type) ?? string(item.kind) ?? 'Subagent activity', string(item.agentPath)]
1995
- .filter((value) => value !== undefined).join(': ');
1996
- }
1997
- return text ?? itemText(record(item.result)) ?? itemText(record(item.output)) ?? string(item.status) ?? type;
1998
- }
1999
- function isSurfaceEvent(type) {
2000
- return type === 'user/message' || type === 'assistant/message' || type === 'tool/result';
2001
- }
2002
- function toolCallView(item, type, args) {
2003
- if (type === 'commandExecution') {
2004
- return {
2005
- card: 'terminal',
2006
- title: commandText(item.command) ?? 'CodeX command',
2007
- ...(typeof item.cwd === 'string' ? { cwd: item.cwd } : {}),
2008
- };
2009
- }
2010
- const locations = fileLocations(item);
2011
- if (type === 'fileChange') {
2012
- return {
2013
- card: 'generic',
2014
- title: locations.length === 0 ? 'CodeX file changes' : `Modify ${locations.length} file${locations.length === 1 ? '' : 's'}`,
2015
- kind: 'edit',
2016
- rawInput: args,
2017
- ...(locations.length === 0 ? {} : { locations }),
2018
- };
2019
- }
2020
- if (type === 'webSearch') {
2021
- return {
2022
- card: 'generic',
2023
- title: string(item.query) ?? 'CodeX web search',
2024
- kind: 'search',
2025
- rawInput: args,
2026
- };
2027
- }
2028
- if (type === 'imageView' || type === 'imageGeneration') {
2029
- const path = string(item.path) ?? string(item.savedPath);
2030
- return {
2031
- card: 'generic',
2032
- title: type === 'imageView' ? 'View image' : 'Generate image',
2033
- kind: type === 'imageView' ? 'read' : 'other',
2034
- ...(path === undefined ? {} : { locations: [{ path }] }),
2035
- };
2036
- }
2037
- if (type === 'collabAgentToolCall' || type === 'subAgentActivity') {
2038
- return {
2039
- card: 'generic',
2040
- title: type === 'collabAgentToolCall'
2041
- ? `Subagent: ${string(item.tool) ?? 'collaboration'}`
2042
- : `Subagent activity: ${string(item.agentPath) ?? string(item.agentThreadId) ?? 'agent'}`,
2043
- kind: 'other',
2044
- rawInput: args,
2045
- };
2046
- }
2047
- if (type === 'contextCompaction')
2048
- return { card: 'generic', title: 'Compact conversation context', kind: 'other' };
2049
- if (type === 'enteredReviewMode')
2050
- return { card: 'generic', title: 'Enter review mode', kind: 'other' };
2051
- if (type === 'exitedReviewMode')
2052
- return { card: 'generic', title: 'Exit review mode', kind: 'other' };
2053
- return {
2054
- card: 'generic',
2055
- title: toolDisplayName(item, type),
2056
- kind: 'other',
2057
- };
2058
- }
2059
- function toolResultView(item, type, resultText) {
2060
- if (type === 'commandExecution') {
2061
- return {
2062
- card: 'terminal',
2063
- output: resultText,
2064
- ...(typeof item.exitCode === 'number' && Number.isSafeInteger(item.exitCode) ? { exitCode: item.exitCode } : {}),
2065
- };
2066
- }
2067
- return {
2068
- card: 'generic',
2069
- ...(type === 'fileChange' ? { title: item.status === 'inProgress' ? 'CodeX file changes' : 'CodeX file changes completed' } : {}),
2070
- ...(type === 'mcpToolCall' && item.status === 'inProgress'
2071
- ? { title: 'CodeX MCP tool in progress', content: [{ type: 'text', text: resultText }] }
2072
- : {}),
2073
- };
2074
- }
2075
- function toolDisplayName(item, type) {
2076
- return string(item.name)
2077
- ?? string(item.tool)
2078
- ?? string(record(item.tool).name)
2079
- ?? (type === 'mcpToolCall' ? 'CodeX MCP tool' : humanizeItemType(type));
2080
- }
2081
- function fileLocations(item) {
2082
- return array(item.changes)
2083
- .map(value => string(record(value).path))
2084
- .filter((path) => path !== undefined && path.length > 0)
2085
- .map(path => ({ path }));
2086
- }
2087
- function nativeWorkspace(view) {
2088
- const { sessionCount: _sessionCount, ...workspace } = view;
2089
- return workspace;
2090
- }
2091
- function nativeVisibleWorkspaces(catalog, selectedWorkspaceId) {
2092
- return catalog.workspaces.filter(workspace => workspace.sessionIds.length > 0 || workspace.workspaceId === selectedWorkspaceId);
2093
- }
2094
- function projectWorkspace(project) {
2095
- const root = project.roots[0];
2096
- return {
2097
- workspaceId: codexProjectWorkspaceId(project.id),
2098
- path: root.path,
2099
- title: project.name.trim() || basename(root.path),
2100
- sessionIds: [],
2101
- sessionCount: 0,
2102
- createdAt: isoTime(project.createdAt),
2103
- updatedAt: isoTime(project.updatedAt || project.createdAt),
2104
- };
2105
- }
2106
- function projectCodexProject(value, fallbackPosition) {
2107
- const source = record(value);
2108
- const id = string(source.id);
2109
- if (id === undefined)
2110
- return undefined;
2111
- const roots = array(source.roots)
2112
- .map(root => string(record(root).path))
2113
- .filter((path) => path !== undefined && path.length > 0)
2114
- .map(path => ({ path }));
2115
- if (roots.length === 0)
2116
- return undefined;
2117
- return {
2118
- id,
2119
- name: string(source.name) ?? basename(roots[0].path),
2120
- roots,
2121
- position: finiteNumber(source.position) ?? fallbackPosition,
2122
- createdAt: normalizeTime(source.createdAt),
2123
- updatedAt: normalizeTime(source.updatedAt),
2124
- };
2125
- }
2126
- function findProjectWorkspace(thread, session, projects, workspaceByProjectId) {
2127
- const explicitProjectId = string(thread.projectId);
2128
- const explicitWorkspace = explicitProjectId === undefined ? undefined : workspaceByProjectId.get(explicitProjectId);
2129
- if (explicitWorkspace !== undefined)
2130
- return explicitWorkspace;
2131
- if (session.cwd === undefined)
2132
- return undefined;
2133
- const match = projects
2134
- .flatMap(project => project.roots.map(root => ({ project, root })))
2135
- .filter(value => containsPath(value.root.path, session.cwd))
2136
- .sort((left, right) => {
2137
- const length = right.root.path.length - left.root.path.length;
2138
- if (length !== 0)
2139
- return length;
2140
- const position = left.project.position - right.project.position;
2141
- if (position !== 0)
2142
- return position;
2143
- return left.project.id.localeCompare(right.project.id);
2144
- })[0];
2145
- return match === undefined ? undefined : workspaceByProjectId.get(match.project.id);
2146
- }
2147
- function addSessionToWorkspace(workspace, session) {
2148
- if (!workspace.sessionIds.includes(session.id)) {
2149
- workspace.sessionIds.push(session.id);
2150
- workspace.sessionCount = workspace.sessionIds.length;
2151
- }
2152
- const createdAt = isoTime(session.createdAt);
2153
- const updatedAt = isoTime(session.updatedAt || session.createdAt);
2154
- if (createdAt < workspace.createdAt)
2155
- workspace.createdAt = createdAt;
2156
- if (updatedAt > workspace.updatedAt)
2157
- workspace.updatedAt = updatedAt;
2158
- }
2159
- function containsPath(root, candidate) {
2160
- const normalizedRoot = normalizePathForCompare(root);
2161
- const normalizedCandidate = normalizePathForCompare(candidate);
2162
- return normalizedCandidate === normalizedRoot
2163
- || normalizedCandidate.startsWith(`${normalizedRoot}/`)
2164
- || normalizedCandidate.startsWith(`${normalizedRoot}\\`);
2165
- }
2166
- function normalizePathForCompare(path) {
2167
- return path.replace(/[\\/]+$/u, '') || path;
2168
- }
2169
- function modelSelection() {
2170
- return { provider: CODEX_PROVIDER, model: CODEX_MODEL };
2171
- }
2172
- function codexPromptInput(content) {
2173
- if (content.length === 0 || content.length > MAX_CODEX_PROMPT_PARTS)
2174
- return undefined;
2175
- const input = [];
2176
- for (const value of content) {
2177
- if (!isRecord(value))
2178
- return undefined;
2179
- if (value.type === 'text') {
2180
- if (typeof value.text !== 'string' || value.text.length === 0 || value.text.length > MAX_CODEX_PROMPT_TEXT)
2181
- return undefined;
2182
- input.push({ type: 'text', text: value.text });
2183
- continue;
2184
- }
2185
- if (value.type === 'image') {
2186
- if (typeof value.mediaType !== 'string' || !CODEX_IMAGE_MEDIA_TYPES.has(value.mediaType)
2187
- || typeof value.data !== 'string' || value.data.length > MAX_CODEX_IMAGE_BASE64
2188
- || !isCanonicalBase64(value.data))
2189
- return undefined;
2190
- input.push({ type: 'image', mediaType: value.mediaType, data: value.data });
2191
- continue;
2192
- }
2193
- return undefined;
2194
- }
2195
- return input;
2196
- }
2197
- function modelSelectionProjection(selection) {
2198
- return { lastUsed: selection, next: selection };
2199
- }
2200
- function codexPermissionsProjection(preset) {
2201
- return {
2202
- options: [
2203
- {
2204
- value: 'workspace-write',
2205
- name: 'workspace-write',
2206
- description: 'Write inside the workspace; wider command and file access requires one-time approval.',
2207
- },
2208
- {
2209
- value: 'danger-full-access',
2210
- name: 'danger-full-access',
2211
- description: 'Full Host file access without approval prompts for subsequent turns in this virtual Session.',
2212
- },
2213
- ],
2214
- currentValue: preset ?? 'Host settings (not reported)',
2215
- };
2216
- }
2217
- function codexImageLimitsProjection() {
2218
- return {
2219
- maxImageBytes: 20 * 1024 * 1024,
2220
- maxImagesPerMessage: MAX_CODEX_PROMPT_PARTS,
2221
- maxMessageImageBytes: 100 * 1024 * 1024,
2222
- maxImagePixels: 40_000_000,
2223
- maxImageDimension: 8_192,
2224
- mediaTypes: [...CODEX_IMAGE_MEDIA_TYPES],
2225
- };
2226
- }
2227
- function isCodexPermissionPreset(value) {
2228
- return value === 'workspace-write' || value === 'danger-full-access';
2229
- }
2230
- function modelCatalog(directory) {
2231
- return {
2232
- default: directory.default,
2233
- routableProviders: [CODEX_PROVIDER],
2234
- groups: directory.groups,
2235
- failures: [],
2236
- };
2237
- }
2238
- function codexModelParams(selection) {
2239
- return {
2240
- model: selection.model,
2241
- ...(selection.reasoningEffort === undefined ? {} : { effort: selection.reasoningEffort }),
2242
- };
2243
- }
2244
- function reasoningEffortName(effort) {
2245
- const names = {
2246
- none: 'None',
2247
- minimal: 'Minimal',
2248
- low: 'Low',
2249
- medium: 'Medium',
2250
- high: 'High',
2251
- xhigh: 'Extra High',
2252
- max: 'Max',
2253
- ultra: 'Ultra',
2254
- };
2255
- return names[effort] ?? effort;
2256
- }
2257
- function displayTitle(session) {
2258
- const value = session.title ?? session.preview;
2259
- return value === undefined || value.trim() === '' ? null : value.slice(0, 256);
2260
- }
2261
- function itemText(value) {
2262
- if (typeof value.text === 'string')
2263
- return value.text;
2264
- if (typeof value.content === 'string')
2265
- return value.content;
2266
- const content = Array.isArray(value.content)
2267
- ? value.content
2268
- : Array.isArray(value.input) ? value.input : undefined;
2269
- if (content !== undefined) {
2270
- const text = content.map(part => {
2271
- if (typeof part === 'string')
2272
- return part;
2273
- const block = record(part);
2274
- return string(block.text) ?? string(block.content) ?? '';
2275
- }).filter(Boolean).join('\n');
2276
- if (text !== '')
2277
- return text;
2278
- }
2279
- if (Array.isArray(value.summary))
2280
- return value.summary.filter(item => typeof item === 'string').join('\n');
2281
- if (typeof value.output === 'string')
2282
- return value.output;
2283
- if (typeof value.aggregatedOutput === 'string')
2284
- return value.aggregatedOutput;
2285
- return undefined;
2286
- }
2287
- function toolArguments(item) {
2288
- if (item.type === 'commandExecution')
2289
- return { command: commandText(item.command) ?? item.command ?? '' };
2290
- if (item.type === 'fileChange')
2291
- return {
2292
- changes: array(item.changes).map(value => {
2293
- const change = record(value);
2294
- const kind = typeof change.kind === 'string' ? change.kind : string(record(change.kind).type);
2295
- return {
2296
- ...(typeof change.path === 'string' ? { path: change.path } : {}),
2297
- ...(kind === undefined ? {} : { kind }),
2298
- };
2299
- }),
2300
- };
2301
- if (item.type === 'webSearch')
2302
- return { query: string(item.query) ?? '' };
2303
- if (item.type === 'imageView')
2304
- return { path: string(item.path) ?? '' };
2305
- if (item.type === 'imageGeneration')
2306
- return {
2307
- ...(string(item.revisedPrompt) === undefined ? {} : { prompt: string(item.revisedPrompt) }),
2308
- transparentBackground: item.transparentBackground === true,
2309
- };
2310
- if (item.type === 'collabAgentToolCall')
2311
- return {
2312
- tool: string(item.tool) ?? 'collaboration',
2313
- receiverThreadIds: array(item.receiverThreadIds).filter(value => typeof value === 'string'),
2314
- ...(string(item.model) === undefined ? {} : { model: string(item.model) }),
2315
- };
2316
- if (item.type === 'subAgentActivity')
2317
- return {
2318
- agentThreadId: string(item.agentThreadId) ?? '',
2319
- agentPath: string(item.agentPath) ?? '',
2320
- kind: string(record(item.kind).type) ?? string(item.kind) ?? 'activity',
2321
- };
2322
- if (item.type === 'sleep')
2323
- return { durationMs: integer(item.durationMs) ?? 0 };
2324
- if (item.type === 'enteredReviewMode' || item.type === 'exitedReviewMode')
2325
- return {
2326
- review: string(item.review) ?? '',
2327
- };
2328
- if (item.type === 'contextCompaction')
2329
- return {};
2330
- return { name: item.name ?? item.tool ?? item.type ?? 'tool', arguments: item.arguments ?? item.input ?? {} };
2331
- }
2332
- function fileChangeSummary(item) {
2333
- const changes = array(item.changes).map(value => {
2334
- const change = record(value);
2335
- const path = string(change.path);
2336
- const kind = typeof change.kind === 'string' ? change.kind : string(record(change.kind).type);
2337
- return [kind, path].filter((part) => part !== undefined && part.length > 0).join(' ');
2338
- }).filter(Boolean);
2339
- return changes.length === 0 ? 'File changes completed.' : changes.join('\n');
2340
- }
2341
- function sanitizedFileChanges(value) {
2342
- return array(value).flatMap(raw => {
2343
- const change = record(raw);
2344
- const path = string(change.path);
2345
- if (path === undefined)
2346
- return [];
2347
- const kind = typeof change.kind === 'string' ? change.kind : string(record(change.kind).type);
2348
- return [{ path, ...(kind === undefined ? {} : { kind }) }];
2349
- });
2350
- }
2351
- function appendBoundedText(current, delta) {
2352
- const value = `${current}${delta}`;
2353
- if (value.length <= MAX_LIVE_TOOL_OUTPUT)
2354
- return value;
2355
- return `… earlier output omitted …\n${value.slice(value.length - MAX_LIVE_TOOL_OUTPUT)}`;
2356
- }
2357
- function humanizeItemType(type) {
2358
- const names = {
2359
- functionCallOutput: 'CodeX function output',
2360
- hookPrompt: 'CodeX hook prompt',
2361
- dynamicToolCall: 'CodeX tool',
2362
- collabAgentToolCall: 'CodeX subagent',
2363
- subAgentActivity: 'CodeX subagent activity',
2364
- webSearch: 'CodeX web search',
2365
- imageView: 'CodeX image view',
2366
- imageGeneration: 'CodeX image generation',
2367
- sleep: 'CodeX wait',
2368
- enteredReviewMode: 'CodeX review mode',
2369
- exitedReviewMode: 'CodeX review mode',
2370
- contextCompaction: 'CodeX context compaction',
2371
- };
2372
- return names[type] ?? 'CodeX tool';
2373
- }
2374
- function commandText(value) {
2375
- if (typeof value === 'string')
2376
- return value;
2377
- if (Array.isArray(value))
2378
- return value.filter(item => typeof item === 'string').join(' ');
2379
- const source = record(value);
2380
- return string(source.command) ?? string(source.text);
2381
- }
2382
- function sessionIdFromAddress(address) {
2383
- if (address.kind === 'session')
2384
- return requiredString(address.sessionId, 'sessionId');
2385
- return requiredString(address.childSessionId, 'childSessionId');
2386
- }
2387
- function nativeThreadId(sessionId) {
2388
- if (!sessionId.startsWith(CODEX_SESSION_PREFIX) || sessionId.length === CODEX_SESSION_PREFIX.length) {
2389
- throw new Error('The selected Session does not belong to CodeX.');
2390
- }
2391
- return sessionId.slice(CODEX_SESSION_PREFIX.length);
2392
- }
2393
- function activeTurnId(value) {
2394
- if (!Array.isArray(value))
2395
- return undefined;
2396
- for (let index = value.length - 1; index >= 0; index -= 1) {
2397
- const turn = record(value[index]);
2398
- if (isActiveCodexTurn(turn) && typeof turn.id === 'string' && turn.id.length > 0)
2399
- return turn.id;
2400
- }
2401
- return undefined;
2402
- }
2403
- function isActiveCodexTurn(turn) {
2404
- return turn.status === 'inProgress' || turn.status === 'running';
2405
- }
2406
- function carrierArgs(payload) {
2407
- return record(record(payload).args);
2408
- }
2409
- function requestArg(args) {
2410
- return record(args.request ?? args._request ?? args);
2411
- }
2412
- function rcEndpoint(endpoint) {
2413
- if (endpoint === 'workspace.list')
2414
- return 'workspace/list';
2415
- return endpoint.replace('.', '/');
2416
- }
2417
- function success(value) {
2418
- return { ok: true, value };
2419
- }
2420
- function failure(code, message, details = {}) {
2421
- return { ok: false, error: { code, message, details } };
2422
- }
2423
- function ok(value) {
2424
- return value === undefined ? { ok: true } : { ok: true, value };
2425
- }
2426
- function business(value) {
2427
- const result = record(value);
2428
- if (result.ok === true)
2429
- return ok(result.value);
2430
- if (result.ok === false) {
2431
- const error = record(result.error);
2432
- return fail(string(error.code) ?? 'internal', string(error.message) ?? 'CodeX virtual Harness rejected the request.', record(error.details));
2433
- }
2434
- return ok(value);
2435
- }
2436
- function isCodexDirectoryListing(value) {
2437
- const listing = record(value);
2438
- return typeof listing.path === 'string'
2439
- && typeof listing.home === 'string'
2440
- && Array.isArray(listing.crumbs)
2441
- && listing.crumbs.every(isCodexDirectoryEntry)
2442
- && Array.isArray(listing.entries)
2443
- && listing.entries.every(isCodexDirectoryEntry)
2444
- && typeof listing.truncated === 'boolean';
2445
- }
2446
- function isCodexDirectoryEntry(value) {
2447
- const entry = record(value);
2448
- return typeof entry.name === 'string'
2449
- && typeof entry.path === 'string'
2450
- && typeof entry.hidden === 'boolean';
2451
- }
2452
- function fail(code, message, details = {}) {
2453
- return { ok: false, error: { code, message, details } };
2454
- }
2455
- function failFrom(error) {
2456
- const source = error instanceof Error ? error : new Error(String(error));
2457
- return fail(errorCode(source) ?? 'internal', source.message, errorDetails(source));
2458
- }
2459
- function isLegacySessionHistoryUnsupported(error) {
2460
- if (!(error instanceof Error))
2461
- return false;
2462
- return errorCode(error) === 'METHOD_NOT_ALLOWED'
2463
- || errorCode(error) === 'METHOD_NOT_FOUND'
2464
- || error.message.includes('The requested Codex method is not available over Remote.');
2465
- }
2466
- function errorCode(error) {
2467
- return 'code' in error && typeof error.code === 'string' ? error.code : undefined;
2468
- }
2469
- function errorDetails(error) {
2470
- return 'details' in error && isRecord(error.details) ? error.details : {};
2471
- }
2472
- function record(value) {
2473
- return isRecord(value) ? value : {};
2474
- }
2475
- function isRecord(value) {
2476
- return typeof value === 'object' && value !== null && !Array.isArray(value);
2477
- }
2478
- function array(value) {
2479
- return Array.isArray(value) ? value : [];
2480
- }
2481
- function string(value) {
2482
- return typeof value === 'string' ? value : undefined;
2483
- }
2484
- function integer(value) {
2485
- return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
2486
- }
2487
- function finiteNumber(value) {
2488
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
2489
- }
2490
- function optionalInteger(value) {
2491
- if (value === undefined)
2492
- return undefined;
2493
- const parsed = integer(value);
2494
- if (parsed === undefined)
2495
- throw new Error('The CodeX History cursor is invalid.');
2496
- return parsed;
2497
- }
2498
- function optionalNonNegativeInteger(value) {
2499
- const parsed = optionalInteger(value);
2500
- if (parsed !== undefined && parsed < 0)
2501
- throw new Error('The CodeX History cursor is invalid.');
2502
- return parsed;
2503
- }
2504
- function optionalPositiveInteger(value) {
2505
- const parsed = optionalInteger(value);
2506
- if (parsed !== undefined && parsed <= 0)
2507
- throw new Error('The CodeX History page size is invalid.');
2508
- return parsed;
2509
- }
2510
- function isCodexNativeHistoryPage(value, sessionId) {
2511
- const page = record(value);
2512
- const header = record(page.header);
2513
- return header.id === sessionId
2514
- && integer(page.cursor) !== undefined
2515
- && integer(page.nextTurn) !== undefined
2516
- && Array.isArray(page.records)
2517
- && typeof page.hasMore === 'boolean'
2518
- && (page.activeTurnId === undefined || typeof page.activeTurnId === 'string');
2519
- }
2520
- function truncateCodePoints(value, maximum) {
2521
- return [...value].slice(0, maximum).join('');
2522
- }
2523
- function requiredString(value, field) {
2524
- if (typeof value !== 'string' || value.length === 0)
2525
- throw new Error(`The CodeX ${field} is required.`);
2526
- return value;
2527
- }
2528
- function normalizeTime(value) {
2529
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
2530
- return 0;
2531
- return value < 10_000_000_000 ? Math.floor(value * 1000) : Math.floor(value);
2532
- }
2533
- function isoTime(value) {
2534
- return new Date(normalizeTime(value) || Date.now()).toISOString();
2535
- }
2536
- function isProjectListUnsupported(error) {
2537
- if (!(error instanceof Error))
2538
- return false;
2539
- const code = errorCode(error);
2540
- return code === 'METHOD_NOT_ALLOWED'
2541
- || code === 'METHOD_NOT_FOUND'
2542
- || code === 'CODEX_UPSTREAM_ERROR'
2543
- || error.message.includes('The requested Codex method is not available over Remote.');
2544
- }
2545
- function basename(path) {
2546
- const normalized = path.replace(/[\\/]+$/u, '');
2547
- const parts = normalized.split(/[\\/]/u);
2548
- return parts.at(-1) || path;
2549
- }
2550
- function hashString(value) {
2551
- let hash = 0x811c9dc5;
2552
- for (let index = 0; index < value.length; index += 1) {
2553
- hash ^= value.charCodeAt(index);
2554
- hash = Math.imul(hash, 0x01000193);
2555
- }
2556
- return (hash >>> 0).toString(36);
2557
- }
2558
- class AsyncValueQueue {
2559
- signal;
2560
- values = [];
2561
- waiters = [];
2562
- closed = false;
2563
- onAbort;
2564
- constructor(signal) {
2565
- this.signal = signal;
2566
- this.onAbort = () => this.close();
2567
- signal.addEventListener('abort', this.onAbort, { once: true });
2568
- if (signal.aborted)
2569
- this.close();
2570
- }
2571
- push(value) {
2572
- if (this.closed)
2573
- return;
2574
- const waiter = this.waiters.shift();
2575
- if (waiter === undefined)
2576
- this.values.push(value);
2577
- else
2578
- waiter({ done: false, value });
2579
- }
2580
- close() {
2581
- if (this.closed)
2582
- return;
2583
- this.closed = true;
2584
- this.signal.removeEventListener('abort', this.onAbort);
2585
- for (const waiter of this.waiters.splice(0))
2586
- waiter({ done: true, value: undefined });
2587
- }
2588
- iterate(dispose) {
2589
- const queue = this;
2590
- return {
2591
- async *[Symbol.asyncIterator]() {
2592
- try {
2593
- yield* queue;
2594
- }
2595
- finally {
2596
- dispose();
2597
- queue.close();
2598
- }
2599
- },
2600
- };
2601
- }
2602
- async *[Symbol.asyncIterator]() {
2603
- while (true) {
2604
- if (this.values.length > 0) {
2605
- yield this.values.shift();
2606
- continue;
2607
- }
2608
- if (this.closed)
2609
- return;
2610
- const next = await new Promise(resolve => this.waiters.push(resolve));
2611
- if (next.done)
2612
- return;
2613
- yield next.value;
2614
- }
2615
- }
2616
- }
2617
- /**
2618
- * Restore bytes the codeX-facing carriers answer as base64.
2619
- *
2620
- * The remote bridge prefers the CodeX workspace projection for a CodeX session, and that projection
2621
- * answers \`workspaceFiles/readBytes\` with base64 for its own consumers. The native UI validates the
2622
- * same result against a generated schema that requires a real \`Uint8Array\`, so the base64 string fails
2623
- * there with \`expected "Uint8Array", path: ["data"]\` and the image never renders. Normalising here -
2624
- * the seam that faces the native UI - leaves every other consumer on its own contract.
2625
- *
2626
- * @param endpoint - endpoint the result belongs to.
2627
- * @param result - the carrier's result.
2628
- * @returns the result with byte-valued fields restored, and a shape warning when they cannot be.
2629
- */
2630
- function normalizeByteResult(endpoint, result) {
2631
- if (endpoint !== 'workspaceFiles/readBytes' || !result.ok)
2632
- return result;
2633
- const value = result.value;
2634
- if (typeof value !== 'object' || value === null || Array.isArray(value))
2635
- return result;
2636
- const data = value.data;
2637
- if (data instanceof Uint8Array)
2638
- return result;
2639
- const bytes = decodeByteValue(data);
2640
- if (bytes !== undefined) {
2641
- return { ...result, value: { ...value, data: bytes } };
2642
- }
2643
- // Shapes only; never the content itself.
2644
- console.warn('[dsh-remote] workspaceFiles/readBytes arrived without usable bytes', {
2645
- dataType: typeof data,
2646
- dataKeys: typeof data === 'object' && data !== null && !Array.isArray(data) ? Object.keys(data).length : 0,
2647
- });
2648
- return result;
2649
- }
2650
- //# sourceMappingURL=virtual-harness.js.map