neoagent 2.4.2-beta.5 → 2.4.2-beta.6

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 (40) hide show
  1. package/README.md +48 -18
  2. package/flutter_app/lib/main_models.dart +28 -0
  3. package/flutter_app/lib/main_settings.dart +9 -0
  4. package/flutter_app/lib/src/backend_client.dart +8 -0
  5. package/package.json +1 -1
  6. package/server/db/database.js +112 -0
  7. package/server/http/routes.js +1 -0
  8. package/server/public/.last_build_id +1 -1
  9. package/server/public/flutter_bootstrap.js +2 -2
  10. package/server/public/main.dart.js +10069 -10049
  11. package/server/routes/mcp.js +9 -0
  12. package/server/routes/memory.js +35 -2
  13. package/server/routes/runtime.js +7 -1
  14. package/server/routes/settings.js +34 -1
  15. package/server/routes/skills.js +42 -0
  16. package/server/routes/task_webhooks.js +65 -0
  17. package/server/services/ai/engine.js +525 -24
  18. package/server/services/ai/learning.js +56 -8
  19. package/server/services/ai/providers/anthropic.js +40 -8
  20. package/server/services/ai/providers/google.js +10 -0
  21. package/server/services/ai/providers/openai.js +3 -15
  22. package/server/services/ai/providers/openaiCompatible.js +11 -0
  23. package/server/services/ai/repetitionGuard.js +60 -0
  24. package/server/services/ai/systemPrompt.js +52 -16
  25. package/server/services/ai/taskAnalysis.js +15 -6
  26. package/server/services/ai/toolEvidence.js +3 -1
  27. package/server/services/ai/toolRunner.js +43 -1
  28. package/server/services/ai/toolSelector.js +92 -46
  29. package/server/services/ai/tools.js +89 -9
  30. package/server/services/ai/usage.js +114 -0
  31. package/server/services/manager.js +34 -0
  32. package/server/services/memory/manager.js +170 -4
  33. package/server/services/security/capability_audit.js +107 -0
  34. package/server/services/tasks/adapters/index.js +1 -0
  35. package/server/services/tasks/adapters/webhook.js +14 -0
  36. package/server/services/tasks/runtime.js +1 -1
  37. package/server/services/tasks/webhooks.js +146 -0
  38. package/server/services/workspace/code_navigation.js +194 -0
  39. package/server/services/workspace/structured_data.js +93 -0
  40. package/server/utils/cloud-security.js +24 -2
@@ -3,26 +3,40 @@
3
3
  /**
4
4
  * Tool selection strategy:
5
5
  *
6
- * Built-ins: always passed in full descriptions are capped short by
7
- * compactToolDefinition({ includeDescriptions: true }) in tools.js, so the
8
- * overhead is a fixed ~100 tokens/tool and the model always knows every tool
9
- * that exists.
10
- *
11
- * MCP tools: user-defined and potentially numerous. Include all when the set
12
- * is small; keyword-filter when the registry grows large.
6
+ * Every tool stays visible in the catalog. Only full JSON schemas are limited
7
+ * per model turn because several providers enforce schema-count limits.
13
8
  */
14
9
 
15
- const MCP_ALWAYS_INCLUDE_THRESHOLD = 20;
16
- const MAX_TOOLS = 128; // Strict provider limit (e.g. Github Copilot / OpenAI)
10
+ const MAX_TOOLS = 20;
17
11
  const ALWAYS_INCLUDE_BUILT_INS = [
12
+ 'task_complete',
13
+ 'activate_tools',
14
+ 'think',
18
15
  'send_message',
19
- 'create_task',
20
- 'list_tasks',
21
- 'delete_task',
22
- 'update_task',
16
+ 'send_interim_update',
23
17
  ];
24
18
 
19
+ function compactDescription(value, maxChars = 180) {
20
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
21
+ if (text.length <= maxChars) return text;
22
+ return `${text.slice(0, maxChars - 3).trimEnd()}...`;
23
+ }
24
+
25
+ function buildToolCatalog(tools = []) {
26
+ return tools
27
+ .filter((tool) => String(tool?.name || '').trim())
28
+ .map((tool) => ({
29
+ name: String(tool.name).trim(),
30
+ description: compactDescription(tool.description),
31
+ source: tool.serverId ? `mcp:${tool.serverId}` : 'built-in',
32
+ }))
33
+ .sort((left, right) => left.name.localeCompare(right.name))
34
+ .map((tool) => `${tool.name} | ${tool.source} | ${tool.description || 'No description supplied.'}`)
35
+ .join('\n');
36
+ }
37
+
25
38
  function ensureRequiredTools(selectedTools = [], builtInTools = [], options = {}) {
39
+ const limit = Number(options.maxTools) || MAX_TOOLS;
26
40
  const requiredNames = [...ALWAYS_INCLUDE_BUILT_INS];
27
41
  if (options.widgetId) requiredNames.push('save_widget_snapshot');
28
42
  if (!requiredNames.length) return selectedTools;
@@ -34,7 +48,7 @@ function ensureRequiredTools(selectedTools = [], builtInTools = [], options = {}
34
48
  const required = builtInTools.find((tool) => tool?.name === toolName);
35
49
  if (!required) continue;
36
50
 
37
- if (selected.length < MAX_TOOLS) {
51
+ if (selected.length < limit) {
38
52
  selected.push(required);
39
53
  continue;
40
54
  }
@@ -57,43 +71,75 @@ function ensureRequiredTools(selectedTools = [], builtInTools = [], options = {}
57
71
  return selected;
58
72
  }
59
73
 
60
- function selectMcpTools(task, mcpTools = []) {
61
- if (!mcpTools.length) return [];
62
- if (mcpTools.length <= MCP_ALWAYS_INCLUDE_THRESHOLD) return mcpTools;
74
+ function selectInitialTools(allTools = [], suggestedNames = [], options = {}) {
75
+ const requested = new Set(
76
+ (Array.isArray(suggestedNames) ? suggestedNames : [])
77
+ .map((name) => String(name || '').trim())
78
+ .filter(Boolean),
79
+ );
80
+ const selected = allTools.filter((tool) => requested.has(tool?.name));
81
+ return ensureRequiredTools(selected.slice(0, MAX_TOOLS), allTools, options).slice(0, MAX_TOOLS);
82
+ }
63
83
 
64
- // Large MCP registry: match by tool name, original name, or server id so we
65
- // still surface the right tools without dumping hundreds of schemas.
66
- const normalized = String(task || '').toLowerCase();
67
- const explicitMcp = /\bmcp\b|\bmodel context protocol\b/.test(normalized);
84
+ function activateTools(currentTools = [], allTools = [], requestedNames = [], options = {}) {
85
+ const knownByName = new Map(allTools.map((tool) => [tool?.name, tool]));
86
+ let next = ensureRequiredTools(currentTools, allTools, options);
87
+ const activated = [];
88
+ const evicted = [];
89
+ const unknown = [];
90
+ const notActivated = [];
91
+ const requested = [...new Set(
92
+ (Array.isArray(requestedNames) ? requestedNames : [])
93
+ .map((rawName) => String(rawName || '').trim())
94
+ .filter(Boolean),
95
+ )];
96
+ for (const name of requested) {
97
+ const tool = knownByName.get(name);
98
+ if (!tool) {
99
+ unknown.push(name);
100
+ continue;
101
+ }
102
+ if (next.some((item) => item?.name === name)) continue;
103
+ if (next.length >= MAX_TOOLS) {
104
+ const replaceIndex = next.findIndex((item) => (
105
+ !ALWAYS_INCLUDE_BUILT_INS.includes(item?.name)
106
+ && !requested.includes(item?.name)
107
+ ));
108
+ if (replaceIndex === -1) {
109
+ notActivated.push(name);
110
+ continue;
111
+ }
112
+ evicted.push(next[replaceIndex].name);
113
+ next.splice(replaceIndex, 1);
114
+ }
115
+ next.push(tool);
116
+ activated.push(name);
117
+ }
118
+ next = ensureRequiredTools(next, allTools, options).slice(0, MAX_TOOLS);
119
+ return {
120
+ tools: next,
121
+ activated,
122
+ evicted,
123
+ unknown,
124
+ notActivated,
125
+ };
126
+ }
68
127
 
69
- return mcpTools.filter((tool) => {
70
- if (explicitMcp) return true;
71
- const name = String(tool.name || '').toLowerCase();
72
- const original = String(tool.originalName || '').toLowerCase();
73
- const server = String(tool.serverId || '').toLowerCase();
74
- return normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
75
- });
128
+ function selectMcpTools(_task, mcpTools = []) {
129
+ return Array.isArray(mcpTools) ? mcpTools : [];
76
130
  }
77
131
 
78
132
  function selectToolsForTask(task, builtInTools = [], mcpTools = [], _options = {}) {
79
133
  const selectedMcp = selectMcpTools(task, mcpTools);
80
- const options = _options || {};
81
- let selected;
82
-
83
- if (builtInTools.length + selectedMcp.length <= MAX_TOOLS) {
84
- selected = [...builtInTools, ...selectedMcp];
85
- return ensureRequiredTools(selected, builtInTools, options);
86
- }
87
-
88
- // If we exceed the limit, prioritize base tools and take as many MCP tools as fit
89
- const remainingSpace = MAX_TOOLS - builtInTools.length;
90
- if (remainingSpace > 0) {
91
- selected = [...builtInTools, ...selectedMcp.slice(0, remainingSpace)];
92
- return ensureRequiredTools(selected, builtInTools, options);
93
- }
94
-
95
- selected = builtInTools.slice(0, MAX_TOOLS);
96
- return ensureRequiredTools(selected, builtInTools, options);
134
+ void _options;
135
+ return [...builtInTools, ...selectedMcp];
97
136
  }
98
137
 
99
- module.exports = { selectToolsForTask, selectMcpTools };
138
+ module.exports = {
139
+ MAX_TOOLS,
140
+ activateTools,
141
+ buildToolCatalog,
142
+ selectInitialTools,
143
+ selectToolsForTask,
144
+ selectMcpTools,
145
+ };
@@ -73,7 +73,7 @@ function compactToolDefinition(tool, options = {}) {
73
73
  };
74
74
 
75
75
  if (options.includeDescriptions) {
76
- compact.description = compactText(tool.description, 120);
76
+ compact.description = compactText(tool.description, 320);
77
77
  }
78
78
 
79
79
  if (tool.parameters?.properties) {
@@ -81,7 +81,7 @@ function compactToolDefinition(tool, options = {}) {
81
81
  for (const [key, value] of Object.entries(tool.parameters.properties)) {
82
82
  properties[key] = { ...value };
83
83
  if (options.includeDescriptions && value.description) {
84
- properties[key].description = compactText(value.description, 70);
84
+ properties[key].description = compactText(value.description, 160);
85
85
  } else {
86
86
  delete properties[key].description;
87
87
  }
@@ -786,9 +786,10 @@ function getAvailableTools(app, options = {}) {
786
786
  type: 'object',
787
787
  properties: {
788
788
  key: { type: 'string', enum: ['user_profile', 'preferences', 'ai_personality'], description: 'user_profile: who the user is, preferences: standing likes/dislikes, ai_personality: concise durable notes for how the agent should behave for this user' },
789
- value: { type: 'string', description: 'Value to set. Keep it concise — this is injected into every single prompt.' }
789
+ value: { type: 'string', description: 'Value to set. Keep it concise — this is injected into every single prompt.' },
790
+ confirmed: { type: 'boolean', description: 'Must be true only when the user explicitly requested this core-memory change in the current conversation.' }
790
791
  },
791
- required: ['key', 'value']
792
+ required: ['key', 'value', 'confirmed']
792
793
  }
793
794
  },
794
795
  {
@@ -919,6 +920,37 @@ function getAvailableTools(app, options = {}) {
919
920
  required: ['path', 'query']
920
921
  }
921
922
  },
923
+ {
924
+ name: 'code_navigate',
925
+ description: 'Navigate source code with ranked lexical matches, AST-derived JavaScript/TypeScript symbols, or workspace-scoped semantic retrieval. Returns excerpts and line references instead of complete files.',
926
+ parameters: {
927
+ type: 'object',
928
+ properties: {
929
+ mode: { type: 'string', enum: ['lexical', 'structure', 'semantic'], description: 'Search mode.' },
930
+ path: { type: 'string', description: 'Workspace file or directory.' },
931
+ query: { type: 'string', description: 'Required for lexical and semantic modes.' },
932
+ include: { type: 'string', description: 'Optional ripgrep glob for lexical mode.' },
933
+ limit: { type: 'number', description: 'Maximum semantic results.' }
934
+ },
935
+ required: ['mode', 'path']
936
+ }
937
+ },
938
+ {
939
+ name: 'query_structured_data',
940
+ description: 'Read workspace CSV, TSV, JSON, or SQLite data using format-aware parsers. SQLite statements must be read-only and parameters are bound separately.',
941
+ parameters: {
942
+ type: 'object',
943
+ properties: {
944
+ path: { type: 'string', description: 'Workspace data file.' },
945
+ sql: { type: 'string', description: 'Read-only SQLite query.' },
946
+ parameters: { type: 'object', description: 'Named SQLite query parameters.' },
947
+ columns: { type: 'array', items: { type: 'string' }, description: 'Columns to return for CSV, TSV, or JSON.' },
948
+ equals: { type: 'object', description: 'Exact field filters for CSV, TSV, or JSON.' },
949
+ limit: { type: 'number', description: 'Maximum rows, capped at 1000.' }
950
+ },
951
+ required: ['path']
952
+ }
953
+ },
922
954
  {
923
955
  name: 'http_request',
924
956
  description: 'Make an HTTP request to any URL',
@@ -989,6 +1021,21 @@ function getAvailableTools(app, options = {}) {
989
1021
  required: ['thought']
990
1022
  }
991
1023
  },
1024
+ {
1025
+ name: 'activate_tools',
1026
+ description: 'Activate tools by exact catalog name for later model turns. When the schema limit is full, unrelated active schemas are replaced; every catalog tool remains available for later activation.',
1027
+ parameters: {
1028
+ type: 'object',
1029
+ properties: {
1030
+ names: {
1031
+ type: 'array',
1032
+ items: { type: 'string' },
1033
+ description: 'Exact tool names from the available tool catalog.'
1034
+ }
1035
+ },
1036
+ required: ['names']
1037
+ }
1038
+ },
992
1039
  {
993
1040
  name: 'spawn_subagent',
994
1041
  description: 'Spawn an independent sub-agent asynchronously. Returns a handle immediately so the parent run can continue, list helpers, wait later, or cancel if plans change.',
@@ -997,7 +1044,9 @@ function getAvailableTools(app, options = {}) {
997
1044
  properties: {
998
1045
  task: { type: 'string', description: 'The task for the sub-agent to complete' },
999
1046
  model: { type: 'string', description: 'Model override for the sub-agent (e.g. gpt-4o-mini for cheap tasks)' },
1000
- context: { type: 'string', description: 'Additional context to pass to the sub-agent' }
1047
+ context: { type: 'string', description: 'Only the constraints and evidence the sub-agent needs.' },
1048
+ required_artifacts: { type: 'array', items: { type: 'string' }, description: 'Artifacts the sub-agent must return.' },
1049
+ tools: { type: 'array', items: { type: 'string' }, description: 'Exact active tool names the sub-agent may need.' }
1001
1050
  },
1002
1051
  required: ['task']
1003
1052
  }
@@ -1879,7 +1928,10 @@ async function executeTool(toolName, args, context, engine) {
1879
1928
  case 'memory_update_core': {
1880
1929
  const { MemoryManager } = require('../memory/manager');
1881
1930
  const mm = new MemoryManager();
1882
- mm.updateCore(userId, args.key, args.value, { agentId });
1931
+ if (args.confirmed !== true) {
1932
+ return { error: 'Core memory updates require explicit current-session user confirmation.' };
1933
+ }
1934
+ mm.updateCore(userId, args.key, args.value, { agentId, confirmed: true });
1883
1935
  return { success: true, key: args.key, message: 'Core memory updated' };
1884
1936
  }
1885
1937
 
@@ -2258,6 +2310,26 @@ async function executeTool(toolName, args, context, engine) {
2258
2310
  }
2259
2311
  }
2260
2312
 
2313
+ case 'code_navigate': {
2314
+ const service = app?.locals?.codeNavigationService;
2315
+ if (!service) return { error: 'Code navigation service is unavailable.' };
2316
+ try {
2317
+ return await service.navigate(userId, args);
2318
+ } catch (err) {
2319
+ return { error: err.message };
2320
+ }
2321
+ }
2322
+
2323
+ case 'query_structured_data': {
2324
+ const service = app?.locals?.structuredDataService;
2325
+ if (!service) return { error: 'Structured data service is unavailable.' };
2326
+ try {
2327
+ return service.query(userId, args);
2328
+ } catch (err) {
2329
+ return { error: err.message };
2330
+ }
2331
+ }
2332
+
2261
2333
  case 'http_request': {
2262
2334
  const controller = new AbortController();
2263
2335
  const timeoutMs = args.timeout_ms || 30000;
@@ -2776,11 +2848,12 @@ async function executeTool(toolName, args, context, engine) {
2776
2848
 
2777
2849
  case 'spawn_subagent': {
2778
2850
  try {
2779
- const task = args.context ? `${args.task}\n\nContext: ${args.context}` : args.task;
2780
- return await engine.spawnSubagent(userId, runId, task, {
2851
+ return await engine.spawnSubagent(userId, runId, args.task, {
2781
2852
  app,
2782
2853
  model: args.model || null,
2783
2854
  context: args.context || null,
2855
+ requiredArtifacts: args.required_artifacts || [],
2856
+ selectedTools: args.tools || engine.getActiveTools(runId).map((tool) => tool.name),
2784
2857
  agentId,
2785
2858
  });
2786
2859
  } catch (err) {
@@ -2788,6 +2861,13 @@ async function executeTool(toolName, args, context, engine) {
2788
2861
  }
2789
2862
  }
2790
2863
 
2864
+ case 'activate_tools':
2865
+ try {
2866
+ return engine.activateToolsForRun(runId, args.names || []);
2867
+ } catch (err) {
2868
+ return { error: `activate_tools failed: ${err.message}` };
2869
+ }
2870
+
2791
2871
  case 'delegate_to_agent': {
2792
2872
  try {
2793
2873
  if (triggerSource === 'agent_delegation') {
@@ -2862,7 +2942,7 @@ async function executeTool(toolName, args, context, engine) {
2862
2942
 
2863
2943
  const skillRunner = sk();
2864
2944
  if (skillRunner) {
2865
- const skillResult = await skillRunner.executeTool(toolName, args, { userId });
2945
+ const skillResult = await skillRunner.executeTool(toolName, args, { userId, agentId, runId });
2866
2946
  if (skillResult !== null) return skillResult;
2867
2947
  }
2868
2948
 
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../db/database');
4
+
5
+ function finiteToken(value) {
6
+ const parsed = Number(value);
7
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
8
+ }
9
+
10
+ function normalizeUsage(usage = {}) {
11
+ if (!usage || typeof usage !== 'object') return null;
12
+ const inputTokens = finiteToken(
13
+ usage.inputTokens
14
+ ?? usage.promptTokens
15
+ ?? usage.input_tokens
16
+ ?? usage.prompt_tokens,
17
+ );
18
+ const outputTokens = finiteToken(
19
+ usage.outputTokens
20
+ ?? usage.completionTokens
21
+ ?? usage.output_tokens
22
+ ?? usage.completion_tokens,
23
+ );
24
+ const reasoningTokens = finiteToken(
25
+ usage.reasoningTokens
26
+ ?? usage.reasoning_tokens
27
+ ?? usage.output_tokens_details?.reasoning_tokens
28
+ ?? usage.completion_tokens_details?.reasoning_tokens,
29
+ );
30
+ const cachedReadTokens = finiteToken(
31
+ usage.cachedReadTokens
32
+ ?? usage.cached_read_tokens
33
+ ?? usage.cache_read_input_tokens
34
+ ?? usage.prompt_tokens_details?.cached_tokens
35
+ ?? usage.input_tokens_details?.cached_tokens
36
+ ?? usage.cachedContentTokenCount,
37
+ );
38
+ const cacheWriteTokens = finiteToken(
39
+ usage.cacheWriteTokens
40
+ ?? usage.cache_write_tokens
41
+ ?? usage.cache_creation_input_tokens,
42
+ );
43
+ const explicitTotal = finiteToken(usage.totalTokens ?? usage.total_tokens);
44
+ const totalTokens = explicitTotal || inputTokens + outputTokens;
45
+ return {
46
+ inputTokens,
47
+ outputTokens,
48
+ reasoningTokens,
49
+ cachedReadTokens,
50
+ cacheWriteTokens,
51
+ totalTokens,
52
+ };
53
+ }
54
+
55
+ function mergeUsage(left, right) {
56
+ const a = normalizeUsage(left) || normalizeUsage({});
57
+ const b = normalizeUsage(right) || normalizeUsage({});
58
+ return {
59
+ inputTokens: a.inputTokens + b.inputTokens,
60
+ outputTokens: a.outputTokens + b.outputTokens,
61
+ reasoningTokens: a.reasoningTokens + b.reasoningTokens,
62
+ cachedReadTokens: a.cachedReadTokens + b.cachedReadTokens,
63
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
64
+ totalTokens: a.totalTokens + b.totalTokens,
65
+ };
66
+ }
67
+
68
+ function recordModelUsage({
69
+ runId,
70
+ stepId = null,
71
+ userId,
72
+ agentId = null,
73
+ provider,
74
+ model,
75
+ phase = 'model_turn',
76
+ usage,
77
+ latencyMs = 0,
78
+ estimatedCostUsd = null,
79
+ metadata = {},
80
+ }) {
81
+ const normalized = normalizeUsage(usage);
82
+ if (!runId || !userId || !provider || !model || !normalized) return null;
83
+ const result = db.prepare(
84
+ `INSERT INTO agent_model_usage (
85
+ run_id, step_id, user_id, agent_id, provider, model, phase,
86
+ input_tokens, output_tokens, reasoning_tokens, cached_read_tokens,
87
+ cache_write_tokens, total_tokens, estimated_cost_usd, latency_ms, metadata_json
88
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
89
+ ).run(
90
+ runId,
91
+ stepId,
92
+ userId,
93
+ agentId,
94
+ String(provider),
95
+ String(model),
96
+ String(phase),
97
+ normalized.inputTokens,
98
+ normalized.outputTokens,
99
+ normalized.reasoningTokens,
100
+ normalized.cachedReadTokens,
101
+ normalized.cacheWriteTokens,
102
+ normalized.totalTokens,
103
+ Number.isFinite(Number(estimatedCostUsd)) ? Number(estimatedCostUsd) : null,
104
+ Math.max(0, Math.floor(Number(latencyMs) || 0)),
105
+ JSON.stringify(metadata && typeof metadata === 'object' ? metadata : {}),
106
+ );
107
+ return Number(result.lastInsertRowid);
108
+ }
109
+
110
+ module.exports = {
111
+ mergeUsage,
112
+ normalizeUsage,
113
+ recordModelUsage,
114
+ };
@@ -21,6 +21,11 @@ const { MemoryIngestionService } = require('./memory/ingestion');
21
21
  const { ArtifactStore } = require('./artifacts/store');
22
22
  const { RuntimeManager } = require('./runtime/manager');
23
23
  const { WorkspaceManager } = require('./workspace/manager');
24
+ const { CodeNavigationService } = require('./workspace/code_navigation');
25
+ const { StructuredDataService } = require('./workspace/structured_data');
26
+ const { TaskWebhookService } = require('./tasks/webhooks');
27
+ const { LearningManager } = require('./ai/learning');
28
+ const { CapabilityAuditService } = require('./security/capability_audit');
24
29
  const { BrowserExtensionRegistry } = require('./browser/extension/registry');
25
30
  const { DesktopCompanionRegistry } = require('./desktop/registry');
26
31
  const { DesktopProvider } = require('./desktop/provider');
@@ -52,6 +57,8 @@ function createArtifactStore(app) {
52
57
 
53
58
  function createWorkspaceManager(app) {
54
59
  const workspaceManager = registerLocal(app, 'workspaceManager', new WorkspaceManager());
60
+ registerLocal(app, 'codeNavigationService', new CodeNavigationService({ workspaceManager }));
61
+ registerLocal(app, 'structuredDataService', new StructuredDataService({ workspaceManager }));
55
62
  logServiceReady('Workspace manager ready');
56
63
  return workspaceManager;
57
64
  }
@@ -88,6 +95,22 @@ function createDesktopCompanionRegistry(app) {
88
95
 
89
96
  function createMemoryManager(app) {
90
97
  const memoryManager = registerLocal(app, 'memoryManager', new MemoryManager());
98
+ const reconcile = () => {
99
+ const users = db.prepare('SELECT id FROM users').all();
100
+ for (const user of users) {
101
+ const agents = db.prepare("SELECT id FROM agents WHERE user_id = ? AND status = 'active'").all(user.id);
102
+ for (const agent of agents) {
103
+ try {
104
+ memoryManager.reconcileFacts(user.id, { agentId: agent.id });
105
+ } catch (err) {
106
+ console.warn('[Memory] Fact reconciliation failed:', err.message);
107
+ }
108
+ }
109
+ }
110
+ };
111
+ const timer = setInterval(reconcile, 6 * 60 * 60 * 1000);
112
+ timer.unref?.();
113
+ registerLocal(app, 'memoryReconciliationTimer', timer);
91
114
  logServiceReady('Memory manager ready');
92
115
  return memoryManager;
93
116
  }
@@ -473,6 +496,12 @@ async function startServices(app, io) {
473
496
  skillRunner,
474
497
  workspaceManager: app.locals.workspaceManager,
475
498
  });
499
+ registerLocal(app, 'learningManager', new LearningManager(skillRunner, io));
500
+ registerLocal(app, 'capabilityAuditService', new CapabilityAuditService({
501
+ mcpClient,
502
+ skillRunner,
503
+ }));
504
+ agentEngine.learningManager = app.locals.learningManager;
476
505
 
477
506
  createMultiStep(app, agentEngine, io);
478
507
  createCommandRouter(app);
@@ -499,6 +528,7 @@ async function startServices(app, io) {
499
528
  });
500
529
 
501
530
  const taskRuntime = startTaskRuntime(app, io, agentEngine);
531
+ registerLocal(app, 'taskWebhookService', new TaskWebhookService({ taskRuntime }));
502
532
 
503
533
  configureRealtime(app, io, {
504
534
  agentEngine,
@@ -525,6 +555,10 @@ async function startServices(app, io) {
525
555
  async function stopServices(app) {
526
556
  const tasks = [];
527
557
  console.log('[Services] Stopping services');
558
+ if (app.locals.memoryReconciliationTimer) {
559
+ clearInterval(app.locals.memoryReconciliationTimer);
560
+ app.locals.memoryReconciliationTimer = null;
561
+ }
528
562
 
529
563
  if (app.locals.taskRuntime) {
530
564
  tasks.push(