@hamp10/agentforge 0.2.26 โ 0.2.28
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 +45 -0
- package/src/OpenClawCLI.js +38 -2
- package/src/browser.js +51 -13
- package/src/taskSemantics.js +2 -2
- package/src/worker.js +10 -0
package/package.json
CHANGED
|
@@ -35,6 +35,12 @@ const cases = [
|
|
|
35
35
|
absent: ['example-com', 'delete', 'rebuild', 'readability', 'design', 'urls', 'site'],
|
|
36
36
|
pageOnly: true,
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
text: 'Delete and rebuild the listing pages for Alpha and Beta from a clean start. Only change those listing pages.',
|
|
40
|
+
slugs: ['alpha', 'beta'],
|
|
41
|
+
absent: ['beta-from', 'clean-start', 'from'],
|
|
42
|
+
pageOnly: true,
|
|
43
|
+
},
|
|
38
44
|
{
|
|
39
45
|
text: 'Work on Alpha pages for readability',
|
|
40
46
|
slugs: ['alpha'],
|
|
@@ -214,6 +220,25 @@ try {
|
|
|
214
220
|
const worker = Object.create(AgentForgeWorker.prototype);
|
|
215
221
|
worker.cli = { isAnthropicApiKey: (key) => /^sk-ant-/i.test(String(key || '')) };
|
|
216
222
|
const cli = Object.create(OpenClawCLI.prototype);
|
|
223
|
+
const deletedScopedPagePath = path.join(fixture.repo, 'public_html', 'domains', 'alpha.html');
|
|
224
|
+
rmSync(deletedScopedPagePath, { force: true });
|
|
225
|
+
assert.equal(
|
|
226
|
+
worker._buildDeletedScopedPageNudge(
|
|
227
|
+
[{ root: fixture.repo, head: fixture.head }],
|
|
228
|
+
'Delete the Example.com listing page for Alpha. Only change that listing page.'
|
|
229
|
+
),
|
|
230
|
+
'',
|
|
231
|
+
'explicit page deletion tasks should be allowed to leave the scoped target page deleted'
|
|
232
|
+
);
|
|
233
|
+
assert.match(
|
|
234
|
+
worker._buildDeletedScopedPageNudge(
|
|
235
|
+
[{ root: fixture.repo, head: fixture.head }],
|
|
236
|
+
'Delete and rebuild the Alpha listing page from a clean start, preserving the same URL. Only change that listing page.'
|
|
237
|
+
),
|
|
238
|
+
/deleted target page source\(s\) still missing/i,
|
|
239
|
+
'clean-start rebuild tasks should still require the scoped target page to be recreated'
|
|
240
|
+
);
|
|
241
|
+
git(fixture.repo, ['restore', '--', 'public_html/domains/alpha.html']);
|
|
217
242
|
const projectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-project-list-'));
|
|
218
243
|
let agentWorkspace = null;
|
|
219
244
|
try {
|
|
@@ -944,6 +969,16 @@ assert.match(
|
|
|
944
969
|
/__agentforge_verify/i,
|
|
945
970
|
'direct browser verification should cache-bust local verification URLs'
|
|
946
971
|
);
|
|
972
|
+
assert.match(
|
|
973
|
+
openClawSource,
|
|
974
|
+
/normalizeForNavigationCompare\(target\?\.url \|\| ''\) === normalizeForNavigationCompare\(effectiveRequestedUrl\)/,
|
|
975
|
+
'direct browser verification should reuse an existing cache-busted tab for the same requested local URL'
|
|
976
|
+
);
|
|
977
|
+
assert.match(
|
|
978
|
+
openClawSource,
|
|
979
|
+
/isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeDuplicateVerificationTargets[\s\S]*\/json\/close\//i,
|
|
980
|
+
'direct browser verification should close duplicate cache-busted local verification tabs'
|
|
981
|
+
);
|
|
947
982
|
assert.match(
|
|
948
983
|
openClawSource,
|
|
949
984
|
/forgetLocalScopedUrl[\s\S]*rendered page content did not identify target|rendered page content did not identify target[\s\S]*forgetLocalScopedUrl/i,
|
|
@@ -959,6 +994,16 @@ assert.match(
|
|
|
959
994
|
/__agentforge_verify/i,
|
|
960
995
|
'AgentForge browser tool should cache-bust localhost navigations'
|
|
961
996
|
);
|
|
997
|
+
assert.match(
|
|
998
|
+
browserSource,
|
|
999
|
+
/sameBrowserTargetUrl[\s\S]*stripLocalCacheBust[\s\S]*getPage\(agentId, url\)/i,
|
|
1000
|
+
'AgentForge browser tool should reuse cache-busted tabs for the same requested local URL'
|
|
1001
|
+
);
|
|
1002
|
+
assert.match(
|
|
1003
|
+
browserSource,
|
|
1004
|
+
/isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeStaleVerificationTabs[\s\S]*page\.close/i,
|
|
1005
|
+
'AgentForge browser tool should close stale cache-busted localhost verification tabs'
|
|
1006
|
+
);
|
|
962
1007
|
assert.match(
|
|
963
1008
|
previewServerSource,
|
|
964
1009
|
/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
|
}
|
package/src/taskSemantics.js
CHANGED
|
@@ -112,7 +112,7 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
112
112
|
addScopeCandidate(candidates, match[1]);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/gi;
|
|
115
|
+
const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/gi;
|
|
116
116
|
for (const match of text.matchAll(afterPage)) {
|
|
117
117
|
const before = text.slice(Math.max(0, match.index - 120), match.index);
|
|
118
118
|
const precedingToken = before.match(/\b([a-z0-9][a-z0-9._/-]{2,})\s*$/i)?.[1] || '';
|
|
@@ -124,7 +124,7 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
124
124
|
|
|
125
125
|
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
126
126
|
const after = text.slice(match.index + match[0].length);
|
|
127
|
-
const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/i);
|
|
127
|
+
const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/i);
|
|
128
128
|
if (pageTarget) {
|
|
129
129
|
const targetCandidates = new Set();
|
|
130
130
|
addScopeCandidate(targetCandidates, pageTarget[1]);
|
package/src/worker.js
CHANGED
|
@@ -501,6 +501,15 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
501
501
|
return /\b(delete|remove|drop|strip|simplify|replace|rewrite|rebuild|from scratch|start over|nuke|wipe|clear out|throw away)\b/i.test(String(userMessage || ''));
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
_allowsScopedPageSourcesToRemainDeleted(userMessage) {
|
|
505
|
+
const text = String(userMessage || '');
|
|
506
|
+
const asksForDeletion = /\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
507
|
+
if (!asksForDeletion) return false;
|
|
508
|
+
|
|
509
|
+
const asksForRebuildOrRepair = /\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|fix|improve|polish|repair|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b/i.test(text);
|
|
510
|
+
return !asksForRebuildOrRepair;
|
|
511
|
+
}
|
|
512
|
+
|
|
504
513
|
_parseNumstat(output, source) {
|
|
505
514
|
const stats = [];
|
|
506
515
|
for (const line of String(output || '').split('\n')) {
|
|
@@ -848,6 +857,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
848
857
|
|
|
849
858
|
_findDeletedScopedPageSources(repoBaselines, userMessage) {
|
|
850
859
|
if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
|
|
860
|
+
if (this._allowsScopedPageSourcesToRemainDeleted(userMessage)) return [];
|
|
851
861
|
const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
|
|
852
862
|
if (allowedSlugs.length === 0 || !pageOnly) return [];
|
|
853
863
|
|