farming-code 2.2.20 → 2.2.22
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 +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +332 -102
- package/backend/storage-layout.js +10 -0
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +199 -81
- package/package.json +9 -3
- package/dist/assets/App-BupehIbc.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-y2aK_56c.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
package/backend/agent-order.js
CHANGED
|
@@ -83,14 +83,16 @@ function reorderedProjectAgentOrders(agents, agentId, beforeAgentId = '', afterA
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const updates = new Map();
|
|
86
|
-
const orderAt = index =>
|
|
86
|
+
const orderAt = index => {
|
|
87
|
+
const agent = fullOrder[index];
|
|
88
|
+
return agent ? (updates.get(agent.id) ?? finiteOrder(agent.projectOrder) ?? 0) : 0;
|
|
89
|
+
};
|
|
87
90
|
let upper = fullInsertIndex > 0 ? orderAt(fullInsertIndex - 1) : null;
|
|
88
91
|
let lower = fullInsertIndex < fullOrder.length ? orderAt(fullInsertIndex) : null;
|
|
89
92
|
if (upper !== null && lower !== null && upper - lower <= 1) {
|
|
90
93
|
fullOrder.forEach((agent, index) => {
|
|
91
94
|
const order = (fullOrder.length - index) * AGENT_ORDER_STEP;
|
|
92
95
|
if (projectOrder(agent) !== order) updates.set(agent.id, order);
|
|
93
|
-
agent.projectOrder = order;
|
|
94
96
|
});
|
|
95
97
|
upper = fullInsertIndex > 0 ? orderAt(fullInsertIndex - 1) : null;
|
|
96
98
|
lower = fullInsertIndex < fullOrder.length ? orderAt(fullInsertIndex) : null;
|
|
@@ -127,14 +129,16 @@ function reorderedPinnedAgentOrders(agents, agentId, beforeAgentId = '', afterAg
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
const updates = new Map();
|
|
130
|
-
const orderAt = index =>
|
|
132
|
+
const orderAt = index => {
|
|
133
|
+
const agent = pinned[index];
|
|
134
|
+
return agent ? (updates.get(agent.id) ?? finiteOrder(agent.pinnedOrder) ?? 0) : 0;
|
|
135
|
+
};
|
|
131
136
|
let previous = insertIndex > 0 ? orderAt(insertIndex - 1) : null;
|
|
132
137
|
let next = insertIndex < pinned.length ? orderAt(insertIndex) : null;
|
|
133
138
|
if (previous !== null && next !== null && next - previous <= 1) {
|
|
134
139
|
pinned.forEach((agent, index) => {
|
|
135
140
|
const order = (index + 1) * AGENT_ORDER_STEP;
|
|
136
141
|
if (pinnedOrder(agent) !== order) updates.set(agent.id, order);
|
|
137
|
-
agent.pinnedOrder = order;
|
|
138
142
|
});
|
|
139
143
|
previous = insertIndex > 0 ? orderAt(insertIndex - 1) : null;
|
|
140
144
|
next = insertIndex < pinned.length ? orderAt(insertIndex) : null;
|
|
@@ -11,6 +11,7 @@ function acpBinding(source = {}) {
|
|
|
11
11
|
error: source.error || source.acpError || '',
|
|
12
12
|
stopReason: source.stopReason || source.acpStopReason || '',
|
|
13
13
|
supportsSteer: source.supportsSteer === true,
|
|
14
|
+
supportsFork: source.supportsFork === true,
|
|
14
15
|
pendingPermission: source.pendingPermission || source.acpPendingPermission || null,
|
|
15
16
|
pendingPermissions: source.pendingPermissions || source.acpPendingPermissions || [],
|
|
16
17
|
pendingElicitation: source.pendingElicitation || source.acpPendingElicitation || null,
|
|
@@ -71,17 +71,21 @@ function runInstaller(payload) {
|
|
|
71
71
|
|
|
72
72
|
async function runBundleUpdate(rawPayload) {
|
|
73
73
|
const payload = validatePayload(rawPayload);
|
|
74
|
-
writeJsonAtomic(payload.stateFile, stateFor(payload, '
|
|
74
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'restarting', {
|
|
75
|
+
preparedAt: payload.preparedAt,
|
|
76
|
+
}));
|
|
75
77
|
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
76
78
|
try {
|
|
77
79
|
await runInstaller(payload);
|
|
78
80
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'succeeded', {
|
|
81
|
+
preparedAt: payload.preparedAt,
|
|
79
82
|
completedAt: new Date().toISOString(),
|
|
80
83
|
}));
|
|
81
84
|
appendLog(payload.logPath, `Farming updated to ${payload.version}`);
|
|
82
85
|
} catch (error) {
|
|
83
86
|
const message = error && error.message ? error.message : String(error);
|
|
84
87
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
|
|
88
|
+
preparedAt: payload.preparedAt,
|
|
85
89
|
error: message,
|
|
86
90
|
completedAt: new Date().toISOString(),
|
|
87
91
|
}));
|
|
@@ -498,6 +498,22 @@ class ConfigManager {
|
|
|
498
498
|
return normalized;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
setProjectName(workspace, name) {
|
|
502
|
+
const normalized = this.normalizeProjectNames({ [workspace]: name });
|
|
503
|
+
const entry = Object.entries(normalized)[0];
|
|
504
|
+
if (!entry) throw new Error('Project workspace and name are required');
|
|
505
|
+
const [normalizedWorkspace, normalizedName] = entry;
|
|
506
|
+
this.settings.projectNames = {
|
|
507
|
+
...this.settings.projectNames,
|
|
508
|
+
[normalizedWorkspace]: normalizedName,
|
|
509
|
+
};
|
|
510
|
+
this.writeSettingsFile();
|
|
511
|
+
return {
|
|
512
|
+
workspace: normalizedWorkspace,
|
|
513
|
+
name: normalizedName,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
501
517
|
normalizeAppearance(appearance) {
|
|
502
518
|
return ['system', 'light', 'dark'].includes(appearance) ? appearance : 'system';
|
|
503
519
|
}
|
package/backend/control-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const { runtimeKind } = require('./agent-runtime-binding');
|
|
3
4
|
const { terminalInputReady } = require('./terminal-status');
|
|
4
5
|
|
|
@@ -19,6 +20,21 @@ function normalizeTail(value, fallback = 4000) {
|
|
|
19
20
|
return Math.max(0, Math.min(100000, Math.floor(tail)));
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function stableJsonValue(value) {
|
|
24
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
25
|
+
if (!value || typeof value !== 'object') return value;
|
|
26
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
27
|
+
result[key] = stableJsonValue(value[key]);
|
|
28
|
+
return result;
|
|
29
|
+
}, {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requestSignature(value) {
|
|
33
|
+
return crypto.createHash('sha256')
|
|
34
|
+
.update(JSON.stringify(stableJsonValue(value)))
|
|
35
|
+
.digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
function terminalReadinessOptions(agent) {
|
|
23
39
|
return {
|
|
24
40
|
command: agent.command,
|
|
@@ -104,6 +120,7 @@ function waitForTerminalInputReadiness(agentManager, agentId, options = {}) {
|
|
|
104
120
|
|
|
105
121
|
function createControlRouter(agentManager, options = {}) {
|
|
106
122
|
const router = express.Router();
|
|
123
|
+
const createRequestAdmissions = new Map();
|
|
107
124
|
const notifyUpdate = typeof options.notifyUpdate === 'function' ? options.notifyUpdate : () => {};
|
|
108
125
|
const initialInputTimeoutMs = Number.isFinite(options.initialInputTimeoutMs)
|
|
109
126
|
? Math.max(1, options.initialInputTimeoutMs)
|
|
@@ -138,13 +155,33 @@ function createControlRouter(agentManager, options = {}) {
|
|
|
138
155
|
const workspace = typeof body.workspace === 'string' ? body.workspace : null;
|
|
139
156
|
const task = typeof body.task === 'string' ? body.task.trim() : '';
|
|
140
157
|
const initialInput = typeof body.initialInput === 'string' ? body.initialInput : task;
|
|
158
|
+
const createRequestId = typeof body.requestId === 'string' && body.requestId.trim()
|
|
159
|
+
? body.requestId
|
|
160
|
+
: req.get('Idempotency-Key') || '';
|
|
161
|
+
const controlRequestSignature = requestSignature(body);
|
|
141
162
|
|
|
142
163
|
if (!command) {
|
|
143
164
|
res.status(400).json({ error: 'command is required' });
|
|
144
165
|
return;
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
|
|
168
|
+
const pendingRequest = createRequestId
|
|
169
|
+
? createRequestAdmissions.get(createRequestId)
|
|
170
|
+
: null;
|
|
171
|
+
if (pendingRequest) {
|
|
172
|
+
if (pendingRequest.signature !== controlRequestSignature) {
|
|
173
|
+
res.status(409).json({
|
|
174
|
+
error: `Create request ${createRequestId} is already in progress with different parameters`,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
void pendingRequest.promise.then(outcome => {
|
|
179
|
+
res.status(outcome.status).json(outcome.body);
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
agentManager.startAgent(command, workspace, (agentId, error, metadata = {}) => {
|
|
148
185
|
if (error) {
|
|
149
186
|
res.status(400).json({ error });
|
|
150
187
|
return;
|
|
@@ -155,11 +192,36 @@ function createControlRouter(agentManager, options = {}) {
|
|
|
155
192
|
return;
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
|
|
195
|
+
if (metadata.deduplicated === true) {
|
|
196
|
+
const recorded = metadata.createResult?.controlApi;
|
|
197
|
+
if (recorded && Number.isInteger(recorded.status) && recorded.body) {
|
|
198
|
+
res.status(recorded.status).json(recorded.body);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const admitted = createRequestId
|
|
202
|
+
? createRequestAdmissions.get(createRequestId)
|
|
203
|
+
: null;
|
|
204
|
+
if (admitted) {
|
|
205
|
+
void admitted.promise.then(outcome => {
|
|
206
|
+
res.status(outcome.status).json(outcome.body);
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
res.status(409).json({
|
|
211
|
+
error: 'Create succeeded, but the initial-input outcome is unknown; input was not replayed',
|
|
212
|
+
code: 'create-result-unknown',
|
|
213
|
+
agentId,
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const deliveryPromise = (async () => {
|
|
159
219
|
if (!initialInput) {
|
|
160
220
|
notifyUpdate();
|
|
161
|
-
|
|
162
|
-
|
|
221
|
+
return {
|
|
222
|
+
status: 201,
|
|
223
|
+
body: { agentId, initialInputDelivered: false },
|
|
224
|
+
};
|
|
163
225
|
}
|
|
164
226
|
|
|
165
227
|
const started = findAgent(agentManager.getState(), agentId);
|
|
@@ -170,8 +232,10 @@ function createControlRouter(agentManager, options = {}) {
|
|
|
170
232
|
if (structuredRuntime) {
|
|
171
233
|
await agentManager.sendComposerMessage(agentId, initialInput);
|
|
172
234
|
notifyUpdate();
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
return {
|
|
236
|
+
status: 201,
|
|
237
|
+
body: { agentId, initialInputDelivered: true, inputMode: 'structured' },
|
|
238
|
+
};
|
|
175
239
|
}
|
|
176
240
|
|
|
177
241
|
const readiness = await waitForTerminalInputReadiness(agentManager, agentId, {
|
|
@@ -204,21 +268,70 @@ function createControlRouter(agentManager, options = {}) {
|
|
|
204
268
|
throw Object.assign(new Error(`Initial Terminal task was not delivered: ${reason}`), { code: reason });
|
|
205
269
|
}
|
|
206
270
|
notifyUpdate();
|
|
207
|
-
|
|
208
|
-
|
|
271
|
+
return {
|
|
272
|
+
status: 201,
|
|
273
|
+
body: { agentId, initialInputDelivered: true, inputMode: 'terminal' },
|
|
274
|
+
};
|
|
275
|
+
})().catch(deliveryError => {
|
|
209
276
|
const status = deliveryError?.code === 'initial-input-timeout' ? 504 : 409;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
277
|
+
return {
|
|
278
|
+
status,
|
|
279
|
+
body: {
|
|
280
|
+
error: deliveryError?.message || 'Initial input delivery failed',
|
|
281
|
+
code: deliveryError?.code || 'initial-input-failed',
|
|
282
|
+
agentId,
|
|
283
|
+
initialInputDelivered: false,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
const durableDeliveryPromise = deliveryPromise.then(outcome => {
|
|
288
|
+
if (!createRequestId) return outcome;
|
|
289
|
+
let persisted;
|
|
290
|
+
try {
|
|
291
|
+
persisted = agentManager.recordCreateRequestResult(
|
|
292
|
+
agentId,
|
|
293
|
+
createRequestId,
|
|
294
|
+
{ controlApi: outcome },
|
|
295
|
+
);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
persisted = { error: error?.message || 'Create result persistence failed' };
|
|
298
|
+
}
|
|
299
|
+
if (persisted?.error) {
|
|
300
|
+
return {
|
|
301
|
+
status: 409,
|
|
302
|
+
body: {
|
|
303
|
+
error: `${persisted.error}; Create outcome was not persisted and initial input was not replayed`,
|
|
304
|
+
code: 'create-result-not-durable',
|
|
305
|
+
agentId,
|
|
306
|
+
initialInputDelivered: outcome.body?.initialInputDelivered === true,
|
|
307
|
+
createResultDurable: false,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return outcome;
|
|
312
|
+
});
|
|
313
|
+
if (createRequestId) {
|
|
314
|
+
const admission = {
|
|
315
|
+
signature: controlRequestSignature,
|
|
316
|
+
promise: durableDeliveryPromise,
|
|
317
|
+
};
|
|
318
|
+
createRequestAdmissions.set(createRequestId, admission);
|
|
319
|
+
void durableDeliveryPromise.finally(() => {
|
|
320
|
+
if (createRequestAdmissions.get(createRequestId) === admission) {
|
|
321
|
+
createRequestAdmissions.delete(createRequestId);
|
|
322
|
+
}
|
|
323
|
+
}).catch(() => {});
|
|
324
|
+
}
|
|
325
|
+
void durableDeliveryPromise.then(outcome => {
|
|
326
|
+
res.status(outcome.status).json(outcome.body);
|
|
216
327
|
});
|
|
217
328
|
}, {
|
|
218
329
|
wantsMain: false,
|
|
219
330
|
parentAgentId: typeof body.parentAgentId === 'string' ? body.parentAgentId : '',
|
|
220
331
|
task,
|
|
221
332
|
source: 'control-cli',
|
|
333
|
+
createRequestId,
|
|
334
|
+
createInitialInputSignature: requestSignature(initialInput),
|
|
222
335
|
agentRuntimeMode: ['json', 'acp', 'chat'].includes(body.agentRuntimeMode) ? body.agentRuntimeMode : 'terminal',
|
|
223
336
|
acpHistoryMode: body.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
224
337
|
providerSessionTitle: typeof body.providerSessionTitle === 'string' ? body.providerSessionTitle : '',
|
|
@@ -306,15 +419,28 @@ function createControlRouter(agentManager, options = {}) {
|
|
|
306
419
|
|
|
307
420
|
router.delete('/agents/:agentId', async (req, res) => {
|
|
308
421
|
const agentId = req.params.agentId;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
422
|
+
let requested;
|
|
423
|
+
try {
|
|
424
|
+
requested = await agentManager.requestKillAgent(agentId);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
res.status(503).json({
|
|
427
|
+
error: error?.message || 'Agent lifecycle recovery is unavailable',
|
|
428
|
+
retryable: true,
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const result = requested.result;
|
|
433
|
+
if (result?.error) {
|
|
434
|
+
res.status(409).json({ error: result.error });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (result?.accepted) {
|
|
438
|
+
void requested.completion.finally(() => notifyUpdate()).catch(() => {});
|
|
439
|
+
res.status(202).json(result);
|
|
312
440
|
return;
|
|
313
441
|
}
|
|
314
|
-
|
|
315
|
-
await agentManager.killAgent(agentId);
|
|
316
442
|
notifyUpdate();
|
|
317
|
-
res.json({ success: true });
|
|
443
|
+
res.json({ success: true, agentId, ...result });
|
|
318
444
|
});
|
|
319
445
|
|
|
320
446
|
return router;
|