zames_pro 2.90.14 → 2.90.16

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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.90.16] - 2026-10-29
11
+
12
+ ### Fixed
13
+
14
+ - **browser:** apply the sanitized headless UA to the relaunch explicitly
15
+
16
+ ## [2.90.15] - 2026-10-29
17
+
18
+ ### Changed
19
+
20
+ - test(browser): pin the headless user-agent sanitizer contract
21
+
10
22
  ## [2.90.14] - 2026-10-29
11
23
 
12
24
  ### Fixed
@@ -383,3 +383,19 @@ export async function nukeProfile(dir = USER_DATA_DIR) {
383
383
  }
384
384
  catch { }
385
385
  }
386
+ /**
387
+ * PURE: does this browser still need the headless User-Agent fix?
388
+ *
389
+ * `_fixHeadlessUserAgent()` bails out for an "already configured" UA — but
390
+ * using `if (this.userAgent) return` was wrong: the field is also the ENGINE's
391
+ * own UA captured from the page, and a config UA that merely CONTAINS the
392
+ * `HeadlessChrome` marker needs the fix just as much. The only value that can
393
+ * skip the fix is one that is already sanitized (and non-empty — an empty
394
+ * string means "ask the engine", not "use a blank UA").
395
+ */
396
+ export function needsHeadlessUaFix(ua) {
397
+ const value = String(ua ?? '');
398
+ if (!value)
399
+ return true;
400
+ return sanitizeHeadlessUA(value) !== value;
401
+ }
package/dist/browser.js CHANGED
@@ -23,7 +23,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
23
23
  // imported for the class below and RE-EXPORTED here, so the public API of this
24
24
  // module is unchanged (tests and other modules import these names from
25
25
  // './browser.js').
26
- import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
26
+ import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, needsHeadlessUaFix, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
27
27
  export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, };
28
28
  export class DeepSeekBrowser {
29
29
  providerName = 'deepseek';
@@ -330,6 +330,12 @@ export class DeepSeekBrowser {
330
330
  async _fixHeadlessUserAgent() {
331
331
  if (!this.headless)
332
332
  return;
333
+ // An already-sanitized UA (from the cache or a previous fix) needs no
334
+ // relaunch; an EMPTY field or a config UA that still carries the marker
335
+ // must be fixed, which the old blanket `if (this.userAgent) return` skipped
336
+ // on Qwen/GLM.
337
+ if (!needsHeadlessUaFix(this.userAgent))
338
+ return;
333
339
  try {
334
340
  const ua = await this.page.evaluate(readUserAgentInDom);
335
341
  const fixed = sanitizeHeadlessUA(ua);
@@ -338,18 +344,28 @@ export class DeepSeekBrowser {
338
344
  this.userAgent = fixed;
339
345
  // Remember the good UA for this engine build so the NEXT headless start
340
346
  // uses it immediately (no relaunch). Keyed by executable path.
341
- void writeCachedHeadlessUA(headlessEngineKey(), fixed);
347
+ await writeCachedHeadlessUA(headlessEngineKey(), fixed);
342
348
  if (this.debug)
343
349
  console.error('profile: headless UA -> relaunching with a plain Chrome UA');
344
350
  await this.context.close().catch(() => { });
345
- await this._launchOnce();
351
+ // Pass the UA EXPLICITLY: it was just sanitized, and relying on the field
352
+ // meant the relaunch could read the page's own UA back and keep the
353
+ // HeadlessChrome marker (a 403 / login wall from DeepSeek's CDN).
354
+ await this._launchOnce(fixed);
346
355
  }
347
356
  catch (e) {
348
357
  if (this.debug)
349
358
  console.error('profile: UA fix failed:', e.message);
350
359
  }
351
360
  }
352
- async _launchOnce() {
361
+ /**
362
+ * Launch the persistent context. `uaOverride` applies a User-Agent to THIS
363
+ * launch without storing it first: the UA fix reads the engine UA from the
364
+ * page it is about to close, so passing the value explicitly is the only way
365
+ * the relaunch cannot fall back to the un-sanitized UA (a stored field was
366
+ * read by the OLD page and then re-read by the new one).
367
+ */
368
+ async _launchOnce(uaOverride) {
353
369
  const options = {
354
370
  headless: this.headless,
355
371
  // 30ms slowMo per Playwright action added up over thousands of DOM
@@ -360,8 +376,9 @@ export class DeepSeekBrowser {
360
376
  };
361
377
  if (this.channel)
362
378
  options.channel = this.channel;
363
- if (this.userAgent)
364
- options.userAgent = this.userAgent;
379
+ const ua = uaOverride || this.userAgent;
380
+ if (ua)
381
+ options.userAgent = ua;
365
382
  try {
366
383
  this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
367
384
  }
@@ -26,7 +26,7 @@ import { recoverFromContextLimit } from './context-limit.js';
26
26
  import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
27
27
  import { theme } from './theme.js';
28
28
  import { scrapeGlmChatList, scrapeGlmChatMessages, parseGlmChatList, parseGlmHistory, } from './glm-chats.js';
29
- import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
29
+ import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, needsHeadlessUaFix, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
30
30
  import { readLastAnswerInDom, readPageToastsInDom, readUserAgentInDom, readInputTrimmedTextInDom, isElementEnabledInDom, setBusyTitleInDom, chatSignalInDom, } from './browser-dom.js';
31
31
  import { probeUiHealth, } from './ui-health.js';
32
32
  import { CHAT_URL, LOGIN_URL, CHAT_ID_RE, chatUrl, INPUT_SELECTORS, UPLOAD_BUTTON_SELECTOR, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, NEW_CHAT_SELECTOR, MODELS_URL, MODEL_TRIGGER_SELECTOR, modelIdFromTriggerId, CHAT_LIST_URL, chatHistoryUrl, AUTH_URL, ANSWER_URL_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, USER_CLASS_RE, CHAT_SIGNAL_NODES_SELECTOR, MARKDOWN_SELECTOR, } from './glm-ui.js';
@@ -261,7 +261,8 @@ export class GlmBrowser {
261
261
  async _fixHeadlessUserAgent() {
262
262
  if (!this.headless)
263
263
  return;
264
- if (this.userAgent)
264
+ // See QwenBrowser: only an ALREADY sanitized UA means "nothing to fix".
265
+ if (!needsHeadlessUaFix(this.userAgent))
265
266
  return;
266
267
  try {
267
268
  const ua = await this.page.evaluate(readUserAgentInDom);
@@ -269,18 +270,18 @@ export class GlmBrowser {
269
270
  if (fixed === ua)
270
271
  return;
271
272
  this.userAgent = fixed;
272
- void writeCachedHeadlessUA(headlessEngineKey(), fixed);
273
+ await writeCachedHeadlessUA(headlessEngineKey(), fixed);
273
274
  if (this.debug)
274
275
  console.error('glm profile: headless UA -> relaunching plain Chrome UA');
275
276
  await this.context.close().catch(() => { });
276
- await this._launchOnce();
277
+ await this._launchOnce(fixed);
277
278
  }
278
279
  catch (e) {
279
280
  if (this.debug)
280
281
  console.error('glm profile: UA fix failed:', e.message);
281
282
  }
282
283
  }
283
- async _launchOnce() {
284
+ async _launchOnce(uaOverride) {
284
285
  const options = {
285
286
  headless: this.headless,
286
287
  slowMo: 10,
@@ -288,8 +289,9 @@ export class GlmBrowser {
288
289
  };
289
290
  if (this.channel)
290
291
  options.channel = this.channel;
291
- if (this.userAgent)
292
- options.userAgent = this.userAgent;
292
+ const ua = uaOverride || this.userAgent;
293
+ if (ua)
294
+ options.userAgent = ua;
293
295
  try {
294
296
  this.context = await chromium.launchPersistentContext(GLM_USER_DATA_DIR, options);
295
297
  }
@@ -27,7 +27,7 @@ import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
27
27
  import { theme } from './theme.js';
28
28
  import { recoverFromContextLimit } from './context-limit.js';
29
29
  import { scrapeQwenChatList, scrapeQwenChatMessages, parseQwenChatList, parseQwenHistory, } from './qwen-chats.js';
30
- import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
30
+ import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, needsHeadlessUaFix, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
31
31
  import { chatSignalInDom, dumpDomCountsInDom, isElementEnabledInDom, readInputTrimmedTextInDom, readLastAnswerInDom, readPageToastsInDom, readUserAgentInDom, setBusyTitleInDom, clickStopInDom, stopButtonVisibleInDom, } from './browser-dom.js';
32
32
  import { probeUiHealth, } from './ui-health.js';
33
33
  import { CHAT_URL, CHAT_ID_RE, chatUrl, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, NEW_CHAT_SELECTOR, MODEL_TRIGGER_SELECTOR, MODELS_URL, CHAT_LIST_URL, chatHistoryUrl, AGE_GATE_MODAL, AGE_GATE_CONTINUE, AGE_GATE_YEAR_SELECT, AGE_GATE_YEAR_OPTION, COOKIE_ACCEPT_RE, ATTACH_SELECTORS, ATTACH_MENU_TRIGGER, ATTACH_MENU_ITEM_RE, THINKING_SELECTOR, THINKING_MODE_RE, FAST_MODE_RE, WEB_SEARCH_MENU_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, ASSISTANT_CLASS_RE, CHAT_SIGNAL_NODES_SELECTOR, MARKDOWN_SELECTOR, TOGGLE_SELECTORS, } from './qwen-ui.js';
@@ -264,7 +264,12 @@ export class QwenBrowser {
264
264
  async _fixHeadlessUserAgent() {
265
265
  if (!this.headless)
266
266
  return;
267
- if (this.userAgent)
267
+ // A UA that is ALREADY sanitized came from the cache; there is nothing to
268
+ // fix and nothing to relaunch for. Any other value (including an empty
269
+ // one) is the engine's own UA, which may still carry the marker — the old
270
+ // blanket `if (this.userAgent) return` skipped the fix for exactly that
271
+ // case, leaving `HeadlessChrome` in the live browser.
272
+ if (!needsHeadlessUaFix(this.userAgent))
268
273
  return;
269
274
  try {
270
275
  const ua = await this.page.evaluate(readUserAgentInDom);
@@ -272,18 +277,20 @@ export class QwenBrowser {
272
277
  if (fixed === ua)
273
278
  return;
274
279
  this.userAgent = fixed;
275
- void writeCachedHeadlessUA(headlessEngineKey(), fixed);
280
+ await writeCachedHeadlessUA(headlessEngineKey(), fixed);
276
281
  if (this.debug)
277
282
  console.error('qwen profile: headless UA -> relaunching plain Chrome UA');
278
283
  await this.context.close().catch(() => { });
279
- await this._launchOnce();
284
+ // Apply the UA to THIS launch explicitly: re-reading it from the fresh
285
+ // page would keep the marker the fix exists to remove.
286
+ await this._launchOnce(fixed);
280
287
  }
281
288
  catch (e) {
282
289
  if (this.debug)
283
290
  console.error('qwen profile: UA fix failed:', e.message);
284
291
  }
285
292
  }
286
- async _launchOnce() {
293
+ async _launchOnce(uaOverride) {
287
294
  const options = {
288
295
  headless: this.headless,
289
296
  slowMo: 10,
@@ -291,8 +298,9 @@ export class QwenBrowser {
291
298
  };
292
299
  if (this.channel)
293
300
  options.channel = this.channel;
294
- if (this.userAgent)
295
- options.userAgent = this.userAgent;
301
+ const ua = uaOverride || this.userAgent;
302
+ if (ua)
303
+ options.userAgent = ua;
296
304
  try {
297
305
  this.context = await chromium.launchPersistentContext(QWEN_USER_DATA_DIR, options);
298
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.90.14",
3
+ "version": "2.90.16",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {