job-forge 2.14.36 → 2.14.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,847 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import {
6
+ defaultOpenCodeDbPath,
7
+ discoverSessions as discoverTraceSessions,
8
+ findSessionById,
9
+ inspectSession,
10
+ iterateEvents,
11
+ loadSessionFromPath,
12
+ parseOpenCode,
13
+ parseSinceCutoff,
14
+ refFromPath,
15
+ sessionRefsFromOpenCodeRows,
16
+ stats as traceStats,
17
+ } from '@razroo/iso-trace';
18
+
19
+ export const DISPATCH_TOOL_NAMES = new Set(['task', 'spawn_agent']);
20
+
21
+ export async function discoverProjectSessions({ cwd, since, harness } = {}) {
22
+ const resolvedCwd = cwd ? resolve(cwd) : undefined;
23
+ if (!resolvedCwd) {
24
+ return discoverTraceSessions({
25
+ cwd: resolvedCwd,
26
+ since,
27
+ ...(harness ? { harness } : {}),
28
+ });
29
+ }
30
+
31
+ const refs = [];
32
+ const byHarness = projectTraceRootsByHarness(resolvedCwd, since);
33
+ const harnesses = harness ? [harness] : ['claude-code', 'cursor', 'codex', 'opencode'];
34
+
35
+ for (const name of harnesses) {
36
+ if (name === 'codex') {
37
+ refs.push(...discoverCodexRefs(resolvedCwd, byHarness.codex || []));
38
+ continue;
39
+ }
40
+ if (name === 'opencode') {
41
+ refs.push(...discoverOpenCodeRefs(resolvedCwd, since, byHarness.opencode?.[0]));
42
+ continue;
43
+ }
44
+ const roots = byHarness[harnessKey(name)] || [];
45
+ if (roots.length === 0) continue;
46
+ const found = await discoverTraceSessions({
47
+ cwd: resolvedCwd,
48
+ since,
49
+ harness: name,
50
+ roots,
51
+ });
52
+ refs.push(...found);
53
+ }
54
+
55
+ refs.sort((a, b) => (b.startedAt > a.startedAt ? 1 : b.startedAt < a.startedAt ? -1 : 0));
56
+ return refs;
57
+ }
58
+
59
+ export function loadObservedSession(ref) {
60
+ if (ref?.source?.harness === 'opencode' && /#session=/.test(ref.source.path || '')) {
61
+ return loadObservedOpenCodeSession(ref);
62
+ }
63
+ return loadSessionFromPath(ref.source.path, ref.source.harness);
64
+ }
65
+
66
+ export function inspectionForSession(session, options = {}) {
67
+ return inspectSession(session, options);
68
+ }
69
+
70
+ export function statsForSessions(sessions) {
71
+ return traceStats(sessions);
72
+ }
73
+
74
+ export function findObservedSession(refs, idOrPrefix) {
75
+ return findSessionById(refs, idOrPrefix);
76
+ }
77
+
78
+ export function messageEvents(session, role) {
79
+ const events = [];
80
+ for (const item of iterateEvents(session)) {
81
+ if (item.event.kind !== 'message') continue;
82
+ if (role && item.event.role !== role) continue;
83
+ events.push({
84
+ at: item.at,
85
+ atMs: Date.parse(item.at),
86
+ role: item.event.role,
87
+ text: item.event.text || '',
88
+ turnIndex: item.turnIndex,
89
+ });
90
+ }
91
+ return events;
92
+ }
93
+
94
+ export function toolRecords(session) {
95
+ const records = [];
96
+ const pending = new Map();
97
+
98
+ for (const item of iterateEvents(session)) {
99
+ if (item.event.kind === 'tool_call') {
100
+ const record = {
101
+ id: item.event.id || '',
102
+ name: item.event.name || '',
103
+ input: item.event.input,
104
+ at: item.at,
105
+ atMs: Date.parse(item.at),
106
+ role: item.role,
107
+ turnIndex: item.turnIndex,
108
+ };
109
+ records.push(record);
110
+ if (record.id) pending.set(record.id, record);
111
+ continue;
112
+ }
113
+
114
+ if (item.event.kind === 'tool_result') {
115
+ const result = {
116
+ toolUseId: item.event.toolUseId || '',
117
+ output: item.event.output || '',
118
+ error: item.event.error || '',
119
+ truncated: Boolean(item.event.truncated),
120
+ at: item.at,
121
+ atMs: Date.parse(item.at),
122
+ role: item.role,
123
+ turnIndex: item.turnIndex,
124
+ };
125
+ const record = pending.get(result.toolUseId);
126
+ if (record && !record.result) {
127
+ record.result = result;
128
+ } else {
129
+ records.push({
130
+ id: result.toolUseId,
131
+ name: '',
132
+ input: null,
133
+ at: result.at,
134
+ atMs: result.atMs,
135
+ role: result.role,
136
+ turnIndex: result.turnIndex,
137
+ result,
138
+ });
139
+ }
140
+ }
141
+ }
142
+
143
+ return records;
144
+ }
145
+
146
+ export function collectDispatchCalls(session) {
147
+ return toolRecords(session)
148
+ .filter((record) => DISPATCH_TOOL_NAMES.has(record.name))
149
+ .map((record) => dispatchSummary(record));
150
+ }
151
+
152
+ export function referencedChildSessionIds(session) {
153
+ return collectDispatchCalls(session)
154
+ .map((call) => call.sessionId)
155
+ .filter(Boolean);
156
+ }
157
+
158
+ export function buildSessionGraph(refs, sessionsById) {
159
+ const childIds = new Set();
160
+ const childrenBySession = new Map();
161
+
162
+ for (const ref of refs) {
163
+ const session = sessionsById.get(ref.id);
164
+ const childList = session ? referencedChildSessionIds(session) : [];
165
+ childrenBySession.set(ref.id, childList);
166
+ for (const childId of childList) childIds.add(childId);
167
+ }
168
+
169
+ const roots = refs.filter((ref) => !childIds.has(ref.id));
170
+ return { childIds, childrenBySession, roots };
171
+ }
172
+
173
+ export function descendantIds(rootId, childrenBySession) {
174
+ const visited = new Set();
175
+ const queue = [...(childrenBySession.get(rootId) || [])];
176
+
177
+ while (queue.length > 0) {
178
+ const current = queue.shift();
179
+ if (!current || visited.has(current)) continue;
180
+ visited.add(current);
181
+ for (const child of childrenBySession.get(current) || []) {
182
+ if (!visited.has(child)) queue.push(child);
183
+ }
184
+ }
185
+
186
+ return [...visited];
187
+ }
188
+
189
+ export function userRequestSummaries(session) {
190
+ return messageEvents(session, 'user')
191
+ .map((event) => {
192
+ const prompt = clean(redactSecrets(event.text));
193
+ return {
194
+ at: event.at,
195
+ atMs: event.atMs,
196
+ prompt,
197
+ requestedJobs: requestedJobCount(prompt),
198
+ };
199
+ })
200
+ .filter((request) => request.prompt.length > 0);
201
+ }
202
+
203
+ export function providerErrorsForSession(session) {
204
+ const seen = new Set();
205
+ const models = modelUsageFromSession(session);
206
+ const primary = models[0] || { provider: '', model: '' };
207
+ const errors = [];
208
+
209
+ for (const item of iterateEvents(session)) {
210
+ let raw = '';
211
+ if (item.event.kind === 'message' && item.event.role === 'assistant') {
212
+ raw = item.event.text || '';
213
+ } else if (item.event.kind === 'tool_result' && item.event.error) {
214
+ raw = item.event.error;
215
+ }
216
+ if (!raw) continue;
217
+
218
+ const statusCode = statusCodeFromText(raw);
219
+ const category = providerErrorCategory(raw, statusCode);
220
+ if (!statusCode && category === 'provider-error' && !/\berror\b/i.test(raw)) continue;
221
+
222
+ const message = redactSecrets(raw);
223
+ const key = `${item.at}\u0000${message}`;
224
+ if (seen.has(key)) continue;
225
+ seen.add(key);
226
+ errors.push({
227
+ at: item.at,
228
+ provider: primary.provider,
229
+ model: primary.model,
230
+ statusCode,
231
+ category,
232
+ message,
233
+ });
234
+ }
235
+
236
+ return errors;
237
+ }
238
+
239
+ export function modelUsageFromSession(session) {
240
+ const counts = new Map();
241
+
242
+ for (const item of iterateEvents(session)) {
243
+ if (item.event.kind !== 'token_usage' || !item.event.model) continue;
244
+ addModelCount(counts, item.event.model);
245
+ }
246
+
247
+ if (counts.size === 0 && session.model) addModelCount(counts, session.model);
248
+
249
+ return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
250
+ }
251
+
252
+ export function mergeModelUsage(groups) {
253
+ const counts = new Map();
254
+ for (const group of groups || []) {
255
+ for (const item of group || []) {
256
+ const provider = stringValue(item.provider);
257
+ const model = stringValue(item.model);
258
+ const key = `${provider}\u0000${model}`;
259
+ const current = counts.get(key) || { provider, model, count: 0 };
260
+ current.count += Number(item.count || 0);
261
+ counts.set(key, current);
262
+ }
263
+ }
264
+ return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
265
+ }
266
+
267
+ export function summarizeChildSession(session) {
268
+ const inspection = inspectionForSession(session);
269
+ const assistantTexts = messageEvents(session, 'assistant').map((event) => event.text || '');
270
+ const finalText = assistantTexts.slice(-5).join('\n');
271
+ const trackerWrites = [
272
+ ...inspection.filesTouched.written,
273
+ ...inspection.filesTouched.edited,
274
+ ].filter((path) => /batch\/tracker-additions\/.*\.tsv/.test(path)).length;
275
+ const providerErrors = providerErrorsForSession(session);
276
+ const dispatchCalls = collectDispatchCalls(session);
277
+ const dedupeMiss = /\b(DUPLICATE|already\s+\*{0,2}Applied|already applied|per \[H2\]|Hard Limit #2|No re-dispatch needed)\b/i.test(finalText) ||
278
+ /\bpreviously applied (on|as|under)\b/i.test(finalText);
279
+
280
+ return {
281
+ id: session.id,
282
+ title: session.title || '',
283
+ startedAt: session.startedAt,
284
+ startedAtMs: Date.parse(session.startedAt),
285
+ endedAt: session.endedAt,
286
+ outcome: outcomeFromText(finalText, trackerWrites),
287
+ providerErrors: providerErrors.length,
288
+ taskCalls: dispatchCalls.filter((call) => call.name === 'task').length,
289
+ dispatchCalls: dispatchCalls.length,
290
+ toolErrors: inspection.toolErrorCount,
291
+ dedupeMiss,
292
+ trackerWrites,
293
+ models: modelUsageFromSession(session),
294
+ };
295
+ }
296
+
297
+ export function outcomeFromText(text, trackerWrites = 0) {
298
+ const explicitFailed = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(FAILED|APPLY FAILED)\b/i.test(text) ||
299
+ /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Failed\*\*?/i.test(text);
300
+ const explicitSkipped = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(SKIP|SKIPPED|DISCARDED|DISCARD)\b/i.test(text) ||
301
+ /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?(SKIP|SKIPPED|Discarded|DISCARDED)\*\*?/i.test(text);
302
+ const explicitApplied = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*APPLIED\b/i.test(text) ||
303
+ /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Applied\*\*?/i.test(text);
304
+
305
+ if (explicitFailed) return 'Failed';
306
+ if (explicitSkipped) return 'Discarded';
307
+ if (explicitApplied) return 'Applied';
308
+
309
+ if (/\bAPPLY FAILED\b/i.test(text) || /^\s*(FAILED|Failed)\b/m.test(text)) return 'Failed';
310
+ if (/^\s*(SKIP|SKIPPED|DISCARDED|Discarded)\b/m.test(text) ||
311
+ /\b(DUPLICATE|job posting closed|role no longer available)\b/i.test(text)) return 'Discarded';
312
+ if (/\bwith\s+\*\*?Applied\*\*?\s+status\b/i.test(text) ||
313
+ /\bAPPLIED\s+https?:\/\//i.test(text) ||
314
+ /\b(successfully submitted|Applied via|Thank you for applying|confirmation page)\b/i.test(text)) return 'Applied';
315
+ if (trackerWrites > 0) return 'TSV written';
316
+ return 'unknown';
317
+ }
318
+
319
+ export function hasOutcome(text) {
320
+ return outcomeFromText(text) !== 'unknown' ||
321
+ /tracker-additions\/.*\.tsv/i.test(text) ||
322
+ /\bAll\s+\d+\s+jobs?\s+dispatched\b/i.test(text) ||
323
+ /\*\*(Applied|Skipped|Failed|Discarded)\s*\(\d+\):\*\*/i.test(text);
324
+ }
325
+
326
+ export function requestedJobCount(prompt) {
327
+ const text = String(prompt || '').toLowerCase();
328
+ if (!/\b(job|jobs|application|applications)\b/.test(text)) return null;
329
+ if (!/\b(apply|applt|another|nother|more|process)\b/.test(text)) return null;
330
+ const match = text.match(/\b(\d{1,3})\b/);
331
+ return match ? Number(match[1]) : null;
332
+ }
333
+
334
+ export function firstUrl(text) {
335
+ const match = String(text || '').match(/https?:\/\/[^\s)>\]]+/i);
336
+ return match ? match[0].replace(/[.,;]+$/, '') : '';
337
+ }
338
+
339
+ export function duplicateDispatchUrlCount(calls) {
340
+ const seen = new Set();
341
+ const duplicates = new Set();
342
+ for (const call of calls) {
343
+ if (!call.url || call.isStatusPoll) continue;
344
+ if (seen.has(call.url)) duplicates.add(call.url);
345
+ seen.add(call.url);
346
+ }
347
+ return duplicates.size;
348
+ }
349
+
350
+ export function mentionsLimitedCandidatePool(text) {
351
+ return /\b(only|just)\s+\d+\s+(candidate|candidates|jobs?|applications?)\b/i.test(text) ||
352
+ /\b(no more|not enough|ran out of|exhausted)\s+(candidate|candidates|jobs?|applications?|pipeline)\b/i.test(text);
353
+ }
354
+
355
+ export function statusCodeFromText(text) {
356
+ const match = String(text).match(/\b(40[0-9]|42[0-9]|50[0-9])\b/);
357
+ return match ? Number(match[1]) : undefined;
358
+ }
359
+
360
+ export function providerErrorCategory(text, statusCode) {
361
+ if (statusCode === 402 || /insufficient|balance|credits|diem/i.test(text)) return 'balance';
362
+ if (statusCode === 429 || /rate.?limit|quota/i.test(text)) return 'rate-limit';
363
+ if (/overload|temporarily unavailable|timeout/i.test(text)) return 'transient';
364
+ return 'provider-error';
365
+ }
366
+
367
+ export function hasProxyLeak(text) {
368
+ const raw = String(text || '');
369
+ if (!/proxy/i.test(raw)) return false;
370
+ return /\b(server|username|password|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/i.test(raw) ||
371
+ /brd-customer|superproxy|oxylabs|smartproxy|soax/i.test(raw);
372
+ }
373
+
374
+ export function redactSecrets(text) {
375
+ return String(text || '')
376
+ .replace(/\b(password|username|server|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/gi, '$1=<redacted>')
377
+ .replace(/brd-customer-[A-Za-z0-9_.-]+/g, '<redacted-proxy-user>');
378
+ }
379
+
380
+ export function relativeToProject(file, projectDir) {
381
+ const root = resolve(projectDir || '.');
382
+ return String(file || '').startsWith(`${root}/`) ? String(file).slice(root.length + 1) : String(file || '');
383
+ }
384
+
385
+ export function clean(text) {
386
+ return String(text || '').replace(/\s+/g, ' ').trim();
387
+ }
388
+
389
+ export function shorten(value, max) {
390
+ const text = clean(value);
391
+ if (text.length <= max) return text;
392
+ return `${text.slice(0, Math.max(1, max - 1)).trimEnd()}...`;
393
+ }
394
+
395
+ export function pad(value, width) {
396
+ const text = String(value ?? '');
397
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
398
+ }
399
+
400
+ export function stringValue(value) {
401
+ return typeof value === 'string' ? value : value == null ? '' : String(value);
402
+ }
403
+
404
+ export function objectOrEmpty(value) {
405
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
406
+ }
407
+
408
+ export function safeJson(value) {
409
+ try {
410
+ return JSON.stringify(value);
411
+ } catch {
412
+ return '';
413
+ }
414
+ }
415
+
416
+ export function modelLabel(model) {
417
+ return `${model.provider || '(unknown)'}/${model.model || '(unknown)'} x${model.count}`;
418
+ }
419
+
420
+ export function isFreeModelRoute(provider, model) {
421
+ const route = `${provider}/${model}`.toLowerCase();
422
+ return route.includes(':free') ||
423
+ route.includes('/big-pickle') ||
424
+ route.includes('minimax-m2.5-free') ||
425
+ route.includes('glm-4.5-air') ||
426
+ route.includes('gpt-oss-20b') ||
427
+ route.includes('qwen3-next-80b-a3b-instruct:free');
428
+ }
429
+
430
+ function dispatchSummary(record) {
431
+ const input = objectOrEmpty(record.input);
432
+ const prompt = promptTextFromInput(input);
433
+ const description = stringValue(input.description || input.message || firstLine(prompt));
434
+ const sessionId = dispatchSessionId(record.name, input, record.result?.output);
435
+ const subagentType = stringValue(
436
+ input.subagent_type ||
437
+ input.agent_type ||
438
+ input.profile ||
439
+ input.agent ||
440
+ input.model ||
441
+ objectOrEmpty(input.metadata).subagent_type ||
442
+ objectOrEmpty(input.metadata).agent,
443
+ );
444
+ const isStatusPoll = record.name === 'task' && (
445
+ Boolean(input.task_id) ||
446
+ /\b(check|poll|status|force|abort|progress|result)\b/i.test(description) ||
447
+ /\b(return your final outcome now|if still working|current status|report your current status|still running)\b/i.test(prompt)
448
+ );
449
+
450
+ return {
451
+ name: record.name,
452
+ at: record.at,
453
+ atMs: record.atMs,
454
+ description,
455
+ prompt,
456
+ promptBytes: Buffer.byteLength(prompt, 'utf8'),
457
+ sessionId,
458
+ subagentType,
459
+ status: record.result ? (record.result.error ? 'failed' : 'completed') : 'unknown',
460
+ isStatusPoll,
461
+ proxyLeak: hasProxyLeak(prompt),
462
+ url: firstUrl(prompt),
463
+ };
464
+ }
465
+
466
+ function dispatchSessionId(toolName, input, outputText) {
467
+ if (toolName === 'spawn_agent') {
468
+ const resultObject = parseMaybeJson(outputText);
469
+ const outputId = objectIdForDispatch(resultObject);
470
+ if (outputId) return outputId;
471
+ }
472
+
473
+ const metadata = objectOrEmpty(input.metadata);
474
+ const direct = [
475
+ stringValue(input.task_id),
476
+ stringValue(input.sessionId),
477
+ stringValue(input.session_id),
478
+ stringValue(metadata.sessionId),
479
+ stringValue(input.target),
480
+ ].find(Boolean);
481
+ if (direct) return direct;
482
+
483
+ const fromInputObject = objectIdForDispatch(input);
484
+ if (fromInputObject) return fromInputObject;
485
+
486
+ const fromOutput = objectIdForDispatch(parseMaybeJson(outputText)) || idFromText(outputText);
487
+ return fromOutput || '';
488
+ }
489
+
490
+ function promptTextFromInput(input) {
491
+ const parts = [];
492
+ if (typeof input.prompt === 'string' && input.prompt) parts.push(input.prompt);
493
+ if (typeof input.message === 'string' && input.message) parts.push(input.message);
494
+ if (Array.isArray(input.items)) {
495
+ for (const item of input.items) {
496
+ if (item && typeof item === 'object' && typeof item.text === 'string' && item.text) {
497
+ parts.push(item.text);
498
+ }
499
+ }
500
+ }
501
+ return parts.join('\n');
502
+ }
503
+
504
+ function firstLine(text) {
505
+ return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
506
+ }
507
+
508
+ function addModelCount(counts, route) {
509
+ const raw = String(route || '').trim();
510
+ if (!raw) return;
511
+ const slash = raw.indexOf('/');
512
+ const provider = slash === -1 ? '' : raw.slice(0, slash);
513
+ const model = slash === -1 ? raw : raw.slice(slash + 1);
514
+ const key = `${provider}\u0000${model}`;
515
+ const current = counts.get(key) || { provider, model, count: 0 };
516
+ current.count += 1;
517
+ counts.set(key, current);
518
+ }
519
+
520
+ function objectIdForDispatch(value, depth = 0) {
521
+ if (!value || depth > 4) return '';
522
+ if (typeof value === 'string') return isLikelySessionId(value) ? value : '';
523
+ if (Array.isArray(value)) {
524
+ for (const item of value) {
525
+ const found = objectIdForDispatch(item, depth + 1);
526
+ if (found) return found;
527
+ }
528
+ return '';
529
+ }
530
+ if (typeof value !== 'object') return '';
531
+
532
+ for (const key of ['sessionId', 'session_id', 'task_id', 'agent_id', 'target']) {
533
+ const direct = value[key];
534
+ if (typeof direct === 'string' && isLikelySessionId(direct)) return direct;
535
+ }
536
+ if (typeof value.id === 'string' && isLikelySessionId(value.id)) return value.id;
537
+
538
+ for (const nested of Object.values(value)) {
539
+ const found = objectIdForDispatch(nested, depth + 1);
540
+ if (found) return found;
541
+ }
542
+ return '';
543
+ }
544
+
545
+ function parseMaybeJson(text) {
546
+ if (typeof text !== 'string' || !text.trim()) return null;
547
+ try {
548
+ return JSON.parse(text);
549
+ } catch {
550
+ const start = text.indexOf('{');
551
+ const end = text.lastIndexOf('}');
552
+ if (start !== -1 && end > start) {
553
+ try {
554
+ return JSON.parse(text.slice(start, end + 1));
555
+ } catch {
556
+ return null;
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+ }
562
+
563
+ function idFromText(text) {
564
+ const raw = String(text || '');
565
+ const patterns = [
566
+ /\b(?:session|task|agent)[-_ ]?id["'\s:=]+([A-Za-z0-9._:-]{8,})/i,
567
+ /\b([0-9a-f]{8}-[0-9a-f-]{27,})\b/i,
568
+ /\b([A-Za-z]{2,8}_[A-Za-z0-9._:-]{6,})\b/,
569
+ ];
570
+ for (const pattern of patterns) {
571
+ const match = raw.match(pattern);
572
+ if (match?.[1] && isLikelySessionId(match[1])) return match[1];
573
+ }
574
+ return '';
575
+ }
576
+
577
+ function isLikelySessionId(value) {
578
+ const text = String(value || '').trim();
579
+ if (text.length < 8 || /\s/.test(text)) return false;
580
+ return /^[A-Za-z0-9._:-]+$/.test(text);
581
+ }
582
+
583
+ function projectTraceRootsByHarness(cwd, since) {
584
+ return {
585
+ claude: [claudeProjectRoot(cwd)].filter(safeExists),
586
+ cursor: [cursorProjectRoot(cwd)].filter(safeExists),
587
+ codex: codexProjectRoots(since).filter(safeExists),
588
+ opencode: [defaultOpenCodeDbPath()].filter(safeExists),
589
+ };
590
+ }
591
+
592
+ function harnessKey(name) {
593
+ if (name === 'claude-code') return 'claude';
594
+ return name;
595
+ }
596
+
597
+ function claudeProjectRoot(cwd) {
598
+ return join(homedir(), '.claude', 'projects', cwd.replace(/\//g, '-'));
599
+ }
600
+
601
+ function cursorProjectRoot(cwd) {
602
+ return join(homedir(), '.cursor', 'projects', cwd.replace(/^\/+/, '').replace(/\//g, '-'));
603
+ }
604
+
605
+ function codexProjectRoots(since) {
606
+ const base = join(homedir(), '.codex', 'sessions');
607
+ if (!safeExists(base)) return [];
608
+ const cutoff = parseSinceCutoff(since);
609
+ if (cutoff === undefined) return [base];
610
+
611
+ const roots = [];
612
+ for (const year of readDirNames(base)) {
613
+ if (!/^\d{4}$/.test(year)) continue;
614
+ const yearDir = join(base, year);
615
+ for (const month of readDirNames(yearDir)) {
616
+ if (!/^\d{2}$/.test(month)) continue;
617
+ const monthDir = join(yearDir, month);
618
+ for (const day of readDirNames(monthDir)) {
619
+ if (!/^\d{2}$/.test(day)) continue;
620
+ const start = Date.UTC(Number(year), Number(month) - 1, Number(day));
621
+ const end = start + 86_400_000 - 1;
622
+ if (end >= cutoff) roots.push(join(monthDir, day));
623
+ }
624
+ }
625
+ }
626
+ return roots.length > 0 ? roots : [base];
627
+ }
628
+
629
+ function discoverCodexRefs(cwd, roots) {
630
+ if (roots.length === 0) return [];
631
+ if (!commandExists('rg')) {
632
+ return discoverCodexRefsFallback(roots, cwd);
633
+ }
634
+
635
+ const pattern = `"cwd":"${cwd}"`;
636
+ const result = spawnSync('rg', [
637
+ '-l',
638
+ '--fixed-strings',
639
+ '--glob',
640
+ '*.jsonl',
641
+ pattern,
642
+ ...roots,
643
+ ], {
644
+ encoding: 'utf8',
645
+ maxBuffer: 32 * 1024 * 1024,
646
+ });
647
+
648
+ if ((result.status ?? 1) !== 0 && (result.status ?? 1) !== 1) {
649
+ return discoverCodexRefsFallback(roots, cwd);
650
+ }
651
+
652
+ const files = (result.stdout || '')
653
+ .split(/\r?\n/)
654
+ .map((line) => line.trim())
655
+ .filter(Boolean);
656
+ return files.map((file) => refFromPath(file, 'codex'));
657
+ }
658
+
659
+ function discoverOpenCodeRefs(cwd, since, dbPath = defaultOpenCodeDbPath()) {
660
+ if (!safeExists(dbPath)) return [];
661
+ const where = [
662
+ 's.time_archived is null',
663
+ `s.directory = ${sqlString(cwd)}`,
664
+ ];
665
+ const sinceMs = parseSinceCutoff(since);
666
+ if (sinceMs !== undefined) where.push(`s.time_created >= ${Number(sinceMs)}`);
667
+
668
+ const sql = [
669
+ 'select',
670
+ ' s.id,',
671
+ " replace(replace(coalesce(s.title, ''), char(10), ' '), char(13), ' ') as title,",
672
+ ' s.directory,',
673
+ ' s.time_created,',
674
+ ' s.time_updated,',
675
+ ' (select count(*) from message m where m.session_id = s.id) as turn_count,',
676
+ ' (',
677
+ ' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
678
+ ' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
679
+ ' ) as size_bytes',
680
+ 'from session s',
681
+ `where ${where.join(' and ')}`,
682
+ 'order by s.time_updated desc',
683
+ ].join(' ');
684
+
685
+ const result = runOpenCodeSqliteQuery(dbPath, sql);
686
+ if ((result.status ?? 0) !== 0) {
687
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
688
+ throw new Error(`job-forge observability: sqlite3 query failed: ${detail}`);
689
+ }
690
+
691
+ const rows = (result.stdout || '')
692
+ .split(/\r?\n/)
693
+ .map((line) => line.trim())
694
+ .filter(Boolean)
695
+ .map((line) => {
696
+ const [id, title, directory, time_created, time_updated, turn_count, size_bytes] = line.split('\x1f');
697
+ return {
698
+ id,
699
+ title: title || null,
700
+ directory,
701
+ time_created: Number(time_created),
702
+ time_updated: Number(time_updated),
703
+ turn_count: Number(turn_count || 0),
704
+ size_bytes: Number(size_bytes || 0),
705
+ };
706
+ });
707
+
708
+ return sessionRefsFromOpenCodeRows(rows, dbPath);
709
+ }
710
+
711
+ function loadObservedOpenCodeSession(ref) {
712
+ const sessionId = sessionIdFromOpenCodeLocator(ref?.source?.path || '');
713
+ if (!sessionId) {
714
+ return loadSessionFromPath(ref.source.path, ref.source.harness);
715
+ }
716
+
717
+ const errors = [];
718
+ for (let attempt = 0; attempt < 3; attempt++) {
719
+ const tmpRoot = mkdtempSync(join(tmpdir(), 'jobforge-opencode-session-'));
720
+ const exportPath = join(tmpRoot, `${sessionId}.json`);
721
+ try {
722
+ exportOpenCodeSessionToFile(sessionId, exportPath);
723
+ const session = parseOpenCode(exportPath);
724
+ return {
725
+ ...session,
726
+ source: ref.source,
727
+ };
728
+ } catch (error) {
729
+ errors.push(error instanceof Error ? error.message : String(error));
730
+ } finally {
731
+ rmSync(tmpRoot, { recursive: true, force: true });
732
+ }
733
+ }
734
+
735
+ throw new Error(`job-forge observability: failed to load OpenCode session ${sessionId}: ${errors.join(' | ')}`);
736
+ }
737
+
738
+ function exportOpenCodeSessionToFile(sessionId, exportPath) {
739
+ const result = spawnSync(process.env.SHELL || 'sh', [
740
+ '-lc',
741
+ `opencode export ${shellQuote(sessionId)} > ${shellQuote(exportPath)}`,
742
+ ], {
743
+ encoding: 'utf8',
744
+ maxBuffer: 1024 * 1024,
745
+ env: process.env,
746
+ });
747
+ if ((result.status ?? 0) !== 0) {
748
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
749
+ throw new Error(`opencode export ${sessionId} failed: ${detail}`);
750
+ }
751
+ }
752
+
753
+ function sessionIdFromOpenCodeLocator(path) {
754
+ const match = String(path || '').match(/#session=([^#]+)$/);
755
+ return match?.[1] ? decodeURIComponent(match[1]) : '';
756
+ }
757
+
758
+ function shellQuote(value) {
759
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
760
+ }
761
+
762
+ function runOpenCodeSqliteQuery(dbPath, sql) {
763
+ let lastResult;
764
+ for (let attempt = 0; attempt < 3; attempt++) {
765
+ const result = spawnSync('sqlite3', ['-separator', '\x1f', dbPath, sql], {
766
+ encoding: 'utf8',
767
+ maxBuffer: 32 * 1024 * 1024,
768
+ });
769
+ if ((result.status ?? 0) === 0) return result;
770
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
771
+ lastResult = result;
772
+ if (!/database is locked|database table is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(detail)) {
773
+ return result;
774
+ }
775
+ sleepMs(100 * (attempt + 1));
776
+ }
777
+ return lastResult;
778
+ }
779
+
780
+ function sleepMs(ms) {
781
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
782
+ }
783
+
784
+ function discoverCodexRefsFallback(roots, cwd) {
785
+ return discoverCodexJsonl(roots)
786
+ .map((file) => {
787
+ try {
788
+ return refFromPath(file, 'codex');
789
+ } catch {
790
+ return null;
791
+ }
792
+ })
793
+ .filter(Boolean)
794
+ .filter((ref) => ref.cwd === cwd);
795
+ }
796
+
797
+ function discoverCodexJsonl(roots) {
798
+ const files = [];
799
+ for (const root of roots) files.push(...walkJsonl(root));
800
+ return files;
801
+ }
802
+
803
+ function walkJsonl(root) {
804
+ const out = [];
805
+ for (const name of readDirNames(root)) {
806
+ const full = join(root, name);
807
+ if (safeDir(full)) {
808
+ out.push(...walkJsonl(full));
809
+ } else if (name.endsWith('.jsonl')) {
810
+ out.push(full);
811
+ }
812
+ }
813
+ return out;
814
+ }
815
+
816
+ function readDirNames(dir) {
817
+ try {
818
+ return readdirSync(dir);
819
+ } catch {
820
+ return [];
821
+ }
822
+ }
823
+
824
+ function safeExists(path) {
825
+ try {
826
+ return existsSync(path);
827
+ } catch {
828
+ return false;
829
+ }
830
+ }
831
+
832
+ function safeDir(path) {
833
+ try {
834
+ return statSync(path).isDirectory();
835
+ } catch {
836
+ return false;
837
+ }
838
+ }
839
+
840
+ function commandExists(command) {
841
+ const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
842
+ return !result.error;
843
+ }
844
+
845
+ function sqlString(value) {
846
+ return `'${String(value).replaceAll("'", "''")}'`;
847
+ }