@nordbyte/nordrelay 0.2.1
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/.env.example +88 -0
- package/Dockerfile +19 -0
- package/LICENSE +21 -0
- package/README.md +749 -0
- package/dist/access-control.js +146 -0
- package/dist/agent-factory.js +22 -0
- package/dist/agent.js +57 -0
- package/dist/artifacts.js +515 -0
- package/dist/attachments.js +69 -0
- package/dist/bot-preferences.js +146 -0
- package/dist/bot-ui.js +161 -0
- package/dist/bot.js +4520 -0
- package/dist/codex-auth.js +150 -0
- package/dist/codex-cli.js +79 -0
- package/dist/codex-config.js +50 -0
- package/dist/codex-launch.js +109 -0
- package/dist/codex-session.js +591 -0
- package/dist/codex-state.js +573 -0
- package/dist/config.js +385 -0
- package/dist/context-key.js +23 -0
- package/dist/error-messages.js +73 -0
- package/dist/format.js +121 -0
- package/dist/index.js +140 -0
- package/dist/logger.js +27 -0
- package/dist/operations.js +133 -0
- package/dist/persistence.js +65 -0
- package/dist/pi-cli.js +19 -0
- package/dist/pi-rpc.js +158 -0
- package/dist/pi-session.js +573 -0
- package/dist/pi-state.js +226 -0
- package/dist/prompt-store.js +241 -0
- package/dist/redaction.js +47 -0
- package/dist/session-format.js +191 -0
- package/dist/session-registry.js +195 -0
- package/dist/telegram-rate-limit.js +136 -0
- package/dist/voice.js +373 -0
- package/dist/workspace-policy.js +41 -0
- package/docker-compose.yml +17 -0
- package/launchd/start.sh +8 -0
- package/package.json +69 -0
- package/plugins/nordrelay/.codex-plugin/plugin.json +48 -0
- package/plugins/nordrelay/assets/nordrelay.svg +5 -0
- package/plugins/nordrelay/commands/remote.md +33 -0
- package/plugins/nordrelay/scripts/nordrelay.mjs +396 -0
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +26 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { Codex, } from "@openai/codex-sdk";
|
|
2
|
+
import { resolveCodexCli } from "./codex-cli.js";
|
|
3
|
+
import { readCodexFastMode, writeCodexFastMode } from "./codex-config.js";
|
|
4
|
+
import { getThread, getThreadUsage, listModels, listThreads, listWorkspaces, } from "./codex-state.js";
|
|
5
|
+
import { createLaunchProfile, findLaunchProfile, formatLaunchProfileBehavior, } from "./codex-launch.js";
|
|
6
|
+
import { CODEX_AGENT_CAPABILITIES, } from "./agent.js";
|
|
7
|
+
export class CodexSessionService {
|
|
8
|
+
config;
|
|
9
|
+
codex = null;
|
|
10
|
+
thread = null;
|
|
11
|
+
currentWorkspace;
|
|
12
|
+
abortController = null;
|
|
13
|
+
currentThreadId = null;
|
|
14
|
+
currentModel;
|
|
15
|
+
currentReasoningEffort;
|
|
16
|
+
currentLaunchProfile;
|
|
17
|
+
activeThreadLaunchProfile = null;
|
|
18
|
+
sessionTokens = { input: 0, cached: 0, output: 0 };
|
|
19
|
+
lastObservedFastMode = null;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.currentWorkspace = config.workspace;
|
|
23
|
+
this.currentLaunchProfile = getLaunchProfile(config, config.defaultLaunchProfileId);
|
|
24
|
+
}
|
|
25
|
+
static async create(config, options) {
|
|
26
|
+
const service = new CodexSessionService(config);
|
|
27
|
+
service.currentWorkspace = options?.workspace ?? config.workspace;
|
|
28
|
+
service.currentModel = options?.model ?? config.codexModel;
|
|
29
|
+
service.currentReasoningEffort = options?.reasoningEffort;
|
|
30
|
+
service.currentLaunchProfile = getLaunchProfile(config, options?.launchProfileId ?? config.defaultLaunchProfileId);
|
|
31
|
+
service.resetCodexClient();
|
|
32
|
+
if (options?.resumeThreadId) {
|
|
33
|
+
await service.resumeThread(options.resumeThreadId);
|
|
34
|
+
return service;
|
|
35
|
+
}
|
|
36
|
+
if (options?.deferThreadStart) {
|
|
37
|
+
return service;
|
|
38
|
+
}
|
|
39
|
+
await service.newThread(service.currentWorkspace, service.currentModel);
|
|
40
|
+
return service;
|
|
41
|
+
}
|
|
42
|
+
getInfo() {
|
|
43
|
+
const activeThreadId = this.thread?.id ?? this.currentThreadId;
|
|
44
|
+
if (activeThreadId && !this.abortController) {
|
|
45
|
+
this.refreshActiveThreadMetadata(activeThreadId);
|
|
46
|
+
}
|
|
47
|
+
const effectiveLaunchProfile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
|
|
48
|
+
const codexFastMode = readCodexFastMode();
|
|
49
|
+
this.lastObservedFastMode = codexFastMode;
|
|
50
|
+
const info = {
|
|
51
|
+
agentId: "codex",
|
|
52
|
+
agentLabel: "Codex",
|
|
53
|
+
threadId: activeThreadId,
|
|
54
|
+
workspace: this.currentWorkspace,
|
|
55
|
+
model: this.currentModel ?? this.config.codexModel,
|
|
56
|
+
launchProfileId: effectiveLaunchProfile.id,
|
|
57
|
+
launchProfileLabel: effectiveLaunchProfile.label,
|
|
58
|
+
launchProfileBehavior: formatLaunchProfileBehavior(effectiveLaunchProfile),
|
|
59
|
+
sandboxMode: effectiveLaunchProfile.sandboxMode,
|
|
60
|
+
approvalPolicy: effectiveLaunchProfile.approvalPolicy,
|
|
61
|
+
fastMode: codexFastMode ?? (effectiveLaunchProfile.approvalPolicy === "never"),
|
|
62
|
+
unsafeLaunch: effectiveLaunchProfile.unsafe,
|
|
63
|
+
capabilities: CODEX_AGENT_CAPABILITIES,
|
|
64
|
+
};
|
|
65
|
+
Object.defineProperties(info, {
|
|
66
|
+
agentId: { value: "codex", enumerable: false },
|
|
67
|
+
agentLabel: { value: "Codex", enumerable: false },
|
|
68
|
+
capabilities: { value: CODEX_AGENT_CAPABILITIES, enumerable: false },
|
|
69
|
+
});
|
|
70
|
+
if (this.currentReasoningEffort) {
|
|
71
|
+
info.reasoningEffort = this.currentReasoningEffort;
|
|
72
|
+
}
|
|
73
|
+
if (this.activeThreadLaunchProfile &&
|
|
74
|
+
this.activeThreadLaunchProfile.id !== this.currentLaunchProfile.id) {
|
|
75
|
+
info.nextLaunchProfileId = this.currentLaunchProfile.id;
|
|
76
|
+
info.nextLaunchProfileLabel = this.currentLaunchProfile.label;
|
|
77
|
+
info.nextLaunchProfileBehavior = formatLaunchProfileBehavior(this.currentLaunchProfile);
|
|
78
|
+
info.nextUnsafeLaunch = this.currentLaunchProfile.unsafe;
|
|
79
|
+
}
|
|
80
|
+
if (this.sessionTokens.input > 0 || this.sessionTokens.cached > 0 || this.sessionTokens.output > 0) {
|
|
81
|
+
info.sessionTokens = { ...this.sessionTokens };
|
|
82
|
+
}
|
|
83
|
+
const threadId = info.threadId;
|
|
84
|
+
if (threadId) {
|
|
85
|
+
const codexUsage = getThreadUsage(threadId);
|
|
86
|
+
if (codexUsage) {
|
|
87
|
+
info.codexUsage = codexUsage;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return info;
|
|
91
|
+
}
|
|
92
|
+
isProcessing() {
|
|
93
|
+
return this.abortController !== null;
|
|
94
|
+
}
|
|
95
|
+
getActiveThreadId() {
|
|
96
|
+
return this.thread?.id ?? this.currentThreadId;
|
|
97
|
+
}
|
|
98
|
+
hasActiveThread() {
|
|
99
|
+
return this.thread !== null;
|
|
100
|
+
}
|
|
101
|
+
getCurrentWorkspace() {
|
|
102
|
+
return this.currentWorkspace;
|
|
103
|
+
}
|
|
104
|
+
async prompt(input, callbacks) {
|
|
105
|
+
if (!this.thread) {
|
|
106
|
+
throw new Error("Codex thread is not initialized");
|
|
107
|
+
}
|
|
108
|
+
if (this.abortController) {
|
|
109
|
+
throw new Error("A Codex turn is already in progress");
|
|
110
|
+
}
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
this.abortController = controller;
|
|
113
|
+
let lastAgentText = "";
|
|
114
|
+
// Track cumulative aggregated_output per command item to compute deltas.
|
|
115
|
+
const lastCommandOutput = new Map();
|
|
116
|
+
try {
|
|
117
|
+
const { events } = await this.thread.runStreamed(this.buildSdkInput(input), { signal: controller.signal });
|
|
118
|
+
for await (const event of events) {
|
|
119
|
+
this.handleThreadEvent(event);
|
|
120
|
+
switch (event.type) {
|
|
121
|
+
case "item.started":
|
|
122
|
+
case "item.updated": {
|
|
123
|
+
const item = event.item;
|
|
124
|
+
if (item.type === "agent_message") {
|
|
125
|
+
const delta = computeTextDelta(lastAgentText, item.text);
|
|
126
|
+
if (delta) {
|
|
127
|
+
lastAgentText = item.text;
|
|
128
|
+
callbacks.onTextDelta(delta);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lastAgentText = item.text;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (item.type === "command_execution") {
|
|
135
|
+
if (event.type === "item.started") {
|
|
136
|
+
// Record baseline so the first item.updated delta is computed correctly.
|
|
137
|
+
lastCommandOutput.set(item.id, item.aggregated_output);
|
|
138
|
+
callbacks.onToolStart(item.command, item.id);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// aggregated_output grows monotonically; pass only the new portion.
|
|
142
|
+
const prev = lastCommandOutput.get(item.id) ?? "";
|
|
143
|
+
const delta = computeTextDelta(prev, item.aggregated_output);
|
|
144
|
+
lastCommandOutput.set(item.id, item.aggregated_output);
|
|
145
|
+
if (delta) {
|
|
146
|
+
callbacks.onToolUpdate(item.id, delta);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (item.type === "web_search") {
|
|
151
|
+
if (event.type === "item.started") {
|
|
152
|
+
const label = truncate(item.query, 60);
|
|
153
|
+
callbacks.onToolStart(`🔍 ${label}`, item.id);
|
|
154
|
+
callbacks.onToolUpdate(item.id, item.query);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (item.type === "todo_list") {
|
|
158
|
+
callbacks.onTodoUpdate?.(item.items);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "item.completed": {
|
|
163
|
+
const item = event.item;
|
|
164
|
+
if (item.type === "agent_message") {
|
|
165
|
+
const delta = computeTextDelta(lastAgentText, item.text);
|
|
166
|
+
if (delta) {
|
|
167
|
+
callbacks.onTextDelta(delta);
|
|
168
|
+
}
|
|
169
|
+
lastAgentText = item.text;
|
|
170
|
+
}
|
|
171
|
+
else if (item.type === "command_execution") {
|
|
172
|
+
// Pass any output that arrived only in the completion event (e.g. fast
|
|
173
|
+
// commands that never fired item.updated).
|
|
174
|
+
const prev = lastCommandOutput.get(item.id) ?? "";
|
|
175
|
+
const delta = computeTextDelta(prev, item.aggregated_output);
|
|
176
|
+
if (delta) {
|
|
177
|
+
callbacks.onToolUpdate(item.id, delta);
|
|
178
|
+
}
|
|
179
|
+
callbacks.onToolEnd(item.id, item.status === "failed");
|
|
180
|
+
}
|
|
181
|
+
else if (item.type === "file_change") {
|
|
182
|
+
const toolId = item.id;
|
|
183
|
+
const summary = item.changes.map((change) => `${change.kind} ${change.path}`).join(", ");
|
|
184
|
+
callbacks.onToolStart("file_change", toolId);
|
|
185
|
+
callbacks.onToolUpdate(toolId, summary);
|
|
186
|
+
callbacks.onToolEnd(toolId, item.status === "failed");
|
|
187
|
+
}
|
|
188
|
+
else if (item.type === "mcp_tool_call") {
|
|
189
|
+
callbacks.onToolStart(`mcp:${item.server}/${item.tool}`, item.id);
|
|
190
|
+
if (item.error) {
|
|
191
|
+
callbacks.onToolUpdate(item.id, item.error.message);
|
|
192
|
+
}
|
|
193
|
+
callbacks.onToolEnd(item.id, item.status === "failed");
|
|
194
|
+
}
|
|
195
|
+
else if (item.type === "web_search") {
|
|
196
|
+
callbacks.onToolEnd(item.id, false);
|
|
197
|
+
}
|
|
198
|
+
else if (item.type === "error") {
|
|
199
|
+
callbacks.onToolStart("⚠️ error", item.id);
|
|
200
|
+
callbacks.onToolUpdate(item.id, item.message);
|
|
201
|
+
callbacks.onToolEnd(item.id, true);
|
|
202
|
+
}
|
|
203
|
+
else if (item.type === "todo_list") {
|
|
204
|
+
callbacks.onTodoUpdate?.(item.items);
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case "turn.completed": {
|
|
209
|
+
// Accumulate and deliver usage BEFORE onAgentEnd so that
|
|
210
|
+
// finalizeResponse() can read lastTurnUsage when building the
|
|
211
|
+
// final message text.
|
|
212
|
+
const u = event.usage;
|
|
213
|
+
this.sessionTokens.input += u.input_tokens;
|
|
214
|
+
this.sessionTokens.cached += u.cached_input_tokens;
|
|
215
|
+
this.sessionTokens.output += u.output_tokens;
|
|
216
|
+
callbacks.onTurnComplete?.({
|
|
217
|
+
inputTokens: u.input_tokens,
|
|
218
|
+
cachedInputTokens: u.cached_input_tokens,
|
|
219
|
+
outputTokens: u.output_tokens,
|
|
220
|
+
});
|
|
221
|
+
callbacks.onAgentEnd();
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "turn.failed":
|
|
225
|
+
throw new Error(event.error.message);
|
|
226
|
+
case "error":
|
|
227
|
+
throw new Error(event.message);
|
|
228
|
+
default:
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
if (this.abortController === controller) {
|
|
235
|
+
this.abortController = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async abort() {
|
|
240
|
+
this.abortController?.abort();
|
|
241
|
+
}
|
|
242
|
+
async newThread(workspace, model) {
|
|
243
|
+
this.ensureIdle("start a new thread");
|
|
244
|
+
const effectiveWorkspace = workspace ?? this.currentWorkspace;
|
|
245
|
+
const effectiveModel = model ?? this.currentModel;
|
|
246
|
+
this.thread = this.getCodex().startThread(this.buildThreadOptions(effectiveWorkspace, effectiveModel));
|
|
247
|
+
this.activeThreadLaunchProfile = this.currentLaunchProfile;
|
|
248
|
+
this.currentWorkspace = effectiveWorkspace;
|
|
249
|
+
this.currentThreadId = this.thread.id ?? null;
|
|
250
|
+
if (model) {
|
|
251
|
+
this.currentModel = model;
|
|
252
|
+
}
|
|
253
|
+
return this.getInfo();
|
|
254
|
+
}
|
|
255
|
+
async resumeThread(threadId) {
|
|
256
|
+
this.ensureIdle("resume a thread");
|
|
257
|
+
this.thread = this.getCodex().resumeThread(threadId, this.buildThreadOptions(this.currentWorkspace, this.currentModel));
|
|
258
|
+
this.activeThreadLaunchProfile = this.currentLaunchProfile;
|
|
259
|
+
this.currentThreadId = threadId;
|
|
260
|
+
return this.getInfo();
|
|
261
|
+
}
|
|
262
|
+
async switchSession(threadId) {
|
|
263
|
+
this.ensureIdle("switch session");
|
|
264
|
+
const record = getThread(threadId);
|
|
265
|
+
const workspace = record?.cwd ?? this.currentWorkspace;
|
|
266
|
+
const model = record?.model || undefined;
|
|
267
|
+
const reasoningEffort = record?.reasoningEffort || undefined;
|
|
268
|
+
const launchProfile = this.resolveThreadLaunchProfile(record);
|
|
269
|
+
this.currentReasoningEffort = reasoningEffort;
|
|
270
|
+
this.thread = this.getCodex().resumeThread(threadId, this.buildThreadOptions(workspace, model, launchProfile));
|
|
271
|
+
this.activeThreadLaunchProfile = launchProfile;
|
|
272
|
+
this.currentWorkspace = workspace;
|
|
273
|
+
this.currentThreadId = threadId;
|
|
274
|
+
if (model) {
|
|
275
|
+
this.currentModel = model;
|
|
276
|
+
}
|
|
277
|
+
return this.getInfo();
|
|
278
|
+
}
|
|
279
|
+
listAllSessions(limit) {
|
|
280
|
+
return listThreads(limit ?? 20).map(toAgentThreadRecord);
|
|
281
|
+
}
|
|
282
|
+
listWorkspaces() {
|
|
283
|
+
return listWorkspaces();
|
|
284
|
+
}
|
|
285
|
+
listModels() {
|
|
286
|
+
return listModels();
|
|
287
|
+
}
|
|
288
|
+
getSessionRecord(threadId) {
|
|
289
|
+
const record = getThread(threadId);
|
|
290
|
+
return record ? toAgentThreadRecord(record) : null;
|
|
291
|
+
}
|
|
292
|
+
setModel(slug) {
|
|
293
|
+
this.currentModel = slug;
|
|
294
|
+
return slug;
|
|
295
|
+
}
|
|
296
|
+
setModelForCurrentSession(slug) {
|
|
297
|
+
this.ensureIdle("change model");
|
|
298
|
+
this.currentModel = slug;
|
|
299
|
+
const appliedToActiveThread = this.reattachActiveThread();
|
|
300
|
+
return { value: slug, appliedToActiveThread };
|
|
301
|
+
}
|
|
302
|
+
setReasoningEffort(effort) {
|
|
303
|
+
this.currentReasoningEffort = effort;
|
|
304
|
+
}
|
|
305
|
+
setReasoningEffortForCurrentSession(effort) {
|
|
306
|
+
this.ensureIdle("change reasoning effort");
|
|
307
|
+
this.currentReasoningEffort = effort;
|
|
308
|
+
const appliedToActiveThread = this.reattachActiveThread();
|
|
309
|
+
return { value: effort, appliedToActiveThread };
|
|
310
|
+
}
|
|
311
|
+
setLaunchProfile(profileId) {
|
|
312
|
+
this.currentLaunchProfile = getLaunchProfile(this.config, profileId);
|
|
313
|
+
this.resetCodexClient();
|
|
314
|
+
return this.currentLaunchProfile;
|
|
315
|
+
}
|
|
316
|
+
setFastMode(enabled) {
|
|
317
|
+
this.ensureIdle("change fast mode");
|
|
318
|
+
const profile = this.findFastModeLaunchProfile(enabled);
|
|
319
|
+
writeCodexFastMode(enabled);
|
|
320
|
+
this.lastObservedFastMode = enabled;
|
|
321
|
+
this.currentLaunchProfile = profile;
|
|
322
|
+
this.resetCodexClient();
|
|
323
|
+
let appliedToActiveThread = false;
|
|
324
|
+
if (this.thread) {
|
|
325
|
+
if (this.currentThreadId) {
|
|
326
|
+
this.thread = this.getCodex().resumeThread(this.currentThreadId, this.buildThreadOptions(this.currentWorkspace, this.currentModel, profile));
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
this.thread = this.getCodex().startThread(this.buildThreadOptions(this.currentWorkspace, this.currentModel, profile));
|
|
330
|
+
}
|
|
331
|
+
this.activeThreadLaunchProfile = profile;
|
|
332
|
+
appliedToActiveThread = true;
|
|
333
|
+
}
|
|
334
|
+
return { enabled, profile, appliedToActiveThread };
|
|
335
|
+
}
|
|
336
|
+
getSelectedLaunchProfile() {
|
|
337
|
+
return this.currentLaunchProfile;
|
|
338
|
+
}
|
|
339
|
+
syncFromCodexState(options = {}) {
|
|
340
|
+
const activeThreadId = this.thread?.id ?? this.currentThreadId;
|
|
341
|
+
const before = {
|
|
342
|
+
workspace: this.currentWorkspace,
|
|
343
|
+
model: this.currentModel,
|
|
344
|
+
reasoningEffort: this.currentReasoningEffort,
|
|
345
|
+
activeLaunchProfileId: this.activeThreadLaunchProfile?.id,
|
|
346
|
+
selectedLaunchProfileId: this.currentLaunchProfile.id,
|
|
347
|
+
};
|
|
348
|
+
const changedFields = new Set();
|
|
349
|
+
if (activeThreadId && !this.abortController) {
|
|
350
|
+
const record = getThread(activeThreadId);
|
|
351
|
+
if (record) {
|
|
352
|
+
if (record.cwd && record.cwd !== this.currentWorkspace)
|
|
353
|
+
changedFields.add("workspace");
|
|
354
|
+
if ((record.model || undefined) !== this.currentModel)
|
|
355
|
+
changedFields.add("model");
|
|
356
|
+
if ((record.reasoningEffort || undefined) !== this.currentReasoningEffort)
|
|
357
|
+
changedFields.add("reasoning");
|
|
358
|
+
const resolvedLaunchProfile = this.resolveThreadLaunchProfile(record);
|
|
359
|
+
if (resolvedLaunchProfile.id !== this.activeThreadLaunchProfile?.id)
|
|
360
|
+
changedFields.add("launch");
|
|
361
|
+
this.currentWorkspace = record.cwd || this.currentWorkspace;
|
|
362
|
+
this.currentModel = record.model || this.currentModel;
|
|
363
|
+
this.currentReasoningEffort = record.reasoningEffort
|
|
364
|
+
? record.reasoningEffort
|
|
365
|
+
: undefined;
|
|
366
|
+
this.activeThreadLaunchProfile = resolvedLaunchProfile;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const codexFastMode = readCodexFastMode();
|
|
370
|
+
if (codexFastMode !== this.lastObservedFastMode) {
|
|
371
|
+
changedFields.add("fast");
|
|
372
|
+
}
|
|
373
|
+
this.lastObservedFastMode = codexFastMode;
|
|
374
|
+
if (codexFastMode !== null) {
|
|
375
|
+
try {
|
|
376
|
+
const fastProfile = this.findFastModeLaunchProfile(codexFastMode);
|
|
377
|
+
if (fastProfile.id !== this.currentLaunchProfile.id) {
|
|
378
|
+
changedFields.add("next-launch");
|
|
379
|
+
}
|
|
380
|
+
this.currentLaunchProfile = fastProfile;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Keep the existing profile if no configured profile maps to this value.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const changed = changedFields.size > 0 ||
|
|
387
|
+
before.workspace !== this.currentWorkspace ||
|
|
388
|
+
before.model !== this.currentModel ||
|
|
389
|
+
before.reasoningEffort !== this.currentReasoningEffort ||
|
|
390
|
+
before.activeLaunchProfileId !== this.activeThreadLaunchProfile?.id ||
|
|
391
|
+
before.selectedLaunchProfileId !== this.currentLaunchProfile.id;
|
|
392
|
+
let reattached = false;
|
|
393
|
+
if (changed && options.reattach && !this.abortController && this.thread) {
|
|
394
|
+
reattached = this.reattachActiveThread();
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
threadId: activeThreadId,
|
|
398
|
+
changed,
|
|
399
|
+
reattached,
|
|
400
|
+
changedFields: [...changedFields],
|
|
401
|
+
info: this.getInfo(),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
handback() {
|
|
405
|
+
const info = { threadId: this.currentThreadId, workspace: this.currentWorkspace };
|
|
406
|
+
this.abortController?.abort();
|
|
407
|
+
this.abortController = null;
|
|
408
|
+
this.thread = null;
|
|
409
|
+
this.currentThreadId = null;
|
|
410
|
+
this.activeThreadLaunchProfile = null;
|
|
411
|
+
return info;
|
|
412
|
+
}
|
|
413
|
+
dispose() {
|
|
414
|
+
this.abortController?.abort();
|
|
415
|
+
this.abortController = null;
|
|
416
|
+
this.thread = null;
|
|
417
|
+
this.currentThreadId = null;
|
|
418
|
+
this.activeThreadLaunchProfile = null;
|
|
419
|
+
}
|
|
420
|
+
buildSdkInput(input) {
|
|
421
|
+
if (typeof input === "string") {
|
|
422
|
+
return input;
|
|
423
|
+
}
|
|
424
|
+
const parts = [];
|
|
425
|
+
const textParts = [];
|
|
426
|
+
if (input.stagedFileInstructions) {
|
|
427
|
+
textParts.push(input.stagedFileInstructions);
|
|
428
|
+
}
|
|
429
|
+
if (input.text) {
|
|
430
|
+
textParts.push(input.text);
|
|
431
|
+
}
|
|
432
|
+
if (textParts.length > 0) {
|
|
433
|
+
parts.push({ type: "text", text: textParts.join("\n\n") });
|
|
434
|
+
}
|
|
435
|
+
for (const imagePath of input.imagePaths ?? []) {
|
|
436
|
+
parts.push({ type: "local_image", path: imagePath });
|
|
437
|
+
}
|
|
438
|
+
if (parts.length === 0) {
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
if (parts.length === 1 && parts[0]?.type === "text") {
|
|
442
|
+
return parts[0].text;
|
|
443
|
+
}
|
|
444
|
+
return parts;
|
|
445
|
+
}
|
|
446
|
+
buildThreadOptions(workspace, model, launchProfile = this.currentLaunchProfile) {
|
|
447
|
+
const effectiveModel = model ?? this.currentModel ?? this.config.codexModel;
|
|
448
|
+
const options = {
|
|
449
|
+
model: effectiveModel,
|
|
450
|
+
sandboxMode: launchProfile.sandboxMode,
|
|
451
|
+
workingDirectory: workspace,
|
|
452
|
+
approvalPolicy: launchProfile.approvalPolicy,
|
|
453
|
+
skipGitRepoCheck: true,
|
|
454
|
+
};
|
|
455
|
+
if (this.currentReasoningEffort) {
|
|
456
|
+
return {
|
|
457
|
+
...options,
|
|
458
|
+
modelReasoningEffort: this.currentReasoningEffort,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return options;
|
|
462
|
+
}
|
|
463
|
+
ensureIdle(action) {
|
|
464
|
+
if (this.abortController) {
|
|
465
|
+
throw new Error(`Cannot ${action} while a turn is in progress`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
handleThreadEvent(event) {
|
|
469
|
+
if (event.type === "thread.started") {
|
|
470
|
+
this.currentThreadId = event.thread_id;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
getCodex() {
|
|
474
|
+
if (!this.codex) {
|
|
475
|
+
this.resetCodexClient();
|
|
476
|
+
}
|
|
477
|
+
return this.codex;
|
|
478
|
+
}
|
|
479
|
+
resetCodexClient() {
|
|
480
|
+
const cli = resolveCodexCli();
|
|
481
|
+
const options = {
|
|
482
|
+
apiKey: this.config.codexApiKey,
|
|
483
|
+
config: {
|
|
484
|
+
approval_policy: this.currentLaunchProfile.approvalPolicy,
|
|
485
|
+
},
|
|
486
|
+
env: buildCodexEnv(this.config.codexApiKey),
|
|
487
|
+
};
|
|
488
|
+
if (cli.path) {
|
|
489
|
+
options.codexPathOverride = cli.path;
|
|
490
|
+
}
|
|
491
|
+
this.codex = new Codex(options);
|
|
492
|
+
}
|
|
493
|
+
resolveThreadLaunchProfile(record) {
|
|
494
|
+
if (!record?.sandboxMode || !record.approvalPolicy) {
|
|
495
|
+
return this.currentLaunchProfile;
|
|
496
|
+
}
|
|
497
|
+
const matchingProfile = this.config.launchProfiles.find((profile) => profile.sandboxMode === record.sandboxMode && profile.approvalPolicy === record.approvalPolicy);
|
|
498
|
+
if (matchingProfile) {
|
|
499
|
+
return matchingProfile;
|
|
500
|
+
}
|
|
501
|
+
return createLaunchProfile({
|
|
502
|
+
id: "attached-thread",
|
|
503
|
+
label: "Attached Thread",
|
|
504
|
+
sandboxMode: record.sandboxMode,
|
|
505
|
+
approvalPolicy: record.approvalPolicy,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
refreshActiveThreadMetadata(threadId) {
|
|
509
|
+
const record = getThread(threadId);
|
|
510
|
+
if (!record) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.currentWorkspace = record.cwd || this.currentWorkspace;
|
|
514
|
+
this.currentModel = record.model || this.currentModel;
|
|
515
|
+
this.currentReasoningEffort = record.reasoningEffort
|
|
516
|
+
? record.reasoningEffort
|
|
517
|
+
: undefined;
|
|
518
|
+
this.activeThreadLaunchProfile = this.resolveThreadLaunchProfile(record);
|
|
519
|
+
}
|
|
520
|
+
reattachActiveThread() {
|
|
521
|
+
if (!this.thread) {
|
|
522
|
+
this.resetCodexClient();
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
const launchProfile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
|
|
526
|
+
if (this.currentThreadId) {
|
|
527
|
+
this.thread = this.getCodex().resumeThread(this.currentThreadId, this.buildThreadOptions(this.currentWorkspace, this.currentModel, launchProfile));
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
this.thread = this.getCodex().startThread(this.buildThreadOptions(this.currentWorkspace, this.currentModel, launchProfile));
|
|
531
|
+
this.currentThreadId = this.thread.id ?? null;
|
|
532
|
+
}
|
|
533
|
+
this.activeThreadLaunchProfile = launchProfile;
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
findFastModeLaunchProfile(enabled) {
|
|
537
|
+
const current = this.currentLaunchProfile;
|
|
538
|
+
const profiles = this.config.launchProfiles;
|
|
539
|
+
const candidates = enabled
|
|
540
|
+
? [
|
|
541
|
+
current.approvalPolicy === "never" ? current : undefined,
|
|
542
|
+
profiles.find((profile) => profile.sandboxMode === current.sandboxMode && profile.approvalPolicy === "never"),
|
|
543
|
+
profiles.find((profile) => profile.id === this.config.defaultLaunchProfileId && profile.approvalPolicy === "never"),
|
|
544
|
+
profiles.find((profile) => !profile.unsafe && profile.approvalPolicy === "never"),
|
|
545
|
+
profiles.find((profile) => profile.approvalPolicy === "never"),
|
|
546
|
+
]
|
|
547
|
+
: [
|
|
548
|
+
current.approvalPolicy !== "never" ? current : undefined,
|
|
549
|
+
profiles.find((profile) => profile.sandboxMode === current.sandboxMode && profile.approvalPolicy !== "never"),
|
|
550
|
+
profiles.find((profile) => profile.id === "review"),
|
|
551
|
+
profiles.find((profile) => !profile.unsafe && profile.approvalPolicy !== "never"),
|
|
552
|
+
profiles.find((profile) => profile.approvalPolicy !== "never"),
|
|
553
|
+
];
|
|
554
|
+
const profile = candidates.find((candidate) => Boolean(candidate));
|
|
555
|
+
if (!profile) {
|
|
556
|
+
throw new Error(`No launch profile is configured for fast mode ${enabled ? "on" : "off"}`);
|
|
557
|
+
}
|
|
558
|
+
return profile;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function getLaunchProfile(config, profileId) {
|
|
562
|
+
const profile = findLaunchProfile(config.launchProfiles, profileId);
|
|
563
|
+
if (!profile) {
|
|
564
|
+
throw new Error(`Unknown launch profile: ${profileId}`);
|
|
565
|
+
}
|
|
566
|
+
return profile;
|
|
567
|
+
}
|
|
568
|
+
function toAgentThreadRecord(record) {
|
|
569
|
+
return {
|
|
570
|
+
...record,
|
|
571
|
+
agentId: "codex",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function buildCodexEnv(apiKey) {
|
|
575
|
+
const env = {};
|
|
576
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
577
|
+
if (value !== undefined) {
|
|
578
|
+
env[key] = value;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (apiKey) {
|
|
582
|
+
env.CODEX_API_KEY = apiKey;
|
|
583
|
+
}
|
|
584
|
+
return env;
|
|
585
|
+
}
|
|
586
|
+
function computeTextDelta(previousText, nextText) {
|
|
587
|
+
return nextText.startsWith(previousText) ? nextText.slice(previousText.length) : nextText;
|
|
588
|
+
}
|
|
589
|
+
function truncate(text, maxLength) {
|
|
590
|
+
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
591
|
+
}
|