farming-code 2.2.7 → 2.2.11

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.
Files changed (102) hide show
  1. package/README.md +110 -199
  2. package/README.zh_cn.md +106 -237
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1496 -737
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +45 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-iA2BU9P6.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-DpYuBonn.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -0,0 +1,372 @@
1
+ (function attachTerminalReplay(global) {
2
+ const RUNTIME_EPOCH_PATTERN = /^farming-runtime-v1:(\d{20}):/;
3
+ const DEFAULT_MAX_QUEUED_TRANSITIONS = 512;
4
+ const DEFAULT_MAX_QUEUED_BYTES = 1024 * 1024;
5
+ const DEFAULT_RETRY_BASE_MS = 250;
6
+ const DEFAULT_RETRY_MAX_MS = 5000;
7
+ const DEFAULT_MAX_IDENTICAL_INVARIANT_FAILURES = 3;
8
+
9
+ function runtimeEpochGeneration(runtimeEpoch) {
10
+ const match = RUNTIME_EPOCH_PATTERN.exec(String(runtimeEpoch || ''));
11
+ if (!match) return null;
12
+ const generation = Number(match[1]);
13
+ return Number.isSafeInteger(generation) && generation > 0 ? generation : null;
14
+ }
15
+
16
+ function compareRuntimeEpochs(left, right) {
17
+ if (left === right) return 0;
18
+ const leftGeneration = runtimeEpochGeneration(left);
19
+ const rightGeneration = runtimeEpochGeneration(right);
20
+ if (leftGeneration === null || rightGeneration === null || leftGeneration === rightGeneration) {
21
+ return null;
22
+ }
23
+ return leftGeneration < rightGeneration ? -1 : 1;
24
+ }
25
+
26
+ function byteLength(value) {
27
+ const text = String(value || '');
28
+ if (typeof global.TextEncoder === 'function') {
29
+ return new global.TextEncoder().encode(text).byteLength;
30
+ }
31
+ return encodeURIComponent(text).replace(/%[0-9A-F]{2}/gi, 'x').length;
32
+ }
33
+
34
+ function createState(options = {}) {
35
+ return {
36
+ runtimeEpoch: '',
37
+ outputSeq: null,
38
+ stateRevision: null,
39
+ replayTargetEpoch: '',
40
+ replayTargetRevision: null,
41
+ recovering: false,
42
+ queuedTransitions: [],
43
+ queuedBytes: 0,
44
+ retiredRuntimeEpochs: new Set(),
45
+ failureCount: 0,
46
+ invariantFailureSignature: '',
47
+ invariantFailureCount: 0,
48
+ halted: false,
49
+ haltMessage: '',
50
+ maxQueuedTransitions: options.maxQueuedTransitions || DEFAULT_MAX_QUEUED_TRANSITIONS,
51
+ maxQueuedBytes: options.maxQueuedBytes || DEFAULT_MAX_QUEUED_BYTES,
52
+ retryBaseMs: options.retryBaseMs || DEFAULT_RETRY_BASE_MS,
53
+ retryMaxMs: options.retryMaxMs || DEFAULT_RETRY_MAX_MS,
54
+ maxIdenticalInvariantFailures:
55
+ options.maxIdenticalInvariantFailures || DEFAULT_MAX_IDENTICAL_INVARIANT_FAILURES,
56
+ };
57
+ }
58
+
59
+ function isTransitionValid(event) {
60
+ return Boolean(event && event.runtimeEpoch)
61
+ && Number.isFinite(event.outputSeq)
62
+ && Number.isFinite(event.stateRevision)
63
+ && (
64
+ event.kind !== 'resize'
65
+ || (Number.isFinite(event.cols) && Number.isFinite(event.rows))
66
+ );
67
+ }
68
+
69
+ function isCheckpointValid(checkpoint) {
70
+ return Boolean(checkpoint && checkpoint.runtimeEpoch)
71
+ && Number.isFinite(checkpoint.outputSeq)
72
+ && Number.isFinite(checkpoint.stateRevision)
73
+ && Number.isFinite(checkpoint.cols)
74
+ && Number.isFinite(checkpoint.rows)
75
+ && checkpoint.cols > 0
76
+ && checkpoint.rows > 0;
77
+ }
78
+
79
+ function noteReplayTarget(state, event) {
80
+ if (!event || !event.runtimeEpoch || !Number.isFinite(event.stateRevision)) return;
81
+ if (!state.replayTargetEpoch) {
82
+ state.replayTargetEpoch = event.runtimeEpoch;
83
+ state.replayTargetRevision = event.stateRevision;
84
+ return;
85
+ }
86
+ if (event.runtimeEpoch === state.replayTargetEpoch) {
87
+ state.replayTargetRevision = Math.max(state.replayTargetRevision || 0, event.stateRevision);
88
+ return;
89
+ }
90
+ const relation = compareRuntimeEpochs(event.runtimeEpoch, state.replayTargetEpoch);
91
+ if (relation === 1) {
92
+ state.replayTargetEpoch = event.runtimeEpoch;
93
+ state.replayTargetRevision = event.stateRevision;
94
+ }
95
+ }
96
+
97
+ function beginRecovery(state, event) {
98
+ state.recovering = true;
99
+ noteReplayTarget(state, event);
100
+ }
101
+
102
+ function isReplayTargetPending(state) {
103
+ if (!state.replayTargetEpoch || !Number.isFinite(state.replayTargetRevision)) return false;
104
+ if (!state.runtimeEpoch || !Number.isFinite(state.stateRevision)) return true;
105
+ if (state.runtimeEpoch === state.replayTargetEpoch) {
106
+ return state.stateRevision < state.replayTargetRevision;
107
+ }
108
+ return compareRuntimeEpochs(state.runtimeEpoch, state.replayTargetEpoch) !== 1;
109
+ }
110
+
111
+ function classifyTransition(state, event) {
112
+ if (!isTransitionValid(event)) {
113
+ beginRecovery(state, event);
114
+ return { action: 'recover', reason: 'invalid-transition' };
115
+ }
116
+ if (state.retiredRuntimeEpochs.has(event.runtimeEpoch)) {
117
+ return { action: 'drop', reason: 'retired-epoch' };
118
+ }
119
+ if (state.runtimeEpoch && event.runtimeEpoch !== state.runtimeEpoch) {
120
+ const relation = compareRuntimeEpochs(event.runtimeEpoch, state.runtimeEpoch);
121
+ if (relation === -1) return { action: 'drop', reason: 'older-epoch' };
122
+ beginRecovery(state, event);
123
+ return { action: 'recover', reason: 'epoch-change' };
124
+ }
125
+ if (!state.runtimeEpoch || !Number.isFinite(state.outputSeq) || !Number.isFinite(state.stateRevision)) {
126
+ beginRecovery(state, event);
127
+ return { action: 'recover', reason: 'missing-cursor' };
128
+ }
129
+ if (event.stateRevision <= state.stateRevision) {
130
+ return { action: 'drop', reason: 'duplicate' };
131
+ }
132
+
133
+ const outputAdvance = event.kind === 'output' ? 1 : 0;
134
+ if (
135
+ event.stateRevision !== state.stateRevision + 1
136
+ || event.outputSeq !== state.outputSeq + outputAdvance
137
+ ) {
138
+ beginRecovery(state, event);
139
+ return { action: 'recover', reason: 'sequence-gap' };
140
+ }
141
+ return { action: 'apply' };
142
+ }
143
+
144
+ function queueTransition(state, event) {
145
+ if (!isTransitionValid(event)) {
146
+ beginRecovery(state, event);
147
+ return { queued: false, overflow: false };
148
+ }
149
+ noteReplayTarget(state, event);
150
+ const bytes = byteLength(event.data);
151
+ if (
152
+ state.queuedTransitions.length >= state.maxQueuedTransitions
153
+ || state.queuedBytes + bytes > state.maxQueuedBytes
154
+ ) {
155
+ state.queuedTransitions = [];
156
+ state.queuedBytes = 0;
157
+ state.recovering = true;
158
+ return { queued: false, overflow: true };
159
+ }
160
+ state.queuedTransitions.push(event);
161
+ state.queuedBytes += bytes;
162
+ return { queued: true, overflow: false };
163
+ }
164
+
165
+ function takeQueuedTransition(state) {
166
+ const event = state.queuedTransitions.shift() || null;
167
+ if (event) state.queuedBytes = Math.max(0, state.queuedBytes - byteLength(event.data));
168
+ return event;
169
+ }
170
+
171
+ function clearQueuedTransitions(state) {
172
+ state.queuedTransitions = [];
173
+ state.queuedBytes = 0;
174
+ }
175
+
176
+ function checkpointInvariant(signature, message) {
177
+ return { action: 'reject', signature, message };
178
+ }
179
+
180
+ function queuedTransitionsCoverTarget(state, checkpoint) {
181
+ if (
182
+ checkpoint.runtimeEpoch !== state.replayTargetEpoch
183
+ || !Number.isFinite(state.replayTargetRevision)
184
+ ) return false;
185
+
186
+ let outputSeq = checkpoint.outputSeq;
187
+ let stateRevision = checkpoint.stateRevision;
188
+ for (const event of state.queuedTransitions) {
189
+ if (!isTransitionValid(event) || event.runtimeEpoch !== checkpoint.runtimeEpoch) return false;
190
+ if (event.stateRevision <= stateRevision) continue;
191
+ const outputAdvance = event.kind === 'output' ? 1 : 0;
192
+ if (
193
+ event.stateRevision !== stateRevision + 1
194
+ || event.outputSeq !== outputSeq + outputAdvance
195
+ ) return false;
196
+ outputSeq = event.outputSeq;
197
+ stateRevision = event.stateRevision;
198
+ if (stateRevision >= state.replayTargetRevision) return true;
199
+ }
200
+ return false;
201
+ }
202
+
203
+ function evaluateCheckpoint(state, checkpoint) {
204
+ if (!isCheckpointValid(checkpoint)) {
205
+ return checkpointInvariant('invalid-checkpoint', 'Terminal replay returned an invalid screen state');
206
+ }
207
+ if (state.runtimeEpoch && checkpoint.runtimeEpoch !== state.runtimeEpoch) {
208
+ const relation = compareRuntimeEpochs(checkpoint.runtimeEpoch, state.runtimeEpoch);
209
+ if (relation === -1 || state.retiredRuntimeEpochs.has(checkpoint.runtimeEpoch)) {
210
+ return checkpointInvariant(
211
+ `older-epoch:${checkpoint.runtimeEpoch}:${state.runtimeEpoch}`,
212
+ 'Terminal replay returned an older runtime epoch',
213
+ );
214
+ }
215
+ } else if (
216
+ checkpoint.runtimeEpoch === state.runtimeEpoch
217
+ && Number.isFinite(state.stateRevision)
218
+ && checkpoint.stateRevision < state.stateRevision
219
+ ) {
220
+ return checkpointInvariant(
221
+ `older-revision:${checkpoint.runtimeEpoch}:${checkpoint.stateRevision}:${state.stateRevision}`,
222
+ 'Terminal replay returned an older screen state',
223
+ );
224
+ }
225
+
226
+ if (state.replayTargetEpoch && Number.isFinite(state.replayTargetRevision)) {
227
+ if (checkpoint.runtimeEpoch === state.replayTargetEpoch) {
228
+ if (
229
+ checkpoint.stateRevision < state.replayTargetRevision
230
+ && !queuedTransitionsCoverTarget(state, checkpoint)
231
+ ) {
232
+ return checkpointInvariant(
233
+ `behind-target:${checkpoint.runtimeEpoch}:${checkpoint.stateRevision}:${state.replayTargetRevision}`,
234
+ 'Terminal replay did not reach the latest observed screen state',
235
+ );
236
+ }
237
+ } else if (compareRuntimeEpochs(checkpoint.runtimeEpoch, state.replayTargetEpoch) !== 1) {
238
+ return checkpointInvariant(
239
+ `wrong-target-epoch:${checkpoint.runtimeEpoch}:${state.replayTargetEpoch}`,
240
+ 'Terminal replay returned a different runtime epoch',
241
+ );
242
+ }
243
+ }
244
+
245
+ const current = checkpoint.runtimeEpoch === state.runtimeEpoch
246
+ && checkpoint.outputSeq === state.outputSeq
247
+ && checkpoint.stateRevision === state.stateRevision;
248
+ return { action: current ? 'current' : 'install' };
249
+ }
250
+
251
+ function removeCheckpointCoveredTransitions(state, checkpoint) {
252
+ state.queuedTransitions = state.queuedTransitions.filter((event) => {
253
+ if (!isTransitionValid(event)) return false;
254
+ if (state.retiredRuntimeEpochs.has(event.runtimeEpoch)) return false;
255
+ if (event.runtimeEpoch === checkpoint.runtimeEpoch) {
256
+ return event.stateRevision > checkpoint.stateRevision;
257
+ }
258
+ return compareRuntimeEpochs(event.runtimeEpoch, checkpoint.runtimeEpoch) !== -1;
259
+ });
260
+ state.queuedBytes = state.queuedTransitions.reduce(
261
+ (total, event) => total + byteLength(event.data),
262
+ 0,
263
+ );
264
+ }
265
+
266
+ function clearFailures(state) {
267
+ state.failureCount = 0;
268
+ state.invariantFailureSignature = '';
269
+ state.invariantFailureCount = 0;
270
+ state.halted = false;
271
+ state.haltMessage = '';
272
+ }
273
+
274
+ function commitCheckpoint(state, checkpoint) {
275
+ if (state.runtimeEpoch && state.runtimeEpoch !== checkpoint.runtimeEpoch) {
276
+ state.retiredRuntimeEpochs.add(state.runtimeEpoch);
277
+ while (state.retiredRuntimeEpochs.size > 4) {
278
+ state.retiredRuntimeEpochs.delete(state.retiredRuntimeEpochs.values().next().value);
279
+ }
280
+ }
281
+ state.runtimeEpoch = checkpoint.runtimeEpoch;
282
+ state.outputSeq = checkpoint.outputSeq;
283
+ state.stateRevision = checkpoint.stateRevision;
284
+ removeCheckpointCoveredTransitions(state, checkpoint);
285
+ clearFailures(state);
286
+ state.recovering = isReplayTargetPending(state);
287
+ if (state.recovering && queuedTransitionsCoverTarget(state, checkpoint)) {
288
+ state.recovering = false;
289
+ }
290
+ if (!state.recovering) {
291
+ state.replayTargetEpoch = '';
292
+ state.replayTargetRevision = null;
293
+ }
294
+ return !state.recovering;
295
+ }
296
+
297
+ function commitTransition(state, event) {
298
+ state.runtimeEpoch = event.runtimeEpoch;
299
+ state.outputSeq = event.outputSeq;
300
+ state.stateRevision = event.stateRevision;
301
+ if (!isReplayTargetPending(state)) {
302
+ state.replayTargetEpoch = '';
303
+ state.replayTargetRevision = null;
304
+ state.recovering = false;
305
+ }
306
+ }
307
+
308
+ function retryDelay(state) {
309
+ const exponent = Math.max(0, state.failureCount - 1);
310
+ return Math.min(state.retryMaxMs, state.retryBaseMs * (2 ** exponent));
311
+ }
312
+
313
+ function recordTransportFailure(state) {
314
+ state.failureCount += 1;
315
+ state.recovering = true;
316
+ return { halted: false, delay: retryDelay(state), message: '' };
317
+ }
318
+
319
+ function recordInvariantFailure(state, signature, message) {
320
+ state.failureCount += 1;
321
+ state.recovering = true;
322
+ if (state.invariantFailureSignature === signature) {
323
+ state.invariantFailureCount += 1;
324
+ } else {
325
+ state.invariantFailureSignature = signature;
326
+ state.invariantFailureCount = 1;
327
+ }
328
+ if (state.invariantFailureCount >= state.maxIdenticalInvariantFailures) {
329
+ state.halted = true;
330
+ state.haltMessage = message || 'Terminal replay could not prove a current screen state';
331
+ }
332
+ return {
333
+ halted: state.halted,
334
+ delay: state.halted ? 0 : retryDelay(state),
335
+ message: state.haltMessage,
336
+ };
337
+ }
338
+
339
+ function resetRecovery(state, options = {}) {
340
+ clearQueuedTransitions(state);
341
+ state.replayTargetEpoch = '';
342
+ state.replayTargetRevision = null;
343
+ state.recovering = false;
344
+ clearFailures(state);
345
+ if (options.keepCursor === false) {
346
+ state.runtimeEpoch = '';
347
+ state.outputSeq = null;
348
+ state.stateRevision = null;
349
+ state.retiredRuntimeEpochs.clear();
350
+ }
351
+ }
352
+
353
+ const api = {
354
+ createState,
355
+ compareRuntimeEpochs,
356
+ beginRecovery,
357
+ isReplayTargetPending,
358
+ classifyTransition,
359
+ queueTransition,
360
+ takeQueuedTransition,
361
+ clearQueuedTransitions,
362
+ evaluateCheckpoint,
363
+ commitCheckpoint,
364
+ commitTransition,
365
+ recordTransportFailure,
366
+ recordInvariantFailure,
367
+ resetRecovery,
368
+ };
369
+
370
+ global.FarmingTerminalReplay = api;
371
+ if (typeof module === 'object' && module.exports) module.exports = api;
372
+ })(typeof globalThis === 'object' ? globalThis : window);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "farming-code",
3
- "version": "2.2.7",
3
+ "version": "2.2.11",
4
4
  "description": "Remote browser workspace for supervising AI coding agents",
5
5
  "main": "backend/server.js",
6
6
  "bin": {
@@ -14,9 +14,11 @@
14
14
  "config/*.example",
15
15
  "dist/",
16
16
  "frontend/*.js",
17
+ "frontend/farming-net/",
17
18
  "frontend/skins/",
18
19
  "frontend/themes/",
19
20
  "frontend/vendor/ghostty-web/",
21
+ "shared/",
20
22
  "scripts/sync-ghostty-vendor.js",
21
23
  "LICENSE",
22
24
  "README.md",
@@ -30,6 +32,7 @@
30
32
  "postinstall": "node scripts/sync-ghostty-vendor.js",
31
33
  "prepack": "FARMING_BASE_PATH=/farming npm run build",
32
34
  "start": "npm run build && node backend/server.js",
35
+ "start:net": "node backend/farming-net-server.js",
33
36
  "start:no-auth": "FARMING_DISABLE_AUTH=1 npm start",
34
37
  "dev:backend": "node backend/server.js",
35
38
  "dev:frontend": "vite",
@@ -50,6 +53,10 @@
50
53
  "docs:product:screenshots": "node scripts/capture-product-screenshots.js",
51
54
  "test": "node scripts/run-tests.js",
52
55
  "test:codex-app-server:real": "FARMING_REAL_CODEX_APP_SERVER_SMOKE=1 node scripts/smoke-codex-app-server.js",
56
+ "test:pre-release:codex-ui": "FARMING_REAL_CODEX_BIN=\"$(command -v codex)\" FARMING_CODEX_BIN=\"$PWD/tests/e2e/fixtures/real-codex-no-update\" FARMING_REAL_CODEX_RELEASE_CASE=1 FARMING_E2E_REAL_CODEX=1 FARMING_PLAYWRIGHT_INTERNAL=1 playwright test tests/e2e/internal/real-codex-release-case.spec.ts --project=chromium --workers=1 --retries=0 --trace=retain-on-failure",
57
+ "test:pre-release:terminal-input": "playwright test tests/e2e/terminal-input-performance.spec.ts --project=chromium --workers=1 --retries=0 --trace=retain-on-failure",
58
+ "test:performance:audit": "node tests/performance/agent-usage-hotpath.js && FARMING_PLAYWRIGHT_INTERNAL=1 playwright test tests/e2e/internal/performance-bottlenecks.spec.ts --project=chromium --workers=1 --retries=0 --trace=retain-on-failure",
59
+ "test:performance:scale": "FARMING_PLAYWRIGHT_INTERNAL=1 playwright test tests/e2e/internal/performance-scaling.spec.ts --project=chromium --workers=1 --retries=0 --trace=retain-on-failure",
53
60
  "test:update:platforms": "node scripts/test-update-platforms.js",
54
61
  "test:e2e": "npm run test:e2e:playwright",
55
62
  "test:e2e:playwright": "playwright test",
@@ -89,8 +96,8 @@
89
96
  "node": ">=22"
90
97
  },
91
98
  "dependencies": {
92
- "@agentclientprotocol/claude-agent-acp": "0.58.1",
93
- "@agentclientprotocol/codex-acp": "1.1.2",
99
+ "@agentclientprotocol/claude-agent-acp": "0.59.0",
100
+ "@agentclientprotocol/codex-acp": "1.1.4",
94
101
  "@agentclientprotocol/sdk": "1.2.1",
95
102
  "@xterm/addon-clipboard": "^0.2.0",
96
103
  "@xterm/addon-fit": "^0.11.0",
@@ -0,0 +1,5 @@
1
+ export const PROTOCOL_VERSION: number
2
+ export const MIN_PROTOCOL_VERSION: number
3
+ export function protocolCompatible(version: number): boolean
4
+ export function validateClientMessage(value: unknown): { ok: true; value: Record<string, unknown> } | { ok: false; error: string }
5
+ export function validateServerMessage(value: unknown): { ok: true; value: Record<string, unknown> } | { ok: false; error: string }
@@ -0,0 +1,130 @@
1
+ const PROTOCOL_VERSION = 2;
2
+ const MIN_PROTOCOL_VERSION = 2;
3
+
4
+ const CLIENT_MESSAGE_TYPES = new Set([
5
+ 'protocol-hello',
6
+ 'start-agent',
7
+ 'input',
8
+ 'composer-input',
9
+ 'app-server-request-response',
10
+ 'acp-permission-response',
11
+ 'interrupt-agent',
12
+ 'focus-agent',
13
+ 'resize-agent',
14
+ 'clear-terminal',
15
+ 'watch-workspace-files',
16
+ 'unwatch-workspace-files',
17
+ 'kill-agent',
18
+ 'restart-main-agent',
19
+ ]);
20
+
21
+ const SERVER_MESSAGE_TYPES = new Set([
22
+ 'protocol-hello',
23
+ 'protocol-error',
24
+ 'command-ack',
25
+ 'state',
26
+ 'error',
27
+ 'agent-started',
28
+ 'session-output',
29
+ 'session-preview',
30
+ 'system-stats',
31
+ 'agent-activity',
32
+ 'agent-update',
33
+ 'agent-read',
34
+ 'workspace-file-watch',
35
+ 'workspace-file-event',
36
+ ]);
37
+
38
+ function objectMessage(value) {
39
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
40
+ }
41
+
42
+ function stringField(value, name, optional = false) {
43
+ return optional && value[name] === undefined ? true : typeof value[name] === 'string';
44
+ }
45
+
46
+ function finiteField(value, name) {
47
+ return typeof value[name] === 'number' && Number.isFinite(value[name]);
48
+ }
49
+
50
+ const AGENT_UPDATE_PATCH_VALIDATORS = {
51
+ terminalInputReceived: value => typeof value === 'boolean',
52
+ terminalBusy: value => value === null || typeof value === 'boolean',
53
+ shellCwd: value => typeof value === 'string',
54
+ shellLastExitCode: value => value === null || Number.isFinite(value),
55
+ shellLastEvent: value => typeof value === 'string',
56
+ shellCommand: value => typeof value === 'string',
57
+ shellLastCommand: value => typeof value === 'string',
58
+ shellCommandStartedAt: value => value === null || Number.isFinite(value),
59
+ shellLastCommandStartedAt: value => value === null || Number.isFinite(value),
60
+ shellLastCommandFinishedAt: value => value === null || Number.isFinite(value),
61
+ shellLastCommandDurationMs: value => value === null || Number.isFinite(value),
62
+ terminalStatus: value => value === null || objectMessage(value),
63
+ runtimeObservation: objectMessage,
64
+ };
65
+
66
+ function sanitizeAgentUpdatePatch(value) {
67
+ if (!objectMessage(value)) return null;
68
+ const entries = Object.entries(value);
69
+ if (entries.length === 0 || entries.some(([name, field]) => (
70
+ !AGENT_UPDATE_PATCH_VALIDATORS[name] || !AGENT_UPDATE_PATCH_VALIDATORS[name](field)
71
+ ))) return null;
72
+ return Object.fromEntries(entries);
73
+ }
74
+
75
+ function validateClientMessage(value) {
76
+ if (!objectMessage(value) || typeof value.type !== 'string') return { ok: false, error: 'message must be an object with a type' };
77
+ if (!CLIENT_MESSAGE_TYPES.has(value.type)) return { ok: false, error: `unsupported client message: ${value.type}` };
78
+ let valid = true;
79
+ switch (value.type) {
80
+ case 'protocol-hello': valid = Number.isInteger(value.protocolVersion); break;
81
+ case 'start-agent': valid = stringField(value, 'command'); break;
82
+ case 'input': valid = stringField(value, 'agentId', true) && (typeof value.input === 'string' || Array.isArray(value.inputParts)); break;
83
+ case 'composer-input': valid = stringField(value, 'message') && stringField(value, 'agentId', true); break;
84
+ case 'app-server-request-response':
85
+ case 'acp-permission-response': valid = stringField(value, 'agentId') && stringField(value, 'requestId'); break;
86
+ case 'focus-agent': valid = value.agentId === null || stringField(value, 'agentId'); break;
87
+ case 'resize-agent': valid = stringField(value, 'agentId') && finiteField(value, 'cols') && finiteField(value, 'rows'); break;
88
+ case 'unwatch-workspace-files': valid = stringField(value, 'agentId', true); break;
89
+ case 'restart-main-agent': valid = stringField(value, 'command'); break;
90
+ default: valid = stringField(value, 'agentId'); break;
91
+ }
92
+ return valid ? { ok: true, value } : { ok: false, error: `invalid ${value.type} message` };
93
+ }
94
+
95
+ function validateServerMessage(value) {
96
+ if (!objectMessage(value) || typeof value.type !== 'string') return { ok: false, error: 'message must be an object with a type' };
97
+ if (!SERVER_MESSAGE_TYPES.has(value.type)) return { ok: false, error: `unsupported server message: ${value.type}` };
98
+ let valid = true;
99
+ switch (value.type) {
100
+ case 'protocol-hello': valid = Number.isInteger(value.protocolVersion) && Number.isInteger(value.minProtocolVersion); break;
101
+ case 'protocol-error':
102
+ case 'error': valid = stringField(value, 'message'); break;
103
+ case 'command-ack': valid = stringField(value, 'requestId') && stringField(value, 'command'); break;
104
+ case 'state': valid = objectMessage(value.state) && Array.isArray(value.state.agents); break;
105
+ case 'agent-started': valid = stringField(value, 'agentId'); break;
106
+ case 'session-output': valid = objectMessage(value.stream) && stringField(value.stream, 'agentId'); break;
107
+ case 'session-preview': valid = objectMessage(value.preview) && stringField(value.preview, 'agentId'); break;
108
+ case 'system-stats': valid = objectMessage(value.stats); break;
109
+ case 'agent-activity': valid = objectMessage(value.activity) && stringField(value.activity, 'agentId'); break;
110
+ case 'agent-update': valid = objectMessage(value.update) && stringField(value.update, 'agentId') && Boolean(sanitizeAgentUpdatePatch(value.update.patch)); break;
111
+ case 'agent-read': valid = objectMessage(value.read) && stringField(value.read, 'agentId'); break;
112
+ case 'workspace-file-watch': valid = stringField(value, 'agentId') && typeof value.watching === 'boolean'; break;
113
+ case 'workspace-file-event': valid = objectMessage(value.event) && stringField(value.event, 'agentId'); break;
114
+ default: break;
115
+ }
116
+ return valid ? { ok: true, value } : { ok: false, error: `invalid ${value.type} message` };
117
+ }
118
+
119
+ function protocolCompatible(version) {
120
+ return Number.isInteger(version) && version >= MIN_PROTOCOL_VERSION && version <= PROTOCOL_VERSION;
121
+ }
122
+
123
+ module.exports = {
124
+ MIN_PROTOCOL_VERSION,
125
+ PROTOCOL_VERSION,
126
+ protocolCompatible,
127
+ sanitizeAgentUpdatePatch,
128
+ validateClientMessage,
129
+ validateServerMessage,
130
+ };