ds-harness-remote 0.5.8 → 0.5.11

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