farming-code 2.2.16 → 2.2.18
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/README.md +2 -2
- package/README.zh_cn.md +2 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/backend/acp-runtime.js +221 -10
- package/backend/agent-manager.js +477 -54
- package/backend/agent-provider-session.js +15 -1
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/config-manager.js +3 -3
- package/backend/farming-app-cli.js +8 -0
- package/backend/native-pty-host-client.js +59 -2
- package/backend/provider-adapters.js +117 -36
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +5 -1
- package/backend/storage-layout.js +5 -0
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/assets/App-DTAQilnX.js +210 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{index-iKBvPjWS.js → index-fp8VhOes.js} +2 -2
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +9 -3
- package/package.json +12 -8
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/dist/assets/App-y6p2yRar.js +0 -206
- package/dist/assets/code-dark-DKBGxoPX.css +0 -1
- package/dist/assets/main-CdkLOXYl.css +0 -1
|
@@ -16,9 +16,13 @@ function emptyPlan(args) {
|
|
|
16
16
|
return {
|
|
17
17
|
provider: '',
|
|
18
18
|
id: '',
|
|
19
|
+
precreate: false,
|
|
19
20
|
temporary: false,
|
|
20
21
|
source: '',
|
|
21
22
|
forkedFromProviderSessionId: '',
|
|
23
|
+
identityWorkspace: '',
|
|
24
|
+
resumeInsertIndex: null,
|
|
25
|
+
error: '',
|
|
22
26
|
args,
|
|
23
27
|
};
|
|
24
28
|
}
|
|
@@ -44,13 +48,23 @@ function buildAgentProviderSessionPlan({ command, program, args, source } = {})
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
const plan = adapter.planSession(rawParts.slice(1), launchArgs);
|
|
47
|
-
if (
|
|
51
|
+
if (plan?.error) {
|
|
52
|
+
return {
|
|
53
|
+
...emptyPlan(launchArgs),
|
|
54
|
+
provider,
|
|
55
|
+
error: String(plan.error),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!plan || (!plan.id && plan.precreate !== true)) return emptyPlan(launchArgs);
|
|
48
59
|
return {
|
|
49
60
|
provider,
|
|
50
61
|
id: plan.id,
|
|
62
|
+
precreate: plan.precreate === true,
|
|
51
63
|
temporary: plan.temporary === true,
|
|
52
64
|
source: plan.source || '',
|
|
53
65
|
forkedFromProviderSessionId: plan.forkedFromProviderSessionId || '',
|
|
66
|
+
identityWorkspace: plan.identityWorkspace || '',
|
|
67
|
+
resumeInsertIndex: Number.isInteger(plan.resumeInsertIndex) ? plan.resumeInsertIndex : null,
|
|
54
68
|
args: Array.isArray(plan.args) ? plan.args : launchArgs,
|
|
55
69
|
};
|
|
56
70
|
}
|
|
@@ -59,6 +59,88 @@ function findCodexRolloutFile(sessionId, options = {}) {
|
|
|
59
59
|
return '';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function recentSessionDirectories(root, now = new Date()) {
|
|
63
|
+
const directories = [];
|
|
64
|
+
for (const offsetDays of [-1, 0, 1]) {
|
|
65
|
+
const date = new Date(now.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
|
66
|
+
directories.push(
|
|
67
|
+
path.join(
|
|
68
|
+
root,
|
|
69
|
+
String(date.getFullYear()),
|
|
70
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
71
|
+
String(date.getDate()).padStart(2, '0'),
|
|
72
|
+
),
|
|
73
|
+
path.join(
|
|
74
|
+
root,
|
|
75
|
+
String(date.getUTCFullYear()),
|
|
76
|
+
String(date.getUTCMonth() + 1).padStart(2, '0'),
|
|
77
|
+
String(date.getUTCDate()).padStart(2, '0'),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return [...new Set(directories)];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function findCodexRolloutFileAsync(sessionId, options = {}) {
|
|
85
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
86
|
+
if (!normalizedSessionId) return '';
|
|
87
|
+
const codexHome = normalizePathValue(options.codexHome || path.join(os.homedir(), '.codex'));
|
|
88
|
+
const maxEntries = Number.isFinite(options.maxEntries)
|
|
89
|
+
? Math.max(1, Math.floor(options.maxEntries))
|
|
90
|
+
: 100_000;
|
|
91
|
+
const deadline = Number.isFinite(options.deadline)
|
|
92
|
+
? Number(options.deadline)
|
|
93
|
+
: Date.now() + 5_000;
|
|
94
|
+
const sessionsRoot = path.join(codexHome, 'sessions');
|
|
95
|
+
const archivedRoot = path.join(codexHome, 'archived_sessions');
|
|
96
|
+
const preferredDirectories = [
|
|
97
|
+
...recentSessionDirectories(sessionsRoot),
|
|
98
|
+
...(options.recentOnly === true ? [] : [archivedRoot]),
|
|
99
|
+
];
|
|
100
|
+
const visited = new Set();
|
|
101
|
+
let inspectedEntries = 0;
|
|
102
|
+
|
|
103
|
+
async function inspectDirectory(directory, descend) {
|
|
104
|
+
if (Date.now() > deadline || inspectedEntries >= maxEntries || visited.has(directory)) return '';
|
|
105
|
+
visited.add(directory);
|
|
106
|
+
let entries;
|
|
107
|
+
try {
|
|
108
|
+
entries = await fsp.readdir(directory, { withFileTypes: true });
|
|
109
|
+
} catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
const subdirectories = [];
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
inspectedEntries += 1;
|
|
115
|
+
if (Date.now() > deadline || inspectedEntries > maxEntries) return '';
|
|
116
|
+
const fullPath = path.join(directory, entry.name);
|
|
117
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl') && sessionIdFromFilePath(fullPath) === normalizedSessionId) {
|
|
118
|
+
return fullPath;
|
|
119
|
+
}
|
|
120
|
+
if (descend && entry.isDirectory()) subdirectories.push(fullPath);
|
|
121
|
+
}
|
|
122
|
+
return subdirectories;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const directory of preferredDirectories) {
|
|
126
|
+
const found = await inspectDirectory(directory, false);
|
|
127
|
+
if (typeof found === 'string' && found) return found;
|
|
128
|
+
}
|
|
129
|
+
if (options.recentOnly === true) return '';
|
|
130
|
+
|
|
131
|
+
const stack = [sessionsRoot, archivedRoot];
|
|
132
|
+
while (stack.length > 0 && Date.now() <= deadline && inspectedEntries < maxEntries) {
|
|
133
|
+
const directory = stack.pop();
|
|
134
|
+
const result = await inspectDirectory(directory, true);
|
|
135
|
+
if (typeof result === 'string') {
|
|
136
|
+
if (result) return result;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
stack.push(...result);
|
|
140
|
+
}
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
62
144
|
function normalizePreviewText(value, maxLength = 400) {
|
|
63
145
|
return String(value || '')
|
|
64
146
|
.replace(/\r\n/g, '\n')
|
|
@@ -239,5 +321,6 @@ class CodexRolloutFollower extends EventEmitter {
|
|
|
239
321
|
module.exports = {
|
|
240
322
|
CodexRolloutFollower,
|
|
241
323
|
findCodexRolloutFile,
|
|
324
|
+
findCodexRolloutFileAsync,
|
|
242
325
|
formatCodexRolloutEvent,
|
|
243
326
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { execFile } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
const os = require('os');
|
|
3
4
|
const { promisify } = require('util');
|
|
4
5
|
const { resolveCompatibleCodexExecutable } = require('./executable-discovery');
|
|
@@ -9,6 +10,13 @@ async function runCodexSessionArchiveCommand(action, sessionId, session = {}, op
|
|
|
9
10
|
const resolveExecutable = options.resolveCompatibleCodexExecutable || resolveCompatibleCodexExecutable;
|
|
10
11
|
const runExecFile = options.execFileAsync || execFileAsync;
|
|
11
12
|
const processEnv = options.processEnv || process.env;
|
|
13
|
+
const directoryExists = options.directoryExists || ((directory) => {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(directory).isDirectory();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
12
20
|
const codexResolution = resolveExecutable(session.cliVersion || '', processEnv.PATH || '');
|
|
13
21
|
if (!codexResolution.compatible) {
|
|
14
22
|
return {
|
|
@@ -18,8 +26,11 @@ async function runCodexSessionArchiveCommand(action, sessionId, session = {}, op
|
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
try {
|
|
29
|
+
const sessionCwd = session.cwd || session.workspace || '';
|
|
21
30
|
await runExecFile(codexResolution.path || 'codex', [action, sessionId], {
|
|
22
|
-
cwd:
|
|
31
|
+
cwd: sessionCwd && directoryExists(sessionCwd)
|
|
32
|
+
? sessionCwd
|
|
33
|
+
: (processEnv.HOME || os.homedir()),
|
|
23
34
|
env: session.providerHomePath
|
|
24
35
|
? { ...processEnv, CODEX_HOME: session.providerHomePath }
|
|
25
36
|
: processEnv,
|
|
@@ -412,6 +412,98 @@ async function readSessionMetadata(filePath, maxLines = 80) {
|
|
|
412
412
|
return metadata.id ? metadata : null;
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
function codexSessionDateKeys(startedAt, windowMs) {
|
|
416
|
+
const center = Number(startedAt);
|
|
417
|
+
const radius = Math.max(0, Number(windowMs) || 0);
|
|
418
|
+
if (!Number.isFinite(center) || center <= 0) return [];
|
|
419
|
+
const cursor = new Date(center - radius);
|
|
420
|
+
cursor.setHours(0, 0, 0, 0);
|
|
421
|
+
const lastDay = new Date(center + radius);
|
|
422
|
+
lastDay.setHours(0, 0, 0, 0);
|
|
423
|
+
const keys = [];
|
|
424
|
+
while (cursor <= lastDay) {
|
|
425
|
+
keys.push([
|
|
426
|
+
cursor.getFullYear(),
|
|
427
|
+
String(cursor.getMonth() + 1).padStart(2, '0'),
|
|
428
|
+
String(cursor.getDate()).padStart(2, '0'),
|
|
429
|
+
].join('-'));
|
|
430
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
431
|
+
}
|
|
432
|
+
return keys;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function readCodexSessionIdentity(filePath) {
|
|
436
|
+
let handle;
|
|
437
|
+
try {
|
|
438
|
+
handle = await fs.promises.open(filePath, 'r');
|
|
439
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
440
|
+
const chunks = [];
|
|
441
|
+
let bytesReadTotal = 0;
|
|
442
|
+
let lineEnd = -1;
|
|
443
|
+
while (bytesReadTotal < 1024 * 1024 && lineEnd < 0) {
|
|
444
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, bytesReadTotal);
|
|
445
|
+
if (bytesRead <= 0) break;
|
|
446
|
+
const current = Buffer.from(chunk.subarray(0, bytesRead));
|
|
447
|
+
chunks.push(current);
|
|
448
|
+
const newlineIndex = current.indexOf(0x0a);
|
|
449
|
+
if (newlineIndex >= 0) lineEnd = bytesReadTotal + newlineIndex;
|
|
450
|
+
bytesReadTotal += bytesRead;
|
|
451
|
+
}
|
|
452
|
+
const header = Buffer.concat(chunks);
|
|
453
|
+
const firstLine = header.subarray(0, lineEnd >= 0 ? lineEnd : header.length).toString('utf8').replace(/\r$/, '');
|
|
454
|
+
const fileSessionId = sessionIdFromFilePath(filePath);
|
|
455
|
+
const event = JSON.parse(firstLine);
|
|
456
|
+
if (event?.type !== 'session_meta') return null;
|
|
457
|
+
const payload = event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
458
|
+
const id = typeof payload.id === 'string' ? payload.id.trim() : '';
|
|
459
|
+
if (!id || (fileSessionId && id !== fileSessionId)) return null;
|
|
460
|
+
const cwd = typeof payload.cwd === 'string' ? normalizePathValue(payload.cwd) : '';
|
|
461
|
+
return {
|
|
462
|
+
id,
|
|
463
|
+
createdAt: typeof event.timestamp === 'string' ? event.timestamp : '',
|
|
464
|
+
cwd,
|
|
465
|
+
workspace: cwd,
|
|
466
|
+
};
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
} finally {
|
|
470
|
+
await handle?.close().catch(() => {});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function listCodexSessionIdentities(options = {}) {
|
|
475
|
+
const codexHome = options.codexHome || path.join(os.homedir(), '.codex');
|
|
476
|
+
const startedAt = Number(options.startedAt);
|
|
477
|
+
const windowMs = Math.max(0, Number(options.windowMs) || 0);
|
|
478
|
+
if (!Number.isFinite(startedAt) || startedAt <= 0) return [];
|
|
479
|
+
|
|
480
|
+
const files = [];
|
|
481
|
+
for (const dateKey of codexSessionDateKeys(startedAt, windowMs)) {
|
|
482
|
+
const [year, month, day] = dateKey.split('-');
|
|
483
|
+
const directory = path.join(codexHome, 'sessions', year, month, day);
|
|
484
|
+
let entries;
|
|
485
|
+
try {
|
|
486
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
487
|
+
} catch {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
492
|
+
files.push(path.join(directory, entry.name));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const identities = [];
|
|
497
|
+
const batchSize = 32;
|
|
498
|
+
for (let offset = 0; offset < files.length; offset += batchSize) {
|
|
499
|
+
const batch = await Promise.all(
|
|
500
|
+
files.slice(offset, offset + batchSize).map(readCodexSessionIdentity)
|
|
501
|
+
);
|
|
502
|
+
identities.push(...batch.filter(Boolean));
|
|
503
|
+
}
|
|
504
|
+
return identities;
|
|
505
|
+
}
|
|
506
|
+
|
|
415
507
|
function getGlobalState(codexHome) {
|
|
416
508
|
const state = readJsonFile(path.join(codexHome, '.codex-global-state.json'), {});
|
|
417
509
|
const atom = state['electron-persisted-atom-state'] || {};
|
|
@@ -540,8 +632,10 @@ async function listCodexSessions(options = {}) {
|
|
|
540
632
|
}
|
|
541
633
|
|
|
542
634
|
module.exports = {
|
|
635
|
+
codexSessionDateKeys,
|
|
543
636
|
hasTemporaryWorkspaceReference,
|
|
544
637
|
isTemporaryWorkspace,
|
|
638
|
+
listCodexSessionIdentities,
|
|
545
639
|
listCodexSessions,
|
|
546
640
|
readSessionIndex,
|
|
547
641
|
readSessionMetadata,
|
|
@@ -306,7 +306,7 @@ class ConfigManager {
|
|
|
306
306
|
projectNames: {},
|
|
307
307
|
instanceName: '',
|
|
308
308
|
theme: 'terminal',
|
|
309
|
-
appearance: '
|
|
309
|
+
appearance: 'system',
|
|
310
310
|
language: 'en',
|
|
311
311
|
heartbeatInterval: 1000,
|
|
312
312
|
dangerouslySkipAgentPermissionsByDefault: false,
|
|
@@ -342,7 +342,7 @@ class ConfigManager {
|
|
|
342
342
|
projectNames: {},
|
|
343
343
|
instanceName: '',
|
|
344
344
|
theme: 'terminal',
|
|
345
|
-
appearance: '
|
|
345
|
+
appearance: 'system',
|
|
346
346
|
language: 'en',
|
|
347
347
|
heartbeatInterval: 1000,
|
|
348
348
|
dangerouslySkipAgentPermissionsByDefault: false,
|
|
@@ -498,7 +498,7 @@ class ConfigManager {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
normalizeAppearance(appearance) {
|
|
501
|
-
return ['light', 'dark'].includes(appearance) ? appearance : '
|
|
501
|
+
return ['system', 'light', 'dark'].includes(appearance) ? appearance : 'system';
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
normalizeLanguage(language) {
|
|
@@ -13,6 +13,7 @@ const storageLayout = require('./storage-layout');
|
|
|
13
13
|
const SERVER_MODE_ARG = '--farming-server';
|
|
14
14
|
const SERVER_MODE_ENV = 'FARMING_RUN_SERVER';
|
|
15
15
|
const NATIVE_PTY_HOST_ARG = '--native-pty-host';
|
|
16
|
+
const USAGE_HISTORY_SMOKE_ARG = '--farming-usage-history-smoke';
|
|
16
17
|
const DEFAULT_PORT = '6694';
|
|
17
18
|
const DEFAULT_BASE_PATH = '/farming';
|
|
18
19
|
const DEFAULT_SERVER_START_TIMEOUT_MS = 30_000;
|
|
@@ -787,6 +788,13 @@ async function run(argv = process.argv.slice(2)) {
|
|
|
787
788
|
return 0;
|
|
788
789
|
}
|
|
789
790
|
|
|
791
|
+
if (argv[0] === USAGE_HISTORY_SMOKE_ARG) {
|
|
792
|
+
const { runUsageHistorySmoke } = require('./usage-history-smoke');
|
|
793
|
+
const result = await runUsageHistorySmoke();
|
|
794
|
+
console.log(JSON.stringify(result));
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
790
798
|
if (argv[0] === NATIVE_PTY_HOST_ARG || process.env.FARMING_RUN_NATIVE_PTY_HOST === '1') {
|
|
791
799
|
runNativePtyHostInCurrentProcess();
|
|
792
800
|
return 0;
|
|
@@ -14,7 +14,9 @@ const {
|
|
|
14
14
|
allocateNativePtyControllerGeneration,
|
|
15
15
|
positiveGeneration,
|
|
16
16
|
} = require('./native-pty-controller-generation');
|
|
17
|
+
const { isTemporaryProviderSessionId } = require('./provider-session-id');
|
|
17
18
|
const storageLayout = require('./storage-layout');
|
|
19
|
+
const { deserializeTerminalState } = require('./terminal-state-serialization');
|
|
18
20
|
|
|
19
21
|
const DEFAULT_CONNECT_RETRIES = 300;
|
|
20
22
|
const DEFAULT_CONNECT_RETRY_MS = 50;
|
|
@@ -463,6 +465,23 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
463
465
|
});
|
|
464
466
|
}
|
|
465
467
|
|
|
468
|
+
async requirePreparedHostRotationResume(preparationToken, actual, primaryError) {
|
|
469
|
+
if (!preparationToken) return;
|
|
470
|
+
try {
|
|
471
|
+
await this.resumePreparedHostRotation(preparationToken);
|
|
472
|
+
} catch (resumeError) {
|
|
473
|
+
const recoveryError = new Error(
|
|
474
|
+
`Cannot confirm recovery of incompatible native PTY host (${actual}) after aborting rotation`
|
|
475
|
+
);
|
|
476
|
+
recoveryError.code = 'FARMING_NATIVE_HOST_ROTATION_RECOVERY_FAILED';
|
|
477
|
+
recoveryError.cause = new AggregateError(
|
|
478
|
+
[primaryError, resumeError].filter(Boolean),
|
|
479
|
+
'Native PTY rotation failed and the prepared host could not be resumed'
|
|
480
|
+
);
|
|
481
|
+
throw recoveryError;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
466
485
|
async rotateMismatchedHost(hostInfo) {
|
|
467
486
|
const expected = runtimeIdentityLabel(this.expectedRuntimeIdentity);
|
|
468
487
|
const actual = runtimeIdentityLabel(hostInfo && hostInfo.runtimeIdentity);
|
|
@@ -513,7 +532,11 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
513
532
|
throw new Error('The native PTY host returned an invalid rotation checkpoint');
|
|
514
533
|
}
|
|
515
534
|
} catch (error) {
|
|
516
|
-
|
|
535
|
+
if (preparationToken) {
|
|
536
|
+
await this.requirePreparedHostRotationResume(preparationToken, actual, error);
|
|
537
|
+
} else {
|
|
538
|
+
await this.resumePreparedHostRotation('').catch(() => {});
|
|
539
|
+
}
|
|
517
540
|
const recovered = await this.requestOnce('recoverSessions', {}, {
|
|
518
541
|
ensureConnected: false,
|
|
519
542
|
retryOnDisconnect: false,
|
|
@@ -532,6 +555,40 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
532
555
|
}
|
|
533
556
|
}
|
|
534
557
|
|
|
558
|
+
let unresumableCodex;
|
|
559
|
+
try {
|
|
560
|
+
unresumableCodex = deserializeTerminalState(serializedTerminalState).find(entry => {
|
|
561
|
+
const metadata = entry.metadata || {};
|
|
562
|
+
const provider = String(metadata.providerSessionProvider || metadata.provider || '').trim();
|
|
563
|
+
return provider === 'codex'
|
|
564
|
+
&& metadata.terminalInputReceived === true
|
|
565
|
+
&& (
|
|
566
|
+
metadata.providerSessionTemporary === true
|
|
567
|
+
|| isTemporaryProviderSessionId(metadata.providerSessionId)
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
571
|
+
await this.requirePreparedHostRotationResume(preparationToken, actual, error);
|
|
572
|
+
const mismatchError = new Error(
|
|
573
|
+
`Cannot rotate incompatible native PTY host (${actual}) with an invalid terminal checkpoint`
|
|
574
|
+
);
|
|
575
|
+
mismatchError.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
576
|
+
mismatchError.cause = error;
|
|
577
|
+
throw mismatchError;
|
|
578
|
+
}
|
|
579
|
+
if (unresumableCodex) {
|
|
580
|
+
const unresumableError = new Error(
|
|
581
|
+
`Codex session ${unresumableCodex.id} has user input but no exact resume id`
|
|
582
|
+
);
|
|
583
|
+
await this.requirePreparedHostRotationResume(preparationToken, actual, unresumableError);
|
|
584
|
+
const mismatchError = new Error(
|
|
585
|
+
`Cannot rotate incompatible native PTY host (${actual}) while Codex session ${unresumableCodex.id} has user input but no exact resume id`
|
|
586
|
+
);
|
|
587
|
+
mismatchError.code = 'FARMING_NATIVE_HOST_UNRESUMABLE_SESSION';
|
|
588
|
+
mismatchError.cause = unresumableError;
|
|
589
|
+
throw mismatchError;
|
|
590
|
+
}
|
|
591
|
+
|
|
535
592
|
const socket = this.socket;
|
|
536
593
|
if (socket) this.suppressedDisconnectSockets.add(socket);
|
|
537
594
|
let shutdownUncertain = false;
|
|
@@ -546,7 +603,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
546
603
|
});
|
|
547
604
|
} catch (error) {
|
|
548
605
|
if (!isConnectRetryable(error)) {
|
|
549
|
-
await this.
|
|
606
|
+
await this.requirePreparedHostRotationResume(preparationToken, actual, error);
|
|
550
607
|
const mismatchError = new Error(
|
|
551
608
|
`Cannot rotate incompatible native PTY host (${actual}); stop the old host and restart Farming`
|
|
552
609
|
);
|
|
@@ -7,45 +7,73 @@ const {
|
|
|
7
7
|
} = require('./provider-session-id');
|
|
8
8
|
|
|
9
9
|
const CODEX_VALUE_OPTIONS = new Set([
|
|
10
|
-
'-a', '-c', '-C', '-m', '-s', '--ask-for-approval', '--cd', '--config',
|
|
11
|
-
'--config-profile', '--model', '--profile', '--sandbox',
|
|
10
|
+
'-a', '-c', '-C', '-m', '-p', '-s', '--ask-for-approval', '--cd', '--config',
|
|
11
|
+
'--config-profile', '--model', '--profile', '--sandbox', '--add-dir',
|
|
12
|
+
'--enable', '--disable', '--remote', '--remote-auth-token-env',
|
|
13
|
+
'--local-provider', '-i', '--image',
|
|
12
14
|
]);
|
|
15
|
+
const OPENCODE_VALUE_OPTIONS = new Set([
|
|
16
|
+
'-m', '-s', '--agent', '--hostname', '--log-level', '--mdns-domain',
|
|
17
|
+
'--model', '--port', '--prompt', '--replay-limit', '--session',
|
|
18
|
+
]);
|
|
19
|
+
const CODEX_SUBCOMMANDS = new Set([
|
|
20
|
+
'exec', 'e', 'review', 'login', 'logout', 'mcp', 'plugin', 'mcp-server',
|
|
21
|
+
'app-server', 'remote-control', 'app', 'completion', 'update', 'doctor',
|
|
22
|
+
'sandbox', 'debug', 'apply', 'a', 'resume', 'archive', 'delete', 'unarchive',
|
|
23
|
+
'fork', 'cloud', 'exec-server', 'features', 'help',
|
|
24
|
+
]);
|
|
25
|
+
const OPENCODE_SUBCOMMANDS = new Set([
|
|
26
|
+
'completion', 'acp', 'mcp', 'attach', 'run', 'debug', 'providers', 'auth',
|
|
27
|
+
'agent', 'upgrade', 'uninstall', 'serve', 'web', 'models', 'stats', 'export',
|
|
28
|
+
'import', 'github', 'pr', 'session', 'plugin', 'plug', 'db',
|
|
29
|
+
]);
|
|
30
|
+
const CODEX_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
31
|
|
|
14
32
|
function optionTakesValue(option, valueOptions) {
|
|
15
33
|
return Boolean(option && !option.includes('=') && valueOptions.has(option));
|
|
16
34
|
}
|
|
17
35
|
|
|
18
|
-
function
|
|
36
|
+
function scanPositionals(args, valueOptions, options = {}) {
|
|
37
|
+
const positionals = [];
|
|
19
38
|
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
-
const arg = args[index];
|
|
39
|
+
const arg = String(args[index] || '');
|
|
21
40
|
if (!arg) continue;
|
|
41
|
+
if (arg === '--') {
|
|
42
|
+
for (let positionalIndex = index + 1; positionalIndex < args.length; positionalIndex += 1) {
|
|
43
|
+
const value = String(args[positionalIndex] || '');
|
|
44
|
+
if (value) positionals.push({ value, index: positionalIndex, afterDelimiter: true });
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
22
48
|
if (arg.startsWith('-')) {
|
|
23
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
options.multiValueOptions?.has(arg)
|
|
51
|
+
&& !arg.includes('=')
|
|
52
|
+
) {
|
|
53
|
+
while (index + 1 < args.length && !String(args[index + 1] || '').startsWith('-')) index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (optionTakesValue(arg, valueOptions)) index += 1;
|
|
24
57
|
continue;
|
|
25
58
|
}
|
|
26
|
-
|
|
59
|
+
positionals.push({ value: arg, index, afterDelimiter: false });
|
|
27
60
|
}
|
|
28
|
-
return
|
|
61
|
+
return positionals;
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
function codexSessionIdAfterSubcommand(args, subcommandIndex) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
positional.push(arg);
|
|
41
|
-
}
|
|
42
|
-
const sessionId = positional.at(-1) || '';
|
|
43
|
-
return isSafeProviderSessionId(sessionId) ? sessionId : '';
|
|
65
|
+
const sessionId = scanPositionals(
|
|
66
|
+
args.slice(subcommandIndex + 1),
|
|
67
|
+
CODEX_VALUE_OPTIONS,
|
|
68
|
+
{ multiValueOptions: new Set(['-i', '--image']) },
|
|
69
|
+
)[0]?.value || '';
|
|
70
|
+
return CODEX_SESSION_ID_RE.test(sessionId) ? sessionId.toLowerCase() : '';
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
function argValue(args, names) {
|
|
47
74
|
for (let index = 0; index < args.length; index += 1) {
|
|
48
75
|
const arg = args[index];
|
|
76
|
+
if (arg === '--') break;
|
|
49
77
|
for (const name of names) {
|
|
50
78
|
if (arg === name) return args[index + 1] || '';
|
|
51
79
|
if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
|
|
@@ -55,13 +83,26 @@ function argValue(args, names) {
|
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
function hasArg(args, names) {
|
|
58
|
-
|
|
86
|
+
for (const arg of args) {
|
|
87
|
+
if (arg === '--') break;
|
|
88
|
+
if (names.includes(arg) || names.some(name => arg.startsWith(`${name}=`))) return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
59
91
|
}
|
|
60
92
|
|
|
61
|
-
function codexSessionPlan(rawArgs) {
|
|
62
|
-
const
|
|
93
|
+
function codexSessionPlan(rawArgs, _launchArgs) {
|
|
94
|
+
const positional = scanPositionals(rawArgs, CODEX_VALUE_OPTIONS, {
|
|
95
|
+
multiValueOptions: new Set(['-i', '--image']),
|
|
96
|
+
});
|
|
97
|
+
const subcommand = positional[0]
|
|
98
|
+
&& positional[0].afterDelimiter !== true
|
|
99
|
+
&& CODEX_SUBCOMMANDS.has(positional[0].value)
|
|
100
|
+
? positional[0]
|
|
101
|
+
: null;
|
|
63
102
|
if (subcommand?.value === 'resume') {
|
|
64
|
-
const id =
|
|
103
|
+
const id = hasArg(rawArgs.slice(subcommand.index + 1), ['--last'])
|
|
104
|
+
? ''
|
|
105
|
+
: codexSessionIdAfterSubcommand(rawArgs, subcommand.index);
|
|
65
106
|
if (id) return { id, temporary: false, source: 'resume' };
|
|
66
107
|
}
|
|
67
108
|
if (subcommand?.value === 'fork') {
|
|
@@ -69,11 +110,22 @@ function codexSessionPlan(rawArgs) {
|
|
|
69
110
|
id: createTemporaryProviderSessionId(),
|
|
70
111
|
temporary: true,
|
|
71
112
|
source: 'codex-fork-temporary',
|
|
72
|
-
forkedFromProviderSessionId:
|
|
113
|
+
forkedFromProviderSessionId: hasArg(rawArgs.slice(subcommand.index + 1), ['--last'])
|
|
114
|
+
? ''
|
|
115
|
+
: codexSessionIdAfterSubcommand(rawArgs, subcommand.index),
|
|
73
116
|
};
|
|
74
117
|
}
|
|
75
|
-
if (subcommand
|
|
76
|
-
|
|
118
|
+
if (subcommand) return null;
|
|
119
|
+
if (hasArg(rawArgs, ['--remote'])) {
|
|
120
|
+
return {
|
|
121
|
+
error: 'A fresh Codex --remote Terminal cannot be correlated with a local resumable session id; resume an explicit remote session id instead',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id: createTemporaryProviderSessionId(),
|
|
126
|
+
temporary: true,
|
|
127
|
+
source: 'codex-temporary',
|
|
128
|
+
};
|
|
77
129
|
}
|
|
78
130
|
|
|
79
131
|
function explicitSessionPlan(provider, rawArgs, launchArgs) {
|
|
@@ -103,17 +155,35 @@ function explicitSessionPlan(provider, rawArgs, launchArgs) {
|
|
|
103
155
|
};
|
|
104
156
|
}
|
|
105
157
|
|
|
106
|
-
function openCodeSessionPlan(rawArgs) {
|
|
158
|
+
function openCodeSessionPlan(rawArgs, launchArgs) {
|
|
107
159
|
const id = argValue(rawArgs, ['--session', '-s']);
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
160
|
+
if (id && isSafeProviderSessionId(id)) {
|
|
161
|
+
return hasArg(rawArgs, ['--fork'])
|
|
162
|
+
? {
|
|
163
|
+
id: createTemporaryProviderSessionId(),
|
|
164
|
+
temporary: true,
|
|
165
|
+
source: 'opencode-fork-temporary',
|
|
166
|
+
forkedFromProviderSessionId: id,
|
|
167
|
+
}
|
|
168
|
+
: { id, temporary: false, source: 'resume' };
|
|
169
|
+
}
|
|
170
|
+
if (hasArg(rawArgs, ['--session', '-s', '--continue', '-c', '--fork'])) return null;
|
|
171
|
+
const positional = scanPositionals(rawArgs, OPENCODE_VALUE_OPTIONS, {
|
|
172
|
+
multiValueOptions: new Set(['--cors']),
|
|
173
|
+
});
|
|
174
|
+
if (
|
|
175
|
+
positional[0]
|
|
176
|
+
&& positional[0].afterDelimiter !== true
|
|
177
|
+
&& OPENCODE_SUBCOMMANDS.has(positional[0].value)
|
|
178
|
+
) return null;
|
|
179
|
+
return {
|
|
180
|
+
id: '',
|
|
181
|
+
precreate: true,
|
|
182
|
+
temporary: false,
|
|
183
|
+
source: 'opencode-precreate',
|
|
184
|
+
identityWorkspace: positional[0]?.value || '',
|
|
185
|
+
resumeInsertIndex: launchArgs.length,
|
|
186
|
+
};
|
|
117
187
|
}
|
|
118
188
|
|
|
119
189
|
function codexAcpEnvironment(options = {}) {
|
|
@@ -148,6 +218,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
148
218
|
commands: ['codex'],
|
|
149
219
|
supportedRuntimes: ['terminal', 'acp', 'json'],
|
|
150
220
|
planSession: codexSessionPlan,
|
|
221
|
+
terminalResumeArgs: (args, sessionId) => ['resume', sessionId, ...args],
|
|
151
222
|
acp: { packageName: '@agentclientprotocol/codex-acp', version: '1.1.4' },
|
|
152
223
|
prepareAcpEnvironment: codexAcpEnvironment,
|
|
153
224
|
capabilities: { runtimeSwitch: true, terminalProfile: true, goals: false },
|
|
@@ -175,6 +246,16 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
175
246
|
commands: ['opencode'],
|
|
176
247
|
supportedRuntimes: ['terminal', 'acp', 'json'],
|
|
177
248
|
planSession: openCodeSessionPlan,
|
|
249
|
+
terminalResumeArgs: (args, sessionId) => {
|
|
250
|
+
const delimiterIndex = args.indexOf('--');
|
|
251
|
+
const insertIndex = delimiterIndex >= 0 ? delimiterIndex : args.length;
|
|
252
|
+
return [
|
|
253
|
+
...args.slice(0, insertIndex),
|
|
254
|
+
'--session',
|
|
255
|
+
sessionId,
|
|
256
|
+
...args.slice(insertIndex),
|
|
257
|
+
];
|
|
258
|
+
},
|
|
178
259
|
acp: {
|
|
179
260
|
version: 'native',
|
|
180
261
|
launch: options => ({
|