@hamp10/agentforge 0.2.26 โ 0.2.27
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/package.json +1 -1
- package/scripts/check-task-semantics.js +20 -0
- package/src/OpenClawCLI.js +38 -2
- package/src/browser.js +51 -13
package/package.json
CHANGED
|
@@ -944,6 +944,16 @@ assert.match(
|
|
|
944
944
|
/__agentforge_verify/i,
|
|
945
945
|
'direct browser verification should cache-bust local verification URLs'
|
|
946
946
|
);
|
|
947
|
+
assert.match(
|
|
948
|
+
openClawSource,
|
|
949
|
+
/normalizeForNavigationCompare\(target\?\.url \|\| ''\) === normalizeForNavigationCompare\(effectiveRequestedUrl\)/,
|
|
950
|
+
'direct browser verification should reuse an existing cache-busted tab for the same requested local URL'
|
|
951
|
+
);
|
|
952
|
+
assert.match(
|
|
953
|
+
openClawSource,
|
|
954
|
+
/isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeDuplicateVerificationTargets[\s\S]*\/json\/close\//i,
|
|
955
|
+
'direct browser verification should close duplicate cache-busted local verification tabs'
|
|
956
|
+
);
|
|
947
957
|
assert.match(
|
|
948
958
|
openClawSource,
|
|
949
959
|
/forgetLocalScopedUrl[\s\S]*rendered page content did not identify target|rendered page content did not identify target[\s\S]*forgetLocalScopedUrl/i,
|
|
@@ -959,6 +969,16 @@ assert.match(
|
|
|
959
969
|
/__agentforge_verify/i,
|
|
960
970
|
'AgentForge browser tool should cache-bust localhost navigations'
|
|
961
971
|
);
|
|
972
|
+
assert.match(
|
|
973
|
+
browserSource,
|
|
974
|
+
/sameBrowserTargetUrl[\s\S]*stripLocalCacheBust[\s\S]*getPage\(agentId, url\)/i,
|
|
975
|
+
'AgentForge browser tool should reuse cache-busted tabs for the same requested local URL'
|
|
976
|
+
);
|
|
977
|
+
assert.match(
|
|
978
|
+
browserSource,
|
|
979
|
+
/isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeStaleVerificationTabs[\s\S]*page\.close/i,
|
|
980
|
+
'AgentForge browser tool should close stale cache-busted localhost verification tabs'
|
|
981
|
+
);
|
|
962
982
|
assert.match(
|
|
963
983
|
previewServerSource,
|
|
964
984
|
/extname\(urlPath\)[\s\S]*writeHead\(404[\s\S]*no-store/i,
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -2761,6 +2761,8 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
2761
2761
|
const normalizeForNavigationCompare = (value) => stripAgentForgeCacheBust(normalize(value));
|
|
2762
2762
|
const isLocalHttpUrl = (value) =>
|
|
2763
2763
|
/^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:\/|$)/i.test(String(value || ''));
|
|
2764
|
+
const isLocalVerificationUrl = (value) =>
|
|
2765
|
+
isLocalHttpUrl(value) && /[?&]__agentforge_verify=/.test(String(value || ''));
|
|
2764
2766
|
const withVerificationCacheBust = (value) => {
|
|
2765
2767
|
if (!value || !isLocalHttpUrl(value)) return value;
|
|
2766
2768
|
try {
|
|
@@ -2790,9 +2792,35 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
2790
2792
|
const usablePages = !taskIsAboutAgentForge
|
|
2791
2793
|
? pages.filter(p => !isAgentForgeControlSurface(p.url))
|
|
2792
2794
|
: pages;
|
|
2795
|
+
const matchesEffectiveRequestedUrl = (target) =>
|
|
2796
|
+
!!effectiveRequestedUrl && normalizeForNavigationCompare(target?.url || '') === normalizeForNavigationCompare(effectiveRequestedUrl);
|
|
2797
|
+
const requestedPageMatches = effectiveRequestedUrl
|
|
2798
|
+
? pages.filter(matchesEffectiveRequestedUrl)
|
|
2799
|
+
: [];
|
|
2793
2800
|
let page = effectiveRequestedUrl
|
|
2794
|
-
?
|
|
2801
|
+
? requestedPageMatches.find(p => !isLocalVerificationUrl(p.url)) || requestedPageMatches[0] || null
|
|
2795
2802
|
: usablePages.find(p => p.url && p.url !== 'about:blank') || null;
|
|
2803
|
+
const closeDuplicateVerificationTargets = async (keepTarget) => {
|
|
2804
|
+
if (!effectiveRequestedUrl || !keepTarget) return;
|
|
2805
|
+
const staleTargets = requestedPageMatches.filter(target =>
|
|
2806
|
+
target.id !== keepTarget.id &&
|
|
2807
|
+
isLocalVerificationUrl(target.url) &&
|
|
2808
|
+
normalizeForNavigationCompare(target.url) === normalizeForNavigationCompare(effectiveRequestedUrl)
|
|
2809
|
+
);
|
|
2810
|
+
if (staleTargets.length === 0) return;
|
|
2811
|
+
let closed = 0;
|
|
2812
|
+
await Promise.all(staleTargets.map(async (target) => {
|
|
2813
|
+
try {
|
|
2814
|
+
const closeResp = await fetch(`${cdpBase}/json/close/${encodeURIComponent(target.id)}`, {
|
|
2815
|
+
signal: AbortSignal.timeout(3_000),
|
|
2816
|
+
});
|
|
2817
|
+
if (closeResp.ok) closed += 1;
|
|
2818
|
+
} catch {}
|
|
2819
|
+
}));
|
|
2820
|
+
if (closed > 0) {
|
|
2821
|
+
console.log(` [${agentId}] ๐งน Closed ${closed} stale verification tab(s) for ${stripAgentForgeCacheBust(effectiveRequestedUrl)}`);
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2796
2824
|
|
|
2797
2825
|
const controlSurfaceUrl = effectiveRequestedUrl || page?.url || '';
|
|
2798
2826
|
if (!taskIsAboutAgentForge && isAgentForgeControlSurface(controlSurfaceUrl)) {
|
|
@@ -2811,6 +2839,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
2811
2839
|
if (!newResp.ok) throw new Error(`CDP new page failed: HTTP ${newResp.status}`);
|
|
2812
2840
|
page = await newResp.json();
|
|
2813
2841
|
}
|
|
2842
|
+
await closeDuplicateVerificationTargets(page);
|
|
2814
2843
|
|
|
2815
2844
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
2816
2845
|
let seq = 0;
|
|
@@ -2892,7 +2921,14 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
2892
2921
|
}
|
|
2893
2922
|
return lastState;
|
|
2894
2923
|
};
|
|
2895
|
-
|
|
2924
|
+
const currentPageMatchesRequest = effectiveRequestedUrl && normalizeForNavigationCompare(page.url) === normalizeForNavigationCompare(effectiveRequestedUrl);
|
|
2925
|
+
const shouldNavigateForFreshVerification = !!(
|
|
2926
|
+
effectiveRequestedUrl &&
|
|
2927
|
+
options?.afterMutation &&
|
|
2928
|
+
navigationRequestedUrl &&
|
|
2929
|
+
navigationRequestedUrl !== effectiveRequestedUrl
|
|
2930
|
+
);
|
|
2931
|
+
if (effectiveRequestedUrl && (!currentPageMatchesRequest || shouldNavigateForFreshVerification)) {
|
|
2896
2932
|
await cdp('Page.navigate', { url: navigationRequestedUrl || effectiveRequestedUrl });
|
|
2897
2933
|
await waitForRequestedDocument(effectiveRequestedUrl);
|
|
2898
2934
|
} else if (effectiveRequestedUrl) {
|
package/src/browser.js
CHANGED
|
@@ -25,6 +25,43 @@ async function getBrowser() {
|
|
|
25
25
|
return _browser;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function stripLocalCacheBust(value) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(value);
|
|
31
|
+
parsed.searchParams.delete('__agentforge_verify');
|
|
32
|
+
return parsed.href.replace(/\/$/, '');
|
|
33
|
+
} catch {
|
|
34
|
+
return String(value || '').replace(/([?&])__agentforge_verify=[^&#]+&?/, '$1').replace(/[?&]$/, '').replace(/\/$/, '');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sameBrowserTargetUrl(left, right) {
|
|
39
|
+
if (!left || !right) return false;
|
|
40
|
+
return stripLocalCacheBust(left) === stripLocalCacheBust(right);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLocalVerificationUrl(value) {
|
|
44
|
+
return isLocalHttpUrl(value) && /[?&]__agentforge_verify=/.test(String(value || ''));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function closeStaleVerificationTabs(browser, keepPage, targetUrl) {
|
|
48
|
+
const expected = targetUrl ? stripLocalCacheBust(targetUrl) : '';
|
|
49
|
+
const pages = await browser.pages();
|
|
50
|
+
let closed = 0;
|
|
51
|
+
await Promise.all(pages.map(async (page) => {
|
|
52
|
+
if (!page || page.isClosed() || page === keepPage) return;
|
|
53
|
+
let url = '';
|
|
54
|
+
try { url = page.url(); } catch { return; }
|
|
55
|
+
if (!isLocalVerificationUrl(url)) return;
|
|
56
|
+
if (expected && stripLocalCacheBust(url) !== expected) return;
|
|
57
|
+
try {
|
|
58
|
+
await page.close();
|
|
59
|
+
closed += 1;
|
|
60
|
+
} catch {}
|
|
61
|
+
}));
|
|
62
|
+
if (closed > 0) console.log(`[browser.js] Closed ${closed} stale verification tab(s) for ${targetUrl || 'local browser'}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
28
65
|
// Return the active page for this agent. Creates a fresh tab on first use.
|
|
29
66
|
// preferUrl: hint โ if the agent's tab already has a better match among all open
|
|
30
67
|
// tabs (e.g. a link opened a new tab), update the agent's pointer to it.
|
|
@@ -37,7 +74,7 @@ async function getPage(agentId, preferUrl) {
|
|
|
37
74
|
// If preferUrl given, check if any tab matches better (handles new-tab links)
|
|
38
75
|
if (preferUrl) {
|
|
39
76
|
const pages = await browser.pages();
|
|
40
|
-
const match = pages.find(p => !p.isClosed() && p.url()
|
|
77
|
+
const match = pages.find(p => !p.isClosed() && sameBrowserTargetUrl(p.url(), preferUrl));
|
|
41
78
|
if (match && match !== existing) {
|
|
42
79
|
_agentPages.set(agentId, match);
|
|
43
80
|
return match;
|
|
@@ -46,6 +83,15 @@ async function getPage(agentId, preferUrl) {
|
|
|
46
83
|
return existing;
|
|
47
84
|
}
|
|
48
85
|
|
|
86
|
+
if (preferUrl) {
|
|
87
|
+
const pages = await browser.pages();
|
|
88
|
+
const match = pages.find(p => !p.isClosed() && sameBrowserTargetUrl(p.url(), preferUrl));
|
|
89
|
+
if (match) {
|
|
90
|
+
_agentPages.set(agentId, match);
|
|
91
|
+
return match;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
49
95
|
// Agent has no page โ open a fresh tab so it doesn't land on another agent's tab
|
|
50
96
|
const fresh = await browser.newPage();
|
|
51
97
|
_agentPages.set(agentId, fresh);
|
|
@@ -71,23 +117,14 @@ function withLocalCacheBust(value) {
|
|
|
71
117
|
}
|
|
72
118
|
|
|
73
119
|
async function waitForPageReady(page, expectedUrl) {
|
|
74
|
-
const
|
|
75
|
-
try {
|
|
76
|
-
const parsed = new URL(value);
|
|
77
|
-
parsed.searchParams.delete('__agentforge_verify');
|
|
78
|
-
return parsed.href.replace(/\/$/, '');
|
|
79
|
-
} catch {
|
|
80
|
-
return String(value || '').replace(/([?&])__agentforge_verify=[^&#]+&?/, '$1').replace(/[?&]$/, '').replace(/\/$/, '');
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const expected = expectedUrl ? stripCacheBust(expectedUrl) : '';
|
|
120
|
+
const expected = expectedUrl ? stripLocalCacheBust(expectedUrl) : '';
|
|
84
121
|
for (let i = 0; i < 45; i++) {
|
|
85
122
|
try {
|
|
86
123
|
const state = await page.evaluate(() => ({
|
|
87
124
|
href: location.href,
|
|
88
125
|
readyState: document.readyState,
|
|
89
126
|
}));
|
|
90
|
-
if ((!expected ||
|
|
127
|
+
if ((!expected || stripLocalCacheBust(state.href) === expected) && state.readyState !== 'loading') return;
|
|
91
128
|
} catch {
|
|
92
129
|
// Page contexts can disappear while navigation commits.
|
|
93
130
|
}
|
|
@@ -200,7 +237,7 @@ async function _browserActionInner(input, agentId, browser) {
|
|
|
200
237
|
case 'navigate':
|
|
201
238
|
case 'open': {
|
|
202
239
|
const url = input.url || input.targetUrl;
|
|
203
|
-
const page = await getPage(agentId);
|
|
240
|
+
const page = await getPage(agentId, url);
|
|
204
241
|
await page.setCacheEnabled(false).catch(() => null);
|
|
205
242
|
const navigationUrl = withLocalCacheBust(url);
|
|
206
243
|
// Use Promise.race to enforce a hard 20s cap โ page.goto timeout option can hang
|
|
@@ -215,6 +252,7 @@ async function _browserActionInner(input, agentId, browser) {
|
|
|
215
252
|
_agentPages.set(agentId, page);
|
|
216
253
|
// Brief pause for initial render, then snapshot
|
|
217
254
|
await waitForPageReady(page, url);
|
|
255
|
+
await closeStaleVerificationTabs(browser, page, url);
|
|
218
256
|
await _wait(400);
|
|
219
257
|
return await _snapshot(page, agentId);
|
|
220
258
|
}
|