@barivia/barmesh-mcp 0.8.4 → 0.8.7
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.
- package/dist/job_monitor.js +25 -6
- package/dist/job_status_format.js +6 -1
- package/dist/shared.js +195 -13
- package/dist/tools/cfd.js +2 -1
- package/dist/tools/datasets.js +9 -5
- package/dist/tools/results.js +4 -4
- package/package.json +1 -1
package/dist/job_monitor.js
CHANGED
|
@@ -35,14 +35,25 @@ function tailOrderingErrors(data, n = 4) {
|
|
|
35
35
|
export function snapshotFromJob(data, elapsedSec, note) {
|
|
36
36
|
const status = String(data.status ?? "unknown");
|
|
37
37
|
const progress_pct = (data.progress ?? 0) * 100;
|
|
38
|
+
const kernelComplete = data.kernel_complete === true;
|
|
39
|
+
const awaitingFinalize = kernelComplete &&
|
|
40
|
+
status !== "completed" &&
|
|
41
|
+
status !== "failed" &&
|
|
42
|
+
status !== "cancelled";
|
|
38
43
|
const snap = {
|
|
39
44
|
elapsed_sec: elapsedSec,
|
|
40
45
|
status,
|
|
41
|
-
progress_pct:
|
|
46
|
+
progress_pct: awaitingFinalize
|
|
47
|
+
? Math.min(Math.round(progress_pct * 10) / 10, 99.0)
|
|
48
|
+
: Math.round(progress_pct * 10) / 10,
|
|
42
49
|
};
|
|
43
50
|
const phase = str(data, "progress_phase");
|
|
44
|
-
if (
|
|
51
|
+
if (awaitingFinalize) {
|
|
52
|
+
snap.phase = phase && phase !== "complete" ? `${phase}+awaiting_finalize` : "awaiting_finalize";
|
|
53
|
+
}
|
|
54
|
+
else if (phase) {
|
|
45
55
|
snap.phase = phase;
|
|
56
|
+
}
|
|
46
57
|
const epoch = num(data, "epoch");
|
|
47
58
|
const total = num(data, "total_epochs");
|
|
48
59
|
if (epoch != null)
|
|
@@ -58,7 +69,7 @@ export function snapshotFromJob(data, elapsedSec, note) {
|
|
|
58
69
|
const panelTe = num(data, "panel_topographic_error");
|
|
59
70
|
if (panelTe != null)
|
|
60
71
|
snap.panel_te = Math.round(panelTe * 10_000) / 10_000;
|
|
61
|
-
if (
|
|
72
|
+
if (kernelComplete)
|
|
62
73
|
snap.kernel_complete = true;
|
|
63
74
|
const failureStage = str(data, "failure_stage");
|
|
64
75
|
if (failureStage)
|
|
@@ -69,8 +80,12 @@ export function snapshotFromJob(data, elapsedSec, note) {
|
|
|
69
80
|
const tail = tailOrderingErrors(data);
|
|
70
81
|
if (tail && tail.length > 0)
|
|
71
82
|
snap.ordering_errors_tail = tail;
|
|
72
|
-
if (note)
|
|
83
|
+
if (note) {
|
|
73
84
|
snap.note = note;
|
|
85
|
+
}
|
|
86
|
+
else if (awaitingFinalize) {
|
|
87
|
+
snap.note = "kernel complete — awaiting finalize (not fully done)";
|
|
88
|
+
}
|
|
74
89
|
return snap;
|
|
75
90
|
}
|
|
76
91
|
/** True when a new snapshot is worth recording (phase/epoch/progress/status change). */
|
|
@@ -96,7 +111,10 @@ export function shouldRecordSnapshot(prev, next) {
|
|
|
96
111
|
return false;
|
|
97
112
|
}
|
|
98
113
|
export function formatSnapshotLine(s) {
|
|
99
|
-
const
|
|
114
|
+
const progressLabel = s.kernel_complete && s.status !== "completed" && s.status !== "failed" && s.status !== "cancelled"
|
|
115
|
+
? `kernel ${s.progress_pct.toFixed(1)}% (awaiting finalize)`
|
|
116
|
+
: `${s.progress_pct.toFixed(1)}%`;
|
|
117
|
+
const parts = [`[+${s.elapsed_sec}s] ${s.status} ${progressLabel}`];
|
|
100
118
|
if (s.phase)
|
|
101
119
|
parts.push(`phase ${s.phase}`);
|
|
102
120
|
if (s.epoch != null && s.total_epochs != null)
|
|
@@ -107,8 +125,9 @@ export function formatSnapshotLine(s) {
|
|
|
107
125
|
parts.push(`Epoch TE ${s.te.toFixed(4)}`);
|
|
108
126
|
if (s.panel_te != null)
|
|
109
127
|
parts.push(`Panel TE ${s.panel_te.toFixed(4)}`);
|
|
110
|
-
if (s.kernel_complete)
|
|
128
|
+
if (s.kernel_complete && (s.status === "completed" || s.status === "failed" || s.status === "cancelled")) {
|
|
111
129
|
parts.push("kernel complete");
|
|
130
|
+
}
|
|
112
131
|
if (s.failure_stage)
|
|
113
132
|
parts.push(`failure_stage ${s.failure_stage}`);
|
|
114
133
|
if (s.eta_sec != null)
|
|
@@ -42,7 +42,12 @@ export async function formatJobStatusText(job_id, data) {
|
|
|
42
42
|
const runConfig = formatRunConfigTable(data);
|
|
43
43
|
if (runConfig)
|
|
44
44
|
parts.push(runConfig);
|
|
45
|
-
|
|
45
|
+
const kernelComplete = data.kernel_complete === true;
|
|
46
|
+
const awaitingFinalize = kernelComplete && status !== "completed" && status !== "failed" && status !== "cancelled";
|
|
47
|
+
const progressBit = awaitingFinalize
|
|
48
|
+
? `kernel ${Math.min(progress, 99).toFixed(1)}% — awaiting finalize`
|
|
49
|
+
: `${progress.toFixed(1)}%`;
|
|
50
|
+
parts.push(`${jobDesc}: ${status} (${progressBit})`);
|
|
46
51
|
const attempt = data.attempt != null ? Number(data.attempt) : null;
|
|
47
52
|
if (attempt != null && attempt > 1) {
|
|
48
53
|
parts.push(`attempt ${attempt} (job was requeued after worker timeout — not stuck from scratch)`);
|
package/dist/shared.js
CHANGED
|
@@ -35,7 +35,7 @@ function newRequestId() {
|
|
|
35
35
|
return randomUUID();
|
|
36
36
|
}
|
|
37
37
|
/** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
|
|
38
|
-
export const CLIENT_VERSION = "0.8.
|
|
38
|
+
export const CLIENT_VERSION = "0.8.7";
|
|
39
39
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
40
40
|
/** Large per-cell CSV uploads may exceed the default fetch timeout. */
|
|
41
41
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
@@ -45,6 +45,94 @@ export const LARGE_UPLOAD_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
|
45
45
|
export const PRESIGNED_PUT_TIMEOUT_MS = 30 * 60_000; // 30 min
|
|
46
46
|
/** Poll window for the async stage_dataset job. */
|
|
47
47
|
export const POLL_STAGE_MAX_MS = 30 * 60_000; // 30 min
|
|
48
|
+
/** Normalize upload JSON after staging completes — never leave staging:true with train_ready. */
|
|
49
|
+
function finalizeStagedUploadResponse(data, ready) {
|
|
50
|
+
const merged = ready
|
|
51
|
+
? { ...data, ...ready }
|
|
52
|
+
: { ...data };
|
|
53
|
+
const trainReady = merged.train_ready === true || String(merged.status ?? "") === "ready";
|
|
54
|
+
if (trainReady) {
|
|
55
|
+
merged.status = ready?.status ?? merged.status ?? "ready";
|
|
56
|
+
merged.train_ready = true;
|
|
57
|
+
merged.staging = false;
|
|
58
|
+
if (ready?.stage_job_id == null)
|
|
59
|
+
delete merged.stage_job_id;
|
|
60
|
+
}
|
|
61
|
+
return merged;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* After a small POST /v1/datasets upload, poll stage_job_id until the dataset is
|
|
65
|
+
* train-ready (mirrors the large-upload finalize path).
|
|
66
|
+
*/
|
|
67
|
+
export async function awaitDatasetStageIfNeeded(data) {
|
|
68
|
+
const stageJobId = data.stage_job_id != null ? String(data.stage_job_id) : "";
|
|
69
|
+
if (!stageJobId) {
|
|
70
|
+
if (data.train_ready === true || data.status === "ready") {
|
|
71
|
+
return finalizeStagedUploadResponse(data);
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
if (data.train_ready === true || data.status === "ready") {
|
|
76
|
+
return finalizeStagedUploadResponse(data);
|
|
77
|
+
}
|
|
78
|
+
const poll = await pollUntilComplete(stageJobId, POLL_STAGE_MAX_MS);
|
|
79
|
+
if (poll.status === "failed") {
|
|
80
|
+
throw new Error(`Dataset staging failed (stage_job_id=${stageJobId}): ${poll.error ?? "unknown error"}`);
|
|
81
|
+
}
|
|
82
|
+
if (poll.status === "timeout") {
|
|
83
|
+
throw new Error(`Dataset staging timed out after ${POLL_STAGE_MAX_MS / 60000} min (stage_job_id=${stageJobId}). Poll barmesh_jobs(action=status, job_id="${stageJobId}").`);
|
|
84
|
+
}
|
|
85
|
+
const datasetId = String(data.id ?? data.dataset_id ?? "");
|
|
86
|
+
if (datasetId) {
|
|
87
|
+
try {
|
|
88
|
+
const ready = (await apiCall("GET", `/v1/datasets/${datasetId}`));
|
|
89
|
+
return finalizeStagedUploadResponse(data, ready);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* fall through */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return finalizeStagedUploadResponse({ ...data, status: "ready", train_ready: true });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Before mesh_convergence submit: ensure the dataset has finished staging.
|
|
99
|
+
*/
|
|
100
|
+
export async function ensureDatasetTrainReady(datasetId) {
|
|
101
|
+
const ds = (await apiCall("GET", `/v1/datasets/${datasetId}`));
|
|
102
|
+
if (ds.train_ready === true)
|
|
103
|
+
return;
|
|
104
|
+
if (String(ds.status) === "failed") {
|
|
105
|
+
const err = ds.ingest_error != null ? String(ds.ingest_error) : "staging failed";
|
|
106
|
+
throw new Error(`Dataset ${datasetId} staging failed: ${err}`);
|
|
107
|
+
}
|
|
108
|
+
const stageJobId = ds.stage_job_id != null ? String(ds.stage_job_id) : "";
|
|
109
|
+
if (stageJobId) {
|
|
110
|
+
const poll = await pollUntilComplete(stageJobId, POLL_STAGE_MAX_MS);
|
|
111
|
+
if (poll.status === "failed") {
|
|
112
|
+
throw new Error(`Dataset staging failed (stage_job_id=${stageJobId}): ${poll.error ?? "unknown error"}`);
|
|
113
|
+
}
|
|
114
|
+
if (poll.status === "timeout") {
|
|
115
|
+
throw new Error(`Dataset staging timed out (stage_job_id=${stageJobId}). Poll barmesh_jobs(action=status, job_id="${stageJobId}").`);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const deadline = Date.now() + 60_000;
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
122
|
+
const again = (await apiCall("GET", `/v1/datasets/${datasetId}`));
|
|
123
|
+
if (again.train_ready === true)
|
|
124
|
+
return;
|
|
125
|
+
if (String(again.status) === "failed") {
|
|
126
|
+
const err = again.ingest_error != null ? String(again.ingest_error) : "staging failed";
|
|
127
|
+
throw new Error(`Dataset ${datasetId} staging failed: ${err}`);
|
|
128
|
+
}
|
|
129
|
+
if (again.stage_job_id != null) {
|
|
130
|
+
await ensureDatasetTrainReady(datasetId);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Dataset ${datasetId} is not train_ready (status=${ds.status ?? "?"}). Wait for staging or poll barmesh_datasets(action=get).`);
|
|
135
|
+
}
|
|
48
136
|
/** Map a local upload path to the API Content-Type (csv vs tsv; ignores .gz suffix). */
|
|
49
137
|
export function resolveUploadContentType(filePath) {
|
|
50
138
|
const lower = filePath.toLowerCase();
|
|
@@ -261,9 +349,48 @@ function enforceWorkspaceSandboxUpload() {
|
|
|
261
349
|
return false;
|
|
262
350
|
return true;
|
|
263
351
|
}
|
|
352
|
+
/** Extra realpath roots allowed for uploads (comma- or colon-separated absolute paths). */
|
|
353
|
+
export function getWorkspaceExtraRoots() {
|
|
354
|
+
const raw = process.env.BARIVIA_WORKSPACE_EXTRA_ROOTS ?? "";
|
|
355
|
+
if (!raw.trim())
|
|
356
|
+
return [];
|
|
357
|
+
return raw
|
|
358
|
+
.split(/[,:]/)
|
|
359
|
+
.map((s) => s.trim())
|
|
360
|
+
.filter(Boolean)
|
|
361
|
+
.map((s) => path.resolve(s));
|
|
362
|
+
}
|
|
363
|
+
async function realpathIfExists(p) {
|
|
364
|
+
try {
|
|
365
|
+
return await fs.realpath(p);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export async function isPathUnderSandboxRoots(realPath, workspaceRoot) {
|
|
372
|
+
const candidates = [workspaceRoot, ...getWorkspaceExtraRoots()];
|
|
373
|
+
for (const root of candidates) {
|
|
374
|
+
const realRoot = await realpathIfExists(root);
|
|
375
|
+
if (!realRoot)
|
|
376
|
+
continue;
|
|
377
|
+
if (realPath === realRoot || realPath.startsWith(realRoot + path.sep))
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
function sandboxEscapeError(realPath, workspaceRoot) {
|
|
383
|
+
const extras = getWorkspaceExtraRoots();
|
|
384
|
+
const extraHint = extras.length > 0
|
|
385
|
+
? ` Also checked BARIVIA_WORKSPACE_EXTRA_ROOTS (${extras.join(", ")}).`
|
|
386
|
+
: " Set BARIVIA_WORKSPACE_EXTRA_ROOTS to allow shared fixture dirs, or copy the file into the workspace.";
|
|
387
|
+
return new Error(`Resolved path escapes workspace — symlink resolves outside BARIVIA_WORKSPACE_ROOT` +
|
|
388
|
+
` (${workspaceRoot}). Target: ${realPath}.${extraHint}`);
|
|
389
|
+
}
|
|
264
390
|
/**
|
|
265
391
|
* Resolve file_path for dataset upload. Rejects "..", enforces workspace sandbox
|
|
266
392
|
* by default (set BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0 to allow absolute paths).
|
|
393
|
+
* Symlink targets under BARIVIA_WORKSPACE_EXTRA_ROOTS are allowed when sandbox is on.
|
|
267
394
|
*/
|
|
268
395
|
export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
269
396
|
const trimmed = filePath.trim();
|
|
@@ -272,9 +399,8 @@ export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
|
272
399
|
const checkUnder = async (p) => {
|
|
273
400
|
const root = await getWorkspaceRootAsync(mcpServer);
|
|
274
401
|
const real = await fs.realpath(p);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
throw new Error(`Path outside the workspace is disabled (BARIVIA_ENFORCE_WORKSPACE_SANDBOX). Workspace root: ${realRoot}`);
|
|
402
|
+
if (!(await isPathUnderSandboxRoots(real, root))) {
|
|
403
|
+
throw sandboxEscapeError(real, root);
|
|
278
404
|
}
|
|
279
405
|
const stat = await fs.stat(real);
|
|
280
406
|
if (!stat.isFile())
|
|
@@ -294,6 +420,9 @@ export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
|
294
420
|
}
|
|
295
421
|
const root = await getWorkspaceRootAsync(mcpServer);
|
|
296
422
|
const resolved = path.resolve(root, trimmed);
|
|
423
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
424
|
+
throw new Error("Path must be within the workspace directory.");
|
|
425
|
+
}
|
|
297
426
|
return checkUnder(resolved);
|
|
298
427
|
}
|
|
299
428
|
function looksLikeHtml(bodyText) {
|
|
@@ -357,7 +486,15 @@ export function classifyApiError(status, bodyText, requestId) {
|
|
|
357
486
|
}
|
|
358
487
|
else if (status === 409) {
|
|
359
488
|
cause = "client";
|
|
360
|
-
|
|
489
|
+
if (apiCode === "staging_not_ready") {
|
|
490
|
+
const stageId = parsed?.stage_job_id != null ? String(parsed.stage_job_id) : "";
|
|
491
|
+
hint = stageId
|
|
492
|
+
? `Wait for stage_dataset to finish: barmesh_jobs(action=monitor, job_id="${stageId}"), then retry. Prefer barmesh_datasets(action=get) train_ready=true.`
|
|
493
|
+
: "Wait for dataset staging (status=ready with staged_prefix / train_ready=true), then retry.";
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
hint = "The job may not be in the expected state.";
|
|
497
|
+
}
|
|
361
498
|
retryable = false;
|
|
362
499
|
}
|
|
363
500
|
else if (status === 429) {
|
|
@@ -685,20 +822,65 @@ export function textResult(data) {
|
|
|
685
822
|
}
|
|
686
823
|
export async function pollUntilComplete(jobId, maxWaitMs = 30_000, intervalMs = 2000) {
|
|
687
824
|
const start = Date.now();
|
|
825
|
+
/** Brief 404s happen when finalize_job_id is visible before the child row is readable. */
|
|
826
|
+
let notFoundStreak = 0;
|
|
827
|
+
const maxNotFoundRetries = 5;
|
|
688
828
|
while (Date.now() - start < maxWaitMs) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
829
|
+
try {
|
|
830
|
+
const data = (await apiCall("GET", `/v1/jobs/${jobId}`));
|
|
831
|
+
notFoundStreak = 0;
|
|
832
|
+
const status = data.status;
|
|
833
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
834
|
+
return {
|
|
835
|
+
status,
|
|
836
|
+
result_ref: data.result_ref,
|
|
837
|
+
error: data.error,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
const status = err?.httpStatus;
|
|
843
|
+
if (status === 404 && notFoundStreak < maxNotFoundRetries) {
|
|
844
|
+
notFoundStreak += 1;
|
|
845
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
throw err;
|
|
697
849
|
}
|
|
698
850
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
699
851
|
}
|
|
700
852
|
return { status: "timeout" };
|
|
701
853
|
}
|
|
854
|
+
const RESULTS_FIGURE_ENUMS = new Set(["default", "all", "none"]);
|
|
855
|
+
/**
|
|
856
|
+
* Coerce host-stringified figures args (e.g. `'["all"]'`) and singleton enum arrays
|
|
857
|
+
* (`["all"]` → `"all"`) before Zod validation.
|
|
858
|
+
*/
|
|
859
|
+
export function coerceFiguresArg(v) {
|
|
860
|
+
if (v === undefined || v === null)
|
|
861
|
+
return v;
|
|
862
|
+
let val = v;
|
|
863
|
+
if (typeof val === "string") {
|
|
864
|
+
const trimmed = val.trim();
|
|
865
|
+
if ((trimmed.startsWith("[") && trimmed.endsWith("]")) ||
|
|
866
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
867
|
+
try {
|
|
868
|
+
val = JSON.parse(trimmed);
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
return v;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (Array.isArray(val)) {
|
|
876
|
+
const asStrings = val.map((x) => String(x));
|
|
877
|
+
if (asStrings.length === 1 && RESULTS_FIGURE_ENUMS.has(asStrings[0])) {
|
|
878
|
+
return asStrings[0];
|
|
879
|
+
}
|
|
880
|
+
return asStrings;
|
|
881
|
+
}
|
|
882
|
+
return val;
|
|
883
|
+
}
|
|
702
884
|
// ---------------------------------------------------------------------------
|
|
703
885
|
// Image helpers
|
|
704
886
|
// ---------------------------------------------------------------------------
|
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, suggestAfterCfdSubmit } from "../shared.js";
|
|
3
|
+
import { apiCall, ensureDatasetTrainReady, 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.
|
|
@@ -53,6 +53,7 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
53
53
|
body.description = description;
|
|
54
54
|
if (tags !== undefined)
|
|
55
55
|
body.tags = tags;
|
|
56
|
+
await ensureDatasetTrainReady(String(dataset_id));
|
|
56
57
|
const data = (await apiCall("POST", "/v1/cfd/mesh-convergence", body));
|
|
57
58
|
await pollCfdPrepareIfPresent(data, "barmesh_mesh_convergence");
|
|
58
59
|
const id = data.id;
|
package/dist/tools/datasets.js
CHANGED
|
@@ -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, resolveUploadContentType, suggestMeshDatasetPreview, } 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, awaitDatasetStageIfNeeded, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestMeshDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../upload_hints.js";
|
|
9
9
|
import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
|
|
10
10
|
/**
|
|
@@ -178,7 +178,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
178
178
|
gzHeaders["X-Dataset-Description"] = description;
|
|
179
179
|
if (tags !== undefined)
|
|
180
180
|
gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
181
|
-
const data = (await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
181
|
+
const data = await awaitDatasetStageIfNeeded((await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS)));
|
|
182
182
|
const gid = data.id ?? data.dataset_id;
|
|
183
183
|
if (gid != null) {
|
|
184
184
|
data.suggested_next_step = suggestMeshDatasetPreview(String(gid));
|
|
@@ -208,7 +208,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
208
208
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
|
209
209
|
uploadHeaders["Content-Encoding"] = "gzip";
|
|
210
210
|
}
|
|
211
|
-
const data = (await apiCall("POST", "/v1/datasets", uploadBody, uploadHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
211
|
+
const data = await awaitDatasetStageIfNeeded((await apiCall("POST", "/v1/datasets", uploadBody, uploadHeaders, UPLOAD_DATASET_TIMEOUT_MS)));
|
|
212
212
|
const id = data.id ?? data.dataset_id;
|
|
213
213
|
if (id != null) {
|
|
214
214
|
data.suggested_next_step = suggestMeshDatasetPreview(String(id));
|
|
@@ -259,16 +259,20 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
259
259
|
throw new Error("barmesh_datasets(get) requires dataset_id.");
|
|
260
260
|
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
261
261
|
const tagStr = formatTags(ds.tags);
|
|
262
|
+
const trainReady = ds.train_ready === true;
|
|
262
263
|
const lines = [
|
|
263
264
|
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
264
|
-
`Status: ${ds.status ?? "
|
|
265
|
+
`Status: ${ds.status ?? "unknown"}`,
|
|
266
|
+
`Train ready: ${trainReady ? "yes" : "no"}`,
|
|
265
267
|
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
266
268
|
ds.size_bytes != null ? `Size: ${Number(ds.size_bytes).toLocaleString()} bytes` : "",
|
|
267
269
|
ds.description != null && String(ds.description).trim() !== "" ? `Description: ${String(ds.description)}` : "",
|
|
268
270
|
tagStr ? `Tags: ${tagStr}` : "",
|
|
269
271
|
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
270
272
|
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
271
|
-
ds.stage_job_id != null
|
|
273
|
+
!trainReady && ds.stage_job_id != null
|
|
274
|
+
? `Stage job: ${String(ds.stage_job_id)} (poll barmesh_jobs(action=status) until train_ready)`
|
|
275
|
+
: "",
|
|
272
276
|
cleanNullable(ds.ingest_error) ? `Ingest error: ${cleanNullable(ds.ingest_error)}` : "",
|
|
273
277
|
ds.created_at != null ? `Created: ${String(ds.created_at)}` : "",
|
|
274
278
|
].filter(Boolean);
|
package/dist/tools/results.js
CHANGED
|
@@ -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, suggestAfterRender, } from "../shared.js";
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, suggestAfterRender, coerceFiguresArg, } from "../shared.js";
|
|
6
6
|
import { formatConvergenceReading } from "../convergence_reading.js";
|
|
7
7
|
import { formatRichardsonResultsSummary, isRichardsonJob } from "../richardson_results.js";
|
|
8
8
|
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
|
|
@@ -73,7 +73,7 @@ export function registerResultsTool(server) {
|
|
|
73
73
|
| feature_divergences | Refresh feature-plane KL matrices / optional Wasserstein on SOM component planes (async; not mesh fingerprint KL). |
|
|
74
74
|
|
|
75
75
|
BEST FOR: After barmesh_jobs(action=status) shows completed (and finalize finished if defer_figures was used).
|
|
76
|
-
FIGURES: Default bundle is combined.png, overview_distances.png (all KL/EMD panels), plot_vol_all_meshes.png, plot_vol_steps.png, and learning_curve.png — not redundant per-feature PNGs or divergence_kl. action=get inlines the headline set; pass figures="all" for every artifact listed in summary.files, figures="none" for metrics only (recommended for agents).
|
|
76
|
+
FIGURES: Default bundle is combined.png, overview_distances.png (all KL/EMD panels), plot_vol_all_meshes.png, plot_vol_steps.png, and learning_curve.png — not redundant per-feature PNGs or divergence_kl. Prefer string figures="all" / "none" (not an array); arrays are for specific base names or filenames. Hosts that stringify arrays are coerced. action=get inlines the headline set; pass figures="all" for every artifact listed in summary.files, figures="none" for metrics only (recommended for agents).
|
|
77
77
|
FEATURE-PLANE KL: mesh SKL/EMD (KL_ref, EMD_*) compare meshes; divergence_kl / feature_divergences.json compare SOM component planes (barsom-parallel). Finalize already writes symmetric KL; use action=feature_divergences for W1 or a refresh.
|
|
78
78
|
PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image or action=download.
|
|
79
79
|
If GET /v1/results returns 404 shortly after status=completed, cfd_finalize may still be rendering — poll barmesh_jobs(status) until finalize_job_id completes.
|
|
@@ -85,10 +85,10 @@ NOT FOR: Submitting jobs.`, {
|
|
|
85
85
|
job_id: z.string().describe("Job ID"),
|
|
86
86
|
filename: z.string().optional().describe("For action=image: the figure filename (e.g. KL_ref.png or, after render, KL_ref.pdf)"),
|
|
87
87
|
format: z.enum(["pdf", "png", "svg"]).optional().describe("For action=render: output format to generate (default pdf). PNG previews already exist by default."),
|
|
88
|
-
figures: z
|
|
88
|
+
figures: z.preprocess(coerceFiguresArg, z
|
|
89
89
|
.union([z.enum(["default", "all", "none"]), z.array(z.string())])
|
|
90
90
|
.optional()
|
|
91
|
-
.describe("
|
|
91
|
+
.describe("Prefer string 'all'/'none'. get/download: omit=headline panels; 'all'; 'none'; or array of base names/filenames. Stringified JSON arrays from hosts are accepted.")),
|
|
92
92
|
folder: z.string().optional().describe("For action=download: directory to save into (relative to workspace). Files land in a per-job subfolder."),
|
|
93
93
|
include_json: z.boolean().optional().describe("For action=download: also save summary.json and text/CSV artifacts"),
|
|
94
94
|
metrics: z
|