farming-code 2.2.6 → 2.2.8
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 +102 -204
- package/README.zh_cn.md +102 -246
- package/backend/acp-runtime.js +93 -1
- package/backend/agent-manager.js +81 -20
- package/backend/cli-agents.js +1 -1
- package/backend/farming-app-cli.js +52 -1
- package/backend/farming-session-store.js +1 -0
- package/backend/npm-update-helper.js +4 -0
- package/backend/server.js +5 -1
- package/backend/update-service.js +14 -2
- package/dist/assets/App-8dYAM6ql.js +124 -0
- package/dist/assets/{FileEditorPane-BQLocXOp.js → FileEditorPane-RWiFD2cq.js} +1 -1
- package/dist/assets/{IconGlyphs-RkMAdBXF.js → IconGlyphs-DfL0EBnj.js} +1 -1
- package/dist/assets/{ProjectFilesSection-lS1azbgj.js → ProjectFilesSection-Q4PDsWmM.js} +1 -1
- package/dist/assets/{ReviewPage-DgnmgU2T.js → ReviewPage-BaXu1ZdX.js} +1 -1
- package/dist/assets/{code-dark-Cjb1QGhu.css → code-dark-CDkOQAtK.css} +1 -1
- package/dist/assets/{index-BpHPeJf0.js → index-BrbljRqn.js} +2 -2
- package/dist/assets/main-D073SnW4.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +58 -2
- package/frontend/skins/crt/index.html +1 -1
- package/frontend/skins/crt/styles/billing.css +6 -0
- package/frontend/skins/crt/styles/effects.css +11 -3
- package/package.json +1 -1
- package/dist/assets/App-Dza5yEDe.js +0 -124
- package/dist/assets/main-DTJwH51M.css +0 -1
package/backend/agent-manager.js
CHANGED
|
@@ -328,6 +328,7 @@ class AgentManager extends EventEmitter {
|
|
|
328
328
|
this.controlUrl = options.controlUrl || '';
|
|
329
329
|
this.tokenFile = options.tokenFile || '';
|
|
330
330
|
this.authDisabled = options.authDisabled === true;
|
|
331
|
+
this.skipExecutablePreflight = options.skipExecutablePreflight === true;
|
|
331
332
|
this.cliBinDir = options.cliBinDir || path.join(__dirname, '..', 'bin');
|
|
332
333
|
this.agentShellEnvProvider = typeof options.agentShellEnvProvider === 'function'
|
|
333
334
|
? options.agentShellEnvProvider
|
|
@@ -1041,6 +1042,7 @@ class AgentManager extends EventEmitter {
|
|
|
1041
1042
|
providerSessionSource: metadata.providerSessionSource || '',
|
|
1042
1043
|
providerSessionResolvedAt: metadata.providerSessionResolvedAt || null,
|
|
1043
1044
|
providerSessionTitle: metadata.providerSessionTitle || '',
|
|
1045
|
+
terminalInputReceived: metadata.terminalInputReceived === true,
|
|
1044
1046
|
// Older persisted sessions predate App Server mode. Also, a Codex
|
|
1045
1047
|
// App Server record without its isolated runtime home is not actually
|
|
1046
1048
|
// attachable; recover it as terminal-owned CLI instead of leaving the
|
|
@@ -1227,6 +1229,7 @@ class AgentManager extends EventEmitter {
|
|
|
1227
1229
|
providerSessionSource: agent.providerSessionSource || '',
|
|
1228
1230
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
1229
1231
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
1232
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
1230
1233
|
codexRuntimeMode: agent.codexRuntimeMode || '',
|
|
1231
1234
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
1232
1235
|
acpState: agent.acpState || '',
|
|
@@ -1881,6 +1884,7 @@ class AgentManager extends EventEmitter {
|
|
|
1881
1884
|
providerSessionSource: agent.providerSessionSource,
|
|
1882
1885
|
providerSessionResolvedAt: agent.providerSessionResolvedAt,
|
|
1883
1886
|
providerSessionTitle: agent.providerSessionTitle,
|
|
1887
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
1884
1888
|
codexRuntimeMode: agent.codexRuntimeMode,
|
|
1885
1889
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
1886
1890
|
jsonCliState: agent.jsonCliState || '',
|
|
@@ -2005,7 +2009,8 @@ class AgentManager extends EventEmitter {
|
|
|
2005
2009
|
const launchPathEnv = typeof userShellEnv?.PATH === 'string' && userShellEnv.PATH.trim()
|
|
2006
2010
|
? userShellEnv.PATH
|
|
2007
2011
|
: (process.env.PATH || '');
|
|
2008
|
-
|
|
2012
|
+
const resolvedExecutable = resolveAgentExecutable(program, launchPathEnv);
|
|
2013
|
+
let spawnProgram = resolvedExecutable || program;
|
|
2009
2014
|
if (path.basename(program) === 'codex') {
|
|
2010
2015
|
const codexResolution = resolveCompatibleCodexExecutable(options.requiredCliVersion || '', launchPathEnv);
|
|
2011
2016
|
if (!codexResolution.compatible) {
|
|
@@ -2014,6 +2019,24 @@ class AgentManager extends EventEmitter {
|
|
|
2014
2019
|
}
|
|
2015
2020
|
spawnProgram = codexResolution.path || spawnProgram;
|
|
2016
2021
|
}
|
|
2022
|
+
if (
|
|
2023
|
+
launch.spec
|
|
2024
|
+
&& path.basename(program) === program
|
|
2025
|
+
&& !resolvedExecutable
|
|
2026
|
+
&& !this.skipExecutablePreflight
|
|
2027
|
+
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
|
|
2028
|
+
) {
|
|
2029
|
+
const displayName = launch.spec.name === 'opencode'
|
|
2030
|
+
? 'OpenCode'
|
|
2031
|
+
: launch.spec.name.charAt(0).toUpperCase() + launch.spec.name.slice(1);
|
|
2032
|
+
if (callback) {
|
|
2033
|
+
callback(
|
|
2034
|
+
null,
|
|
2035
|
+
`${displayName} executable "${program}" was not found in the user shell PATH. Install it or refresh the Agent list, then try again.`
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2017
2040
|
|
|
2018
2041
|
const parentAgentId = typeof options.parentAgentId === 'string' ? options.parentAgentId : '';
|
|
2019
2042
|
const parentAgent = parentAgentId ? this.agents.get(parentAgentId) : null;
|
|
@@ -2117,6 +2140,16 @@ class AgentManager extends EventEmitter {
|
|
|
2117
2140
|
resolvedProviderHomeId = defaultCodexHome.id || 'default';
|
|
2118
2141
|
}
|
|
2119
2142
|
}
|
|
2143
|
+
const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
|
|
2144
|
+
? options.agentRuntimeMode
|
|
2145
|
+
: 'terminal';
|
|
2146
|
+
// A fresh structured runtime does not need a provider CLI resume id yet:
|
|
2147
|
+
// ACP/JSON creates the provider session and writes the resulting id back
|
|
2148
|
+
// after connecting. OpenCode is the important case here because its
|
|
2149
|
+
// terminal CLI does not accept a pre-generated id for a fresh session.
|
|
2150
|
+
const structuredRuntimeProvider = ['json', 'acp'].includes(requestedAgentRuntimeMode)
|
|
2151
|
+
? homeProvider
|
|
2152
|
+
: providerSessionPlan.provider;
|
|
2120
2153
|
const requestedCodexRuntimeMode = options.codexRuntimeMode === 'app-server' || options.codexRuntimeMode === 'cli'
|
|
2121
2154
|
? options.codexRuntimeMode
|
|
2122
2155
|
: (this.configManager && typeof this.configManager.getCodexRuntimeMode === 'function'
|
|
@@ -2131,15 +2164,12 @@ class AgentManager extends EventEmitter {
|
|
|
2131
2164
|
// on the path they are designed to exercise.
|
|
2132
2165
|
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
|
|
2133
2166
|
&& normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
|
|
2134
|
-
const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
|
|
2135
|
-
? options.agentRuntimeMode
|
|
2136
|
-
: 'terminal';
|
|
2137
2167
|
const useJsonCli = requestedAgentRuntimeMode === 'json'
|
|
2138
|
-
&& ['codex', 'opencode'].includes(
|
|
2168
|
+
&& ['codex', 'opencode'].includes(structuredRuntimeProvider)
|
|
2139
2169
|
&& !useCodexAppServer
|
|
2140
2170
|
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
|
|
2141
2171
|
const useAcp = requestedAgentRuntimeMode === 'acp'
|
|
2142
|
-
&& ['codex', 'claude', 'opencode', 'qoder'].includes(
|
|
2172
|
+
&& ['codex', 'claude', 'opencode', 'qoder'].includes(structuredRuntimeProvider)
|
|
2143
2173
|
&& !useCodexAppServer
|
|
2144
2174
|
&& (
|
|
2145
2175
|
process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
|
|
@@ -2180,7 +2210,7 @@ class AgentManager extends EventEmitter {
|
|
|
2180
2210
|
task: typeof options.task === 'string' ? options.task : '',
|
|
2181
2211
|
workflowTemplate: typeof options.workflowTemplate === 'string' ? options.workflowTemplate : '',
|
|
2182
2212
|
source: typeof options.source === 'string' ? options.source : 'ui',
|
|
2183
|
-
providerSessionProvider: providerSessionPlan.provider || '',
|
|
2213
|
+
providerSessionProvider: useAcp || useJsonCli ? structuredRuntimeProvider : (providerSessionPlan.provider || ''),
|
|
2184
2214
|
providerHomeId: resolvedProviderHomeId,
|
|
2185
2215
|
providerHomePath,
|
|
2186
2216
|
providerSessionId: providerSessionPlan.id || '',
|
|
@@ -2189,7 +2219,8 @@ class AgentManager extends EventEmitter {
|
|
|
2189
2219
|
providerSessionSource: providerSessionPlan.source || '',
|
|
2190
2220
|
providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
|
|
2191
2221
|
providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
|
|
2192
|
-
|
|
2222
|
+
terminalInputReceived: false,
|
|
2223
|
+
codexRuntimeMode: structuredRuntimeProvider === 'codex'
|
|
2193
2224
|
? (useCodexAppServer ? 'app-server' : 'cli')
|
|
2194
2225
|
: '',
|
|
2195
2226
|
agentRuntimeMode: useAcp ? 'acp' : (useJsonCli ? 'json' : 'terminal'),
|
|
@@ -2320,7 +2351,7 @@ class AgentManager extends EventEmitter {
|
|
|
2320
2351
|
if (useJsonCli) {
|
|
2321
2352
|
this.jsonCliRuntime.registerAgent({
|
|
2322
2353
|
agentId,
|
|
2323
|
-
provider:
|
|
2354
|
+
provider: structuredRuntimeProvider,
|
|
2324
2355
|
executable: spawnProgram,
|
|
2325
2356
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2326
2357
|
cwd: workspace,
|
|
@@ -2334,11 +2365,13 @@ class AgentManager extends EventEmitter {
|
|
|
2334
2365
|
if (useAcp) {
|
|
2335
2366
|
const prepared = await this.acpRuntime.prepareAgent({
|
|
2336
2367
|
agentId,
|
|
2337
|
-
provider:
|
|
2368
|
+
provider: structuredRuntimeProvider,
|
|
2338
2369
|
executable: spawnProgram,
|
|
2339
2370
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2340
2371
|
cwd: workspace,
|
|
2341
|
-
sessionId:
|
|
2372
|
+
sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
|
|
2373
|
+
? ''
|
|
2374
|
+
: agentRecord.providerSessionId,
|
|
2342
2375
|
historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
2343
2376
|
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2344
2377
|
model: this.configManager && this.configManager.getCodexModel
|
|
@@ -2353,7 +2386,7 @@ class AgentManager extends EventEmitter {
|
|
|
2353
2386
|
});
|
|
2354
2387
|
agentRecord.providerSessionId = prepared.sessionId;
|
|
2355
2388
|
agentRecord.providerSessionKey = this.providerSessionKey(
|
|
2356
|
-
|
|
2389
|
+
structuredRuntimeProvider,
|
|
2357
2390
|
prepared.sessionId,
|
|
2358
2391
|
agentRecord.providerHomeId || 'default'
|
|
2359
2392
|
);
|
|
@@ -2721,6 +2754,11 @@ class AgentManager extends EventEmitter {
|
|
|
2721
2754
|
return this.acpRuntime.setSessionConfigOption(agentId, configId, value);
|
|
2722
2755
|
}
|
|
2723
2756
|
|
|
2757
|
+
setAcpSessionConfigOptions(agentId, changes) {
|
|
2758
|
+
this.getAcpSession(agentId);
|
|
2759
|
+
return this.acpRuntime.setSessionConfigOptions(agentId, changes);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2724
2762
|
async setCodexAppServerGoal(agentId, patch = {}) {
|
|
2725
2763
|
const agent = this.assertCodexAppServerGoalAgent(agentId);
|
|
2726
2764
|
const objective = typeof patch.objective === 'string' ? patch.objective.trim().slice(0, 4000) : undefined;
|
|
@@ -2763,6 +2801,12 @@ class AgentManager extends EventEmitter {
|
|
|
2763
2801
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
2764
2802
|
if (!engine) return;
|
|
2765
2803
|
|
|
2804
|
+
if (agent.terminalInputReceived !== true) {
|
|
2805
|
+
agent.terminalInputReceived = true;
|
|
2806
|
+
this.ensurePersistentAgentSession(agent);
|
|
2807
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2766
2810
|
for (let attempt = 0; ; attempt += 1) {
|
|
2767
2811
|
try {
|
|
2768
2812
|
await engine.sendInput(agentId, input);
|
|
@@ -3158,7 +3202,14 @@ class AgentManager extends EventEmitter {
|
|
|
3158
3202
|
return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
|
|
3159
3203
|
}
|
|
3160
3204
|
const sessionId = String(agent.providerSessionId || '').trim();
|
|
3161
|
-
|
|
3205
|
+
const canStartFreshAcpSession = nextMode === 'acp'
|
|
3206
|
+
&& agent.agentRuntimeMode === 'terminal'
|
|
3207
|
+
&& agent.terminalInputReceived !== true
|
|
3208
|
+
&& (
|
|
3209
|
+
agent.providerSessionTemporary === true
|
|
3210
|
+
|| ['codex-temporary', 'claude-session-id', 'qoder-session-id'].includes(agent.providerSessionSource || '')
|
|
3211
|
+
);
|
|
3212
|
+
if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
|
|
3162
3213
|
return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
|
|
3163
3214
|
}
|
|
3164
3215
|
// A live ACP binding is the authoritative owner of a newly-created
|
|
@@ -3177,15 +3228,19 @@ class AgentManager extends EventEmitter {
|
|
|
3177
3228
|
}
|
|
3178
3229
|
}
|
|
3179
3230
|
const previouslyVerifiedSession = String(agent.runtimeSwitchVerifiedSessionId || '') === sessionId;
|
|
3180
|
-
|
|
3231
|
+
let startsFreshAcpSession = canStartFreshAcpSession && !isSafeProviderSessionId(sessionId);
|
|
3232
|
+
if (!startsFreshAcpSession && !liveAcpSession && !previouslyVerifiedSession) {
|
|
3181
3233
|
const providerSession = await this.findRuntimeSwitchSession(agent);
|
|
3182
3234
|
if (!providerSession) {
|
|
3183
|
-
|
|
3235
|
+
if (canStartFreshAcpSession) startsFreshAcpSession = true;
|
|
3236
|
+
else return { error: 'The saved Agent session is no longer available in the selected Agent Home.' };
|
|
3184
3237
|
}
|
|
3185
3238
|
}
|
|
3186
|
-
const command =
|
|
3187
|
-
|
|
3188
|
-
|
|
3239
|
+
const command = startsFreshAcpSession
|
|
3240
|
+
? (agent.forkCommand || agent.command)
|
|
3241
|
+
: buildAgentSessionResumeCommand(provider, sessionId, {
|
|
3242
|
+
cwd: agent.cwd || agent.projectWorkspace || '',
|
|
3243
|
+
});
|
|
3189
3244
|
if (!command) return { error: 'Failed to build provider resume command' };
|
|
3190
3245
|
const preserved = {
|
|
3191
3246
|
pinned: agent.pinned === true,
|
|
@@ -3202,7 +3257,9 @@ class AgentManager extends EventEmitter {
|
|
|
3202
3257
|
task: agent.task || agent.providerSessionTitle || '',
|
|
3203
3258
|
workflowTemplate: agent.workflowTemplate || '',
|
|
3204
3259
|
projectWorkspace: agent.projectWorkspace || agent.cwd || '',
|
|
3205
|
-
source:
|
|
3260
|
+
source: startsFreshAcpSession
|
|
3261
|
+
? 'ui-runtime-switch-fresh'
|
|
3262
|
+
: resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
|
|
3206
3263
|
providerHomeId: agent.providerHomeId || '',
|
|
3207
3264
|
providerHomePath: agent.providerHomePath || '',
|
|
3208
3265
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
@@ -3215,16 +3272,18 @@ class AgentManager extends EventEmitter {
|
|
|
3215
3272
|
projectOrder: preserved.projectOrder,
|
|
3216
3273
|
pinnedOrder: preserved.pinnedOrder,
|
|
3217
3274
|
agentRuntimeMode: nextMode,
|
|
3275
|
+
acpStartFresh: startsFreshAcpSession,
|
|
3218
3276
|
codexRuntimeMode: 'cli',
|
|
3219
3277
|
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
3220
3278
|
jsonCliEvents: preserved.jsonCliEvents,
|
|
3221
|
-
runtimeSwitchVerifiedSessionId: sessionId,
|
|
3279
|
+
runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
|
|
3222
3280
|
};
|
|
3223
3281
|
const originalMode = agent.agentRuntimeMode || 'terminal';
|
|
3224
3282
|
const originalOptions = {
|
|
3225
3283
|
...restartOptions,
|
|
3226
3284
|
agentRuntimeMode: originalMode,
|
|
3227
3285
|
codexRuntimeMode: agent.codexRuntimeMode || 'cli',
|
|
3286
|
+
acpStartFresh: false,
|
|
3228
3287
|
};
|
|
3229
3288
|
const startReplacement = options => new Promise(resolve => {
|
|
3230
3289
|
let settled = false;
|
|
@@ -3930,6 +3989,7 @@ class AgentManager extends EventEmitter {
|
|
|
3930
3989
|
providerSessionSource: agent.providerSessionSource || '',
|
|
3931
3990
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
3932
3991
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
3992
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
3933
3993
|
codexRuntimeMode: agent.codexRuntimeMode || '',
|
|
3934
3994
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
3935
3995
|
acpState: agent.acpState || '',
|
|
@@ -4095,6 +4155,7 @@ class AgentManager extends EventEmitter {
|
|
|
4095
4155
|
providerSessionSource: agent.providerSessionSource || '',
|
|
4096
4156
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
4097
4157
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
4158
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
4098
4159
|
codexRuntimeMode: agent.codexRuntimeMode || '',
|
|
4099
4160
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
4100
4161
|
jsonCliState: agent.jsonCliState || '',
|
package/backend/cli-agents.js
CHANGED
|
@@ -417,7 +417,7 @@ function resolveLaunchCommand(command, options = {}) {
|
|
|
417
417
|
if (effort && effort !== 'config' && shouldApplyModelProfile && !hasCodexEffortOverride) {
|
|
418
418
|
launchArgs.unshift('-c', `model_reasoning_effort="${effort}"`);
|
|
419
419
|
}
|
|
420
|
-
if (codexServiceTier && codexServiceTier !== '
|
|
420
|
+
if (codexServiceTier && codexServiceTier !== 'config' && shouldApplyModelProfile && !hasCodexServiceTierOverride) {
|
|
421
421
|
launchArgs.unshift('-c', `service_tier="${codexServiceTier}"`);
|
|
422
422
|
}
|
|
423
423
|
}
|
|
@@ -15,6 +15,7 @@ const NATIVE_PTY_HOST_ARG = '--native-pty-host';
|
|
|
15
15
|
const DEFAULT_PORT = '6694';
|
|
16
16
|
const DEFAULT_BASE_PATH = '/farming';
|
|
17
17
|
const DEFAULT_SERVER_START_TIMEOUT_MS = 30_000;
|
|
18
|
+
const DEFAULT_SERVER_START_STABILITY_MS = 1_500;
|
|
18
19
|
const SERVER_COMMANDS = new Set(['start', 'serve', 'daemon', 'stop', 'status', 'logs', 'url', 'help']);
|
|
19
20
|
const CONTROL_COMMANDS = new Set(['skills', 'memory', 'report', 'list', 'spawn', 'output', 'send', 'kill']);
|
|
20
21
|
const SERVER_BACKED_CONTROL_COMMANDS = new Set(['list', 'spawn', 'output', 'send', 'kill']);
|
|
@@ -416,7 +417,14 @@ function childInvocation(env = process.env) {
|
|
|
416
417
|
if (process.pkg) {
|
|
417
418
|
return { command: '/bin/sh', args: ['-c', buildCleanEnvExecCommand(env, process.execPath, ['--'])] };
|
|
418
419
|
}
|
|
419
|
-
|
|
420
|
+
const nodePath = env.FARMING_NODE_BIN || process.execPath;
|
|
421
|
+
if (env.FARMING_NODE_LD && env.FARMING_NODE_LIBRARY_PATH) {
|
|
422
|
+
return {
|
|
423
|
+
command: env.FARMING_NODE_LD,
|
|
424
|
+
args: ['--library-path', env.FARMING_NODE_LIBRARY_PATH, nodePath, __filename],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return { command: nodePath, args: [__filename] };
|
|
420
428
|
}
|
|
421
429
|
|
|
422
430
|
function ensureConfigDir(configDir) {
|
|
@@ -500,6 +508,44 @@ function serverStartTimeoutMs(env) {
|
|
|
500
508
|
return DEFAULT_SERVER_START_TIMEOUT_MS;
|
|
501
509
|
}
|
|
502
510
|
|
|
511
|
+
function serverStartStabilityMs(env) {
|
|
512
|
+
const parsed = Number(env.FARMING_START_STABILITY_MS || env.FARMING_SERVER_START_STABILITY_MS);
|
|
513
|
+
if (Number.isFinite(parsed) && parsed >= 0) return Math.min(parsed, 60_000);
|
|
514
|
+
return DEFAULT_SERVER_START_STABILITY_MS;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function waitForProcessStability(pid, durationMs = DEFAULT_SERVER_START_STABILITY_MS) {
|
|
518
|
+
const startedAt = Date.now();
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
const tick = () => {
|
|
521
|
+
if (!isRunning(pid)) {
|
|
522
|
+
reject(new Error('server process exited during startup stability check'));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const remainingMs = durationMs - (Date.now() - startedAt);
|
|
526
|
+
if (remainingMs <= 0) {
|
|
527
|
+
resolve();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
setTimeout(tick, Math.min(100, remainingMs));
|
|
531
|
+
};
|
|
532
|
+
tick();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function cleanupFailedDaemonStart(configDir, childPid) {
|
|
537
|
+
if (isRunning(childPid)) {
|
|
538
|
+
try {
|
|
539
|
+
process.kill(childPid, 'SIGTERM');
|
|
540
|
+
} catch {
|
|
541
|
+
// The child may exit between the liveness check and the signal.
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (readPid(configDir) !== childPid) return;
|
|
545
|
+
fs.rmSync(pidFile(configDir), { force: true });
|
|
546
|
+
fs.rmSync(serverStateFile(configDir), { force: true });
|
|
547
|
+
}
|
|
548
|
+
|
|
503
549
|
function waitForServer(env, timeoutMs = serverStartTimeoutMs(env), childPid = 0) {
|
|
504
550
|
const startedAt = Date.now();
|
|
505
551
|
const port = Number(env.PORT || DEFAULT_PORT);
|
|
@@ -628,7 +674,10 @@ async function startDaemon(parsed) {
|
|
|
628
674
|
|
|
629
675
|
try {
|
|
630
676
|
await waitForServer(env, serverStartTimeoutMs(env), child.pid);
|
|
677
|
+
await waitForProcessStability(child.pid, serverStartStabilityMs(env));
|
|
678
|
+
await waitForServer(env, Math.min(serverStartTimeoutMs(env), 5_000), child.pid);
|
|
631
679
|
} catch (error) {
|
|
680
|
+
cleanupFailedDaemonStart(configDir, child.pid);
|
|
632
681
|
console.error(error.message);
|
|
633
682
|
const logs = tailFile(logFile(configDir), 80);
|
|
634
683
|
if (logs) console.error(logs);
|
|
@@ -773,6 +822,7 @@ module.exports = {
|
|
|
773
822
|
NATIVE_PTY_HOST_ARG,
|
|
774
823
|
buildCleanEnvExecCommand,
|
|
775
824
|
childInvocation,
|
|
825
|
+
cleanupFailedDaemonStart,
|
|
776
826
|
buildControlEnv,
|
|
777
827
|
buildServerEnv,
|
|
778
828
|
computeNodeHeapMb,
|
|
@@ -784,6 +834,7 @@ module.exports = {
|
|
|
784
834
|
reviewUrl,
|
|
785
835
|
readServerState,
|
|
786
836
|
serverStartTimeoutMs,
|
|
837
|
+
serverStartStabilityMs,
|
|
787
838
|
splitControlArgs,
|
|
788
839
|
run,
|
|
789
840
|
serverStateFile,
|
|
@@ -166,6 +166,7 @@ class FarmingSessionStore {
|
|
|
166
166
|
providerSessionSource: typeof agent.providerSessionSource === 'string' ? agent.providerSessionSource : '',
|
|
167
167
|
providerSessionResolvedAt: typeof agent.providerSessionResolvedAt === 'number' ? agent.providerSessionResolvedAt : null,
|
|
168
168
|
providerSessionTitle: typeof agent.providerSessionTitle === 'string' ? agent.providerSessionTitle : '',
|
|
169
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
169
170
|
codexRuntimeMode: typeof agent.codexRuntimeMode === 'string' ? agent.codexRuntimeMode : '',
|
|
170
171
|
agentRuntimeMode: typeof agent.agentRuntimeMode === 'string' ? agent.agentRuntimeMode : 'terminal',
|
|
171
172
|
acpState: typeof agent.acpState === 'string' ? agent.acpState : '',
|
|
@@ -102,6 +102,8 @@ function startArguments(payload) {
|
|
|
102
102
|
function commandEnvironment() {
|
|
103
103
|
const env = { ...process.env };
|
|
104
104
|
delete env.FARMING_NPM_UPDATE_PAYLOAD;
|
|
105
|
+
delete env.FARMING_RUN_SERVER;
|
|
106
|
+
delete env.FARMING_RUN_NATIVE_PTY_HOST;
|
|
105
107
|
return env;
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -112,6 +114,7 @@ async function installPackage(payload, version) {
|
|
|
112
114
|
if (payload.npmPrefix) args.push('--prefix', payload.npmPrefix);
|
|
113
115
|
args.push(packageSpec, '--no-audit', '--no-fund');
|
|
114
116
|
await runCommand(payload.npmCommand || 'npm', args, {
|
|
117
|
+
cwd: payload.configDir,
|
|
115
118
|
env: commandEnvironment(),
|
|
116
119
|
logPath: payload.logPath,
|
|
117
120
|
});
|
|
@@ -120,6 +123,7 @@ async function installPackage(payload, version) {
|
|
|
120
123
|
async function startServer(payload, version = payload.targetVersion) {
|
|
121
124
|
appendLog(payload.logPath, `Starting Farming ${version}`);
|
|
122
125
|
await runCommand(payload.nodePath, startArguments(payload), {
|
|
126
|
+
cwd: payload.configDir,
|
|
123
127
|
env: commandEnvironment(),
|
|
124
128
|
logPath: payload.logPath,
|
|
125
129
|
});
|
package/backend/server.js
CHANGED
|
@@ -1168,6 +1168,10 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json
|
|
|
1168
1168
|
res.json(await agentManager.setAcpSessionMode(req.params.agentId, req.body.modeId));
|
|
1169
1169
|
return;
|
|
1170
1170
|
}
|
|
1171
|
+
if (Array.isArray(req.body?.configOptions)) {
|
|
1172
|
+
res.json(await agentManager.setAcpSessionConfigOptions(req.params.agentId, req.body.configOptions));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1171
1175
|
if (typeof req.body?.configId === 'string' && Object.prototype.hasOwnProperty.call(req.body, 'value')) {
|
|
1172
1176
|
res.json(await agentManager.setAcpSessionConfigOption(
|
|
1173
1177
|
req.params.agentId,
|
|
@@ -1176,7 +1180,7 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json
|
|
|
1176
1180
|
));
|
|
1177
1181
|
return;
|
|
1178
1182
|
}
|
|
1179
|
-
res.status(400).json({ error: 'ACP modeId or configId/value is required' });
|
|
1183
|
+
res.status(400).json({ error: 'ACP modeId, configOptions, or configId/value is required' });
|
|
1180
1184
|
} catch (error) {
|
|
1181
1185
|
const message = error && error.message ? error.message : 'Failed to update ACP session';
|
|
1182
1186
|
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
@@ -666,6 +666,16 @@ function releaseInstallDir(rootDir, env = process.env) {
|
|
|
666
666
|
return path.join(os.homedir(), 'farming');
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
function nodeScriptInvocation(nodePath, scriptPath, env = process.env) {
|
|
670
|
+
if (env.FARMING_NODE_LD && env.FARMING_NODE_LIBRARY_PATH) {
|
|
671
|
+
return {
|
|
672
|
+
command: env.FARMING_NODE_LD,
|
|
673
|
+
args: ['--library-path', env.FARMING_NODE_LIBRARY_PATH, nodePath, scriptPath],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return { command: nodePath, args: [scriptPath] };
|
|
677
|
+
}
|
|
678
|
+
|
|
669
679
|
function npmPackageMetadataUrl(registryUrl, packageName) {
|
|
670
680
|
const registry = String(registryUrl || DEFAULT_NPM_REGISTRY).replace(/\/+$/, '');
|
|
671
681
|
return `${registry}/${encodeURIComponent(packageName).replace(/^%40/, '@')}`;
|
|
@@ -739,7 +749,7 @@ class FarmingUpdateService {
|
|
|
739
749
|
compatibilityProfile: String(release.compatibilityProfile || ''),
|
|
740
750
|
bundledGlibcRuntime: release.bundledGlibcRuntime === true,
|
|
741
751
|
type: this.installMethod,
|
|
742
|
-
installDir: releaseInstallDir(this.rootDir),
|
|
752
|
+
installDir: this.installMethod === 'npm' ? this.rootDir : releaseInstallDir(this.rootDir),
|
|
743
753
|
};
|
|
744
754
|
}
|
|
745
755
|
|
|
@@ -1081,7 +1091,9 @@ class FarmingUpdateService {
|
|
|
1081
1091
|
serverHome: process.env.FARMING_SERVER_HOME || '',
|
|
1082
1092
|
disableAuth: /^(1|true|yes|on)$/i.test(String(process.env.FARMING_DISABLE_AUTH || '')),
|
|
1083
1093
|
};
|
|
1084
|
-
const
|
|
1094
|
+
const helperInvocation = nodeScriptInvocation(nodePath, helperPath);
|
|
1095
|
+
const child = this.spawn(helperInvocation.command, helperInvocation.args, {
|
|
1096
|
+
cwd: this.configDir,
|
|
1085
1097
|
detached: true,
|
|
1086
1098
|
stdio: 'ignore',
|
|
1087
1099
|
env: {
|