research-copilot 0.2.1 → 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 (66) hide show
  1. package/app/out/main/index.mjs +2519 -201
  2. package/app/out/preload/index.js +21 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-D7GYpVZn.js → MilkdownMarkdownEditor-jaF-aGPn.js} +50 -50
  4. package/app/out/renderer/assets/{arc-Kp4J_Jd7.js → arc-C1kBmvvR.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-DkMSdn8j.js → blockDiagram-c4efeb88-Do93X2rs.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-DqAGxrYw.js → c4Diagram-c83219d4-DgxxcZWC.js} +3 -3
  7. package/app/out/renderer/assets/{channel-S4GQrISQ.js → channel-Co_M0Svj.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-B7AsTCEg.js → classDiagram-beda092f-CQlHgE6H.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-B4oFy-In.js → classDiagram-v2-2358418a-CkGG3aI2.js} +10 -10
  10. package/app/out/renderer/assets/{clone-Dv1e6zYr.js → clone-C18Y6dgC.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-HBXHvWlI.js → createText-1719965b-DGRc6nys.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-B6X5lcC0.js → edges-96097737-BXvJ4fAK.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-BmBmTBlH.js → erDiagram-0228fc6a-CXjPp0pt.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-CObz36ob.js → flowDb-c6c81e3f-CNhpbtw_.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-C2hFHxwF.js → flowDiagram-50d868cf-KZ_BUCPA.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-DEe8EygW.js → flowDiagram-v2-4f6560a1-IMv50KZP.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-CgTtfYKk.js → flowchart-elk-definition-6af322e1-BFwFiPvq.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-C5Pq4zEy.js → ganttDiagram-a2739b55-D0-ehN-T.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-oLp0f8Ll.js → gitGraphDiagram-82fe8481-DUyIR0Dv.js} +2 -2
  20. package/app/out/renderer/assets/{graph-51iZ6wgR.js → graph-DnTq2_3F.js} +1 -1
  21. package/app/out/renderer/assets/{index-5325376f-yLvOW-Os.js → index-5325376f-CBwuFbRF.js} +6 -6
  22. package/app/out/renderer/assets/{index-DppxBL77.js → index-7hDGClrI.js} +3 -3
  23. package/app/out/renderer/assets/{index-Du-Z3sl4.js → index-BB-a1ajC.js} +1295 -487
  24. package/app/out/renderer/assets/{index-shoMWskw.js → index-BHcU72Rm.js} +3 -3
  25. package/app/out/renderer/assets/{index-y1Od1ed6.js → index-BQ7qz1CD.js} +3 -3
  26. package/app/out/renderer/assets/{index-UajPJYNV.js → index-BVYoMX5H.js} +3 -3
  27. package/app/out/renderer/assets/{index-_Z53hJps.js → index-BpKrXGYD.js} +3 -3
  28. package/app/out/renderer/assets/{index-32eUzqVW.js → index-C1oXjI4L.js} +3 -3
  29. package/app/out/renderer/assets/{index-CmpSV9Ld.js → index-CKXwBmK7.js} +5 -5
  30. package/app/out/renderer/assets/{index-ohN9yRWw.js → index-COZSDrEw.js} +6 -6
  31. package/app/out/renderer/assets/{index-L4DJn7cw.css → index-CT1HtzVp.css} +157 -10
  32. package/app/out/renderer/assets/{index-BfWWn8B_.js → index-CjffvluT.js} +6 -6
  33. package/app/out/renderer/assets/{index-D_Y7v6pE.js → index-D6jljsup.js} +3 -3
  34. package/app/out/renderer/assets/{index-B9a4DKM-.js → index-D6r8msaQ.js} +3 -3
  35. package/app/out/renderer/assets/{index-_iFRQTkA.js → index-DWU4ia28.js} +6 -6
  36. package/app/out/renderer/assets/{index-DjqJjt6u.js → index-DZbrRR7w.js} +6 -6
  37. package/app/out/renderer/assets/{index-CTmGCKqa.js → index-Diy30-34.js} +4 -4
  38. package/app/out/renderer/assets/{index-BMsuFGn6.js → index-DuhageEr.js} +3 -3
  39. package/app/out/renderer/assets/{index-BQA_Kvr6.js → index-ESFHcvWy.js} +3 -3
  40. package/app/out/renderer/assets/{index-FGsCVYSr.js → index-JT8OCsRP.js} +1 -1
  41. package/app/out/renderer/assets/{index-Cn2e13ja.js → index-bMe3RSkw.js} +6 -6
  42. package/app/out/renderer/assets/{index-Bscx_5dF.js → index-gH-w4EHk.js} +3 -3
  43. package/app/out/renderer/assets/{index-AuZa-hTj.js → index-h_fNksib.js} +3 -3
  44. package/app/out/renderer/assets/{index-BSd80-j9.js → index-u0FZRZON.js} +4 -4
  45. package/app/out/renderer/assets/{index-CAOQIqEc.js → index-yanwpi6t.js} +6 -6
  46. package/app/out/renderer/assets/{infoDiagram-8eee0895-Cm0Hm5ZX.js → infoDiagram-8eee0895-Qra4japr.js} +2 -2
  47. package/app/out/renderer/assets/{journeyDiagram-c64418c1-A2Gw9bVu.js → journeyDiagram-c64418c1-BTN9SgOL.js} +4 -4
  48. package/app/out/renderer/assets/{layout-C5N2nTfF.js → layout-DGrHHJdN.js} +2 -2
  49. package/app/out/renderer/assets/{line-Dn6BEQAK.js → line-DXtxdS2B.js} +1 -1
  50. package/app/out/renderer/assets/{linear-8wk0rPUX.js → linear-CexrSQK6.js} +1 -1
  51. package/app/out/renderer/assets/{mindmap-definition-8da855dc-BVy6ISnb.js → mindmap-definition-8da855dc-pvG2hzEB.js} +3 -3
  52. package/app/out/renderer/assets/{pieDiagram-a8764435-B9_axIHE.js → pieDiagram-a8764435-D_neFVMq.js} +3 -3
  53. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-B1kmkDFg.js → quadrantDiagram-1e28029f-C47W3UMp.js} +3 -3
  54. package/app/out/renderer/assets/{requirementDiagram-08caed73-C_bNWUtT.js → requirementDiagram-08caed73-DW4Bo_fu.js} +5 -5
  55. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-CD2h1LiI.js → sankeyDiagram-a04cb91d-D_3PD7JI.js} +2 -2
  56. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-B6d6cuqi.js → sequenceDiagram-c5b8d532-BW6nGtuQ.js} +3 -3
  57. package/app/out/renderer/assets/{stateDiagram-1ecb1508-CkuNj_3H.js → stateDiagram-1ecb1508-CDgBJ3-T.js} +6 -6
  58. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-CevZ3tno.js → stateDiagram-v2-c2b004d7-CBw5TtXo.js} +10 -10
  59. package/app/out/renderer/assets/{styles-b4e223ce-DAe5WQrg.js → styles-b4e223ce-DeeiEsuW.js} +1 -1
  60. package/app/out/renderer/assets/{styles-ca3715f6-BDSX88bY.js → styles-ca3715f6-CMpiebrG.js} +1 -1
  61. package/app/out/renderer/assets/{styles-d45a18b0-SE9h7les.js → styles-d45a18b0-CZe9hU7H.js} +4 -4
  62. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-D1mpNbDQ.js → svgDrawCommon-b86b1483-CmJZfZzJ.js} +1 -1
  63. package/app/out/renderer/assets/{timeline-definition-faaaa080-7Ha-nm4M.js → timeline-definition-faaaa080-Beo2kiiz.js} +3 -3
  64. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-DLy7iyZW.js → xychartDiagram-f5964ef8-DYmo7moz.js} +5 -5
  65. package/app/out/renderer/index.html +2 -2
  66. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
2
2
  import { setMaxListeners } from "node:events";
3
3
  import fs, { existsSync as existsSync$1 } from "node:fs";
4
- import { execFile, execSync } from "node:child_process";
4
+ import { execFile, spawn, execSync } from "node:child_process";
5
5
  import path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
6
6
  import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
7
7
  import os$1, { homedir } from "os";
@@ -13,7 +13,7 @@ import { Type } from "@sinclair/typebox";
13
13
  import { mkdir, writeFile } from "fs/promises";
14
14
  import path$1 from "node:path";
15
15
  import fsp from "node:fs/promises";
16
- import { createHash as createHash$1 } from "node:crypto";
16
+ import crypto$1, { createHash as createHash$1 } from "node:crypto";
17
17
  import { promisify } from "node:util";
18
18
  import os from "node:os";
19
19
  import { fileURLToPath } from "node:url";
@@ -262,30 +262,83 @@ function loadOrCreateSessionId(rootPathKey, path2) {
262
262
  writeFileSync(sessionFile, JSON.stringify({ sessionId: newId }));
263
263
  return newId;
264
264
  }
265
- 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) {
266
306
  const openaiApiKey = (process.env.OPENAI_API_KEY || "").trim();
267
307
  const anthropicApiKey = (process.env.ANTHROPIC_API_KEY || "").trim();
268
- const isAnthropic = modelId.startsWith("claude-");
269
- if (!isAnthropic) {
270
- if (!openaiApiKey) {
271
- throw new Error("OPENAI_API_KEY is required for the selected OpenAI model.");
272
- }
273
- return {
274
- apiKey: openaiApiKey,
275
- authMode: "api-key",
276
- isAnthropicModel: false,
277
- billingSource: "api-key"
278
- };
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";
279
314
  }
280
- if (!anthropicApiKey) {
281
- 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
+ }
282
341
  }
283
- return {
284
- apiKey: anthropicApiKey,
285
- authMode: "api-key",
286
- isAnthropicModel: true,
287
- billingSource: "api-key"
288
- };
289
342
  }
290
343
  function registerFileHandlers(handle, getCtx) {
291
344
  handle("file:list-root", () => {
@@ -520,6 +573,51 @@ function registerAuthHandlers(handleRaw) {
520
573
  hasApiKey: !!(process.env.OPENAI_API_KEY || "").trim()
521
574
  };
522
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
+ });
523
621
  }
524
622
  function registerFolderOpenHandler(handle, getCtx) {
525
623
  handle("folder:open-with", async (appName) => {
@@ -664,7 +762,9 @@ const PATHS = {
664
762
  memory: ".research-pilot/memory",
665
763
  // Skills
666
764
  skills: ".research-pilot/skills",
667
- skillsConfig: ".research-pilot/skills-config.json"
765
+ skillsConfig: ".research-pilot/skills-config.json",
766
+ // Local compute runs
767
+ computeRuns: ".research-pilot/compute-runs"
668
768
  };
669
769
  function nowIso() {
670
770
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -3100,7 +3200,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
3100
3200
  }
3101
3201
  };
3102
3202
  }
3103
- const execFileAsync$1 = promisify(execFile);
3203
+ const execFileAsync$3 = promisify(execFile);
3104
3204
  const DEFAULT_MAX_OUTPUT_CHARS = 5e5;
3105
3205
  const DEFAULT_PREVIEW_CHARS = 4e3;
3106
3206
  const DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
@@ -3231,7 +3331,7 @@ async function detectTextLikeFile(filePath) {
3231
3331
  }
3232
3332
  async function runCommand(command, args, timeoutMs) {
3233
3333
  try {
3234
- await execFileAsync$1(command, args, {
3334
+ await execFileAsync$3(command, args, {
3235
3335
  timeout: timeoutMs,
3236
3336
  maxBuffer: 10 * 1024 * 1024
3237
3337
  });
@@ -3322,7 +3422,7 @@ print(json.dumps(result, ensure_ascii=False))
3322
3422
  let foundAnyRuntime = false;
3323
3423
  for (const attempt of attempts) {
3324
3424
  try {
3325
- const { stdout } = await execFileAsync$1(
3425
+ const { stdout } = await execFileAsync$3(
3326
3426
  attempt.command,
3327
3427
  ["-c", script, inputPath, JSON.stringify(ranges)],
3328
3428
  { timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024, encoding: "utf8" }
@@ -3828,187 +3928,2245 @@ function createConvertDocumentTool(ctx) {
3828
3928
  page_count: pageCount,
3829
3929
  page_ranges: pageRanges.length > 0 ? pageRanges.map((r) => r.raw) : void 0
3830
3930
  };
3831
- return success(payload);
3931
+ return success(payload);
3932
+ }
3933
+ };
3934
+ }
3935
+ const execFileAsync$2 = promisify(execFile);
3936
+ const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
3937
+ const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
3938
+ const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
3939
+ const TASK_DESCRIPTIONS = {
3940
+ analyze: "Statistical Analysis",
3941
+ visualize: "Data Visualization",
3942
+ transform: "Data Transformation",
3943
+ model: "Statistical Modeling"
3944
+ };
3945
+ const DataAnalyzeSchema = Type.Object({
3946
+ file_path: Type.String({ description: "Relative path to data file (CSV, JSON, TSV, XLSX)" }),
3947
+ instructions: Type.String({ description: "What analysis to perform" }),
3948
+ task_type: Type.Optional(
3949
+ Type.String({ description: "analyze | visualize | transform | model (default: analyze)" })
3950
+ )
3951
+ });
3952
+ function createDataAnalyzeTool(ctx) {
3953
+ return {
3954
+ name: "data_analyze",
3955
+ label: "Data Analysis",
3956
+ description: "Analyze a dataset using Python. Supports statistics, visualization (matplotlib/seaborn), data transformation, and modeling. Generated outputs (figures, tables) are saved to disk.\nUsage guidelines: (1) Use this tool for ANY analysis, visualization, statistics, or modeling — do not compute from raw data with read/grep. (2) Generate only the outputs the user requested; no extras.",
3957
+ parameters: DataAnalyzeSchema,
3958
+ execute: async (_toolCallId, rawParams) => {
3959
+ const params = rawParams;
3960
+ const filePath = typeof params.file_path === "string" ? params.file_path.trim() : "";
3961
+ const instructions = typeof params.instructions === "string" ? params.instructions.trim() : "";
3962
+ const taskType = typeof params.task_type === "string" ? params.task_type.trim().toLowerCase() : "analyze";
3963
+ if (!filePath) {
3964
+ return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing file_path.", {
3965
+ suggestions: ["Provide a relative path to the data file (CSV, JSON, TSV, or XLSX)."]
3966
+ }));
3967
+ }
3968
+ if (!instructions) {
3969
+ return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing instructions.", {
3970
+ suggestions: ["Describe what analysis to perform on the data."]
3971
+ }));
3972
+ }
3973
+ if (!["analyze", "visualize", "transform", "model"].includes(taskType)) {
3974
+ return toAgentResult("data_analyze", toolError("INVALID_PARAMETER", `Invalid task_type: ${taskType}. Use: analyze | visualize | transform | model.`, {
3975
+ suggestions: ["Valid task types: analyze, visualize, transform, model."]
3976
+ }));
3977
+ }
3978
+ const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
3979
+ if (!fs.existsSync(absDataFile)) {
3980
+ return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
3981
+ suggestions: [
3982
+ "Verify the file path is relative to the workspace root.",
3983
+ "Use the find or glob tool to locate the data file."
3984
+ ],
3985
+ context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
3986
+ }));
3987
+ }
3988
+ const runId = Date.now().toString(36);
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");
3993
+ fs.mkdirSync(figuresDir, { recursive: true });
3994
+ fs.mkdirSync(tablesDir, { recursive: true });
3995
+ fs.mkdirSync(dataDir, { recursive: true });
3996
+ const resultsFile = path$1.join(outputBase, "results.json");
3997
+ const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
3998
+ const ext = path$1.extname(absDataFile).toLowerCase();
3999
+ const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
4000
+ if (!ctx.callLlm) {
4001
+ return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
4002
+ suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
4003
+ }));
4004
+ }
4005
+ const taskSection = DATA_ANALYSIS_TASKS.split(/^## /m).find((s) => s.startsWith(taskType));
4006
+ const taskDesc = taskSection ? `## ${taskSection}` : "";
4007
+ const userPrompt = [
4008
+ `Task type: ${taskType} (${TASK_DESCRIPTIONS[taskType] ?? "Analysis"})`,
4009
+ "",
4010
+ taskDesc,
4011
+ "",
4012
+ `Data file format: ${formatHint}`,
4013
+ `Data file preview (first 2000 chars):`,
4014
+ "```",
4015
+ rawPreview,
4016
+ "```",
4017
+ "",
4018
+ `Instructions: ${instructions}`,
4019
+ "",
4020
+ "IMPORTANT: Use the pre-defined path variables (DATA_FILE, FIGURES_DIR, TABLES_DIR, DATA_DIR, RESULTS_FILE).",
4021
+ "Call write_results() at the end with your outputs list and summary dict.",
4022
+ "Output ONLY the Python code in a ```python code block."
4023
+ ].join("\n");
4024
+ let generatedCode;
4025
+ try {
4026
+ const llmResponse = await ctx.callLlm(DATA_ANALYSIS_SYSTEM, userPrompt);
4027
+ const codeMatch = llmResponse.match(/```python\n([\s\S]*?)```/) || llmResponse.match(/```\n([\s\S]*?)```/);
4028
+ generatedCode = codeMatch ? codeMatch[1] : llmResponse;
4029
+ } catch (err) {
4030
+ return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Code generation failed: ${err.message}`, {
4031
+ retryable: true,
4032
+ suggestions: ["Retry — the LLM may produce valid code on a subsequent attempt.", "Try simplifying the instructions."]
4033
+ }));
4034
+ }
4035
+ const pathDefinitions = [
4036
+ "",
4037
+ "# Pre-defined paths (set by the tool runtime)",
4038
+ `DATA_FILE = ${JSON.stringify(absDataFile)}`,
4039
+ `FIGURES_DIR = ${JSON.stringify(figuresDir)}`,
4040
+ `TABLES_DIR = ${JSON.stringify(tablesDir)}`,
4041
+ `DATA_DIR = ${JSON.stringify(dataDir)}`,
4042
+ `RESULTS_FILE = ${JSON.stringify(resultsFile)}`,
4043
+ ""
4044
+ ].join("\n");
4045
+ const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
4046
+ const scriptPath = path$1.join(outputBase, "script.py");
4047
+ fs.writeFileSync(scriptPath, fullScript, "utf-8");
4048
+ try {
4049
+ const { stdout, stderr } = await execFileAsync$2("python3", [scriptPath], {
4050
+ cwd: ctx.workspacePath,
4051
+ timeout: 12e4,
4052
+ // 2 minutes
4053
+ maxBuffer: 10 * 1024 * 1024
4054
+ });
4055
+ let manifest = null;
4056
+ if (fs.existsSync(resultsFile)) {
4057
+ try {
4058
+ manifest = JSON.parse(fs.readFileSync(resultsFile, "utf-8"));
4059
+ } catch {
4060
+ }
4061
+ }
4062
+ const outputs = [];
4063
+ for (const [dir, type] of [
4064
+ [figuresDir, "figure"],
4065
+ [tablesDir, "table"],
4066
+ [dataDir, "data"]
4067
+ ]) {
4068
+ if (fs.existsSync(dir)) {
4069
+ for (const f of fs.readdirSync(dir)) {
4070
+ outputs.push({
4071
+ name: f,
4072
+ type,
4073
+ path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
4074
+ });
4075
+ }
4076
+ }
4077
+ }
4078
+ const payload = {
4079
+ stdout: stdout.slice(0, 4e3),
4080
+ stderr: stderr ? stderr.slice(0, 1e3) : void 0,
4081
+ outputs,
4082
+ manifest: manifest ? {
4083
+ summary: manifest.summary,
4084
+ warnings: manifest.warnings
4085
+ } : void 0,
4086
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
4087
+ runId
4088
+ };
4089
+ return toAgentResult("data_analyze", { success: true, data: payload });
4090
+ } catch (err) {
4091
+ const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
4092
+ return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
4093
+ retryable: true,
4094
+ suggestions: [
4095
+ `Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
4096
+ "Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
4097
+ "Try simplifying the analysis instructions."
4098
+ ],
4099
+ context: {
4100
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
4101
+ runId
4102
+ },
4103
+ data: {
4104
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
4105
+ runId
4106
+ }
4107
+ }));
4108
+ }
4109
+ }
4110
+ };
4111
+ }
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;
4140
+ try {
4141
+ const record = JSON.parse(trimmed);
4142
+ this.records.set(record.runId, record);
4143
+ } catch {
4144
+ }
4145
+ }
4146
+ }
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;
4154
+ }
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();
4165
+ }
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();
4212
+ }
4213
+ return updated;
4214
+ }
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++;
4233
+ }
4234
+ }
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");
4247
+ }
4248
+ }
4249
+ class ProcessSandbox {
4250
+ constructor() {
4251
+ this.name = "process";
4252
+ }
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
+ });
3832
6005
  }
3833
6006
  };
3834
6007
  }
3835
- const execFileAsync = promisify(execFile);
3836
- const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
3837
- const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
3838
- const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
3839
- const TASK_DESCRIPTIONS = {
3840
- analyze: "Statistical Analysis",
3841
- visualize: "Data Visualization",
3842
- transform: "Data Transformation",
3843
- model: "Statistical Modeling"
3844
- };
3845
- const DataAnalyzeSchema = Type.Object({
3846
- file_path: Type.String({ description: "Relative path to data file (CSV, JSON, TSV, XLSX)" }),
3847
- instructions: Type.String({ description: "What analysis to perform" }),
3848
- task_type: Type.Optional(
3849
- Type.String({ description: "analyze | visualize | transform | model (default: analyze)" })
3850
- )
3851
- });
3852
- function createDataAnalyzeTool(ctx) {
6008
+ function createExecuteTool(runner, ctx) {
3853
6009
  return {
3854
- name: "data_analyze",
3855
- label: "Data Analysis",
3856
- description: "Analyze a dataset using Python. Supports statistics, visualization (matplotlib/seaborn), data transformation, and modeling. Generated outputs (figures, tables) are saved to disk.\nUsage guidelines: (1) Use this tool for ANY analysis, visualization, statistics, or modeling do not compute from raw data with read/grep. (2) Generate only the outputs the user requested; no extras.",
3857
- parameters: DataAnalyzeSchema,
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
+ }),
3858
6023
  execute: async (_toolCallId, rawParams) => {
3859
6024
  const params = rawParams;
3860
- const filePath = typeof params.file_path === "string" ? params.file_path.trim() : "";
3861
- const instructions = typeof params.instructions === "string" ? params.instructions.trim() : "";
3862
- const taskType = typeof params.task_type === "string" ? params.task_type.trim().toLowerCase() : "analyze";
3863
- if (!filePath) {
3864
- return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing file_path.", {
3865
- suggestions: ["Provide a relative path to the data file (CSV, JSON, TSV, or XLSX)."]
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").']
3866
6029
  }));
3867
6030
  }
3868
- if (!instructions) {
3869
- return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing instructions.", {
3870
- suggestions: ["Describe what analysis to perform on the data."]
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."]
3871
6066
  }));
3872
6067
  }
3873
- if (!["analyze", "visualize", "transform", "model"].includes(taskType)) {
3874
- return toAgentResult("data_analyze", toolError("INVALID_PARAMETER", `Invalid task_type: ${taskType}. Use: analyze | visualize | transform | model.`, {
3875
- suggestions: ["Valid task types: analyze, visualize, transform, model."]
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."]
3876
6091
  }));
3877
6092
  }
3878
- const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
3879
- if (!fs.existsSync(absDataFile)) {
3880
- return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
3881
- suggestions: [
3882
- "Verify the file path is relative to the workspace root.",
3883
- "Use the find or glob tool to locate the data file."
3884
- ],
3885
- context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
3886
- }));
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."));
3887
6113
  }
3888
- const runId = Date.now().toString(36);
3889
- const outputBase = path$1.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3890
- const figuresDir = path$1.join(outputBase, "figures");
3891
- const tablesDir = path$1.join(outputBase, "tables");
3892
- const dataDir = path$1.join(outputBase, "data");
3893
- fs.mkdirSync(figuresDir, { recursive: true });
3894
- fs.mkdirSync(tablesDir, { recursive: true });
3895
- fs.mkdirSync(dataDir, { recursive: true });
3896
- const resultsFile = path$1.join(outputBase, "results.json");
3897
- const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
3898
- const ext = path$1.extname(absDataFile).toLowerCase();
3899
- const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
3900
- if (!ctx.callLlm) {
3901
- return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
3902
- suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
3903
- }));
6114
+ const result = runner.getStatus(runId);
6115
+ if (!result) {
6116
+ return toAgentResult("local_compute_status", toolError("NOT_FOUND", `Run not found: ${runId}`));
3904
6117
  }
3905
- const taskSection = DATA_ANALYSIS_TASKS.split(/^## /m).find((s) => s.startsWith(taskType));
3906
- const taskDesc = taskSection ? `## ${taskSection}` : "";
3907
- const userPrompt = [
3908
- `Task type: ${taskType} (${TASK_DESCRIPTIONS[taskType] ?? "Analysis"})`,
3909
- "",
3910
- taskDesc,
3911
- "",
3912
- `Data file format: ${formatHint}`,
3913
- `Data file preview (first 2000 chars):`,
3914
- "```",
3915
- rawPreview,
3916
- "```",
3917
- "",
3918
- `Instructions: ${instructions}`,
3919
- "",
3920
- "IMPORTANT: Use the pre-defined path variables (DATA_FILE, FIGURES_DIR, TABLES_DIR, DATA_DIR, RESULTS_FILE).",
3921
- "Call write_results() at the end with your outputs list and summary dict.",
3922
- "Output ONLY the Python code in a ```python code block."
3923
- ].join("\n");
3924
- let generatedCode;
3925
- try {
3926
- const llmResponse = await ctx.callLlm(DATA_ANALYSIS_SYSTEM, userPrompt);
3927
- const codeMatch = llmResponse.match(/```python\n([\s\S]*?)```/) || llmResponse.match(/```\n([\s\S]*?)```/);
3928
- generatedCode = codeMatch ? codeMatch[1] : llmResponse;
3929
- } catch (err) {
3930
- return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Code generation failed: ${err.message}`, {
3931
- retryable: true,
3932
- suggestions: ["Retry — the LLM may produce valid code on a subsequent attempt.", "Try simplifying the instructions."]
3933
- }));
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."));
3934
6138
  }
3935
- const pathDefinitions = [
3936
- "",
3937
- "# Pre-defined paths (set by the tool runtime)",
3938
- `DATA_FILE = ${JSON.stringify(absDataFile)}`,
3939
- `FIGURES_DIR = ${JSON.stringify(figuresDir)}`,
3940
- `TABLES_DIR = ${JSON.stringify(tablesDir)}`,
3941
- `DATA_DIR = ${JSON.stringify(dataDir)}`,
3942
- `RESULTS_FILE = ${JSON.stringify(resultsFile)}`,
3943
- ""
3944
- ].join("\n");
3945
- const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
3946
- const scriptPath = path$1.join(outputBase, "script.py");
3947
- fs.writeFileSync(scriptPath, fullScript, "utf-8");
3948
6139
  try {
3949
- const { stdout, stderr } = await execFileAsync("python3", [scriptPath], {
3950
- cwd: ctx.workspacePath,
3951
- timeout: 12e4,
3952
- // 2 minutes
3953
- maxBuffer: 10 * 1024 * 1024
6140
+ await runner.stop(runId);
6141
+ return toAgentResult("local_compute_stop", {
6142
+ success: true,
6143
+ data: { run_id: runId, status: "cancelled" }
3954
6144
  });
3955
- let manifest = null;
3956
- if (fs.existsSync(resultsFile)) {
3957
- try {
3958
- manifest = JSON.parse(fs.readFileSync(resultsFile, "utf-8"));
3959
- } catch {
3960
- }
3961
- }
3962
- const outputs = [];
3963
- for (const [dir, type] of [
3964
- [figuresDir, "figure"],
3965
- [tablesDir, "table"],
3966
- [dataDir, "data"]
3967
- ]) {
3968
- if (fs.existsSync(dir)) {
3969
- for (const f of fs.readdirSync(dir)) {
3970
- outputs.push({
3971
- name: f,
3972
- type,
3973
- path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
3974
- });
3975
- }
3976
- }
3977
- }
3978
- const payload = {
3979
- stdout: stdout.slice(0, 4e3),
3980
- stderr: stderr ? stderr.slice(0, 1e3) : void 0,
3981
- outputs,
3982
- manifest: manifest ? {
3983
- summary: manifest.summary,
3984
- warnings: manifest.warnings
3985
- } : void 0,
3986
- scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3987
- runId
3988
- };
3989
- return toAgentResult("data_analyze", { success: true, data: payload });
3990
6145
  } catch (err) {
3991
- const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
3992
- return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
3993
- retryable: true,
3994
- suggestions: [
3995
- `Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
3996
- "Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
3997
- "Try simplifying the analysis instructions."
3998
- ],
3999
- context: {
4000
- scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
4001
- runId
4002
- },
4003
- data: {
4004
- scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
4005
- runId
4006
- }
4007
- }));
6146
+ return toAgentResult("local_compute_stop", toolError(
6147
+ "EXECUTION_FAILED",
6148
+ err instanceof Error ? err.message : String(err)
6149
+ ));
4008
6150
  }
4009
6151
  }
4010
6152
  };
4011
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
+ }
4012
6170
  function wrapResearchTool(tool) {
4013
6171
  const properties = {};
4014
6172
  const jsonProps = tool.parameters.properties ?? {};
@@ -4049,6 +6207,7 @@ function wrapResearchTool(tool) {
4049
6207
  }
4050
6208
  function createResearchTools(ctx) {
4051
6209
  const tools = [];
6210
+ const destroyers = [];
4052
6211
  tools.push(createWebSearchTool());
4053
6212
  tools.push(createWebFetchTool(ctx));
4054
6213
  tools.push(createLiteratureSearchTool(ctx));
@@ -4065,7 +6224,20 @@ function createResearchTools(ctx) {
4065
6224
  for (const tool of structuredMemoryTools) {
4066
6225
  tools.push(wrapResearchTool(tool));
4067
6226
  }
4068
- return tools;
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
+ };
4069
6241
  }
4070
6242
  const VALID_TYPES = ["user", "feedback", "project", "reference"];
4071
6243
  const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
@@ -4611,6 +6783,7 @@ function writeExplainSnapshot(projectPath, snapshot) {
4611
6783
  async function createCoordinator(config) {
4612
6784
  const {
4613
6785
  apiKey,
6786
+ getApiKeyOverride,
4614
6787
  model: modelId,
4615
6788
  projectPath = process.cwd(),
4616
6789
  debug = false,
@@ -4623,6 +6796,7 @@ async function createCoordinator(config) {
4623
6796
  onUsage,
4624
6797
  onSkillLoaded
4625
6798
  } = config;
6799
+ const resolveApiKey = getApiKeyOverride ?? (async () => apiKey);
4626
6800
  let turnCount = 0;
4627
6801
  let activeTurnToolCallCount = null;
4628
6802
  const turnHistory = [];
@@ -4667,13 +6841,14 @@ async function createCoordinator(config) {
4667
6841
  const routerByProvider = {
4668
6842
  anthropic: "claude-haiku-4-5-20251001",
4669
6843
  openai: "gpt-5.4-nano",
6844
+ "openai-codex": "gpt-5.4-nano",
4670
6845
  google: "gemini-2.0-flash-lite"
4671
6846
  };
4672
6847
  let mainProvider = null;
4673
6848
  if (modelId) {
4674
6849
  const parts = modelId.split(":");
4675
6850
  if (parts.length === 2) {
4676
- mainProvider = parts[0];
6851
+ mainProvider = parts[0] === "openai-codex" ? "openai-codex" : parts[0];
4677
6852
  } else {
4678
6853
  mainProvider = modelId.startsWith("claude-") ? "anthropic" : modelId.startsWith("gpt-") || modelId.startsWith("o3") || modelId.startsWith("o4") ? "openai" : modelId.startsWith("gemini-") ? "google" : null;
4679
6854
  }
@@ -4703,17 +6878,18 @@ async function createCoordinator(config) {
4703
6878
  projectPath,
4704
6879
  callLlm: async (systemPrompt, userContent) => {
4705
6880
  if (!piModel) throw new Error("No model available for sub-call");
6881
+ const currentKey = await resolveApiKey();
4706
6882
  const result = await completeSimple(piModel, {
4707
6883
  systemPrompt,
4708
6884
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }]
4709
- }, { maxTokens: 4096, apiKey });
6885
+ }, { maxTokens: 4096, apiKey: currentKey });
4710
6886
  const textContent = result.content.find((c) => c.type === "text");
4711
6887
  return textContent?.text ?? "";
4712
6888
  },
4713
6889
  onToolCall,
4714
6890
  onToolResult: wrappedOnToolResult
4715
6891
  };
4716
- const researchAgentTools = createResearchTools(toolCtx);
6892
+ const { tools: researchAgentTools, destroy: destroyResearchTools } = createResearchTools(toolCtx);
4717
6893
  const codingTools = createCodingTools(projectPath);
4718
6894
  const grepTool = createGrepTool(projectPath);
4719
6895
  const findTool = createFindTool(projectPath);
@@ -4736,17 +6912,17 @@ async function createCoordinator(config) {
4736
6912
  loadSkillTool
4737
6913
  ];
4738
6914
  const skillsCatalog = buildSkillsCatalogPrompt(skills);
4739
- const fullSystemPrompt = skillsCatalog ? SYSTEM_PROMPT + "\n\n" + skillsCatalog : SYSTEM_PROMPT;
6915
+ const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
4740
6916
  let compactionSummary;
4741
6917
  const agent = new Agent({
4742
6918
  initialState: {
4743
- systemPrompt: fullSystemPrompt,
6919
+ systemPrompt: baseSystemPrompt,
4744
6920
  model: piModel ?? void 0,
4745
6921
  tools: allTools,
4746
6922
  thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
4747
6923
  },
4748
6924
  sessionId,
4749
- getApiKey: async () => apiKey,
6925
+ getApiKey: resolveApiKey,
4750
6926
  // ── Context compaction via transformContext ──
4751
6927
  // Before each LLM call, check if accumulated messages exceed the model's
4752
6928
  // context window. If so, summarize old messages and keep only recent ones.
@@ -4785,11 +6961,12 @@ async function createCoordinator(config) {
4785
6961
  const messagesToSummarize = messages.slice(0, cutIndex);
4786
6962
  const messagesToKeep = messages.slice(cutIndex);
4787
6963
  try {
6964
+ const currentKey = await resolveApiKey();
4788
6965
  const summary = await generateSummary(
4789
6966
  messagesToSummarize,
4790
6967
  piModel,
4791
6968
  settings.reserveTokens,
4792
- apiKey,
6969
+ currentKey,
4793
6970
  signal,
4794
6971
  void 0,
4795
6972
  compactionSummary
@@ -4878,6 +7055,7 @@ The conversation continues below.`,
4878
7055
  const historyText = turnHistory.map((t, i) => `Turn ${turnCount - turnHistory.length + i + 1}: User: ${t.userMessage}
4879
7056
  Assistant: ${t.response}`).join("\n\n");
4880
7057
  try {
7058
+ const currentKey = await resolveApiKey();
4881
7059
  const result = await completeSimple(intentRouterModel, {
4882
7060
  systemPrompt: "You summarize research conversations concisely. Output JSON with keys: summary (string), topicsDiscussed (string[]), openQuestions (string[]). Output ONLY valid JSON.",
4883
7061
  messages: [{
@@ -4889,7 +7067,7 @@ ${historyText}`,
4889
7067
  }]
4890
7068
  }, {
4891
7069
  maxTokens: 512,
4892
- apiKey
7070
+ apiKey: currentKey
4893
7071
  });
4894
7072
  const textContent = result.content.find((c) => c.type === "text");
4895
7073
  const text = textContent?.text?.trim() ?? "";
@@ -4915,12 +7093,20 @@ ${historyText}`,
4915
7093
  }
4916
7094
  }
4917
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
+ }
4918
7103
  return {
4919
7104
  agent,
4920
7105
  async chat(message, mentions, images) {
4921
7106
  try {
4922
7107
  const intents = detectIntentsByRules(message);
4923
- const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, apiKey, message, skills);
7108
+ const currentKey = await resolveApiKey();
7109
+ const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, currentKey, message, skills);
4924
7110
  const matchedSkills = matchedSkillNames.map((name) => skills.find((s) => s.name === name)).filter((s) => s !== void 0);
4925
7111
  for (const s of matchedSkills) {
4926
7112
  onSkillLoaded?.(s.name);
@@ -4961,7 +7147,7 @@ ${historyText}`,
4961
7147
  console.log(`[Chat] Matched skills: ${skillList}`);
4962
7148
  console.log(`[Chat] Sending message to agent (${mentions?.filter((m) => !m.error).length ?? 0} mentions, summary=${!!latestSummary})...`);
4963
7149
  }
4964
- let enrichedSystem = fullSystemPrompt;
7150
+ let enrichedSystem = baseSystemPrompt;
4965
7151
  if (agentMdContent) {
4966
7152
  enrichedSystem = `${enrichedSystem}
4967
7153
 
@@ -5024,8 +7210,9 @@ ${message}` : message;
5024
7210
  });
5025
7211
  if (turnHistory.length > 8) turnHistory.shift();
5026
7212
  void maybeGenerateSummary();
7213
+ const memoryKey = await resolveApiKey();
5027
7214
  void maybeExtractMemories(
5028
- { projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
7215
+ { projectPath, model: piModel, apiKey: memoryKey, systemPrompt: enrichedSystem, debug },
5029
7216
  agent.state.messages,
5030
7217
  turnCount
5031
7218
  );
@@ -5048,6 +7235,7 @@ ${message}` : message;
5048
7235
  clearSessionMemory,
5049
7236
  async destroy() {
5050
7237
  agent.abort();
7238
+ await destroyResearchTools();
5051
7239
  }
5052
7240
  };
5053
7241
  }
@@ -6717,7 +8905,7 @@ let ipcHandlersRegistered = false;
6717
8905
  function createWindowRuntimeState() {
6718
8906
  return {
6719
8907
  coordinator: null,
6720
- currentModel: "gpt-5.4",
8908
+ currentModel: "openai:gpt-5.4",
6721
8909
  currentReasoningEffort: "medium",
6722
8910
  currentAuthMode: "none",
6723
8911
  projectPath: "",
@@ -6848,11 +9036,27 @@ async function ensureCoordinator(state, win, model, options) {
6848
9036
  if (!state.coordinator) {
6849
9037
  const apiKey = resolvedAuth.apiKey;
6850
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;
6851
9054
  const initEvent = { type: "system", summary: "Initializing agent (first run may take 1-2 minutes for document processing setup)..." };
6852
9055
  state.realtimeBuffer.pushActivity(initEvent);
6853
9056
  safeSend(win, "agent:activity", initEvent);
6854
9057
  state.coordinator = await createCoordinator({
6855
9058
  apiKey,
9059
+ getApiKeyOverride,
6856
9060
  model: state.currentModel,
6857
9061
  reasoningEffort: state.currentReasoningEffort,
6858
9062
  projectPath: state.projectPath,
@@ -6925,6 +9129,49 @@ async function ensureCoordinator(state, win, model, options) {
6925
9129
  }
6926
9130
  }
6927
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
+ }
6928
9175
  const r = result;
6929
9176
  const success2 = r?.success !== false;
6930
9177
  const error = !success2 ? r?.error || "Unknown error" : void 0;
@@ -6980,9 +9227,36 @@ async function ensureCoordinator(state, win, model, options) {
6980
9227
  const readyEvent = { type: "system", summary: "Agent ready" };
6981
9228
  state.realtimeBuffer.pushActivity(readyEvent);
6982
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
+ });
6983
9242
  }
6984
9243
  return state.coordinator;
6985
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
+ }
6986
9260
  function registerIpcHandlers() {
6987
9261
  if (ipcHandlersRegistered) return;
6988
9262
  ipcHandlersRegistered = true;
@@ -7449,6 +9723,24 @@ function registerIpcHandlers() {
7449
9723
  }
7450
9724
  });
7451
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
+ });
7452
9744
  handleWindow("project:pick-folder", async ({ win, state }) => {
7453
9745
  const result = await dialog.showOpenDialog(win, {
7454
9746
  properties: ["openDirectory", "createDirectory"]
@@ -7473,11 +9765,33 @@ function registerIpcHandlers() {
7473
9765
  if (existsSync(prefsFile)) {
7474
9766
  try {
7475
9767
  const prefs = JSON.parse(readFileSync(prefsFile, "utf-8"));
7476
- 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
+ }
7477
9777
  if (prefs.reasoningEffort) state.currentReasoningEffort = prefs.reasoningEffort;
7478
9778
  } catch {
7479
9779
  }
7480
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
+ }
7481
9795
  return { projectPath: state.projectPath, sessionId: state.sessionId };
7482
9796
  }
7483
9797
  return null;
@@ -7503,7 +9817,7 @@ function registerIpcHandlers() {
7503
9817
  state.realtimeBuffer.reset();
7504
9818
  state.projectPath = "";
7505
9819
  state.sessionId = crypto.randomUUID();
7506
- state.currentModel = "gpt-5.4";
9820
+ state.currentModel = "openai:gpt-5.4";
7507
9821
  state.currentReasoningEffort = "medium";
7508
9822
  state.currentAuthMode = "none";
7509
9823
  } finally {
@@ -7751,6 +10065,10 @@ app.on("window-all-closed", () => {
7751
10065
  destroyAllTerminals();
7752
10066
  if (process.platform !== "darwin") app.quit();
7753
10067
  });
7754
- app.on("before-quit", () => {
10068
+ app.on("before-quit", (event) => {
7755
10069
  destroyAllTerminals();
10070
+ event.preventDefault();
10071
+ destroyAllCoordinators().finally(() => {
10072
+ app.exit(0);
10073
+ });
7756
10074
  });