research-copilot 0.1.3 → 0.2.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 (65) hide show
  1. package/app/out/main/index.mjs +588 -81
  2. package/app/out/renderer/assets/{MilkdownMarkdownEditor-CCiFOpuq.js → MilkdownMarkdownEditor-Czh2N6UQ.js} +50 -50
  3. package/app/out/renderer/assets/{arc-BR5G9xaE.js → arc-BWoErJNa.js} +1 -1
  4. package/app/out/renderer/assets/{blockDiagram-c4efeb88-JmvDTsGU.js → blockDiagram-c4efeb88-Bod-vAlS.js} +8 -8
  5. package/app/out/renderer/assets/{c4Diagram-c83219d4-Daf_3gE1.js → c4Diagram-c83219d4-CTVUA_li.js} +3 -3
  6. package/app/out/renderer/assets/{channel-xtutyETs.js → channel-CxGr5Q5E.js} +1 -1
  7. package/app/out/renderer/assets/{classDiagram-beda092f-BFWEqrCW.js → classDiagram-beda092f-DABwUrsU.js} +6 -6
  8. package/app/out/renderer/assets/{classDiagram-v2-2358418a-BQw7RI0A.js → classDiagram-v2-2358418a-CFt8hqf5.js} +10 -10
  9. package/app/out/renderer/assets/{clone-uoV60hcB.js → clone-BL91dKYn.js} +1 -1
  10. package/app/out/renderer/assets/{createText-1719965b-BaRII2sm.js → createText-1719965b-DGkv4rEO.js} +2 -2
  11. package/app/out/renderer/assets/{edges-96097737-CL7Yc4hz.js → edges-96097737-Gf41lQOd.js} +3 -3
  12. package/app/out/renderer/assets/{erDiagram-0228fc6a-B9hgyxu6.js → erDiagram-0228fc6a-Dj75BiRy.js} +5 -5
  13. package/app/out/renderer/assets/{flowDb-c6c81e3f-b_RS-jIJ.js → flowDb-c6c81e3f-C_xVBMxS.js} +1 -1
  14. package/app/out/renderer/assets/{flowDiagram-50d868cf-CPB3IueC.js → flowDiagram-50d868cf-B-lLn2XC.js} +12 -12
  15. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-DM8cFvdZ.js → flowDiagram-v2-4f6560a1-BFnLU3PE.js} +12 -12
  16. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BsxABHy9.js → flowchart-elk-definition-6af322e1-DmjfyXbt.js} +6 -6
  17. package/app/out/renderer/assets/{ganttDiagram-a2739b55-DpMib95K.js → ganttDiagram-a2739b55-BTPRekAy.js} +3 -3
  18. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-C0OtwErh.js → gitGraphDiagram-82fe8481-1riYxgGS.js} +2 -2
  19. package/app/out/renderer/assets/{graph-CXef_RHM.js → graph-CvDtMlX-.js} +1 -1
  20. package/app/out/renderer/assets/{index-Bg4LHaeu.js → index-0kPJXDfu.js} +3 -3
  21. package/app/out/renderer/assets/{index-CTF1A-5m.js → index-3LdRym1K.js} +3 -3
  22. package/app/out/renderer/assets/{index-5325376f-0FtzFTBH.js → index-5325376f-BGaoNMNN.js} +6 -6
  23. package/app/out/renderer/assets/{index-Crf9Pipm.js → index-8tvmsRje.js} +3 -3
  24. package/app/out/renderer/assets/{index-CMDsy41q.js → index-B4djqBxS.js} +1 -1
  25. package/app/out/renderer/assets/{index-CKjCQ1EB.js → index-B9lieynj.js} +6 -6
  26. package/app/out/renderer/assets/{index-Cx3Vwh3q.js → index-BCOrnr8q.js} +4 -4
  27. package/app/out/renderer/assets/{index-DRyElXV-.js → index-BK5rYWMs.js} +5 -5
  28. package/app/out/renderer/assets/{index-vGIhunyU.js → index-BVNrdWzl.js} +6 -6
  29. package/app/out/renderer/assets/{index-BBH0Chbw.js → index-BgSz3yUy.js} +6 -6
  30. package/app/out/renderer/assets/{index-Dy2bySYF.js → index-Bii7x9Rr.js} +3 -3
  31. package/app/out/renderer/assets/{index-CFaiDIr7.js → index-BnRwUKpv.js} +3 -3
  32. package/app/out/renderer/assets/{index-uZnv8lTU.js → index-BxOmAXUZ.js} +3 -3
  33. package/app/out/renderer/assets/{index-C_cgOzmt.js → index-CUPy7R5v.js} +39 -20
  34. package/app/out/renderer/assets/{index-DeVfJmHc.js → index-CXN1f9OT.js} +3 -3
  35. package/app/out/renderer/assets/{index-BE4XBnng.js → index-CnL9yPzK.js} +3 -3
  36. package/app/out/renderer/assets/{index-C-_uCjZJ.css → index-Ctwkk-AW.css} +2 -10
  37. package/app/out/renderer/assets/{index-BHo8axTp.js → index-D2fFfHUR.js} +6 -6
  38. package/app/out/renderer/assets/{index-DuAPj57k.js → index-DrvR7Peq.js} +3 -3
  39. package/app/out/renderer/assets/{index-CabfPYgf.js → index-NHbUPOmb.js} +3 -3
  40. package/app/out/renderer/assets/{index-tz7ZKjP9.js → index-O3gvL3-Z.js} +3 -3
  41. package/app/out/renderer/assets/{index-BohTbJeP.js → index-cAZJ88Np.js} +6 -6
  42. package/app/out/renderer/assets/{index-OqY0JVi2.js → index-qS7qbXvX.js} +3 -3
  43. package/app/out/renderer/assets/{index-D9-3cc7l.js → index-y5XZ-0EB.js} +4 -4
  44. package/app/out/renderer/assets/{index-C1Hf3CJw.js → index-zr8uxb8p.js} +6 -6
  45. package/app/out/renderer/assets/{infoDiagram-8eee0895-CPFVhSvg.js → infoDiagram-8eee0895-Cq8aXV8u.js} +2 -2
  46. package/app/out/renderer/assets/{journeyDiagram-c64418c1-PKaxJ2mn.js → journeyDiagram-c64418c1-D4ewDrYD.js} +4 -4
  47. package/app/out/renderer/assets/{layout-CawlN23W.js → layout-CZmLZO9t.js} +2 -2
  48. package/app/out/renderer/assets/{line-C_cMMDTP.js → line-D7kWOiRx.js} +1 -1
  49. package/app/out/renderer/assets/{linear-CnzgpVoT.js → linear-B055Dz0c.js} +1 -1
  50. package/app/out/renderer/assets/{mindmap-definition-8da855dc-2dVBAm3g.js → mindmap-definition-8da855dc-D6EW4QCj.js} +3 -3
  51. package/app/out/renderer/assets/{pieDiagram-a8764435-p6hNN8aY.js → pieDiagram-a8764435-BX_Dz4T9.js} +3 -3
  52. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-JJT_eOsi.js → quadrantDiagram-1e28029f-BsI6xGsm.js} +3 -3
  53. package/app/out/renderer/assets/{requirementDiagram-08caed73-Ck4auzva.js → requirementDiagram-08caed73-c2d8T0BS.js} +5 -5
  54. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-DICtg7Jw.js → sankeyDiagram-a04cb91d-CkDhRKRC.js} +2 -2
  55. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-Bv7njQoz.js → sequenceDiagram-c5b8d532-DS0RKYnD.js} +3 -3
  56. package/app/out/renderer/assets/{stateDiagram-1ecb1508-CmuiBQ0q.js → stateDiagram-1ecb1508-BjTK27QX.js} +6 -6
  57. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-DErEMYcv.js → stateDiagram-v2-c2b004d7-D1wWbeR3.js} +10 -10
  58. package/app/out/renderer/assets/{styles-b4e223ce-CEjXkOYY.js → styles-b4e223ce-DXUfbXTM.js} +1 -1
  59. package/app/out/renderer/assets/{styles-ca3715f6-BJWKCKia.js → styles-ca3715f6-CE_JRTmB.js} +1 -1
  60. package/app/out/renderer/assets/{styles-d45a18b0-BrhRky7i.js → styles-d45a18b0-CdtAXXSE.js} +4 -4
  61. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-RWkoQoOd.js → svgDrawCommon-b86b1483-dCxPWgBl.js} +1 -1
  62. package/app/out/renderer/assets/{timeline-definition-faaaa080-25xmyyis.js → timeline-definition-faaaa080-B7ZP3Dqw.js} +3 -3
  63. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-DwBkod9W.js → xychartDiagram-f5964ef8-CXagmo1Q.js} +5 -5
  64. package/app/out/renderer/index.html +2 -2
  65. package/package.json +1 -1
@@ -2,10 +2,10 @@ import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
2
2
  import fs, { existsSync as existsSync$1 } from "node:fs";
3
3
  import { execFile, execSync } from "node:child_process";
4
4
  import { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
5
- import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, renameSync, unlinkSync } from "fs";
5
+ import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
6
6
  import os$1, { homedir } from "os";
7
7
  import { Agent } from "@mariozechner/pi-agent-core";
8
- import { getModel, completeSimple } from "@mariozechner/pi-ai";
8
+ import { completeSimple, getModel } from "@mariozechner/pi-ai";
9
9
  import { createCodingTools, createGrepTool, createFindTool, createLsTool, DEFAULT_COMPACTION_SETTINGS, estimateTokens, shouldCompact, generateSummary } from "@mariozechner/pi-coding-agent";
10
10
  import { Type } from "@sinclair/typebox";
11
11
  import path from "node:path";
@@ -15,6 +15,7 @@ import { promisify } from "node:util";
15
15
  import os from "node:os";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { createHash as createHash$1 } from "crypto";
18
+ import { execFile as execFile$1 } from "child_process";
18
19
  import __cjs_mod__ from "node:module";
19
20
  const __filename = import.meta.filename;
20
21
  const __dirname = import.meta.dirname;
@@ -634,6 +635,8 @@ const PATHS = {
634
635
  memoryRoot: ".research-pilot/memory-v2",
635
636
  explainDir: ".research-pilot/memory-v2/explain",
636
637
  sessionSummaries: ".research-pilot/memory-v2/session-summaries",
638
+ // Structured long-term memory (auto-memory)
639
+ memory: ".research-pilot/memory",
637
640
  // Skills
638
641
  skills: ".research-pilot/skills",
639
642
  skillsConfig: ".research-pilot/skills-config.json"
@@ -1244,56 +1247,268 @@ function createArtifactSearchTool(projectPath) {
1244
1247
  }
1245
1248
  };
1246
1249
  }
1247
- function createUpdateMemoryTool(projectPath) {
1250
+ function createResearchMemoryTools(params) {
1251
+ return [
1252
+ createArtifactCreateTool(params.sessionId, params.projectPath),
1253
+ createArtifactUpdateTool(params.projectPath),
1254
+ createArtifactSearchTool(params.projectPath)
1255
+ ];
1256
+ }
1257
+ function slugify(text) {
1258
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1259
+ }
1260
+ function memoryFilename(type, name) {
1261
+ return `${type}_${slugify(name)}.md`;
1262
+ }
1263
+ function memoryDir(projectPath) {
1264
+ return join(projectPath, PATHS.memory);
1265
+ }
1266
+ function ensureMemoryDir(projectPath) {
1267
+ const dir = memoryDir(projectPath);
1268
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1269
+ }
1270
+ function formatFrontmatter(fm) {
1271
+ return [
1272
+ "---",
1273
+ `name: ${fm.name}`,
1274
+ `description: ${fm.description}`,
1275
+ `type: ${fm.type}`,
1276
+ "---"
1277
+ ].join("\n");
1278
+ }
1279
+ function parseFrontmatter(text) {
1280
+ const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1281
+ if (!match) return null;
1282
+ const fm = {};
1283
+ for (const line of match[1].split("\n")) {
1284
+ const kv = line.match(/^(\w+):\s*(.+)$/);
1285
+ if (kv) fm[kv[1]] = kv[2].trim();
1286
+ }
1287
+ if (!fm.name || !fm.type) return null;
1288
+ const validTypes = ["user", "feedback", "project", "reference"];
1289
+ if (!validTypes.includes(fm.type)) return null;
1248
1290
  return {
1249
- name: "update-memory",
1250
- description: 'Write to the "## Agent Memory" section of agent.md — your persistent memory across sessions. Use this to save: user preferences, project context, key decisions, important findings. The content you provide REPLACES the entire Agent Memory section (User Instructions section is preserved automatically). Keep it concise (<5000 chars total). Consolidate and remove outdated entries rather than appending.',
1291
+ frontmatter: { name: fm.name, description: fm.description || "", type: fm.type },
1292
+ body: match[2].trim()
1293
+ };
1294
+ }
1295
+ function writeMemoryFile(projectPath, entry) {
1296
+ ensureMemoryDir(projectPath);
1297
+ const filePath = join(memoryDir(projectPath), entry.filename);
1298
+ const content = `${formatFrontmatter(entry.frontmatter)}
1299
+
1300
+ ${entry.content}
1301
+ `;
1302
+ writeFileSync(filePath, content, "utf-8");
1303
+ return filePath;
1304
+ }
1305
+ function readMemoryFile(projectPath, filename) {
1306
+ const filePath = join(memoryDir(projectPath), filename);
1307
+ if (!existsSync(filePath)) return null;
1308
+ try {
1309
+ const text = readFileSync(filePath, "utf-8");
1310
+ const parsed = parseFrontmatter(text);
1311
+ if (!parsed) return null;
1312
+ return { frontmatter: parsed.frontmatter, content: parsed.body, filename };
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ function deleteMemoryFile(projectPath, filename) {
1318
+ const filePath = join(memoryDir(projectPath), filename);
1319
+ if (!existsSync(filePath)) return false;
1320
+ try {
1321
+ unlinkSync(filePath);
1322
+ return true;
1323
+ } catch {
1324
+ return false;
1325
+ }
1326
+ }
1327
+ function listMemoryFiles(projectPath) {
1328
+ const dir = memoryDir(projectPath);
1329
+ if (!existsSync(dir)) return [];
1330
+ try {
1331
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
1332
+ const entries = [];
1333
+ for (const filename of files) {
1334
+ const entry = readMemoryFile(projectPath, filename);
1335
+ if (entry) entries.push(entry);
1336
+ }
1337
+ return entries;
1338
+ } catch {
1339
+ return [];
1340
+ }
1341
+ }
1342
+ function findMemoryByName(projectPath, name) {
1343
+ const lower = name.toLowerCase();
1344
+ return listMemoryFiles(projectPath).find((e) => e.frontmatter.name.toLowerCase() === lower) ?? null;
1345
+ }
1346
+ function buildMemoryIndex(entries) {
1347
+ if (entries.length === 0) return "";
1348
+ return entries.map((e) => {
1349
+ const desc = e.frontmatter.description.slice(0, 100).replace(/\n/g, " ");
1350
+ return `- [${e.frontmatter.name}](memory/${e.filename}) — ${desc}`;
1351
+ }).join("\n");
1352
+ }
1353
+ function updateAgentMdIndex(projectPath, entries) {
1354
+ const record = findArtifactById(projectPath, AGENT_MD_ID);
1355
+ const currentContent = record?.artifact?.type === "note" ? record.artifact.content || "" : "";
1356
+ const marker = "## Agent Memory";
1357
+ const markerIdx = currentContent.indexOf(marker);
1358
+ const userInstructions = markerIdx >= 0 ? currentContent.slice(0, markerIdx).trimEnd() : currentContent.trimEnd();
1359
+ const indexContent = buildMemoryIndex(entries);
1360
+ const newContent = indexContent ? `${userInstructions}
1361
+
1362
+ ${marker}
1363
+
1364
+ ${indexContent}
1365
+ ` : `${userInstructions}
1366
+
1367
+ ${marker}
1368
+ `;
1369
+ if (newContent.length > AGENT_MD_MAX_CHARS) {
1370
+ return { success: false, charCount: newContent.length };
1371
+ }
1372
+ updateArtifact(projectPath, AGENT_MD_ID, { content: newContent });
1373
+ return { success: true, charCount: newContent.length };
1374
+ }
1375
+ function migrateAgentMemoryToFile(projectPath) {
1376
+ const record = findArtifactById(projectPath, AGENT_MD_ID);
1377
+ if (!record) return false;
1378
+ const content = record.artifact?.type === "note" ? record.artifact.content || "" : "";
1379
+ const marker = "## Agent Memory";
1380
+ const markerIdx = content.indexOf(marker);
1381
+ if (markerIdx < 0) return false;
1382
+ const agentMemory = content.slice(markerIdx + marker.length).trim();
1383
+ if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
1384
+ ensureMemoryDir(projectPath);
1385
+ const entry = {
1386
+ frontmatter: {
1387
+ name: "Legacy notes",
1388
+ description: "Migrated from agent.md Agent Memory section",
1389
+ type: "project"
1390
+ },
1391
+ content: agentMemory,
1392
+ filename: memoryFilename("project", "legacy-notes")
1393
+ };
1394
+ writeMemoryFile(projectPath, entry);
1395
+ const allEntries = listMemoryFiles(projectPath);
1396
+ updateAgentMdIndex(projectPath, allEntries);
1397
+ return true;
1398
+ }
1399
+ const VALID_TYPES$1 = ["user", "feedback", "project", "reference"];
1400
+ function createSaveMemoryTool(projectPath) {
1401
+ return {
1402
+ name: "save-memory",
1403
+ description: 'Save a memory to long-term storage. Each memory becomes a file in .research-pilot/memory/ and an index entry in agent.md (visible every turn). Types: "user" (preferences/background), "feedback" (corrections to behavior), "project" (decisions/deadlines/context), "reference" (external pointers/reusable info). If a memory with the same name+type exists, it is updated.',
1251
1404
  parameters: {
1252
1405
  type: "object",
1253
1406
  properties: {
1254
- memory: {
1407
+ type: {
1408
+ type: "string",
1409
+ enum: VALID_TYPES$1,
1410
+ description: "Memory category"
1411
+ },
1412
+ name: {
1255
1413
  type: "string",
1256
- description: 'The full content for the "## Agent Memory" section. Markdown format.'
1414
+ description: "Short identifier (used as title and filename slug)"
1415
+ },
1416
+ content: {
1417
+ type: "string",
1418
+ description: "The memory content (markdown). Keep it concise and focused."
1257
1419
  }
1258
1420
  },
1259
- required: ["memory"]
1421
+ required: ["type", "name", "content"]
1260
1422
  },
1261
1423
  execute: async (input) => {
1262
- const args = input;
1263
- const memory = String(args.memory || "").trim();
1264
- if (!memory) return toolError("MISSING_PARAMETER", "memory content is required.", {
1265
- suggestions: ["Provide the content to save in the Agent Memory section."]
1424
+ const type = String(input.type || "");
1425
+ const name = String(input.name || "").trim();
1426
+ const content = String(input.content || "").trim();
1427
+ if (!VALID_TYPES$1.includes(type)) {
1428
+ return toolError("INVALID_PARAMETER", `Invalid memory type: ${type}. Must be one of: ${VALID_TYPES$1.join(", ")}`, {
1429
+ suggestions: ['Use "user", "feedback", "project", or "reference".']
1430
+ });
1431
+ }
1432
+ if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1433
+ suggestions: ["Provide a short descriptive name for this memory."]
1266
1434
  });
1267
- const record = findArtifactById(projectPath, AGENT_MD_ID);
1268
- const currentContent = record?.artifact?.type === "note" ? record.artifact.content || "" : "";
1269
- const agentMemoryMarker = "## Agent Memory";
1270
- const markerIdx = currentContent.indexOf(agentMemoryMarker);
1271
- const userInstructions = markerIdx >= 0 ? currentContent.slice(0, markerIdx).trimEnd() : currentContent.split("\n").filter((l) => !l.startsWith("## Agent Memory")).join("\n").trimEnd();
1272
- const newContent = `${userInstructions}
1273
-
1274
- ${agentMemoryMarker}
1275
-
1276
- ${memory}
1277
- `;
1278
- const updated = updateArtifact(projectPath, AGENT_MD_ID, { content: newContent });
1279
- if (!updated) {
1280
- return toolError("UPDATE_FAILED", "Failed to update agent.md.", {
1281
- suggestions: ["agent.md may not exist. Try opening a project folder first."]
1435
+ if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
1436
+ suggestions: ["Provide the memory content to save."]
1437
+ });
1438
+ ensureMemoryDir(projectPath);
1439
+ const filename = memoryFilename(type, name);
1440
+ const description = content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
1441
+ const entry = {
1442
+ frontmatter: { name, description, type },
1443
+ content,
1444
+ filename
1445
+ };
1446
+ writeMemoryFile(projectPath, entry);
1447
+ const allEntries = listMemoryFiles(projectPath);
1448
+ const indexResult = updateAgentMdIndex(projectPath, allEntries);
1449
+ if (!indexResult.success) {
1450
+ return toolError(
1451
+ "OUTPUT_TOO_LARGE",
1452
+ "agent.md index exceeded size limit. Remove some memories first.",
1453
+ {
1454
+ suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1455
+ }
1456
+ );
1457
+ }
1458
+ return {
1459
+ success: true,
1460
+ data: {
1461
+ message: `Memory saved: ${name} (${type})`,
1462
+ filename,
1463
+ totalMemories: allEntries.length,
1464
+ agentMdChars: indexResult.charCount
1465
+ }
1466
+ };
1467
+ }
1468
+ };
1469
+ }
1470
+ function createDeleteMemoryTool(projectPath) {
1471
+ return {
1472
+ name: "delete-memory",
1473
+ description: "Delete a memory by name. Removes the file and its index entry in agent.md.",
1474
+ parameters: {
1475
+ type: "object",
1476
+ properties: {
1477
+ name: {
1478
+ type: "string",
1479
+ description: "Name of the memory to delete (case-insensitive match)"
1480
+ }
1481
+ },
1482
+ required: ["name"]
1483
+ },
1484
+ execute: async (input) => {
1485
+ const name = String(input.name || "").trim();
1486
+ if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1487
+ suggestions: ["Provide the name of the memory to delete."]
1488
+ });
1489
+ const existing = findMemoryByName(projectPath, name);
1490
+ if (!existing) {
1491
+ return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
1492
+ suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
1282
1493
  });
1283
1494
  }
1495
+ deleteMemoryFile(projectPath, existing.filename);
1496
+ const allEntries = listMemoryFiles(projectPath);
1497
+ updateAgentMdIndex(projectPath, allEntries);
1284
1498
  return {
1285
1499
  success: true,
1286
- data: { message: "Agent memory updated.", charCount: newContent.length }
1500
+ data: {
1501
+ message: `Memory deleted: ${name}`,
1502
+ totalMemories: allEntries.length
1503
+ }
1287
1504
  };
1288
1505
  }
1289
1506
  };
1290
1507
  }
1291
- function createResearchMemoryTools(params) {
1508
+ function createMemoryTools(projectPath) {
1292
1509
  return [
1293
- createArtifactCreateTool(params.sessionId, params.projectPath),
1294
- createArtifactUpdateTool(params.projectPath),
1295
- createArtifactSearchTool(params.projectPath),
1296
- createUpdateMemoryTool(params.projectPath)
1510
+ createSaveMemoryTool(projectPath),
1511
+ createDeleteMemoryTool(projectPath)
1297
1512
  ];
1298
1513
  }
1299
1514
  const WEB_DEFAULTS = {
@@ -1741,11 +1956,20 @@ Memory model:
1741
1956
  - Session context is maintained automatically via periodic summaries.
1742
1957
  - For quick-reference info, create a note via artifact-create({ type: "note", ... }).
1743
1958
 
1744
- Long-term memory:
1745
- - Use the update-memory tool to persist information across sessions. It writes to the "## Agent Memory" section of agent.md, which is injected into your context every turn.
1746
- - WHEN to save: user states a preference ("always do X", "I prefer Y"), a key project decision is made, you discover something important about the project that future sessions need.
1747
- - WHEN NOT to save: routine task results, things already visible in files/git, ephemeral conversation details.
1748
- - Keep it concise consolidate and remove outdated entries. It's a living summary, not a log.
1959
+ Long-term memory (auto-memory):
1960
+ - Your agent.md "## Agent Memory" section shows an index of saved memories. It is injected into your context every turn, so you always see what is remembered.
1961
+ - Use save-memory to persist information across sessions. Each memory becomes a file in .research-pilot/memory/ with one of four types:
1962
+ * user who the user is: role, expertise, preferences, communication style
1963
+ * feedback corrections to your behavior: "don't do X", "when I say Y I mean Z"
1964
+ * project — key decisions, deadlines, collaborators, research directions
1965
+ * reference — pointers to external resources, reusable facts, definitions
1966
+ - Use delete-memory to remove outdated entries by name.
1967
+ - WHEN to save: user explicitly states a preference, corrects your behavior, a non-obvious project decision is made, or user points to an external resource. Most turns do NOT warrant saving a memory — only save when you learn something genuinely new that a future session would need.
1968
+ - WHEN NOT to save: routine task results, things already in workspace files or git, ephemeral conversation details, information derivable from the codebase, anything already captured in an existing memory.
1969
+ - Before saving, check agent.md index — if a similar memory exists, update it instead of creating a duplicate.
1970
+ - Keep each memory atomic (one concept) and concise.
1971
+ - Note: save-memory is for cross-session meta-information (preferences, context). Use artifact-create for work products (notes, analysis results, review memos).
1972
+ - You can read full memory files with the read tool at .research-pilot/memory/<filename>.
1749
1973
 
1750
1974
  Coding tasks:
1751
1975
  - For code implementation, follow test-first workflow: write/update test → confirm it fails → implement → confirm it passes.
@@ -3722,15 +3946,139 @@ function createResearchTools(ctx) {
3722
3946
  tools.push(createLiteratureSearchTool(ctx));
3723
3947
  tools.push(createConvertDocumentTool(ctx));
3724
3948
  tools.push(createDataAnalyzeTool(ctx));
3725
- const memoryTools = createResearchMemoryTools({
3949
+ const artifactTools = createResearchMemoryTools({
3726
3950
  sessionId: ctx.sessionId,
3727
3951
  projectPath: ctx.projectPath
3728
3952
  });
3729
- for (const tool of memoryTools) {
3953
+ for (const tool of artifactTools) {
3954
+ tools.push(wrapResearchTool(tool));
3955
+ }
3956
+ const structuredMemoryTools = createMemoryTools(ctx.projectPath);
3957
+ for (const tool of structuredMemoryTools) {
3730
3958
  tools.push(wrapResearchTool(tool));
3731
3959
  }
3732
3960
  return tools;
3733
3961
  }
3962
+ const VALID_TYPES = ["user", "feedback", "project", "reference"];
3963
+ const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
3964
+
3965
+ Rules:
3966
+ - Only extract DURABLE, IMPORTANT information — things a future session would need.
3967
+ - Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
3968
+ - Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
3969
+ - Do NOT extract: routine task results, ephemeral details, things already in workspace files.
3970
+ - Each memory should be atomic — one concept per entry.
3971
+ - If nothing is worth saving, return an empty array.
3972
+
3973
+ Return ONLY a JSON array (no markdown fences, no explanation):
3974
+ [{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
3975
+ Or: []`;
3976
+ function simplifyMessages(messages, maxMessages) {
3977
+ const recent = messages.slice(-20);
3978
+ const result = [];
3979
+ for (const msg of recent) {
3980
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
3981
+ let content = "";
3982
+ if (typeof msg.content === "string") {
3983
+ content = msg.content;
3984
+ } else if (Array.isArray(msg.content)) {
3985
+ for (const block of msg.content) {
3986
+ if (block && typeof block === "object" && "type" in block) {
3987
+ if (block.type === "text" && "text" in block) {
3988
+ const text = block.text;
3989
+ content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
3990
+ content += "\n";
3991
+ } else if (block.type === "tool_use" && "name" in block) {
3992
+ content += `[Called ${block.name}]
3993
+ `;
3994
+ }
3995
+ }
3996
+ }
3997
+ }
3998
+ content = content.trim();
3999
+ if (content) {
4000
+ result.push({
4001
+ role: msg.role,
4002
+ content: content.slice(0, 2e3),
4003
+ timestamp: Date.now()
4004
+ });
4005
+ }
4006
+ }
4007
+ return result;
4008
+ }
4009
+ function agentCalledSaveMemoryThisTurn(messages) {
4010
+ for (let i = messages.length - 1; i >= 0; i--) {
4011
+ const msg = messages[i];
4012
+ if (msg.role === "user") break;
4013
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
4014
+ for (const block of msg.content) {
4015
+ if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
4016
+ if (block.name === "save-memory") return true;
4017
+ }
4018
+ }
4019
+ }
4020
+ }
4021
+ return false;
4022
+ }
4023
+ async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
4024
+ if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT === "0") return;
4025
+ if (turnCount % extractEveryN !== 0) return;
4026
+ if (agentCalledSaveMemoryThisTurn(messages)) {
4027
+ if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
4028
+ return;
4029
+ }
4030
+ try {
4031
+ const simplified = simplifyMessages(messages, 20);
4032
+ if (simplified.length < 2) return;
4033
+ simplified.push({
4034
+ role: "user",
4035
+ content: EXTRACTION_PROMPT,
4036
+ timestamp: Date.now()
4037
+ });
4038
+ const result = await completeSimple(config.model, {
4039
+ systemPrompt: config.systemPrompt,
4040
+ messages: simplified
4041
+ }, {
4042
+ maxTokens: 1024,
4043
+ apiKey: config.apiKey
4044
+ });
4045
+ const textContent = result.content.find((c) => c.type === "text");
4046
+ const text = textContent?.text?.trim() ?? "";
4047
+ if (!text || text === "[]") return;
4048
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) ?? text.match(/(\[[\s\S]*?\])/);
4049
+ const jsonStr = jsonMatch?.[1]?.trim() ?? text;
4050
+ const extracted = JSON.parse(jsonStr);
4051
+ if (!Array.isArray(extracted) || extracted.length === 0) return;
4052
+ ensureMemoryDir(config.projectPath);
4053
+ let written = 0;
4054
+ for (const mem of extracted) {
4055
+ if (!mem.type || !mem.name || !mem.content) continue;
4056
+ if (!VALID_TYPES.includes(mem.type)) continue;
4057
+ const entry = {
4058
+ frontmatter: {
4059
+ name: mem.name,
4060
+ description: (mem.description || mem.content.slice(0, 120)).replace(/\n/g, " "),
4061
+ type: mem.type
4062
+ },
4063
+ content: mem.content,
4064
+ filename: memoryFilename(mem.type, mem.name)
4065
+ };
4066
+ writeMemoryFile(config.projectPath, entry);
4067
+ written++;
4068
+ }
4069
+ if (written > 0) {
4070
+ const allEntries = listMemoryFiles(config.projectPath);
4071
+ updateAgentMdIndex(config.projectPath, allEntries);
4072
+ if (config.debug) {
4073
+ console.log(`[Extractor] Saved ${written} memories from conversation`);
4074
+ }
4075
+ }
4076
+ } catch (err) {
4077
+ if (config.debug) {
4078
+ console.warn("[Extractor] Failed:", err);
4079
+ }
4080
+ }
4081
+ }
3734
4082
  const SKILL_FILE_NAME = "SKILL.md";
3735
4083
  const MAX_SCAN_DEPTH = 3;
3736
4084
  let _builtinSkillsRoot = null;
@@ -4541,6 +4889,11 @@ ${message}`;
4541
4889
  });
4542
4890
  if (turnHistory.length > 8) turnHistory.shift();
4543
4891
  void maybeGenerateSummary();
4892
+ void maybeExtractMemories(
4893
+ { projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
4894
+ agent.state.messages,
4895
+ turnCount
4896
+ );
4544
4897
  if (debug) {
4545
4898
  console.log(`[Chat] Result: success=true, hasOutput=${!!responseText}, turn=${turnCount}`);
4546
4899
  }
@@ -5200,11 +5553,11 @@ function searchEntities(projectPath, query, types) {
5200
5553
  score: hit.score
5201
5554
  }));
5202
5555
  }
5203
- const MENTION_RE = /@(note|paper|data|file|url):(?:"([^"]+)"|(\S+))/g;
5556
+ const MENTION_RE = /@(note|paper|data|file|url):(?:"((?:[^"\\]|\\.)*)"|(\S+))/g;
5204
5557
  function parseMentions(message) {
5205
5558
  const mentions = [];
5206
5559
  const cleanMessage = message.replace(MENTION_RE, (_match, type, quoted, unquoted) => {
5207
- const key = quoted || unquoted;
5560
+ const key = (quoted || unquoted).replace(/\\"/g, '"');
5208
5561
  const raw = _match;
5209
5562
  mentions.push({ type, key, raw });
5210
5563
  return `[${type}: ${key}]`;
@@ -5229,11 +5582,11 @@ function getCachedMarkdown(filePath, projectPath) {
5229
5582
  const mtime = stat.mtimeMs;
5230
5583
  const cacheDir = join(projectPath, PATHS.documentCache);
5231
5584
  const cacheKey2 = getCacheKey(filePath, mtime);
5232
- const cachePath = join(cacheDir, cacheKey2);
5233
- if (!existsSync(cachePath)) {
5585
+ const cachePath2 = join(cacheDir, cacheKey2);
5586
+ if (!existsSync(cachePath2)) {
5234
5587
  return null;
5235
5588
  }
5236
- const entry = JSON.parse(readFileSync(cachePath, "utf-8"));
5589
+ const entry = JSON.parse(readFileSync(cachePath2, "utf-8"));
5237
5590
  if (entry.sourcePath !== filePath || entry.sourceMtime !== mtime) {
5238
5591
  return null;
5239
5592
  }
@@ -5248,14 +5601,14 @@ function setCachedMarkdown(filePath, markdown, projectPath) {
5248
5601
  const mtime = stat.mtimeMs;
5249
5602
  const cacheDir = ensureCacheDir(projectPath);
5250
5603
  const cacheKey2 = getCacheKey(filePath, mtime);
5251
- const cachePath = join(cacheDir, cacheKey2);
5604
+ const cachePath2 = join(cacheDir, cacheKey2);
5252
5605
  const entry = {
5253
5606
  sourcePath: filePath,
5254
5607
  sourceMtime: mtime,
5255
5608
  markdown,
5256
5609
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
5257
5610
  };
5258
- writeFileSync(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5611
+ writeFileSync(cachePath2, JSON.stringify(entry, null, 2), "utf-8");
5259
5612
  } catch (err) {
5260
5613
  console.warn("[document-cache] Failed to cache markdown:", err);
5261
5614
  }
@@ -5315,6 +5668,10 @@ function resolveEntity(ref, dir, entityType, projectPath) {
5315
5668
  const DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".docx", ".xlsx", ".pptx", ".doc", ".xls", ".ppt", ".epub"]);
5316
5669
  function resolveFile(ref, projectPath) {
5317
5670
  const filePath = resolve(projectPath, ref.key);
5671
+ const normalizedProject = resolve(projectPath);
5672
+ if (!filePath.startsWith(normalizedProject + "/") && filePath !== normalizedProject) {
5673
+ return { ref, label: `file: ${ref.key}`, content: "", error: `Path outside workspace: ${ref.key}` };
5674
+ }
5318
5675
  if (!existsSync(filePath)) {
5319
5676
  return { ref, label: `file: ${ref.key}`, content: "", error: `File not found: ${ref.key}` };
5320
5677
  }
@@ -5427,6 +5784,9 @@ NOTE: This is a data entity. The actual data is in the file at the path above. U
5427
5784
  }
5428
5785
  return JSON.stringify(entity, null, 2);
5429
5786
  }
5787
+ const REFRESH_THROTTLE_MS = 5e3;
5788
+ const MAX_FILES = 5e3;
5789
+ const MAX_DEPTH = 8;
5430
5790
  const SKIP_DIRS = /* @__PURE__ */ new Set([
5431
5791
  "node_modules",
5432
5792
  "__pycache__",
@@ -5439,10 +5799,58 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
5439
5799
  ".venv",
5440
5800
  "venv"
5441
5801
  ]);
5442
- const MAX_FILE_CANDIDATES = 500;
5443
- const MAX_DEPTH = 8;
5444
- function walkFiles(root, rel, depth, out) {
5445
- if (depth > MAX_DEPTH || out.length >= MAX_FILE_CANDIDATES) return;
5802
+ let cachedFiles = [];
5803
+ let cachedProjectPath = "";
5804
+ let lastRefreshAt = 0;
5805
+ let refreshPromise = null;
5806
+ async function getFileList(projectPath) {
5807
+ if (projectPath !== cachedProjectPath) {
5808
+ cachedFiles = [];
5809
+ cachedProjectPath = projectPath;
5810
+ lastRefreshAt = 0;
5811
+ }
5812
+ const now = Date.now();
5813
+ if (cachedFiles.length > 0 && now - lastRefreshAt < REFRESH_THROTTLE_MS) {
5814
+ return cachedFiles;
5815
+ }
5816
+ if (refreshPromise) return refreshPromise;
5817
+ refreshPromise = refreshFileList(projectPath).finally(() => {
5818
+ refreshPromise = null;
5819
+ });
5820
+ return refreshPromise;
5821
+ }
5822
+ async function refreshFileList(projectPath) {
5823
+ try {
5824
+ const files = await gitLsFiles(projectPath);
5825
+ cachedFiles = files.slice(0, MAX_FILES);
5826
+ } catch {
5827
+ cachedFiles = walkFilesSync(projectPath);
5828
+ }
5829
+ lastRefreshAt = Date.now();
5830
+ return cachedFiles;
5831
+ }
5832
+ function gitLsFiles(cwd) {
5833
+ return new Promise((resolve2, reject) => {
5834
+ execFile$1(
5835
+ "git",
5836
+ ["ls-files", "-c", "-o", "--exclude-standard"],
5837
+ { cwd, maxBuffer: 20 * 1024 * 1024, timeout: 5e3 },
5838
+ (err, stdout) => {
5839
+ if (err) return reject(err);
5840
+ const files = stdout.split("\n").filter(Boolean);
5841
+ if (files.length === 0) return reject(new Error("empty"));
5842
+ resolve2(files);
5843
+ }
5844
+ );
5845
+ });
5846
+ }
5847
+ function walkFilesSync(root) {
5848
+ const out = [];
5849
+ walk(root, "", 0, out);
5850
+ return out;
5851
+ }
5852
+ function walk(root, rel, depth, out) {
5853
+ if (depth > MAX_DEPTH || out.length >= MAX_FILES) return;
5446
5854
  const dir = rel ? join(root, rel) : root;
5447
5855
  let entries;
5448
5856
  try {
@@ -5452,7 +5860,7 @@ function walkFiles(root, rel, depth, out) {
5452
5860
  }
5453
5861
  for (const name of entries) {
5454
5862
  if (name.startsWith(".")) continue;
5455
- if (out.length >= MAX_FILE_CANDIDATES) return;
5863
+ if (out.length >= MAX_FILES) return;
5456
5864
  const childRel = rel ? `${rel}/${name}` : name;
5457
5865
  const full = join(dir, name);
5458
5866
  let stat;
@@ -5463,22 +5871,107 @@ function walkFiles(root, rel, depth, out) {
5463
5871
  }
5464
5872
  if (stat.isDirectory()) {
5465
5873
  if (SKIP_DIRS.has(name)) continue;
5466
- walkFiles(root, childRel, depth + 1, out);
5874
+ walk(root, childRel, depth + 1, out);
5467
5875
  } else {
5468
- out.push({
5469
- type: "file",
5470
- value: childRel,
5471
- label: childRel,
5472
- detail: `${(stat.size / 1024).toFixed(1)}KB`
5473
- });
5876
+ out.push(childRel);
5474
5877
  }
5475
5878
  }
5476
5879
  }
5477
- function getCandidates(projectPath, typeFilter, query) {
5880
+ let cache = null;
5881
+ let cachePath = "";
5882
+ function getEntityCache(projectPath) {
5883
+ if (cache && cachePath === projectPath) return cache;
5884
+ cache = {
5885
+ notes: listNotes(projectPath),
5886
+ papers: listLiterature(projectPath),
5887
+ data: listData(projectPath)
5888
+ };
5889
+ cachePath = projectPath;
5890
+ return cache;
5891
+ }
5892
+ function invalidateEntityCache() {
5893
+ cache = null;
5894
+ }
5895
+ const SCORE_MATCH = 16;
5896
+ const BONUS_FIRST_CHAR = 8;
5897
+ const BONUS_BOUNDARY = 8;
5898
+ const BONUS_CONSECUTIVE = 4;
5899
+ const PENALTY_GAP_START = 3;
5900
+ const PENALTY_GAP_EXT = 1;
5901
+ function fuzzyMatch(items, needle, getText, limit = 30) {
5902
+ if (!needle) {
5903
+ return items.slice(0, limit).map((item) => ({ item, score: 0 }));
5904
+ }
5905
+ const needleLower = needle.toLowerCase();
5906
+ const results = [];
5907
+ let worstInTopK = -Infinity;
5908
+ for (const item of items) {
5909
+ const haystack = getText(item);
5910
+ const score = scoreMatch(needleLower, haystack.toLowerCase(), haystack);
5911
+ if (score <= 0) continue;
5912
+ if (results.length >= limit && score <= worstInTopK) continue;
5913
+ insertSorted(results, { item, score }, limit);
5914
+ if (results.length >= limit) {
5915
+ worstInTopK = results[results.length - 1].score;
5916
+ }
5917
+ }
5918
+ return results;
5919
+ }
5920
+ function scoreMatch(needle, haystackLower, haystack) {
5921
+ let score = 0;
5922
+ let ni = 0;
5923
+ let consecutive = 0;
5924
+ let lastMatchIdx = -1;
5925
+ for (let hi = 0; hi < haystackLower.length && ni < needle.length; hi++) {
5926
+ if (haystackLower[hi] === needle[ni]) {
5927
+ score += SCORE_MATCH;
5928
+ if (hi === 0) score += BONUS_FIRST_CHAR;
5929
+ if (isBoundary(haystack, hi)) score += BONUS_BOUNDARY;
5930
+ if (lastMatchIdx === hi - 1) {
5931
+ consecutive++;
5932
+ score += BONUS_CONSECUTIVE * consecutive;
5933
+ } else {
5934
+ const gap = lastMatchIdx >= 0 ? hi - lastMatchIdx - 1 : 0;
5935
+ if (gap > 0) score -= PENALTY_GAP_START + PENALTY_GAP_EXT * (gap - 1);
5936
+ consecutive = 1;
5937
+ }
5938
+ lastMatchIdx = hi;
5939
+ ni++;
5940
+ }
5941
+ }
5942
+ return ni === needle.length ? score : 0;
5943
+ }
5944
+ function isBoundary(s, i) {
5945
+ if (i === 0) return true;
5946
+ const prev = s.charCodeAt(i - 1);
5947
+ const cur = s.charCodeAt(i);
5948
+ if (prev === 47 || prev === 92 || prev === 45 || prev === 95 || prev === 46 || prev === 32) {
5949
+ return true;
5950
+ }
5951
+ if (prev >= 97 && prev <= 122 && cur >= 65 && cur <= 90) return true;
5952
+ return false;
5953
+ }
5954
+ function insertSorted(arr, entry, limit) {
5955
+ let lo = 0;
5956
+ let hi = arr.length;
5957
+ while (lo < hi) {
5958
+ const mid = lo + hi >>> 1;
5959
+ if (arr[mid].score >= entry.score) {
5960
+ lo = mid + 1;
5961
+ } else {
5962
+ hi = mid;
5963
+ }
5964
+ }
5965
+ arr.splice(lo, 0, entry);
5966
+ if (arr.length > limit) arr.pop();
5967
+ }
5968
+ const MAX_EMPTY_QUERY_FILES = 30;
5969
+ async function getCandidates(projectPath, typeFilter, query) {
5478
5970
  const candidates = [];
5479
5971
  const q = query?.toLowerCase() ?? "";
5972
+ const entities = getEntityCache(projectPath);
5480
5973
  if (!typeFilter || typeFilter === "note") {
5481
- for (const n of listNotes(projectPath)) {
5974
+ for (const n of entities.notes) {
5482
5975
  candidates.push({
5483
5976
  type: "note",
5484
5977
  value: n.id.slice(0, 8),
@@ -5488,7 +5981,7 @@ function getCandidates(projectPath, typeFilter, query) {
5488
5981
  }
5489
5982
  }
5490
5983
  if (!typeFilter || typeFilter === "paper") {
5491
- for (const l of listLiterature(projectPath)) {
5984
+ for (const l of entities.papers) {
5492
5985
  candidates.push({
5493
5986
  type: "paper",
5494
5987
  value: l.citeKey,
@@ -5498,7 +5991,7 @@ function getCandidates(projectPath, typeFilter, query) {
5498
5991
  }
5499
5992
  }
5500
5993
  if (!typeFilter || typeFilter === "data") {
5501
- for (const d of listData(projectPath)) {
5994
+ for (const d of entities.data) {
5502
5995
  candidates.push({
5503
5996
  type: "data",
5504
5997
  value: d.id.slice(0, 8),
@@ -5508,16 +6001,25 @@ function getCandidates(projectPath, typeFilter, query) {
5508
6001
  }
5509
6002
  }
5510
6003
  if (!typeFilter || typeFilter === "file") {
5511
- if (existsSync(projectPath)) {
5512
- walkFiles(projectPath, "", 0, candidates);
6004
+ const files = await getFileList(projectPath);
6005
+ for (const rel of files) {
6006
+ let detail;
6007
+ try {
6008
+ const stat = statSync(join(projectPath, rel));
6009
+ detail = `${(stat.size / 1024).toFixed(1)}KB`;
6010
+ } catch {
6011
+ }
6012
+ candidates.push({ type: "file", value: rel, label: rel, detail });
5513
6013
  }
5514
6014
  }
5515
6015
  if (q) {
5516
- return candidates.filter(
5517
- (c) => c.label.toLowerCase().includes(q) || c.value.toLowerCase().includes(q) || c.detail?.toLowerCase().includes(q)
5518
- );
6016
+ return fuzzyMatch(
6017
+ candidates,
6018
+ q,
6019
+ (c) => `${c.label} ${c.value} ${c.detail ?? ""}`,
6020
+ 50
6021
+ ).map((r) => r.item);
5519
6022
  }
5520
- const MAX_EMPTY_QUERY_FILES = 30;
5521
6023
  const fileCount = candidates.filter((c) => c.type === "file").length;
5522
6024
  if (fileCount > MAX_EMPTY_QUERY_FILES) {
5523
6025
  const nonFiles = candidates.filter((c) => c.type !== "file");
@@ -5835,7 +6337,8 @@ function initializeProject(path2) {
5835
6337
  PATHS.memoryRoot,
5836
6338
  PATHS.explainDir,
5837
6339
  PATHS.sessionSummaries,
5838
- PATHS.skills
6340
+ PATHS.skills,
6341
+ PATHS.memory
5839
6342
  ];
5840
6343
  for (const dir of dirs) {
5841
6344
  const fullPath = join(path2, dir);
@@ -5856,6 +6359,7 @@ function initializeProject(path2) {
5856
6359
  writeFileSync(projectFile, JSON.stringify(defaultConfig, null, 2));
5857
6360
  }
5858
6361
  ensureAgentMd(path2);
6362
+ migrateAgentMemoryToFile(path2);
5859
6363
  const migration = migrateLegacyArtifacts(path2);
5860
6364
  if (migration.updatedFiles > 0 && process.env.RESEARCH_COPILOT_DEBUG) {
5861
6365
  console.log(`[ResearchPilot] migrated legacy artifacts: files=${migration.updatedFiles}, literature->paper=${migration.convertedLiteratureType}, data.name removed=${migration.removedDataNameField}`);
@@ -5933,17 +6437,20 @@ async function ensureCoordinator(state, win, model, options) {
5933
6437
  }
5934
6438
  }
5935
6439
  }
5936
- if (tool === "artifact-create" && result && typeof result === "object" && "success" in result) {
6440
+ if ((tool === "artifact-create" || tool === "artifact-update") && result && typeof result === "object" && "success" in result) {
5937
6441
  const r2 = result;
5938
6442
  if (r2.success) {
5939
- safeSend(win, "agent:entity-created", {
5940
- type: r2.data?.type || "artifact",
5941
- id: r2.data?.id,
5942
- title: r2.data?.title
5943
- });
5944
- if (r2.data?.filePath) {
5945
- const absPath = isAbsolute(r2.data.filePath) ? r2.data.filePath : resolve(runProjectPath, r2.data.filePath);
5946
- safeSend(win, "agent:file-created", absPath);
6443
+ invalidateEntityCache();
6444
+ if (tool === "artifact-create") {
6445
+ safeSend(win, "agent:entity-created", {
6446
+ type: r2.data?.type || "artifact",
6447
+ id: r2.data?.id,
6448
+ title: r2.data?.title
6449
+ });
6450
+ if (r2.data?.filePath) {
6451
+ const absPath = isAbsolute(r2.data.filePath) ? r2.data.filePath : resolve(runProjectPath, r2.data.filePath);
6452
+ safeSend(win, "agent:file-created", absPath);
6453
+ }
5947
6454
  }
5948
6455
  }
5949
6456
  }
@@ -6148,10 +6655,10 @@ function registerIpcHandlers() {
6148
6655
  }
6149
6656
  });
6150
6657
  });
6151
- handleWindow("mention:candidates", ({ state }, query, type) => {
6658
+ handleWindow("mention:candidates", async ({ state }, query, type) => {
6152
6659
  if (!state.projectPath) return [];
6153
6660
  try {
6154
- return getCandidates(state.projectPath, type, query);
6661
+ return await getCandidates(state.projectPath, type, query);
6155
6662
  } catch {
6156
6663
  return [];
6157
6664
  }