farming-code 2.2.19 → 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 +337 -103
- package/backend/storage-layout.js +10 -0
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/usage-monitor.js +11 -0
- 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-DQdtPfCk.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 +365 -107
- package/frontend/skins/crt/index.html +5 -10
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +9 -3
- package/dist/assets/App-CbXYBccJ.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-qIBiLBWV.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-yzZEfgft.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { isDeepStrictEqual } = require('util');
|
|
4
5
|
const { legacyRuntimeMetadata } = require('./agent-runtime-binding');
|
|
6
|
+
const { lifecycleJournal } = require('./agent-lifecycle-journal');
|
|
5
7
|
const storageLayout = require('./storage-layout');
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const AGENT_RECORD_ID_PREFIX = 'agent';
|
|
10
|
+
const AGENT_RECORD_VERSION = 1;
|
|
11
|
+
const AGENT_STATE_VERSION = 1;
|
|
8
12
|
const MAX_MAIN_PAGE_SESSION_KEYS = 50;
|
|
13
|
+
const AGENT_STATE_FIELDS = [
|
|
14
|
+
'acpState',
|
|
15
|
+
'acpError',
|
|
16
|
+
'acpStopReason',
|
|
17
|
+
'acpPendingPermission',
|
|
18
|
+
'acpPendingPermissions',
|
|
19
|
+
'acpPendingElicitation',
|
|
20
|
+
'acpPendingElicitations',
|
|
21
|
+
'acpActiveElicitations',
|
|
22
|
+
'acpSessionUpdatedAt',
|
|
23
|
+
'acpSessionRevision',
|
|
24
|
+
'jsonCliState',
|
|
25
|
+
'jsonCliError',
|
|
26
|
+
'jsonCliTranscriptUpdatedAt',
|
|
27
|
+
'attentionSeq',
|
|
28
|
+
'readAttentionSeq',
|
|
29
|
+
'attentionUpdatedAt',
|
|
30
|
+
'readAttentionAt',
|
|
31
|
+
'attentionReason',
|
|
32
|
+
'attentionOutputEpoch',
|
|
33
|
+
'attentionOutputSeq',
|
|
34
|
+
'readOutputEpoch',
|
|
35
|
+
'readOutputSeq',
|
|
36
|
+
'unread',
|
|
37
|
+
];
|
|
38
|
+
const AGENT_STATE_FIELD_SET = new Set(AGENT_STATE_FIELDS);
|
|
9
39
|
const PRODUCT_STATE_FIELDS = [
|
|
10
40
|
'projectWorkspace',
|
|
11
41
|
'task',
|
|
@@ -29,15 +59,45 @@ function now() {
|
|
|
29
59
|
return Date.now();
|
|
30
60
|
}
|
|
31
61
|
|
|
32
|
-
function
|
|
62
|
+
function createAgentRecordId() {
|
|
33
63
|
const stamp = now().toString(36);
|
|
34
64
|
const random = crypto.randomBytes(6).toString('hex');
|
|
35
|
-
return `${
|
|
65
|
+
return `${AGENT_RECORD_ID_PREFIX}_${stamp}_${random}`;
|
|
36
66
|
}
|
|
37
67
|
|
|
38
68
|
function safeSessionFileName(id) {
|
|
39
69
|
const value = String(id || '').trim();
|
|
40
|
-
return /^
|
|
70
|
+
return /^(?:agent|fsess)_[A-Za-z0-9_-]+$/.test(value) ? `${value}.json` : '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isLegacySessionId(id) {
|
|
74
|
+
return /^fsess_[A-Za-z0-9_-]+$/.test(String(id || '').trim());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isAgentRecordId(id) {
|
|
78
|
+
return /^agent_[A-Za-z0-9_-]+$/.test(String(id || '').trim());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function agentRecordIdFor(agent) {
|
|
82
|
+
if (agent && Object.prototype.hasOwnProperty.call(agent, 'persistentSessionId')) {
|
|
83
|
+
return typeof agent.persistentSessionId === 'string' ? agent.persistentSessionId : '';
|
|
84
|
+
}
|
|
85
|
+
return typeof agent?.agentRecordId === 'string' ? agent.agentRecordId : '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cloneJson(value) {
|
|
89
|
+
if (value === undefined) return undefined;
|
|
90
|
+
return JSON.parse(JSON.stringify(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sameJson(left, right) {
|
|
94
|
+
return isDeepStrictEqual(left, right);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withoutUpdatedAt(value) {
|
|
98
|
+
const copy = { ...(value || {}) };
|
|
99
|
+
delete copy.updatedAt;
|
|
100
|
+
return copy;
|
|
41
101
|
}
|
|
42
102
|
|
|
43
103
|
function atomicWriteJson(file, value) {
|
|
@@ -77,17 +137,62 @@ class FarmingSessionStore {
|
|
|
77
137
|
init({ legacyMainPageSessionKeys = [] } = {}) {
|
|
78
138
|
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
79
139
|
this.index = this.readIndex();
|
|
140
|
+
this.reconcileProviderSessionIndex();
|
|
80
141
|
if (!Array.isArray(this.index.mainPageSessionKeys) || this.index.mainPageSessionKeys.length === 0) {
|
|
81
|
-
const migrated = this.
|
|
142
|
+
const migrated = this.filterRecoverableMainPageSessionKeys(legacyMainPageSessionKeys);
|
|
82
143
|
if (migrated.length > 0) {
|
|
83
144
|
this.setMainPageSessionKeys(migrated);
|
|
84
145
|
return;
|
|
85
146
|
}
|
|
86
147
|
}
|
|
87
|
-
this.index.mainPageSessionKeys = this.
|
|
148
|
+
this.index.mainPageSessionKeys = this.filterRecoverableMainPageSessionKeys(
|
|
149
|
+
this.index.mainPageSessionKeys,
|
|
150
|
+
);
|
|
88
151
|
this.writeIndex();
|
|
89
152
|
}
|
|
90
153
|
|
|
154
|
+
reconcileProviderSessionIndex() {
|
|
155
|
+
const index = this.ensureIndex();
|
|
156
|
+
const recordsByProviderKey = new Map();
|
|
157
|
+
const records = this.listStoredAgentRecords();
|
|
158
|
+
const successors = this.legacySuccessors(records);
|
|
159
|
+
for (const record of records) {
|
|
160
|
+
if (successors.has(record.id)) continue;
|
|
161
|
+
const sessionKey = String(record?.providerSessionKey || '');
|
|
162
|
+
if (!record || record.kind !== 'agent' || !parseProviderSessionKey(sessionKey)) continue;
|
|
163
|
+
const matches = recordsByProviderKey.get(sessionKey) || [];
|
|
164
|
+
matches.push(record);
|
|
165
|
+
recordsByProviderKey.set(sessionKey, matches);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const [sessionKey, records] of recordsByProviderKey) {
|
|
169
|
+
const indexedId = index.providerSessionRecords[sessionKey];
|
|
170
|
+
const indexedRecord = indexedId ? records.find(record => record.id === indexedId) : null;
|
|
171
|
+
if (records.length > 1) {
|
|
172
|
+
const ids = records.map(record => record.id).sort().join(', ');
|
|
173
|
+
throw new Error(`Conflicting Farming session records for ${sessionKey}: ${ids}`);
|
|
174
|
+
}
|
|
175
|
+
if (!indexedRecord || isLegacySessionId(indexedId)) {
|
|
176
|
+
index.providerSessionRecords[sessionKey] = records[0].id;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
filterRecoverableMainPageSessionKeys(keys) {
|
|
182
|
+
return this.normalizeMainPageSessionKeys(keys).filter(sessionKey => {
|
|
183
|
+
const record = this.getRecordForProviderSessionKey(sessionKey);
|
|
184
|
+
if (!record) return true;
|
|
185
|
+
if (record.archived === true) return false;
|
|
186
|
+
const entries = lifecycleJournal(record).entries;
|
|
187
|
+
const latest = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
188
|
+
return !(
|
|
189
|
+
latest
|
|
190
|
+
&& ['delete', 'archive'].includes(latest.type)
|
|
191
|
+
&& latest.state === 'succeeded'
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
91
196
|
readIndex() {
|
|
92
197
|
try {
|
|
93
198
|
if (fs.existsSync(this.indexFile)) {
|
|
@@ -135,7 +240,11 @@ class FarmingSessionStore {
|
|
|
135
240
|
return fileName ? path.join(this.sessionsDir, fileName) : '';
|
|
136
241
|
}
|
|
137
242
|
|
|
138
|
-
|
|
243
|
+
agentStateFile(id) {
|
|
244
|
+
return isAgentRecordId(id) ? storageLayout.agentStateFile(this.configDir, id) : '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
readMetadataRecord(id) {
|
|
139
248
|
const file = this.sessionFile(id);
|
|
140
249
|
if (!file) return null;
|
|
141
250
|
try {
|
|
@@ -147,11 +256,122 @@ class FarmingSessionStore {
|
|
|
147
256
|
}
|
|
148
257
|
}
|
|
149
258
|
|
|
259
|
+
readAgentState(id) {
|
|
260
|
+
const file = this.agentStateFile(id);
|
|
261
|
+
if (!file) return null;
|
|
262
|
+
try {
|
|
263
|
+
if (!fs.existsSync(file)) return null;
|
|
264
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
265
|
+
if (
|
|
266
|
+
!parsed
|
|
267
|
+
|| typeof parsed !== 'object'
|
|
268
|
+
|| parsed.agentRecordId !== id
|
|
269
|
+
|| parsed.agentStateVersion !== AGENT_STATE_VERSION
|
|
270
|
+
) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return parsed;
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
readRecord(id) {
|
|
280
|
+
const metadata = this.readMetadataRecord(id);
|
|
281
|
+
if (!metadata) return null;
|
|
282
|
+
if (!isAgentRecordId(id)) return metadata;
|
|
283
|
+
const state = this.readAgentState(id);
|
|
284
|
+
if (!state) return { ...metadata, agentRecordId: id };
|
|
285
|
+
const merged = { ...metadata, agentRecordId: id };
|
|
286
|
+
AGENT_STATE_FIELDS.forEach(field => {
|
|
287
|
+
if (Object.prototype.hasOwnProperty.call(state, field)) merged[field] = cloneJson(state[field]);
|
|
288
|
+
});
|
|
289
|
+
return merged;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
splitAgentRecord(record, id) {
|
|
293
|
+
const metadata = {};
|
|
294
|
+
const state = {
|
|
295
|
+
agentStateVersion: AGENT_STATE_VERSION,
|
|
296
|
+
agentRecordId: id,
|
|
297
|
+
kind: 'agent-state',
|
|
298
|
+
};
|
|
299
|
+
Object.entries(record || {}).forEach(([field, value]) => {
|
|
300
|
+
if (AGENT_STATE_FIELD_SET.has(field)) state[field] = cloneJson(value);
|
|
301
|
+
else metadata[field] = cloneJson(value);
|
|
302
|
+
});
|
|
303
|
+
metadata.id = id;
|
|
304
|
+
metadata.agentRecordId = id;
|
|
305
|
+
metadata.kind = 'agent';
|
|
306
|
+
metadata.recordVersion = AGENT_RECORD_VERSION;
|
|
307
|
+
return { metadata, state };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
promoteIndexRecordId(previousId, nextId) {
|
|
311
|
+
if (!this.index || !previousId || previousId === nextId) return;
|
|
312
|
+
Object.entries(this.index.providerSessionRecords).forEach(([key, id]) => {
|
|
313
|
+
if (id === previousId) this.index.providerSessionRecords[key] = nextId;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
150
317
|
writeRecord(record) {
|
|
151
318
|
if (!record || !safeSessionFileName(record.id)) return '';
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return
|
|
319
|
+
const previousId = record.id;
|
|
320
|
+
const id = isLegacySessionId(previousId) ? createAgentRecordId() : previousId;
|
|
321
|
+
if (!isAgentRecordId(id)) return '';
|
|
322
|
+
const promoted = id !== previousId;
|
|
323
|
+
const nextRecord = {
|
|
324
|
+
...record,
|
|
325
|
+
id,
|
|
326
|
+
agentRecordId: id,
|
|
327
|
+
...(promoted ? { legacyRecordId: previousId } : {}),
|
|
328
|
+
};
|
|
329
|
+
const { metadata, state } = this.splitAgentRecord(nextRecord, id);
|
|
330
|
+
const existingMetadata = this.readMetadataRecord(id);
|
|
331
|
+
const existingState = this.readAgentState(id);
|
|
332
|
+
const metadataChanged = !existingMetadata
|
|
333
|
+
|| !sameJson(withoutUpdatedAt(existingMetadata), withoutUpdatedAt(metadata));
|
|
334
|
+
const stateChanged = !existingState
|
|
335
|
+
|| !sameJson(withoutUpdatedAt(existingState), withoutUpdatedAt(state));
|
|
336
|
+
if (stateChanged) {
|
|
337
|
+
state.updatedAt = now();
|
|
338
|
+
atomicWriteJson(this.agentStateFile(id), state);
|
|
339
|
+
}
|
|
340
|
+
if (metadataChanged) {
|
|
341
|
+
metadata.updatedAt = now();
|
|
342
|
+
atomicWriteJson(this.sessionFile(id), metadata);
|
|
343
|
+
}
|
|
344
|
+
if (promoted) this.promoteIndexRecordId(previousId, id);
|
|
345
|
+
return id;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
listStoredAgentRecords() {
|
|
349
|
+
let names = [];
|
|
350
|
+
try {
|
|
351
|
+
names = fs.readdirSync(this.sessionsDir);
|
|
352
|
+
} catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
return names
|
|
356
|
+
.filter(name => name.endsWith('.json') && !name.endsWith('.state.json'))
|
|
357
|
+
.map(name => name.slice(0, -5))
|
|
358
|
+
.filter(id => safeSessionFileName(id))
|
|
359
|
+
.map(id => this.readRecord(id))
|
|
360
|
+
.filter(record => record && record.kind === 'agent');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
legacySuccessors(records) {
|
|
364
|
+
const successors = new Map();
|
|
365
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
366
|
+
const legacyId = String(record?.legacyRecordId || '').trim();
|
|
367
|
+
if (!isLegacySessionId(legacyId)) continue;
|
|
368
|
+
const previous = successors.get(legacyId);
|
|
369
|
+
if (previous && previous !== record.id) {
|
|
370
|
+
throw new Error(`Conflicting Agent record successors for ${legacyId}: ${previous}, ${record.id}`);
|
|
371
|
+
}
|
|
372
|
+
successors.set(legacyId, record.id);
|
|
373
|
+
}
|
|
374
|
+
return successors;
|
|
155
375
|
}
|
|
156
376
|
|
|
157
377
|
providerSessionKeyForAgent(agent) {
|
|
@@ -188,6 +408,15 @@ class FarmingSessionStore {
|
|
|
188
408
|
providerSessionTitle: typeof agent.providerSessionTitle === 'string' ? agent.providerSessionTitle : '',
|
|
189
409
|
providerSessionWorkspace: typeof agent.providerSessionWorkspace === 'string' ? agent.providerSessionWorkspace : '',
|
|
190
410
|
terminalInputReceived: agent.terminalInputReceived === true,
|
|
411
|
+
structuredRuntimeProcess: agent.structuredRuntimeProcess
|
|
412
|
+
&& typeof agent.structuredRuntimeProcess === 'object'
|
|
413
|
+
? JSON.parse(JSON.stringify(agent.structuredRuntimeProcess))
|
|
414
|
+
: null,
|
|
415
|
+
legacyAcpProcessExitAcknowledgedAt:
|
|
416
|
+
typeof agent.legacyAcpProcessExitAcknowledgedAt === 'number'
|
|
417
|
+
? agent.legacyAcpProcessExitAcknowledgedAt
|
|
418
|
+
: null,
|
|
419
|
+
...(agent.lifecycleJournal ? { lifecycleJournal: lifecycleJournal(agent) } : {}),
|
|
191
420
|
...legacyRuntimeMetadata(agent),
|
|
192
421
|
engine: typeof agent.engineName === 'string' ? agent.engineName : '',
|
|
193
422
|
category: typeof agent.category === 'string' ? agent.category : '',
|
|
@@ -217,7 +446,6 @@ class FarmingSessionStore {
|
|
|
217
446
|
? agent.providerSessionTitle
|
|
218
447
|
: (typeof agent.sessionTitle === 'string' ? agent.sessionTitle : '')),
|
|
219
448
|
startedAt: typeof agent.startedAt === 'number' ? agent.startedAt : null,
|
|
220
|
-
lastSeenAt: now(),
|
|
221
449
|
};
|
|
222
450
|
}
|
|
223
451
|
|
|
@@ -226,9 +454,21 @@ class FarmingSessionStore {
|
|
|
226
454
|
if (!parsed) return '';
|
|
227
455
|
const index = this.ensureIndex();
|
|
228
456
|
const existingId = index.providerSessionRecords[sessionKey];
|
|
457
|
+
const normalizedPreferredId = safeSessionFileName(preferredId) ? preferredId : '';
|
|
458
|
+
const preferredRecord = normalizedPreferredId ? this.readRecord(normalizedPreferredId) : null;
|
|
459
|
+
if (
|
|
460
|
+
!safeSessionFileName(existingId)
|
|
461
|
+
&& preferredRecord?.providerSessionTemporary !== true
|
|
462
|
+
&& preferredRecord?.providerSessionKey
|
|
463
|
+
&& preferredRecord.providerSessionKey !== sessionKey
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Farming session ${normalizedPreferredId} is already bound to ${preferredRecord.providerSessionKey}`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
229
469
|
const id = safeSessionFileName(existingId)
|
|
230
470
|
? existingId
|
|
231
|
-
: (
|
|
471
|
+
: (normalizedPreferredId || createAgentRecordId());
|
|
232
472
|
const existing = this.readRecord(id) || {};
|
|
233
473
|
const record = {
|
|
234
474
|
id,
|
|
@@ -248,17 +488,21 @@ class FarmingSessionStore {
|
|
|
248
488
|
if (typeof record.customTitle === 'string' && record.customTitle) {
|
|
249
489
|
record.title = record.customTitle;
|
|
250
490
|
}
|
|
251
|
-
index.providerSessionRecords[
|
|
252
|
-
|
|
491
|
+
Object.entries(index.providerSessionRecords).forEach(([key, recordId]) => {
|
|
492
|
+
if (key !== sessionKey && recordId === id) delete index.providerSessionRecords[key];
|
|
493
|
+
});
|
|
494
|
+
const writtenId = this.writeRecord(record);
|
|
495
|
+
index.providerSessionRecords[sessionKey] = writtenId;
|
|
253
496
|
this.writeIndex();
|
|
254
|
-
return
|
|
497
|
+
return writtenId;
|
|
255
498
|
}
|
|
256
499
|
|
|
257
500
|
ensureRecordForAgent(agent, patch = {}) {
|
|
258
501
|
const providerSessionKey = this.providerSessionKeyForAgent(agent);
|
|
259
502
|
if (providerSessionKey) {
|
|
260
|
-
const
|
|
261
|
-
|
|
503
|
+
const preferredId = agentRecordIdFor(agent);
|
|
504
|
+
const previousId = safeSessionFileName(preferredId)
|
|
505
|
+
? preferredId
|
|
262
506
|
: '';
|
|
263
507
|
const previous = previousId ? this.readRecord(previousId) : null;
|
|
264
508
|
const canonical = this.getRecordForProviderSessionKey(providerSessionKey);
|
|
@@ -275,10 +519,15 @@ class FarmingSessionStore {
|
|
|
275
519
|
const id = this.ensureRecordForProviderSessionKey(providerSessionKey, {
|
|
276
520
|
...agentPatch,
|
|
277
521
|
...patch,
|
|
278
|
-
},
|
|
522
|
+
}, preferredId || '');
|
|
279
523
|
if (previousId && previousId !== id) {
|
|
280
|
-
|
|
281
|
-
|
|
524
|
+
const promotedFromPrevious = this.readRecord(id)?.legacyRecordId === previousId;
|
|
525
|
+
if (
|
|
526
|
+
!promotedFromPrevious
|
|
527
|
+
&& previous
|
|
528
|
+
&& (previous.providerSessionTemporary === true || !previous.providerSessionKey)
|
|
529
|
+
) {
|
|
530
|
+
const mergedId = this.writeRecord({
|
|
282
531
|
...previous,
|
|
283
532
|
runtimeAgentId: '',
|
|
284
533
|
visibleOnMainPage: false,
|
|
@@ -287,13 +536,15 @@ class FarmingSessionStore {
|
|
|
287
536
|
mergedInto: id,
|
|
288
537
|
updatedAt: now(),
|
|
289
538
|
});
|
|
539
|
+
if (mergedId && mergedId !== previousId) this.writeIndex();
|
|
290
540
|
}
|
|
291
541
|
}
|
|
292
542
|
return id;
|
|
293
543
|
}
|
|
294
544
|
|
|
295
|
-
const
|
|
296
|
-
const
|
|
545
|
+
const preferredId = agentRecordIdFor(agent);
|
|
546
|
+
const existingId = safeSessionFileName(preferredId) ? preferredId : '';
|
|
547
|
+
const id = existingId || createAgentRecordId();
|
|
297
548
|
const existing = this.readRecord(id) || {};
|
|
298
549
|
const record = {
|
|
299
550
|
id,
|
|
@@ -306,8 +557,7 @@ class FarmingSessionStore {
|
|
|
306
557
|
...patch,
|
|
307
558
|
updatedAt: now(),
|
|
308
559
|
};
|
|
309
|
-
this.writeRecord(record);
|
|
310
|
-
return id;
|
|
560
|
+
return this.writeRecord(record);
|
|
311
561
|
}
|
|
312
562
|
|
|
313
563
|
setProviderSessionDisplayState(sessionKey, patch = {}) {
|
|
@@ -358,11 +608,12 @@ class FarmingSessionStore {
|
|
|
358
608
|
const id = index.providerSessionRecords[key];
|
|
359
609
|
const existing = this.readRecord(id);
|
|
360
610
|
if (existing) {
|
|
361
|
-
this.writeRecord({
|
|
611
|
+
const writtenId = this.writeRecord({
|
|
362
612
|
...existing,
|
|
363
613
|
visibleOnMainPage: false,
|
|
364
614
|
updatedAt: now(),
|
|
365
615
|
});
|
|
616
|
+
if (writtenId) index.providerSessionRecords[key] = writtenId;
|
|
366
617
|
}
|
|
367
618
|
});
|
|
368
619
|
index.mainPageSessionKeys = normalized;
|
|
@@ -377,11 +628,12 @@ class FarmingSessionStore {
|
|
|
377
628
|
const id = index.providerSessionRecords[sessionKey];
|
|
378
629
|
const existing = this.readRecord(id);
|
|
379
630
|
if (existing) {
|
|
380
|
-
this.writeRecord({
|
|
631
|
+
const writtenId = this.writeRecord({
|
|
381
632
|
...existing,
|
|
382
633
|
visibleOnMainPage: false,
|
|
383
634
|
updatedAt: now(),
|
|
384
635
|
});
|
|
636
|
+
if (writtenId) index.providerSessionRecords[sessionKey] = writtenId;
|
|
385
637
|
}
|
|
386
638
|
this.writeIndex();
|
|
387
639
|
return true;
|
|
@@ -407,20 +659,15 @@ class FarmingSessionStore {
|
|
|
407
659
|
|
|
408
660
|
listAgentRecords() {
|
|
409
661
|
this.ensureIndex();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} catch {
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
416
|
-
return names
|
|
417
|
-
.filter(name => name.endsWith('.json') && safeSessionFileName(name.slice(0, -5)))
|
|
418
|
-
.map(name => this.readRecord(name.slice(0, -5)))
|
|
419
|
-
.filter(record => record && record.kind === 'agent');
|
|
662
|
+
const records = this.listStoredAgentRecords();
|
|
663
|
+
const successors = this.legacySuccessors(records);
|
|
664
|
+
return records.filter(record => !successors.has(record.id));
|
|
420
665
|
}
|
|
421
666
|
}
|
|
422
667
|
|
|
423
668
|
module.exports = {
|
|
669
|
+
AGENT_RECORD_VERSION,
|
|
670
|
+
AGENT_STATE_VERSION,
|
|
424
671
|
FarmingSessionStore,
|
|
425
672
|
MAX_MAIN_PAGE_SESSION_KEYS,
|
|
426
673
|
parseProviderSessionKey,
|
|
@@ -3,6 +3,48 @@ const { spawn } = require('child_process');
|
|
|
3
3
|
const { AgentJsonStreamParser } = require('./agent-json-stream');
|
|
4
4
|
|
|
5
5
|
const MAX_EVENTS = 12_000;
|
|
6
|
+
const PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
7
|
+
const PROCESS_KILL_TIMEOUT_MS = 1000;
|
|
8
|
+
|
|
9
|
+
function childHasExited(child) {
|
|
10
|
+
return !child
|
|
11
|
+
|| (child.exitCode !== null && child.exitCode !== undefined)
|
|
12
|
+
|| (child.signalCode !== null && child.signalCode !== undefined);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function processTreeHasExited(child, ownsProcessGroup) {
|
|
16
|
+
if (!ownsProcessGroup || !child?.pid || process.platform === 'win32') {
|
|
17
|
+
return childHasExited(child);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
process.kill(-child.pid, 0);
|
|
21
|
+
return false;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return error?.code === 'ESRCH';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function waitForProcessTreeExit(child, ownsProcessGroup, timeoutMs) {
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
while (Date.now() <= deadline) {
|
|
30
|
+
if (processTreeHasExited(child, ownsProcessGroup)) return true;
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
32
|
+
}
|
|
33
|
+
return processTreeHasExited(child, ownsProcessGroup);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function signalProcessTree(child, ownsProcessGroup, signal) {
|
|
37
|
+
if (!child || processTreeHasExited(child, ownsProcessGroup)) return;
|
|
38
|
+
if (ownsProcessGroup && child.pid && process.platform !== 'win32') {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(-child.pid, signal);
|
|
41
|
+
return;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === 'ESRCH') return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
child.kill(signal);
|
|
47
|
+
}
|
|
6
48
|
|
|
7
49
|
function codexPermissionArgs(mode) {
|
|
8
50
|
if (mode === 'full') return ['--dangerously-bypass-approvals-and-sandbox'];
|
|
@@ -35,13 +77,23 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
35
77
|
constructor(options = {}) {
|
|
36
78
|
super();
|
|
37
79
|
this.spawn = options.spawn || spawn;
|
|
80
|
+
this.processExitTimeoutMs = options.processExitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS;
|
|
81
|
+
this.processKillTimeoutMs = options.processKillTimeoutMs ?? PROCESS_KILL_TIMEOUT_MS;
|
|
82
|
+
this.ownsProcessGroups = options.ownsProcessGroups ?? process.platform !== 'win32';
|
|
38
83
|
this.bindings = new Map();
|
|
84
|
+
this.disposing = false;
|
|
85
|
+
this.disposePromise = null;
|
|
86
|
+
this.disposed = false;
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
registerAgent(options) {
|
|
90
|
+
if (this.disposing || this.disposed) {
|
|
91
|
+
throw new Error('JSON CLI runtime is shutting down');
|
|
92
|
+
}
|
|
42
93
|
const binding = {
|
|
43
94
|
...options,
|
|
44
95
|
events: Array.isArray(options.initialEvents) ? [...options.initialEvents].slice(-MAX_EVENTS) : [],
|
|
96
|
+
ownsProcessGroup: this.ownsProcessGroups,
|
|
45
97
|
child: null,
|
|
46
98
|
operationSeq: 0,
|
|
47
99
|
state: 'idle',
|
|
@@ -53,11 +105,31 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
53
105
|
|
|
54
106
|
unregisterAgent(agentId) {
|
|
55
107
|
const binding = this.bindings.get(agentId);
|
|
56
|
-
if (binding?.child
|
|
108
|
+
if (binding?.child) signalProcessTree(binding.child, binding.ownsProcessGroup, 'SIGTERM');
|
|
57
109
|
this.bindings.delete(agentId);
|
|
58
110
|
}
|
|
59
111
|
|
|
112
|
+
async unregisterAgentAndWait(agentId) {
|
|
113
|
+
const binding = this.bindings.get(agentId);
|
|
114
|
+
if (!binding) return false;
|
|
115
|
+
const child = binding.child;
|
|
116
|
+
if (child && !processTreeHasExited(child, binding.ownsProcessGroup)) {
|
|
117
|
+
signalProcessTree(child, binding.ownsProcessGroup, 'SIGTERM');
|
|
118
|
+
if (!await waitForProcessTreeExit(child, binding.ownsProcessGroup, this.processExitTimeoutMs)) {
|
|
119
|
+
signalProcessTree(child, binding.ownsProcessGroup, 'SIGKILL');
|
|
120
|
+
if (!await waitForProcessTreeExit(child, binding.ownsProcessGroup, this.processKillTimeoutMs)) {
|
|
121
|
+
throw new Error(`JSON Agent process tree ${child.pid || ''} did not exit`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (this.bindings.get(agentId) === binding) this.bindings.delete(agentId);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
60
129
|
async submitComposerMessage(agentId, message, patch = {}) {
|
|
130
|
+
if (this.disposing || this.disposed) {
|
|
131
|
+
throw new Error('JSON CLI runtime is shutting down');
|
|
132
|
+
}
|
|
61
133
|
const binding = this.bindings.get(agentId);
|
|
62
134
|
if (!binding) throw new Error('JSON CLI Agent is not registered');
|
|
63
135
|
if (binding.child) throw new Error('Agent is already working');
|
|
@@ -75,6 +147,7 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
75
147
|
cwd: binding.cwd,
|
|
76
148
|
env: binding.env,
|
|
77
149
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
|
+
detached: binding.ownsProcessGroup,
|
|
78
151
|
});
|
|
79
152
|
binding.child = child;
|
|
80
153
|
this.emitRuntime(binding);
|
|
@@ -89,7 +162,7 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
89
162
|
stderr = `${stderr}${chunk.toString('utf8')}`.slice(-8000);
|
|
90
163
|
});
|
|
91
164
|
child.on('error', error => {
|
|
92
|
-
binding.child = null;
|
|
165
|
+
if (!child.pid) binding.child = null;
|
|
93
166
|
binding.state = 'error';
|
|
94
167
|
binding.error = error.message;
|
|
95
168
|
this.emitRuntime(binding);
|
|
@@ -100,12 +173,19 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
100
173
|
binding.events.push(...parser.events);
|
|
101
174
|
if (binding.events.length > MAX_EVENTS) binding.events.splice(0, binding.events.length - MAX_EVENTS);
|
|
102
175
|
if (parser.sessionId) binding.sessionId = parser.sessionId;
|
|
103
|
-
|
|
104
|
-
binding.
|
|
105
|
-
binding.
|
|
176
|
+
const processTreeExited = processTreeHasExited(child, binding.ownsProcessGroup);
|
|
177
|
+
if (processTreeExited) binding.child = null;
|
|
178
|
+
binding.state = code === 0 && processTreeExited ? 'idle' : 'error';
|
|
179
|
+
binding.error = code === 0 && processTreeExited
|
|
180
|
+
? ''
|
|
181
|
+
: (
|
|
182
|
+
!processTreeExited
|
|
183
|
+
? 'JSON CLI root exited while its process tree remained live'
|
|
184
|
+
: (stderr.trim() || `JSON CLI exited with code ${code}${signal ? ` (${signal})` : ''}`)
|
|
185
|
+
);
|
|
106
186
|
this.emit('transcript', { agentId, transcript: this.getTranscript(agentId) });
|
|
107
187
|
this.emitRuntime(binding);
|
|
108
|
-
if (code === 0) resolve({ sessionId: binding.sessionId });
|
|
188
|
+
if (code === 0 && processTreeExited) resolve({ sessionId: binding.sessionId });
|
|
109
189
|
else reject(new Error(binding.error));
|
|
110
190
|
});
|
|
111
191
|
if (launch.stdin) child.stdin.end(launch.stdin);
|
|
@@ -148,6 +228,40 @@ class JsonCliRuntime extends EventEmitter {
|
|
|
148
228
|
};
|
|
149
229
|
}
|
|
150
230
|
|
|
231
|
+
dispose() {
|
|
232
|
+
if (this.disposed) return Promise.resolve();
|
|
233
|
+
if (this.disposePromise) return this.disposePromise;
|
|
234
|
+
|
|
235
|
+
this.disposing = true;
|
|
236
|
+
const disposePromise = this.performDispose();
|
|
237
|
+
this.disposePromise = disposePromise;
|
|
238
|
+
void disposePromise.finally(() => {
|
|
239
|
+
if (this.disposePromise === disposePromise) this.disposePromise = null;
|
|
240
|
+
if (!this.disposed) this.disposing = false;
|
|
241
|
+
}).catch(() => {});
|
|
242
|
+
return disposePromise;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
resumeAfterDisposeAbort() {
|
|
246
|
+
this.disposed = false;
|
|
247
|
+
this.disposing = false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async performDispose() {
|
|
251
|
+
const failures = [];
|
|
252
|
+
for (const agentId of [...this.bindings.keys()]) {
|
|
253
|
+
try {
|
|
254
|
+
await this.unregisterAgentAndWait(agentId);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
failures.push(error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (failures.length > 0) {
|
|
260
|
+
throw new AggregateError(failures, 'One or more JSON Agent process trees did not exit');
|
|
261
|
+
}
|
|
262
|
+
this.disposed = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
151
265
|
emitRuntime(binding) {
|
|
152
266
|
this.emit('agent-runtime', {
|
|
153
267
|
agentId: binding.agentId,
|