farming-code 2.2.8 → 2.2.12
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 +42 -28
- package/README.zh_cn.md +42 -28
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1592 -741
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +54 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +35 -0
- package/backend/control-api.js +192 -17
- package/backend/executable-discovery.js +2 -0
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +354 -84
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +176 -17
- package/backend/workspace-directory.js +232 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-DWsa7DXG.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1978 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /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.
|
|
3
|
+
"version": "2.2.12",
|
|
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.
|
|
93
|
-
"@agentclientprotocol/codex-acp": "1.1.
|
|
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
|
+
};
|