farming-code 2.2.6 → 2.2.7
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/backend/acp-runtime.js +93 -1
- package/backend/agent-manager.js +64 -10
- 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-iA2BU9P6.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-B8SHldPE.js} +2 -2
- package/dist/assets/main-DpYuBonn.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/acp-runtime.js
CHANGED
|
@@ -23,6 +23,7 @@ const DEFAULT_CANCEL_TIMEOUT_MS = 15_000;
|
|
|
23
23
|
const DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS = 350;
|
|
24
24
|
const DEFAULT_HISTORY_REPLAY_QUIET_MS = 150;
|
|
25
25
|
const DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS = 5_000;
|
|
26
|
+
const CODEX_SET_SESSION_MODEL_METHOD = 'session/set_model';
|
|
26
27
|
|
|
27
28
|
let sdkPromise;
|
|
28
29
|
const runtimeRequire = createRequire(__filename);
|
|
@@ -386,6 +387,16 @@ class AcpRuntime extends EventEmitter {
|
|
|
386
387
|
binding.configOptions = sessionResponse?.configOptions || [];
|
|
387
388
|
binding.sessionState.currentModeId = String(binding.modes?.currentModeId || '');
|
|
388
389
|
binding.sessionState.configOptions = JSON.parse(JSON.stringify(binding.configOptions));
|
|
390
|
+
if (provider === 'codex' && options.serviceTier && options.serviceTier !== 'config') {
|
|
391
|
+
const fastOption = binding.configOptions.find(option => (
|
|
392
|
+
option.type === 'boolean'
|
|
393
|
+
&& /fast/i.test(`${option.id || ''} ${option.name || ''} ${option.category || ''}`)
|
|
394
|
+
));
|
|
395
|
+
const fastEnabled = ['fast', 'priority'].includes(options.serviceTier);
|
|
396
|
+
if (fastOption && fastOption.currentValue !== fastEnabled) {
|
|
397
|
+
await this.applySessionConfigOption(binding, fastOption.id, fastEnabled, { emit: false });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
389
400
|
binding.state = 'idle';
|
|
390
401
|
binding.updatedAt = new Date().toISOString();
|
|
391
402
|
this.emitRuntime(binding);
|
|
@@ -857,6 +868,87 @@ class AcpRuntime extends EventEmitter {
|
|
|
857
868
|
|
|
858
869
|
async setSessionConfigOption(agentId, configId, value) {
|
|
859
870
|
const binding = this.requireBinding(agentId);
|
|
871
|
+
const option = binding.configOptions?.find(candidate => candidate.id === String(configId || ''));
|
|
872
|
+
if (
|
|
873
|
+
binding.provider === 'codex'
|
|
874
|
+
&& option?.type === 'select'
|
|
875
|
+
&& /(^|[\s_-])model([\s_-]|$)/i.test(`${option.id} ${option.name || ''} ${option.category || ''}`)
|
|
876
|
+
) {
|
|
877
|
+
// Let the adapter choose a supported fallback effort from its current
|
|
878
|
+
// snapshot first. The refresh extension requires an explicit effort and
|
|
879
|
+
// would otherwise reject a valid model change (for example ultra -> a
|
|
880
|
+
// model that tops out at max).
|
|
881
|
+
await this.applySessionConfigOption(binding, configId, value, { emit: false });
|
|
882
|
+
const reasoning = binding.configOptions?.find(candidate => (
|
|
883
|
+
candidate.type === 'select'
|
|
884
|
+
&& /(reasoning|thought)/i.test(`${candidate.id} ${candidate.name || ''} ${candidate.category || ''}`)
|
|
885
|
+
));
|
|
886
|
+
if (typeof reasoning?.currentValue === 'string' && reasoning.currentValue) {
|
|
887
|
+
await this.refreshCodexSessionModel(binding, String(value ?? ''), reasoning.currentValue);
|
|
888
|
+
return this.applySessionConfigOption(binding, configId, value);
|
|
889
|
+
}
|
|
890
|
+
this.emitSession(binding);
|
|
891
|
+
return { sessionId: binding.sessionId, configOptions: binding.configOptions };
|
|
892
|
+
}
|
|
893
|
+
return this.applySessionConfigOption(binding, configId, value);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async setSessionConfigOptions(agentId, changes) {
|
|
897
|
+
const binding = this.requireBinding(agentId);
|
|
898
|
+
const normalized = Array.isArray(changes)
|
|
899
|
+
? changes.filter(change => change && typeof change.configId === 'string' && Object.prototype.hasOwnProperty.call(change, 'value'))
|
|
900
|
+
: [];
|
|
901
|
+
if (normalized.length === 0) throw new Error('ACP config options are required');
|
|
902
|
+
|
|
903
|
+
const configById = new Map((binding.configOptions || []).map(option => [option.id, option]));
|
|
904
|
+
const modelChange = normalized.find(change => {
|
|
905
|
+
const option = configById.get(change.configId);
|
|
906
|
+
return option?.type === 'select'
|
|
907
|
+
&& /(^|[\s_-])model([\s_-]|$)/i.test(`${option.id} ${option.name || ''} ${option.category || ''}`);
|
|
908
|
+
});
|
|
909
|
+
const reasoningChange = normalized.find(change => {
|
|
910
|
+
const option = configById.get(change.configId);
|
|
911
|
+
return option?.type === 'select'
|
|
912
|
+
&& /(reasoning|thought)/i.test(`${option.id} ${option.name || ''} ${option.category || ''}`);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
let response;
|
|
916
|
+
const handled = new Set();
|
|
917
|
+
if (binding.provider === 'codex' && modelChange && reasoningChange) {
|
|
918
|
+
await this.applySessionConfigOption(binding, modelChange.configId, modelChange.value, { emit: false });
|
|
919
|
+
await this.applySessionConfigOption(binding, reasoningChange.configId, reasoningChange.value, { emit: false });
|
|
920
|
+
const currentReasoning = binding.configOptions?.find(candidate => (
|
|
921
|
+
candidate.type === 'select'
|
|
922
|
+
&& /(reasoning|thought)/i.test(`${candidate.id} ${candidate.name || ''} ${candidate.category || ''}`)
|
|
923
|
+
));
|
|
924
|
+
await this.refreshCodexSessionModel(
|
|
925
|
+
binding,
|
|
926
|
+
String(modelChange.value ?? ''),
|
|
927
|
+
String(currentReasoning?.currentValue || reasoningChange.value || '')
|
|
928
|
+
);
|
|
929
|
+
response = await this.applySessionConfigOption(binding, modelChange.configId, modelChange.value);
|
|
930
|
+
handled.add(modelChange);
|
|
931
|
+
handled.add(reasoningChange);
|
|
932
|
+
}
|
|
933
|
+
for (const change of normalized) {
|
|
934
|
+
if (handled.has(change)) continue;
|
|
935
|
+
response = await this.setSessionConfigOption(agentId, change.configId, change.value);
|
|
936
|
+
}
|
|
937
|
+
return response;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async refreshCodexSessionModel(binding, model, effort) {
|
|
941
|
+
await withTimeout(
|
|
942
|
+
binding.connection.request(CODEX_SET_SESSION_MODEL_METHOD, {
|
|
943
|
+
sessionId: binding.sessionId,
|
|
944
|
+
modelId: `${model}[${effort}]`,
|
|
945
|
+
}),
|
|
946
|
+
this.requestTimeoutMs,
|
|
947
|
+
'Codex ACP session/set_model capability refresh'
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async applySessionConfigOption(binding, configId, value, options = {}) {
|
|
860
952
|
const request = typeof value === 'boolean'
|
|
861
953
|
? { sessionId: binding.sessionId, configId: String(configId || ''), type: 'boolean', value }
|
|
862
954
|
: { sessionId: binding.sessionId, configId: String(configId || ''), value: String(value ?? '') };
|
|
@@ -867,7 +959,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
867
959
|
);
|
|
868
960
|
binding.configOptions = response?.configOptions || binding.configOptions;
|
|
869
961
|
binding.sessionState.configOptions = JSON.parse(JSON.stringify(binding.configOptions));
|
|
870
|
-
this.emitSession(binding);
|
|
962
|
+
if (options.emit !== false) this.emitSession(binding);
|
|
871
963
|
return { sessionId: binding.sessionId, configOptions: binding.configOptions };
|
|
872
964
|
}
|
|
873
965
|
|
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;
|
|
@@ -2189,6 +2212,7 @@ class AgentManager extends EventEmitter {
|
|
|
2189
2212
|
providerSessionSource: providerSessionPlan.source || '',
|
|
2190
2213
|
providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
|
|
2191
2214
|
providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
|
|
2215
|
+
terminalInputReceived: false,
|
|
2192
2216
|
codexRuntimeMode: providerSessionPlan.provider === 'codex'
|
|
2193
2217
|
? (useCodexAppServer ? 'app-server' : 'cli')
|
|
2194
2218
|
: '',
|
|
@@ -2338,7 +2362,9 @@ class AgentManager extends EventEmitter {
|
|
|
2338
2362
|
executable: spawnProgram,
|
|
2339
2363
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2340
2364
|
cwd: workspace,
|
|
2341
|
-
sessionId:
|
|
2365
|
+
sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
|
|
2366
|
+
? ''
|
|
2367
|
+
: agentRecord.providerSessionId,
|
|
2342
2368
|
historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
2343
2369
|
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2344
2370
|
model: this.configManager && this.configManager.getCodexModel
|
|
@@ -2721,6 +2747,11 @@ class AgentManager extends EventEmitter {
|
|
|
2721
2747
|
return this.acpRuntime.setSessionConfigOption(agentId, configId, value);
|
|
2722
2748
|
}
|
|
2723
2749
|
|
|
2750
|
+
setAcpSessionConfigOptions(agentId, changes) {
|
|
2751
|
+
this.getAcpSession(agentId);
|
|
2752
|
+
return this.acpRuntime.setSessionConfigOptions(agentId, changes);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2724
2755
|
async setCodexAppServerGoal(agentId, patch = {}) {
|
|
2725
2756
|
const agent = this.assertCodexAppServerGoalAgent(agentId);
|
|
2726
2757
|
const objective = typeof patch.objective === 'string' ? patch.objective.trim().slice(0, 4000) : undefined;
|
|
@@ -2763,6 +2794,12 @@ class AgentManager extends EventEmitter {
|
|
|
2763
2794
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
2764
2795
|
if (!engine) return;
|
|
2765
2796
|
|
|
2797
|
+
if (agent.terminalInputReceived !== true) {
|
|
2798
|
+
agent.terminalInputReceived = true;
|
|
2799
|
+
this.ensurePersistentAgentSession(agent);
|
|
2800
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2766
2803
|
for (let attempt = 0; ; attempt += 1) {
|
|
2767
2804
|
try {
|
|
2768
2805
|
await engine.sendInput(agentId, input);
|
|
@@ -3158,7 +3195,14 @@ class AgentManager extends EventEmitter {
|
|
|
3158
3195
|
return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
|
|
3159
3196
|
}
|
|
3160
3197
|
const sessionId = String(agent.providerSessionId || '').trim();
|
|
3161
|
-
|
|
3198
|
+
const canStartFreshAcpSession = nextMode === 'acp'
|
|
3199
|
+
&& agent.agentRuntimeMode === 'terminal'
|
|
3200
|
+
&& agent.terminalInputReceived !== true
|
|
3201
|
+
&& (
|
|
3202
|
+
agent.providerSessionTemporary === true
|
|
3203
|
+
|| ['codex-temporary', 'claude-session-id', 'qoder-session-id'].includes(agent.providerSessionSource || '')
|
|
3204
|
+
);
|
|
3205
|
+
if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
|
|
3162
3206
|
return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
|
|
3163
3207
|
}
|
|
3164
3208
|
// A live ACP binding is the authoritative owner of a newly-created
|
|
@@ -3177,15 +3221,19 @@ class AgentManager extends EventEmitter {
|
|
|
3177
3221
|
}
|
|
3178
3222
|
}
|
|
3179
3223
|
const previouslyVerifiedSession = String(agent.runtimeSwitchVerifiedSessionId || '') === sessionId;
|
|
3180
|
-
|
|
3224
|
+
let startsFreshAcpSession = canStartFreshAcpSession && !isSafeProviderSessionId(sessionId);
|
|
3225
|
+
if (!startsFreshAcpSession && !liveAcpSession && !previouslyVerifiedSession) {
|
|
3181
3226
|
const providerSession = await this.findRuntimeSwitchSession(agent);
|
|
3182
3227
|
if (!providerSession) {
|
|
3183
|
-
|
|
3228
|
+
if (canStartFreshAcpSession) startsFreshAcpSession = true;
|
|
3229
|
+
else return { error: 'The saved Agent session is no longer available in the selected Agent Home.' };
|
|
3184
3230
|
}
|
|
3185
3231
|
}
|
|
3186
|
-
const command =
|
|
3187
|
-
|
|
3188
|
-
|
|
3232
|
+
const command = startsFreshAcpSession
|
|
3233
|
+
? (agent.forkCommand || agent.command)
|
|
3234
|
+
: buildAgentSessionResumeCommand(provider, sessionId, {
|
|
3235
|
+
cwd: agent.cwd || agent.projectWorkspace || '',
|
|
3236
|
+
});
|
|
3189
3237
|
if (!command) return { error: 'Failed to build provider resume command' };
|
|
3190
3238
|
const preserved = {
|
|
3191
3239
|
pinned: agent.pinned === true,
|
|
@@ -3202,7 +3250,9 @@ class AgentManager extends EventEmitter {
|
|
|
3202
3250
|
task: agent.task || agent.providerSessionTitle || '',
|
|
3203
3251
|
workflowTemplate: agent.workflowTemplate || '',
|
|
3204
3252
|
projectWorkspace: agent.projectWorkspace || agent.cwd || '',
|
|
3205
|
-
source:
|
|
3253
|
+
source: startsFreshAcpSession
|
|
3254
|
+
? 'ui-runtime-switch-fresh'
|
|
3255
|
+
: resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
|
|
3206
3256
|
providerHomeId: agent.providerHomeId || '',
|
|
3207
3257
|
providerHomePath: agent.providerHomePath || '',
|
|
3208
3258
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
@@ -3215,16 +3265,18 @@ class AgentManager extends EventEmitter {
|
|
|
3215
3265
|
projectOrder: preserved.projectOrder,
|
|
3216
3266
|
pinnedOrder: preserved.pinnedOrder,
|
|
3217
3267
|
agentRuntimeMode: nextMode,
|
|
3268
|
+
acpStartFresh: startsFreshAcpSession,
|
|
3218
3269
|
codexRuntimeMode: 'cli',
|
|
3219
3270
|
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
3220
3271
|
jsonCliEvents: preserved.jsonCliEvents,
|
|
3221
|
-
runtimeSwitchVerifiedSessionId: sessionId,
|
|
3272
|
+
runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
|
|
3222
3273
|
};
|
|
3223
3274
|
const originalMode = agent.agentRuntimeMode || 'terminal';
|
|
3224
3275
|
const originalOptions = {
|
|
3225
3276
|
...restartOptions,
|
|
3226
3277
|
agentRuntimeMode: originalMode,
|
|
3227
3278
|
codexRuntimeMode: agent.codexRuntimeMode || 'cli',
|
|
3279
|
+
acpStartFresh: false,
|
|
3228
3280
|
};
|
|
3229
3281
|
const startReplacement = options => new Promise(resolve => {
|
|
3230
3282
|
let settled = false;
|
|
@@ -3930,6 +3982,7 @@ class AgentManager extends EventEmitter {
|
|
|
3930
3982
|
providerSessionSource: agent.providerSessionSource || '',
|
|
3931
3983
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
3932
3984
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
3985
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
3933
3986
|
codexRuntimeMode: agent.codexRuntimeMode || '',
|
|
3934
3987
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
3935
3988
|
acpState: agent.acpState || '',
|
|
@@ -4095,6 +4148,7 @@ class AgentManager extends EventEmitter {
|
|
|
4095
4148
|
providerSessionSource: agent.providerSessionSource || '',
|
|
4096
4149
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
4097
4150
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
4151
|
+
terminalInputReceived: agent.terminalInputReceived === true,
|
|
4098
4152
|
codexRuntimeMode: agent.codexRuntimeMode || '',
|
|
4099
4153
|
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
4100
4154
|
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: {
|