@tuned-tensor/cli 0.20.0 → 0.22.0

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.
@@ -615,23 +615,52 @@ var TRAINING_MODELS = [
615
615
  gradientCheckpointing: true
616
616
  }
617
617
  ];
618
- function resolveTrainingModel(modelId) {
618
+ var HUGGING_FACE_REPO_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
619
+ function uncertifiedTrainingModel(id) {
620
+ return {
621
+ id,
622
+ family: "uncertified",
623
+ modelLoader: "causal_lm",
624
+ defaultLearningRate: 1e-4,
625
+ defaultPerDeviceBatchSize: 1,
626
+ defaultGradientAccumulationSteps: 8,
627
+ defaultLoraRank: 16,
628
+ defaultLoraAlpha: 32,
629
+ defaultLoraDropout: 0.05,
630
+ defaultMaxSeqLength: 2048,
631
+ loraTargetModules: "all-linear",
632
+ gradientCheckpointing: true
633
+ };
634
+ }
635
+ function findCertifiedTrainingModel(modelId) {
619
636
  const normalized = modelId.trim().toLowerCase();
620
- const model = TRAINING_MODELS.find(
621
- (candidate) => candidate.id.toLowerCase() === normalized
637
+ return TRAINING_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
638
+ }
639
+ function isCertifiedTrainingModel(modelId) {
640
+ return findCertifiedTrainingModel(modelId) !== void 0;
641
+ }
642
+ function resolveTrainingModel(modelId) {
643
+ const model = findCertifiedTrainingModel(modelId);
644
+ if (model) return model;
645
+ const id = modelId.trim();
646
+ if (HUGGING_FACE_REPO_ID.test(id)) return uncertifiedTrainingModel(id);
647
+ throw new Error(
648
+ `Unsupported base model "${modelId}". Use a certified model (${TRAINING_MODELS.map((item) => item.id).join(", ")}) or a Hugging Face repo id such as owner/name.`
649
+ );
650
+ }
651
+ function uncertifiedRevisionError(modelId) {
652
+ return new Error(
653
+ `${modelId} is not a TT-certified base model. To fine-tune it anyway, set hyperparameters.base_model_revision to its 40-character commit SHA (see https://huggingface.co/${modelId}/commits/main). Uncertified models load as text causal LMs with safetensors weights and no remote code.`
622
654
  );
623
- if (!model) {
624
- throw new Error(
625
- `Unsupported base model "${modelId}". Supported models: ${TRAINING_MODELS.map((item) => item.id).join(", ")}`
626
- );
627
- }
628
- return model;
629
655
  }
630
656
  function canonicalizeTrainingModel(modelId) {
631
657
  return resolveTrainingModel(modelId).id;
632
658
  }
633
659
  function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
634
660
  const model = resolveTrainingModel(modelId);
661
+ if (!isCertifiedTrainingModel(model.id) && !requestedRevision) {
662
+ throw uncertifiedRevisionError(model.id);
663
+ }
635
664
  if (model.defaultRevision && requestedRevision && requestedRevision !== model.defaultRevision) {
636
665
  throw new Error(
637
666
  `${model.id} must use certified revision ${model.defaultRevision}; got ${requestedRevision}.`
@@ -640,7 +669,7 @@ function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
640
669
  return requestedRevision ?? model.defaultRevision;
641
670
  }
642
671
  function defaultBaseModelRevision(modelId) {
643
- return resolveRequestedBaseModelRevision(modelId);
672
+ return resolveTrainingModel(modelId).defaultRevision;
644
673
  }
645
674
  function resolveModelLoader(modelId) {
646
675
  return resolveTrainingModel(modelId).modelLoader;
@@ -703,6 +732,7 @@ function assertCertifiedBaseModelConfig(value, label = "base-model config.json",
703
732
  throw new Error(`${label} must contain a JSON object.`);
704
733
  }
705
734
  const config = value;
735
+ if (expectedModelId && !isCertifiedTrainingModel(expectedModelId)) return;
706
736
  const expectedFamily = expectedModelId ? resolveTrainingModel(expectedModelId).family : void 0;
707
737
  if (config.model_type === "qwen3_5") {
708
738
  if (expectedFamily && expectedFamily !== "qwen3_5") {
@@ -846,6 +876,7 @@ var TUNED_TENSOR_DIR = ".tuned-tensor";
846
876
  var DEFAULT_ARTIFACT_ROOT = `${TUNED_TENSOR_DIR}/artifacts`;
847
877
  var DEFAULT_PROJECT_STORE_ROOT = `${TUNED_TENSOR_DIR}/store`;
848
878
  var DEFAULT_FOUNDATION_RUNS_DIR = `${TUNED_TENSOR_DIR}/foundation-runs`;
879
+ var DEFAULT_DECISION_RUNS_DIR = `${TUNED_TENSOR_DIR}/decision-runs`;
849
880
  function envHome(env = process.env) {
850
881
  const home = env.HOME?.trim();
851
882
  return home ? resolve3(home) : homedir();
@@ -902,6 +933,38 @@ function pythonEnvironmentPath(kind, hash, env = process.env) {
902
933
  return next;
903
934
  }
904
935
 
936
+ // src/local-runtime/decision-models.ts
937
+ var LAYA_REVISION = "55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851";
938
+ var DECISION_MODELS = [
939
+ {
940
+ id: "convaiinnovations/laya",
941
+ family: "laya",
942
+ revision: LAYA_REVISION,
943
+ parameters: "421M (ModernBERT-large encoder + typed decision head)",
944
+ license: "Apache-2.0",
945
+ defaultLearningRate: 2e-5,
946
+ defaultBatchSize: 8,
947
+ defaultEpochs: 3
948
+ }
949
+ ];
950
+ var DEFAULT_DECISION_MODEL = DECISION_MODELS[0].id;
951
+ function resolveDecisionModel(modelId) {
952
+ const normalized = modelId.trim().toLowerCase();
953
+ const model = DECISION_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
954
+ if (!model) {
955
+ throw new Error(
956
+ `Unsupported decision model "${modelId}". Supported decision models: ${DECISION_MODELS.map((item) => item.id).join(", ")}`
957
+ );
958
+ }
959
+ return model;
960
+ }
961
+ function isDecisionModel(modelId) {
962
+ return DECISION_MODELS.some((candidate) => candidate.id.toLowerCase() === modelId.trim().toLowerCase());
963
+ }
964
+ function canonicalizeDecisionModel(modelId) {
965
+ return resolveDecisionModel(modelId).id;
966
+ }
967
+
905
968
  // src/local-runtime/contracts.ts
906
969
  var datasetFormatSchema = z.literal("chat_jsonl");
907
970
  var baseModelRevisionSchema = z.string().regex(/^[0-9a-f]{40}$/i, "base_model_revision must be a 40-character Hugging Face commit SHA").transform((value) => value.toLowerCase());
@@ -947,11 +1010,34 @@ var foundationHyperparametersSchema = z.object({
947
1010
  keep_checkpoints: z.number().int().min(1).max(100).optional(),
948
1011
  log_interval_steps: z.number().int().min(1).max(1e6).optional()
949
1012
  }).strict();
1013
+ var huggingFaceRepoIdSchema = z.string().regex(
1014
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/,
1015
+ "huggingface.repo must be a Hugging Face dataset id such as org/name"
1016
+ );
1017
+ var datasetColumnsSchema = z.object({
1018
+ input: z.string().min(1),
1019
+ output: z.string().min(1)
1020
+ }).strict();
1021
+ var huggingFaceDatasetSourceSchema = z.object({
1022
+ repo: huggingFaceRepoIdSchema,
1023
+ revision: baseModelRevisionSchema.optional(),
1024
+ /**
1025
+ * Field names for plain JSONL records such as `{"text": ..., "label": ...}`.
1026
+ * Each record becomes a chat row with the spec's compiled system message.
1027
+ * Omit for files that are already TT chat JSONL.
1028
+ */
1029
+ columns: datasetColumnsSchema.optional()
1030
+ }).strict();
1031
+ function isRepoRelativePath(value) {
1032
+ if (/^(?:[A-Za-z]:|[/\\]|file:)/.test(value)) return false;
1033
+ return value.split(/[/\\]/).every((part) => part !== "" && part !== "." && part !== "..");
1034
+ }
950
1035
  var datasetPrebuiltSchema = z.object({
951
1036
  training: z.string().min(1),
952
1037
  validation: z.string().min(1).optional(),
953
1038
  test: z.string().min(1).optional(),
954
- format: datasetFormatSchema.default("chat_jsonl")
1039
+ format: datasetFormatSchema.default("chat_jsonl"),
1040
+ huggingface: huggingFaceDatasetSourceSchema.optional()
955
1041
  }).strict().superRefine((dataset, context) => {
956
1042
  if (!dataset.validation && !dataset.test) {
957
1043
  context.addIssue({
@@ -960,7 +1046,57 @@ var datasetPrebuiltSchema = z.object({
960
1046
  message: "Provide a distinct validation or test split for held-out evaluation"
961
1047
  });
962
1048
  }
1049
+ if (!dataset.huggingface) return;
1050
+ for (const key of ["training", "validation", "test"]) {
1051
+ const value = dataset[key];
1052
+ if (value === void 0) continue;
1053
+ if (!isRepoRelativePath(value)) {
1054
+ context.addIssue({
1055
+ code: z.ZodIssueCode.custom,
1056
+ path: [key],
1057
+ message: `With dataset_prebuilt.huggingface, ${key} must be a file path inside the dataset repo, such as data/${key}.jsonl`
1058
+ });
1059
+ } else if (!value.toLowerCase().endsWith(".jsonl")) {
1060
+ context.addIssue({
1061
+ code: z.ZodIssueCode.custom,
1062
+ path: [key],
1063
+ message: `${key} must be a chat JSONL (.jsonl) file; Parquet, CSV and other formats are not supported yet`
1064
+ });
1065
+ }
1066
+ }
963
1067
  });
1068
+ var datasetSourceFileSchema = z.object({
1069
+ path: z.string().min(1),
1070
+ size_bytes: z.number().int().nonnegative(),
1071
+ sha256: z.string().regex(/^[a-f0-9]{64}$/)
1072
+ }).strict();
1073
+ var datasetConversionStatsSchema = z.object({
1074
+ records: z.number().int().nonnegative(),
1075
+ kept: z.number().int().nonnegative(),
1076
+ skipped_missing_fields: z.number().int().nonnegative(),
1077
+ dropped_duplicate_inputs: z.number().int().nonnegative(),
1078
+ dropped_eval_overlap: z.number().int().nonnegative()
1079
+ }).strict();
1080
+ var datasetSourceSchema = z.object({
1081
+ provider: z.literal("huggingface"),
1082
+ repo: huggingFaceRepoIdSchema,
1083
+ requested_revision: baseModelRevisionSchema.nullable(),
1084
+ revision: baseModelRevisionSchema,
1085
+ pinned: z.boolean(),
1086
+ columns: datasetColumnsSchema.nullable(),
1087
+ /** Per-split record accounting when `columns` converted plain JSONL records. */
1088
+ conversion: z.object({
1089
+ training: datasetConversionStatsSchema,
1090
+ validation: datasetConversionStatsSchema.optional(),
1091
+ test: datasetConversionStatsSchema.optional()
1092
+ }).strict().nullable().optional(),
1093
+ snapshot_path: z.string().min(1),
1094
+ files: z.object({
1095
+ training: datasetSourceFileSchema,
1096
+ validation: datasetSourceFileSchema.optional(),
1097
+ test: datasetSourceFileSchema.optional()
1098
+ }).strict()
1099
+ }).strict();
964
1100
  var fineTuneHyperparametersSchema = z.object({
965
1101
  n_epochs: z.number().int().min(1).max(20).default(1),
966
1102
  learning_rate: z.number().positive().optional(),
@@ -974,6 +1110,15 @@ var fineTuneHyperparametersSchema = z.object({
974
1110
  /** Immutable Hugging Face commit used by every stage of the run. */
975
1111
  base_model_revision: baseModelRevisionSchema.optional()
976
1112
  }).strict();
1113
+ function requirePinnedUncertifiedModel(spec, context) {
1114
+ const baseModel = "spec_snapshot" in spec ? spec.spec_snapshot.base_model : spec.base_model;
1115
+ if (isCertifiedTrainingModel(baseModel) || spec.hyperparameters?.base_model_revision) return;
1116
+ context.addIssue({
1117
+ code: z.ZodIssueCode.custom,
1118
+ path: ["hyperparameters", "base_model_revision"],
1119
+ message: uncertifiedRevisionError(baseModel).message
1120
+ });
1121
+ }
977
1122
  var fineTuneRunRequestSchema = z.object({
978
1123
  run_id: z.string().uuid(),
979
1124
  user_id: z.string().min(1),
@@ -981,8 +1126,10 @@ var fineTuneRunRequestSchema = z.object({
981
1126
  run_number: z.number().int().min(1),
982
1127
  spec_snapshot: specSnapshotSchema,
983
1128
  hyperparameters: fineTuneHyperparametersSchema.default({ n_epochs: 1 }),
984
- dataset_prebuilt: datasetPrebuiltSchema.optional()
1129
+ dataset_prebuilt: datasetPrebuiltSchema.optional(),
1130
+ dataset_source: datasetSourceSchema.optional()
985
1131
  }).strict().superRefine((request, context) => {
1132
+ requirePinnedUncertifiedModel(request, context);
986
1133
  if (request.spec_snapshot.examples.length === 0 && !request.dataset_prebuilt) {
987
1134
  context.addIssue({
988
1135
  code: z.ZodIssueCode.custom,
@@ -1070,6 +1217,55 @@ var trainingReportSchema = z.object({
1070
1217
  log_uri: z.string(),
1071
1218
  command: z.array(z.string()).optional()
1072
1219
  }).strict();
1220
+ var runProvenanceSchema = z.object({
1221
+ schema_version: z.literal(1),
1222
+ recorded_at: z.string(),
1223
+ software: z.object({
1224
+ tt_version: z.string(),
1225
+ node_version: z.string(),
1226
+ platform: z.string(),
1227
+ arch: z.string(),
1228
+ runtime_fingerprint: z.string(),
1229
+ python_lock_sha256: z.string().nullable()
1230
+ }).strict(),
1231
+ execution: z.object({
1232
+ target: z.enum(["local", "aws"]),
1233
+ training_backend: z.string(),
1234
+ instance_id: z.string().optional(),
1235
+ region: z.string().optional()
1236
+ }).strict(),
1237
+ spec: z.object({
1238
+ behavior_spec_id: z.string(),
1239
+ run_number: z.number().int(),
1240
+ snapshot_sha256: z.string(),
1241
+ system_prompt_sha256: z.string(),
1242
+ file_path: z.string().optional(),
1243
+ file_sha256: z.string().optional()
1244
+ }).strict(),
1245
+ base_model: z.object({
1246
+ id: z.string(),
1247
+ revision: z.string().nullable(),
1248
+ fingerprint: z.string().nullable()
1249
+ }).strict(),
1250
+ data: z.object({
1251
+ prebuilt: z.boolean(),
1252
+ format: z.string().nullable(),
1253
+ file_sha256: z.record(z.string(), z.string()),
1254
+ compiled_training_sha256: z.string().nullable(),
1255
+ training_examples: z.number().int().nonnegative().nullable(),
1256
+ eval_split: z.string(),
1257
+ eval_sample_seed: z.number().int(),
1258
+ eval_examples_total: z.number().int().nonnegative(),
1259
+ eval_examples_used: z.number().int().nonnegative()
1260
+ }).strict(),
1261
+ training: z.object({
1262
+ hyperparameters: z.record(z.string(), z.unknown()),
1263
+ exit_code: z.number().int().nullable(),
1264
+ log_uri: z.string()
1265
+ }).strict(),
1266
+ /** Hash over request, runtime, preparation config and input digests. */
1267
+ source_fingerprint: z.string()
1268
+ }).strict();
1073
1269
  var runReportSchema = z.object({
1074
1270
  run_id: z.string().uuid(),
1075
1271
  behavior_spec_id: z.string().uuid(),
@@ -1095,6 +1291,7 @@ var runReportSchema = z.object({
1095
1291
  failures: z.array(z.string())
1096
1292
  }).strict().optional(),
1097
1293
  training: trainingReportSchema,
1294
+ provenance: runProvenanceSchema.optional(),
1098
1295
  artifact_uris: z.object({
1099
1296
  dataset: z.string(),
1100
1297
  baseline_eval: z.string(),
@@ -1109,6 +1306,7 @@ var runReportSchema = z.object({
1109
1306
  dataset_prebuilt: z.boolean(),
1110
1307
  dataset_format: datasetFormatSchema.nullable().optional(),
1111
1308
  dataset_uri: z.string(),
1309
+ dataset_source: datasetSourceSchema.optional(),
1112
1310
  spec_example_count: z.number().int().nonnegative(),
1113
1311
  training_example_count: z.number().int().nonnegative().nullable(),
1114
1312
  eval_examples_total: z.number().int().nonnegative(),
@@ -1205,9 +1403,54 @@ var localAdapterSpecFileSchema = specSnapshotSchema.extend({
1205
1403
  id: z.string().uuid().optional(),
1206
1404
  hyperparameters: fineTuneHyperparametersSchema.optional(),
1207
1405
  dataset_prebuilt: datasetPrebuiltSchema.optional()
1406
+ }).strict().superRefine(requirePinnedUncertifiedModel);
1407
+ var decisionQuestionSchema = z.discriminatedUnion("type", [
1408
+ z.object({
1409
+ type: z.literal("choice"),
1410
+ /** Answer label -> description the model reads. Key order is option order. */
1411
+ criteria: z.record(z.string().min(1), z.string()).refine(
1412
+ (criteria) => Object.keys(criteria).length >= 2 && Object.keys(criteria).length <= 32,
1413
+ "A choice decision needs 2 to 32 labels."
1414
+ )
1415
+ }).strict(),
1416
+ z.object({
1417
+ type: z.literal("noul"),
1418
+ criteria: z.object({ true: z.string().optional(), false: z.string().optional() }).strict().optional()
1419
+ }).strict(),
1420
+ z.object({
1421
+ type: z.literal("score"),
1422
+ /** Level descriptions, index 0 first; example outputs are level indices. */
1423
+ criteria: z.array(z.string().min(1)).min(2).max(10)
1424
+ }).strict()
1425
+ ]);
1426
+ var decisionHyperparametersSchema = z.object({
1427
+ n_epochs: z.number().int().min(1).max(50).optional(),
1428
+ learning_rate: z.number().positive().max(1).optional(),
1429
+ batch_size: z.number().int().min(1).max(256).optional(),
1430
+ /** Train only the decision head; faster on CPU, usually less accurate. */
1431
+ freeze_encoder: z.boolean().optional(),
1432
+ /** Shuffle option order per training row so the head learns meaning, not position. */
1433
+ shuffle_options: z.boolean().optional(),
1434
+ seed: z.number().int().min(0).max(2147483647).optional(),
1435
+ device: z.enum(["auto", "cpu", "cuda", "mps"]).optional()
1208
1436
  }).strict();
1437
+ var localDecisionSpecFileSchema = behaviorSpecSchema.extend({
1438
+ ...projectWorkflowFields,
1439
+ engine: z.literal("decision"),
1440
+ id: z.string().uuid().optional(),
1441
+ // Refine first: a union member must report an issue, not throw, for other engines' specs.
1442
+ base_model: z.string().refine(isDecisionModel, `Unsupported decision model. Supported decision models: ${DECISION_MODELS.map((model) => model.id).join(", ")}`).transform((value) => canonicalizeDecisionModel(value)),
1443
+ decision: decisionQuestionSchema,
1444
+ hyperparameters: decisionHyperparametersSchema.optional()
1445
+ });
1446
+ function decisionLabels(question) {
1447
+ if (question.type === "choice") return Object.keys(question.criteria);
1448
+ if (question.type === "noul") return ["false", "true"];
1449
+ return question.criteria.map((_, index) => String(index));
1450
+ }
1209
1451
  var localBehaviorSpecFileSchema = z.union([
1210
1452
  localFoundationSpecFileSchema,
1453
+ localDecisionSpecFileSchema,
1211
1454
  localAdapterSpecFileSchema
1212
1455
  ]).superRefine((spec, ctx) => {
1213
1456
  if (spec.pipeline && isFoundationPipeline(parsePipeline(spec.pipeline)) !== (spec.engine === "foundation")) {
@@ -1216,14 +1459,145 @@ var localBehaviorSpecFileSchema = z.union([
1216
1459
  if (spec.engine === "foundation" && (spec.evaluation || spec.runtime?.storeRoot || spec.runtime?.paths)) {
1217
1460
  ctx.addIssue({ code: "custom", path: ["runtime"], message: "Foundation uses foundation settings for evaluation and checkpoints; runtime.artifactRoot and runtime.gpu are supported." });
1218
1461
  }
1462
+ if (spec.engine === "decision" && (spec.evaluation || spec.runtime?.gpu || spec.runtime?.storeRoot || spec.runtime?.paths)) {
1463
+ ctx.addIssue({ code: "custom", path: ["runtime"], message: "Decision specs support runtime.artifactRoot only; the device is set in hyperparameters.device." });
1464
+ }
1219
1465
  });
1220
1466
  function isFoundationSpecFile(spec) {
1221
1467
  return spec.engine === "foundation";
1222
1468
  }
1469
+ function isDecisionSpecFile(spec) {
1470
+ return spec.engine === "decision";
1471
+ }
1472
+
1473
+ // src/local-runtime/dataset.ts
1474
+ import { readFile as readFile2 } from "fs/promises";
1475
+ async function loadNormalizedChatJsonl(path) {
1476
+ const text = await readFile2(path, "utf8");
1477
+ const rows = [];
1478
+ const jsonLines = [];
1479
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
1480
+ if (!line.trim()) continue;
1481
+ let value;
1482
+ try {
1483
+ value = JSON.parse(line);
1484
+ } catch (error) {
1485
+ throw new Error(`Invalid chat JSONL row ${index + 1}: malformed JSON`, { cause: error });
1486
+ }
1487
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1488
+ throw new Error(`Invalid chat JSONL row ${index + 1}: expected an object`);
1489
+ }
1490
+ const messages = value.messages;
1491
+ if (!Array.isArray(messages) || ![2, 3].includes(messages.length) || messages.some(
1492
+ (message) => !message || typeof message !== "object" || Array.isArray(message) || !["system", "user", "assistant"].includes(String(message.role)) || typeof message.content !== "string"
1493
+ )) {
1494
+ throw new Error(
1495
+ `Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
1496
+ );
1497
+ }
1498
+ const row = { messages };
1499
+ const offset = row.messages.length === 3 ? 1 : 0;
1500
+ if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
1501
+ throw new Error(
1502
+ `Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
1503
+ );
1504
+ }
1505
+ rows.push(row);
1506
+ jsonLines.push(JSON.stringify(row));
1507
+ }
1508
+ if (rows.length === 0) throw new Error(`Chat JSONL contains no examples: ${path}`);
1509
+ return {
1510
+ rows,
1511
+ jsonl: jsonLines.join("\n")
1512
+ };
1513
+ }
1514
+ async function normalizeChatJsonlForRelocation(path) {
1515
+ return (await loadNormalizedChatJsonl(path)).jsonl;
1516
+ }
1517
+ function datasetInputIdentity(value) {
1518
+ return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
1519
+ }
1520
+ function buildSystemMessage(spec) {
1521
+ const parts = [];
1522
+ const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
1523
+ const constraints = spec.constraints.map((value) => value.trim()).filter(Boolean);
1524
+ if (spec.system_prompt.trim()) parts.push(spec.system_prompt.trim());
1525
+ if (guidelines.length > 0) {
1526
+ parts.push(`Guidelines:
1527
+ ${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
1528
+ }
1529
+ if (constraints.length > 0) {
1530
+ parts.push(`Constraints:
1531
+ ${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
1532
+ }
1533
+ return parts.join("\n\n") || "Follow the demonstrated behavior.";
1534
+ }
1535
+ function exampleToChatRow(spec, example) {
1536
+ const system = buildSystemMessage(spec);
1537
+ return {
1538
+ messages: [
1539
+ { role: "system", content: system },
1540
+ { role: "user", content: example.input },
1541
+ { role: "assistant", content: example.output }
1542
+ ]
1543
+ };
1544
+ }
1545
+ function compileSpecToJsonl(spec) {
1546
+ return spec.examples.map((example) => JSON.stringify(exampleToChatRow(spec, example))).join("\n");
1547
+ }
1548
+ function examplesFromSpec(spec) {
1549
+ return spec.examples;
1550
+ }
1551
+ async function examplesFromChatJsonl(path) {
1552
+ const normalized = await loadNormalizedChatJsonl(path);
1553
+ const examples = [];
1554
+ for (const [index, row] of normalized.rows.entries()) {
1555
+ const user = row.messages.at(-2);
1556
+ const assistant = row.messages.at(-1);
1557
+ examples.push({
1558
+ input: user.content,
1559
+ output: assistant.content
1560
+ });
1561
+ }
1562
+ return examples;
1563
+ }
1564
+ async function evaluationSuiteFromChatJsonl(path, configuredSystem) {
1565
+ const normalized = await loadNormalizedChatJsonl(path);
1566
+ const inputIdentities = /* @__PURE__ */ new Set();
1567
+ for (const row of normalized.rows) {
1568
+ const input = row.messages.at(-2).content;
1569
+ const identity = input.trim().replace(/\s+/g, " ").toLowerCase();
1570
+ if (inputIdentities.has(identity)) {
1571
+ throw new Error(
1572
+ "General regression data contains duplicate inputs; evaluation prompts must be unique."
1573
+ );
1574
+ }
1575
+ inputIdentities.add(identity);
1576
+ }
1577
+ const systems = new Set(
1578
+ normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
1579
+ );
1580
+ if (systems.size > 1) {
1581
+ throw new Error("General regression rows must use one consistent system message.");
1582
+ }
1583
+ const embeddedSystem = [...systems][0];
1584
+ if (configuredSystem !== void 0 && embeddedSystem !== void 0 && configuredSystem !== embeddedSystem) {
1585
+ throw new Error(
1586
+ "evaluation.generalRegression.systemPrompt must match the system message embedded in the dataset."
1587
+ );
1588
+ }
1589
+ return {
1590
+ examples: normalized.rows.map((row) => ({
1591
+ input: row.messages.at(-2).content,
1592
+ output: row.messages.at(-1).content
1593
+ })),
1594
+ system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
1595
+ };
1596
+ }
1223
1597
 
1224
1598
  // src/local-runtime/local-project.ts
1225
1599
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1226
- import { readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1600
+ import { readFile as readFile3, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1227
1601
  import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
1228
1602
  import { homedir as homedir2 } from "os";
1229
1603
  import { mkdir as mkdir2 } from "fs/promises";
@@ -1360,7 +1734,7 @@ function resolveLocalRunInputPaths(raw, inputPath) {
1360
1734
  const dataset = value.dataset_prebuilt;
1361
1735
  if (dataset && typeof dataset === "object" && !Array.isArray(dataset)) {
1362
1736
  const fields = dataset;
1363
- for (const key of ["training", "validation", "test"]) {
1737
+ if (fields.huggingface === void 0) for (const key of ["training", "validation", "test"]) {
1364
1738
  if (fields[key] !== void 0) fields[key] = resolveLocalReference(fields[key], baseDirectory);
1365
1739
  }
1366
1740
  }
@@ -1432,6 +1806,34 @@ async function initLocalSpecFile(args) {
1432
1806
  });
1433
1807
  await mkdir2(dirname2(args.outputPath), { recursive: true });
1434
1808
  await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
1809
+ `, "utf8");
1810
+ return spec2;
1811
+ }
1812
+ if (args.engine === "decision") {
1813
+ const spec2 = localDecisionSpecFileSchema.parse({
1814
+ engine: "decision",
1815
+ id: randomUUID2(),
1816
+ name: args.name,
1817
+ description: "Typed decision model: one labelled answer per input.",
1818
+ system_prompt: "Describe the decision this local model should make.",
1819
+ guidelines: [],
1820
+ constraints: [],
1821
+ base_model: args.baseModel ?? DEFAULT_DECISION_MODEL,
1822
+ decision: {
1823
+ type: "choice",
1824
+ criteria: {
1825
+ first_label: "Replace this with what the first label means.",
1826
+ second_label: "Replace this with what the second label means."
1827
+ }
1828
+ },
1829
+ examples: [
1830
+ { input: "Replace this with a representative input.", output: "first_label" },
1831
+ { input: "Replace this with a different input.", output: "second_label" }
1832
+ ],
1833
+ hyperparameters: { n_epochs: 3 }
1834
+ });
1835
+ await mkdir2(dirname2(args.outputPath), { recursive: true });
1836
+ await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
1435
1837
  `, "utf8");
1436
1838
  return spec2;
1437
1839
  }
@@ -1491,7 +1893,7 @@ async function initLocalRunnerConfigFile(args) {
1491
1893
  }
1492
1894
  function placeholderIssues(systemPrompt, examples) {
1493
1895
  const issues = [];
1494
- if (/describe the behavior this local model should learn/i.test(systemPrompt)) {
1896
+ if (/describe the (behavior|decision) this local model should (learn|make)/i.test(systemPrompt)) {
1495
1897
  issues.push("system_prompt still contains the generated placeholder");
1496
1898
  }
1497
1899
  examples.forEach((example, index) => {
@@ -1513,6 +1915,14 @@ function assertLocalRunInputReady(request) {
1513
1915
  function foundationPlaceholderIssues(spec) {
1514
1916
  return placeholderIssues(spec.system_prompt, spec.examples);
1515
1917
  }
1918
+ function decisionPlaceholderIssues(spec) {
1919
+ const issues = placeholderIssues(spec.system_prompt, spec.examples);
1920
+ const descriptions = spec.decision.type === "noul" ? Object.values(spec.decision.criteria ?? {}) : Object.values(spec.decision.criteria);
1921
+ if (descriptions.some((value) => /replace this with/i.test(value ?? ""))) {
1922
+ issues.push("decision.criteria still contains generated placeholder text");
1923
+ }
1924
+ return issues;
1925
+ }
1516
1926
  function assertFoundationSpecReady(spec) {
1517
1927
  const issues = foundationPlaceholderIssues(spec);
1518
1928
  if (issues.length > 0) {
@@ -1520,9 +1930,9 @@ function assertFoundationSpecReady(spec) {
1520
1930
  }
1521
1931
  }
1522
1932
  function runRequestFromLocalSpec(spec, options = {}) {
1523
- if (isFoundationSpecFile(spec)) {
1933
+ if (isFoundationSpecFile(spec) || isDecisionSpecFile(spec)) {
1524
1934
  throw new Error(
1525
- "Foundation specs cannot be converted into a LoRA run request. Use `tt pipeline run --spec`."
1935
+ `${isFoundationSpecFile(spec) ? "Foundation" : "Decision"} specs cannot be converted into a LoRA run request. Use \`tt pipeline run --spec\`.`
1526
1936
  );
1527
1937
  }
1528
1938
  const {
@@ -1570,6 +1980,9 @@ function parseLocalRunInput(rawInput, path, options = {}) {
1570
1980
  if (isFoundationSpecFile(spec)) {
1571
1981
  return { kind: "foundation-spec", path, spec };
1572
1982
  }
1983
+ if (isDecisionSpecFile(spec)) {
1984
+ return { kind: "decision-spec", path, spec };
1985
+ }
1573
1986
  return {
1574
1987
  kind: "spec",
1575
1988
  path,
@@ -1579,7 +1992,7 @@ function parseLocalRunInput(rawInput, path, options = {}) {
1579
1992
  }
1580
1993
  async function loadLocalRunInput(path, options = {}) {
1581
1994
  return parseLocalRunInput(
1582
- JSON.parse(await readFile2(path, "utf8")),
1995
+ JSON.parse(await readFile3(path, "utf8")),
1583
1996
  path,
1584
1997
  options
1585
1998
  );
@@ -1594,7 +2007,7 @@ function validateBehaviorSpec(input) {
1594
2007
  return { valid: false, errors: ["Use a behavior spec, not a run request."], warnings };
1595
2008
  }
1596
2009
  const { spec } = input;
1597
- const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
2010
+ const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : input.kind === "decision-spec" ? decisionPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
1598
2011
  if (placeholders.length) {
1599
2012
  errors.push(`Edit the generated behavior spec before training: ${placeholders.join("; ")}.`);
1600
2013
  }
@@ -1609,6 +2022,29 @@ function validateBehaviorSpec(input) {
1609
2022
  errors.push("Foundation RL requires numeric expected outputs in every example.");
1610
2023
  }
1611
2024
  }
2025
+ if (input.kind === "decision-spec") {
2026
+ const labels = decisionLabels(input.spec.decision);
2027
+ const normalized = new Map(labels.map((label) => [label.trim().toLowerCase(), label]));
2028
+ if (normalized.size !== labels.length) {
2029
+ errors.push("decision labels must stay distinct ignoring case and surrounding spaces.");
2030
+ }
2031
+ const counts = /* @__PURE__ */ new Map();
2032
+ for (const [index, example] of spec.examples.entries()) {
2033
+ const label = normalized.get(example.output.trim().toLowerCase());
2034
+ if (!label) {
2035
+ errors.push(`examples[${index}].output must be one of the decision labels: ${labels.join(", ")}.`);
2036
+ } else {
2037
+ counts.set(label, (counts.get(label) ?? 0) + 1);
2038
+ }
2039
+ }
2040
+ const missing = labels.filter((label) => !counts.has(label));
2041
+ if (missing.length && spec.examples.length) {
2042
+ warnings.push(`No examples use decision label(s) ${missing.join(", ")}; the tuned model learns those only from their descriptions.`);
2043
+ }
2044
+ if (buildSystemMessage(spec).length > 600) {
2045
+ warnings.push("The compiled decision instruction exceeds 600 characters; the decision model reads about 190 tokens of instruction and options, so later text may be truncated.");
2046
+ }
2047
+ }
1612
2048
  for (const key of ["name", "system_prompt"]) {
1613
2049
  if (!spec[key].trim()) errors.push(`${key} must not be blank.`);
1614
2050
  }
@@ -1784,7 +2220,7 @@ ${validation.errors.join("\n")}`);
1784
2220
  const pipeline = parsePipeline2(recipe);
1785
2221
  if (isFoundationPipeline2(pipeline) !== foundation) {
1786
2222
  const recipeEngine = isFoundationPipeline2(pipeline) ? "a foundation" : "an adapter";
1787
- const specEngine = foundation ? "a foundation" : "an adapter";
2223
+ const specEngine = foundation ? "a foundation" : input.kind === "decision-spec" ? "a decision" : "an adapter";
1788
2224
  throw new Error(`Pipeline is ${recipeEngine} recipe, but the behavior spec is ${specEngine} spec. Use a matching recipe or omit --file.`);
1789
2225
  }
1790
2226
  if (foundation) assertFoundationSettingsMatch(input.spec.foundation, pipeline);
@@ -1833,177 +2269,346 @@ function assertFoundationSettingsMatch(hp, plan) {
1833
2269
  }
1834
2270
  }
1835
2271
 
1836
- // src/local-runtime/dataset.ts
1837
- import { readFile as readFile3 } from "fs/promises";
1838
- async function loadNormalizedChatJsonl(path) {
1839
- const text = await readFile3(path, "utf8");
1840
- const rows = [];
1841
- const jsonLines = [];
1842
- for (const [index, line] of text.split(/\r?\n/).entries()) {
1843
- if (!line.trim()) continue;
1844
- let value;
1845
- try {
1846
- value = JSON.parse(line);
1847
- } catch (error) {
1848
- throw new Error(`Invalid chat JSONL row ${index + 1}: malformed JSON`, { cause: error });
2272
+ // src/local-runtime/run-reporter.ts
2273
+ function reportInBackground(action) {
2274
+ try {
2275
+ void Promise.resolve(action()).catch(() => void 0);
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ function sanitizeLogLine(line) {
2280
+ return line.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/((?:api[_-]?key|token|secret|password)=)[^\s]+/gi, "$1[redacted]");
2281
+ }
2282
+ function forwardStreamLines(stream, onLine) {
2283
+ let buffered = "";
2284
+ stream.setEncoding("utf8");
2285
+ stream.on("data", (chunk) => {
2286
+ buffered += chunk;
2287
+ const lines = buffered.split(/\r\n|\n|\r/);
2288
+ buffered = lines.pop() ?? "";
2289
+ for (const line of lines) {
2290
+ if (line.trim()) onLine(sanitizeLogLine(line));
1849
2291
  }
1850
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1851
- throw new Error(`Invalid chat JSONL row ${index + 1}: expected an object`);
2292
+ });
2293
+ stream.on("end", () => {
2294
+ if (buffered.trim()) onLine(sanitizeLogLine(buffered));
2295
+ });
2296
+ }
2297
+
2298
+ // src/local-runtime/package-root.ts
2299
+ import { existsSync as existsSync2 } from "fs";
2300
+ import { dirname as dirname3, resolve as resolve5 } from "path";
2301
+ import { fileURLToPath as fileURLToPath2 } from "url";
2302
+ function localRuntimePackageRoot(moduleUrl) {
2303
+ const moduleDirectory = dirname3(fileURLToPath2(moduleUrl));
2304
+ const candidates = [
2305
+ dirname3(moduleDirectory),
2306
+ dirname3(dirname3(moduleDirectory))
2307
+ ];
2308
+ const root = candidates.find(
2309
+ (candidate) => existsSync2(resolve5(candidate, "package.json")) && existsSync2(resolve5(candidate, "training/adapter"))
2310
+ );
2311
+ if (!root) {
2312
+ throw new Error("Unable to locate the bundled Tuned Tensor local runtime assets.");
2313
+ }
2314
+ return root;
2315
+ }
2316
+
2317
+ // src/local-runtime/audit.ts
2318
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
2319
+ import { createReadStream as createReadStream2 } from "fs";
2320
+ import { mkdir as mkdir3, open as open2, readFile as readFile4, rename as rename2, rm, stat as stat3, writeFile as writeFile3 } from "fs/promises";
2321
+ import { hostname } from "os";
2322
+ import { join as join3 } from "path";
2323
+ function canonicalJson3(value) {
2324
+ const normalized = JSON.parse(JSON.stringify(value ?? null));
2325
+ const encode = (entry) => {
2326
+ if (entry === null || typeof entry !== "object") return JSON.stringify(entry);
2327
+ if (Array.isArray(entry)) return `[${entry.map(encode).join(",")}]`;
2328
+ const object = entry;
2329
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${encode(object[key])}`).join(",")}}`;
2330
+ };
2331
+ return encode(normalized);
2332
+ }
2333
+ function eventHash(event) {
2334
+ const { hash: _hash, ...body } = event;
2335
+ return createHash3("sha256").update(canonicalJson3(body)).digest("hex");
2336
+ }
2337
+ function chainEvent(event, previous) {
2338
+ const body = JSON.parse(JSON.stringify({
2339
+ ...event,
2340
+ seq: typeof previous?.seq === "number" ? previous.seq + 1 : 0,
2341
+ prev_hash: previous?.hash ?? null
2342
+ }));
2343
+ return { ...body, hash: eventHash(body) };
2344
+ }
2345
+ function verifyEventChain(events) {
2346
+ let legacy = 0;
2347
+ let unchainedAfter = 0;
2348
+ let previous;
2349
+ const result = (status, brk) => ({
2350
+ status,
2351
+ events: events.length,
2352
+ chained_events: events.filter((event) => event.hash !== void 0).length,
2353
+ legacy_events: legacy,
2354
+ unchained_after_chain: unchainedAfter,
2355
+ head_hash: previous?.hash ?? null,
2356
+ ...brk ? { break: brk } : {}
2357
+ });
2358
+ for (const [index, event] of events.entries()) {
2359
+ const chained = event;
2360
+ if (chained.hash === void 0) {
2361
+ if (previous) unchainedAfter += 1;
2362
+ else legacy += 1;
2363
+ continue;
1852
2364
  }
1853
- const messages = value.messages;
1854
- if (!Array.isArray(messages) || ![2, 3].includes(messages.length) || messages.some(
1855
- (message) => !message || typeof message !== "object" || Array.isArray(message) || !["system", "user", "assistant"].includes(String(message.role)) || typeof message.content !== "string"
1856
- )) {
1857
- throw new Error(
1858
- `Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
1859
- );
2365
+ const expectedSeq = previous ? (previous.seq ?? -1) + 1 : 0;
2366
+ if (chained.seq !== expectedSeq) {
2367
+ return result("broken", { index, seq: chained.seq ?? null, reason: `Expected sequence ${expectedSeq}; an event was removed, reordered or inserted.` });
1860
2368
  }
1861
- const row = { messages };
1862
- const offset = row.messages.length === 3 ? 1 : 0;
1863
- if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
1864
- throw new Error(
1865
- `Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
1866
- );
2369
+ if ((chained.prev_hash ?? null) !== (previous?.hash ?? null)) {
2370
+ return result("broken", { index, seq: chained.seq, reason: "The previous-event hash does not match; an earlier event was changed or removed." });
1867
2371
  }
1868
- rows.push(row);
1869
- jsonLines.push(JSON.stringify(row));
2372
+ if (eventHash(chained) !== chained.hash) {
2373
+ return result("broken", { index, seq: chained.seq, reason: "The event's contents do not match its hash; it was edited." });
2374
+ }
2375
+ previous = chained;
1870
2376
  }
1871
- if (rows.length === 0) throw new Error(`Chat JSONL contains no examples: ${path}`);
1872
- return {
1873
- rows,
1874
- jsonl: jsonLines.join("\n")
1875
- };
2377
+ if (events.length === 0) return result("empty");
2378
+ if (!previous) return result("legacy");
2379
+ if (unchainedAfter > 0) return result("mixed");
2380
+ return result(legacy > 0 ? "legacy" : "verified");
1876
2381
  }
1877
- async function normalizeChatJsonlForRelocation(path) {
1878
- return (await loadNormalizedChatJsonl(path)).jsonl;
2382
+ async function sha256File2(path) {
2383
+ const hash = createHash3("sha256");
2384
+ for await (const chunk of createReadStream2(path)) hash.update(chunk);
2385
+ return hash.digest("hex");
1879
2386
  }
1880
- function buildSystemMessage(spec) {
1881
- const parts = [];
1882
- const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
1883
- const constraints = spec.constraints.map((value) => value.trim()).filter(Boolean);
1884
- if (spec.system_prompt.trim()) parts.push(spec.system_prompt.trim());
1885
- if (guidelines.length > 0) {
1886
- parts.push(`Guidelines:
1887
- ${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
1888
- }
1889
- if (constraints.length > 0) {
1890
- parts.push(`Constraints:
1891
- ${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
2387
+ function processAlive(pid) {
2388
+ try {
2389
+ process.kill(pid, 0);
2390
+ return true;
2391
+ } catch (error) {
2392
+ return error.code === "EPERM";
1892
2393
  }
1893
- return parts.join("\n\n") || "Follow the demonstrated behavior.";
1894
- }
1895
- function exampleToChatRow(spec, example) {
1896
- const system = buildSystemMessage(spec);
1897
- return {
1898
- messages: [
1899
- { role: "system", content: system },
1900
- { role: "user", content: example.input },
1901
- { role: "assistant", content: example.output }
1902
- ]
1903
- };
1904
2394
  }
1905
- function compileSpecToJsonl(spec) {
1906
- return spec.examples.map((example) => JSON.stringify(exampleToChatRow(spec, example))).join("\n");
2395
+ async function readLockOwner(lockPath) {
2396
+ try {
2397
+ return JSON.parse(await readFile4(join3(lockPath, "owner.json"), "utf8"));
2398
+ } catch {
2399
+ return null;
2400
+ }
1907
2401
  }
1908
- function examplesFromSpec(spec) {
1909
- return spec.examples;
2402
+ async function abandonedLockOwner(lockPath, orphanMs, remoteMs) {
2403
+ const metadata = await stat3(lockPath).catch(() => null);
2404
+ if (!metadata) return null;
2405
+ const owner = await readLockOwner(lockPath);
2406
+ const age = Date.now() - metadata.mtimeMs;
2407
+ if (!owner) return age > orphanMs ? "ownerless" : null;
2408
+ if (owner.host === hostname()) return processAlive(owner.pid) ? null : owner;
2409
+ return age > remoteMs ? owner : null;
1910
2410
  }
1911
- async function examplesFromChatJsonl(path) {
1912
- const normalized = await loadNormalizedChatJsonl(path);
1913
- const examples = [];
1914
- for (const [index, row] of normalized.rows.entries()) {
1915
- const user = row.messages.at(-2);
1916
- const assistant = row.messages.at(-1);
1917
- examples.push({
1918
- input: user.content,
1919
- output: assistant.content
1920
- });
2411
+ async function breakAbandonedLock(lockPath, abandoned) {
2412
+ const moved = `${lockPath}.abandoned-${randomUUID3()}`;
2413
+ try {
2414
+ await rename2(lockPath, moved);
2415
+ } catch (error) {
2416
+ if (error.code === "ENOENT") return;
2417
+ throw error;
1921
2418
  }
1922
- return examples;
2419
+ const owner = await readLockOwner(moved);
2420
+ const expected = abandoned === "ownerless" ? void 0 : abandoned.token;
2421
+ if (owner?.token !== expected) {
2422
+ await rename2(moved, lockPath).catch(() => void 0);
2423
+ }
2424
+ await rm(moved, { recursive: true, force: true });
1923
2425
  }
1924
- async function evaluationSuiteFromChatJsonl(path, configuredSystem) {
1925
- const normalized = await loadNormalizedChatJsonl(path);
1926
- const inputIdentities = /* @__PURE__ */ new Set();
1927
- for (const row of normalized.rows) {
1928
- const input = row.messages.at(-2).content;
1929
- const identity = input.trim().replace(/\s+/g, " ").toLowerCase();
1930
- if (inputIdentities.has(identity)) {
1931
- throw new Error(
1932
- "General regression data contains duplicate inputs; evaluation prompts must be unique."
1933
- );
2426
+ async function withAppendLock(lockPath, action, options = {}) {
2427
+ const timeoutMs = options.timeoutMs ?? 6e4;
2428
+ const token = randomUUID3();
2429
+ const deadline = Date.now() + timeoutMs;
2430
+ for (; ; ) {
2431
+ try {
2432
+ await mkdir3(lockPath, { mode: 448 });
2433
+ await writeFile3(join3(lockPath, "owner.json"), JSON.stringify({ token, pid: process.pid, host: hostname() }), { mode: 384 });
2434
+ break;
2435
+ } catch (error) {
2436
+ if (error.code !== "EEXIST") throw error;
2437
+ const abandoned = await abandonedLockOwner(lockPath, options.orphanMs ?? 5e3, options.remoteMs ?? 6e5);
2438
+ if (abandoned) {
2439
+ await breakAbandonedLock(lockPath, abandoned);
2440
+ continue;
2441
+ }
2442
+ if (Date.now() > deadline) throw new Error(`Timed out waiting for the run event log lock: ${lockPath}`);
2443
+ await new Promise((resolve17) => setTimeout(resolve17, 20));
1934
2444
  }
1935
- inputIdentities.add(identity);
1936
- }
1937
- const systems = new Set(
1938
- normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
1939
- );
1940
- if (systems.size > 1) {
1941
- throw new Error("General regression rows must use one consistent system message.");
1942
2445
  }
1943
- const embeddedSystem = [...systems][0];
1944
- if (configuredSystem !== void 0 && embeddedSystem !== void 0 && configuredSystem !== embeddedSystem) {
1945
- throw new Error(
1946
- "evaluation.generalRegression.systemPrompt must match the system message embedded in the dataset."
1947
- );
2446
+ try {
2447
+ return await action();
2448
+ } finally {
2449
+ if ((await readLockOwner(lockPath))?.token === token) await rm(lockPath, { recursive: true, force: true });
1948
2450
  }
1949
- return {
1950
- examples: normalized.rows.map((row) => ({
1951
- input: row.messages.at(-2).content,
1952
- output: row.messages.at(-1).content
1953
- })),
1954
- system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
1955
- };
1956
2451
  }
1957
-
1958
- // src/local-runtime/run-reporter.ts
1959
- function reportInBackground(action) {
2452
+ async function readJsonlTail(path, accept = () => true) {
2453
+ let handle;
1960
2454
  try {
1961
- void Promise.resolve(action()).catch(() => void 0);
1962
- } catch {
2455
+ handle = await open2(path, "r");
2456
+ } catch (error) {
2457
+ if (error.code === "ENOENT") return { last: void 0, partial: false };
2458
+ throw error;
2459
+ }
2460
+ try {
2461
+ const size = (await handle.stat()).size;
2462
+ if (size === 0) return { last: void 0, partial: false };
2463
+ let window = Math.min(size, 64 * 1024);
2464
+ for (; ; ) {
2465
+ const buffer = Buffer.alloc(window);
2466
+ await handle.read(buffer, 0, window, size - window);
2467
+ const text = buffer.toString("utf8");
2468
+ const partial = !text.endsWith("\n");
2469
+ const lines = text.split("\n");
2470
+ const complete = window === size ? lines : lines.slice(1);
2471
+ for (const line of complete.reverse()) {
2472
+ if (!line.trim()) continue;
2473
+ let row;
2474
+ try {
2475
+ row = JSON.parse(line);
2476
+ } catch {
2477
+ continue;
2478
+ }
2479
+ if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
2480
+ if (accept(row)) return { last: row, partial };
2481
+ }
2482
+ if (window === size) return { last: void 0, partial };
2483
+ window = Math.min(size, window * 4);
2484
+ }
2485
+ } finally {
2486
+ await handle.close();
1963
2487
  }
1964
2488
  }
1965
- function sanitizeLogLine(line) {
1966
- return line.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/((?:api[_-]?key|token|secret|password)=)[^\s]+/gi, "$1[redacted]");
1967
- }
1968
- function forwardStreamLines(stream, onLine) {
1969
- let buffered = "";
1970
- stream.setEncoding("utf8");
1971
- stream.on("data", (chunk) => {
1972
- buffered += chunk;
1973
- const lines = buffered.split(/\r\n|\n|\r/);
1974
- buffered = lines.pop() ?? "";
1975
- for (const line of lines) {
1976
- if (line.trim()) onLine(sanitizeLogLine(line));
2489
+ function parseJsonlTolerant(text) {
2490
+ const rows = [];
2491
+ let unreadable = 0;
2492
+ for (const line of text.split(/\r?\n/)) {
2493
+ if (!line.trim()) continue;
2494
+ try {
2495
+ const row = JSON.parse(line);
2496
+ if (row === null || typeof row !== "object" || Array.isArray(row)) {
2497
+ unreadable += 1;
2498
+ continue;
2499
+ }
2500
+ rows.push(row);
2501
+ } catch {
2502
+ unreadable += 1;
1977
2503
  }
1978
- });
1979
- stream.on("end", () => {
1980
- if (buffered.trim()) onLine(sanitizeLogLine(buffered));
1981
- });
2504
+ }
2505
+ return { rows, unreadable };
1982
2506
  }
1983
-
1984
- // src/local-runtime/package-root.ts
1985
- import { existsSync as existsSync2 } from "fs";
1986
- import { dirname as dirname3, resolve as resolve5 } from "path";
1987
- import { fileURLToPath as fileURLToPath2 } from "url";
1988
- function localRuntimePackageRoot(moduleUrl) {
1989
- const moduleDirectory = dirname3(fileURLToPath2(moduleUrl));
1990
- const candidates = [
1991
- dirname3(moduleDirectory),
1992
- dirname3(dirname3(moduleDirectory))
1993
- ];
1994
- const root = candidates.find(
1995
- (candidate) => existsSync2(resolve5(candidate, "package.json")) && existsSync2(resolve5(candidate, "training/adapter"))
1996
- );
1997
- if (!root) {
1998
- throw new Error("Unable to locate the bundled Tuned Tensor local runtime assets.");
2507
+ function buildTimeline(events) {
2508
+ const timeline = [];
2509
+ for (const event of events) {
2510
+ const last2 = timeline.at(-1);
2511
+ if (last2 && last2.stage === event.stage) {
2512
+ last2.status = event.status;
2513
+ last2.message = event.message;
2514
+ continue;
2515
+ }
2516
+ if (last2) {
2517
+ last2.ended_at = event.occurred_at;
2518
+ last2.duration_seconds = Math.max(0, (Date.parse(event.occurred_at) - Date.parse(last2.started_at)) / 1e3);
2519
+ }
2520
+ timeline.push({
2521
+ stage: event.stage,
2522
+ status: event.status,
2523
+ message: event.message,
2524
+ started_at: event.occurred_at,
2525
+ ended_at: null,
2526
+ duration_seconds: null
2527
+ });
1999
2528
  }
2000
- return root;
2529
+ const last = timeline.at(-1);
2530
+ if (last && ["completed", "failed", "cancelled", "stage_completed"].includes(last.stage)) {
2531
+ last.ended_at = last.started_at;
2532
+ last.duration_seconds = 0;
2533
+ }
2534
+ return timeline;
2535
+ }
2536
+ async function auditRun(store, runId) {
2537
+ const state = await store.getRun(runId);
2538
+ const log = await store.getRunEventLog(state.id);
2539
+ const events = log.events;
2540
+ const chain = verifyEventChain(events);
2541
+ const completion = [...events].reverse().find((event) => event.stage === "completed");
2542
+ const recorded = typeof completion?.details?.report_sha256 === "string" ? completion.details.report_sha256 : null;
2543
+ const recordedManifest = typeof completion?.details?.artifact_manifest_sha256 === "string" ? completion.details.artifact_manifest_sha256 : null;
2544
+ const files = await Promise.all((await store.getRunReportPaths(state.id)).map(async (path) => {
2545
+ const sha256 = await sha256File2(path);
2546
+ return { path, sha256, matches_recorded: recorded ? sha256 === recorded : null };
2547
+ }));
2548
+ const manifestPath = state.artifact_dir ? join3(state.artifact_dir, "artifact-manifest.json") : null;
2549
+ const manifestFile = manifestPath && (await stat3(manifestPath).catch(() => null))?.isFile() ? await sha256File2(manifestPath).then((sha256) => ({
2550
+ path: manifestPath,
2551
+ sha256,
2552
+ matches_recorded: recordedManifest ? sha256 === recordedManifest : null
2553
+ })) : null;
2554
+ const report = await store.getRunReport(state.id).catch(() => null);
2555
+ const findings = [];
2556
+ let verdict = "verified";
2557
+ const tampered = (finding) => {
2558
+ findings.push(finding);
2559
+ verdict = "tampered";
2560
+ };
2561
+ const incomplete = (finding) => {
2562
+ findings.push(finding);
2563
+ if (verdict === "verified") verdict = "incomplete";
2564
+ };
2565
+ const completed = state.status === "completed";
2566
+ if (chain.status === "broken") {
2567
+ tampered(`Event log chain is broken at event ${chain.break.index}: ${chain.break.reason}`);
2568
+ } else if (chain.status === "empty") {
2569
+ if (completed) tampered("The run is completed but its event log is missing or empty.");
2570
+ else incomplete("The run has no events.");
2571
+ } else if (chain.status === "legacy") {
2572
+ incomplete(`${chain.legacy_events} event(s) were recorded before event chaining (older TT version) and cannot be verified.`);
2573
+ } else if (chain.status === "mixed") {
2574
+ incomplete(`${chain.unchained_after_chain} event(s) were written without hashes after chaining began, e.g. by an older TT process.`);
2575
+ }
2576
+ if (log.unreadable_lines > 0) incomplete(`${log.unreadable_lines} event line(s) are unreadable, likely from an interrupted write.`);
2577
+ for (const file of files) {
2578
+ if (file.matches_recorded === false) {
2579
+ tampered(`Report ${file.path} changed after the run completed (sha256 ${file.sha256.slice(0, 12)}, recorded ${recorded.slice(0, 12)}).`);
2580
+ }
2581
+ }
2582
+ if (manifestFile?.matches_recorded === false) {
2583
+ tampered(`Artifact manifest ${manifestFile.path} changed after the run completed.`);
2584
+ }
2585
+ if (!completed) {
2586
+ incomplete(`The run is ${state.status}; only completed runs have recorded report digests.`);
2587
+ } else if (!recorded) {
2588
+ if (chain.status === "verified") tampered("The completion event is missing from a chained event log; the log was truncated.");
2589
+ else if (chain.status !== "broken" && chain.status !== "empty") incomplete("No report digest was recorded at completion (older TT version).");
2590
+ } else if (files.length === 0) {
2591
+ incomplete("The recorded report file is missing.");
2592
+ }
2593
+ if (completed && recordedManifest && !manifestFile) incomplete("The artifact manifest recorded at completion is missing.");
2594
+ if (completed && report && !report.provenance) incomplete("The report has no provenance block (older TT version).");
2595
+ return {
2596
+ run_id: state.id,
2597
+ status: state.status,
2598
+ verdict,
2599
+ findings,
2600
+ chain: { ...chain, unreadable_lines: log.unreadable_lines },
2601
+ report: { recorded_sha256: recorded, files },
2602
+ artifact_manifest: { recorded_sha256: recordedManifest, file: manifestFile },
2603
+ timeline: buildTimeline(events),
2604
+ provenance: report?.provenance ?? null
2605
+ };
2001
2606
  }
2002
2607
 
2003
2608
  // src/local-runtime/store.ts
2004
- import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as mkdir3, readdir as readdir3, readFile as readFile4, rename as rename2, rm, stat as stat3, writeFile as writeFile3 } from "fs/promises";
2005
- import { dirname as dirname4, join as join3, resolve as resolve6, sep as sep3 } from "path";
2006
- import { randomUUID as randomUUID3 } from "crypto";
2609
+ import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, rm as rm2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
2610
+ import { dirname as dirname4, join as join4, resolve as resolve6, sep as sep3 } from "path";
2611
+ import { randomUUID as randomUUID4 } from "crypto";
2007
2612
  function isTerminalRunState(state) {
2008
2613
  return state.status === "stage_completed" || state.status === "completed" || state.status === "failed" || state.status === "cancelled" && state.current_stage === "cancelled" && Boolean(state.completed_at);
2009
2614
  }
@@ -2013,22 +2618,22 @@ function defaultLocalHome() {
2013
2618
  function localStorePaths(root) {
2014
2619
  return {
2015
2620
  root,
2016
- specsDir: join3(root, "specs"),
2017
- runsDir: join3(root, "runs"),
2018
- modelsDir: join3(root, "models"),
2019
- datasetsDir: join3(root, "datasets")
2621
+ specsDir: join4(root, "specs"),
2622
+ runsDir: join4(root, "runs"),
2623
+ modelsDir: join4(root, "models"),
2624
+ datasetsDir: join4(root, "datasets")
2020
2625
  };
2021
2626
  }
2022
2627
  async function exists2(path) {
2023
2628
  try {
2024
- await stat3(path);
2629
+ await stat4(path);
2025
2630
  return true;
2026
2631
  } catch {
2027
2632
  return false;
2028
2633
  }
2029
2634
  }
2030
2635
  async function ensurePrivateDirectory(path) {
2031
- await mkdir3(path, { recursive: true, mode: 448 });
2636
+ await mkdir4(path, { recursive: true, mode: 448 });
2032
2637
  const metadata = await lstat3(path);
2033
2638
  if (metadata.isSymbolicLink()) {
2034
2639
  throw new Error(`Refusing symbolic link in local store: ${path}`);
@@ -2046,7 +2651,7 @@ async function hardenPrivateTree(path) {
2046
2651
  if (metadata.isDirectory()) {
2047
2652
  await chmod(path, 448);
2048
2653
  for (const entry of await readdir3(path)) {
2049
- await hardenPrivateTree(join3(path, entry));
2654
+ await hardenPrivateTree(join4(path, entry));
2050
2655
  }
2051
2656
  return;
2052
2657
  }
@@ -2076,17 +2681,17 @@ async function hardenExistingLocalStore(root) {
2076
2681
  }
2077
2682
  async function writePrivateJsonAtomic(path, value) {
2078
2683
  await ensurePrivateDirectory(dirname4(path));
2079
- const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID3()}.tmp`;
2080
- await writeFile3(tmp, `${JSON.stringify(value, null, 2)}
2684
+ const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID4()}.tmp`;
2685
+ await writeFile4(tmp, `${JSON.stringify(value, null, 2)}
2081
2686
  `, {
2082
2687
  encoding: "utf8",
2083
2688
  mode: 384
2084
2689
  });
2085
- await rename2(tmp, path);
2690
+ await rename3(tmp, path);
2086
2691
  await chmod(path, 384);
2087
2692
  }
2088
2693
  async function readJson2(path) {
2089
- return JSON.parse(await readFile4(path, "utf8"));
2694
+ return JSON.parse(await readFile5(path, "utf8"));
2090
2695
  }
2091
2696
  async function appendJsonl(path, value) {
2092
2697
  await ensurePrivateDirectory(dirname4(path));
@@ -2099,13 +2704,9 @@ async function appendJsonl(path, value) {
2099
2704
  }
2100
2705
  async function writePrivateText(path, value) {
2101
2706
  await ensurePrivateDirectory(dirname4(path));
2102
- await writeFile3(path, value, { encoding: "utf8", mode: 384 });
2707
+ await writeFile4(path, value, { encoding: "utf8", mode: 384 });
2103
2708
  await chmod(path, 384);
2104
2709
  }
2105
- async function readJsonl(path) {
2106
- if (!await exists2(path)) return [];
2107
- return (await readFile4(path, "utf8")).split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
2108
- }
2109
2710
  async function copyIfExists(from, to) {
2110
2711
  if (!await exists2(from)) return;
2111
2712
  await ensurePrivateDirectory(dirname4(to));
@@ -2144,15 +2745,15 @@ function isPathInside(root, candidate) {
2144
2745
  function createLocalStore(root = defaultLocalHome()) {
2145
2746
  const resolvedRoot = resolve6(root);
2146
2747
  const paths = localStorePaths(resolvedRoot);
2147
- const runDir = (id) => join3(paths.runsDir, id);
2148
- const runStatePath = (id) => join3(runDir(id), "state.json");
2149
- const runEventsPath = (id) => join3(runDir(id), "progress.jsonl");
2150
- const runRequestPath = (id) => join3(runDir(id), "request.json");
2151
- const runReportPath = (id) => join3(runDir(id), "run-report.json");
2152
- const cancellationPath = (id) => join3(runDir(id), "cancel.requested");
2153
- const specDir = (id) => join3(paths.specsDir, id);
2154
- const specPath = (id) => join3(specDir(id), "spec.json");
2155
- const modelPath = (id) => join3(paths.modelsDir, id, "model.json");
2748
+ const runDir = (id) => join4(paths.runsDir, id);
2749
+ const runStatePath = (id) => join4(runDir(id), "state.json");
2750
+ const runEventsPath = (id) => join4(runDir(id), "progress.jsonl");
2751
+ const runRequestPath = (id) => join4(runDir(id), "request.json");
2752
+ const runReportPath = (id) => join4(runDir(id), "run-report.json");
2753
+ const cancellationPath = (id) => join4(runDir(id), "cancel.requested");
2754
+ const specDir = (id) => join4(paths.specsDir, id);
2755
+ const specPath = (id) => join4(specDir(id), "spec.json");
2756
+ const modelPath = (id) => join4(paths.modelsDir, id, "model.json");
2156
2757
  let ensurePromise;
2157
2758
  async function ensureOnce() {
2158
2759
  await ensurePrivateDirectory(paths.root);
@@ -2180,8 +2781,8 @@ function createLocalStore(root = defaultLocalHome()) {
2180
2781
  if (!await exists2(path)) return null;
2181
2782
  const [spec, fileStats, directoryStats] = await Promise.all([
2182
2783
  readJson2(path),
2183
- stat3(path),
2184
- stat3(specDir(id))
2784
+ stat4(path),
2785
+ stat4(specDir(id))
2185
2786
  ]);
2186
2787
  const createdAt = directoryStats.birthtimeMs > 0 ? directoryStats.birthtime : directoryStats.ctime;
2187
2788
  return {
@@ -2228,15 +2829,30 @@ function createLocalStore(root = defaultLocalHome()) {
2228
2829
  if (!record) throw new Error(`Failed to persist spec: ${specId}`);
2229
2830
  return record;
2230
2831
  }
2832
+ async function readRunEventLog(id) {
2833
+ const state = await getRun(id);
2834
+ const path = runEventsPath(state.id);
2835
+ if (!await exists2(path)) return { events: [], unreadable_lines: 0 };
2836
+ const parsed = parseJsonlTolerant(await readFile5(path, "utf8"));
2837
+ return { events: parsed.rows, unreadable_lines: parsed.unreadable };
2838
+ }
2231
2839
  async function appendRunEvent(state, event) {
2232
- const row = {
2233
- id: randomUUID3(),
2234
- run_id: state.id,
2235
- occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
2236
- ...event
2237
- };
2238
- await appendJsonl(runEventsPath(state.id), row);
2239
- await appendJsonl(join3(state.artifact_dir, "progress.jsonl"), {
2840
+ const path = runEventsPath(state.id);
2841
+ await ensurePrivateDirectory(dirname4(path));
2842
+ const row = await withAppendLock(`${path}.lock`, async () => {
2843
+ const { partial } = await readJsonlTail(path);
2844
+ const { last: previous } = await readJsonlTail(path, (row2) => typeof row2.hash === "string");
2845
+ if (partial) await appendFile(path, "\n", { encoding: "utf8", mode: 384 });
2846
+ const chained = chainEvent({
2847
+ id: randomUUID4(),
2848
+ run_id: state.id,
2849
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
2850
+ ...event
2851
+ }, previous);
2852
+ await appendJsonl(path, chained);
2853
+ return chained;
2854
+ });
2855
+ await appendJsonl(join4(state.artifact_dir, "progress.jsonl"), {
2240
2856
  at: row.occurred_at,
2241
2857
  stage: row.stage,
2242
2858
  status: row.status,
@@ -2267,8 +2883,8 @@ function createLocalStore(root = defaultLocalHome()) {
2267
2883
  const previous = await getRun(runId);
2268
2884
  if (isTerminalRunState(previous)) return previous;
2269
2885
  await Promise.all([
2270
- rm(join3(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true }),
2271
- rm(runReportPath(previous.id), { force: true })
2886
+ rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true }),
2887
+ rm2(runReportPath(previous.id), { force: true })
2272
2888
  ]);
2273
2889
  if (previous.status === "cancelled" && previous.current_stage === "cancel_requested") return previous;
2274
2890
  const requested = {
@@ -2434,7 +3050,7 @@ function createLocalStore(root = defaultLocalHome()) {
2434
3050
  createdAt: completedAt
2435
3051
  });
2436
3052
  if (!model) {
2437
- await rm(join3(paths.modelsDir, `local-${report.run_id}`), { recursive: true, force: true });
3053
+ await rm2(join4(paths.modelsDir, `local-${report.run_id}`), { recursive: true, force: true });
2438
3054
  }
2439
3055
  const previous = await getRun(report.run_id);
2440
3056
  const state = {
@@ -2455,20 +3071,27 @@ function createLocalStore(root = defaultLocalHome()) {
2455
3071
  if (await exists2(cancellationPath(report.run_id))) {
2456
3072
  return preserveCancellationRequestState(report.run_id);
2457
3073
  }
3074
+ const manifestPath = join4(artifactDir, "artifact-manifest.json");
2458
3075
  await appendRunEvent(state, {
2459
3076
  stage: "completed",
2460
3077
  status: "completed",
2461
3078
  message: "Run completed successfully.",
2462
- details: { report_path: reportPath, ...model ? { model_id: model.id } : {} }
3079
+ details: {
3080
+ report_path: reportPath,
3081
+ // Pin the published evidence: `tt runs audit` re-hashes these files.
3082
+ report_sha256: await sha256File2(runReportPath(report.run_id)),
3083
+ ...await exists2(manifestPath) ? { artifact_manifest_sha256: await sha256File2(manifestPath) } : {},
3084
+ ...model ? { model_id: model.id } : {}
3085
+ }
2463
3086
  });
2464
3087
  return state;
2465
3088
  },
2466
3089
  registerModel: registerModelRecord,
2467
3090
  async invalidateRunOutputs(runId, options) {
2468
3091
  const previous = await getRun(runId);
2469
- if (options.report) await rm(runReportPath(previous.id), { force: true });
3092
+ if (options.report) await rm2(runReportPath(previous.id), { force: true });
2470
3093
  if (options.model) {
2471
- await rm(join3(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true });
3094
+ await rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true });
2472
3095
  }
2473
3096
  const state = {
2474
3097
  ...previous,
@@ -2511,7 +3134,7 @@ function createLocalStore(root = defaultLocalHome()) {
2511
3134
  `);
2512
3135
  const latest = await getRun(state.id);
2513
3136
  if (isTerminalRunState(latest)) {
2514
- if (latest.status !== "cancelled") await rm(cancellationPath(state.id), { force: true });
3137
+ if (latest.status !== "cancelled") await rm2(cancellationPath(state.id), { force: true });
2515
3138
  return;
2516
3139
  }
2517
3140
  const updated = {
@@ -2538,8 +3161,23 @@ function createLocalStore(root = defaultLocalHome()) {
2538
3161
  },
2539
3162
  getRun,
2540
3163
  async getRunEvents(id) {
3164
+ return (await readRunEventLog(id)).events;
3165
+ },
3166
+ getRunEventLog: readRunEventLog,
3167
+ async getRunReportPaths(id) {
2541
3168
  const state = await getRun(id);
2542
- return readJsonl(runEventsPath(state.id));
3169
+ const candidates = [];
3170
+ if (state.report_path) {
3171
+ const reportPath = resolve6(state.report_path);
3172
+ const allowedRoots = [resolvedRoot, ...state.artifact_dir ? [resolve6(state.artifact_dir)] : []];
3173
+ if (allowedRoots.some((allowed) => isPathInside(allowed, reportPath))) candidates.push(reportPath);
3174
+ }
3175
+ candidates.push(runReportPath(state.id));
3176
+ const existing = [];
3177
+ for (const path of new Set(candidates)) {
3178
+ if ((await lstat3(path).catch(() => null))?.isFile()) existing.push(path);
3179
+ }
3180
+ return existing;
2543
3181
  },
2544
3182
  async getRunReport(id) {
2545
3183
  const state = await getRun(id);
@@ -2570,38 +3208,216 @@ function createLocalStore(root = defaultLocalHome()) {
2570
3208
  }
2571
3209
 
2572
3210
  // src/local-runtime/prefetch.ts
2573
- import { createHash as createHash4 } from "crypto";
2574
- import { createReadStream as createReadStream2 } from "fs";
2575
- import { lstat as lstat4, mkdir as mkdir5, readFile as readFile5, readdir as readdir4, realpath as realpath2, stat as stat4 } from "fs/promises";
2576
- import { isAbsolute as isAbsolute4, join as join6, relative as relative3, resolve as resolve9, sep as sep4 } from "path";
3211
+ import { createHash as createHash5 } from "crypto";
3212
+ import { createReadStream as createReadStream3 } from "fs";
3213
+ import { lstat as lstat4, mkdir as mkdir6, readFile as readFile6, readdir as readdir4, realpath as realpath2, stat as stat5 } from "fs/promises";
3214
+ import { isAbsolute as isAbsolute4, join as join7, relative as relative3, resolve as resolve9, sep as sep4 } from "path";
2577
3215
 
2578
3216
  // src/local-runtime/process-runner.ts
2579
- import { createHash as createHash3 } from "crypto";
3217
+ import { createHash as createHash4 } from "crypto";
2580
3218
  import { spawn } from "child_process";
2581
3219
  import { constants, readFileSync } from "fs";
2582
- import { mkdir as mkdir4, open as open2 } from "fs/promises";
2583
- import { dirname as dirname5, join as join4, resolve as resolve7 } from "path";
3220
+ import { mkdir as mkdir5, open as open3 } from "fs/promises";
3221
+ import { dirname as dirname5, join as join5, resolve as resolve7 } from "path";
3222
+
3223
+ // src/local-runtime/progress-bar.ts
3224
+ var PROGRESS_PREFIX = "@@tt-progress ";
3225
+ function nonNegativeInteger(value) {
3226
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
3227
+ }
3228
+ function parseProgressLine(line) {
3229
+ const start = line.indexOf(PROGRESS_PREFIX);
3230
+ if (start !== 0) return null;
3231
+ let value;
3232
+ try {
3233
+ value = JSON.parse(line.slice(PROGRESS_PREFIX.length));
3234
+ } catch {
3235
+ return null;
3236
+ }
3237
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3238
+ const record = value;
3239
+ const completed = nonNegativeInteger(record.completed_bytes);
3240
+ const total = nonNegativeInteger(record.total_bytes);
3241
+ if (completed === null || total === null) return null;
3242
+ return {
3243
+ label: typeof record.label === "string" ? record.label : "download",
3244
+ phase: typeof record.phase === "string" ? record.phase : "download",
3245
+ completed_bytes: completed,
3246
+ total_bytes: total,
3247
+ files_completed: nonNegativeInteger(record.files_completed) ?? 0,
3248
+ files_total: nonNegativeInteger(record.files_total) ?? 0
3249
+ };
3250
+ }
3251
+ function formatBytes(bytes) {
3252
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
3253
+ let value = bytes;
3254
+ let unit = 0;
3255
+ while (value >= 1024 && unit < units.length - 1) {
3256
+ value /= 1024;
3257
+ unit += 1;
3258
+ }
3259
+ return `${unit === 0 ? value.toFixed(0) : value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unit]}`;
3260
+ }
3261
+ function formatDuration(seconds) {
3262
+ if (!Number.isFinite(seconds) || seconds < 0) return "--";
3263
+ const whole = Math.round(seconds);
3264
+ const hours = Math.floor(whole / 3600);
3265
+ const minutes = Math.floor(whole % 3600 / 60);
3266
+ const rest = whole % 60;
3267
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
3268
+ if (minutes > 0) return `${minutes}m ${String(rest).padStart(2, "0")}s`;
3269
+ return `${rest}s`;
3270
+ }
3271
+ var RateTracker = class {
3272
+ constructor(windowMs = 15e3) {
3273
+ this.windowMs = windowMs;
3274
+ }
3275
+ samples = [];
3276
+ add(at, bytes) {
3277
+ const last = this.samples.at(-1);
3278
+ if (last && bytes < last.bytes) this.samples = [];
3279
+ this.samples.push({ at, bytes });
3280
+ while (this.samples.length > 2 && at - this.samples[0].at > this.windowMs) this.samples.shift();
3281
+ }
3282
+ bytesPerSecond() {
3283
+ const first = this.samples[0];
3284
+ const last = this.samples.at(-1);
3285
+ if (!first || !last || last.at - first.at < 500) return null;
3286
+ return (last.bytes - first.bytes) / ((last.at - first.at) / 1e3);
3287
+ }
3288
+ };
3289
+ function formatProgressLine(progress, rate, columns) {
3290
+ const total = progress.total_bytes;
3291
+ const fraction = total > 0 ? Math.min(1, progress.completed_bytes / total) : 1;
3292
+ const fields = [
3293
+ { text: `${(fraction * 100).toFixed(1).padStart(5)}%` },
3294
+ { text: `${formatBytes(progress.completed_bytes)} / ${formatBytes(total)}` }
3295
+ ];
3296
+ const done = progress.completed_bytes >= total;
3297
+ if (!done && rate !== null && rate > 0) {
3298
+ fields.push(
3299
+ { text: `${formatBytes(rate)}/s`, drop: 2 },
3300
+ { text: `ETA ${formatDuration((total - progress.completed_bytes) / rate)}`, drop: 3 }
3301
+ );
3302
+ }
3303
+ if (progress.files_total > 0) fields.push({ text: `${progress.files_completed}/${progress.files_total} files`, drop: 1 });
3304
+ const prefix = `[tt] ${progress.label}: `;
3305
+ const join16 = () => fields.map((field) => field.text).join(" ");
3306
+ if (columns === void 0) return `${prefix}${join16()}`;
3307
+ for (const rank of [1, 2, 3]) {
3308
+ if (prefix.length + join16().length <= columns - 1) break;
3309
+ const index = fields.findIndex((field) => field.drop === rank);
3310
+ if (index !== -1) fields.splice(index, 1);
3311
+ }
3312
+ const text = join16();
3313
+ const barWidth = Math.min(30, columns - 1 - prefix.length - text.length - 3);
3314
+ if (barWidth < 10) return `${prefix}${text}`.slice(0, Math.max(0, columns - 1));
3315
+ const filled = Math.round(fraction * barWidth);
3316
+ return `${prefix}[${"#".repeat(filled)}${"-".repeat(barWidth - filled)}] ${text}`;
3317
+ }
3318
+ function createProgressRenderer(options) {
3319
+ const now = options.now ?? (() => Date.now());
3320
+ const rates = /* @__PURE__ */ new Map();
3321
+ const lastLine = /* @__PURE__ */ new Map();
3322
+ const completed = /* @__PURE__ */ new Set();
3323
+ let drawn = false;
3324
+ let lastDraw = Number.NEGATIVE_INFINITY;
3325
+ const finished = (progress) => progress.phase === "downloaded" || progress.total_bytes > 0 && progress.completed_bytes >= progress.total_bytes;
3326
+ return {
3327
+ update(progress) {
3328
+ const at = now();
3329
+ const tracker = rates.get(progress.label) ?? new RateTracker();
3330
+ rates.set(progress.label, tracker);
3331
+ tracker.add(at, progress.completed_bytes);
3332
+ const done = finished(progress);
3333
+ if (options.tty) {
3334
+ if (done && completed.has(progress.label)) return;
3335
+ if (!done) completed.delete(progress.label);
3336
+ if (!done && at - lastDraw < 100) return;
3337
+ lastDraw = at;
3338
+ const line = formatProgressLine(progress, tracker.bytesPerSecond(), Math.max(40, options.columns ?? 80));
3339
+ options.write(`\r\x1B[2K${line}${done ? "\n" : ""}`);
3340
+ drawn = !done;
3341
+ if (done) {
3342
+ rates.delete(progress.label);
3343
+ completed.add(progress.label);
3344
+ }
3345
+ return;
3346
+ }
3347
+ const fraction = progress.total_bytes > 0 ? progress.completed_bytes / progress.total_bytes : 1;
3348
+ const decile = Math.floor(fraction * 10);
3349
+ const previous = lastLine.get(progress.label);
3350
+ const interval = options.lineIntervalMs ?? 3e4;
3351
+ if (previous && !done && decile <= previous.decile && at - previous.at < interval) return;
3352
+ if (previous && done && previous.decile === 10) return;
3353
+ lastLine.set(progress.label, { at, decile: done ? 10 : decile });
3354
+ options.write(`${formatProgressLine(progress, tracker.bytesPerSecond())}
3355
+ `);
3356
+ },
3357
+ interrupt() {
3358
+ if (!drawn) return;
3359
+ options.write("\r\x1B[2K");
3360
+ drawn = false;
3361
+ }
3362
+ };
3363
+ }
3364
+ function progressTty(env = process.env, stream = process.stderr) {
3365
+ if (env.TT_PROGRESS_TTY === "0") return false;
3366
+ return env.TT_PROGRESS_TTY === "1" || Boolean(stream.isTTY);
3367
+ }
3368
+ function progressColumns(env = process.env, stream = process.stderr) {
3369
+ const hinted = Number(env.TT_PROGRESS_COLUMNS);
3370
+ if (Number.isInteger(hinted) && hinted > 0) return hinted;
3371
+ return stream.columns;
3372
+ }
3373
+
3374
+ // src/local-runtime/process-runner.ts
2584
3375
  var packageRoot = localRuntimePackageRoot(import.meta.url);
2585
- var bundledProject = join4(packageRoot, "training/adapter");
3376
+ var bundledProject = join5(packageRoot, "training/adapter");
2586
3377
  var bundledRuntimeHash = (() => {
2587
- const hash = createHash3("sha256");
3378
+ const hash = createHash4("sha256");
2588
3379
  for (const name of ["pyproject.toml", "uv.lock"]) {
2589
- hash.update(readFileSync(join4(bundledProject, name)));
3380
+ hash.update(readFileSync(join5(bundledProject, name)));
2590
3381
  }
2591
3382
  return hash.digest("hex").slice(0, 20);
2592
3383
  })();
2593
3384
  var BUNDLED_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv", bundledRuntimeHash);
2594
- var foundationProject = join4(packageRoot, "training/foundation");
3385
+ var foundationProject = join5(packageRoot, "training/foundation");
2595
3386
  var foundationRuntimeHash = (() => {
2596
- const hash = createHash3("sha256");
3387
+ const hash = createHash4("sha256");
2597
3388
  for (const name of ["pyproject.toml", "uv.lock"]) {
2598
- hash.update(readFileSync(join4(foundationProject, name)));
3389
+ hash.update(readFileSync(join5(foundationProject, name)));
2599
3390
  }
2600
3391
  return hash.digest("hex").slice(0, 20);
2601
3392
  })();
2602
3393
  var FOUNDATION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-foundation", foundationRuntimeHash);
3394
+ var decisionProject = join5(packageRoot, "training/decision");
3395
+ var decisionRuntimeHash = (() => {
3396
+ const hash = createHash4("sha256");
3397
+ for (const name of ["pyproject.toml", "uv.lock"]) {
3398
+ hash.update(readFileSync(join5(decisionProject, name)));
3399
+ }
3400
+ return hash.digest("hex").slice(0, 20);
3401
+ })();
3402
+ var DECISION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-decision", decisionRuntimeHash);
3403
+ function buildDecisionPythonCommand(entrypoint, args = []) {
3404
+ const commandArgs = [
3405
+ "run",
3406
+ "--frozen",
3407
+ "--quiet",
3408
+ "--project",
3409
+ decisionProject,
3410
+ "python",
3411
+ join5(decisionProject, "src", entrypoint),
3412
+ ...args
3413
+ ];
3414
+ return { command: "uv", commandArgs, displayCommand: ["uv", ...commandArgs] };
3415
+ }
3416
+ function withDecisionPythonEnvironment(env) {
3417
+ return { ...env, UV_PROJECT_ENVIRONMENT: DECISION_PYTHON_ENVIRONMENT };
3418
+ }
2603
3419
  function buildBundledPythonCommand(entrypoint, args = []) {
2604
- const target = entrypoint === "-c" ? entrypoint : join4(bundledProject, "src", entrypoint);
3420
+ const target = entrypoint === "-c" ? entrypoint : join5(bundledProject, "src", entrypoint);
2605
3421
  const commandArgs = [
2606
3422
  "run",
2607
3423
  "--frozen",
@@ -2625,7 +3441,7 @@ function withBundledPythonEnvironment(env) {
2625
3441
  };
2626
3442
  }
2627
3443
  function buildFoundationPythonCommand(entrypoint, args = []) {
2628
- const target = entrypoint === "-c" ? entrypoint : join4(foundationProject, "src", entrypoint);
3444
+ const target = entrypoint === "-c" ? entrypoint : join5(foundationProject, "src", entrypoint);
2629
3445
  const commandArgs = [
2630
3446
  "run",
2631
3447
  "--frozen",
@@ -2656,7 +3472,7 @@ var ProcessCancelledError = class extends Error {
2656
3472
  };
2657
3473
  async function openProcessLog(path, exclusive, append) {
2658
3474
  const flags = constants.O_WRONLY | constants.O_CREAT | (exclusive ? constants.O_EXCL : append ? constants.O_APPEND : constants.O_TRUNC) | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW);
2659
- const handle = await open2(path, flags, 384);
3475
+ const handle = await open3(path, flags, 384);
2660
3476
  try {
2661
3477
  return handle.createWriteStream({ autoClose: true });
2662
3478
  } catch (error) {
@@ -2665,7 +3481,7 @@ async function openProcessLog(path, exclusive, append) {
2665
3481
  }
2666
3482
  }
2667
3483
  async function runLoggedProcess(args) {
2668
- if (args.logPath) await mkdir4(dirname5(args.logPath), { recursive: true });
3484
+ if (args.logPath) await mkdir5(dirname5(args.logPath), { recursive: true });
2669
3485
  const logStream = args.logPath ? await openProcessLog(args.logPath, args.exclusiveLog ?? false, args.appendLog ?? false) : null;
2670
3486
  let stderr = "";
2671
3487
  try {
@@ -2739,6 +3555,11 @@ async function runLoggedProcess(args) {
2739
3555
  child.stderr.pipe(logStream, { end: false });
2740
3556
  }
2741
3557
  forwardStreamLines(child.stdout, (line) => {
3558
+ const progress = parseProgressLine(line);
3559
+ if (progress) {
3560
+ reportInBackground(() => args.reporter?.onProgress?.(progress));
3561
+ return;
3562
+ }
2742
3563
  args.onLine?.(line, "stdout");
2743
3564
  if (args.reporter?.verbose) {
2744
3565
  reportInBackground(() => args.reporter?.onLog?.({ stage: args.stage, stream: "stdout", message: line }));
@@ -2788,7 +3609,7 @@ async function runLoggedProcess(args) {
2788
3609
 
2789
3610
  // src/local-runtime/huggingface-cache.ts
2790
3611
  import { homedir as homedir3 } from "os";
2791
- import { dirname as dirname6, join as join5, resolve as resolve8 } from "path";
3612
+ import { dirname as dirname6, join as join6, resolve as resolve8 } from "path";
2792
3613
  function minimalMachineLearningEnvironment(env) {
2793
3614
  const exact = /* @__PURE__ */ new Set([
2794
3615
  "PATH",
@@ -2820,6 +3641,9 @@ function minimalMachineLearningEnvironment(env) {
2820
3641
  "HF_HUB_DISABLE_TELEMETRY",
2821
3642
  "TRANSFORMERS_OFFLINE",
2822
3643
  "HF_HUB_ENABLE_HF_TRANSFER",
3644
+ "HF_HUB_DOWNLOAD_TIMEOUT",
3645
+ "HF_HUB_ETAG_TIMEOUT",
3646
+ "TT_HF_DOWNLOAD_MAX_ATTEMPTS",
2823
3647
  "PYTHONUTF8",
2824
3648
  "PYTHONIOENCODING"
2825
3649
  ]);
@@ -2845,12 +3669,12 @@ function resolveHuggingFaceCacheLayout(modelCache) {
2845
3669
  const hfHome = resolve8(modelCache);
2846
3670
  return {
2847
3671
  hfHome,
2848
- hubCache: join5(hfHome, "hub")
3672
+ hubCache: join6(hfHome, "hub")
2849
3673
  };
2850
3674
  }
2851
3675
  function withHuggingFaceCacheEnvironment(env, modelCache) {
2852
3676
  const inheritedHub = env.HF_HUB_CACHE ?? env.HUGGINGFACE_HUB_CACHE;
2853
- const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) : join5(homedir3(), ".cache", "huggingface"));
3677
+ const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) : join6(homedir3(), ".cache", "huggingface"));
2854
3678
  const layout = resolveHuggingFaceCacheLayout(canonicalHome);
2855
3679
  const result = {
2856
3680
  ...env,
@@ -2930,10 +3754,10 @@ function certifiedFileDigests(modelId) {
2930
3754
  }
2931
3755
  }
2932
3756
  async function certifiedFileDigest(path, expected) {
2933
- const metadata = await stat4(path);
2934
- const digest = createHash4(expected.algorithm);
3757
+ const metadata = await stat5(path);
3758
+ const digest = createHash5(expected.algorithm);
2935
3759
  if (expected.algorithm === "sha1") digest.update(`blob ${metadata.size}\0`);
2936
- for await (const chunk of createReadStream2(path)) digest.update(chunk);
3760
+ for await (const chunk of createReadStream3(path)) digest.update(chunk);
2937
3761
  return digest.digest("hex");
2938
3762
  }
2939
3763
  function isStrictPhysicalDescendant(parent, child) {
@@ -2943,8 +3767,8 @@ function isStrictPhysicalDescendant(parent, child) {
2943
3767
  async function verifySnapshotCacheLocation(path, modelId, revision, modelCache) {
2944
3768
  const repository = `models--${modelId.replaceAll("/", "--")}`;
2945
3769
  const layout = resolveHuggingFaceCacheLayout(modelCache);
2946
- const repositoryRoot = join6(layout.hubCache, repository);
2947
- const expectedPath = join6(repositoryRoot, "snapshots", revision);
3770
+ const repositoryRoot = join7(layout.hubCache, repository);
3771
+ const expectedPath = join7(repositoryRoot, "snapshots", revision);
2948
3772
  const [
2949
3773
  physicalCacheRoot,
2950
3774
  physicalHubRoot,
@@ -2956,7 +3780,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
2956
3780
  realpath2(layout.hfHome).catch(() => null),
2957
3781
  realpath2(layout.hubCache).catch(() => null),
2958
3782
  realpath2(repositoryRoot).catch(() => null),
2959
- realpath2(join6(repositoryRoot, "blobs")).catch(() => null),
3783
+ realpath2(join7(repositoryRoot, "blobs")).catch(() => null),
2960
3784
  realpath2(path).catch(() => null),
2961
3785
  realpath2(expectedPath).catch(() => null)
2962
3786
  ]);
@@ -2972,7 +3796,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
2972
3796
  throw new Error(`Hugging Face repository escapes its configured hub cache: ${repositoryRoot}`);
2973
3797
  }
2974
3798
  if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalBlobRoot)) {
2975
- throw new Error(`Hugging Face blob store escapes its repository: ${join6(repositoryRoot, "blobs")}`);
3799
+ throw new Error(`Hugging Face blob store escapes its repository: ${join7(repositoryRoot, "blobs")}`);
2976
3800
  }
2977
3801
  if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalExpectedPath)) {
2978
3802
  throw new Error(`Hugging Face snapshot escapes its configured repository: ${expectedPath}`);
@@ -3034,13 +3858,13 @@ async function verifyCertifiedLocalSnapshot(path, modelId, snapshotFiles, modelC
3034
3858
  const expectedFiles = new Set(Object.keys(digests));
3035
3859
  const unexpected = [...actualFiles].filter((name) => !expectedFiles.has(name)).sort();
3036
3860
  if (unexpected.length > 0) {
3037
- throw new Error(`Certified snapshot contains unexpected file: ${join6(path, unexpected[0])}`);
3861
+ throw new Error(`Certified snapshot contains unexpected file: ${join7(path, unexpected[0])}`);
3038
3862
  }
3039
3863
  for (const [name, expected] of Object.entries(digests)) {
3040
3864
  if (!actualFiles.has(name)) {
3041
- throw new Error(`Certified snapshot file is missing: ${join6(path, name)}`);
3865
+ throw new Error(`Certified snapshot file is missing: ${join7(path, name)}`);
3042
3866
  }
3043
- const file = join6(path, name);
3867
+ const file = join7(path, name);
3044
3868
  const physicalFile = physicalFiles.get(file);
3045
3869
  if (await certifiedFileDigest(physicalFile, expected) !== expected.digest) {
3046
3870
  throw new Error(`Certified snapshot file checksum mismatch: ${file}`);
@@ -3083,9 +3907,9 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
3083
3907
  const files = [];
3084
3908
  const visit = async (directory) => {
3085
3909
  for (const entry of await readdir4(directory, { withFileTypes: true })) {
3086
- const child = join6(directory, entry.name);
3910
+ const child = join7(directory, entry.name);
3087
3911
  if (entry.isSymbolicLink()) {
3088
- const metadata2 = await stat4(child);
3912
+ const metadata2 = await stat5(child);
3089
3913
  if (!metadata2.isFile()) {
3090
3914
  throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
3091
3915
  }
@@ -3096,7 +3920,7 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
3096
3920
  await visit(child);
3097
3921
  continue;
3098
3922
  }
3099
- const metadata = await stat4(child);
3923
+ const metadata = await stat5(child);
3100
3924
  if (metadata.isFile()) files.push({ path: child, size: metadata.size });
3101
3925
  }
3102
3926
  };
@@ -3107,15 +3931,15 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
3107
3931
  throw new Error(`Local base-model directory contains no non-empty Transformers model weights: ${path}`);
3108
3932
  }
3109
3933
  const required = async (name) => {
3110
- const metadata = await stat4(join6(path, name)).catch(() => null);
3934
+ const metadata = await stat5(join7(path, name)).catch(() => null);
3111
3935
  return Boolean(metadata?.isFile() && metadata.size > 0);
3112
3936
  };
3113
3937
  if (!await required("config.json")) throw new Error(`Local base-model directory is missing config.json: ${path}`);
3114
3938
  try {
3115
- const config = JSON.parse(await readFile5(join6(path, "config.json"), "utf8"));
3939
+ const config = JSON.parse(await readFile6(join7(path, "config.json"), "utf8"));
3116
3940
  assertCertifiedBaseModelConfig(
3117
3941
  config,
3118
- `Local base-model config ${join6(path, "config.json")}`,
3942
+ `Local base-model config ${join7(path, "config.json")}`,
3119
3943
  expectedModelId
3120
3944
  );
3121
3945
  } catch (error) {
@@ -3144,13 +3968,17 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
3144
3968
  if (!await required("tokenizer_config.json") || !(await Promise.all(vocabNames.map(required))).some(Boolean)) {
3145
3969
  throw new Error(`Local base-model directory is missing tokenizer metadata or vocabulary: ${path}`);
3146
3970
  }
3971
+ const inventory = new Set(nonEmpty.map((file) => resolve9(file.path)));
3147
3972
  for (const file of files.filter((candidate) => candidate.path.endsWith(".index.json"))) {
3148
- const parsed = JSON.parse(await readFile5(file.path, "utf8"));
3973
+ const parsed = JSON.parse(await readFile6(file.path, "utf8"));
3149
3974
  if (!parsed.weight_map || typeof parsed.weight_map !== "object" || Array.isArray(parsed.weight_map)) {
3150
3975
  throw new Error(`Local base-model weight index is invalid: ${file.path}`);
3151
3976
  }
3152
3977
  for (const shard of new Set(Object.values(parsed.weight_map))) {
3153
- if (typeof shard !== "string" || !await required(shard)) {
3978
+ if (typeof shard !== "string" || !shard || /^(?:[A-Za-z]:|[/\\])/.test(shard) || shard.split(/[/\\]/).includes("..")) {
3979
+ throw new Error("Local base-model weight index has an invalid indexed weight shard path");
3980
+ }
3981
+ if (!inventory.has(resolve9(path, shard)) || !await required(shard)) {
3154
3982
  throw new Error(`Local base-model directory is missing indexed weight shard: ${String(shard)}`);
3155
3983
  }
3156
3984
  }
@@ -3170,7 +3998,7 @@ async function prefetchBaseModel(args) {
3170
3998
  args.localOnly ? "verify-base" : "prefetch",
3171
3999
  `${safeName(args.request.spec_snapshot.base_model)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
3172
4000
  );
3173
- await mkdir5(artifactDir, { recursive: true });
4001
+ await mkdir6(artifactDir, { recursive: true });
3174
4002
  if (args.config.paths.baseModel) {
3175
4003
  const localPath = resolve9(args.config.paths.baseModel);
3176
4004
  const verified = await verifyLocalBaseModel(
@@ -3189,9 +4017,9 @@ async function prefetchBaseModel(args) {
3189
4017
  reason: args.localOnly ? "Verified the configured local base-model artifact; no network access was used." : "paths.baseModel points at a verified local base-model artifact; no Hugging Face download is needed."
3190
4018
  };
3191
4019
  }
3192
- const inputPath = join6(artifactDir, "prefetch-input.json");
3193
- const outputPath = join6(artifactDir, "prefetch-output.json");
3194
- const logPath = join6(artifactDir, "prefetch.log");
4020
+ const inputPath = join7(artifactDir, "prefetch-input.json");
4021
+ const outputPath = join7(artifactDir, "prefetch-output.json");
4022
+ const logPath = join7(artifactDir, "prefetch.log");
3195
4023
  await writeJson(inputPath, payload);
3196
4024
  const entrypoint = buildBundledPythonCommand(
3197
4025
  "prefetch.py",
@@ -3228,7 +4056,7 @@ async function prefetchBaseModel(args) {
3228
4056
  if (exitCode !== 0) {
3229
4057
  throw new Error(`Model prefetch exited with code ${exitCode}. See ${logPath}.`);
3230
4058
  }
3231
- const output = JSON.parse(await readFile5(outputPath, "utf8"));
4059
+ const output = JSON.parse(await readFile6(outputPath, "utf8"));
3232
4060
  const snapshotRevision = baseModelRevisionSchema.safeParse(output.snapshot_revision);
3233
4061
  if (!snapshotRevision.success) {
3234
4062
  throw new Error(
@@ -3291,6 +4119,346 @@ async function prefetchBaseModel(args) {
3291
4119
  };
3292
4120
  }
3293
4121
 
4122
+ // src/local-runtime/dataset-prefetch.ts
4123
+ import { createHash as createHash6 } from "crypto";
4124
+ import { createReadStream as createReadStream4 } from "fs";
4125
+ import { mkdir as mkdir7, readFile as readFile7, realpath as realpath3, stat as stat6 } from "fs/promises";
4126
+ import { createInterface } from "readline";
4127
+ import { basename, isAbsolute as isAbsolute5, join as join8, relative as relative4, resolve as resolve10, sep as sep5 } from "path";
4128
+ var SPLITS = ["training", "validation", "test"];
4129
+ var DatasetNotDownloadedError = class extends Error {
4130
+ constructor(message) {
4131
+ super(message);
4132
+ this.name = "DatasetNotDownloadedError";
4133
+ }
4134
+ };
4135
+ function huggingFaceDataset(request) {
4136
+ const dataset = request.dataset_prebuilt;
4137
+ return dataset?.huggingface ? dataset : void 0;
4138
+ }
4139
+ function datasetFiles(dataset) {
4140
+ const files = {};
4141
+ for (const split of SPLITS) if (dataset[split]) files[split] = dataset[split];
4142
+ return files;
4143
+ }
4144
+ function huggingFaceHubCache(modelCache) {
4145
+ return withHuggingFaceCacheEnvironment(process.env, modelCache).HF_HUB_CACHE;
4146
+ }
4147
+ function repositoryDirectory(hubCache, repo) {
4148
+ return join8(hubCache, `datasets--${repo.replaceAll("/", "--")}`);
4149
+ }
4150
+ function isStrictDescendant(parent, child) {
4151
+ const path = relative4(parent, child);
4152
+ return path !== "" && path !== ".." && !path.startsWith(`..${sep5}`) && !isAbsolute5(path);
4153
+ }
4154
+ async function digestFile(path, size) {
4155
+ const sha256 = createHash6("sha256");
4156
+ const gitSha1 = createHash6("sha1").update(`blob ${size}\0`);
4157
+ for await (const chunk of createReadStream4(path)) {
4158
+ sha256.update(chunk);
4159
+ gitSha1.update(chunk);
4160
+ }
4161
+ return { sha256: sha256.digest("hex"), gitSha1: gitSha1.digest("hex") };
4162
+ }
4163
+ function describe(dataset) {
4164
+ return `${dataset.repo}@${dataset.revision ?? "main"}`;
4165
+ }
4166
+ async function resolveCachedHuggingFaceDataset(args) {
4167
+ const source = args.dataset.huggingface;
4168
+ const hubCache = huggingFaceHubCache(args.modelCache);
4169
+ const repository = repositoryDirectory(hubCache, source.repo);
4170
+ const prefetchHint = "Run `tt datasets prefetch` with this spec (for example `tt datasets prefetch tunedtensor.json`) to download it.";
4171
+ let revision = source.revision;
4172
+ if (!revision) {
4173
+ const ref = await readFile7(join8(repository, "refs", "main"), "utf8").catch(() => null);
4174
+ const parsed = baseModelRevisionSchema.safeParse(ref?.trim());
4175
+ if (!parsed.success) {
4176
+ throw new DatasetNotDownloadedError(
4177
+ `Hugging Face dataset ${describe(source)} is not in the local cache (${hubCache}). ${prefetchHint}`
4178
+ );
4179
+ }
4180
+ revision = parsed.data;
4181
+ }
4182
+ const snapshot = join8(repository, "snapshots", revision);
4183
+ const physicalRepository = await realpath3(repository).catch(() => null);
4184
+ const blobs = physicalRepository ? join8(physicalRepository, "blobs") : null;
4185
+ const files = {};
4186
+ const localPaths = {};
4187
+ for (const [split, path] of Object.entries(datasetFiles(args.dataset))) {
4188
+ const local = join8(snapshot, ...path.split(/[/\\]/));
4189
+ const metadata = await stat6(local).catch(() => null);
4190
+ if (!metadata || !physicalRepository || !blobs) {
4191
+ throw new DatasetNotDownloadedError(
4192
+ `Hugging Face dataset file ${path} (${split}) from ${source.repo}@${revision} is not in the local cache (${hubCache}). ${prefetchHint}`
4193
+ );
4194
+ }
4195
+ const target = await realpath3(local);
4196
+ if (!metadata.isFile() || !isStrictDescendant(physicalRepository, target)) {
4197
+ throw new Error(`Cached dataset file ${local} must resolve inside its Hugging Face cache repository ${physicalRepository}.`);
4198
+ }
4199
+ if (metadata.size === 0) throw new Error(`Cached dataset file ${path} (${split}) is empty: ${local}`);
4200
+ const digests = await digestFile(target, metadata.size);
4201
+ const blobName = isStrictDescendant(blobs, target) ? basename(target).toLowerCase() : "";
4202
+ const expected = /^[0-9a-f]{64}$/.test(blobName) ? digests.sha256 : /^[0-9a-f]{40}$/.test(blobName) ? digests.gitSha1 : null;
4203
+ if (expected !== null && expected !== blobName) {
4204
+ throw new Error(
4205
+ `Cached dataset file ${path} (${split}) failed its checksum (${local}). Delete ${target} and run \`tt datasets prefetch\` again.`
4206
+ );
4207
+ }
4208
+ files[split] = { path, size_bytes: metadata.size, sha256: digests.sha256 };
4209
+ localPaths[split] = local;
4210
+ }
4211
+ return {
4212
+ source: datasetSourceSchema.parse({
4213
+ provider: "huggingface",
4214
+ repo: source.repo,
4215
+ requested_revision: source.revision ?? null,
4216
+ revision,
4217
+ pinned: Boolean(source.revision),
4218
+ columns: source.columns ?? null,
4219
+ snapshot_path: snapshot,
4220
+ files
4221
+ }),
4222
+ localPaths
4223
+ };
4224
+ }
4225
+ function mappedField(row, name) {
4226
+ const value = row[name];
4227
+ if (typeof value === "string") return value.trim() ? value : null;
4228
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4229
+ return null;
4230
+ }
4231
+ async function readMappedRecords(path, split, columns) {
4232
+ const examples = [];
4233
+ let records = 0;
4234
+ let skipped = 0;
4235
+ const availableFields = /* @__PURE__ */ new Set();
4236
+ const lines = createInterface({ input: createReadStream4(path, { encoding: "utf8" }), crlfDelay: Infinity });
4237
+ let index = -1;
4238
+ for await (const line of lines) {
4239
+ index += 1;
4240
+ if (!line.trim()) continue;
4241
+ records += 1;
4242
+ let value;
4243
+ try {
4244
+ value = JSON.parse(line);
4245
+ } catch (error) {
4246
+ throw new Error(`${split} line ${index + 1}: malformed JSON in ${path}`, { cause: error });
4247
+ }
4248
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4249
+ throw new Error(`${split} line ${index + 1}: expected a JSON object in ${path}`);
4250
+ }
4251
+ const row = value;
4252
+ for (const name of Object.keys(row)) availableFields.add(name);
4253
+ const input = mappedField(row, columns.input);
4254
+ const output = mappedField(row, columns.output);
4255
+ if (input === null || output === null) {
4256
+ skipped += 1;
4257
+ continue;
4258
+ }
4259
+ examples.push({ input, output });
4260
+ }
4261
+ if (records > 0) {
4262
+ for (const name of [columns.input, columns.output]) {
4263
+ if (!availableFields.has(name)) {
4264
+ throw new Error(
4265
+ `${split} records have no "${name}" field (dataset_prebuilt.huggingface.columns). Available fields: ${[...availableFields].join(", ")}`
4266
+ );
4267
+ }
4268
+ }
4269
+ }
4270
+ return { examples, records, skipped };
4271
+ }
4272
+ async function convertMappedDataset(args) {
4273
+ const system = buildSystemMessage(args.spec);
4274
+ const splits = Object.keys(args.resolved.localPaths);
4275
+ const key = createHash6("sha256").update(JSON.stringify({
4276
+ version: 1,
4277
+ sources: splits.map((split) => [split, args.resolved.source.files[split]?.sha256]),
4278
+ columns: args.columns,
4279
+ system
4280
+ })).digest("hex").slice(0, 16);
4281
+ const loaded = /* @__PURE__ */ new Map();
4282
+ for (const split of splits) {
4283
+ loaded.set(split, await readMappedRecords(args.resolved.localPaths[split], split, args.columns));
4284
+ }
4285
+ const evaluationInputs = /* @__PURE__ */ new Set();
4286
+ const kept = /* @__PURE__ */ new Map();
4287
+ const conversion = {};
4288
+ for (const split of splits.filter((name) => name !== "training")) {
4289
+ const seen = /* @__PURE__ */ new Set();
4290
+ const data = loaded.get(split);
4291
+ const examples = data.examples.filter((example) => {
4292
+ const identity = datasetInputIdentity(example.input);
4293
+ if (seen.has(identity)) return false;
4294
+ seen.add(identity);
4295
+ evaluationInputs.add(identity);
4296
+ return true;
4297
+ });
4298
+ kept.set(split, examples);
4299
+ conversion[split] = {
4300
+ records: data.records,
4301
+ kept: examples.length,
4302
+ skipped_missing_fields: data.skipped,
4303
+ dropped_duplicate_inputs: data.examples.length - examples.length,
4304
+ dropped_eval_overlap: 0
4305
+ };
4306
+ }
4307
+ const training = loaded.get("training");
4308
+ const trainingExamples = training.examples.filter((example) => !evaluationInputs.has(datasetInputIdentity(example.input)));
4309
+ kept.set("training", trainingExamples);
4310
+ conversion.training = {
4311
+ records: training.records,
4312
+ kept: trainingExamples.length,
4313
+ skipped_missing_fields: training.skipped,
4314
+ dropped_duplicate_inputs: 0,
4315
+ dropped_eval_overlap: training.examples.length - trainingExamples.length
4316
+ };
4317
+ await mkdir7(args.outputRoot, { recursive: true });
4318
+ const localPaths = {};
4319
+ for (const split of splits) {
4320
+ const examples = kept.get(split);
4321
+ if (examples.length === 0) {
4322
+ throw new Error(`No usable ${split} records remain after applying dataset_prebuilt.huggingface.columns.`);
4323
+ }
4324
+ const output = join8(args.outputRoot, `${split}-${key}.jsonl`);
4325
+ const content = `${examples.map((example) => JSON.stringify(exampleToChatRow(args.spec, example))).join("\n")}
4326
+ `;
4327
+ if (await readFile7(output, "utf8").catch(() => null) !== content) {
4328
+ await writeFileAtomic(output, content);
4329
+ }
4330
+ localPaths[split] = output;
4331
+ }
4332
+ return { localPaths, conversion };
4333
+ }
4334
+ async function resolveRequestDataset(request, config) {
4335
+ const dataset = huggingFaceDataset(request);
4336
+ if (!dataset) return request;
4337
+ const resolved = await resolveCachedHuggingFaceDataset({ dataset, modelCache: config.paths.modelCache });
4338
+ let localPaths = resolved.localPaths;
4339
+ let source = resolved.source;
4340
+ const columns = dataset.huggingface.columns;
4341
+ if (columns) {
4342
+ const converted = await convertMappedDataset({
4343
+ resolved,
4344
+ columns,
4345
+ spec: request.spec_snapshot,
4346
+ outputRoot: resolve10(config.artifactRoot, "datasets", "huggingface", safeName(dataset.huggingface.repo), resolved.source.revision)
4347
+ });
4348
+ localPaths = converted.localPaths;
4349
+ source = datasetSourceSchema.parse({ ...source, conversion: converted.conversion });
4350
+ }
4351
+ return fineTuneRunRequestSchema.parse({
4352
+ ...request,
4353
+ dataset_prebuilt: {
4354
+ ...localPaths,
4355
+ format: dataset.format
4356
+ },
4357
+ dataset_source: source
4358
+ });
4359
+ }
4360
+ async function prefetchHuggingFaceDataset(args) {
4361
+ const dataset = huggingFaceDataset(args.request);
4362
+ if (!dataset) {
4363
+ throw new Error(
4364
+ "This spec has no dataset_prebuilt.huggingface source. Local dataset files need no download; set dataset_prebuilt.huggingface.repo to train on a Hugging Face dataset."
4365
+ );
4366
+ }
4367
+ const modelCache = args.config.paths.modelCache ? resolve10(args.config.paths.modelCache) : void 0;
4368
+ const hubCache = huggingFaceHubCache(modelCache);
4369
+ if (args.localOnly) {
4370
+ const resolved2 = await resolveCachedHuggingFaceDataset({ dataset, modelCache });
4371
+ return {
4372
+ ok: true,
4373
+ status: "verified",
4374
+ local_files_only: true,
4375
+ hub_cache: hubCache,
4376
+ size_bytes: Object.values(resolved2.source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0),
4377
+ ...resolved2.source
4378
+ };
4379
+ }
4380
+ const artifactDir = resolve10(
4381
+ args.config.artifactRoot,
4382
+ "dataset-prefetch",
4383
+ `${safeName(dataset.huggingface.repo)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
4384
+ );
4385
+ await mkdir7(artifactDir, { recursive: true });
4386
+ const inputPath = join8(artifactDir, "prefetch-input.json");
4387
+ const outputPath = join8(artifactDir, "prefetch-output.json");
4388
+ const logPath = join8(artifactDir, "prefetch.log");
4389
+ await writeJson(inputPath, {
4390
+ repo: dataset.huggingface.repo,
4391
+ ...dataset.huggingface.revision ? { revision: dataset.huggingface.revision } : {},
4392
+ files: datasetFiles(dataset),
4393
+ ...modelCache ? { model_cache: modelCache } : {}
4394
+ });
4395
+ const entrypoint = buildBundledPythonCommand("prefetch_dataset.py", ["--input", inputPath, "--output", outputPath]);
4396
+ await args.reporter?.onEvent?.({
4397
+ stage: "dataset_prefetch",
4398
+ status: "running",
4399
+ message: "Downloading Hugging Face dataset files.",
4400
+ details: { repo: dataset.huggingface.repo, revision: dataset.huggingface.revision ?? "main", hub_cache: hubCache, log_path: logPath }
4401
+ });
4402
+ const { exitCode, stderr } = await runLoggedProcess({
4403
+ command: entrypoint.command,
4404
+ commandArgs: entrypoint.commandArgs,
4405
+ env: withBundledPythonEnvironment(withHuggingFaceCacheEnvironment(minimalMachineLearningEnvironment(process.env), modelCache)),
4406
+ logPath,
4407
+ reporter: args.reporter ? { ...args.reporter, verbose: true } : void 0,
4408
+ stage: "dataset_prefetch"
4409
+ });
4410
+ if (exitCode !== 0) {
4411
+ const reason = stderr.trim().split("\n").filter(Boolean).at(-1);
4412
+ throw new Error(
4413
+ `Dataset prefetch exited with code ${exitCode}${reason ? `: ${reason}` : ""}. See ${logPath}. Rerunning resumes from the files already downloaded.`
4414
+ );
4415
+ }
4416
+ const output = JSON.parse(await readFile7(outputPath, "utf8"));
4417
+ const revision = baseModelRevisionSchema.parse(output.snapshot_revision);
4418
+ const resolved = await resolveCachedHuggingFaceDataset({
4419
+ dataset: { ...dataset, huggingface: { ...dataset.huggingface, revision } },
4420
+ modelCache
4421
+ });
4422
+ for (const [split, file] of Object.entries(resolved.source.files)) {
4423
+ if (output.files?.[split]?.sha256 !== file?.sha256) {
4424
+ throw new Error(`Dataset prefetch reported a different digest for ${split} than the cached file.`);
4425
+ }
4426
+ }
4427
+ const source = datasetSourceSchema.parse({
4428
+ ...resolved.source,
4429
+ requested_revision: dataset.huggingface.revision ?? null,
4430
+ pinned: Boolean(dataset.huggingface.revision)
4431
+ });
4432
+ const sizeBytes = Object.values(source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0);
4433
+ await args.reporter?.onEvent?.({
4434
+ stage: "dataset_prefetch",
4435
+ status: "completed",
4436
+ message: "Dataset files are available in the local Hugging Face cache.",
4437
+ details: { repo: source.repo, revision: source.revision, pinned: source.pinned, size_bytes: sizeBytes }
4438
+ });
4439
+ return {
4440
+ ok: true,
4441
+ status: "completed",
4442
+ local_files_only: false,
4443
+ hub_cache: hubCache,
4444
+ size_bytes: sizeBytes,
4445
+ ...source,
4446
+ artifact_dir: artifactDir,
4447
+ log_uri: fileUri(logPath),
4448
+ command: entrypoint.displayCommand
4449
+ };
4450
+ }
4451
+ async function ensureDatasetCached(args) {
4452
+ if (!huggingFaceDataset(args.request)) return { request: args.request, prefetch: null };
4453
+ try {
4454
+ return { request: await resolveRequestDataset(args.request, args.config), prefetch: null };
4455
+ } catch (error) {
4456
+ if (!(error instanceof DatasetNotDownloadedError) || args.config.dryRun) throw error;
4457
+ }
4458
+ const prefetch = await prefetchHuggingFaceDataset(args);
4459
+ return { request: await resolveRequestDataset(args.request, args.config), prefetch };
4460
+ }
4461
+
3294
4462
  // src/local-runtime/general-regression.ts
3295
4463
  function evaluateGeneralRegressionGate(comparison, policy) {
3296
4464
  const failures = [];
@@ -3311,18 +4479,18 @@ function evaluateGeneralRegressionGate(comparison, policy) {
3311
4479
  }
3312
4480
 
3313
4481
  // src/local-runtime/orchestrator.ts
3314
- import { lstat as lstat5, mkdir as mkdir9, open as open3, readFile as readFile8, readdir as readdir5, rename as rename3, rm as rm4, stat as stat7 } from "fs/promises";
3315
- import { createReadStream as createReadStream3 } from "fs";
3316
- import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
4482
+ import { lstat as lstat5, mkdir as mkdir11, open as open4, readFile as readFile10, readdir as readdir5, rename as rename4, rm as rm5, stat as stat9 } from "fs/promises";
4483
+ import { createReadStream as createReadStream5 } from "fs";
4484
+ import { createHash as createHash9, randomUUID as randomUUID6 } from "crypto";
3317
4485
  import { performance } from "perf_hooks";
3318
4486
  import { homedir as homedir4 } from "os";
3319
- import { basename as basename2, dirname as dirname9, join as join10, relative as relative4, resolve as resolve13 } from "path";
4487
+ import { basename as basename3, dirname as dirname9, join as join12, relative as relative5, resolve as resolve14 } from "path";
3320
4488
 
3321
4489
  // src/local-runtime/gpu-executor.ts
3322
- import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
3323
- import { mkdtemp, mkdir as mkdir6, realpath as realpath3, rm as rm2, stat as stat5, writeFile as writeFile4 } from "fs/promises";
4490
+ import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
4491
+ import { mkdtemp, mkdir as mkdir8, realpath as realpath4, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
3324
4492
  import { tmpdir } from "os";
3325
- import { basename, dirname as dirname7, join as join7, resolve as resolve10 } from "path";
4493
+ import { basename as basename2, dirname as dirname7, join as join9, resolve as resolve11 } from "path";
3326
4494
  function shellQuote(value) {
3327
4495
  return `'${value.replaceAll("'", "'\\''")}'`;
3328
4496
  }
@@ -3387,23 +4555,23 @@ async function checkAwsGpu(gpu) {
3387
4555
  if (result.exitCode !== 0) throw new Error(`AWS GPU preflight failed: ${result.stderr}`);
3388
4556
  }
3389
4557
  function gpuBaseModelPath(config, model, revision) {
3390
- if (config.paths.baseModel) return resolve10(config.paths.baseModel);
4558
+ if (config.paths.baseModel) return resolve11(config.paths.baseModel);
3391
4559
  if (!revision || !/^[a-f0-9]{40}$/i.test(revision)) {
3392
4560
  throw new Error("AWS GPU execution needs an immutable base-model revision. Run tt models prefetch first or configure paths.baseModel.");
3393
4561
  }
3394
4562
  const env = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
3395
- return join7(env.HF_HUB_CACHE, `models--${model.replaceAll("/", "--")}`, "snapshots", revision);
4563
+ return join9(env.HF_HUB_CACHE, `models--${model.replaceAll("/", "--")}`, "snapshots", revision);
3396
4564
  }
3397
4565
  async function physicalTransferPath(path) {
3398
4566
  try {
3399
- return await realpath3(path);
4567
+ return await realpath4(path);
3400
4568
  } catch (error) {
3401
4569
  if (error.code !== "ENOENT" || dirname7(path) === path) throw error;
3402
- return join7(await physicalTransferPath(dirname7(path)), basename(path));
4570
+ return join9(await physicalTransferPath(dirname7(path)), basename2(path));
3403
4571
  }
3404
4572
  }
3405
4573
  async function assertSeparateTransferPaths(files) {
3406
- const paths = await Promise.all(files.map((file) => physicalTransferPath(resolve10(file.path))));
4574
+ const paths = await Promise.all(files.map((file) => physicalTransferPath(resolve11(file.path))));
3407
4575
  for (let i = 0; i < files.length; i += 1) {
3408
4576
  for (let j = 0; j < i; j += 1) {
3409
4577
  if (files[i].direction === "input" && files[j].direction === "input") continue;
@@ -3420,13 +4588,13 @@ async function runGpuProcess(args) {
3420
4588
  await assertSeparateTransferPaths(args.files);
3421
4589
  const host = await resolveGpuHost(args.gpu);
3422
4590
  const options = sshOptions(args.gpu);
3423
- const identity = args.logPath ? createHash5("sha256").update(resolve10(args.logPath)).digest("hex").slice(0, 32) : randomUUID4();
4591
+ const identity = args.logPath ? createHash7("sha256").update(resolve11(args.logPath)).digest("hex").slice(0, 32) : randomUUID5();
3424
4592
  const root = `/tmp/tt-gpu-${identity}`;
3425
- const scratch = await mkdtemp(join7(tmpdir(), "tt-gpu-"));
3426
- const runtime = join7(localRuntimePackageRoot(import.meta.url), "training", args.runtime);
3427
- const mappings = args.files.map((file, index) => ({ ...file, path: resolve10(file.path), remote: `${root}/files/${index}/${basename(file.path)}` }));
4593
+ const scratch = await mkdtemp(join9(tmpdir(), "tt-gpu-"));
4594
+ const runtime = join9(localRuntimePackageRoot(import.meta.url), "training", args.runtime);
4595
+ const mappings = args.files.map((file, index) => ({ ...file, path: resolve11(file.path), remote: `${root}/files/${index}/${basename2(file.path)}` }));
3428
4596
  const mapPath = (path) => {
3429
- const absolute = resolve10(path);
4597
+ const absolute = resolve11(path);
3430
4598
  const match = [...mappings].sort((a, b) => b.path.length - a.path.length).find((file) => absolute === file.path || file.directory && absolute.startsWith(`${file.path}/`));
3431
4599
  if (match) return match.remote + absolute.slice(match.path.length);
3432
4600
  if (absolute === runtime || absolute.startsWith(`${runtime}/`)) return `${root}/runtime${absolute.slice(runtime.length)}`;
@@ -3464,7 +4632,7 @@ async function runGpuProcess(args) {
3464
4632
  };
3465
4633
  const grace = Math.ceil((args.shutdownGraceMs ?? 5e3) / 1e3);
3466
4634
  const stop = `kill -TERM -- -"$pid" 2>/dev/null || true; remaining=${grace}; while kill -0 -- -"$pid" 2>/dev/null && [ "$remaining" -gt 0 ]; do sleep 1; remaining=$((remaining - 1)); done; kill -KILL -- -"$pid" 2>/dev/null || true`;
3467
- const owner = randomUUID4();
4635
+ const owner = randomUUID5();
3468
4636
  let reservationAttempted = false;
3469
4637
  let started = false;
3470
4638
  let completed = false;
@@ -3475,12 +4643,12 @@ async function runGpuProcess(args) {
3475
4643
  throw new Error(`Cannot reserve GPU staging directory ${host}:${root}. Another process or unrecovered artifacts may be present. ${error instanceof Error ? error.message : error}`);
3476
4644
  });
3477
4645
  for (const name of ["pyproject.toml", "uv.lock", "src"]) {
3478
- await sync(join7(runtime, name), `${root}/runtime/`, true);
4646
+ await sync(join9(runtime, name), `${root}/runtime/`, true);
3479
4647
  }
3480
4648
  for (const file of mappings) {
3481
4649
  await ssh(`mkdir -p ${shellQuote(file.directory ? file.remote : dirname7(file.remote))}`);
3482
4650
  if (file.direction !== "output") {
3483
- const exists3 = await stat5(file.path).catch((error) => {
4651
+ const exists3 = await stat7(file.path).catch((error) => {
3484
4652
  if (error.code === "ENOENT" && file.direction === "both") return void 0;
3485
4653
  throw error;
3486
4654
  });
@@ -3490,8 +4658,8 @@ async function runGpuProcess(args) {
3490
4658
  if (args.document) {
3491
4659
  const value = { ...args.document.value };
3492
4660
  for (const key of args.document.pathKeys) if (typeof value[key] === "string") value[key] = mapPath(value[key]);
3493
- const path = join7(scratch, "document.json");
3494
- await writeFile4(path, JSON.stringify(value), { mode: 384 });
4661
+ const path = join9(scratch, "document.json");
4662
+ await writeFile5(path, JSON.stringify(value), { mode: 384 });
3495
4663
  await sync(path, mapPath(args.document.path), true);
3496
4664
  }
3497
4665
  const env = {
@@ -3523,7 +4691,7 @@ async function runGpuProcess(args) {
3523
4691
  if (started) {
3524
4692
  await ssh(`bash -c ${shellQuote(`if test -f ${root}/pid; then pid=$(cat ${root}/pid); ${stop}; fi`)}`, false);
3525
4693
  for (const file of mappings.filter((file2) => file2.direction !== "input")) {
3526
- await mkdir6(file.directory ? file.path : dirname7(file.path), { recursive: true });
4694
+ await mkdir8(file.directory ? file.path : dirname7(file.path), { recursive: true });
3527
4695
  if (!completed && !file.directory) {
3528
4696
  const exists3 = await runLoggedProcess({ command: "ssh", commandArgs: [...options, host, `test -e ${shellQuote(file.remote)}`], stage: args.stage, timeoutMs: 3e4 });
3529
4697
  if (exists3.exitCode === 1) continue;
@@ -3536,15 +4704,15 @@ async function runGpuProcess(args) {
3536
4704
  } catch (error) {
3537
4705
  throw new Error(`GPU cleanup or artifact retrieval failed. Remote files remain at ${host}:${root}. ${error instanceof Error ? error.message : error}`);
3538
4706
  } finally {
3539
- await rm2(scratch, { recursive: true, force: true });
4707
+ await rm3(scratch, { recursive: true, force: true });
3540
4708
  }
3541
4709
  }
3542
4710
  }
3543
4711
 
3544
4712
  // src/local-runtime/evaluation.ts
3545
- import { createHash as createHash6 } from "crypto";
3546
- import { mkdir as mkdir7, readFile as readFile6, rm as rm3, stat as stat6, writeFile as writeFile5 } from "fs/promises";
3547
- import { dirname as dirname8, join as join8, resolve as resolve11 } from "path";
4713
+ import { createHash as createHash8 } from "crypto";
4714
+ import { mkdir as mkdir9, readFile as readFile8, rm as rm4, stat as stat8, writeFile as writeFile6 } from "fs/promises";
4715
+ import { dirname as dirname8, join as join10, resolve as resolve12 } from "path";
3548
4716
  function normalize(value) {
3549
4717
  return value.trim().replace(/\s+/g, " ").toLowerCase();
3550
4718
  }
@@ -3638,11 +4806,11 @@ function extractJsonObject(value) {
3638
4806
  }
3639
4807
  return null;
3640
4808
  }
3641
- function canonicalJson3(value) {
3642
- if (Array.isArray(value)) return value.map(canonicalJson3);
4809
+ function canonicalJson4(value) {
4810
+ if (Array.isArray(value)) return value.map(canonicalJson4);
3643
4811
  if (value && typeof value === "object") {
3644
4812
  return Object.fromEntries(
3645
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson3(item)])
4813
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson4(item)])
3646
4814
  );
3647
4815
  }
3648
4816
  return value;
@@ -3668,7 +4836,7 @@ function scoreJsonFields(expected, actual, configuredFields) {
3668
4836
  const schemaMatch = Boolean(actualJson) && expectedKeys.length === actualKeys.length && expectedKeys.every((key, index) => key === actualKeys[index]);
3669
4837
  const fieldResults = {};
3670
4838
  for (const field of fields) {
3671
- fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(canonicalJson3(expectedJson[field])) === JSON.stringify(canonicalJson3(actualJson?.[field]));
4839
+ fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(canonicalJson4(expectedJson[field])) === JSON.stringify(canonicalJson4(actualJson?.[field]));
3672
4840
  }
3673
4841
  const correct = Object.values(fieldResults).filter(Boolean).length;
3674
4842
  const score = fields.length > 0 ? correct / fields.length : 0;
@@ -3764,8 +4932,8 @@ async function runTransformersInference(args) {
3764
4932
  const inputPath = `${args.outputPath}.inference-input.json`;
3765
4933
  const outputPath = `${args.outputPath}.inference-output.json`;
3766
4934
  const logPath = `${args.outputPath}.inference.log`;
3767
- await mkdir7(dirname8(inputPath), { recursive: true });
3768
- await writeFile5(inputPath, `${JSON.stringify({
4935
+ await mkdir9(dirname8(inputPath), { recursive: true });
4936
+ await writeFile6(inputPath, `${JSON.stringify({
3769
4937
  protocol_version: INFERENCE_PROTOCOL_VERSION,
3770
4938
  kind: args.kind,
3771
4939
  model_id: args.modelId,
@@ -3779,7 +4947,7 @@ async function runTransformersInference(args) {
3779
4947
  id: String(index),
3780
4948
  input: example.input
3781
4949
  })),
3782
- model_cache: args.config.paths.modelCache ? resolve11(args.config.paths.modelCache) : void 0,
4950
+ model_cache: args.config.paths.modelCache ? resolve12(args.config.paths.modelCache) : void 0,
3783
4951
  trust_remote_code: false,
3784
4952
  device: args.config.evaluation.inference.device,
3785
4953
  generation: {
@@ -3789,7 +4957,7 @@ async function runTransformersInference(args) {
3789
4957
  }
3790
4958
  }, null, 2)}
3791
4959
  `, "utf8");
3792
- await rm3(outputPath, { force: true });
4960
+ await rm4(outputPath, { force: true });
3793
4961
  const entrypoint = buildBundledPythonCommand(
3794
4962
  "evaluate.py",
3795
4963
  ["--input", inputPath, "--output", outputPath]
@@ -3807,7 +4975,7 @@ async function runTransformersInference(args) {
3807
4975
  });
3808
4976
  const gpu = args.config.evaluation.inference.device === "cuda" ? args.config.gpu : void 0;
3809
4977
  const execute = gpu ? async (processArgs) => {
3810
- const document = JSON.parse(await readFile6(inputPath, "utf8"));
4978
+ const document = JSON.parse(await readFile8(inputPath, "utf8"));
3811
4979
  document.model_source = gpuBaseModelPath(args.config, args.baseModelId, args.baseModelRevision);
3812
4980
  delete document.model_cache;
3813
4981
  const files = [
@@ -3817,7 +4985,7 @@ async function runTransformersInference(args) {
3817
4985
  ...document.adapter_path ? [{
3818
4986
  path: document.adapter_path,
3819
4987
  direction: "input",
3820
- directory: (await stat6(document.adapter_path)).isDirectory()
4988
+ directory: (await stat8(document.adapter_path)).isDirectory()
3821
4989
  }] : []
3822
4990
  ];
3823
4991
  return runGpuProcess({
@@ -3851,7 +5019,7 @@ async function runTransformersInference(args) {
3851
5019
  }
3852
5020
  let output;
3853
5021
  try {
3854
- output = JSON.parse(await readFile6(outputPath, "utf8"));
5022
+ output = JSON.parse(await readFile8(outputPath, "utf8"));
3855
5023
  } catch (error) {
3856
5024
  throw new Error(
3857
5025
  `Transformers inference did not write valid JSON: ${error instanceof Error ? error.message : String(error)}`
@@ -3868,7 +5036,7 @@ function assertEvaluationScoringReady(config) {
3868
5036
  }
3869
5037
  }
3870
5038
  function baselineCacheKey(args) {
3871
- return createHash6("sha256").update(JSON.stringify({
5039
+ return createHash8("sha256").update(JSON.stringify({
3872
5040
  v: 6,
3873
5041
  inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
3874
5042
  package_version: args.packageVersion,
@@ -3888,15 +5056,15 @@ function baselineInputsAreStable(args) {
3888
5056
  return args.config.paths.baseModel ? Boolean(args.sourceFingerprint) : Boolean(args.baseModelRevision);
3889
5057
  }
3890
5058
  function baselineStoreRoot(config) {
3891
- return config.storeRoot ? resolve11(config.storeRoot) : defaultLocalHome();
5059
+ return config.storeRoot ? resolve12(config.storeRoot) : defaultLocalHome();
3892
5060
  }
3893
5061
  function baselineCachePath(config, key) {
3894
- return join8(baselineStoreRoot(config), "cache", "baseline-evals", `${key}.json`);
5062
+ return join10(baselineStoreRoot(config), "cache", "baseline-evals", `${key}.json`);
3895
5063
  }
3896
5064
  async function readBaselineCache(config, path) {
3897
5065
  await hardenExistingLocalStore(baselineStoreRoot(config));
3898
5066
  try {
3899
- return evalReportSchema.parse(JSON.parse(await readFile6(path, "utf8")));
5067
+ return evalReportSchema.parse(JSON.parse(await readFile8(path, "utf8")));
3900
5068
  } catch {
3901
5069
  return null;
3902
5070
  }
@@ -3905,7 +5073,7 @@ var cachedPackageVersion = null;
3905
5073
  async function packageVersion() {
3906
5074
  if (cachedPackageVersion !== null) return cachedPackageVersion;
3907
5075
  try {
3908
- const raw = await readFile6(resolve11(localRuntimePackageRoot(import.meta.url), "package.json"), "utf8");
5076
+ const raw = await readFile8(resolve12(localRuntimePackageRoot(import.meta.url), "package.json"), "utf8");
3909
5077
  cachedPackageVersion = String(
3910
5078
  JSON.parse(raw).version ?? "unknown"
3911
5079
  );
@@ -3974,7 +5142,7 @@ async function evaluateExamples(args) {
3974
5142
  baseModelId,
3975
5143
  args.baseModelRevision
3976
5144
  );
3977
- const localModelSource = args.modelSource ? resolve11(args.modelSource) !== resolve11(baseModelId) : false;
5145
+ const localModelSource = args.modelSource ? resolve12(args.modelSource) !== resolve12(baseModelId) : false;
3978
5146
  const baseModelRevision = localModelSource ? defaultBaseModelRevision(baseModelId) : requestedBaseModelRevision;
3979
5147
  const maxExamples = args.config.evaluation.maxExamples ?? args.maxExamples ?? args.examples.length;
3980
5148
  const truncated = args.examples.length > maxExamples;
@@ -4090,8 +5258,8 @@ function compareEvalReports(baseline, candidate) {
4090
5258
  }
4091
5259
 
4092
5260
  // src/local-runtime/process-training.ts
4093
- import { access, mkdir as mkdir8, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
4094
- import { join as join9, resolve as resolve12 } from "path";
5261
+ import { access, mkdir as mkdir10, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
5262
+ import { join as join11, resolve as resolve13 } from "path";
4095
5263
  function buildTrainingHyperparameters(request, options = {}) {
4096
5264
  const model = resolveTrainingModel(request.spec_snapshot.base_model);
4097
5265
  const hyper = request.hyperparameters;
@@ -4182,15 +5350,15 @@ function createTrainingProgressForwarder(reporter) {
4182
5350
  async function launchProcessTraining(args) {
4183
5351
  const { request, artifacts, config } = args;
4184
5352
  const jobName = `tt-local-${request.run_id}`;
4185
- await mkdir8(artifacts.trainingDir, { recursive: true });
5353
+ await mkdir10(artifacts.trainingDir, { recursive: true });
4186
5354
  await copyDatasetToTrainingChannel(artifacts);
4187
5355
  const hyperparameters = buildTrainingHyperparameters(request, {
4188
5356
  baseModelRevision: args.baseModelRevision
4189
5357
  });
4190
- await writeJson(join9(artifacts.trainingConfigDir, "hyperparameters.json"), hyperparameters);
5358
+ await writeJson(join11(artifacts.trainingConfigDir, "hyperparameters.json"), hyperparameters);
4191
5359
  const entrypoint = buildBundledPythonCommand("train.py");
4192
5360
  if (config.dryRun) {
4193
- await writeFile6(
5361
+ await writeFile7(
4194
5362
  artifacts.trainingLog,
4195
5363
  "Dry run enabled. Training process was not launched.\n",
4196
5364
  "utf8"
@@ -4212,11 +5380,11 @@ async function launchProcessTraining(args) {
4212
5380
  withOfflineHuggingFaceCacheEnvironment({
4213
5381
  ...inheritedEnv,
4214
5382
  BACKEND: "local",
4215
- SM_CHANNEL_TRAINING: resolve12(artifacts.trainingInputDir),
4216
- SM_CHANNEL_BASE_MODEL: config.paths.baseModel ? resolve12(config.paths.baseModel) : void 0,
4217
- SM_MODEL_DIR: resolve12(artifacts.trainingModelDir),
4218
- SM_OUTPUT_DIR: resolve12(artifacts.trainingOutputDir),
4219
- TT_HYPERPARAMETERS_PATH: resolve12(artifacts.trainingConfigDir, "hyperparameters.json")
5383
+ SM_CHANNEL_TRAINING: resolve13(artifacts.trainingInputDir),
5384
+ SM_CHANNEL_BASE_MODEL: config.paths.baseModel ? resolve13(config.paths.baseModel) : void 0,
5385
+ SM_MODEL_DIR: resolve13(artifacts.trainingModelDir),
5386
+ SM_OUTPUT_DIR: resolve13(artifacts.trainingOutputDir),
5387
+ TT_HYPERPARAMETERS_PATH: resolve13(artifacts.trainingConfigDir, "hyperparameters.json")
4220
5388
  }, config.paths.modelCache)
4221
5389
  );
4222
5390
  for (const [key, value] of Object.entries(childEnv)) {
@@ -4224,10 +5392,10 @@ async function launchProcessTraining(args) {
4224
5392
  }
4225
5393
  const remoteBaseModel = config.gpu ? gpuBaseModelPath(config, request.spec_snapshot.base_model, args.baseModelRevision) : void 0;
4226
5394
  if (remoteBaseModel) childEnv.SM_CHANNEL_BASE_MODEL = remoteBaseModel;
4227
- if (config.paths.modelCache) await mkdir8(resolve12(config.paths.modelCache), { recursive: true });
5395
+ if (config.paths.modelCache) await mkdir10(resolve13(config.paths.modelCache), { recursive: true });
4228
5396
  if (config.paths.baseModel) {
4229
5397
  await verifyLocalBaseModel(
4230
- resolve12(config.paths.baseModel),
5398
+ resolve13(config.paths.baseModel),
4231
5399
  request.spec_snapshot.base_model,
4232
5400
  config.paths.modelCache
4233
5401
  );
@@ -4266,12 +5434,12 @@ async function launchProcessTraining(args) {
4266
5434
  onLine: (line) => forwardTrainingProgress(line),
4267
5435
  shouldCancel: args.shouldCancel
4268
5436
  });
4269
- const metricsPath = join9(artifacts.trainingModelDir, "training-metrics.json");
4270
- const metrics = await pathExists(metricsPath) ? JSON.parse(await readFile7(metricsPath, "utf8")) : null;
5437
+ const metricsPath = join11(artifacts.trainingModelDir, "training-metrics.json");
5438
+ const metrics = await pathExists(metricsPath) ? JSON.parse(await readFile9(metricsPath, "utf8")) : null;
4271
5439
  if (exitCode !== 0) {
4272
5440
  throw new Error(`Training process exited with code ${exitCode}. See ${artifacts.trainingLog}.`);
4273
5441
  }
4274
- const modelTarPath = join9(artifacts.trainingOutputDir, "model.tar.gz");
5442
+ const modelTarPath = join11(artifacts.trainingOutputDir, "model.tar.gz");
4275
5443
  const modelUri = await pathExists(modelTarPath) ? fileUri(modelTarPath) : fileUri(artifacts.trainingModelDir);
4276
5444
  await args.reporter?.onEvent?.({
4277
5445
  stage: "training",
@@ -4366,7 +5534,7 @@ function validateLocalPipeline(pipeline) {
4366
5534
  return pipeline;
4367
5535
  }
4368
5536
  async function loadJsonFile(path) {
4369
- return JSON.parse(await readFile8(path, "utf8"));
5537
+ return JSON.parse(await readFile10(path, "utf8"));
4370
5538
  }
4371
5539
  async function loadRunRequest(path) {
4372
5540
  return fineTuneRunRequestSchema.parse(await loadJsonFile(path));
@@ -4374,13 +5542,13 @@ async function loadRunRequest(path) {
4374
5542
  function parseLocalRunnerConfig(document, path) {
4375
5543
  const config = localRunnerConfigSchema.parse(document);
4376
5544
  if (!path) return config;
4377
- const configPath = resolve13(path);
5545
+ const configPath = resolve14(path);
4378
5546
  const base = dirname9(configPath);
4379
5547
  const configPathValue = (value) => {
4380
5548
  if (!value) return void 0;
4381
5549
  if (value === "~") return homedir4();
4382
- if (value.startsWith("~/")) return resolve13(homedir4(), value.slice(2));
4383
- return resolve13(base, value);
5550
+ if (value.startsWith("~/")) return resolve14(homedir4(), value.slice(2));
5551
+ return resolve14(base, value);
4384
5552
  };
4385
5553
  return {
4386
5554
  ...config,
@@ -4402,7 +5570,7 @@ function parseLocalRunnerConfig(document, path) {
4402
5570
  }
4403
5571
  async function loadLocalRunnerConfig(path) {
4404
5572
  if (!path) return parseLocalRunnerConfig({});
4405
- const configPath = resolve13(path);
5573
+ const configPath = resolve14(path);
4406
5574
  return parseLocalRunnerConfig(
4407
5575
  await loadJsonFile(configPath),
4408
5576
  configPath
@@ -4417,7 +5585,7 @@ function stripFileUri(path) {
4417
5585
  }
4418
5586
  async function pathExists2(path) {
4419
5587
  try {
4420
- await stat7(path);
5588
+ await stat9(path);
4421
5589
  return true;
4422
5590
  } catch {
4423
5591
  return false;
@@ -4461,7 +5629,7 @@ function addDryRunPlaceholders(request, dryRun) {
4461
5629
  };
4462
5630
  }
4463
5631
  async function validateDatasetInputs(request, config) {
4464
- const inputIdentity = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
5632
+ const inputIdentity = datasetInputIdentity;
4465
5633
  const evalSampleSeed = config.evaluation.sampleSeed ?? deriveSampleSeed(request.behavior_spec_id);
4466
5634
  if (!request.dataset_prebuilt) {
4467
5635
  const inlineExamples = examplesFromSpec(request.spec_snapshot);
@@ -4551,7 +5719,10 @@ async function validateDatasetInputs(request, config) {
4551
5719
  }
4552
5720
  async function validateLocalFineTuneInput(input) {
4553
5721
  const config = localRunnerConfigSchema.parse(input.config);
4554
- const request = fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun));
5722
+ const request = await resolveRequestDataset(
5723
+ fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun)),
5724
+ config
5725
+ );
4555
5726
  await validateDatasetInputs(request, config);
4556
5727
  if (config.evaluation.generalRegression) {
4557
5728
  await evaluationSuiteFromChatJsonl(
@@ -4568,14 +5739,14 @@ function artifactPrefix(request) {
4568
5739
  runId: request.run_id
4569
5740
  });
4570
5741
  }
4571
- function canonicalJson4(value) {
5742
+ function canonicalJson5(value) {
4572
5743
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
4573
- if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson4(entry)).join(",")}]`;
5744
+ if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson5(entry)).join(",")}]`;
4574
5745
  const object = value;
4575
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson4(object[key])}`).join(",")}}`;
5746
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson5(object[key])}`).join(",")}}`;
4576
5747
  }
4577
5748
  function hashJson(value) {
4578
- return createHash7("sha256").update(canonicalJson4(value)).digest("hex");
5749
+ return createHash9("sha256").update(canonicalJson5(value)).digest("hex");
4579
5750
  }
4580
5751
  var packageRoot2 = localRuntimePackageRoot(import.meta.url);
4581
5752
  async function hashFileIfPresent(path) {
@@ -4587,21 +5758,21 @@ async function hashFileIfPresent(path) {
4587
5758
  }
4588
5759
  async function packageVersion2() {
4589
5760
  try {
4590
- const parsed = JSON.parse(await readFile8(resolve13(packageRoot2, "package.json"), "utf8"));
5761
+ const parsed = JSON.parse(await readFile10(resolve14(packageRoot2, "package.json"), "utf8"));
4591
5762
  return typeof parsed.version === "string" ? parsed.version : "unknown";
4592
5763
  } catch {
4593
5764
  return "unknown";
4594
5765
  }
4595
5766
  }
4596
5767
  async function runtimeFingerprint() {
4597
- const bundledProject2 = resolve13(packageRoot2, "training/adapter");
5768
+ const bundledProject2 = resolve14(packageRoot2, "training/adapter");
4598
5769
  return hashJson({
4599
5770
  tt_local_version: await packageVersion2(),
4600
5771
  node_version: process.version,
4601
5772
  platform: process.platform,
4602
5773
  architecture: process.arch,
4603
- runner_pyproject: await hashFileIfPresent(resolve13(bundledProject2, "pyproject.toml")),
4604
- runner_uv_lock: await hashFileIfPresent(resolve13(bundledProject2, "uv.lock"))
5774
+ runner_pyproject: await hashFileIfPresent(resolve14(bundledProject2, "pyproject.toml")),
5775
+ runner_uv_lock: await hashFileIfPresent(resolve14(bundledProject2, "uv.lock"))
4605
5776
  });
4606
5777
  }
4607
5778
  function preparedSourceFingerprint(args) {
@@ -4615,8 +5786,8 @@ function preparedSourceFingerprint(args) {
4615
5786
  });
4616
5787
  }
4617
5788
  async function hashFile(path) {
4618
- const hash = createHash7("sha256");
4619
- for await (const chunk of createReadStream3(path)) hash.update(chunk);
5789
+ const hash = createHash9("sha256");
5790
+ for await (const chunk of createReadStream5(path)) hash.update(chunk);
4620
5791
  return hash.digest("hex");
4621
5792
  }
4622
5793
  async function datasetFingerprints(request) {
@@ -4640,7 +5811,7 @@ async function resolveBaseModelRevision(request, config) {
4640
5811
  request.hyperparameters.base_model_revision
4641
5812
  );
4642
5813
  if (config.paths.baseModel) {
4643
- return defaultBaseModelRevision(request.spec_snapshot.base_model);
5814
+ return isCertifiedTrainingModel(request.spec_snapshot.base_model) ? defaultBaseModelRevision(request.spec_snapshot.base_model) : requested;
4644
5815
  }
4645
5816
  if (requested) return requested;
4646
5817
  if (!request.spec_snapshot.base_model.includes("/")) {
@@ -4648,9 +5819,9 @@ async function resolveBaseModelRevision(request, config) {
4648
5819
  }
4649
5820
  const repository = `models--${request.spec_snapshot.base_model.replaceAll("/", "--")}`;
4650
5821
  const cacheEnvironment = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
4651
- const refPath = resolve13(cacheEnvironment.HF_HUB_CACHE, repository, "refs", "main");
5822
+ const refPath = resolve14(cacheEnvironment.HF_HUB_CACHE, repository, "refs", "main");
4652
5823
  try {
4653
- const revision = (await readFile8(refPath, "utf8")).trim();
5824
+ const revision = (await readFile10(refPath, "utf8")).trim();
4654
5825
  if (revision) return revision;
4655
5826
  } catch {
4656
5827
  }
@@ -4669,18 +5840,18 @@ async function fingerprintLocalBaseModel(uri, expectedModelId, modelCache) {
4669
5840
  const files = [];
4670
5841
  const visit = async (directory) => {
4671
5842
  for (const entry of await readdir5(directory, { withFileTypes: true })) {
4672
- const child = resolve13(directory, entry.name);
5843
+ const child = resolve14(directory, entry.name);
4673
5844
  if (entry.isDirectory()) {
4674
5845
  await visit(child);
4675
5846
  continue;
4676
5847
  }
4677
- const childMetadata = await stat7(child);
5848
+ const childMetadata = await stat9(child);
4678
5849
  if (entry.isSymbolicLink() && !childMetadata.isFile()) {
4679
5850
  throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
4680
5851
  }
4681
5852
  if (!childMetadata.isFile()) continue;
4682
5853
  files.push({
4683
- path: relative4(root, child).split("\\").join("/"),
5854
+ path: relative5(root, child).split("\\").join("/"),
4684
5855
  size_bytes: childMetadata.size,
4685
5856
  sha256: await hashFile(child)
4686
5857
  });
@@ -4709,26 +5880,26 @@ async function readStageMetadata(path) {
4709
5880
  }
4710
5881
  async function removePrefixedArtifacts(path) {
4711
5882
  const directory = dirname9(path);
4712
- const prefix = `${basename2(path)}.`;
5883
+ const prefix = `${basename3(path)}.`;
4713
5884
  const names = await readdir5(directory).catch(() => []);
4714
5885
  await Promise.all([
4715
- rm4(path, { force: true }),
4716
- ...names.filter((name) => name.startsWith(prefix)).map((name) => rm4(resolve13(directory, name), { recursive: true, force: true }))
5886
+ rm5(path, { force: true }),
5887
+ ...names.filter((name) => name.startsWith(prefix)).map((name) => rm5(resolve14(directory, name), { recursive: true, force: true }))
4717
5888
  ]);
4718
5889
  }
4719
5890
  async function cleanupStageArtifacts(artifacts, stage) {
4720
- const removeReport = async () => rm4(artifacts.runReportJson, { force: true });
5891
+ const removeReport = async () => rm5(artifacts.runReportJson, { force: true });
4721
5892
  if (stage === "prepare") {
4722
5893
  await Promise.all([
4723
5894
  removePrefixedArtifacts(artifacts.baselineEvalJson),
4724
5895
  removePrefixedArtifacts(artifacts.candidateEvalJson),
4725
5896
  removePrefixedArtifacts(artifacts.generalBaselineEvalJson),
4726
5897
  removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
4727
- rm4(artifacts.trainingDir, { recursive: true, force: true }),
5898
+ rm5(artifacts.trainingDir, { recursive: true, force: true }),
4728
5899
  removePrefixedArtifacts(artifacts.trainingReportJson),
4729
- rm4(resolve13(artifacts.runDir, "model.tar.gz"), { force: true }),
5900
+ rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
4730
5901
  removeReport(),
4731
- rm4(artifacts.artifactManifestJson, { force: true })
5902
+ rm5(artifacts.artifactManifestJson, { force: true })
4732
5903
  ]);
4733
5904
  return;
4734
5905
  }
@@ -4742,9 +5913,9 @@ async function cleanupStageArtifacts(artifacts, stage) {
4742
5913
  }
4743
5914
  if (stage === "train") {
4744
5915
  await Promise.all([
4745
- rm4(artifacts.trainingDir, { recursive: true, force: true }),
5916
+ rm5(artifacts.trainingDir, { recursive: true, force: true }),
4746
5917
  removePrefixedArtifacts(artifacts.trainingReportJson),
4747
- rm4(resolve13(artifacts.runDir, "model.tar.gz"), { force: true }),
5918
+ rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
4748
5919
  removePrefixedArtifacts(artifacts.candidateEvalJson),
4749
5920
  removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
4750
5921
  removeReport()
@@ -4763,7 +5934,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
4763
5934
  await removeReport();
4764
5935
  }
4765
5936
  function manifestRelativePath(artifacts, path) {
4766
- return relative4(artifacts.runDir, path).split("\\").join("/");
5937
+ return relative5(artifacts.runDir, path).split("\\").join("/");
4767
5938
  }
4768
5939
  async function verifyReusableArtifacts(artifacts, paths, options = {}) {
4769
5940
  if (!await pathExists2(artifacts.artifactManifestJson)) return false;
@@ -4780,11 +5951,11 @@ async function throwIfCancelled(store, request) {
4780
5951
  }
4781
5952
  }
4782
5953
  async function acquireWorkflowLock(lockPath, description) {
4783
- await mkdir9(dirname9(lockPath), { recursive: true });
4784
- const token = randomUUID5();
5954
+ await mkdir11(dirname9(lockPath), { recursive: true });
5955
+ const token = randomUUID6();
4785
5956
  for (let attempt = 0; attempt < 3; attempt += 1) {
4786
5957
  try {
4787
- const handle = await open3(lockPath, "wx", 384);
5958
+ const handle = await open4(lockPath, "wx", 384);
4788
5959
  try {
4789
5960
  await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
4790
5961
  `);
@@ -4793,17 +5964,17 @@ async function acquireWorkflowLock(lockPath, description) {
4793
5964
  }
4794
5965
  return async () => {
4795
5966
  try {
4796
- const owner = JSON.parse(await readFile8(lockPath, "utf8"));
4797
- if (owner.token === token) await rm4(lockPath, { force: true });
5967
+ const owner = JSON.parse(await readFile10(lockPath, "utf8"));
5968
+ if (owner.token === token) await rm5(lockPath, { force: true });
4798
5969
  } catch {
4799
5970
  }
4800
5971
  };
4801
5972
  } catch (error) {
4802
5973
  if (error.code !== "EEXIST") throw error;
4803
- const metadata = await stat7(lockPath).catch(() => null);
5974
+ const metadata = await stat9(lockPath).catch(() => null);
4804
5975
  let owner = null;
4805
5976
  try {
4806
- owner = JSON.parse(await readFile8(lockPath, "utf8"));
5977
+ owner = JSON.parse(await readFile10(lockPath, "utf8"));
4807
5978
  } catch {
4808
5979
  owner = null;
4809
5980
  }
@@ -4822,10 +5993,10 @@ async function acquireWorkflowLock(lockPath, description) {
4822
5993
  `${description} already has an active local workflow (lock: ${lockPath}).`
4823
5994
  );
4824
5995
  }
4825
- const stalePath = `${lockPath}.stale.${randomUUID5()}`;
5996
+ const stalePath = `${lockPath}.stale.${randomUUID6()}`;
4826
5997
  try {
4827
- await rename3(lockPath, stalePath);
4828
- await rm4(stalePath, { force: true });
5998
+ await rename4(lockPath, stalePath);
5999
+ await rm5(stalePath, { force: true });
4829
6000
  } catch (renameError) {
4830
6001
  if (renameError.code !== "ENOENT") throw renameError;
4831
6002
  }
@@ -4835,11 +6006,11 @@ async function acquireWorkflowLock(lockPath, description) {
4835
6006
  }
4836
6007
  async function acquireRunLock(store, runId) {
4837
6008
  await store.ensure();
4838
- return acquireWorkflowLock(resolve13(store.paths.runsDir, runId, "workflow.lock"), `Run ${runId}`);
6009
+ return acquireWorkflowLock(resolve14(store.paths.runsDir, runId, "workflow.lock"), `Run ${runId}`);
4839
6010
  }
4840
6011
  async function acquireArtifactLock(artifacts) {
4841
6012
  return acquireWorkflowLock(
4842
- join10(artifacts.runDir, ARTIFACT_WORKFLOW_LOCK_FILE),
6013
+ join12(artifacts.runDir, ARTIFACT_WORKFLOW_LOCK_FILE),
4843
6014
  `Artifact directory ${artifacts.runDir}`
4844
6015
  );
4845
6016
  }
@@ -4886,13 +6057,13 @@ function stageFingerprintPath(prepared, stage) {
4886
6057
  return `${stageOutputPath(prepared, stage)}.stage.json`;
4887
6058
  }
4888
6059
  async function runnerFileFingerprints() {
4889
- const project = resolve13(packageRoot2, "training/adapter");
4890
- const bundledSource = resolve13(packageRoot2, "training/adapter/src");
4891
- const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) => resolve13(bundledSource, entry.name));
6060
+ const project = resolve14(packageRoot2, "training/adapter");
6061
+ const bundledSource = resolve14(packageRoot2, "training/adapter/src");
6062
+ const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) => resolve14(bundledSource, entry.name));
4892
6063
  const candidates = [.../* @__PURE__ */ new Set([
4893
6064
  ...pythonSources,
4894
- resolve13(project, "pyproject.toml"),
4895
- resolve13(project, "uv.lock")
6065
+ resolve14(project, "pyproject.toml"),
6066
+ resolve14(project, "uv.lock")
4896
6067
  ])];
4897
6068
  const fingerprints = {};
4898
6069
  for (const path of candidates.sort()) fingerprints[path] = await hashFileIfPresent(path);
@@ -5006,7 +6177,7 @@ async function ensureRunRecord(args) {
5006
6177
  `Run ${args.request.run_id} cannot be reused with different user, behavior spec, or run number identity.`
5007
6178
  );
5008
6179
  }
5009
- if (resolve13(existing.artifact_dir) !== resolve13(args.artifacts.runDir)) {
6180
+ if (resolve14(existing.artifact_dir) !== resolve14(args.artifacts.runDir)) {
5010
6181
  throw new Error(
5011
6182
  `Run ${args.request.run_id} is already bound to artifact directory ${existing.artifact_dir}; it cannot be resumed with ${args.artifacts.runDir}.`
5012
6183
  );
@@ -5084,7 +6255,7 @@ async function computePreparedRun(args) {
5084
6255
  base_model_for_evaluation: baseModelForEvaluation,
5085
6256
  base_model_revision: baseModelRevision ?? null,
5086
6257
  base_model_fingerprint: baseModelFingerprint ?? null,
5087
- system_prompt_sha256: createHash7("sha256").update(system).digest("hex"),
6258
+ system_prompt_sha256: createHash9("sha256").update(system).digest("hex"),
5088
6259
  prepared_at: (/* @__PURE__ */ new Date()).toISOString()
5089
6260
  };
5090
6261
  if (args.writeArtifacts) await writeJsonAtomic(artifacts.stageMetadataJson, metadata);
@@ -5113,7 +6284,7 @@ async function prepareStage(args) {
5113
6284
  const preparedExists = await pathExists2(args.artifacts.stageMetadataJson) && await pathExists2(args.artifacts.trainingJsonl);
5114
6285
  const prepared = await computePreparedRun({ ...args, writeArtifacts: false });
5115
6286
  await args.store.syncRunRequest(args.request, args.artifacts.runDir);
5116
- await writeJsonAtomic(resolve13(args.artifacts.runDir, "request.json"), args.request);
6287
+ await writeJsonAtomic(resolve14(args.artifacts.runDir, "request.json"), args.request);
5117
6288
  const existingMetadata = preparedExists ? await readStageMetadata(args.artifacts.stageMetadataJson) : null;
5118
6289
  let canReuse = preparedExists && existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
5119
6290
  if (canReuse) {
@@ -5447,6 +6618,56 @@ async function runCandidateStage(args) {
5447
6618
  });
5448
6619
  return report;
5449
6620
  }
6621
+ async function buildRunProvenance(args) {
6622
+ const { metadata, request } = args.prepared;
6623
+ const gpu = args.config.gpu;
6624
+ return runProvenanceSchema.parse({
6625
+ schema_version: 1,
6626
+ recorded_at: args.recordedAt,
6627
+ software: {
6628
+ tt_version: await packageVersion2(),
6629
+ node_version: process.version,
6630
+ platform: process.platform,
6631
+ arch: process.arch,
6632
+ runtime_fingerprint: metadata.runtime_fingerprint,
6633
+ python_lock_sha256: await hashFileIfPresent(resolve14(packageRoot2, "training/adapter/uv.lock"))
6634
+ },
6635
+ execution: {
6636
+ target: gpu ? "aws" : "local",
6637
+ training_backend: args.training.provider,
6638
+ ...gpu ? { instance_id: gpu.instanceId, ...gpu.region ? { region: gpu.region } : {} } : {}
6639
+ },
6640
+ spec: {
6641
+ behavior_spec_id: request.behavior_spec_id,
6642
+ run_number: request.run_number,
6643
+ snapshot_sha256: hashJson(request.spec_snapshot),
6644
+ system_prompt_sha256: metadata.system_prompt_sha256,
6645
+ ...args.specFile ? { file_path: args.specFile.path, file_sha256: args.specFile.sha256 } : {}
6646
+ },
6647
+ base_model: {
6648
+ id: request.spec_snapshot.base_model,
6649
+ revision: metadata.base_model_revision,
6650
+ fingerprint: metadata.base_model_fingerprint
6651
+ },
6652
+ data: {
6653
+ prebuilt: metadata.dataset_prebuilt,
6654
+ format: metadata.dataset_format,
6655
+ file_sha256: metadata.dataset_fingerprints,
6656
+ compiled_training_sha256: await hashFileIfPresent(args.prepared.artifacts.trainingJsonl),
6657
+ training_examples: metadata.training_example_count,
6658
+ eval_split: metadata.eval_split,
6659
+ eval_sample_seed: metadata.eval_sample_seed,
6660
+ eval_examples_total: metadata.eval_examples_total,
6661
+ eval_examples_used: metadata.eval_examples_used
6662
+ },
6663
+ training: {
6664
+ hyperparameters: request.hyperparameters,
6665
+ exit_code: args.training.exit_code,
6666
+ log_uri: args.training.log_uri
6667
+ },
6668
+ source_fingerprint: metadata.source_fingerprint
6669
+ });
6670
+ }
5450
6671
  async function runReportStage(args) {
5451
6672
  if (!await pathExists2(args.prepared.artifacts.baselineEvalJson)) {
5452
6673
  throw new Error("Run reporting requires baseline-eval.json.");
@@ -5539,6 +6760,13 @@ async function runReportStage(args) {
5539
6760
  }
5540
6761
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5541
6762
  const duration = elapsed(args.startedPerf);
6763
+ const provenance = await buildRunProvenance({
6764
+ prepared: args.prepared,
6765
+ config: args.config,
6766
+ training,
6767
+ recordedAt: completedAt,
6768
+ specFile: args.specFile
6769
+ });
5542
6770
  const report = runReportSchema.parse({
5543
6771
  run_id: args.prepared.request.run_id,
5544
6772
  behavior_spec_id: args.prepared.request.behavior_spec_id,
@@ -5552,6 +6780,7 @@ async function runReportStage(args) {
5552
6780
  comparison,
5553
6781
  general_regression: generalRegression,
5554
6782
  training,
6783
+ provenance,
5555
6784
  artifact_uris: {
5556
6785
  dataset: fileUri(args.prepared.artifacts.trainingJsonl),
5557
6786
  baseline_eval: fileUri(args.prepared.artifacts.baselineEvalJson),
@@ -5566,6 +6795,7 @@ async function runReportStage(args) {
5566
6795
  dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
5567
6796
  dataset_format: args.prepared.metadata.dataset_format,
5568
6797
  dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
6798
+ ...args.prepared.request.dataset_source ? { dataset_source: args.prepared.request.dataset_source } : {},
5569
6799
  spec_example_count: args.prepared.request.spec_snapshot.examples.length,
5570
6800
  training_example_count: args.prepared.metadata.training_example_count,
5571
6801
  eval_examples_total: baseline.eval_examples_total,
@@ -5638,7 +6868,7 @@ async function runLocalPipeline(input) {
5638
6868
  store,
5639
6869
  reporter: input.reporter
5640
6870
  });
5641
- await writeJsonAtomic(join10(artifacts.runDir, "resolved-workflow.json"), {
6871
+ await writeJsonAtomic(join12(artifacts.runDir, "resolved-workflow.json"), {
5642
6872
  spec: input.projectSpec ?? {
5643
6873
  ...input.request.spec_snapshot,
5644
6874
  hyperparameters: input.request.hyperparameters,
@@ -5660,7 +6890,7 @@ async function runLocalPipeline(input) {
5660
6890
  if (step.uses === "evaluate") {
5661
6891
  const isBase = step.with.model === "base";
5662
6892
  const report = isBase ? await runBaselineStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter }) : await runCandidateStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter });
5663
- const reportPath = join10(artifacts.runDir, "evaluations", `${step.id}.json`);
6893
+ const reportPath = join12(artifacts.runDir, "evaluations", `${step.id}.json`);
5664
6894
  await writeJsonAtomic(reportPath, { ...report, artifact_uri: fileUri(reportPath) });
5665
6895
  outputs[step.id] = { report: { ...report, artifact_uri: fileUri(reportPath) }, reportPath };
5666
6896
  await refreshArtifactManifest(prepared);
@@ -5679,7 +6909,8 @@ async function runLocalPipeline(input) {
5679
6909
  store,
5680
6910
  reporter: input.reporter,
5681
6911
  startedAt,
5682
- startedPerf
6912
+ startedPerf,
6913
+ specFile: input.specFile
5683
6914
  });
5684
6915
  return { request: input.request, pipeline, status: "completed", outputs, report, reportPath: artifacts.runReportJson, artifactDir: artifacts.runDir };
5685
6916
  }
@@ -5737,8 +6968,8 @@ async function runLocalFineTune(input) {
5737
6968
 
5738
6969
  // src/local-runtime/project-workflow.ts
5739
6970
  import { existsSync as existsSync3 } from "fs";
5740
- import { readFile as readFile9 } from "fs/promises";
5741
- import { dirname as dirname10, join as join11, resolve as resolve14 } from "path";
6971
+ import { readFile as readFile11 } from "fs/promises";
6972
+ import { dirname as dirname10, join as join13, resolve as resolve15 } from "path";
5742
6973
  function configForSpec(spec, specPath) {
5743
6974
  return parseLocalRunnerConfig({
5744
6975
  storeRoot: process.env.TT_LOCAL_HOME?.trim() ? expandUserPath(process.env.TT_LOCAL_HOME.trim()) : DEFAULT_PROJECT_STORE_ROOT,
@@ -5749,9 +6980,9 @@ function configForSpec(spec, specPath) {
5749
6980
  async function resolveProjectConfig(specPath, configPath, warn, loaded) {
5750
6981
  const input = loaded ?? (existsSync3(specPath) ? await loadLocalRunInput(specPath) : void 0);
5751
6982
  const spec = input && input.kind !== "request" ? input.spec : void 0;
5752
- const candidate = configPath ? resolve14(configPath) : join11(dirname10(resolve14(specPath)), "local-runner.json");
6983
+ const candidate = configPath ? resolve15(configPath) : join13(dirname10(resolve15(specPath)), "local-runner.json");
5753
6984
  if (configPath || existsSync3(candidate)) {
5754
- const legacy = parseLocalRunnerConfig(JSON.parse(await readFile9(candidate, "utf8")), candidate);
6985
+ const legacy = parseLocalRunnerConfig(JSON.parse(await readFile11(candidate, "utf8")), candidate);
5755
6986
  if (spec?.runtime || spec?.evaluation) {
5756
6987
  throw new Error("Conflicting configuration sources: tunedtensor.json contains runtime/evaluation settings and a legacy runner config is present. Migrate to tunedtensor.json; do not silently override the spec.");
5757
6988
  }
@@ -5881,6 +7112,9 @@ function trainingGpu(inventory) {
5881
7112
  if (!listed.memory_total_bytes && inventory.python?.total_memory_bytes) {
5882
7113
  return { ...listed, memory_total_bytes: inventory.python.total_memory_bytes };
5883
7114
  }
7115
+ if (!listed.memory_total_bytes && (listed.unified_memory || isUnifiedGpuName(listed.name))) {
7116
+ return { ...listed, memory_total_bytes: inventory.os.total_memory_bytes, unified_memory: true };
7117
+ }
5884
7118
  return listed;
5885
7119
  }
5886
7120
  if (inventory.python?.cuda_device) {
@@ -6026,6 +7260,12 @@ function formatCapabilitySummary(report) {
6026
7260
  }
6027
7261
  function warningsFromSnapshot(report, target) {
6028
7262
  const warnings = [];
7263
+ if (target.engine === "decision") {
7264
+ if (!report.cuda_available) {
7265
+ warnings.push("No CUDA GPU detected: decision training runs on CPU or Apple MPS (roughly 15 s per 8-example step on a 4-core CPU).");
7266
+ }
7267
+ return warnings;
7268
+ }
6029
7269
  if (target.engine === "foundation") {
6030
7270
  for (const kind of ["train", "finetune"]) {
6031
7271
  const verdict = report.foundation[kind];
@@ -6058,7 +7298,7 @@ function warningsFromSnapshot(report, target) {
6058
7298
  import { constants as constants2 } from "fs";
6059
7299
  import { access as access2, statfs } from "fs/promises";
6060
7300
  import { cpus, freemem, homedir as homedir5, totalmem, type } from "os";
6061
- import { dirname as dirname11, join as join12, resolve as resolve15 } from "path";
7301
+ import { dirname as dirname11, join as join14, resolve as resolve16 } from "path";
6062
7302
  import { spawn as spawn2 } from "child_process";
6063
7303
  async function runCommand(command, args, options = {}) {
6064
7304
  const timeoutMs = options.timeoutMs ?? 1e4;
@@ -6212,7 +7452,7 @@ async function queryNvidiaGpus(env) {
6212
7452
  return { result: table.code === 0 || !last ? table : last, gpus };
6213
7453
  }
6214
7454
  async function diskCheck(name, path) {
6215
- const resolvedPath = resolve15(path);
7455
+ const resolvedPath = resolve16(path);
6216
7456
  try {
6217
7457
  await access2(resolvedPath, constants2.R_OK);
6218
7458
  const fs = await statfs(resolvedPath);
@@ -6313,10 +7553,10 @@ async function collectHostInventory(options = {}) {
6313
7553
  const cwd = options.cwd ?? process.cwd();
6314
7554
  const nodeVersion = process.versions.node;
6315
7555
  const nodeMajor = Number(nodeVersion.split(".")[0]);
6316
- const artifactRoot = resolve15(cwd, config?.artifactRoot ?? join12(cwd, DEFAULT_ARTIFACT_ROOT));
6317
- const storeRoot = resolve15(config?.storeRoot ?? defaultStoreRoot(env));
6318
- const modelCache = resolve15(
6319
- config?.paths.modelCache ?? env.HF_HOME ?? join12(homedir5(), ".cache", "huggingface")
7556
+ const artifactRoot = resolve16(cwd, config?.artifactRoot ?? join14(cwd, DEFAULT_ARTIFACT_ROOT));
7557
+ const storeRoot = resolve16(config?.storeRoot ?? defaultStoreRoot(env));
7558
+ const modelCache = resolve16(
7559
+ config?.paths.modelCache ?? env.HF_HOME ?? join14(homedir5(), ".cache", "huggingface")
6320
7560
  );
6321
7561
  const [artifactDisk, storeDisk, cacheDisk, uvVersion, nvidiaQueried] = await Promise.all([
6322
7562
  diskCheck("artifact-root", artifactRoot),
@@ -6362,17 +7602,17 @@ async function collectHostInventory(options = {}) {
6362
7602
  }
6363
7603
 
6364
7604
  // src/local-runtime/hardware-snapshot.ts
6365
- import { chmod as chmod2, mkdir as mkdir10, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
6366
- import { dirname as dirname12, join as join13 } from "path";
7605
+ import { chmod as chmod2, mkdir as mkdir12, readFile as readFile12, writeFile as writeFile8 } from "fs/promises";
7606
+ import { dirname as dirname12, join as join15 } from "path";
6367
7607
  var HARDWARE_SNAPSHOT_VERSION = 1;
6368
7608
  var HARDWARE_SNAPSHOT_TTL_MS = 24 * 60 * 60 * 1e3;
6369
7609
  function hardwareSnapshotPath(env = process.env) {
6370
- return join13(getTunedTensorHome(env), "hardware.json");
7610
+ return join15(getTunedTensorHome(env), "hardware.json");
6371
7611
  }
6372
7612
  async function readHardwareSnapshot(env = process.env) {
6373
7613
  const path = hardwareSnapshotPath(env);
6374
7614
  try {
6375
- const parsed = JSON.parse(await readFile10(path, "utf8"));
7615
+ const parsed = JSON.parse(await readFile12(path, "utf8"));
6376
7616
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
6377
7617
  const snapshot = parsed;
6378
7618
  if (snapshot.version !== HARDWARE_SNAPSHOT_VERSION) return void 0;
@@ -6389,7 +7629,7 @@ async function readHardwareSnapshot(env = process.env) {
6389
7629
  }
6390
7630
  async function writeHardwareSnapshot(snapshot, env = process.env) {
6391
7631
  const path = hardwareSnapshotPath(env);
6392
- await mkdir10(dirname12(path), { recursive: true, mode: 448 });
7632
+ await mkdir12(dirname12(path), { recursive: true, mode: 448 });
6393
7633
  const gpu = snapshot.capabilities.gpu;
6394
7634
  const record = {
6395
7635
  version: HARDWARE_SNAPSHOT_VERSION,
@@ -6400,7 +7640,7 @@ async function writeHardwareSnapshot(snapshot, env = process.env) {
6400
7640
  capabilities: snapshot.capabilities,
6401
7641
  summary: snapshot.summary ?? formatCapabilitySummary(snapshot.capabilities)
6402
7642
  };
6403
- await writeFile7(path, `${JSON.stringify(record, null, 2)}
7643
+ await writeFile8(path, `${JSON.stringify(record, null, 2)}
6404
7644
  `, { mode: 384 });
6405
7645
  await chmod2(path, 384);
6406
7646
  return record;
@@ -6459,7 +7699,61 @@ async function assessHardware(options = {}) {
6459
7699
  }, env);
6460
7700
  }
6461
7701
 
7702
+ // src/local-runtime/console-reporter.ts
7703
+ function shortValue(value) {
7704
+ if (value === void 0 || value === null) return null;
7705
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
7706
+ return String(value);
7707
+ }
7708
+ if (Array.isArray(value)) return `[${value.length} items]`;
7709
+ return null;
7710
+ }
7711
+ function formatRunEvent(event) {
7712
+ const detailText = Object.entries(event.details ?? {}).filter(([key]) => key !== "metrics").map(([key, value]) => {
7713
+ const formatted = key === "command" && Array.isArray(value) ? value.join(" ") : shortValue(value);
7714
+ return formatted ? `${key}=${formatted}` : null;
7715
+ }).filter((value) => Boolean(value)).slice(0, 5).join(" ");
7716
+ return sanitizeLogLine(`[tt] ${event.stage}: ${event.message}${detailText ? ` (${detailText})` : ""}`);
7717
+ }
7718
+ function elapsedLabel(milliseconds) {
7719
+ const total = Math.max(0, Math.floor(milliseconds / 1e3));
7720
+ const hours = Math.floor(total / 3600);
7721
+ const minutes = Math.floor(total % 3600 / 60);
7722
+ const seconds = total % 60;
7723
+ return `+${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
7724
+ }
7725
+ function createRunConsoleReporter(options = {}) {
7726
+ const write = options.write ?? ((text) => {
7727
+ process.stderr.write(text);
7728
+ });
7729
+ const now = options.now ?? (() => Date.now());
7730
+ const started = now();
7731
+ const stamp = (line) => line.replace(/^\[tt\] /, `[tt] ${elapsedLabel(now() - started)} `);
7732
+ const progress = createProgressRenderer({ write, tty: progressTty(), columns: progressColumns() });
7733
+ let lastLog = "";
7734
+ return {
7735
+ verbose: options.verbose ?? false,
7736
+ onProgress(update) {
7737
+ progress.update(update);
7738
+ },
7739
+ onEvent(event) {
7740
+ progress.interrupt();
7741
+ write(`${stamp(formatRunEvent(event))}
7742
+ `);
7743
+ },
7744
+ onLog(log) {
7745
+ const line = sanitizeLogLine(`[tt] ${log.stage}${log.stream ? ` ${log.stream}` : ""}: ${log.message}`);
7746
+ if (line === lastLog) return;
7747
+ lastLog = line;
7748
+ progress.interrupt();
7749
+ write(`${stamp(line)}
7750
+ `);
7751
+ }
7752
+ };
7753
+ }
7754
+
6462
7755
  export {
7756
+ fileUri,
6463
7757
  assertArtifactManifest,
6464
7758
  TRAINING_MODELS,
6465
7759
  resolveTrainingModel,
@@ -6467,9 +7761,12 @@ export {
6467
7761
  defaultBaseModelRevision,
6468
7762
  resolveModelLoader,
6469
7763
  assertUsableModelArtifact,
7764
+ DECISION_MODELS,
7765
+ resolveDecisionModel,
6470
7766
  DEFAULT_ARTIFACT_ROOT,
6471
7767
  DEFAULT_PROJECT_STORE_ROOT,
6472
7768
  DEFAULT_FOUNDATION_RUNS_DIR,
7769
+ DEFAULT_DECISION_RUNS_DIR,
6473
7770
  expandUserPath,
6474
7771
  getConfigDir,
6475
7772
  getAgentConfigDir,
@@ -6481,7 +7778,12 @@ export {
6481
7778
  behaviorSpecSchema,
6482
7779
  specSnapshotSchema,
6483
7780
  foundationHyperparametersSchema,
7781
+ huggingFaceRepoIdSchema,
7782
+ datasetColumnsSchema,
7783
+ huggingFaceDatasetSourceSchema,
7784
+ isRepoRelativePath,
6484
7785
  datasetPrebuiltSchema,
7786
+ datasetSourceSchema,
6485
7787
  fineTuneHyperparametersSchema,
6486
7788
  fineTuneRunRequestSchema,
6487
7789
  evalExampleResultSchema,
@@ -6490,13 +7792,27 @@ export {
6490
7792
  evalReportSchema,
6491
7793
  comparisonReportSchema,
6492
7794
  trainingReportSchema,
7795
+ runProvenanceSchema,
6493
7796
  runReportSchema,
6494
7797
  localRunnerConfigSchema,
6495
7798
  projectRuntimeSchema,
6496
7799
  localFoundationSpecFileSchema,
6497
7800
  localAdapterSpecFileSchema,
7801
+ decisionQuestionSchema,
7802
+ decisionHyperparametersSchema,
7803
+ localDecisionSpecFileSchema,
7804
+ decisionLabels,
6498
7805
  localBehaviorSpecFileSchema,
6499
7806
  isFoundationSpecFile,
7807
+ isDecisionSpecFile,
7808
+ normalizeChatJsonlForRelocation,
7809
+ datasetInputIdentity,
7810
+ buildSystemMessage,
7811
+ exampleToChatRow,
7812
+ compileSpecToJsonl,
7813
+ examplesFromSpec,
7814
+ examplesFromChatJsonl,
7815
+ evaluationSuiteFromChatJsonl,
6500
7816
  validateEvalCases,
6501
7817
  DEFAULT_LOCAL_SPEC_PATH,
6502
7818
  localPathsOverlap,
@@ -6506,6 +7822,7 @@ export {
6506
7822
  generatedPlaceholderIssues,
6507
7823
  assertLocalRunInputReady,
6508
7824
  foundationPlaceholderIssues,
7825
+ decisionPlaceholderIssues,
6509
7826
  assertFoundationSpecReady,
6510
7827
  runRequestFromLocalSpec,
6511
7828
  parseLocalRunInput,
@@ -6522,17 +7839,12 @@ export {
6522
7839
  canonicalJson2 as canonicalJson,
6523
7840
  isFoundationPipeline3 as isFoundationPipeline,
6524
7841
  parsePipeline3 as parsePipeline,
6525
- normalizeChatJsonlForRelocation,
6526
- buildSystemMessage,
6527
- exampleToChatRow,
6528
- compileSpecToJsonl,
6529
- examplesFromSpec,
6530
- examplesFromChatJsonl,
6531
- evaluationSuiteFromChatJsonl,
6532
7842
  reportInBackground,
6533
7843
  sanitizeLogLine,
6534
7844
  forwardStreamLines,
6535
7845
  localRuntimePackageRoot,
7846
+ buildDecisionPythonCommand,
7847
+ withDecisionPythonEnvironment,
6536
7848
  buildBundledPythonCommand,
6537
7849
  withBundledPythonEnvironment,
6538
7850
  buildFoundationPythonCommand,
@@ -6543,6 +7855,7 @@ export {
6543
7855
  withOfflineHuggingFaceCacheEnvironment,
6544
7856
  checkAwsGpu,
6545
7857
  runGpuProcess,
7858
+ auditRun,
6546
7859
  isTerminalRunState,
6547
7860
  defaultLocalHome,
6548
7861
  localStorePaths,
@@ -6550,10 +7863,18 @@ export {
6550
7863
  writePrivateJsonAtomic,
6551
7864
  createLocalStore,
6552
7865
  tokenF1,
7866
+ deriveSampleSeed,
7867
+ splitSpecExamples,
7868
+ compareEvalReports,
6553
7869
  verifyModelPrefetchCacheReport,
6554
7870
  buildModelPrefetchPayload,
7871
+ safeName,
6555
7872
  verifyLocalBaseModel,
6556
7873
  prefetchBaseModel,
7874
+ DatasetNotDownloadedError,
7875
+ huggingFaceDataset,
7876
+ prefetchHuggingFaceDataset,
7877
+ ensureDatasetCached,
6557
7878
  evaluateGeneralRegressionGate,
6558
7879
  canonicalLocalPipeline,
6559
7880
  validateLocalPipeline,
@@ -6567,10 +7888,12 @@ export {
6567
7888
  runLocalFineTune,
6568
7889
  configForSpec,
6569
7890
  resolveProjectConfig,
7891
+ evaluateCapabilities,
6570
7892
  warningsFromSnapshot,
6571
7893
  readHardwareSnapshot,
6572
7894
  formatHostStatusLine,
6573
7895
  formatAgentHostBlock,
6574
- assessHardware
7896
+ assessHardware,
7897
+ createRunConsoleReporter
6575
7898
  };
6576
- //# sourceMappingURL=chunk-4Q54NHKR.js.map
7899
+ //# sourceMappingURL=chunk-4SUXBHIY.js.map