lunel-cli 0.1.114 → 0.1.116
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/dist/ai/codex.d.ts +18 -2
- package/dist/ai/codex.js +524 -90
- package/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.js +1 -0
- package/dist/ai/interface.d.ts +4 -0
- package/dist/ai/opencode.d.ts +5 -1
- package/dist/ai/opencode.js +317 -34
- package/dist/index.js +125 -6
- package/package.json +1 -1
package/dist/ai/codex.js
CHANGED
|
@@ -6,6 +6,18 @@ import * as path from "path";
|
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
8
|
const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
|
|
9
|
+
const CODEX_AGENTS = [
|
|
10
|
+
{
|
|
11
|
+
name: "Build",
|
|
12
|
+
mode: "default",
|
|
13
|
+
description: "Default Codex collaboration mode.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "plan",
|
|
17
|
+
mode: "plan",
|
|
18
|
+
description: "Plan-first Codex collaboration mode.",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
9
21
|
const DEBUG_MODE = process.env.LUNEL_DEBUG === "1" || process.env.LUNEL_DEBUG_AI === "1";
|
|
10
22
|
function joinStreamingText(previousText, nextChunk) {
|
|
11
23
|
if (!previousText) {
|
|
@@ -29,14 +41,22 @@ export class CodexProvider {
|
|
|
29
41
|
proc = null;
|
|
30
42
|
shuttingDown = false;
|
|
31
43
|
emitter = null;
|
|
44
|
+
defaultModelContextWindow = null;
|
|
32
45
|
nextId = 1;
|
|
33
46
|
pending = new Map();
|
|
34
47
|
sessions = new Map();
|
|
35
48
|
deletedThreadIds = new Set();
|
|
36
49
|
resumedThreadIds = new Set();
|
|
37
50
|
pendingPermissionRequestIds = new Map();
|
|
51
|
+
pendingQuestionRequestIds = new Map();
|
|
38
52
|
assistantMessageIdByTurnId = new Map();
|
|
39
53
|
partTextById = new Map();
|
|
54
|
+
debugLog(message, fields) {
|
|
55
|
+
if (!DEBUG_MODE)
|
|
56
|
+
return;
|
|
57
|
+
const suffix = fields ? ` ${JSON.stringify(fields)}` : "";
|
|
58
|
+
console.log(`[codex] ${message}${suffix}`);
|
|
59
|
+
}
|
|
40
60
|
async init() {
|
|
41
61
|
if (DEBUG_MODE)
|
|
42
62
|
console.log("Starting Codex app-server...");
|
|
@@ -61,7 +81,15 @@ export class CodexProvider {
|
|
|
61
81
|
this.emitter?.({ type: "sse_dead", properties: { error: msg } });
|
|
62
82
|
}
|
|
63
83
|
});
|
|
64
|
-
await this.call("initialize", {
|
|
84
|
+
await this.call("initialize", {
|
|
85
|
+
clientInfo: { name: "lunel", version: "1.0" },
|
|
86
|
+
capabilities: {
|
|
87
|
+
experimentalApi: true,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
await this.refreshConfigDefaults().catch(() => {
|
|
91
|
+
// Config defaults are best-effort metadata only.
|
|
92
|
+
});
|
|
65
93
|
if (DEBUG_MODE)
|
|
66
94
|
console.log("Codex ready.\n");
|
|
67
95
|
}
|
|
@@ -78,7 +106,10 @@ export class CodexProvider {
|
|
|
78
106
|
};
|
|
79
107
|
}
|
|
80
108
|
async createSession(title) {
|
|
81
|
-
const result = await this.call("thread/start", {
|
|
109
|
+
const result = await this.call("thread/start", {
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
persistExtendedHistory: true,
|
|
112
|
+
});
|
|
82
113
|
const threadObject = this.extractThreadObject(result);
|
|
83
114
|
const threadId = this.extractThreadId(threadObject);
|
|
84
115
|
if (!threadId) {
|
|
@@ -143,8 +174,53 @@ export class CodexProvider {
|
|
|
143
174
|
}
|
|
144
175
|
return { deleted: true };
|
|
145
176
|
}
|
|
177
|
+
async renameSession(id, title) {
|
|
178
|
+
const trimmed = title.trim();
|
|
179
|
+
if (!trimmed) {
|
|
180
|
+
throw new Error("Session title cannot be empty");
|
|
181
|
+
}
|
|
182
|
+
const methodsToTry = [
|
|
183
|
+
{ method: "thread/name/set", params: { threadId: id, name: trimmed } },
|
|
184
|
+
{ method: "thread/name/set", params: { threadId: id, title: trimmed } },
|
|
185
|
+
{ method: "thread/name/update", params: { threadId: id, name: trimmed } },
|
|
186
|
+
{ method: "thread/name/update", params: { threadId: id, title: trimmed } },
|
|
187
|
+
{ method: "thread/metadata/update", params: { threadId: id, title: trimmed } },
|
|
188
|
+
{ method: "thread/update", params: { threadId: id, title: trimmed } },
|
|
189
|
+
{ method: "thread/rename", params: { threadId: id, title: trimmed } },
|
|
190
|
+
];
|
|
191
|
+
let renamed = false;
|
|
192
|
+
let lastError = null;
|
|
193
|
+
for (const entry of methodsToTry) {
|
|
194
|
+
try {
|
|
195
|
+
await this.call(entry.method, entry.params);
|
|
196
|
+
renamed = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
lastError = err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!renamed && lastError) {
|
|
204
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
205
|
+
}
|
|
206
|
+
const existing = this.sessions.get(id) ?? this.ensureLocalSession(id);
|
|
207
|
+
const session = this.upsertSession({
|
|
208
|
+
id,
|
|
209
|
+
title: trimmed,
|
|
210
|
+
createdAt: existing.createdAt,
|
|
211
|
+
updatedAt: Date.now(),
|
|
212
|
+
archived: existing.archived,
|
|
213
|
+
cwd: existing.cwd,
|
|
214
|
+
}, true);
|
|
215
|
+
return { session: this.toSessionInfo(session) };
|
|
216
|
+
}
|
|
146
217
|
async getMessages(sessionId) {
|
|
147
218
|
const session = this.ensureLocalSession(sessionId);
|
|
219
|
+
this.debugLog("getMessages start", {
|
|
220
|
+
sessionId,
|
|
221
|
+
existingMessageCount: session.messages.length,
|
|
222
|
+
existingPartCount: session.messages.reduce((sum, message) => sum + (message.parts?.length || 0), 0),
|
|
223
|
+
});
|
|
148
224
|
const result = await this.call("thread/read", {
|
|
149
225
|
threadId: session.id,
|
|
150
226
|
includeTurns: true,
|
|
@@ -153,7 +229,16 @@ export class CodexProvider {
|
|
|
153
229
|
if (!threadObject) {
|
|
154
230
|
return { messages: session.messages };
|
|
155
231
|
}
|
|
232
|
+
this.logThreadReadSummary(sessionId, threadObject);
|
|
156
233
|
const historyMessages = this.decodeMessagesFromThreadRead(sessionId, threadObject);
|
|
234
|
+
this.debugLog("getMessages decoded thread", {
|
|
235
|
+
sessionId,
|
|
236
|
+
turnCount: this.readArray(threadObject.turns).length,
|
|
237
|
+
decodedMessageCount: historyMessages.length,
|
|
238
|
+
decodedPartCount: historyMessages.reduce((sum, message) => sum + (message.parts?.length || 0), 0),
|
|
239
|
+
decodedRoles: historyMessages.map((message) => message.role),
|
|
240
|
+
decodedPartTypes: historyMessages.flatMap((message) => (message.parts || []).map((part) => String(this.asRecord(part).type ?? "unknown"))),
|
|
241
|
+
});
|
|
157
242
|
if (historyMessages.length > 0) {
|
|
158
243
|
session.messages = historyMessages;
|
|
159
244
|
}
|
|
@@ -165,6 +250,17 @@ export class CodexProvider {
|
|
|
165
250
|
archived: false,
|
|
166
251
|
cwd: this.extractThreadCwd(threadObject) ?? session.cwd,
|
|
167
252
|
});
|
|
253
|
+
if (this.defaultModelContextWindow && this.defaultModelContextWindow > 0) {
|
|
254
|
+
this.emitter?.({
|
|
255
|
+
type: "session.usage",
|
|
256
|
+
properties: {
|
|
257
|
+
sessionID: sessionId,
|
|
258
|
+
tokenUsage: {
|
|
259
|
+
modelContextWindow: this.defaultModelContextWindow,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
168
264
|
return { messages: session.messages };
|
|
169
265
|
}
|
|
170
266
|
async prompt(sessionId, text, model, agent, files = [], codexOptions) {
|
|
@@ -172,26 +268,31 @@ export class CodexProvider {
|
|
|
172
268
|
session.updatedAt = Date.now();
|
|
173
269
|
(async () => {
|
|
174
270
|
try {
|
|
175
|
-
|
|
271
|
+
const effortLevels = ["low", "medium", "high"];
|
|
272
|
+
const speedDelta = {
|
|
273
|
+
fast: -1,
|
|
274
|
+
balanced: 0,
|
|
275
|
+
quality: 1,
|
|
276
|
+
};
|
|
277
|
+
const baseEffort = codexOptions?.reasoningEffort ?? "medium";
|
|
278
|
+
const baseIndex = effortLevels.indexOf(baseEffort);
|
|
279
|
+
const adjustedIndex = Math.max(0, Math.min(effortLevels.length - 1, baseIndex + (codexOptions?.speed ? speedDelta[codexOptions.speed] : 0)));
|
|
280
|
+
const reasoningEffort = effortLevels[adjustedIndex];
|
|
281
|
+
const modelId = await this.resolveModelId(model);
|
|
282
|
+
const collaborationMode = this.buildCollaborationMode(agent, modelId, reasoningEffort);
|
|
283
|
+
// Freshly created sessions already have a live backend thread from
|
|
284
|
+
// thread/start. Forcing thread/resume here can attach stale state to a
|
|
285
|
+
// brand-new session and trigger rollout lookup failures on turn/start.
|
|
286
|
+
await this.ensureThreadResumed(session.id, false, codexOptions, collaborationMode);
|
|
176
287
|
let imageUrlKey = "url";
|
|
177
288
|
while (true) {
|
|
178
289
|
try {
|
|
179
|
-
const effortLevels = ["low", "medium", "high"];
|
|
180
|
-
const speedDelta = {
|
|
181
|
-
fast: -1,
|
|
182
|
-
balanced: 0,
|
|
183
|
-
quality: 1,
|
|
184
|
-
};
|
|
185
|
-
const baseEffort = codexOptions?.reasoningEffort ?? "medium";
|
|
186
|
-
const baseIndex = effortLevels.indexOf(baseEffort);
|
|
187
|
-
const adjustedIndex = Math.max(0, Math.min(effortLevels.length - 1, baseIndex + (codexOptions?.speed ? speedDelta[codexOptions.speed] : 0)));
|
|
188
|
-
const reasoningEffort = effortLevels[adjustedIndex];
|
|
189
290
|
await this.call("turn/start", {
|
|
190
291
|
threadId: session.id,
|
|
191
292
|
input: this.makeTurnInputPayload(text, files, imageUrlKey),
|
|
192
|
-
...(
|
|
193
|
-
...(agent ? { agent } : {}),
|
|
293
|
+
...(modelId ? { model: modelId } : {}),
|
|
194
294
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
295
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
195
296
|
});
|
|
196
297
|
break;
|
|
197
298
|
}
|
|
@@ -225,7 +326,7 @@ export class CodexProvider {
|
|
|
225
326
|
return {};
|
|
226
327
|
}
|
|
227
328
|
async agents() {
|
|
228
|
-
return { agents: [] };
|
|
329
|
+
return { agents: [...CODEX_AGENTS] };
|
|
229
330
|
}
|
|
230
331
|
async providers() {
|
|
231
332
|
const items = await this.fetchModels();
|
|
@@ -283,11 +384,53 @@ export class CodexProvider {
|
|
|
283
384
|
});
|
|
284
385
|
return {};
|
|
285
386
|
}
|
|
286
|
-
async questionReply() {
|
|
287
|
-
|
|
387
|
+
async questionReply(sessionId, questionId, answers) {
|
|
388
|
+
const pending = this.pendingQuestionRequestIds.get(questionId);
|
|
389
|
+
if (!pending) {
|
|
390
|
+
this.emitter?.({
|
|
391
|
+
type: "question.replied",
|
|
392
|
+
properties: { sessionID: sessionId, requestID: questionId, answers, skipped: true },
|
|
393
|
+
});
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
const responseAnswers = {};
|
|
397
|
+
pending.questionIds.forEach((id, index) => {
|
|
398
|
+
responseAnswers[id] = {
|
|
399
|
+
answers: Array.isArray(answers[index]) ? answers[index].filter((value) => typeof value === "string") : [],
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
this.send({
|
|
403
|
+
jsonrpc: "2.0",
|
|
404
|
+
id: pending.requestId,
|
|
405
|
+
result: { answers: responseAnswers },
|
|
406
|
+
});
|
|
407
|
+
this.pendingQuestionRequestIds.delete(questionId);
|
|
408
|
+
this.emitter?.({
|
|
409
|
+
type: "question.replied",
|
|
410
|
+
properties: { sessionID: sessionId, requestID: questionId, answers },
|
|
411
|
+
});
|
|
412
|
+
return {};
|
|
288
413
|
}
|
|
289
|
-
async questionReject() {
|
|
290
|
-
|
|
414
|
+
async questionReject(sessionId, questionId) {
|
|
415
|
+
const pending = this.pendingQuestionRequestIds.get(questionId);
|
|
416
|
+
if (!pending) {
|
|
417
|
+
this.emitter?.({
|
|
418
|
+
type: "question.rejected",
|
|
419
|
+
properties: { sessionID: sessionId, requestID: questionId, skipped: true },
|
|
420
|
+
});
|
|
421
|
+
return {};
|
|
422
|
+
}
|
|
423
|
+
this.send({
|
|
424
|
+
jsonrpc: "2.0",
|
|
425
|
+
id: pending.requestId,
|
|
426
|
+
result: { answers: {} },
|
|
427
|
+
});
|
|
428
|
+
this.pendingQuestionRequestIds.delete(questionId);
|
|
429
|
+
this.emitter?.({
|
|
430
|
+
type: "question.rejected",
|
|
431
|
+
properties: { sessionID: sessionId, requestID: questionId },
|
|
432
|
+
});
|
|
433
|
+
return {};
|
|
291
434
|
}
|
|
292
435
|
send(req) {
|
|
293
436
|
if (!this.proc?.stdin?.writable)
|
|
@@ -344,10 +487,28 @@ export class CodexProvider {
|
|
|
344
487
|
handleServerRequest(method, requestId, params) {
|
|
345
488
|
const session = this.resolveSessionFromPayload(params);
|
|
346
489
|
if (method === "item/tool/requestUserInput") {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
490
|
+
const questionRequestId = String(requestId);
|
|
491
|
+
const callId = this.extractItemId(params) ?? undefined;
|
|
492
|
+
const messageId = session ? this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`) : undefined;
|
|
493
|
+
const questions = this.extractStructuredUserInputQuestions(params);
|
|
494
|
+
this.pendingQuestionRequestIds.set(questionRequestId, {
|
|
495
|
+
sessionId: session?.id ?? "",
|
|
496
|
+
requestId,
|
|
497
|
+
messageId,
|
|
498
|
+
callId,
|
|
499
|
+
questionIds: questions.map((question) => this.readString(this.asRecord(question).id)).filter((value) => Boolean(value)),
|
|
500
|
+
});
|
|
501
|
+
this.emitter?.({
|
|
502
|
+
type: "question.asked",
|
|
503
|
+
properties: {
|
|
504
|
+
id: questionRequestId,
|
|
505
|
+
sessionID: session?.id,
|
|
506
|
+
questions,
|
|
507
|
+
tool: {
|
|
508
|
+
...(messageId ? { messageID: messageId } : {}),
|
|
509
|
+
...(callId ? { callID: callId } : {}),
|
|
510
|
+
},
|
|
511
|
+
},
|
|
351
512
|
});
|
|
352
513
|
return;
|
|
353
514
|
}
|
|
@@ -427,6 +588,17 @@ export class CodexProvider {
|
|
|
427
588
|
});
|
|
428
589
|
}
|
|
429
590
|
return;
|
|
591
|
+
case "thread/tokenUsage/updated":
|
|
592
|
+
if (session) {
|
|
593
|
+
this.emitter?.({
|
|
594
|
+
type: "session.usage",
|
|
595
|
+
properties: {
|
|
596
|
+
sessionID: session.id,
|
|
597
|
+
tokenUsage: params.tokenUsage ?? null,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
430
602
|
case "turn/completed":
|
|
431
603
|
case "turn/failed":
|
|
432
604
|
if (session) {
|
|
@@ -503,6 +675,9 @@ export class CodexProvider {
|
|
|
503
675
|
this.pendingPermissionRequestIds.delete(permissionId);
|
|
504
676
|
this.emitter?.({ type: "permission.replied", properties: { permissionId } });
|
|
505
677
|
}
|
|
678
|
+
if (permissionId && this.pendingQuestionRequestIds.has(permissionId)) {
|
|
679
|
+
this.pendingQuestionRequestIds.delete(permissionId);
|
|
680
|
+
}
|
|
506
681
|
return;
|
|
507
682
|
}
|
|
508
683
|
default:
|
|
@@ -621,7 +796,7 @@ export class CodexProvider {
|
|
|
621
796
|
const itemId = this.extractItemId(params) ?? this.readString(item.id) ?? normalizedType ?? "tool";
|
|
622
797
|
const fileChangeLike = this.isFileChangeStructuredItem(normalizedType, item, params);
|
|
623
798
|
const emittedPartType = normalizedType === "plan"
|
|
624
|
-
? "
|
|
799
|
+
? "plan"
|
|
625
800
|
: (fileChangeLike ? "file-change" : "tool");
|
|
626
801
|
const partId = `${messageId}:${emittedPartType}:${itemId}`;
|
|
627
802
|
const nextText = this.extractStructuredOutput(params, item, normalizedType);
|
|
@@ -644,8 +819,8 @@ export class CodexProvider {
|
|
|
644
819
|
sessionID: session.id,
|
|
645
820
|
messageID: messageId,
|
|
646
821
|
type: emittedPartType,
|
|
647
|
-
...(emittedPartType === "
|
|
648
|
-
? { text: outputValue ?? "Planning..." }
|
|
822
|
+
...(emittedPartType === "plan"
|
|
823
|
+
? { text: outputValue ?? (emittedPartType === "plan" ? "Planning..." : "") }
|
|
649
824
|
: { name, toolName: name, input, output: outputValue, state, ...(patch ? { patch } : {}) }),
|
|
650
825
|
};
|
|
651
826
|
this.upsertLocalMessagePart(session, messageId, part);
|
|
@@ -720,6 +895,22 @@ export class CodexProvider {
|
|
|
720
895
|
async fetchServerThreads() {
|
|
721
896
|
return this.fetchServerThreadsByArchiveState(false);
|
|
722
897
|
}
|
|
898
|
+
async refreshConfigDefaults() {
|
|
899
|
+
const result = await this.call("config/read", undefined);
|
|
900
|
+
const payload = this.asRecord(result);
|
|
901
|
+
const config = this.asRecord(payload.config ?? result);
|
|
902
|
+
const raw = config.model_context_window;
|
|
903
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
904
|
+
this.defaultModelContextWindow = raw;
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
908
|
+
const parsed = Number(raw);
|
|
909
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
910
|
+
this.defaultModelContextWindow = parsed;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
723
914
|
async refreshSessionMetadata(sessionId) {
|
|
724
915
|
const session = this.sessions.get(sessionId);
|
|
725
916
|
const result = await this.call("thread/read", {
|
|
@@ -800,6 +991,30 @@ export class CodexProvider {
|
|
|
800
991
|
})
|
|
801
992
|
.filter((value) => Boolean(value));
|
|
802
993
|
}
|
|
994
|
+
async resolveModelId(model) {
|
|
995
|
+
if (model) {
|
|
996
|
+
return model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}`;
|
|
997
|
+
}
|
|
998
|
+
const items = await this.fetchModels();
|
|
999
|
+
return items.find((item) => item.isDefault)?.model ?? items[0]?.model;
|
|
1000
|
+
}
|
|
1001
|
+
buildCollaborationMode(agent, modelId, reasoningEffort) {
|
|
1002
|
+
const normalizedAgent = (agent ?? "").trim().toLowerCase();
|
|
1003
|
+
const mode = normalizedAgent === "build" ? "default" : normalizedAgent;
|
|
1004
|
+
if (mode !== "default" && mode !== "plan") {
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
1007
|
+
if (!modelId) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
mode,
|
|
1012
|
+
settings: {
|
|
1013
|
+
model: modelId,
|
|
1014
|
+
reasoning_effort: reasoningEffort,
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
803
1018
|
parseThreadListEntry(value, archived = false) {
|
|
804
1019
|
const obj = this.asRecord(value);
|
|
805
1020
|
const id = this.extractThreadId(obj);
|
|
@@ -909,16 +1124,27 @@ export class CodexProvider {
|
|
|
909
1124
|
return existing;
|
|
910
1125
|
return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
|
|
911
1126
|
}
|
|
912
|
-
async ensureThreadResumed(threadId, force = false) {
|
|
1127
|
+
async ensureThreadResumed(threadId, force = false, codexOptions, collaborationMode) {
|
|
913
1128
|
if (!threadId || this.resumedThreadIds.has(threadId)) {
|
|
914
1129
|
if (!force)
|
|
915
1130
|
return;
|
|
916
1131
|
}
|
|
917
1132
|
const session = this.sessions.get(threadId);
|
|
918
|
-
const params = {
|
|
1133
|
+
const params = {
|
|
1134
|
+
threadId,
|
|
1135
|
+
persistExtendedHistory: true,
|
|
1136
|
+
};
|
|
919
1137
|
if (session?.cwd) {
|
|
920
1138
|
params.cwd = session.cwd;
|
|
921
1139
|
}
|
|
1140
|
+
const permissionMode = codexOptions?.permissionMode ?? "default";
|
|
1141
|
+
if (permissionMode === "full-access") {
|
|
1142
|
+
params.approvalPolicy = "never";
|
|
1143
|
+
params.sandbox = "danger-full-access";
|
|
1144
|
+
}
|
|
1145
|
+
if (collaborationMode) {
|
|
1146
|
+
params.collaborationMode = collaborationMode;
|
|
1147
|
+
}
|
|
922
1148
|
const result = await this.call("thread/resume", params);
|
|
923
1149
|
const payload = this.asRecord(result);
|
|
924
1150
|
const threadValue = payload.thread;
|
|
@@ -971,9 +1197,14 @@ export class CodexProvider {
|
|
|
971
1197
|
const turnId = this.readString(turnObject.id);
|
|
972
1198
|
const turnTime = this.extractUpdatedAt(turnObject) ?? this.extractCreatedAt(turnObject) ?? Date.now();
|
|
973
1199
|
const items = this.readArray(turnObject.items);
|
|
1200
|
+
const assistantParts = [];
|
|
1201
|
+
let assistantMessageId;
|
|
1202
|
+
let assistantTimestamp = turnTime;
|
|
1203
|
+
const turnItemTypes = [];
|
|
974
1204
|
for (const item of items) {
|
|
975
1205
|
const itemObject = this.asRecord(item);
|
|
976
1206
|
const type = this.normalizedItemType(this.readString(itemObject.type) ?? "");
|
|
1207
|
+
turnItemTypes.push(type || "unknown");
|
|
977
1208
|
const itemId = this.readString(itemObject.id) ?? crypto.randomUUID();
|
|
978
1209
|
const timestamp = this.extractUpdatedAt(itemObject) ?? this.extractCreatedAt(itemObject) ?? (turnTime + orderOffset++);
|
|
979
1210
|
if (type === "usermessage") {
|
|
@@ -992,88 +1223,256 @@ export class CodexProvider {
|
|
|
992
1223
|
const text = this.decodeItemText(itemObject);
|
|
993
1224
|
if (!text)
|
|
994
1225
|
continue;
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1226
|
+
assistantMessageId = assistantMessageId ?? itemId;
|
|
1227
|
+
assistantTimestamp = Math.max(assistantTimestamp, timestamp);
|
|
1228
|
+
assistantParts.push({
|
|
1229
|
+
id: `${assistantMessageId}:text:${itemId}`,
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text,
|
|
1232
|
+
sessionID: threadId,
|
|
1233
|
+
messageID: assistantMessageId,
|
|
1000
1234
|
});
|
|
1001
|
-
if (turnId)
|
|
1002
|
-
this.assistantMessageIdByTurnId.set(turnId, itemId);
|
|
1003
1235
|
continue;
|
|
1004
1236
|
}
|
|
1005
1237
|
if (type === "reasoning") {
|
|
1006
1238
|
const text = this.decodeReasoningItemText(itemObject);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1239
|
+
assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
|
|
1240
|
+
assistantTimestamp = Math.max(assistantTimestamp, timestamp);
|
|
1241
|
+
assistantParts.push({
|
|
1242
|
+
id: `${assistantMessageId}:reasoning:${itemId}`,
|
|
1243
|
+
type: "reasoning",
|
|
1244
|
+
text,
|
|
1245
|
+
sessionID: threadId,
|
|
1246
|
+
messageID: assistantMessageId,
|
|
1012
1247
|
});
|
|
1013
1248
|
continue;
|
|
1014
1249
|
}
|
|
1015
1250
|
if (type === "plan") {
|
|
1016
1251
|
const text = this.decodePlanItemText(itemObject);
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1252
|
+
assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
|
|
1253
|
+
assistantTimestamp = Math.max(assistantTimestamp, timestamp);
|
|
1254
|
+
assistantParts.push({
|
|
1255
|
+
id: `${assistantMessageId}:plan:${itemId}`,
|
|
1256
|
+
type: "plan",
|
|
1257
|
+
text,
|
|
1258
|
+
sessionID: threadId,
|
|
1259
|
+
messageID: assistantMessageId,
|
|
1022
1260
|
});
|
|
1023
1261
|
continue;
|
|
1024
1262
|
}
|
|
1025
|
-
if (type === "commandexecution"
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
state: "completed",
|
|
1039
|
-
sessionID: threadId,
|
|
1040
|
-
messageID: itemId,
|
|
1041
|
-
}],
|
|
1042
|
-
time: timestamp,
|
|
1043
|
-
});
|
|
1263
|
+
if (type === "commandexecution"
|
|
1264
|
+
|| type === "enteredreviewmode"
|
|
1265
|
+
|| type === "exitedreviewmode"
|
|
1266
|
+
|| type === "contextcompaction"
|
|
1267
|
+
|| type === "mcptoolcall"
|
|
1268
|
+
|| type === "dynamictoolcall"
|
|
1269
|
+
|| type === "collabtoolcall"
|
|
1270
|
+
|| type === "collabagenttoolcall"
|
|
1271
|
+
|| type === "websearch"
|
|
1272
|
+
|| type === "imageview") {
|
|
1273
|
+
assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
|
|
1274
|
+
assistantTimestamp = Math.max(assistantTimestamp, timestamp);
|
|
1275
|
+
assistantParts.push(this.decodeStoredToolLikePart(type, itemObject, threadId, assistantMessageId, itemId));
|
|
1044
1276
|
continue;
|
|
1045
1277
|
}
|
|
1046
1278
|
if (type === "filechange" || type === "toolcall" || type === "diff") {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
...(patch ? { patch } : {}),
|
|
1067
|
-
sessionID: threadId,
|
|
1068
|
-
messageID: itemId,
|
|
1069
|
-
}],
|
|
1070
|
-
time: timestamp,
|
|
1071
|
-
});
|
|
1279
|
+
assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
|
|
1280
|
+
assistantTimestamp = Math.max(assistantTimestamp, timestamp);
|
|
1281
|
+
assistantParts.push(this.decodeStoredToolLikePart(type, itemObject, threadId, assistantMessageId, itemId));
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (assistantParts.length > 0) {
|
|
1285
|
+
const resolvedAssistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : crypto.randomUUID());
|
|
1286
|
+
messages.push({
|
|
1287
|
+
id: resolvedAssistantMessageId,
|
|
1288
|
+
role: "assistant",
|
|
1289
|
+
parts: assistantParts.map((part) => ({
|
|
1290
|
+
...part,
|
|
1291
|
+
sessionID: threadId,
|
|
1292
|
+
messageID: resolvedAssistantMessageId,
|
|
1293
|
+
})),
|
|
1294
|
+
time: assistantTimestamp,
|
|
1295
|
+
});
|
|
1296
|
+
if (turnId) {
|
|
1297
|
+
this.assistantMessageIdByTurnId.set(turnId, resolvedAssistantMessageId);
|
|
1072
1298
|
}
|
|
1073
1299
|
}
|
|
1300
|
+
this.debugLog("decoded stored turn", {
|
|
1301
|
+
threadId,
|
|
1302
|
+
turnId: turnId ?? null,
|
|
1303
|
+
itemCount: items.length,
|
|
1304
|
+
itemTypes: turnItemTypes,
|
|
1305
|
+
emittedUserMessages: messages.filter((message) => message.role === "user").length,
|
|
1306
|
+
emittedAssistantParts: assistantParts.length,
|
|
1307
|
+
});
|
|
1074
1308
|
}
|
|
1075
1309
|
return messages;
|
|
1076
1310
|
}
|
|
1311
|
+
logThreadReadSummary(sessionId, threadObject) {
|
|
1312
|
+
const turns = this.readArray(threadObject.turns);
|
|
1313
|
+
const turnSummaries = turns.map((turn, index) => {
|
|
1314
|
+
const turnObject = this.asRecord(turn);
|
|
1315
|
+
const items = this.readArray(turnObject.items);
|
|
1316
|
+
return {
|
|
1317
|
+
index,
|
|
1318
|
+
turnId: this.readString(turnObject.id) ?? null,
|
|
1319
|
+
status: this.readString(turnObject.status) ?? this.readString(this.asRecord(turnObject.status).type) ?? null,
|
|
1320
|
+
itemCount: items.length,
|
|
1321
|
+
itemTypes: items.map((item) => this.normalizedItemType(this.readString(this.asRecord(item).type) ?? "") || "unknown"),
|
|
1322
|
+
itemSummaries: items.map((item) => {
|
|
1323
|
+
const itemObject = this.asRecord(item);
|
|
1324
|
+
const type = this.normalizedItemType(this.readString(itemObject.type) ?? "") || "unknown";
|
|
1325
|
+
return {
|
|
1326
|
+
id: this.readString(itemObject.id) ?? null,
|
|
1327
|
+
type,
|
|
1328
|
+
keys: Object.keys(itemObject).sort(),
|
|
1329
|
+
textPreview: this.firstString(itemObject, ["text", "summary", "message", "query", "path", "tool", "command"])?.slice(0, 120) ?? null,
|
|
1330
|
+
hasAggregatedOutput: typeof itemObject.aggregatedOutput === "string" && itemObject.aggregatedOutput.length > 0,
|
|
1331
|
+
aggregatedOutputLength: typeof itemObject.aggregatedOutput === "string" ? itemObject.aggregatedOutput.length : 0,
|
|
1332
|
+
changesCount: Array.isArray(itemObject.changes) ? itemObject.changes.length : 0,
|
|
1333
|
+
hasResult: itemObject.result != null,
|
|
1334
|
+
hasContentItems: Array.isArray(itemObject.contentItems) && itemObject.contentItems.length > 0,
|
|
1335
|
+
status: this.readString(itemObject.status) ?? null,
|
|
1336
|
+
};
|
|
1337
|
+
}),
|
|
1338
|
+
};
|
|
1339
|
+
});
|
|
1340
|
+
console.log(`[codex-history] thread/read summary ${JSON.stringify({
|
|
1341
|
+
sessionId,
|
|
1342
|
+
threadId: this.readString(threadObject.id) ?? null,
|
|
1343
|
+
turnCount: turns.length,
|
|
1344
|
+
turnSummaries,
|
|
1345
|
+
})}`);
|
|
1346
|
+
}
|
|
1347
|
+
decodeStoredToolLikePart(type, itemObject, threadId, messageId, itemId) {
|
|
1348
|
+
if (type === "filechange" || type === "diff" || this.isFileChangeStructuredItem(type, itemObject)) {
|
|
1349
|
+
const output = this.decodeFileLikeItemText(itemObject) ?? this.describeCompletedItemOutput(itemObject, itemObject, type) ?? "File changes";
|
|
1350
|
+
const patch = this.extractCanonicalPatch(itemObject, itemObject);
|
|
1351
|
+
return {
|
|
1352
|
+
id: `${messageId}:file-change:${itemId}`,
|
|
1353
|
+
type: "file-change",
|
|
1354
|
+
name: this.describeToolPart(type, type, itemObject),
|
|
1355
|
+
toolName: this.describeToolPart(type, type, itemObject),
|
|
1356
|
+
output,
|
|
1357
|
+
state: "completed",
|
|
1358
|
+
...(patch ? { patch } : {}),
|
|
1359
|
+
sessionID: threadId,
|
|
1360
|
+
messageID: messageId,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const name = this.describeStoredToolName(type, itemObject);
|
|
1364
|
+
const input = this.extractStoredToolInput(type, itemObject);
|
|
1365
|
+
const output = this.extractStoredToolOutput(type, itemObject);
|
|
1366
|
+
return {
|
|
1367
|
+
id: `${messageId}:tool:${itemId}`,
|
|
1368
|
+
type: "tool",
|
|
1369
|
+
name,
|
|
1370
|
+
toolName: name,
|
|
1371
|
+
...(input !== undefined ? { input } : {}),
|
|
1372
|
+
...(output !== undefined ? { output } : {}),
|
|
1373
|
+
state: "completed",
|
|
1374
|
+
sessionID: threadId,
|
|
1375
|
+
messageID: messageId,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
describeStoredToolName(type, itemObject) {
|
|
1379
|
+
if (type === "commandexecution") {
|
|
1380
|
+
return "command";
|
|
1381
|
+
}
|
|
1382
|
+
if (type === "websearch") {
|
|
1383
|
+
return "web-search";
|
|
1384
|
+
}
|
|
1385
|
+
if (type === "imageview") {
|
|
1386
|
+
return "image-view";
|
|
1387
|
+
}
|
|
1388
|
+
if (type === "collabtoolcall" || type === "collabagenttoolcall") {
|
|
1389
|
+
return "agent";
|
|
1390
|
+
}
|
|
1391
|
+
if (type === "enteredreviewmode") {
|
|
1392
|
+
return "review";
|
|
1393
|
+
}
|
|
1394
|
+
if (type === "exitedreviewmode") {
|
|
1395
|
+
return "review";
|
|
1396
|
+
}
|
|
1397
|
+
if (type === "contextcompaction") {
|
|
1398
|
+
return "context";
|
|
1399
|
+
}
|
|
1400
|
+
return this.firstString(itemObject, [
|
|
1401
|
+
"name",
|
|
1402
|
+
"toolName",
|
|
1403
|
+
"tool_name",
|
|
1404
|
+
"title",
|
|
1405
|
+
"serverToolName",
|
|
1406
|
+
"server_tool_name",
|
|
1407
|
+
"kind",
|
|
1408
|
+
]) ?? type;
|
|
1409
|
+
}
|
|
1410
|
+
extractStoredToolInput(type, itemObject) {
|
|
1411
|
+
if (type === "commandexecution") {
|
|
1412
|
+
return this.extractCommandExecutionInput(itemObject, itemObject);
|
|
1413
|
+
}
|
|
1414
|
+
return itemObject.input
|
|
1415
|
+
?? itemObject.arguments
|
|
1416
|
+
?? itemObject.args
|
|
1417
|
+
?? itemObject.query
|
|
1418
|
+
?? itemObject.path
|
|
1419
|
+
?? itemObject.url
|
|
1420
|
+
?? itemObject.command
|
|
1421
|
+
?? itemObject.pattern
|
|
1422
|
+
?? undefined;
|
|
1423
|
+
}
|
|
1424
|
+
extractStoredToolOutput(type, itemObject) {
|
|
1425
|
+
if (type === "commandexecution") {
|
|
1426
|
+
return this.decodeStoredCommandExecutionOutput(itemObject);
|
|
1427
|
+
}
|
|
1428
|
+
if (type === "enteredreviewmode") {
|
|
1429
|
+
return `Reviewing ${this.readString(itemObject.review) ?? "changes"}...`;
|
|
1430
|
+
}
|
|
1431
|
+
if (type === "exitedreviewmode") {
|
|
1432
|
+
return this.firstString(itemObject, ["summary", "text", "message"]) ?? "Exited review mode";
|
|
1433
|
+
}
|
|
1434
|
+
if (type === "contextcompaction") {
|
|
1435
|
+
return "Context compacted";
|
|
1436
|
+
}
|
|
1437
|
+
if (type === "imageview") {
|
|
1438
|
+
const path = this.firstString(itemObject, ["path", "file", "filePath", "file_path", "url"]);
|
|
1439
|
+
return path ? `Viewed image ${path}` : "Viewed image";
|
|
1440
|
+
}
|
|
1441
|
+
const direct = this.firstString(itemObject, [
|
|
1442
|
+
"aggregatedOutput",
|
|
1443
|
+
"output",
|
|
1444
|
+
"outputText",
|
|
1445
|
+
"output_text",
|
|
1446
|
+
"text",
|
|
1447
|
+
"message",
|
|
1448
|
+
"summary",
|
|
1449
|
+
"result",
|
|
1450
|
+
"content",
|
|
1451
|
+
]);
|
|
1452
|
+
if (direct?.trim()) {
|
|
1453
|
+
return direct.trim();
|
|
1454
|
+
}
|
|
1455
|
+
const flattened = this.flattenTextValue(itemObject.output ?? itemObject.result ?? itemObject.content).trim();
|
|
1456
|
+
return flattened || undefined;
|
|
1457
|
+
}
|
|
1458
|
+
decodeStoredCommandExecutionOutput(itemObject) {
|
|
1459
|
+
const aggregatedOutput = this.firstString(itemObject, [
|
|
1460
|
+
"aggregatedOutput",
|
|
1461
|
+
"aggregated_output",
|
|
1462
|
+
"output",
|
|
1463
|
+
"outputText",
|
|
1464
|
+
"output_text",
|
|
1465
|
+
"stdout",
|
|
1466
|
+
"stderr",
|
|
1467
|
+
"text",
|
|
1468
|
+
"message",
|
|
1469
|
+
"summary",
|
|
1470
|
+
]);
|
|
1471
|
+
if (aggregatedOutput?.trim()) {
|
|
1472
|
+
return aggregatedOutput.trim();
|
|
1473
|
+
}
|
|
1474
|
+
return this.decodeCommandExecutionItemText(itemObject, "commandexecution");
|
|
1475
|
+
}
|
|
1077
1476
|
decodeUserMessageParts(itemObject, threadId, itemId) {
|
|
1078
1477
|
const parts = [];
|
|
1079
1478
|
const content = this.readArray(itemObject.content);
|
|
@@ -1306,6 +1705,41 @@ export class CodexProvider {
|
|
|
1306
1705
|
?? this.readRawString(this.asRecord(payload.event).delta)
|
|
1307
1706
|
?? this.readRawString(this.asRecord(payload.event).message));
|
|
1308
1707
|
}
|
|
1708
|
+
extractStructuredUserInputQuestions(payload) {
|
|
1709
|
+
const rawQuestions = this.readArray(payload.questions);
|
|
1710
|
+
const questions = [];
|
|
1711
|
+
for (const value of rawQuestions) {
|
|
1712
|
+
const question = this.asRecord(value);
|
|
1713
|
+
const options = this.readArray(question.options)
|
|
1714
|
+
.map((option) => {
|
|
1715
|
+
const optionObject = this.asRecord(option);
|
|
1716
|
+
const label = this.readString(optionObject.label);
|
|
1717
|
+
const description = this.readString(optionObject.description);
|
|
1718
|
+
if (!label)
|
|
1719
|
+
return undefined;
|
|
1720
|
+
return {
|
|
1721
|
+
label,
|
|
1722
|
+
...(description ? { description } : {}),
|
|
1723
|
+
};
|
|
1724
|
+
})
|
|
1725
|
+
.filter((value) => Boolean(value));
|
|
1726
|
+
const id = this.readString(question.id);
|
|
1727
|
+
const header = this.readString(question.header);
|
|
1728
|
+
const prompt = this.readString(question.question);
|
|
1729
|
+
if (!id || !header || !prompt) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
questions.push({
|
|
1733
|
+
id,
|
|
1734
|
+
header,
|
|
1735
|
+
question: prompt,
|
|
1736
|
+
...(options.length > 0 ? { options } : {}),
|
|
1737
|
+
...(typeof question.isOther === "boolean" ? { isOther: question.isOther } : {}),
|
|
1738
|
+
...(typeof question.isSecret === "boolean" ? { isSecret: question.isSecret } : {}),
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
return questions;
|
|
1742
|
+
}
|
|
1309
1743
|
extractThreadId(payload) {
|
|
1310
1744
|
if (!payload || typeof payload !== "object")
|
|
1311
1745
|
return undefined;
|