ticlawk 0.1.12-dev.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.
Files changed (55) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +426 -0
  3. package/agent-freeway.mjs +2 -0
  4. package/assets/ticlawk-concept.svg +137 -0
  5. package/bin/agent-freeway.mjs +4 -0
  6. package/bin/ticlawk.mjs +594 -0
  7. package/cc-watcher.mjs +3 -0
  8. package/package.json +72 -0
  9. package/scripts/postinstall.mjs +61 -0
  10. package/src/adapters/telegram/index.mjs +359 -0
  11. package/src/adapters/ticlawk/api.mjs +360 -0
  12. package/src/adapters/ticlawk/cards.mjs +149 -0
  13. package/src/adapters/ticlawk/credentials.mjs +25 -0
  14. package/src/adapters/ticlawk/index.mjs +1229 -0
  15. package/src/adapters/ticlawk/wake-client.mjs +204 -0
  16. package/src/core/adapter-registry.mjs +50 -0
  17. package/src/core/argv.mjs +38 -0
  18. package/src/core/bindings/store.mjs +81 -0
  19. package/src/core/bus.mjs +91 -0
  20. package/src/core/config.mjs +203 -0
  21. package/src/core/daemon-install.mjs +246 -0
  22. package/src/core/diagnostics.mjs +79 -0
  23. package/src/core/events/worker-events.mjs +80 -0
  24. package/src/core/executables.mjs +106 -0
  25. package/src/core/host-id.mjs +48 -0
  26. package/src/core/http.mjs +65 -0
  27. package/src/core/logger.mjs +34 -0
  28. package/src/core/media/inbound.mjs +127 -0
  29. package/src/core/media/outbound.mjs +163 -0
  30. package/src/core/profiles.mjs +173 -0
  31. package/src/core/runtime-contract.mjs +68 -0
  32. package/src/core/runtime-env.mjs +9 -0
  33. package/src/core/runtime-registry.mjs +93 -0
  34. package/src/core/runtime-support.mjs +197 -0
  35. package/src/core/setup-readiness.mjs +86 -0
  36. package/src/core/store/json-file-store.mjs +47 -0
  37. package/src/core/ticlawk-control.mjs +92 -0
  38. package/src/core/uninstall.mjs +142 -0
  39. package/src/core/update-state.mjs +62 -0
  40. package/src/core/update.mjs +178 -0
  41. package/src/runtimes/claude-code/index.mjs +363 -0
  42. package/src/runtimes/claude-code/session.mjs +388 -0
  43. package/src/runtimes/claude-code/transcripts.mjs +206 -0
  44. package/src/runtimes/codex/index.mjs +306 -0
  45. package/src/runtimes/codex/session.mjs +750 -0
  46. package/src/runtimes/openclaw/gateway.mjs +269 -0
  47. package/src/runtimes/openclaw/identity.mjs +34 -0
  48. package/src/runtimes/openclaw/index.mjs +228 -0
  49. package/src/runtimes/openclaw/inflight.mjs +46 -0
  50. package/src/runtimes/openclaw/target.mjs +57 -0
  51. package/src/runtimes/opencode/index.mjs +318 -0
  52. package/src/runtimes/opencode/session.mjs +413 -0
  53. package/src/runtimes/pi/index.mjs +287 -0
  54. package/src/runtimes/pi/session.mjs +423 -0
  55. package/ticlawk.mjs +260 -0
@@ -0,0 +1,750 @@
1
+ /**
2
+ * Codex local session helpers.
3
+ *
4
+ * Codex sessions are jsonl files in `~/.codex/sessions/**`. The first line
5
+ * of each file is a `session_meta` record with the canonical sessionId and
6
+ * working directory. Spawning `codex exec` (with or without `resume`)
7
+ * creates or continues a session; the new session id appears as a
8
+ * `thread.started` event on stdout.
9
+ */
10
+
11
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
+ import { spawn } from 'node:child_process';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { debugLog, debugError } from '../../core/logger.mjs';
16
+ import { buildRuntimeEnv } from '../../core/runtime-env.mjs';
17
+ import { getRuntimeExecutableConfig } from '../../core/config.mjs';
18
+ import { getExecutableVersion, isExecutablePath, resolveExecutable } from '../../core/executables.mjs';
19
+
20
+ export const CODEX_SESSIONS_DIR = process.env.CODEX_SESSIONS_DIR || `${homedir()}/.codex/sessions`;
21
+ export const CODEX_MAX_AGE_MS = 24 * 60 * 60 * 1000;
22
+ export const DEFAULT_CODEX_EXEC_TIMEOUT_MS = 7200 * 1000;
23
+ export const DEFAULT_CODEX_COMMAND = 'codex';
24
+
25
+ export function resolveCodexPath(preferredPath = null) {
26
+ return resolveExecutable({
27
+ command: DEFAULT_CODEX_COMMAND,
28
+ preferredPath,
29
+ configuredPath: getRuntimeExecutableConfig('codex'),
30
+ envKey: 'CODEX_BIN',
31
+ });
32
+ }
33
+
34
+ export function requireCodexPath(preferredPath = null) {
35
+ const requested = String(preferredPath || '').trim();
36
+ if (requested && requested.includes('/') && !isExecutablePath(requested)) {
37
+ throw new Error(`Codex CLI is no longer available at: ${requested}. Reconnect this Codex agent or set CODEX_BIN / \`ticlawk config set runtimes.codex.path <path>\`.`);
38
+ }
39
+ const codexPath = resolveCodexPath(preferredPath);
40
+ if (codexPath) return codexPath;
41
+ throw new Error('Codex CLI not found. Install Codex, ensure it is on PATH, or set CODEX_BIN / `ticlawk config set runtimes.codex.path <path>`.');
42
+ }
43
+
44
+ export function getCodexRuntimeHealth(preferredPath = null) {
45
+ const codexPath = resolveCodexPath(preferredPath);
46
+ return {
47
+ available: Boolean(codexPath),
48
+ path: codexPath,
49
+ version: getExecutableVersion(codexPath),
50
+ };
51
+ }
52
+
53
+ function walkJsonlFiles(rootDir) {
54
+ if (!existsSync(rootDir)) return [];
55
+
56
+ const files = [];
57
+ const stack = [rootDir];
58
+ while (stack.length > 0) {
59
+ const current = stack.pop();
60
+ let entries = [];
61
+ try {
62
+ entries = readdirSync(current, { withFileTypes: true });
63
+ } catch {
64
+ continue;
65
+ }
66
+ for (const entry of entries) {
67
+ const fullPath = join(current, entry.name);
68
+ if (entry.isDirectory()) {
69
+ stack.push(fullPath);
70
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
71
+ files.push(fullPath);
72
+ }
73
+ }
74
+ }
75
+ return files;
76
+ }
77
+
78
+ function readFirstJsonLine(filePath) {
79
+ try {
80
+ const content = readFileSync(filePath, 'utf8');
81
+ const firstLine = content.split('\n').find(line => line.trim());
82
+ return firstLine ? JSON.parse(firstLine) : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export function discoverCodexSessions(rootDir = CODEX_SESSIONS_DIR) {
89
+ return walkJsonlFiles(rootDir)
90
+ .map((filePath) => {
91
+ const first = readFirstJsonLine(filePath);
92
+ if (!first || first.type !== 'session_meta') return null;
93
+ const sessionId = first.payload?.id;
94
+ const cwd = first.payload?.cwd;
95
+ if (!sessionId || !cwd) return null;
96
+ return {
97
+ sessionId,
98
+ cwd,
99
+ path: filePath,
100
+ };
101
+ })
102
+ .filter(Boolean);
103
+ }
104
+
105
+ export async function waitForCodexSessionById(sessionId, timeoutMs = 10000) {
106
+ const startedAt = Date.now();
107
+ while (Date.now() - startedAt < timeoutMs) {
108
+ const sessions = discoverCodexSessions(CODEX_SESSIONS_DIR);
109
+ const found = sessions.find(session => session.sessionId === sessionId);
110
+ if (found) return found;
111
+ await new Promise(resolve => setTimeout(resolve, 250));
112
+ }
113
+ return null;
114
+ }
115
+
116
+ // Try to recover a human-readable error string from `codex exec resume
117
+ // --json` stdout. Codex prints a stream of jsonl events; on failure
118
+ // the most useful one is `{type:"error", message}` or
119
+ // `{type:"turn.failed", error:{message}}`.
120
+ function extractCodexError(stdout) {
121
+ if (!stdout) return null;
122
+ const lines = stdout.split('\n').map(l => l.trim()).filter(Boolean);
123
+ for (let i = lines.length - 1; i >= 0; i--) {
124
+ let parsed;
125
+ try { parsed = JSON.parse(lines[i]); } catch { continue; }
126
+ if (parsed?.type === 'turn.failed' && typeof parsed.error?.message === 'string') {
127
+ return parsed.error.message;
128
+ }
129
+ if (parsed?.type === 'error' && typeof parsed.message === 'string') {
130
+ return parsed.message;
131
+ }
132
+ if (parsed?.type === 'item.completed' && parsed.item?.type === 'error' && typeof parsed.item.message === 'string') {
133
+ return parsed.item.message;
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
139
+ function buildCodexExecArgs({ sessionId, message }) {
140
+ return sessionId
141
+ ? [
142
+ 'exec',
143
+ 'resume',
144
+ sessionId,
145
+ '--json',
146
+ '--skip-git-repo-check',
147
+ '--dangerously-bypass-approvals-and-sandbox',
148
+ message,
149
+ ]
150
+ : [
151
+ 'exec',
152
+ '--json',
153
+ '--skip-git-repo-check',
154
+ '--dangerously-bypass-approvals-and-sandbox',
155
+ message,
156
+ ];
157
+ }
158
+
159
+ function parseCodexExecStream({ stdout, onLine }) {
160
+ return (bufferState, chunk) => {
161
+ bufferState.raw += chunk.toString('utf8');
162
+ bufferState.buffer += chunk.toString('utf8');
163
+ const lines = bufferState.buffer.split('\n');
164
+ bufferState.buffer = lines.pop() || '';
165
+ for (const line of lines) {
166
+ const trimmed = line.trim();
167
+ if (!trimmed) continue;
168
+ let parsed;
169
+ try { parsed = JSON.parse(trimmed); } catch { continue; }
170
+ onLine(parsed);
171
+ }
172
+ };
173
+ }
174
+
175
+ function buildCodexTurnError(message, info) {
176
+ const wrapped = new Error(message);
177
+ wrapped.info = info;
178
+ return wrapped;
179
+ }
180
+
181
+ function buildJsonRpcResponseError(error, method) {
182
+ const message = error?.message || `codex app-server ${method || 'request'} failed`;
183
+ const wrapped = new Error(message);
184
+ wrapped.rpcError = error || null;
185
+ wrapped.rpcMethod = method || null;
186
+ return wrapped;
187
+ }
188
+
189
+ function extractCodexErrorInfo(error) {
190
+ return error?.codexErrorInfo
191
+ || error?.data?.codexErrorInfo
192
+ || error?.rpcError?.data?.codexErrorInfo
193
+ || null;
194
+ }
195
+
196
+ function buildGatewayErrorInfo({ startedAt, messageText, source, method, params, err, activeThreadId, activeTurnId }) {
197
+ const rawError = params?.error || params?.data || err?.rpcError || null;
198
+ return {
199
+ ok: false,
200
+ code: null,
201
+ signal: null,
202
+ durationMs: Date.now() - startedAt,
203
+ kind: 'gateway-error',
204
+ errorMessage: messageText,
205
+ gatewaySource: source,
206
+ gatewayMethod: method || null,
207
+ rpcCode: err?.rpcError?.code ?? null,
208
+ rpcMethod: err?.rpcMethod || null,
209
+ willRetry: typeof params?.willRetry === 'boolean' ? params.willRetry : null,
210
+ threadId: params?.threadId || activeThreadId || null,
211
+ turnId: params?.turnId || activeTurnId || null,
212
+ codexErrorInfo: extractCodexErrorInfo(rawError || err) || null,
213
+ additionalDetails: params?.additionalDetails || rawError?.additionalDetails || null,
214
+ rawError,
215
+ };
216
+ }
217
+
218
+ function isSubAgentThread(thread) {
219
+ const source = thread?.source;
220
+ if (!source || typeof source !== 'object') return false;
221
+ return Boolean(source.subAgent || source.sub_agent);
222
+ }
223
+
224
+ export function runCodexPrompt({ sessionId, cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
225
+ return new Promise((resolve, reject) => {
226
+ const startedAt = Date.now();
227
+ const codexCommand = requireCodexPath(codexPath);
228
+ const child = spawn(codexCommand, buildCodexExecArgs({ sessionId, message }), {
229
+ cwd,
230
+ env: buildRuntimeEnv(),
231
+ stdio: ['ignore', 'pipe', 'ignore'],
232
+ });
233
+
234
+ let stdout = '';
235
+ let buffer = '';
236
+ let activeSessionId = sessionId || null;
237
+ let finalText = '';
238
+ let settled = false;
239
+
240
+ const settle = (fn, value) => {
241
+ if (settled) return;
242
+ settled = true;
243
+ if (timeout) clearTimeout(timeout);
244
+ fn(value);
245
+ };
246
+
247
+ child.stdout.on('data', (chunk) => {
248
+ stdout += chunk.toString('utf8');
249
+ buffer += chunk.toString('utf8');
250
+ const lines = buffer.split('\n');
251
+ buffer = lines.pop() || '';
252
+ for (const line of lines) {
253
+ const trimmed = line.trim();
254
+ if (!trimmed) continue;
255
+ let event;
256
+ try { event = JSON.parse(trimmed); } catch { continue; }
257
+ if (event?.type === 'thread.started' && typeof event.thread_id === 'string') {
258
+ activeSessionId = event.thread_id;
259
+ }
260
+ if (event?.type === 'item.completed' && event.item?.type === 'agent_message' && typeof event.item?.text === 'string') {
261
+ finalText = event.item.text;
262
+ }
263
+ }
264
+ });
265
+
266
+ let timeout = null;
267
+ if (timeoutMs > 0) {
268
+ timeout = setTimeout(() => { child.kill('SIGTERM'); }, timeoutMs);
269
+ timeout.unref();
270
+ }
271
+
272
+ child.on('error', (err) => {
273
+ settle(reject, buildCodexTurnError(err.message || 'codex spawn failed', {
274
+ ok: false,
275
+ code: null,
276
+ signal: null,
277
+ durationMs: Date.now() - startedAt,
278
+ kind: 'spawn-failed',
279
+ error: err.message,
280
+ codexCommand,
281
+ }));
282
+ });
283
+
284
+ child.on('exit', async (code, signal) => {
285
+ const durationMs = Date.now() - startedAt;
286
+ if (signal) {
287
+ settle(reject, buildCodexTurnError(extractCodexError(stdout) || `codex killed by ${signal}`, {
288
+ ok: false,
289
+ code: null,
290
+ signal,
291
+ durationMs,
292
+ kind: 'killed',
293
+ errorMessage: extractCodexError(stdout),
294
+ }));
295
+ return;
296
+ }
297
+ if (code !== 0) {
298
+ settle(reject, buildCodexTurnError(extractCodexError(stdout) || `codex exited with code ${code}`, {
299
+ ok: false,
300
+ code,
301
+ signal: null,
302
+ durationMs,
303
+ kind: 'exit-error',
304
+ errorMessage: extractCodexError(stdout),
305
+ }));
306
+ return;
307
+ }
308
+ if (!activeSessionId) {
309
+ settle(reject, buildCodexTurnError('codex session_id missing from output', {
310
+ ok: false,
311
+ code: 0,
312
+ signal: null,
313
+ durationMs,
314
+ kind: 'invalid-output',
315
+ errorMessage: 'codex session_id missing from output',
316
+ }));
317
+ return;
318
+ }
319
+ const session = await waitForCodexSessionById(activeSessionId, 10000);
320
+ if (!finalText) {
321
+ settle(reject, buildCodexTurnError('codex final agent message missing from output', {
322
+ ok: false,
323
+ code: 0,
324
+ signal: null,
325
+ durationMs,
326
+ kind: 'invalid-output',
327
+ errorMessage: 'codex final agent message missing from output',
328
+ }));
329
+ return;
330
+ }
331
+ settle(resolve, {
332
+ sessionId: activeSessionId,
333
+ cwd: session?.cwd || cwd,
334
+ path: session?.path || null,
335
+ text: finalText,
336
+ durationMs,
337
+ });
338
+ });
339
+ });
340
+ }
341
+
342
+ export function streamCodexPrompt({
343
+ sessionId,
344
+ cwd,
345
+ message,
346
+ codexPath = null,
347
+ input = null,
348
+ timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS),
349
+ onEvent,
350
+ }) {
351
+ return new Promise((resolve, reject) => {
352
+ const startedAt = Date.now();
353
+ const codexCommand = requireCodexPath(codexPath);
354
+ const child = spawn(codexCommand, ['app-server'], {
355
+ cwd,
356
+ env: buildRuntimeEnv(),
357
+ stdio: ['pipe', 'pipe', 'ignore'],
358
+ });
359
+
360
+ let settled = false;
361
+ let lineBuffer = '';
362
+ let nextId = 1;
363
+ const pending = new Map();
364
+ let rootThreadId = sessionId || null;
365
+ let rootTurnId = null;
366
+ let finalText = '';
367
+ let threadPath = null;
368
+ const ignoredChildEventsLogged = new Set();
369
+ let eventChain = Promise.resolve();
370
+
371
+ const emit = (event) => {
372
+ if (typeof onEvent !== 'function') return;
373
+ eventChain = eventChain
374
+ .then(() => onEvent(event))
375
+ .catch(() => {});
376
+ return eventChain;
377
+ };
378
+
379
+ const settle = (fn, value) => {
380
+ if (settled) return;
381
+ settled = true;
382
+ if (timeout) clearTimeout(timeout);
383
+ for (const { reject: rejectPending } of pending.values()) {
384
+ try { rejectPending(new Error('codex app-server closed')); } catch {}
385
+ }
386
+ pending.clear();
387
+ try { child.kill('SIGTERM'); } catch {}
388
+ eventChain
389
+ .catch(() => {})
390
+ .finally(() => fn(value));
391
+ };
392
+
393
+ const send = (method, params = {}) => {
394
+ const id = nextId++;
395
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
396
+ return new Promise((resolvePending, rejectPending) => {
397
+ pending.set(id, { resolve: resolvePending, reject: rejectPending, method });
398
+ });
399
+ };
400
+
401
+ const isRootContext = (params) => {
402
+ if (!rootThreadId || !rootTurnId) return false;
403
+ return params?.threadId === rootThreadId && params?.turnId === rootTurnId;
404
+ };
405
+
406
+ const logIgnoredChildEvent = (event, params = {}) => {
407
+ const threadId = params?.threadId || null;
408
+ const turnId = params?.turnId || params?.turn?.id || null;
409
+ const key = `${event}:${threadId || ''}:${turnId || ''}`;
410
+ if (ignoredChildEventsLogged.has(key)) return;
411
+ ignoredChildEventsLogged.add(key);
412
+ debugLog('codex', event, {
413
+ rootThreadId,
414
+ rootTurnId,
415
+ threadId,
416
+ turnId,
417
+ });
418
+ };
419
+
420
+ const handleNotification = (msg) => {
421
+ const method = msg?.method;
422
+ const params = msg?.params || {};
423
+ if (method === 'thread/started') {
424
+ const thread = params?.thread || null;
425
+ if (!rootThreadId && thread?.id && !isSubAgentThread(thread)) {
426
+ rootThreadId = thread.id;
427
+ }
428
+ if (thread?.id === rootThreadId) {
429
+ threadPath = thread?.path || threadPath;
430
+ }
431
+ return;
432
+ }
433
+ if (method === 'turn/started') {
434
+ const turnId = params?.turn?.id || null;
435
+ const threadId = params?.threadId || null;
436
+ if (threadId === rootThreadId && turnId) {
437
+ if (!rootTurnId) {
438
+ rootTurnId = turnId;
439
+ }
440
+ if (turnId === rootTurnId) {
441
+ emit({ type: 'turn.started', sessionId: rootThreadId, turnId: rootTurnId });
442
+ }
443
+ } else {
444
+ logIgnoredChildEvent('codex.non-root-turn-started', params);
445
+ }
446
+ return;
447
+ }
448
+ if (method === 'item/agentMessage/delta') {
449
+ const delta = params?.delta;
450
+ if (typeof delta === 'string' && delta && isRootContext(params)) {
451
+ finalText += delta;
452
+ emit({
453
+ type: 'message.delta',
454
+ sessionId: rootThreadId,
455
+ turnId: rootTurnId,
456
+ text: delta,
457
+ });
458
+ } else if (typeof delta === 'string' && delta) {
459
+ logIgnoredChildEvent('codex.non-root-agent-delta', params);
460
+ }
461
+ return;
462
+ }
463
+ if (method === 'item/completed' && params?.item?.type === 'agentMessage' && typeof params?.item?.text === 'string') {
464
+ if (isRootContext(params)) {
465
+ finalText = params.item.text;
466
+ } else {
467
+ logIgnoredChildEvent('codex.non-root-agent-message-completed', params);
468
+ }
469
+ return;
470
+ }
471
+ if (method === 'error') {
472
+ const messageText = params?.message
473
+ || params?.error?.message
474
+ || params?.data?.message
475
+ || (params && Object.keys(params).length > 0 ? JSON.stringify(params) : null)
476
+ || 'codex app-server error';
477
+ settle(reject, buildCodexTurnError(messageText, buildGatewayErrorInfo({
478
+ startedAt,
479
+ messageText,
480
+ source: 'notification',
481
+ method,
482
+ params,
483
+ activeThreadId: rootThreadId,
484
+ activeTurnId: rootTurnId,
485
+ })));
486
+ return;
487
+ }
488
+ if (method === 'turn/completed') {
489
+ const completedThreadId = params?.threadId || null;
490
+ const completedTurnId = params?.turn?.id || null;
491
+ if (completedThreadId !== rootThreadId || completedTurnId !== rootTurnId) {
492
+ logIgnoredChildEvent('codex.non-root-turn-completed', params);
493
+ return;
494
+ }
495
+ if (params?.turn?.error) {
496
+ const turnErr = params.turn.error;
497
+ const messageText = turnErr?.message
498
+ || turnErr?.data?.message
499
+ || (turnErr && typeof turnErr === 'object' ? JSON.stringify(turnErr) : null)
500
+ || 'codex turn failed';
501
+ settle(reject, buildCodexTurnError(messageText, buildGatewayErrorInfo({
502
+ startedAt,
503
+ messageText,
504
+ source: 'turn-completed',
505
+ method,
506
+ params: { ...params, error: turnErr },
507
+ activeThreadId: rootThreadId,
508
+ activeTurnId: rootTurnId,
509
+ })));
510
+ return;
511
+ }
512
+ settle(resolve, {
513
+ sessionId: rootThreadId,
514
+ turnId: rootTurnId,
515
+ cwd,
516
+ path: threadPath,
517
+ text: finalText,
518
+ durationMs: Date.now() - startedAt,
519
+ });
520
+ }
521
+ };
522
+
523
+ child.stdout.on('data', (chunk) => {
524
+ lineBuffer += chunk.toString('utf8');
525
+ const lines = lineBuffer.split('\n');
526
+ lineBuffer = lines.pop() || '';
527
+ for (const line of lines) {
528
+ const trimmed = line.trim();
529
+ if (!trimmed) continue;
530
+ let msg;
531
+ try { msg = JSON.parse(trimmed); } catch { continue; }
532
+ if (msg.id && pending.has(msg.id)) {
533
+ const pendingRequest = pending.get(msg.id);
534
+ pending.delete(msg.id);
535
+ if (msg.error) {
536
+ pendingRequest.reject(buildJsonRpcResponseError(msg.error, pendingRequest.method));
537
+ } else {
538
+ if (pendingRequest.method === 'thread/start') {
539
+ rootThreadId = msg.result?.thread?.id || rootThreadId;
540
+ threadPath = msg.result?.thread?.path || threadPath;
541
+ }
542
+ if (pendingRequest.method === 'turn/start') {
543
+ rootTurnId = msg.result?.turn?.id || rootTurnId;
544
+ }
545
+ pendingRequest.resolve(msg.result);
546
+ }
547
+ continue;
548
+ }
549
+ if (msg.method) handleNotification(msg);
550
+ }
551
+ });
552
+
553
+ let timeout = null;
554
+ if (timeoutMs > 0) {
555
+ timeout = setTimeout(() => {
556
+ settle(reject, buildCodexTurnError('codex turn timed out', {
557
+ ok: false,
558
+ code: null,
559
+ signal: 'SIGTERM',
560
+ durationMs: Date.now() - startedAt,
561
+ kind: 'killed',
562
+ errorMessage: 'codex turn timed out',
563
+ }));
564
+ }, timeoutMs);
565
+ timeout.unref();
566
+ }
567
+
568
+ child.on('error', (err) => {
569
+ settle(reject, buildCodexTurnError(err.message || 'codex app-server spawn failed', {
570
+ ok: false,
571
+ code: null,
572
+ signal: null,
573
+ durationMs: Date.now() - startedAt,
574
+ kind: 'spawn-failed',
575
+ error: err.message,
576
+ codexCommand,
577
+ }));
578
+ });
579
+
580
+ child.on('exit', (code, signal) => {
581
+ if (settled) return;
582
+ settle(reject, buildCodexTurnError(`codex app-server exited before turn completion${code !== null ? ` (code ${code})` : ''}${signal ? ` (${signal})` : ''}`, {
583
+ ok: false,
584
+ code,
585
+ signal,
586
+ durationMs: Date.now() - startedAt,
587
+ kind: signal ? 'killed' : 'exit-error',
588
+ errorMessage: 'codex app-server exited before turn completion',
589
+ }));
590
+ });
591
+
592
+ (async () => {
593
+ try {
594
+ await send('initialize', { protocolVersion: 2, clientInfo: { name: 'ticlawk', version: '0.1.0' } });
595
+ if (rootThreadId) {
596
+ await send('thread/resume', {
597
+ threadId: rootThreadId,
598
+ cwd,
599
+ approvalPolicy: 'never',
600
+ sandbox: 'danger-full-access',
601
+ });
602
+ } else {
603
+ const started = await send('thread/start', {
604
+ cwd,
605
+ model: process.env.CODEX_MODEL || null,
606
+ approvalPolicy: 'never',
607
+ sandbox: 'danger-full-access',
608
+ });
609
+ rootThreadId = started?.thread?.id || rootThreadId;
610
+ threadPath = started?.thread?.path || threadPath;
611
+ }
612
+ const turnInput = Array.isArray(input) && input.length > 0
613
+ ? input
614
+ : [{ type: 'text', text: message }];
615
+ const startedTurn = await send('turn/start', {
616
+ threadId: rootThreadId,
617
+ input: turnInput,
618
+ approvalPolicy: 'never',
619
+ sandboxPolicy: { type: 'dangerFullAccess' },
620
+ });
621
+ rootTurnId = startedTurn?.turn?.id || rootTurnId;
622
+ } catch (err) {
623
+ const messageText = err.message || 'codex app-server request failed';
624
+ settle(reject, buildCodexTurnError(messageText, buildGatewayErrorInfo({
625
+ startedAt,
626
+ messageText,
627
+ source: 'request',
628
+ method: err?.rpcMethod || null,
629
+ params: {},
630
+ err,
631
+ activeThreadId: rootThreadId,
632
+ activeTurnId: rootTurnId,
633
+ })));
634
+ }
635
+ })();
636
+ });
637
+ }
638
+
639
+ export function createCodexSession({ cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
640
+ return new Promise((resolve, reject) => {
641
+ const startedAt = Date.now();
642
+ const codexCommand = requireCodexPath(codexPath);
643
+ const child = spawn(
644
+ codexCommand,
645
+ [
646
+ 'exec',
647
+ '--json',
648
+ '--skip-git-repo-check',
649
+ '--dangerously-bypass-approvals-and-sandbox',
650
+ message,
651
+ ],
652
+ {
653
+ cwd,
654
+ env: buildRuntimeEnv(),
655
+ stdio: ['ignore', 'pipe', 'ignore'],
656
+ }
657
+ );
658
+
659
+ debugLog('codex', 'session.spawned', {
660
+ pid: child.pid,
661
+ cwd,
662
+ timeoutMs,
663
+ textLength: message.length,
664
+ codexCommand,
665
+ });
666
+
667
+ let settled = false;
668
+ let timeout = null;
669
+ let buffer = '';
670
+
671
+ const settle = (fn, value) => {
672
+ if (settled) return;
673
+ settled = true;
674
+ if (timeout) clearTimeout(timeout);
675
+ fn(value);
676
+ };
677
+
678
+ if (timeoutMs > 0) {
679
+ timeout = setTimeout(() => {
680
+ debugError('codex', 'session.timeout', {
681
+ pid: child.pid,
682
+ durationMs: Date.now() - startedAt,
683
+ timeoutMs,
684
+ });
685
+ child.kill('SIGTERM');
686
+ settle(reject, new Error('codex session creation timed out'));
687
+ }, timeoutMs);
688
+ timeout.unref();
689
+ }
690
+
691
+ child.stdout.on('data', (chunk) => {
692
+ buffer += chunk.toString('utf8');
693
+ const lines = buffer.split('\n');
694
+ buffer = lines.pop() || '';
695
+ for (const line of lines) {
696
+ if (!line.trim()) continue;
697
+ let event;
698
+ try {
699
+ event = JSON.parse(line);
700
+ } catch {
701
+ continue;
702
+ }
703
+ if (event.type === 'thread.started' && event.thread_id) {
704
+ debugLog('codex', 'session.started', {
705
+ pid: child.pid,
706
+ sessionId: String(event.thread_id).slice(0, 8),
707
+ cwd,
708
+ });
709
+ settle(resolve, String(event.thread_id));
710
+ return;
711
+ }
712
+ }
713
+ });
714
+
715
+ child.on('error', (err) => {
716
+ debugError('codex', 'session.spawn-failed', {
717
+ pid: child.pid,
718
+ durationMs: Date.now() - startedAt,
719
+ error: err.message,
720
+ codexCommand,
721
+ });
722
+ settle(reject, err);
723
+ });
724
+
725
+ child.on('exit', (code, signal) => {
726
+ if (signal) {
727
+ debugError('codex', 'session.exit-signal', {
728
+ pid: child.pid,
729
+ durationMs: Date.now() - startedAt,
730
+ signal,
731
+ });
732
+ settle(reject, new Error(`codex session exited via ${signal}`));
733
+ return;
734
+ }
735
+ if (code !== 0) {
736
+ debugError('codex', 'session.exit-error', {
737
+ pid: child.pid,
738
+ durationMs: Date.now() - startedAt,
739
+ code,
740
+ });
741
+ settle(reject, new Error(`codex session exited with code ${code}`));
742
+ return;
743
+ }
744
+ debugLog('codex', 'session.exit-ok', {
745
+ pid: child.pid,
746
+ durationMs: Date.now() - startedAt,
747
+ });
748
+ });
749
+ });
750
+ }