nx_aimode_lib.nx source
↩ module page · 384 lines · 21182 B
1// nx_aimode_lib.nx -- BR50: read a Google AI Mode share link as its ANSWER TEXT, never as a script shell.
2//
3// WHAT THE WIRE SHOWS (measured 2026-09-18/19 with the CDP oracle ac_cdp/aimode_oracle over the operator's
4// share https://share.google/aimode/vVS1jO66c6HI3D4YN, network capture + DOM poll):
5// * share.google/aimode/<id> --302--> www.google.com/share.google?q=aimode/<id> --301-->
6// www.google.com/search?...&udm=50... (a 92,807 B script shell, title "Google Search", noscript
7// enable-JS redirect, five scripts, NO answer text). A COLD fetch of that /search url (no cookies)
8// returns the same shell -- proven twice sovereignly (nx_research_fetch).
9// * The FIRST /search response SETS cookies. A SECOND, cookie-WARMED GET of the same /search url
10// (the oracle's idx=1, cookie_header_len 0->1812) returns a 406,554 B server-rendered document that
11// is NOT the shell: it carries the AI Mode streaming containers AND, server-rendered as plain text in
12// its markup, the continuation URL /async/folwr?... (garc/mlro/elrc/_xsrf tokens all present -- 12
13// hits; the cold shell has ZERO). NO JavaScript computed those tokens: they are in the SSR HTML.
14// * A GET of that /async/folwr url (with the same cookie jar) returns a 1,452,920 B HTML fragment that
15// carries the WHOLE conversation's answer prose ("Neo-Hookean", "Hyperelastic", "Tyranny",
16// "Neuromorphic", "Tesla" all present). This is the ANSWER, delivered in ONE synchronous response --
17// no streaming wait is needed on the sovereign side (the referee browser waits; we do not).
18// * The oracle was NOT signed in ("Sign in" in the nav) and no request that carries the answer is a POST
19// or a batchexecute with a JS-signed body: every answer-bearing request is a plain same-origin GET.
20//
21// SO THE SOVEREIGN PATH IS THREE COOKIE-SHARING GETs, no js_eval (BR3), no signed session:
22// PASS A follow share.google -> ... -> /search (cold), collecting Set-Cookie into a jar, capture final url
23// PASS B warm GET the final /search url WITH the jar -> extract the /async/folwr continuation url
24// PASS C GET /async/folwr WITH the jar -> the answer HTML fragment -> nx_html_to_text
25// A sign-in / consent wall is a NAMED refusal; a shell that never yields a continuation or answer is
26// REFUSED-NO-ANSWER by name; an empty body is REFUSED-EMPTY-BODY.
27//
28// COMPOSES INCUMBENTS, DUPLICATES NOTHING: the TLS-1.3 session, redirect Location resolution, cookie jar,
29// gzip and chunked decoders are the estate's (nx_https_fetch_follow's ff_path/ff_resolve_location/
30// ff_collect_cookies/ff_gunzip, nx_https_get_complete's req driver, nx_http_response_parse/nx_http_dechunk,
31// nx_https_redirect). This file writes only the jar-keeping loop those helpers were never wired into, and
32// the answer parser. ms_find (balanced-substring) is nx_media_state's.
33// license_tier: ORIGINAL No hw writes (Rule 26).
34import "nx_syscalls.nx"
35import "nx_x509_trust_store.nx"
36import "nx_trust_store_load_from_certdata.nx"
37import "nx_https_fetch_follow.nx" // brings the whole HTTPS closure: url_for_fetch/connect, tls session,
38 // nx_https_req_complete, nx_http_response_parse, nx_http_dechunk,
39 // nx_redir_is_redirect/location, ff_path/ff_resolve_location/
40 // ff_collect_cookies/ff_gunzip, nx_csprng_fill, TrustStore/NxHttpsTarget
41import "nx_html_to_text.nx" // nx_html_to_text: tags stripped, entities decoded, whitespace collapsed
42import "nx_media_state.nx" // ms_find: string-aware substring search
43
44// ---- status / refusal codes (br_share_read return value) ----
45const AM_OK: i64 = 0
46const AM_SIGNIN: i64 = 10 // REFUSED-SIGNIN
47const AM_CONSENT: i64 = 11 // REFUSED-CONSENT-WALL
48const AM_NO_ANSWER: i64 = 12 // REFUSED-NO-ANSWER (shell never populated / async carried no answer)
49const AM_EMPTY: i64 = 13 // REFUSED-EMPTY-BODY
50const AM_FETCH: i64 = 14 // a pass could not fetch at all (connect/handshake/parse)
51const AM_NOFINAL: i64 = 15 // redirects never settled on a 200
52const AM_NOCONT: i64 = 16 // warm doc carried no /async/folwr continuation AND no inline answer
53
54// ---- sizes ----
55const AM_RAW: i64 = 12582912 // 12 MB raw response reserve per hop (async decoded ~1.45MB; gzip smaller)
56const AM_JAR: i64 = 8192 // cookie jar (n=v; n=v)
57const AM_URL: i64 = 16384 // a resolved url (google search urls are long)
58const AM_HOPS: i64 = 8 // share.google->/share.google->/search is 2; head-room for extra bounces
59const AM_MINANS: i64 = 400 // an answer shorter than this is not an answer (a shell yields ~little text)
60
61const AM_CHROME_UA: *u8 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36"
62
63func am_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
64func am_cat(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o }
65func am_catn(dst: *u8, o: i64, v: i64) -> i64 {
66 if v == 0 { dst[o] = 48 as u8; return o + 1 }
67 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
68 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
69 var j: i64 = k - 1
70 while j >= 0 { dst[o] = t[j]; o = o + 1; j = j - 1 }
71 return o
72}
73
74// Case-insensitive substring test of a lowercase literal `pat` inside buf[0..n). Returns 1/0.
75func am_has_ci(buf: *u8, n: i64, pat: *u8) -> i64 {
76 let pl: i64 = am_slen(pat)
77 if pl == 0 { return 0 }
78 var i: i64 = 0
79 let last: i64 = n - pl
80 while i <= last {
81 var j: i64 = 0
82 var m: i64 = 1
83 while j < pl {
84 var c: i64 = buf[i+j] as i64
85 if c >= 65 { if c <= 90 { c = c + 32 } }
86 if c != (pat[j] as i64) { m = 0; j = pl } else { j = j + 1 }
87 }
88 if m == 1 { return 1 }
89 i = i + 1
90 }
91 return 0
92}
93
94// Build a GET request for host/path with the cookie jar (jl bytes) and an optional extra header line
95// (xhdr, NUL-terminated CRLF-terminated, or 0). Advertises gzip (ff_gunzip decodes it). Connection: close
96// so the body is everything after the header terminator (no keep-alive framing to track).
97func am_build_get(req: *u8, host: *u8, hlen: i64, path: *u8, plen: i64, jar: *u8, jl: i64, xhdr: *u8) -> i64 {
98 var o: i64 = am_cat(req, 0, "GET " as *u8)
99 var i: i64 = 0
100 while i < plen { req[o] = path[i]; o = o + 1; i = i + 1 }
101 o = am_cat(req, o, " HTTP/1.1\r\nHost: " as *u8)
102 i = 0
103 while i < hlen { req[o] = host[i]; o = o + 1; i = i + 1 }
104 o = am_cat(req, o, "\r\nUser-Agent: " as *u8)
105 o = am_cat(req, o, AM_CHROME_UA)
106 o = am_cat(req, o, "\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.9\r\nAccept-Encoding: gzip\r\n" as *u8)
107 if (xhdr as i64) != 0 { o = am_cat(req, o, xhdr) }
108 if jl > 0 {
109 o = am_cat(req, o, "Cookie: " as *u8)
110 i = 0
111 while i < jl { req[o] = jar[i]; o = o + 1; i = i + 1 }
112 o = am_cat(req, o, "\r\n" as *u8)
113 }
114 o = am_cat(req, o, "Connection: close\r\n\r\n" as *u8)
115 return o
116}
117
118// One HTTP hop over sovereign TLS-1.3 to the absolute https url in `urlbuf`. Sends the GET (with jar+xhdr),
119// parses the response, folds Set-Cookie into the jar. On a 3xx it writes the resolved next url into
120// next_out and returns AM_REDIR-marker (-1). On a final response it decodes the body into out and returns
121// body bytes (>=0), status in status_out[0]. On transport failure returns a negative code (< -1).
122const AM_HOP_REDIR: i64 = 0 - 1
123func am_hop(urlbuf: *u8, store: *TrustStore, jar: *u8, jl: *i64, xhdr: *u8,
124 out: *u8, out_cap: i64, status_out: *i64, next_out: *u8) -> i64 {
125 let target_raw: *u8 = sys_mmap(64)
126 let target: *NxHttpsTarget = target_raw as *NxHttpsTarget
127 target.url = nx_url_new()
128 target.port = 0
129 if nx_https_url_for_fetch(urlbuf, target) != NX_HTTPS_URL_OK { return 0 - AM_FETCH }
130 let fd_p: *i64 = (sys_mmap(16)) as *i64
131 if nx_https_url_connect(target, urlbuf, sys_now_realtime_sec(), fd_p) != NX_HTTPS_CONNECT_OK { return 0 - AM_FETCH }
132 let fd: i64 = fd_p[0]
133 sys_set_socket_timeout(fd, 12)
134 let cr: *u8 = sys_mmap(32); nx_csprng_fill(cr, 32)
135 let pk: *u8 = sys_mmap(32); nx_csprng_fill(pk, 32)
136 let vc_raw: *u8 = sys_mmap(64)
137 let vc: *TlsValidationContext = vc_raw as *TlsValidationContext
138 vc.store = store
139 vc.sni_host = urlbuf + target.url.host_off
140 vc.sni_host_len = target.url.host_len
141 vc.now_epoch = sys_now_realtime_sec()
142 let sr: i64 = nx_tls13_client_session_run(fd, urlbuf + target.url.host_off, target.url.host_len, cr, pk, vc)
143 if sr <= 0 { sys_close(fd); return 0 - AM_FETCH }
144 let session: *Tls13ClientSession = sr as *Tls13ClientSession
145 let path: *u8 = sys_mmap(4096)
146 let plen: i64 = ff_path(urlbuf, target, path)
147 let req: *u8 = sys_mmap(AM_JAR + 8192)
148 let req_len: i64 = am_build_get(req, urlbuf + target.url.host_off, target.url.host_len, path, plen, jar, jl[0], xhdr)
149 let buf: *u8 = sys_mmap(AM_RAW)
150 let gc: i64 = nx_https_req_complete(session, fd, req, req_len, buf, AM_RAW)
151 sys_close(fd)
152 if gc <= 0 { return 0 - AM_FETCH }
153 let r: *i64 = (sys_mmap(128)) as *i64
154 if nx_http_response_parse(buf, gc, r) != 0 { return 0 - AM_FETCH }
155 let status: i64 = r[1]
156 status_out[0] = status
157 ff_collect_cookies(buf, gc, jar, jl, AM_JAR)
158 if nx_redir_is_redirect(status) == 1 {
159 let loc: *u8 = sys_mmap(AM_URL)
160 let ln: i64 = nx_redir_location(buf, gc, loc, AM_URL)
161 if ln <= 0 { return 0 - AM_NOFINAL }
162 ff_resolve_location(loc, urlbuf, target, next_out)
163 return AM_HOP_REDIR
164 }
165 let body_off: i64 = r[6]
166 if r[10] == 1 {
167 let dn: i64 = nx_http_dechunk(buf + body_off, gc - body_off, out, out_cap)
168 if dn < 0 { return 0 - AM_FETCH }
169 return ff_gunzip(out, dn, out_cap)
170 }
171 var blen: i64 = gc - body_off
172 if blen < 0 { blen = 0 }
173 if blen > out_cap { blen = out_cap }
174 var c: i64 = 0
175 while c < blen { out[c] = buf[body_off + c]; c = c + 1 }
176 return ff_gunzip(out, blen, out_cap)
177}
178
179// Follow redirects from `start_url` (cold or warmed by the jar) to the first non-3xx response. On success
180// writes the FINAL url into final_out and returns the body bytes (final response body in `out`).
181func am_follow(start_url: *u8, store: *TrustStore, jar: *u8, jl: *i64, xhdr: *u8,
182 out: *u8, out_cap: i64, status_out: *i64, final_out: *u8) -> i64 {
183 let cur: *u8 = sys_mmap(AM_URL)
184 var i: i64 = 0
185 while start_url[i] != (0 as u8) { cur[i] = start_url[i]; i = i + 1 }
186 cur[i] = 0 as u8
187 let nxt: *u8 = sys_mmap(AM_URL)
188 var hop: i64 = 0
189 while hop < AM_HOPS {
190 let n: i64 = am_hop(cur, store, jar, jl, xhdr, out, out_cap, status_out, nxt)
191 if n == AM_HOP_REDIR {
192 var k: i64 = 0
193 while nxt[k] != (0 as u8) { cur[k] = nxt[k]; k = k + 1 }
194 cur[k] = 0 as u8
195 hop = hop + 1
196 } else {
197 if n < 0 { return n }
198 var k: i64 = 0
199 while cur[k] != (0 as u8) { final_out[k] = cur[k]; k = k + 1 }
200 final_out[k] = 0 as u8
201 return n
202 }
203 }
204 return 0 - AM_NOFINAL
205}
206
207// Extract the server-rendered /async/folwr continuation url out of the warm search document into out as an
208// ABSOLUTE https url on www.google.com. Un-escapes the three ways the url is escaped in the markup:
209// & -> & & -> & \/ -> / (a bare backslash otherwise ends the run). Returns len or 0.
210func am_extract_folwr(doc: *u8, n: i64, out: *u8) -> i64 {
211 let mark: *u8 = "/async/folwr?" as *u8
212 let mpos: i64 = ms_find(doc, n, mark, 13, 0)
213 if mpos < 0 { return 0 }
214 var o: i64 = am_cat(out, 0, "https://www.google.com" as *u8)
215 var i: i64 = mpos
216 var go: i64 = 1
217 while go == 1 {
218 if i >= n { go = 0 }
219 else {
220 let c: i64 = doc[i] & 0xff
221 if c == 34 { go = 0 } // "
222 else { if c == 39 { go = 0 } // '
223 else { if c == 60 { go = 0 } // <
224 else { if c == 32 { go = 0 } // space
225 else { if c == 9 { go = 0 }
226 else { if c == 10 { go = 0 }
227 else { if c == 13 { go = 0 }
228 else {
229 if c == 38 { // & : maybe &
230 if i + 4 < n { if (doc[i+1]&0xff)==97 { if (doc[i+2]&0xff)==109 { if (doc[i+3]&0xff)==112 { if (doc[i+4]&0xff)==59 {
231 out[o] = 38 as u8; o = o + 1; i = i + 5
232 } else { out[o]=38 as u8; o=o+1; i=i+1 } } else { out[o]=38 as u8; o=o+1; i=i+1 } } else { out[o]=38 as u8; o=o+1; i=i+1 } } else { out[o]=38 as u8; o=o+1; i=i+1 } }
233 else { out[o]=38 as u8; o=o+1; i=i+1 }
234 }
235 else { if c == 92 { // backslash: \/ or & else stop
236 if i + 1 < n {
237 let d: i64 = doc[i+1] & 0xff
238 if d == 47 { out[o]=47 as u8; o=o+1; i=i+2 } // \/
239 else { if d == 117 { // \u
240 if i + 5 < n { if (doc[i+2]&0xff)==48 { if (doc[i+3]&0xff)==48 { if (doc[i+4]&0xff)==50 { if (doc[i+5]&0xff)==54 {
241 out[o]=38 as u8; o=o+1; i=i+6 // & -> &
242 } else { go=0 } } else { go=0 } } else { go=0 } } else { go=0 } }
243 else { go = 0 } }
244 else { go = 0 } }
245 } else { go = 0 }
246 }
247 else { out[o] = c as u8; o = o + 1; i = i + 1 } }
248 } } } } } } }
249 }
250 }
251 out[o] = 0 as u8
252 return o
253}
254
255// Count AND list citation references in an answer HTML fragment. Google AI Mode renders each citation as a
256// link that beacons the click to /url via a ping="/url?...&url=<opaque>" attribute; the destination is an
257// OPAQUE server-side redirect token (url=CAES...), not a plaintext URL, and the visible host is JS-decorated,
258// so the ping BEACON is the stable server-rendered citation signal. This counts those beacons and, if
259// sink != 0, appends each beacon reference (with & decoded to &) one per line. MEASURED PROXY, declared:
260// it counts citation-beacon links; it does not de-duplicate multi-source chips or resolve the opaque token.
261func am_citations(doc: *u8, n: i64, sink: *u8, sink_cap: i64) -> i64 {
262 let mark: *u8 = "ping=\"/url" as *u8
263 let ml: i64 = am_slen(mark)
264 var cnt: i64 = 0
265 var so: i64 = 0
266 var i: i64 = 0
267 let last: i64 = n - ml
268 while i <= last {
269 if ms_find(doc, i + ml, mark, ml, i) == i {
270 cnt = cnt + 1
271 if (sink as i64) != 0 {
272 // copy from after ping=" up to the closing quote, decoding & -> &, into the sink line.
273 var j: i64 = i + 6 // past ping="; retain the leading slash
274 var go: i64 = 1
275 while go == 1 {
276 if j >= n { go = 0 }
277 else {
278 let c: i64 = doc[j] & 0xff
279 if c == 34 { go = 0 }
280 else {
281 if c == 38 { // & -> &
282 if j + 4 < n { if (doc[j+1]&0xff)==97 { if (doc[j+2]&0xff)==109 { if (doc[j+3]&0xff)==112 { if (doc[j+4]&0xff)==59 { j = j + 4 } } } } }
283 }
284 if so < sink_cap - 2 { sink[so] = c as u8; so = so + 1 }
285 j = j + 1
286 }
287 }
288 }
289 if so < sink_cap - 1 { sink[so] = 10 as u8; so = so + 1 }
290 }
291 i = i + ml
292 } else { i = i + 1 }
293 }
294 if (sink as i64) != 0 { if so < sink_cap { sink[so] = 0 as u8 } }
295 return cnt
296}
297
298// A response that is the enable-JS SHELL, not an answer: title "Google Search" + the noscript enablejs
299// redirect and no answer container. Used to classify the warm doc / async body.
300func am_is_shell(buf: *u8, n: i64) -> i64 {
301 if am_has_ci(buf, n, "httpservice/retry/enablejs" as *u8) == 1 { return 1 }
302 return 0
303}
304func am_is_signin(buf: *u8, n: i64) -> i64 {
305 if am_has_ci(buf, n, "accounts.google.com/servicelogin" as *u8) == 1 { return 1 }
306 if am_has_ci(buf, n, "sign in to continue" as *u8) == 1 { return 1 }
307 return 0
308}
309func am_is_consent(buf: *u8, n: i64) -> i64 {
310 if am_has_ci(buf, n, "consent.google.com" as *u8) == 1 { return 1 }
311 if am_has_ci(buf, n, "before you continue" as *u8) == 1 { return 1 }
312 return 0
313}
314
315func am_dbg(a: *u8, x: i64, b: *u8, y: i64, c: *u8, z: i64) -> i64 {
316 sys_write(2, a, am_slen(a)); let t: *u8 = sys_mmap(24); var o: i64 = am_catn(t, 0, x); sys_write(2, t, o)
317 sys_write(2, b, am_slen(b)); o = am_catn(t, 0, y); sys_write(2, t, o)
318 sys_write(2, c, am_slen(c)); o = am_catn(t, 0, z); sys_write(2, t, o)
319 sys_write(2, "\n" as *u8, 1); return 0
320}
321func am_dbg_url(a: *u8, u: *u8) -> i64 { sys_write(2, a, am_slen(a)); sys_write(2, u, am_slen(u)); sys_write(2, "\n" as *u8, 1); return 0 }
322
323// br_share_read: the BR50 contract. Reads the AI Mode share link at `share_url` and writes the extracted
324// answer TEXT (tags stripped, entities decoded) into out[0..out_cap), NUL-terminated; ncit_out[0] = citation
325// count; textlen_out[0] = answer text length. Returns AM_OK or a NAMED refusal/error code above.
326// `dbg`=1 prints one-line per-pass diagnostics to stderr (final urls, sizes, status).
327func br_share_read(share_url: *u8, store: *TrustStore, out: *u8, out_cap: i64,
328 cites_out: *u8, cites_cap: i64, final_out: *u8, final_cap: i64,
329 ncit_out: *i64, textlen_out: *i64, dbg: i64) -> i64 {
330 ncit_out[0] = 0
331 textlen_out[0] = 0
332 if (cites_out as i64) != 0 { cites_out[0] = 0 as u8 }
333 if (final_out as i64) != 0 { final_out[0] = 0 as u8 }
334 let jar: *u8 = sys_mmap(AM_JAR)
335 let jl: *i64 = (sys_mmap(8)) as *i64
336 jl[0] = 0
337 let st: *i64 = (sys_mmap(8)) as *i64
338 let finalu: *u8 = sys_mmap(AM_URL)
339 let warmu: *u8 = sys_mmap(AM_URL)
340 let folwr: *u8 = sys_mmap(AM_URL)
341 let doc: *u8 = sys_mmap(AM_RAW)
342
343 // PASS A: cold follow share.google -> ... -> /search (the 200 shell); collect Set-Cookie; capture final url.
344 let na: i64 = am_follow(share_url, store, jar, jl, 0 as *u8, doc, AM_RAW, st, finalu)
345 if dbg == 1 { am_dbg("PASS-A status=" as *u8, st[0], " bytes=" as *u8, na, " cookies=" as *u8, jl[0]); am_dbg_url("PASS-A final=" as *u8, finalu) }
346 if na < 0 { return AM_FETCH }
347 if (final_out as i64) != 0 { var fu: i64 = 0; while finalu[fu] != (0 as u8) { if fu < final_cap - 1 { final_out[fu] = finalu[fu] } fu = fu + 1 } final_out[fu] = 0 as u8 }
348 if am_is_signin(doc, na) == 1 { return AM_SIGNIN }
349 if am_is_consent(doc, na) == 1 { return AM_CONSENT }
350
351 // PASS B: warm GET the final /search url WITH the jar -> the SSR document that carries the continuation.
352 let nb: i64 = am_follow(finalu, store, jar, jl, 0 as *u8, doc, AM_RAW, st, warmu)
353 if dbg == 1 { am_dbg("PASS-B status=" as *u8, st[0], " bytes=" as *u8, nb, " cookies=" as *u8, jl[0]) }
354 if nb < 0 { return AM_FETCH }
355 if nb == 0 { return AM_EMPTY }
356 if am_is_signin(doc, nb) == 1 { return AM_SIGNIN }
357 if am_is_consent(doc, nb) == 1 { return AM_CONSENT }
358
359 // extract the server-rendered /async/folwr continuation url from the warm doc (BEFORE doc is reused).
360 let fl: i64 = am_extract_folwr(doc, nb, folwr)
361 if dbg == 1 { if fl > 0 { am_dbg_url("PASS-B folwr=" as *u8, folwr) } else { am_dbg_url("PASS-B folwr=" as *u8, "<none>" as *u8) } }
362 if fl == 0 {
363 // No continuation. The answer may be inline in the warm doc (a fully-SSR'd share); try it, else NO-ANSWER.
364 if am_is_shell(doc, nb) == 1 { return AM_NO_ANSWER }
365 let tlz: i64 = nx_html_to_text(doc, nb, out, out_cap)
366 if tlz >= AM_MINANS { ncit_out[0] = am_citations(doc, nb, cites_out, cites_cap); textlen_out[0] = tlz; return AM_OK }
367 return AM_NOCONT
368 }
369
370 // PASS C: GET /async/folwr WITH the jar (+ Referer) -> the answer HTML fragment.
371 let nc: i64 = am_follow(folwr, store, jar, jl, "Referer: https://www.google.com/\r\n" as *u8, doc, AM_RAW, st, warmu)
372 if dbg == 1 { am_dbg("PASS-C status=" as *u8, st[0], " bytes=" as *u8, nc, " cookies=" as *u8, jl[0]) }
373 if nc < 0 { return AM_FETCH }
374 if nc == 0 { return AM_EMPTY }
375 if am_is_signin(doc, nc) == 1 { return AM_SIGNIN }
376 if am_is_consent(doc, nc) == 1 { return AM_CONSENT }
377
378 let ncit: i64 = am_citations(doc, nc, cites_out, cites_cap)
379 let tl: i64 = nx_html_to_text(doc, nc, out, out_cap)
380 ncit_out[0] = ncit
381 textlen_out[0] = tl
382 if tl < AM_MINANS { return AM_NO_ANSWER }
383 return AM_OK
384}