Ship Assistent 0.8.1: split modules and shared+personal vector memory.

Personal RAG never leaks into the shared store; retrieve merges shared plus the persona chain, with personal overwrite on kind+key.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 00:38:54 +03:00
co-authored by Cursor
parent 6d8aaefc38
commit 880e2dbea2
21 changed files with 3946 additions and 2040 deletions
+64 -33
View File
@@ -189,7 +189,8 @@ public sealed class AssistentConfig
return last;
}
List<string> PersonaExtendsChain(string personaId)
/// <summary>Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).</summary>
public List<string> PersonaExtendsChain(string personaId)
{
List<string> chain = [];
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
@@ -498,27 +499,35 @@ public sealed class AssistentConfig
JObject rules = MergeJsonLayers("rules.json", roots);
string extra = MergeTextLayers("extra.md", roots);
// Legacy personas.json prompt → extra overlay
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
JObject legacy = TryReadJson(overlayJson);
if (legacy?["personas"] is JArray arr)
// Legacy personas.json: only when no overlay persona folder exists for this id
// (gpu-rent now seeds personas/<id>/extra.md instead of dumping personas.json).
string id = SafeId(personaId) ?? "neutral";
string overlayPersonaDir = Path.Combine(_overlayRoot, "personas", id);
bool hasOverlayFolder = Directory.Exists(overlayPersonaDir)
&& (File.Exists(Path.Combine(overlayPersonaDir, "persona.json"))
|| File.Exists(Path.Combine(overlayPersonaDir, "extra.md")));
if (!hasOverlayFolder)
{
string id = SafeId(personaId) ?? "neutral";
foreach (JToken t in arr)
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
JObject legacy = TryReadJson(overlayJson);
if (legacy?["personas"] is JArray arr)
{
if (t is JObject po && string.Equals(SafeId(po["id"]?.ToString()), id, StringComparison.OrdinalIgnoreCase))
foreach (JToken t in arr)
{
string title = po["title"]?.ToString();
if (!string.IsNullOrWhiteSpace(title))
if (t is JObject po && string.Equals(SafeId(po["id"]?.ToString()), id, StringComparison.OrdinalIgnoreCase))
{
persona["title"] = title;
string title = po["title"]?.ToString();
if (!string.IsNullOrWhiteSpace(title))
{
persona["title"] = title;
}
string prompt = po["prompt"]?.ToString();
if (!string.IsNullOrWhiteSpace(prompt))
{
extra = string.IsNullOrWhiteSpace(extra) ? prompt : extra + "\n\n" + prompt;
}
break;
}
string prompt = po["prompt"]?.ToString();
if (!string.IsNullOrWhiteSpace(prompt))
{
extra = string.IsNullOrWhiteSpace(extra) ? prompt : extra + "\n\n" + prompt;
}
break;
}
}
}
@@ -641,35 +650,51 @@ public sealed class AssistentConfig
return sb.ToString().TrimEnd();
}
static IEnumerable<string> PersonaIdsUnder(string root)
{
string dir = Path.Combine(root ?? "", "personas");
if (!Directory.Exists(dir))
{
yield break;
}
foreach (string folder in Directory.GetDirectories(dir))
{
string id = SafeId(Path.GetFileName(folder));
if (id is not null)
{
yield return id;
}
}
}
/// <summary>Seed docs: shared from _base/memory-seed, personal from personas/&lt;id&gt;/memory-seed. Later files overwrite earlier same kind+key in the list; persona is stamped on each doc.</summary>
public List<JObject> LoadMemorySeedDocs()
{
List<JObject> docs = [];
void Scan(string root)
void Scan(string dir, string persona)
{
string dir = Path.Combine(root, "memory-seed");
if (!Directory.Exists(dir))
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
{
return;
}
string stamp = AssistentMemory.NormalizePersona(persona);
foreach (string file in Directory.GetFiles(dir, "*.json").OrderBy(f => f, StringComparer.OrdinalIgnoreCase))
{
try
{
string raw = File.ReadAllText(file, Encoding.UTF8);
JToken parsed = JToken.Parse(raw);
if (parsed is JArray arr)
IEnumerable<JObject> items = parsed is JArray arr
? arr.OfType<JObject>()
: parsed is JObject single ? new[] { single } : [];
foreach (JObject jo in items)
{
foreach (JToken t in arr)
JObject clone = (JObject)jo.DeepClone();
if (clone["persona"] is null || string.IsNullOrWhiteSpace(clone["persona"]?.ToString()))
{
if (t is JObject jo)
{
docs.Add(jo);
}
clone["persona"] = stamp;
}
}
else if (parsed is JObject single)
{
docs.Add(single);
docs.Add(clone);
}
}
catch (Exception ex)
@@ -678,9 +703,15 @@ public sealed class AssistentConfig
}
}
}
Scan(Path.Combine(_bundledRoot, "_base"));
Scan(Path.Combine(_overlayRoot, "_base"));
Scan(Path.Combine(_overlayRoot, "memory-seed"));
Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "memory-seed"), AssistentMemory.SharedPersona);
foreach (string id in PersonaIdsUnder(_bundledRoot).Concat(PersonaIdsUnder(_overlayRoot)).Distinct(StringComparer.OrdinalIgnoreCase))
{
Scan(Path.Combine(_bundledRoot, "personas", id, "memory-seed"), id);
Scan(Path.Combine(_overlayRoot, "personas", id, "memory-seed"), id);
}
return docs;
}