research-copilot 0.2.0 → 0.2.2

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 +3222 -412
  2. package/app/out/preload/index.js +26 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-Czh2N6UQ.js → MilkdownMarkdownEditor-jaF-aGPn.js} +50 -50
  4. package/app/out/renderer/assets/{arc-BWoErJNa.js → arc-C1kBmvvR.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-Bod-vAlS.js → blockDiagram-c4efeb88-Do93X2rs.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-CTVUA_li.js → c4Diagram-c83219d4-DgxxcZWC.js} +3 -3
  7. package/app/out/renderer/assets/{channel-CxGr5Q5E.js → channel-Co_M0Svj.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-DABwUrsU.js → classDiagram-beda092f-CQlHgE6H.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-CFt8hqf5.js → classDiagram-v2-2358418a-CkGG3aI2.js} +10 -10
  10. package/app/out/renderer/assets/{clone-BL91dKYn.js → clone-C18Y6dgC.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-DGkv4rEO.js → createText-1719965b-DGRc6nys.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-Gf41lQOd.js → edges-96097737-BXvJ4fAK.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-Dj75BiRy.js → erDiagram-0228fc6a-CXjPp0pt.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-C_xVBMxS.js → flowDb-c6c81e3f-CNhpbtw_.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-B-lLn2XC.js → flowDiagram-50d868cf-KZ_BUCPA.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-BFnLU3PE.js → flowDiagram-v2-4f6560a1-IMv50KZP.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-DmjfyXbt.js → flowchart-elk-definition-6af322e1-BFwFiPvq.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-BTPRekAy.js → ganttDiagram-a2739b55-D0-ehN-T.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-1riYxgGS.js → gitGraphDiagram-82fe8481-DUyIR0Dv.js} +2 -2
  20. package/app/out/renderer/assets/{graph-CvDtMlX-.js → graph-DnTq2_3F.js} +1 -1
  21. package/app/out/renderer/assets/{index-5325376f-BGaoNMNN.js → index-5325376f-CBwuFbRF.js} +6 -6
  22. package/app/out/renderer/assets/{index-8tvmsRje.js → index-7hDGClrI.js} +3 -3
  23. package/app/out/renderer/assets/{index-CUPy7R5v.js → index-BB-a1ajC.js} +2122 -471
  24. package/app/out/renderer/assets/{index-Bii7x9Rr.js → index-BHcU72Rm.js} +3 -3
  25. package/app/out/renderer/assets/{index-qS7qbXvX.js → index-BQ7qz1CD.js} +3 -3
  26. package/app/out/renderer/assets/{index-DrvR7Peq.js → index-BVYoMX5H.js} +3 -3
  27. package/app/out/renderer/assets/{index-CXN1f9OT.js → index-BpKrXGYD.js} +3 -3
  28. package/app/out/renderer/assets/{index-0kPJXDfu.js → index-C1oXjI4L.js} +3 -3
  29. package/app/out/renderer/assets/{index-BK5rYWMs.js → index-CKXwBmK7.js} +5 -5
  30. package/app/out/renderer/assets/{index-B9lieynj.js → index-COZSDrEw.js} +6 -6
  31. package/app/out/renderer/assets/{index-Ctwkk-AW.css → index-CT1HtzVp.css} +165 -14
  32. package/app/out/renderer/assets/{index-zr8uxb8p.js → index-CjffvluT.js} +6 -6
  33. package/app/out/renderer/assets/{index-CnL9yPzK.js → index-D6jljsup.js} +3 -3
  34. package/app/out/renderer/assets/{index-BxOmAXUZ.js → index-D6r8msaQ.js} +3 -3
  35. package/app/out/renderer/assets/{index-D2fFfHUR.js → index-DWU4ia28.js} +6 -6
  36. package/app/out/renderer/assets/{index-BVNrdWzl.js → index-DZbrRR7w.js} +6 -6
  37. package/app/out/renderer/assets/{index-BCOrnr8q.js → index-Diy30-34.js} +4 -4
  38. package/app/out/renderer/assets/{index-NHbUPOmb.js → index-DuhageEr.js} +3 -3
  39. package/app/out/renderer/assets/{index-BnRwUKpv.js → index-ESFHcvWy.js} +3 -3
  40. package/app/out/renderer/assets/{index-B4djqBxS.js → index-JT8OCsRP.js} +1 -1
  41. package/app/out/renderer/assets/{index-cAZJ88Np.js → index-bMe3RSkw.js} +6 -6
  42. package/app/out/renderer/assets/{index-3LdRym1K.js → index-gH-w4EHk.js} +3 -3
  43. package/app/out/renderer/assets/{index-O3gvL3-Z.js → index-h_fNksib.js} +3 -3
  44. package/app/out/renderer/assets/{index-y5XZ-0EB.js → index-u0FZRZON.js} +4 -4
  45. package/app/out/renderer/assets/{index-BgSz3yUy.js → index-yanwpi6t.js} +6 -6
  46. package/app/out/renderer/assets/{infoDiagram-8eee0895-Cq8aXV8u.js → infoDiagram-8eee0895-Qra4japr.js} +2 -2
  47. package/app/out/renderer/assets/{journeyDiagram-c64418c1-D4ewDrYD.js → journeyDiagram-c64418c1-BTN9SgOL.js} +4 -4
  48. package/app/out/renderer/assets/{layout-CZmLZO9t.js → layout-DGrHHJdN.js} +2 -2
  49. package/app/out/renderer/assets/{line-D7kWOiRx.js → line-DXtxdS2B.js} +1 -1
  50. package/app/out/renderer/assets/{linear-B055Dz0c.js → linear-CexrSQK6.js} +1 -1
  51. package/app/out/renderer/assets/{mindmap-definition-8da855dc-D6EW4QCj.js → mindmap-definition-8da855dc-pvG2hzEB.js} +3 -3
  52. package/app/out/renderer/assets/{pieDiagram-a8764435-BX_Dz4T9.js → pieDiagram-a8764435-D_neFVMq.js} +3 -3
  53. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-BsI6xGsm.js → quadrantDiagram-1e28029f-C47W3UMp.js} +3 -3
  54. package/app/out/renderer/assets/{requirementDiagram-08caed73-c2d8T0BS.js → requirementDiagram-08caed73-DW4Bo_fu.js} +5 -5
  55. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-CkDhRKRC.js → sankeyDiagram-a04cb91d-D_3PD7JI.js} +2 -2
  56. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-DS0RKYnD.js → sequenceDiagram-c5b8d532-BW6nGtuQ.js} +3 -3
  57. package/app/out/renderer/assets/{stateDiagram-1ecb1508-BjTK27QX.js → stateDiagram-1ecb1508-CDgBJ3-T.js} +6 -6
  58. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-D1wWbeR3.js → stateDiagram-v2-c2b004d7-CBw5TtXo.js} +10 -10
  59. package/app/out/renderer/assets/{styles-b4e223ce-DXUfbXTM.js → styles-b4e223ce-DeeiEsuW.js} +1 -1
  60. package/app/out/renderer/assets/{styles-ca3715f6-CE_JRTmB.js → styles-ca3715f6-CMpiebrG.js} +1 -1
  61. package/app/out/renderer/assets/{styles-d45a18b0-CdtAXXSE.js → styles-d45a18b0-CZe9hU7H.js} +4 -4
  62. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-dCxPWgBl.js → svgDrawCommon-b86b1483-CmJZfZzJ.js} +1 -1
  63. package/app/out/renderer/assets/{timeline-definition-faaaa080-B7ZP3Dqw.js → timeline-definition-faaaa080-Beo2kiiz.js} +3 -3
  64. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-CXagmo1Q.js → xychartDiagram-f5964ef8-DYmo7moz.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,22 @@
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
- import { execFile, execSync } from "node:child_process";
4
- import { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
4
+ import { execFile, spawn, execSync } from "node:child_process";
5
+ import path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
5
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
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 crypto$1, { 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";
18
20
  import { execFile as execFile$1 } from "child_process";
19
21
  import __cjs_mod__ from "node:module";
20
22
  const __filename = import.meta.filename;
@@ -233,10 +235,6 @@ function registerConfigHandlers(handleRaw) {
233
235
  return { success: true };
234
236
  });
235
237
  }
236
- function getFileName(path2) {
237
- if (!path2) return "";
238
- return path2.split("/").pop() || path2;
239
- }
240
238
  function inferMimeType(path2) {
241
239
  const ext = extname(path2).toLowerCase();
242
240
  if (ext === ".md" || ext === ".txt") return "text/plain";
@@ -264,30 +262,83 @@ function loadOrCreateSessionId(rootPathKey, path2) {
264
262
  writeFileSync(sessionFile, JSON.stringify({ sessionId: newId }));
265
263
  return newId;
266
264
  }
267
- function resolveCoordinatorAuth(modelId) {
265
+ const CODEX_CRED_FILE = join(CONFIG_DIR, "openai-codex-credentials.json");
266
+ function loadCodexCredentials() {
267
+ try {
268
+ if (existsSync(CODEX_CRED_FILE)) {
269
+ const data = JSON.parse(readFileSync(CODEX_CRED_FILE, "utf-8"));
270
+ if (data.access && data.refresh) return data;
271
+ }
272
+ } catch {
273
+ }
274
+ try {
275
+ const codexHome = join(homedir(), ".codex");
276
+ const codexAuth = join(codexHome, "auth.json");
277
+ if (existsSync(codexAuth)) {
278
+ const data = JSON.parse(readFileSync(codexAuth, "utf-8"));
279
+ if (data.tokens?.access_token && data.tokens?.refresh_token) {
280
+ return {
281
+ access: data.tokens.access_token,
282
+ refresh: data.tokens.refresh_token,
283
+ expires: data.tokens.expires_at ? data.tokens.expires_at * 1e3 : Date.now() + 36e5,
284
+ accountId: data.tokens.account_id
285
+ };
286
+ }
287
+ }
288
+ } catch {
289
+ }
290
+ return null;
291
+ }
292
+ function saveCodexCredentials(creds) {
293
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
294
+ writeFileSync(CODEX_CRED_FILE, JSON.stringify(creds, null, 2), { mode: 384 });
295
+ }
296
+ function clearCodexCredentials() {
297
+ try {
298
+ if (existsSync(CODEX_CRED_FILE)) {
299
+ const { unlinkSync: unlinkSync2 } = require2("fs");
300
+ unlinkSync2(CODEX_CRED_FILE);
301
+ }
302
+ } catch {
303
+ }
304
+ }
305
+ function resolveCoordinatorAuth(compositeKey) {
268
306
  const openaiApiKey = (process.env.OPENAI_API_KEY || "").trim();
269
307
  const anthropicApiKey = (process.env.ANTHROPIC_API_KEY || "").trim();
270
- const isAnthropic = modelId.startsWith("claude-");
271
- if (!isAnthropic) {
272
- if (!openaiApiKey) {
273
- throw new Error("OPENAI_API_KEY is required for the selected OpenAI model.");
274
- }
275
- return {
276
- apiKey: openaiApiKey,
277
- authMode: "api-key",
278
- isAnthropicModel: false,
279
- billingSource: "api-key"
280
- };
308
+ const i = compositeKey.indexOf(":");
309
+ let provider;
310
+ if (i > 0) {
311
+ provider = compositeKey.slice(0, i);
312
+ } else {
313
+ provider = compositeKey.startsWith("claude-") ? "anthropic" : "openai";
281
314
  }
282
- if (!anthropicApiKey) {
283
- throw new Error("ANTHROPIC_API_KEY is required for the selected Anthropic model.");
315
+ switch (provider) {
316
+ case "openai-codex": {
317
+ const creds = loadCodexCredentials();
318
+ if (!creds) {
319
+ throw new Error("ChatGPT subscription login required. Please sign in via the model selector.");
320
+ }
321
+ return {
322
+ apiKey: creds.access,
323
+ authMode: "subscription",
324
+ isAnthropicModel: false,
325
+ billingSource: "subscription",
326
+ piProvider: "openai-codex"
327
+ };
328
+ }
329
+ case "anthropic": {
330
+ if (!anthropicApiKey) {
331
+ throw new Error("ANTHROPIC_API_KEY is required for the selected Anthropic model.");
332
+ }
333
+ return { apiKey: anthropicApiKey, authMode: "api-key", isAnthropicModel: true, billingSource: "api-key" };
334
+ }
335
+ default: {
336
+ if (!openaiApiKey) {
337
+ throw new Error("OPENAI_API_KEY is required for the selected OpenAI model.");
338
+ }
339
+ return { apiKey: openaiApiKey, authMode: "api-key", isAnthropicModel: false, billingSource: "api-key" };
340
+ }
284
341
  }
285
- return {
286
- apiKey: anthropicApiKey,
287
- authMode: "api-key",
288
- isAnthropicModel: true,
289
- billingSource: "api-key"
290
- };
291
342
  }
292
343
  function registerFileHandlers(handle, getCtx) {
293
344
  handle("file:list-root", () => {
@@ -522,6 +573,51 @@ function registerAuthHandlers(handleRaw) {
522
573
  hasApiKey: !!(process.env.OPENAI_API_KEY || "").trim()
523
574
  };
524
575
  });
576
+ handleRaw("auth:get-openai-codex-status", () => {
577
+ const creds = loadCodexCredentials();
578
+ return {
579
+ isLoggedIn: !!creds,
580
+ isExpired: creds ? creds.expires < Date.now() : false
581
+ };
582
+ });
583
+ handleRaw("auth:openai-codex-login", async () => {
584
+ const { loginOpenAICodex } = await import("@mariozechner/pi-ai/oauth");
585
+ const { shell: shell2 } = await import("electron");
586
+ try {
587
+ const creds = await loginOpenAICodex({
588
+ onAuth: (info) => {
589
+ shell2.openExternal(info.url);
590
+ },
591
+ onPrompt: async (prompt) => {
592
+ console.warn("[OAuth] Unexpected prompt:", prompt.message);
593
+ return "";
594
+ },
595
+ onProgress: (msg) => {
596
+ console.log("[OAuth]", msg);
597
+ }
598
+ });
599
+ saveCodexCredentials(creds);
600
+ return { success: true };
601
+ } catch (err) {
602
+ return { success: false, error: err.message || "OAuth login failed" };
603
+ }
604
+ });
605
+ handleRaw("auth:openai-codex-logout", () => {
606
+ clearCodexCredentials();
607
+ return { success: true };
608
+ });
609
+ handleRaw("auth:openai-codex-refresh", async () => {
610
+ const creds = loadCodexCredentials();
611
+ if (!creds) return { success: false, error: "Not logged in" };
612
+ try {
613
+ const { refreshOpenAICodexToken } = await import("@mariozechner/pi-ai/oauth");
614
+ const newCreds = await refreshOpenAICodexToken(creds);
615
+ saveCodexCredentials(newCreds);
616
+ return { success: true };
617
+ } catch (err) {
618
+ return { success: false, error: err.message || "Token refresh failed" };
619
+ }
620
+ });
525
621
  }
526
622
  function registerFolderOpenHandler(handle, getCtx) {
527
623
  handle("folder:open-with", async (appName) => {
@@ -576,13 +672,39 @@ function truncateHeadTail(text, maxChars, headRatio = 0.7) {
576
672
  ...[truncated ${truncatedChars} chars]
577
673
  ${text.slice(-tailChars)}`;
578
674
  }
675
+ function truncateStructuredData(data, maxChars) {
676
+ const json = JSON.stringify(data, null, 2);
677
+ if (json.length <= maxChars) return data;
678
+ const obj = { ...data };
679
+ let largestKey = "";
680
+ let largestSize = 0;
681
+ for (const [k, v] of Object.entries(obj)) {
682
+ if (typeof v === "string" && v.length > largestSize) {
683
+ largestKey = k;
684
+ largestSize = v.length;
685
+ }
686
+ }
687
+ if (largestKey) {
688
+ const overhead = json.length - largestSize;
689
+ const fieldBudget = Math.max(1e3, maxChars - overhead);
690
+ obj[largestKey] = truncateHeadTail(obj[largestKey], fieldBudget);
691
+ }
692
+ return obj;
693
+ }
579
694
  function toAgentResult(toolName, result) {
580
695
  let text;
696
+ const MAX_RESULT_CHARS = 1e5;
581
697
  if (result.success) {
582
698
  if (result.data === void 0 || result.data === null) {
583
699
  text = `[${toolName}] OK`;
584
700
  } else if (typeof result.data === "string") {
585
- text = result.data;
701
+ text = truncateHeadTail(result.data, MAX_RESULT_CHARS);
702
+ } else if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
703
+ const bounded = truncateStructuredData(
704
+ result.data,
705
+ MAX_RESULT_CHARS
706
+ );
707
+ text = JSON.stringify(bounded, null, 2);
586
708
  } else {
587
709
  text = JSON.stringify(result.data, null, 2);
588
710
  }
@@ -607,10 +729,11 @@ ${result.suggestions.map((s) => `- ${s}`).join("\n")}`);
607
729
  }
608
730
  text = parts.join("\n");
609
731
  }
610
- const MAX_RESULT_CHARS = 1e5;
611
- const bounded = truncateHeadTail(text, MAX_RESULT_CHARS);
732
+ if (text.length > MAX_RESULT_CHARS) {
733
+ text = truncateHeadTail(text, MAX_RESULT_CHARS);
734
+ }
612
735
  return {
613
- content: [{ type: "text", text: bounded }],
736
+ content: [{ type: "text", text }],
614
737
  details: { success: result.success, tool_name: toolName }
615
738
  };
616
739
  }
@@ -639,7 +762,9 @@ const PATHS = {
639
762
  memory: ".research-pilot/memory",
640
763
  // Skills
641
764
  skills: ".research-pilot/skills",
642
- skillsConfig: ".research-pilot/skills-config.json"
765
+ skillsConfig: ".research-pilot/skills-config.json",
766
+ // Local compute runs
767
+ computeRuns: ".research-pilot/compute-runs"
643
768
  };
644
769
  function nowIso() {
645
770
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1255,7 +1380,12 @@ function createResearchMemoryTools(params) {
1255
1380
  ];
1256
1381
  }
1257
1382
  function slugify(text) {
1258
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1383
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1384
+ if (slug.length < 3) {
1385
+ const hash = createHash("sha256").update(text.toLowerCase().trim()).digest("hex").slice(0, 12);
1386
+ return slug ? `${slug}-${hash}` : hash;
1387
+ }
1388
+ return slug;
1259
1389
  }
1260
1390
  function memoryFilename(type, name) {
1261
1391
  return `${type}_${slugify(name)}.md`;
@@ -1267,22 +1397,40 @@ function ensureMemoryDir(projectPath) {
1267
1397
  const dir = memoryDir(projectPath);
1268
1398
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1269
1399
  }
1400
+ function yamlSafe(value) {
1401
+ if (!value) return '""';
1402
+ if (/[:\#{}\[\]"'`\n\r|>]/.test(value) || value !== value.trim()) {
1403
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1404
+ return `"${escaped}"`;
1405
+ }
1406
+ return value;
1407
+ }
1270
1408
  function formatFrontmatter(fm) {
1271
1409
  return [
1272
1410
  "---",
1273
- `name: ${fm.name}`,
1274
- `description: ${fm.description}`,
1411
+ `name: ${yamlSafe(fm.name)}`,
1412
+ `description: ${yamlSafe(fm.description)}`,
1275
1413
  `type: ${fm.type}`,
1276
1414
  "---"
1277
1415
  ].join("\n");
1278
1416
  }
1417
+ function yamlUnescape(raw) {
1418
+ const trimmed = raw.trim();
1419
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1420
+ return trimmed.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1421
+ }
1422
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
1423
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1424
+ }
1425
+ return trimmed;
1426
+ }
1279
1427
  function parseFrontmatter(text) {
1280
1428
  const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1281
1429
  if (!match) return null;
1282
1430
  const fm = {};
1283
1431
  for (const line of match[1].split("\n")) {
1284
1432
  const kv = line.match(/^(\w+):\s*(.+)$/);
1285
- if (kv) fm[kv[1]] = kv[2].trim();
1433
+ if (kv) fm[kv[1]] = yamlUnescape(kv[2]);
1286
1434
  }
1287
1435
  if (!fm.name || !fm.type) return null;
1288
1436
  const validTypes = ["user", "feedback", "project", "reference"];
@@ -1339,9 +1487,24 @@ function listMemoryFiles(projectPath) {
1339
1487
  return [];
1340
1488
  }
1341
1489
  }
1342
- function findMemoryByName(projectPath, name) {
1490
+ function findMemoryByName(projectPath, name, type) {
1491
+ const lower = name.toLowerCase();
1492
+ const entries = listMemoryFiles(projectPath);
1493
+ return entries.find(
1494
+ (e) => e.frontmatter.name.toLowerCase() === lower && (!type || e.frontmatter.type === type)
1495
+ ) ?? null;
1496
+ }
1497
+ function findAllMemoriesByName(projectPath, name) {
1343
1498
  const lower = name.toLowerCase();
1344
- return listMemoryFiles(projectPath).find((e) => e.frontmatter.name.toLowerCase() === lower) ?? null;
1499
+ return listMemoryFiles(projectPath).filter((e) => e.frontmatter.name.toLowerCase() === lower);
1500
+ }
1501
+ let _indexWriteLock = Promise.resolve();
1502
+ function withIndexLock(fn) {
1503
+ const next = _indexWriteLock.then(fn, fn);
1504
+ _indexWriteLock = next.then(() => {
1505
+ }, () => {
1506
+ });
1507
+ return next;
1345
1508
  }
1346
1509
  function buildMemoryIndex(entries) {
1347
1510
  if (entries.length === 0) return "";
@@ -1381,6 +1544,9 @@ function migrateAgentMemoryToFile(projectPath) {
1381
1544
  if (markerIdx < 0) return false;
1382
1545
  const agentMemory = content.slice(markerIdx + marker.length).trim();
1383
1546
  if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
1547
+ const legacyFilename = memoryFilename("project", "legacy-notes");
1548
+ const legacyPath = join(memoryDir(projectPath), legacyFilename);
1549
+ if (existsSync(legacyPath)) return false;
1384
1550
  ensureMemoryDir(projectPath);
1385
1551
  const entry = {
1386
1552
  frontmatter: {
@@ -1435,73 +1601,98 @@ function createSaveMemoryTool(projectPath) {
1435
1601
  if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
1436
1602
  suggestions: ["Provide the memory content to save."]
1437
1603
  });
1438
- ensureMemoryDir(projectPath);
1439
- const filename = memoryFilename(type, name);
1440
- const description = content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
1441
- const entry = {
1442
- frontmatter: { name, description, type },
1443
- content,
1444
- filename
1445
- };
1446
- writeMemoryFile(projectPath, entry);
1447
- const allEntries = listMemoryFiles(projectPath);
1448
- const indexResult = updateAgentMdIndex(projectPath, allEntries);
1449
- if (!indexResult.success) {
1450
- return toolError(
1451
- "OUTPUT_TOO_LARGE",
1452
- "agent.md index exceeded size limit. Remove some memories first.",
1453
- {
1454
- suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1455
- }
1456
- );
1457
- }
1458
- return {
1459
- success: true,
1460
- data: {
1461
- message: `Memory saved: ${name} (${type})`,
1462
- filename,
1463
- totalMemories: allEntries.length,
1464
- agentMdChars: indexResult.charCount
1604
+ const lines = content.split("\n");
1605
+ const firstNonEmpty = lines.find((l) => l.trim().length > 0) || "";
1606
+ const description = firstNonEmpty.replace(/^#+\s*/, "").trim().slice(0, 120) || name;
1607
+ return withIndexLock(() => {
1608
+ ensureMemoryDir(projectPath);
1609
+ const filename = memoryFilename(type, name);
1610
+ const entry = {
1611
+ frontmatter: { name, description, type },
1612
+ content,
1613
+ filename
1614
+ };
1615
+ writeMemoryFile(projectPath, entry);
1616
+ const allEntries = listMemoryFiles(projectPath);
1617
+ const indexResult = updateAgentMdIndex(projectPath, allEntries);
1618
+ if (!indexResult.success) {
1619
+ deleteMemoryFile(projectPath, filename);
1620
+ return toolError(
1621
+ "OUTPUT_TOO_LARGE",
1622
+ "agent.md index exceeded size limit. Remove some memories first.",
1623
+ {
1624
+ suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1625
+ }
1626
+ );
1465
1627
  }
1466
- };
1628
+ return {
1629
+ success: true,
1630
+ data: {
1631
+ message: `Memory saved: ${name} (${type})`,
1632
+ filename,
1633
+ totalMemories: allEntries.length,
1634
+ agentMdChars: indexResult.charCount
1635
+ }
1636
+ };
1637
+ });
1467
1638
  }
1468
1639
  };
1469
1640
  }
1470
1641
  function createDeleteMemoryTool(projectPath) {
1471
1642
  return {
1472
1643
  name: "delete-memory",
1473
- description: "Delete a memory by name. Removes the file and its index entry in agent.md.",
1644
+ 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.",
1474
1645
  parameters: {
1475
1646
  type: "object",
1476
1647
  properties: {
1477
1648
  name: {
1478
1649
  type: "string",
1479
1650
  description: "Name of the memory to delete (case-insensitive match)"
1651
+ },
1652
+ type: {
1653
+ type: "string",
1654
+ enum: VALID_TYPES$1,
1655
+ description: "Optional: memory type to disambiguate when multiple memories share the same name"
1480
1656
  }
1481
1657
  },
1482
1658
  required: ["name"]
1483
1659
  },
1484
1660
  execute: async (input) => {
1485
1661
  const name = String(input.name || "").trim();
1662
+ const type = input.type ? String(input.type) : void 0;
1486
1663
  if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1487
1664
  suggestions: ["Provide the name of the memory to delete."]
1488
1665
  });
1489
- const existing = findMemoryByName(projectPath, name);
1490
- if (!existing) {
1666
+ const allMatches = findAllMemoriesByName(projectPath, name);
1667
+ if (allMatches.length === 0) {
1491
1668
  return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
1492
1669
  suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
1493
1670
  });
1494
1671
  }
1495
- deleteMemoryFile(projectPath, existing.filename);
1496
- const allEntries = listMemoryFiles(projectPath);
1497
- updateAgentMdIndex(projectPath, allEntries);
1498
- return {
1499
- success: true,
1500
- data: {
1501
- message: `Memory deleted: ${name}`,
1502
- totalMemories: allEntries.length
1503
- }
1504
- };
1672
+ if (allMatches.length > 1 && !type) {
1673
+ const types = allMatches.map((m) => m.frontmatter.type).join(", ");
1674
+ return toolError("AMBIGUOUS", `Multiple memories named "${name}" (types: ${types}). Specify type to disambiguate.`, {
1675
+ suggestions: [`Add type parameter: one of ${types}`]
1676
+ });
1677
+ }
1678
+ const existing = type ? findMemoryByName(projectPath, name, type) : allMatches[0];
1679
+ if (!existing) {
1680
+ return toolError("NOT_FOUND", `Memory not found: "${name}" with type "${type}"`, {
1681
+ suggestions: ["Check the memory name and type."]
1682
+ });
1683
+ }
1684
+ return withIndexLock(() => {
1685
+ deleteMemoryFile(projectPath, existing.filename);
1686
+ const allEntries = listMemoryFiles(projectPath);
1687
+ updateAgentMdIndex(projectPath, allEntries);
1688
+ return {
1689
+ success: true,
1690
+ data: {
1691
+ message: `Memory deleted: ${name}`,
1692
+ totalMemories: allEntries.length
1693
+ }
1694
+ };
1695
+ });
1505
1696
  }
1506
1697
  };
1507
1698
  }
@@ -1526,8 +1717,11 @@ const WEB_DEFAULTS = {
1526
1717
  maxFetchMaxChars: 2e5,
1527
1718
  defaultFetchTimeoutMs: 3e4,
1528
1719
  maxArxivCacheEntries: 100,
1529
- arxivSearchCacheTtlMs: 10 * 60 * 1e3
1720
+ arxivSearchCacheTtlMs: 10 * 60 * 1e3,
1530
1721
  // 10 min
1722
+ /** Content above this size is saved to disk; agent gets preview + file path */
1723
+ fetchPersistThresholdChars: 3e4,
1724
+ fetchPreviewChars: 2e3
1531
1725
  };
1532
1726
  class ProviderRateGate {
1533
1727
  constructor(minIntervalMs) {
@@ -1774,7 +1968,6 @@ function createWebSearchTool(ctx) {
1774
1968
  const providerRequested = normalizeSearchProvider(params.provider);
1775
1969
  const braveApiKey = process.env.BRAVE_API_KEY?.trim();
1776
1970
  let effectiveProvider = providerRequested === "auto" ? braveApiKey ? "brave" : "arxiv" : providerRequested;
1777
- ctx.onToolCall?.("web_search", { query, count, provider: effectiveProvider });
1778
1971
  let results = [];
1779
1972
  try {
1780
1973
  if (effectiveProvider === "brave") {
@@ -1818,7 +2011,6 @@ function createWebSearchTool(ctx) {
1818
2011
  count: results.length,
1819
2012
  results
1820
2013
  };
1821
- ctx.onToolResult?.("web_search", payload);
1822
2014
  return toAgentResult("web_search", {
1823
2015
  success: true,
1824
2016
  data: payload
@@ -1830,7 +2022,7 @@ function createWebFetchTool(ctx) {
1830
2022
  return {
1831
2023
  name: "web_fetch",
1832
2024
  label: "Web Fetch",
1833
- description: "Fetch a URL and extract readable text or markdown for downstream analysis.",
2025
+ 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.",
1834
2026
  parameters: WebFetchSchema,
1835
2027
  execute: async (_toolCallId, rawParams) => {
1836
2028
  const params = rawParams;
@@ -1859,7 +2051,6 @@ function createWebFetchTool(ctx) {
1859
2051
  const maxChars = typeof maxCharsRaw === "number" ? Math.max(100, Math.min(WEB_DEFAULTS.maxFetchMaxChars, Math.floor(maxCharsRaw))) : WEB_DEFAULTS.defaultFetchMaxChars;
1860
2052
  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);
1861
2053
  const timeoutMs = typeof timeoutSecRaw === "number" ? Math.max(1e3, Math.floor(timeoutSecRaw * 1e3)) : WEB_DEFAULTS.defaultFetchTimeoutMs;
1862
- ctx.onToolCall?.("web_fetch", { url: url.toString(), extractMode, maxChars });
1863
2054
  let response;
1864
2055
  const controller = new AbortController();
1865
2056
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1897,16 +2088,37 @@ Source: ${url.toString()}
1897
2088
  ---
1898
2089
 
1899
2090
  ${sliced}` : sliced;
1900
- const payload = {
1901
- url: url.toString(),
1902
- status_code: response.status,
1903
- content_type: contentType,
1904
- extract_mode: extractMode,
1905
- chars: normalized.length,
1906
- truncated,
1907
- content: output || "(empty response)"
1908
- };
1909
- ctx.onToolResult?.("web_fetch", payload);
2091
+ let payload;
2092
+ if (output.length > WEB_DEFAULTS.fetchPersistThresholdChars) {
2093
+ const hash = createHash("md5").update(url.toString() + Date.now()).digest("hex").slice(0, 12);
2094
+ const ext = extractMode === "markdown" ? "md" : "txt";
2095
+ const contentDir = path.join(ctx.projectPath, "web-content");
2096
+ await mkdir(contentDir, { recursive: true });
2097
+ const filePath = path.join(contentDir, `${hash}.${ext}`);
2098
+ await writeFile(filePath, output, "utf-8");
2099
+ const previewRaw = output.slice(0, WEB_DEFAULTS.fetchPreviewChars);
2100
+ const lastNl = previewRaw.lastIndexOf("\n");
2101
+ const preview = (lastNl > WEB_DEFAULTS.fetchPreviewChars * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw) + "\n...";
2102
+ payload = {
2103
+ url: url.toString(),
2104
+ status_code: response.status,
2105
+ content_type: contentType,
2106
+ extract_mode: extractMode,
2107
+ chars: normalized.length,
2108
+ content_path: path.relative(ctx.workspacePath, filePath),
2109
+ preview
2110
+ };
2111
+ } else {
2112
+ payload = {
2113
+ url: url.toString(),
2114
+ status_code: response.status,
2115
+ content_type: contentType,
2116
+ extract_mode: extractMode,
2117
+ chars: normalized.length,
2118
+ truncated,
2119
+ content: output || "(empty response)"
2120
+ };
2121
+ }
1910
2122
  return toAgentResult("web_fetch", {
1911
2123
  success: response.ok,
1912
2124
  data: payload,
@@ -2774,7 +2986,6 @@ function createLiteratureSearchTool(ctx) {
2774
2986
  suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
2775
2987
  }));
2776
2988
  }
2777
- ctx.onToolCall?.("literature-search", { query, context: extraContext });
2778
2989
  const planUserPrompt = extraContext ? `Research request: ${query}
2779
2990
 
2780
2991
  Additional context: ${extraContext}` : `Research request: ${query}`;
@@ -2951,9 +3162,9 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2951
3162
  }
2952
3163
  }
2953
3164
  }
2954
- const reviewDir = path.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
3165
+ const reviewDir = path$1.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
2955
3166
  fs.mkdirSync(reviewDir, { recursive: true });
2956
- const fullReviewPath = path.join(reviewDir, "review.json");
3167
+ const fullReviewPath = path$1.join(reviewDir, "review.json");
2957
3168
  fs.writeFileSync(fullReviewPath, JSON.stringify({
2958
3169
  plan,
2959
3170
  allPapersCount: deduplicated.length,
@@ -2962,7 +3173,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2962
3173
  queriesUsed,
2963
3174
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2964
3175
  }, null, 2), "utf-8");
2965
- const relReviewPath = path.relative(ctx.projectPath, fullReviewPath);
3176
+ const relReviewPath = path$1.relative(ctx.projectPath, fullReviewPath);
2966
3177
  const payload = {
2967
3178
  totalFound: deduplicated.length,
2968
3179
  reviewedCount: review.relevantPapers.length,
@@ -2985,12 +3196,11 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2985
3196
  runId,
2986
3197
  queriesUsed: queriesUsed.slice(0, 10)
2987
3198
  };
2988
- ctx.onToolResult?.("literature-search", payload);
2989
3199
  return toAgentResult("literature-search", toolSuccess(payload, pipelineWarnings.length > 0 ? pipelineWarnings : void 0));
2990
3200
  }
2991
3201
  };
2992
3202
  }
2993
- const execFileAsync$1 = promisify(execFile);
3203
+ const execFileAsync$3 = promisify(execFile);
2994
3204
  const DEFAULT_MAX_OUTPUT_CHARS = 5e5;
2995
3205
  const DEFAULT_PREVIEW_CHARS = 4e3;
2996
3206
  const DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
@@ -3028,16 +3238,16 @@ const FORMAT_EXTENSIONS = {
3028
3238
  zip: "zip"
3029
3239
  };
3030
3240
  function resolveWithinProject(projectPath, targetPath) {
3031
- const root = path.resolve(projectPath);
3032
- const resolved = targetPath.startsWith("/") ? path.resolve(targetPath) : path.resolve(root, targetPath);
3033
- const rel = path.relative(root, resolved);
3034
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
3241
+ const root = path$1.resolve(projectPath);
3242
+ const resolved = targetPath.startsWith("/") ? path$1.resolve(targetPath) : path$1.resolve(root, targetPath);
3243
+ const rel = path$1.relative(root, resolved);
3244
+ if (rel.startsWith("..") || path$1.isAbsolute(rel)) {
3035
3245
  throw new Error(`Path escapes project directory: ${targetPath}`);
3036
3246
  }
3037
3247
  return resolved;
3038
3248
  }
3039
3249
  function toProjectRelative(projectPath, absolutePath) {
3040
- return path.relative(path.resolve(projectPath), absolutePath);
3250
+ return path$1.relative(path$1.resolve(projectPath), absolutePath);
3041
3251
  }
3042
3252
  function isHttpUrl(value) {
3043
3253
  try {
@@ -3057,7 +3267,7 @@ function sanitizeBaseName(value) {
3057
3267
  function inferUrlBaseName(sourceUrl) {
3058
3268
  try {
3059
3269
  const parsed = new URL(sourceUrl);
3060
- const candidate = path.parse(parsed.pathname).name || parsed.hostname;
3270
+ const candidate = path$1.parse(parsed.pathname).name || parsed.hostname;
3061
3271
  return sanitizeBaseName(candidate);
3062
3272
  } catch {
3063
3273
  return sanitizeBaseName(sourceUrl);
@@ -3067,7 +3277,7 @@ function outputExtensionForMode(mode) {
3067
3277
  return mode === "text" ? ".txt" : ".md";
3068
3278
  }
3069
3279
  function extensionFromPath(filePath) {
3070
- const ext = path.extname(filePath).replace(".", "").toLowerCase();
3280
+ const ext = path$1.extname(filePath).replace(".", "").toLowerCase();
3071
3281
  return ext || void 0;
3072
3282
  }
3073
3283
  function normalizeMode(value) {
@@ -3121,7 +3331,7 @@ async function detectTextLikeFile(filePath) {
3121
3331
  }
3122
3332
  async function runCommand(command, args, timeoutMs) {
3123
3333
  try {
3124
- await execFileAsync$1(command, args, {
3334
+ await execFileAsync$3(command, args, {
3125
3335
  timeout: timeoutMs,
3126
3336
  maxBuffer: 10 * 1024 * 1024
3127
3337
  });
@@ -3212,7 +3422,7 @@ print(json.dumps(result, ensure_ascii=False))
3212
3422
  let foundAnyRuntime = false;
3213
3423
  for (const attempt of attempts) {
3214
3424
  try {
3215
- const { stdout } = await execFileAsync$1(
3425
+ const { stdout } = await execFileAsync$3(
3216
3426
  attempt.command,
3217
3427
  ["-c", script, inputPath, JSON.stringify(ranges)],
3218
3428
  { timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024, encoding: "utf8" }
@@ -3301,15 +3511,15 @@ async function downloadToProject(projectPath, sourceUrl) {
3301
3511
  if (bytes.length > DEFAULT_MAX_DOWNLOAD_BYTES) {
3302
3512
  return { ok: false, error: `Downloaded file too large (${bytes.length} bytes > ${DEFAULT_MAX_DOWNLOAD_BYTES} bytes)` };
3303
3513
  }
3304
- const downloadDir = path.join(path.resolve(projectPath), ".research-pilot", "cache", "downloads");
3514
+ const downloadDir = path$1.join(path$1.resolve(projectPath), ".research-pilot", "cache", "downloads");
3305
3515
  await fsp.mkdir(downloadDir, { recursive: true });
3306
3516
  const urlObj = new URL(sourceUrl);
3307
3517
  const fromExt = extensionFromPath(urlObj.pathname);
3308
3518
  const fromContentType = detectFormatFromContentType(response.headers.get("content-type"));
3309
3519
  const chosenExt = FORMAT_EXTENSIONS[fromContentType || ""] || FORMAT_EXTENSIONS[fromExt || ""] || "bin";
3310
- const hash = createHash("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3520
+ const hash = createHash$1("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3311
3521
  const fileName = `${isoStamp()}-${hash}.${chosenExt}`;
3312
- const inputPath = path.join(downloadDir, fileName);
3522
+ const inputPath = path$1.join(downloadDir, fileName);
3313
3523
  await fsp.writeFile(inputPath, bytes);
3314
3524
  return {
3315
3525
  ok: true,
@@ -3389,9 +3599,9 @@ ${sections}
3389
3599
  `;
3390
3600
  }
3391
3601
  function buildPerRangeOutputPath(baseAbsolutePath, range, mode) {
3392
- const parsed = path.parse(baseAbsolutePath);
3602
+ const parsed = path$1.parse(baseAbsolutePath);
3393
3603
  const ext = parsed.ext || outputExtensionForMode(mode);
3394
- return path.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3604
+ return path$1.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3395
3605
  }
3396
3606
  function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3397
3607
  const content = text.slice(0, maxPreviewChars);
@@ -3403,14 +3613,14 @@ function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3403
3613
  };
3404
3614
  }
3405
3615
  function defaultOutputPath(args) {
3406
- const outputDir = path.join(
3407
- path.resolve(args.projectPath),
3616
+ const outputDir = path$1.join(
3617
+ path$1.resolve(args.projectPath),
3408
3618
  ".research-pilot",
3409
3619
  "cache",
3410
3620
  "converted"
3411
3621
  );
3412
- const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path.parse(args.inputPath || "document").name || "document");
3413
- return path.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3622
+ const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path$1.parse(args.inputPath || "document").name || "document");
3623
+ return path$1.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3414
3624
  }
3415
3625
  function failure(payload) {
3416
3626
  return toAgentResult("convert_document", { success: false, error: payload.error, data: payload });
@@ -3593,7 +3803,7 @@ function createConvertDocumentTool(ctx) {
3593
3803
  mode
3594
3804
  });
3595
3805
  }
3596
- await fsp.mkdir(path.dirname(outputAbsolutePath), { recursive: true });
3806
+ await fsp.mkdir(path$1.dirname(outputAbsolutePath), { recursive: true });
3597
3807
  let converter;
3598
3808
  let producedText = "";
3599
3809
  let pageCount;
@@ -3626,7 +3836,7 @@ function createConvertDocumentTool(ctx) {
3626
3836
  anyTruncated = true;
3627
3837
  }
3628
3838
  const segmentAbsPath = buildPerRangeOutputPath(outputAbsolutePath, range, mode);
3629
- await fsp.mkdir(path.dirname(segmentAbsPath), { recursive: true });
3839
+ await fsp.mkdir(path$1.dirname(segmentAbsPath), { recursive: true });
3630
3840
  await fsp.writeFile(segmentAbsPath, segmentText, "utf8");
3631
3841
  const segmentRelPath = toProjectRelative(projectPath, segmentAbsPath);
3632
3842
  const segmentPreview = buildPreview(segmentText);
@@ -3722,7 +3932,7 @@ function createConvertDocumentTool(ctx) {
3722
3932
  }
3723
3933
  };
3724
3934
  }
3725
- const execFileAsync = promisify(execFile);
3935
+ const execFileAsync$2 = promisify(execFile);
3726
3936
  const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
3727
3937
  const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
3728
3938
  const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
@@ -3765,7 +3975,7 @@ function createDataAnalyzeTool(ctx) {
3765
3975
  suggestions: ["Valid task types: analyze, visualize, transform, model."]
3766
3976
  }));
3767
3977
  }
3768
- const absDataFile = path.resolve(ctx.workspacePath, filePath);
3978
+ const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
3769
3979
  if (!fs.existsSync(absDataFile)) {
3770
3980
  return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
3771
3981
  suggestions: [
@@ -3775,18 +3985,17 @@ function createDataAnalyzeTool(ctx) {
3775
3985
  context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
3776
3986
  }));
3777
3987
  }
3778
- ctx.onToolCall?.("data_analyze", { file_path: filePath, instructions, task_type: taskType });
3779
3988
  const runId = Date.now().toString(36);
3780
- const outputBase = path.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3781
- const figuresDir = path.join(outputBase, "figures");
3782
- const tablesDir = path.join(outputBase, "tables");
3783
- const dataDir = path.join(outputBase, "data");
3989
+ const outputBase = path$1.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3990
+ const figuresDir = path$1.join(outputBase, "figures");
3991
+ const tablesDir = path$1.join(outputBase, "tables");
3992
+ const dataDir = path$1.join(outputBase, "data");
3784
3993
  fs.mkdirSync(figuresDir, { recursive: true });
3785
3994
  fs.mkdirSync(tablesDir, { recursive: true });
3786
3995
  fs.mkdirSync(dataDir, { recursive: true });
3787
- const resultsFile = path.join(outputBase, "results.json");
3996
+ const resultsFile = path$1.join(outputBase, "results.json");
3788
3997
  const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
3789
- const ext = path.extname(absDataFile).toLowerCase();
3998
+ const ext = path$1.extname(absDataFile).toLowerCase();
3790
3999
  const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
3791
4000
  if (!ctx.callLlm) {
3792
4001
  return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
@@ -3834,10 +4043,10 @@ function createDataAnalyzeTool(ctx) {
3834
4043
  ""
3835
4044
  ].join("\n");
3836
4045
  const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
3837
- const scriptPath = path.join(outputBase, "script.py");
4046
+ const scriptPath = path$1.join(outputBase, "script.py");
3838
4047
  fs.writeFileSync(scriptPath, fullScript, "utf-8");
3839
4048
  try {
3840
- const { stdout, stderr } = await execFileAsync("python3", [scriptPath], {
4049
+ const { stdout, stderr } = await execFileAsync$2("python3", [scriptPath], {
3841
4050
  cwd: ctx.workspacePath,
3842
4051
  timeout: 12e4,
3843
4052
  // 2 minutes
@@ -3861,7 +4070,7 @@ function createDataAnalyzeTool(ctx) {
3861
4070
  outputs.push({
3862
4071
  name: f,
3863
4072
  type,
3864
- path: path.relative(ctx.workspacePath, path.join(dir, f))
4073
+ path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
3865
4074
  });
3866
4075
  }
3867
4076
  }
@@ -3874,26 +4083,25 @@ function createDataAnalyzeTool(ctx) {
3874
4083
  summary: manifest.summary,
3875
4084
  warnings: manifest.warnings
3876
4085
  } : void 0,
3877
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4086
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3878
4087
  runId
3879
4088
  };
3880
- ctx.onToolResult?.("data_analyze", payload);
3881
4089
  return toAgentResult("data_analyze", { success: true, data: payload });
3882
4090
  } catch (err) {
3883
4091
  const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
3884
4092
  return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
3885
4093
  retryable: true,
3886
4094
  suggestions: [
3887
- `Review the generated script at ${path.relative(ctx.workspacePath, scriptPath)} for errors.`,
4095
+ `Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
3888
4096
  "Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
3889
4097
  "Try simplifying the analysis instructions."
3890
4098
  ],
3891
4099
  context: {
3892
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4100
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3893
4101
  runId
3894
4102
  },
3895
4103
  data: {
3896
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4104
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3897
4105
  runId
3898
4106
  }
3899
4107
  }));
@@ -3901,133 +4109,2205 @@ function createDataAnalyzeTool(ctx) {
3901
4109
  }
3902
4110
  };
3903
4111
  }
3904
- function wrapResearchTool(tool) {
3905
- const properties = {};
3906
- const jsonProps = tool.parameters.properties ?? {};
3907
- const requiredFields = tool.parameters.required ?? [];
3908
- for (const [key, prop] of Object.entries(jsonProps)) {
3909
- const isRequired = requiredFields.includes(key);
3910
- let schema;
3911
- if (prop.enum) {
3912
- schema = Type.Union(prop.enum.map((v) => Type.Literal(v)));
3913
- } else if (prop.type === "number") {
3914
- schema = Type.Number({ description: prop.description });
3915
- } else if (prop.type === "array") {
3916
- schema = Type.Array(Type.String(), { description: prop.description });
3917
- } else {
3918
- schema = Type.String({ description: prop.description });
3919
- }
3920
- properties[key] = isRequired ? schema : Type.Optional(schema);
3921
- }
3922
- const parametersSchema = Type.Object(properties);
3923
- return {
3924
- name: tool.name,
3925
- description: tool.description,
3926
- label: tool.name,
3927
- parameters: parametersSchema,
3928
- execute: async (_toolCallId, params, _signal) => {
4112
+ function isTerminal(state) {
4113
+ return state === "completed" || state === "failed" || state === "timed_out" || state === "cancelled";
4114
+ }
4115
+ const RUNS_FILE = "runs.jsonl";
4116
+ const EVICT_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
4117
+ const FLUSH_INTERVAL_MS = 3e4;
4118
+ class RunStore {
4119
+ constructor(projectPath) {
4120
+ this.records = /* @__PURE__ */ new Map();
4121
+ this.loaded = false;
4122
+ this.dirty = false;
4123
+ this.flushTimer = null;
4124
+ this.dir = path$1.join(projectPath, ".research-pilot", "compute-runs");
4125
+ this.filePath = path$1.join(this.dir, RUNS_FILE);
4126
+ }
4127
+ ensureDir() {
4128
+ if (!fs.existsSync(this.dir)) {
4129
+ fs.mkdirSync(this.dir, { recursive: true });
4130
+ }
4131
+ }
4132
+ load() {
4133
+ if (this.loaded) return;
4134
+ this.loaded = true;
4135
+ if (!fs.existsSync(this.filePath)) return;
4136
+ const content = fs.readFileSync(this.filePath, "utf-8");
4137
+ for (const line of content.split("\n")) {
4138
+ const trimmed = line.trim();
4139
+ if (!trimmed) continue;
3929
4140
  try {
3930
- const result = await tool.execute(params);
3931
- return toAgentResult(tool.name, result);
3932
- } catch (err) {
3933
- const errorMsg = err instanceof Error ? err.message : String(err);
3934
- return toAgentResult(tool.name, toolError("EXECUTION_FAILED", errorMsg, {
3935
- retryable: false,
3936
- suggestions: ["Check tool parameters and try again."]
3937
- }));
4141
+ const record = JSON.parse(trimmed);
4142
+ this.records.set(record.runId, record);
4143
+ } catch {
3938
4144
  }
3939
4145
  }
3940
- };
3941
- }
3942
- function createResearchTools(ctx) {
3943
- const tools = [];
3944
- tools.push(createWebSearchTool(ctx));
3945
- tools.push(createWebFetchTool(ctx));
3946
- tools.push(createLiteratureSearchTool(ctx));
3947
- tools.push(createConvertDocumentTool(ctx));
3948
- tools.push(createDataAnalyzeTool(ctx));
3949
- const artifactTools = createResearchMemoryTools({
3950
- sessionId: ctx.sessionId,
3951
- projectPath: ctx.projectPath
3952
- });
3953
- for (const tool of artifactTools) {
3954
- tools.push(wrapResearchTool(tool));
3955
4146
  }
3956
- const structuredMemoryTools = createMemoryTools(ctx.projectPath);
3957
- for (const tool of structuredMemoryTools) {
3958
- tools.push(wrapResearchTool(tool));
4147
+ writeToDisk() {
4148
+ this.ensureDir();
4149
+ const lines = Array.from(this.records.values()).map((r) => JSON.stringify(r)).join("\n");
4150
+ const tmpPath = this.filePath + ".tmp." + process.pid + "." + Date.now();
4151
+ fs.writeFileSync(tmpPath, lines + "\n", "utf-8");
4152
+ fs.renameSync(tmpPath, this.filePath);
4153
+ this.dirty = false;
3959
4154
  }
3960
- return tools;
3961
- }
3962
- const VALID_TYPES = ["user", "feedback", "project", "reference"];
3963
- const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
3964
-
3965
- Rules:
3966
- - Only extract DURABLE, IMPORTANT information — things a future session would need.
3967
- - Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
3968
- - Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
3969
- - Do NOT extract: routine task results, ephemeral details, things already in workspace files.
3970
- - Each memory should be atomic — one concept per entry.
3971
- - If nothing is worth saving, return an empty array.
3972
-
3973
- Return ONLY a JSON array (no markdown fences, no explanation):
3974
- [{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
3975
- Or: []`;
3976
- function simplifyMessages(messages, maxMessages) {
3977
- const recent = messages.slice(-20);
3978
- const result = [];
3979
- for (const msg of recent) {
3980
- if (msg.role !== "user" && msg.role !== "assistant") continue;
3981
- let content = "";
3982
- if (typeof msg.content === "string") {
3983
- content = msg.content;
3984
- } else if (Array.isArray(msg.content)) {
3985
- for (const block of msg.content) {
3986
- if (block && typeof block === "object" && "type" in block) {
3987
- if (block.type === "text" && "text" in block) {
3988
- const text = block.text;
3989
- content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
3990
- content += "\n";
3991
- } else if (block.type === "tool_use" && "name" in block) {
3992
- content += `[Called ${block.name}]
3993
- `;
3994
- }
3995
- }
3996
- }
4155
+ /**
4156
+ * Mark store as dirty. Starts the periodic flush timer if not running.
4157
+ */
4158
+ markDirty() {
4159
+ this.dirty = true;
4160
+ if (!this.flushTimer) {
4161
+ this.flushTimer = setInterval(() => {
4162
+ if (this.dirty) this.writeToDisk();
4163
+ }, FLUSH_INTERVAL_MS);
4164
+ if (this.flushTimer.unref) this.flushTimer.unref();
3997
4165
  }
3998
- content = content.trim();
3999
- if (content) {
4000
- result.push({
4001
- role: msg.role,
4002
- content: content.slice(0, 2e3),
4003
- timestamp: Date.now()
4004
- });
4166
+ }
4167
+ /**
4168
+ * Flush immediately (synchronous). Called on critical events:
4169
+ * - createRun (new record must survive crash)
4170
+ * - Terminal state transition (completed/failed/cancelled/timed_out)
4171
+ * - Shutdown (destroy)
4172
+ */
4173
+ flushNow() {
4174
+ if (this.dirty) this.writeToDisk();
4175
+ }
4176
+ /**
4177
+ * Stop the periodic flush timer. Called on destroy.
4178
+ */
4179
+ stopFlushTimer() {
4180
+ if (this.flushTimer) {
4181
+ clearInterval(this.flushTimer);
4182
+ this.flushTimer = null;
4183
+ }
4184
+ }
4185
+ getRun(runId) {
4186
+ this.load();
4187
+ return this.records.get(runId);
4188
+ }
4189
+ getAllRuns() {
4190
+ this.load();
4191
+ return Array.from(this.records.values());
4192
+ }
4193
+ getActiveRuns() {
4194
+ this.load();
4195
+ return Array.from(this.records.values()).filter((r) => !isTerminal(r.status));
4196
+ }
4197
+ createRun(record) {
4198
+ this.load();
4199
+ this.records.set(record.runId, record);
4200
+ this.writeToDisk();
4201
+ }
4202
+ updateRun(runId, patch) {
4203
+ this.load();
4204
+ const existing = this.records.get(runId);
4205
+ if (!existing) return void 0;
4206
+ const updated = { ...existing, ...patch };
4207
+ this.records.set(runId, updated);
4208
+ if (patch.status && isTerminal(patch.status)) {
4209
+ this.writeToDisk();
4210
+ } else {
4211
+ this.markDirty();
4005
4212
  }
4213
+ return updated;
4006
4214
  }
4007
- return result;
4008
- }
4009
- function agentCalledSaveMemoryThisTurn(messages) {
4010
- for (let i = messages.length - 1; i >= 0; i--) {
4011
- const msg = messages[i];
4012
- if (msg.role === "user") break;
4013
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
4014
- for (const block of msg.content) {
4015
- if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
4016
- if (block.name === "save-memory") return true;
4215
+ /**
4216
+ * Remove completed runs older than maxAgeMs.
4217
+ */
4218
+ evictOld(maxAgeMs = EVICT_AGE_MS) {
4219
+ this.load();
4220
+ const cutoff = Date.now() - maxAgeMs;
4221
+ let evicted = 0;
4222
+ for (const [id, record] of this.records) {
4223
+ if (isTerminal(record.status) && record.completedAt) {
4224
+ const completedTime = new Date(record.completedAt).getTime();
4225
+ if (completedTime < cutoff) {
4226
+ this.records.delete(id);
4227
+ const runDir = this.getRunDir(id);
4228
+ try {
4229
+ fs.rmSync(runDir, { recursive: true, force: true });
4230
+ } catch {
4231
+ }
4232
+ evicted++;
4017
4233
  }
4018
4234
  }
4019
4235
  }
4236
+ if (evicted > 0) this.writeToDisk();
4237
+ return evicted;
4238
+ }
4239
+ getRunDir(runId) {
4240
+ return path$1.join(this.dir, runId);
4241
+ }
4242
+ getOutputPath(runId) {
4243
+ return path$1.join(this.dir, runId, "output.log");
4244
+ }
4245
+ getStderrPath(runId) {
4246
+ return path$1.join(this.dir, runId, "output.log.stderr");
4020
4247
  }
4021
- return false;
4022
4248
  }
4023
- async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
4024
- if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT === "0") return;
4025
- if (turnCount % extractEveryN !== 0) return;
4026
- if (agentCalledSaveMemoryThisTurn(messages)) {
4027
- if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
4028
- return;
4249
+ class ProcessSandbox {
4250
+ constructor() {
4251
+ this.name = "process";
4029
4252
  }
4030
- try {
4253
+ async available() {
4254
+ return true;
4255
+ }
4256
+ async spawn(config) {
4257
+ const outputDir = path$1.dirname(config.outputPath);
4258
+ fs.mkdirSync(outputDir, { recursive: true });
4259
+ const stderrPath = config.outputPath + ".stderr";
4260
+ const wrappedCommand = `( ${config.command} ) 2> >(tee -a ${JSON.stringify(stderrPath)} >&2) >> ${JSON.stringify(config.outputPath)} 2>&1`;
4261
+ fs.writeFileSync(config.outputPath, "");
4262
+ fs.writeFileSync(stderrPath, "");
4263
+ const child = spawn("/bin/bash", ["-c", wrappedCommand], {
4264
+ cwd: config.workDir,
4265
+ env: { ...process.env, ...config.env },
4266
+ stdio: ["ignore", "ignore", "ignore"],
4267
+ detached: true
4268
+ });
4269
+ const pid = child.pid;
4270
+ if (pid === void 0) {
4271
+ throw new Error("Failed to spawn process: no PID returned");
4272
+ }
4273
+ if (config.signal) {
4274
+ const onAbort = () => {
4275
+ try {
4276
+ process.kill(-pid, "SIGTERM");
4277
+ } catch {
4278
+ }
4279
+ };
4280
+ config.signal.addEventListener("abort", onAbort, { once: true });
4281
+ child.on("exit", () => config.signal.removeEventListener("abort", onAbort));
4282
+ }
4283
+ let settled = false;
4284
+ const waitPromise = new Promise((resolve2) => {
4285
+ child.on("exit", (code, signal) => {
4286
+ if (settled) return;
4287
+ settled = true;
4288
+ resolve2({
4289
+ exitCode: code ?? 1,
4290
+ exitSignal: signal ?? void 0
4291
+ });
4292
+ });
4293
+ child.on("error", () => {
4294
+ if (settled) return;
4295
+ settled = true;
4296
+ resolve2({ exitCode: 1, exitSignal: void 0 });
4297
+ });
4298
+ });
4299
+ return {
4300
+ pid,
4301
+ async kill(sig = "SIGTERM") {
4302
+ try {
4303
+ process.kill(-pid, sig);
4304
+ } catch {
4305
+ }
4306
+ },
4307
+ wait() {
4308
+ return waitPromise;
4309
+ },
4310
+ async cleanup() {
4311
+ try {
4312
+ child.unref();
4313
+ } catch {
4314
+ }
4315
+ }
4316
+ };
4317
+ }
4318
+ }
4319
+ promisify(execFile);
4320
+ let cachedProviders = null;
4321
+ async function detectProviders() {
4322
+ if (cachedProviders) return cachedProviders;
4323
+ const providers = [];
4324
+ providers.push(new ProcessSandbox());
4325
+ cachedProviders = providers;
4326
+ return providers;
4327
+ }
4328
+ async function getProvider(preference) {
4329
+ const providers = await detectProviders();
4330
+ if (preference && preference !== "auto") {
4331
+ const match = providers.find((p) => p.name === preference);
4332
+ if (match) return match;
4333
+ }
4334
+ return providers[0];
4335
+ }
4336
+ function deriveFailure(run) {
4337
+ if (run.status === "completed") return void 0;
4338
+ if (run.status === "cancelled") return void 0;
4339
+ const stderr = run.stderrTail ?? "";
4340
+ const exit = run.exitCode;
4341
+ if (exit === 137 || /\bMemoryError\b|OOM|Cannot allocate memory/i.test(stderr) || exit === 137 && /\bKilled\b$/m.test(stderr)) {
4342
+ return {
4343
+ code: "OOM_KILLED",
4344
+ retryable: true,
4345
+ message: "Process was killed due to insufficient memory.",
4346
+ suggestions: [
4347
+ "Reduce the dataset size or batch size.",
4348
+ "Close other memory-intensive applications.",
4349
+ "Process large data in chunks if possible."
4350
+ ]
4351
+ };
4352
+ }
4353
+ if (run.status === "timed_out") {
4354
+ const timeoutMin = Math.round(run.timeoutMs / 6e4);
4355
+ return {
4356
+ code: "TIMEOUT",
4357
+ retryable: true,
4358
+ message: `Process exceeded the ${timeoutMin}-minute timeout.`,
4359
+ suggestions: [
4360
+ "Increase the timeout_minutes parameter.",
4361
+ "Optimize the code for faster execution.",
4362
+ "Process a smaller subset of the data."
4363
+ ]
4364
+ };
4365
+ }
4366
+ if (run.status === "stalled") {
4367
+ return {
4368
+ code: "STALL",
4369
+ retryable: true,
4370
+ message: "Process stopped producing output and appears stuck.",
4371
+ suggestions: [
4372
+ "Check for deadlocks or blocking I/O in the code.",
4373
+ "Add progress logging to detect where execution hangs.",
4374
+ "Check if the process is waiting for interactive input."
4375
+ ]
4376
+ };
4377
+ }
4378
+ if (/ModuleNotFoundError|No module named/i.test(stderr)) {
4379
+ const moduleMatch = stderr.match(/No module named '([^']+)'/i) ?? stderr.match(/ModuleNotFoundError:\s*No module named\s+'?([^\s']+)/i);
4380
+ const moduleName = moduleMatch?.[1] ?? "unknown";
4381
+ return {
4382
+ code: "MODULE_NOT_FOUND",
4383
+ retryable: true,
4384
+ message: `Python module not found: ${moduleName}`,
4385
+ suggestions: [
4386
+ `Add "${moduleName}" to requirements.txt and retry.`,
4387
+ `Or install directly: pip install ${moduleName}`
4388
+ ]
4389
+ };
4390
+ }
4391
+ if (/PermissionError|EACCES|Permission denied/i.test(stderr)) {
4392
+ return {
4393
+ code: "PERMISSION_DENIED",
4394
+ retryable: false,
4395
+ message: "Permission denied during execution.",
4396
+ suggestions: [
4397
+ "Check file permissions on input/output paths.",
4398
+ "Ensure the output directory is writable."
4399
+ ]
4400
+ };
4401
+ }
4402
+ if (/Traceback \(most recent call last\)/i.test(stderr) || /^\w+Error:/m.test(stderr) || /^\w+Exception:/m.test(stderr)) {
4403
+ const lines = stderr.split("\n").filter((l) => l.trim());
4404
+ const lastErrorLine = lines[lines.length - 1] ?? "";
4405
+ return {
4406
+ code: "PYTHON_ERROR",
4407
+ retryable: true,
4408
+ message: `Python error: ${lastErrorLine.slice(0, 200)}`,
4409
+ suggestions: [
4410
+ "Read the stderr output to understand the error.",
4411
+ "Fix the code and retry."
4412
+ ]
4413
+ };
4414
+ }
4415
+ if (run.exitSignal) {
4416
+ return {
4417
+ code: "SIGNAL_KILLED",
4418
+ retryable: run.exitSignal === "SIGTERM",
4419
+ // SIGTERM is often retryable
4420
+ message: `Process was killed by signal: ${run.exitSignal}`,
4421
+ suggestions: [
4422
+ "Check if another process or the system killed this task.",
4423
+ "If the signal was SIGTERM, it may be safe to retry."
4424
+ ]
4425
+ };
4426
+ }
4427
+ return {
4428
+ code: "COMMAND_FAILED",
4429
+ retryable: false,
4430
+ message: `Command exited with code ${exit ?? "unknown"}.`,
4431
+ suggestions: [
4432
+ "Review stderr output for details on the failure.",
4433
+ "Fix the script error and re-submit."
4434
+ ]
4435
+ };
4436
+ }
4437
+ function extractProgress(tail) {
4438
+ const cooperative = extractCooperativeProgress(tail);
4439
+ if (cooperative) return cooperative;
4440
+ return extractRegexProgress(tail);
4441
+ }
4442
+ function extractCooperativeProgress(tail) {
4443
+ const lines = tail.split("\n");
4444
+ for (let i = lines.length - 1; i >= 0; i--) {
4445
+ const line = lines[i].trim();
4446
+ if (!line.startsWith("##PROGRESS##")) continue;
4447
+ const jsonStr = line.slice("##PROGRESS##".length).trim();
4448
+ try {
4449
+ const data = JSON.parse(jsonStr);
4450
+ const progress = {};
4451
+ if (typeof data.step === "number") progress.currentStep = data.step;
4452
+ if (typeof data.total === "number") progress.totalSteps = data.total;
4453
+ if (typeof data.percentage === "number") progress.percentage = data.percentage;
4454
+ if (typeof data.phase === "string") progress.phase = data.phase;
4455
+ if (typeof data.eta === "number") progress.etaSeconds = data.eta;
4456
+ if (typeof data.eta_seconds === "number") progress.etaSeconds = data.eta_seconds;
4457
+ const knownKeys = /* @__PURE__ */ new Set(["step", "total", "percentage", "phase", "eta", "eta_seconds"]);
4458
+ const metrics = {};
4459
+ for (const [k, v] of Object.entries(data)) {
4460
+ if (!knownKeys.has(k) && typeof v === "number") metrics[k] = v;
4461
+ }
4462
+ if (Object.keys(metrics).length > 0) progress.metrics = metrics;
4463
+ if (progress.percentage === void 0 && progress.currentStep !== void 0 && progress.totalSteps) {
4464
+ progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
4465
+ }
4466
+ return progress;
4467
+ } catch {
4468
+ }
4469
+ }
4470
+ return void 0;
4471
+ }
4472
+ function extractRegexProgress(tail) {
4473
+ const progress = {};
4474
+ let found = false;
4475
+ const normalizedTail = tail.replace(/\r/g, "\n");
4476
+ const tqdmMatches = [...normalizedTail.matchAll(/(\d+)%\|[^|]*\|\s*(\d+)\/(\d+)\s*\[[\d:]+<([\d:?]+)/g)];
4477
+ const tqdm = tqdmMatches.length > 0 ? tqdmMatches[tqdmMatches.length - 1] : null;
4478
+ if (tqdm) {
4479
+ progress.percentage = parseInt(tqdm[1], 10);
4480
+ progress.currentStep = parseInt(tqdm[2], 10);
4481
+ progress.totalSteps = parseInt(tqdm[3], 10);
4482
+ progress.etaSeconds = parseTimeStr(tqdm[4]);
4483
+ found = true;
4484
+ }
4485
+ if (!found) {
4486
+ const epoch = normalizedTail.match(/[Ee]pochs?\s+(\d+)\s*[/of]+\s*(\d+)/);
4487
+ if (epoch) {
4488
+ progress.currentStep = parseInt(epoch[1], 10);
4489
+ progress.totalSteps = parseInt(epoch[2], 10);
4490
+ progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
4491
+ progress.phase = "training";
4492
+ found = true;
4493
+ }
4494
+ }
4495
+ if (!found) {
4496
+ const step = normalizedTail.match(/[Ss]teps?\s+(\d+)\s*[/of]+\s*(\d+)/);
4497
+ if (step) {
4498
+ progress.currentStep = parseInt(step[1], 10);
4499
+ progress.totalSteps = parseInt(step[2], 10);
4500
+ progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
4501
+ found = true;
4502
+ }
4503
+ }
4504
+ if (!found) {
4505
+ const pctMatches = [...normalizedTail.matchAll(/(\d{1,3})%/g)];
4506
+ const pct = pctMatches.length > 0 ? pctMatches[pctMatches.length - 1] : null;
4507
+ if (pct) {
4508
+ const val = parseInt(pct[1], 10);
4509
+ if (val >= 0 && val <= 100) {
4510
+ progress.percentage = val;
4511
+ found = true;
4512
+ }
4513
+ }
4514
+ }
4515
+ const metrics = {};
4516
+ const metricPattern = /\b(loss|acc(?:uracy)?|f1|auc|mse|mae|rmse|r2|lr|learning_rate|val_loss|val_acc(?:uracy)?)\s*[=:]\s*([\d.]+(?:e[+-]?\d+)?)/gi;
4517
+ for (const m of normalizedTail.matchAll(metricPattern)) {
4518
+ const key = m[1].toLowerCase();
4519
+ const val = parseFloat(m[2]);
4520
+ if (!isNaN(val)) metrics[key] = val;
4521
+ }
4522
+ if (Object.keys(metrics).length > 0) {
4523
+ progress.metrics = metrics;
4524
+ found = true;
4525
+ }
4526
+ const lastLines = normalizedTail.slice(-2048).toLowerCase();
4527
+ if (/download|fetching|pulling/i.test(lastLines)) progress.phase = "downloading";
4528
+ else if (/train|fitting|epoch/i.test(lastLines)) progress.phase = "training";
4529
+ else if (/evaluat|validat|testing/i.test(lastLines)) progress.phase = "evaluating";
4530
+ else if (/preprocess|clean|transform/i.test(lastLines)) progress.phase = "preprocessing";
4531
+ else if (/saving|export|writing.*output/i.test(lastLines)) progress.phase = "saving";
4532
+ return found ? progress : void 0;
4533
+ }
4534
+ function parseTimeStr(s) {
4535
+ if (!s || s.includes("?")) return void 0;
4536
+ const parts = s.split(":").map((p) => parseInt(p, 10));
4537
+ if (parts.some(isNaN)) return void 0;
4538
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
4539
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
4540
+ return void 0;
4541
+ }
4542
+ const MAX_HEAVY_CONCURRENT = 1;
4543
+ const MAX_TOTAL_CONCURRENT = 3;
4544
+ const MIN_FREE_MEMORY_MB = 500;
4545
+ const MIN_FREE_DISK_MB = 500;
4546
+ function classifyWeight(timeoutMinutes, command) {
4547
+ if (timeoutMinutes <= 2) return "light";
4548
+ if (timeoutMinutes <= 10 && /\b(plot|viz|chart|figure|draw|render)\b/i.test(command)) return "light";
4549
+ return "heavy";
4550
+ }
4551
+ function canAdmit(snapshot, weight) {
4552
+ const activeHeavy = snapshot.activeRuns.filter((r) => r.weight === "heavy").length;
4553
+ const totalActive = snapshot.activeRuns.length;
4554
+ if (weight === "heavy" && activeHeavy >= MAX_HEAVY_CONCURRENT) {
4555
+ const heavyRun = snapshot.activeRuns.find((r) => r.weight === "heavy");
4556
+ return {
4557
+ allowed: false,
4558
+ reason: `A heavy compute run is already active${heavyRun ? ` (${heavyRun.runId})` : ""}. Wait for it to finish, or stop it first.`
4559
+ };
4560
+ }
4561
+ if (totalActive >= MAX_TOTAL_CONCURRENT) {
4562
+ return {
4563
+ allowed: false,
4564
+ reason: `Too many concurrent runs (${totalActive}/${MAX_TOTAL_CONCURRENT}). Wait for one to finish, or stop one.`
4565
+ };
4566
+ }
4567
+ if (snapshot.freeMemoryMb < MIN_FREE_MEMORY_MB) {
4568
+ return {
4569
+ allowed: false,
4570
+ reason: `Low memory (${Math.round(snapshot.freeMemoryMb)}MB free, need ${MIN_FREE_MEMORY_MB}MB). Close applications to free resources.`
4571
+ };
4572
+ }
4573
+ if (snapshot.freeDiskMb < MIN_FREE_DISK_MB) {
4574
+ return {
4575
+ allowed: false,
4576
+ reason: `Low disk space (${Math.round(snapshot.freeDiskMb)}MB free, need ${MIN_FREE_DISK_MB}MB).`
4577
+ };
4578
+ }
4579
+ return { allowed: true, reason: "Resources available" };
4580
+ }
4581
+ const execFileAsync$1 = promisify(execFile);
4582
+ async function checkSyntax(scriptPath, pythonPath) {
4583
+ const start = Date.now();
4584
+ if (!scriptPath.endsWith(".py")) {
4585
+ return { name: "syntax", status: "passed", message: "Not a Python script — skipped.", durationMs: Date.now() - start };
4586
+ }
4587
+ if (!fs.existsSync(scriptPath)) {
4588
+ return { name: "syntax", status: "failed", message: `Script not found: ${scriptPath}`, durationMs: Date.now() - start };
4589
+ }
4590
+ try {
4591
+ await execFileAsync$1(pythonPath, ["-m", "py_compile", scriptPath], { timeout: 1e4 });
4592
+ return { name: "syntax", status: "passed", message: "Python syntax OK.", durationMs: Date.now() - start };
4593
+ } catch (err) {
4594
+ const msg = err instanceof Error ? err.message : String(err);
4595
+ const syntaxLine = msg.split("\n").find((l) => /SyntaxError|IndentationError|TabError/i.test(l)) ?? msg.slice(0, 200);
4596
+ return { name: "syntax", status: "failed", message: `Syntax error: ${syntaxLine}`, durationMs: Date.now() - start };
4597
+ }
4598
+ }
4599
+ async function checkImports(scriptPath, pythonPath) {
4600
+ const start = Date.now();
4601
+ if (!scriptPath.endsWith(".py") || !fs.existsSync(scriptPath)) {
4602
+ return { name: "imports", status: "passed", message: "Skipped.", durationMs: Date.now() - start };
4603
+ }
4604
+ const content = fs.readFileSync(scriptPath, "utf-8");
4605
+ const imports = /* @__PURE__ */ new Set();
4606
+ for (const match of content.matchAll(/^\s*(?:import|from)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm)) {
4607
+ const mod = match[1];
4608
+ if (STDLIB_MODULES.has(mod)) continue;
4609
+ imports.add(mod);
4610
+ }
4611
+ if (imports.size === 0) {
4612
+ return { name: "imports", status: "passed", message: "No third-party imports detected.", durationMs: Date.now() - start };
4613
+ }
4614
+ const importList = Array.from(imports);
4615
+ const testScript = importList.map((mod) => `try:
4616
+ import ${mod}
4617
+ except ImportError:
4618
+ print("MISSING:" + "${mod}")`).join("\n");
4619
+ const missing = [];
4620
+ try {
4621
+ const { stdout } = await execFileAsync$1(pythonPath, ["-c", testScript], { timeout: 3e4 });
4622
+ for (const line of stdout.split("\n")) {
4623
+ if (line.startsWith("MISSING:")) missing.push(line.slice(8));
4624
+ }
4625
+ } catch {
4626
+ return { name: "imports", status: "warning", message: `Could not verify imports: ${importList.join(", ")}`, durationMs: Date.now() - start };
4627
+ }
4628
+ if (missing.length === 0) {
4629
+ return { name: "imports", status: "passed", message: `All ${imports.size} imports available.`, durationMs: Date.now() - start };
4630
+ }
4631
+ return {
4632
+ name: "imports",
4633
+ status: "failed",
4634
+ message: `Missing modules: ${missing.join(", ")}. Add to requirements.txt or pip install.`,
4635
+ durationMs: Date.now() - start
4636
+ };
4637
+ }
4638
+ function checkDataPaths(command, workDir) {
4639
+ const start = Date.now();
4640
+ const pathPattern = /(?:["']([^"']+\.(?:csv|tsv|json|jsonl|xlsx|xls|parquet|feather|h5|hdf5|npy|npz|pkl|pickle|txt|dat))["']|(\S+\.(?:csv|tsv|json|jsonl|xlsx|xls|parquet|feather|h5|hdf5|npy|npz|pkl|pickle)))/gi;
4641
+ const refs = [];
4642
+ for (const m of command.matchAll(pathPattern)) {
4643
+ refs.push(m[1] ?? m[2]);
4644
+ }
4645
+ if (refs.length === 0) {
4646
+ return { name: "data_paths", status: "passed", message: "No data file references detected in command.", durationMs: Date.now() - start };
4647
+ }
4648
+ const missing = [];
4649
+ for (const ref of refs) {
4650
+ const abs = path$1.isAbsolute(ref) ? ref : path$1.resolve(workDir, ref);
4651
+ if (!fs.existsSync(abs)) missing.push(ref);
4652
+ }
4653
+ if (missing.length === 0) {
4654
+ return { name: "data_paths", status: "passed", message: `All ${refs.length} data files found.`, durationMs: Date.now() - start };
4655
+ }
4656
+ return {
4657
+ name: "data_paths",
4658
+ status: "failed",
4659
+ message: `Data files not found: ${missing.join(", ")}`,
4660
+ durationMs: Date.now() - start
4661
+ };
4662
+ }
4663
+ function checkDiskSpace(workDir) {
4664
+ const start = Date.now();
4665
+ try {
4666
+ const output = execSync("df -m .", { cwd: workDir, encoding: "utf-8", timeout: 3e3 });
4667
+ const lines = output.trim().split("\n");
4668
+ if (lines.length >= 2) {
4669
+ const parts = lines[1].split(/\s+/);
4670
+ const availMb = parseInt(parts[3], 10);
4671
+ if (!isNaN(availMb)) {
4672
+ if (availMb < 500) {
4673
+ return { name: "disk_space", status: "failed", message: `Only ${availMb}MB free disk space. Need at least 500MB.`, durationMs: Date.now() - start };
4674
+ }
4675
+ if (availMb < 2e3) {
4676
+ return { name: "disk_space", status: "warning", message: `${availMb}MB free disk space. May be tight for large outputs.`, durationMs: Date.now() - start };
4677
+ }
4678
+ return { name: "disk_space", status: "passed", message: `${availMb}MB free disk space.`, durationMs: Date.now() - start };
4679
+ }
4680
+ }
4681
+ } catch {
4682
+ }
4683
+ return { name: "disk_space", status: "passed", message: "Could not detect disk space — proceeding.", durationMs: Date.now() - start };
4684
+ }
4685
+ function checkOutputDir(workDir) {
4686
+ const start = Date.now();
4687
+ try {
4688
+ const testFile = path$1.join(workDir, ".compute-preflight-test");
4689
+ fs.writeFileSync(testFile, "test");
4690
+ fs.unlinkSync(testFile);
4691
+ return { name: "output_dir", status: "passed", message: "Working directory is writable.", durationMs: Date.now() - start };
4692
+ } catch {
4693
+ return { name: "output_dir", status: "failed", message: `Working directory is not writable: ${workDir}`, durationMs: Date.now() - start };
4694
+ }
4695
+ }
4696
+ async function runPreflight(opts) {
4697
+ const pythonPath = opts.pythonPath ?? "python3";
4698
+ const scriptPath = opts.scriptPath ?? extractScriptPath(opts.command, opts.workDir);
4699
+ const [syntax, imports, dataPaths, diskSpace, outputDir] = await Promise.all([
4700
+ scriptPath ? checkSyntax(scriptPath, pythonPath) : Promise.resolve(null),
4701
+ scriptPath ? checkImports(scriptPath, pythonPath) : Promise.resolve(null),
4702
+ Promise.resolve(checkDataPaths(opts.command, opts.workDir)),
4703
+ Promise.resolve(checkDiskSpace(opts.workDir)),
4704
+ Promise.resolve(checkOutputDir(opts.workDir))
4705
+ ]);
4706
+ const checks = [syntax, imports, dataPaths, diskSpace, outputDir].filter(Boolean);
4707
+ const blockingIssues = checks.filter((c) => c.status === "failed").map((c) => c.message);
4708
+ const warnings = checks.filter((c) => c.status === "warning").map((c) => c.message);
4709
+ return {
4710
+ passed: blockingIssues.length === 0,
4711
+ checks,
4712
+ blockingIssues,
4713
+ warnings
4714
+ };
4715
+ }
4716
+ function extractScriptPath(command, workDir) {
4717
+ const match = command.match(/python3?\s+([^\s]+\.py)/);
4718
+ if (!match) return void 0;
4719
+ const scriptRef = match[1];
4720
+ return path$1.isAbsolute(scriptRef) ? scriptRef : path$1.resolve(workDir, scriptRef);
4721
+ }
4722
+ const STDLIB_MODULES = /* @__PURE__ */ new Set([
4723
+ "abc",
4724
+ "aifc",
4725
+ "argparse",
4726
+ "array",
4727
+ "ast",
4728
+ "asynchat",
4729
+ "asyncio",
4730
+ "asyncore",
4731
+ "atexit",
4732
+ "base64",
4733
+ "bdb",
4734
+ "binascii",
4735
+ "binhex",
4736
+ "bisect",
4737
+ "builtins",
4738
+ "bz2",
4739
+ "calendar",
4740
+ "cgi",
4741
+ "cgitb",
4742
+ "chunk",
4743
+ "cmath",
4744
+ "cmd",
4745
+ "code",
4746
+ "codecs",
4747
+ "codeop",
4748
+ "collections",
4749
+ "colorsys",
4750
+ "compileall",
4751
+ "concurrent",
4752
+ "configparser",
4753
+ "contextlib",
4754
+ "contextvars",
4755
+ "copy",
4756
+ "copyreg",
4757
+ "cProfile",
4758
+ "crypt",
4759
+ "csv",
4760
+ "ctypes",
4761
+ "curses",
4762
+ "dataclasses",
4763
+ "datetime",
4764
+ "dbm",
4765
+ "decimal",
4766
+ "difflib",
4767
+ "dis",
4768
+ "distutils",
4769
+ "doctest",
4770
+ "email",
4771
+ "encodings",
4772
+ "enum",
4773
+ "errno",
4774
+ "faulthandler",
4775
+ "fcntl",
4776
+ "filecmp",
4777
+ "fileinput",
4778
+ "fnmatch",
4779
+ "fractions",
4780
+ "ftplib",
4781
+ "functools",
4782
+ "gc",
4783
+ "getopt",
4784
+ "getpass",
4785
+ "gettext",
4786
+ "glob",
4787
+ "grp",
4788
+ "gzip",
4789
+ "hashlib",
4790
+ "heapq",
4791
+ "hmac",
4792
+ "html",
4793
+ "http",
4794
+ "idlelib",
4795
+ "imaplib",
4796
+ "imghdr",
4797
+ "imp",
4798
+ "importlib",
4799
+ "inspect",
4800
+ "io",
4801
+ "ipaddress",
4802
+ "itertools",
4803
+ "json",
4804
+ "keyword",
4805
+ "lib2to3",
4806
+ "linecache",
4807
+ "locale",
4808
+ "logging",
4809
+ "lzma",
4810
+ "mailbox",
4811
+ "mailcap",
4812
+ "marshal",
4813
+ "math",
4814
+ "mimetypes",
4815
+ "mmap",
4816
+ "modulefinder",
4817
+ "multiprocessing",
4818
+ "netrc",
4819
+ "nis",
4820
+ "nntplib",
4821
+ "numbers",
4822
+ "operator",
4823
+ "optparse",
4824
+ "os",
4825
+ "ossaudiodev",
4826
+ "pathlib",
4827
+ "pdb",
4828
+ "pickle",
4829
+ "pickletools",
4830
+ "pipes",
4831
+ "pkgutil",
4832
+ "platform",
4833
+ "plistlib",
4834
+ "poplib",
4835
+ "posix",
4836
+ "posixpath",
4837
+ "pprint",
4838
+ "profile",
4839
+ "pstats",
4840
+ "pty",
4841
+ "pwd",
4842
+ "py_compile",
4843
+ "pyclbr",
4844
+ "pydoc",
4845
+ "queue",
4846
+ "quopri",
4847
+ "random",
4848
+ "re",
4849
+ "readline",
4850
+ "reprlib",
4851
+ "resource",
4852
+ "rlcompleter",
4853
+ "runpy",
4854
+ "sched",
4855
+ "secrets",
4856
+ "select",
4857
+ "selectors",
4858
+ "shelve",
4859
+ "shlex",
4860
+ "shutil",
4861
+ "signal",
4862
+ "site",
4863
+ "smtpd",
4864
+ "smtplib",
4865
+ "sndhdr",
4866
+ "socket",
4867
+ "socketserver",
4868
+ "sqlite3",
4869
+ "sre_compile",
4870
+ "sre_constants",
4871
+ "sre_parse",
4872
+ "ssl",
4873
+ "stat",
4874
+ "statistics",
4875
+ "string",
4876
+ "stringprep",
4877
+ "struct",
4878
+ "subprocess",
4879
+ "sunau",
4880
+ "symtable",
4881
+ "sys",
4882
+ "sysconfig",
4883
+ "syslog",
4884
+ "tabnanny",
4885
+ "tarfile",
4886
+ "telnetlib",
4887
+ "tempfile",
4888
+ "termios",
4889
+ "test",
4890
+ "textwrap",
4891
+ "threading",
4892
+ "time",
4893
+ "timeit",
4894
+ "tkinter",
4895
+ "token",
4896
+ "tokenize",
4897
+ "tomllib",
4898
+ "trace",
4899
+ "traceback",
4900
+ "tracemalloc",
4901
+ "tty",
4902
+ "turtle",
4903
+ "turtledemo",
4904
+ "types",
4905
+ "typing",
4906
+ "unicodedata",
4907
+ "unittest",
4908
+ "urllib",
4909
+ "uu",
4910
+ "uuid",
4911
+ "venv",
4912
+ "warnings",
4913
+ "wave",
4914
+ "weakref",
4915
+ "webbrowser",
4916
+ "winreg",
4917
+ "winsound",
4918
+ "wsgiref",
4919
+ "xdrlib",
4920
+ "xml",
4921
+ "xmlrpc",
4922
+ "zipapp",
4923
+ "zipfile",
4924
+ "zipimport",
4925
+ "zlib",
4926
+ // Common aliases / submodules
4927
+ "os",
4928
+ "sys",
4929
+ "typing",
4930
+ "collections",
4931
+ "__future__"
4932
+ ]);
4933
+ const EXPERIENCE_FILE = "experience.jsonl";
4934
+ class ExperienceStore {
4935
+ constructor(projectPath) {
4936
+ this.records = null;
4937
+ const dir = path$1.join(projectPath, ".research-pilot", "compute-runs");
4938
+ this.filePath = path$1.join(dir, EXPERIENCE_FILE);
4939
+ }
4940
+ load() {
4941
+ if (this.records !== null) return this.records;
4942
+ this.records = [];
4943
+ if (!fs.existsSync(this.filePath)) return this.records;
4944
+ const content = fs.readFileSync(this.filePath, "utf-8");
4945
+ for (const line of content.split("\n")) {
4946
+ const trimmed = line.trim();
4947
+ if (!trimmed) continue;
4948
+ try {
4949
+ this.records.push(JSON.parse(trimmed));
4950
+ } catch {
4951
+ }
4952
+ }
4953
+ return this.records;
4954
+ }
4955
+ flush() {
4956
+ const dir = path$1.dirname(this.filePath);
4957
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4958
+ const records = this.load();
4959
+ const trimmed = records.slice(-200);
4960
+ const content = trimmed.map((r) => JSON.stringify(r)).join("\n") + "\n";
4961
+ const tmpPath = this.filePath + ".tmp." + process.pid;
4962
+ fs.writeFileSync(tmpPath, content, "utf-8");
4963
+ fs.renameSync(tmpPath, this.filePath);
4964
+ this.records = trimmed;
4965
+ }
4966
+ /**
4967
+ * Record a completed run's experience.
4968
+ */
4969
+ record(entry) {
4970
+ this.load();
4971
+ this.records.push(entry);
4972
+ this.flush();
4973
+ }
4974
+ /**
4975
+ * Find relevant past experience by taskKind.
4976
+ * Primary: exact match. Fallback: most recent regardless of kind.
4977
+ */
4978
+ findRelevant(taskKind, limit = 10) {
4979
+ const all = this.load();
4980
+ const exact = all.filter((r) => r.taskKind === taskKind);
4981
+ if (exact.length > 0) return exact.slice(-limit);
4982
+ return all.slice(-limit);
4983
+ }
4984
+ /**
4985
+ * Get all records (for UI display or export).
4986
+ */
4987
+ getAll() {
4988
+ return [...this.load()];
4989
+ }
4990
+ /**
4991
+ * Compute summary statistics for a taskKind.
4992
+ */
4993
+ summarize(taskKind) {
4994
+ const records = this.findRelevant(taskKind);
4995
+ if (records.length === 0) return void 0;
4996
+ const successes = records.filter((r) => r.outcome === "success");
4997
+ const failures = records.filter((r) => r.outcome === "failed");
4998
+ const durations = successes.map((r) => r.durationSeconds);
4999
+ const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : void 0;
5000
+ const failureCodes = {};
5001
+ for (const f of failures) {
5002
+ if (f.failureCode) {
5003
+ failureCodes[f.failureCode] = (failureCodes[f.failureCode] ?? 0) + 1;
5004
+ }
5005
+ }
5006
+ return {
5007
+ taskKind,
5008
+ totalRuns: records.length,
5009
+ successes: successes.length,
5010
+ failures: failures.length,
5011
+ avgDurationSeconds: avgDuration,
5012
+ commonFailures: failureCodes,
5013
+ lastRun: records[records.length - 1]
5014
+ };
5015
+ }
5016
+ }
5017
+ function inferTaskKind(command, scriptContent) {
5018
+ const combined = (command + " " + (scriptContent ?? "")).toLowerCase();
5019
+ let framework = "python";
5020
+ if (/\bimport\s+(?:torch|pytorch)\b|from\s+torch\b/.test(combined)) framework = "pytorch";
5021
+ else if (/\bimport\s+tensorflow\b|from\s+tensorflow\b|import\s+keras\b/.test(combined)) framework = "tensorflow";
5022
+ else if (/\bimport\s+mlx\b|from\s+mlx\b/.test(combined)) framework = "mlx";
5023
+ else if (/\bimport\s+sklearn\b|from\s+sklearn\b/.test(combined)) framework = "sklearn";
5024
+ else if (/\bimport\s+pandas\b|from\s+pandas\b/.test(combined)) framework = "pandas";
5025
+ else if (/\bimport\s+(?:matplotlib|seaborn|plotly)\b/.test(combined)) framework = "matplotlib";
5026
+ else if (/\bimport\s+(?:numpy|scipy)\b/.test(combined)) framework = "numpy";
5027
+ let action = "script";
5028
+ if (/\.fit\(|\.train\(|train|epoch/i.test(combined)) action = "training";
5029
+ else if (/\.predict\(|\.score\(|evaluat|test.*accuracy/i.test(combined)) action = "evaluation";
5030
+ else if (/preprocess|clean|transform|etl|\.to_csv\(|\.to_parquet\(/i.test(combined)) action = "etl";
5031
+ else if (/\.savefig\(|\.show\(|plot|chart|graph|viz/i.test(combined)) action = "viz";
5032
+ else if (/download|fetch|scrape|request/i.test(combined)) action = "download";
5033
+ else if (/statist|correlat|describe|summary/i.test(combined)) action = "analysis";
5034
+ return `${framework}-${action}`;
5035
+ }
5036
+ const POLL_INTERVAL_MS = 5e3;
5037
+ const OUTPUT_TAIL_BYTES = 8192;
5038
+ const STDERR_TAIL_BYTES = 4096;
5039
+ const MAX_TIMEOUT_MS = 24 * 60 * 6e4;
5040
+ const MAX_OUTPUT_BYTES = 1024 * 1024 * 1024;
5041
+ const DESTROY_KILL_TIMEOUT_MS = 5e3;
5042
+ function nextRunId() {
5043
+ return "cr-" + crypto$1.randomBytes(4).toString("hex");
5044
+ }
5045
+ function getPidStartTime(pid) {
5046
+ try {
5047
+ if (process.platform === "darwin") {
5048
+ const raw = execSync(`ps -p ${pid} -o lstart=`, { encoding: "utf-8", timeout: 3e3 }).trim();
5049
+ if (!raw) return void 0;
5050
+ return new Date(raw).getTime();
5051
+ } else {
5052
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
5053
+ const fields = stat.split(" ");
5054
+ const startTicks = parseInt(fields[21], 10);
5055
+ if (isNaN(startTicks)) return void 0;
5056
+ const uptime = parseFloat(fs.readFileSync("/proc/uptime", "utf-8").split(" ")[0]);
5057
+ const hz = 100;
5058
+ const bootTime = Date.now() - uptime * 1e3;
5059
+ return Math.round(bootTime + startTicks / hz * 1e3);
5060
+ }
5061
+ } catch {
5062
+ return void 0;
5063
+ }
5064
+ }
5065
+ function isPidAlive(pid) {
5066
+ try {
5067
+ process.kill(pid, 0);
5068
+ return true;
5069
+ } catch {
5070
+ return false;
5071
+ }
5072
+ }
5073
+ function isStaleRun(record) {
5074
+ if (!record.pid) return true;
5075
+ if (!isPidAlive(record.pid)) return true;
5076
+ if (record.pidStartTime) {
5077
+ const currentStartTime = getPidStartTime(record.pid);
5078
+ if (currentStartTime === void 0) return true;
5079
+ if (Math.abs(currentStartTime - record.pidStartTime) > 2e3) return true;
5080
+ }
5081
+ return false;
5082
+ }
5083
+ function takeSnapshot(store) {
5084
+ const activeRuns = store.getActiveRuns().map((r) => ({
5085
+ runId: r.runId,
5086
+ weight: r.weight
5087
+ }));
5088
+ let freeDiskMb = 1e4;
5089
+ try {
5090
+ const dfOutput = execSync("df -m .", { encoding: "utf-8", timeout: 3e3 });
5091
+ const lines = dfOutput.trim().split("\n");
5092
+ if (lines.length >= 2) {
5093
+ const parts = lines[1].split(/\s+/);
5094
+ const available = parseInt(parts[3], 10);
5095
+ if (!isNaN(available)) freeDiskMb = available;
5096
+ }
5097
+ } catch {
5098
+ }
5099
+ return {
5100
+ freeMemoryMb: Math.round(os.freemem() / (1024 * 1024)),
5101
+ cpuLoadPercent: Math.round(os.loadavg()[0] / os.cpus().length * 100),
5102
+ freeDiskMb,
5103
+ activeRuns
5104
+ };
5105
+ }
5106
+ function readFileTail(filePath, maxBytes) {
5107
+ try {
5108
+ const stat = fs.statSync(filePath);
5109
+ if (stat.size === 0) return "";
5110
+ const start = Math.max(0, stat.size - maxBytes);
5111
+ const fd = fs.openSync(filePath, "r");
5112
+ const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
5113
+ fs.readSync(fd, buf, 0, buf.length, start);
5114
+ fs.closeSync(fd);
5115
+ return buf.toString("utf-8");
5116
+ } catch {
5117
+ return "";
5118
+ }
5119
+ }
5120
+ function getFileSize(filePath) {
5121
+ try {
5122
+ return fs.statSync(filePath).size;
5123
+ } catch {
5124
+ return 0;
5125
+ }
5126
+ }
5127
+ function estimateLines(bytes, tail) {
5128
+ if (bytes === 0 || tail.length === 0) return 0;
5129
+ const tailLines = tail.split("\n").length;
5130
+ if (tail.length >= bytes) return tailLines;
5131
+ return Math.max(tailLines, Math.round(bytes / tail.length * tailLines));
5132
+ }
5133
+ class ComputeRunner {
5134
+ constructor(opts) {
5135
+ this.handles = /* @__PURE__ */ new Map();
5136
+ this.pollTimer = null;
5137
+ this.pollInFlight = /* @__PURE__ */ new Set();
5138
+ this.store = new RunStore(opts.projectPath);
5139
+ this.experience = new ExperienceStore(opts.projectPath);
5140
+ this.workspacePath = opts.workspacePath;
5141
+ this.projectPath = opts.projectPath;
5142
+ this.reconcileStaleRuns();
5143
+ const active = this.store.getActiveRuns();
5144
+ if (active.length > 0) this.ensurePolling();
5145
+ this.store.evictOld();
5146
+ }
5147
+ /**
5148
+ * On startup, detect and transition stale 'running'/'stalled' records whose
5149
+ * processes are gone (crashed app, killed process, PID reused).
5150
+ */
5151
+ reconcileStaleRuns() {
5152
+ const active = this.store.getActiveRuns();
5153
+ for (const run of active) {
5154
+ if (isStaleRun(run)) {
5155
+ this.store.updateRun(run.runId, {
5156
+ status: "failed",
5157
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5158
+ error: "Process no longer running (app crashed or process was killed).",
5159
+ stalled: false
5160
+ });
5161
+ }
5162
+ }
5163
+ }
5164
+ // -------------------------------------------------------------------------
5165
+ // Submit
5166
+ // -------------------------------------------------------------------------
5167
+ async submit(config) {
5168
+ const timeoutMs = Math.min(
5169
+ (config.timeoutMinutes ?? 60) * 6e4,
5170
+ MAX_TIMEOUT_MS
5171
+ );
5172
+ const stallThresholdMs = (config.stallThresholdMinutes ?? 5) * 6e4;
5173
+ const weight = classifyWeight(config.timeoutMinutes ?? 60, config.command);
5174
+ const snapshot = takeSnapshot(this.store);
5175
+ const decision = canAdmit(snapshot, weight);
5176
+ if (!decision.allowed) {
5177
+ throw new Error(`Scheduler: ${decision.reason}`);
5178
+ }
5179
+ const workDir = config.workDir ? path$1.resolve(this.workspacePath, config.workDir) : this.workspacePath;
5180
+ const preflight = await runPreflight({
5181
+ command: config.command,
5182
+ workDir
5183
+ });
5184
+ if (!preflight.passed) {
5185
+ throw new Error(`Preflight failed: ${preflight.blockingIssues.join("; ")}`);
5186
+ }
5187
+ const provider = await getProvider(config.sandbox);
5188
+ const runId = nextRunId();
5189
+ this.store.getRunDir(runId);
5190
+ const outputPath = this.store.getOutputPath(runId);
5191
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5192
+ const record = {
5193
+ runId,
5194
+ status: "running",
5195
+ weight,
5196
+ currentPhase: config.smokeCommand ? "smoke" : "full",
5197
+ command: config.command,
5198
+ smokeCommand: config.smokeCommand,
5199
+ workDir: config.workDir ?? ".",
5200
+ sandboxWorkDir: workDir,
5201
+ sandbox: provider.name,
5202
+ env: config.env,
5203
+ createdAt: now,
5204
+ startedAt: now,
5205
+ outputPath,
5206
+ outputBytes: 0,
5207
+ outputLines: 0,
5208
+ timeoutMs,
5209
+ stallThresholdMs,
5210
+ stalled: false,
5211
+ retryCount: config.parentRunId ? (this.store.getRun(config.parentRunId)?.retryCount ?? 0) + 1 : 0,
5212
+ parentRunId: config.parentRunId
5213
+ };
5214
+ this.store.createRun(record);
5215
+ const commandToRun = config.smokeCommand ?? config.command;
5216
+ try {
5217
+ const handle = await provider.spawn({
5218
+ command: commandToRun,
5219
+ workDir,
5220
+ outputPath,
5221
+ env: config.env
5222
+ });
5223
+ this.handles.set(runId, handle);
5224
+ this.ensurePolling();
5225
+ const pid = typeof handle.pid === "number" ? handle.pid : void 0;
5226
+ const pidStartTime = pid ? getPidStartTime(pid) : void 0;
5227
+ if (pid) {
5228
+ this.store.updateRun(runId, { pid, pidStartTime });
5229
+ }
5230
+ handle.wait().then(async (result) => {
5231
+ await this.handleExit(runId, result, config);
5232
+ }).catch(() => {
5233
+ });
5234
+ } catch (err) {
5235
+ this.store.updateRun(runId, {
5236
+ status: "failed",
5237
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5238
+ error: err instanceof Error ? err.message : String(err)
5239
+ });
5240
+ throw err;
5241
+ }
5242
+ return Object.assign(record, { preflight });
5243
+ }
5244
+ // -------------------------------------------------------------------------
5245
+ // Exit handler
5246
+ // -------------------------------------------------------------------------
5247
+ async handleExit(runId, result, config) {
5248
+ const run = this.store.getRun(runId);
5249
+ if (!run || isTerminal(run.status)) return;
5250
+ const stderrPath = this.store.getStderrPath(runId);
5251
+ const stderrTail = readFileTail(stderrPath, STDERR_TAIL_BYTES);
5252
+ if (run.currentPhase === "smoke" && result.exitCode === 0 && config.smokeCommand && config.command !== config.smokeCommand) {
5253
+ this.store.updateRun(runId, {
5254
+ currentPhase: "full",
5255
+ stderrTail
5256
+ });
5257
+ const provider = await getProvider(run.sandbox);
5258
+ try {
5259
+ const handle2 = await provider.spawn({
5260
+ command: config.command,
5261
+ workDir: run.sandboxWorkDir,
5262
+ outputPath: run.outputPath,
5263
+ env: config.env
5264
+ });
5265
+ this.handles.set(runId, handle2);
5266
+ handle2.wait().then(async (fullResult) => {
5267
+ await this.handleExit(runId, fullResult, { ...config, smokeCommand: void 0 });
5268
+ }).catch(() => {
5269
+ });
5270
+ return;
5271
+ } catch (err) {
5272
+ this.store.updateRun(runId, {
5273
+ status: "failed",
5274
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5275
+ error: err instanceof Error ? err.message : String(err)
5276
+ });
5277
+ return;
5278
+ }
5279
+ }
5280
+ const status = result.exitCode === 0 ? "completed" : "failed";
5281
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5282
+ const updated = this.store.updateRun(runId, {
5283
+ status,
5284
+ exitCode: result.exitCode,
5285
+ exitSignal: result.exitSignal,
5286
+ completedAt,
5287
+ stderrTail,
5288
+ stalled: false
5289
+ // Clear stall flag on terminal transition
5290
+ });
5291
+ if (updated) {
5292
+ try {
5293
+ const durationSeconds = updated.startedAt ? Math.round((Date.now() - new Date(updated.startedAt).getTime()) / 1e3) : 0;
5294
+ const taskKind = inferTaskKind(updated.command);
5295
+ const failure2 = status !== "completed" ? deriveFailure(updated) : void 0;
5296
+ this.experience.record({
5297
+ runId,
5298
+ taskKind,
5299
+ sandbox: updated.sandbox,
5300
+ outcome: status === "completed" ? "success" : "failed",
5301
+ failureCode: failure2?.code,
5302
+ durationSeconds,
5303
+ retryCount: updated.retryCount,
5304
+ timestamp: completedAt
5305
+ });
5306
+ } catch {
5307
+ }
5308
+ }
5309
+ const handle = this.handles.get(runId);
5310
+ if (handle) {
5311
+ await handle.cleanup().catch(() => {
5312
+ });
5313
+ this.handles.delete(runId);
5314
+ }
5315
+ this.stopPollingIfIdle();
5316
+ }
5317
+ // -------------------------------------------------------------------------
5318
+ // Stop (cancel)
5319
+ // -------------------------------------------------------------------------
5320
+ async stop(runId) {
5321
+ const run = this.store.getRun(runId);
5322
+ if (!run || isTerminal(run.status)) return;
5323
+ const handle = this.handles.get(runId);
5324
+ if (handle) {
5325
+ await handle.kill("SIGTERM");
5326
+ setTimeout(async () => {
5327
+ await handle.kill("SIGKILL").catch(() => {
5328
+ });
5329
+ }, 3e3);
5330
+ }
5331
+ const stderrPath = this.store.getStderrPath(runId);
5332
+ const stderrTail = readFileTail(stderrPath, STDERR_TAIL_BYTES);
5333
+ this.store.updateRun(runId, {
5334
+ status: "cancelled",
5335
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5336
+ stderrTail
5337
+ });
5338
+ this.handles.delete(runId);
5339
+ this.stopPollingIfIdle();
5340
+ }
5341
+ // -------------------------------------------------------------------------
5342
+ // Status queries
5343
+ // -------------------------------------------------------------------------
5344
+ getStatus(runId) {
5345
+ const run = this.store.getRun(runId);
5346
+ if (!run) return void 0;
5347
+ const outputTail = readFileTail(run.outputPath, OUTPUT_TAIL_BYTES);
5348
+ const structured = extractProgress(outputTail);
5349
+ const elapsed = run.startedAt ? (Date.now() - new Date(run.startedAt).getTime()) / 1e3 : 0;
5350
+ const failure2 = isTerminal(run.status) ? deriveFailure(run) : void 0;
5351
+ return {
5352
+ status: run.status,
5353
+ currentPhase: run.currentPhase,
5354
+ exitCode: run.exitCode,
5355
+ outputTail,
5356
+ outputBytes: run.outputBytes,
5357
+ outputLines: run.outputLines,
5358
+ elapsedSeconds: Math.round(elapsed),
5359
+ stalled: run.stalled,
5360
+ progress: structured,
5361
+ failure: failure2
5362
+ };
5363
+ }
5364
+ /**
5365
+ * Block until the run reaches a terminal state, stalls, or timeout.
5366
+ */
5367
+ async waitForCompletion(runId, timeoutMs) {
5368
+ const deadline = Date.now() + timeoutMs;
5369
+ while (Date.now() < deadline) {
5370
+ const status = this.getStatus(runId);
5371
+ if (!status) return void 0;
5372
+ if (isTerminal(status.status)) return status;
5373
+ if (status.stalled) return status;
5374
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(5e3, deadline - Date.now())));
5375
+ }
5376
+ return this.getStatus(runId);
5377
+ }
5378
+ // -------------------------------------------------------------------------
5379
+ // Polling — shared interval for all active runs
5380
+ // -------------------------------------------------------------------------
5381
+ ensurePolling() {
5382
+ if (this.pollTimer) return;
5383
+ this.pollTimer = setInterval(() => {
5384
+ this.pollAll();
5385
+ }, POLL_INTERVAL_MS);
5386
+ if (this.pollTimer.unref) this.pollTimer.unref();
5387
+ }
5388
+ stopPollingIfIdle() {
5389
+ const active = this.store.getActiveRuns();
5390
+ if (active.length === 0 && this.pollTimer) {
5391
+ clearInterval(this.pollTimer);
5392
+ this.pollTimer = null;
5393
+ }
5394
+ }
5395
+ pollAll() {
5396
+ const active = this.store.getActiveRuns();
5397
+ for (const record of active) {
5398
+ if (this.pollInFlight.has(record.runId)) continue;
5399
+ this.pollInFlight.add(record.runId);
5400
+ try {
5401
+ this.pollOnce(record.runId);
5402
+ } finally {
5403
+ this.pollInFlight.delete(record.runId);
5404
+ }
5405
+ }
5406
+ this.stopPollingIfIdle();
5407
+ }
5408
+ pollOnce(runId) {
5409
+ const run = this.store.getRun(runId);
5410
+ if (!run || isTerminal(run.status)) return;
5411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5412
+ const currentBytes = getFileSize(run.outputPath);
5413
+ const outputGrew = currentBytes > run.outputBytes;
5414
+ const tail = readFileTail(run.outputPath, OUTPUT_TAIL_BYTES);
5415
+ const lines = estimateLines(currentBytes, tail);
5416
+ const patch = {
5417
+ outputBytes: currentBytes,
5418
+ outputLines: lines
5419
+ };
5420
+ if (outputGrew) {
5421
+ patch.lastOutputAt = now;
5422
+ if (run.stalled) {
5423
+ patch.stalled = false;
5424
+ patch.status = "running";
5425
+ }
5426
+ }
5427
+ if (!outputGrew && run.lastOutputAt) {
5428
+ const silentMs = Date.now() - new Date(run.lastOutputAt).getTime();
5429
+ if (silentMs > run.stallThresholdMs && !run.stalled) {
5430
+ patch.stalled = true;
5431
+ patch.status = "stalled";
5432
+ }
5433
+ }
5434
+ if (currentBytes > MAX_OUTPUT_BYTES) {
5435
+ const handle = this.handles.get(run.runId);
5436
+ if (handle) {
5437
+ handle.kill("SIGTERM").catch(() => {
5438
+ });
5439
+ setTimeout(() => {
5440
+ handle.kill("SIGKILL").catch(() => {
5441
+ });
5442
+ }, 3e3);
5443
+ this.handles.delete(run.runId);
5444
+ }
5445
+ const stderrTail = readFileTail(this.store.getStderrPath(run.runId), STDERR_TAIL_BYTES);
5446
+ this.store.updateRun(run.runId, {
5447
+ ...patch,
5448
+ status: "failed",
5449
+ completedAt: now,
5450
+ stalled: false,
5451
+ stderrTail,
5452
+ error: `Output exceeded ${Math.round(MAX_OUTPUT_BYTES / (1024 * 1024))}MB limit. Process killed.`
5453
+ });
5454
+ return;
5455
+ }
5456
+ if (run.startedAt) {
5457
+ const elapsedMs = Date.now() - new Date(run.startedAt).getTime();
5458
+ if (elapsedMs > run.timeoutMs) {
5459
+ const handle = this.handles.get(run.runId);
5460
+ if (handle) {
5461
+ handle.kill("SIGTERM").catch(() => {
5462
+ });
5463
+ setTimeout(() => {
5464
+ handle.kill("SIGKILL").catch(() => {
5465
+ });
5466
+ }, 3e3);
5467
+ this.handles.delete(run.runId);
5468
+ }
5469
+ const stderrTail = readFileTail(this.store.getStderrPath(run.runId), STDERR_TAIL_BYTES);
5470
+ this.store.updateRun(run.runId, {
5471
+ ...patch,
5472
+ status: "timed_out",
5473
+ completedAt: now,
5474
+ stalled: false,
5475
+ // Clear stall flag on terminal transition
5476
+ stderrTail
5477
+ });
5478
+ return;
5479
+ }
5480
+ }
5481
+ this.store.updateRun(run.runId, patch);
5482
+ }
5483
+ // -------------------------------------------------------------------------
5484
+ // Lifecycle
5485
+ // -------------------------------------------------------------------------
5486
+ /**
5487
+ * Stop all active runs and clean up. Called on coordinator destroy / app quit.
5488
+ * Sends SIGTERM, waits up to 5s, then SIGKILL any survivors.
5489
+ */
5490
+ async destroy() {
5491
+ if (this.pollTimer) {
5492
+ clearInterval(this.pollTimer);
5493
+ this.pollTimer = null;
5494
+ }
5495
+ this.store.stopFlushTimer();
5496
+ this.store.flushNow();
5497
+ if (this.handles.size === 0) return;
5498
+ const entries = [...this.handles.entries()];
5499
+ for (const [, handle] of entries) {
5500
+ await handle.kill("SIGTERM").catch(() => {
5501
+ });
5502
+ }
5503
+ const deadline = Date.now() + DESTROY_KILL_TIMEOUT_MS;
5504
+ const pending = new Map(entries);
5505
+ while (pending.size > 0 && Date.now() < deadline) {
5506
+ for (const [runId, handle] of pending) {
5507
+ const record = this.store.getRun(runId);
5508
+ if (!record?.pid || !isPidAlive(record.pid)) {
5509
+ pending.delete(runId);
5510
+ }
5511
+ }
5512
+ if (pending.size > 0) {
5513
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
5514
+ }
5515
+ }
5516
+ for (const [, handle] of pending) {
5517
+ await handle.kill("SIGKILL").catch(() => {
5518
+ });
5519
+ }
5520
+ for (const [runId, handle] of entries) {
5521
+ await handle.cleanup().catch(() => {
5522
+ });
5523
+ this.store.updateRun(runId, {
5524
+ status: "cancelled",
5525
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5526
+ stalled: false
5527
+ });
5528
+ }
5529
+ this.handles.clear();
5530
+ this.store.flushNow();
5531
+ }
5532
+ /**
5533
+ * Get the RunStore for direct queries (used by tools).
5534
+ */
5535
+ getStore() {
5536
+ return this.store;
5537
+ }
5538
+ /**
5539
+ * Get the ExperienceStore for queries.
5540
+ */
5541
+ getExperience() {
5542
+ return this.experience;
5543
+ }
5544
+ }
5545
+ const PROFILER_SYSTEM_PROMPT = `You are a compute task profiler. Analyze the given script and command, then output a JSON object with these fields:
5546
+
5547
+ {
5548
+ "cpuDensity": "low" | "medium" | "high",
5549
+ "gpuDensity": "none" | "light" | "heavy",
5550
+ "memoryPattern": "constant" | "growing" | "spike",
5551
+ "ioPattern": "read_heavy" | "write_heavy" | "balanced" | "minimal",
5552
+ "chunkable": boolean, // Can be split into smaller data pieces
5553
+ "resumable": boolean, // Produces checkpoints, can restart
5554
+ "idempotent": boolean, // Safe to re-run without side effects
5555
+ "hasExternalSideEffects": boolean, // API calls, DB writes, emails
5556
+ "networkRequired": boolean,
5557
+ "smokeSupported": boolean, // Has --smoke or similar quick-validation flag
5558
+ "expectedDurationClass": "seconds" | "minutes" | "hours",
5559
+ "confidence": "high" | "medium" | "low",
5560
+ "reasoning": "Brief explanation of your analysis"
5561
+ }
5562
+
5563
+ Guidelines:
5564
+ - cpuDensity: "high" for training loops, simulations, heavy computation
5565
+ - gpuDensity: "heavy" if torch/tensorflow/mlx with .fit() or training loops; "light" for inference; "none" for CPU-only
5566
+ - memoryPattern: "spike" if large dataset loaded at once; "growing" if accumulating; "constant" if streaming/fixed
5567
+ - ioPattern: "read_heavy" if large data loads; "write_heavy" if many output files; "balanced" if both
5568
+ - chunkable: true if data can be split (pandas, numpy batch processing)
5569
+ - resumable: true if checkpointing detected (torch.save, callbacks)
5570
+ - smokeSupported: true ONLY if --smoke, --dry-run, or --validate flag is in argparse/click
5571
+ - Be conservative with confidence: "high" only if the pattern is unmistakable
5572
+
5573
+ Output ONLY the JSON object, no explanation outside it.`;
5574
+ async function profileTask(command, scriptContent, callLlm) {
5575
+ if (!callLlm || !scriptContent) return defaultProfile(command);
5576
+ try {
5577
+ const response = await callLlm(
5578
+ PROFILER_SYSTEM_PROMPT,
5579
+ `Command: ${command}
5580
+
5581
+ Script content:
5582
+ ${scriptContent.slice(0, 8e3)}`
5583
+ );
5584
+ const parsed = parseJsonFromLlm(response);
5585
+ if (!parsed) return defaultProfile(command);
5586
+ return mergeWithDefaults$1(parsed, command);
5587
+ } catch {
5588
+ return defaultProfile(command);
5589
+ }
5590
+ }
5591
+ function defaultProfile(command) {
5592
+ const cmd = command.toLowerCase();
5593
+ const isTrain = /train|fit|epoch/i.test(cmd);
5594
+ const isViz = /plot|chart|viz|figure|graph/i.test(cmd);
5595
+ return {
5596
+ cpuDensity: isTrain ? "high" : isViz ? "low" : "medium",
5597
+ gpuDensity: "none",
5598
+ memoryPattern: "constant",
5599
+ ioPattern: "balanced",
5600
+ chunkable: false,
5601
+ resumable: false,
5602
+ idempotent: true,
5603
+ hasExternalSideEffects: false,
5604
+ networkRequired: false,
5605
+ smokeSupported: false,
5606
+ expectedDurationClass: isViz ? "seconds" : isTrain ? "hours" : "minutes",
5607
+ confidence: "low",
5608
+ reasoning: "Default profile (LLM unavailable or no script content)."
5609
+ };
5610
+ }
5611
+ function mergeWithDefaults$1(partial, command) {
5612
+ const defaults = defaultProfile(command);
5613
+ return {
5614
+ cpuDensity: partial.cpuDensity ?? defaults.cpuDensity,
5615
+ gpuDensity: partial.gpuDensity ?? defaults.gpuDensity,
5616
+ memoryPattern: partial.memoryPattern ?? defaults.memoryPattern,
5617
+ ioPattern: partial.ioPattern ?? defaults.ioPattern,
5618
+ chunkable: partial.chunkable ?? defaults.chunkable,
5619
+ resumable: partial.resumable ?? defaults.resumable,
5620
+ idempotent: partial.idempotent ?? defaults.idempotent,
5621
+ hasExternalSideEffects: partial.hasExternalSideEffects ?? defaults.hasExternalSideEffects,
5622
+ networkRequired: partial.networkRequired ?? defaults.networkRequired,
5623
+ smokeSupported: partial.smokeSupported ?? defaults.smokeSupported,
5624
+ expectedDurationClass: partial.expectedDurationClass ?? defaults.expectedDurationClass,
5625
+ confidence: partial.confidence ?? defaults.confidence,
5626
+ reasoning: partial.reasoning ?? defaults.reasoning
5627
+ };
5628
+ }
5629
+ function parseJsonFromLlm(response) {
5630
+ let text = response.replace(/```json\s*/g, "").replace(/```\s*/g, "").trim();
5631
+ const start = text.indexOf("{");
5632
+ const end = text.lastIndexOf("}");
5633
+ if (start === -1 || end === -1 || end <= start) return null;
5634
+ try {
5635
+ return JSON.parse(text.slice(start, end + 1));
5636
+ } catch {
5637
+ return null;
5638
+ }
5639
+ }
5640
+ const execFileAsync = promisify(execFile);
5641
+ async function probePython() {
5642
+ let version = "unknown";
5643
+ try {
5644
+ const { stdout } = await execFileAsync("python3", ["--version"], { timeout: 5e3 });
5645
+ version = stdout.trim().replace("Python ", "");
5646
+ } catch {
5647
+ }
5648
+ let packages = [];
5649
+ try {
5650
+ const { stdout } = await execFileAsync("python3", ["-m", "pip", "list", "--format=json"], { timeout: 15e3 });
5651
+ const parsed = JSON.parse(stdout);
5652
+ packages = parsed.map((p) => p.name.toLowerCase());
5653
+ } catch {
5654
+ try {
5655
+ const { stdout } = await execFileAsync("pip3", ["list", "--format=json"], { timeout: 15e3 });
5656
+ const parsed = JSON.parse(stdout);
5657
+ packages = parsed.map((p) => p.name.toLowerCase());
5658
+ } catch {
5659
+ }
5660
+ }
5661
+ return { version, packages };
5662
+ }
5663
+ async function probeGpu(pipPackages) {
5664
+ const platform = os.platform();
5665
+ const arch = os.arch();
5666
+ if (platform === "darwin" && arch === "arm64") {
5667
+ let mlxAvailable = false;
5668
+ const mlxPackages = [];
5669
+ const mlxRelated = ["mlx", "mlx-nn", "mlx-data", "mlx-lm", "mlx-optimizers", "mlx-audio", "mlx-vlm"];
5670
+ for (const pkg of mlxRelated) {
5671
+ if (pipPackages.includes(pkg)) {
5672
+ mlxPackages.push(pkg);
5673
+ }
5674
+ }
5675
+ if (pipPackages.includes("mlx")) {
5676
+ try {
5677
+ await execFileAsync("python3", ["-c", "import mlx; print(mlx.__version__)"], { timeout: 1e4 });
5678
+ mlxAvailable = true;
5679
+ } catch {
5680
+ }
5681
+ }
5682
+ let model = "Apple Silicon";
5683
+ try {
5684
+ const output = execSync("sysctl -n machdep.cpu.brand_string", { encoding: "utf-8", timeout: 3e3 }).trim();
5685
+ if (output) model = output;
5686
+ } catch {
5687
+ }
5688
+ return {
5689
+ type: "apple_silicon",
5690
+ model,
5691
+ memoryMb: Math.round(os.totalmem() / (1024 * 1024)),
5692
+ // Unified memory
5693
+ mlxAvailable,
5694
+ mlxPackages,
5695
+ cudaAvailable: false,
5696
+ metalAvailable: true
5697
+ };
5698
+ }
5699
+ try {
5700
+ const { stdout } = await execFileAsync("nvidia-smi", [
5701
+ "--query-gpu=name,memory.total",
5702
+ "--format=csv,noheader,nounits"
5703
+ ], { timeout: 5e3 });
5704
+ const parts = stdout.trim().split(",").map((s) => s.trim());
5705
+ return {
5706
+ type: "nvidia",
5707
+ model: parts[0] ?? "NVIDIA GPU",
5708
+ memoryMb: parseInt(parts[1] ?? "0", 10) || void 0,
5709
+ mlxAvailable: false,
5710
+ mlxPackages: [],
5711
+ cudaAvailable: true,
5712
+ metalAvailable: false
5713
+ };
5714
+ } catch {
5715
+ }
5716
+ return {
5717
+ type: "none",
5718
+ model: "No GPU detected",
5719
+ mlxAvailable: false,
5720
+ mlxPackages: [],
5721
+ cudaAvailable: false,
5722
+ metalAvailable: false
5723
+ };
5724
+ }
5725
+ async function probeDocker() {
5726
+ try {
5727
+ await execFileAsync("docker", ["info"], { timeout: 5e3 });
5728
+ return true;
5729
+ } catch {
5730
+ return false;
5731
+ }
5732
+ }
5733
+ let cachedProfile = null;
5734
+ async function probeStaticProfile() {
5735
+ if (cachedProfile) return cachedProfile;
5736
+ const [python, docker] = await Promise.all([
5737
+ probePython(),
5738
+ probeDocker()
5739
+ ]);
5740
+ const gpu = await probeGpu(python.packages);
5741
+ const platform = os.platform();
5742
+ cachedProfile = {
5743
+ os: platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : "other",
5744
+ arch: os.arch(),
5745
+ cpuCores: os.cpus().length,
5746
+ cpuModel: os.cpus()[0]?.model ?? "Unknown CPU",
5747
+ totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
5748
+ gpu,
5749
+ pythonVersion: python.version,
5750
+ pipPackages: python.packages,
5751
+ dockerAvailable: docker
5752
+ };
5753
+ return cachedProfile;
5754
+ }
5755
+ function generateAgentGuidance(profile) {
5756
+ const lines = [];
5757
+ lines.push("## Local Compute Environment");
5758
+ lines.push("");
5759
+ lines.push(`Machine: ${profile.os} ${profile.arch}, ${profile.cpuCores} CPU cores, ${profile.totalMemoryMb}MB RAM`);
5760
+ lines.push(`Python: ${profile.pythonVersion}`);
5761
+ if (profile.gpu.type === "apple_silicon") {
5762
+ lines.push(`GPU: ${profile.gpu.model} (${profile.gpu.memoryMb}MB unified memory, Metal available)`);
5763
+ if (profile.gpu.mlxAvailable) {
5764
+ lines.push("");
5765
+ lines.push("### MLX Acceleration Available");
5766
+ lines.push(`Installed MLX packages: ${profile.gpu.mlxPackages.join(", ")}`);
5767
+ lines.push("When writing ML training code for local_compute_execute:");
5768
+ lines.push("- Prefer mlx over PyTorch/TensorFlow for training — native Metal acceleration, no CUDA needed");
5769
+ lines.push("- Use mlx.core.array for GPU-accelerated array operations");
5770
+ lines.push("- Check mlx-community on HuggingFace for MLX-converted model weights");
5771
+ lines.push("- mlx uses lazy evaluation — call mx.eval() to force computation");
5772
+ lines.push("- mlx supports 4-bit quantization via mlx-lm for large models");
5773
+ } else {
5774
+ lines.push("");
5775
+ lines.push("MLX is NOT installed. For Apple Silicon ML acceleration, install: pip install mlx mlx-nn");
5776
+ lines.push("Without MLX, PyTorch MPS backend is available for GPU-accelerated training on Apple Silicon.");
5777
+ }
5778
+ } else if (profile.gpu.type === "nvidia") {
5779
+ lines.push(`GPU: ${profile.gpu.model}${profile.gpu.memoryMb ? ` (${profile.gpu.memoryMb}MB VRAM)` : ""}, CUDA available`);
5780
+ lines.push("PyTorch/TensorFlow can use CUDA acceleration.");
5781
+ } else {
5782
+ lines.push("GPU: None detected. ML training will use CPU only.");
5783
+ lines.push("Consider using smaller models/datasets, or running on a machine with a GPU.");
5784
+ }
5785
+ if (profile.dockerAvailable) {
5786
+ lines.push("");
5787
+ lines.push("Docker: Available. Can use docker sandbox for stronger isolation.");
5788
+ }
5789
+ lines.push("");
5790
+ lines.push("### Sandbox Guidelines");
5791
+ lines.push("When writing scripts for local_compute_execute:");
5792
+ lines.push("- All required packages must be importable (preflight checks imports before running)");
5793
+ lines.push("- For long-running tasks, consider adding a --smoke flag for quick validation");
5794
+ lines.push('- Print progress lines: ##PROGRESS## {"step": N, "total": M, "loss": 0.85, "phase": "training"}');
5795
+ lines.push("- Write output files to the working directory (results, figures, checkpoints)");
5796
+ return lines.join("\n");
5797
+ }
5798
+ const RISK_ASSESSMENT_PROMPT = `You are a compute execution risk assessor. Given the task profile, system environment, and past experience, assess risks and recommend execution parameters.
5799
+
5800
+ Output a JSON object:
5801
+ {
5802
+ "feasible": boolean,
5803
+ "risks": [
5804
+ { "severity": "low"|"medium"|"high"|"blocking", "category": "memory"|"disk"|"gpu"|"dependency"|"network"|"timeout", "message": "...", "mitigation": "..." }
5805
+ ],
5806
+ "recommendedSandbox": "docker" | "process",
5807
+ "recommendedTimeoutMinutes": number,
5808
+ "recommendedStallThresholdMinutes": number,
5809
+ "warnings": ["..."],
5810
+ "agentGuidance": ["tips for the coding agent..."]
5811
+ }
5812
+
5813
+ Guidelines:
5814
+ - Mark "blocking" severity only for showstopper issues (no GPU when GPU required, <100MB disk)
5815
+ - recommendedTimeoutMinutes: use experience average × 2 if available, else estimate from task type
5816
+ - recommendedSandbox: prefer "process" on macOS (Docker lacks GPU passthrough), "docker" on Linux with NVIDIA
5817
+ - For Apple Silicon with MLX: recommend process sandbox for Metal GPU access
5818
+ - Be actionable in mitigations and guidance
5819
+
5820
+ Output ONLY the JSON object.`;
5821
+ async function assessRisk(opts) {
5822
+ const { taskProfile, env, snapshot, experience, command, callLlm } = opts;
5823
+ if (!callLlm) return defaultAdvice(env, snapshot);
5824
+ try {
5825
+ const facts = formatFacts(taskProfile, env, snapshot, experience, command);
5826
+ const response = await callLlm(RISK_ASSESSMENT_PROMPT, facts);
5827
+ const parsed = parseJsonFromLlm(response);
5828
+ if (!parsed) return defaultAdvice(env, snapshot);
5829
+ return mergeWithDefaults(parsed, env, snapshot);
5830
+ } catch {
5831
+ return defaultAdvice(env, snapshot);
5832
+ }
5833
+ }
5834
+ function formatFacts(taskProfile, env, snapshot, experience, command) {
5835
+ const sections = [];
5836
+ if (taskProfile) {
5837
+ sections.push(`## Task Profile
5838
+ ${JSON.stringify(taskProfile, null, 2)}`);
5839
+ }
5840
+ sections.push(`## Command
5841
+ ${command}`);
5842
+ sections.push(`## Environment
5843
+ OS: ${env.os} ${env.arch}
5844
+ CPU: ${env.cpuCores} cores (${env.cpuModel})
5845
+ RAM: ${env.totalMemoryMb}MB total, ~${snapshot.freeMemoryMb}MB free
5846
+ GPU: ${env.gpu.type === "none" ? "None" : `${env.gpu.model} (${env.gpu.type})`}
5847
+ MLX: ${env.gpu.mlxAvailable ? `Yes (${env.gpu.mlxPackages.join(", ")})` : "No"}
5848
+ CUDA: ${env.gpu.cudaAvailable ? "Yes" : "No"}
5849
+ Python: ${env.pythonVersion}
5850
+ Docker: ${env.dockerAvailable ? "Yes" : "No"}
5851
+ Disk free: ~${snapshot.freeDiskMb}MB
5852
+ CPU load: ${snapshot.cpuLoadPercent}%
5853
+ Active runs: ${snapshot.activeRuns.length}`);
5854
+ if (experience) {
5855
+ sections.push(`## Past Experience (${experience.taskKind})
5856
+ Total runs: ${experience.totalRuns}
5857
+ Successes: ${experience.successes}, Failures: ${experience.failures}
5858
+ ${experience.avgDurationSeconds ? `Avg duration: ${Math.round(experience.avgDurationSeconds / 60)} min` : ""}
5859
+ ${Object.keys(experience.commonFailures).length > 0 ? `Common failures: ${JSON.stringify(experience.commonFailures)}` : ""}`);
5860
+ }
5861
+ return sections.join("\n\n");
5862
+ }
5863
+ function defaultAdvice(env, snapshot) {
5864
+ const risks = [];
5865
+ const warnings = [];
5866
+ if (snapshot.freeMemoryMb < 1e3) {
5867
+ risks.push({
5868
+ severity: snapshot.freeMemoryMb < 500 ? "high" : "medium",
5869
+ category: "memory",
5870
+ message: `Only ${snapshot.freeMemoryMb}MB free memory.`,
5871
+ mitigation: "Close memory-intensive applications."
5872
+ });
5873
+ }
5874
+ if (snapshot.freeDiskMb < 2e3) {
5875
+ risks.push({
5876
+ severity: snapshot.freeDiskMb < 500 ? "blocking" : "medium",
5877
+ category: "disk",
5878
+ message: `Only ${snapshot.freeDiskMb}MB free disk space.`
5879
+ });
5880
+ }
5881
+ if (snapshot.cpuLoadPercent > 80) {
5882
+ warnings.push(`High CPU load (${snapshot.cpuLoadPercent}%). Run may be slower than expected.`);
5883
+ }
5884
+ return {
5885
+ feasible: !risks.some((r) => r.severity === "blocking"),
5886
+ risks,
5887
+ recommendedSandbox: "process",
5888
+ recommendedTimeoutMinutes: 60,
5889
+ recommendedStallThresholdMinutes: 5,
5890
+ warnings,
5891
+ agentGuidance: []
5892
+ };
5893
+ }
5894
+ function mergeWithDefaults(partial, env, snapshot) {
5895
+ const defaults = defaultAdvice(env, snapshot);
5896
+ return {
5897
+ feasible: partial.feasible ?? defaults.feasible,
5898
+ risks: partial.risks ?? defaults.risks,
5899
+ recommendedSandbox: partial.recommendedSandbox ?? defaults.recommendedSandbox,
5900
+ recommendedTimeoutMinutes: partial.recommendedTimeoutMinutes ?? defaults.recommendedTimeoutMinutes,
5901
+ recommendedStallThresholdMinutes: partial.recommendedStallThresholdMinutes ?? defaults.recommendedStallThresholdMinutes,
5902
+ warnings: partial.warnings ?? defaults.warnings,
5903
+ agentGuidance: partial.agentGuidance ?? defaults.agentGuidance
5904
+ };
5905
+ }
5906
+ function createLocalComputeTools(ctx) {
5907
+ const runner = new ComputeRunner({
5908
+ projectPath: ctx.projectPath,
5909
+ workspacePath: ctx.workspacePath
5910
+ });
5911
+ const tools = [
5912
+ createPlanTool(runner, ctx),
5913
+ createExecuteTool(runner, ctx),
5914
+ createWaitTool(runner),
5915
+ createStatusTool(runner),
5916
+ createStopTool(runner)
5917
+ ];
5918
+ return {
5919
+ tools,
5920
+ destroy: () => runner.destroy()
5921
+ };
5922
+ }
5923
+ function createPlanTool(runner, ctx) {
5924
+ return {
5925
+ name: "local_compute_plan",
5926
+ label: "Local Compute: Plan",
5927
+ description: "Analyze a script before execution: profile the task, assess risks, and get recommendations.\nOptional — you can skip this and call local_compute_execute directly for simple tasks.\nUse this for complex or risky tasks (large datasets, GPU training, unfamiliar code).",
5928
+ parameters: Type.Object({
5929
+ command: Type.String({ description: "Shell command to execute" }),
5930
+ script_path: Type.Optional(Type.String({ description: "Relative path to the main script (for deeper analysis)" })),
5931
+ sandbox: Type.Optional(Type.String({ description: '"docker" | "process" | "auto"' })),
5932
+ timeout_minutes: Type.Optional(Type.Number({ description: "Suggested timeout" })),
5933
+ smoke_command: Type.Optional(Type.String({ description: 'Quick validation command (e.g., "python3 script.py --smoke")' }))
5934
+ }),
5935
+ execute: async (_toolCallId, rawParams) => {
5936
+ const params = rawParams;
5937
+ const command = typeof params.command === "string" ? params.command.trim() : "";
5938
+ if (!command) {
5939
+ return toAgentResult("local_compute_plan", toolError("MISSING_PARAMETER", "command is required."));
5940
+ }
5941
+ let scriptContent;
5942
+ if (typeof params.script_path === "string") {
5943
+ const scriptPath = path$1.isAbsolute(params.script_path) ? params.script_path : path$1.resolve(ctx.workspacePath, params.script_path);
5944
+ try {
5945
+ scriptContent = fs.readFileSync(scriptPath, "utf-8");
5946
+ } catch {
5947
+ }
5948
+ }
5949
+ const taskProfile = await profileTask(command, scriptContent, ctx.callLlm);
5950
+ let env;
5951
+ try {
5952
+ env = await probeStaticProfile();
5953
+ } catch {
5954
+ return toAgentResult("local_compute_plan", toolError("EXECUTION_FAILED", "Failed to probe system environment."));
5955
+ }
5956
+ let freeDiskMb = 1e4;
5957
+ try {
5958
+ const dfOut = execSync("df -m .", { cwd: ctx.workspacePath, encoding: "utf-8", timeout: 3e3 });
5959
+ const parts = dfOut.trim().split("\n")[1]?.split(/\s+/);
5960
+ const avail = parseInt(parts?.[3] ?? "", 10);
5961
+ if (!isNaN(avail)) freeDiskMb = avail;
5962
+ } catch {
5963
+ }
5964
+ const snapshot = {
5965
+ freeMemoryMb: Math.round(os.freemem() / (1024 * 1024)),
5966
+ cpuLoadPercent: Math.round(os.loadavg()[0] / os.cpus().length * 100),
5967
+ freeDiskMb,
5968
+ activeRuns: runner.getStore().getActiveRuns().map((r) => ({ runId: r.runId, weight: r.weight }))
5969
+ };
5970
+ const taskKind = inferTaskKind(command, scriptContent);
5971
+ const experience = runner.getExperience().summarize(taskKind);
5972
+ const riskAdvice = await assessRisk({
5973
+ taskProfile,
5974
+ env,
5975
+ snapshot,
5976
+ experience,
5977
+ command,
5978
+ callLlm: ctx.callLlm
5979
+ });
5980
+ return toAgentResult("local_compute_plan", {
5981
+ success: true,
5982
+ data: {
5983
+ task_profile: taskProfile,
5984
+ risk_assessment: {
5985
+ feasible: riskAdvice.feasible,
5986
+ risks: riskAdvice.risks,
5987
+ warnings: riskAdvice.warnings
5988
+ },
5989
+ recommendations: {
5990
+ sandbox: riskAdvice.recommendedSandbox,
5991
+ timeout_minutes: riskAdvice.recommendedTimeoutMinutes,
5992
+ stall_threshold_minutes: riskAdvice.recommendedStallThresholdMinutes,
5993
+ agent_guidance: riskAdvice.agentGuidance
5994
+ },
5995
+ experience_summary: experience ? {
5996
+ task_kind: experience.taskKind,
5997
+ total_runs: experience.totalRuns,
5998
+ successes: experience.successes,
5999
+ failures: experience.failures,
6000
+ avg_duration_seconds: experience.avgDurationSeconds,
6001
+ common_failures: experience.commonFailures
6002
+ } : null
6003
+ }
6004
+ });
6005
+ }
6006
+ };
6007
+ }
6008
+ function createExecuteTool(runner, ctx) {
6009
+ return {
6010
+ name: "local_compute_execute",
6011
+ label: "Local Compute: Execute",
6012
+ description: 'Execute a command in a sandboxed local environment. Use this for long-running tasks like ML training, data preprocessing, or heavy analysis that may take minutes to hours.\nThe command runs asynchronously. Use local_compute_wait or local_compute_status to monitor.\nFor long-running scripts, consider adding a --smoke flag for quick validation.\n\nProgress monitoring: print lines starting with ##PROGRESS## followed by JSON for structured progress:\n ##PROGRESS## {"step": 3, "total": 10, "loss": 0.85, "phase": "training"}',
6013
+ parameters: Type.Object({
6014
+ command: Type.String({ description: 'Shell command to execute (e.g., "python3 train.py")' }),
6015
+ work_dir: Type.Optional(Type.String({ description: "Relative path within workspace (default: workspace root)" })),
6016
+ sandbox: Type.Optional(Type.String({ description: '"docker" | "process" | "auto" (default: auto)' })),
6017
+ timeout_minutes: Type.Optional(Type.Number({ description: "Max runtime in minutes (default: 60, max: 1440)" })),
6018
+ stall_threshold_minutes: Type.Optional(Type.Number({ description: "Minutes without output before flagging stall (default: 5)" })),
6019
+ env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Extra environment variables" })),
6020
+ smoke_command: Type.Optional(Type.String({ description: 'Quick validation command (e.g., "python3 train.py --smoke"). Runs before full command.' })),
6021
+ parent_run_id: Type.Optional(Type.String({ description: "Previous failed run ID (for retry lineage tracking)" }))
6022
+ }),
6023
+ execute: async (_toolCallId, rawParams) => {
6024
+ const params = rawParams;
6025
+ const command = typeof params.command === "string" ? params.command.trim() : "";
6026
+ if (!command) {
6027
+ return toAgentResult("local_compute_execute", toolError("MISSING_PARAMETER", "command is required.", {
6028
+ suggestions: ['Provide a shell command to execute (e.g., "python3 train.py").']
6029
+ }));
6030
+ }
6031
+ const config = {
6032
+ command,
6033
+ workDir: typeof params.work_dir === "string" ? params.work_dir : void 0,
6034
+ sandbox: typeof params.sandbox === "string" ? params.sandbox : "auto",
6035
+ timeoutMinutes: typeof params.timeout_minutes === "number" ? params.timeout_minutes : 60,
6036
+ stallThresholdMinutes: typeof params.stall_threshold_minutes === "number" ? params.stall_threshold_minutes : 5,
6037
+ env: typeof params.env === "object" && params.env !== null ? params.env : void 0,
6038
+ smokeCommand: typeof params.smoke_command === "string" ? params.smoke_command : void 0,
6039
+ parentRunId: typeof params.parent_run_id === "string" ? params.parent_run_id : void 0
6040
+ };
6041
+ try {
6042
+ const run = await runner.submit(config);
6043
+ ctx.onToolCall?.("local_compute_execute", { command, runId: run.runId });
6044
+ return toAgentResult("local_compute_execute", {
6045
+ success: true,
6046
+ data: {
6047
+ run_id: run.runId,
6048
+ sandbox: run.sandbox,
6049
+ status: run.status,
6050
+ current_phase: run.currentPhase,
6051
+ output_path: run.outputPath,
6052
+ weight: run.weight
6053
+ }
6054
+ });
6055
+ } catch (err) {
6056
+ const msg = err instanceof Error ? err.message : String(err);
6057
+ if (msg.startsWith("Scheduler:")) {
6058
+ return toAgentResult("local_compute_execute", toolError("EXECUTION_FAILED", msg, {
6059
+ retryable: true,
6060
+ suggestions: ["Wait for the active run to finish, or stop it with local_compute_stop."]
6061
+ }));
6062
+ }
6063
+ return toAgentResult("local_compute_execute", toolError("EXECUTION_FAILED", msg, {
6064
+ retryable: false,
6065
+ suggestions: ["Check the command and try again."]
6066
+ }));
6067
+ }
6068
+ }
6069
+ };
6070
+ }
6071
+ function createWaitTool(runner) {
6072
+ return {
6073
+ name: "local_compute_wait",
6074
+ label: "Local Compute: Wait",
6075
+ description: "Wait for a compute run to complete. Blocks until the run finishes, stalls, or the wait timeout elapses.\nReturns immediately if the run has already completed or stalled.",
6076
+ parameters: Type.Object({
6077
+ run_id: Type.String({ description: "The run ID returned by local_compute_execute" }),
6078
+ timeout_seconds: Type.Optional(Type.Number({ description: "Max seconds to wait (default: 120, max: 600)" }))
6079
+ }),
6080
+ execute: async (_toolCallId, rawParams) => {
6081
+ const params = rawParams;
6082
+ const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
6083
+ if (!runId) {
6084
+ return toAgentResult("local_compute_wait", toolError("MISSING_PARAMETER", "run_id is required."));
6085
+ }
6086
+ const timeoutSec = typeof params.timeout_seconds === "number" ? Math.min(params.timeout_seconds, 600) : 120;
6087
+ const result = await runner.waitForCompletion(runId, timeoutSec * 1e3);
6088
+ if (!result) {
6089
+ return toAgentResult("local_compute_wait", toolError("NOT_FOUND", `Run not found: ${runId}`, {
6090
+ suggestions: ["Check the run_id. Use local_compute_execute to start a new run."]
6091
+ }));
6092
+ }
6093
+ return toAgentResult("local_compute_wait", {
6094
+ success: true,
6095
+ data: formatStatusResult(runId, result)
6096
+ });
6097
+ }
6098
+ };
6099
+ }
6100
+ function createStatusTool(runner) {
6101
+ return {
6102
+ name: "local_compute_status",
6103
+ label: "Local Compute: Status",
6104
+ description: "Check the current status of a compute run. Non-blocking — returns immediately.\nIncludes output tail, progress, stall detection, and failure analysis.",
6105
+ parameters: Type.Object({
6106
+ run_id: Type.String({ description: "The run ID to check" })
6107
+ }),
6108
+ execute: async (_toolCallId, rawParams) => {
6109
+ const params = rawParams;
6110
+ const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
6111
+ if (!runId) {
6112
+ return toAgentResult("local_compute_status", toolError("MISSING_PARAMETER", "run_id is required."));
6113
+ }
6114
+ const result = runner.getStatus(runId);
6115
+ if (!result) {
6116
+ return toAgentResult("local_compute_status", toolError("NOT_FOUND", `Run not found: ${runId}`));
6117
+ }
6118
+ return toAgentResult("local_compute_status", {
6119
+ success: true,
6120
+ data: formatStatusResult(runId, result)
6121
+ });
6122
+ }
6123
+ };
6124
+ }
6125
+ function createStopTool(runner) {
6126
+ return {
6127
+ name: "local_compute_stop",
6128
+ label: "Local Compute: Stop",
6129
+ description: "Stop (cancel) a running compute task. The process is killed.",
6130
+ parameters: Type.Object({
6131
+ run_id: Type.String({ description: "The run ID to stop" })
6132
+ }),
6133
+ execute: async (_toolCallId, rawParams) => {
6134
+ const params = rawParams;
6135
+ const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
6136
+ if (!runId) {
6137
+ return toAgentResult("local_compute_stop", toolError("MISSING_PARAMETER", "run_id is required."));
6138
+ }
6139
+ try {
6140
+ await runner.stop(runId);
6141
+ return toAgentResult("local_compute_stop", {
6142
+ success: true,
6143
+ data: { run_id: runId, status: "cancelled" }
6144
+ });
6145
+ } catch (err) {
6146
+ return toAgentResult("local_compute_stop", toolError(
6147
+ "EXECUTION_FAILED",
6148
+ err instanceof Error ? err.message : String(err)
6149
+ ));
6150
+ }
6151
+ }
6152
+ };
6153
+ }
6154
+ function formatStatusResult(runId, result) {
6155
+ const data = {
6156
+ run_id: runId,
6157
+ status: result.status,
6158
+ current_phase: result.currentPhase,
6159
+ elapsed_seconds: result.elapsedSeconds,
6160
+ output_bytes: result.outputBytes,
6161
+ output_lines: result.outputLines,
6162
+ stalled: result.stalled
6163
+ };
6164
+ if (result.exitCode !== void 0) data.exit_code = result.exitCode;
6165
+ if (result.outputTail) data.output_tail = result.outputTail;
6166
+ if (result.progress) data.progress = result.progress;
6167
+ if (result.failure) data.failure = result.failure;
6168
+ return data;
6169
+ }
6170
+ function wrapResearchTool(tool) {
6171
+ const properties = {};
6172
+ const jsonProps = tool.parameters.properties ?? {};
6173
+ const requiredFields = tool.parameters.required ?? [];
6174
+ for (const [key, prop] of Object.entries(jsonProps)) {
6175
+ const isRequired = requiredFields.includes(key);
6176
+ let schema;
6177
+ if (prop.enum) {
6178
+ schema = Type.Union(prop.enum.map((v) => Type.Literal(v)));
6179
+ } else if (prop.type === "number") {
6180
+ schema = Type.Number({ description: prop.description });
6181
+ } else if (prop.type === "array") {
6182
+ schema = Type.Array(Type.String(), { description: prop.description });
6183
+ } else {
6184
+ schema = Type.String({ description: prop.description });
6185
+ }
6186
+ properties[key] = isRequired ? schema : Type.Optional(schema);
6187
+ }
6188
+ const parametersSchema = Type.Object(properties);
6189
+ return {
6190
+ name: tool.name,
6191
+ description: tool.description,
6192
+ label: tool.name,
6193
+ parameters: parametersSchema,
6194
+ execute: async (_toolCallId, params, _signal) => {
6195
+ try {
6196
+ const result = await tool.execute(params);
6197
+ return toAgentResult(tool.name, result);
6198
+ } catch (err) {
6199
+ const errorMsg = err instanceof Error ? err.message : String(err);
6200
+ return toAgentResult(tool.name, toolError("EXECUTION_FAILED", errorMsg, {
6201
+ retryable: false,
6202
+ suggestions: ["Check tool parameters and try again."]
6203
+ }));
6204
+ }
6205
+ }
6206
+ };
6207
+ }
6208
+ function createResearchTools(ctx) {
6209
+ const tools = [];
6210
+ const destroyers = [];
6211
+ tools.push(createWebSearchTool());
6212
+ tools.push(createWebFetchTool(ctx));
6213
+ tools.push(createLiteratureSearchTool(ctx));
6214
+ tools.push(createConvertDocumentTool(ctx));
6215
+ tools.push(createDataAnalyzeTool(ctx));
6216
+ const artifactTools = createResearchMemoryTools({
6217
+ sessionId: ctx.sessionId,
6218
+ projectPath: ctx.projectPath
6219
+ });
6220
+ for (const tool of artifactTools) {
6221
+ tools.push(wrapResearchTool(tool));
6222
+ }
6223
+ const structuredMemoryTools = createMemoryTools(ctx.projectPath);
6224
+ for (const tool of structuredMemoryTools) {
6225
+ tools.push(wrapResearchTool(tool));
6226
+ }
6227
+ if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
6228
+ const compute = createLocalComputeTools(ctx);
6229
+ tools.push(...compute.tools);
6230
+ destroyers.push(compute.destroy);
6231
+ }
6232
+ return {
6233
+ tools,
6234
+ destroy: async () => {
6235
+ for (const d of destroyers) {
6236
+ await d().catch(() => {
6237
+ });
6238
+ }
6239
+ }
6240
+ };
6241
+ }
6242
+ const VALID_TYPES = ["user", "feedback", "project", "reference"];
6243
+ const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
6244
+
6245
+ Rules:
6246
+ - Only extract DURABLE, IMPORTANT information — things a future session would need.
6247
+ - Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
6248
+ - Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
6249
+ - Do NOT extract: routine task results, ephemeral details, things already in workspace files.
6250
+ - Each memory should be atomic — one concept per entry.
6251
+ - If nothing is worth saving, return an empty array.
6252
+
6253
+ Return ONLY a JSON array (no markdown fences, no explanation):
6254
+ [{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
6255
+ Or: []`;
6256
+ function simplifyMessages(messages, maxMessages) {
6257
+ const recent = messages.slice(-20);
6258
+ const result = [];
6259
+ for (const msg of recent) {
6260
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
6261
+ let content = "";
6262
+ if (typeof msg.content === "string") {
6263
+ content = msg.content;
6264
+ } else if (Array.isArray(msg.content)) {
6265
+ for (const block of msg.content) {
6266
+ if (block && typeof block === "object" && "type" in block) {
6267
+ if (block.type === "text" && "text" in block) {
6268
+ const text = block.text;
6269
+ content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
6270
+ content += "\n";
6271
+ } else if (block.type === "tool_use" && "name" in block) {
6272
+ content += `[Called ${block.name}]
6273
+ `;
6274
+ }
6275
+ }
6276
+ }
6277
+ }
6278
+ content = content.trim();
6279
+ if (content) {
6280
+ result.push({
6281
+ role: msg.role,
6282
+ content: content.slice(0, 2e3),
6283
+ timestamp: Date.now()
6284
+ });
6285
+ }
6286
+ }
6287
+ return result;
6288
+ }
6289
+ function agentCalledSaveMemoryThisTurn(messages) {
6290
+ for (let i = messages.length - 1; i >= 0; i--) {
6291
+ const msg = messages[i];
6292
+ if (msg.role === "user") break;
6293
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
6294
+ for (const block of msg.content) {
6295
+ if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
6296
+ if (block.name === "save-memory") return true;
6297
+ }
6298
+ }
6299
+ }
6300
+ }
6301
+ return false;
6302
+ }
6303
+ async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
6304
+ if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT !== "1") return;
6305
+ if (turnCount % extractEveryN !== 0) return;
6306
+ if (agentCalledSaveMemoryThisTurn(messages)) {
6307
+ if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
6308
+ return;
6309
+ }
6310
+ try {
4031
6311
  const simplified = simplifyMessages(messages, 20);
4032
6312
  if (simplified.length < 2) return;
4033
6313
  simplified.push({
@@ -4045,33 +6325,57 @@ async function maybeExtractMemories(config, messages, turnCount, extractEveryN =
4045
6325
  const textContent = result.content.find((c) => c.type === "text");
4046
6326
  const text = textContent?.text?.trim() ?? "";
4047
6327
  if (!text || text === "[]") return;
4048
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) ?? text.match(/(\[[\s\S]*?\])/);
4049
- const jsonStr = jsonMatch?.[1]?.trim() ?? text;
4050
- const extracted = JSON.parse(jsonStr);
6328
+ let jsonStr;
6329
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
6330
+ if (fenceMatch) {
6331
+ jsonStr = fenceMatch[1].trim();
6332
+ } else {
6333
+ const firstBracket = text.indexOf("[");
6334
+ const lastBracket = text.lastIndexOf("]");
6335
+ if (firstBracket !== -1 && lastBracket > firstBracket) {
6336
+ jsonStr = text.slice(firstBracket, lastBracket + 1);
6337
+ } else {
6338
+ jsonStr = text;
6339
+ }
6340
+ }
6341
+ let extracted;
6342
+ try {
6343
+ extracted = JSON.parse(jsonStr);
6344
+ } catch (parseErr) {
6345
+ if (config.debug) {
6346
+ console.warn("[Extractor] JSON parse failed:", parseErr, "Raw text:", text.slice(0, 200));
6347
+ }
6348
+ return;
6349
+ }
4051
6350
  if (!Array.isArray(extracted) || extracted.length === 0) return;
4052
6351
  ensureMemoryDir(config.projectPath);
4053
- let written = 0;
6352
+ const validEntries = [];
4054
6353
  for (const mem of extracted) {
4055
6354
  if (!mem.type || !mem.name || !mem.content) continue;
4056
6355
  if (!VALID_TYPES.includes(mem.type)) continue;
4057
- const entry = {
6356
+ const desc = (mem.description || "").trim();
6357
+ const contentFirstLine = mem.content.split("\n").find((l) => l.trim().length > 0) || "";
6358
+ const description = (desc || contentFirstLine.replace(/^#+\s*/, "").trim().slice(0, 120) || mem.name).replace(/\n/g, " ");
6359
+ validEntries.push({
4058
6360
  frontmatter: {
4059
6361
  name: mem.name,
4060
- description: (mem.description || mem.content.slice(0, 120)).replace(/\n/g, " "),
6362
+ description,
4061
6363
  type: mem.type
4062
6364
  },
4063
6365
  content: mem.content,
4064
6366
  filename: memoryFilename(mem.type, mem.name)
4065
- };
4066
- writeMemoryFile(config.projectPath, entry);
4067
- written++;
6367
+ });
4068
6368
  }
4069
- if (written > 0) {
6369
+ if (validEntries.length === 0) return;
6370
+ await withIndexLock(() => {
6371
+ for (const entry of validEntries) {
6372
+ writeMemoryFile(config.projectPath, entry);
6373
+ }
4070
6374
  const allEntries = listMemoryFiles(config.projectPath);
4071
6375
  updateAgentMdIndex(config.projectPath, allEntries);
4072
- if (config.debug) {
4073
- console.log(`[Extractor] Saved ${written} memories from conversation`);
4074
- }
6376
+ });
6377
+ if (config.debug) {
6378
+ console.log(`[Extractor] Saved ${validEntries.length} memories from conversation`);
4075
6379
  }
4076
6380
  } catch (err) {
4077
6381
  if (config.debug) {
@@ -4123,7 +6427,7 @@ function discoverSkillFiles(rootDir) {
4123
6427
  continue;
4124
6428
  }
4125
6429
  for (const entry of entries) {
4126
- const abs = path.join(current.dir, entry.name);
6430
+ const abs = path$1.join(current.dir, entry.name);
4127
6431
  if (entry.isFile() && entry.name === SKILL_FILE_NAME) {
4128
6432
  files.push(abs);
4129
6433
  continue;
@@ -4154,7 +6458,7 @@ function parseSkillFile(skillFile, displayPath, source) {
4154
6458
  tags,
4155
6459
  triggers,
4156
6460
  path: displayPath,
4157
- dir: path.dirname(skillFile),
6461
+ dir: path$1.dirname(skillFile),
4158
6462
  source,
4159
6463
  content
4160
6464
  };
@@ -4170,34 +6474,34 @@ function inferCategory(name, description) {
4170
6474
  return "General";
4171
6475
  }
4172
6476
  function loadBuiltinSkills() {
4173
- const skillsRoot = _builtinSkillsRoot ?? path.dirname(fileURLToPath(import.meta.url));
6477
+ const skillsRoot = _builtinSkillsRoot ?? path$1.dirname(fileURLToPath(import.meta.url));
4174
6478
  if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
4175
6479
  return [];
4176
6480
  }
4177
6481
  const files = discoverSkillFiles(skillsRoot);
4178
6482
  const byName = /* @__PURE__ */ new Map();
4179
6483
  for (const file of files) {
4180
- const entry = parseSkillFile(file, `[builtin] ${path.relative(skillsRoot, file)}`, "builtin");
6484
+ const entry = parseSkillFile(file, `[builtin] ${path$1.relative(skillsRoot, file)}`, "builtin");
4181
6485
  if (entry) byName.set(entry.name, entry);
4182
6486
  }
4183
6487
  return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
4184
6488
  }
4185
6489
  function loadWorkspaceSkills(workspacePath) {
4186
- const skillRoot = path.resolve(workspacePath, ".research-pilot", "skills");
6490
+ const skillRoot = path$1.resolve(workspacePath, ".research-pilot", "skills");
4187
6491
  if (!fs.existsSync(skillRoot) || !fs.statSync(skillRoot).isDirectory()) {
4188
6492
  return [];
4189
6493
  }
4190
6494
  const files = discoverSkillFiles(skillRoot);
4191
- const entries = files.map((file) => parseSkillFile(file, path.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
6495
+ const entries = files.map((file) => parseSkillFile(file, path$1.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
4192
6496
  return entries.sort((a, b) => a.name.localeCompare(b.name));
4193
6497
  }
4194
6498
  function loadUserSkills() {
4195
- const userRoot = path.resolve(os.homedir(), ".research-pilot", "skills");
6499
+ const userRoot = path$1.resolve(os.homedir(), ".research-pilot", "skills");
4196
6500
  if (!fs.existsSync(userRoot) || !fs.statSync(userRoot).isDirectory()) {
4197
6501
  return [];
4198
6502
  }
4199
6503
  const files = discoverSkillFiles(userRoot);
4200
- const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
6504
+ const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path$1.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
4201
6505
  return entries.sort((a, b) => a.name.localeCompare(b.name));
4202
6506
  }
4203
6507
  function loadAllSkills(workspacePath) {
@@ -4242,7 +6546,7 @@ function resolveSkillDependencies(allSkills, directSelection) {
4242
6546
  return result;
4243
6547
  }
4244
6548
  function readEnabledSkills(workspacePath) {
4245
- const configPath = path.resolve(workspacePath, ".research-pilot", "skills-config.json");
6549
+ const configPath = path$1.resolve(workspacePath, ".research-pilot", "skills-config.json");
4246
6550
  try {
4247
6551
  const raw = fs.readFileSync(configPath, "utf8");
4248
6552
  const config = JSON.parse(raw);
@@ -4252,9 +6556,9 @@ function readEnabledSkills(workspacePath) {
4252
6556
  return null;
4253
6557
  }
4254
6558
  function writeEnabledSkills(workspacePath, enabledSkills) {
4255
- const configDir = path.resolve(workspacePath, ".research-pilot");
6559
+ const configDir = path$1.resolve(workspacePath, ".research-pilot");
4256
6560
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
4257
- const configPath = path.join(configDir, "skills-config.json");
6561
+ const configPath = path$1.join(configDir, "skills-config.json");
4258
6562
  fs.writeFileSync(configPath, JSON.stringify({ enabledSkills }, null, 2), "utf8");
4259
6563
  }
4260
6564
  function buildSkillManifests(workspacePath) {
@@ -4278,15 +6582,15 @@ function buildSkillManifests(workspacePath) {
4278
6582
  });
4279
6583
  }
4280
6584
  function installSkillToWorkspace(workspacePath, skillName, skillDir) {
4281
- const destDir = path.resolve(workspacePath, ".research-pilot", "skills", skillName);
6585
+ const destDir = path$1.resolve(workspacePath, ".research-pilot", "skills", skillName);
4282
6586
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
4283
6587
  copyDirSync(skillDir, destDir);
4284
6588
  }
4285
6589
  function copyDirSync(src, dest) {
4286
6590
  const entries = fs.readdirSync(src, { withFileTypes: true });
4287
6591
  for (const entry of entries) {
4288
- const srcPath = path.join(src, entry.name);
4289
- const destPath = path.join(dest, entry.name);
6592
+ const srcPath = path$1.join(src, entry.name);
6593
+ const destPath = path$1.join(dest, entry.name);
4290
6594
  if (entry.isDirectory()) {
4291
6595
  if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
4292
6596
  copyDirSync(srcPath, destPath);
@@ -4479,6 +6783,7 @@ function writeExplainSnapshot(projectPath, snapshot) {
4479
6783
  async function createCoordinator(config) {
4480
6784
  const {
4481
6785
  apiKey,
6786
+ getApiKeyOverride,
4482
6787
  model: modelId,
4483
6788
  projectPath = process.cwd(),
4484
6789
  debug = false,
@@ -4487,9 +6792,11 @@ async function createCoordinator(config) {
4487
6792
  onStream,
4488
6793
  onToolCall,
4489
6794
  onToolResult,
6795
+ onToolProgress,
4490
6796
  onUsage,
4491
6797
  onSkillLoaded
4492
6798
  } = config;
6799
+ const resolveApiKey = getApiKeyOverride ?? (async () => apiKey);
4493
6800
  let turnCount = 0;
4494
6801
  let activeTurnToolCallCount = null;
4495
6802
  const turnHistory = [];
@@ -4534,13 +6841,14 @@ async function createCoordinator(config) {
4534
6841
  const routerByProvider = {
4535
6842
  anthropic: "claude-haiku-4-5-20251001",
4536
6843
  openai: "gpt-5.4-nano",
6844
+ "openai-codex": "gpt-5.4-nano",
4537
6845
  google: "gemini-2.0-flash-lite"
4538
6846
  };
4539
6847
  let mainProvider = null;
4540
6848
  if (modelId) {
4541
6849
  const parts = modelId.split(":");
4542
6850
  if (parts.length === 2) {
4543
- mainProvider = parts[0];
6851
+ mainProvider = parts[0] === "openai-codex" ? "openai-codex" : parts[0];
4544
6852
  } else {
4545
6853
  mainProvider = modelId.startsWith("claude-") ? "anthropic" : modelId.startsWith("gpt-") || modelId.startsWith("o3") || modelId.startsWith("o4") ? "openai" : modelId.startsWith("gemini-") ? "google" : null;
4546
6854
  }
@@ -4558,11 +6866,11 @@ async function createCoordinator(config) {
4558
6866
  }
4559
6867
  }
4560
6868
  }
4561
- const wrappedOnToolResult = (tool, result, args) => {
6869
+ const wrappedOnToolResult = (tool, result, args, toolCallId) => {
4562
6870
  if (activeTurnToolCallCount !== null) {
4563
6871
  activeTurnToolCallCount++;
4564
6872
  }
4565
- onToolResult?.(tool, result, args);
6873
+ onToolResult?.(tool, result, args, toolCallId);
4566
6874
  };
4567
6875
  const toolCtx = {
4568
6876
  workspacePath: projectPath,
@@ -4570,17 +6878,18 @@ async function createCoordinator(config) {
4570
6878
  projectPath,
4571
6879
  callLlm: async (systemPrompt, userContent) => {
4572
6880
  if (!piModel) throw new Error("No model available for sub-call");
6881
+ const currentKey = await resolveApiKey();
4573
6882
  const result = await completeSimple(piModel, {
4574
6883
  systemPrompt,
4575
6884
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }]
4576
- }, { maxTokens: 4096, apiKey });
6885
+ }, { maxTokens: 4096, apiKey: currentKey });
4577
6886
  const textContent = result.content.find((c) => c.type === "text");
4578
6887
  return textContent?.text ?? "";
4579
6888
  },
4580
6889
  onToolCall,
4581
6890
  onToolResult: wrappedOnToolResult
4582
6891
  };
4583
- const researchAgentTools = createResearchTools(toolCtx);
6892
+ const { tools: researchAgentTools, destroy: destroyResearchTools } = createResearchTools(toolCtx);
4584
6893
  const codingTools = createCodingTools(projectPath);
4585
6894
  const grepTool = createGrepTool(projectPath);
4586
6895
  const findTool = createFindTool(projectPath);
@@ -4603,17 +6912,17 @@ async function createCoordinator(config) {
4603
6912
  loadSkillTool
4604
6913
  ];
4605
6914
  const skillsCatalog = buildSkillsCatalogPrompt(skills);
4606
- const fullSystemPrompt = skillsCatalog ? SYSTEM_PROMPT + "\n\n" + skillsCatalog : SYSTEM_PROMPT;
6915
+ const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
4607
6916
  let compactionSummary;
4608
6917
  const agent = new Agent({
4609
6918
  initialState: {
4610
- systemPrompt: fullSystemPrompt,
6919
+ systemPrompt: baseSystemPrompt,
4611
6920
  model: piModel ?? void 0,
4612
6921
  tools: allTools,
4613
6922
  thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
4614
6923
  },
4615
6924
  sessionId,
4616
- getApiKey: async () => apiKey,
6925
+ getApiKey: resolveApiKey,
4617
6926
  // ── Context compaction via transformContext ──
4618
6927
  // Before each LLM call, check if accumulated messages exceed the model's
4619
6928
  // context window. If so, summarize old messages and keep only recent ones.
@@ -4652,11 +6961,12 @@ async function createCoordinator(config) {
4652
6961
  const messagesToSummarize = messages.slice(0, cutIndex);
4653
6962
  const messagesToKeep = messages.slice(cutIndex);
4654
6963
  try {
6964
+ const currentKey = await resolveApiKey();
4655
6965
  const summary = await generateSummary(
4656
6966
  messagesToSummarize,
4657
6967
  piModel,
4658
6968
  settings.reserveTokens,
4659
- apiKey,
6969
+ currentKey,
4660
6970
  signal,
4661
6971
  void 0,
4662
6972
  compactionSummary
@@ -4685,14 +6995,14 @@ The conversation continues below.`,
4685
6995
  }
4686
6996
  },
4687
6997
  beforeToolCall: async (ctx) => {
4688
- onToolCall?.(ctx.toolCall.name, ctx.args);
6998
+ onToolCall?.(ctx.toolCall.name, ctx.args, ctx.toolCall.id);
4689
6999
  if (debug) {
4690
7000
  console.log(` [Tool] ${ctx.toolCall.name}(${JSON.stringify(ctx.args).slice(0, 120)}...)`);
4691
7001
  }
4692
7002
  return void 0;
4693
7003
  },
4694
7004
  afterToolCall: async (ctx) => {
4695
- wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args);
7005
+ wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args, ctx.toolCall.id);
4696
7006
  if (ctx.toolCall.name === "load_skill" && onSkillLoaded) {
4697
7007
  const args = ctx.args;
4698
7008
  const result = ctx.result;
@@ -4703,7 +7013,7 @@ The conversation continues below.`,
4703
7013
  return void 0;
4704
7014
  }
4705
7015
  });
4706
- if (onStream || onUsage) {
7016
+ if (onStream || onUsage || onToolProgress) {
4707
7017
  agent.subscribe((event) => {
4708
7018
  if (event.type === "message_update" && onStream) {
4709
7019
  if (event.assistantMessageEvent.type === "text_delta") {
@@ -4717,6 +7027,15 @@ The conversation continues below.`,
4717
7027
  onUsage(usage, usage.cost);
4718
7028
  }
4719
7029
  }
7030
+ if (onToolProgress) {
7031
+ if (event.type === "tool_execution_start") {
7032
+ onToolProgress(event.toolName, event.toolCallId, "start", { args: event.args });
7033
+ } else if (event.type === "tool_execution_update") {
7034
+ onToolProgress(event.toolName, event.toolCallId, "update", { partialResult: event.partialResult });
7035
+ } else if (event.type === "tool_execution_end") {
7036
+ onToolProgress(event.toolName, event.toolCallId, "end", { result: event.result, isError: event.isError });
7037
+ }
7038
+ }
4720
7039
  });
4721
7040
  }
4722
7041
  async function clearSessionMemory() {
@@ -4736,6 +7055,7 @@ The conversation continues below.`,
4736
7055
  const historyText = turnHistory.map((t, i) => `Turn ${turnCount - turnHistory.length + i + 1}: User: ${t.userMessage}
4737
7056
  Assistant: ${t.response}`).join("\n\n");
4738
7057
  try {
7058
+ const currentKey = await resolveApiKey();
4739
7059
  const result = await completeSimple(intentRouterModel, {
4740
7060
  systemPrompt: "You summarize research conversations concisely. Output JSON with keys: summary (string), topicsDiscussed (string[]), openQuestions (string[]). Output ONLY valid JSON.",
4741
7061
  messages: [{
@@ -4747,7 +7067,7 @@ ${historyText}`,
4747
7067
  }]
4748
7068
  }, {
4749
7069
  maxTokens: 512,
4750
- apiKey
7070
+ apiKey: currentKey
4751
7071
  });
4752
7072
  const textContent = result.content.find((c) => c.type === "text");
4753
7073
  const text = textContent?.text?.trim() ?? "";
@@ -4773,12 +7093,20 @@ ${historyText}`,
4773
7093
  }
4774
7094
  }
4775
7095
  }
7096
+ if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
7097
+ probeStaticProfile().then((profile) => {
7098
+ const envGuidance = generateAgentGuidance(profile);
7099
+ agent.setSystemPrompt(baseSystemPrompt + "\n\n" + envGuidance);
7100
+ }).catch(() => {
7101
+ });
7102
+ }
4776
7103
  return {
4777
7104
  agent,
4778
7105
  async chat(message, mentions, images) {
4779
7106
  try {
4780
7107
  const intents = detectIntentsByRules(message);
4781
- const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, apiKey, message, skills);
7108
+ const currentKey = await resolveApiKey();
7109
+ const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, currentKey, message, skills);
4782
7110
  const matchedSkills = matchedSkillNames.map((name) => skills.find((s) => s.name === name)).filter((s) => s !== void 0);
4783
7111
  for (const s of matchedSkills) {
4784
7112
  onSkillLoaded?.(s.name);
@@ -4819,31 +7147,24 @@ ${historyText}`,
4819
7147
  console.log(`[Chat] Matched skills: ${skillList}`);
4820
7148
  console.log(`[Chat] Sending message to agent (${mentions?.filter((m) => !m.error).length ?? 0} mentions, summary=${!!latestSummary})...`);
4821
7149
  }
4822
- let enrichedSystem = fullSystemPrompt;
7150
+ let enrichedSystem = baseSystemPrompt;
4823
7151
  if (agentMdContent) {
4824
7152
  enrichedSystem = `${enrichedSystem}
4825
7153
 
4826
7154
  ## User Instructions (agent.md)
4827
7155
 
4828
7156
  ${agentMdContent}`;
4829
- }
4830
- if (skillSummariesPrompt) {
4831
- enrichedSystem = `${enrichedSystem}
4832
-
4833
- ${skillSummariesPrompt}`;
4834
7157
  }
4835
7158
  agent.setSystemPrompt(enrichedSystem);
4836
- let userMessage = message;
4837
- if (mentionContext || summaryContext) {
4838
- const contextParts = [];
4839
- if (summaryContext) contextParts.push(summaryContext);
4840
- if (mentionContext) contextParts.push(mentionContext);
4841
- userMessage = `${contextParts.join("\n\n")}
7159
+ const contextParts = [];
7160
+ if (summaryContext) contextParts.push(summaryContext);
7161
+ if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
7162
+ if (mentionContext) contextParts.push(mentionContext);
7163
+ let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
4842
7164
 
4843
7165
  ---
4844
7166
 
4845
- ${message}`;
4846
- }
7167
+ ${message}` : message;
4847
7168
  let perTurnToolCallCount = 0;
4848
7169
  activeTurnToolCallCount = 0;
4849
7170
  try {
@@ -4889,8 +7210,9 @@ ${message}`;
4889
7210
  });
4890
7211
  if (turnHistory.length > 8) turnHistory.shift();
4891
7212
  void maybeGenerateSummary();
7213
+ const memoryKey = await resolveApiKey();
4892
7214
  void maybeExtractMemories(
4893
- { projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
7215
+ { projectPath, model: piModel, apiKey: memoryKey, systemPrompt: enrichedSystem, debug },
4894
7216
  agent.state.messages,
4895
7217
  turnCount
4896
7218
  );
@@ -4913,6 +7235,7 @@ ${message}`;
4913
7235
  clearSessionMemory,
4914
7236
  async destroy() {
4915
7237
  agent.abort();
7238
+ await destroyResearchTools();
4916
7239
  }
4917
7240
  };
4918
7241
  }
@@ -4949,10 +7272,10 @@ function sessionSummaryGet(projectPath, sessionId) {
4949
7272
  }
4950
7273
  }
4951
7274
  class RateLimiter {
4952
- constructor(configs) {
7275
+ constructor(configs2) {
4953
7276
  this.timestamps = /* @__PURE__ */ new Map();
4954
7277
  this.activeCounts = /* @__PURE__ */ new Map();
4955
- this.configs = configs;
7278
+ this.configs = configs2;
4956
7279
  }
4957
7280
  /**
4958
7281
  * Wait until a request slot is available for the given source.
@@ -5565,7 +7888,7 @@ function parseMentions(message) {
5565
7888
  return { cleanMessage, mentions };
5566
7889
  }
5567
7890
  function getCacheKey(filePath, mtime) {
5568
- const hash = createHash$1("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
7891
+ const hash = createHash("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
5569
7892
  const name = basename(filePath).replace(/[^a-zA-Z0-9.-]/g, "_");
5570
7893
  return `${name}-${hash}.json`;
5571
7894
  }
@@ -6037,6 +8360,10 @@ class RealtimeBuffer {
6037
8360
  isStreaming = false;
6038
8361
  progressItems = [];
6039
8362
  activityEvents = [];
8363
+ /** Tool events for chat-inline rendering (mirrors tool-events-store) */
8364
+ toolEvents = [];
8365
+ /** Track tool-call start times keyed by toolCallId for duration computation */
8366
+ toolCallStartTimes = /* @__PURE__ */ new Map();
6040
8367
  /** Append a streaming text chunk (called from onStream callback) */
6041
8368
  appendChunk(chunk) {
6042
8369
  this.streamingText += chunk;
@@ -6051,18 +8378,45 @@ class RealtimeBuffer {
6051
8378
  this.progressItems.push(item);
6052
8379
  }
6053
8380
  }
6054
- /** Record an activity event */
8381
+ /** Record an activity event and track tool-call start times */
6055
8382
  pushActivity(event) {
8383
+ if (event.type === "tool-call" && event.toolCallId) {
8384
+ this.toolCallStartTimes.set(event.toolCallId, Date.now());
8385
+ }
6056
8386
  this.activityEvents.push(event);
6057
8387
  }
8388
+ /** Record a tool event for chat-inline rendering */
8389
+ pushToolEvent(event) {
8390
+ this.toolEvents.push(event);
8391
+ }
8392
+ /** Update a tool event by toolCallId (for tool-result merge) */
8393
+ updateToolEvent(toolCallId, patch) {
8394
+ const idx = this.toolEvents.findLastIndex((e) => e.toolCallId === toolCallId);
8395
+ if (idx !== -1) {
8396
+ this.toolEvents[idx] = { ...this.toolEvents[idx], ...patch };
8397
+ }
8398
+ }
8399
+ /** Clear tool events (on new run or finalize) */
8400
+ clearToolEvents() {
8401
+ this.toolEvents = [];
8402
+ }
8403
+ /** Pop and return the start time for a tool-call, or undefined if not found */
8404
+ popToolCallStartTime(toolCallId) {
8405
+ const t = this.toolCallStartTimes.get(toolCallId);
8406
+ if (t !== void 0) this.toolCallStartTimes.delete(toolCallId);
8407
+ return t;
8408
+ }
6058
8409
  /** Clear progress and activity (called on project close or explicit reset) */
6059
8410
  clearRun() {
6060
8411
  this.progressItems = [];
6061
8412
  this.activityEvents = [];
8413
+ this.toolEvents = [];
6062
8414
  }
6063
8415
  /** Clear only activity events (called on new agent run) */
6064
8416
  clearActivity() {
6065
8417
  this.activityEvents = [];
8418
+ this.toolEvents = [];
8419
+ this.toolCallStartTimes.clear();
6066
8420
  }
6067
8421
  /** Mark streaming finished (called on agent:done) */
6068
8422
  finishStreaming() {
@@ -6075,6 +8429,8 @@ class RealtimeBuffer {
6075
8429
  this.isStreaming = false;
6076
8430
  this.progressItems = [];
6077
8431
  this.activityEvents = [];
8432
+ this.toolEvents = [];
8433
+ this.toolCallStartTimes.clear();
6078
8434
  }
6079
8435
  /** Return a snapshot the renderer can use to hydrate stores */
6080
8436
  getSnapshot() {
@@ -6082,17 +8438,366 @@ class RealtimeBuffer {
6082
8438
  streamingText: this.streamingText,
6083
8439
  isStreaming: this.isStreaming,
6084
8440
  progressItems: [...this.progressItems],
6085
- activityEvents: [...this.activityEvents]
8441
+ activityEvents: [...this.activityEvents],
8442
+ toolEvents: [...this.toolEvents]
6086
8443
  };
6087
8444
  }
6088
8445
  }
6089
8446
  function createRealtimeBuffer() {
6090
8447
  return new RealtimeBuffer();
6091
8448
  }
8449
+ function getFileName(path2) {
8450
+ if (!path2) return "";
8451
+ const parts = path2.replace(/\\/g, "/").split("/");
8452
+ return parts[parts.length - 1] || path2;
8453
+ }
8454
+ function truncStr(s, max) {
8455
+ if (!s) return "";
8456
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
8457
+ }
8458
+ function safeRecord(obj) {
8459
+ return obj && typeof obj === "object" ? obj : {};
8460
+ }
8461
+ function extractResultText(result) {
8462
+ const r = safeRecord(result);
8463
+ const content = r.content;
8464
+ return content?.[0]?.text || "";
8465
+ }
8466
+ function lastNLines(text, n) {
8467
+ const lines = text.split("\n").filter(Boolean);
8468
+ return lines.slice(-n).join("\n");
8469
+ }
8470
+ const configs = [
8471
+ // ── File tools ────────────────────────
8472
+ {
8473
+ name: "read",
8474
+ displayName: "Read",
8475
+ icon: "FileText",
8476
+ category: "file",
8477
+ formatCallSummary: (a) => {
8478
+ const path2 = a.path || "";
8479
+ const offset = a.offset;
8480
+ const limit = a.limit;
8481
+ const suffix = offset ? ` · lines ${offset}-${offset + (limit || 2e3)}` : "";
8482
+ return `${getFileName(path2)}${suffix}`;
8483
+ },
8484
+ formatCallDetail: (a) => ({ path: a.path, offset: a.offset, limit: a.limit }),
8485
+ formatResultSummary: (result) => {
8486
+ const text = extractResultText(result);
8487
+ const lineCount = text ? text.split("\n").length : 0;
8488
+ return lineCount ? `${lineCount} lines` : "Read completed";
8489
+ },
8490
+ formatResultDetail: (result) => {
8491
+ const text = extractResultText(result);
8492
+ return { lineCount: text ? text.split("\n").length : 0 };
8493
+ }
8494
+ },
8495
+ {
8496
+ name: "write",
8497
+ displayName: "Write",
8498
+ icon: "FileText",
8499
+ category: "file",
8500
+ formatCallSummary: (a) => getFileName(a.path || ""),
8501
+ formatCallDetail: (a) => ({ path: a.path }),
8502
+ formatResultSummary: (_, a) => `Written: ${getFileName(a?.path || "")}`,
8503
+ formatResultDetail: (_, a) => ({ path: a?.path })
8504
+ },
8505
+ {
8506
+ name: "edit",
8507
+ displayName: "Edit",
8508
+ icon: "FileText",
8509
+ category: "file",
8510
+ formatCallSummary: (a) => getFileName(a.path || ""),
8511
+ formatCallDetail: (a) => ({ path: a.path }),
8512
+ formatResultSummary: (_, a) => `Edited: ${getFileName(a?.path || "")}`,
8513
+ formatResultDetail: (_, a) => ({ path: a?.path })
8514
+ },
8515
+ // ── Code tools ────────────────────────
8516
+ {
8517
+ name: "bash",
8518
+ displayName: "Bash",
8519
+ icon: "Terminal",
8520
+ category: "code",
8521
+ formatCallSummary: (a) => {
8522
+ const cmd = a.command || "";
8523
+ return cmd.length > 60 ? `$ ${cmd.slice(0, 57)}...` : `$ ${cmd}`;
8524
+ },
8525
+ formatCallDetail: (a) => ({ command: truncStr(a.command, 200) }),
8526
+ formatResultSummary: () => "Command completed",
8527
+ formatResultDetail: (result) => {
8528
+ const text = extractResultText(result);
8529
+ const lines = text.split("\n").filter(Boolean);
8530
+ return { outputLines: lines.length, outputPreview: truncStr(lastNLines(text, 3), 200) };
8531
+ },
8532
+ formatProgress: (partial) => {
8533
+ const text = partial?.content?.[0]?.text;
8534
+ if (typeof text === "string" && text.length > 0) {
8535
+ return lastNLines(text, 5);
8536
+ }
8537
+ return void 0;
8538
+ }
8539
+ },
8540
+ // ── Search tools ────────────────────────
8541
+ {
8542
+ name: "grep",
8543
+ displayName: "Search",
8544
+ icon: "Search",
8545
+ category: "search",
8546
+ formatCallSummary: (a) => `"${truncStr(a.pattern, 30)}"${a.path ? ` in ${a.path}` : ""}`,
8547
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path, glob: a.glob }),
8548
+ formatResultSummary: (result) => {
8549
+ const text = extractResultText(result);
8550
+ const count = text.split("\n").filter(Boolean).length;
8551
+ return `${count} results`;
8552
+ },
8553
+ formatResultDetail: (result) => {
8554
+ const text = extractResultText(result);
8555
+ return { matchCount: text.split("\n").filter(Boolean).length };
8556
+ }
8557
+ },
8558
+ {
8559
+ name: "glob",
8560
+ displayName: "Find Files",
8561
+ icon: "Search",
8562
+ category: "search",
8563
+ formatCallSummary: (a) => a.pattern || "",
8564
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
8565
+ formatResultSummary: (result) => {
8566
+ const text = extractResultText(result);
8567
+ const count = text.split("\n").filter(Boolean).length;
8568
+ return `${count} files`;
8569
+ },
8570
+ formatResultDetail: (result) => {
8571
+ const text = extractResultText(result);
8572
+ return { fileCount: text.split("\n").filter(Boolean).length };
8573
+ }
8574
+ },
8575
+ {
8576
+ name: "find",
8577
+ displayName: "Find",
8578
+ icon: "Search",
8579
+ category: "search",
8580
+ formatCallSummary: (a) => truncStr(a.pattern || a.path, 40),
8581
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
8582
+ formatResultSummary: () => "Find completed",
8583
+ formatResultDetail: () => ({})
8584
+ },
8585
+ {
8586
+ name: "ls",
8587
+ displayName: "List",
8588
+ icon: "FileText",
8589
+ category: "file",
8590
+ formatCallSummary: (a) => a.path || ".",
8591
+ formatCallDetail: (a) => ({ path: a.path }),
8592
+ formatResultSummary: () => "Listed",
8593
+ formatResultDetail: () => ({})
8594
+ },
8595
+ // ── Web tools ────────────────────────
8596
+ {
8597
+ name: "fetch",
8598
+ displayName: "Fetch",
8599
+ icon: "Globe",
8600
+ category: "web",
8601
+ formatCallSummary: (a) => truncStr(a.url, 50),
8602
+ formatCallDetail: (a) => ({ url: a.url }),
8603
+ formatResultSummary: (result) => {
8604
+ const text = extractResultText(result);
8605
+ const kb = (text.length / 1024).toFixed(1);
8606
+ return `${kb}KB received`;
8607
+ },
8608
+ formatResultDetail: (result) => {
8609
+ const text = extractResultText(result);
8610
+ return { sizeKB: parseFloat((text.length / 1024).toFixed(1)) };
8611
+ }
8612
+ },
8613
+ {
8614
+ name: "web_fetch",
8615
+ displayName: "Web Fetch",
8616
+ icon: "Globe",
8617
+ category: "web",
8618
+ formatCallSummary: (a) => truncStr(a.url, 50),
8619
+ formatCallDetail: (a) => ({ url: a.url }),
8620
+ formatResultSummary: (result) => {
8621
+ const r = safeRecord(result);
8622
+ const data = safeRecord(r.data);
8623
+ const charCount = data.charCount;
8624
+ if (charCount) return `${(charCount / 1024).toFixed(1)}KB received`;
8625
+ return "Fetch completed";
8626
+ },
8627
+ formatResultDetail: (result) => {
8628
+ const r = safeRecord(result);
8629
+ const data = safeRecord(r.data);
8630
+ return { charCount: data.charCount, url: data.url };
8631
+ }
8632
+ },
8633
+ {
8634
+ name: "web_search",
8635
+ displayName: "Web Search",
8636
+ icon: "Globe",
8637
+ category: "web",
8638
+ formatCallSummary: (a) => truncStr(a.query, 50),
8639
+ formatCallDetail: (a) => ({ query: a.query }),
8640
+ formatResultSummary: () => "Search completed",
8641
+ formatResultDetail: () => ({})
8642
+ },
8643
+ // ── Research tools ────────────────────────
8644
+ {
8645
+ name: "literature-search",
8646
+ displayName: "Literature Search",
8647
+ icon: "BookOpen",
8648
+ category: "research",
8649
+ formatCallSummary: (a) => truncStr(a.query, 40),
8650
+ formatCallDetail: (a) => ({ query: a.query, maxResults: a.max_results }),
8651
+ formatResultSummary: (result) => {
8652
+ const r = safeRecord(result);
8653
+ const data = safeRecord(r.data);
8654
+ const totalFound = data.totalPapersFound ?? 0;
8655
+ const saved = data.papersAutoSaved ?? 0;
8656
+ const coverage = data.coverage;
8657
+ if (totalFound > 0) {
8658
+ let s = `Found ${totalFound} papers`;
8659
+ if (coverage?.score != null) s += ` (${Math.round(coverage.score * 100)}%)`;
8660
+ if (saved > 0) s += `, saved ${saved}`;
8661
+ return s;
8662
+ }
8663
+ const local = data.localPapersUsed ?? 0;
8664
+ const external = data.externalPapersUsed ?? 0;
8665
+ return `Found ${local + external} papers`;
8666
+ },
8667
+ formatResultDetail: (result) => {
8668
+ const r = safeRecord(result);
8669
+ const data = safeRecord(r.data);
8670
+ return {
8671
+ papersFound: data.totalPapersFound ?? 0,
8672
+ papersSaved: data.papersAutoSaved ?? 0,
8673
+ coverage: data.coverage?.score
8674
+ };
8675
+ }
8676
+ },
8677
+ {
8678
+ name: "lit-subtopic",
8679
+ displayName: "Sub-topic Search",
8680
+ icon: "BookOpen",
8681
+ category: "research",
8682
+ formatCallSummary: (a) => a._summary || "Searching sub-topic",
8683
+ formatCallDetail: (a) => ({ summary: a._summary }),
8684
+ formatResultSummary: (result) => safeRecord(result).data || "Search completed",
8685
+ formatResultDetail: () => ({})
8686
+ },
8687
+ {
8688
+ name: "lit-enrich",
8689
+ displayName: "Enrich Papers",
8690
+ icon: "BookOpen",
8691
+ category: "research",
8692
+ formatCallSummary: (a) => a._summary || "Enriching paper metadata",
8693
+ formatCallDetail: (a) => ({ summary: a._summary }),
8694
+ formatResultSummary: (result) => safeRecord(result).data || "Enriched metadata",
8695
+ formatResultDetail: () => ({})
8696
+ },
8697
+ {
8698
+ name: "lit-autosave",
8699
+ displayName: "Save Papers",
8700
+ icon: "BookOpen",
8701
+ category: "research",
8702
+ formatCallSummary: (a) => a._summary || "Saving papers",
8703
+ formatCallDetail: (a) => ({ summary: a._summary }),
8704
+ formatResultSummary: (result) => safeRecord(result).data || "Saved papers",
8705
+ formatResultDetail: () => ({})
8706
+ },
8707
+ {
8708
+ name: "data_analyze",
8709
+ displayName: "Data Analysis",
8710
+ icon: "Database",
8711
+ category: "research",
8712
+ formatCallSummary: (a) => getFileName(a.file_path || "") || "data",
8713
+ formatCallDetail: (a) => ({ file_path: a.file_path }),
8714
+ formatResultSummary: () => "Analysis completed",
8715
+ formatResultDetail: () => ({})
8716
+ },
8717
+ // ── Artifact tools ────────────────────────
8718
+ {
8719
+ name: "artifact-create",
8720
+ displayName: "Create Artifact",
8721
+ icon: "Sparkles",
8722
+ category: "memory",
8723
+ formatCallSummary: (a) => {
8724
+ const type = (a.type || "artifact").toLowerCase();
8725
+ const title = truncStr(a.title, 35);
8726
+ return `${type}: ${title}`;
8727
+ },
8728
+ formatCallDetail: (a) => ({ type: a.type, title: a.title }),
8729
+ formatResultSummary: (result) => {
8730
+ const data = safeRecord(safeRecord(result).data);
8731
+ const type = data.type || "artifact";
8732
+ const title = truncStr(data.title, 30);
8733
+ return title ? `Created ${type}: ${title}` : `Created ${type}`;
8734
+ },
8735
+ formatResultDetail: (result) => {
8736
+ const data = safeRecord(safeRecord(result).data);
8737
+ return { type: data.type, title: data.title };
8738
+ }
8739
+ },
8740
+ {
8741
+ name: "artifact-update",
8742
+ displayName: "Update Artifact",
8743
+ icon: "Sparkles",
8744
+ category: "memory",
8745
+ formatCallSummary: (a) => truncStr(a.id, 30),
8746
+ formatCallDetail: (a) => ({ id: a.id }),
8747
+ formatResultSummary: () => "Updated",
8748
+ formatResultDetail: () => ({})
8749
+ },
8750
+ {
8751
+ name: "artifact-search",
8752
+ displayName: "Search Artifacts",
8753
+ icon: "Search",
8754
+ category: "memory",
8755
+ formatCallSummary: (a) => truncStr(a.query, 40),
8756
+ formatCallDetail: (a) => ({ query: a.query, types: a.types }),
8757
+ formatResultSummary: () => "Search completed",
8758
+ formatResultDetail: () => ({})
8759
+ },
8760
+ // ── System tools ────────────────────────
8761
+ {
8762
+ name: "convert_document",
8763
+ displayName: "Convert Document",
8764
+ icon: "FileText",
8765
+ category: "system",
8766
+ formatCallSummary: (a) => getFileName(a.source || ""),
8767
+ formatCallDetail: (a) => ({ source: a.source }),
8768
+ formatResultSummary: (result, a) => {
8769
+ const data = safeRecord(safeRecord(result).data);
8770
+ const skill = data.converterSkill;
8771
+ const sourceName = getFileName(a?.source || "");
8772
+ return skill ? `Converted ${sourceName} via ${skill}` : `Converted ${sourceName}`;
8773
+ },
8774
+ formatResultDetail: (result) => {
8775
+ const data = safeRecord(safeRecord(result).data);
8776
+ return { converterSkill: data.converterSkill, outputFile: data.outputFile };
8777
+ }
8778
+ },
8779
+ {
8780
+ name: "load_skill",
8781
+ displayName: "Load Skill",
8782
+ icon: "Sparkles",
8783
+ category: "system",
8784
+ formatCallSummary: (a) => a.name || "skill",
8785
+ formatCallDetail: (a) => ({ name: a.name }),
8786
+ formatResultSummary: (_, a) => `Loaded: ${a?.name || "skill"}`,
8787
+ formatResultDetail: () => ({})
8788
+ }
8789
+ ];
8790
+ const registry = /* @__PURE__ */ new Map();
8791
+ for (const config of configs) {
8792
+ registry.set(config.name, config);
8793
+ }
8794
+ function getToolRenderConfig(toolName) {
8795
+ return registry.get(toolName);
8796
+ }
6092
8797
  const EMPTY = {
6093
8798
  version: 1,
6094
8799
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
6095
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
8800
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
6096
8801
  };
6097
8802
  function usagePath(baseDir) {
6098
8803
  return join(baseDir, "usage.json");
@@ -6102,6 +8807,9 @@ function loadUsageTotals(baseDir) {
6102
8807
  const raw = readFileSync(usagePath(baseDir), "utf-8");
6103
8808
  const parsed = JSON.parse(raw);
6104
8809
  if (!parsed?.totals) return { ...EMPTY };
8810
+ const t = parsed.totals;
8811
+ t.completionTokens ??= 0;
8812
+ t.cacheWriteTokens ??= 0;
6105
8813
  return parsed;
6106
8814
  } catch {
6107
8815
  return { ...EMPTY };
@@ -6130,15 +8838,17 @@ function writeAtomically(filePath, data) {
6130
8838
  }
6131
8839
  }
6132
8840
  }
6133
- function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cost) {
8841
+ function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, cost) {
6134
8842
  const existing = loadUsageTotals(baseDir);
6135
8843
  const next = {
6136
8844
  version: 1,
6137
8845
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6138
8846
  totals: {
6139
- tokens: existing.totals.tokens + promptTokens + completionTokens,
8847
+ tokens: existing.totals.tokens + promptTokens + completionTokens + cachedTokens,
6140
8848
  promptTokens: existing.totals.promptTokens + promptTokens,
8849
+ completionTokens: existing.totals.completionTokens + completionTokens,
6141
8850
  cachedTokens: existing.totals.cachedTokens + cachedTokens,
8851
+ cacheWriteTokens: existing.totals.cacheWriteTokens + cacheWriteTokens,
6142
8852
  cost: existing.totals.cost + cost,
6143
8853
  calls: existing.totals.calls + 1
6144
8854
  }
@@ -6150,105 +8860,52 @@ function resetUsageTotals(baseDir) {
6150
8860
  const cleared = {
6151
8861
  version: 1,
6152
8862
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6153
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
8863
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
6154
8864
  };
6155
8865
  writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
6156
8866
  return cleared;
6157
8867
  }
6158
8868
  function formatToolCall(tool, args) {
6159
8869
  const a = args && typeof args === "object" ? args : {};
6160
- switch (tool) {
6161
- case "literature-search":
6162
- return { label: `Search: ${(a.query || "").slice(0, 40)}${(a.query || "").length > 40 ? "..." : ""}`, icon: "search" };
6163
- case "lit-subtopic":
6164
- return { label: a._summary || "Searching sub-topic", icon: "search" };
6165
- case "lit-enrich":
6166
- return { label: a._summary || "Enriching paper metadata", icon: "search" };
6167
- case "lit-autosave":
6168
- return { label: a._summary || "Saving papers", icon: "file" };
6169
- case "data-analyze":
6170
- return { label: `Analyze: ${getFileName(a.filePath || "") || "data"}`, icon: "file" };
6171
- case "convert_to_markdown": {
6172
- const sourcePath = a.path || a.uri || "";
6173
- return { label: `Convert: ${getFileName(sourcePath)}`, icon: "file" };
6174
- }
6175
- case "artifact-create": {
6176
- const type = (a.type || "artifact").toLowerCase();
6177
- const title = (a.title || type).slice(0, 35);
6178
- return { label: `Create ${type}: ${title}`, icon: "file" };
6179
- }
6180
- case "read":
6181
- return { label: `Read: ${getFileName(a.path || "")}`, icon: "file" };
6182
- case "write":
6183
- return { label: `Write: ${getFileName(a.path || "")}`, icon: "file" };
6184
- case "edit":
6185
- return { label: `Edit: ${getFileName(a.path || "")}`, icon: "file" };
6186
- case "bash":
6187
- return { label: `Run command`, icon: "terminal" };
6188
- case "glob":
6189
- return { label: `Search files: ${a.pattern || ""}`, icon: "search" };
6190
- case "grep":
6191
- return { label: `Search content: ${(a.pattern || "").slice(0, 30)}`, icon: "search" };
6192
- case "fetch":
6193
- return { label: `Fetch: ${(a.url || "").slice(0, 40)}`, icon: "network" };
6194
- default:
6195
- return { label: `${tool}`, icon: "tool" };
8870
+ const config = getToolRenderConfig(tool);
8871
+ if (config) {
8872
+ return {
8873
+ label: `${config.displayName}: ${config.formatCallSummary(a)}`,
8874
+ icon: config.icon,
8875
+ detail: config.formatCallDetail(a)
8876
+ };
6196
8877
  }
8878
+ return { label: `${tool}`, icon: "tool" };
6197
8879
  }
6198
8880
  function formatToolResult(tool, result, args) {
6199
- const r = result && typeof result === "object" ? result : {};
6200
8881
  const a = args && typeof args === "object" ? args : {};
6201
- const data = r.data && typeof r.data === "object" ? r.data : {};
6202
- switch (tool) {
6203
- case "literature-search": {
6204
- const totalFound = data.totalPapersFound ?? 0;
6205
- const saved = data.papersAutoSaved ?? 0;
6206
- const coverage = data.coverage;
6207
- if (totalFound > 0) {
6208
- let summary2 = `Found ${totalFound} papers`;
6209
- if (coverage?.score != null) summary2 += ` (coverage: ${Math.round(coverage.score * 100)}%)`;
6210
- if (saved > 0) summary2 += `, saved ${saved}`;
6211
- return { label: summary2, icon: "search" };
6212
- }
6213
- const local = data.localPapersUsed ?? 0;
6214
- const external = data.externalPapersUsed ?? 0;
6215
- const savedV1 = data.savedPapers ?? 0;
6216
- let summary = `Found ${local + external} papers`;
6217
- if (local > 0) summary += ` (${local} local)`;
6218
- if (savedV1 > 0) summary += `, saved ${savedV1}`;
6219
- return { label: summary, icon: "search" };
6220
- }
6221
- case "lit-subtopic":
6222
- return { label: r.data || "Search completed", icon: "search" };
6223
- case "lit-enrich":
6224
- return { label: r.data || "Enriched metadata", icon: "search" };
6225
- case "lit-autosave":
6226
- return { label: r.data || "Saved papers", icon: "file" };
6227
- case "convert_to_markdown": {
6228
- const sourcePath = a.path || a.uri || "";
6229
- const skill = typeof data.converterSkill === "string" ? data.converterSkill : "";
6230
- const script = typeof data.converterScript === "string" ? data.converterScript : "";
6231
- if (skill && script) return { label: `Converted ${getFileName(sourcePath)} via ${skill}/${script}`, icon: "file" };
6232
- if (skill) return { label: `Converted ${getFileName(sourcePath)} via ${skill}`, icon: "file" };
6233
- return { label: `Converted ${getFileName(sourcePath)}`, icon: "file" };
6234
- }
6235
- case "artifact-create": {
6236
- const type = data.type || "artifact";
6237
- const title = data.title || "";
6238
- return { label: title ? `Created ${type}: ${title.slice(0, 30)}` : `Created ${type}`, icon: "file" };
6239
- }
6240
- default: {
6241
- const success2 = r.success !== false;
6242
- return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool" };
6243
- }
8882
+ const r = result && typeof result === "object" ? result : {};
8883
+ const success2 = r.success !== false;
8884
+ const config = getToolRenderConfig(tool);
8885
+ if (config && success2) {
8886
+ return {
8887
+ label: config.formatResultSummary(result, a),
8888
+ icon: config.icon,
8889
+ detail: config.formatResultDetail(result, a)
8890
+ };
6244
8891
  }
8892
+ if (config && !success2) {
8893
+ const errorMsg = r.error || "";
8894
+ const brief = errorMsg.length > 60 ? errorMsg.slice(0, 57) + "..." : errorMsg;
8895
+ return {
8896
+ label: brief ? `${config.displayName} failed: ${brief}` : `${config.displayName} failed`,
8897
+ icon: config.icon,
8898
+ detail: config.formatResultDetail(result, a)
8899
+ };
8900
+ }
8901
+ return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool", detail: { success: success2 } };
6245
8902
  }
6246
8903
  const windowStates = /* @__PURE__ */ new Map();
6247
8904
  let ipcHandlersRegistered = false;
6248
8905
  function createWindowRuntimeState() {
6249
8906
  return {
6250
8907
  coordinator: null,
6251
- currentModel: "gpt-5.4",
8908
+ currentModel: "openai:gpt-5.4",
6252
8909
  currentReasoningEffort: "medium",
6253
8910
  currentAuthMode: "none",
6254
8911
  projectPath: "",
@@ -6379,11 +9036,27 @@ async function ensureCoordinator(state, win, model, options) {
6379
9036
  if (!state.coordinator) {
6380
9037
  const apiKey = resolvedAuth.apiKey;
6381
9038
  const runProjectPath = state.projectPath;
9039
+ const getApiKeyOverride = resolvedAuth.authMode === "subscription" ? async () => {
9040
+ const creds = loadCodexCredentials();
9041
+ if (!creds) throw new Error("ChatGPT subscription credentials not found. Please sign in again.");
9042
+ if (creds.expires < Date.now() + 6e4) {
9043
+ try {
9044
+ const { refreshOpenAICodexToken } = await import("@mariozechner/pi-ai/oauth");
9045
+ const newCreds = await refreshOpenAICodexToken(creds);
9046
+ saveCodexCredentials(newCreds);
9047
+ return newCreds.access;
9048
+ } catch {
9049
+ return creds.access;
9050
+ }
9051
+ }
9052
+ return creds.access;
9053
+ } : void 0;
6382
9054
  const initEvent = { type: "system", summary: "Initializing agent (first run may take 1-2 minutes for document processing setup)..." };
6383
9055
  state.realtimeBuffer.pushActivity(initEvent);
6384
9056
  safeSend(win, "agent:activity", initEvent);
6385
9057
  state.coordinator = await createCoordinator({
6386
9058
  apiKey,
9059
+ getApiKeyOverride,
6387
9060
  model: state.currentModel,
6388
9061
  reasoningEffort: state.currentReasoningEffort,
6389
9062
  projectPath: state.projectPath,
@@ -6393,13 +9066,15 @@ async function ensureCoordinator(state, win, model, options) {
6393
9066
  state.realtimeBuffer.appendChunk(chunk);
6394
9067
  safeSend(win, "agent:stream-chunk", chunk);
6395
9068
  },
6396
- onToolCall: (tool, args) => {
6397
- const summary = formatToolCall(tool, args).label;
6398
- const event = { type: "tool-call", tool, summary };
9069
+ onToolCall: (tool, args, toolCallId) => {
9070
+ const id = toolCallId || randomUUID();
9071
+ const { label, detail } = formatToolCall(tool, args);
9072
+ const event = { type: "tool-call", tool, toolCallId: id, summary: label, detail };
6399
9073
  state.realtimeBuffer.pushActivity(event);
9074
+ state.realtimeBuffer.pushToolEvent({ type: "tool-call", tool, toolCallId: id, summary: label, detail });
6400
9075
  safeSend(win, "agent:activity", event);
6401
9076
  },
6402
- onToolResult: (tool, result, args) => {
9077
+ onToolResult: (tool, result, args, toolCallId) => {
6403
9078
  if (tool.startsWith("todo-") && result && typeof result === "object" && "success" in result) {
6404
9079
  const r2 = result;
6405
9080
  if (r2.success && r2.item) {
@@ -6414,16 +9089,16 @@ async function ensureCoordinator(state, win, model, options) {
6414
9089
  safeSend(win, "agent:file-created", absPath);
6415
9090
  }
6416
9091
  }
6417
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
9092
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
6418
9093
  const r2 = result;
6419
9094
  if (r2.success && r2.data?.outputFile) {
6420
9095
  safeSend(win, "agent:file-created", r2.data.outputFile);
6421
9096
  }
6422
9097
  }
6423
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
9098
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
6424
9099
  const r2 = result;
6425
- if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "path" in args) {
6426
- const sourcePath = args.path;
9100
+ if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "source" in args) {
9101
+ const sourcePath = args.source;
6427
9102
  const absSourcePath = isAbsolute(sourcePath) ? sourcePath : resolve(runProjectPath, sourcePath);
6428
9103
  const absOutputPath = resolve(runProjectPath, r2.data.outputFile);
6429
9104
  if (existsSync(absOutputPath)) {
@@ -6454,14 +9129,72 @@ async function ensureCoordinator(state, win, model, options) {
6454
9129
  }
6455
9130
  }
6456
9131
  }
9132
+ if (tool === "local_compute_execute" && result && typeof result === "object" && "success" in result) {
9133
+ const cr = result;
9134
+ if (cr.success && cr.data) {
9135
+ safeSend(win, "compute:run-update", {
9136
+ runId: cr.data.run_id,
9137
+ status: cr.data.status,
9138
+ currentPhase: cr.data.current_phase,
9139
+ command: args?.command ?? "",
9140
+ sandbox: cr.data.sandbox,
9141
+ weight: cr.data.weight,
9142
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
9143
+ });
9144
+ }
9145
+ }
9146
+ if ((tool === "local_compute_status" || tool === "local_compute_wait") && result && typeof result === "object" && "success" in result) {
9147
+ const cr = result;
9148
+ if (cr.success && cr.data?.run_id) {
9149
+ const isComplete = ["completed", "failed", "timed_out", "cancelled"].includes(cr.data.status);
9150
+ const channel = isComplete ? "compute:run-complete" : "compute:run-update";
9151
+ safeSend(win, channel, {
9152
+ runId: cr.data.run_id,
9153
+ status: cr.data.status,
9154
+ currentPhase: cr.data.current_phase,
9155
+ exitCode: cr.data.exit_code,
9156
+ elapsedSeconds: cr.data.elapsed_seconds,
9157
+ outputBytes: cr.data.output_bytes,
9158
+ outputLines: cr.data.output_lines,
9159
+ stalled: cr.data.stalled,
9160
+ progress: cr.data.progress,
9161
+ outputTail: cr.data.output_tail?.slice(-2048),
9162
+ failure: cr.data.failure
9163
+ });
9164
+ }
9165
+ }
9166
+ if (tool === "local_compute_stop" && result && typeof result === "object" && "success" in result) {
9167
+ const cr = result;
9168
+ if (cr.success && cr.data?.run_id) {
9169
+ safeSend(win, "compute:run-complete", {
9170
+ runId: cr.data.run_id,
9171
+ status: "cancelled"
9172
+ });
9173
+ }
9174
+ }
6457
9175
  const r = result;
6458
9176
  const success2 = r?.success !== false;
6459
9177
  const error = !success2 ? r?.error || "Unknown error" : void 0;
6460
- const summary = formatToolResult(tool, result, args).label;
6461
- const actEvent = { type: "tool-result", tool, summary, success: success2, error };
9178
+ const { label: resultLabel, detail: resultDetail } = formatToolResult(tool, result, args);
9179
+ const startTime = toolCallId ? state.realtimeBuffer.popToolCallStartTime(toolCallId) : void 0;
9180
+ const durationMs = startTime ? Date.now() - startTime : void 0;
9181
+ const actEvent = { type: "tool-result", tool, toolCallId, summary: resultLabel, success: success2, error, resultDetail, durationMs };
6462
9182
  state.realtimeBuffer.pushActivity(actEvent);
9183
+ if (toolCallId) {
9184
+ state.realtimeBuffer.updateToolEvent(toolCallId, {
9185
+ type: "tool-result",
9186
+ summary: resultLabel,
9187
+ success: success2,
9188
+ resultDetail,
9189
+ durationMs
9190
+ });
9191
+ }
6463
9192
  safeSend(win, "agent:activity", actEvent);
6464
9193
  },
9194
+ // Tool execution progress (real-time updates during tool execution)
9195
+ onToolProgress: (tool, toolCallId, phase, data) => {
9196
+ safeSend(win, "agent:tool-progress", { tool, toolCallId, phase, data, timestamp: Date.now() });
9197
+ },
6465
9198
  // Skill activation tracking
6466
9199
  onSkillLoaded: (skillName) => {
6467
9200
  safeSend(win, "agent:skill-loaded", skillName);
@@ -6473,12 +9206,14 @@ async function ensureCoordinator(state, win, model, options) {
6473
9206
  const promptTokens = usage.input ?? 0;
6474
9207
  const completionTokens = usage.output ?? 0;
6475
9208
  const cachedTokens = usage.cacheRead ?? 0;
9209
+ const cacheWriteTokens = usage.cacheWrite ?? 0;
6476
9210
  const baseDir = join(runProjectPath, PATHS.root);
6477
- accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, rawCost);
9211
+ accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, rawCost);
6478
9212
  const usageEvent = {
6479
9213
  promptTokens,
6480
9214
  completionTokens,
6481
9215
  cachedTokens,
9216
+ cacheWriteTokens,
6482
9217
  cost: rawCost,
6483
9218
  rawCost,
6484
9219
  billableCost: rawCost,
@@ -6492,9 +9227,36 @@ async function ensureCoordinator(state, win, model, options) {
6492
9227
  const readyEvent = { type: "system", summary: "Agent ready" };
6493
9228
  state.realtimeBuffer.pushActivity(readyEvent);
6494
9229
  safeSend(win, "agent:activity", readyEvent);
9230
+ probeStaticProfile().then((profile) => {
9231
+ safeSend(win, "compute:environment", {
9232
+ os: profile.os,
9233
+ arch: profile.arch,
9234
+ cpuCores: profile.cpuCores,
9235
+ totalMemoryMb: profile.totalMemoryMb,
9236
+ gpu: profile.gpu.model,
9237
+ mlxAvailable: profile.gpu.mlxAvailable,
9238
+ sandbox: profile.dockerAvailable ? "docker" : "process"
9239
+ });
9240
+ }).catch(() => {
9241
+ });
6495
9242
  }
6496
9243
  return state.coordinator;
6497
9244
  }
9245
+ async function destroyAllCoordinators() {
9246
+ const promises = [];
9247
+ for (const [, state] of windowStates) {
9248
+ if (state.coordinator) {
9249
+ promises.push(
9250
+ state.coordinator.destroy().catch(() => {
9251
+ })
9252
+ );
9253
+ }
9254
+ }
9255
+ await Promise.race([
9256
+ Promise.all(promises),
9257
+ new Promise((resolve2) => setTimeout(resolve2, 8e3))
9258
+ ]);
9259
+ }
6498
9260
  function registerIpcHandlers() {
6499
9261
  if (ipcHandlersRegistered) return;
6500
9262
  ipcHandlersRegistered = true;
@@ -6961,6 +9723,24 @@ function registerIpcHandlers() {
6961
9723
  }
6962
9724
  });
6963
9725
  handleWindow("session:current", ({ state }) => ({ sessionId: state.sessionId, projectPath: state.projectPath }));
9726
+ handleWindow("compute:probe-environment", async ({ win }) => {
9727
+ try {
9728
+ const profile = await probeStaticProfile();
9729
+ const env = {
9730
+ os: profile.os,
9731
+ arch: profile.arch,
9732
+ cpuCores: profile.cpuCores,
9733
+ totalMemoryMb: profile.totalMemoryMb,
9734
+ gpu: profile.gpu.model,
9735
+ mlxAvailable: profile.gpu.mlxAvailable,
9736
+ sandbox: profile.dockerAvailable ? "docker" : "process"
9737
+ };
9738
+ safeSend(win, "compute:environment", env);
9739
+ return env;
9740
+ } catch {
9741
+ return null;
9742
+ }
9743
+ });
6964
9744
  handleWindow("project:pick-folder", async ({ win, state }) => {
6965
9745
  const result = await dialog.showOpenDialog(win, {
6966
9746
  properties: ["openDirectory", "createDirectory"]
@@ -6985,11 +9765,33 @@ function registerIpcHandlers() {
6985
9765
  if (existsSync(prefsFile)) {
6986
9766
  try {
6987
9767
  const prefs = JSON.parse(readFileSync(prefsFile, "utf-8"));
6988
- if (prefs.selectedModel) state.currentModel = prefs.selectedModel;
9768
+ if (prefs.selectedModel) {
9769
+ const m = prefs.selectedModel;
9770
+ if (!m.includes(":")) {
9771
+ const provider = m.startsWith("claude-") ? "anthropic" : m.startsWith("gemini-") ? "google" : "openai";
9772
+ state.currentModel = `${provider}:${m}`;
9773
+ } else {
9774
+ state.currentModel = m;
9775
+ }
9776
+ }
6989
9777
  if (prefs.reasoningEffort) state.currentReasoningEffort = prefs.reasoningEffort;
6990
9778
  } catch {
6991
9779
  }
6992
9780
  }
9781
+ if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
9782
+ probeStaticProfile().then((profile) => {
9783
+ safeSend(win, "compute:environment", {
9784
+ os: profile.os,
9785
+ arch: profile.arch,
9786
+ cpuCores: profile.cpuCores,
9787
+ totalMemoryMb: profile.totalMemoryMb,
9788
+ gpu: profile.gpu.model,
9789
+ mlxAvailable: profile.gpu.mlxAvailable,
9790
+ sandbox: profile.dockerAvailable ? "docker" : "process"
9791
+ });
9792
+ }).catch(() => {
9793
+ });
9794
+ }
6993
9795
  return { projectPath: state.projectPath, sessionId: state.sessionId };
6994
9796
  }
6995
9797
  return null;
@@ -7015,7 +9817,7 @@ function registerIpcHandlers() {
7015
9817
  state.realtimeBuffer.reset();
7016
9818
  state.projectPath = "";
7017
9819
  state.sessionId = crypto.randomUUID();
7018
- state.currentModel = "gpt-5.4";
9820
+ state.currentModel = "openai:gpt-5.4";
7019
9821
  state.currentReasoningEffort = "medium";
7020
9822
  state.currentAuthMode = "none";
7021
9823
  } finally {
@@ -7104,7 +9906,11 @@ function destroyAllTerminals() {
7104
9906
  terminals.delete(id);
7105
9907
  }
7106
9908
  }
9909
+ setMaxListeners(20);
7107
9910
  loadApiKeysFromConfig();
9911
+ if (!process.env.PI_CACHE_RETENTION) {
9912
+ process.env.PI_CACHE_RETENTION = "long";
9913
+ }
7108
9914
  if (process.platform === "darwin" && !is.dev) {
7109
9915
  try {
7110
9916
  const shellPath = process.env.SHELL || "/bin/zsh";
@@ -7259,6 +10065,10 @@ app.on("window-all-closed", () => {
7259
10065
  destroyAllTerminals();
7260
10066
  if (process.platform !== "darwin") app.quit();
7261
10067
  });
7262
- app.on("before-quit", () => {
10068
+ app.on("before-quit", (event) => {
7263
10069
  destroyAllTerminals();
10070
+ event.preventDefault();
10071
+ destroyAllCoordinators().finally(() => {
10072
+ app.exit(0);
10073
+ });
7264
10074
  });