pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* driver-codex.ts — Codex CLI agent driver.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execSync, spawn } from 'node:child_process';
|
|
7
|
+
import { registerDriver } from './agent-driver.js';
|
|
8
|
+
import { terminateProcessTree } from './process-control.js';
|
|
9
|
+
import {
|
|
10
|
+
// shared helpers
|
|
11
|
+
agentLog, detectAgentBin, buildStreamPreviewMeta, pushRecentActivity, normalizeActivityLine, shortValue, numberOrNull, IMAGE_EXTS, listPikiclawSessions, isPendingSessionId, stripInjectedPrompts, computeContext, readTailLines, roundPercent, toIsoFromEpochSeconds, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, emptyUsage, Q, } from './code-agent.js';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// App-server JSON-RPC client
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const CODEX_APPSERVER_SPAWN_TIMEOUT_MS = 15_000;
|
|
16
|
+
export class CodexAppServer {
|
|
17
|
+
proc = null;
|
|
18
|
+
buf = '';
|
|
19
|
+
nextId = 1;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
notificationHandlers = new Set();
|
|
22
|
+
ready = false;
|
|
23
|
+
startPromise = null;
|
|
24
|
+
configOverrides = [];
|
|
25
|
+
async ensureRunning(extraConfig) {
|
|
26
|
+
if (this.ready && this.proc && !this.proc.killed)
|
|
27
|
+
return true;
|
|
28
|
+
if (this.startPromise)
|
|
29
|
+
return this.startPromise;
|
|
30
|
+
this.configOverrides = extraConfig ?? [];
|
|
31
|
+
this.startPromise = this._start();
|
|
32
|
+
const ok = await this.startPromise;
|
|
33
|
+
this.startPromise = null;
|
|
34
|
+
return ok;
|
|
35
|
+
}
|
|
36
|
+
_start() {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const timer = setTimeout(() => { this.kill(); resolve(false); }, CODEX_APPSERVER_SPAWN_TIMEOUT_MS);
|
|
39
|
+
const args = ['app-server'];
|
|
40
|
+
for (const c of this.configOverrides)
|
|
41
|
+
args.push('-c', c);
|
|
42
|
+
agentLog(`[codex-rpc] spawning: codex ${args.join(' ')}`);
|
|
43
|
+
const proc = spawn('codex', args, {
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
detached: process.platform !== 'win32',
|
|
46
|
+
});
|
|
47
|
+
this.proc = proc;
|
|
48
|
+
this.buf = '';
|
|
49
|
+
this.nextId = 1;
|
|
50
|
+
this.pending.clear();
|
|
51
|
+
this.ready = false;
|
|
52
|
+
proc.stderr?.on('data', (c) => { agentLog(`[codex-rpc][stderr] ${c.toString().trim().slice(0, 200)}`); });
|
|
53
|
+
proc.stdout.on('data', (chunk) => {
|
|
54
|
+
this.buf += chunk.toString('utf-8');
|
|
55
|
+
const lines = this.buf.split('\n');
|
|
56
|
+
this.buf = lines.pop() || '';
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (!line.trim())
|
|
59
|
+
continue;
|
|
60
|
+
let msg;
|
|
61
|
+
try {
|
|
62
|
+
msg = JSON.parse(line);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (msg.id != null) {
|
|
68
|
+
const cb = this.pending.get(msg.id);
|
|
69
|
+
if (cb) {
|
|
70
|
+
this.pending.delete(msg.id);
|
|
71
|
+
cb(msg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (msg.method && msg.id == null) {
|
|
75
|
+
for (const handler of [...this.notificationHandlers])
|
|
76
|
+
handler(msg.method, msg.params ?? {});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
proc.on('error', () => { clearTimeout(timer); this.ready = false; resolve(false); });
|
|
81
|
+
proc.on('close', () => { this.ready = false; this.proc = null; });
|
|
82
|
+
this.call('initialize', { clientInfo: { name: 'pikiclaw', version: '0.2.0' } })
|
|
83
|
+
.then(resp => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
if (resp.error) {
|
|
86
|
+
agentLog(`[codex-rpc] init error: ${resp.error.message}`);
|
|
87
|
+
resolve(false);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.ready = true;
|
|
91
|
+
agentLog(`[codex-rpc] initialized`);
|
|
92
|
+
resolve(true);
|
|
93
|
+
})
|
|
94
|
+
.catch(() => { clearTimeout(timer); resolve(false); });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
call(method, params) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
if (!this.proc || this.proc.killed) {
|
|
100
|
+
resolve({ error: { message: 'not connected' } });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const id = this.nextId++;
|
|
104
|
+
this.pending.set(id, resolve);
|
|
105
|
+
const msg = { jsonrpc: '2.0', id, method };
|
|
106
|
+
if (params !== undefined)
|
|
107
|
+
msg.params = params;
|
|
108
|
+
try {
|
|
109
|
+
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
resolve({ error: { message: 'write failed' } });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
notify(method, params) {
|
|
117
|
+
if (!this.proc || this.proc.killed)
|
|
118
|
+
return;
|
|
119
|
+
const msg = { jsonrpc: '2.0', method };
|
|
120
|
+
if (params !== undefined)
|
|
121
|
+
msg.params = params;
|
|
122
|
+
try {
|
|
123
|
+
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
}
|
|
127
|
+
onNotification(handler) {
|
|
128
|
+
this.notificationHandlers.add(handler);
|
|
129
|
+
return () => { this.notificationHandlers.delete(handler); };
|
|
130
|
+
}
|
|
131
|
+
offNotification(handler) {
|
|
132
|
+
if (!handler) {
|
|
133
|
+
this.notificationHandlers.clear();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.notificationHandlers.delete(handler);
|
|
137
|
+
}
|
|
138
|
+
kill() {
|
|
139
|
+
terminateProcessTree(this.proc, { signal: 'SIGTERM', forceSignal: 'SIGKILL', forceAfterMs: 2000 });
|
|
140
|
+
this.proc = null;
|
|
141
|
+
this.ready = false;
|
|
142
|
+
this.pending.clear();
|
|
143
|
+
this.notificationHandlers.clear();
|
|
144
|
+
}
|
|
145
|
+
get isRunning() { return this.ready && !!this.proc && !this.proc.killed; }
|
|
146
|
+
}
|
|
147
|
+
/** Singleton app-server for shared operations (sessions, models, usage). */
|
|
148
|
+
let _sharedServer = null;
|
|
149
|
+
function getSharedServer() {
|
|
150
|
+
if (!_sharedServer)
|
|
151
|
+
_sharedServer = new CodexAppServer();
|
|
152
|
+
return _sharedServer;
|
|
153
|
+
}
|
|
154
|
+
export function shutdownCodexServer() {
|
|
155
|
+
_sharedServer?.kill();
|
|
156
|
+
_sharedServer = null;
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Effort mapping
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
const EFFORT_MAP = {
|
|
162
|
+
low: 'low', medium: 'medium', high: 'high', min: 'minimal', max: 'xhigh',
|
|
163
|
+
};
|
|
164
|
+
function mapEffort(effort) { return EFFORT_MAP[effort] ?? effort; }
|
|
165
|
+
function isCodexToolCallItem(item) {
|
|
166
|
+
return item?.type === 'dynamicToolCall' || item?.type === 'mcpToolCall' || item?.type === 'collabAgentToolCall';
|
|
167
|
+
}
|
|
168
|
+
function codexToolKind(name) {
|
|
169
|
+
const raw = typeof name === 'string' ? name.trim() : '';
|
|
170
|
+
if (!raw)
|
|
171
|
+
return 'tool';
|
|
172
|
+
const parts = raw.split('.');
|
|
173
|
+
return parts[parts.length - 1] || raw;
|
|
174
|
+
}
|
|
175
|
+
function compactPathTarget(value, max = 80) {
|
|
176
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
177
|
+
if (!raw)
|
|
178
|
+
return '';
|
|
179
|
+
const normalized = raw.replace(/\\/g, '/');
|
|
180
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
181
|
+
const compact = parts.length >= 2 ? parts.slice(-2).join('/') : normalized;
|
|
182
|
+
if (compact.length <= max)
|
|
183
|
+
return compact;
|
|
184
|
+
return `...${compact.slice(-(max - 3))}`;
|
|
185
|
+
}
|
|
186
|
+
function summarizeCodexToolCall(item) {
|
|
187
|
+
const kind = codexToolKind(item?.tool);
|
|
188
|
+
switch (kind) {
|
|
189
|
+
case 'apply_patch': return { kind, summary: 'Edit files' };
|
|
190
|
+
case 'exec_command': return { kind, summary: 'Run shell command' };
|
|
191
|
+
case 'update_plan': return { kind, summary: 'Update plan' };
|
|
192
|
+
case 'request_user_input': return { kind, summary: 'Request user input' };
|
|
193
|
+
case 'view_image': return { kind, summary: 'Inspect image' };
|
|
194
|
+
case 'parallel': return { kind, summary: 'Run multiple tools' };
|
|
195
|
+
default: {
|
|
196
|
+
const label = shortValue(kind.replace(/_/g, ' '), 80);
|
|
197
|
+
return label ? { kind, summary: `Use ${label}` } : null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function summarizeCodexFileChange(item) {
|
|
202
|
+
const changes = Array.isArray(item?.changes) ? item.changes : [];
|
|
203
|
+
const paths = changes.map((c) => compactPathTarget(c?.path, 90)).filter(Boolean);
|
|
204
|
+
if (paths.length === 1)
|
|
205
|
+
return `Updated ${paths[0]}`;
|
|
206
|
+
if (paths.length > 1)
|
|
207
|
+
return `Updated ${paths.length} files`;
|
|
208
|
+
return 'Updated files';
|
|
209
|
+
}
|
|
210
|
+
function isCodexToolCallFailure(item) {
|
|
211
|
+
if (!item || !isCodexToolCallItem(item))
|
|
212
|
+
return false;
|
|
213
|
+
return item.success === false || !!item.error || item.status === 'failed' || item.status === 'error';
|
|
214
|
+
}
|
|
215
|
+
function buildCodexActivityPreview(s) {
|
|
216
|
+
const lines = [...s.recentNarrative];
|
|
217
|
+
for (const text of s.commentaryByItem.values()) {
|
|
218
|
+
const cleaned = normalizeActivityLine(text);
|
|
219
|
+
if (cleaned && lines[lines.length - 1] !== cleaned)
|
|
220
|
+
lines.push(cleaned);
|
|
221
|
+
}
|
|
222
|
+
for (const failure of s.recentFailures) {
|
|
223
|
+
if (lines[lines.length - 1] !== failure)
|
|
224
|
+
lines.push(failure);
|
|
225
|
+
}
|
|
226
|
+
if (s.completedCommands > 0)
|
|
227
|
+
lines.push(s.completedCommands === 1 ? 'Executed 1 command.' : `Executed ${s.completedCommands} commands.`);
|
|
228
|
+
if (s.activeCommands.size > 0)
|
|
229
|
+
lines.push(s.activeCommands.size === 1 ? 'Running 1 command...' : `Running ${s.activeCommands.size} commands...`);
|
|
230
|
+
for (const tool of s.activeToolCalls.values()) {
|
|
231
|
+
const running = tool.summary.endsWith('...') ? tool.summary : `${tool.summary}...`;
|
|
232
|
+
if (lines[lines.length - 1] !== running)
|
|
233
|
+
lines.push(running);
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Token usage
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
function buildCodexCumulativeUsage(raw) {
|
|
241
|
+
if (!raw || typeof raw !== 'object')
|
|
242
|
+
return null;
|
|
243
|
+
const input = numberOrNull(raw.inputTokens, raw.input_tokens);
|
|
244
|
+
const output = numberOrNull(raw.outputTokens, raw.output_tokens);
|
|
245
|
+
const cached = numberOrNull(raw.cachedInputTokens, raw.cached_input_tokens);
|
|
246
|
+
if (input == null && output == null && cached == null)
|
|
247
|
+
return null;
|
|
248
|
+
return { input: input ?? 0, output: output ?? 0, cached: cached ?? 0 };
|
|
249
|
+
}
|
|
250
|
+
function applyCodexTokenUsage(s, rawUsage, prev) {
|
|
251
|
+
if (!rawUsage || typeof rawUsage !== 'object')
|
|
252
|
+
return;
|
|
253
|
+
const last = rawUsage.last;
|
|
254
|
+
const lastInput = numberOrNull(last?.inputTokens, last?.input_tokens);
|
|
255
|
+
const lastOutput = numberOrNull(last?.outputTokens, last?.output_tokens);
|
|
256
|
+
const lastCached = numberOrNull(last?.cachedInputTokens, last?.cached_input_tokens);
|
|
257
|
+
const lastCacheCreation = numberOrNull(last?.cacheCreationInputTokens, last?.cache_creation_input_tokens);
|
|
258
|
+
if (lastInput != null)
|
|
259
|
+
s.inputTokens = lastInput;
|
|
260
|
+
if (lastOutput != null)
|
|
261
|
+
s.outputTokens = lastOutput;
|
|
262
|
+
if (lastCached != null)
|
|
263
|
+
s.cachedInputTokens = lastCached;
|
|
264
|
+
if (lastCacheCreation != null)
|
|
265
|
+
s.cacheCreationInputTokens = lastCacheCreation;
|
|
266
|
+
const total = buildCodexCumulativeUsage(rawUsage.total ?? rawUsage);
|
|
267
|
+
if (total) {
|
|
268
|
+
s.codexCumulative = total;
|
|
269
|
+
if (lastInput == null)
|
|
270
|
+
s.inputTokens = prev ? Math.max(0, total.input - prev.input) : total.input;
|
|
271
|
+
if (lastOutput == null)
|
|
272
|
+
s.outputTokens = prev ? Math.max(0, total.output - prev.output) : total.output;
|
|
273
|
+
if (lastCached == null)
|
|
274
|
+
s.cachedInputTokens = prev ? Math.max(0, total.cached - prev.cached) : total.cached;
|
|
275
|
+
}
|
|
276
|
+
const contextWindow = numberOrNull(rawUsage.modelContextWindow, rawUsage.model_context_window);
|
|
277
|
+
if (contextWindow != null && contextWindow > 0)
|
|
278
|
+
s.contextWindow = contextWindow;
|
|
279
|
+
}
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Turn input
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
export function buildCodexTurnInput(prompt, attachments) {
|
|
284
|
+
const input = [];
|
|
285
|
+
for (const filePath of attachments) {
|
|
286
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
287
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
288
|
+
input.push({ type: 'localImage', path: filePath });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
input.push({ type: 'text', text: `[Attached file: ${filePath}]` });
|
|
292
|
+
}
|
|
293
|
+
input.push({ type: 'text', text: prompt });
|
|
294
|
+
return input;
|
|
295
|
+
}
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Stream via app-server
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
export async function doCodexStream(opts) {
|
|
300
|
+
const start = Date.now();
|
|
301
|
+
const srv = new CodexAppServer();
|
|
302
|
+
let timedOut = false;
|
|
303
|
+
let unsubscribeNotifications = () => { };
|
|
304
|
+
try {
|
|
305
|
+
const config = [];
|
|
306
|
+
if (opts.codexExtraArgs?.length) {
|
|
307
|
+
for (let i = 0; i < opts.codexExtraArgs.length; i++) {
|
|
308
|
+
if (opts.codexExtraArgs[i] === '-c' && opts.codexExtraArgs[i + 1])
|
|
309
|
+
config.push(opts.codexExtraArgs[++i]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!(await srv.ensureRunning(config))) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false, message: 'Failed to start codex app-server.', thinking: null,
|
|
315
|
+
sessionId: opts.sessionId, workspacePath: null,
|
|
316
|
+
model: opts.model, thinkingEffort: opts.thinkingEffort,
|
|
317
|
+
elapsedS: (Date.now() - start) / 1000, inputTokens: null, outputTokens: null,
|
|
318
|
+
cachedInputTokens: null, cacheCreationInputTokens: null, contextWindow: null,
|
|
319
|
+
contextUsedTokens: null, contextPercent: null, error: 'Failed to start codex app-server.',
|
|
320
|
+
codexCumulative: null, stopReason: null, incomplete: true, activity: null,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const s = {
|
|
324
|
+
sessionId: opts.sessionId,
|
|
325
|
+
text: '', thinking: '', activity: '', msgs: [], thinkParts: [],
|
|
326
|
+
model: opts.model, thinkingEffort: opts.thinkingEffort,
|
|
327
|
+
inputTokens: null, outputTokens: null,
|
|
328
|
+
cachedInputTokens: null, cacheCreationInputTokens: null,
|
|
329
|
+
contextWindow: null, codexCumulative: null,
|
|
330
|
+
turnId: null, turnStatus: null, turnError: null,
|
|
331
|
+
messagePhases: new Map(),
|
|
332
|
+
commentaryByItem: new Map(),
|
|
333
|
+
activeCommands: new Map(),
|
|
334
|
+
activeToolCalls: new Map(),
|
|
335
|
+
recentNarrative: [], recentFailures: [],
|
|
336
|
+
completedCommands: 0,
|
|
337
|
+
plan: null,
|
|
338
|
+
};
|
|
339
|
+
// thread/start or thread/resume
|
|
340
|
+
let threadResp;
|
|
341
|
+
const threadParams = {
|
|
342
|
+
cwd: opts.workdir,
|
|
343
|
+
model: opts.codexModel || null,
|
|
344
|
+
approvalPolicy: opts.codexFullAccess ? 'never' : undefined,
|
|
345
|
+
sandbox: opts.codexFullAccess ? 'danger-full-access' : undefined,
|
|
346
|
+
developerInstructions: opts.codexDeveloperInstructions || undefined,
|
|
347
|
+
};
|
|
348
|
+
if (opts.sessionId) {
|
|
349
|
+
agentLog(`[codex-rpc] thread/resume id=${opts.sessionId}`);
|
|
350
|
+
threadResp = await srv.call('thread/resume', { threadId: opts.sessionId, ...threadParams });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
agentLog(`[codex-rpc] thread/start cwd=${opts.workdir} model=${opts.codexModel || '(default)'}`);
|
|
354
|
+
threadResp = await srv.call('thread/start', threadParams);
|
|
355
|
+
}
|
|
356
|
+
if (threadResp.error) {
|
|
357
|
+
const errMsg = threadResp.error.message || 'thread/start failed';
|
|
358
|
+
agentLog(`[codex-rpc] thread error: ${errMsg}`);
|
|
359
|
+
return {
|
|
360
|
+
ok: false, message: errMsg, thinking: null,
|
|
361
|
+
sessionId: opts.sessionId, workspacePath: null,
|
|
362
|
+
model: opts.model, thinkingEffort: opts.thinkingEffort,
|
|
363
|
+
elapsedS: (Date.now() - start) / 1000, inputTokens: null, outputTokens: null,
|
|
364
|
+
cachedInputTokens: null, cacheCreationInputTokens: null, contextWindow: null,
|
|
365
|
+
contextUsedTokens: null, contextPercent: null, error: errMsg,
|
|
366
|
+
codexCumulative: null, stopReason: null, incomplete: true, activity: null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const threadResult = threadResp.result;
|
|
370
|
+
s.sessionId = threadResult.thread?.id ?? s.sessionId;
|
|
371
|
+
s.model = threadResult.model ?? s.model;
|
|
372
|
+
agentLog(`[codex-rpc] thread ready: id=${s.sessionId} model=${s.model}`);
|
|
373
|
+
// turn/start
|
|
374
|
+
const input = buildCodexTurnInput(opts.prompt, opts.attachments || []);
|
|
375
|
+
const turnDone = new Promise((resolve) => {
|
|
376
|
+
const deadline = start + opts.timeout * 1000;
|
|
377
|
+
const hardTimer = setTimeout(() => {
|
|
378
|
+
timedOut = true;
|
|
379
|
+
agentLog(`[codex-rpc] timeout: interrupting turn`);
|
|
380
|
+
if (s.turnId && s.sessionId)
|
|
381
|
+
srv.call('turn/interrupt', { threadId: s.sessionId, turnId: s.turnId }).catch(() => { });
|
|
382
|
+
resolve();
|
|
383
|
+
}, opts.timeout * 1000 + 5_000);
|
|
384
|
+
const emit = () => {
|
|
385
|
+
s.activity = buildCodexActivityPreview(s);
|
|
386
|
+
opts.onText(s.text, s.thinking, s.activity, buildStreamPreviewMeta(s), s.plan);
|
|
387
|
+
};
|
|
388
|
+
const handleNotification = (method, params) => {
|
|
389
|
+
if (Date.now() > deadline)
|
|
390
|
+
return;
|
|
391
|
+
if (method === 'item/started' && params.threadId === s.sessionId) {
|
|
392
|
+
const item = params.item || {};
|
|
393
|
+
if (item.type === 'agentMessage' && item.id) {
|
|
394
|
+
const phase = item.phase || 'final_answer';
|
|
395
|
+
s.messagePhases.set(item.id, phase);
|
|
396
|
+
if (phase !== 'final_answer') {
|
|
397
|
+
s.commentaryByItem.set(item.id, item.text || '');
|
|
398
|
+
emit();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (item.type === 'commandExecution' && item.id && item.command) {
|
|
402
|
+
s.activeCommands.set(item.id, item.command);
|
|
403
|
+
emit();
|
|
404
|
+
}
|
|
405
|
+
if (item.id && isCodexToolCallItem(item)) {
|
|
406
|
+
const toolCall = summarizeCodexToolCall(item);
|
|
407
|
+
if (toolCall) {
|
|
408
|
+
s.activeToolCalls.set(item.id, toolCall);
|
|
409
|
+
emit();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (method === 'item/agentMessage/delta' && params.threadId === s.sessionId) {
|
|
414
|
+
const delta = params.delta || '';
|
|
415
|
+
const phase = params.itemId ? (s.messagePhases.get(params.itemId) || 'final_answer') : 'final_answer';
|
|
416
|
+
if (phase === 'final_answer')
|
|
417
|
+
s.text += delta;
|
|
418
|
+
else if (params.itemId) {
|
|
419
|
+
const prev = s.commentaryByItem.get(params.itemId) || '';
|
|
420
|
+
s.commentaryByItem.set(params.itemId, prev + delta);
|
|
421
|
+
}
|
|
422
|
+
emit();
|
|
423
|
+
}
|
|
424
|
+
if ((method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') && params.threadId === s.sessionId) {
|
|
425
|
+
s.thinking += params.delta || '';
|
|
426
|
+
emit();
|
|
427
|
+
}
|
|
428
|
+
if (method === 'item/completed' && params.threadId === s.sessionId) {
|
|
429
|
+
const item = params.item || {};
|
|
430
|
+
if (item.type === 'agentMessage' && item.id) {
|
|
431
|
+
const phase = item.phase || s.messagePhases.get(item.id) || 'final_answer';
|
|
432
|
+
if (phase === 'final_answer') {
|
|
433
|
+
if (item.text?.trim())
|
|
434
|
+
s.msgs.push(item.text.trim());
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const commentary = item.text?.trim() || s.commentaryByItem.get(item.id)?.trim() || '';
|
|
438
|
+
if (commentary)
|
|
439
|
+
pushRecentActivity(s.recentNarrative, commentary);
|
|
440
|
+
s.commentaryByItem.delete(item.id);
|
|
441
|
+
emit();
|
|
442
|
+
}
|
|
443
|
+
s.messagePhases.delete(item.id);
|
|
444
|
+
}
|
|
445
|
+
if (item.type === 'reasoning') {
|
|
446
|
+
const parts = [...(item.summary || []), ...(item.content || [])];
|
|
447
|
+
const text = parts.join('\n').trim();
|
|
448
|
+
if (text) {
|
|
449
|
+
s.thinkParts.push(text);
|
|
450
|
+
emit();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (item.type === 'commandExecution' && item.id) {
|
|
454
|
+
const cmd = item.command || s.activeCommands.get(item.id) || '';
|
|
455
|
+
s.activeCommands.delete(item.id);
|
|
456
|
+
if (cmd) {
|
|
457
|
+
const exitCode = typeof item.exitCode === 'number' ? item.exitCode : null;
|
|
458
|
+
if (exitCode != null && exitCode !== 0)
|
|
459
|
+
pushRecentActivity(s.recentFailures, `Command failed (${exitCode}): ${cmd}`, 4);
|
|
460
|
+
else
|
|
461
|
+
s.completedCommands++;
|
|
462
|
+
}
|
|
463
|
+
emit();
|
|
464
|
+
}
|
|
465
|
+
if (item.id && isCodexToolCallItem(item)) {
|
|
466
|
+
const toolCall = s.activeToolCalls.get(item.id) || summarizeCodexToolCall(item);
|
|
467
|
+
s.activeToolCalls.delete(item.id);
|
|
468
|
+
if (toolCall) {
|
|
469
|
+
if (isCodexToolCallFailure(item))
|
|
470
|
+
pushRecentActivity(s.recentFailures, `${toolCall.summary} failed`, 4);
|
|
471
|
+
else if (toolCall.kind !== 'apply_patch')
|
|
472
|
+
pushRecentActivity(s.recentNarrative, `${toolCall.summary} done`);
|
|
473
|
+
}
|
|
474
|
+
emit();
|
|
475
|
+
}
|
|
476
|
+
if (item.type === 'fileChange') {
|
|
477
|
+
pushRecentActivity(s.recentNarrative, summarizeCodexFileChange(item));
|
|
478
|
+
emit();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (method === 'thread/tokenUsage/updated' && params.threadId === s.sessionId) {
|
|
482
|
+
applyCodexTokenUsage(s, params.tokenUsage, opts.codexPrevCumulative);
|
|
483
|
+
emit();
|
|
484
|
+
}
|
|
485
|
+
if (method === 'turn/plan/updated' && params.threadId === s.sessionId) {
|
|
486
|
+
const rawPlan = Array.isArray(params.plan) ? params.plan : [];
|
|
487
|
+
s.plan = {
|
|
488
|
+
explanation: typeof params.explanation === 'string' ? params.explanation : null,
|
|
489
|
+
steps: rawPlan
|
|
490
|
+
.map((entry) => ({
|
|
491
|
+
step: typeof entry?.step === 'string' ? entry.step : '',
|
|
492
|
+
status: entry?.status === 'completed' || entry?.status === 'pending' || entry?.status === 'inProgress' ? entry.status : 'pending',
|
|
493
|
+
}))
|
|
494
|
+
.filter((entry) => entry.step.trim()),
|
|
495
|
+
};
|
|
496
|
+
emit();
|
|
497
|
+
}
|
|
498
|
+
if (method === 'turn/completed' && params.threadId === s.sessionId) {
|
|
499
|
+
const turn = params.turn || {};
|
|
500
|
+
applyCodexTokenUsage(s, params.tokenUsage || turn.tokenUsage || turn.usage, opts.codexPrevCumulative);
|
|
501
|
+
s.turnStatus = turn.status ?? null;
|
|
502
|
+
if (turn.error)
|
|
503
|
+
s.turnError = turn.error.message || turn.error.code || JSON.stringify(turn.error);
|
|
504
|
+
s.turnId = turn.id ?? s.turnId;
|
|
505
|
+
clearTimeout(hardTimer);
|
|
506
|
+
resolve();
|
|
507
|
+
}
|
|
508
|
+
if (method === 'turn/started' && params.threadId === s.sessionId)
|
|
509
|
+
s.turnId = params.turn?.id ?? null;
|
|
510
|
+
if (method === 'model/rerouted' && params.threadId === s.sessionId)
|
|
511
|
+
s.model = params.model ?? s.model;
|
|
512
|
+
};
|
|
513
|
+
unsubscribeNotifications = srv.onNotification(handleNotification);
|
|
514
|
+
});
|
|
515
|
+
// Log equivalent CLI command for reproducibility
|
|
516
|
+
const cliParts = ['codex'];
|
|
517
|
+
if (opts.codexModel)
|
|
518
|
+
cliParts.push('--model', opts.codexModel);
|
|
519
|
+
if (opts.codexFullAccess)
|
|
520
|
+
cliParts.push('--full-access');
|
|
521
|
+
const effort = mapEffort(opts.thinkingEffort);
|
|
522
|
+
if (effort)
|
|
523
|
+
cliParts.push('--effort', effort);
|
|
524
|
+
if (opts.sessionId)
|
|
525
|
+
cliParts.push('--resume', opts.sessionId);
|
|
526
|
+
if (opts.codexExtraArgs?.length)
|
|
527
|
+
cliParts.push(...opts.codexExtraArgs);
|
|
528
|
+
cliParts.push('-p', `"${opts.prompt.slice(0, 300)}${opts.prompt.length > 300 ? '…' : ''}"`);
|
|
529
|
+
agentLog(`[codex-rpc] full command: cd ${Q(opts.workdir)} && ${cliParts.join(' ')}`);
|
|
530
|
+
agentLog(`[codex-rpc] turn/start prompt="${opts.prompt.slice(0, 300)}${opts.prompt.length > 300 ? '…' : ''}" effort=${effort}`);
|
|
531
|
+
const turnResp = await srv.call('turn/start', {
|
|
532
|
+
threadId: s.sessionId, input,
|
|
533
|
+
model: opts.codexModel || undefined,
|
|
534
|
+
effort: mapEffort(opts.thinkingEffort),
|
|
535
|
+
});
|
|
536
|
+
if (turnResp.error) {
|
|
537
|
+
unsubscribeNotifications();
|
|
538
|
+
const errMsg = turnResp.error.message || 'turn/start failed';
|
|
539
|
+
agentLog(`[codex-rpc] turn/start error: ${errMsg}`);
|
|
540
|
+
return {
|
|
541
|
+
ok: false, message: errMsg, thinking: null,
|
|
542
|
+
sessionId: s.sessionId, workspacePath: null,
|
|
543
|
+
model: s.model, thinkingEffort: s.thinkingEffort,
|
|
544
|
+
elapsedS: (Date.now() - start) / 1000, inputTokens: null, outputTokens: null,
|
|
545
|
+
cachedInputTokens: null, cacheCreationInputTokens: null, contextWindow: null,
|
|
546
|
+
contextUsedTokens: null, contextPercent: null, error: errMsg,
|
|
547
|
+
codexCumulative: null, stopReason: null, incomplete: true, activity: null,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
s.turnId = turnResp.result?.turn?.id ?? null;
|
|
551
|
+
await turnDone;
|
|
552
|
+
unsubscribeNotifications();
|
|
553
|
+
if (!s.text.trim() && s.msgs.length)
|
|
554
|
+
s.text = s.msgs.join('\n\n');
|
|
555
|
+
if (!s.thinking.trim() && s.thinkParts.length)
|
|
556
|
+
s.thinking = s.thinkParts.join('\n\n');
|
|
557
|
+
const ok = s.turnStatus === 'completed' && !timedOut;
|
|
558
|
+
const error = s.turnError
|
|
559
|
+
|| (timedOut ? `Timed out after ${opts.timeout}s waiting for turn completion.` : null)
|
|
560
|
+
|| (!ok ? `Turn ${s.turnStatus || 'unknown'}.` : null);
|
|
561
|
+
const stopReason = timedOut ? 'timeout' : (s.turnStatus === 'interrupted' ? 'interrupted' : null);
|
|
562
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
563
|
+
agentLog(`[codex-rpc] result: ok=${ok} elapsed=${elapsed}s text=${s.text.length}chars session=${s.sessionId} status=${s.turnStatus}`);
|
|
564
|
+
return {
|
|
565
|
+
ok, sessionId: s.sessionId,
|
|
566
|
+
workspacePath: null, model: s.model, thinkingEffort: s.thinkingEffort,
|
|
567
|
+
message: s.text.trim() || error || '(no textual response)',
|
|
568
|
+
thinking: s.thinking.trim() || null,
|
|
569
|
+
elapsedS: (Date.now() - start) / 1000,
|
|
570
|
+
inputTokens: s.inputTokens, outputTokens: s.outputTokens,
|
|
571
|
+
cachedInputTokens: s.cachedInputTokens, cacheCreationInputTokens: s.cacheCreationInputTokens,
|
|
572
|
+
contextWindow: s.contextWindow, ...computeContext(s),
|
|
573
|
+
codexCumulative: s.codexCumulative, error, stopReason, incomplete: !ok,
|
|
574
|
+
activity: s.activity.trim() || null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
unsubscribeNotifications();
|
|
579
|
+
srv.kill();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// Sessions
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
/** Load title index from ~/.codex/session_index.jsonl (deduped, last entry wins). */
|
|
586
|
+
function loadCodexSessionIndex() {
|
|
587
|
+
const home = process.env.HOME || '';
|
|
588
|
+
if (!home)
|
|
589
|
+
return new Map();
|
|
590
|
+
const indexPath = path.join(home, '.codex', 'session_index.jsonl');
|
|
591
|
+
if (!fs.existsSync(indexPath))
|
|
592
|
+
return new Map();
|
|
593
|
+
const map = new Map();
|
|
594
|
+
try {
|
|
595
|
+
const data = fs.readFileSync(indexPath, 'utf8');
|
|
596
|
+
for (const line of data.split('\n')) {
|
|
597
|
+
if (!line.trim())
|
|
598
|
+
continue;
|
|
599
|
+
try {
|
|
600
|
+
const entry = JSON.parse(line);
|
|
601
|
+
if (entry.id)
|
|
602
|
+
map.set(entry.id, { threadName: entry.thread_name || '', updatedAt: entry.updated_at || '' });
|
|
603
|
+
}
|
|
604
|
+
catch { /* skip */ }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch { /* skip */ }
|
|
608
|
+
return map;
|
|
609
|
+
}
|
|
610
|
+
/** Scan ~/.codex/sessions/ rollout files to find sessions matching the given workdir. */
|
|
611
|
+
function getNativeCodexSessions(workdir) {
|
|
612
|
+
const home = process.env.HOME || '';
|
|
613
|
+
if (!home)
|
|
614
|
+
return [];
|
|
615
|
+
const sessionsDir = path.join(home, '.codex', 'sessions');
|
|
616
|
+
if (!fs.existsSync(sessionsDir))
|
|
617
|
+
return [];
|
|
618
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
619
|
+
const titleIndex = loadCodexSessionIndex();
|
|
620
|
+
const sessions = [];
|
|
621
|
+
const seenIds = new Set();
|
|
622
|
+
// Walk year/month/day directories
|
|
623
|
+
const walkDir = (dir) => {
|
|
624
|
+
let entries;
|
|
625
|
+
try {
|
|
626
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
const fullPath = path.join(dir, entry.name);
|
|
633
|
+
if (entry.isDirectory()) {
|
|
634
|
+
walkDir(fullPath);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (!entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl'))
|
|
638
|
+
continue;
|
|
639
|
+
// Read first chunk to extract session_meta fields via regex
|
|
640
|
+
// (first line can be very large due to base_instructions, so we avoid full JSON parse)
|
|
641
|
+
try {
|
|
642
|
+
const fd = fs.openSync(fullPath, 'r');
|
|
643
|
+
const buf = Buffer.alloc(1024); // only need the first ~1KB for type, id, cwd, timestamp
|
|
644
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
645
|
+
fs.closeSync(fd);
|
|
646
|
+
const head = buf.toString('utf8', 0, bytesRead);
|
|
647
|
+
if (!head.includes('"session_meta"'))
|
|
648
|
+
continue;
|
|
649
|
+
// Extract fields with regex (they appear early in the JSON before base_instructions)
|
|
650
|
+
const idMatch = head.match(/"id"\s*:\s*"([^"]+)"/);
|
|
651
|
+
const cwdMatch = head.match(/"cwd"\s*:\s*"([^"]+)"/);
|
|
652
|
+
const tsMatch = head.match(/"timestamp"\s*:\s*"([^"]+)"/);
|
|
653
|
+
if (!idMatch || !cwdMatch)
|
|
654
|
+
continue;
|
|
655
|
+
const metaId = idMatch[1];
|
|
656
|
+
const metaCwd = cwdMatch[1];
|
|
657
|
+
if (path.resolve(metaCwd) !== resolvedWorkdir)
|
|
658
|
+
continue;
|
|
659
|
+
if (seenIds.has(metaId))
|
|
660
|
+
continue;
|
|
661
|
+
seenIds.add(metaId);
|
|
662
|
+
const stat = fs.statSync(fullPath);
|
|
663
|
+
const idx = titleIndex.get(metaId);
|
|
664
|
+
const title = idx?.threadName || null;
|
|
665
|
+
const updatedAt = idx?.updatedAt || stat.mtime.toISOString();
|
|
666
|
+
sessions.push({
|
|
667
|
+
sessionId: metaId,
|
|
668
|
+
agent: 'codex',
|
|
669
|
+
workdir: metaCwd,
|
|
670
|
+
workspacePath: null,
|
|
671
|
+
model: null,
|
|
672
|
+
createdAt: tsMatch?.[1] || stat.birthtime.toISOString(),
|
|
673
|
+
title,
|
|
674
|
+
running: Date.now() - Date.parse(updatedAt) < 10_000,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch { /* skip */ }
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
walkDir(sessionsDir);
|
|
681
|
+
return sessions;
|
|
682
|
+
}
|
|
683
|
+
function readCodexSessionMeta(filePath) {
|
|
684
|
+
try {
|
|
685
|
+
const fd = fs.openSync(filePath, 'r');
|
|
686
|
+
const buf = Buffer.alloc(4096);
|
|
687
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
688
|
+
fs.closeSync(fd);
|
|
689
|
+
const firstLine = buf.toString('utf8', 0, bytesRead).split('\n')[0];
|
|
690
|
+
if (!firstLine)
|
|
691
|
+
return null;
|
|
692
|
+
const parsed = JSON.parse(firstLine);
|
|
693
|
+
if (parsed?.type !== 'session_meta')
|
|
694
|
+
return null;
|
|
695
|
+
const sessionId = typeof parsed?.payload?.id === 'string' ? parsed.payload.id : '';
|
|
696
|
+
const cwd = typeof parsed?.payload?.cwd === 'string' ? parsed.payload.cwd : '';
|
|
697
|
+
if (!sessionId || !cwd)
|
|
698
|
+
return null;
|
|
699
|
+
return { sessionId, cwd };
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function findCodexRolloutPath(sessionId, workdir) {
|
|
706
|
+
const home = process.env.HOME || '';
|
|
707
|
+
if (!home)
|
|
708
|
+
return null;
|
|
709
|
+
const sessionsRoot = path.join(home, '.codex', 'sessions');
|
|
710
|
+
if (!fs.existsSync(sessionsRoot))
|
|
711
|
+
return null;
|
|
712
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
713
|
+
const walkDir = (dir) => {
|
|
714
|
+
let entries;
|
|
715
|
+
try {
|
|
716
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
for (const entry of entries) {
|
|
722
|
+
const fullPath = path.join(dir, entry.name);
|
|
723
|
+
if (entry.isDirectory()) {
|
|
724
|
+
const found = walkDir(fullPath);
|
|
725
|
+
if (found)
|
|
726
|
+
return found;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (!entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl'))
|
|
730
|
+
continue;
|
|
731
|
+
const meta = readCodexSessionMeta(fullPath);
|
|
732
|
+
if (!meta)
|
|
733
|
+
continue;
|
|
734
|
+
if (meta.sessionId === sessionId && path.resolve(meta.cwd) === resolvedWorkdir)
|
|
735
|
+
return fullPath;
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
};
|
|
739
|
+
return walkDir(sessionsRoot);
|
|
740
|
+
}
|
|
741
|
+
function getCodexSessionTailFromRollout(opts) {
|
|
742
|
+
const limit = opts.limit ?? 4;
|
|
743
|
+
const rolloutPath = findCodexRolloutPath(opts.sessionId, opts.workdir);
|
|
744
|
+
if (!rolloutPath)
|
|
745
|
+
return { ok: false, messages: [], error: 'Session history file not found' };
|
|
746
|
+
try {
|
|
747
|
+
const lines = readTailLines(rolloutPath, 512 * 1024);
|
|
748
|
+
const allMsgs = [];
|
|
749
|
+
for (const raw of lines) {
|
|
750
|
+
if (!raw || raw[0] !== '{' || !raw.includes('"event_msg"'))
|
|
751
|
+
continue;
|
|
752
|
+
let ev;
|
|
753
|
+
try {
|
|
754
|
+
ev = JSON.parse(raw);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (ev?.type !== 'event_msg' || !ev.payload || typeof ev.payload !== 'object')
|
|
760
|
+
continue;
|
|
761
|
+
if (ev.payload.type === 'user_message' && typeof ev.payload.message === 'string') {
|
|
762
|
+
const text = stripInjectedPrompts(ev.payload.message).trim();
|
|
763
|
+
if (text)
|
|
764
|
+
allMsgs.push({ role: 'user', text });
|
|
765
|
+
}
|
|
766
|
+
else if (ev.payload.type === 'agent_message' && typeof ev.payload.message === 'string') {
|
|
767
|
+
const text = ev.payload.message.trim();
|
|
768
|
+
if (text)
|
|
769
|
+
allMsgs.push({ role: 'assistant', text });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return { ok: true, messages: allMsgs.slice(-limit), error: null };
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
return { ok: false, messages: [], error: error?.message || 'Failed to read session history' };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function getCodexSessions(workdir, limit) {
|
|
779
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
780
|
+
// Merge pikiclaw-tracked sessions with native Codex sessions
|
|
781
|
+
const pikiclawSessions = listPikiclawSessions(resolvedWorkdir, 'codex').map(record => ({
|
|
782
|
+
sessionId: record.sessionId,
|
|
783
|
+
agent: 'codex',
|
|
784
|
+
workdir: record.workdir,
|
|
785
|
+
workspacePath: record.workspacePath,
|
|
786
|
+
model: record.model,
|
|
787
|
+
createdAt: record.createdAt,
|
|
788
|
+
title: record.title,
|
|
789
|
+
running: Date.now() - Date.parse(record.updatedAt) < 10_000,
|
|
790
|
+
}));
|
|
791
|
+
const nativeSessions = getNativeCodexSessions(resolvedWorkdir);
|
|
792
|
+
// Merge: pikiclaw records take precedence
|
|
793
|
+
// Filter out pending sessions — they haven't been confirmed by the agent yet
|
|
794
|
+
const seen = new Set();
|
|
795
|
+
const merged = [];
|
|
796
|
+
for (const s of pikiclawSessions) {
|
|
797
|
+
if (isPendingSessionId(s.sessionId))
|
|
798
|
+
continue;
|
|
799
|
+
if (s.sessionId)
|
|
800
|
+
seen.add(s.sessionId);
|
|
801
|
+
merged.push(s);
|
|
802
|
+
}
|
|
803
|
+
for (const s of nativeSessions) {
|
|
804
|
+
if (s.sessionId && !seen.has(s.sessionId))
|
|
805
|
+
merged.push(s);
|
|
806
|
+
}
|
|
807
|
+
merged.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''));
|
|
808
|
+
const sessions = typeof limit === 'number' ? merged.slice(0, limit) : merged;
|
|
809
|
+
const sessionsDir = path.join(process.env.HOME || '', '.codex', 'sessions');
|
|
810
|
+
agentLog(`[sessions:codex] workdir=${resolvedWorkdir} sessionsDir=${sessionsDir} sessionsDirExists=${fs.existsSync(sessionsDir)} ` +
|
|
811
|
+
`pikiclaw=${pikiclawSessions.length} native=${nativeSessions.length} merged=${sessions.length}`);
|
|
812
|
+
return { ok: true, sessions, error: null };
|
|
813
|
+
}
|
|
814
|
+
async function getCodexSessionTail(opts) {
|
|
815
|
+
const limit = opts.limit ?? 4;
|
|
816
|
+
const srv = getSharedServer();
|
|
817
|
+
if (!(await srv.ensureRunning()))
|
|
818
|
+
return getCodexSessionTailFromRollout(opts);
|
|
819
|
+
const resp = await srv.call('thread/read', { threadId: opts.sessionId, includeTurns: true });
|
|
820
|
+
if (resp.error) {
|
|
821
|
+
const fallback = getCodexSessionTailFromRollout(opts);
|
|
822
|
+
return fallback.ok ? fallback : { ok: false, messages: [], error: resp.error.message || fallback.error || 'thread/read failed' };
|
|
823
|
+
}
|
|
824
|
+
const thread = resp.result?.thread;
|
|
825
|
+
if (!thread) {
|
|
826
|
+
const fallback = getCodexSessionTailFromRollout(opts);
|
|
827
|
+
return fallback.ok ? fallback : { ok: false, messages: [], error: 'No thread data returned' };
|
|
828
|
+
}
|
|
829
|
+
const allMsgs = [];
|
|
830
|
+
for (const turn of (thread.turns ?? [])) {
|
|
831
|
+
for (const item of (turn.items ?? [])) {
|
|
832
|
+
if (item.type === 'userMessage') {
|
|
833
|
+
const parts = [];
|
|
834
|
+
for (const c of (item.content ?? [])) {
|
|
835
|
+
if (c.type === 'text' && c.text)
|
|
836
|
+
parts.push(c.text);
|
|
837
|
+
}
|
|
838
|
+
if (parts.length)
|
|
839
|
+
allMsgs.push({ role: 'user', text: stripInjectedPrompts(parts.join('\n')) });
|
|
840
|
+
}
|
|
841
|
+
else if (item.type === 'agentMessage') {
|
|
842
|
+
if (item.text)
|
|
843
|
+
allMsgs.push({ role: 'assistant', text: item.text });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const messages = allMsgs.slice(-limit);
|
|
848
|
+
if (messages.length > 0)
|
|
849
|
+
return { ok: true, messages, error: null };
|
|
850
|
+
return getCodexSessionTailFromRollout(opts);
|
|
851
|
+
}
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
// Models
|
|
854
|
+
// ---------------------------------------------------------------------------
|
|
855
|
+
function pushModel(models, seen, id, alias) {
|
|
856
|
+
const cleanId = id.trim();
|
|
857
|
+
if (!cleanId || seen.has(cleanId))
|
|
858
|
+
return;
|
|
859
|
+
seen.add(cleanId);
|
|
860
|
+
models.push({ id: cleanId, alias: alias?.trim() || null });
|
|
861
|
+
}
|
|
862
|
+
async function discoverCodexModels(opts) {
|
|
863
|
+
const srv = getSharedServer();
|
|
864
|
+
if (!(await srv.ensureRunning()))
|
|
865
|
+
return { agent: 'codex', models: [], sources: [], note: 'Failed to start codex app-server.' };
|
|
866
|
+
const resp = await srv.call('model/list', { includeHidden: false });
|
|
867
|
+
if (resp.error)
|
|
868
|
+
return { agent: 'codex', models: [], sources: [], note: resp.error.message || 'model/list failed' };
|
|
869
|
+
const data = resp.result?.data ?? [];
|
|
870
|
+
const models = [];
|
|
871
|
+
const seen = new Set();
|
|
872
|
+
if (opts.currentModel?.trim())
|
|
873
|
+
pushModel(models, seen, opts.currentModel.trim(), null);
|
|
874
|
+
for (const entry of data) {
|
|
875
|
+
const id = entry.model || entry.id;
|
|
876
|
+
if (!id || seen.has(id))
|
|
877
|
+
continue;
|
|
878
|
+
pushModel(models, seen, id, entry.displayName && entry.displayName !== id ? entry.displayName : null);
|
|
879
|
+
}
|
|
880
|
+
return { agent: 'codex', models, sources: ['app-server model/list'], note: null };
|
|
881
|
+
}
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
// Usage
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
function getCodexStateDbPath(home) {
|
|
886
|
+
const root = path.join(home, '.codex');
|
|
887
|
+
if (!fs.existsSync(root))
|
|
888
|
+
return null;
|
|
889
|
+
try {
|
|
890
|
+
const files = fs.readdirSync(root)
|
|
891
|
+
.filter(name => /^state.*\.sqlite$/i.test(name))
|
|
892
|
+
.map(name => ({ name, full: path.join(root, name), mtime: fs.statSync(path.join(root, name)).mtimeMs }))
|
|
893
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
894
|
+
return files[0]?.full || null;
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function codexUsageFromRateLimits(rateLimits, capturedAt, source) {
|
|
901
|
+
if (!rateLimits || typeof rateLimits !== 'object')
|
|
902
|
+
return null;
|
|
903
|
+
const windows = [
|
|
904
|
+
usageWindowFromRateLimit('Primary', rateLimits.primary),
|
|
905
|
+
usageWindowFromRateLimit('Secondary', rateLimits.secondary),
|
|
906
|
+
].filter((v) => !!v);
|
|
907
|
+
if (!windows.length)
|
|
908
|
+
return null;
|
|
909
|
+
let status = null;
|
|
910
|
+
if (rateLimits.limit_reached === true)
|
|
911
|
+
status = 'limit_reached';
|
|
912
|
+
else if (rateLimits.allowed === true)
|
|
913
|
+
status = 'allowed';
|
|
914
|
+
return { ok: true, agent: 'codex', source, capturedAt, status, windows, error: null };
|
|
915
|
+
}
|
|
916
|
+
function getCodexUsageFromStateDb(home) {
|
|
917
|
+
const dbPath = getCodexStateDbPath(home);
|
|
918
|
+
if (!dbPath)
|
|
919
|
+
return null;
|
|
920
|
+
try {
|
|
921
|
+
const query = "SELECT ts || '|' || message FROM logs WHERE message LIKE '%codex.rate_limits%' ORDER BY ts DESC LIMIT 1;";
|
|
922
|
+
const out = execSync(`sqlite3 -noheader ${Q(dbPath)} ${Q(query)}`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
923
|
+
if (!out)
|
|
924
|
+
return null;
|
|
925
|
+
const sep = out.indexOf('|');
|
|
926
|
+
const rawTs = sep >= 0 ? out.slice(0, sep) : '';
|
|
927
|
+
const rawMessage = sep >= 0 ? out.slice(sep + 1) : out;
|
|
928
|
+
const payload = parseJsonTail(rawMessage);
|
|
929
|
+
const capturedAt = toIsoFromEpochSeconds(rawTs);
|
|
930
|
+
return codexUsageFromRateLimits(payload?.rate_limits, capturedAt, 'state-db');
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function getCodexUsageFromSessions(home) {
|
|
937
|
+
const sessionsRoot = path.join(home, '.codex', 'sessions');
|
|
938
|
+
if (!fs.existsSync(sessionsRoot))
|
|
939
|
+
return null;
|
|
940
|
+
const all = [];
|
|
941
|
+
try {
|
|
942
|
+
for (const year of fs.readdirSync(sessionsRoot)) {
|
|
943
|
+
const yp = path.join(sessionsRoot, year);
|
|
944
|
+
if (!fs.statSync(yp).isDirectory())
|
|
945
|
+
continue;
|
|
946
|
+
for (const month of fs.readdirSync(yp)) {
|
|
947
|
+
const mp = path.join(yp, month);
|
|
948
|
+
if (!fs.statSync(mp).isDirectory())
|
|
949
|
+
continue;
|
|
950
|
+
for (const day of fs.readdirSync(mp)) {
|
|
951
|
+
const dp = path.join(mp, day);
|
|
952
|
+
if (!fs.statSync(dp).isDirectory())
|
|
953
|
+
continue;
|
|
954
|
+
for (const f of fs.readdirSync(dp)) {
|
|
955
|
+
if (!f.endsWith('.jsonl'))
|
|
956
|
+
continue;
|
|
957
|
+
all.push({ path: path.join(dp, f), mtime: fs.statSync(path.join(dp, f)).mtimeMs });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
all.sort((a, b) => b.mtime - a.mtime);
|
|
967
|
+
for (const entry of all.slice(0, 30)) {
|
|
968
|
+
try {
|
|
969
|
+
const lines = fs.readFileSync(entry.path, 'utf-8').trim().split('\n');
|
|
970
|
+
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 200; i--) {
|
|
971
|
+
const raw = lines[i];
|
|
972
|
+
if (!raw || raw[0] !== '{' || !raw.includes('rate_limits'))
|
|
973
|
+
continue;
|
|
974
|
+
let ev;
|
|
975
|
+
try {
|
|
976
|
+
ev = JSON.parse(raw);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const result = codexUsageFromRateLimits(ev?.payload?.rate_limits, typeof ev?.timestamp === 'string' ? ev.timestamp : null, 'session-history');
|
|
982
|
+
if (result)
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch { }
|
|
987
|
+
}
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
function parseRateLimitWindow(label, rl) {
|
|
991
|
+
if (!rl || typeof rl !== 'object')
|
|
992
|
+
return null;
|
|
993
|
+
const usedPercent = roundPercent(rl.usedPercent);
|
|
994
|
+
return {
|
|
995
|
+
label: labelFromWindowMinutes(rl.windowDurationMins, label),
|
|
996
|
+
usedPercent,
|
|
997
|
+
remainingPercent: usedPercent == null ? null : Math.max(0, Math.round((100 - usedPercent) * 10) / 10),
|
|
998
|
+
resetAt: toIsoFromEpochSeconds(rl.resetsAt),
|
|
999
|
+
resetAfterSeconds: rl.resetsAt ? Math.max(0, Math.round(rl.resetsAt - Date.now() / 1000)) : null,
|
|
1000
|
+
status: null,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
export async function getCodexUsageLive() {
|
|
1004
|
+
const home = process.env.HOME || '';
|
|
1005
|
+
const srv = getSharedServer();
|
|
1006
|
+
if (!(await srv.ensureRunning())) {
|
|
1007
|
+
return getCodexUsageFromStateDb(home) || emptyUsage('codex', 'Failed to start codex app-server.');
|
|
1008
|
+
}
|
|
1009
|
+
const resp = await srv.call('account/rateLimits/read');
|
|
1010
|
+
if (resp.error)
|
|
1011
|
+
return getCodexUsageFromStateDb(home) || emptyUsage('codex', resp.error.message || 'account/rateLimits/read failed');
|
|
1012
|
+
const rl = resp.result?.rateLimits;
|
|
1013
|
+
if (!rl)
|
|
1014
|
+
return getCodexUsageFromStateDb(home) || emptyUsage('codex', 'No rate limits in response.');
|
|
1015
|
+
const capturedAt = new Date().toISOString();
|
|
1016
|
+
const windows = [];
|
|
1017
|
+
const w1 = parseRateLimitWindow('Primary', rl.primary);
|
|
1018
|
+
if (w1)
|
|
1019
|
+
windows.push(w1);
|
|
1020
|
+
const w2 = parseRateLimitWindow('Secondary', rl.secondary);
|
|
1021
|
+
if (w2)
|
|
1022
|
+
windows.push(w2);
|
|
1023
|
+
return {
|
|
1024
|
+
ok: windows.length > 0, agent: 'codex', source: 'app-server-live', capturedAt, status: null,
|
|
1025
|
+
windows, error: windows.length > 0 ? null : 'No rate limit windows.',
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
// ---------------------------------------------------------------------------
|
|
1029
|
+
// Driver
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
class CodexDriver {
|
|
1032
|
+
id = 'codex';
|
|
1033
|
+
cmd = 'codex';
|
|
1034
|
+
thinkLabel = 'Reasoning';
|
|
1035
|
+
detect() { return detectAgentBin('codex', 'codex'); }
|
|
1036
|
+
async doStream(opts) { return doCodexStream(opts); }
|
|
1037
|
+
async getSessions(workdir, limit) {
|
|
1038
|
+
return getCodexSessions(workdir, limit);
|
|
1039
|
+
}
|
|
1040
|
+
async getSessionTail(opts) {
|
|
1041
|
+
return getCodexSessionTail(opts);
|
|
1042
|
+
}
|
|
1043
|
+
async listModels(opts) { return discoverCodexModels(opts); }
|
|
1044
|
+
getUsage(opts) {
|
|
1045
|
+
const home = process.env.HOME || '';
|
|
1046
|
+
if (!home)
|
|
1047
|
+
return emptyUsage('codex', 'HOME is not set.');
|
|
1048
|
+
return getCodexUsageFromStateDb(home)
|
|
1049
|
+
|| getCodexUsageFromSessions(home)
|
|
1050
|
+
|| emptyUsage('codex', 'No recent Codex usage data found.');
|
|
1051
|
+
}
|
|
1052
|
+
async getUsageLive(opts) { return getCodexUsageLive(); }
|
|
1053
|
+
shutdown() { shutdownCodexServer(); }
|
|
1054
|
+
}
|
|
1055
|
+
registerDriver(new CodexDriver());
|