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