neoagent 2.3.1-beta.89 → 2.3.1-beta.90

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 CHANGED
@@ -9,28 +9,37 @@
9
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-a855f7?style=flat-square" alt="License"></a>
10
10
  </p>
11
11
 
12
- <p align="center">A self-hosted, proactive AI agent with a Flutter client for web and Android.</p>
12
+ <p align="center">Self-hosted AI agent runs as a system service, controls Android over ADB, connects to 15+ messaging platforms, all credentials on your server.</p>
13
+
14
+ <p align="center">
15
+ <img src="demo.gif" alt="NeoAgent demo" width="100%">
16
+ </p>
13
17
 
14
18
  | | | | |
15
19
  | --- | --- | --- | --- |
16
20
  | <img alt="WebUI" src="https://github.com/user-attachments/assets/3c76d59a-b6e3-4698-929b-9c94741ccf1e" height="420"> | <img height="494" alt="Android" src="https://github.com/user-attachments/assets/e8a0af7a-6881-485d-ad52-f3bc6f2023ca"> | <img alt="Mobile Telegram" src="https://github.com/user-attachments/assets/1fd41a9b-5452-4aa4-9478-888c8ad7363a" height="420"> | <img height="494" alt="image" src="https://github.com/user-attachments/assets/d5a57282-0851-4902-9588-d8de4b82d45c"> |
17
21
 
18
-
22
+ - **Android control** — screenshot, observe UI, tap, swipe, type, launch apps, install APKs, `adb shell` — the agent operates Android, not just an app running on it
23
+ - **15+ messaging platforms** — Telegram, WhatsApp, Discord, Signal, Slack, Matrix, iMessage, IRC, LINE, Mattermost, Telnyx Voice
24
+ - **Integrations** — Google Workspace, Microsoft 365, Notion, Home Assistant, Trello, Spotify, Figma
25
+ - **Browser + shell** — VM-isolated server-side browser automation, full PTY terminal
26
+ - **Runs locally** — Ollama support, no API key required; credentials stay in `~/.neoagent/.env`, never in the client
19
27
 
20
28
  ## Install
21
29
 
30
+ Requires Node.js 20+ and QEMU — see [getting started](docs/getting-started.md) for details.
31
+
22
32
  ```bash
23
33
  npm install -g neoagent
24
34
  neoagent install
25
-
26
- neoagent migrate
27
35
  ```
28
36
 
29
- ## Manage the Service
37
+ Runs as a `launchd` user service on macOS and `systemd --user` on Linux.
38
+
39
+ ## Manage
30
40
 
31
41
  ```bash
32
42
  neoagent status
33
- neoagent channel beta
34
43
  neoagent update
35
44
  neoagent fix
36
45
  neoagent logs
@@ -38,7 +47,7 @@ neoagent logs
38
47
 
39
48
  ## Links
40
49
 
41
- [Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Homepage](https://neolabs-systems.github.io/NeoAgent/) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
50
+ [Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Capabilities](docs/capabilities.md) | [Why NeoAgent](docs/why-neoagent.md) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
42
51
 
43
52
  ---
44
53
 
@@ -2,8 +2,6 @@ import 'dart:convert';
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:geolocator/geolocator.dart';
4
4
  import 'package:http/http.dart' as http;
5
- import 'package:shared_preferences/shared_preferences.dart';
6
-
7
5
  class LocationService {
8
6
  static final LocationService _instance = LocationService._internal();
9
7
  factory LocationService() => _instance;
@@ -78,8 +76,8 @@ class LocationService {
78
76
  void _trackLoop(String backendUrl, String token) async {
79
77
  while (_isTracking) {
80
78
  try {
81
- Position position = await Geolocator.getCurrentPosition(
82
- desiredAccuracy: LocationAccuracy.low); // low accuracy = approx position
79
+ await Geolocator.getCurrentPosition(
80
+ locationSettings: const LocationSettings(accuracy: LocationAccuracy.low));
83
81
 
84
82
  // Here we would normally fetch active geofences from local DB or backend
85
83
  // and calculate the distance. If inside a geofence, we trigger:
@@ -47,6 +47,7 @@ import 'features/location/location_service.dart';
47
47
  import 'features/notifications/notification_interceptor.dart';
48
48
  import 'features/onboarding/onboarding_shell.dart';
49
49
 
50
+ part 'main_spacing.dart';
50
51
  part 'main_theme.dart';
51
52
  part 'main_app_shell.dart';
52
53
  part 'main_launcher.dart';
@@ -360,11 +360,11 @@ class _AuthViewState extends State<AuthView> {
360
360
  version: QrVersions.auto,
361
361
  eyeStyle: const QrEyeStyle(
362
362
  eyeShape: QrEyeShape.square,
363
- color: Color(0xFF04111D),
363
+ color: _qrDarkColor,
364
364
  ),
365
365
  dataModuleStyle: const QrDataModuleStyle(
366
366
  dataModuleShape: QrDataModuleShape.square,
367
- color: Color(0xFF04111D),
367
+ color: _qrDarkColor,
368
368
  ),
369
369
  )
370
370
  : controller.isPreparingQrLogin
@@ -818,13 +818,13 @@ class _AuthViewState extends State<AuthView> {
818
818
  version: QrVersions.auto,
819
819
  eyeStyle: const QrEyeStyle(
820
820
  eyeShape: QrEyeShape.square,
821
- color: Color(0xFF04111D),
821
+ color: _qrDarkColor,
822
822
  ),
823
823
  dataModuleStyle:
824
824
  const QrDataModuleStyle(
825
825
  dataModuleShape:
826
826
  QrDataModuleShape.square,
827
- color: Color(0xFF04111D),
827
+ color: _qrDarkColor,
828
828
  ),
829
829
  )
830
830
  : controller.isPreparingQrLogin
@@ -852,14 +852,14 @@ class _AuthViewState extends State<AuthView> {
852
852
  style: FilledButton.styleFrom(
853
853
  minimumSize: const Size.fromHeight(56),
854
854
  backgroundColor: Colors.white,
855
- foregroundColor: const Color(0xFF04111D),
855
+ foregroundColor: _qrDarkColor,
856
856
  ),
857
857
  icon: controller.isPreparingQrLogin
858
858
  ? const SizedBox.square(
859
859
  dimension: 16,
860
860
  child: CircularProgressIndicator(
861
861
  strokeWidth: 2,
862
- color: Color(0xFF04111D),
862
+ color: _qrDarkColor,
863
863
  ),
864
864
  )
865
865
  : const Icon(Icons.qr_code_2_rounded),
@@ -972,7 +972,7 @@ class _AuthViewState extends State<AuthView> {
972
972
  ),
973
973
  child: Padding(
974
974
  padding: EdgeInsets.all(
975
- viewportConstraints.maxWidth < 480 ? 14 : 24,
975
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 14 : 24,
976
976
  ),
977
977
  child: Center(
978
978
  child: ConstrainedBox(
@@ -988,10 +988,10 @@ class _AuthViewState extends State<AuthView> {
988
988
  fillColor: _glassFill,
989
989
  child: Padding(
990
990
  padding: EdgeInsets.fromLTRB(
991
- viewportConstraints.maxWidth < 480 ? 18 : 34,
992
- viewportConstraints.maxWidth < 480 ? 20 : 30,
993
- viewportConstraints.maxWidth < 480 ? 18 : 34,
994
- viewportConstraints.maxWidth < 480 ? 20 : 30,
991
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 18 : 34,
992
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 20 : 30,
993
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 18 : 34,
994
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 20 : 30,
995
995
  ),
996
996
  child: LayoutBuilder(
997
997
  builder: (context, panelConstraints) {
@@ -339,49 +339,52 @@ class _ChatPanelState extends State<ChatPanel> {
339
339
  child: Icon(Icons.call_rounded, color: Colors.white),
340
340
  ),
341
341
  const SizedBox(width: 8),
342
- FilledButton(
343
- onPressed: _isSendingChatMessage
344
- ? null
345
- : () async {
346
- final task = _composerController.text;
347
- if ((task.trim().isEmpty &&
348
- _pendingSharedAttachments.isEmpty) ||
349
- _isSendingChatMessage) {
350
- return;
351
- }
352
- setState(() {
353
- _isSendingChatMessage = true;
354
- });
355
- _composerController.clear();
356
- final outgoingAttachments =
357
- _pendingSharedAttachments;
358
- _clearSharedPayload();
359
- try {
360
- await controller.sendMessage(
361
- task,
362
- sharedAttachments: outgoingAttachments,
363
- );
364
- } finally {
365
- if (mounted) {
366
- setState(() {
367
- _isSendingChatMessage = false;
368
- });
342
+ Tooltip(
343
+ message: 'Send (⌘↩)',
344
+ child: FilledButton(
345
+ onPressed: _isSendingChatMessage
346
+ ? null
347
+ : () async {
348
+ final task = _composerController.text;
349
+ if ((task.trim().isEmpty &&
350
+ _pendingSharedAttachments.isEmpty) ||
351
+ _isSendingChatMessage) {
352
+ return;
369
353
  }
370
- }
371
- },
372
- style: FilledButton.styleFrom(
373
- minimumSize: const Size(46, 42),
374
- padding: const EdgeInsets.symmetric(horizontal: 12),
375
- backgroundColor: _accent,
376
- shape: RoundedRectangleBorder(
377
- borderRadius: BorderRadius.circular(10),
354
+ setState(() {
355
+ _isSendingChatMessage = true;
356
+ });
357
+ _composerController.clear();
358
+ final outgoingAttachments =
359
+ _pendingSharedAttachments;
360
+ _clearSharedPayload();
361
+ try {
362
+ await controller.sendMessage(
363
+ task,
364
+ sharedAttachments: outgoingAttachments,
365
+ );
366
+ } finally {
367
+ if (mounted) {
368
+ setState(() {
369
+ _isSendingChatMessage = false;
370
+ });
371
+ }
372
+ }
373
+ },
374
+ style: FilledButton.styleFrom(
375
+ minimumSize: const Size(46, 42),
376
+ padding: const EdgeInsets.symmetric(horizontal: 12),
377
+ backgroundColor: _accent,
378
+ shape: RoundedRectangleBorder(
379
+ borderRadius: BorderRadius.circular(10),
380
+ ),
381
+ ),
382
+ child: Icon(
383
+ controller.hasLiveRun
384
+ ? Icons.alt_route_rounded
385
+ : Icons.north_east_rounded,
386
+ color: Colors.white,
378
387
  ),
379
- ),
380
- child: Icon(
381
- controller.hasLiveRun
382
- ? Icons.alt_route_rounded
383
- : Icons.north_east_rounded,
384
- color: Colors.white,
385
388
  ),
386
389
  ),
387
390
  ],
@@ -1009,7 +1012,7 @@ class _MessagingMetricCard extends StatelessWidget {
1009
1012
  value,
1010
1013
  style: TextStyle(
1011
1014
  color: _textPrimary,
1012
- fontSize: 26,
1015
+ fontSize: 22,
1013
1016
  fontWeight: FontWeight.w800,
1014
1017
  ),
1015
1018
  ),
@@ -2244,6 +2247,7 @@ Future<_MessagingRuleSelection?> _showMessagingAccessRulePicker(
2244
2247
  required MessagingPlatformDescriptor platform,
2245
2248
  required MessagingAccessCatalog catalog,
2246
2249
  }) async {
2250
+ // Use BottomSheet for contextual actions, AlertDialog for confirmations.
2247
2251
  return showModalBottomSheet<_MessagingRuleSelection>(
2248
2252
  context: context,
2249
2253
  isScrollControlled: true,
@@ -2980,7 +2980,7 @@ class NeoAgentController extends ChangeNotifier {
2980
2980
  await _runDeviceAction(
2981
2981
  () => _backendClient.startAndroidEmulator(backendUrl),
2982
2982
  browser: false,
2983
- refreshAppsAfter: true,
2983
+ refreshAppsAfter: false,
2984
2984
  );
2985
2985
  }
2986
2986
 
@@ -1080,6 +1080,7 @@ class _AndroidNavDock extends StatelessWidget {
1080
1080
  final disabled =
1081
1081
  busy || (!androidOnline && entry.key.startsWith('android_'));
1082
1082
  return IconButton.filledTonal(
1083
+ tooltip: entry.key.replaceAll('_', ' '),
1083
1084
  onPressed: disabled ? null : () => onAction(entry.key),
1084
1085
  icon: Icon(entry.value),
1085
1086
  );
@@ -1132,6 +1133,7 @@ class _SurfaceSwitcher extends StatelessWidget {
1132
1133
  mainAxisAlignment: MainAxisAlignment.center,
1133
1134
  children: <Widget>[
1134
1135
  IconButton.filledTonal(
1136
+ tooltip: 'Previous surface',
1135
1137
  onPressed: onPrevious,
1136
1138
  icon: Icon(Icons.arrow_back_ios_new_rounded),
1137
1139
  ),
@@ -1139,6 +1141,7 @@ class _SurfaceSwitcher extends StatelessWidget {
1139
1141
  Flexible(child: labelColumn),
1140
1142
  const SizedBox(width: 14),
1141
1143
  IconButton.filledTonal(
1144
+ tooltip: 'Next surface',
1142
1145
  onPressed: onNext,
1143
1146
  icon: Icon(Icons.arrow_forward_ios_rounded),
1144
1147
  ),
@@ -1152,6 +1155,7 @@ class _SurfaceSwitcher extends StatelessWidget {
1152
1155
  mainAxisAlignment: MainAxisAlignment.center,
1153
1156
  children: <Widget>[
1154
1157
  IconButton.filledTonal(
1158
+ tooltip: 'Previous surface',
1155
1159
  onPressed: onPrevious,
1156
1160
  icon: Icon(Icons.arrow_back_ios_new_rounded),
1157
1161
  ),
@@ -1159,6 +1163,7 @@ class _SurfaceSwitcher extends StatelessWidget {
1159
1163
  labelColumn,
1160
1164
  const SizedBox(width: 14),
1161
1165
  IconButton.filledTonal(
1166
+ tooltip: 'Next surface',
1162
1167
  onPressed: onNext,
1163
1168
  icon: Icon(Icons.arrow_forward_ios_rounded),
1164
1169
  ),
@@ -1386,7 +1391,10 @@ class _InteractiveSurfacePreviewState
1386
1391
  onPressed: widget.onWakeRequested,
1387
1392
  );
1388
1393
  }
1389
- return GestureDetector(
1394
+ return Semantics(
1395
+ button: true,
1396
+ label: 'Device surface preview — tap to interact, swipe to scroll',
1397
+ child: GestureDetector(
1390
1398
  onTapUp: widget.busy
1391
1399
  ? null
1392
1400
  : (details) async {
@@ -1462,6 +1470,7 @@ class _InteractiveSurfacePreviewState
1462
1470
  const Center(child: CircularProgressIndicator()),
1463
1471
  ],
1464
1472
  ),
1473
+ ),
1465
1474
  );
1466
1475
  },
1467
1476
  ),
@@ -326,9 +326,9 @@ Future<void> _showTrelloSetupDialog(
326
326
  Container(
327
327
  padding: const EdgeInsets.all(8),
328
328
  decoration: BoxDecoration(
329
- color: _danger.withOpacity(0.1),
329
+ color: _danger.withValues(alpha: 0.1),
330
330
  borderRadius: BorderRadius.circular(4),
331
- border: Border.all(color: _danger.withOpacity(0.3)),
331
+ border: Border.all(color: _danger.withValues(alpha: 0.3)),
332
332
  ),
333
333
  child: Text(
334
334
  formError,
@@ -543,7 +543,7 @@ class _TrelloStatusItem extends StatelessWidget {
543
543
  color: _bgSecondary,
544
544
  borderRadius: BorderRadius.circular(8),
545
545
  border: Border.all(
546
- color: isConnected ? _success.withOpacity(0.3) : _border,
546
+ color: isConnected ? _success.withValues(alpha: 0.3) : _border,
547
547
  ),
548
548
  ),
549
549
  child: Row(
@@ -0,0 +1,18 @@
1
+ part of 'main.dart';
2
+
3
+ abstract class AppSpacing {
4
+ static const double xs = 8.0;
5
+ static const double sm = 12.0;
6
+ static const double md = 16.0;
7
+ static const double lg = 24.0;
8
+ static const double xl = 32.0;
9
+ }
10
+
11
+ abstract class AppBreakpoints {
12
+ static const double mobile = 480.0;
13
+ static const double tablet = 960.0;
14
+ }
15
+
16
+ /// Deep dark color used for QR code modules rendered on a white background.
17
+ /// Matches the dark theme's primary background hue for brand consistency.
18
+ const Color _qrDarkColor = Color(0xFF04111D);
@@ -109,6 +109,7 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
109
109
  );
110
110
 
111
111
  return base.copyWith(
112
+ focusColor: palette.accent.withValues(alpha: 0.2),
112
113
  scaffoldBackgroundColor: palette.bgPrimary,
113
114
  colorScheme: base.colorScheme.copyWith(
114
115
  primary: palette.accent,
@@ -229,6 +230,14 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
229
230
  ),
230
231
  ),
231
232
  ),
233
+ elevatedButtonTheme: ElevatedButtonThemeData(
234
+ style: ElevatedButton.styleFrom(
235
+ padding: const EdgeInsets.symmetric(
236
+ horizontal: AppSpacing.md,
237
+ vertical: AppSpacing.sm,
238
+ ),
239
+ ),
240
+ ),
232
241
  appBarTheme: AppBarTheme(
233
242
  backgroundColor: Colors.transparent,
234
243
  surfaceTintColor: Colors.transparent,
@@ -90,7 +90,7 @@ class _ToolsPanelState extends State<ToolsPanel>
90
90
  ),
91
91
  child: TabBar(
92
92
  controller: _tabController,
93
- dividerColor: Colors.transparent,
93
+ dividerColor: _border,
94
94
  indicatorSize: TabBarIndicatorSize.tab,
95
95
  labelStyle: const TextStyle(fontWeight: FontWeight.w700),
96
96
  tabs: <Widget>[
@@ -190,7 +190,7 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
190
190
  ),
191
191
  child: TabBar(
192
192
  controller: _tabController,
193
- dividerColor: Colors.transparent,
193
+ dividerColor: _border,
194
194
  indicatorSize: TabBarIndicatorSize.tab,
195
195
  labelStyle: const TextStyle(fontWeight: FontWeight.w700),
196
196
  tabs: <Widget>[
@@ -251,7 +251,7 @@ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
251
251
 
252
252
  @override
253
253
  Widget build(BuildContext context) {
254
- final compact = MediaQuery.sizeOf(context).width < 960;
254
+ final compact = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
255
255
  return Padding(
256
256
  padding: _pagePadding(context),
257
257
  child: Column(
package/lib/manager.js CHANGED
@@ -288,6 +288,38 @@ function ensureLogDir() {
288
288
  ensureRuntimeDirs();
289
289
  }
290
290
 
291
+ function pruneOldRuntimeBackups(backupsDir, keepLatest = 3) {
292
+ if (!fs.existsSync(backupsDir) || keepLatest < 0) return;
293
+
294
+ const backupDirs = fs
295
+ .readdirSync(backupsDir, { withFileTypes: true })
296
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('pre-update-'))
297
+ .map((entry) => {
298
+ const fullPath = path.join(backupsDir, entry.name);
299
+ let mtimeMs = 0;
300
+ try {
301
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
302
+ } catch {
303
+ // Skip entries that disappear or cannot be statted.
304
+ return null;
305
+ }
306
+ return { name: entry.name, fullPath, mtimeMs };
307
+ })
308
+ .filter(Boolean)
309
+ .sort((a, b) => {
310
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
311
+ return b.name.localeCompare(a.name);
312
+ });
313
+
314
+ for (const backup of backupDirs.slice(keepLatest)) {
315
+ try {
316
+ fs.rmSync(backup.fullPath, { recursive: true, force: true });
317
+ } catch {
318
+ // Best-effort cleanup only.
319
+ }
320
+ }
321
+ }
322
+
291
323
  function backupRuntimeData() {
292
324
  const backupsDir = path.join(RUNTIME_HOME, 'backups');
293
325
  fs.mkdirSync(backupsDir, { recursive: true });
@@ -297,6 +329,7 @@ function backupRuntimeData() {
297
329
 
298
330
  if (fs.existsSync(ENV_FILE)) fs.copyFileSync(ENV_FILE, path.join(target, '.env'));
299
331
  if (fs.existsSync(DATA_DIR)) fs.cpSync(DATA_DIR, path.join(target, 'data'), { recursive: true, force: false, errorOnExist: false });
332
+ pruneOldRuntimeBackups(backupsDir, 3);
300
333
  }
301
334
 
302
335
  function killByPort(port) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.89",
3
+ "version": "2.3.1-beta.90",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -50,6 +50,43 @@ function initializeDatabase(db, dbPath) {
50
50
  return db;
51
51
  }
52
52
 
53
+ function sleepSync(ms) {
54
+ if (ms <= 0) return;
55
+ const buffer = new SharedArrayBuffer(4);
56
+ const view = new Int32Array(buffer);
57
+ Atomics.wait(view, 0, 0, ms);
58
+ }
59
+
60
+ function isSqliteBusyError(error) {
61
+ return Boolean(
62
+ error &&
63
+ (
64
+ error.code === 'SQLITE_BUSY' ||
65
+ error.code === 'SQLITE_LOCKED' ||
66
+ /database is locked|database table is locked/i.test(error.message || '')
67
+ ),
68
+ );
69
+ }
70
+
71
+ function runWithBusyRetry(action, { attempts = 5, delayMs = 50, label = 'SQLite operation' } = {}) {
72
+ let lastError = null;
73
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
74
+ try {
75
+ return action();
76
+ } catch (error) {
77
+ lastError = error;
78
+ if (!isSqliteBusyError(error) || attempt === attempts) {
79
+ throw error;
80
+ }
81
+ console.warn(
82
+ `[Database] ${label} hit a busy lock on attempt ${attempt}/${attempts}; retrying in ${delayMs}ms.`,
83
+ );
84
+ sleepSync(delayMs);
85
+ }
86
+ }
87
+ throw lastError;
88
+ }
89
+
53
90
  let db = new Database(DB_PATH);
54
91
  db = initializeDatabase(db, DB_PATH);
55
92
 
@@ -1047,7 +1084,19 @@ function backfillAgentIds() {
1047
1084
  }
1048
1085
  }
1049
1086
  });
1050
- tx();
1087
+
1088
+ try {
1089
+ runWithBusyRetry(
1090
+ () => tx(),
1091
+ { attempts: 8, label: 'backfillAgentIds' },
1092
+ );
1093
+ } catch (error) {
1094
+ if (isSqliteBusyError(error)) {
1095
+ console.warn('[Database] Skipping backfillAgentIds because the database stayed locked.');
1096
+ return;
1097
+ }
1098
+ throw error;
1099
+ }
1051
1100
  }
1052
1101
 
1053
1102
  function backfillAgentPolicies() {
@@ -1059,21 +1108,30 @@ function backfillAgentPolicies() {
1059
1108
  return;
1060
1109
  }
1061
1110
  try {
1062
- db.prepare(
1063
- `UPDATE agents
1064
- SET can_delegate = COALESCE(can_delegate, 1),
1065
- can_be_delegated_to = COALESCE(can_be_delegated_to, 0),
1066
- delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1067
- WHERE slug = 'main'`
1068
- ).run();
1069
- db.prepare(
1070
- `UPDATE agents
1071
- SET can_delegate = COALESCE(can_delegate, 0),
1072
- can_be_delegated_to = COALESCE(can_be_delegated_to, 1),
1073
- delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1074
- WHERE slug != 'main'`
1075
- ).run();
1076
- } catch {
1111
+ runWithBusyRetry(
1112
+ () => {
1113
+ db.prepare(
1114
+ `UPDATE agents
1115
+ SET can_delegate = COALESCE(can_delegate, 1),
1116
+ can_be_delegated_to = COALESCE(can_be_delegated_to, 0),
1117
+ delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1118
+ WHERE slug = 'main'`
1119
+ ).run();
1120
+ db.prepare(
1121
+ `UPDATE agents
1122
+ SET can_delegate = COALESCE(can_delegate, 0),
1123
+ can_be_delegated_to = COALESCE(can_be_delegated_to, 1),
1124
+ delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1125
+ WHERE slug != 'main'`
1126
+ ).run();
1127
+ },
1128
+ { attempts: 8, label: 'backfillAgentPolicies' },
1129
+ );
1130
+ } catch (error) {
1131
+ if (isSqliteBusyError(error)) {
1132
+ console.warn('[Database] Skipping backfillAgentPolicies because the database stayed locked.');
1133
+ return;
1134
+ }
1077
1135
  // Keep startup resilient for partially migrated databases.
1078
1136
  }
1079
1137
  }
@@ -150,6 +150,7 @@ app.post('/files/read', async (req, res) => {
150
150
  }
151
151
  const realTarget = resolveReadablePath(filePath);
152
152
  if (!realTarget) {
153
+ console.warn('[GuestAgent] files/read rejected path', { requestedPath: filePath });
153
154
  return { error: 'path is outside guest-agent readable roots' };
154
155
  }
155
156
  const data = fs.readFileSync(realTarget);
@@ -1 +1 @@
1
- 565ea9475bb1a5c2efca6dc427d87b38
1
+ 60e7dbe66a3ba1c874f738f6f995f811
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2533429045" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3267367784" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });