research-copilot 0.1.3 → 0.2.1

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 (67) hide show
  1. package/app/out/main/index.mjs +1281 -282
  2. package/app/out/preload/index.js +5 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-CCiFOpuq.js → MilkdownMarkdownEditor-D7GYpVZn.js} +50 -50
  4. package/app/out/renderer/assets/{arc-BR5G9xaE.js → arc-Kp4J_Jd7.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-JmvDTsGU.js → blockDiagram-c4efeb88-DkMSdn8j.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-Daf_3gE1.js → c4Diagram-c83219d4-DqAGxrYw.js} +3 -3
  7. package/app/out/renderer/assets/{channel-xtutyETs.js → channel-S4GQrISQ.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-BFWEqrCW.js → classDiagram-beda092f-B7AsTCEg.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-BQw7RI0A.js → classDiagram-v2-2358418a-B4oFy-In.js} +10 -10
  10. package/app/out/renderer/assets/{clone-uoV60hcB.js → clone-Dv1e6zYr.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-BaRII2sm.js → createText-1719965b-HBXHvWlI.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-CL7Yc4hz.js → edges-96097737-B6X5lcC0.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-B9hgyxu6.js → erDiagram-0228fc6a-BmBmTBlH.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-b_RS-jIJ.js → flowDb-c6c81e3f-CObz36ob.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-CPB3IueC.js → flowDiagram-50d868cf-C2hFHxwF.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-DM8cFvdZ.js → flowDiagram-v2-4f6560a1-DEe8EygW.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BsxABHy9.js → flowchart-elk-definition-6af322e1-CgTtfYKk.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-DpMib95K.js → ganttDiagram-a2739b55-C5Pq4zEy.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-C0OtwErh.js → gitGraphDiagram-82fe8481-oLp0f8Ll.js} +2 -2
  20. package/app/out/renderer/assets/{graph-CXef_RHM.js → graph-51iZ6wgR.js} +1 -1
  21. package/app/out/renderer/assets/{index-Bg4LHaeu.js → index-32eUzqVW.js} +3 -3
  22. package/app/out/renderer/assets/{index-5325376f-0FtzFTBH.js → index-5325376f-yLvOW-Os.js} +6 -6
  23. package/app/out/renderer/assets/{index-tz7ZKjP9.js → index-AuZa-hTj.js} +3 -3
  24. package/app/out/renderer/assets/{index-uZnv8lTU.js → index-B9a4DKM-.js} +3 -3
  25. package/app/out/renderer/assets/{index-CabfPYgf.js → index-BMsuFGn6.js} +3 -3
  26. package/app/out/renderer/assets/{index-CFaiDIr7.js → index-BQA_Kvr6.js} +3 -3
  27. package/app/out/renderer/assets/{index-D9-3cc7l.js → index-BSd80-j9.js} +4 -4
  28. package/app/out/renderer/assets/{index-C1Hf3CJw.js → index-BfWWn8B_.js} +6 -6
  29. package/app/out/renderer/assets/{index-CTF1A-5m.js → index-Bscx_5dF.js} +3 -3
  30. package/app/out/renderer/assets/{index-BBH0Chbw.js → index-CAOQIqEc.js} +6 -6
  31. package/app/out/renderer/assets/{index-Cx3Vwh3q.js → index-CTmGCKqa.js} +4 -4
  32. package/app/out/renderer/assets/{index-DRyElXV-.js → index-CmpSV9Ld.js} +5 -5
  33. package/app/out/renderer/assets/{index-BohTbJeP.js → index-Cn2e13ja.js} +6 -6
  34. package/app/out/renderer/assets/{index-BE4XBnng.js → index-D_Y7v6pE.js} +3 -3
  35. package/app/out/renderer/assets/{index-vGIhunyU.js → index-DjqJjt6u.js} +6 -6
  36. package/app/out/renderer/assets/{index-Crf9Pipm.js → index-DppxBL77.js} +3 -3
  37. package/app/out/renderer/assets/{index-C_cgOzmt.js → index-Du-Z3sl4.js} +952 -90
  38. package/app/out/renderer/assets/{index-CMDsy41q.js → index-FGsCVYSr.js} +1 -1
  39. package/app/out/renderer/assets/{index-C-_uCjZJ.css → index-L4DJn7cw.css} +14 -18
  40. package/app/out/renderer/assets/{index-DuAPj57k.js → index-UajPJYNV.js} +3 -3
  41. package/app/out/renderer/assets/{index-DeVfJmHc.js → index-_Z53hJps.js} +3 -3
  42. package/app/out/renderer/assets/{index-BHo8axTp.js → index-_iFRQTkA.js} +6 -6
  43. package/app/out/renderer/assets/{index-CKjCQ1EB.js → index-ohN9yRWw.js} +6 -6
  44. package/app/out/renderer/assets/{index-Dy2bySYF.js → index-shoMWskw.js} +3 -3
  45. package/app/out/renderer/assets/{index-OqY0JVi2.js → index-y1Od1ed6.js} +3 -3
  46. package/app/out/renderer/assets/{infoDiagram-8eee0895-CPFVhSvg.js → infoDiagram-8eee0895-Cm0Hm5ZX.js} +2 -2
  47. package/app/out/renderer/assets/{journeyDiagram-c64418c1-PKaxJ2mn.js → journeyDiagram-c64418c1-A2Gw9bVu.js} +4 -4
  48. package/app/out/renderer/assets/{layout-CawlN23W.js → layout-C5N2nTfF.js} +2 -2
  49. package/app/out/renderer/assets/{line-C_cMMDTP.js → line-Dn6BEQAK.js} +1 -1
  50. package/app/out/renderer/assets/{linear-CnzgpVoT.js → linear-8wk0rPUX.js} +1 -1
  51. package/app/out/renderer/assets/{mindmap-definition-8da855dc-2dVBAm3g.js → mindmap-definition-8da855dc-BVy6ISnb.js} +3 -3
  52. package/app/out/renderer/assets/{pieDiagram-a8764435-p6hNN8aY.js → pieDiagram-a8764435-B9_axIHE.js} +3 -3
  53. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-JJT_eOsi.js → quadrantDiagram-1e28029f-B1kmkDFg.js} +3 -3
  54. package/app/out/renderer/assets/{requirementDiagram-08caed73-Ck4auzva.js → requirementDiagram-08caed73-C_bNWUtT.js} +5 -5
  55. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-DICtg7Jw.js → sankeyDiagram-a04cb91d-CD2h1LiI.js} +2 -2
  56. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-Bv7njQoz.js → sequenceDiagram-c5b8d532-B6d6cuqi.js} +3 -3
  57. package/app/out/renderer/assets/{stateDiagram-1ecb1508-CmuiBQ0q.js → stateDiagram-1ecb1508-CkuNj_3H.js} +6 -6
  58. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-DErEMYcv.js → stateDiagram-v2-c2b004d7-CevZ3tno.js} +10 -10
  59. package/app/out/renderer/assets/{styles-b4e223ce-CEjXkOYY.js → styles-b4e223ce-DAe5WQrg.js} +1 -1
  60. package/app/out/renderer/assets/{styles-ca3715f6-BJWKCKia.js → styles-ca3715f6-BDSX88bY.js} +1 -1
  61. package/app/out/renderer/assets/{styles-d45a18b0-BrhRky7i.js → styles-d45a18b0-SE9h7les.js} +4 -4
  62. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-RWkoQoOd.js → svgDrawCommon-b86b1483-D1mpNbDQ.js} +1 -1
  63. package/app/out/renderer/assets/{timeline-definition-faaaa080-25xmyyis.js → timeline-definition-faaaa080-7Ha-nm4M.js} +3 -3
  64. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-DwBkod9W.js → xychartDiagram-f5964ef8-DLy7iyZW.js} +5 -5
  65. package/app/out/renderer/index.html +2 -2
  66. package/app/package.json +1 -1
  67. package/package.json +1 -1
@@ -1,20 +1,23 @@
1
1
  import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
2
+ import { setMaxListeners } from "node:events";
2
3
  import fs, { existsSync as existsSync$1 } from "node:fs";
3
4
  import { execFile, execSync } from "node:child_process";
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 path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
6
+ import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
6
7
  import os$1, { homedir } from "os";
8
+ import { createHash, randomUUID } from "crypto";
7
9
  import { Agent } from "@mariozechner/pi-agent-core";
8
- import { getModel, completeSimple } from "@mariozechner/pi-ai";
10
+ import { completeSimple, getModel } from "@mariozechner/pi-ai";
9
11
  import { createCodingTools, createGrepTool, createFindTool, createLsTool, DEFAULT_COMPACTION_SETTINGS, estimateTokens, shouldCompact, generateSummary } from "@mariozechner/pi-coding-agent";
10
12
  import { Type } from "@sinclair/typebox";
11
- import path from "node:path";
13
+ import { mkdir, writeFile } from "fs/promises";
14
+ import path$1 from "node:path";
12
15
  import fsp from "node:fs/promises";
13
- import { createHash } from "node:crypto";
16
+ import { createHash as createHash$1 } from "node:crypto";
14
17
  import { promisify } from "node:util";
15
18
  import os from "node:os";
16
19
  import { fileURLToPath } from "node:url";
17
- import { createHash as createHash$1 } from "crypto";
20
+ import { execFile as execFile$1 } from "child_process";
18
21
  import __cjs_mod__ from "node:module";
19
22
  const __filename = import.meta.filename;
20
23
  const __dirname = import.meta.dirname;
@@ -232,10 +235,6 @@ function registerConfigHandlers(handleRaw) {
232
235
  return { success: true };
233
236
  });
234
237
  }
235
- function getFileName(path2) {
236
- if (!path2) return "";
237
- return path2.split("/").pop() || path2;
238
- }
239
238
  function inferMimeType(path2) {
240
239
  const ext = extname(path2).toLowerCase();
241
240
  if (ext === ".md" || ext === ".txt") return "text/plain";
@@ -575,13 +574,39 @@ function truncateHeadTail(text, maxChars, headRatio = 0.7) {
575
574
  ...[truncated ${truncatedChars} chars]
576
575
  ${text.slice(-tailChars)}`;
577
576
  }
577
+ function truncateStructuredData(data, maxChars) {
578
+ const json = JSON.stringify(data, null, 2);
579
+ if (json.length <= maxChars) return data;
580
+ const obj = { ...data };
581
+ let largestKey = "";
582
+ let largestSize = 0;
583
+ for (const [k, v] of Object.entries(obj)) {
584
+ if (typeof v === "string" && v.length > largestSize) {
585
+ largestKey = k;
586
+ largestSize = v.length;
587
+ }
588
+ }
589
+ if (largestKey) {
590
+ const overhead = json.length - largestSize;
591
+ const fieldBudget = Math.max(1e3, maxChars - overhead);
592
+ obj[largestKey] = truncateHeadTail(obj[largestKey], fieldBudget);
593
+ }
594
+ return obj;
595
+ }
578
596
  function toAgentResult(toolName, result) {
579
597
  let text;
598
+ const MAX_RESULT_CHARS = 1e5;
580
599
  if (result.success) {
581
600
  if (result.data === void 0 || result.data === null) {
582
601
  text = `[${toolName}] OK`;
583
602
  } else if (typeof result.data === "string") {
584
- text = result.data;
603
+ text = truncateHeadTail(result.data, MAX_RESULT_CHARS);
604
+ } else if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
605
+ const bounded = truncateStructuredData(
606
+ result.data,
607
+ MAX_RESULT_CHARS
608
+ );
609
+ text = JSON.stringify(bounded, null, 2);
585
610
  } else {
586
611
  text = JSON.stringify(result.data, null, 2);
587
612
  }
@@ -606,10 +631,11 @@ ${result.suggestions.map((s) => `- ${s}`).join("\n")}`);
606
631
  }
607
632
  text = parts.join("\n");
608
633
  }
609
- const MAX_RESULT_CHARS = 1e5;
610
- const bounded = truncateHeadTail(text, MAX_RESULT_CHARS);
634
+ if (text.length > MAX_RESULT_CHARS) {
635
+ text = truncateHeadTail(text, MAX_RESULT_CHARS);
636
+ }
611
637
  return {
612
- content: [{ type: "text", text: bounded }],
638
+ content: [{ type: "text", text }],
613
639
  details: { success: result.success, tool_name: toolName }
614
640
  };
615
641
  }
@@ -634,6 +660,8 @@ const PATHS = {
634
660
  memoryRoot: ".research-pilot/memory-v2",
635
661
  explainDir: ".research-pilot/memory-v2/explain",
636
662
  sessionSummaries: ".research-pilot/memory-v2/session-summaries",
663
+ // Structured long-term memory (auto-memory)
664
+ memory: ".research-pilot/memory",
637
665
  // Skills
638
666
  skills: ".research-pilot/skills",
639
667
  skillsConfig: ".research-pilot/skills-config.json"
@@ -1244,56 +1272,334 @@ function createArtifactSearchTool(projectPath) {
1244
1272
  }
1245
1273
  };
1246
1274
  }
1247
- function createUpdateMemoryTool(projectPath) {
1275
+ function createResearchMemoryTools(params) {
1276
+ return [
1277
+ createArtifactCreateTool(params.sessionId, params.projectPath),
1278
+ createArtifactUpdateTool(params.projectPath),
1279
+ createArtifactSearchTool(params.projectPath)
1280
+ ];
1281
+ }
1282
+ function slugify(text) {
1283
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1284
+ if (slug.length < 3) {
1285
+ const hash = createHash("sha256").update(text.toLowerCase().trim()).digest("hex").slice(0, 12);
1286
+ return slug ? `${slug}-${hash}` : hash;
1287
+ }
1288
+ return slug;
1289
+ }
1290
+ function memoryFilename(type, name) {
1291
+ return `${type}_${slugify(name)}.md`;
1292
+ }
1293
+ function memoryDir(projectPath) {
1294
+ return join(projectPath, PATHS.memory);
1295
+ }
1296
+ function ensureMemoryDir(projectPath) {
1297
+ const dir = memoryDir(projectPath);
1298
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1299
+ }
1300
+ function yamlSafe(value) {
1301
+ if (!value) return '""';
1302
+ if (/[:\#{}\[\]"'`\n\r|>]/.test(value) || value !== value.trim()) {
1303
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1304
+ return `"${escaped}"`;
1305
+ }
1306
+ return value;
1307
+ }
1308
+ function formatFrontmatter(fm) {
1309
+ return [
1310
+ "---",
1311
+ `name: ${yamlSafe(fm.name)}`,
1312
+ `description: ${yamlSafe(fm.description)}`,
1313
+ `type: ${fm.type}`,
1314
+ "---"
1315
+ ].join("\n");
1316
+ }
1317
+ function yamlUnescape(raw) {
1318
+ const trimmed = raw.trim();
1319
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1320
+ return trimmed.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1321
+ }
1322
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
1323
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1324
+ }
1325
+ return trimmed;
1326
+ }
1327
+ function parseFrontmatter(text) {
1328
+ const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1329
+ if (!match) return null;
1330
+ const fm = {};
1331
+ for (const line of match[1].split("\n")) {
1332
+ const kv = line.match(/^(\w+):\s*(.+)$/);
1333
+ if (kv) fm[kv[1]] = yamlUnescape(kv[2]);
1334
+ }
1335
+ if (!fm.name || !fm.type) return null;
1336
+ const validTypes = ["user", "feedback", "project", "reference"];
1337
+ if (!validTypes.includes(fm.type)) return null;
1248
1338
  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.',
1339
+ frontmatter: { name: fm.name, description: fm.description || "", type: fm.type },
1340
+ body: match[2].trim()
1341
+ };
1342
+ }
1343
+ function writeMemoryFile(projectPath, entry) {
1344
+ ensureMemoryDir(projectPath);
1345
+ const filePath = join(memoryDir(projectPath), entry.filename);
1346
+ const content = `${formatFrontmatter(entry.frontmatter)}
1347
+
1348
+ ${entry.content}
1349
+ `;
1350
+ writeFileSync(filePath, content, "utf-8");
1351
+ return filePath;
1352
+ }
1353
+ function readMemoryFile(projectPath, filename) {
1354
+ const filePath = join(memoryDir(projectPath), filename);
1355
+ if (!existsSync(filePath)) return null;
1356
+ try {
1357
+ const text = readFileSync(filePath, "utf-8");
1358
+ const parsed = parseFrontmatter(text);
1359
+ if (!parsed) return null;
1360
+ return { frontmatter: parsed.frontmatter, content: parsed.body, filename };
1361
+ } catch {
1362
+ return null;
1363
+ }
1364
+ }
1365
+ function deleteMemoryFile(projectPath, filename) {
1366
+ const filePath = join(memoryDir(projectPath), filename);
1367
+ if (!existsSync(filePath)) return false;
1368
+ try {
1369
+ unlinkSync(filePath);
1370
+ return true;
1371
+ } catch {
1372
+ return false;
1373
+ }
1374
+ }
1375
+ function listMemoryFiles(projectPath) {
1376
+ const dir = memoryDir(projectPath);
1377
+ if (!existsSync(dir)) return [];
1378
+ try {
1379
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
1380
+ const entries = [];
1381
+ for (const filename of files) {
1382
+ const entry = readMemoryFile(projectPath, filename);
1383
+ if (entry) entries.push(entry);
1384
+ }
1385
+ return entries;
1386
+ } catch {
1387
+ return [];
1388
+ }
1389
+ }
1390
+ function findMemoryByName(projectPath, name, type) {
1391
+ const lower = name.toLowerCase();
1392
+ const entries = listMemoryFiles(projectPath);
1393
+ return entries.find(
1394
+ (e) => e.frontmatter.name.toLowerCase() === lower && (!type || e.frontmatter.type === type)
1395
+ ) ?? null;
1396
+ }
1397
+ function findAllMemoriesByName(projectPath, name) {
1398
+ const lower = name.toLowerCase();
1399
+ return listMemoryFiles(projectPath).filter((e) => e.frontmatter.name.toLowerCase() === lower);
1400
+ }
1401
+ let _indexWriteLock = Promise.resolve();
1402
+ function withIndexLock(fn) {
1403
+ const next = _indexWriteLock.then(fn, fn);
1404
+ _indexWriteLock = next.then(() => {
1405
+ }, () => {
1406
+ });
1407
+ return next;
1408
+ }
1409
+ function buildMemoryIndex(entries) {
1410
+ if (entries.length === 0) return "";
1411
+ return entries.map((e) => {
1412
+ const desc = e.frontmatter.description.slice(0, 100).replace(/\n/g, " ");
1413
+ return `- [${e.frontmatter.name}](memory/${e.filename}) — ${desc}`;
1414
+ }).join("\n");
1415
+ }
1416
+ function updateAgentMdIndex(projectPath, entries) {
1417
+ const record = findArtifactById(projectPath, AGENT_MD_ID);
1418
+ const currentContent = record?.artifact?.type === "note" ? record.artifact.content || "" : "";
1419
+ const marker = "## Agent Memory";
1420
+ const markerIdx = currentContent.indexOf(marker);
1421
+ const userInstructions = markerIdx >= 0 ? currentContent.slice(0, markerIdx).trimEnd() : currentContent.trimEnd();
1422
+ const indexContent = buildMemoryIndex(entries);
1423
+ const newContent = indexContent ? `${userInstructions}
1424
+
1425
+ ${marker}
1426
+
1427
+ ${indexContent}
1428
+ ` : `${userInstructions}
1429
+
1430
+ ${marker}
1431
+ `;
1432
+ if (newContent.length > AGENT_MD_MAX_CHARS) {
1433
+ return { success: false, charCount: newContent.length };
1434
+ }
1435
+ updateArtifact(projectPath, AGENT_MD_ID, { content: newContent });
1436
+ return { success: true, charCount: newContent.length };
1437
+ }
1438
+ function migrateAgentMemoryToFile(projectPath) {
1439
+ const record = findArtifactById(projectPath, AGENT_MD_ID);
1440
+ if (!record) return false;
1441
+ const content = record.artifact?.type === "note" ? record.artifact.content || "" : "";
1442
+ const marker = "## Agent Memory";
1443
+ const markerIdx = content.indexOf(marker);
1444
+ if (markerIdx < 0) return false;
1445
+ const agentMemory = content.slice(markerIdx + marker.length).trim();
1446
+ if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
1447
+ const legacyFilename = memoryFilename("project", "legacy-notes");
1448
+ const legacyPath = join(memoryDir(projectPath), legacyFilename);
1449
+ if (existsSync(legacyPath)) return false;
1450
+ ensureMemoryDir(projectPath);
1451
+ const entry = {
1452
+ frontmatter: {
1453
+ name: "Legacy notes",
1454
+ description: "Migrated from agent.md Agent Memory section",
1455
+ type: "project"
1456
+ },
1457
+ content: agentMemory,
1458
+ filename: memoryFilename("project", "legacy-notes")
1459
+ };
1460
+ writeMemoryFile(projectPath, entry);
1461
+ const allEntries = listMemoryFiles(projectPath);
1462
+ updateAgentMdIndex(projectPath, allEntries);
1463
+ return true;
1464
+ }
1465
+ const VALID_TYPES$1 = ["user", "feedback", "project", "reference"];
1466
+ function createSaveMemoryTool(projectPath) {
1467
+ return {
1468
+ name: "save-memory",
1469
+ 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
1470
  parameters: {
1252
1471
  type: "object",
1253
1472
  properties: {
1254
- memory: {
1473
+ type: {
1474
+ type: "string",
1475
+ enum: VALID_TYPES$1,
1476
+ description: "Memory category"
1477
+ },
1478
+ name: {
1255
1479
  type: "string",
1256
- description: 'The full content for the "## Agent Memory" section. Markdown format.'
1480
+ description: "Short identifier (used as title and filename slug)"
1481
+ },
1482
+ content: {
1483
+ type: "string",
1484
+ description: "The memory content (markdown). Keep it concise and focused."
1257
1485
  }
1258
1486
  },
1259
- required: ["memory"]
1487
+ required: ["type", "name", "content"]
1260
1488
  },
1261
1489
  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."]
1490
+ const type = String(input.type || "");
1491
+ const name = String(input.name || "").trim();
1492
+ const content = String(input.content || "").trim();
1493
+ if (!VALID_TYPES$1.includes(type)) {
1494
+ return toolError("INVALID_PARAMETER", `Invalid memory type: ${type}. Must be one of: ${VALID_TYPES$1.join(", ")}`, {
1495
+ suggestions: ['Use "user", "feedback", "project", or "reference".']
1496
+ });
1497
+ }
1498
+ if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1499
+ suggestions: ["Provide a short descriptive name for this memory."]
1266
1500
  });
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."]
1501
+ if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
1502
+ suggestions: ["Provide the memory content to save."]
1503
+ });
1504
+ const lines = content.split("\n");
1505
+ const firstNonEmpty = lines.find((l) => l.trim().length > 0) || "";
1506
+ const description = firstNonEmpty.replace(/^#+\s*/, "").trim().slice(0, 120) || name;
1507
+ return withIndexLock(() => {
1508
+ ensureMemoryDir(projectPath);
1509
+ const filename = memoryFilename(type, name);
1510
+ const entry = {
1511
+ frontmatter: { name, description, type },
1512
+ content,
1513
+ filename
1514
+ };
1515
+ writeMemoryFile(projectPath, entry);
1516
+ const allEntries = listMemoryFiles(projectPath);
1517
+ const indexResult = updateAgentMdIndex(projectPath, allEntries);
1518
+ if (!indexResult.success) {
1519
+ deleteMemoryFile(projectPath, filename);
1520
+ return toolError(
1521
+ "OUTPUT_TOO_LARGE",
1522
+ "agent.md index exceeded size limit. Remove some memories first.",
1523
+ {
1524
+ suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1525
+ }
1526
+ );
1527
+ }
1528
+ return {
1529
+ success: true,
1530
+ data: {
1531
+ message: `Memory saved: ${name} (${type})`,
1532
+ filename,
1533
+ totalMemories: allEntries.length,
1534
+ agentMdChars: indexResult.charCount
1535
+ }
1536
+ };
1537
+ });
1538
+ }
1539
+ };
1540
+ }
1541
+ function createDeleteMemoryTool(projectPath) {
1542
+ return {
1543
+ name: "delete-memory",
1544
+ description: "Delete a memory by name. Removes the file and its index entry in agent.md. If multiple memories share the same name (different types), specify type to disambiguate.",
1545
+ parameters: {
1546
+ type: "object",
1547
+ properties: {
1548
+ name: {
1549
+ type: "string",
1550
+ description: "Name of the memory to delete (case-insensitive match)"
1551
+ },
1552
+ type: {
1553
+ type: "string",
1554
+ enum: VALID_TYPES$1,
1555
+ description: "Optional: memory type to disambiguate when multiple memories share the same name"
1556
+ }
1557
+ },
1558
+ required: ["name"]
1559
+ },
1560
+ execute: async (input) => {
1561
+ const name = String(input.name || "").trim();
1562
+ const type = input.type ? String(input.type) : void 0;
1563
+ if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1564
+ suggestions: ["Provide the name of the memory to delete."]
1565
+ });
1566
+ const allMatches = findAllMemoriesByName(projectPath, name);
1567
+ if (allMatches.length === 0) {
1568
+ return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
1569
+ suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
1282
1570
  });
1283
1571
  }
1284
- return {
1285
- success: true,
1286
- data: { message: "Agent memory updated.", charCount: newContent.length }
1287
- };
1572
+ if (allMatches.length > 1 && !type) {
1573
+ const types = allMatches.map((m) => m.frontmatter.type).join(", ");
1574
+ return toolError("AMBIGUOUS", `Multiple memories named "${name}" (types: ${types}). Specify type to disambiguate.`, {
1575
+ suggestions: [`Add type parameter: one of ${types}`]
1576
+ });
1577
+ }
1578
+ const existing = type ? findMemoryByName(projectPath, name, type) : allMatches[0];
1579
+ if (!existing) {
1580
+ return toolError("NOT_FOUND", `Memory not found: "${name}" with type "${type}"`, {
1581
+ suggestions: ["Check the memory name and type."]
1582
+ });
1583
+ }
1584
+ return withIndexLock(() => {
1585
+ deleteMemoryFile(projectPath, existing.filename);
1586
+ const allEntries = listMemoryFiles(projectPath);
1587
+ updateAgentMdIndex(projectPath, allEntries);
1588
+ return {
1589
+ success: true,
1590
+ data: {
1591
+ message: `Memory deleted: ${name}`,
1592
+ totalMemories: allEntries.length
1593
+ }
1594
+ };
1595
+ });
1288
1596
  }
1289
1597
  };
1290
1598
  }
1291
- function createResearchMemoryTools(params) {
1599
+ function createMemoryTools(projectPath) {
1292
1600
  return [
1293
- createArtifactCreateTool(params.sessionId, params.projectPath),
1294
- createArtifactUpdateTool(params.projectPath),
1295
- createArtifactSearchTool(params.projectPath),
1296
- createUpdateMemoryTool(params.projectPath)
1601
+ createSaveMemoryTool(projectPath),
1602
+ createDeleteMemoryTool(projectPath)
1297
1603
  ];
1298
1604
  }
1299
1605
  const WEB_DEFAULTS = {
@@ -1311,8 +1617,11 @@ const WEB_DEFAULTS = {
1311
1617
  maxFetchMaxChars: 2e5,
1312
1618
  defaultFetchTimeoutMs: 3e4,
1313
1619
  maxArxivCacheEntries: 100,
1314
- arxivSearchCacheTtlMs: 10 * 60 * 1e3
1620
+ arxivSearchCacheTtlMs: 10 * 60 * 1e3,
1315
1621
  // 10 min
1622
+ /** Content above this size is saved to disk; agent gets preview + file path */
1623
+ fetchPersistThresholdChars: 3e4,
1624
+ fetchPreviewChars: 2e3
1316
1625
  };
1317
1626
  class ProviderRateGate {
1318
1627
  constructor(minIntervalMs) {
@@ -1559,7 +1868,6 @@ function createWebSearchTool(ctx) {
1559
1868
  const providerRequested = normalizeSearchProvider(params.provider);
1560
1869
  const braveApiKey = process.env.BRAVE_API_KEY?.trim();
1561
1870
  let effectiveProvider = providerRequested === "auto" ? braveApiKey ? "brave" : "arxiv" : providerRequested;
1562
- ctx.onToolCall?.("web_search", { query, count, provider: effectiveProvider });
1563
1871
  let results = [];
1564
1872
  try {
1565
1873
  if (effectiveProvider === "brave") {
@@ -1603,7 +1911,6 @@ function createWebSearchTool(ctx) {
1603
1911
  count: results.length,
1604
1912
  results
1605
1913
  };
1606
- ctx.onToolResult?.("web_search", payload);
1607
1914
  return toAgentResult("web_search", {
1608
1915
  success: true,
1609
1916
  data: payload
@@ -1615,7 +1922,7 @@ function createWebFetchTool(ctx) {
1615
1922
  return {
1616
1923
  name: "web_fetch",
1617
1924
  label: "Web Fetch",
1618
- description: "Fetch a URL and extract readable text or markdown for downstream analysis.",
1925
+ description: "Fetch a URL and extract readable text or markdown. Content over 30K chars is saved to disk — use the read tool on the returned content_path to access full content.",
1619
1926
  parameters: WebFetchSchema,
1620
1927
  execute: async (_toolCallId, rawParams) => {
1621
1928
  const params = rawParams;
@@ -1644,7 +1951,6 @@ function createWebFetchTool(ctx) {
1644
1951
  const maxChars = typeof maxCharsRaw === "number" ? Math.max(100, Math.min(WEB_DEFAULTS.maxFetchMaxChars, Math.floor(maxCharsRaw))) : WEB_DEFAULTS.defaultFetchMaxChars;
1645
1952
  const timeoutSecRaw = (typeof params.timeout_sec === "number" && Number.isFinite(params.timeout_sec) ? params.timeout_sec : void 0) ?? (typeof params.timeoutSec === "number" && Number.isFinite(params.timeoutSec) ? params.timeoutSec : void 0);
1646
1953
  const timeoutMs = typeof timeoutSecRaw === "number" ? Math.max(1e3, Math.floor(timeoutSecRaw * 1e3)) : WEB_DEFAULTS.defaultFetchTimeoutMs;
1647
- ctx.onToolCall?.("web_fetch", { url: url.toString(), extractMode, maxChars });
1648
1954
  let response;
1649
1955
  const controller = new AbortController();
1650
1956
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1682,16 +1988,37 @@ Source: ${url.toString()}
1682
1988
  ---
1683
1989
 
1684
1990
  ${sliced}` : sliced;
1685
- const payload = {
1686
- url: url.toString(),
1687
- status_code: response.status,
1688
- content_type: contentType,
1689
- extract_mode: extractMode,
1690
- chars: normalized.length,
1691
- truncated,
1692
- content: output || "(empty response)"
1693
- };
1694
- ctx.onToolResult?.("web_fetch", payload);
1991
+ let payload;
1992
+ if (output.length > WEB_DEFAULTS.fetchPersistThresholdChars) {
1993
+ const hash = createHash("md5").update(url.toString() + Date.now()).digest("hex").slice(0, 12);
1994
+ const ext = extractMode === "markdown" ? "md" : "txt";
1995
+ const contentDir = path.join(ctx.projectPath, "web-content");
1996
+ await mkdir(contentDir, { recursive: true });
1997
+ const filePath = path.join(contentDir, `${hash}.${ext}`);
1998
+ await writeFile(filePath, output, "utf-8");
1999
+ const previewRaw = output.slice(0, WEB_DEFAULTS.fetchPreviewChars);
2000
+ const lastNl = previewRaw.lastIndexOf("\n");
2001
+ const preview = (lastNl > WEB_DEFAULTS.fetchPreviewChars * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw) + "\n...";
2002
+ payload = {
2003
+ url: url.toString(),
2004
+ status_code: response.status,
2005
+ content_type: contentType,
2006
+ extract_mode: extractMode,
2007
+ chars: normalized.length,
2008
+ content_path: path.relative(ctx.workspacePath, filePath),
2009
+ preview
2010
+ };
2011
+ } else {
2012
+ payload = {
2013
+ url: url.toString(),
2014
+ status_code: response.status,
2015
+ content_type: contentType,
2016
+ extract_mode: extractMode,
2017
+ chars: normalized.length,
2018
+ truncated,
2019
+ content: output || "(empty response)"
2020
+ };
2021
+ }
1695
2022
  return toAgentResult("web_fetch", {
1696
2023
  success: response.ok,
1697
2024
  data: payload,
@@ -1741,11 +2068,20 @@ Memory model:
1741
2068
  - Session context is maintained automatically via periodic summaries.
1742
2069
  - For quick-reference info, create a note via artifact-create({ type: "note", ... }).
1743
2070
 
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.
2071
+ Long-term memory (auto-memory):
2072
+ - 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.
2073
+ - Use save-memory to persist information across sessions. Each memory becomes a file in .research-pilot/memory/ with one of four types:
2074
+ * user who the user is: role, expertise, preferences, communication style
2075
+ * feedback corrections to your behavior: "don't do X", "when I say Y I mean Z"
2076
+ * project — key decisions, deadlines, collaborators, research directions
2077
+ * reference — pointers to external resources, reusable facts, definitions
2078
+ - Use delete-memory to remove outdated entries by name.
2079
+ - 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.
2080
+ - 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.
2081
+ - Before saving, check agent.md index — if a similar memory exists, update it instead of creating a duplicate.
2082
+ - Keep each memory atomic (one concept) and concise.
2083
+ - Note: save-memory is for cross-session meta-information (preferences, context). Use artifact-create for work products (notes, analysis results, review memos).
2084
+ - You can read full memory files with the read tool at .research-pilot/memory/<filename>.
1749
2085
 
1750
2086
  Coding tasks:
1751
2087
  - For code implementation, follow test-first workflow: write/update test → confirm it fails → implement → confirm it passes.
@@ -2550,7 +2886,6 @@ function createLiteratureSearchTool(ctx) {
2550
2886
  suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
2551
2887
  }));
2552
2888
  }
2553
- ctx.onToolCall?.("literature-search", { query, context: extraContext });
2554
2889
  const planUserPrompt = extraContext ? `Research request: ${query}
2555
2890
 
2556
2891
  Additional context: ${extraContext}` : `Research request: ${query}`;
@@ -2727,9 +3062,9 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2727
3062
  }
2728
3063
  }
2729
3064
  }
2730
- const reviewDir = path.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
3065
+ const reviewDir = path$1.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
2731
3066
  fs.mkdirSync(reviewDir, { recursive: true });
2732
- const fullReviewPath = path.join(reviewDir, "review.json");
3067
+ const fullReviewPath = path$1.join(reviewDir, "review.json");
2733
3068
  fs.writeFileSync(fullReviewPath, JSON.stringify({
2734
3069
  plan,
2735
3070
  allPapersCount: deduplicated.length,
@@ -2738,7 +3073,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2738
3073
  queriesUsed,
2739
3074
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2740
3075
  }, null, 2), "utf-8");
2741
- const relReviewPath = path.relative(ctx.projectPath, fullReviewPath);
3076
+ const relReviewPath = path$1.relative(ctx.projectPath, fullReviewPath);
2742
3077
  const payload = {
2743
3078
  totalFound: deduplicated.length,
2744
3079
  reviewedCount: review.relevantPapers.length,
@@ -2761,7 +3096,6 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2761
3096
  runId,
2762
3097
  queriesUsed: queriesUsed.slice(0, 10)
2763
3098
  };
2764
- ctx.onToolResult?.("literature-search", payload);
2765
3099
  return toAgentResult("literature-search", toolSuccess(payload, pipelineWarnings.length > 0 ? pipelineWarnings : void 0));
2766
3100
  }
2767
3101
  };
@@ -2804,16 +3138,16 @@ const FORMAT_EXTENSIONS = {
2804
3138
  zip: "zip"
2805
3139
  };
2806
3140
  function resolveWithinProject(projectPath, targetPath) {
2807
- const root = path.resolve(projectPath);
2808
- const resolved = targetPath.startsWith("/") ? path.resolve(targetPath) : path.resolve(root, targetPath);
2809
- const rel = path.relative(root, resolved);
2810
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
3141
+ const root = path$1.resolve(projectPath);
3142
+ const resolved = targetPath.startsWith("/") ? path$1.resolve(targetPath) : path$1.resolve(root, targetPath);
3143
+ const rel = path$1.relative(root, resolved);
3144
+ if (rel.startsWith("..") || path$1.isAbsolute(rel)) {
2811
3145
  throw new Error(`Path escapes project directory: ${targetPath}`);
2812
3146
  }
2813
3147
  return resolved;
2814
3148
  }
2815
3149
  function toProjectRelative(projectPath, absolutePath) {
2816
- return path.relative(path.resolve(projectPath), absolutePath);
3150
+ return path$1.relative(path$1.resolve(projectPath), absolutePath);
2817
3151
  }
2818
3152
  function isHttpUrl(value) {
2819
3153
  try {
@@ -2833,7 +3167,7 @@ function sanitizeBaseName(value) {
2833
3167
  function inferUrlBaseName(sourceUrl) {
2834
3168
  try {
2835
3169
  const parsed = new URL(sourceUrl);
2836
- const candidate = path.parse(parsed.pathname).name || parsed.hostname;
3170
+ const candidate = path$1.parse(parsed.pathname).name || parsed.hostname;
2837
3171
  return sanitizeBaseName(candidate);
2838
3172
  } catch {
2839
3173
  return sanitizeBaseName(sourceUrl);
@@ -2843,7 +3177,7 @@ function outputExtensionForMode(mode) {
2843
3177
  return mode === "text" ? ".txt" : ".md";
2844
3178
  }
2845
3179
  function extensionFromPath(filePath) {
2846
- const ext = path.extname(filePath).replace(".", "").toLowerCase();
3180
+ const ext = path$1.extname(filePath).replace(".", "").toLowerCase();
2847
3181
  return ext || void 0;
2848
3182
  }
2849
3183
  function normalizeMode(value) {
@@ -3077,15 +3411,15 @@ async function downloadToProject(projectPath, sourceUrl) {
3077
3411
  if (bytes.length > DEFAULT_MAX_DOWNLOAD_BYTES) {
3078
3412
  return { ok: false, error: `Downloaded file too large (${bytes.length} bytes > ${DEFAULT_MAX_DOWNLOAD_BYTES} bytes)` };
3079
3413
  }
3080
- const downloadDir = path.join(path.resolve(projectPath), ".research-pilot", "cache", "downloads");
3414
+ const downloadDir = path$1.join(path$1.resolve(projectPath), ".research-pilot", "cache", "downloads");
3081
3415
  await fsp.mkdir(downloadDir, { recursive: true });
3082
3416
  const urlObj = new URL(sourceUrl);
3083
3417
  const fromExt = extensionFromPath(urlObj.pathname);
3084
3418
  const fromContentType = detectFormatFromContentType(response.headers.get("content-type"));
3085
3419
  const chosenExt = FORMAT_EXTENSIONS[fromContentType || ""] || FORMAT_EXTENSIONS[fromExt || ""] || "bin";
3086
- const hash = createHash("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3420
+ const hash = createHash$1("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3087
3421
  const fileName = `${isoStamp()}-${hash}.${chosenExt}`;
3088
- const inputPath = path.join(downloadDir, fileName);
3422
+ const inputPath = path$1.join(downloadDir, fileName);
3089
3423
  await fsp.writeFile(inputPath, bytes);
3090
3424
  return {
3091
3425
  ok: true,
@@ -3165,9 +3499,9 @@ ${sections}
3165
3499
  `;
3166
3500
  }
3167
3501
  function buildPerRangeOutputPath(baseAbsolutePath, range, mode) {
3168
- const parsed = path.parse(baseAbsolutePath);
3502
+ const parsed = path$1.parse(baseAbsolutePath);
3169
3503
  const ext = parsed.ext || outputExtensionForMode(mode);
3170
- return path.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3504
+ return path$1.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3171
3505
  }
3172
3506
  function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3173
3507
  const content = text.slice(0, maxPreviewChars);
@@ -3179,14 +3513,14 @@ function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3179
3513
  };
3180
3514
  }
3181
3515
  function defaultOutputPath(args) {
3182
- const outputDir = path.join(
3183
- path.resolve(args.projectPath),
3516
+ const outputDir = path$1.join(
3517
+ path$1.resolve(args.projectPath),
3184
3518
  ".research-pilot",
3185
3519
  "cache",
3186
3520
  "converted"
3187
3521
  );
3188
- const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path.parse(args.inputPath || "document").name || "document");
3189
- return path.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3522
+ const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path$1.parse(args.inputPath || "document").name || "document");
3523
+ return path$1.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3190
3524
  }
3191
3525
  function failure(payload) {
3192
3526
  return toAgentResult("convert_document", { success: false, error: payload.error, data: payload });
@@ -3369,7 +3703,7 @@ function createConvertDocumentTool(ctx) {
3369
3703
  mode
3370
3704
  });
3371
3705
  }
3372
- await fsp.mkdir(path.dirname(outputAbsolutePath), { recursive: true });
3706
+ await fsp.mkdir(path$1.dirname(outputAbsolutePath), { recursive: true });
3373
3707
  let converter;
3374
3708
  let producedText = "";
3375
3709
  let pageCount;
@@ -3402,7 +3736,7 @@ function createConvertDocumentTool(ctx) {
3402
3736
  anyTruncated = true;
3403
3737
  }
3404
3738
  const segmentAbsPath = buildPerRangeOutputPath(outputAbsolutePath, range, mode);
3405
- await fsp.mkdir(path.dirname(segmentAbsPath), { recursive: true });
3739
+ await fsp.mkdir(path$1.dirname(segmentAbsPath), { recursive: true });
3406
3740
  await fsp.writeFile(segmentAbsPath, segmentText, "utf8");
3407
3741
  const segmentRelPath = toProjectRelative(projectPath, segmentAbsPath);
3408
3742
  const segmentPreview = buildPreview(segmentText);
@@ -3541,7 +3875,7 @@ function createDataAnalyzeTool(ctx) {
3541
3875
  suggestions: ["Valid task types: analyze, visualize, transform, model."]
3542
3876
  }));
3543
3877
  }
3544
- const absDataFile = path.resolve(ctx.workspacePath, filePath);
3878
+ const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
3545
3879
  if (!fs.existsSync(absDataFile)) {
3546
3880
  return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
3547
3881
  suggestions: [
@@ -3551,18 +3885,17 @@ function createDataAnalyzeTool(ctx) {
3551
3885
  context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
3552
3886
  }));
3553
3887
  }
3554
- ctx.onToolCall?.("data_analyze", { file_path: filePath, instructions, task_type: taskType });
3555
3888
  const runId = Date.now().toString(36);
3556
- const outputBase = path.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3557
- const figuresDir = path.join(outputBase, "figures");
3558
- const tablesDir = path.join(outputBase, "tables");
3559
- const dataDir = path.join(outputBase, "data");
3889
+ const outputBase = path$1.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3890
+ const figuresDir = path$1.join(outputBase, "figures");
3891
+ const tablesDir = path$1.join(outputBase, "tables");
3892
+ const dataDir = path$1.join(outputBase, "data");
3560
3893
  fs.mkdirSync(figuresDir, { recursive: true });
3561
3894
  fs.mkdirSync(tablesDir, { recursive: true });
3562
3895
  fs.mkdirSync(dataDir, { recursive: true });
3563
- const resultsFile = path.join(outputBase, "results.json");
3896
+ const resultsFile = path$1.join(outputBase, "results.json");
3564
3897
  const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
3565
- const ext = path.extname(absDataFile).toLowerCase();
3898
+ const ext = path$1.extname(absDataFile).toLowerCase();
3566
3899
  const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
3567
3900
  if (!ctx.callLlm) {
3568
3901
  return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
@@ -3610,7 +3943,7 @@ function createDataAnalyzeTool(ctx) {
3610
3943
  ""
3611
3944
  ].join("\n");
3612
3945
  const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
3613
- const scriptPath = path.join(outputBase, "script.py");
3946
+ const scriptPath = path$1.join(outputBase, "script.py");
3614
3947
  fs.writeFileSync(scriptPath, fullScript, "utf-8");
3615
3948
  try {
3616
3949
  const { stdout, stderr } = await execFileAsync("python3", [scriptPath], {
@@ -3637,7 +3970,7 @@ function createDataAnalyzeTool(ctx) {
3637
3970
  outputs.push({
3638
3971
  name: f,
3639
3972
  type,
3640
- path: path.relative(ctx.workspacePath, path.join(dir, f))
3973
+ path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
3641
3974
  });
3642
3975
  }
3643
3976
  }
@@ -3650,26 +3983,25 @@ function createDataAnalyzeTool(ctx) {
3650
3983
  summary: manifest.summary,
3651
3984
  warnings: manifest.warnings
3652
3985
  } : void 0,
3653
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
3986
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3654
3987
  runId
3655
3988
  };
3656
- ctx.onToolResult?.("data_analyze", payload);
3657
3989
  return toAgentResult("data_analyze", { success: true, data: payload });
3658
3990
  } catch (err) {
3659
3991
  const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
3660
3992
  return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
3661
3993
  retryable: true,
3662
3994
  suggestions: [
3663
- `Review the generated script at ${path.relative(ctx.workspacePath, scriptPath)} for errors.`,
3995
+ `Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
3664
3996
  "Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
3665
3997
  "Try simplifying the analysis instructions."
3666
3998
  ],
3667
3999
  context: {
3668
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4000
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3669
4001
  runId
3670
4002
  },
3671
4003
  data: {
3672
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4004
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3673
4005
  runId
3674
4006
  }
3675
4007
  }));
@@ -3717,20 +4049,168 @@ function wrapResearchTool(tool) {
3717
4049
  }
3718
4050
  function createResearchTools(ctx) {
3719
4051
  const tools = [];
3720
- tools.push(createWebSearchTool(ctx));
4052
+ tools.push(createWebSearchTool());
3721
4053
  tools.push(createWebFetchTool(ctx));
3722
4054
  tools.push(createLiteratureSearchTool(ctx));
3723
4055
  tools.push(createConvertDocumentTool(ctx));
3724
4056
  tools.push(createDataAnalyzeTool(ctx));
3725
- const memoryTools = createResearchMemoryTools({
4057
+ const artifactTools = createResearchMemoryTools({
3726
4058
  sessionId: ctx.sessionId,
3727
4059
  projectPath: ctx.projectPath
3728
4060
  });
3729
- for (const tool of memoryTools) {
4061
+ for (const tool of artifactTools) {
4062
+ tools.push(wrapResearchTool(tool));
4063
+ }
4064
+ const structuredMemoryTools = createMemoryTools(ctx.projectPath);
4065
+ for (const tool of structuredMemoryTools) {
3730
4066
  tools.push(wrapResearchTool(tool));
3731
4067
  }
3732
4068
  return tools;
3733
4069
  }
4070
+ const VALID_TYPES = ["user", "feedback", "project", "reference"];
4071
+ const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
4072
+
4073
+ Rules:
4074
+ - Only extract DURABLE, IMPORTANT information — things a future session would need.
4075
+ - Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
4076
+ - Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
4077
+ - Do NOT extract: routine task results, ephemeral details, things already in workspace files.
4078
+ - Each memory should be atomic — one concept per entry.
4079
+ - If nothing is worth saving, return an empty array.
4080
+
4081
+ Return ONLY a JSON array (no markdown fences, no explanation):
4082
+ [{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
4083
+ Or: []`;
4084
+ function simplifyMessages(messages, maxMessages) {
4085
+ const recent = messages.slice(-20);
4086
+ const result = [];
4087
+ for (const msg of recent) {
4088
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
4089
+ let content = "";
4090
+ if (typeof msg.content === "string") {
4091
+ content = msg.content;
4092
+ } else if (Array.isArray(msg.content)) {
4093
+ for (const block of msg.content) {
4094
+ if (block && typeof block === "object" && "type" in block) {
4095
+ if (block.type === "text" && "text" in block) {
4096
+ const text = block.text;
4097
+ content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
4098
+ content += "\n";
4099
+ } else if (block.type === "tool_use" && "name" in block) {
4100
+ content += `[Called ${block.name}]
4101
+ `;
4102
+ }
4103
+ }
4104
+ }
4105
+ }
4106
+ content = content.trim();
4107
+ if (content) {
4108
+ result.push({
4109
+ role: msg.role,
4110
+ content: content.slice(0, 2e3),
4111
+ timestamp: Date.now()
4112
+ });
4113
+ }
4114
+ }
4115
+ return result;
4116
+ }
4117
+ function agentCalledSaveMemoryThisTurn(messages) {
4118
+ for (let i = messages.length - 1; i >= 0; i--) {
4119
+ const msg = messages[i];
4120
+ if (msg.role === "user") break;
4121
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
4122
+ for (const block of msg.content) {
4123
+ if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
4124
+ if (block.name === "save-memory") return true;
4125
+ }
4126
+ }
4127
+ }
4128
+ }
4129
+ return false;
4130
+ }
4131
+ async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
4132
+ if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT !== "1") return;
4133
+ if (turnCount % extractEveryN !== 0) return;
4134
+ if (agentCalledSaveMemoryThisTurn(messages)) {
4135
+ if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
4136
+ return;
4137
+ }
4138
+ try {
4139
+ const simplified = simplifyMessages(messages, 20);
4140
+ if (simplified.length < 2) return;
4141
+ simplified.push({
4142
+ role: "user",
4143
+ content: EXTRACTION_PROMPT,
4144
+ timestamp: Date.now()
4145
+ });
4146
+ const result = await completeSimple(config.model, {
4147
+ systemPrompt: config.systemPrompt,
4148
+ messages: simplified
4149
+ }, {
4150
+ maxTokens: 1024,
4151
+ apiKey: config.apiKey
4152
+ });
4153
+ const textContent = result.content.find((c) => c.type === "text");
4154
+ const text = textContent?.text?.trim() ?? "";
4155
+ if (!text || text === "[]") return;
4156
+ let jsonStr;
4157
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
4158
+ if (fenceMatch) {
4159
+ jsonStr = fenceMatch[1].trim();
4160
+ } else {
4161
+ const firstBracket = text.indexOf("[");
4162
+ const lastBracket = text.lastIndexOf("]");
4163
+ if (firstBracket !== -1 && lastBracket > firstBracket) {
4164
+ jsonStr = text.slice(firstBracket, lastBracket + 1);
4165
+ } else {
4166
+ jsonStr = text;
4167
+ }
4168
+ }
4169
+ let extracted;
4170
+ try {
4171
+ extracted = JSON.parse(jsonStr);
4172
+ } catch (parseErr) {
4173
+ if (config.debug) {
4174
+ console.warn("[Extractor] JSON parse failed:", parseErr, "Raw text:", text.slice(0, 200));
4175
+ }
4176
+ return;
4177
+ }
4178
+ if (!Array.isArray(extracted) || extracted.length === 0) return;
4179
+ ensureMemoryDir(config.projectPath);
4180
+ const validEntries = [];
4181
+ for (const mem of extracted) {
4182
+ if (!mem.type || !mem.name || !mem.content) continue;
4183
+ if (!VALID_TYPES.includes(mem.type)) continue;
4184
+ const desc = (mem.description || "").trim();
4185
+ const contentFirstLine = mem.content.split("\n").find((l) => l.trim().length > 0) || "";
4186
+ const description = (desc || contentFirstLine.replace(/^#+\s*/, "").trim().slice(0, 120) || mem.name).replace(/\n/g, " ");
4187
+ validEntries.push({
4188
+ frontmatter: {
4189
+ name: mem.name,
4190
+ description,
4191
+ type: mem.type
4192
+ },
4193
+ content: mem.content,
4194
+ filename: memoryFilename(mem.type, mem.name)
4195
+ });
4196
+ }
4197
+ if (validEntries.length === 0) return;
4198
+ await withIndexLock(() => {
4199
+ for (const entry of validEntries) {
4200
+ writeMemoryFile(config.projectPath, entry);
4201
+ }
4202
+ const allEntries = listMemoryFiles(config.projectPath);
4203
+ updateAgentMdIndex(config.projectPath, allEntries);
4204
+ });
4205
+ if (config.debug) {
4206
+ console.log(`[Extractor] Saved ${validEntries.length} memories from conversation`);
4207
+ }
4208
+ } catch (err) {
4209
+ if (config.debug) {
4210
+ console.warn("[Extractor] Failed:", err);
4211
+ }
4212
+ }
4213
+ }
3734
4214
  const SKILL_FILE_NAME = "SKILL.md";
3735
4215
  const MAX_SCAN_DEPTH = 3;
3736
4216
  let _builtinSkillsRoot = null;
@@ -3775,7 +4255,7 @@ function discoverSkillFiles(rootDir) {
3775
4255
  continue;
3776
4256
  }
3777
4257
  for (const entry of entries) {
3778
- const abs = path.join(current.dir, entry.name);
4258
+ const abs = path$1.join(current.dir, entry.name);
3779
4259
  if (entry.isFile() && entry.name === SKILL_FILE_NAME) {
3780
4260
  files.push(abs);
3781
4261
  continue;
@@ -3806,7 +4286,7 @@ function parseSkillFile(skillFile, displayPath, source) {
3806
4286
  tags,
3807
4287
  triggers,
3808
4288
  path: displayPath,
3809
- dir: path.dirname(skillFile),
4289
+ dir: path$1.dirname(skillFile),
3810
4290
  source,
3811
4291
  content
3812
4292
  };
@@ -3822,34 +4302,34 @@ function inferCategory(name, description) {
3822
4302
  return "General";
3823
4303
  }
3824
4304
  function loadBuiltinSkills() {
3825
- const skillsRoot = _builtinSkillsRoot ?? path.dirname(fileURLToPath(import.meta.url));
4305
+ const skillsRoot = _builtinSkillsRoot ?? path$1.dirname(fileURLToPath(import.meta.url));
3826
4306
  if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
3827
4307
  return [];
3828
4308
  }
3829
4309
  const files = discoverSkillFiles(skillsRoot);
3830
4310
  const byName = /* @__PURE__ */ new Map();
3831
4311
  for (const file of files) {
3832
- const entry = parseSkillFile(file, `[builtin] ${path.relative(skillsRoot, file)}`, "builtin");
4312
+ const entry = parseSkillFile(file, `[builtin] ${path$1.relative(skillsRoot, file)}`, "builtin");
3833
4313
  if (entry) byName.set(entry.name, entry);
3834
4314
  }
3835
4315
  return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
3836
4316
  }
3837
4317
  function loadWorkspaceSkills(workspacePath) {
3838
- const skillRoot = path.resolve(workspacePath, ".research-pilot", "skills");
4318
+ const skillRoot = path$1.resolve(workspacePath, ".research-pilot", "skills");
3839
4319
  if (!fs.existsSync(skillRoot) || !fs.statSync(skillRoot).isDirectory()) {
3840
4320
  return [];
3841
4321
  }
3842
4322
  const files = discoverSkillFiles(skillRoot);
3843
- const entries = files.map((file) => parseSkillFile(file, path.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
4323
+ const entries = files.map((file) => parseSkillFile(file, path$1.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
3844
4324
  return entries.sort((a, b) => a.name.localeCompare(b.name));
3845
4325
  }
3846
4326
  function loadUserSkills() {
3847
- const userRoot = path.resolve(os.homedir(), ".research-pilot", "skills");
4327
+ const userRoot = path$1.resolve(os.homedir(), ".research-pilot", "skills");
3848
4328
  if (!fs.existsSync(userRoot) || !fs.statSync(userRoot).isDirectory()) {
3849
4329
  return [];
3850
4330
  }
3851
4331
  const files = discoverSkillFiles(userRoot);
3852
- const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
4332
+ const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path$1.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
3853
4333
  return entries.sort((a, b) => a.name.localeCompare(b.name));
3854
4334
  }
3855
4335
  function loadAllSkills(workspacePath) {
@@ -3894,7 +4374,7 @@ function resolveSkillDependencies(allSkills, directSelection) {
3894
4374
  return result;
3895
4375
  }
3896
4376
  function readEnabledSkills(workspacePath) {
3897
- const configPath = path.resolve(workspacePath, ".research-pilot", "skills-config.json");
4377
+ const configPath = path$1.resolve(workspacePath, ".research-pilot", "skills-config.json");
3898
4378
  try {
3899
4379
  const raw = fs.readFileSync(configPath, "utf8");
3900
4380
  const config = JSON.parse(raw);
@@ -3904,9 +4384,9 @@ function readEnabledSkills(workspacePath) {
3904
4384
  return null;
3905
4385
  }
3906
4386
  function writeEnabledSkills(workspacePath, enabledSkills) {
3907
- const configDir = path.resolve(workspacePath, ".research-pilot");
4387
+ const configDir = path$1.resolve(workspacePath, ".research-pilot");
3908
4388
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
3909
- const configPath = path.join(configDir, "skills-config.json");
4389
+ const configPath = path$1.join(configDir, "skills-config.json");
3910
4390
  fs.writeFileSync(configPath, JSON.stringify({ enabledSkills }, null, 2), "utf8");
3911
4391
  }
3912
4392
  function buildSkillManifests(workspacePath) {
@@ -3930,15 +4410,15 @@ function buildSkillManifests(workspacePath) {
3930
4410
  });
3931
4411
  }
3932
4412
  function installSkillToWorkspace(workspacePath, skillName, skillDir) {
3933
- const destDir = path.resolve(workspacePath, ".research-pilot", "skills", skillName);
4413
+ const destDir = path$1.resolve(workspacePath, ".research-pilot", "skills", skillName);
3934
4414
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
3935
4415
  copyDirSync(skillDir, destDir);
3936
4416
  }
3937
4417
  function copyDirSync(src, dest) {
3938
4418
  const entries = fs.readdirSync(src, { withFileTypes: true });
3939
4419
  for (const entry of entries) {
3940
- const srcPath = path.join(src, entry.name);
3941
- const destPath = path.join(dest, entry.name);
4420
+ const srcPath = path$1.join(src, entry.name);
4421
+ const destPath = path$1.join(dest, entry.name);
3942
4422
  if (entry.isDirectory()) {
3943
4423
  if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
3944
4424
  copyDirSync(srcPath, destPath);
@@ -4139,6 +4619,7 @@ async function createCoordinator(config) {
4139
4619
  onStream,
4140
4620
  onToolCall,
4141
4621
  onToolResult,
4622
+ onToolProgress,
4142
4623
  onUsage,
4143
4624
  onSkillLoaded
4144
4625
  } = config;
@@ -4210,11 +4691,11 @@ async function createCoordinator(config) {
4210
4691
  }
4211
4692
  }
4212
4693
  }
4213
- const wrappedOnToolResult = (tool, result, args) => {
4694
+ const wrappedOnToolResult = (tool, result, args, toolCallId) => {
4214
4695
  if (activeTurnToolCallCount !== null) {
4215
4696
  activeTurnToolCallCount++;
4216
4697
  }
4217
- onToolResult?.(tool, result, args);
4698
+ onToolResult?.(tool, result, args, toolCallId);
4218
4699
  };
4219
4700
  const toolCtx = {
4220
4701
  workspacePath: projectPath,
@@ -4337,14 +4818,14 @@ The conversation continues below.`,
4337
4818
  }
4338
4819
  },
4339
4820
  beforeToolCall: async (ctx) => {
4340
- onToolCall?.(ctx.toolCall.name, ctx.args);
4821
+ onToolCall?.(ctx.toolCall.name, ctx.args, ctx.toolCall.id);
4341
4822
  if (debug) {
4342
4823
  console.log(` [Tool] ${ctx.toolCall.name}(${JSON.stringify(ctx.args).slice(0, 120)}...)`);
4343
4824
  }
4344
4825
  return void 0;
4345
4826
  },
4346
4827
  afterToolCall: async (ctx) => {
4347
- wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args);
4828
+ wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args, ctx.toolCall.id);
4348
4829
  if (ctx.toolCall.name === "load_skill" && onSkillLoaded) {
4349
4830
  const args = ctx.args;
4350
4831
  const result = ctx.result;
@@ -4355,7 +4836,7 @@ The conversation continues below.`,
4355
4836
  return void 0;
4356
4837
  }
4357
4838
  });
4358
- if (onStream || onUsage) {
4839
+ if (onStream || onUsage || onToolProgress) {
4359
4840
  agent.subscribe((event) => {
4360
4841
  if (event.type === "message_update" && onStream) {
4361
4842
  if (event.assistantMessageEvent.type === "text_delta") {
@@ -4369,6 +4850,15 @@ The conversation continues below.`,
4369
4850
  onUsage(usage, usage.cost);
4370
4851
  }
4371
4852
  }
4853
+ if (onToolProgress) {
4854
+ if (event.type === "tool_execution_start") {
4855
+ onToolProgress(event.toolName, event.toolCallId, "start", { args: event.args });
4856
+ } else if (event.type === "tool_execution_update") {
4857
+ onToolProgress(event.toolName, event.toolCallId, "update", { partialResult: event.partialResult });
4858
+ } else if (event.type === "tool_execution_end") {
4859
+ onToolProgress(event.toolName, event.toolCallId, "end", { result: event.result, isError: event.isError });
4860
+ }
4861
+ }
4372
4862
  });
4373
4863
  }
4374
4864
  async function clearSessionMemory() {
@@ -4478,24 +4968,17 @@ ${historyText}`,
4478
4968
  ## User Instructions (agent.md)
4479
4969
 
4480
4970
  ${agentMdContent}`;
4481
- }
4482
- if (skillSummariesPrompt) {
4483
- enrichedSystem = `${enrichedSystem}
4484
-
4485
- ${skillSummariesPrompt}`;
4486
4971
  }
4487
4972
  agent.setSystemPrompt(enrichedSystem);
4488
- let userMessage = message;
4489
- if (mentionContext || summaryContext) {
4490
- const contextParts = [];
4491
- if (summaryContext) contextParts.push(summaryContext);
4492
- if (mentionContext) contextParts.push(mentionContext);
4493
- userMessage = `${contextParts.join("\n\n")}
4973
+ const contextParts = [];
4974
+ if (summaryContext) contextParts.push(summaryContext);
4975
+ if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
4976
+ if (mentionContext) contextParts.push(mentionContext);
4977
+ let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
4494
4978
 
4495
4979
  ---
4496
4980
 
4497
- ${message}`;
4498
- }
4981
+ ${message}` : message;
4499
4982
  let perTurnToolCallCount = 0;
4500
4983
  activeTurnToolCallCount = 0;
4501
4984
  try {
@@ -4541,6 +5024,11 @@ ${message}`;
4541
5024
  });
4542
5025
  if (turnHistory.length > 8) turnHistory.shift();
4543
5026
  void maybeGenerateSummary();
5027
+ void maybeExtractMemories(
5028
+ { projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
5029
+ agent.state.messages,
5030
+ turnCount
5031
+ );
4544
5032
  if (debug) {
4545
5033
  console.log(`[Chat] Result: success=true, hasOutput=${!!responseText}, turn=${turnCount}`);
4546
5034
  }
@@ -4596,10 +5084,10 @@ function sessionSummaryGet(projectPath, sessionId) {
4596
5084
  }
4597
5085
  }
4598
5086
  class RateLimiter {
4599
- constructor(configs) {
5087
+ constructor(configs2) {
4600
5088
  this.timestamps = /* @__PURE__ */ new Map();
4601
5089
  this.activeCounts = /* @__PURE__ */ new Map();
4602
- this.configs = configs;
5090
+ this.configs = configs2;
4603
5091
  }
4604
5092
  /**
4605
5093
  * Wait until a request slot is available for the given source.
@@ -5200,11 +5688,11 @@ function searchEntities(projectPath, query, types) {
5200
5688
  score: hit.score
5201
5689
  }));
5202
5690
  }
5203
- const MENTION_RE = /@(note|paper|data|file|url):(?:"([^"]+)"|(\S+))/g;
5691
+ const MENTION_RE = /@(note|paper|data|file|url):(?:"((?:[^"\\]|\\.)*)"|(\S+))/g;
5204
5692
  function parseMentions(message) {
5205
5693
  const mentions = [];
5206
5694
  const cleanMessage = message.replace(MENTION_RE, (_match, type, quoted, unquoted) => {
5207
- const key = quoted || unquoted;
5695
+ const key = (quoted || unquoted).replace(/\\"/g, '"');
5208
5696
  const raw = _match;
5209
5697
  mentions.push({ type, key, raw });
5210
5698
  return `[${type}: ${key}]`;
@@ -5212,7 +5700,7 @@ function parseMentions(message) {
5212
5700
  return { cleanMessage, mentions };
5213
5701
  }
5214
5702
  function getCacheKey(filePath, mtime) {
5215
- const hash = createHash$1("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
5703
+ const hash = createHash("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
5216
5704
  const name = basename(filePath).replace(/[^a-zA-Z0-9.-]/g, "_");
5217
5705
  return `${name}-${hash}.json`;
5218
5706
  }
@@ -5229,11 +5717,11 @@ function getCachedMarkdown(filePath, projectPath) {
5229
5717
  const mtime = stat.mtimeMs;
5230
5718
  const cacheDir = join(projectPath, PATHS.documentCache);
5231
5719
  const cacheKey2 = getCacheKey(filePath, mtime);
5232
- const cachePath = join(cacheDir, cacheKey2);
5233
- if (!existsSync(cachePath)) {
5720
+ const cachePath2 = join(cacheDir, cacheKey2);
5721
+ if (!existsSync(cachePath2)) {
5234
5722
  return null;
5235
5723
  }
5236
- const entry = JSON.parse(readFileSync(cachePath, "utf-8"));
5724
+ const entry = JSON.parse(readFileSync(cachePath2, "utf-8"));
5237
5725
  if (entry.sourcePath !== filePath || entry.sourceMtime !== mtime) {
5238
5726
  return null;
5239
5727
  }
@@ -5248,14 +5736,14 @@ function setCachedMarkdown(filePath, markdown, projectPath) {
5248
5736
  const mtime = stat.mtimeMs;
5249
5737
  const cacheDir = ensureCacheDir(projectPath);
5250
5738
  const cacheKey2 = getCacheKey(filePath, mtime);
5251
- const cachePath = join(cacheDir, cacheKey2);
5739
+ const cachePath2 = join(cacheDir, cacheKey2);
5252
5740
  const entry = {
5253
5741
  sourcePath: filePath,
5254
5742
  sourceMtime: mtime,
5255
5743
  markdown,
5256
5744
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
5257
5745
  };
5258
- writeFileSync(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5746
+ writeFileSync(cachePath2, JSON.stringify(entry, null, 2), "utf-8");
5259
5747
  } catch (err) {
5260
5748
  console.warn("[document-cache] Failed to cache markdown:", err);
5261
5749
  }
@@ -5315,6 +5803,10 @@ function resolveEntity(ref, dir, entityType, projectPath) {
5315
5803
  const DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".docx", ".xlsx", ".pptx", ".doc", ".xls", ".ppt", ".epub"]);
5316
5804
  function resolveFile(ref, projectPath) {
5317
5805
  const filePath = resolve(projectPath, ref.key);
5806
+ const normalizedProject = resolve(projectPath);
5807
+ if (!filePath.startsWith(normalizedProject + "/") && filePath !== normalizedProject) {
5808
+ return { ref, label: `file: ${ref.key}`, content: "", error: `Path outside workspace: ${ref.key}` };
5809
+ }
5318
5810
  if (!existsSync(filePath)) {
5319
5811
  return { ref, label: `file: ${ref.key}`, content: "", error: `File not found: ${ref.key}` };
5320
5812
  }
@@ -5427,6 +5919,9 @@ NOTE: This is a data entity. The actual data is in the file at the path above. U
5427
5919
  }
5428
5920
  return JSON.stringify(entity, null, 2);
5429
5921
  }
5922
+ const REFRESH_THROTTLE_MS = 5e3;
5923
+ const MAX_FILES = 5e3;
5924
+ const MAX_DEPTH = 8;
5430
5925
  const SKIP_DIRS = /* @__PURE__ */ new Set([
5431
5926
  "node_modules",
5432
5927
  "__pycache__",
@@ -5439,10 +5934,58 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
5439
5934
  ".venv",
5440
5935
  "venv"
5441
5936
  ]);
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;
5937
+ let cachedFiles = [];
5938
+ let cachedProjectPath = "";
5939
+ let lastRefreshAt = 0;
5940
+ let refreshPromise = null;
5941
+ async function getFileList(projectPath) {
5942
+ if (projectPath !== cachedProjectPath) {
5943
+ cachedFiles = [];
5944
+ cachedProjectPath = projectPath;
5945
+ lastRefreshAt = 0;
5946
+ }
5947
+ const now = Date.now();
5948
+ if (cachedFiles.length > 0 && now - lastRefreshAt < REFRESH_THROTTLE_MS) {
5949
+ return cachedFiles;
5950
+ }
5951
+ if (refreshPromise) return refreshPromise;
5952
+ refreshPromise = refreshFileList(projectPath).finally(() => {
5953
+ refreshPromise = null;
5954
+ });
5955
+ return refreshPromise;
5956
+ }
5957
+ async function refreshFileList(projectPath) {
5958
+ try {
5959
+ const files = await gitLsFiles(projectPath);
5960
+ cachedFiles = files.slice(0, MAX_FILES);
5961
+ } catch {
5962
+ cachedFiles = walkFilesSync(projectPath);
5963
+ }
5964
+ lastRefreshAt = Date.now();
5965
+ return cachedFiles;
5966
+ }
5967
+ function gitLsFiles(cwd) {
5968
+ return new Promise((resolve2, reject) => {
5969
+ execFile$1(
5970
+ "git",
5971
+ ["ls-files", "-c", "-o", "--exclude-standard"],
5972
+ { cwd, maxBuffer: 20 * 1024 * 1024, timeout: 5e3 },
5973
+ (err, stdout) => {
5974
+ if (err) return reject(err);
5975
+ const files = stdout.split("\n").filter(Boolean);
5976
+ if (files.length === 0) return reject(new Error("empty"));
5977
+ resolve2(files);
5978
+ }
5979
+ );
5980
+ });
5981
+ }
5982
+ function walkFilesSync(root) {
5983
+ const out = [];
5984
+ walk(root, "", 0, out);
5985
+ return out;
5986
+ }
5987
+ function walk(root, rel, depth, out) {
5988
+ if (depth > MAX_DEPTH || out.length >= MAX_FILES) return;
5446
5989
  const dir = rel ? join(root, rel) : root;
5447
5990
  let entries;
5448
5991
  try {
@@ -5452,7 +5995,7 @@ function walkFiles(root, rel, depth, out) {
5452
5995
  }
5453
5996
  for (const name of entries) {
5454
5997
  if (name.startsWith(".")) continue;
5455
- if (out.length >= MAX_FILE_CANDIDATES) return;
5998
+ if (out.length >= MAX_FILES) return;
5456
5999
  const childRel = rel ? `${rel}/${name}` : name;
5457
6000
  const full = join(dir, name);
5458
6001
  let stat;
@@ -5463,22 +6006,107 @@ function walkFiles(root, rel, depth, out) {
5463
6006
  }
5464
6007
  if (stat.isDirectory()) {
5465
6008
  if (SKIP_DIRS.has(name)) continue;
5466
- walkFiles(root, childRel, depth + 1, out);
6009
+ walk(root, childRel, depth + 1, out);
5467
6010
  } else {
5468
- out.push({
5469
- type: "file",
5470
- value: childRel,
5471
- label: childRel,
5472
- detail: `${(stat.size / 1024).toFixed(1)}KB`
5473
- });
6011
+ out.push(childRel);
6012
+ }
6013
+ }
6014
+ }
6015
+ let cache = null;
6016
+ let cachePath = "";
6017
+ function getEntityCache(projectPath) {
6018
+ if (cache && cachePath === projectPath) return cache;
6019
+ cache = {
6020
+ notes: listNotes(projectPath),
6021
+ papers: listLiterature(projectPath),
6022
+ data: listData(projectPath)
6023
+ };
6024
+ cachePath = projectPath;
6025
+ return cache;
6026
+ }
6027
+ function invalidateEntityCache() {
6028
+ cache = null;
6029
+ }
6030
+ const SCORE_MATCH = 16;
6031
+ const BONUS_FIRST_CHAR = 8;
6032
+ const BONUS_BOUNDARY = 8;
6033
+ const BONUS_CONSECUTIVE = 4;
6034
+ const PENALTY_GAP_START = 3;
6035
+ const PENALTY_GAP_EXT = 1;
6036
+ function fuzzyMatch(items, needle, getText, limit = 30) {
6037
+ if (!needle) {
6038
+ return items.slice(0, limit).map((item) => ({ item, score: 0 }));
6039
+ }
6040
+ const needleLower = needle.toLowerCase();
6041
+ const results = [];
6042
+ let worstInTopK = -Infinity;
6043
+ for (const item of items) {
6044
+ const haystack = getText(item);
6045
+ const score = scoreMatch(needleLower, haystack.toLowerCase(), haystack);
6046
+ if (score <= 0) continue;
6047
+ if (results.length >= limit && score <= worstInTopK) continue;
6048
+ insertSorted(results, { item, score }, limit);
6049
+ if (results.length >= limit) {
6050
+ worstInTopK = results[results.length - 1].score;
6051
+ }
6052
+ }
6053
+ return results;
6054
+ }
6055
+ function scoreMatch(needle, haystackLower, haystack) {
6056
+ let score = 0;
6057
+ let ni = 0;
6058
+ let consecutive = 0;
6059
+ let lastMatchIdx = -1;
6060
+ for (let hi = 0; hi < haystackLower.length && ni < needle.length; hi++) {
6061
+ if (haystackLower[hi] === needle[ni]) {
6062
+ score += SCORE_MATCH;
6063
+ if (hi === 0) score += BONUS_FIRST_CHAR;
6064
+ if (isBoundary(haystack, hi)) score += BONUS_BOUNDARY;
6065
+ if (lastMatchIdx === hi - 1) {
6066
+ consecutive++;
6067
+ score += BONUS_CONSECUTIVE * consecutive;
6068
+ } else {
6069
+ const gap = lastMatchIdx >= 0 ? hi - lastMatchIdx - 1 : 0;
6070
+ if (gap > 0) score -= PENALTY_GAP_START + PENALTY_GAP_EXT * (gap - 1);
6071
+ consecutive = 1;
6072
+ }
6073
+ lastMatchIdx = hi;
6074
+ ni++;
5474
6075
  }
5475
6076
  }
6077
+ return ni === needle.length ? score : 0;
6078
+ }
6079
+ function isBoundary(s, i) {
6080
+ if (i === 0) return true;
6081
+ const prev = s.charCodeAt(i - 1);
6082
+ const cur = s.charCodeAt(i);
6083
+ if (prev === 47 || prev === 92 || prev === 45 || prev === 95 || prev === 46 || prev === 32) {
6084
+ return true;
6085
+ }
6086
+ if (prev >= 97 && prev <= 122 && cur >= 65 && cur <= 90) return true;
6087
+ return false;
5476
6088
  }
5477
- function getCandidates(projectPath, typeFilter, query) {
6089
+ function insertSorted(arr, entry, limit) {
6090
+ let lo = 0;
6091
+ let hi = arr.length;
6092
+ while (lo < hi) {
6093
+ const mid = lo + hi >>> 1;
6094
+ if (arr[mid].score >= entry.score) {
6095
+ lo = mid + 1;
6096
+ } else {
6097
+ hi = mid;
6098
+ }
6099
+ }
6100
+ arr.splice(lo, 0, entry);
6101
+ if (arr.length > limit) arr.pop();
6102
+ }
6103
+ const MAX_EMPTY_QUERY_FILES = 30;
6104
+ async function getCandidates(projectPath, typeFilter, query) {
5478
6105
  const candidates = [];
5479
6106
  const q = query?.toLowerCase() ?? "";
6107
+ const entities = getEntityCache(projectPath);
5480
6108
  if (!typeFilter || typeFilter === "note") {
5481
- for (const n of listNotes(projectPath)) {
6109
+ for (const n of entities.notes) {
5482
6110
  candidates.push({
5483
6111
  type: "note",
5484
6112
  value: n.id.slice(0, 8),
@@ -5488,7 +6116,7 @@ function getCandidates(projectPath, typeFilter, query) {
5488
6116
  }
5489
6117
  }
5490
6118
  if (!typeFilter || typeFilter === "paper") {
5491
- for (const l of listLiterature(projectPath)) {
6119
+ for (const l of entities.papers) {
5492
6120
  candidates.push({
5493
6121
  type: "paper",
5494
6122
  value: l.citeKey,
@@ -5498,7 +6126,7 @@ function getCandidates(projectPath, typeFilter, query) {
5498
6126
  }
5499
6127
  }
5500
6128
  if (!typeFilter || typeFilter === "data") {
5501
- for (const d of listData(projectPath)) {
6129
+ for (const d of entities.data) {
5502
6130
  candidates.push({
5503
6131
  type: "data",
5504
6132
  value: d.id.slice(0, 8),
@@ -5508,16 +6136,25 @@ function getCandidates(projectPath, typeFilter, query) {
5508
6136
  }
5509
6137
  }
5510
6138
  if (!typeFilter || typeFilter === "file") {
5511
- if (existsSync(projectPath)) {
5512
- walkFiles(projectPath, "", 0, candidates);
6139
+ const files = await getFileList(projectPath);
6140
+ for (const rel of files) {
6141
+ let detail;
6142
+ try {
6143
+ const stat = statSync(join(projectPath, rel));
6144
+ detail = `${(stat.size / 1024).toFixed(1)}KB`;
6145
+ } catch {
6146
+ }
6147
+ candidates.push({ type: "file", value: rel, label: rel, detail });
5513
6148
  }
5514
6149
  }
5515
6150
  if (q) {
5516
- return candidates.filter(
5517
- (c) => c.label.toLowerCase().includes(q) || c.value.toLowerCase().includes(q) || c.detail?.toLowerCase().includes(q)
5518
- );
6151
+ return fuzzyMatch(
6152
+ candidates,
6153
+ q,
6154
+ (c) => `${c.label} ${c.value} ${c.detail ?? ""}`,
6155
+ 50
6156
+ ).map((r) => r.item);
5519
6157
  }
5520
- const MAX_EMPTY_QUERY_FILES = 30;
5521
6158
  const fileCount = candidates.filter((c) => c.type === "file").length;
5522
6159
  if (fileCount > MAX_EMPTY_QUERY_FILES) {
5523
6160
  const nonFiles = candidates.filter((c) => c.type !== "file");
@@ -5535,6 +6172,10 @@ class RealtimeBuffer {
5535
6172
  isStreaming = false;
5536
6173
  progressItems = [];
5537
6174
  activityEvents = [];
6175
+ /** Tool events for chat-inline rendering (mirrors tool-events-store) */
6176
+ toolEvents = [];
6177
+ /** Track tool-call start times keyed by toolCallId for duration computation */
6178
+ toolCallStartTimes = /* @__PURE__ */ new Map();
5538
6179
  /** Append a streaming text chunk (called from onStream callback) */
5539
6180
  appendChunk(chunk) {
5540
6181
  this.streamingText += chunk;
@@ -5549,18 +6190,45 @@ class RealtimeBuffer {
5549
6190
  this.progressItems.push(item);
5550
6191
  }
5551
6192
  }
5552
- /** Record an activity event */
6193
+ /** Record an activity event and track tool-call start times */
5553
6194
  pushActivity(event) {
6195
+ if (event.type === "tool-call" && event.toolCallId) {
6196
+ this.toolCallStartTimes.set(event.toolCallId, Date.now());
6197
+ }
5554
6198
  this.activityEvents.push(event);
5555
6199
  }
6200
+ /** Record a tool event for chat-inline rendering */
6201
+ pushToolEvent(event) {
6202
+ this.toolEvents.push(event);
6203
+ }
6204
+ /** Update a tool event by toolCallId (for tool-result merge) */
6205
+ updateToolEvent(toolCallId, patch) {
6206
+ const idx = this.toolEvents.findLastIndex((e) => e.toolCallId === toolCallId);
6207
+ if (idx !== -1) {
6208
+ this.toolEvents[idx] = { ...this.toolEvents[idx], ...patch };
6209
+ }
6210
+ }
6211
+ /** Clear tool events (on new run or finalize) */
6212
+ clearToolEvents() {
6213
+ this.toolEvents = [];
6214
+ }
6215
+ /** Pop and return the start time for a tool-call, or undefined if not found */
6216
+ popToolCallStartTime(toolCallId) {
6217
+ const t = this.toolCallStartTimes.get(toolCallId);
6218
+ if (t !== void 0) this.toolCallStartTimes.delete(toolCallId);
6219
+ return t;
6220
+ }
5556
6221
  /** Clear progress and activity (called on project close or explicit reset) */
5557
6222
  clearRun() {
5558
6223
  this.progressItems = [];
5559
6224
  this.activityEvents = [];
6225
+ this.toolEvents = [];
5560
6226
  }
5561
6227
  /** Clear only activity events (called on new agent run) */
5562
6228
  clearActivity() {
5563
6229
  this.activityEvents = [];
6230
+ this.toolEvents = [];
6231
+ this.toolCallStartTimes.clear();
5564
6232
  }
5565
6233
  /** Mark streaming finished (called on agent:done) */
5566
6234
  finishStreaming() {
@@ -5573,6 +6241,8 @@ class RealtimeBuffer {
5573
6241
  this.isStreaming = false;
5574
6242
  this.progressItems = [];
5575
6243
  this.activityEvents = [];
6244
+ this.toolEvents = [];
6245
+ this.toolCallStartTimes.clear();
5576
6246
  }
5577
6247
  /** Return a snapshot the renderer can use to hydrate stores */
5578
6248
  getSnapshot() {
@@ -5580,17 +6250,366 @@ class RealtimeBuffer {
5580
6250
  streamingText: this.streamingText,
5581
6251
  isStreaming: this.isStreaming,
5582
6252
  progressItems: [...this.progressItems],
5583
- activityEvents: [...this.activityEvents]
6253
+ activityEvents: [...this.activityEvents],
6254
+ toolEvents: [...this.toolEvents]
5584
6255
  };
5585
6256
  }
5586
6257
  }
5587
6258
  function createRealtimeBuffer() {
5588
6259
  return new RealtimeBuffer();
5589
6260
  }
6261
+ function getFileName(path2) {
6262
+ if (!path2) return "";
6263
+ const parts = path2.replace(/\\/g, "/").split("/");
6264
+ return parts[parts.length - 1] || path2;
6265
+ }
6266
+ function truncStr(s, max) {
6267
+ if (!s) return "";
6268
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
6269
+ }
6270
+ function safeRecord(obj) {
6271
+ return obj && typeof obj === "object" ? obj : {};
6272
+ }
6273
+ function extractResultText(result) {
6274
+ const r = safeRecord(result);
6275
+ const content = r.content;
6276
+ return content?.[0]?.text || "";
6277
+ }
6278
+ function lastNLines(text, n) {
6279
+ const lines = text.split("\n").filter(Boolean);
6280
+ return lines.slice(-n).join("\n");
6281
+ }
6282
+ const configs = [
6283
+ // ── File tools ────────────────────────
6284
+ {
6285
+ name: "read",
6286
+ displayName: "Read",
6287
+ icon: "FileText",
6288
+ category: "file",
6289
+ formatCallSummary: (a) => {
6290
+ const path2 = a.path || "";
6291
+ const offset = a.offset;
6292
+ const limit = a.limit;
6293
+ const suffix = offset ? ` · lines ${offset}-${offset + (limit || 2e3)}` : "";
6294
+ return `${getFileName(path2)}${suffix}`;
6295
+ },
6296
+ formatCallDetail: (a) => ({ path: a.path, offset: a.offset, limit: a.limit }),
6297
+ formatResultSummary: (result) => {
6298
+ const text = extractResultText(result);
6299
+ const lineCount = text ? text.split("\n").length : 0;
6300
+ return lineCount ? `${lineCount} lines` : "Read completed";
6301
+ },
6302
+ formatResultDetail: (result) => {
6303
+ const text = extractResultText(result);
6304
+ return { lineCount: text ? text.split("\n").length : 0 };
6305
+ }
6306
+ },
6307
+ {
6308
+ name: "write",
6309
+ displayName: "Write",
6310
+ icon: "FileText",
6311
+ category: "file",
6312
+ formatCallSummary: (a) => getFileName(a.path || ""),
6313
+ formatCallDetail: (a) => ({ path: a.path }),
6314
+ formatResultSummary: (_, a) => `Written: ${getFileName(a?.path || "")}`,
6315
+ formatResultDetail: (_, a) => ({ path: a?.path })
6316
+ },
6317
+ {
6318
+ name: "edit",
6319
+ displayName: "Edit",
6320
+ icon: "FileText",
6321
+ category: "file",
6322
+ formatCallSummary: (a) => getFileName(a.path || ""),
6323
+ formatCallDetail: (a) => ({ path: a.path }),
6324
+ formatResultSummary: (_, a) => `Edited: ${getFileName(a?.path || "")}`,
6325
+ formatResultDetail: (_, a) => ({ path: a?.path })
6326
+ },
6327
+ // ── Code tools ────────────────────────
6328
+ {
6329
+ name: "bash",
6330
+ displayName: "Bash",
6331
+ icon: "Terminal",
6332
+ category: "code",
6333
+ formatCallSummary: (a) => {
6334
+ const cmd = a.command || "";
6335
+ return cmd.length > 60 ? `$ ${cmd.slice(0, 57)}...` : `$ ${cmd}`;
6336
+ },
6337
+ formatCallDetail: (a) => ({ command: truncStr(a.command, 200) }),
6338
+ formatResultSummary: () => "Command completed",
6339
+ formatResultDetail: (result) => {
6340
+ const text = extractResultText(result);
6341
+ const lines = text.split("\n").filter(Boolean);
6342
+ return { outputLines: lines.length, outputPreview: truncStr(lastNLines(text, 3), 200) };
6343
+ },
6344
+ formatProgress: (partial) => {
6345
+ const text = partial?.content?.[0]?.text;
6346
+ if (typeof text === "string" && text.length > 0) {
6347
+ return lastNLines(text, 5);
6348
+ }
6349
+ return void 0;
6350
+ }
6351
+ },
6352
+ // ── Search tools ────────────────────────
6353
+ {
6354
+ name: "grep",
6355
+ displayName: "Search",
6356
+ icon: "Search",
6357
+ category: "search",
6358
+ formatCallSummary: (a) => `"${truncStr(a.pattern, 30)}"${a.path ? ` in ${a.path}` : ""}`,
6359
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path, glob: a.glob }),
6360
+ formatResultSummary: (result) => {
6361
+ const text = extractResultText(result);
6362
+ const count = text.split("\n").filter(Boolean).length;
6363
+ return `${count} results`;
6364
+ },
6365
+ formatResultDetail: (result) => {
6366
+ const text = extractResultText(result);
6367
+ return { matchCount: text.split("\n").filter(Boolean).length };
6368
+ }
6369
+ },
6370
+ {
6371
+ name: "glob",
6372
+ displayName: "Find Files",
6373
+ icon: "Search",
6374
+ category: "search",
6375
+ formatCallSummary: (a) => a.pattern || "",
6376
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
6377
+ formatResultSummary: (result) => {
6378
+ const text = extractResultText(result);
6379
+ const count = text.split("\n").filter(Boolean).length;
6380
+ return `${count} files`;
6381
+ },
6382
+ formatResultDetail: (result) => {
6383
+ const text = extractResultText(result);
6384
+ return { fileCount: text.split("\n").filter(Boolean).length };
6385
+ }
6386
+ },
6387
+ {
6388
+ name: "find",
6389
+ displayName: "Find",
6390
+ icon: "Search",
6391
+ category: "search",
6392
+ formatCallSummary: (a) => truncStr(a.pattern || a.path, 40),
6393
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
6394
+ formatResultSummary: () => "Find completed",
6395
+ formatResultDetail: () => ({})
6396
+ },
6397
+ {
6398
+ name: "ls",
6399
+ displayName: "List",
6400
+ icon: "FileText",
6401
+ category: "file",
6402
+ formatCallSummary: (a) => a.path || ".",
6403
+ formatCallDetail: (a) => ({ path: a.path }),
6404
+ formatResultSummary: () => "Listed",
6405
+ formatResultDetail: () => ({})
6406
+ },
6407
+ // ── Web tools ────────────────────────
6408
+ {
6409
+ name: "fetch",
6410
+ displayName: "Fetch",
6411
+ icon: "Globe",
6412
+ category: "web",
6413
+ formatCallSummary: (a) => truncStr(a.url, 50),
6414
+ formatCallDetail: (a) => ({ url: a.url }),
6415
+ formatResultSummary: (result) => {
6416
+ const text = extractResultText(result);
6417
+ const kb = (text.length / 1024).toFixed(1);
6418
+ return `${kb}KB received`;
6419
+ },
6420
+ formatResultDetail: (result) => {
6421
+ const text = extractResultText(result);
6422
+ return { sizeKB: parseFloat((text.length / 1024).toFixed(1)) };
6423
+ }
6424
+ },
6425
+ {
6426
+ name: "web_fetch",
6427
+ displayName: "Web Fetch",
6428
+ icon: "Globe",
6429
+ category: "web",
6430
+ formatCallSummary: (a) => truncStr(a.url, 50),
6431
+ formatCallDetail: (a) => ({ url: a.url }),
6432
+ formatResultSummary: (result) => {
6433
+ const r = safeRecord(result);
6434
+ const data = safeRecord(r.data);
6435
+ const charCount = data.charCount;
6436
+ if (charCount) return `${(charCount / 1024).toFixed(1)}KB received`;
6437
+ return "Fetch completed";
6438
+ },
6439
+ formatResultDetail: (result) => {
6440
+ const r = safeRecord(result);
6441
+ const data = safeRecord(r.data);
6442
+ return { charCount: data.charCount, url: data.url };
6443
+ }
6444
+ },
6445
+ {
6446
+ name: "web_search",
6447
+ displayName: "Web Search",
6448
+ icon: "Globe",
6449
+ category: "web",
6450
+ formatCallSummary: (a) => truncStr(a.query, 50),
6451
+ formatCallDetail: (a) => ({ query: a.query }),
6452
+ formatResultSummary: () => "Search completed",
6453
+ formatResultDetail: () => ({})
6454
+ },
6455
+ // ── Research tools ────────────────────────
6456
+ {
6457
+ name: "literature-search",
6458
+ displayName: "Literature Search",
6459
+ icon: "BookOpen",
6460
+ category: "research",
6461
+ formatCallSummary: (a) => truncStr(a.query, 40),
6462
+ formatCallDetail: (a) => ({ query: a.query, maxResults: a.max_results }),
6463
+ formatResultSummary: (result) => {
6464
+ const r = safeRecord(result);
6465
+ const data = safeRecord(r.data);
6466
+ const totalFound = data.totalPapersFound ?? 0;
6467
+ const saved = data.papersAutoSaved ?? 0;
6468
+ const coverage = data.coverage;
6469
+ if (totalFound > 0) {
6470
+ let s = `Found ${totalFound} papers`;
6471
+ if (coverage?.score != null) s += ` (${Math.round(coverage.score * 100)}%)`;
6472
+ if (saved > 0) s += `, saved ${saved}`;
6473
+ return s;
6474
+ }
6475
+ const local = data.localPapersUsed ?? 0;
6476
+ const external = data.externalPapersUsed ?? 0;
6477
+ return `Found ${local + external} papers`;
6478
+ },
6479
+ formatResultDetail: (result) => {
6480
+ const r = safeRecord(result);
6481
+ const data = safeRecord(r.data);
6482
+ return {
6483
+ papersFound: data.totalPapersFound ?? 0,
6484
+ papersSaved: data.papersAutoSaved ?? 0,
6485
+ coverage: data.coverage?.score
6486
+ };
6487
+ }
6488
+ },
6489
+ {
6490
+ name: "lit-subtopic",
6491
+ displayName: "Sub-topic Search",
6492
+ icon: "BookOpen",
6493
+ category: "research",
6494
+ formatCallSummary: (a) => a._summary || "Searching sub-topic",
6495
+ formatCallDetail: (a) => ({ summary: a._summary }),
6496
+ formatResultSummary: (result) => safeRecord(result).data || "Search completed",
6497
+ formatResultDetail: () => ({})
6498
+ },
6499
+ {
6500
+ name: "lit-enrich",
6501
+ displayName: "Enrich Papers",
6502
+ icon: "BookOpen",
6503
+ category: "research",
6504
+ formatCallSummary: (a) => a._summary || "Enriching paper metadata",
6505
+ formatCallDetail: (a) => ({ summary: a._summary }),
6506
+ formatResultSummary: (result) => safeRecord(result).data || "Enriched metadata",
6507
+ formatResultDetail: () => ({})
6508
+ },
6509
+ {
6510
+ name: "lit-autosave",
6511
+ displayName: "Save Papers",
6512
+ icon: "BookOpen",
6513
+ category: "research",
6514
+ formatCallSummary: (a) => a._summary || "Saving papers",
6515
+ formatCallDetail: (a) => ({ summary: a._summary }),
6516
+ formatResultSummary: (result) => safeRecord(result).data || "Saved papers",
6517
+ formatResultDetail: () => ({})
6518
+ },
6519
+ {
6520
+ name: "data_analyze",
6521
+ displayName: "Data Analysis",
6522
+ icon: "Database",
6523
+ category: "research",
6524
+ formatCallSummary: (a) => getFileName(a.file_path || "") || "data",
6525
+ formatCallDetail: (a) => ({ file_path: a.file_path }),
6526
+ formatResultSummary: () => "Analysis completed",
6527
+ formatResultDetail: () => ({})
6528
+ },
6529
+ // ── Artifact tools ────────────────────────
6530
+ {
6531
+ name: "artifact-create",
6532
+ displayName: "Create Artifact",
6533
+ icon: "Sparkles",
6534
+ category: "memory",
6535
+ formatCallSummary: (a) => {
6536
+ const type = (a.type || "artifact").toLowerCase();
6537
+ const title = truncStr(a.title, 35);
6538
+ return `${type}: ${title}`;
6539
+ },
6540
+ formatCallDetail: (a) => ({ type: a.type, title: a.title }),
6541
+ formatResultSummary: (result) => {
6542
+ const data = safeRecord(safeRecord(result).data);
6543
+ const type = data.type || "artifact";
6544
+ const title = truncStr(data.title, 30);
6545
+ return title ? `Created ${type}: ${title}` : `Created ${type}`;
6546
+ },
6547
+ formatResultDetail: (result) => {
6548
+ const data = safeRecord(safeRecord(result).data);
6549
+ return { type: data.type, title: data.title };
6550
+ }
6551
+ },
6552
+ {
6553
+ name: "artifact-update",
6554
+ displayName: "Update Artifact",
6555
+ icon: "Sparkles",
6556
+ category: "memory",
6557
+ formatCallSummary: (a) => truncStr(a.id, 30),
6558
+ formatCallDetail: (a) => ({ id: a.id }),
6559
+ formatResultSummary: () => "Updated",
6560
+ formatResultDetail: () => ({})
6561
+ },
6562
+ {
6563
+ name: "artifact-search",
6564
+ displayName: "Search Artifacts",
6565
+ icon: "Search",
6566
+ category: "memory",
6567
+ formatCallSummary: (a) => truncStr(a.query, 40),
6568
+ formatCallDetail: (a) => ({ query: a.query, types: a.types }),
6569
+ formatResultSummary: () => "Search completed",
6570
+ formatResultDetail: () => ({})
6571
+ },
6572
+ // ── System tools ────────────────────────
6573
+ {
6574
+ name: "convert_document",
6575
+ displayName: "Convert Document",
6576
+ icon: "FileText",
6577
+ category: "system",
6578
+ formatCallSummary: (a) => getFileName(a.source || ""),
6579
+ formatCallDetail: (a) => ({ source: a.source }),
6580
+ formatResultSummary: (result, a) => {
6581
+ const data = safeRecord(safeRecord(result).data);
6582
+ const skill = data.converterSkill;
6583
+ const sourceName = getFileName(a?.source || "");
6584
+ return skill ? `Converted ${sourceName} via ${skill}` : `Converted ${sourceName}`;
6585
+ },
6586
+ formatResultDetail: (result) => {
6587
+ const data = safeRecord(safeRecord(result).data);
6588
+ return { converterSkill: data.converterSkill, outputFile: data.outputFile };
6589
+ }
6590
+ },
6591
+ {
6592
+ name: "load_skill",
6593
+ displayName: "Load Skill",
6594
+ icon: "Sparkles",
6595
+ category: "system",
6596
+ formatCallSummary: (a) => a.name || "skill",
6597
+ formatCallDetail: (a) => ({ name: a.name }),
6598
+ formatResultSummary: (_, a) => `Loaded: ${a?.name || "skill"}`,
6599
+ formatResultDetail: () => ({})
6600
+ }
6601
+ ];
6602
+ const registry = /* @__PURE__ */ new Map();
6603
+ for (const config of configs) {
6604
+ registry.set(config.name, config);
6605
+ }
6606
+ function getToolRenderConfig(toolName) {
6607
+ return registry.get(toolName);
6608
+ }
5590
6609
  const EMPTY = {
5591
6610
  version: 1,
5592
6611
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
5593
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
6612
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
5594
6613
  };
5595
6614
  function usagePath(baseDir) {
5596
6615
  return join(baseDir, "usage.json");
@@ -5600,6 +6619,9 @@ function loadUsageTotals(baseDir) {
5600
6619
  const raw = readFileSync(usagePath(baseDir), "utf-8");
5601
6620
  const parsed = JSON.parse(raw);
5602
6621
  if (!parsed?.totals) return { ...EMPTY };
6622
+ const t = parsed.totals;
6623
+ t.completionTokens ??= 0;
6624
+ t.cacheWriteTokens ??= 0;
5603
6625
  return parsed;
5604
6626
  } catch {
5605
6627
  return { ...EMPTY };
@@ -5628,15 +6650,17 @@ function writeAtomically(filePath, data) {
5628
6650
  }
5629
6651
  }
5630
6652
  }
5631
- function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cost) {
6653
+ function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, cost) {
5632
6654
  const existing = loadUsageTotals(baseDir);
5633
6655
  const next = {
5634
6656
  version: 1,
5635
6657
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5636
6658
  totals: {
5637
- tokens: existing.totals.tokens + promptTokens + completionTokens,
6659
+ tokens: existing.totals.tokens + promptTokens + completionTokens + cachedTokens,
5638
6660
  promptTokens: existing.totals.promptTokens + promptTokens,
6661
+ completionTokens: existing.totals.completionTokens + completionTokens,
5639
6662
  cachedTokens: existing.totals.cachedTokens + cachedTokens,
6663
+ cacheWriteTokens: existing.totals.cacheWriteTokens + cacheWriteTokens,
5640
6664
  cost: existing.totals.cost + cost,
5641
6665
  calls: existing.totals.calls + 1
5642
6666
  }
@@ -5648,98 +6672,45 @@ function resetUsageTotals(baseDir) {
5648
6672
  const cleared = {
5649
6673
  version: 1,
5650
6674
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5651
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
6675
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
5652
6676
  };
5653
6677
  writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
5654
6678
  return cleared;
5655
6679
  }
5656
6680
  function formatToolCall(tool, args) {
5657
6681
  const a = args && typeof args === "object" ? args : {};
5658
- switch (tool) {
5659
- case "literature-search":
5660
- return { label: `Search: ${(a.query || "").slice(0, 40)}${(a.query || "").length > 40 ? "..." : ""}`, icon: "search" };
5661
- case "lit-subtopic":
5662
- return { label: a._summary || "Searching sub-topic", icon: "search" };
5663
- case "lit-enrich":
5664
- return { label: a._summary || "Enriching paper metadata", icon: "search" };
5665
- case "lit-autosave":
5666
- return { label: a._summary || "Saving papers", icon: "file" };
5667
- case "data-analyze":
5668
- return { label: `Analyze: ${getFileName(a.filePath || "") || "data"}`, icon: "file" };
5669
- case "convert_to_markdown": {
5670
- const sourcePath = a.path || a.uri || "";
5671
- return { label: `Convert: ${getFileName(sourcePath)}`, icon: "file" };
5672
- }
5673
- case "artifact-create": {
5674
- const type = (a.type || "artifact").toLowerCase();
5675
- const title = (a.title || type).slice(0, 35);
5676
- return { label: `Create ${type}: ${title}`, icon: "file" };
5677
- }
5678
- case "read":
5679
- return { label: `Read: ${getFileName(a.path || "")}`, icon: "file" };
5680
- case "write":
5681
- return { label: `Write: ${getFileName(a.path || "")}`, icon: "file" };
5682
- case "edit":
5683
- return { label: `Edit: ${getFileName(a.path || "")}`, icon: "file" };
5684
- case "bash":
5685
- return { label: `Run command`, icon: "terminal" };
5686
- case "glob":
5687
- return { label: `Search files: ${a.pattern || ""}`, icon: "search" };
5688
- case "grep":
5689
- return { label: `Search content: ${(a.pattern || "").slice(0, 30)}`, icon: "search" };
5690
- case "fetch":
5691
- return { label: `Fetch: ${(a.url || "").slice(0, 40)}`, icon: "network" };
5692
- default:
5693
- return { label: `${tool}`, icon: "tool" };
6682
+ const config = getToolRenderConfig(tool);
6683
+ if (config) {
6684
+ return {
6685
+ label: `${config.displayName}: ${config.formatCallSummary(a)}`,
6686
+ icon: config.icon,
6687
+ detail: config.formatCallDetail(a)
6688
+ };
5694
6689
  }
6690
+ return { label: `${tool}`, icon: "tool" };
5695
6691
  }
5696
6692
  function formatToolResult(tool, result, args) {
5697
- const r = result && typeof result === "object" ? result : {};
5698
6693
  const a = args && typeof args === "object" ? args : {};
5699
- const data = r.data && typeof r.data === "object" ? r.data : {};
5700
- switch (tool) {
5701
- case "literature-search": {
5702
- const totalFound = data.totalPapersFound ?? 0;
5703
- const saved = data.papersAutoSaved ?? 0;
5704
- const coverage = data.coverage;
5705
- if (totalFound > 0) {
5706
- let summary2 = `Found ${totalFound} papers`;
5707
- if (coverage?.score != null) summary2 += ` (coverage: ${Math.round(coverage.score * 100)}%)`;
5708
- if (saved > 0) summary2 += `, saved ${saved}`;
5709
- return { label: summary2, icon: "search" };
5710
- }
5711
- const local = data.localPapersUsed ?? 0;
5712
- const external = data.externalPapersUsed ?? 0;
5713
- const savedV1 = data.savedPapers ?? 0;
5714
- let summary = `Found ${local + external} papers`;
5715
- if (local > 0) summary += ` (${local} local)`;
5716
- if (savedV1 > 0) summary += `, saved ${savedV1}`;
5717
- return { label: summary, icon: "search" };
5718
- }
5719
- case "lit-subtopic":
5720
- return { label: r.data || "Search completed", icon: "search" };
5721
- case "lit-enrich":
5722
- return { label: r.data || "Enriched metadata", icon: "search" };
5723
- case "lit-autosave":
5724
- return { label: r.data || "Saved papers", icon: "file" };
5725
- case "convert_to_markdown": {
5726
- const sourcePath = a.path || a.uri || "";
5727
- const skill = typeof data.converterSkill === "string" ? data.converterSkill : "";
5728
- const script = typeof data.converterScript === "string" ? data.converterScript : "";
5729
- if (skill && script) return { label: `Converted ${getFileName(sourcePath)} via ${skill}/${script}`, icon: "file" };
5730
- if (skill) return { label: `Converted ${getFileName(sourcePath)} via ${skill}`, icon: "file" };
5731
- return { label: `Converted ${getFileName(sourcePath)}`, icon: "file" };
5732
- }
5733
- case "artifact-create": {
5734
- const type = data.type || "artifact";
5735
- const title = data.title || "";
5736
- return { label: title ? `Created ${type}: ${title.slice(0, 30)}` : `Created ${type}`, icon: "file" };
5737
- }
5738
- default: {
5739
- const success2 = r.success !== false;
5740
- return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool" };
5741
- }
6694
+ const r = result && typeof result === "object" ? result : {};
6695
+ const success2 = r.success !== false;
6696
+ const config = getToolRenderConfig(tool);
6697
+ if (config && success2) {
6698
+ return {
6699
+ label: config.formatResultSummary(result, a),
6700
+ icon: config.icon,
6701
+ detail: config.formatResultDetail(result, a)
6702
+ };
6703
+ }
6704
+ if (config && !success2) {
6705
+ const errorMsg = r.error || "";
6706
+ const brief = errorMsg.length > 60 ? errorMsg.slice(0, 57) + "..." : errorMsg;
6707
+ return {
6708
+ label: brief ? `${config.displayName} failed: ${brief}` : `${config.displayName} failed`,
6709
+ icon: config.icon,
6710
+ detail: config.formatResultDetail(result, a)
6711
+ };
5742
6712
  }
6713
+ return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool", detail: { success: success2 } };
5743
6714
  }
5744
6715
  const windowStates = /* @__PURE__ */ new Map();
5745
6716
  let ipcHandlersRegistered = false;
@@ -5835,7 +6806,8 @@ function initializeProject(path2) {
5835
6806
  PATHS.memoryRoot,
5836
6807
  PATHS.explainDir,
5837
6808
  PATHS.sessionSummaries,
5838
- PATHS.skills
6809
+ PATHS.skills,
6810
+ PATHS.memory
5839
6811
  ];
5840
6812
  for (const dir of dirs) {
5841
6813
  const fullPath = join(path2, dir);
@@ -5856,6 +6828,7 @@ function initializeProject(path2) {
5856
6828
  writeFileSync(projectFile, JSON.stringify(defaultConfig, null, 2));
5857
6829
  }
5858
6830
  ensureAgentMd(path2);
6831
+ migrateAgentMemoryToFile(path2);
5859
6832
  const migration = migrateLegacyArtifacts(path2);
5860
6833
  if (migration.updatedFiles > 0 && process.env.RESEARCH_COPILOT_DEBUG) {
5861
6834
  console.log(`[ResearchPilot] migrated legacy artifacts: files=${migration.updatedFiles}, literature->paper=${migration.convertedLiteratureType}, data.name removed=${migration.removedDataNameField}`);
@@ -5889,13 +6862,15 @@ async function ensureCoordinator(state, win, model, options) {
5889
6862
  state.realtimeBuffer.appendChunk(chunk);
5890
6863
  safeSend(win, "agent:stream-chunk", chunk);
5891
6864
  },
5892
- onToolCall: (tool, args) => {
5893
- const summary = formatToolCall(tool, args).label;
5894
- const event = { type: "tool-call", tool, summary };
6865
+ onToolCall: (tool, args, toolCallId) => {
6866
+ const id = toolCallId || randomUUID();
6867
+ const { label, detail } = formatToolCall(tool, args);
6868
+ const event = { type: "tool-call", tool, toolCallId: id, summary: label, detail };
5895
6869
  state.realtimeBuffer.pushActivity(event);
6870
+ state.realtimeBuffer.pushToolEvent({ type: "tool-call", tool, toolCallId: id, summary: label, detail });
5896
6871
  safeSend(win, "agent:activity", event);
5897
6872
  },
5898
- onToolResult: (tool, result, args) => {
6873
+ onToolResult: (tool, result, args, toolCallId) => {
5899
6874
  if (tool.startsWith("todo-") && result && typeof result === "object" && "success" in result) {
5900
6875
  const r2 = result;
5901
6876
  if (r2.success && r2.item) {
@@ -5910,16 +6885,16 @@ async function ensureCoordinator(state, win, model, options) {
5910
6885
  safeSend(win, "agent:file-created", absPath);
5911
6886
  }
5912
6887
  }
5913
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
6888
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
5914
6889
  const r2 = result;
5915
6890
  if (r2.success && r2.data?.outputFile) {
5916
6891
  safeSend(win, "agent:file-created", r2.data.outputFile);
5917
6892
  }
5918
6893
  }
5919
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
6894
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
5920
6895
  const r2 = result;
5921
- if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "path" in args) {
5922
- const sourcePath = args.path;
6896
+ if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "source" in args) {
6897
+ const sourcePath = args.source;
5923
6898
  const absSourcePath = isAbsolute(sourcePath) ? sourcePath : resolve(runProjectPath, sourcePath);
5924
6899
  const absOutputPath = resolve(runProjectPath, r2.data.outputFile);
5925
6900
  if (existsSync(absOutputPath)) {
@@ -5933,28 +6908,46 @@ async function ensureCoordinator(state, win, model, options) {
5933
6908
  }
5934
6909
  }
5935
6910
  }
5936
- if (tool === "artifact-create" && result && typeof result === "object" && "success" in result) {
6911
+ if ((tool === "artifact-create" || tool === "artifact-update") && result && typeof result === "object" && "success" in result) {
5937
6912
  const r2 = result;
5938
6913
  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);
6914
+ invalidateEntityCache();
6915
+ if (tool === "artifact-create") {
6916
+ safeSend(win, "agent:entity-created", {
6917
+ type: r2.data?.type || "artifact",
6918
+ id: r2.data?.id,
6919
+ title: r2.data?.title
6920
+ });
6921
+ if (r2.data?.filePath) {
6922
+ const absPath = isAbsolute(r2.data.filePath) ? r2.data.filePath : resolve(runProjectPath, r2.data.filePath);
6923
+ safeSend(win, "agent:file-created", absPath);
6924
+ }
5947
6925
  }
5948
6926
  }
5949
6927
  }
5950
6928
  const r = result;
5951
6929
  const success2 = r?.success !== false;
5952
6930
  const error = !success2 ? r?.error || "Unknown error" : void 0;
5953
- const summary = formatToolResult(tool, result, args).label;
5954
- const actEvent = { type: "tool-result", tool, summary, success: success2, error };
6931
+ const { label: resultLabel, detail: resultDetail } = formatToolResult(tool, result, args);
6932
+ const startTime = toolCallId ? state.realtimeBuffer.popToolCallStartTime(toolCallId) : void 0;
6933
+ const durationMs = startTime ? Date.now() - startTime : void 0;
6934
+ const actEvent = { type: "tool-result", tool, toolCallId, summary: resultLabel, success: success2, error, resultDetail, durationMs };
5955
6935
  state.realtimeBuffer.pushActivity(actEvent);
6936
+ if (toolCallId) {
6937
+ state.realtimeBuffer.updateToolEvent(toolCallId, {
6938
+ type: "tool-result",
6939
+ summary: resultLabel,
6940
+ success: success2,
6941
+ resultDetail,
6942
+ durationMs
6943
+ });
6944
+ }
5956
6945
  safeSend(win, "agent:activity", actEvent);
5957
6946
  },
6947
+ // Tool execution progress (real-time updates during tool execution)
6948
+ onToolProgress: (tool, toolCallId, phase, data) => {
6949
+ safeSend(win, "agent:tool-progress", { tool, toolCallId, phase, data, timestamp: Date.now() });
6950
+ },
5958
6951
  // Skill activation tracking
5959
6952
  onSkillLoaded: (skillName) => {
5960
6953
  safeSend(win, "agent:skill-loaded", skillName);
@@ -5966,12 +6959,14 @@ async function ensureCoordinator(state, win, model, options) {
5966
6959
  const promptTokens = usage.input ?? 0;
5967
6960
  const completionTokens = usage.output ?? 0;
5968
6961
  const cachedTokens = usage.cacheRead ?? 0;
6962
+ const cacheWriteTokens = usage.cacheWrite ?? 0;
5969
6963
  const baseDir = join(runProjectPath, PATHS.root);
5970
- accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, rawCost);
6964
+ accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, rawCost);
5971
6965
  const usageEvent = {
5972
6966
  promptTokens,
5973
6967
  completionTokens,
5974
6968
  cachedTokens,
6969
+ cacheWriteTokens,
5975
6970
  cost: rawCost,
5976
6971
  rawCost,
5977
6972
  billableCost: rawCost,
@@ -6148,10 +7143,10 @@ function registerIpcHandlers() {
6148
7143
  }
6149
7144
  });
6150
7145
  });
6151
- handleWindow("mention:candidates", ({ state }, query, type) => {
7146
+ handleWindow("mention:candidates", async ({ state }, query, type) => {
6152
7147
  if (!state.projectPath) return [];
6153
7148
  try {
6154
- return getCandidates(state.projectPath, type, query);
7149
+ return await getCandidates(state.projectPath, type, query);
6155
7150
  } catch {
6156
7151
  return [];
6157
7152
  }
@@ -6597,7 +7592,11 @@ function destroyAllTerminals() {
6597
7592
  terminals.delete(id);
6598
7593
  }
6599
7594
  }
7595
+ setMaxListeners(20);
6600
7596
  loadApiKeysFromConfig();
7597
+ if (!process.env.PI_CACHE_RETENTION) {
7598
+ process.env.PI_CACHE_RETENTION = "long";
7599
+ }
6601
7600
  if (process.platform === "darwin" && !is.dev) {
6602
7601
  try {
6603
7602
  const shellPath = process.env.SHELL || "/bin/zsh";