feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
+216 -73
View File
@@ -1,100 +1,243 @@
using Nexus.Api.Helpers;
using Nexus.Api.Models;
namespace Nexus.Api.Services;
public sealed class MemoryService : IMemoryService
public sealed class MemoryService(
IOpenClawAgentConfigurationService configuration) : IMemoryService
{
private const string BasePath = "/mnt/workspace-iris/memory";
private const string LongTermPath = "/mnt/workspace-iris/MEMORY.md";
private const int MaxFileSize = 1_000_000;
private const int MaxFiles = 50;
private const string MemoryDirectory = "memory";
public Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync()
public async Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync(
string agentId,
CancellationToken cancellationToken = default)
{
var files = new List<MemoryFileInfo>();
if (File.Exists(LongTermPath))
var normalizedAgentId = NormalizeAgentId(agentId);
var result = new List<MemoryFileInfo>();
var agentFiles = await configuration.GetAgentFilesAsync(
normalizedAgentId,
cancellationToken);
var longTerm = agentFiles.Files.SingleOrDefault(file =>
string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase));
if (longTerm is { Missing: false })
{
var fi = new FileInfo(LongTermPath);
files.Add(new MemoryFileInfo("MEMORY.md", "MEMORY.md", fi.Length, fi.LastWriteTimeUtc));
result.Add(new MemoryFileInfo(
"MEMORY.md",
"MEMORY.md",
longTerm.Size ?? 0,
(longTerm.UpdatedAt ?? agentFiles.CheckedAt).UtcDateTime,
normalizedAgentId,
"MEMORY.md"));
}
if (Directory.Exists(BasePath))
var workspace = await TryGetMemoryWorkspaceAsync(
normalizedAgentId,
cancellationToken);
if (workspace is not null)
{
var memFiles = Directory.GetFiles(BasePath, "*.md")
.Select(f => new FileInfo(f))
.OrderByDescending(f => f.Name)
.Select(f => new MemoryFileInfo(
f.Name,
f.FullName.Replace(BasePath, "").TrimStart('/'),
f.Length,
f.LastWriteTimeUtc));
files.AddRange(memFiles);
result.AddRange(workspace.Entries
.Where(OpenClawContentReadHelpers.IsMarkdownFile)
.OrderByDescending(
entry => entry.Name,
StringComparer.OrdinalIgnoreCase)
.Select(entry => new MemoryFileInfo(
entry.Name,
OpenClawContentReadHelpers.LegacyPath(
entry.Path,
MemoryDirectory),
entry.Size ?? 0,
(entry.UpdatedAt ?? workspace.CheckedAt).UtcDateTime,
normalizedAgentId,
entry.Path)));
}
return Task.FromResult<IReadOnlyList<MemoryFileInfo>>(files);
return result;
}
public async Task<IReadOnlyList<MemorySearchResult>> SearchAsync(string query)
public async Task<IReadOnlyList<MemorySearchResult>> SearchAsync(
string query,
string agentId,
CancellationToken cancellationToken = default)
{
var results = new List<MemorySearchResult>();
async Task SearchDir(string dir)
var normalizedAgentId = NormalizeAgentId(agentId);
var normalizedQuery = query.Trim();
var candidates = new List<MemoryCandidate>();
var agentFiles = await configuration.GetAgentFilesAsync(
normalizedAgentId,
cancellationToken);
if (agentFiles.Files.Any(file =>
string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase)
&& !file.Missing
&& (file.Size is null
|| file.Size <= OpenClawContentReadHelpers.MaxContentBytes)))
{
if (!Directory.Exists(dir)) return;
foreach (var file in Directory.GetFiles(dir, "*.md").Take(MaxFiles))
{
var fi = new FileInfo(file);
if (fi.Length > MaxFileSize) continue;
var content = await File.ReadAllTextAsync(file);
if (!content.Contains(query, StringComparison.OrdinalIgnoreCase)) continue;
candidates.Add(new MemoryCandidate(
"MEMORY.md",
"MEMORY.md",
"MEMORY.md",
true,
0));
}
var idx = content.IndexOf(query, StringComparison.OrdinalIgnoreCase);
var start = Math.Max(0, idx - 60);
var excerpt = (start > 0 ? "…" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "…";
results.Add(new MemorySearchResult(
Path.GetFileName(file),
file.Replace(BasePath, "").TrimStart('/'),
var workspace = await TryGetMemoryWorkspaceAsync(
normalizedAgentId,
cancellationToken);
if (workspace is not null)
{
candidates.AddRange(workspace.Entries
.Where(OpenClawContentReadHelpers.IsMarkdownFile)
.Select(entry => new MemoryCandidate(
entry.Name,
OpenClawContentReadHelpers.LegacyPath(
entry.Path,
MemoryDirectory),
entry.Path,
false,
entry.Size ?? 0)));
}
return await OpenClawContentReadHelpers.SelectBoundedAsync<
MemoryCandidate,
MemorySearchResult>(
candidates,
async (candidate, token) =>
{
string? content;
long size;
if (candidate.AgentFile)
{
var file = await configuration.GetAgentFileAsync(
normalizedAgentId,
"MEMORY.md",
token);
content = file.Missing
|| file.Content is null
|| file.Size > OpenClawContentReadHelpers.MaxContentBytes
|| System.Text.Encoding.UTF8.GetByteCount(
file.Content) >
OpenClawContentReadHelpers.MaxContentBytes
? null
: file.Content;
size = file.Size ?? 0;
}
else
{
var file = await configuration.GetWorkspaceFileAsync(
normalizedAgentId,
candidate.WorkspacePath,
token);
content = OpenClawContentReadHelpers.ReadText(file);
size = file.Size;
}
if (content is null)
return null;
var index = content.IndexOf(
normalizedQuery,
StringComparison.OrdinalIgnoreCase);
if (index < 0)
return null;
var start = Math.Max(0, index - 60);
var length = Math.Min(200, content.Length - start);
var excerpt =
(start > 0 ? "…" : string.Empty)
+ content.Substring(start, length)
+ (start + length < content.Length ? "…" : string.Empty);
return new MemorySearchResult(
candidate.Name,
candidate.LegacyPath,
excerpt,
fi.Length));
}
}
await SearchDir(BasePath);
if (File.Exists(LongTermPath))
{
var content = await File.ReadAllTextAsync(LongTermPath);
if (content.Contains(query, StringComparison.OrdinalIgnoreCase))
{
var idx = content.IndexOf(query, StringComparison.OrdinalIgnoreCase);
var start = Math.Max(0, idx - 60);
var excerpt = (start > 0 ? "…" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "…";
results.Insert(0, new MemorySearchResult("MEMORY.md", "MEMORY.md", excerpt, content.Length));
}
}
return results;
size,
normalizedAgentId,
candidate.WorkspacePath);
},
cancellationToken);
}
public async Task<MemoryFileContent?> GetFileAsync(string name)
public async Task<MemoryFileContent?> GetFileAsync(
string name,
string agentId,
CancellationToken cancellationToken = default)
{
string? filePath;
if (name.Equals("MEMORY.md", StringComparison.OrdinalIgnoreCase))
var normalizedAgentId = NormalizeAgentId(agentId);
if (string.Equals(name, "MEMORY.md", StringComparison.OrdinalIgnoreCase))
{
filePath = LongTermPath;
}
else
{
if (!PathSecurityHelper.TryResolveSafePath(BasePath, name, out filePath))
var file = await configuration.GetAgentFileAsync(
normalizedAgentId,
"MEMORY.md",
cancellationToken);
if (file.Missing
|| file.Content is null
|| file.Size > OpenClawContentReadHelpers.MaxContentBytes
|| System.Text.Encoding.UTF8.GetByteCount(file.Content) >
OpenClawContentReadHelpers.MaxContentBytes)
return null;
return new MemoryFileContent(
"MEMORY.md",
"MEMORY.md",
file.Content,
file.Size ?? 0,
(file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
normalizedAgentId,
"MEMORY.md");
}
if (!File.Exists(filePath!))
if (!OpenClawContentReadHelpers.IsSafeFileName(name))
return null;
var content = await File.ReadAllTextAsync(filePath!);
return new MemoryFileContent(name, name, content, content.Length, File.GetLastWriteTimeUtc(filePath!));
var workspacePath = $"{MemoryDirectory}/{name}";
try
{
var file = await configuration.GetWorkspaceFileAsync(
normalizedAgentId,
workspacePath,
cancellationToken);
var content = OpenClawContentReadHelpers.ReadText(file);
if (content is null)
return null;
return new MemoryFileContent(
file.Name,
name,
content,
file.Size,
(file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
normalizedAgentId,
file.Path);
}
catch (OpenClawGatewayRpcException exception)
when (OpenClawContentReadHelpers.IsNotFound(exception))
{
return null;
}
}
private static string NormalizeAgentId(string? agentId)
=> string.IsNullOrWhiteSpace(agentId)
? "iris"
: agentId.Trim().ToLowerInvariant();
private async Task<OpenClawWorkspaceCollectionDto?>
TryGetMemoryWorkspaceAsync(
string agentId,
CancellationToken cancellationToken)
{
try
{
return await configuration.GetWorkspaceAsync(
agentId,
MemoryDirectory,
0,
OpenClawContentReadHelpers.MaxFiles,
cancellationToken);
}
catch (OpenClawGatewayRpcException exception)
when (OpenClawContentReadHelpers.IsNotFound(exception))
{
return null;
}
}
private sealed record MemoryCandidate(
string Name,
string LegacyPath,
string WorkspacePath,
bool AgentFile,
long Size);
}