neoagent 2.5.2-beta.9 → 3.0.1-beta.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 (52) hide show
  1. package/README.md +5 -1
  2. package/docs/integrations.md +11 -7
  3. package/flutter_app/lib/main_controller.dart +34 -0
  4. package/flutter_app/lib/main_security.dart +19 -0
  5. package/lib/schema_migrations.js +224 -0
  6. package/package.json +2 -2
  7. package/server/admin/admin.css +29 -0
  8. package/server/admin/admin.js +81 -1
  9. package/server/admin/index.html +97 -9
  10. package/server/db/database.js +57 -1
  11. package/server/public/.last_build_id +1 -1
  12. package/server/public/flutter_bootstrap.js +1 -1
  13. package/server/public/main.dart.js +32788 -32752
  14. package/server/routes/security.js +19 -0
  15. package/server/routes/settings.js +1 -0
  16. package/server/routes/skills.js +9 -5
  17. package/server/services/ai/deliverables/artifact_helpers.js +92 -9
  18. package/server/services/ai/deliverables/selector.js +17 -0
  19. package/server/services/ai/hooks.js +3 -2
  20. package/server/services/ai/learning.js +8 -3
  21. package/server/services/ai/loop/agent_engine_core.js +67 -0
  22. package/server/services/ai/loop/blank_recovery.js +37 -0
  23. package/server/services/ai/loop/conversation_loop.js +391 -252
  24. package/server/services/ai/loop/error_recovery.js +38 -0
  25. package/server/services/ai/loop/messaging_delivery.js +52 -6
  26. package/server/services/ai/loop/progress_classification.js +178 -0
  27. package/server/services/ai/loop/progress_monitor.js +6 -5
  28. package/server/services/ai/loop/tool_dispatch.js +1 -0
  29. package/server/services/ai/loopPolicy.js +15 -6
  30. package/server/services/ai/messagingFallback.js +23 -1
  31. package/server/services/ai/preModelCompaction.js +31 -0
  32. package/server/services/ai/repetitionGuard.js +47 -2
  33. package/server/services/ai/settings.js +8 -0
  34. package/server/services/ai/systemPrompt.js +24 -0
  35. package/server/services/ai/taskAnalysis.js +10 -0
  36. package/server/services/ai/toolEvidence.js +39 -3
  37. package/server/services/ai/toolResult.js +29 -0
  38. package/server/services/ai/toolRunner.js +262 -55
  39. package/server/services/ai/toolSelector.js +20 -3
  40. package/server/services/ai/tools.js +201 -31
  41. package/server/services/cli/shell_worker_pool.js +87 -5
  42. package/server/services/integrations/github/common.js +2 -2
  43. package/server/services/integrations/github/repos.js +123 -55
  44. package/server/services/manager.js +16 -0
  45. package/server/services/memory/manager.js +39 -24
  46. package/server/services/runtime/docker-vm-manager.js +21 -1
  47. package/server/services/runtime/manager.js +6 -2
  48. package/server/services/security/approval_gate_service.js +108 -5
  49. package/server/services/security/tool_categories.js +113 -4
  50. package/server/services/security/tool_security_hook.js +7 -2
  51. package/server/services/skills/runtime.js +2 -0
  52. package/server/services/workspace/manager.js +56 -0
@@ -490,29 +490,6 @@ const githubToolDefinitions = [
490
490
  required: ['owner_repo'],
491
491
  },
492
492
  },
493
- {
494
- name: 'github_get_content',
495
- access: 'read',
496
- description: 'Get file or directory contents from a repository.',
497
- parameters: {
498
- type: 'object',
499
- properties: {
500
- owner_repo: {
501
- type: 'string',
502
- description: 'Repository in format "owner/repo".',
503
- },
504
- path: {
505
- type: 'string',
506
- description: 'File or directory path.',
507
- },
508
- ref: {
509
- type: 'string',
510
- description: 'Git ref (branch, tag, or SHA).',
511
- },
512
- },
513
- required: ['owner_repo', 'path'],
514
- },
515
- },
516
493
  {
517
494
  name: 'github_create_or_update_file',
518
495
  access: 'write',
@@ -534,7 +511,12 @@ const githubToolDefinitions = [
534
511
  },
535
512
  content: {
536
513
  type: 'string',
537
- description: 'Base64-encoded file content.',
514
+ description: 'File content as normal UTF-8 text by default. The tool handles the GitHub Contents API encoding.',
515
+ },
516
+ encoding: {
517
+ type: 'string',
518
+ enum: ['utf-8', 'base64'],
519
+ description: 'Input encoding for content. Default utf-8. Use base64 only for already-encoded binary content.',
538
520
  },
539
521
  sha: {
540
522
  type: 'string',
@@ -717,7 +699,7 @@ const githubToolDefinitions = [
717
699
  {
718
700
  name: 'github_api_request',
719
701
  access: 'dynamic_http_method',
720
- description: 'Make an authenticated GitHub API request for advanced operations not covered by dedicated tools.',
702
+ description: 'Make an authenticated GitHub API request for advanced operations not covered by dedicated tools. Path must be the FULL API path starting with /repos/{owner}/{repo}/... — e.g. /repos/NeoLabs-Systems/NeoAgent/git/trees/main?recursive=1. Alternatively, supply owner_repo and a relative path like /git/trees/main and the prefix is prepended automatically.',
721
703
  parameters: {
722
704
  type: 'object',
723
705
  properties: {
@@ -728,7 +710,19 @@ const githubToolDefinitions = [
728
710
  },
729
711
  path: {
730
712
  type: 'string',
731
- description: 'API path or full URL.',
713
+ description: 'Full API path (e.g. /repos/owner/repo/git/trees/main) or a relative path like /git/trees/main when owner_repo is also provided.',
714
+ },
715
+ owner_repo: {
716
+ type: 'string',
717
+ description: 'Repository in "owner/repo" format. When provided together with a relative path, the /repos/{owner}/{repo} prefix is automatically prepended.',
718
+ },
719
+ endpoint: {
720
+ type: 'string',
721
+ description: 'Alias for path.',
722
+ },
723
+ url: {
724
+ type: 'string',
725
+ description: 'Full GitHub API URL (https://api.github.com/...).',
732
726
  },
733
727
  query: {
734
728
  type: 'object',
@@ -738,8 +732,12 @@ const githubToolDefinitions = [
738
732
  type: 'object',
739
733
  description: 'Optional JSON request body.',
740
734
  },
735
+ payload: {
736
+ type: 'object',
737
+ description: 'Alias for body.',
738
+ },
741
739
  },
742
- required: ['method', 'path'],
740
+ required: ['method'],
743
741
  },
744
742
  },
745
743
  ];
@@ -749,6 +747,63 @@ function parseCommaSeparatedList(value) {
749
747
  return String(value).split(',').map((s) => s.trim()).filter(Boolean);
750
748
  }
751
749
 
750
+ function resolveApiRequestPath(args = {}) {
751
+ for (const key of ['path', 'endpoint', 'url']) {
752
+ const value = args?.[key];
753
+ if (typeof value === 'string' && value.trim()) return value.trim();
754
+ }
755
+ return '';
756
+ }
757
+
758
+ function hasOwnerRepoArg(args = {}) {
759
+ if (typeof args.owner_repo === 'string' && args.owner_repo.trim()) return true;
760
+ return Boolean(String(args.owner || '').trim() && String(args.repo || '').trim());
761
+ }
762
+
763
+ function parseOwnerRepoArg(args = {}) {
764
+ if (typeof args.owner_repo === 'string' && args.owner_repo.trim()) {
765
+ return parseOwnerRepo(args.owner_repo);
766
+ }
767
+ const owner = String(args.owner || '').trim();
768
+ const repo = String(args.repo || '').trim();
769
+ if (owner && repo) {
770
+ return parseOwnerRepo(`${owner}/${repo}`);
771
+ }
772
+ return parseOwnerRepo(args.owner_repo);
773
+ }
774
+
775
+ function encodeGithubFileContent(args = {}) {
776
+ const content = String(args.content ?? '');
777
+ const encoding = String(args.encoding || args.content_encoding || 'utf-8').toLowerCase();
778
+ if (encoding === 'base64') {
779
+ return content.replace(/\s/g, '');
780
+ }
781
+ return Buffer.from(content, 'utf8').toString('base64');
782
+ }
783
+
784
+ function resolveApiRequestBody(args = {}) {
785
+ if (args.body && typeof args.body === 'object') return args.body;
786
+ if (args.payload && typeof args.payload === 'object') return args.payload;
787
+ return null;
788
+ }
789
+
790
+ for (const definition of githubToolDefinitions) {
791
+ const parameters = definition.parameters;
792
+ const properties = parameters?.properties;
793
+ if (!properties?.owner_repo) continue;
794
+ properties.owner = {
795
+ type: 'string',
796
+ description: 'Repository owner alias. Use together with repo when owner_repo is not supplied.',
797
+ };
798
+ properties.repo = {
799
+ type: 'string',
800
+ description: 'Repository name alias. Use together with owner when owner_repo is not supplied.',
801
+ };
802
+ if (Array.isArray(parameters.required) && parameters.required.includes('owner_repo')) {
803
+ parameters.required = parameters.required.filter((item) => item !== 'owner_repo');
804
+ }
805
+ }
806
+
752
807
  async function executeGithubTool(toolName, args, auth) {
753
808
  switch (toolName) {
754
809
  case 'github_get_auth_user': {
@@ -768,14 +823,14 @@ async function executeGithubTool(toolName, args, auth) {
768
823
  }
769
824
 
770
825
  case 'github_get_repo': {
771
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
826
+ const { owner, repo } = parseOwnerRepoArg(args);
772
827
  return await githubApiRequest(auth, {
773
828
  path: `/repos/${owner}/${repo}`,
774
829
  });
775
830
  }
776
831
 
777
832
  case 'github_list_issues': {
778
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
833
+ const { owner, repo } = parseOwnerRepoArg(args);
779
834
  const query = { per_page: Math.min(Number(args.max_results) || 30, 100) };
780
835
  if (args.state) query.state = args.state;
781
836
  if (args.labels) query.labels = args.labels;
@@ -789,14 +844,14 @@ async function executeGithubTool(toolName, args, auth) {
789
844
  }
790
845
 
791
846
  case 'github_get_issue': {
792
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
847
+ const { owner, repo } = parseOwnerRepoArg(args);
793
848
  return await githubApiRequest(auth, {
794
849
  path: `/repos/${owner}/${repo}/issues/${Number(args.issue_number)}`,
795
850
  });
796
851
  }
797
852
 
798
853
  case 'github_create_issue': {
799
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
854
+ const { owner, repo } = parseOwnerRepoArg(args);
800
855
  return await githubApiRequest(auth, {
801
856
  method: 'POST',
802
857
  path: `/repos/${owner}/${repo}/issues`,
@@ -810,7 +865,7 @@ async function executeGithubTool(toolName, args, auth) {
810
865
  }
811
866
 
812
867
  case 'github_update_issue': {
813
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
868
+ const { owner, repo } = parseOwnerRepoArg(args);
814
869
  const body = {};
815
870
  if (args.title) body.title = String(args.title);
816
871
  if (args.body !== undefined) body.body = String(args.body);
@@ -825,7 +880,7 @@ async function executeGithubTool(toolName, args, auth) {
825
880
  }
826
881
 
827
882
  case 'github_add_issue_labels': {
828
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
883
+ const { owner, repo } = parseOwnerRepoArg(args);
829
884
  return await githubApiRequest(auth, {
830
885
  method: 'POST',
831
886
  path: `/repos/${owner}/${repo}/issues/${Number(args.issue_number)}/labels`,
@@ -836,7 +891,7 @@ async function executeGithubTool(toolName, args, auth) {
836
891
  }
837
892
 
838
893
  case 'github_add_issue_assignees': {
839
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
894
+ const { owner, repo } = parseOwnerRepoArg(args);
840
895
  return await githubApiRequest(auth, {
841
896
  method: 'POST',
842
897
  path: `/repos/${owner}/${repo}/issues/${Number(args.issue_number)}/assignees`,
@@ -847,7 +902,7 @@ async function executeGithubTool(toolName, args, auth) {
847
902
  }
848
903
 
849
904
  case 'github_list_prs': {
850
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
905
+ const { owner, repo } = parseOwnerRepoArg(args);
851
906
  const query = { per_page: Math.min(Number(args.max_results) || 30, 100) };
852
907
  if (args.state) query.state = args.state;
853
908
  if (args.sort) query.sort = args.sort;
@@ -859,14 +914,14 @@ async function executeGithubTool(toolName, args, auth) {
859
914
  }
860
915
 
861
916
  case 'github_get_pr': {
862
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
917
+ const { owner, repo } = parseOwnerRepoArg(args);
863
918
  return await githubApiRequest(auth, {
864
919
  path: `/repos/${owner}/${repo}/pulls/${Number(args.pr_number)}`,
865
920
  });
866
921
  }
867
922
 
868
923
  case 'github_create_pr': {
869
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
924
+ const { owner, repo } = parseOwnerRepoArg(args);
870
925
  return await githubApiRequest(auth, {
871
926
  method: 'POST',
872
927
  path: `/repos/${owner}/${repo}/pulls`,
@@ -882,7 +937,7 @@ async function executeGithubTool(toolName, args, auth) {
882
937
  }
883
938
 
884
939
  case 'github_update_pr': {
885
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
940
+ const { owner, repo } = parseOwnerRepoArg(args);
886
941
  const body = {};
887
942
  if (args.title) body.title = String(args.title);
888
943
  if (args.body !== undefined) body.body = String(args.body);
@@ -895,7 +950,7 @@ async function executeGithubTool(toolName, args, auth) {
895
950
  }
896
951
 
897
952
  case 'github_merge_pr': {
898
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
953
+ const { owner, repo } = parseOwnerRepoArg(args);
899
954
  return await githubApiRequest(auth, {
900
955
  method: 'PUT',
901
956
  path: `/repos/${owner}/${repo}/pulls/${Number(args.pr_number)}/merge`,
@@ -908,7 +963,7 @@ async function executeGithubTool(toolName, args, auth) {
908
963
  }
909
964
 
910
965
  case 'github_list_commits': {
911
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
966
+ const { owner, repo } = parseOwnerRepoArg(args);
912
967
  const query = { per_page: Math.min(Number(args.max_results) || 30, 100) };
913
968
  if (args.sha) query.sha = String(args.sha);
914
969
  if (args.path) query.path = String(args.path);
@@ -919,7 +974,7 @@ async function executeGithubTool(toolName, args, auth) {
919
974
  }
920
975
 
921
976
  case 'github_list_branches': {
922
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
977
+ const { owner, repo } = parseOwnerRepoArg(args);
923
978
  return await githubApiRequest(auth, {
924
979
  path: `/repos/${owner}/${repo}/branches`,
925
980
  query: buildPaginationParams({ per_page: args.max_results }),
@@ -927,7 +982,7 @@ async function executeGithubTool(toolName, args, auth) {
927
982
  }
928
983
 
929
984
  case 'github_get_branch': {
930
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
985
+ const { owner, repo } = parseOwnerRepoArg(args);
931
986
  const branch = encodeURIComponent(String(args.branch || ''));
932
987
  return await githubApiRequest(auth, {
933
988
  path: `/repos/${owner}/${repo}/branches/${branch}`,
@@ -935,7 +990,7 @@ async function executeGithubTool(toolName, args, auth) {
935
990
  }
936
991
 
937
992
  case 'github_list_collaborators': {
938
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
993
+ const { owner, repo } = parseOwnerRepoArg(args);
939
994
  const maxResults = parsePositiveInt(args.max_results, 30);
940
995
  const perPage = Math.min(100, maxResults);
941
996
  const query = {
@@ -958,23 +1013,28 @@ async function executeGithubTool(toolName, args, auth) {
958
1013
  }
959
1014
 
960
1015
  case 'github_get_content': {
961
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1016
+ const { owner, repo } = parseOwnerRepoArg(args);
962
1017
  const query = {};
963
1018
  if (args.ref) query.ref = String(args.ref);
964
- return await githubApiRequest(auth, {
1019
+ const result = await githubApiRequest(auth, {
965
1020
  path: `/repos/${owner}/${repo}/contents/${String(args.path || '')}`,
966
1021
  query,
967
1022
  });
1023
+ if (result && result.encoding === 'base64' && typeof result.content === 'string') {
1024
+ result.content = Buffer.from(result.content.replace(/\n/g, ''), 'base64').toString('utf8');
1025
+ result.encoding = 'utf8';
1026
+ }
1027
+ return result;
968
1028
  }
969
1029
 
970
1030
  case 'github_create_or_update_file': {
971
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1031
+ const { owner, repo } = parseOwnerRepoArg(args);
972
1032
  return await githubApiRequest(auth, {
973
1033
  method: 'PUT',
974
1034
  path: `/repos/${owner}/${repo}/contents/${String(args.path || '')}`,
975
1035
  body: {
976
1036
  message: String(args.message || ''),
977
- content: String(args.content || ''),
1037
+ content: encodeGithubFileContent(args),
978
1038
  sha: args.sha ? String(args.sha) : undefined,
979
1039
  branch: args.branch ? String(args.branch) : undefined,
980
1040
  },
@@ -982,7 +1042,7 @@ async function executeGithubTool(toolName, args, auth) {
982
1042
  }
983
1043
 
984
1044
  case 'github_delete_file': {
985
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1045
+ const { owner, repo } = parseOwnerRepoArg(args);
986
1046
  return await githubApiRequest(auth, {
987
1047
  method: 'DELETE',
988
1048
  path: `/repos/${owner}/${repo}/contents/${String(args.path || '')}`,
@@ -995,7 +1055,7 @@ async function executeGithubTool(toolName, args, auth) {
995
1055
  }
996
1056
 
997
1057
  case 'github_list_workflow_runs': {
998
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1058
+ const { owner, repo } = parseOwnerRepoArg(args);
999
1059
  let path = `/repos/${owner}/${repo}/actions/runs`;
1000
1060
  if (args.workflow_id) {
1001
1061
  path = `/repos/${owner}/${repo}/actions/workflows/${String(args.workflow_id)}/runs`;
@@ -1024,14 +1084,14 @@ async function executeGithubTool(toolName, args, auth) {
1024
1084
  }
1025
1085
 
1026
1086
  case 'github_get_workflow_run': {
1027
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1087
+ const { owner, repo } = parseOwnerRepoArg(args);
1028
1088
  return await githubApiRequest(auth, {
1029
1089
  path: `/repos/${owner}/${repo}/actions/runs/${Number(args.run_id)}`,
1030
1090
  });
1031
1091
  }
1032
1092
 
1033
1093
  case 'github_trigger_workflow': {
1034
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1094
+ const { owner, repo } = parseOwnerRepoArg(args);
1035
1095
  let inputs = {};
1036
1096
  if (args.inputs) {
1037
1097
  try {
@@ -1051,7 +1111,7 @@ async function executeGithubTool(toolName, args, auth) {
1051
1111
  }
1052
1112
 
1053
1113
  case 'github_list_workflows': {
1054
- const { owner, repo } = parseOwnerRepo(args.owner_repo);
1114
+ const { owner, repo } = parseOwnerRepoArg(args);
1055
1115
  return await githubApiRequest(auth, {
1056
1116
  path: `/repos/${owner}/${repo}/actions/workflows`,
1057
1117
  });
@@ -1085,7 +1145,10 @@ async function executeGithubTool(toolName, args, auth) {
1085
1145
 
1086
1146
  case 'github_api_request': {
1087
1147
  let baseUrl = 'https://api.github.com';
1088
- let path = String(args.path || '');
1148
+ let path = resolveApiRequestPath(args);
1149
+ if (!path) {
1150
+ throw new Error('github_api_request requires path, endpoint, or url.');
1151
+ }
1089
1152
  let query = args.query || null;
1090
1153
  if (path.startsWith('http')) {
1091
1154
  const url = new URL(path);
@@ -1103,12 +1166,17 @@ async function executeGithubTool(toolName, args, auth) {
1103
1166
  ...parsedQuery,
1104
1167
  ...(args.query && typeof args.query === 'object' ? args.query : {}),
1105
1168
  };
1169
+ } else if (hasOwnerRepoArg(args) && !path.startsWith('/repos/') && !path.startsWith('/user') && !path.startsWith('/orgs/') && !path.startsWith('/search/')) {
1170
+ // Convenience: prepend /repos/{owner}/{repo} for relative paths
1171
+ const { owner, repo } = parseOwnerRepoArg(args);
1172
+ const relativePath = path.startsWith('/') ? path : `/${path}`;
1173
+ path = `/repos/${owner}/${repo}${relativePath}`;
1106
1174
  }
1107
1175
  return await githubApiRequest(auth, {
1108
1176
  method: args.method || 'GET',
1109
1177
  path,
1110
1178
  query,
1111
- body: args.body || null,
1179
+ body: resolveApiRequestBody(args),
1112
1180
  baseUrl,
1113
1181
  });
1114
1182
  }
@@ -1121,4 +1189,4 @@ async function executeGithubTool(toolName, args, auth) {
1121
1189
  module.exports = {
1122
1190
  executeGithubTool,
1123
1191
  githubToolDefinitions,
1124
- };
1192
+ };
@@ -571,6 +571,22 @@ async function startServices(app, io) {
571
571
  async function stopServices(app) {
572
572
  const tasks = [];
573
573
  console.log('[Services] Stopping services');
574
+ if (app.locals.approvalGateService && typeof app.locals.approvalGateService.shutdown === 'function') {
575
+ try {
576
+ app.locals.approvalGateService.shutdown();
577
+ logServiceReady('Pending approvals expired');
578
+ } catch (err) {
579
+ console.error('[ApprovalGate] Shutdown error:', getErrorMessage(err));
580
+ }
581
+ }
582
+ if (app.locals.agentEngine && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function') {
583
+ try {
584
+ app.locals.agentEngine.interruptAllActiveRuns();
585
+ logServiceReady('Active runs marked interrupted');
586
+ } catch (err) {
587
+ console.error('[AgentEngine] Interrupt error:', getErrorMessage(err));
588
+ }
589
+ }
574
590
  if (app.locals.memoryManager) {
575
591
  app.locals.memoryManager.stopEmbeddingIndexBackfill();
576
592
  }
@@ -309,6 +309,34 @@ class MemoryManager {
309
309
  return resolveAgentId(userId, options?.agentId || options?.agent_id || null);
310
310
  }
311
311
 
312
+ _findExactMemory(userId, agentId, memoryHash, scope) {
313
+ return db.prepare(
314
+ `SELECT id FROM memories
315
+ WHERE user_id = ? AND agent_id = ? AND archived = 0
316
+ AND memory_hash = ?
317
+ AND COALESCE(scope_type, 'agent') = ?
318
+ AND COALESCE(scope_id, '') = COALESCE(?, '')
319
+ LIMIT 1`
320
+ ).get(userId, agentId, memoryHash, scope.scopeType, scope.scopeId);
321
+ }
322
+
323
+ _reinforceExactMemory(memoryId, importance) {
324
+ db.prepare(
325
+ `UPDATE memories
326
+ SET importance = MAX(importance, ?),
327
+ confidence = MIN(1, confidence + 0.03),
328
+ updated_at = datetime('now')
329
+ WHERE id = ?`
330
+ ).run(importance, memoryId);
331
+ db.prepare(
332
+ `UPDATE memory_facts
333
+ SET confidence = MIN(1, confidence + 0.03),
334
+ updated_at = datetime('now')
335
+ WHERE memory_id = ? AND status = 'active'`
336
+ ).run(memoryId);
337
+ return memoryId;
338
+ }
339
+
312
340
  _backfillMemoryIntelligence(limit = 1000) {
313
341
  try {
314
342
  const rows = db.prepare(
@@ -1602,29 +1630,9 @@ class MemoryManager {
1602
1630
  );
1603
1631
  const embedding = embeddingResult?.vector || null;
1604
1632
 
1605
- const exact = db.prepare(
1606
- `SELECT id FROM memories
1607
- WHERE user_id = ? AND agent_id = ? AND archived = 0
1608
- AND memory_hash = ?
1609
- AND COALESCE(scope_type, 'agent') = ?
1610
- AND COALESCE(scope_id, '') = COALESCE(?, '')
1611
- LIMIT 1`
1612
- ).get(userId, agentId, memoryHash, scope.scopeType, scope.scopeId);
1633
+ const exact = this._findExactMemory(userId, agentId, memoryHash, scope);
1613
1634
  if (exact?.id) {
1614
- db.prepare(
1615
- `UPDATE memories
1616
- SET importance = MAX(importance, ?),
1617
- confidence = MIN(1, confidence + 0.03),
1618
- updated_at = datetime('now')
1619
- WHERE id = ?`
1620
- ).run(importance, exact.id);
1621
- db.prepare(
1622
- `UPDATE memory_facts
1623
- SET confidence = MIN(1, confidence + 0.03),
1624
- updated_at = datetime('now')
1625
- WHERE memory_id = ? AND status = 'active'`
1626
- ).run(exact.id);
1627
- return exact.id;
1635
+ return this._reinforceExactMemory(exact.id, importance);
1628
1636
  }
1629
1637
 
1630
1638
  const duplicateCandidateIds = embedding
@@ -1717,8 +1725,8 @@ class MemoryManager {
1717
1725
 
1718
1726
  // Save new
1719
1727
  const id = uuidv4();
1720
- db.prepare(
1721
- `INSERT INTO memories (
1728
+ const insertResult = db.prepare(
1729
+ `INSERT OR IGNORE INTO memories (
1722
1730
  id, user_id, agent_id, category, scope_type, scope_id, source_type, source_id, source_label,
1723
1731
  stale_after_days, metadata_json, content, summary, importance, confidence, memory_hash, embedding,
1724
1732
  embedding_provider, embedding_model, embedding_dimensions, embedded_at
@@ -1747,6 +1755,13 @@ class MemoryManager {
1747
1755
  embeddingResult?.dimensions || null,
1748
1756
  embedding ? new Date().toISOString() : null,
1749
1757
  );
1758
+ if (insertResult.changes === 0) {
1759
+ const existingExact = this._findExactMemory(userId, agentId, memoryHash, scope);
1760
+ if (existingExact?.id) {
1761
+ return this._reinforceExactMemory(existingExact.id, importance);
1762
+ }
1763
+ throw new Error('Memory insert was ignored but no matching memory row was found');
1764
+ }
1750
1765
 
1751
1766
  this._upsertMemoryIntelligence(userId, agentId, id, {
1752
1767
  content,
@@ -1,11 +1,25 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
4
6
  const http = require('http');
5
7
  const net = require('net');
8
+ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
9
+ const { sanitizeWorkspaceKey } = require('../workspace/manager');
6
10
 
7
11
  const CONTAINER_IMAGE = 'mcr.microsoft.com/playwright:v1.44.0-focal';
8
12
  const CONTAINER_LABEL = 'neoagent.managed=1';
13
+ // The per-user host workspace is bind-mounted here so the shell (execute_command)
14
+ // and the workspace file tools (read_file/write_file/list_directory/search_files)
15
+ // operate on the SAME files. The guest agent defaults its shell cwd to this path.
16
+ const GUEST_WORKSPACE = '/workspace';
17
+
18
+ // Host path of a user's workspace — must match WorkspaceManager's layout exactly
19
+ // (AGENT_DATA_DIR/workspaces/<sanitized key>) so both sides see one directory.
20
+ function hostWorkspaceDir(key) {
21
+ return path.join(AGENT_DATA_DIR, 'workspaces', sanitizeWorkspaceKey(key));
22
+ }
9
23
 
10
24
  // ─── Guest agent ─────────────────────────────────────────────────────────────
11
25
  // Injected into every container. Pure Node.js — only built-in modules + playwright
@@ -104,7 +118,7 @@ const server = http.createServer(async (req, res) => {
104
118
  if (req.method === 'POST' && url === '/exec') {
105
119
  const timeoutMs = Math.min(Number(b.timeout) || 15 * 60 * 1000, 20 * 60 * 1000);
106
120
  const child = spawn('sh', ['-c', b.command || 'true'], {
107
- cwd: b.cwd || '/tmp',
121
+ cwd: b.cwd || '/workspace',
108
122
  env: { ...process.env, ...b.env },
109
123
  });
110
124
  const pid = child.pid;
@@ -325,6 +339,10 @@ class DockerVMManager {
325
339
  const port = await findAvailablePort();
326
340
  console.log(`[DockerVM:${this.profile}] Starting container for user ${key} on port ${port}`);
327
341
 
342
+ // Bind-mount the user's host workspace so shell and file tools share one place.
343
+ const workspaceDir = hostWorkspaceDir(key);
344
+ try { fs.mkdirSync(workspaceDir, { recursive: true }); } catch { /* best effort */ }
345
+
328
346
  const containerId = docker([
329
347
  'run', '-d',
330
348
  '--memory', `${this.memoryMb}m`,
@@ -334,6 +352,8 @@ class DockerVMManager {
334
352
  ...(this.guestToken ? ['-e', `NEOAGENT_VM_GUEST_TOKEN=${this.guestToken}`] : []),
335
353
  '--shm-size=2g',
336
354
  '--security-opt', 'no-new-privileges',
355
+ '-v', `${workspaceDir}:${GUEST_WORKSPACE}`,
356
+ '-w', GUEST_WORKSPACE,
337
357
  '--label', CONTAINER_LABEL,
338
358
  '--label', `neoagent.profile=${this.profile}`,
339
359
  '--label', `neoagent.user=${key}`,
@@ -161,9 +161,13 @@ class RuntimeManager {
161
161
  }
162
162
 
163
163
  async shutdown() {
164
- await Promise.allSettled([
164
+ const tasks = [
165
165
  this.browserBackend?.shutdown?.(),
166
- ]);
166
+ ];
167
+ if (typeof this.shellWorkerPool?.shutdown === 'function') {
168
+ tasks.push(Promise.resolve().then(() => this.shellWorkerPool.shutdown()));
169
+ }
170
+ await Promise.allSettled(tasks);
167
171
  }
168
172
  }
169
173