@taurgis/bonsai 4.0.0 → 4.1.1
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 +24 -3
- package/agents/agents/official-docs-researcher.agent.md +102 -0
- package/agents/agents/salesforce-docs-researcher.agent.md +16 -14
- package/agents/hooks/README.md +18 -2
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +5 -4
- package/agents/instructions/web-research.instructions.md +45 -0
- package/agents/skills/web-research/SKILL.md +139 -0
- package/bin/cli.mjs +0 -11
- package/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +9 -0
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/get.js +1 -1
- package/dist/commands/config/list.d.ts +1 -1
- package/dist/commands/config/list.js +1 -1
- package/dist/commands/config/set.d.ts +1 -1
- package/dist/commands/config/set.js +1 -1
- package/dist/commands/config/unset.d.ts +1 -1
- package/dist/commands/config/unset.js +1 -1
- package/dist/commands/context.d.ts +20 -0
- package/dist/commands/context.js +65 -0
- package/dist/commands/list.d.ts +12 -3
- package/dist/commands/list.js +61 -100
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +233 -0
- package/dist/commands/setup.d.ts +27 -0
- package/dist/commands/setup.js +148 -0
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +6 -0
- package/dist/lib/argv.js +4 -1
- package/dist/lib/cli-flag-manifest.d.ts +2 -2
- package/dist/lib/cli-flag-manifest.js +15 -7
- package/dist/lib/cli-presentation.d.ts +19 -0
- package/dist/lib/cli-presentation.js +27 -0
- package/dist/lib/cli-result-types.d.ts +59 -0
- package/dist/lib/color.d.ts +2 -0
- package/dist/lib/color.js +6 -0
- package/dist/lib/common-metadata-filter-flags.d.ts +14 -0
- package/dist/lib/common-metadata-filter-flags.js +37 -0
- package/dist/{commands/config/base.d.ts → lib/config-command.d.ts} +7 -9
- package/dist/{commands/config/base.js → lib/config-command.js} +9 -20
- package/dist/lib/next-command.d.ts +15 -0
- package/dist/lib/next-command.js +40 -0
- package/dist/lib/research/context-dashboard.d.ts +19 -0
- package/dist/lib/research/context-dashboard.js +43 -0
- package/dist/lib/research/fetch-command-service.js +10 -0
- package/dist/lib/research/fetch-result.d.ts +1 -0
- package/dist/lib/research/fetch-result.js +4 -0
- package/dist/lib/research/fetcher.d.ts +11 -0
- package/dist/lib/research/fetcher.js +26 -0
- package/dist/lib/research/list-row.d.ts +15 -0
- package/dist/lib/research/list-row.js +31 -0
- package/dist/lib/research/metadata-filters.d.ts +22 -1
- package/dist/lib/research/metadata-filters.js +34 -0
- package/dist/lib/research/regression.js +6 -1
- package/dist/lib/research/schema.d.ts +2 -0
- package/dist/lib/research/schema.js +4 -0
- package/dist/lib/research/search-match.d.ts +39 -0
- package/dist/lib/research/search-match.js +134 -0
- package/dist/lib/scope-flags.d.ts +19 -0
- package/dist/lib/scope-flags.js +27 -0
- package/dist/lib/setup/agents.d.ts +16 -0
- package/dist/lib/setup/agents.js +20 -0
- package/dist/lib/setup/bin-command.d.ts +14 -0
- package/dist/lib/setup/bin-command.js +42 -0
- package/dist/lib/setup/hook-file-io.d.ts +37 -0
- package/dist/lib/setup/hook-file-io.js +80 -0
- package/dist/lib/text.d.ts +6 -0
- package/dist/lib/text.js +11 -0
- package/dist/lib/value-flags.d.ts +2 -0
- package/dist/lib/value-flags.js +11 -3
- package/dist/sites/salesforce-developer/fetch-page.d.ts +1 -1
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/route-markdown.d.ts +3 -2
- package/dist/sites/salesforce-developer/route-markdown.js +8 -6
- package/dist/sites/salesforce-doc-fetch.d.ts +23 -11
- package/dist/sites/salesforce-doc-fetch.js +89 -27
- package/dist/sites/salesforce-dom-probe.d.ts +6 -0
- package/dist/sites/salesforce-dom-probe.js +40 -0
- package/oclif.manifest.json +428 -54
- package/package.json +8 -13
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { openCdpPage, waitForLoad, ResponseCapture, describeNavigationFailure, } from '../lib/research/browser.js';
|
|
2
|
-
import { checkDnsSafety } from '../lib/research/fetcher.js';
|
|
2
|
+
import { checkDnsSafety, fetchStatus } from '../lib/research/fetcher.js';
|
|
3
3
|
import { htmlToMarkdown } from '../lib/research/markdown.js';
|
|
4
4
|
import { sanitizePromptInjection } from '../lib/research/prompt-injection.js';
|
|
5
5
|
import { contentMetrics, contentRicherThan } from '../lib/research/regression.js';
|
|
6
|
-
import { SALESFORCE_SHADOW_DOM_HELPERS, buildPriorityContainerPickBlock, pollSalesforceContentReady, } from './salesforce-dom-probe.js';
|
|
6
|
+
import { SALESFORCE_SHADOW_DOM_HELPERS, buildPriorityContainerPickBlock, pollSalesforceContentReady, looksLikeSalesforceError, } from './salesforce-dom-probe.js';
|
|
7
|
+
export { looksLikeSalesforceError };
|
|
7
8
|
// Both help.salesforce.com and developer.salesforce.com are LWR sites that render doc content
|
|
8
9
|
// client-side inside web components (shadow DOM). document.documentElement.outerHTML captures
|
|
9
10
|
// only the "Loading…" shell, and Readability prunes the structured result to a single block —
|
|
@@ -19,20 +20,11 @@ const BODY_LIMIT_BYTES = 6 * 1024 * 1024;
|
|
|
19
20
|
/** Grace period after DOM settle for late /docs/get_document_content/ responses on guide pages. */
|
|
20
21
|
const DOCS_API_GRACE_MS = 500;
|
|
21
22
|
const ACCEPT_CONSENT_EXPRESSION = "(() => { const b = document.querySelector('#onetrust-accept-btn-handler'); if (b) b.click(); })()";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/
|
|
26
|
-
|
|
27
|
-
];
|
|
28
|
-
/** True when the extracted text is an error/not-found/loading shell rather than real content.
|
|
29
|
-
*
|
|
30
|
-
* @param text - Extracted Markdown text to check.
|
|
31
|
-
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
32
|
-
*/
|
|
33
|
-
export function looksLikeSalesforceError(text) {
|
|
34
|
-
return ERROR_PATTERNS.some((re) => re.test(text));
|
|
35
|
-
}
|
|
23
|
+
// Salesforce's WAF answers 403 unless the User-Agent leads with a recognized HTTP-tool token.
|
|
24
|
+
// Same token the Markdown probe sends, so a missing-doc check sees the real status.
|
|
25
|
+
const DOC_PROBE_HEADERS = {
|
|
26
|
+
'user-agent': 'curl/8.7.1 bonsai-research-cache (+https://bonsai.rhino-inquisitor.com)',
|
|
27
|
+
};
|
|
36
28
|
// Chrome present on every LWR doc page; stripped before serialization. Per-site noise (e.g.
|
|
37
29
|
// Help's article-feedback widget) is appended via SalesforceDocOptions.removeSelectors.
|
|
38
30
|
const BASE_REMOVE_SELECTORS = [
|
|
@@ -69,25 +61,28 @@ function confidenceFor(length) {
|
|
|
69
61
|
// page. Help wraps it in a removable component (see fetch-page.ts), but Developer renders it
|
|
70
62
|
// as bare divs, so we also strip its lines from the converted Markdown for both sites.
|
|
71
63
|
const BOILERPLATE_LINE = /(did this article solve your issue|let us know so we can improve|share your feedback)/i;
|
|
72
|
-
// Toolbar
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const MARKDOWN_TOOLBAR_LINE = /^(copy as markdown|view as markdown|copy url to markdown)$/i;
|
|
64
|
+
// Toolbar labels that serialize as their own paragraph: "View as Markdown" on articles that
|
|
65
|
+
// missed the .md probe, and the llms.txt / "Open in ChatGPT" cluster on the newer guide chrome.
|
|
66
|
+
const DOC_CHROME_LINE = /^(copy as markdown|view as markdown|copy url to markdown|view llms\.txt|copy url to llms\.txt|open in chatgpt|open in claude|open in perplexity|this page|all documentation|more actions)$/i;
|
|
76
67
|
// A link with empty anchor text — e.g. an icon-only "home" logo link. It carries no readable
|
|
77
68
|
// content and renders as a bare `[](url)`. (Images `` are intentionally not matched.)
|
|
78
69
|
const EMPTY_LINK_LINE = /^\[\]\([^)]*\)$/;
|
|
70
|
+
// Atlas books render a release switcher as one link per season. Turndown splits the anchor
|
|
71
|
+
// text across lines, so the link is not a single line. Drop the whole link; the article follows.
|
|
72
|
+
const VERSION_PICKER_LINK = /\[\s*(?:Spring|Summer|Winter|Fall)\s+'\d{2}\s+\(API version\s+[\d.]+\)\s*\]\([^)\n]*\)/gi;
|
|
79
73
|
/**
|
|
80
|
-
* Removes known boilerplate
|
|
81
|
-
*
|
|
74
|
+
* Removes known boilerplate from converted Salesforce Markdown: the feedback widget, doc-chrome
|
|
75
|
+
* toolbar labels, the atlas release switcher, and empty icon-anchor lines.
|
|
82
76
|
*
|
|
83
77
|
* @param markdown - Markdown output from the DOM or content API conversion.
|
|
84
|
-
* @returns Markdown with boilerplate
|
|
78
|
+
* @returns Markdown with boilerplate stripped and excess blank lines collapsed.
|
|
85
79
|
*/
|
|
86
80
|
export function stripBoilerplate(markdown) {
|
|
87
81
|
return markdown
|
|
82
|
+
.replace(VERSION_PICKER_LINK, '')
|
|
88
83
|
.split('\n')
|
|
89
84
|
.filter((line) => !BOILERPLATE_LINE.test(line) &&
|
|
90
|
-
!
|
|
85
|
+
!DOC_CHROME_LINE.test(line.trim()) &&
|
|
91
86
|
!EMPTY_LINK_LINE.test(line.trim()))
|
|
92
87
|
.join('\n')
|
|
93
88
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -286,6 +281,65 @@ function buildCaptureScript(contentSelectors, extraRemove) {
|
|
|
286
281
|
return { html: serialize(container), title };
|
|
287
282
|
})()`;
|
|
288
283
|
}
|
|
284
|
+
/** Article identity for a Salesforce doc URL: Help `id`, otherwise the last path segment.
|
|
285
|
+
* Underscores and hyphens match so a REST rename (`intro_what_is_rest_api` →
|
|
286
|
+
* `intro-what-is-rest-api`) stays the same article.
|
|
287
|
+
*
|
|
288
|
+
* @param url - Requested or post-navigation URL.
|
|
289
|
+
* @returns Normalized article key, or null when the URL has no article identity.
|
|
290
|
+
*/
|
|
291
|
+
export function salesforceArticleKey(url) {
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = new URL(url);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const id = parsed.searchParams.get('id');
|
|
300
|
+
const raw = (id ?? parsed.pathname.replace(/\/+$/, '').split('/').pop() ?? '').trim();
|
|
301
|
+
const stripped = raw.replace(/\.(html?|md)$/i, '');
|
|
302
|
+
if (!stripped)
|
|
303
|
+
return null;
|
|
304
|
+
return stripped.toLowerCase().replace(/_/g, '-');
|
|
305
|
+
}
|
|
306
|
+
/** True when both URLs name the same Salesforce article, or either key can't be read.
|
|
307
|
+
*
|
|
308
|
+
* @param requested - URL the caller asked for.
|
|
309
|
+
* @param finalUrl - URL the browser landed on.
|
|
310
|
+
* @returns False only when both keys exist and differ.
|
|
311
|
+
*/
|
|
312
|
+
export function sameSalesforceArticle(requested, finalUrl) {
|
|
313
|
+
const requestedKey = salesforceArticleKey(requested);
|
|
314
|
+
const finalKey = salesforceArticleKey(finalUrl);
|
|
315
|
+
if (!requestedKey || !finalKey)
|
|
316
|
+
return true;
|
|
317
|
+
return requestedKey === finalKey;
|
|
318
|
+
}
|
|
319
|
+
async function assertSalesforceDocumentExists(url) {
|
|
320
|
+
try {
|
|
321
|
+
await fetchStatus(url, { headers: DOC_PROBE_HEADERS, timeoutMs: 8_000 });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
const message = err instanceof Error ? err.message : '';
|
|
325
|
+
// Any other failure (WAF 403, timeout) still goes to the browser. Only a real
|
|
326
|
+
// not-found status is enough to skip Chrome.
|
|
327
|
+
if (/\bstatus (404|410)\b/.test(message)) {
|
|
328
|
+
throw new Error(`Salesforce doc not found: ${url}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function assertLandedOnRequestedArticle(page, url) {
|
|
333
|
+
const result = await page.client
|
|
334
|
+
.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, page.sessionId)
|
|
335
|
+
.catch(() => null);
|
|
336
|
+
const finalHref = result?.result?.value;
|
|
337
|
+
if (typeof finalHref !== 'string' || !finalHref)
|
|
338
|
+
return;
|
|
339
|
+
if (!sameSalesforceArticle(url, finalHref)) {
|
|
340
|
+
throw new Error(`Salesforce doc ${url} redirected to a different article (${finalHref}).`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
289
343
|
function parseSalesforceDocUrl(url, allowedHost) {
|
|
290
344
|
let parsed;
|
|
291
345
|
try {
|
|
@@ -300,7 +354,10 @@ function parseSalesforceDocUrl(url, allowedHost) {
|
|
|
300
354
|
return parsed;
|
|
301
355
|
}
|
|
302
356
|
function assertReadableSalesforceContent(detailedMarkdown, url) {
|
|
303
|
-
if (
|
|
357
|
+
if (looksLikeSalesforceError(detailedMarkdown)) {
|
|
358
|
+
throw new Error(`Salesforce doc not found: ${url}`);
|
|
359
|
+
}
|
|
360
|
+
if (detailedMarkdown.length < MIN_CONTAINER_CHARS) {
|
|
304
361
|
throw new Error(`Salesforce returned no readable content for ${url} (page may be missing, gated, or still loading).`);
|
|
305
362
|
}
|
|
306
363
|
}
|
|
@@ -351,6 +408,9 @@ async function captureSalesforceAttempt(page, url, captureScript, contentSelecto
|
|
|
351
408
|
}
|
|
352
409
|
let detailedMarkdown = stripBoilerplate(htmlToMarkdown(html));
|
|
353
410
|
let usedDocsApi = false;
|
|
411
|
+
// Retired atlas leaves (apex_qs_hello.htm) stay HTTP 200, then the SPA swaps in the book
|
|
412
|
+
// home. Storing that home under the requested URL would cache the wrong article.
|
|
413
|
+
await assertLandedOnRequestedArticle(page, url);
|
|
354
414
|
if (docsApiHtml) {
|
|
355
415
|
const picked = preferRicherMarkdown(detailedMarkdown, docsApiHtml, title, docsApiTitle);
|
|
356
416
|
detailedMarkdown = picked.markdown;
|
|
@@ -393,12 +453,14 @@ function buildSalesforceFetchResult(url, html, title, detailedMarkdown, qualityN
|
|
|
393
453
|
* @param options - Allowed host, priority content selectors, and optional per-site remove selectors.
|
|
394
454
|
* @returns Extracted Markdown, fetch metadata, and browser-capture provenance.
|
|
395
455
|
* @throws {Error} When the URL is invalid, the host is not the allowed host, DNS is unsafe,
|
|
396
|
-
* Chrome/CDP navigation or evaluation fails, the
|
|
397
|
-
*
|
|
456
|
+
* the document itself is HTTP 404/410, Chrome/CDP navigation or evaluation fails, the page
|
|
457
|
+
* redirects to a different article, the response body exceeds the size limit, or the captured
|
|
458
|
+
* content is too short / looks like an error page.
|
|
398
459
|
*/
|
|
399
460
|
export async function fetchSalesforceDoc(url, { allowedHost, contentSelectors, removeSelectors = [] }) {
|
|
400
461
|
const parsed = parseSalesforceDocUrl(url, allowedHost);
|
|
401
462
|
await checkDnsSafety(parsed.hostname);
|
|
463
|
+
await assertSalesforceDocumentExists(url);
|
|
402
464
|
const captureScript = buildCaptureScript(contentSelectors, removeSelectors);
|
|
403
465
|
const page = await openCdpPage();
|
|
404
466
|
try {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { CdpPage } from '../lib/research/browser.js';
|
|
2
2
|
/** Shared in-page helpers for traversing Salesforce Lightning Web Runtime (LWR) open shadow roots. */
|
|
3
3
|
export declare const SALESFORCE_SHADOW_DOM_HELPERS = "\nconst deepElements = (root) => {\n const out = [];\n const visit = (node) => {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.children || [])) {\n out.push(child);\n visit(child);\n }\n };\n visit(root);\n return out;\n};\n\nconst deepText = (root) => {\n let txt = '';\n const visit = (node) => {\n if (node.nodeType === 3) {\n txt += node.textContent || '';\n } else if (node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11) {\n const tag = node.tagName ? node.tagName.toLowerCase() : '';\n if (tag !== 'style' && tag !== 'script') {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.childNodes || [])) visit(child);\n }\n }\n };\n visit(root);\n return txt;\n};\n\nconst textLenFor = (el) =>\n Math.max((el.innerText || '').trim().length, deepText(el).trim().length);\n";
|
|
4
|
+
/** True when extracted text is a Salesforce not-found or error shell rather than an article.
|
|
5
|
+
*
|
|
6
|
+
* @param text - Extracted Markdown or visible page text.
|
|
7
|
+
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
8
|
+
*/
|
|
9
|
+
export declare function looksLikeSalesforceError(text: string): boolean;
|
|
4
10
|
/**
|
|
5
11
|
* In-page block: pick the highest-priority content container that exists on the page.
|
|
6
12
|
* Does not fall through to lower-priority selectors when a higher-priority host is present
|
|
@@ -33,6 +33,23 @@ const deepText = (root) => {
|
|
|
33
33
|
const textLenFor = (el) =>
|
|
34
34
|
Math.max((el.innerText || '').trim().length, deepText(el).trim().length);
|
|
35
35
|
`;
|
|
36
|
+
// Shells Salesforce renders for a missing article. Shared with the readiness probe so a
|
|
37
|
+
// not-found page ends the wait instead of burning the full content timeout.
|
|
38
|
+
const SALESFORCE_ERROR_PATTERNS = [
|
|
39
|
+
/page\s*(not\s*found|doesn'?t\s*exist|can'?t\s*be\s*found)/i,
|
|
40
|
+
/we\s*couldn'?t\s*find/i,
|
|
41
|
+
/we looked high and low/i,
|
|
42
|
+
/sorry to interrupt/i,
|
|
43
|
+
/head back to the space station/i,
|
|
44
|
+
];
|
|
45
|
+
/** True when extracted text is a Salesforce not-found or error shell rather than an article.
|
|
46
|
+
*
|
|
47
|
+
* @param text - Extracted Markdown or visible page text.
|
|
48
|
+
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
49
|
+
*/
|
|
50
|
+
export function looksLikeSalesforceError(text) {
|
|
51
|
+
return SALESFORCE_ERROR_PATTERNS.some((re) => re.test(text));
|
|
52
|
+
}
|
|
36
53
|
/**
|
|
37
54
|
* In-page block: pick the highest-priority content container that exists on the page.
|
|
38
55
|
* Does not fall through to lower-priority selectors when a higher-priority host is present
|
|
@@ -64,6 +81,20 @@ export function buildPriorityContainerPickBlock(selectors, minChars, containerVa
|
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
${containerVar} = ${containerVar} || document.body || document.documentElement;
|
|
84
|
+
// A Redoc hash (#tag/…/operation/…) names one operation inside the reference.
|
|
85
|
+
// getElementById stops at the shadow boundary, so match the id while walking.
|
|
86
|
+
try {
|
|
87
|
+
const hashId = decodeURIComponent((location.hash || '').replace(/^#/, ''));
|
|
88
|
+
if (hashId) {
|
|
89
|
+
let hashBest = null, hashLen = 0;
|
|
90
|
+
for (const el of deepElements(document)) {
|
|
91
|
+
if (el.id !== hashId) continue;
|
|
92
|
+
const len = textLenFor(el);
|
|
93
|
+
if (len > hashLen) { hashBest = el; hashLen = len; }
|
|
94
|
+
}
|
|
95
|
+
if (hashBest && hashLen >= MIN_CHARS) ${containerVar} = hashBest;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
67
98
|
`;
|
|
68
99
|
}
|
|
69
100
|
/**
|
|
@@ -75,8 +106,14 @@ export function buildPriorityContainerPickBlock(selectors, minChars, containerVa
|
|
|
75
106
|
* @returns A self-invoking JS expression string that evaluates to `{ has: boolean, len: number }`.
|
|
76
107
|
*/
|
|
77
108
|
export function buildContentReadyProbeExpression(selectors, minChars) {
|
|
109
|
+
const errorSource = SALESFORCE_ERROR_PATTERNS.map((re) => re.source).join('|');
|
|
78
110
|
return `(() => {
|
|
79
111
|
${SALESFORCE_SHADOW_DOM_HELPERS}
|
|
112
|
+
const errorRe = new RegExp(${JSON.stringify(errorSource)}, 'i');
|
|
113
|
+
const pageText = deepText(document.body || document.documentElement);
|
|
114
|
+
if (errorRe.test(pageText)) {
|
|
115
|
+
return { has: true, len: pageText.length, error: true };
|
|
116
|
+
}
|
|
80
117
|
const SELS = ${JSON.stringify(selectors)};
|
|
81
118
|
const MIN = ${minChars};
|
|
82
119
|
let has = false;
|
|
@@ -119,6 +156,9 @@ export async function pollSalesforceContentReady(page, selectors, minChars, time
|
|
|
119
156
|
.send('Runtime.evaluate', { expression, returnByValue: true }, page.sessionId)
|
|
120
157
|
.catch(() => null);
|
|
121
158
|
const value = rawResult?.result?.value;
|
|
159
|
+
// A not-found shell is ready now; waiting for it to "settle" only delays the failure.
|
|
160
|
+
if (value?.error)
|
|
161
|
+
return;
|
|
122
162
|
if (value?.has) {
|
|
123
163
|
const grew = Math.abs((value.len || 0) - previousLength) > Math.max(40, previousLength * 0.02);
|
|
124
164
|
if (!grew && ++stablePolls >= 2)
|