neoagent 3.0.1-beta.21 → 3.0.1-beta.23

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.
Binary file
@@ -33,10 +33,10 @@ addEventListener("message", eventListener);
33
33
  if (!window._flutter) {
34
34
  window._flutter = {};
35
35
  }
36
- _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19cab","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
36
+ _flutter.buildConfig = {"engineRevision":"a4ce257c68517c1410f4b48ac9852ab5642a3f8d","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1637623305" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2640036147" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -40951,7 +40951,7 @@ $S:4}
40951
40951
  A.bgv.prototype={
40952
40952
  $1(a){var s=A.fb().b
40953
40953
  s=s==null?null:s.canvasKitBaseUrl
40954
- return(s==null?"https://www.gstatic.com/flutter-canvaskit/77e2e94772b6eb43759e34ed1ad7da4674e19cab/":s)+a},
40954
+ return(s==null?"https://www.gstatic.com/flutter-canvaskit/a4ce257c68517c1410f4b48ac9852ab5642a3f8d/":s)+a},
40955
40955
  $S:45}
40956
40956
  A.a_n.prototype={
40957
40957
  gH(a){var s=this.a
@@ -138284,7 +138284,7 @@ r===$&&A.b()
138284
138284
  p.push(A.iR(q,A.jp(!1,new A.Z(B.vj,A.cX(new A.cA(B.jP,new A.a8Q(r,q),q),q,q),q),!1,B.I,!0),q,q,0,0,0,q))}r=!1
138285
138285
  if(!s.ay)if(!s.ch){r=s.e
138286
138286
  r===$&&A.b()
138287
- r=B.b.v("mqnohp81-bb5c923").length!==0&&r.b}if(r){r=s.d
138287
+ r=B.b.v("mqqp9zyc-84cecca").length!==0&&r.b}if(r){r=s.d
138288
138288
  r===$&&A.b()
138289
138289
  r=r.aD&&!r.au?84:0
138290
138290
  s=s.e
@@ -144158,7 +144158,7 @@ $S:0}
144158
144158
  A.a00.prototype={}
144159
144159
  A.TK.prototype={
144160
144160
  no(a){var s=this
144161
- if(B.b.v("mqnohp81-bb5c923").length===0||s.a!=null)return
144161
+ if(B.b.v("mqqp9zyc-84cecca").length===0||s.a!=null)return
144162
144162
  s.Bg()
144163
144163
  s.a=A.m0(B.SS,new A.bfB(s))},
144164
144164
  Bg(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -144176,7 +144176,7 @@ if(!t.f.b(k)){s=1
144176
144176
  break}i=J.a1(k,"buildId")
144177
144177
  h=i==null?null:B.b.v(J.p(i))
144178
144178
  j=h==null?"":h
144179
- if(J.bd(j)===0||J.f(j,"mqnohp81-bb5c923")){s=1
144179
+ if(J.bd(j)===0||J.f(j,"mqqp9zyc-84cecca")){s=1
144180
144180
  break}n.b=!0
144181
144181
  n.E()
144182
144182
  p=2
@@ -144193,7 +144193,7 @@ case 2:return A.i(o.at(-1),r)}})
144193
144193
  return A.k($async$Bg,r)},
144194
144194
  vW(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
144195
144195
  var $async$vW=A.h(function(a2,a3){if(a2===1){o.push(a3)
144196
- s=p}for(;;)switch(s){case 0:if(B.b.v("mqnohp81-bb5c923").length===0||n.c){s=1
144196
+ s=p}for(;;)switch(s){case 0:if(B.b.v("mqqp9zyc-84cecca").length===0||n.c){s=1
144197
144197
  break}n.c=!0
144198
144198
  n.E()
144199
144199
  p=4
@@ -145,6 +145,7 @@ const {
145
145
  } = require('../messagingFallback');
146
146
  const {
147
147
  classifyToolExecution,
148
+ gatheredNewEvidence,
148
149
  isSubstantiveProgressToolName,
149
150
  summarizeProgressToolExecutions,
150
151
  summarizeToolExecutions,
@@ -292,11 +293,11 @@ function summarizeReadTargets(toolExecutions = []) {
292
293
 
293
294
  function buildNoProgressWrapupPrompt({ readOnlyCount = 0, alreadyRead = '', platform = null } = {}) {
294
295
  return [
295
- `No-progress limit reached after ${Math.max(0, Number(readOnlyCount) || 0)} consecutive turns without substantive progress.`,
296
- alreadyRead ? `Already inspected/searched: ${alreadyRead}.` : '',
297
- 'This is the final turn for this run. Do not call tools.',
298
- 'From the existing evidence, return the concrete result, a no-op/no-change answer, or a clear blocker.',
299
- 'If an external site, service, permission, unavailable target, or missing user input prevents completion, say that clearly and stop.',
296
+ `This is the final turn for this run (no further tool calls; ${Math.max(0, Number(readOnlyCount) || 0)} read-only turns without a state change).`,
297
+ alreadyRead ? `Already gathered: ${alreadyRead}.` : '',
298
+ 'Write the answer now from everything you have already gathered in this conversation. Deliver the useful result you DO have — calendar, weather, emails, search findings, whatever was collected — formatted as the actual answer to the original request.',
299
+ 'If one part could not be retrieved, still deliver everything else and note the missing part in at most one short clause. Never withhold a useful answer because a single detail is missing.',
300
+ 'Only report a pure blocker if you genuinely gathered nothing usable at all. Do not describe the result as unfinished, unconfirmed, "blocked", or "still working" when you have something useful this IS the final answer.',
300
301
  buildMaxIterationWrapupPrompt(platform),
301
302
  ].filter(Boolean).join('\n\n');
302
303
  }
@@ -1671,6 +1672,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1671
1672
  options,
1672
1673
  });
1673
1674
  stepIndex = batch.endingStepIndex;
1675
+ let batchGatheredNewEvidence = false;
1674
1676
  for (const item of batch.results) {
1675
1677
  const execution = classifyToolExecution(
1676
1678
  item.toolName,
@@ -1681,7 +1683,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1681
1683
  execution.input = item.toolArgs;
1682
1684
  execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
1683
1685
  toolExecutions.push(execution);
1684
- engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
1686
+ const observation = engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
1687
+ if (gatheredNewEvidence(execution, observation)) batchGatheredNewEvidence = true;
1685
1688
  if (item.error) failedStepCount += 1;
1686
1689
  const modelPayload = compactPayloadForModel(item.toolName, item.result);
1687
1690
  const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
@@ -1720,7 +1723,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1720
1723
  if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
1721
1724
  const iterMeta = engine.getRunMeta(runId);
1722
1725
  if (iterMeta) {
1723
- iterMeta.consecutiveReadOnlyIterations = (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
1726
+ iterMeta.consecutiveReadOnlyIterations = batchGatheredNewEvidence
1727
+ ? 0
1728
+ : (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
1724
1729
  }
1725
1730
  }
1726
1731
  continue;
@@ -1941,10 +1946,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1941
1946
 
1942
1947
  const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
1943
1948
  execution.input = toolArgs;
1944
- if (execution.stateChanged && isProgressToolCall(toolName, toolArgs)) {
1949
+ const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
1950
+ if ((execution.stateChanged && isProgressToolCall(toolName, toolArgs))
1951
+ || gatheredNewEvidence(execution, repetitionObservation)) {
1945
1952
  iterationConcreteProgress = true;
1946
1953
  }
1947
- repetitionGuard?.observe(toolName, toolArgs, toolResult);
1948
1954
  execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
1949
1955
  toolExecutions.push(execution);
1950
1956
  if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
@@ -153,6 +153,20 @@ function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
153
153
  }).join('\n');
154
154
  }
155
155
 
156
+ // A read-only turn that pulls in NEW information is real progress, even though
157
+ // it changes no state — research, browsing, reading, and searching are how an
158
+ // agent makes headway on "find out X" tasks. Treating those as "no progress"
159
+ // (and force-wrapping the run) is what guillotines legitimate research. Genuine
160
+ // churn — failed calls, pure `think`, or re-running an identical call that
161
+ // returns an unchanged result — is excluded, so the read-only guard still fires
162
+ // on real spinning.
163
+ function gatheredNewEvidence(execution, repetitionObservation = null) {
164
+ if (!execution || execution.ok !== true) return false;
165
+ if (!execution.evidenceRelevant) return false;
166
+ if (repetitionObservation && repetitionObservation.unchangedCount >= 2) return false;
167
+ return true;
168
+ }
169
+
156
170
  function isSubstantiveProgressToolName(toolName = '') {
157
171
  const name = String(toolName || '').trim();
158
172
  if (!name) return false;
@@ -235,6 +249,7 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
235
249
  module.exports = {
236
250
  classifyToolExecution,
237
251
  deriveEvidenceSource,
252
+ gatheredNewEvidence,
238
253
  isSubstantiveProgressEvidence,
239
254
  isSubstantiveProgressToolName,
240
255
  summarizeProgressToolExecutions,
@@ -323,6 +323,46 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
323
323
  });
324
324
  break;
325
325
 
326
+ case 'google_workspace_calendar_list_events': {
327
+ // The raw result leads with a verbose `nextTimedEvent` plus full event
328
+ // objects (description/htmlLink/attendees), so generic JSON truncation
329
+ // cuts the actual `events` array off mid-object — the model never sees
330
+ // what is scheduled and re-queries adjacent windows forever. Emit a
331
+ // compact, COMPLETE digest instead: every event the model needs to reason
332
+ // about "what's coming up", none of the noise it doesn't.
333
+ const events = Array.isArray(toolResult?.events)
334
+ ? toolResult.events
335
+ : [
336
+ ...(Array.isArray(toolResult?.timedEvents) ? toolResult.timedEvents : []),
337
+ ...(Array.isArray(toolResult?.allDayEvents) ? toolResult.allDayEvents : []),
338
+ ];
339
+ const timed = events.filter((event) => !event?.allDay);
340
+ const allDay = events.filter((event) => event?.allDay);
341
+ envelope = trimObject({
342
+ tool: toolName,
343
+ count: typeof toolResult?.count === 'number' ? toolResult.count : events.length,
344
+ timedCount: typeof toolResult?.timedCount === 'number' ? toolResult.timedCount : timed.length,
345
+ allDayCount: typeof toolResult?.allDayCount === 'number' ? toolResult.allDayCount : allDay.length,
346
+ hasOnlyAllDayEvents: toolResult?.hasOnlyAllDayEvents === true ? true : undefined,
347
+ // Timed appointments are what "starting soon" cares about — keep them all,
348
+ // with the fields a reminder actually uses.
349
+ timed: timed.map((event) => trimObject({
350
+ summary: clampText(event?.summary || '(no title)', 140),
351
+ start: event?.start || null,
352
+ end: event?.end || null,
353
+ location: event?.location ? clampText(event.location, 80) : undefined,
354
+ status: event?.status && event.status !== 'confirmed' ? event.status : undefined,
355
+ })),
356
+ // All-day entries are usually birthdays / markers — names + dates suffice.
357
+ allDay: allDay.map((event) => trimObject({
358
+ summary: clampText(event?.summary || '(no title)', 140),
359
+ start: event?.start || null,
360
+ end: event?.end || null,
361
+ })),
362
+ });
363
+ break;
364
+ }
365
+
326
366
  default:
327
367
  envelope = trimObject({
328
368
  tool: toolName,
@@ -57,6 +57,12 @@ function dockerfileFor(profile) {
57
57
  // container runs as root (macOS) or as the host uid:gid (Linux, for shared
58
58
  // file ownership). Chromium needs PLAYWRIGHT_BROWSERS_PATH readable by all.
59
59
  'RUN mkdir -p /opt/neoagent/.runtime && chmod -R 0777 /opt/neoagent/.runtime && chmod -R a+rX /ms-playwright',
60
+ // Chromium is baked in at build time, so the runtime-ready marker the browser
61
+ // controller polls for (BROWSER_READY_MARKER in server/services/browser/controller.js)
62
+ // is already satisfied. Create it here so ensureBrowser() launches immediately
63
+ // instead of waiting out its full 10-minute timeout for a marker that only the
64
+ // legacy QEMU per-boot bootstrap ever writes.
65
+ 'RUN mkdir -p /var/lib/neoagent && touch /var/lib/neoagent/browser-runtime-ready',
60
66
  'CMD ["node", "server/guest_agent.js"]',
61
67
  '',
62
68
  ].join('\n');
@@ -22,6 +22,41 @@ fs.mkdirSync(SOCIAL_VIDEO_TMP_DIR, { recursive: true });
22
22
 
23
23
  const HEALTH_CACHE_TTL_MS = 5 * 60 * 1000;
24
24
 
25
+ // A realistic desktop browser UA. Social platforms frequently return 403/bot
26
+ // challenge pages to the default Node/yt-dlp user agents, so we present a
27
+ // browser-like identity for every outbound request.
28
+ const BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
29
+ + 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
30
+
31
+ // Network hardening flags applied to every yt-dlp invocation. Transient
32
+ // failures (rate limits, dropped fragments, slow extractors) are the most
33
+ // common cause of unreliable extraction, so we retry aggressively and cap
34
+ // socket waits instead of failing on the first hiccup.
35
+ const YT_DLP_NETWORK_FLAGS = [
36
+ '--retries', '5',
37
+ '--fragment-retries', '5',
38
+ '--extractor-retries', '3',
39
+ '--socket-timeout', '30',
40
+ '--user-agent', BROWSER_USER_AGENT,
41
+ ];
42
+
43
+ // Platforms that gate most video content behind authentication or aggressive
44
+ // bot detection. For these we always try to attach the user's browser cookies.
45
+ const COOKIE_ASSISTED_PLATFORMS = new Set(['instagram', 'youtube', 'tiktok', 'x']);
46
+
47
+ async function fetchWithBrowserHeaders(url, options = {}) {
48
+ return fetch(url, {
49
+ redirect: 'follow',
50
+ ...options,
51
+ headers: {
52
+ 'user-agent': BROWSER_USER_AGENT,
53
+ 'accept-language': 'en-US,en;q=0.9',
54
+ accept: '*/*',
55
+ ...(options.headers || {}),
56
+ },
57
+ });
58
+ }
59
+
25
60
  function shellEscape(value) {
26
61
  const text = String(value ?? '');
27
62
  if (!text.length) return process.platform === 'win32' ? '""' : "''";
@@ -178,12 +213,21 @@ function classifyExtractionError(error) {
178
213
  if (/unsupported social video url|unsupported url/.test(normalized)) {
179
214
  return { code: 'unsupported_url', message };
180
215
  }
181
- if (/private|login required|sign in to confirm/.test(normalized)) {
216
+ if (/private|login required|sign in to confirm|requested content is not available|account is private/.test(normalized)) {
182
217
  return { code: 'private_or_auth_required', message };
183
218
  }
184
- if (/403|forbidden|blocked/.test(normalized)) {
219
+ if (/\b429\b|rate.?limit|too many requests/.test(normalized)) {
220
+ return { code: 'rate_limited', message };
221
+ }
222
+ if (/\b403\b|forbidden|blocked|not a bot/.test(normalized)) {
185
223
  return { code: 'blocked_or_unavailable', message };
186
224
  }
225
+ if (/timed out|timeout|etimedout|socket hang up|network is unreachable/.test(normalized)) {
226
+ return { code: 'network_error', message };
227
+ }
228
+ if (/this video is unavailable|video unavailable|removed|404|not found/.test(normalized)) {
229
+ return { code: 'content_unavailable', message };
230
+ }
187
231
  return { code: 'social_video_extract_failed', message };
188
232
  }
189
233
 
@@ -358,6 +402,10 @@ class SocialVideoService {
358
402
  }
359
403
  }
360
404
 
405
+ #networkFlags() {
406
+ return YT_DLP_NETWORK_FLAGS.map(shellEscape).join(' ');
407
+ }
408
+
361
409
  async #runCommand(command, options = {}) {
362
410
  const result = await this.cliExecutor.execute(command, {
363
411
  cwd: options.cwd || process.cwd(),
@@ -423,7 +471,7 @@ class SocialVideoService {
423
471
  return browserMetadata;
424
472
  }
425
473
 
426
- const response = await fetch(normalizedUrl, { redirect: 'follow' });
474
+ const response = await fetchWithBrowserHeaders(normalizedUrl);
427
475
  const html = await response.text();
428
476
  const metadata = extractPublicMetadataFromHtml(html, response.url || normalizedUrl);
429
477
  return {
@@ -473,7 +521,7 @@ class SocialVideoService {
473
521
  const infoTemplate = path.join(jobDir, 'media.%(ext)s');
474
522
  const infoPath = path.join(jobDir, 'media.info.json');
475
523
  const cookieArg = cookieFilePath ? ` --cookies ${shellEscape(cookieFilePath)}` : '';
476
- const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist --skip-download --write-info-json --no-clean-infojson${cookieArg} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
524
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()} --skip-download --write-info-json --no-clean-infojson${cookieArg} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
477
525
  await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000 });
478
526
  if (!fileExists(infoPath)) {
479
527
  throw new Error('yt-dlp did not produce an info JSON artifact.');
@@ -514,7 +562,7 @@ class SocialVideoService {
514
562
  }
515
563
 
516
564
  async #readTranscriptFromCaption(captionTrack) {
517
- const response = await fetch(captionTrack.url, { redirect: 'follow' });
565
+ const response = await fetchWithBrowserHeaders(captionTrack.url);
518
566
  if (!response.ok) {
519
567
  throw new Error(`Caption request failed (${response.status}).`);
520
568
  }
@@ -525,7 +573,7 @@ class SocialVideoService {
525
573
  async #transcribeViaStt(context) {
526
574
  const template = path.join(context.jobDir, 'audio.%(ext)s');
527
575
  const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
528
- const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f bestaudio -- ${shellEscape(context.sourceUrl)}`;
576
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg} -o ${shellEscape(template)} -f bestaudio/best -- ${shellEscape(context.sourceUrl)}`;
529
577
  await this.#runCommand(command, { cwd: context.jobDir, timeout: 10 * 60 * 1000 });
530
578
 
531
579
  const audioPath = firstFileMatching(context.jobDir, 'audio.');
@@ -561,7 +609,7 @@ class SocialVideoService {
561
609
  }
562
610
 
563
611
  async #resolveCookieFile(context) {
564
- if (context.platform !== 'instagram') {
612
+ if (!COOKIE_ASSISTED_PLATFORMS.has(context.platform)) {
565
613
  return null;
566
614
  }
567
615
  if (!this.runtimeManager || typeof this.runtimeManager.getBrowserProviderForUser !== 'function') {
@@ -581,7 +629,9 @@ class SocialVideoService {
581
629
  });
582
630
  const cookies = Array.isArray(payload?.cookies) ? payload.cookies : [];
583
631
  if (cookies.length === 0) {
584
- context.warnings.push('Browser cookie export returned no cookies for Instagram.');
632
+ // Not fatal: many videos are public. yt-dlp will simply attempt the
633
+ // extraction without an authenticated session.
634
+ context.warnings.push(`Browser cookie export returned no cookies for ${context.platform}.`);
585
635
  return null;
586
636
  }
587
637
 
@@ -610,7 +660,7 @@ class SocialVideoService {
610
660
  async #extractFrameFromVideo(context) {
611
661
  const template = path.join(context.jobDir, 'video.%(ext)s');
612
662
  const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
613
- const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
663
+ const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
614
664
  await this.#runCommand(downloadCommand, { cwd: context.jobDir, timeout: 14 * 60 * 1000 });
615
665
 
616
666
  const videoPath = firstFileMatching(context.jobDir, 'video.');
@@ -630,7 +680,7 @@ class SocialVideoService {
630
680
  }
631
681
 
632
682
  async #downloadThumbnailArtifact(userId, thumbnailUrl) {
633
- const response = await fetch(thumbnailUrl, { redirect: 'follow' });
683
+ const response = await fetchWithBrowserHeaders(thumbnailUrl);
634
684
  if (!response.ok) {
635
685
  throw new Error(`Thumbnail request failed (${response.status}).`);
636
686
  }