ever-terminal 1.0.0

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.
@@ -0,0 +1,362 @@
1
+ import { listCodexSessions, getCodexSessionHistory } from "./storage.js";
2
+ import { CodexSession } from "./session.js";
3
+ import { sessionHasClients } from "../routes/events.js";
4
+ import { codexThreadStatus } from "./status.js";
5
+ export { codexThreadStatus } from "./status.js";
6
+ export function createCodexProvider(emit, getClient) {
7
+ const sessions = new Map();
8
+ const promptQueues = new Map();
9
+ const client = getClient();
10
+ // ── Subscription state ─────────────────────────────────
11
+ const subscribed = new Set();
12
+ const pending = new Set();
13
+ const subscribing = new Set();
14
+ const pendingRetryCounts = new Map();
15
+ const pendingRetryTimers = new Map();
16
+ const idleSince = new Map();
17
+ const IDLE_TTL_MS = 3 * 60 * 1000;
18
+ const MAX_SUBSCRIBE_FAILURES = 10;
19
+ const SUBSCRIBE_RETRY_DELAYS_MS = [2_000, 4_000, 8_000];
20
+ function isSessionNotFoundError(err) {
21
+ return (err?.rpcCode === -32004 ||
22
+ /(?:thread|session).*(?:not found|missing|unknown|not loaded)|not found.*(?:thread|session)/i.test(err?.message ?? ""));
23
+ }
24
+ function sessionNotFound(sessionId) {
25
+ return Object.assign(new Error(`Codex session not found: ${sessionId}`), { statusCode: 404 });
26
+ }
27
+ function markSubscribed(threadId) {
28
+ pending.delete(threadId);
29
+ pendingRetryCounts.delete(threadId);
30
+ subscribed.add(threadId);
31
+ }
32
+ function subscribe(threadId, retry = true) {
33
+ if (subscribed.has(threadId) || subscribing.has(threadId))
34
+ return;
35
+ const timer = pendingRetryTimers.get(threadId);
36
+ if (timer) {
37
+ clearTimeout(timer);
38
+ pendingRetryTimers.delete(threadId);
39
+ }
40
+ subscribing.add(threadId);
41
+ client
42
+ .threadResume({ threadId })
43
+ .then(() => {
44
+ subscribing.delete(threadId);
45
+ markSubscribed(threadId);
46
+ console.log(`[codex-provider] Subscribed to thread ${threadId}`);
47
+ })
48
+ .catch((err) => {
49
+ subscribing.delete(threadId);
50
+ subscribed.delete(threadId);
51
+ if (!retry) {
52
+ console.log(`[codex-provider] Subscribe skipped for ${threadId}: ${err.message}`);
53
+ return;
54
+ }
55
+ const failures = (pendingRetryCounts.get(threadId) ?? 0) + 1;
56
+ pendingRetryCounts.set(threadId, failures);
57
+ if (failures > MAX_SUBSCRIBE_FAILURES) {
58
+ pending.delete(threadId);
59
+ pendingRetryCounts.delete(threadId);
60
+ console.log(`[codex-provider] Subscribe gave up for ${threadId} after ${failures} failures: ${err.message}`);
61
+ return;
62
+ }
63
+ pending.add(threadId);
64
+ const delay = SUBSCRIBE_RETRY_DELAYS_MS[Math.min(failures - 1, SUBSCRIBE_RETRY_DELAYS_MS.length - 1)];
65
+ const retryTimer = setTimeout(() => {
66
+ pendingRetryTimers.delete(threadId);
67
+ if (pending.has(threadId))
68
+ subscribe(threadId);
69
+ }, delay);
70
+ pendingRetryTimers.set(threadId, retryTimer);
71
+ console.log(`[codex-provider] Subscribe deferred for ${threadId}: ${err.message}; retry ${failures}/${MAX_SUBSCRIBE_FAILURES} in ${delay / 1000}s`);
72
+ });
73
+ }
74
+ function unsubscribe(threadId) {
75
+ const timer = pendingRetryTimers.get(threadId);
76
+ if (timer)
77
+ clearTimeout(timer);
78
+ client.threadUnsubscribe(threadId).catch(() => { });
79
+ sessions.delete(threadId);
80
+ subscribed.delete(threadId);
81
+ pending.delete(threadId);
82
+ subscribing.delete(threadId);
83
+ pendingRetryCounts.delete(threadId);
84
+ pendingRetryTimers.delete(threadId);
85
+ idleSince.delete(threadId);
86
+ }
87
+ // Sweep: unsubscribe server-originated sessions idle >3min with no SSE clients
88
+ setInterval(() => {
89
+ const now = Date.now();
90
+ for (const [threadId, since] of idleSince) {
91
+ if (now - since < IDLE_TTL_MS)
92
+ continue;
93
+ if (sessionHasClients(threadId))
94
+ continue;
95
+ const session = sessions.get(threadId);
96
+ if (session && session.status !== "idle")
97
+ continue;
98
+ console.log(`[codex-provider] Unsubscribing idle thread ${threadId} (${Math.round((now - since) / 1000)}s)`);
99
+ unsubscribe(threadId);
100
+ }
101
+ }, 60_000);
102
+ // ── Prompt queue ───────────────────────────────────────
103
+ function dispatchNext(sessionId) {
104
+ const queue = promptQueues.get(sessionId);
105
+ if (!queue || queue.length === 0)
106
+ return;
107
+ const next = queue.shift();
108
+ if (queue.length === 0)
109
+ promptQueues.delete(sessionId);
110
+ const session = sessions.get(sessionId);
111
+ if (!session)
112
+ return;
113
+ console.log(`[codex-provider] Dispatching queued prompt for session ${sessionId}`);
114
+ session.run(next).catch((err) => {
115
+ console.error(`[codex-provider] Failed to dispatch queued prompt: ${err.message}`);
116
+ });
117
+ }
118
+ const wrappedEmit = (sessionId, msg) => {
119
+ emit(sessionId, msg);
120
+ if (msg.type === "status" && msg.state === "idle" && sessionId) {
121
+ idleSince.set(sessionId, Date.now());
122
+ dispatchNext(sessionId);
123
+ }
124
+ else if (msg.type === "status" && msg.state === "busy" && sessionId) {
125
+ idleSince.delete(sessionId);
126
+ }
127
+ };
128
+ // ── Session factory ────────────────────────────────────
129
+ function makeSession(sessionId) {
130
+ if (sessionId) {
131
+ const existing = sessions.get(sessionId);
132
+ if (existing)
133
+ return existing;
134
+ }
135
+ const session = new CodexSession(wrappedEmit, client);
136
+ session.onIdReady((sid) => {
137
+ if (!sessions.has(sid))
138
+ sessions.set(sid, session);
139
+ if (promptQueues.has("") && !promptQueues.has(sid)) {
140
+ const queue = promptQueues.get("");
141
+ promptQueues.delete("");
142
+ if (queue.length > 0) {
143
+ promptQueues.set(sid, queue);
144
+ if (!session.busy)
145
+ dispatchNext(sid);
146
+ }
147
+ }
148
+ });
149
+ if (sessionId)
150
+ sessions.set(sessionId, session);
151
+ return session;
152
+ }
153
+ // ── Notification routing ───────────────────────────────
154
+ function resolveThreadId(method, params) {
155
+ const p = params ?? {};
156
+ if (typeof p.threadId === "string" && p.threadId)
157
+ return p.threadId;
158
+ if (typeof p.thread?.id === "string" && p.thread.id)
159
+ return p.thread.id;
160
+ if (typeof p.turn?.threadId === "string" && p.turn.threadId)
161
+ return p.turn.threadId;
162
+ if (typeof p.item?.threadId === "string" && p.item.threadId)
163
+ return p.item.threadId;
164
+ if ((method === "error" || method === "turn/completed") &&
165
+ sessions.size === 1) {
166
+ return [...sessions.keys()][0];
167
+ }
168
+ return undefined;
169
+ }
170
+ function resolveThreadCwd(params) {
171
+ const p = params ?? {};
172
+ if (typeof p.cwd === "string" && p.cwd)
173
+ return p.cwd;
174
+ if (typeof p.thread?.cwd === "string" && p.thread.cwd)
175
+ return p.thread.cwd;
176
+ if (typeof p.turn?.cwd === "string" && p.turn.cwd)
177
+ return p.turn.cwd;
178
+ if (typeof p.item?.cwd === "string" && p.item.cwd)
179
+ return p.item.cwd;
180
+ return undefined;
181
+ }
182
+ client.handleNotification = (method, params) => {
183
+ const threadId = resolveThreadId(method, params);
184
+ if (!threadId) {
185
+ if (!method.startsWith("account/")) {
186
+ console.error(`[codex-provider] notification missing threadId: method=${method}`);
187
+ }
188
+ return;
189
+ }
190
+ if (method === "thread/closed") {
191
+ if (sessions.has(threadId)) {
192
+ console.log(`[codex-provider] Thread ${threadId} closed, unsubscribing`);
193
+ unsubscribe(threadId);
194
+ }
195
+ return;
196
+ }
197
+ if (!sessions.has(threadId)) {
198
+ console.log(`[codex-provider] Discovered thread ${threadId}`);
199
+ const session = new CodexSession(wrappedEmit, client);
200
+ session.start(threadId, resolveThreadCwd(params)).catch(() => { });
201
+ sessions.set(threadId, session);
202
+ subscribe(threadId);
203
+ }
204
+ sessions.get(threadId).handleNotification(method, params);
205
+ };
206
+ client.handleServerRequest = (requestId, method, params) => {
207
+ const threadId = typeof params?.threadId === "string" ? params.threadId : undefined;
208
+ if (!threadId) {
209
+ console.error(`[codex-provider] server request missing threadId: id=${String(requestId)} method=${method}`);
210
+ return;
211
+ }
212
+ sessions
213
+ .get(threadId)
214
+ ?.handleServerRequest(requestId, method, params);
215
+ };
216
+ client.handleClose = (error) => {
217
+ for (const session of sessions.values()) {
218
+ session.handleClientClose(error);
219
+ }
220
+ };
221
+ // ── API handlers ───────────────────────────────────────
222
+ async function getSessionStatus(sessionId) {
223
+ const session = sessions.get(sessionId);
224
+ if (session)
225
+ return session.status;
226
+ try {
227
+ const thread = await client.threadRead(sessionId, false);
228
+ return codexThreadStatus(thread);
229
+ }
230
+ catch {
231
+ return "idle";
232
+ }
233
+ }
234
+ async function listSessions(limit, cwd) {
235
+ const results = await listCodexSessions(client, limit, cwd);
236
+ return results.map((s) => ({
237
+ ...s,
238
+ provider: "codex",
239
+ status: s.status ?? null,
240
+ }));
241
+ }
242
+ async function getInfo() {
243
+ const { exec } = await import("node:child_process");
244
+ const { promisify } = await import("node:util");
245
+ const execAsync = promisify(exec);
246
+ let version = "";
247
+ try {
248
+ const { stdout } = await execAsync("codex --version", {
249
+ timeout: 3000,
250
+ });
251
+ version = stdout.trim();
252
+ }
253
+ catch { }
254
+ let account = {};
255
+ try {
256
+ const acct = await client.getAccount();
257
+ if (acct) {
258
+ account = {
259
+ email: acct.email ?? "",
260
+ planType: acct.planType ?? "",
261
+ type: acct.type ?? "",
262
+ };
263
+ }
264
+ }
265
+ catch { }
266
+ return {
267
+ account,
268
+ model: "Codex",
269
+ version: version || "Unknown",
270
+ provider: "codex",
271
+ };
272
+ }
273
+ async function getHistory(sessionId, limit) {
274
+ return getCodexSessionHistory(client, sessionId, limit);
275
+ }
276
+ async function prompt(sessionId, text, cwd) {
277
+ let session;
278
+ if (sessionId)
279
+ session = sessions.get(sessionId);
280
+ if (!session) {
281
+ if (sessionId) {
282
+ try {
283
+ const thread = await client.threadRead(sessionId, false);
284
+ if (!thread)
285
+ throw sessionNotFound(sessionId);
286
+ }
287
+ catch (err) {
288
+ if (err?.statusCode === 404 || isSessionNotFoundError(err))
289
+ throw sessionNotFound(sessionId);
290
+ throw err;
291
+ }
292
+ }
293
+ session = makeSession(sessionId);
294
+ await session.start(sessionId, cwd);
295
+ }
296
+ if (session.busy) {
297
+ const queueKey = session.id || sessionId || "";
298
+ const queue = promptQueues.get(queueKey) || [];
299
+ queue.push(text);
300
+ promptQueues.set(queueKey, queue);
301
+ console.log(`[codex-provider] Queued prompt for session ${queueKey} (queue size: ${queue.length})`);
302
+ }
303
+ else {
304
+ try {
305
+ await session.run(text);
306
+ if (session.id)
307
+ markSubscribed(session.id);
308
+ }
309
+ catch (err) {
310
+ if (sessionId && isSessionNotFoundError(err)) {
311
+ sessions.delete(sessionId);
312
+ throw sessionNotFound(sessionId);
313
+ }
314
+ throw err;
315
+ }
316
+ }
317
+ const resolvedId = session.id ??
318
+ (await session.waitForId(10000).catch(() => null)) ??
319
+ "";
320
+ return { sessionId: resolvedId, provider: "codex" };
321
+ }
322
+ function respondPermission(sessionId, decision) {
323
+ sessions.get(sessionId)?.respondPermission(decision);
324
+ }
325
+ function respondQuestion(sessionId, answer) {
326
+ sessions.get(sessionId)?.respondQuestion(answer);
327
+ }
328
+ function interrupt(sessionId) {
329
+ sessions.get(sessionId)?.interrupt();
330
+ }
331
+ function getStatus(sessionId) {
332
+ const session = sessions.get(sessionId);
333
+ if (!session)
334
+ return null;
335
+ return { state: session.status, provider: "codex" };
336
+ }
337
+ function getSubscribedSessions() {
338
+ const now = Date.now();
339
+ const result = [];
340
+ for (const [threadId, session] of sessions) {
341
+ const idle = idleSince.get(threadId);
342
+ result.push({
343
+ threadId,
344
+ status: session.status,
345
+ idleSinceMs: idle != null ? now - idle : null,
346
+ });
347
+ }
348
+ return result;
349
+ }
350
+ return {
351
+ listSessions,
352
+ getSessionStatus,
353
+ getInfo,
354
+ getHistory,
355
+ prompt,
356
+ respondPermission,
357
+ respondQuestion,
358
+ interrupt,
359
+ getStatus,
360
+ getSubscribedSessions,
361
+ };
362
+ }