farming-code 2.2.17 → 2.2.19

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.
@@ -222,13 +222,14 @@ class ConfigManager {
222
222
  if (!expanded) continue;
223
223
  const resolved = path.resolve(expanded);
224
224
  if (resolved === path.parse(resolved).root || this.isInternalWorkspace(resolved)) continue;
225
- let canonical;
225
+ let canonical = resolved;
226
226
  try {
227
227
  canonical = fs.realpathSync(resolved);
228
- if (!fs.statSync(canonical).isDirectory()) continue;
229
228
  } catch {
230
- continue;
229
+ // Project membership is durable. A temporarily unavailable path remains
230
+ // until the user explicitly removes it.
231
231
  }
232
+ if (this.isInternalWorkspace(canonical)) continue;
232
233
  if (seen.has(canonical)) continue;
233
234
  seen.add(canonical);
234
235
  result.push(canonical);
@@ -752,6 +753,97 @@ class ConfigManager {
752
753
  };
753
754
  }
754
755
 
756
+ mountProjectWorkspace(workspace) {
757
+ const expanded = this.expandWorkspacePath(workspace);
758
+ const resolved = expanded ? path.resolve(expanded) : '';
759
+ let canonicalWorkspace = '';
760
+ if (resolved) {
761
+ try {
762
+ canonicalWorkspace = fs.realpathSync(resolved);
763
+ if (!fs.statSync(canonicalWorkspace).isDirectory()) canonicalWorkspace = '';
764
+ } catch {
765
+ canonicalWorkspace = '';
766
+ }
767
+ }
768
+ if (
769
+ !canonicalWorkspace
770
+ || canonicalWorkspace === path.parse(canonicalWorkspace).root
771
+ || this.isInternalWorkspace(canonicalWorkspace)
772
+ ) {
773
+ throw new Error('Project workspace is invalid or unavailable');
774
+ }
775
+ const current = this.settings.projectWorkspaces || [];
776
+ if (!current.includes(canonicalWorkspace)) {
777
+ this.commitProjectMembership({
778
+ projectWorkspaces: [canonicalWorkspace, ...current],
779
+ });
780
+ }
781
+ return {
782
+ workspace: canonicalWorkspace,
783
+ projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
784
+ pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
785
+ };
786
+ }
787
+
788
+ removeProjectWorkspace(workspace) {
789
+ const expanded = this.expandWorkspacePath(workspace);
790
+ if (!expanded) throw new Error('Project workspace is required');
791
+ const resolved = path.resolve(expanded);
792
+ const candidates = new Set([expanded, resolved]);
793
+ try {
794
+ candidates.add(fs.realpathSync(resolved));
795
+ } catch {
796
+ // A deleted worktree can still be removed by its last canonical path.
797
+ }
798
+ const current = this.settings.projectWorkspaces || [];
799
+ const nextProjects = current.filter(entry => !candidates.has(entry));
800
+ const currentPinned = this.settings.pinnedProjectWorkspaces || [];
801
+ const nextPinned = currentPinned.filter(entry => !candidates.has(entry));
802
+ if (nextProjects.length !== current.length || nextPinned.length !== currentPinned.length) {
803
+ this.commitProjectMembership({
804
+ projectWorkspaces: nextProjects,
805
+ pinnedProjectWorkspaces: nextPinned,
806
+ });
807
+ }
808
+ return {
809
+ workspace: current.find(entry => candidates.has(entry)) || resolved,
810
+ projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
811
+ pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
812
+ };
813
+ }
814
+
815
+ setProjectWorkspacePinned(workspace, pinned) {
816
+ const [canonicalWorkspace] = this.normalizeProjectWorkspaces([workspace]);
817
+ if (!canonicalWorkspace || !(this.settings.projectWorkspaces || []).includes(canonicalWorkspace)) {
818
+ throw new Error('Project does not exist');
819
+ }
820
+ const current = this.settings.pinnedProjectWorkspaces || [];
821
+ const nextPinned = pinned
822
+ ? [...current.filter(entry => entry !== canonicalWorkspace), canonicalWorkspace]
823
+ : current.filter(entry => entry !== canonicalWorkspace);
824
+ if (
825
+ nextPinned.length !== current.length
826
+ || nextPinned.some((entry, index) => entry !== current[index])
827
+ ) {
828
+ this.commitProjectMembership({ pinnedProjectWorkspaces: nextPinned });
829
+ }
830
+ return {
831
+ workspace: canonicalWorkspace,
832
+ projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
833
+ pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
834
+ };
835
+ }
836
+
837
+ commitProjectMembership(settingsPatch) {
838
+ const previousSettings = this.settings;
839
+ try {
840
+ this.updateSettings(settingsPatch);
841
+ } catch (error) {
842
+ this.settings = previousSettings;
843
+ throw error;
844
+ }
845
+ }
846
+
755
847
  getMainPageSessionKeys() {
756
848
  return this.sessionStore ? this.sessionStore.getMainPageSessionKeys() : [];
757
849
  }
@@ -776,6 +868,10 @@ class ConfigManager {
776
868
  return this.sessionStore ? this.sessionStore.ensureRecordForAgent(agent, patch) : '';
777
869
  }
778
870
 
871
+ getAgentSessionRecordForProviderSessionKey(sessionKey) {
872
+ return this.sessionStore ? this.sessionStore.getRecordForProviderSessionKey(sessionKey) : null;
873
+ }
874
+
779
875
  setProviderSessionDisplayState(sessionKey, patch = {}) {
780
876
  return this.sessionStore ? this.sessionStore.setProviderSessionDisplayState(sessionKey, patch) : '';
781
877
  }
@@ -794,7 +890,17 @@ class ConfigManager {
794
890
 
795
891
  writeSettingsFile() {
796
892
  fs.mkdirSync(this.farmingDir, { recursive: true });
797
- fs.writeFileSync(this.settingsFile, JSON.stringify(this.settings, null, 2));
893
+ const tempFile = `${this.settingsFile}.${process.pid}.${Date.now()}.tmp`;
894
+ try {
895
+ fs.writeFileSync(tempFile, JSON.stringify(this.settings, null, 2));
896
+ fs.renameSync(tempFile, this.settingsFile);
897
+ } finally {
898
+ try {
899
+ fs.unlinkSync(tempFile);
900
+ } catch {
901
+ // A successful rename already removed the temporary path.
902
+ }
903
+ }
798
904
  }
799
905
 
800
906
  appendTaskHistory(entry) {
@@ -6,6 +6,24 @@ const storageLayout = require('./storage-layout');
6
6
 
7
7
  const SESSION_ID_PREFIX = 'fsess';
8
8
  const MAX_MAIN_PAGE_SESSION_KEYS = 50;
9
+ const PRODUCT_STATE_FIELDS = [
10
+ 'projectWorkspace',
11
+ 'task',
12
+ 'workflowTemplate',
13
+ 'pinned',
14
+ 'projectOrder',
15
+ 'pinnedOrder',
16
+ 'attentionSeq',
17
+ 'readAttentionSeq',
18
+ 'attentionUpdatedAt',
19
+ 'readAttentionAt',
20
+ 'attentionReason',
21
+ 'attentionOutputEpoch',
22
+ 'attentionOutputSeq',
23
+ 'readOutputEpoch',
24
+ 'readOutputSeq',
25
+ 'customTitle',
26
+ ];
9
27
 
10
28
  function now() {
11
29
  return Date.now();
@@ -184,10 +202,15 @@ class FarmingSessionStore {
184
202
  attentionUpdatedAt: typeof agent.attentionUpdatedAt === 'number' ? agent.attentionUpdatedAt : null,
185
203
  readAttentionAt: typeof agent.readAttentionAt === 'number' ? agent.readAttentionAt : null,
186
204
  attentionReason: typeof agent.attentionReason === 'string' ? agent.attentionReason : '',
205
+ attentionOutputEpoch: typeof agent.attentionOutputEpoch === 'string' ? agent.attentionOutputEpoch : '',
187
206
  attentionOutputSeq: typeof agent.attentionOutputSeq === 'number' ? agent.attentionOutputSeq : null,
207
+ readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
208
+ readOutputSeq: typeof agent.readOutputSeq === 'number' ? agent.readOutputSeq : null,
188
209
  archived: agent.archived === true,
189
210
  archivedAt: typeof agent.archivedAt === 'number' ? agent.archivedAt : null,
190
- customTitle: typeof agent.customTitle === 'string' ? agent.customTitle : '',
211
+ ...(typeof agent.customTitle === 'string' && agent.customTitle
212
+ ? { customTitle: agent.customTitle }
213
+ : {}),
191
214
  title: typeof agent.customTitle === 'string' && agent.customTitle
192
215
  ? agent.customTitle
193
216
  : (typeof agent.providerSessionTitle === 'string' && agent.providerSessionTitle
@@ -222,6 +245,9 @@ class FarmingSessionStore {
222
245
  providerSessionTemporary: false,
223
246
  updatedAt: now(),
224
247
  };
248
+ if (typeof record.customTitle === 'string' && record.customTitle) {
249
+ record.title = record.customTitle;
250
+ }
225
251
  index.providerSessionRecords[sessionKey] = id;
226
252
  this.writeRecord(record);
227
253
  this.writeIndex();
@@ -231,10 +257,38 @@ class FarmingSessionStore {
231
257
  ensureRecordForAgent(agent, patch = {}) {
232
258
  const providerSessionKey = this.providerSessionKeyForAgent(agent);
233
259
  if (providerSessionKey) {
260
+ const previousId = safeSessionFileName(agent?.persistentSessionId)
261
+ ? agent.persistentSessionId
262
+ : '';
263
+ const previous = previousId ? this.readRecord(previousId) : null;
264
+ const canonical = this.getRecordForProviderSessionKey(providerSessionKey);
265
+ const agentPatch = this.recordPatchFromAgent(agent);
266
+ if (canonical && previousId && canonical.id !== previousId) {
267
+ PRODUCT_STATE_FIELDS.forEach(field => {
268
+ if (Object.prototype.hasOwnProperty.call(canonical, field)) {
269
+ agentPatch[field] = canonical[field];
270
+ } else if (previous && Object.prototype.hasOwnProperty.call(previous, field)) {
271
+ agentPatch[field] = previous[field];
272
+ }
273
+ });
274
+ }
234
275
  const id = this.ensureRecordForProviderSessionKey(providerSessionKey, {
235
- ...this.recordPatchFromAgent(agent),
276
+ ...agentPatch,
236
277
  ...patch,
237
278
  }, agent?.persistentSessionId || '');
279
+ if (previousId && previousId !== id) {
280
+ if (previous && (previous.providerSessionTemporary === true || !previous.providerSessionKey)) {
281
+ this.writeRecord({
282
+ ...previous,
283
+ runtimeAgentId: '',
284
+ visibleOnMainPage: false,
285
+ archived: true,
286
+ archivedAt: now(),
287
+ mergedInto: id,
288
+ updatedAt: now(),
289
+ });
290
+ }
291
+ }
238
292
  return id;
239
293
  }
240
294
 
@@ -345,6 +399,12 @@ class FarmingSessionStore {
345
399
  return this.ensureIndex().mainPageSessionKeys.slice();
346
400
  }
347
401
 
402
+ getRecordForProviderSessionKey(sessionKey) {
403
+ const index = this.ensureIndex();
404
+ const id = index.providerSessionRecords[sessionKey];
405
+ return safeSessionFileName(id) ? this.readRecord(id) : null;
406
+ }
407
+
348
408
  listAgentRecords() {
349
409
  this.ensureIndex();
350
410
  let names = [];