neoagent 3.0.1-beta.8 → 3.1.0

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.
Files changed (100) hide show
  1. package/.env.example +2 -10
  2. package/README.md +12 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +34 -8
  6. package/docs/configuration.md +11 -2
  7. package/docs/getting-started.md +10 -8
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +3 -0
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +38 -13
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1612 -214
  14. package/flutter_app/lib/main_controller.dart +398 -26
  15. package/flutter_app/lib/main_devices.dart +293 -207
  16. package/flutter_app/lib/main_models.dart +142 -0
  17. package/flutter_app/lib/main_navigation.dart +19 -1
  18. package/flutter_app/lib/main_operations.dart +288 -9
  19. package/flutter_app/lib/main_settings.dart +510 -300
  20. package/flutter_app/lib/main_shared.dart +2 -0
  21. package/flutter_app/lib/main_timeline.dart +1378 -0
  22. package/flutter_app/lib/src/backend_client.dart +149 -19
  23. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  24. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  25. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  26. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  27. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  29. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  30. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  31. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  32. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  33. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  34. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  35. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  36. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  37. package/landing/index.html +3 -3
  38. package/lib/manager.js +184 -66
  39. package/package.json +4 -1
  40. package/server/admin/access.js +12 -7
  41. package/server/admin/admin.css +78 -0
  42. package/server/admin/admin.js +436 -10
  43. package/server/admin/billing.js +165 -42
  44. package/server/admin/index.html +80 -5
  45. package/server/admin/users.js +15 -15
  46. package/server/db/database.js +125 -20
  47. package/server/http/routes.js +1 -0
  48. package/server/http/static.js +4 -2
  49. package/server/public/.last_build_id +1 -1
  50. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  51. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  52. package/server/public/canvaskit/wimp.wasm +0 -0
  53. package/server/public/flutter_bootstrap.js +2 -2
  54. package/server/public/main.dart.js +91077 -87037
  55. package/server/routes/account.js +53 -0
  56. package/server/routes/admin.js +345 -64
  57. package/server/routes/agents.js +203 -21
  58. package/server/routes/billing.js +5 -0
  59. package/server/routes/browser.js +8 -1
  60. package/server/routes/recordings.js +96 -6
  61. package/server/routes/screenHistory.js +140 -2
  62. package/server/routes/timeline.js +43 -0
  63. package/server/services/account/erasure.js +263 -0
  64. package/server/services/ai/hooks.js +4 -1
  65. package/server/services/ai/loop/agent_engine_core.js +8 -1
  66. package/server/services/ai/loop/blank_recovery.js +36 -0
  67. package/server/services/ai/loop/conversation_loop.js +166 -33
  68. package/server/services/ai/messagingFallback.js +22 -0
  69. package/server/services/ai/rate_limits.js +28 -5
  70. package/server/services/ai/systemPrompt.js +6 -5
  71. package/server/services/ai/taskAnalysis.js +2 -0
  72. package/server/services/ai/toolEvidence.js +15 -0
  73. package/server/services/ai/toolResult.js +40 -0
  74. package/server/services/ai/tools.js +163 -4
  75. package/server/services/android/controller.js +6 -2
  76. package/server/services/billing/plans.js +2 -1
  77. package/server/services/browser/anti_detection.js +192 -0
  78. package/server/services/browser/controller.js +180 -54
  79. package/server/services/desktop/auth.js +3 -0
  80. package/server/services/desktop/registry.js +50 -2
  81. package/server/services/integrations/google/calendar.js +22 -14
  82. package/server/services/manager.js +12 -42
  83. package/server/services/memory/ingestion_chunking.js +268 -0
  84. package/server/services/messaging/telnyx.js +9 -8
  85. package/server/services/recordings/manager.js +60 -27
  86. package/server/services/runtime/backends/local-vm.js +40 -22
  87. package/server/services/runtime/docker-vm-manager.js +157 -266
  88. package/server/services/runtime/guest_bootstrap.js +17 -5
  89. package/server/services/runtime/guest_image.js +188 -0
  90. package/server/services/runtime/manager.js +0 -1
  91. package/server/services/runtime/validation.js +3 -8
  92. package/server/services/social_video/service.js +60 -10
  93. package/server/services/tasks/runtime.js +234 -9
  94. package/server/services/tasks/schedule_utils.js +5 -5
  95. package/server/services/tasks/task_repository.js +13 -0
  96. package/server/services/timeline/service.js +558 -0
  97. package/server/services/wearable/gateway.js +1 -1
  98. package/server/services/websocket.js +21 -3
  99. package/server/services/desktop/screenRecorder.js +0 -292
  100. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -0,0 +1,263 @@
1
+ 'use strict';
2
+
3
+ // Complete, schema-driven erasure and export of a single user's data.
4
+ //
5
+ // Both the admin "GDPR: delete all user data" action and the self-service
6
+ // account-deletion endpoint go through eraseUserData() so the two can never
7
+ // drift apart. Rather than hand-maintain a list of tables (which previously
8
+ // missed billing, 2FA, embeddings and other user-scoped tables), we discover
9
+ // every table that has a `user_id` column at runtime via PRAGMA table_info and
10
+ // clear all of them. Child tables that are keyed by a parent id instead of
11
+ // user_id are cleared explicitly below.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const db = require('../../db/database');
16
+ const sessionsDb = require('../../db/sessions_db');
17
+ const { DATA_DIR, AGENT_DATA_DIR } = require('../../../runtime/paths');
18
+ const { sanitizeWorkspaceKey } = require('../workspace/manager');
19
+
20
+ // Tables that carry a `user_id` column but are global/shared configuration and
21
+ // must never be deleted as part of erasing one user.
22
+ const GLOBAL_TABLES = new Set(['billing_plans']);
23
+
24
+ // Column names whose values are credentials/secrets and must be redacted from a
25
+ // portability export (the user already controls these; echoing them back adds
26
+ // risk without adding portability value).
27
+ const SENSITIVE_COLUMN = /pass|secret|token|hash|cipher|encrypt|_iv$|nonce|totp|recovery|priv(ate)?_?key|api_?key|credential/i;
28
+
29
+ function userScopedTables() {
30
+ const rows = db
31
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
32
+ .all();
33
+ const tables = [];
34
+ for (const { name } of rows) {
35
+ if (name === 'users' || GLOBAL_TABLES.has(name) || name.startsWith('admin_')) {
36
+ continue;
37
+ }
38
+ if (name.startsWith('sqlite_')) {
39
+ continue;
40
+ }
41
+ let columns;
42
+ try {
43
+ columns = db.prepare(`PRAGMA table_info(${name})`).all();
44
+ } catch {
45
+ continue;
46
+ }
47
+ if (columns.some((column) => column.name === 'user_id')) {
48
+ tables.push(name);
49
+ }
50
+ }
51
+ return tables;
52
+ }
53
+
54
+ // Child tables keyed by a parent id rather than user_id. Foreign keys are
55
+ // declared ON DELETE CASCADE, so with PRAGMA foreign_keys = ON these are removed
56
+ // automatically when the parent row goes — but we delete them explicitly first
57
+ // so erasure is correct even if foreign keys are ever disabled.
58
+ function deleteChildRows(uid) {
59
+ const childDeletes = [
60
+ 'DELETE FROM conversation_messages WHERE conversation_id IN (SELECT id FROM conversations WHERE user_id = ?)',
61
+ 'DELETE FROM agent_steps WHERE run_id IN (SELECT id FROM agent_runs WHERE user_id = ?)',
62
+ 'DELETE FROM ai_widget_snapshots WHERE widget_id IN (SELECT id FROM ai_widgets WHERE user_id = ?)',
63
+ 'DELETE FROM recording_transcript_segments WHERE session_id IN (SELECT id FROM recording_sessions WHERE user_id = ?)',
64
+ 'DELETE FROM recording_chunks WHERE source_id IN (SELECT s.id FROM recording_sources s JOIN recording_sessions rs ON s.session_id = rs.id WHERE rs.user_id = ?)',
65
+ 'DELETE FROM recording_sources WHERE session_id IN (SELECT id FROM recording_sessions WHERE user_id = ?)',
66
+ 'DELETE FROM memory_source_links WHERE memory_id IN (SELECT id FROM memories WHERE user_id = ?)',
67
+ ];
68
+ for (const sql of childDeletes) {
69
+ try {
70
+ db.prepare(sql).run(uid);
71
+ } catch {
72
+ // Table may not exist on older schema versions — ignore.
73
+ }
74
+ }
75
+ }
76
+
77
+ function collectArtifactPaths(uid) {
78
+ try {
79
+ return db
80
+ .prepare('SELECT storage_path FROM artifacts WHERE user_id = ?')
81
+ .all(uid)
82
+ .map((row) => row.storage_path)
83
+ .filter(Boolean);
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
89
+ function removeArtifactFiles(uid, storagePaths) {
90
+ const artifactsRoot = path.resolve(path.join(DATA_DIR, 'artifacts'));
91
+ for (const storagePath of storagePaths) {
92
+ try {
93
+ const abs = path.resolve(
94
+ path.isAbsolute(storagePath) ? storagePath : path.join(DATA_DIR, storagePath),
95
+ );
96
+ if (abs.startsWith(artifactsRoot + path.sep)) {
97
+ fs.rmSync(abs, { force: true });
98
+ }
99
+ } catch {
100
+ /* best effort */
101
+ }
102
+ }
103
+ const userArtifactDir = path.resolve(path.join(DATA_DIR, 'artifacts', String(uid)));
104
+ if (userArtifactDir.startsWith(artifactsRoot + path.sep)) {
105
+ try {
106
+ fs.rmSync(userArtifactDir, { recursive: true, force: true });
107
+ } catch {
108
+ /* best effort */
109
+ }
110
+ }
111
+ }
112
+
113
+ function removeWorkspaceDir(uid) {
114
+ const workspacesRoot = path.resolve(path.join(AGENT_DATA_DIR, 'workspaces'));
115
+ const workspaceDir = path.resolve(
116
+ path.join(workspacesRoot, sanitizeWorkspaceKey(uid)),
117
+ );
118
+ if (workspaceDir.startsWith(workspacesRoot + path.sep)) {
119
+ try {
120
+ fs.rmSync(workspaceDir, { recursive: true, force: true });
121
+ } catch {
122
+ /* best effort */
123
+ }
124
+ }
125
+ }
126
+
127
+ function purgeSessionStore(uid) {
128
+ // better-sqlite3-session-store keeps a `sessions` table whose `sess` column is
129
+ // the JSON-serialised session. Remove any row belonging to this user so their
130
+ // login is invalidated everywhere immediately.
131
+ for (const needle of [`%"userId":${uid}%`, `%"userId":"${uid}"%`]) {
132
+ try {
133
+ sessionsDb.prepare('DELETE FROM sessions WHERE sess LIKE ?').run(needle);
134
+ } catch {
135
+ /* table/store may differ — best effort */
136
+ }
137
+ }
138
+ }
139
+
140
+ function killUserRuntime(uid, runtimeManager) {
141
+ try {
142
+ const vmManager = runtimeManager?.browserBackend?.vmManager;
143
+ if (vmManager && typeof vmManager.killVm === 'function') {
144
+ // Fire-and-forget: container teardown should not block the HTTP response.
145
+ Promise.resolve(vmManager.killVm(String(uid))).catch(() => {});
146
+ }
147
+ } catch {
148
+ /* best effort */
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Permanently erase all data belonging to a user (GDPR Art. 17).
154
+ *
155
+ * @param {number|string} userId
156
+ * @param {object} [opts]
157
+ * @param {object} [opts.runtimeManager] live RuntimeManager so the user's
158
+ * sandbox container can be torn down as part of erasure.
159
+ * @returns {{ ok: true, tablesCleared: number }}
160
+ */
161
+ function eraseUserData(userId, opts = {}) {
162
+ const uid = Number(userId);
163
+ if (!Number.isInteger(uid) || uid <= 0) {
164
+ const error = new Error('Invalid user id');
165
+ error.code = 'INVALID_ID';
166
+ throw error;
167
+ }
168
+ const exists = db.prepare('SELECT id FROM users WHERE id = ?').get(uid);
169
+ if (!exists) {
170
+ const error = new Error('User not found');
171
+ error.code = 'NOT_FOUND';
172
+ throw error;
173
+ }
174
+
175
+ const artifactPaths = collectArtifactPaths(uid);
176
+ const tables = userScopedTables();
177
+
178
+ const erase = db.transaction(() => {
179
+ deleteChildRows(uid);
180
+ for (const table of tables) {
181
+ // `table` comes from sqlite_master introspection, never from user input.
182
+ db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).run(uid);
183
+ }
184
+ db.prepare('DELETE FROM users WHERE id = ?').run(uid);
185
+ });
186
+ erase();
187
+
188
+ // Off-database state: files on disk, the running sandbox, and active sessions.
189
+ removeArtifactFiles(uid, artifactPaths);
190
+ removeWorkspaceDir(uid);
191
+ purgeSessionStore(uid);
192
+ killUserRuntime(uid, opts.runtimeManager);
193
+
194
+ return { ok: true, tablesCleared: tables.length };
195
+ }
196
+
197
+ function redactRow(row) {
198
+ const out = {};
199
+ for (const [key, value] of Object.entries(row)) {
200
+ out[key] = SENSITIVE_COLUMN.test(key) && value != null ? '[redacted]' : value;
201
+ }
202
+ return out;
203
+ }
204
+
205
+ /**
206
+ * Build a structured, machine-readable export of a user's personal data
207
+ * (GDPR Art. 20 data portability). Credential/secret columns are redacted.
208
+ *
209
+ * @param {number|string} userId
210
+ * @returns {object}
211
+ */
212
+ function exportUserData(userId) {
213
+ const uid = Number(userId);
214
+ if (!Number.isInteger(uid) || uid <= 0) {
215
+ const error = new Error('Invalid user id');
216
+ error.code = 'INVALID_ID';
217
+ throw error;
218
+ }
219
+ const user = db.prepare('SELECT * FROM users WHERE id = ?').get(uid);
220
+ if (!user) {
221
+ const error = new Error('User not found');
222
+ error.code = 'NOT_FOUND';
223
+ throw error;
224
+ }
225
+
226
+ const data = { account: redactRow(user) };
227
+ for (const table of userScopedTables()) {
228
+ try {
229
+ const rows = db.prepare(`SELECT * FROM ${table} WHERE user_id = ?`).all(uid);
230
+ if (rows.length > 0) {
231
+ data[table] = rows.map(redactRow);
232
+ }
233
+ } catch {
234
+ /* skip unreadable table */
235
+ }
236
+ }
237
+ // Conversation messages are keyed by conversation id, not user_id.
238
+ try {
239
+ const messages = db
240
+ .prepare(
241
+ 'SELECT m.* FROM conversation_messages m JOIN conversations c ON m.conversation_id = c.id WHERE c.user_id = ?',
242
+ )
243
+ .all(uid);
244
+ if (messages.length > 0) {
245
+ data.conversation_messages = messages.map(redactRow);
246
+ }
247
+ } catch {
248
+ /* best effort */
249
+ }
250
+
251
+ return {
252
+ exportedAt: new Date().toISOString(),
253
+ schema: 'neoagent.user-export.v1',
254
+ userId: uid,
255
+ data,
256
+ };
257
+ }
258
+
259
+ module.exports = {
260
+ eraseUserData,
261
+ exportUserData,
262
+ userScopedTables,
263
+ };
@@ -17,11 +17,14 @@
17
17
  * Use for self-improvement, memory consolidation, analytics.
18
18
  * Errors are swallowed — this hook must not affect run outcome.
19
19
  *
20
+ * on_loop_iteration(ctx: { userId, runId, agentId, iteration, triggerType, triggerSource, totalTokens })
21
+ * → Steering. Return { systemSteering } to inject a system message for the
22
+ * current iteration, or { stop: true, reason } to stop before more work.
23
+ *
20
24
  * ── NOT YET WIRED (planned) ────────────────────────────────────────────────
21
25
  *
22
26
  * before_prompt_build — inject extra system messages before model call
23
27
  * after_tool_call — observe/transform tool result after execution
24
- * on_loop_iteration — called at the top of each iteration; can inject steering
25
28
  *
26
29
  * To wire one, call globalHooks.run(event, ctx) at the relevant point in
27
30
  * engine.js and handle the returned object. Follow the before_tool_call
@@ -247,6 +247,13 @@ class AgentEngine {
247
247
  const { MemoryManager } = require('../../memory/manager');
248
248
  const memoryManager = this.memoryManager || new MemoryManager();
249
249
  const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
250
+ const timelineService = context.timelineService || this.app?.locals?.timelineService || null;
251
+ const timelinePrompt = timelineService?.buildPromptContext?.(userId, {
252
+ agentId: context.agentId || null,
253
+ query: context.rawUserMessage || context.userMessage || '',
254
+ limit: 6,
255
+ sources: ['screen', 'tasks', 'runs'],
256
+ }) || '';
250
257
  const skillRunner = context.skillRunner || this.skillRunner || null;
251
258
  const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
252
259
  userId,
@@ -256,7 +263,7 @@ class AgentEngine {
256
263
  }) || '';
257
264
  return {
258
265
  stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
259
- dynamic: promptSections.dynamic,
266
+ dynamic: [promptSections.dynamic, timelinePrompt].filter(Boolean).join('\n\n'),
260
267
  };
261
268
  }
262
269
 
@@ -1,11 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  const { summarizeForLog } = require('../logFormat');
4
+ const { isInternalToolingFailure, normalizeOutgoingMessage } = require('../messagingFallback');
4
5
 
5
6
  function latestFailedToolExecution(toolExecutions = []) {
6
7
  return [...toolExecutions].reverse().find((item) => item && item.ok === false) || null;
7
8
  }
8
9
 
10
+ function isRecoverableInternalToolFailure(toolExecution = null) {
11
+ if (!toolExecution || toolExecution.ok !== false) return false;
12
+ const failure = String(toolExecution.error || toolExecution.summary || toolExecution.status || '').trim();
13
+ return isInternalToolingFailure(failure);
14
+ }
15
+
9
16
  function shouldContinueAfterBlankToolFailure({
10
17
  lastContent = '',
11
18
  failedStepCount = 0,
@@ -18,6 +25,19 @@ function shouldContinueAfterBlankToolFailure({
18
25
  && Boolean(latestFailedToolExecution(toolExecutions));
19
26
  }
20
27
 
28
+ function shouldContinueAfterRecoverableToolFailure({
29
+ lastContent = '',
30
+ remainingIterations = 0,
31
+ toolExecutions = [],
32
+ } = {}) {
33
+ if (Number(remainingIterations || 0) <= 0) return false;
34
+ const failedExecution = latestFailedToolExecution(toolExecutions);
35
+ if (!isRecoverableInternalToolFailure(failedExecution)) return false;
36
+ const visible = normalizeOutgoingMessage(lastContent || '');
37
+ if (!visible) return false;
38
+ return /\b(blocked|could not|cannot|can't|do not have a confirmed finished result|not found|missing|internal tool issue)\b/i.test(visible);
39
+ }
40
+
21
41
  function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
22
42
  const failedExecution = latestFailedToolExecution(toolExecutions);
23
43
  if (!failedExecution) return '';
@@ -30,8 +50,24 @@ function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
30
50
  ].join(' ');
31
51
  }
32
52
 
53
+ function buildRecoverableToolFailureGuidance(toolExecutions = []) {
54
+ const failedExecution = latestFailedToolExecution(toolExecutions);
55
+ if (!failedExecution) return '';
56
+ const toolName = failedExecution.toolName || failedExecution.tool || 'the previous tool';
57
+ const failure = failedExecution.error || failedExecution.summary || failedExecution.status || 'unknown failure';
58
+ return [
59
+ `The latest blocker came from an internal tool/path issue in "${toolName}": ${summarizeForLog(failure, 240)}.`,
60
+ 'This is a recoverable execution problem, not a final user-facing blocker yet.',
61
+ 'Do another autonomous recovery step now: locate the correct file or directory, switch to the right workspace path, use list_directory/search_files to discover the target, or rely on user-provided logs if the file only exists on another server.',
62
+ 'Do not send a blocker reply until those recovery steps are exhausted.',
63
+ ].join(' ');
64
+ }
65
+
33
66
  module.exports = {
34
67
  buildBlankAfterToolFailureGuidance,
68
+ buildRecoverableToolFailureGuidance,
69
+ isRecoverableInternalToolFailure,
35
70
  latestFailedToolExecution,
36
71
  shouldContinueAfterBlankToolFailure,
72
+ shouldContinueAfterRecoverableToolFailure,
37
73
  };