@barivia/barmesh-mcp 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,14 @@ export function snapshotFromJob(data, elapsedSec, note) {
55
55
  snap.qe = Math.round(qe * 10_000) / 10_000;
56
56
  if (te != null)
57
57
  snap.te = Math.round(te * 10_000) / 10_000;
58
+ const panelTe = num(data, "panel_topographic_error");
59
+ if (panelTe != null)
60
+ snap.panel_te = Math.round(panelTe * 10_000) / 10_000;
61
+ if (data.kernel_complete === true)
62
+ snap.kernel_complete = true;
63
+ const failureStage = str(data, "failure_stage");
64
+ if (failureStage)
65
+ snap.failure_stage = failureStage;
58
66
  const eta = num(data, "training_eta_sec");
59
67
  if (eta != null && eta > 0)
60
68
  snap.eta_sec = Math.round(eta);
@@ -79,7 +87,11 @@ export function shouldRecordSnapshot(prev, next) {
79
87
  return true;
80
88
  if (Math.abs(prev.progress_pct - next.progress_pct) >= 1)
81
89
  return true;
82
- if (prev.qe !== next.qe || prev.te !== next.te)
90
+ if (prev.qe !== next.qe || prev.te !== next.te || prev.panel_te !== next.panel_te)
91
+ return true;
92
+ if (prev.kernel_complete !== next.kernel_complete)
93
+ return true;
94
+ if (prev.failure_stage !== next.failure_stage)
83
95
  return true;
84
96
  return false;
85
97
  }
@@ -93,6 +105,12 @@ export function formatSnapshotLine(s) {
93
105
  parts.push(`QE ${s.qe.toFixed(4)}`);
94
106
  if (s.te != null)
95
107
  parts.push(`Epoch TE ${s.te.toFixed(4)}`);
108
+ if (s.panel_te != null)
109
+ parts.push(`Panel TE ${s.panel_te.toFixed(4)}`);
110
+ if (s.kernel_complete)
111
+ parts.push("kernel complete");
112
+ if (s.failure_stage)
113
+ parts.push(`failure_stage ${s.failure_stage}`);
96
114
  if (s.eta_sec != null)
97
115
  parts.push(`ETA ~${s.eta_sec}s`);
98
116
  if (s.ordering_errors_tail?.length) {
@@ -119,6 +137,22 @@ export function formatMonitorText(result, opts) {
119
137
  lines.push(result.suggested_next_step);
120
138
  return lines.join("\n");
121
139
  }
140
+ function failureStageHint(stage, error) {
141
+ const err = (error ?? "").toLowerCase();
142
+ if (stage === "preprocessing") {
143
+ return "Check barmesh_datasets(action=preview) and prepare job status.";
144
+ }
145
+ if (stage === "training") {
146
+ if (err.includes("memory") || err.includes("readonlymemory")) {
147
+ return "Reduce grid size or batch_size and retrain.";
148
+ }
149
+ return "Review mesh convergence parameters (grid, batch_size, features).";
150
+ }
151
+ if (stage === "visualization" || stage === "upload" || stage === "metrics") {
152
+ return "Re-run barmesh_training_monitor with wait_finalize=true or barmesh_results(action=render) for figures.";
153
+ }
154
+ return "Read the error above before retrying.";
155
+ }
122
156
  function suggestedNextStep(job_id, data) {
123
157
  const status = String(data.status ?? "");
124
158
  if (status === "completed") {
@@ -130,7 +164,9 @@ function suggestedNextStep(job_id, data) {
130
164
  }
131
165
  if (status === "failed") {
132
166
  const stage = str(data, "failure_stage");
133
- return `Job failed${stage ? ` at ${stage}` : ""}. Read the error above before retrying.`;
167
+ const err = str(data, "error");
168
+ const hint = stage ? failureStageHint(stage, err) : "Read the error above before retrying.";
169
+ return `Job failed${stage ? ` at ${stage}` : ""}. ${hint}`;
134
170
  }
135
171
  if (status === "cancelled")
136
172
  return `Job cancelled. Confirm with barmesh_jobs(action=status, job_id="${job_id}").`;
package/dist/logger.js CHANGED
@@ -29,6 +29,9 @@ function emit(fields) {
29
29
  export function logInfo(msg, fields = {}) {
30
30
  emit({ ...fields, level: "info", msg });
31
31
  }
32
+ export function logWarn(msg, fields = {}) {
33
+ emit({ ...fields, level: "warn", msg });
34
+ }
32
35
  export function logAudit(fields) {
33
36
  emit({ ...fields, event: "mcp_tool_call", level: "info", msg: "mcp_tool_call" });
34
37
  }
package/dist/shared.js CHANGED
@@ -13,17 +13,25 @@ import { pipeline } from "node:stream/promises";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
- import { logInfo } from "./logger.js";
16
+ import { logInfo, logWarn } from "./logger.js";
17
17
  // ---------------------------------------------------------------------------
18
18
  // Config
19
19
  // ---------------------------------------------------------------------------
20
- export const API_URL = process.env.BARIVIA_API_URL ?? process.env.BARSOM_API_URL ?? "https://api.barivia.se";
21
- export const API_KEY = process.env.BARIVIA_API_KEY ?? process.env.BARSOM_API_KEY ?? "";
20
+ export const API_URL = process.env.BARIVIA_API_URL ?? "https://api.barivia.se";
21
+ export const API_KEY = process.env.BARIVIA_API_KEY ?? "";
22
22
  export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ?? "60000", 10);
23
23
  export const MAX_RETRIES = 2;
24
+ export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "1000", 10);
24
25
  export const RETRYABLE_STATUS = new Set([502, 503, 504]);
26
+ /** Exponential backoff for transient API retries (attempt 0 → base, 1 → 2×, …). */
27
+ export function retryDelayMs(attempt) {
28
+ return RETRY_BASE_MS * 2 ** attempt;
29
+ }
30
+ function newRequestId() {
31
+ return randomUUID();
32
+ }
25
33
  /** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
26
- export const CLIENT_VERSION = "0.7.0";
34
+ export const CLIENT_VERSION = "0.7.1";
27
35
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
28
36
  /** Large per-cell CSV uploads may exceed the default fetch timeout. */
29
37
  export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
@@ -33,6 +41,21 @@ export const LARGE_UPLOAD_BYTES = 64 * 1024 * 1024; // 64 MB
33
41
  export const PRESIGNED_PUT_TIMEOUT_MS = 30 * 60_000; // 30 min
34
42
  /** Poll window for the async stage_dataset job. */
35
43
  export const POLL_STAGE_MAX_MS = 30 * 60_000; // 30 min
44
+ /** Map a local upload path to the API Content-Type (csv vs tsv; ignores .gz suffix). */
45
+ export function resolveUploadContentType(filePath) {
46
+ const lower = filePath.toLowerCase();
47
+ const base = lower.endsWith(".gz") ? lower.slice(0, -3) : lower;
48
+ return path.extname(base) === ".tsv" ? "text/tab-separated-values" : "text/csv";
49
+ }
50
+ export function suggestMeshDatasetPreview(datasetId) {
51
+ return `barmesh_datasets(action=preview, dataset_id=${datasetId}) to verify mesh, feature, and volume columns.`;
52
+ }
53
+ export function suggestAfterCfdSubmit(jobId, prepSuffix = "") {
54
+ return `Run barmesh_training_monitor(job_id="${jobId}") or barmesh_jobs(action=monitor, job_id="${jobId}")${prepSuffix}; on completion call barmesh_results(action=get, job_id="${jobId}").`;
55
+ }
56
+ export function suggestAfterRender(parentJobId, fmt) {
57
+ return `Figures rendered as ${fmt}. Download with barmesh_results(action=download, job_id="${parentJobId}") or barmesh_results(action=image, job_id="${parentJobId}", filename=<figure>.${fmt}).`;
58
+ }
36
59
  /** Streaming SHA-256 of a file (idempotency key) without reading it into memory. */
37
60
  export async function streamFileSha256(srcPath) {
38
61
  return new Promise((resolve, reject) => {
@@ -80,7 +103,11 @@ export async function putPresignedStream(url, srcPath, contentType, timeoutMs =
80
103
  const resp = await fetch(url, {
81
104
  method: "PUT",
82
105
  body: webStream,
83
- headers: { "Content-Type": contentType, "Content-Length": String(contentLength) },
106
+ headers: {
107
+ "Content-Type": contentType,
108
+ "Content-Length": String(contentLength),
109
+ "Content-Encoding": "gzip",
110
+ },
84
111
  duplex: "half",
85
112
  signal: controller.signal,
86
113
  });
@@ -147,7 +174,7 @@ export async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS)
147
174
  // Path / sandbox helpers
148
175
  // ---------------------------------------------------------------------------
149
176
  export function getSandboxRoot() {
150
- const raw = process.env.BARIVIA_WORKSPACE_ROOT ?? process.env.BARSOM_WORKSPACE_ROOT;
177
+ const raw = process.env.BARIVIA_WORKSPACE_ROOT;
151
178
  if (raw)
152
179
  return path.resolve(process.cwd(), raw);
153
180
  const workspaceFolder = process.env.CURSOR_WORKSPACE_FOLDER ?? process.env.WORKSPACE_FOLDER;
@@ -282,7 +309,7 @@ function _traceparentHeader() {
282
309
  export async function apiCall(method, pathPart, body, extraHeaders, requestTimeoutMs) {
283
310
  const url = `${API_URL}${pathPart}`;
284
311
  const contentType = extraHeaders?.["Content-Type"] ?? "application/json";
285
- const requestId = Math.random().toString(36).slice(2, 10);
312
+ const requestId = newRequestId();
286
313
  const headers = {
287
314
  Authorization: `Bearer ${API_KEY}`,
288
315
  "Content-Type": contentType,
@@ -310,7 +337,17 @@ export async function apiCall(method, pathPart, body, extraHeaders, requestTimeo
310
337
  const text = await resp.text();
311
338
  if (!resp.ok) {
312
339
  if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
313
- await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
340
+ const delayMs = retryDelayMs(attempt);
341
+ logWarn("API retry", {
342
+ rid: requestId,
343
+ action: method,
344
+ path: pathPart,
345
+ attempt: attempt + 1,
346
+ max_retries: MAX_RETRIES,
347
+ delay_ms: delayMs,
348
+ error_code: `http_${resp.status}`,
349
+ });
350
+ await new Promise((r) => setTimeout(r, delayMs));
314
351
  continue;
315
352
  }
316
353
  logInfo("API response", { rid: requestId, outcome: "error", duration_ms: Date.now() - t0, error_code: `http_${resp.status}` });
@@ -322,7 +359,17 @@ export async function apiCall(method, pathPart, body, extraHeaders, requestTimeo
322
359
  catch (err) {
323
360
  lastError = err;
324
361
  if (attempt < MAX_RETRIES && isTransientError(err)) {
325
- await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
362
+ const delayMs = retryDelayMs(attempt);
363
+ logWarn("API retry", {
364
+ rid: requestId,
365
+ action: method,
366
+ path: pathPart,
367
+ attempt: attempt + 1,
368
+ max_retries: MAX_RETRIES,
369
+ delay_ms: delayMs,
370
+ error_code: err instanceof Error ? err.name : "fetch_error",
371
+ });
372
+ await new Promise((r) => setTimeout(r, delayMs));
326
373
  continue;
327
374
  }
328
375
  if (err instanceof DOMException && err.name === "AbortError" && !err.httpStatus) {
@@ -336,7 +383,7 @@ export async function apiCall(method, pathPart, body, extraHeaders, requestTimeo
336
383
  /** Fetch raw bytes from the API (for image downloads). */
337
384
  export async function apiRawCall(pathPart, requestTimeoutMs) {
338
385
  const url = `${API_URL}${pathPart}`;
339
- const requestId = Math.random().toString(36).slice(2, 10);
386
+ const requestId = newRequestId();
340
387
  const effectiveTimeout = requestTimeoutMs ?? FETCH_TIMEOUT_MS;
341
388
  let lastError;
342
389
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -344,7 +391,16 @@ export async function apiRawCall(pathPart, requestTimeoutMs) {
344
391
  const resp = await fetchWithTimeout(url, { method: "GET", headers: { Authorization: `Bearer ${API_KEY}`, "X-Request-ID": requestId, traceparent: _traceparentHeader() } }, effectiveTimeout);
345
392
  if (!resp.ok) {
346
393
  if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
347
- await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
394
+ const delayMs = retryDelayMs(attempt);
395
+ logWarn("API retry", {
396
+ rid: requestId,
397
+ path: pathPart,
398
+ attempt: attempt + 1,
399
+ max_retries: MAX_RETRIES,
400
+ delay_ms: delayMs,
401
+ error_code: `http_${resp.status}`,
402
+ });
403
+ await new Promise((r) => setTimeout(r, delayMs));
348
404
  continue;
349
405
  }
350
406
  const text = await resp.text();
@@ -359,7 +415,16 @@ export async function apiRawCall(pathPart, requestTimeoutMs) {
359
415
  catch (err) {
360
416
  lastError = err;
361
417
  if (attempt < MAX_RETRIES && isTransientError(err)) {
362
- await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
418
+ const delayMs = retryDelayMs(attempt);
419
+ logWarn("API retry", {
420
+ rid: requestId,
421
+ path: pathPart,
422
+ attempt: attempt + 1,
423
+ max_retries: MAX_RETRIES,
424
+ delay_ms: delayMs,
425
+ error_code: err instanceof Error ? err.name : "fetch_error",
426
+ });
427
+ await new Promise((r) => setTimeout(r, delayMs));
363
428
  continue;
364
429
  }
365
430
  if (err instanceof DOMException && err.name === "AbortError" && !err.httpStatus) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
- import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
3
+ import { runMcpToolAudit } from "../audit.js";
4
4
  import { apiCall, apiRawCall, getClientSupportsMcpApps, getVizPort, getCaptionForImage, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
5
5
  import { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection, sortRank, } from "../figure_sections.js";
6
6
  import { buildMetadataStrip } from "../results_metadata.js";
@@ -178,9 +178,14 @@ export function registerResultsExplorerTool(server) {
178
178
  _meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
179
179
  };
180
180
  registerAppTool(server, "barmesh_results_explorer", toolConfig, async (args) => runMcpToolAudit("barmesh_results_explorer", "default", args, () => handleResultsExplorer(String(args.job_id))));
181
- registerAuditedTool(server, "_barmesh_fetch_figure", "[INTERNAL — host / MCP App only; agents MUST NOT call this.] The Mesh Convergence Results Explorer view uses it to lazy-load one raster figure as base64. From chat, use barmesh_results(action=get) or barmesh_results_explorer instead.", {
182
- job_id: z.string(),
183
- filename: z.string(),
181
+ registerAppTool(server, "_barmesh_fetch_figure", {
182
+ title: "Fetch figure (internal)",
183
+ description: "[INTERNAL — MCP App only; agents MUST NOT call.] Lazy-loads one raster figure as base64 for the Mesh Convergence Results Explorer.",
184
+ inputSchema: {
185
+ job_id: z.string(),
186
+ filename: z.string(),
187
+ },
188
+ _meta: { ui: { resourceUri: RESULTS_EXPLORER_URI, visibility: ["app"] } },
184
189
  }, async ({ job_id, filename }) => {
185
190
  const safeFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "");
186
191
  const mime = mimeForFilename(safeFilename);
package/dist/tools/cfd.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
- import { apiCall, textResult } from "../shared.js";
3
+ import { apiCall, textResult, suggestAfterCfdSubmit } from "../shared.js";
4
4
  import { pollCfdPrepareIfPresent } from "../cfd_prepare.js";
5
5
  export function registerCfdTools(server) {
6
6
  registerAuditedTool(server, "barmesh_mesh_convergence", `Run SOM-based mesh-convergence analysis on an uploaded combined per-cell CSV.
@@ -52,7 +52,7 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
52
52
  const id = data.id;
53
53
  if (id != null) {
54
54
  const prep = data.prepare_job_id != null ? " (dataset prepare complete)" : "";
55
- data.suggested_next_step = `Run barmesh_training_monitor(job_id="${id}") or barmesh_jobs(action=monitor, job_id="${id}")${prep}; on completion call barmesh_results(action=get, job_id="${id}").`;
55
+ data.suggested_next_step = suggestAfterCfdSubmit(String(id), prep);
56
56
  }
57
57
  return textResult(data);
58
58
  });
@@ -85,7 +85,7 @@ COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with diff
85
85
  const data = (await apiCall("POST", "/v1/cfd/richardson", body));
86
86
  const id = data.id;
87
87
  if (id != null)
88
- data.suggested_next_step = `Run barmesh_training_monitor(job_id="${id}") or barmesh_jobs(action=monitor, job_id="${id}"); on completion call barmesh_results(action=get, job_id="${id}").`;
88
+ data.suggested_next_step = suggestAfterCfdSubmit(String(id));
89
89
  return textResult(data);
90
90
  });
91
91
  }
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { registerAuditedTool } from "../audit.js";
7
- import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, } from "../shared.js";
7
+ import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestMeshDatasetPreview, } from "../shared.js";
8
8
  import { GZIP_UPLOAD_HINT } from "../upload_hints.js";
9
9
  /**
10
10
  * Normalize a nullable string field from the API. Returns "" for absent values,
@@ -91,9 +91,11 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
91
91
  if (!name)
92
92
  throw new Error("barmesh_datasets(upload) requires name.");
93
93
  let body;
94
+ let uploadContentType = "text/csv";
94
95
  if (file_path && file_path.length > 0) {
95
96
  await apiCall("GET", "/v1/system/info");
96
97
  const resolved = await resolveFilePathForUpload(file_path, server);
98
+ uploadContentType = resolveUploadContentType(resolved);
97
99
  const lower = resolved.toLowerCase();
98
100
  const isGzipInput = lower.endsWith(".gz");
99
101
  const baseExt = path.extname(isGzipInput ? lower.slice(0, -3) : lower);
@@ -134,10 +136,10 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
134
136
  id: datasetId,
135
137
  status: init.status,
136
138
  idempotent_replay: true,
137
- suggested_next_step: `barmesh_datasets(action=preview, dataset_id=${datasetId})`,
139
+ suggested_next_step: suggestMeshDatasetPreview(datasetId),
138
140
  });
139
141
  }
140
- await putPresignedStream(init.upload_url, resolved, init.content_type ?? "application/octet-stream", PRESIGNED_PUT_TIMEOUT_MS, isGzipInput);
142
+ await putPresignedStream(init.upload_url, resolved, init.content_type ?? uploadContentType, PRESIGNED_PUT_TIMEOUT_MS, isGzipInput);
141
143
  const fin = (await apiCall("POST", `/v1/datasets/${datasetId}/finalize`, {}));
142
144
  const jobId = (fin.id ?? fin.job_id);
143
145
  const poll = await pollUntilComplete(jobId, POLL_STAGE_MAX_MS);
@@ -150,21 +152,21 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
150
152
  status: ready ? "ready" : "staging",
151
153
  job_id: jobId,
152
154
  suggested_next_step: ready
153
- ? `barmesh_datasets(action=preview, dataset_id=${datasetId}) to verify mesh, feature, and volume columns.`
154
- : `Still staging; poll barmesh_jobs(action=status, job_id="${jobId}") then barmesh_datasets(action=preview, dataset_id=${datasetId}).`,
155
+ ? suggestMeshDatasetPreview(datasetId)
156
+ : `Still staging; poll barmesh_jobs(action=status, job_id="${jobId}") then ${suggestMeshDatasetPreview(datasetId)}.`,
155
157
  });
156
158
  }
157
159
  if (isGzipInput) {
158
160
  const gzBytes = await fs.readFile(resolved);
159
161
  const data = (await apiCall("POST", "/v1/datasets", gzBytes, {
160
162
  "X-Dataset-Name": name,
161
- "Content-Type": "text/csv",
163
+ "Content-Type": uploadContentType,
162
164
  "Content-Encoding": "gzip",
163
165
  "Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
164
166
  }, UPLOAD_DATASET_TIMEOUT_MS));
165
167
  const gid = data.id ?? data.dataset_id;
166
168
  if (gid != null) {
167
- data.suggested_next_step = `Next: barmesh_datasets(action=preview, dataset_id=${gid}) to verify the mesh, feature, and volume columns.`;
169
+ data.suggested_next_step = suggestMeshDatasetPreview(String(gid));
168
170
  }
169
171
  return textResult(data);
170
172
  }
@@ -179,7 +181,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
179
181
  const GZIP_THRESHOLD = 1024 * 1024;
180
182
  const uploadHeaders = {
181
183
  "X-Dataset-Name": name,
182
- "Content-Type": "text/csv",
184
+ "Content-Type": uploadContentType,
183
185
  "Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
184
186
  };
185
187
  let uploadBody = body;
@@ -190,7 +192,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
190
192
  const data = (await apiCall("POST", "/v1/datasets", uploadBody, uploadHeaders, UPLOAD_DATASET_TIMEOUT_MS));
191
193
  const id = data.id ?? data.dataset_id;
192
194
  if (id != null) {
193
- data.suggested_next_step = `Next: barmesh_datasets(action=preview, dataset_id=${id}) to verify the mesh, feature, and volume columns.`;
195
+ data.suggested_next_step = suggestMeshDatasetPreview(String(id));
194
196
  }
195
197
  return textResult(data);
196
198
  }
@@ -1,9 +1,19 @@
1
1
  import { registerAuditedTool } from "../audit.js";
2
2
  import { apiCall, textResult } from "../shared.js";
3
3
  const OFFLINE_GUIDE = `barmesh: CFD mesh-convergence on the Barivia API.
4
- Two tracks: barmesh_mesh_convergence (SOM fingerprint distances) and barmesh_richardson (classical GCI).
5
- Workflow: barmesh_prepare_mesh_data -> barmesh_datasets(upload) -> barmesh_mesh_convergence / barmesh_richardson -> barmesh_training_monitor (default visual MCP App; post-hoc review when already done) or barmesh_jobs(monitor/status) headless fallback -> barmesh_results(get) -> barmesh_results_explorer for figures.
6
- (API unreachable; this is the offline summary. Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
4
+
5
+ Two tracks:
6
+ - barmesh_mesh_convergence SOM volume-fingerprint distances (symmetric KL + Wasserstein-1 vs reference and stepwise)
7
+ - barmesh_richardson — classical Richardson / GCI on scalar QoIs
8
+
9
+ Workflow:
10
+ 1. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V)
11
+ 2. barmesh_datasets(upload) → barmesh_datasets(preview)
12
+ 3. barmesh_mesh_convergence and/or barmesh_richardson — returns job_id
13
+ 4. barmesh_training_monitor (visual MCP App) or barmesh_jobs(action=monitor) headless — poll until complete
14
+ 5. barmesh_results(action=get) → barmesh_results_explorer for figures
15
+
16
+ (API unreachable; offline summary. Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
7
17
  const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
8
18
  Build ONE combined per-cell CSV across all meshes of the refinement study:
9
19
  - one row per cell; a mesh label column (mesh_id); the physical channels you want compared (e.g. p, U_mag, k, log_epsilon — log-compress turbulence quantities); and a cell-volume column (V) for fingerprint weighting.
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { registerAuditedTool } from "../audit.js";
5
- import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
5
+ import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, suggestAfterRender, } from "../shared.js";
6
6
  import { formatConvergenceReading } from "../convergence_reading.js";
7
7
  const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
8
8
  function formatMeshResultsSummary(jobId, data, summary) {
@@ -84,7 +84,7 @@ NOT FOR: Submitting jobs.`, {
84
84
  status: "completed",
85
85
  render_job_id: renderId,
86
86
  format: fmt,
87
- suggested_next_step: `Figures rendered as ${fmt}. Download with barmesh_results(action=download, job_id="${job_id}") or fetch one file with action=image.`,
87
+ suggested_next_step: suggestAfterRender(job_id, fmt),
88
88
  });
89
89
  }
90
90
  if (action === "image") {
@@ -16,7 +16,7 @@ export function isKernelTrainingComplete(data, status) {
16
16
  return true;
17
17
  return status === "completed";
18
18
  }
19
- export function teCurveLabel(base, _kernelComplete = false) {
19
+ export function teCurveLabel(base) {
20
20
  return `${base} (panel)`;
21
21
  }
22
22
  export function formatCurveSourceNote(data) {