Setting the file. One moment.
Index · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 151
async function findCodexHandoff
— line 151
This file
Number 45.47
Position 47 of 182
Type JavaScript
Size 15 KB
Lines 358 lib/agent-adapters/ index.js
JavaScript · 358 lines · 15 KB
7
8 // Resolve this from the adapter module, never from the caller's cwd or a user-level
9 // skill installation. The runner and the toolkit it launches must be the same copy.
10 const TOOLKIT_ROOT = path. resolve (__dirname, '../..' );
11
12 function pinnedPrompt ( prompt , { projectRoot = null } = {}) {
13 const scope = projectRoot ? ` Treat ${ projectRoot } as the only writable migration project root; keep the toolkit read-only.` : '' ;
14 return `${ prompt } \n\n Use only the wix-replatform toolkit at ${ TOOLKIT_ROOT }. Do not discover, load, or follow any other copy of this toolkit from the project, home directory, global installation, or another path on disk.${ scope }` ;
15 }
16
17 const CODEX_NON_INTERACTIVE_FLAGS = [
18 '--dangerously-bypass-approvals-and-sandbox' ,
19 '--skip-git-repo-check' ,
20 ];
21
22 function workerLabel ( item , workerId ) {
23 const prompt = typeof item?.prompt === 'string' ? item.prompt. trim () : '' ;
24 const firstLine = prompt. split ( / \r ? \n / , 1 )[ 0 ]. trim ();
25 return (firstLine || `Codex worker ${ workerId }` ). slice ( 0 , 200 );
26 }
27
28 function codexWorkerReports ( raw ) {
29 const item = raw?.item;
30 if ( ! item || item.type !== 'collab_tool_call' ) return [];
31 if ( ! [ 'spawn_agent' , 'wait' , 'close_agent' ]. includes (item.tool)) return [];
32 const states = item.agents_states;
33 if ( ! states || typeof states !== 'object' || Array. isArray (states)) return [];
34 const stateMap = {
35 pending_init: 'starting' ,
36 running: 'running' ,
37 completed: 'completed' ,
38 failed: 'failed' ,
39 canceled: 'canceled' ,
40 cancelled: 'canceled' ,
41 interrupted: 'interrupted' ,
42 };
43 return Object. entries (states). flatMap (([ workerId , value ]) => {
44 const state = stateMap[value?.status];
45 return state ? [{
46 workerId,
47 label: workerLabel (item, workerId),
48 state,
49 ... (item.parent_worker_id ? { parentWorkerId: item.parent_worker_id } : {}),
50 }] : [];
51 });
52 }
53
54 const codexSessionCache = new Map ();
55 const CODEX_SESSION_SCAN_INTERVAL_MS = 1000 ;
56 const CODEX_SESSION_TAIL_BYTES = 256 * 1024 ;
57
58 function codexHome ( env = process.env) {
59 return env. CODEX_HOME || path. join (os. homedir (), '.codex' );
60 }
61
62 async function walkFiles ( directory , depth , predicate , files = []) {
63 if (depth < 0 ) return files;
64 let entries;
65 try { entries = await fs. readdir (directory, { withFileTypes: true }); } catch { return files; }
66 for ( const entry of entries) {
67 const entryPath = path. join (directory, entry.name);
68 if (entry. isFile () && predicate (entry.name)) files. push (entryPath);
69 else if (entry. isDirectory ()) await walkFiles (entryPath, depth - 1 , predicate, files);
70 }
71 return files;
72 }
73
74 async function findCodexSessionFile ( workerId , { home = codexHome (), now = Date. now () } = {}) {
75 const sessionsRoot = path. join (home, 'sessions' );
76 const cacheKey = `${ sessionsRoot } \0 ${ workerId }` ;
77 const cached = codexSessionCache. get (cacheKey);
78 if (cached && (cached.path || now - cached.scannedAt < CODEX_SESSION_SCAN_INTERVAL_MS )) return cached.path;
79 const files = await walkFiles (sessionsRoot, 3 , ( name ) => name. endsWith ( '.jsonl' ) && name. includes ( `-${ workerId }.` ));
80 const file = files. sort (). at ( - 1 ) || null ;
81 codexSessionCache. set (cacheKey, { path: file, scannedAt: now });
82 return file;
83 }
84
85 async function readTail ( filePath ) {
86 let handle;
87 try {
88 handle = await fs. open (filePath, 'r' );
89 const stat = await handle. stat ();
90 const length = Math. min (stat.size, CODEX_SESSION_TAIL_BYTES );
91 const buffer = Buffer. alloc (length);
92 await handle. read (buffer, 0 , length, Math. max ( 0 , stat.size - length));
93 const offset = Math. max ( 0 , stat.size - length);
94 const lines = buffer. toString ( 'utf8' ). split ( / \r ? \n / );
95 if (offset > 0 ) lines. shift ();
96 return { stat, lines: lines. flatMap (( line ) => {
97 if ( ! line. trim ()) return [];
98 try { return [ JSON . parse (line)]; } catch { return []; }
99 }) };
100 } catch {
101 return null ;
102 } finally {
103 if (handle) await handle. close (). catch (() => {});
104 }
105 }
106
107 function codexEventType ( record ) {
108 if (record?.type === 'response_item' ) return 'response_item' ;
109 return record?.payload?.type || record?.type || null ;
110 }
111
112 const CODEX_ACTIVITY_ITEM_TYPES = new Set ([
113 'message' ,
114 'reasoning' ,
115 'custom_tool_call' ,
116 'custom_tool_call_output' ,
117 'function_call' ,
118 'function_call_output' ,
119 'web_search_call' ,
120 'web_search_call_output' ,
121 'computer_call' ,
122 'computer_call_output' ,
123 'local_shell_call' ,
124 'local_shell_call_output' ,
125 ]);
126
127 function isCodexOutput ( record ) {
128 const type = codexEventType (record);
129 if (type === 'response_item' ) {
130 return CODEX_ACTIVITY_ITEM_TYPES . has (record.payload?.payload?.type)
131 || CODEX_ACTIVITY_ITEM_TYPES . has (record.payload?.type);
132 }
133 return [ 'item_completed' , 'turn_aborted' ]. includes (type);
134 }
135
136 function codexTerminalState ( records ) {
137 for ( const record of records) {
138 const type = codexEventType (record);
139 if (type === 'task_complete' ) return 'completed' ;
140 if (type === 'turn_aborted' ) return 'interrupted' ;
141 }
142 return null ;
143 }
144
145 function handoffWorkerId ( handoff ) {
146 if ( ! handoff || typeof handoff !== 'object' || Array. isArray (handoff)) return null ;
147 const value = handoff.workerId ?? handoff.worker_id;
148 return typeof value === 'string' && value. trim () ? value. trim () : null ;
149 }
150
151 async function findCodexHandoff ( projectRoot , worker , since ) {
152 const root = path. join (projectRoot, 'state' , 'handoffs' );
153 const files = await walkFiles (root, 2 , ( name ) => name. endsWith ( '.json' ));
154 const candidates = [];
155 for ( const file of files) {
156 try {
157 const stat = await fs. stat (file);
158 if (stat.mtimeMs < since) continue ;
159 // Handoffs are project-level files. An unscoped handoff cannot prove which
160 // simultaneously pending child produced it, so it must never complete a worker.
161 const handoff = JSON . parse ( await fs. readFile (file, 'utf8' ));
162 if ( handoffWorkerId (handoff) !== worker.workerId) continue ;
163 candidates. push ({ file, mtimeMs: stat.mtimeMs });
164 } catch { /* the child may still be atomically replacing the handoff */ }
165 }
166 return candidates. sort (( a , b ) => b.mtimeMs - a.mtimeMs)[ 0 ] || null ;
167 }
168
169 async function reconcileCodexWorkers ( workers , { cwd , env = process.env, codexHome : home = codexHome (env), now = Date. now () } = {}) {
170 if ( ! cwd || ! Array. isArray (workers)) return [];
171 return Promise . all (workers. map ( async ( worker ) => {
172 const handoff = await findCodexHandoff (cwd, worker, worker.firstSeenAt || now);
173 if (handoff) {
174 return { ... worker, state: 'completed' , activity: 'handoff' , handoffPath: path. relative (cwd, handoff.file). replaceAll (path.sep, '/' ), lastActivityAt: handoff.mtimeMs };
175 }
176 const sessionFile = await findCodexSessionFile (worker.workerId, { home, now });
177 if ( ! sessionFile) return { ... worker, state: 'starting' , activity: 'none' };
178 const tail = await readTail (sessionFile);
179 if ( ! tail) return { ... worker, state: 'starting' , activity: 'none' };
180 const terminalState = codexTerminalState (tail.lines);
181 if (terminalState) return { ... worker, state: terminalState, activity: 'terminal' , sessionFile, lastActivityAt: tail.stat.mtimeMs };
182 const output = tail.lines. filter (isCodexOutput);
183 if (output. length > 0 ) return { ... worker, state: 'running' , activity: 'session_output' , sessionFile, lastActivityAt: tail.stat.mtimeMs };
184 return { ... worker, state: 'starting' , activity: 'none' , sessionFile };
185 }));
186 }
187
188 const ADAPTERS = {
189 codex: {
190 executable: 'codex' ,
191 args ( prompt , { cwd } = {}) {
192 if ( ! cwd) return [ 'exec' , '--json' , ... CODEX_NON_INTERACTIVE_FLAGS , prompt];
193 return [ 'exec' , ... CODEX_NON_INTERACTIVE_FLAGS , '--cd' , cwd, '--json' , pinnedPrompt (prompt, { projectRoot: cwd })];
194 },
195 workerReports: codexWorkerReports,
196 reconcileWorkers: reconcileCodexWorkers,
197 },
198 claude: {
199 executable: 'claude' ,
200 args ( prompt , { cwd } = {}) {
201 return [ '--dangerously-skip-permissions' , '-p' , '--output-format' , 'stream-json' , '--verbose' , pinnedPrompt (prompt, { projectRoot: cwd })];
202 },
203 },
204 };
205
206 function probe ( id , { executablePath , env = process.env } = {}) {
207 const adapter = ADAPTERS [id];
208 if ( ! adapter) return { ok: false , id, reason: 'unknown_adapter' };
209 const executable = executablePath || adapter.executable;
210 const result = spawnSync (executable, [ '--version' ], { encoding: 'utf8' , timeout: 5000 , env });
211 if (result.error) return { ok: false , id, executable, reason: result.error.code === 'ENOENT' ? 'not_installed' : 'probe_failed' };
212 if (result.status !== 0 ) return { ok: false , id, executable, reason: 'version_failed' };
213 return { ok: true , id, executable, version: `${ result . stdout || result . stderr }` . trim (), capabilities: [ 'headless' , 'events' , 'session' , 'cwd' , 'cancel' ] };
214 }
215
216 const INVOCATION_MARKERS = {
217 codex: [
218 [ 'CODEX_THREAD_ID' , ( value ) => Boolean ( String (value || '' ). trim ())],
219 [ 'CODEX_CI' , ( value ) => String (value || '' ). trim () === '1' ],
220 ],
221 claude: [
222 [ 'CLAUDE_CODE_SESSION_ID' , ( value ) => Boolean ( String (value || '' ). trim ())],
223 [ 'CLAUDECODE' , ( value ) => Boolean ( String (value || '' ). trim ())],
224 [ 'CLAUDE_CODE_ENTRYPOINT' , ( value ) => Boolean ( String (value || '' ). trim ())],
225 ],
226 };
227
228 function detectInvokingAgent ( env = process.env) {
229 const matches = Object. entries ( INVOCATION_MARKERS ). flatMap (([ id , markers ]) => markers
230 . filter (([ name , matchesMarker ]) => matchesMarker (env[name]))
231 . map (([ name ]) => ({ id, name })));
232 const ids = [ ...new Set (matches. map (( match ) => match.id))];
233 if (ids. length === 1 ) {
234 return {
235 id: ids[ 0 ],
236 mode: 'invocation' ,
237 reason: `invoking ${ ids [ 0 ] } environment detected (${ matches . filter (( match ) => match . id === ids [ 0 ]). map (( match ) => match . name ). join ( ', ' ) })` ,
238 markers: matches. filter (( match ) => match.id === ids[ 0 ]). map (( match ) => match.name),
239 };
240 }
241 if (ids. length > 1 ) {
242 return {
243 id: null ,
244 mode: 'availability' ,
245 reason: `conflicting invoking-agent markers (${ matches . map (( match ) => `${ match . id }:${ match . name }` ). join ( ', ' ) })` ,
246 markers: matches. map (( match ) => `${ match . id }:${ match . name }` ),
247 };
248 }
249 return { id: null , mode: 'availability' , reason: 'no invoking-agent environment marker detected' , markers: [] };
250 }
251
252 function authProbe ( id , executable , env ) {
253 const args = id === 'codex' ? [ 'login' , 'status' ] : [ 'auth' , 'status' ];
254 const result = spawnSync (executable, args, { encoding: 'utf8' , timeout: 5000 , env });
255 if (result.error) return { ok: false , reason: result.error.code === 'ENOENT' ? 'not_installed' : 'auth_probe_failed' };
256 if (result.status !== 0 ) return { ok: false , reason: 'not_authenticated' };
257 if (id === 'claude' ) {
258 try {
259 const parsed = JSON . parse ( `${ result . stdout || result . stderr }` );
260 if (parsed.loggedIn !== true ) return { ok: false , reason: 'not_authenticated' };
261 } catch {
262 return { ok: false , reason: 'auth_probe_failed' };
263 }
264 }
265 return { ok: true };
266 }
267
268 function probeWithAuth ( id , options = {}) {
269 const result = probe (id, { ... options, executablePath: options.executablePath || options.agentPath });
270 if ( ! result.ok || ! options.checkAuth) return result;
271 const auth = authProbe (id, result.executable, options.env || process.env);
272 return auth.ok ? { ... result, authenticated: true } : { ... result, ok: false , reason: auth.reason, authenticated: false };
273 }
274
275 function chooseAdapter ( id = 'auto' , options = {}) {
276 if (id !== 'auto' ) {
277 const selected = probeWithAuth (id, { ... options, checkAuth: options.checkAuth === true });
278 return selected.ok
279 ? { ... selected, selection: { mode: 'explicit' , requested: id, reason: `explicit --agent ${ id }` }, probes: [selected] }
280 : { ... selected, reason: selected.reason || 'agent_unavailable' , selection: { mode: 'explicit' , requested: id, reason: `explicit --agent ${ id }` }, probes: [selected] };
281 }
282
283 const invocation = detectInvokingAgent (options.env || process.env);
284 const ids = invocation.id ? [invocation.id] : [ 'codex' , 'claude' ];
285 const probes = ids. map (( candidate ) => probeWithAuth (candidate, { ... options, checkAuth: true }));
286 const selected = probes. find (( result ) => result.ok);
287 const selection = invocation.id
288 ? { ... invocation, requested: 'auto' }
289 : {
290 mode: 'availability' ,
291 requested: 'auto' ,
292 reason: `${ invocation . reason }; selected ${ selected ? selected . id : 'none'} from installed and authenticated adapters${ selected && probes . filter (( probe ) => probe . ok ). length > 1 ? ' (codex tie-break order)' : ''}` ,
293 markers: invocation.markers,
294 };
295 return selected
296 ? { ... selected, probes, selection }
297 : { ok: false , reason: invocation.id ? 'invoking_agent_unavailable' : 'no_compatible_agent' , probes, selection };
298 }
299
300 function launch ( id , { cwd , prompt , executablePath , onEvent , onWorker }) {
301 const adapter = ADAPTERS [id];
302 if ( ! adapter) throw new Error ( `unknown adapter: ${ id }` );
303 if ( ! cwd || ! path. isAbsolute (cwd)) throw new Error ( 'agent cwd must be an absolute project root' );
304 const projectRoot = path. resolve (cwd);
305 const child = spawn (executablePath || adapter.executable, adapter. args (prompt, { cwd: projectRoot }), {
306 cwd: projectRoot,
307 stdio: [ 'ignore' , 'pipe' , 'pipe' ],
308 detached: process.platform !== 'win32' ,
309 });
310 let buffered = '' ;
311 child.stdout. on ( 'data' , ( chunk ) => {
312 buffered += chunk;
313 let newline;
314 while ((newline = buffered. indexOf ( ' \n ' )) >= 0 ) {
315 const line = buffered. slice ( 0 , newline);
316 buffered = buffered. slice (newline + 1 );
317 if ( ! line. trim ()) continue ;
318 let raw;
319 try { raw = JSON . parse (line); } catch { raw = { text: line }; }
320 onEvent ?.({ type: 'vendor_event' , raw });
321 // Worker reporting is an explicit adapter capability. Do not infer workers from a
322 // vendor event unless the adapter supplies a parser for its own schema.
323 if ( typeof adapter.workerReports === 'function' ) {
324 const workers = adapter. workerReports (raw);
325 for ( const worker of workers || []) onWorker ?.(worker);
326 }
327 }
328 });
329 child.stderr. on ( 'data' , ( chunk ) => onEvent ?.({ type: 'vendor_stderr' , message: String (chunk). trim () }));
330 return child;
331 }
332
333 function requestCancel ( child ) {
334 if ( ! child || child.exitCode !== null ) return false ;
335 if (process.platform !== 'win32' && child.pid) process. kill ( - child.pid, 'SIGTERM' );
336 else child. kill ( 'SIGTERM' );
337 return true ;
338 }
339
340 async function reconcileWorkers ( id , workers , options = {}) {
341 const reconcile = ADAPTERS [id]?.reconcileWorkers;
342 return typeof reconcile === 'function' ? reconcile (workers, options) : [];
343 }
344
345 module . exports = {
346 ADAPTERS,
347 TOOLKIT_ROOT,
348 pinnedPrompt,
349 CODEX_NON_INTERACTIVE_FLAGS,
350 codexWorkerReports,
351 reconcileCodexWorkers,
352 probe,
353 chooseAdapter,
354 detectInvokingAgent,
355 launch,
356 reconcileWorkers,
357 requestCancel,
358 };