neoagent 3.0.1-beta.24 → 3.0.1-beta.25

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.
@@ -29,21 +29,19 @@ class DiskChunkUploadQueue {
29
29
  bool _draining = false;
30
30
  bool _disposed = false;
31
31
  Timer? _retryTimer;
32
+ final Set<Future<void>> _pendingEnqueues = <Future<void>>{};
32
33
 
33
34
  Future<void> enqueue(PendingChunk chunk) async {
34
35
  if (_disposed) {
35
36
  return;
36
37
  }
37
- await _dir.create(recursive: true);
38
- final base = '${chunk.sourceKey}-${chunk.sequence.toString().padLeft(6, '0')}';
39
- final audioFile = File('${_dir.path}/$base.bin');
40
- final metaFile = File('${_dir.path}/$base.json');
41
- final audioTmp = File('${audioFile.path}.tmp');
42
- final metaTmp = File('${metaFile.path}.tmp');
43
- await audioTmp.writeAsBytes(chunk.bytes, flush: true);
44
- await metaTmp.writeAsString(jsonEncode(chunk.toMeta()), flush: true);
45
- await audioTmp.rename(audioFile.path);
46
- await metaTmp.rename(metaFile.path);
38
+ final write = _persistChunk(chunk);
39
+ _pendingEnqueues.add(write);
40
+ try {
41
+ await write;
42
+ } finally {
43
+ _pendingEnqueues.remove(write);
44
+ }
47
45
  unawaited(drain());
48
46
  }
49
47
 
@@ -129,6 +127,25 @@ class DiskChunkUploadQueue {
129
127
 
130
128
  Future<int> pendingCount() async => (await _listMetaFiles()).length;
131
129
 
130
+ Future<void> _persistChunk(PendingChunk chunk) async {
131
+ await _dir.create(recursive: true);
132
+ final base = '${chunk.sourceKey}-${chunk.sequence.toString().padLeft(6, '0')}';
133
+ final audioFile = File('${_dir.path}/$base.bin');
134
+ final metaFile = File('${_dir.path}/$base.json');
135
+ final audioTmp = File('${audioFile.path}.tmp');
136
+ final metaTmp = File('${metaFile.path}.tmp');
137
+ await audioTmp.writeAsBytes(chunk.bytes, flush: true);
138
+ await metaTmp.writeAsString(jsonEncode(chunk.toMeta()), flush: true);
139
+ await audioTmp.rename(audioFile.path);
140
+ await metaTmp.rename(metaFile.path);
141
+ }
142
+
143
+ Future<void> _waitForPendingEnqueues() async {
144
+ while (_pendingEnqueues.isNotEmpty && !_disposed) {
145
+ await Future.wait<void>(List<Future<void>>.from(_pendingEnqueues));
146
+ }
147
+ }
148
+
132
149
  void _scheduleRetry() {
133
150
  _retryTimer?.cancel();
134
151
  _retryTimer = Timer(retryBackoff, () => unawaited(drain()));
@@ -148,8 +165,9 @@ class DiskChunkUploadQueue {
148
165
  Future<void> flush({Duration timeout = const Duration(seconds: 30)}) async {
149
166
  final deadline = DateTime.now().add(timeout);
150
167
  while (!_disposed && DateTime.now().isBefore(deadline)) {
168
+ await _waitForPendingEnqueues();
151
169
  await drain();
152
- if (await pendingCount() == 0) {
170
+ if (_pendingEnqueues.isEmpty && await pendingCount() == 0) {
153
171
  break;
154
172
  }
155
173
  await Future<void>.delayed(const Duration(milliseconds: 200));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.0.1-beta.24",
3
+ "version": "3.0.1-beta.25",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -506,7 +506,6 @@ db.exec(`
506
506
  CREATE INDEX IF NOT EXISTS idx_browser_extension_pairing_status ON browser_extension_pairing_requests(status, expires_at);
507
507
  CREATE INDEX IF NOT EXISTS idx_browser_extension_tokens_user ON browser_extension_tokens(user_id, status, created_at DESC);
508
508
  CREATE INDEX IF NOT EXISTS idx_browser_extension_tokens_hash_status ON browser_extension_tokens(token_hash, status);
509
- CREATE INDEX IF NOT EXISTS idx_desktop_companion_devices_user ON desktop_companion_devices(user_id, status, created_at DESC);
510
509
  CREATE INDEX IF NOT EXISTS idx_messages_user ON messages(user_id, created_at DESC);
511
510
  CREATE INDEX IF NOT EXISTS idx_messages_platform ON messages(platform, platform_chat_id);
512
511
  CREATE INDEX IF NOT EXISTS idx_messages_dedup ON messages(user_id, platform, platform_msg_id) WHERE platform_msg_id IS NOT NULL;
@@ -1074,7 +1073,6 @@ db.exec(`
1074
1073
  );
1075
1074
 
1076
1075
  CREATE INDEX IF NOT EXISTS idx_screen_history_user ON screen_history(user_id, timestamp DESC);
1077
- CREATE INDEX IF NOT EXISTS idx_screen_history_device ON screen_history(user_id, device_id, captured_at DESC);
1078
1076
  CREATE INDEX IF NOT EXISTS idx_notification_history_user ON notification_history(user_id, timestamp DESC);
1079
1077
  CREATE INDEX IF NOT EXISTS idx_timeline_events_user ON timeline_events(user_id, occurred_at DESC, id DESC);
1080
1078
  CREATE INDEX IF NOT EXISTS idx_timeline_events_source ON timeline_events(user_id, source_kind, occurred_at DESC, id DESC);
@@ -1377,6 +1375,30 @@ function tableHasColumn(tableName, columnName) {
1377
1375
  }
1378
1376
  }
1379
1377
 
1378
+ function createLegacyCompatibleIndexes() {
1379
+ const deferredIndexes = [
1380
+ {
1381
+ table: 'desktop_companion_devices',
1382
+ columns: ['user_id', 'status', 'created_at'],
1383
+ sql: 'CREATE INDEX IF NOT EXISTS idx_desktop_companion_devices_user ON desktop_companion_devices(user_id, status, created_at DESC)',
1384
+ },
1385
+ {
1386
+ table: 'screen_history',
1387
+ columns: ['user_id', 'device_id', 'captured_at'],
1388
+ sql: 'CREATE INDEX IF NOT EXISTS idx_screen_history_device ON screen_history(user_id, device_id, captured_at DESC)',
1389
+ },
1390
+ ];
1391
+
1392
+ for (const { table, columns, sql } of deferredIndexes) {
1393
+ if (!columns.every((column) => tableHasColumn(table, column))) continue;
1394
+ try {
1395
+ db.exec(sql);
1396
+ } catch {
1397
+ // Keep startup resilient for partially migrated local databases.
1398
+ }
1399
+ }
1400
+ }
1401
+
1380
1402
  function createAgentScopedIndexes() {
1381
1403
  const statements = [
1382
1404
  ['agent_runs', 'CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(user_id, agent_id, created_at DESC)'],
@@ -2031,14 +2053,9 @@ rebuildCoreMemoryForAgents();
2031
2053
  migrateIntegrationConnectionsTable();
2032
2054
  migrateIntegrationOauthStatesTable();
2033
2055
  migrateIntegrationProviderConfigsTable();
2056
+ createLegacyCompatibleIndexes();
2034
2057
  createAgentScopedIndexes();
2035
2058
 
2036
- try {
2037
- db.exec('CREATE INDEX IF NOT EXISTS idx_screen_history_device ON screen_history(user_id, device_id, captured_at DESC)');
2038
- } catch {
2039
- // Keep startup resilient for partially migrated local databases.
2040
- }
2041
-
2042
2059
  try {
2043
2060
  db.exec(`
2044
2061
  CREATE TABLE IF NOT EXISTS timeline_events (
@@ -1 +1 @@
1
- edf5036aeed42e698ede929766ca8ff4
1
+ 5bdb494f3a900af0165cb368a571ff54
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"a4ce257c68517c1410f4b48ac9852ab5642a3f
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "645227359" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3511135638" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -138523,7 +138523,7 @@ r===$&&A.b()
138523
138523
  p.push(A.iR(q,A.jq(!1,new A.Y(B.vo,A.cR(new A.cA(B.jR,new A.a9_(r,q),q),q,q),q),!1,B.I,!0),q,q,0,0,0,q))}r=!1
138524
138524
  if(!s.ay)if(!s.ch){r=s.e
138525
138525
  r===$&&A.b()
138526
- r=B.b.t("mqr7lj61-c2c3345").length!==0&&r.b}if(r){r=s.d
138526
+ r=B.b.t("mqr8ict9-258fe74").length!==0&&r.b}if(r){r=s.d
138527
138527
  r===$&&A.b()
138528
138528
  r=r.aD&&!r.av?84:0
138529
138529
  s=s.e
@@ -144618,7 +144618,7 @@ $S:0}
144618
144618
  A.a0a.prototype={}
144619
144619
  A.TU.prototype={
144620
144620
  nl(a){var s=this
144621
- if(B.b.t("mqr7lj61-c2c3345").length===0||s.a!=null)return
144621
+ if(B.b.t("mqr8ict9-258fe74").length===0||s.a!=null)return
144622
144622
  s.Be()
144623
144623
  s.a=A.m1(B.T0,new A.bfY(s))},
144624
144624
  Be(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -144636,7 +144636,7 @@ if(!t.f.b(k)){s=1
144636
144636
  break}i=J.a2(k,"buildId")
144637
144637
  h=i==null?null:B.b.t(J.p(i))
144638
144638
  j=h==null?"":h
144639
- if(J.bd(j)===0||J.f(j,"mqr7lj61-c2c3345")){s=1
144639
+ if(J.bd(j)===0||J.f(j,"mqr8ict9-258fe74")){s=1
144640
144640
  break}n.b=!0
144641
144641
  n.E()
144642
144642
  p=2
@@ -144653,7 +144653,7 @@ case 2:return A.i(o.at(-1),r)}})
144653
144653
  return A.k($async$Be,r)},
144654
144654
  vT(){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
144655
144655
  var $async$vT=A.h(function(a2,a3){if(a2===1){o.push(a3)
144656
- s=p}for(;;)switch(s){case 0:if(B.b.t("mqr7lj61-c2c3345").length===0||n.c){s=1
144656
+ s=p}for(;;)switch(s){case 0:if(B.b.t("mqr8ict9-258fe74").length===0||n.c){s=1
144657
144657
  break}n.c=!0
144658
144658
  n.E()
144659
144659
  p=4
@@ -347,9 +347,45 @@ class TimelineService {
347
347
  return { insertedCount: 0, timelineItems: [] };
348
348
  }
349
349
 
350
+ const batchDedupKeys = new Set();
351
+ const findExistingEntry = this.db.prepare(
352
+ `SELECT id
353
+ FROM screen_history
354
+ WHERE user_id = ?
355
+ AND captured_at = ?
356
+ AND device_id = ?
357
+ AND COALESCE(app_name, '') = ?
358
+ AND COALESCE(window_title, '') = ?
359
+ AND text_content = ?
360
+ LIMIT 1`
361
+ );
362
+
350
363
  const tx = this.db.transaction(() => {
351
364
  const timelineItems = [];
365
+ let insertedCount = 0;
352
366
  for (const entry of normalizedEntries) {
367
+ const dedupKey = JSON.stringify([
368
+ entry.capturedAt,
369
+ normalizedDeviceId,
370
+ entry.appName,
371
+ entry.windowTitle,
372
+ entry.text,
373
+ ]);
374
+ if (batchDedupKeys.has(dedupKey)) {
375
+ continue;
376
+ }
377
+ batchDedupKeys.add(dedupKey);
378
+ const existing = findExistingEntry.get(
379
+ userId,
380
+ entry.capturedAt,
381
+ normalizedDeviceId,
382
+ entry.appName || '',
383
+ entry.windowTitle || '',
384
+ entry.text,
385
+ );
386
+ if (existing) {
387
+ continue;
388
+ }
353
389
  const insert = this.db.prepare(
354
390
  `INSERT INTO screen_history (
355
391
  user_id, timestamp, captured_at, device_id, device_label, app_name, window_title, text_content, ocr_engine, ocr_confidence
@@ -366,6 +402,7 @@ class TimelineService {
366
402
  ocrEngine,
367
403
  entry.ocrConfidence,
368
404
  );
405
+ insertedCount += 1;
369
406
  const screenHistoryId = Number(insert.lastInsertRowid);
370
407
  const timelineItem = this._upsertScreenSession({
371
408
  userId,
@@ -383,7 +420,7 @@ class TimelineService {
383
420
  }
384
421
  }
385
422
  return {
386
- insertedCount: normalizedEntries.length,
423
+ insertedCount,
387
424
  timelineItems,
388
425
  };
389
426
  });