@hviana/sema 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -5
- package/CITATION.cff +49 -0
- package/HOW_IT_WORKS.md +11 -12
- package/README.md +7 -5
- package/dist/example/demo.js +25 -25
- package/dist/example/train_base.d.ts +19 -21
- package/dist/example/train_base.js +1700 -1952
- package/dist/src/alphabet.d.ts +3 -3
- package/dist/src/alphabet.js +24 -27
- package/dist/src/alu/src/alu.d.ts +169 -194
- package/dist/src/alu/src/alu.js +374 -414
- package/dist/src/alu/src/expr.d.ts +36 -43
- package/dist/src/alu/src/expr.js +252 -278
- package/dist/src/alu/src/index.d.ts +9 -98
- package/dist/src/alu/src/index.js +10 -64
- package/dist/src/alu/src/kernel-arith.d.ts +1 -5
- package/dist/src/alu/src/kernel-arith.js +195 -285
- package/dist/src/alu/src/kernel-bits.d.ts +1 -5
- package/dist/src/alu/src/kernel-bits.js +81 -115
- package/dist/src/alu/src/kernel-logic.js +33 -63
- package/dist/src/alu/src/kernel-nd.js +155 -195
- package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
- package/dist/src/alu/src/kernel-numeric.js +259 -331
- package/dist/src/alu/src/operation.d.ts +112 -132
- package/dist/src/alu/src/operation.js +149 -163
- package/dist/src/alu/src/parser.d.ts +168 -175
- package/dist/src/alu/src/parser.js +382 -421
- package/dist/src/alu/src/resonance.d.ts +16 -25
- package/dist/src/alu/src/resonance.js +49 -56
- package/dist/src/alu/src/text.d.ts +3 -7
- package/dist/src/alu/src/text.js +31 -37
- package/dist/src/alu/src/value.d.ts +14 -14
- package/dist/src/alu/src/value.js +150 -160
- package/dist/src/alu/test/alu.test.js +543 -656
- package/dist/src/bytes.d.ts +1 -5
- package/dist/src/bytes.js +33 -40
- package/dist/src/config.d.ts +99 -102
- package/dist/src/config.js +78 -83
- package/dist/src/derive/src/deduction.d.ts +60 -62
- package/dist/src/derive/src/deduction.js +103 -109
- package/dist/src/derive/src/index.d.ts +1 -7
- package/dist/src/derive/src/priority-queue.d.ts +8 -8
- package/dist/src/derive/src/priority-queue.js +57 -61
- package/dist/src/derive/src/rewrite.d.ts +15 -18
- package/dist/src/derive/src/rewrite.js +59 -67
- package/dist/src/derive/src/trie.d.ts +56 -56
- package/dist/src/derive/src/trie.js +175 -179
- package/dist/src/derive/test/derive.test.js +96 -100
- package/dist/src/extension.d.ts +13 -16
- package/dist/src/geometry.d.ts +29 -43
- package/dist/src/geometry.js +324 -268
- package/dist/src/index.d.ts +2 -17
- package/dist/src/index.js +2 -12
- package/dist/src/ingest-cache.d.ts +25 -30
- package/dist/src/ingest-cache.js +108 -127
- package/dist/src/mind/articulation.d.ts +1 -5
- package/dist/src/mind/articulation.js +77 -112
- package/dist/src/mind/attention.d.ts +40 -108
- package/dist/src/mind/attention.js +754 -872
- package/dist/src/mind/canonical.d.ts +4 -19
- package/dist/src/mind/canonical.js +27 -31
- package/dist/src/mind/graph-search.d.ts +201 -225
- package/dist/src/mind/graph-search.js +742 -821
- package/dist/src/mind/index.d.ts +2 -10
- package/dist/src/mind/junction.d.ts +31 -58
- package/dist/src/mind/junction.js +172 -237
- package/dist/src/mind/learning.d.ts +16 -52
- package/dist/src/mind/learning.js +143 -165
- package/dist/src/mind/match.d.ts +20 -69
- package/dist/src/mind/match.js +259 -318
- package/dist/src/mind/mechanisms/alu.js +16 -16
- package/dist/src/mind/mechanisms/cast.d.ts +9 -13
- package/dist/src/mind/mechanisms/cast.js +363 -456
- package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
- package/dist/src/mind/mechanisms/confluence.js +152 -183
- package/dist/src/mind/mechanisms/cover.d.ts +2 -8
- package/dist/src/mind/mechanisms/cover.js +148 -210
- package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
- package/dist/src/mind/mechanisms/extraction.js +234 -288
- package/dist/src/mind/mechanisms/recall.d.ts +6 -10
- package/dist/src/mind/mechanisms/recall.js +141 -185
- package/dist/src/mind/mind.d.ts +194 -155
- package/dist/src/mind/mind.js +392 -289
- package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
- package/dist/src/mind/pipeline-mechanism.js +161 -172
- package/dist/src/mind/pipeline.d.ts +4 -14
- package/dist/src/mind/pipeline.js +125 -189
- package/dist/src/mind/primitives.d.ts +20 -33
- package/dist/src/mind/primitives.js +124 -120
- package/dist/src/mind/rationale.d.ts +86 -98
- package/dist/src/mind/rationale.js +113 -121
- package/dist/src/mind/reasoning.d.ts +2 -13
- package/dist/src/mind/reasoning.js +129 -166
- package/dist/src/mind/recognition.d.ts +1 -4
- package/dist/src/mind/recognition.js +198 -210
- package/dist/src/mind/resonance.d.ts +5 -22
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +6 -25
- package/dist/src/mind/trace.js +50 -58
- package/dist/src/mind/traverse.d.ts +29 -58
- package/dist/src/mind/traverse.js +344 -356
- package/dist/src/mind/types.d.ts +122 -127
- package/dist/src/mind/types.js +60 -69
- package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
- package/dist/src/rabitq-ivf/src/database.js +201 -0
- package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
- package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
- package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
- package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
- package/dist/src/rabitq-ivf/src/prng.js +36 -0
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
- package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
- package/dist/src/sema.d.ts +9 -13
- package/dist/src/sema.js +26 -40
- package/dist/src/store-sqlite.d.ts +171 -166
- package/dist/src/store-sqlite.js +768 -662
- package/dist/src/store.d.ts +597 -630
- package/dist/src/store.js +1434 -1581
- package/dist/src/vec.d.ts +5 -9
- package/dist/src/vec.js +61 -73
- package/example/train_base.ts +33 -15
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/README.md +1 -1
- package/src/alu/src/index.ts +1 -1
- package/src/config.ts +19 -27
- package/src/geometry.ts +93 -1
- package/src/index.ts +6 -11
- package/src/mind/attention.ts +11 -6
- package/src/mind/mechanisms/recall.ts +29 -13
- package/src/mind/mind.ts +250 -19
- package/src/mind/primitives.ts +43 -6
- package/src/mind/recognition.ts +26 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +20 -21
- package/src/rabitq-ivf/README.md +56 -0
- package/src/rabitq-ivf/src/database.ts +276 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
- package/src/rabitq-ivf/src/ivf.ts +1330 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
- package/src/store-sqlite.ts +204 -9
- package/src/store.ts +27 -34
- package/test/08-storage.test.mjs +3 -3
- package/test/13-conversation.test.mjs +190 -20
- package/test/14-scaling.test.mjs +2 -2
- package/test/35-ivf.test.mjs +263 -0
- package/test/36-bloom.test.mjs +123 -0
- package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
- package/dist/src/rabitq-hnsw/src/database.js +0 -405
- package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
- package/dist/src/rabitq-hnsw/src/heap.js +0 -94
- package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
- package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
- package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
- package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
- package/dist/src/rabitq-hnsw/src/prng.js +0 -38
- package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
- package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
- package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
- package/dist/src/rabitq-hnsw/src/store.js +0 -916
- package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
- package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
- package/src/rabitq-hnsw/README.md +0 -303
- package/src/rabitq-hnsw/src/database.ts +0 -492
- package/src/rabitq-hnsw/src/heap.ts +0 -90
- package/src/rabitq-hnsw/src/hnsw.ts +0 -514
- package/src/rabitq-hnsw/src/store.ts +0 -994
- package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
- /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
|
@@ -101,19 +101,7 @@ import { CachedIngest, Mind, SQliteStore } from "../src/index.js";
|
|
|
101
101
|
// writing a file is the single capability the web platform does not expose, so
|
|
102
102
|
// the download sink uses the synchronous fs descriptor calls below. The durable
|
|
103
103
|
// disk cache is therefore the sole, irreducible Node dependency.
|
|
104
|
-
import {
|
|
105
|
-
closeSync,
|
|
106
|
-
existsSync,
|
|
107
|
-
fsyncSync,
|
|
108
|
-
mkdirSync,
|
|
109
|
-
openAsBlob,
|
|
110
|
-
openSync,
|
|
111
|
-
readdirSync,
|
|
112
|
-
renameSync,
|
|
113
|
-
statSync,
|
|
114
|
-
unlinkSync,
|
|
115
|
-
writeSync,
|
|
116
|
-
} from "node:fs";
|
|
104
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openAsBlob, openSync, readdirSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
117
105
|
import { basename, join } from "node:path";
|
|
118
106
|
// The ONLY third-party dependencies, and only for the one source that ships
|
|
119
107
|
// exclusively as Snappy-compressed Parquet (Aya): hyparquet is a pure-JS,
|
|
@@ -143,16 +131,13 @@ const env = (k, d) => process.env[k] ?? d;
|
|
|
143
131
|
const SMOLSENT = env("SMOLSENT", "1") !== "0";
|
|
144
132
|
const SMOLSENT_DATASET = env("SMOLSENT_DATASET", "google/smol");
|
|
145
133
|
const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
|
|
146
|
-
|
|
134
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
147
135
|
// The resume id PREFIX for the SmolSent stage; one completed-files entry per
|
|
148
136
|
// file (e.g. "smolsent::ha_en.jsonl").
|
|
149
137
|
const SMOLSENT_ID = "smolsent";
|
|
150
138
|
// A SmolSent side longer than this is skipped (a sentence pair is short; a huge
|
|
151
139
|
// value is corruption, not a sentence).
|
|
152
|
-
const MAX_SMOLSENT_CHARS = Math.max(
|
|
153
|
-
2_000,
|
|
154
|
-
Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000,
|
|
155
|
-
);
|
|
140
|
+
const MAX_SMOLSENT_CHARS = Math.max(2_000, Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000);
|
|
156
141
|
const DB_PATH = env("DB_PATH", "sema"); // → {DB_PATH}.sqlite
|
|
157
142
|
const D = Number(env("D", "1024"));
|
|
158
143
|
const SEED = Number(env("SEED", "7"));
|
|
@@ -162,10 +147,7 @@ const SEED = Number(env("SEED", "7"));
|
|
|
162
147
|
// deposit checkpoint, nor silently disable checkpointing. The tail (a run that
|
|
163
148
|
// learns less than one interval, or the remainder past the last interval) is
|
|
164
149
|
// always saved by finish() at exit — a complete point.
|
|
165
|
-
const CHECKPOINT_BYTES = Math.max(
|
|
166
|
-
1_000_000,
|
|
167
|
-
Math.floor(Number(env("CHECKPOINT_MB", "100")) * 1_000_000) || 100_000_000,
|
|
168
|
-
);
|
|
150
|
+
const CHECKPOINT_BYTES = Math.max(1_000_000, Math.floor(Number(env("CHECKPOINT_MB", "100")) * 1_000_000) || 100_000_000);
|
|
169
151
|
const LOCAL_PATH = env("LOCAL_PATH", ""); // train from a local dir of *.zip
|
|
170
152
|
const CACHE_DIR = env("CACHE_DIR", join(process.cwd(), "cache"));
|
|
171
153
|
const MAX_CACHE_BYTES = Number(env("MAX_CACHE_GB", "100")) * 1e9;
|
|
@@ -200,10 +182,8 @@ let lastThrottleLog = 0;
|
|
|
200
182
|
// carries the running total to/past the ceiling (that item is still counted).
|
|
201
183
|
const MAX_MB = Number(env("MAX_MB", "Infinity"));
|
|
202
184
|
if (isNaN(MAX_MB) || MAX_MB < 0) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
206
|
-
process.exit(1);
|
|
185
|
+
process.stderr.write(`fatal: MAX_MB must be a non-negative number or "Infinity"\n`);
|
|
186
|
+
process.exit(1);
|
|
207
187
|
}
|
|
208
188
|
const MAX_BYTES = MAX_MB * 1_000_000; // Infinity stays Infinity
|
|
209
189
|
// ── CohereLabs/aya_dataset (the second training stage, after SmolSent) ──
|
|
@@ -215,20 +195,14 @@ const MAX_BYTES = MAX_MB * 1_000_000; // Infinity stays Infinity
|
|
|
215
195
|
// web-standard Blob byte source — no whole-file-in-memory load. AYA=0 disables
|
|
216
196
|
// the stage; AYA_URL overrides the Parquet source.
|
|
217
197
|
const AYA = env("AYA", "1") !== "0";
|
|
218
|
-
const AYA_URL = env(
|
|
219
|
-
"AYA_URL",
|
|
220
|
-
"https://huggingface.co/datasets/CohereLabs/aya_dataset/resolve/main/data/train-00000-of-00001.parquet",
|
|
221
|
-
);
|
|
198
|
+
const AYA_URL = env("AYA_URL", "https://huggingface.co/datasets/CohereLabs/aya_dataset/resolve/main/data/train-00000-of-00001.parquet");
|
|
222
199
|
// The resume id of the Aya stage, kept in the same completed-files set as the
|
|
223
200
|
// other stages, so one store records the whole curriculum.
|
|
224
201
|
const AYA_ID = "aya::dataset";
|
|
225
202
|
// A single Aya field this many chars or longer is skipped: inputs/targets range
|
|
226
203
|
// up to ~3.3M chars, and a multi-MB "pair" is documentation/dump noise, not a
|
|
227
204
|
// cognitive example.
|
|
228
|
-
const MAX_AYA_FIELD_CHARS = Math.max(
|
|
229
|
-
10_000,
|
|
230
|
-
Math.floor(Number(env("MAX_AYA_FIELD_KB", "256")) * 1000) || 256_000,
|
|
231
|
-
);
|
|
205
|
+
const MAX_AYA_FIELD_CHARS = Math.max(10_000, Math.floor(Number(env("MAX_AYA_FIELD_KB", "256")) * 1000) || 256_000);
|
|
232
206
|
// ── OpenAssistant/oasst2 (the fourth training stage, after Aya) ──
|
|
233
207
|
// oasst2 is a corpus of human↔assistant conversation TREES. Its richest, most
|
|
234
208
|
// stream-friendly artifact is "<date>_oasst2_ready.trees.jsonl.gz": one JSON
|
|
@@ -241,25 +215,16 @@ const MAX_AYA_FIELD_CHARS = Math.max(
|
|
|
241
215
|
// least two full user→assistant exchanges) — single Q→A trees are skipped, by
|
|
242
216
|
// design. OASST=0 disables the stage; OASST_URL overrides the source.
|
|
243
217
|
const OASST = env("OASST", "1") !== "0";
|
|
244
|
-
const OASST_URL = env(
|
|
245
|
-
"OASST_URL",
|
|
246
|
-
"https://huggingface.co/datasets/OpenAssistant/oasst2/resolve/main/2023-11-05_oasst2_ready.trees.jsonl.gz",
|
|
247
|
-
);
|
|
218
|
+
const OASST_URL = env("OASST_URL", "https://huggingface.co/datasets/OpenAssistant/oasst2/resolve/main/2023-11-05_oasst2_ready.trees.jsonl.gz");
|
|
248
219
|
// The resume id of the oasst2 stage, in the same completed-files set as the
|
|
249
220
|
// other stages, so one store records the whole curriculum.
|
|
250
221
|
const OASST_ID = "oasst2::trees";
|
|
251
222
|
// Multi-turn threshold: a conversation must have at least this many turns to be
|
|
252
223
|
// trained (4 = user→assistant→user→assistant, the smallest real multi-turn).
|
|
253
|
-
const OASST_MIN_TURNS = Math.max(
|
|
254
|
-
2,
|
|
255
|
-
Math.floor(Number(env("OASST_MIN_TURNS", "4"))) || 4,
|
|
256
|
-
);
|
|
224
|
+
const OASST_MIN_TURNS = Math.max(2, Math.floor(Number(env("OASST_MIN_TURNS", "4"))) || 4);
|
|
257
225
|
// Skip a tree whose decoded JSON line exceeds this (a pathological record); the
|
|
258
226
|
// real maximum is far smaller, so this only guards against corruption.
|
|
259
|
-
const MAX_OASST_LINE_CHARS = Math.max(
|
|
260
|
-
100_000,
|
|
261
|
-
Math.floor(Number(env("MAX_OASST_LINE_MB", "8")) * 1_000_000) || 8_000_000,
|
|
262
|
-
);
|
|
227
|
+
const MAX_OASST_LINE_CHARS = Math.max(100_000, Math.floor(Number(env("MAX_OASST_LINE_MB", "8")) * 1_000_000) || 8_000_000);
|
|
263
228
|
// ── MuskumPillerum/General-Knowledge (the fourth training stage, after oasst2) ──
|
|
264
229
|
// A ~37.6k-row general-knowledge Q&A set: each row is a single {Question, Answer}
|
|
265
230
|
// pair. A row is a pure RELATION (question → answer), so it becomes exactly ONE
|
|
@@ -267,19 +232,13 @@ const MAX_OASST_LINE_CHARS = Math.max(
|
|
|
267
232
|
// file (output.json); we DOWNLOAD it and stream the array. GENKNOW=0 disables
|
|
268
233
|
// the stage; GENKNOW_URL overrides the source.
|
|
269
234
|
const GENKNOW = env("GENKNOW", "1") !== "0";
|
|
270
|
-
const GENKNOW_URL = env(
|
|
271
|
-
"GENKNOW_URL",
|
|
272
|
-
"https://huggingface.co/datasets/MuskumPillerum/General-Knowledge/resolve/main/output.json",
|
|
273
|
-
);
|
|
235
|
+
const GENKNOW_URL = env("GENKNOW_URL", "https://huggingface.co/datasets/MuskumPillerum/General-Knowledge/resolve/main/output.json");
|
|
274
236
|
// The resume id of the General-Knowledge stage, in the same completed-files set
|
|
275
237
|
// as the other stages, so one store records the whole curriculum.
|
|
276
238
|
const GENKNOW_ID = "genknow::qa";
|
|
277
239
|
// A Question/Answer longer than this is skipped (answers run to a few hundred
|
|
278
240
|
// chars; this only guards against a corrupt/runaway field).
|
|
279
|
-
const MAX_GENKNOW_CHARS = Math.max(
|
|
280
|
-
4_000,
|
|
281
|
-
Math.floor(Number(env("MAX_GENKNOW_KB", "64")) * 1000) || 64_000,
|
|
282
|
-
);
|
|
241
|
+
const MAX_GENKNOW_CHARS = Math.max(4_000, Math.floor(Number(env("MAX_GENKNOW_KB", "64")) * 1000) || 64_000);
|
|
283
242
|
// ═══════════════════════════════════════════════════════════════════════
|
|
284
243
|
// §2 Terminal + formatting helpers
|
|
285
244
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -290,105 +249,91 @@ const YEL = `${CSI}33m`, RED = `${CSI}31m`;
|
|
|
290
249
|
const HIDE = `${CSI}?25l`, SHOW = `${CSI}?25h`;
|
|
291
250
|
/** Sleep `ms`, but wake early if the shutdown signal fires — so a long back-off
|
|
292
251
|
* (e.g. a rate-limit wait) never swallows Ctrl+C. Resolves either way. */
|
|
293
|
-
const waitMs = (ms) =>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return resolve();
|
|
297
|
-
}
|
|
252
|
+
const waitMs = (ms) => new Promise((resolve) => {
|
|
253
|
+
if (shutdown.signal.aborted)
|
|
254
|
+
return resolve();
|
|
298
255
|
// NOTE: the timer is deliberately NOT unref'd — an unref'd timer does not
|
|
299
256
|
// keep the event loop alive, so a pending wait (e.g. the pace between page
|
|
300
257
|
// requests, or a rate-limit back-off) would let Node exit early and the run
|
|
301
258
|
// would "do nothing and close". The listener lets a shutdown wake it early.
|
|
302
259
|
const t = setTimeout(done, ms);
|
|
303
260
|
function done() {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
261
|
+
clearTimeout(t);
|
|
262
|
+
shutdown.signal.removeEventListener("abort", done);
|
|
263
|
+
resolve();
|
|
307
264
|
}
|
|
308
265
|
shutdown.signal.addEventListener("abort", done, { once: true });
|
|
309
|
-
|
|
266
|
+
});
|
|
310
267
|
/** Resolve `p`, but reject with a TimeoutError if it takes longer than `ms`.
|
|
311
268
|
* The underlying promise is left to settle on its own (we just stop waiting),
|
|
312
269
|
* so a slow black-box call can never wedge the caller. */
|
|
313
270
|
function withTimeout(p, ms, label = "operation") {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const t = setTimeout(() => {
|
|
273
|
+
const e = new Error(`${label} timed out after ${ms}ms`);
|
|
274
|
+
e.name = "TimeoutError";
|
|
275
|
+
reject(e);
|
|
276
|
+
}, ms);
|
|
277
|
+
if (typeof t.unref === "function")
|
|
278
|
+
t.unref();
|
|
279
|
+
p.then((v) => {
|
|
280
|
+
clearTimeout(t);
|
|
281
|
+
resolve(v);
|
|
282
|
+
}, (e) => {
|
|
283
|
+
clearTimeout(t);
|
|
284
|
+
reject(e);
|
|
285
|
+
});
|
|
329
286
|
});
|
|
330
|
-
});
|
|
331
287
|
}
|
|
332
288
|
/** Human-readable duration from seconds. */
|
|
333
289
|
function dur(seconds) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return `${m}m ${s}s`;
|
|
345
|
-
}
|
|
346
|
-
return `${s}s`;
|
|
290
|
+
if (!isFinite(seconds) || seconds < 0)
|
|
291
|
+
return "--";
|
|
292
|
+
const h = Math.floor(seconds / 3600);
|
|
293
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
294
|
+
const s = Math.floor(seconds % 60);
|
|
295
|
+
if (h > 0)
|
|
296
|
+
return `${h}h ${m}m ${s}s`;
|
|
297
|
+
if (m > 0)
|
|
298
|
+
return `${m}m ${s}s`;
|
|
299
|
+
return `${s}s`;
|
|
347
300
|
}
|
|
348
301
|
/** Human-readable byte size. */
|
|
349
302
|
function bytes(n) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (n < 1e9) {
|
|
360
|
-
return `${(n / 1e6).toFixed(1)} MB`;
|
|
361
|
-
}
|
|
362
|
-
return `${(n / 1e9).toFixed(2)} GB`;
|
|
303
|
+
if (!isFinite(n) || n < 0)
|
|
304
|
+
return "--";
|
|
305
|
+
if (n < 1024)
|
|
306
|
+
return `${n} B`;
|
|
307
|
+
if (n < 1e6)
|
|
308
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
309
|
+
if (n < 1e9)
|
|
310
|
+
return `${(n / 1e6).toFixed(1)} MB`;
|
|
311
|
+
return `${(n / 1e9).toFixed(2)} GB`;
|
|
363
312
|
}
|
|
364
313
|
/** Short count: 1234567 → "1.23M". */
|
|
365
314
|
function num(n) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return `${(n / 1e3).toFixed(1)}K`;
|
|
374
|
-
}
|
|
375
|
-
return String(n);
|
|
315
|
+
if (n >= 1e9)
|
|
316
|
+
return `${(n / 1e9).toFixed(2)}B`;
|
|
317
|
+
if (n >= 1e6)
|
|
318
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
319
|
+
if (n >= 1e3)
|
|
320
|
+
return `${(n / 1e3).toFixed(1)}K`;
|
|
321
|
+
return String(n);
|
|
376
322
|
}
|
|
377
323
|
const int = (n) => Math.round(n).toLocaleString("en-US");
|
|
378
324
|
const clamp01 = (f) => Math.max(0, Math.min(1, f));
|
|
379
325
|
const pct = (f) => `${(clamp01(f) * 100).toFixed(1)}%`;
|
|
380
326
|
/** A progress bar of width `w` filled to fraction `frac`. */
|
|
381
327
|
function bar(w, frac) {
|
|
382
|
-
|
|
383
|
-
|
|
328
|
+
const filled = Math.round(clamp01(frac) * w);
|
|
329
|
+
return `${GRN}${"█".repeat(filled)}${GREY}${"░".repeat(w - filled)}${R}`;
|
|
384
330
|
}
|
|
385
331
|
/** Collapse whitespace and clip to `max` chars with an ellipsis. */
|
|
386
332
|
function clip(text, max) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return t.length <= max ? t : t.slice(0, max - 1) + "…";
|
|
333
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
334
|
+
if (max < 1)
|
|
335
|
+
return "";
|
|
336
|
+
return t.length <= max ? t : t.slice(0, max - 1) + "…";
|
|
392
337
|
}
|
|
393
338
|
/** Retry `fn` with exponential backoff.
|
|
394
339
|
*
|
|
@@ -405,46 +350,46 @@ function clip(text, max) {
|
|
|
405
350
|
* `onFail` is called after each non-throttle failed attempt; `onThrottle` after
|
|
406
351
|
* each throttle wait (for a "waiting…" notice). */
|
|
407
352
|
async function retry(label, fn, tries, onFail, onThrottle) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
353
|
+
let wait = 1000, last = "", throttleWait = 1000, throttleHits = 0;
|
|
354
|
+
for (let attempt = 1; attempt <= tries;) {
|
|
355
|
+
if (shutdown.signal.aborted) {
|
|
356
|
+
const e = new Error("aborted");
|
|
357
|
+
e.fatal = true;
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
return await fn();
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
const err = e;
|
|
365
|
+
if (err.name === "AbortError" || err.fatal)
|
|
366
|
+
throw err;
|
|
367
|
+
// Rate-limited / overloaded: wait it out. Does NOT advance `attempt`, so a
|
|
368
|
+
// busy server can never exhaust the retry budget and drop the request.
|
|
369
|
+
if (err.throttle && !shutdown.signal.aborted) {
|
|
370
|
+
throttleHits++;
|
|
371
|
+
// Honour Retry-After when the server sent one; else exponential back-off
|
|
372
|
+
// with jitter, capped, so a fleet of requests does not resynchronise.
|
|
373
|
+
const base = err.retryAfterMs && err.retryAfterMs > 0
|
|
374
|
+
? err.retryAfterMs
|
|
375
|
+
: throttleWait;
|
|
376
|
+
const ms = Math.min(base, 60_000) +
|
|
377
|
+
Math.floor(base * 0.25 * Math.random());
|
|
378
|
+
onThrottle?.(ms);
|
|
379
|
+
await waitMs(ms);
|
|
380
|
+
throttleWait = Math.min(throttleWait * 2, 60_000);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
last = err.message;
|
|
384
|
+
onFail?.(attempt, err);
|
|
385
|
+
attempt++;
|
|
386
|
+
if (attempt <= tries) {
|
|
387
|
+
await waitMs(wait);
|
|
388
|
+
wait = Math.min(wait * 2, 30_000);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
445
391
|
}
|
|
446
|
-
|
|
447
|
-
throw new Error(`${label} failed after ${tries} attempts: ${last}`);
|
|
392
|
+
throw new Error(`${label} failed after ${tries} attempts: ${last}`);
|
|
448
393
|
}
|
|
449
394
|
/** Classify a non-OK HTTP response into an {@link HttpError} for {@link retry}:
|
|
450
395
|
* • 429 / 503 → THROTTLE (rate-limited / overloaded): retried indefinitely,
|
|
@@ -453,25 +398,26 @@ async function retry(label, fn, tries, onFail, onThrottle) {
|
|
|
453
398
|
* • other 4xx → FATAL: a real client error (404, 401, …) — not retried.
|
|
454
399
|
* Never throttles forever silently: the wait is interruptible by shutdown. */
|
|
455
400
|
function httpError(res) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
401
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
402
|
+
if (res.status === 429 || res.status === 503) {
|
|
403
|
+
err.throttle = true;
|
|
404
|
+
const ra = res.headers.get("retry-after");
|
|
405
|
+
if (ra) {
|
|
406
|
+
const secs = Number(ra);
|
|
407
|
+
if (Number.isFinite(secs))
|
|
408
|
+
err.retryAfterMs = Math.max(0, secs * 1000);
|
|
409
|
+
else {
|
|
410
|
+
const when = Date.parse(ra);
|
|
411
|
+
if (Number.isFinite(when)) {
|
|
412
|
+
err.retryAfterMs = Math.max(0, when - Date.now());
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
470
416
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
417
|
+
else if (res.status < 500) {
|
|
418
|
+
err.fatal = true; // genuine client error — do not retry
|
|
419
|
+
} // other 5xx: neither fatal nor throttle → ordinary bounded retry
|
|
420
|
+
return err;
|
|
475
421
|
}
|
|
476
422
|
/** GET a URL and parse JSON, with the shared retry policy: rate-limits (429/503)
|
|
477
423
|
* WAIT indefinitely (surfaced to the progress log via onThrottleWait, throttled
|
|
@@ -479,25 +425,18 @@ function httpError(res) {
|
|
|
479
425
|
* DOWNLOAD_TRIES. Used by every datasets-server API stage so all share the same
|
|
480
426
|
* never-drop-on-throttle behaviour. */
|
|
481
427
|
async function getJson(url, label) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const now = Date.now();
|
|
495
|
-
if (onThrottleWait && now - lastThrottleLog > 3000) {
|
|
496
|
-
lastThrottleLog = now;
|
|
497
|
-
onThrottleWait(ms, label);
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
);
|
|
428
|
+
return retry(label, async () => {
|
|
429
|
+
const res = await fetch(url, { signal: shutdown.signal });
|
|
430
|
+
if (res.ok)
|
|
431
|
+
return res.json();
|
|
432
|
+
throw httpError(res);
|
|
433
|
+
}, DOWNLOAD_TRIES, undefined, (ms) => {
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
if (onThrottleWait && now - lastThrottleLog > 3000) {
|
|
436
|
+
lastThrottleLog = now;
|
|
437
|
+
onThrottleWait(ms, label);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
501
440
|
}
|
|
502
441
|
// ═══════════════════════════════════════════════════════════════════════
|
|
503
442
|
// §3 Cache + download helpers (a downloaded file is bounded by MAX_CACHE_GB)
|
|
@@ -509,161 +448,144 @@ async function getJson(url, label) {
|
|
|
509
448
|
/** A cheap HEAD to learn a download's size (for the cache ceiling and a real
|
|
510
449
|
* ETA). Rate-limits wait; other 4xx is fatal; total failure → 0. */
|
|
511
450
|
async function headSize(url) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}, 4);
|
|
451
|
+
return retry(`HEAD ${url}`, async () => {
|
|
452
|
+
const res = await fetch(url, { method: "HEAD", signal: shutdown.signal });
|
|
453
|
+
if (res.ok)
|
|
454
|
+
return Number(res.headers.get("content-length")) || 0;
|
|
455
|
+
throw httpError(res);
|
|
456
|
+
}, 4);
|
|
519
457
|
}
|
|
520
458
|
function cacheSize() {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
459
|
+
if (!existsSync(CACHE_DIR))
|
|
460
|
+
return 0;
|
|
461
|
+
let total = 0;
|
|
462
|
+
for (const name of readdirSync(CACHE_DIR)) {
|
|
463
|
+
try {
|
|
464
|
+
total += statSync(join(CACHE_DIR, name)).size;
|
|
465
|
+
}
|
|
466
|
+
catch { /* raced with a delete */ }
|
|
467
|
+
}
|
|
468
|
+
return total;
|
|
531
469
|
}
|
|
532
470
|
/** Block until there is room for a file of `fileBytes` under the ceiling.
|
|
533
471
|
* A single file larger than the whole ceiling can never "fit", so we let it
|
|
534
472
|
* through (it is deleted right after processing) rather than wait forever. */
|
|
535
473
|
async function ensureCacheRoom(fileBytes, warn) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
`${YEL}⚠${R} cache at ${
|
|
549
|
-
(MAX_CACHE_BYTES / 1e9).toFixed(0)
|
|
550
|
-
} GB ceiling — waiting for room…`,
|
|
551
|
-
);
|
|
552
|
-
warned = true;
|
|
474
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
475
|
+
if (fileBytes >= MAX_CACHE_BYTES)
|
|
476
|
+
return;
|
|
477
|
+
let warned = false;
|
|
478
|
+
// Stop waiting the moment a shutdown is requested — the abort signal unblocks
|
|
479
|
+
// a long cache-full wait so Ctrl+C is never swallowed by the ceiling.
|
|
480
|
+
while (!shutdown.signal.aborted && cacheSize() + fileBytes > MAX_CACHE_BYTES) {
|
|
481
|
+
if (!warned) {
|
|
482
|
+
warn?.(`${YEL}⚠${R} cache at ${(MAX_CACHE_BYTES / 1e9).toFixed(0)} GB ceiling — waiting for room…`);
|
|
483
|
+
warned = true;
|
|
484
|
+
}
|
|
485
|
+
await waitMs(5_000);
|
|
553
486
|
}
|
|
554
|
-
await waitMs(5_000);
|
|
555
|
-
}
|
|
556
487
|
}
|
|
557
488
|
// ═══════════════════════════════════════════════════════════════════════
|
|
558
489
|
// §5 Download (streamed to disk, with retry + cleanup on failure)
|
|
559
490
|
// ═══════════════════════════════════════════════════════════════════════
|
|
560
|
-
async function downloadFile(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
off += writeSync(fd, chunk, off, chunk.length - off);
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
close() {
|
|
623
|
-
fsyncSync(fd); // durable bytes before the rename promotes them
|
|
624
|
-
closeFd();
|
|
625
|
-
},
|
|
626
|
-
abort() {
|
|
627
|
-
closeFd();
|
|
628
|
-
},
|
|
629
|
-
});
|
|
630
|
-
try {
|
|
631
|
-
await res.body.pipeThrough(meter).pipeTo(sink, {
|
|
632
|
-
signal: shutdown.signal,
|
|
491
|
+
async function downloadFile(url, destPath, tries = DOWNLOAD_TRIES, onFail, onProgress) {
|
|
492
|
+
const partPath = destPath + PART_SUFFIX;
|
|
493
|
+
await retry(`download ${basename(destPath)}`, async () => {
|
|
494
|
+
// Abort promptly on shutdown rather than waiting out a slow socket.
|
|
495
|
+
if (shutdown.signal.aborted) {
|
|
496
|
+
const e = new Error("aborted");
|
|
497
|
+
e.fatal = true;
|
|
498
|
+
throw e;
|
|
499
|
+
}
|
|
500
|
+
const res = await fetch(url, { signal: shutdown.signal });
|
|
501
|
+
if (!res.ok)
|
|
502
|
+
throw httpError(res);
|
|
503
|
+
if (!res.body)
|
|
504
|
+
throw new Error("empty response body");
|
|
505
|
+
const total = Number(res.headers.get("content-length")) || 0;
|
|
506
|
+
let done = 0;
|
|
507
|
+
// Stream straight to a ".part" sibling using pure WHATWG streams. A
|
|
508
|
+
// TransformStream meters progress; pipeTo into a WritableStream gives REAL
|
|
509
|
+
// backpressure natively — the sink's write() returns a promise the
|
|
510
|
+
// readable side awaits, so a fast server can never outrun the disk (no
|
|
511
|
+
// whole-file heap buffering). The sink wraps a single raw fs descriptor
|
|
512
|
+
// (the one capability the web platform lacks); writing to disk is the only
|
|
513
|
+
// Node operation in the whole pipeline. The final, valid file only ever
|
|
514
|
+
// appears via the atomic rename below, so a crash mid-transfer can never
|
|
515
|
+
// leave a truncated file at the real path.
|
|
516
|
+
const meter = new TransformStream({
|
|
517
|
+
transform(chunk, controller) {
|
|
518
|
+
done += chunk.length;
|
|
519
|
+
onProgress?.(done, total);
|
|
520
|
+
controller.enqueue(chunk);
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
const fd = openSync(partPath, "w");
|
|
524
|
+
let closed = false;
|
|
525
|
+
const closeFd = () => {
|
|
526
|
+
if (closed)
|
|
527
|
+
return;
|
|
528
|
+
closed = true;
|
|
529
|
+
try {
|
|
530
|
+
closeSync(fd);
|
|
531
|
+
}
|
|
532
|
+
catch { /* already closed */ }
|
|
533
|
+
};
|
|
534
|
+
const sink = new WritableStream({
|
|
535
|
+
write(chunk) {
|
|
536
|
+
// writeSync drains the whole chunk before returning, so the readable
|
|
537
|
+
// side is paused for exactly as long as the disk needs — backpressure.
|
|
538
|
+
let off = 0;
|
|
539
|
+
while (off < chunk.length) {
|
|
540
|
+
off += writeSync(fd, chunk, off, chunk.length - off);
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
close() {
|
|
544
|
+
fsyncSync(fd); // durable bytes before the rename promotes them
|
|
545
|
+
closeFd();
|
|
546
|
+
},
|
|
547
|
+
abort() {
|
|
548
|
+
closeFd();
|
|
549
|
+
},
|
|
633
550
|
});
|
|
634
|
-
} catch (e) {
|
|
635
|
-
// pipeTo's abort() ran the sink's abort() (closing the descriptor); if
|
|
636
|
-
// it didn't (a non-abort throw), make sure the descriptor is not leaked.
|
|
637
|
-
closeFd();
|
|
638
551
|
try {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
552
|
+
await res.body.pipeThrough(meter).pipeTo(sink, {
|
|
553
|
+
signal: shutdown.signal,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
// pipeTo's abort() ran the sink's abort() (closing the descriptor); if
|
|
558
|
+
// it didn't (a non-abort throw), make sure the descriptor is not leaked.
|
|
559
|
+
closeFd();
|
|
560
|
+
try {
|
|
561
|
+
unlinkSync(partPath);
|
|
562
|
+
}
|
|
563
|
+
catch { /* best effort */ }
|
|
564
|
+
throw e;
|
|
565
|
+
}
|
|
566
|
+
// Optional integrity guard: when the server advertised a size, a complete
|
|
567
|
+
// file must match it. A short read (silent truncation) is retried rather
|
|
568
|
+
// than promoted, so the parser never sees a partial ZIP.
|
|
569
|
+
try {
|
|
570
|
+
const got = statSync(partPath).size;
|
|
571
|
+
if (total > 0 && got !== total) {
|
|
572
|
+
try {
|
|
573
|
+
unlinkSync(partPath);
|
|
574
|
+
}
|
|
575
|
+
catch { /* best effort */ }
|
|
576
|
+
throw new Error(`size mismatch: got ${got}, expected ${total}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
if (e instanceof Error && e.message.startsWith("size mismatch")) {
|
|
581
|
+
throw e;
|
|
582
|
+
}
|
|
583
|
+
// statSync failure is non-fatal here; the rename below will surface it.
|
|
584
|
+
}
|
|
585
|
+
// Atomic publish: rename is atomic within a filesystem, so the final path
|
|
586
|
+
// flips from "absent" to "complete" in one step — never an in-between.
|
|
587
|
+
renameSync(partPath, destPath);
|
|
588
|
+
}, tries, onFail);
|
|
667
589
|
}
|
|
668
590
|
const isEpisode = (it) => typeof it !== "string";
|
|
669
591
|
/** Build the accumulated-context episodes of a turn sequence: each successive
|
|
@@ -671,125 +593,109 @@ const isEpisode = (it) => typeof it !== "string";
|
|
|
671
593
|
* the same cumulative-context shape a multi-turn conversation deposits, so the
|
|
672
594
|
* store learns to continue a growing context. */
|
|
673
595
|
function accumulate(turns) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
596
|
+
const out = [];
|
|
597
|
+
for (let i = 1; i < turns.length; i++) {
|
|
598
|
+
out.push({ context: turns.slice(0, i).join("\n"), continuation: turns[i] });
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
679
601
|
}
|
|
680
602
|
/** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
|
|
681
603
|
* repeats so a concept never deposits the same form twice. */
|
|
682
604
|
export function refineItems(items) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
605
|
+
const out = [];
|
|
606
|
+
const seen = new Set();
|
|
607
|
+
for (const it of items) {
|
|
608
|
+
if (!isEpisode(it)) {
|
|
609
|
+
const exp = it.trim();
|
|
610
|
+
const key = "E:" + exp;
|
|
611
|
+
if (exp && !seen.has(key)) {
|
|
612
|
+
seen.add(key);
|
|
613
|
+
out.push(exp);
|
|
614
|
+
}
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const ctx = it.context.trim();
|
|
618
|
+
const cont = it.continuation.trim();
|
|
619
|
+
if (!ctx || !cont || ctx === cont)
|
|
620
|
+
continue;
|
|
621
|
+
const key = "P:" + ctx + "\u0000" + cont;
|
|
622
|
+
if (seen.has(key))
|
|
623
|
+
continue;
|
|
690
624
|
seen.add(key);
|
|
691
|
-
out.push(
|
|
692
|
-
}
|
|
693
|
-
continue;
|
|
625
|
+
out.push({ context: ctx, continuation: cont });
|
|
694
626
|
}
|
|
695
|
-
|
|
696
|
-
const cont = it.continuation.trim();
|
|
697
|
-
if (!ctx || !cont || ctx === cont) {
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
const key = "P:" + ctx + "\u0000" + cont;
|
|
701
|
-
if (seen.has(key)) {
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
seen.add(key);
|
|
705
|
-
out.push({ context: ctx, continuation: cont });
|
|
706
|
-
}
|
|
707
|
-
return out;
|
|
627
|
+
return out;
|
|
708
628
|
}
|
|
709
629
|
/** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
|
|
710
630
|
* both sides or a side is implausibly large (a dump, not a sentence). */
|
|
711
631
|
export function toSmolSentRow(row) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const sl = typeof r.sl === "string" ? r.sl.trim() : "";
|
|
727
|
-
const tl = typeof r.tl === "string" ? r.tl.trim() : "";
|
|
728
|
-
return { src, trg, sl, tl };
|
|
632
|
+
if (!row || typeof row !== "object")
|
|
633
|
+
return null;
|
|
634
|
+
const r = row;
|
|
635
|
+
const src = typeof r.src === "string" ? r.src.trim() : "";
|
|
636
|
+
// `trg` is a single string in smolsent; tolerate a list form defensively.
|
|
637
|
+
const trgRaw = Array.isArray(r.trgs) ? r.trgs[0] : r.trg;
|
|
638
|
+
const trg = typeof trgRaw === "string" ? trgRaw.trim() : "";
|
|
639
|
+
if (!src || !trg)
|
|
640
|
+
return null;
|
|
641
|
+
if (src.length > MAX_SMOLSENT_CHARS || trg.length > MAX_SMOLSENT_CHARS)
|
|
642
|
+
return null;
|
|
643
|
+
const sl = typeof r.sl === "string" ? r.sl.trim() : "";
|
|
644
|
+
const tl = typeof r.tl === "string" ? r.tl.trim() : "";
|
|
645
|
+
return { src, trg, sl, tl };
|
|
729
646
|
}
|
|
730
647
|
/** Translate ONE SmolSent pair into SEMA facts: the two sentences are one
|
|
731
648
|
* meaning in two languages, so bind them BOTH ways. refineItems drops the
|
|
732
649
|
* degenerate case where src === trg. */
|
|
733
650
|
export function smolSentRowToItems(row) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
651
|
+
const { src, trg } = row;
|
|
652
|
+
return refineItems([
|
|
653
|
+
{ context: src, continuation: trg },
|
|
654
|
+
{ context: trg, continuation: src },
|
|
655
|
+
]);
|
|
739
656
|
}
|
|
740
657
|
/** Normalize a raw datasets-server row object into an AyaRow, or null when it
|
|
741
658
|
* lacks a usable prompt/answer or a field is implausibly large (a dump, not a
|
|
742
659
|
* cognitive example). Trims surrounding whitespace; keeps inner text verbatim
|
|
743
660
|
* (human prose, possibly multi-paragraph). */
|
|
744
661
|
export function toAyaRow(row) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
) {
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
const language = typeof r.language === "string" ? r.language.trim() : "";
|
|
760
|
-
return { inputs, targets, language };
|
|
662
|
+
if (!row || typeof row !== "object")
|
|
663
|
+
return null;
|
|
664
|
+
const r = row;
|
|
665
|
+
const inputs = typeof r.inputs === "string" ? r.inputs.trim() : "";
|
|
666
|
+
const targets = typeof r.targets === "string" ? r.targets.trim() : "";
|
|
667
|
+
if (!inputs || !targets)
|
|
668
|
+
return null;
|
|
669
|
+
if (inputs.length > MAX_AYA_FIELD_CHARS || targets.length > MAX_AYA_FIELD_CHARS)
|
|
670
|
+
return null;
|
|
671
|
+
const language = typeof r.language === "string" ? r.language.trim() : "";
|
|
672
|
+
return { inputs, targets, language };
|
|
761
673
|
}
|
|
762
674
|
/** Translate ONE Aya row into SEMA training items. A row is a single human
|
|
763
675
|
* (question → answer) exchange — exactly one FACT, the (inputs → targets) edge.
|
|
764
676
|
* No standalone-answer experience and no one-exchange "cumulative" walk: a lone
|
|
765
677
|
* Q→A is not multi-turn, and both would only replicate the same edge. */
|
|
766
678
|
export function ayaRowToItems(row) {
|
|
767
|
-
|
|
768
|
-
|
|
679
|
+
const { inputs, targets } = row;
|
|
680
|
+
return refineItems([{ context: inputs, continuation: targets }]);
|
|
769
681
|
}
|
|
770
682
|
/** Collapse a conversation tree to ONE linear path: at each node, descend into
|
|
771
683
|
* its best-ranked, non-deleted reply (rank 0 preferred; unranked sorts last).
|
|
772
684
|
* Returns the ordered turns (already strictly alternating in this corpus). */
|
|
773
685
|
export function bestOasstPath(root) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
break;
|
|
686
|
+
const turns = [];
|
|
687
|
+
let node = root;
|
|
688
|
+
while (node) {
|
|
689
|
+
const text = typeof node.text === "string" ? node.text.trim() : "";
|
|
690
|
+
if (text)
|
|
691
|
+
turns.push({ role: String(node.role ?? "?"), text });
|
|
692
|
+
const live = (node.replies ?? []).filter((r) => r && !r.deleted && typeof r.text === "string" && r.text.trim() !== "");
|
|
693
|
+
if (live.length === 0)
|
|
694
|
+
break;
|
|
695
|
+
live.sort((a, b) => (a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER));
|
|
696
|
+
node = live[0];
|
|
786
697
|
}
|
|
787
|
-
|
|
788
|
-
(a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER)
|
|
789
|
-
);
|
|
790
|
-
node = live[0];
|
|
791
|
-
}
|
|
792
|
-
return turns;
|
|
698
|
+
return turns;
|
|
793
699
|
}
|
|
794
700
|
/** Translate ONE multi-turn oasst2 conversation into SEMA training items.
|
|
795
701
|
*
|
|
@@ -811,47 +717,41 @@ export function bestOasstPath(root) {
|
|
|
811
717
|
* Returns [] for a conversation below the multi-turn threshold, so callers can
|
|
812
718
|
* simply skip empties. */
|
|
813
719
|
export function oasstConversationToItems(turns) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
return refineItems(accumulate(turns.map((t) => t.text)));
|
|
720
|
+
if (turns.length < OASST_MIN_TURNS)
|
|
721
|
+
return []; // not multi-turn — skip
|
|
722
|
+
return refineItems(accumulate(turns.map((t) => t.text)));
|
|
818
723
|
}
|
|
819
724
|
/** Turn a source value into clean prose: decode the literal "\n"/"\t"/"\r"
|
|
820
725
|
* two-character escapes the source JSON left in the text, collapse the runs of
|
|
821
726
|
* whitespace that creates, and trim. */
|
|
822
727
|
function unescapePlain(s) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
728
|
+
return s
|
|
729
|
+
.replace(/\\r\\n|\\n|\\r/g, "\n")
|
|
730
|
+
.replace(/\\t/g, " ")
|
|
731
|
+
.replace(/[ \t]+/g, " ")
|
|
732
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
733
|
+
.trim();
|
|
829
734
|
}
|
|
830
735
|
/** Normalize a raw datasets-server row into a GenKnowRow, or null when it lacks
|
|
831
736
|
* a usable question/answer or a side is implausibly large (corruption). */
|
|
832
737
|
export function toGenKnowRow(row) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
: "";
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
question.length > MAX_GENKNOW_CHARS || answer.length > MAX_GENKNOW_CHARS
|
|
846
|
-
) {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
return { question, answer };
|
|
738
|
+
if (!row || typeof row !== "object")
|
|
739
|
+
return null;
|
|
740
|
+
const r = row;
|
|
741
|
+
const question = typeof r.Question === "string"
|
|
742
|
+
? unescapePlain(r.Question)
|
|
743
|
+
: "";
|
|
744
|
+
const answer = typeof r.Answer === "string" ? unescapePlain(r.Answer) : "";
|
|
745
|
+
if (!question || !answer)
|
|
746
|
+
return null;
|
|
747
|
+
if (question.length > MAX_GENKNOW_CHARS || answer.length > MAX_GENKNOW_CHARS)
|
|
748
|
+
return null;
|
|
749
|
+
return { question, answer };
|
|
850
750
|
}
|
|
851
751
|
/** Translate ONE General-Knowledge row into SEMA items: exactly one
|
|
852
752
|
* (question → answer) FACT. refineItems drops a degenerate question === answer. */
|
|
853
753
|
export function genKnowRowToItems(row) {
|
|
854
|
-
|
|
754
|
+
return refineItems([{ context: row.question, continuation: row.answer }]);
|
|
855
755
|
}
|
|
856
756
|
// ═══════════════════════════════════════════════════════════════════════
|
|
857
757
|
// §7 Ingestion
|
|
@@ -867,23 +767,20 @@ const ENC = new TextEncoder();
|
|
|
867
767
|
/** Content size of a training item in UTF-8 bytes — the same quantity the
|
|
868
768
|
* scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
|
|
869
769
|
* context plus the continuation, for a bare experience its own text. */
|
|
870
|
-
const itemBytes = (it) =>
|
|
871
|
-
isEpisode(it)
|
|
770
|
+
const itemBytes = (it) => isEpisode(it)
|
|
872
771
|
? ENC.encode(it.context).length + ENC.encode(it.continuation).length
|
|
873
772
|
: ENC.encode(it).length;
|
|
874
773
|
async function ingestItems(ci, items, onItem, sample) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
return true;
|
|
774
|
+
for (const it of items) {
|
|
775
|
+
if (isEpisode(it))
|
|
776
|
+
await ci.ingest(it.context, it.continuation);
|
|
777
|
+
else
|
|
778
|
+
await ci.ingest(it);
|
|
779
|
+
sample?.(it);
|
|
780
|
+
if (!(await onItem(itemBytes(it))))
|
|
781
|
+
return false; // stop requested
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
887
784
|
}
|
|
888
785
|
// ── §7a′ oasst2 — stream the gzipped JSONL of trees and deposit multi-turn ──
|
|
889
786
|
//
|
|
@@ -894,495 +791,462 @@ async function ingestItems(ci, items, onItem, sample) {
|
|
|
894
791
|
// line that fails to parse (or is oversize) is counted skipped and the stream
|
|
895
792
|
// continues; a cap/signal stops cleanly at a conversation boundary.
|
|
896
793
|
async function processOasst(filePath, ci, onExample, sample) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
794
|
+
const blob = await openAsBlob(filePath);
|
|
795
|
+
const reader = blob.stream()
|
|
796
|
+
.pipeThrough(new DecompressionStream("gzip"))
|
|
797
|
+
.pipeThrough(new TextDecoderStream())
|
|
798
|
+
.getReader();
|
|
799
|
+
let examples = 0;
|
|
800
|
+
let skipped = 0; // malformed/oversize lines
|
|
801
|
+
let multi = 0; // multi-turn conversations deposited
|
|
802
|
+
let leftover = "";
|
|
803
|
+
let droppingLine = false;
|
|
804
|
+
const processLine = async (line) => {
|
|
805
|
+
if (!line.trim())
|
|
806
|
+
return true;
|
|
807
|
+
let tree;
|
|
808
|
+
try {
|
|
809
|
+
tree = JSON.parse(line);
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
skipped++;
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
if (!tree.prompt)
|
|
816
|
+
return true;
|
|
817
|
+
const turns = bestOasstPath(tree.prompt);
|
|
818
|
+
const items = oasstConversationToItems(turns); // [] when not multi-turn
|
|
819
|
+
if (items.length === 0)
|
|
820
|
+
return true; // single-turn / empty — skipped
|
|
821
|
+
multi++;
|
|
822
|
+
return ingestItems(ci, items, async (contentBytes) => {
|
|
823
|
+
examples++;
|
|
824
|
+
return onExample(contentBytes);
|
|
825
|
+
}, sample);
|
|
826
|
+
};
|
|
912
827
|
try {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
828
|
+
while (true) {
|
|
829
|
+
const { done, value } = await reader.read();
|
|
830
|
+
if (done)
|
|
831
|
+
break;
|
|
832
|
+
let chunk = value;
|
|
833
|
+
for (;;) {
|
|
834
|
+
const nl = chunk.indexOf("\n");
|
|
835
|
+
if (nl < 0) {
|
|
836
|
+
if (!droppingLine) {
|
|
837
|
+
if (leftover.length + chunk.length > MAX_OASST_LINE_CHARS) {
|
|
838
|
+
leftover = "";
|
|
839
|
+
droppingLine = true;
|
|
840
|
+
skipped++;
|
|
841
|
+
}
|
|
842
|
+
else
|
|
843
|
+
leftover += chunk;
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
const part = chunk.slice(0, nl);
|
|
848
|
+
chunk = chunk.slice(nl + 1);
|
|
849
|
+
if (droppingLine) {
|
|
850
|
+
droppingLine = false;
|
|
851
|
+
leftover = "";
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (leftover.length + part.length > MAX_OASST_LINE_CHARS) {
|
|
855
|
+
leftover = "";
|
|
856
|
+
skipped++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const line = leftover + part;
|
|
860
|
+
leftover = "";
|
|
861
|
+
if (!(await processLine(line))) {
|
|
862
|
+
return { examples, stopped: true, skipped, multi };
|
|
863
|
+
}
|
|
949
864
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
leftover = "";
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
if (leftover.length + part.length > MAX_OASST_LINE_CHARS) {
|
|
961
|
-
leftover = "";
|
|
962
|
-
skipped++;
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const line = leftover + part;
|
|
966
|
-
leftover = "";
|
|
967
|
-
if (!(await processLine(line))) {
|
|
968
|
-
return { examples, stopped: true, skipped, multi };
|
|
969
|
-
}
|
|
970
|
-
}
|
|
865
|
+
}
|
|
866
|
+
if (!droppingLine && leftover.trim()) {
|
|
867
|
+
if (!(await processLine(leftover))) {
|
|
868
|
+
return { examples, stopped: true, skipped, multi };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return { examples, stopped: false, skipped, multi };
|
|
971
872
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
873
|
+
finally {
|
|
874
|
+
try {
|
|
875
|
+
reader.releaseLock();
|
|
876
|
+
}
|
|
877
|
+
catch { /* best effort */ }
|
|
976
878
|
}
|
|
977
|
-
return { examples, stopped: false, skipped, multi };
|
|
978
|
-
} finally {
|
|
979
|
-
try {
|
|
980
|
-
reader.releaseLock();
|
|
981
|
-
} catch { /* best effort */ }
|
|
982
|
-
}
|
|
983
879
|
}
|
|
984
880
|
/** Discover the SmolSent per-pair JSONL files from the HF repo tree, restricted
|
|
985
881
|
* to SMOLSENT_PAIRS (basenames without .jsonl) when set. Each entry is the
|
|
986
882
|
* repo-relative path, e.g. "smolsent/ha_en.jsonl". */
|
|
987
883
|
async function listSmolSentFiles() {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return paths.filter((p) => want.has(basename(p).replace(/\.jsonl$/i, "")));
|
|
884
|
+
// The dataset id ("owner/name") is a PATH here, so its "/" must not be
|
|
885
|
+
// percent-encoded. `recursive=true` returns every file under smolsent/.
|
|
886
|
+
const url = `https://huggingface.co/api/datasets/${SMOLSENT_DATASET}` +
|
|
887
|
+
`/tree/main/smolsent?recursive=true`;
|
|
888
|
+
const body = await getJson(url, `GET smol tree`);
|
|
889
|
+
const paths = Array.isArray(body)
|
|
890
|
+
? body
|
|
891
|
+
.filter((e) => e?.type === "file" && /\.jsonl$/i.test(e?.path))
|
|
892
|
+
.map((e) => String(e.path))
|
|
893
|
+
: [];
|
|
894
|
+
paths.sort();
|
|
895
|
+
if (!SMOLSENT_PAIRS.length)
|
|
896
|
+
return paths;
|
|
897
|
+
const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
|
|
898
|
+
return paths.filter((p) => want.has(basename(p).replace(/\.jsonl$/i, "")));
|
|
1004
899
|
}
|
|
1005
900
|
/** Stream a plain-JSONL file from disk, deposit each parsed row via `toItems`.
|
|
1006
901
|
* Lines are split without buffering the whole file; an oversize/malformed line
|
|
1007
902
|
* is counted skipped and the stream continues. Shared by SmolSent (and any
|
|
1008
903
|
* future JSONL source). */
|
|
1009
|
-
async function processJsonl(
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
904
|
+
async function processJsonl(filePath, toItems, ci, onExample, sample, maxLineChars) {
|
|
905
|
+
const blob = await openAsBlob(filePath);
|
|
906
|
+
const reader = blob.stream().pipeThrough(new TextDecoderStream()).getReader();
|
|
907
|
+
let examples = 0, skipped = 0, leftover = "", dropping = false;
|
|
908
|
+
const processLine = async (line) => {
|
|
909
|
+
if (!line.trim())
|
|
910
|
+
return true;
|
|
911
|
+
let row;
|
|
912
|
+
try {
|
|
913
|
+
row = JSON.parse(line);
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
skipped++;
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const items = toItems(row);
|
|
920
|
+
if (!items || items.length === 0) {
|
|
921
|
+
skipped++;
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
return ingestItems(ci, items, async (contentBytes) => {
|
|
925
|
+
examples++;
|
|
926
|
+
return onExample(contentBytes);
|
|
927
|
+
}, sample);
|
|
928
|
+
};
|
|
1025
929
|
try {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
930
|
+
while (true) {
|
|
931
|
+
const { done, value } = await reader.read();
|
|
932
|
+
if (done)
|
|
933
|
+
break;
|
|
934
|
+
let chunk = value;
|
|
935
|
+
for (;;) {
|
|
936
|
+
const nl = chunk.indexOf("\n");
|
|
937
|
+
if (nl < 0) {
|
|
938
|
+
if (!dropping) {
|
|
939
|
+
if (leftover.length + chunk.length > maxLineChars) {
|
|
940
|
+
leftover = "";
|
|
941
|
+
dropping = true;
|
|
942
|
+
skipped++;
|
|
943
|
+
}
|
|
944
|
+
else
|
|
945
|
+
leftover += chunk;
|
|
946
|
+
}
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
const part = chunk.slice(0, nl);
|
|
950
|
+
chunk = chunk.slice(nl + 1);
|
|
951
|
+
if (dropping) {
|
|
952
|
+
dropping = false;
|
|
953
|
+
leftover = "";
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (leftover.length + part.length > maxLineChars) {
|
|
957
|
+
leftover = "";
|
|
958
|
+
skipped++;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const line = leftover + part;
|
|
962
|
+
leftover = "";
|
|
963
|
+
if (!(await processLine(line))) {
|
|
964
|
+
return { examples, stopped: true, skipped };
|
|
965
|
+
}
|
|
1058
966
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
leftover = "";
|
|
1067
|
-
continue;
|
|
1068
|
-
}
|
|
1069
|
-
if (leftover.length + part.length > maxLineChars) {
|
|
1070
|
-
leftover = "";
|
|
1071
|
-
skipped++;
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
const line = leftover + part;
|
|
1075
|
-
leftover = "";
|
|
1076
|
-
if (!(await processLine(line))) {
|
|
1077
|
-
return { examples, stopped: true, skipped };
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
967
|
+
}
|
|
968
|
+
if (!dropping && leftover.trim()) {
|
|
969
|
+
if (!(await processLine(leftover))) {
|
|
970
|
+
return { examples, stopped: true, skipped };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return { examples, stopped: false, skipped };
|
|
1080
974
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
975
|
+
finally {
|
|
976
|
+
try {
|
|
977
|
+
reader.releaseLock();
|
|
978
|
+
}
|
|
979
|
+
catch { /* best effort */ }
|
|
1085
980
|
}
|
|
1086
|
-
return { examples, stopped: false, skipped };
|
|
1087
|
-
} finally {
|
|
1088
|
-
try {
|
|
1089
|
-
reader.releaseLock();
|
|
1090
|
-
} catch { /* best effort */ }
|
|
1091
|
-
}
|
|
1092
981
|
}
|
|
1093
982
|
/** Read a downloaded Parquet file row-group by row-group with hyparquet (+Snappy
|
|
1094
983
|
* from hyparquet-compressors) over a web-standard Blob byte source, depositing
|
|
1095
984
|
* each row via `toItems`. Only one row-group is materialised at a time, so a
|
|
1096
985
|
* multi-hundred-MB file never loads whole into memory. */
|
|
1097
986
|
async function processParquet(filePath, toItems, ci, onExample, sample) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return { examples, stopped: true, skipped };
|
|
1133
|
-
}
|
|
987
|
+
const blob = await openAsBlob(filePath);
|
|
988
|
+
const file = {
|
|
989
|
+
byteLength: blob.size,
|
|
990
|
+
slice: async (start, end) => await blob.slice(start, end ?? blob.size).arrayBuffer(),
|
|
991
|
+
};
|
|
992
|
+
const meta = await parquetMetadataAsync(file);
|
|
993
|
+
let examples = 0, skipped = 0;
|
|
994
|
+
let rowStart = 0;
|
|
995
|
+
for (const rg of meta.row_groups) {
|
|
996
|
+
if (shutdown.signal.aborted)
|
|
997
|
+
return { examples, stopped: true, skipped };
|
|
998
|
+
const rgRows = Number(rg.num_rows);
|
|
999
|
+
const rowEnd = rowStart + rgRows;
|
|
1000
|
+
// Materialise exactly one row-group, then deposit its rows.
|
|
1001
|
+
const rows = await parquetReadObjects({
|
|
1002
|
+
file,
|
|
1003
|
+
compressors,
|
|
1004
|
+
rowStart,
|
|
1005
|
+
rowEnd,
|
|
1006
|
+
});
|
|
1007
|
+
rowStart = rowEnd;
|
|
1008
|
+
for (const row of rows) {
|
|
1009
|
+
const items = toItems(row);
|
|
1010
|
+
if (!items || items.length === 0) {
|
|
1011
|
+
skipped++;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1015
|
+
examples++;
|
|
1016
|
+
return onExample(contentBytes);
|
|
1017
|
+
}, sample);
|
|
1018
|
+
if (!ok)
|
|
1019
|
+
return { examples, stopped: true, skipped };
|
|
1020
|
+
}
|
|
1134
1021
|
}
|
|
1135
|
-
|
|
1136
|
-
return { examples, stopped: false, skipped };
|
|
1022
|
+
return { examples, stopped: false, skipped };
|
|
1137
1023
|
}
|
|
1138
1024
|
/** Read a downloaded JSON-array file (General-Knowledge output.json) and deposit
|
|
1139
1025
|
* each element via `toItems`. The array is small enough (~16 MB) to parse whole;
|
|
1140
1026
|
* a huge file would be rejected by the cache ceiling long before this. */
|
|
1141
1027
|
async function processJsonArray(filePath, toItems, ci, onExample, sample) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
} catch (e) {
|
|
1147
|
-
throw new Error(`invalid JSON: ${e.message}`);
|
|
1148
|
-
}
|
|
1149
|
-
const rows = Array.isArray(arr) ? arr : [];
|
|
1150
|
-
let examples = 0, skipped = 0;
|
|
1151
|
-
for (const row of rows) {
|
|
1152
|
-
if (shutdown.signal.aborted) {
|
|
1153
|
-
return { examples, stopped: true, skipped };
|
|
1028
|
+
const blob = await openAsBlob(filePath);
|
|
1029
|
+
let arr;
|
|
1030
|
+
try {
|
|
1031
|
+
arr = JSON.parse(await blob.text());
|
|
1154
1032
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
skipped++;
|
|
1158
|
-
continue;
|
|
1033
|
+
catch (e) {
|
|
1034
|
+
throw new Error(`invalid JSON: ${e.message}`);
|
|
1159
1035
|
}
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1036
|
+
const rows = Array.isArray(arr) ? arr : [];
|
|
1037
|
+
let examples = 0, skipped = 0;
|
|
1038
|
+
for (const row of rows) {
|
|
1039
|
+
if (shutdown.signal.aborted)
|
|
1040
|
+
return { examples, stopped: true, skipped };
|
|
1041
|
+
const items = toItems(row);
|
|
1042
|
+
if (!items || items.length === 0) {
|
|
1043
|
+
skipped++;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1047
|
+
examples++;
|
|
1048
|
+
return onExample(contentBytes);
|
|
1049
|
+
}, sample);
|
|
1050
|
+
if (!ok)
|
|
1051
|
+
return { examples, stopped: true, skipped };
|
|
1166
1052
|
}
|
|
1167
|
-
|
|
1168
|
-
return { examples, stopped: false, skipped };
|
|
1053
|
+
return { examples, stopped: false, skipped };
|
|
1169
1054
|
}
|
|
1170
1055
|
/** A prompt/expected pair to display for an item. */
|
|
1171
1056
|
function promptOf(it) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1057
|
+
return isEpisode(it)
|
|
1058
|
+
? { prompt: it.context, expected: it.continuation, kind: "episode" }
|
|
1059
|
+
: { prompt: it.slice(0, 200), expected: null, kind: "experience" };
|
|
1175
1060
|
}
|
|
1176
1061
|
/** A coarse, honest similarity between an expected continuation and SEMA's
|
|
1177
1062
|
* recall. Both are normalized (lowercased, whitespace-collapsed) and compared
|
|
1178
1063
|
* by the longest shared leading run plus token overlap, so the verdict is a
|
|
1179
1064
|
* heuristic signal of recall quality rather than a brittle fixed-prefix test. */
|
|
1180
1065
|
function recallSimilarity(expected, response) {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
lead
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
const jac = inter / Math.max(1, ta.size + tb.size - inter);
|
|
1200
|
-
return Math.max(leadFrac, jac);
|
|
1066
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1067
|
+
const a = norm(expected), b = norm(response);
|
|
1068
|
+
if (!a || !b)
|
|
1069
|
+
return 0;
|
|
1070
|
+
let lead = 0;
|
|
1071
|
+
const lim = Math.min(a.length, b.length);
|
|
1072
|
+
while (lead < lim && a[lead] === b[lead])
|
|
1073
|
+
lead++;
|
|
1074
|
+
const leadFrac = lead / Math.max(1, Math.min(a.length, b.length));
|
|
1075
|
+
const ta = new Set(a.split(" ")), tb = new Set(b.split(" "));
|
|
1076
|
+
let inter = 0;
|
|
1077
|
+
for (const w of ta)
|
|
1078
|
+
if (tb.has(w))
|
|
1079
|
+
inter++;
|
|
1080
|
+
const jac = inter / Math.max(1, ta.size + tb.size - inter);
|
|
1081
|
+
return Math.max(leadFrac, jac);
|
|
1201
1082
|
}
|
|
1202
1083
|
/** A framed recall sample. Pinned in the panel on a TTY (so the most recent
|
|
1203
1084
|
* example is always on screen) and logged once per checkpoint when piped. */
|
|
1204
1085
|
function renderInferenceBox(prompt, expected, response, kind, checkpointN) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1086
|
+
const W = 68;
|
|
1087
|
+
const hr = `${DIM}${"─".repeat(W)}${R}`;
|
|
1088
|
+
const title = kind === "episode"
|
|
1089
|
+
? "latest recall"
|
|
1090
|
+
: "latest recall (experience)";
|
|
1091
|
+
const head = `${title} · checkpoint #${checkpointN} `;
|
|
1092
|
+
const shown = response.trim() ? response : "(empty)";
|
|
1093
|
+
const lines = [
|
|
1094
|
+
`${B}╭─ ${head}${"─".repeat(Math.max(0, W - 2 - head.length))}╮${R}`,
|
|
1095
|
+
`${B}│${R} ${hr}`,
|
|
1096
|
+
`${B}│${R} ${CYAN}${B}Context:${R} ${clip(prompt, W - 13)}`,
|
|
1097
|
+
];
|
|
1098
|
+
if (expected) {
|
|
1099
|
+
lines.push(`${B}│${R} ${YEL}${B}Expected:${R} ${clip(expected, W - 13)}`);
|
|
1100
|
+
}
|
|
1101
|
+
lines.push(`${B}│${R} ${GRN}${B}SEMA:${R} ${clip(shown, W - 13)}`);
|
|
1102
|
+
lines.push(`${B}│${R} ${hr}`);
|
|
1103
|
+
let verdict;
|
|
1104
|
+
if (expected) {
|
|
1105
|
+
const sim = recallSimilarity(expected, response);
|
|
1106
|
+
const pctStr = `${Math.round(sim * 100)}%`;
|
|
1107
|
+
verdict = sim >= 0.6
|
|
1108
|
+
? `${GRN}✓${R} recall close to expected ${DIM}(~${pctStr} overlap)${R}`
|
|
1109
|
+
: sim >= 0.25
|
|
1110
|
+
? `${YEL}△${R} partial recall ${DIM}(~${pctStr} overlap)${R}`
|
|
1111
|
+
: `${RED}✗${R} recall diverges ${DIM}(~${pctStr} overlap)${R}`;
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
verdict = `${DIM}·${R} plain experience — no expected answer`;
|
|
1115
|
+
}
|
|
1116
|
+
lines.push(`${B}│${R} ${verdict}`);
|
|
1117
|
+
lines.push(`${B}╰${"─".repeat(W)}╯${R}`);
|
|
1118
|
+
return lines.join("\n");
|
|
1237
1119
|
}
|
|
1238
1120
|
function renderPanel(s) {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1121
|
+
const targetKnown = isFinite(s.target);
|
|
1122
|
+
// Primary progress: by learned-content bytes when a MAX_MB target is set,
|
|
1123
|
+
// else by how far we are through the corpus on disk (bytes) — so the default
|
|
1124
|
+
// unbounded run still shows a real fraction and a real ETA.
|
|
1125
|
+
const frac = targetKnown
|
|
1126
|
+
? (s.target > 0 ? s.trainedBytes / s.target : 0)
|
|
1127
|
+
: (s.bytesTotal > 0 ? s.bytesDone / s.bytesTotal : 0);
|
|
1128
|
+
const etaStr = (() => {
|
|
1129
|
+
if (targetKnown) {
|
|
1130
|
+
return s.trainedRate > 0
|
|
1131
|
+
? dur((s.target - s.trainedBytes) / s.trainedRate)
|
|
1132
|
+
: "∞";
|
|
1133
|
+
}
|
|
1134
|
+
if (s.bytesTotal > 0 && s.bytesRate > 0) {
|
|
1135
|
+
return dur((s.bytesTotal - s.bytesDone) / s.bytesRate);
|
|
1136
|
+
}
|
|
1137
|
+
return "∞";
|
|
1138
|
+
})();
|
|
1139
|
+
const fileFrac = s.fileTotal > 0 ? s.fileIndex / s.fileTotal : 0;
|
|
1140
|
+
let actIcon = `${DIM}·${R}`, actText = "waiting…";
|
|
1141
|
+
if (s.activity === "download") {
|
|
1142
|
+
actIcon = `${CYAN}⬇${R}`;
|
|
1143
|
+
const name = s.filePath;
|
|
1144
|
+
const total = s.dlTotal > 0 ? s.dlTotal : s.fileSize;
|
|
1145
|
+
if (total > 0 && s.dlDone > 0) {
|
|
1146
|
+
const dlFrac = clamp01(s.dlDone / total);
|
|
1147
|
+
actText =
|
|
1148
|
+
`downloading ${name} ${bar(18, dlFrac)} ${B}${pct(dlFrac)}${R}` +
|
|
1149
|
+
` ${DIM}${bytes(s.dlDone)}/${bytes(total)}${R}`;
|
|
1150
|
+
if (s.dlSpeed > 0)
|
|
1151
|
+
actText += ` ${DIM}@ ${bytes(s.dlSpeed)}/s${R}`;
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
actText = total > 0
|
|
1155
|
+
? `downloading ${name} · ${bytes(total)}…`
|
|
1156
|
+
: `downloading ${name}…`;
|
|
1157
|
+
}
|
|
1275
1158
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
`learned ${DIM}·${R} checkpoint every ${bytes(CHECKPOINT_BYTES)}`,
|
|
1303
|
-
`\n${bar(40, frac)} ${B}${pct(frac)}${R} ${headExamples}`,
|
|
1304
|
-
`\n${B}⚡${R} ${bytes(s.trainedRate)}/s learned ${B}🧠${R} ${
|
|
1305
|
-
bytes(s.trainedBytes)
|
|
1306
|
-
} content ${B}⏱${R} ${dur(s.elapsedS)} elapsed ${B}🕐${R} ${etaStr} ETA`,
|
|
1307
|
-
`${fileInfo} ${corpusInfo} ${B}🗄${R} ${num(s.storeEntries)} entries ` +
|
|
1308
|
-
`${B}💾${R} cache ${bytes(s.cacheBytes)}`,
|
|
1309
|
-
`\n${actIcon} ${actText}`,
|
|
1310
|
-
].join("");
|
|
1311
|
-
return s.lastSample ? `${panel}\n${s.lastSample}` : panel;
|
|
1159
|
+
else if (s.activity === "process") {
|
|
1160
|
+
actIcon = `${GRN}✓${R}`;
|
|
1161
|
+
actText = `processing ${s.filePath} · ${int(s.fileExamples)} examples so far`;
|
|
1162
|
+
}
|
|
1163
|
+
const targetStr = targetKnown ? bytes(s.target) : "∞";
|
|
1164
|
+
const headExamples = targetKnown
|
|
1165
|
+
? `${CYAN}${bytes(s.trainedBytes)}${R} / ${targetStr} learned ${DIM}·${R} ${int(s.exampleCount)} examples`
|
|
1166
|
+
: `${CYAN}${int(s.exampleCount)}${R} examples`;
|
|
1167
|
+
const corpusInfo = s.bytesTotal > 0
|
|
1168
|
+
? `${B}📦${R} ${bytes(s.bytesDone)}/${bytes(s.bytesTotal)} (${pct(s.bytesDone / s.bytesTotal)})`
|
|
1169
|
+
: `${B}📦${R} ${bytes(s.bytesDone)} processed`;
|
|
1170
|
+
const fileInfo = s.fileTotal > 0
|
|
1171
|
+
? `${B}🌐${R} ${s.fileIndex}/${s.fileTotal} (${pct(fileFrac)})`
|
|
1172
|
+
: `${B}🌐${R} ${s.fileIndex} languages`;
|
|
1173
|
+
const panel = [
|
|
1174
|
+
`${B}╭${R}${B} sema train${R} ${DIM}·${R} SmolSent+Aya+oasst2 ${DIM}·${R} ` +
|
|
1175
|
+
`D=${D} ${DIM}·${R} seed=${SEED} ${DIM}·${R} ` +
|
|
1176
|
+
`store=${basename(DB_PATH)}.sqlite\n${B}╰${R} target=${CYAN}${targetStr}${R} ` +
|
|
1177
|
+
`learned ${DIM}·${R} checkpoint every ${bytes(CHECKPOINT_BYTES)}`,
|
|
1178
|
+
`\n${bar(40, frac)} ${B}${pct(frac)}${R} ${headExamples}`,
|
|
1179
|
+
`\n${B}⚡${R} ${bytes(s.trainedRate)}/s learned ${B}🧠${R} ${bytes(s.trainedBytes)} content ${B}⏱${R} ${dur(s.elapsedS)} elapsed ${B}🕐${R} ${etaStr} ETA`,
|
|
1180
|
+
`${fileInfo} ${corpusInfo} ${B}🗄${R} ${num(s.storeEntries)} entries ` +
|
|
1181
|
+
`${B}💾${R} cache ${bytes(s.cacheBytes)}`,
|
|
1182
|
+
`\n${actIcon} ${actText}`,
|
|
1183
|
+
].join("");
|
|
1184
|
+
return s.lastSample ? `${panel}\n${s.lastSample}` : panel;
|
|
1312
1185
|
}
|
|
1313
1186
|
/** A live panel pinned to the bottom of stderr. On a TTY it redraws in place,
|
|
1314
1187
|
* clearing only its own lines; logs are flushed into the scrollback above it.
|
|
1315
1188
|
* Off a TTY (piped/CI) the panel is suppressed and a plain status line is
|
|
1316
1189
|
* emitted occasionally, so logs stay clean and parseable. */
|
|
1317
1190
|
class Progress {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
let out = `${this.clearPanel()}${msg}\n`;
|
|
1373
|
-
this.lines = 0;
|
|
1374
|
-
if (this.last) {
|
|
1375
|
-
const text = renderPanel(this.last);
|
|
1376
|
-
out += `${HIDE}${text}`;
|
|
1377
|
-
this.lines = text.split("\n").length;
|
|
1191
|
+
lines = 0; // height of the panel currently on screen
|
|
1192
|
+
lastPaint = 0;
|
|
1193
|
+
lastStatus = 0;
|
|
1194
|
+
last = null;
|
|
1195
|
+
tty = process.stderr.isTTY === true;
|
|
1196
|
+
/** True when attached to an interactive terminal (panel is live). */
|
|
1197
|
+
get interactive() {
|
|
1198
|
+
return this.tty;
|
|
1199
|
+
}
|
|
1200
|
+
/** Cursor sequence that returns to the top of the panel and clears it. */
|
|
1201
|
+
clearPanel() {
|
|
1202
|
+
if (this.lines <= 0)
|
|
1203
|
+
return "";
|
|
1204
|
+
const up = this.lines - 1; // cursor is on the panel's last line
|
|
1205
|
+
return (up > 0 ? `${CSI}${up}F` : "\r") + `${CSI}0J`;
|
|
1206
|
+
}
|
|
1207
|
+
render(s, force = false) {
|
|
1208
|
+
this.last = s;
|
|
1209
|
+
const now = Date.now();
|
|
1210
|
+
if (!force && now - this.lastPaint < PROGRESS_MS)
|
|
1211
|
+
return;
|
|
1212
|
+
this.lastPaint = now;
|
|
1213
|
+
if (!this.tty) {
|
|
1214
|
+
if (force || now - this.lastStatus >= 10_000) {
|
|
1215
|
+
this.lastStatus = now;
|
|
1216
|
+
const targetKnown = isFinite(s.target);
|
|
1217
|
+
const where = s.bytesTotal > 0
|
|
1218
|
+
? ` ${pct(s.bytesDone / s.bytesTotal)} of corpus`
|
|
1219
|
+
: "";
|
|
1220
|
+
process.stderr.write(`[sema] ${bytes(s.trainedBytes)}${targetKnown ? "/" + bytes(s.target) : ""} learned · ${int(s.exampleCount)} examples · ` +
|
|
1221
|
+
`${bytes(s.trainedRate)}/s · lang ${s.fileIndex}/${s.fileTotal}${where} · ` +
|
|
1222
|
+
`${num(s.storeEntries)} entries\n`);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const text = renderPanel(s);
|
|
1227
|
+
process.stderr.write(`${this.clearPanel()}${HIDE}${text}`);
|
|
1228
|
+
this.lines = text.split("\n").length;
|
|
1229
|
+
}
|
|
1230
|
+
/** Emit a line (or block) into the scrollback above the panel; the panel is
|
|
1231
|
+
* redrawn immediately beneath it so it never disappears between frames. */
|
|
1232
|
+
log(msg) {
|
|
1233
|
+
if (!this.tty) {
|
|
1234
|
+
process.stderr.write(`${msg}\n`);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
let out = `${this.clearPanel()}${msg}\n`;
|
|
1238
|
+
this.lines = 0;
|
|
1239
|
+
if (this.last) {
|
|
1240
|
+
const text = renderPanel(this.last);
|
|
1241
|
+
out += `${HIDE}${text}`;
|
|
1242
|
+
this.lines = text.split("\n").length;
|
|
1243
|
+
}
|
|
1244
|
+
process.stderr.write(out);
|
|
1378
1245
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (this.tty) {
|
|
1383
|
-
process.stderr.write(`${SHOW}\n`);
|
|
1246
|
+
dispose() {
|
|
1247
|
+
if (this.tty)
|
|
1248
|
+
process.stderr.write(`${SHOW}\n`);
|
|
1384
1249
|
}
|
|
1385
|
-
}
|
|
1386
1250
|
}
|
|
1387
1251
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1388
1252
|
// §9 Progress persistence (inside the store — resume from the store alone)
|
|
@@ -1393,1092 +1257,976 @@ const META_TRAINED_BYTES = "train.trainedContentBytes";
|
|
|
1393
1257
|
const META_BYTES = "train.totalBytesProcessed";
|
|
1394
1258
|
const META_CORPUS_BYTES = "train.totalCorpusBytes";
|
|
1395
1259
|
async function loadProgress(store) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1260
|
+
try {
|
|
1261
|
+
const raw = await store.getMeta(META_COMPLETED);
|
|
1262
|
+
const deps = await store.getMeta(META_DEPOSITS);
|
|
1263
|
+
const b = await store.getMeta(META_BYTES);
|
|
1264
|
+
if (raw !== null && deps !== null && b !== null) {
|
|
1265
|
+
const completedFiles = JSON.parse(raw);
|
|
1266
|
+
if (Array.isArray(completedFiles)) {
|
|
1267
|
+
const trained = await store.getMeta(META_TRAINED_BYTES);
|
|
1268
|
+
const corpus = await store.getMeta(META_CORPUS_BYTES);
|
|
1269
|
+
return {
|
|
1270
|
+
completedFiles,
|
|
1271
|
+
depositCount: Number(deps) || 0,
|
|
1272
|
+
trainedContentBytes: Number(trained) || 0,
|
|
1273
|
+
totalBytesProcessed: Number(b) || 0,
|
|
1274
|
+
totalCorpusBytes: Number(corpus) || 0,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1413
1278
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1279
|
+
catch { /* corrupt/missing — start fresh */ }
|
|
1280
|
+
return {
|
|
1281
|
+
completedFiles: [],
|
|
1282
|
+
depositCount: 0,
|
|
1283
|
+
trainedContentBytes: 0,
|
|
1284
|
+
totalBytesProcessed: 0,
|
|
1285
|
+
totalCorpusBytes: 0,
|
|
1286
|
+
};
|
|
1422
1287
|
}
|
|
1423
1288
|
async function saveProgress(store, p) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1289
|
+
await store.setMeta(META_COMPLETED, JSON.stringify(p.completedFiles));
|
|
1290
|
+
await store.setMeta(META_DEPOSITS, String(p.depositCount));
|
|
1291
|
+
await store.setMeta(META_TRAINED_BYTES, String(p.trainedContentBytes));
|
|
1292
|
+
await store.setMeta(META_BYTES, String(p.totalBytesProcessed));
|
|
1293
|
+
await store.setMeta(META_CORPUS_BYTES, String(p.totalCorpusBytes));
|
|
1294
|
+
await store.setMeta("train.updatedAt", new Date().toISOString());
|
|
1295
|
+
store.commit();
|
|
1431
1296
|
}
|
|
1432
1297
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1433
1298
|
// §10 Main
|
|
1434
1299
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1435
1300
|
async function main() {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
const ci = new CachedIngest(mind);
|
|
1471
|
-
const prevD = await store.getMeta("train.D");
|
|
1472
|
-
const prevSeed = await store.getMeta("train.seed");
|
|
1473
|
-
if (
|
|
1474
|
-
(prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)
|
|
1475
|
-
) {
|
|
1476
|
-
process.stderr.write(
|
|
1477
|
-
`fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
|
|
1478
|
-
`requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
|
|
1479
|
-
`to start fresh.\n`,
|
|
1480
|
-
);
|
|
1481
|
-
process.exit(1);
|
|
1482
|
-
}
|
|
1483
|
-
await store.setMeta("train.dataset", "SmolSent+Aya+oasst2");
|
|
1484
|
-
await store.setMeta("train.D", String(D));
|
|
1485
|
-
await store.setMeta("train.seed", String(SEED));
|
|
1486
|
-
await store.setMeta("train.createdAt", new Date().toISOString());
|
|
1487
|
-
// ── counters & sampling ──
|
|
1488
|
-
let depositCount = 0;
|
|
1489
|
-
let trainedContentBytes = 0;
|
|
1490
|
-
let bytesSinceCkpt = 0;
|
|
1491
|
-
let checkpointNum = 0;
|
|
1492
|
-
let totalBytesProcessed = 0;
|
|
1493
|
-
let totalCorpusBytes = 0;
|
|
1494
|
-
const langTally = {};
|
|
1495
|
-
const t0 = Date.now();
|
|
1496
|
-
// Reservoir sample: one uniformly-random item from the current window, shown
|
|
1497
|
-
// in the recall box at each checkpoint.
|
|
1498
|
-
let sampleItem = null;
|
|
1499
|
-
let seenInWindow = 0;
|
|
1500
|
-
const sample = (it) => {
|
|
1501
|
-
seenInWindow++;
|
|
1502
|
-
if (Math.random() < 1 / seenInWindow) {
|
|
1503
|
-
sampleItem = it;
|
|
1504
|
-
}
|
|
1505
|
-
};
|
|
1506
|
-
// ── progress panel ──
|
|
1507
|
-
const progress = new Progress();
|
|
1508
|
-
// Surface rate-limit waits from the low-level fetch retries into the live log,
|
|
1509
|
-
// so a 429 back-off reads as "waiting", never a silent hang or a dropped file.
|
|
1510
|
-
onThrottleWait = (ms, label) => {
|
|
1511
|
-
progress.log(
|
|
1512
|
-
` ${YEL}⏳${R} rate-limited (${label}); waiting ${
|
|
1513
|
-
(ms / 1000).toFixed(1)
|
|
1514
|
-
}s and retrying — not skipping`,
|
|
1515
|
-
);
|
|
1516
|
-
};
|
|
1517
|
-
const state = {
|
|
1518
|
-
exampleCount: 0,
|
|
1519
|
-
target: MAX_BYTES,
|
|
1520
|
-
elapsedS: 0,
|
|
1521
|
-
trainedBytes: 0,
|
|
1522
|
-
trainedRate: 0,
|
|
1523
|
-
bytesDone: 0,
|
|
1524
|
-
bytesTotal: 0,
|
|
1525
|
-
bytesRate: 0,
|
|
1526
|
-
fileIndex: 0,
|
|
1527
|
-
fileTotal: 0,
|
|
1528
|
-
filePath: "",
|
|
1529
|
-
fileSize: 0,
|
|
1530
|
-
fileExamples: 0,
|
|
1531
|
-
activity: "idle",
|
|
1532
|
-
dlSpeed: 0,
|
|
1533
|
-
dlDone: 0,
|
|
1534
|
-
dlTotal: 0,
|
|
1535
|
-
storeEntries: 0,
|
|
1536
|
-
cacheBytes: 0,
|
|
1537
|
-
lastSample: null,
|
|
1538
|
-
};
|
|
1539
|
-
// store.size() is async; refresh it on a slow cadence so the hot loop and
|
|
1540
|
-
// the repaint never block on a query.
|
|
1541
|
-
let cachedEntries = 0;
|
|
1542
|
-
let sizeInFlight = false;
|
|
1543
|
-
const refreshSize = () => {
|
|
1544
|
-
if (sizeInFlight) {
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
sizeInFlight = true;
|
|
1548
|
-
void mind.store.size()
|
|
1549
|
-
.then((n) => (cachedEntries = n))
|
|
1550
|
-
.catch(() => undefined)
|
|
1551
|
-
.finally(() => (sizeInFlight = false));
|
|
1552
|
-
};
|
|
1553
|
-
// Cache size changes only at download/delete boundaries — recompute it
|
|
1554
|
-
// lazily rather than statting the dir on every deposit.
|
|
1555
|
-
let cachedCacheBytes = 0;
|
|
1556
|
-
let lastCacheUpdate = 0;
|
|
1557
|
-
// Live download progress for the panel.
|
|
1558
|
-
let dlSlot = null;
|
|
1559
|
-
// Rolling throughput: a short EMA over wall-clock windows, so the headline
|
|
1560
|
-
// figures reflect CURRENT speed rather than a lifetime average diluted by the
|
|
1561
|
-
// listing and download phases (which train nothing).
|
|
1562
|
-
let rateT = t0;
|
|
1563
|
-
let rateTrained = 0;
|
|
1564
|
-
let rateBytes = 0;
|
|
1565
|
-
const syncState = () => {
|
|
1566
|
-
const now = Date.now();
|
|
1567
|
-
state.exampleCount = depositCount;
|
|
1568
|
-
state.trainedBytes = trainedContentBytes;
|
|
1569
|
-
state.elapsedS = (now - t0) / 1000;
|
|
1570
|
-
state.storeEntries = cachedEntries;
|
|
1571
|
-
state.bytesDone = totalBytesProcessed;
|
|
1572
|
-
state.bytesTotal = totalCorpusBytes;
|
|
1573
|
-
if (dlSlot && state.activity === "download") {
|
|
1574
|
-
state.dlDone = dlSlot.done;
|
|
1575
|
-
state.dlTotal = dlSlot.total;
|
|
1576
|
-
const ds = (now - dlSlot.t0) / 1000;
|
|
1577
|
-
state.dlSpeed = ds > 0.2 ? dlSlot.done / ds : 0;
|
|
1578
|
-
} else {
|
|
1579
|
-
state.dlDone = 0;
|
|
1580
|
-
state.dlTotal = 0;
|
|
1581
|
-
}
|
|
1582
|
-
const dt = (now - rateT) / 1000;
|
|
1583
|
-
if (dt >= 0.5) {
|
|
1584
|
-
const instTrained = (trainedContentBytes - rateTrained) / dt;
|
|
1585
|
-
const instByte = (totalBytesProcessed - rateBytes) / dt;
|
|
1586
|
-
const a = 0.3; // EMA weight on the newest sample
|
|
1587
|
-
state.trainedRate = state.trainedRate === 0
|
|
1588
|
-
? instTrained
|
|
1589
|
-
: state.trainedRate * (1 - a) + instTrained * a;
|
|
1590
|
-
state.bytesRate = state.bytesRate === 0
|
|
1591
|
-
? instByte
|
|
1592
|
-
: state.bytesRate * (1 - a) + instByte * a;
|
|
1593
|
-
rateT = now;
|
|
1594
|
-
rateTrained = trainedContentBytes;
|
|
1595
|
-
rateBytes = totalBytesProcessed;
|
|
1596
|
-
}
|
|
1597
|
-
if (now - lastCacheUpdate > 2000) {
|
|
1598
|
-
cachedCacheBytes = cacheSize();
|
|
1599
|
-
lastCacheUpdate = now;
|
|
1600
|
-
}
|
|
1601
|
-
state.cacheBytes = cachedCacheBytes;
|
|
1602
|
-
};
|
|
1603
|
-
const tick = (force = false) => {
|
|
1604
|
-
syncState();
|
|
1605
|
-
progress.render(state, force);
|
|
1606
|
-
};
|
|
1607
|
-
const paintTimer = setInterval(() => {
|
|
1608
|
-
refreshSize();
|
|
1609
|
-
tick(false);
|
|
1610
|
-
}, PROGRESS_MS);
|
|
1611
|
-
if (typeof paintTimer.unref === "function") {
|
|
1612
|
-
paintTimer.unref();
|
|
1613
|
-
}
|
|
1614
|
-
// ── keep-alive: the process must never exit on its own mid-training ──
|
|
1615
|
-
// The CPU-bound processing phase (perceive + intern + the batched vector-index
|
|
1616
|
-
// writes) hands control back to the event loop between batches via the store's
|
|
1617
|
-
// yieldToEventLoop(), which parks on an UNREF'd setImmediate so the library
|
|
1618
|
-
// never holds a process open by itself. node:sqlite is synchronous and the
|
|
1619
|
-
// vector index is in-memory, so the store's awaits resolve as microtasks with
|
|
1620
|
-
// no I/O handle, and the paint timer above is unref'd too. That leaves a window
|
|
1621
|
-
// — a batch flush that fires while we're processing an in-memory chunk, not
|
|
1622
|
-
// awaiting a disk read — in which the ONLY pending work is that unref'd
|
|
1623
|
-
// setImmediate and NOTHING is ref'd. Node's rule is to exit when only unref'd
|
|
1624
|
-
// handles remain, WITHOUT running them: the yield's continuation never fires,
|
|
1625
|
-
// main() is abandoned, and the process exits 0 silently mid-file — no error for
|
|
1626
|
-
// the fault-tolerance to catch. This one ref'd (NOT unref'd) timer guarantees a
|
|
1627
|
-
// live handle for the whole run, so the loop can never drain from under a
|
|
1628
|
-
// pending yield. Every real exit is an explicit process.exit() (finish(), the
|
|
1629
|
-
// shutdown watchdog, the second-signal path, the fatal catch), so keeping this
|
|
1630
|
-
// handle alive never delays a genuine shutdown; finish() clears it before that
|
|
1631
|
-
// final exit for tidiness. Same lesson as waitMs above (deliberately un-unref'd).
|
|
1632
|
-
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
1633
|
-
const checkpoint = () => mind.save();
|
|
1634
|
-
/** Run index maintenance: compact (remove garbage) then repair (fill gaps).
|
|
1635
|
-
* Both are idempotent — running twice produces the same result as once.
|
|
1636
|
-
* Compaction frees index space first; repair then adds back the bridges
|
|
1637
|
-
* whose gists were evicted before indexing, completing the coverage that
|
|
1638
|
-
* incremental bridge promotion alone cannot guarantee.
|
|
1639
|
-
*
|
|
1640
|
-
* Logs the number of entries removed/added so a run that silently degrades
|
|
1641
|
-
* (growing compaction count, or repair never recovering anything) is
|
|
1642
|
-
* visible in the training log. */
|
|
1643
|
-
const runIndexMaintenance = async () => {
|
|
1644
|
-
if (!INDEX_MAINTENANCE) {
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
try {
|
|
1648
|
-
const removed = await mind.store.compactContentIndex();
|
|
1649
|
-
if (removed > 0) {
|
|
1650
|
-
progress.log(
|
|
1651
|
-
` ${DIM}index compact: removed ${int(removed)} isolated entries${R}`,
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
} catch (err) {
|
|
1655
|
-
progress.log(
|
|
1656
|
-
` ${YEL}⚠ index compact failed${R}: ${
|
|
1657
|
-
err instanceof Error ? err.message : String(err)
|
|
1658
|
-
}`,
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
|
-
try {
|
|
1662
|
-
const added = await mind.repairContentIndex();
|
|
1663
|
-
if (added > 0) {
|
|
1664
|
-
progress.log(
|
|
1665
|
-
` ${GRN}index repair: added ${int(added)} missing bridges${R}`,
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
} catch (err) {
|
|
1669
|
-
progress.log(
|
|
1670
|
-
` ${YEL}⚠ index repair failed${R}: ${
|
|
1671
|
-
err instanceof Error ? err.message : String(err)
|
|
1672
|
-
}`,
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
// The checkpoint recall is a best-effort diagnostic. It is time-bounded so a
|
|
1677
|
-
// slow/large store can never freeze the deposit loop, and guarded so a still
|
|
1678
|
-
// running recall is never stacked on top of another.
|
|
1679
|
-
let inferBusy = false;
|
|
1680
|
-
const runRecall = async (item, n) => {
|
|
1681
|
-
if (inferBusy) {
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
inferBusy = true;
|
|
1685
|
-
try {
|
|
1686
|
-
const info = promptOf(item);
|
|
1687
|
-
const r = await withTimeout(
|
|
1688
|
-
mind.respond(info.prompt),
|
|
1689
|
-
INFER_TIMEOUT_MS,
|
|
1690
|
-
"recall",
|
|
1691
|
-
);
|
|
1692
|
-
const resp = new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
|
|
1693
|
-
const box = renderInferenceBox(
|
|
1694
|
-
info.prompt,
|
|
1695
|
-
info.expected,
|
|
1696
|
-
resp,
|
|
1697
|
-
info.kind,
|
|
1698
|
-
n,
|
|
1699
|
-
);
|
|
1700
|
-
state.lastSample = box;
|
|
1701
|
-
if (!progress.interactive) {
|
|
1702
|
-
progress.log(box);
|
|
1703
|
-
}
|
|
1704
|
-
tick(true);
|
|
1705
|
-
} catch (err) {
|
|
1706
|
-
progress.log(
|
|
1707
|
-
` ${DIM}· checkpoint #${n} recall skipped: ${
|
|
1708
|
-
err instanceof Error ? err.message : String(err)
|
|
1709
|
-
}${R}`,
|
|
1710
|
-
);
|
|
1711
|
-
} finally {
|
|
1712
|
-
inferBusy = false;
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
// ── graceful shutdown (always leaves the store consistent) ──
|
|
1716
|
-
let stopRequested = false;
|
|
1717
|
-
let stopReason = "interrupted";
|
|
1718
|
-
let finishing = false;
|
|
1719
|
-
const finish = async (why) => {
|
|
1720
|
-
if (finishing) {
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
finishing = true;
|
|
1724
|
-
shutdown.abort(); // unblock any straggling fetch/pipeTo
|
|
1725
|
-
tick(true);
|
|
1726
|
-
await store.setMeta("train.completedAt", new Date().toISOString());
|
|
1727
|
-
await store.setMeta("train.totalDeposits", String(depositCount));
|
|
1728
|
-
await store.setMeta("train.totalTrainedBytes", String(trainedContentBytes));
|
|
1729
|
-
await store.setMeta("train.totalBytes", String(totalBytesProcessed));
|
|
1730
|
-
await store.setMeta("train.totalCorpusBytes", String(totalCorpusBytes));
|
|
1731
|
-
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1732
|
-
try {
|
|
1733
|
-
await runIndexMaintenance();
|
|
1734
|
-
await checkpoint();
|
|
1735
|
-
} catch (err) {
|
|
1736
|
-
process.stderr.write(
|
|
1737
|
-
`\n ${YEL}⚠ final checkpoint failed${R}: ${
|
|
1738
|
-
err instanceof Error ? err.message : String(err)
|
|
1739
|
-
}\n`,
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
clearInterval(paintTimer);
|
|
1743
|
-
clearInterval(keepAlive);
|
|
1744
|
-
progress.dispose();
|
|
1745
|
-
const elapsedS = (Date.now() - t0) / 1000;
|
|
1746
|
-
const elapsed = dur(elapsedS);
|
|
1747
|
-
const avgRate = elapsedS > 0 ? trainedContentBytes / elapsedS : 0;
|
|
1748
|
-
const tally = Object.entries(langTally)
|
|
1749
|
-
.sort((a, b) => b[1] - a[1])
|
|
1750
|
-
.map(([k, v]) => `${k}:${int(v)}`)
|
|
1751
|
-
.join(", ");
|
|
1752
|
-
let entries = depositCount;
|
|
1753
|
-
try {
|
|
1754
|
-
entries = await mind.store.size();
|
|
1755
|
-
} catch { /* best effort */ }
|
|
1756
|
-
console.log(
|
|
1757
|
-
`\n${GRN}✓${R} ${why}. ${basename(DB_PATH)}.sqlite: ` +
|
|
1758
|
-
`${int(entries)} entries, ${int(depositCount)} examples, ` +
|
|
1759
|
-
`${bytes(trainedContentBytes)} content learned ` +
|
|
1760
|
-
`${DIM}(${bytes(avgRate)}/s avg)${R}, ` +
|
|
1761
|
-
`${bytes(totalBytesProcessed)} corpus processed, ${elapsed} elapsed.` +
|
|
1762
|
-
(tally ? `\n ${DIM}per language:${R} ${tally}` : ""),
|
|
1763
|
-
);
|
|
1764
|
-
try {
|
|
1765
|
-
await store.close();
|
|
1766
|
-
} catch { /* best effort */ }
|
|
1767
|
-
process.exit(0);
|
|
1768
|
-
};
|
|
1769
|
-
const requestStop = (reason) => {
|
|
1770
|
-
if (stopRequested) {
|
|
1771
|
-
process.stderr.write(`\n${YEL}⚠ second signal — exiting now${R}\n`);
|
|
1772
|
-
process.stderr.write(SHOW);
|
|
1773
|
-
process.exit(130);
|
|
1774
|
-
}
|
|
1775
|
-
stopRequested = true;
|
|
1776
|
-
stopReason = reason;
|
|
1777
|
-
shutdown.abort();
|
|
1778
|
-
progress.log(` ${YEL}⏸${R} ${reason} — finishing current item, saving…`);
|
|
1779
|
-
const watchdog = setTimeout(() => {
|
|
1780
|
-
process.stderr.write(
|
|
1781
|
-
`\n${YEL}⚠ shutdown watchdog fired — forcing exit${R}\n`,
|
|
1782
|
-
);
|
|
1783
|
-
process.stderr.write(SHOW);
|
|
1784
|
-
process.exit(130);
|
|
1785
|
-
}, 60_000);
|
|
1786
|
-
if (typeof watchdog.unref === "function") {
|
|
1787
|
-
watchdog.unref();
|
|
1788
|
-
}
|
|
1789
|
-
};
|
|
1790
|
-
process.on("SIGINT", () => requestStop("interrupted"));
|
|
1791
|
-
process.on("SIGTERM", () => requestStop("terminated"));
|
|
1792
|
-
// ── fail-safe: a dropped connection must never kill a long run ──
|
|
1793
|
-
process.on("unhandledRejection", (reason) => {
|
|
1794
|
-
progress.log(
|
|
1795
|
-
` ${YEL}⚠ unhandled rejection${R}: ${
|
|
1796
|
-
reason instanceof Error ? reason.message : String(reason)
|
|
1797
|
-
}`,
|
|
1798
|
-
);
|
|
1799
|
-
});
|
|
1800
|
-
process.on("uncaughtException", (err) => {
|
|
1801
|
-
const code = err?.code ?? err?.cause?.code;
|
|
1802
|
-
if (err.message === "terminated" || code === "UND_ERR_SOCKET") {
|
|
1803
|
-
progress.log(` ${YEL}⚠ connection error (ignored)${R}: ${err.message}`);
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
process.stderr.write(
|
|
1807
|
-
`\n${RED}uncaught exception${R}: ${err.message}\n${err.stack ?? ""}\n`,
|
|
1808
|
-
);
|
|
1809
|
-
try {
|
|
1810
|
-
void store.setMeta("train.crashedAt", new Date().toISOString());
|
|
1811
|
-
void store.setMeta("train.crashError", err.message);
|
|
1812
|
-
void store.setMeta("train.totalDeposits", String(depositCount));
|
|
1813
|
-
} catch { /* best effort */ }
|
|
1814
|
-
process.exit(1);
|
|
1815
|
-
});
|
|
1816
|
-
// ── per-example callback: gates MAX_MB, drives checkpoints + samples ──
|
|
1817
|
-
const onDeposit = async (contentBytes) => {
|
|
1818
|
-
depositCount++;
|
|
1819
|
-
trainedContentBytes += contentBytes;
|
|
1820
|
-
bytesSinceCkpt += contentBytes;
|
|
1821
|
-
state.fileExamples++;
|
|
1822
|
-
if (bytesSinceCkpt >= CHECKPOINT_BYTES) {
|
|
1823
|
-
bytesSinceCkpt %= CHECKPOINT_BYTES;
|
|
1824
|
-
const n = ++checkpointNum;
|
|
1825
|
-
const item = sampleItem;
|
|
1826
|
-
sampleItem = null;
|
|
1827
|
-
seenInWindow = 0;
|
|
1828
|
-
if (item) {
|
|
1829
|
-
await runRecall(item, n);
|
|
1830
|
-
}
|
|
1831
|
-
try {
|
|
1832
|
-
await runIndexMaintenance();
|
|
1833
|
-
await checkpoint();
|
|
1834
|
-
} catch (err) {
|
|
1835
|
-
progress.log(
|
|
1836
|
-
` ${YEL}⚠ checkpoint failed${R}: ${
|
|
1837
|
-
err instanceof Error ? err.message : String(err)
|
|
1838
|
-
}`,
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
tick(true);
|
|
1842
|
-
} else {
|
|
1843
|
-
tick();
|
|
1844
|
-
}
|
|
1845
|
-
// Stop AFTER the deposit is counted/displayed so the final item is never
|
|
1846
|
-
// lost from the totals. A pending signal stops at this same boundary, so a
|
|
1847
|
-
// clean shutdown and a MAX_MB cap unwind through identical, tested code.
|
|
1848
|
-
return !stopRequested && trainedContentBytes < MAX_BYTES;
|
|
1849
|
-
};
|
|
1850
|
-
const cacheWarn = (m) => progress.log(` ${m}`);
|
|
1851
|
-
/** Acquire a source file: reuse a cached copy, else download `url` into the
|
|
1852
|
-
* cache under `destName` (atomic, retried, rate-limit-tolerant, shows live
|
|
1853
|
-
* byte progress). Returns the local path, or null on a non-abort failure
|
|
1854
|
-
* (logged). `label` names the file in the panel/log. */
|
|
1855
|
-
const acquire = async (url, destName, label) => {
|
|
1856
|
-
const dest = join(CACHE_DIR, destName);
|
|
1857
|
-
if (existsSync(dest)) {
|
|
1858
|
-
progress.log(` ${GRN}✓${R} ${label} ${DIM}(cached)${R}`);
|
|
1859
|
-
return dest;
|
|
1860
|
-
}
|
|
1861
|
-
let size = 0;
|
|
1862
|
-
try {
|
|
1863
|
-
size = await headSize(url);
|
|
1864
|
-
} catch { /* unknown — proceed without a cache-room reservation */ }
|
|
1865
|
-
state.activity = "download";
|
|
1866
|
-
state.filePath = label;
|
|
1867
|
-
state.fileSize = size;
|
|
1868
|
-
const slot = { done: 0, total: size, t0: Date.now() };
|
|
1869
|
-
dlSlot = slot;
|
|
1870
|
-
tick(true);
|
|
1871
|
-
try {
|
|
1872
|
-
await ensureCacheRoom(size, cacheWarn);
|
|
1873
|
-
slot.t0 = Date.now();
|
|
1874
|
-
await downloadFile(
|
|
1875
|
-
url,
|
|
1876
|
-
dest,
|
|
1877
|
-
DOWNLOAD_TRIES,
|
|
1878
|
-
(n, e) =>
|
|
1879
|
-
progress.log(
|
|
1880
|
-
` ${YEL}⚠${R} ${label} download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`,
|
|
1881
|
-
),
|
|
1882
|
-
(done, total) => {
|
|
1883
|
-
slot.done = done;
|
|
1884
|
-
if (total > 0) {
|
|
1885
|
-
slot.total = total;
|
|
1886
|
-
}
|
|
1887
|
-
},
|
|
1888
|
-
);
|
|
1889
|
-
} catch (e) {
|
|
1890
|
-
dlSlot = null;
|
|
1891
|
-
if (stopRequested || e?.name === "AbortError") {
|
|
1892
|
-
return null;
|
|
1893
|
-
}
|
|
1894
|
-
progress.log(` ${RED}✗${R} ${label} download failed: ${e.message}`);
|
|
1895
|
-
try {
|
|
1896
|
-
unlinkSync(dest);
|
|
1897
|
-
} catch { /* best effort */ }
|
|
1898
|
-
return null;
|
|
1899
|
-
}
|
|
1900
|
-
dlSlot = null;
|
|
1901
|
-
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
1902
|
-
const sz = statSync(dest).size;
|
|
1903
|
-
progress.log(
|
|
1904
|
-
` ${CYAN}⬇${R} ${label} ${bytes(sz)} ${DIM}${dur(dlS)} @ ${
|
|
1905
|
-
bytes(sz / dlS)
|
|
1906
|
-
}/s${R}`,
|
|
1907
|
-
);
|
|
1908
|
-
return dest;
|
|
1909
|
-
};
|
|
1910
|
-
// ── §10a SmolSent stage (the FIRST stage) ──
|
|
1911
|
-
//
|
|
1912
|
-
// Downloads each SmolSent per-pair JSONL file and streams its lines. Resume is
|
|
1913
|
-
// per-file: a fully-consumed file is recorded in completedFiles
|
|
1914
|
-
// ("smolsent::<name>"); an interrupted file is re-streamed from the top on
|
|
1915
|
-
// resume (re-deposition is idempotent). LOCAL_PATH may hold pre-downloaded
|
|
1916
|
-
// smolsent *.jsonl files.
|
|
1917
|
-
const smolToItems = (row) => {
|
|
1918
|
-
const r = toSmolSentRow(row);
|
|
1919
|
-
return r ? smolSentRowToItems(r) : null;
|
|
1920
|
-
};
|
|
1921
|
-
const trainSmolSent = async () => {
|
|
1922
|
-
if (!SMOLSENT) {
|
|
1923
|
-
return;
|
|
1924
|
-
}
|
|
1925
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
// Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
|
|
1929
|
-
let files;
|
|
1930
|
-
if (LOCAL_PATH) {
|
|
1931
|
-
files = readdirSync(LOCAL_PATH)
|
|
1932
|
-
.filter((f) => /\.jsonl$/i.test(f))
|
|
1933
|
-
.sort()
|
|
1934
|
-
.map((f) => ({
|
|
1935
|
-
id: `${SMOLSENT_ID}::${f}`,
|
|
1936
|
-
name: f,
|
|
1937
|
-
local: join(LOCAL_PATH, f),
|
|
1938
|
-
}));
|
|
1939
|
-
if (SMOLSENT_PAIRS.length) {
|
|
1940
|
-
const want = new Set(
|
|
1941
|
-
SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")),
|
|
1942
|
-
);
|
|
1943
|
-
files = files.filter((f) => want.has(f.name.replace(/\.jsonl$/i, "")));
|
|
1944
|
-
}
|
|
1945
|
-
} else {
|
|
1946
|
-
let paths;
|
|
1947
|
-
try {
|
|
1948
|
-
paths = await listSmolSentFiles();
|
|
1949
|
-
} catch (e) {
|
|
1950
|
-
if (stopRequested || e?.name === "AbortError") {
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
progress.log(
|
|
1954
|
-
` ${RED}✗${R} SmolSent file listing failed: ${e.message}`,
|
|
1955
|
-
);
|
|
1956
|
-
return;
|
|
1957
|
-
}
|
|
1958
|
-
files = paths.map((path) => ({
|
|
1959
|
-
id: `${SMOLSENT_ID}::${basename(path)}`,
|
|
1960
|
-
name: basename(path),
|
|
1961
|
-
// owner/name and the file path are URL PATH segments — do not encode "/".
|
|
1962
|
-
url:
|
|
1963
|
-
`https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
|
|
1964
|
-
}));
|
|
1965
|
-
}
|
|
1966
|
-
if (files.length === 0) {
|
|
1967
|
-
progress.log(` ${DIM}· no SmolSent files found — skipping${R}`);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
const p = await loadProgress(store);
|
|
1971
|
-
const done = new Set(p.completedFiles);
|
|
1972
|
-
const remaining = files.filter((f) => !done.has(f.id));
|
|
1973
|
-
if (remaining.length === 0) {
|
|
1974
|
-
progress.log(` ${DIM}· SmolSent already trained — skipping${R}`);
|
|
1975
|
-
return;
|
|
1301
|
+
// The vector indices' memory knob (MiB) — each index's SQLite page cache.
|
|
1302
|
+
// The IVF index routes inserts through a RAM-resident pivot table and
|
|
1303
|
+
// appends to chunk blobs, so this cache mostly serves query-time cluster
|
|
1304
|
+
// scans; 256 MiB comfortably covers the probed working set of a trained
|
|
1305
|
+
// store. Override with VECTOR_CACHE_MB (64 is the library default).
|
|
1306
|
+
const VECTOR_CACHE_MB = Math.max(0, Number(env("VECTOR_CACHE_MB", "256")));
|
|
1307
|
+
// Page cache for the MAIN DAG database (node/kid/edge/contain tables).
|
|
1308
|
+
// Training issues millions of content-addressed point probes per session
|
|
1309
|
+
// against a GB-scale file; the library default (64 MiB) is sized for a
|
|
1310
|
+
// small machine — a training box affords more. Override with
|
|
1311
|
+
// SQLITE_CACHE_MB.
|
|
1312
|
+
const SQLITE_CACHE_MB = Math.max(0, Number(env("SQLITE_CACHE_MB", "256")));
|
|
1313
|
+
const store = new SQliteStore({
|
|
1314
|
+
path: DB_PATH,
|
|
1315
|
+
D,
|
|
1316
|
+
vectorCacheMb: VECTOR_CACHE_MB,
|
|
1317
|
+
sqliteCacheMb: SQLITE_CACHE_MB,
|
|
1318
|
+
});
|
|
1319
|
+
// The store IS the model: memories, progress, and metadata all persist in
|
|
1320
|
+
// it, so a resumed run just reopens the same store and continues. Guard
|
|
1321
|
+
// against a changed D/SEED by comparing against what a previous run recorded.
|
|
1322
|
+
const mind = new Mind({ seed: SEED, store });
|
|
1323
|
+
// Pre-fill the vector indices' RAM caches with sequential scans (bounded by
|
|
1324
|
+
// VECTOR_CACHE_MB). A resumed run over a large store otherwise spends its
|
|
1325
|
+
// first minutes warming those caches through random point reads — the
|
|
1326
|
+
// ingest hot path is cache-miss bound until then. Seconds, once, up front.
|
|
1327
|
+
if (VECTOR_CACHE_MB > 0) {
|
|
1328
|
+
const t = Date.now();
|
|
1329
|
+
const warmed = await store.warmVectorCaches();
|
|
1330
|
+
if (warmed > 0) {
|
|
1331
|
+
process.stderr.write(` warmed vector caches: ${num(warmed)} rows in ${dur((Date.now() - t) / 1000)}\n`);
|
|
1332
|
+
}
|
|
1976
1333
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
)
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
1334
|
+
const ci = new CachedIngest(mind);
|
|
1335
|
+
const prevD = await store.getMeta("train.D");
|
|
1336
|
+
const prevSeed = await store.getMeta("train.seed");
|
|
1337
|
+
if ((prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)) {
|
|
1338
|
+
process.stderr.write(`fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
|
|
1339
|
+
`requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
|
|
1340
|
+
`to start fresh.\n`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
await store.setMeta("train.dataset", "SmolSent+Aya+oasst2");
|
|
1344
|
+
await store.setMeta("train.D", String(D));
|
|
1345
|
+
await store.setMeta("train.seed", String(SEED));
|
|
1346
|
+
await store.setMeta("train.createdAt", new Date().toISOString());
|
|
1347
|
+
// ── counters & sampling ──
|
|
1348
|
+
let depositCount = 0;
|
|
1349
|
+
let trainedContentBytes = 0;
|
|
1350
|
+
let bytesSinceCkpt = 0;
|
|
1351
|
+
let checkpointNum = 0;
|
|
1352
|
+
let totalBytesProcessed = 0;
|
|
1353
|
+
let totalCorpusBytes = 0;
|
|
1354
|
+
const langTally = {};
|
|
1355
|
+
const t0 = Date.now();
|
|
1356
|
+
// Reservoir sample: one uniformly-random item from the current window, shown
|
|
1357
|
+
// in the recall box at each checkpoint.
|
|
1358
|
+
let sampleItem = null;
|
|
1359
|
+
let seenInWindow = 0;
|
|
1360
|
+
const sample = (it) => {
|
|
1361
|
+
seenInWindow++;
|
|
1362
|
+
if (Math.random() < 1 / seenInWindow)
|
|
1363
|
+
sampleItem = it;
|
|
1364
|
+
};
|
|
1365
|
+
// ── progress panel ──
|
|
1366
|
+
const progress = new Progress();
|
|
1367
|
+
// Surface rate-limit waits from the low-level fetch retries into the live log,
|
|
1368
|
+
// so a 429 back-off reads as "waiting", never a silent hang or a dropped file.
|
|
1369
|
+
onThrottleWait = (ms, label) => {
|
|
1370
|
+
progress.log(` ${YEL}⏳${R} rate-limited (${label}); waiting ${(ms / 1000).toFixed(1)}s and retrying — not skipping`);
|
|
1371
|
+
};
|
|
1372
|
+
const state = {
|
|
1373
|
+
exampleCount: 0,
|
|
1374
|
+
target: MAX_BYTES,
|
|
1375
|
+
elapsedS: 0,
|
|
1376
|
+
trainedBytes: 0,
|
|
1377
|
+
trainedRate: 0,
|
|
1378
|
+
bytesDone: 0,
|
|
1379
|
+
bytesTotal: 0,
|
|
1380
|
+
bytesRate: 0,
|
|
1381
|
+
fileIndex: 0,
|
|
1382
|
+
fileTotal: 0,
|
|
1383
|
+
filePath: "",
|
|
1384
|
+
fileSize: 0,
|
|
1385
|
+
fileExamples: 0,
|
|
1386
|
+
activity: "idle",
|
|
1387
|
+
dlSpeed: 0,
|
|
1388
|
+
dlDone: 0,
|
|
1389
|
+
dlTotal: 0,
|
|
1390
|
+
storeEntries: 0,
|
|
1391
|
+
cacheBytes: 0,
|
|
1392
|
+
lastSample: null,
|
|
1393
|
+
};
|
|
1394
|
+
// store.size() is async; refresh it on a slow cadence so the hot loop and
|
|
1395
|
+
// the repaint never block on a query.
|
|
1396
|
+
let cachedEntries = 0;
|
|
1397
|
+
let sizeInFlight = false;
|
|
1398
|
+
const refreshSize = () => {
|
|
1399
|
+
if (sizeInFlight)
|
|
1400
|
+
return;
|
|
1401
|
+
sizeInFlight = true;
|
|
1402
|
+
void mind.store.size()
|
|
1403
|
+
.then((n) => (cachedEntries = n))
|
|
1404
|
+
.catch(() => undefined)
|
|
1405
|
+
.finally(() => (sizeInFlight = false));
|
|
1406
|
+
};
|
|
1407
|
+
// Cache size changes only at download/delete boundaries — recompute it
|
|
1408
|
+
// lazily rather than statting the dir on every deposit.
|
|
1409
|
+
let cachedCacheBytes = 0;
|
|
1410
|
+
let lastCacheUpdate = 0;
|
|
1411
|
+
// Live download progress for the panel.
|
|
1412
|
+
let dlSlot = null;
|
|
1413
|
+
// Rolling throughput: a short EMA over wall-clock windows, so the headline
|
|
1414
|
+
// figures reflect CURRENT speed rather than a lifetime average diluted by the
|
|
1415
|
+
// listing and download phases (which train nothing).
|
|
1416
|
+
let rateT = t0;
|
|
1417
|
+
let rateTrained = 0;
|
|
1418
|
+
let rateBytes = 0;
|
|
1419
|
+
const syncState = () => {
|
|
1420
|
+
const now = Date.now();
|
|
1421
|
+
state.exampleCount = depositCount;
|
|
1422
|
+
state.trainedBytes = trainedContentBytes;
|
|
1423
|
+
state.elapsedS = (now - t0) / 1000;
|
|
1424
|
+
state.storeEntries = cachedEntries;
|
|
1425
|
+
state.bytesDone = totalBytesProcessed;
|
|
1426
|
+
state.bytesTotal = totalCorpusBytes;
|
|
1427
|
+
if (dlSlot && state.activity === "download") {
|
|
1428
|
+
state.dlDone = dlSlot.done;
|
|
1429
|
+
state.dlTotal = dlSlot.total;
|
|
1430
|
+
const ds = (now - dlSlot.t0) / 1000;
|
|
1431
|
+
state.dlSpeed = ds > 0.2 ? dlSlot.done / ds : 0;
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
state.dlDone = 0;
|
|
1435
|
+
state.dlTotal = 0;
|
|
1436
|
+
}
|
|
1437
|
+
const dt = (now - rateT) / 1000;
|
|
1438
|
+
if (dt >= 0.5) {
|
|
1439
|
+
const instTrained = (trainedContentBytes - rateTrained) / dt;
|
|
1440
|
+
const instByte = (totalBytesProcessed - rateBytes) / dt;
|
|
1441
|
+
const a = 0.3; // EMA weight on the newest sample
|
|
1442
|
+
state.trainedRate = state.trainedRate === 0
|
|
1443
|
+
? instTrained
|
|
1444
|
+
: state.trainedRate * (1 - a) + instTrained * a;
|
|
1445
|
+
state.bytesRate = state.bytesRate === 0
|
|
1446
|
+
? instByte
|
|
1447
|
+
: state.bytesRate * (1 - a) + instByte * a;
|
|
1448
|
+
rateT = now;
|
|
1449
|
+
rateTrained = trainedContentBytes;
|
|
1450
|
+
rateBytes = totalBytesProcessed;
|
|
1451
|
+
}
|
|
1452
|
+
if (now - lastCacheUpdate > 2000) {
|
|
1453
|
+
cachedCacheBytes = cacheSize();
|
|
1454
|
+
lastCacheUpdate = now;
|
|
1455
|
+
}
|
|
1456
|
+
state.cacheBytes = cachedCacheBytes;
|
|
1457
|
+
};
|
|
1458
|
+
const tick = (force = false) => {
|
|
1459
|
+
syncState();
|
|
1460
|
+
progress.render(state, force);
|
|
1461
|
+
};
|
|
1462
|
+
const paintTimer = setInterval(() => {
|
|
1463
|
+
refreshSize();
|
|
1464
|
+
tick(false);
|
|
1465
|
+
}, PROGRESS_MS);
|
|
1466
|
+
if (typeof paintTimer.unref === "function")
|
|
1467
|
+
paintTimer.unref();
|
|
1468
|
+
// ── keep-alive: the process must never exit on its own mid-training ──
|
|
1469
|
+
// The CPU-bound processing phase (perceive + intern + the batched vector-index
|
|
1470
|
+
// writes) hands control back to the event loop between batches via the store's
|
|
1471
|
+
// yieldToEventLoop(), which parks on an UNREF'd setImmediate so the library
|
|
1472
|
+
// never holds a process open by itself. node:sqlite is synchronous and the
|
|
1473
|
+
// vector index is in-memory, so the store's awaits resolve as microtasks with
|
|
1474
|
+
// no I/O handle, and the paint timer above is unref'd too. That leaves a window
|
|
1475
|
+
// — a batch flush that fires while we're processing an in-memory chunk, not
|
|
1476
|
+
// awaiting a disk read — in which the ONLY pending work is that unref'd
|
|
1477
|
+
// setImmediate and NOTHING is ref'd. Node's rule is to exit when only unref'd
|
|
1478
|
+
// handles remain, WITHOUT running them: the yield's continuation never fires,
|
|
1479
|
+
// main() is abandoned, and the process exits 0 silently mid-file — no error for
|
|
1480
|
+
// the fault-tolerance to catch. This one ref'd (NOT unref'd) timer guarantees a
|
|
1481
|
+
// live handle for the whole run, so the loop can never drain from under a
|
|
1482
|
+
// pending yield. Every real exit is an explicit process.exit() (finish(), the
|
|
1483
|
+
// shutdown watchdog, the second-signal path, the fatal catch), so keeping this
|
|
1484
|
+
// handle alive never delays a genuine shutdown; finish() clears it before that
|
|
1485
|
+
// final exit for tidiness. Same lesson as waitMs above (deliberately un-unref'd).
|
|
1486
|
+
const keepAlive = setInterval(() => { }, 1 << 30);
|
|
1487
|
+
const checkpoint = () => mind.save();
|
|
1488
|
+
/** Run index maintenance: compact (remove garbage) then repair (fill gaps).
|
|
1489
|
+
* Both are idempotent — running twice produces the same result as once.
|
|
1490
|
+
* Compaction frees index space first; repair then adds back every
|
|
1491
|
+
* edge/halo-bearing node whose gist was evicted from the pending cache
|
|
1492
|
+
* before it reached the content index, completing the coverage that
|
|
1493
|
+
* incremental promotion alone cannot guarantee.
|
|
1494
|
+
*
|
|
1495
|
+
* repair runs with minParents = 0, NOT the library default of 2. The
|
|
1496
|
+
* default repairs only structural BRIDGES (≥2 parents), but this
|
|
1497
|
+
* trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
|
|
1498
|
+
* structural parents ("The capital of France is Paris." as the dst of a
|
|
1499
|
+
* Q→A edge is a root of its own tree, contained in nothing). Those are
|
|
1500
|
+
* resonance targets recall depends on — a trained store shipped without
|
|
1501
|
+
* them cannot ground statement-shaped queries against its own answers
|
|
1502
|
+
* (observed: 33 such roots missing after a full curriculum, including
|
|
1503
|
+
* high-traffic conversation replies). minParents = 0 admits every
|
|
1504
|
+
* edge/halo bearer; the candidate set is still corpus-of-experiences-
|
|
1505
|
+
* sized, so the pass stays cheap.
|
|
1506
|
+
*
|
|
1507
|
+
* Logs the number of entries removed/added so a run that silently degrades
|
|
1508
|
+
* (growing compaction count, or repair never recovering anything) is
|
|
1509
|
+
* visible in the training log. */
|
|
1510
|
+
const runIndexMaintenance = async () => {
|
|
1511
|
+
if (!INDEX_MAINTENANCE)
|
|
1512
|
+
return;
|
|
2056
1513
|
try {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
1514
|
+
const removed = await mind.store.compactContentIndex();
|
|
1515
|
+
if (removed > 0) {
|
|
1516
|
+
progress.log(` ${DIM}index compact: removed ${int(removed)} isolated entries${R}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
catch (err) {
|
|
1520
|
+
progress.log(` ${YEL}⚠ index compact failed${R}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const added = await mind.repairContentIndex(0);
|
|
1524
|
+
if (added > 0) {
|
|
1525
|
+
progress.log(` ${GRN}index repair: added ${int(added)} missing resonance targets${R}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
catch (err) {
|
|
1529
|
+
progress.log(` ${YEL}⚠ index repair failed${R}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
// The checkpoint recall is a best-effort diagnostic. It is time-bounded so a
|
|
1533
|
+
// slow/large store can never freeze the deposit loop, and guarded so a still
|
|
1534
|
+
// running recall is never stacked on top of another.
|
|
1535
|
+
let inferBusy = false;
|
|
1536
|
+
const runRecall = async (item, n) => {
|
|
1537
|
+
if (inferBusy)
|
|
1538
|
+
return;
|
|
1539
|
+
inferBusy = true;
|
|
1540
|
+
try {
|
|
1541
|
+
const info = promptOf(item);
|
|
1542
|
+
const r = await withTimeout(mind.respond(info.prompt), INFER_TIMEOUT_MS, "recall");
|
|
1543
|
+
const resp = new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
|
|
1544
|
+
const box = renderInferenceBox(info.prompt, info.expected, resp, info.kind, n);
|
|
1545
|
+
state.lastSample = box;
|
|
1546
|
+
if (!progress.interactive)
|
|
1547
|
+
progress.log(box);
|
|
1548
|
+
tick(true);
|
|
1549
|
+
}
|
|
1550
|
+
catch (err) {
|
|
1551
|
+
progress.log(` ${DIM}· checkpoint #${n} recall skipped: ${err instanceof Error ? err.message : String(err)}${R}`);
|
|
1552
|
+
}
|
|
1553
|
+
finally {
|
|
1554
|
+
inferBusy = false;
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
// ── graceful shutdown (always leaves the store consistent) ──
|
|
1558
|
+
let stopRequested = false;
|
|
1559
|
+
let stopReason = "interrupted";
|
|
1560
|
+
let finishing = false;
|
|
1561
|
+
const finish = async (why) => {
|
|
1562
|
+
if (finishing)
|
|
1563
|
+
return;
|
|
1564
|
+
finishing = true;
|
|
1565
|
+
shutdown.abort(); // unblock any straggling fetch/pipeTo
|
|
1566
|
+
tick(true);
|
|
1567
|
+
await store.setMeta("train.completedAt", new Date().toISOString());
|
|
1568
|
+
await store.setMeta("train.totalDeposits", String(depositCount));
|
|
1569
|
+
await store.setMeta("train.totalTrainedBytes", String(trainedContentBytes));
|
|
1570
|
+
await store.setMeta("train.totalBytes", String(totalBytesProcessed));
|
|
1571
|
+
await store.setMeta("train.totalCorpusBytes", String(totalCorpusBytes));
|
|
2075
1572
|
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2076
|
-
} catch { /* best effort — finish() will retry */ }
|
|
2077
|
-
if (res.stopped) {
|
|
2078
|
-
break; // cap/signal — leave file un-completed for resume
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
};
|
|
2082
|
-
// ── §10b Aya Dataset stage (runs AFTER SmolSent) ──
|
|
2083
|
-
//
|
|
2084
|
-
// Downloads the one train Parquet file and reads it row-group by row-group
|
|
2085
|
-
// (hyparquet + Snappy) — one (inputs → targets) fact per row. Marked complete
|
|
2086
|
-
// only when fully consumed; an interrupted run re-reads from the top on resume
|
|
2087
|
-
// (re-deposition is idempotent). LOCAL_PATH may hold a pre-downloaded *.parquet.
|
|
2088
|
-
const ayaToItems = (row) => {
|
|
2089
|
-
const r = toAyaRow(row);
|
|
2090
|
-
return r ? ayaRowToItems(r) : null;
|
|
2091
|
-
};
|
|
2092
|
-
const trainAya = async () => {
|
|
2093
|
-
if (!AYA) {
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
const p = await loadProgress(store);
|
|
2100
|
-
if (p.completedFiles.includes(AYA_ID)) {
|
|
2101
|
-
progress.log(` ${DIM}· Aya Dataset already trained — skipping${R}`);
|
|
2102
|
-
return;
|
|
2103
|
-
}
|
|
2104
|
-
let path = "", downloaded = false;
|
|
2105
|
-
if (LOCAL_PATH) {
|
|
2106
|
-
const hit = readdirSync(LOCAL_PATH).find((f) =>
|
|
2107
|
-
/aya.*\.parquet$/i.test(f) || /\.parquet$/i.test(f)
|
|
2108
|
-
);
|
|
2109
|
-
if (!hit) {
|
|
2110
|
-
progress.log(
|
|
2111
|
-
` ${DIM}· no Aya *.parquet in ${LOCAL_PATH} — skipping${R}`,
|
|
2112
|
-
);
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
path = join(LOCAL_PATH, hit);
|
|
2116
|
-
} else {
|
|
2117
|
-
const got = await acquire(AYA_URL, "aya_train.parquet", "Aya Dataset");
|
|
2118
|
-
if (!got) {
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
|
-
path = got;
|
|
2122
|
-
downloaded = true;
|
|
2123
|
-
}
|
|
2124
|
-
try {
|
|
2125
|
-
totalCorpusBytes += statSync(path).size;
|
|
2126
|
-
} catch { /* best effort */ }
|
|
2127
|
-
state.fileTotal = 1;
|
|
2128
|
-
state.fileIndex = 1;
|
|
2129
|
-
state.activity = "process";
|
|
2130
|
-
state.filePath = "Aya Dataset";
|
|
2131
|
-
state.fileExamples = 0;
|
|
2132
|
-
tick(true);
|
|
2133
|
-
const p0 = Date.now();
|
|
2134
|
-
let res;
|
|
2135
|
-
try {
|
|
2136
|
-
res = await processParquet(path, ayaToItems, ci, onDeposit, sample);
|
|
2137
|
-
} catch (e) {
|
|
2138
|
-
if (stopRequested || e?.name === "AbortError") {
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
progress.log(` ${RED}✗${R} Aya processing failed: ${e.message}`);
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
langTally["aya"] = (langTally["aya"] ?? 0) + res.examples;
|
|
2145
|
-
progress.log(
|
|
2146
|
-
` ${GRN}✓${R} Aya Dataset ${DIM}[multilingual chat]${R} → ${
|
|
2147
|
-
int(res.examples)
|
|
2148
|
-
} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2149
|
-
(res.skipped
|
|
2150
|
-
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2151
|
-
: "") +
|
|
2152
|
-
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2153
|
-
);
|
|
2154
|
-
if (!res.stopped) {
|
|
2155
|
-
try {
|
|
2156
|
-
totalBytesProcessed += statSync(path).size;
|
|
2157
|
-
} catch { /* best effort */ }
|
|
2158
|
-
if (downloaded) {
|
|
2159
1573
|
try {
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
progress.log(
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
1574
|
+
await runIndexMaintenance();
|
|
1575
|
+
await checkpoint();
|
|
1576
|
+
}
|
|
1577
|
+
catch (err) {
|
|
1578
|
+
process.stderr.write(`\n ${YEL}⚠ final checkpoint failed${R}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1579
|
+
}
|
|
1580
|
+
clearInterval(paintTimer);
|
|
1581
|
+
clearInterval(keepAlive);
|
|
1582
|
+
progress.dispose();
|
|
1583
|
+
const elapsedS = (Date.now() - t0) / 1000;
|
|
1584
|
+
const elapsed = dur(elapsedS);
|
|
1585
|
+
const avgRate = elapsedS > 0 ? trainedContentBytes / elapsedS : 0;
|
|
1586
|
+
const tally = Object.entries(langTally)
|
|
1587
|
+
.sort((a, b) => b[1] - a[1])
|
|
1588
|
+
.map(([k, v]) => `${k}:${int(v)}`)
|
|
1589
|
+
.join(", ");
|
|
1590
|
+
let entries = depositCount;
|
|
1591
|
+
try {
|
|
1592
|
+
entries = await mind.store.size();
|
|
1593
|
+
}
|
|
1594
|
+
catch { /* best effort */ }
|
|
1595
|
+
console.log(`\n${GRN}✓${R} ${why}. ${basename(DB_PATH)}.sqlite: ` +
|
|
1596
|
+
`${int(entries)} entries, ${int(depositCount)} examples, ` +
|
|
1597
|
+
`${bytes(trainedContentBytes)} content learned ` +
|
|
1598
|
+
`${DIM}(${bytes(avgRate)}/s avg)${R}, ` +
|
|
1599
|
+
`${bytes(totalBytesProcessed)} corpus processed, ${elapsed} elapsed.` +
|
|
1600
|
+
(tally ? `\n ${DIM}per language:${R} ${tally}` : ""));
|
|
1601
|
+
try {
|
|
1602
|
+
await store.close();
|
|
1603
|
+
}
|
|
1604
|
+
catch { /* best effort */ }
|
|
1605
|
+
process.exit(0);
|
|
1606
|
+
};
|
|
1607
|
+
const requestStop = (reason) => {
|
|
1608
|
+
if (stopRequested) {
|
|
1609
|
+
process.stderr.write(`\n${YEL}⚠ second signal — exiting now${R}\n`);
|
|
1610
|
+
process.stderr.write(SHOW);
|
|
1611
|
+
process.exit(130);
|
|
1612
|
+
}
|
|
1613
|
+
stopRequested = true;
|
|
1614
|
+
stopReason = reason;
|
|
1615
|
+
shutdown.abort();
|
|
1616
|
+
progress.log(` ${YEL}⏸${R} ${reason} — finishing current item, saving…`);
|
|
1617
|
+
const watchdog = setTimeout(() => {
|
|
1618
|
+
process.stderr.write(`\n${YEL}⚠ shutdown watchdog fired — forcing exit${R}\n`);
|
|
1619
|
+
process.stderr.write(SHOW);
|
|
1620
|
+
process.exit(130);
|
|
1621
|
+
}, 60_000);
|
|
1622
|
+
if (typeof watchdog.unref === "function")
|
|
1623
|
+
watchdog.unref();
|
|
1624
|
+
};
|
|
1625
|
+
process.on("SIGINT", () => requestStop("interrupted"));
|
|
1626
|
+
process.on("SIGTERM", () => requestStop("terminated"));
|
|
1627
|
+
// ── fail-safe: a dropped connection must never kill a long run ──
|
|
1628
|
+
process.on("unhandledRejection", (reason) => {
|
|
1629
|
+
progress.log(` ${YEL}⚠ unhandled rejection${R}: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
1630
|
+
});
|
|
1631
|
+
process.on("uncaughtException", (err) => {
|
|
1632
|
+
const code = err?.code ?? err?.cause?.code;
|
|
1633
|
+
if (err.message === "terminated" || code === "UND_ERR_SOCKET") {
|
|
1634
|
+
progress.log(` ${YEL}⚠ connection error (ignored)${R}: ${err.message}`);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
process.stderr.write(`\n${RED}uncaught exception${R}: ${err.message}\n${err.stack ?? ""}\n`);
|
|
1638
|
+
try {
|
|
1639
|
+
void store.setMeta("train.crashedAt", new Date().toISOString());
|
|
1640
|
+
void store.setMeta("train.crashError", err.message);
|
|
1641
|
+
void store.setMeta("train.totalDeposits", String(depositCount));
|
|
1642
|
+
}
|
|
1643
|
+
catch { /* best effort */ }
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
});
|
|
1646
|
+
// ── per-example callback: gates MAX_MB, drives checkpoints + samples ──
|
|
1647
|
+
const onDeposit = async (contentBytes) => {
|
|
1648
|
+
depositCount++;
|
|
1649
|
+
trainedContentBytes += contentBytes;
|
|
1650
|
+
bytesSinceCkpt += contentBytes;
|
|
1651
|
+
state.fileExamples++;
|
|
1652
|
+
if (bytesSinceCkpt >= CHECKPOINT_BYTES) {
|
|
1653
|
+
bytesSinceCkpt %= CHECKPOINT_BYTES;
|
|
1654
|
+
const n = ++checkpointNum;
|
|
1655
|
+
const item = sampleItem;
|
|
1656
|
+
sampleItem = null;
|
|
1657
|
+
seenInWindow = 0;
|
|
1658
|
+
if (item)
|
|
1659
|
+
await runRecall(item, n);
|
|
1660
|
+
try {
|
|
1661
|
+
await runIndexMaintenance();
|
|
1662
|
+
await checkpoint();
|
|
1663
|
+
}
|
|
1664
|
+
catch (err) {
|
|
1665
|
+
progress.log(` ${YEL}⚠ checkpoint failed${R}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1666
|
+
}
|
|
1667
|
+
tick(true);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
tick();
|
|
1671
|
+
}
|
|
1672
|
+
// Stop AFTER the deposit is counted/displayed so the final item is never
|
|
1673
|
+
// lost from the totals. A pending signal stops at this same boundary, so a
|
|
1674
|
+
// clean shutdown and a MAX_MB cap unwind through identical, tested code.
|
|
1675
|
+
return !stopRequested && trainedContentBytes < MAX_BYTES;
|
|
1676
|
+
};
|
|
1677
|
+
const cacheWarn = (m) => progress.log(` ${m}`);
|
|
1678
|
+
/** Acquire a source file: reuse a cached copy, else download `url` into the
|
|
1679
|
+
* cache under `destName` (atomic, retried, rate-limit-tolerant, shows live
|
|
1680
|
+
* byte progress). Returns the local path, or null on a non-abort failure
|
|
1681
|
+
* (logged). `label` names the file in the panel/log. */
|
|
1682
|
+
const acquire = async (url, destName, label) => {
|
|
1683
|
+
const dest = join(CACHE_DIR, destName);
|
|
1684
|
+
if (existsSync(dest)) {
|
|
1685
|
+
progress.log(` ${GRN}✓${R} ${label} ${DIM}(cached)${R}`);
|
|
1686
|
+
return dest;
|
|
1687
|
+
}
|
|
2214
1688
|
let size = 0;
|
|
2215
1689
|
try {
|
|
2216
|
-
|
|
2217
|
-
}
|
|
1690
|
+
size = await headSize(url);
|
|
1691
|
+
}
|
|
1692
|
+
catch { /* unknown — proceed without a cache-room reservation */ }
|
|
2218
1693
|
state.activity = "download";
|
|
2219
|
-
state.filePath =
|
|
1694
|
+
state.filePath = label;
|
|
2220
1695
|
state.fileSize = size;
|
|
2221
1696
|
const slot = { done: 0, total: size, t0: Date.now() };
|
|
2222
1697
|
dlSlot = slot;
|
|
2223
1698
|
tick(true);
|
|
2224
1699
|
try {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
(
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
dlSlot = null;
|
|
2244
|
-
if (stopRequested || e?.name === "AbortError") {
|
|
2245
|
-
return;
|
|
2246
|
-
}
|
|
2247
|
-
progress.log(` ${RED}✗${R} oasst2 download failed: ${e.message}`);
|
|
2248
|
-
try {
|
|
2249
|
-
unlinkSync(dest);
|
|
2250
|
-
} catch { /* best effort */ }
|
|
2251
|
-
return;
|
|
1700
|
+
await ensureCacheRoom(size, cacheWarn);
|
|
1701
|
+
slot.t0 = Date.now();
|
|
1702
|
+
await downloadFile(url, dest, DOWNLOAD_TRIES, (n, e) => progress.log(` ${YEL}⚠${R} ${label} download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`), (done, total) => {
|
|
1703
|
+
slot.done = done;
|
|
1704
|
+
if (total > 0)
|
|
1705
|
+
slot.total = total;
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
catch (e) {
|
|
1709
|
+
dlSlot = null;
|
|
1710
|
+
if (stopRequested || e?.name === "AbortError")
|
|
1711
|
+
return null;
|
|
1712
|
+
progress.log(` ${RED}✗${R} ${label} download failed: ${e.message}`);
|
|
1713
|
+
try {
|
|
1714
|
+
unlinkSync(dest);
|
|
1715
|
+
}
|
|
1716
|
+
catch { /* best effort */ }
|
|
1717
|
+
return null;
|
|
2252
1718
|
}
|
|
2253
1719
|
dlSlot = null;
|
|
2254
1720
|
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
2255
1721
|
const sz = statSync(dest).size;
|
|
2256
|
-
progress.log(
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
1722
|
+
progress.log(` ${CYAN}⬇${R} ${label} ${bytes(sz)} ${DIM}${dur(dlS)} @ ${bytes(sz / dlS)}/s${R}`);
|
|
1723
|
+
return dest;
|
|
1724
|
+
};
|
|
1725
|
+
// ── §10a SmolSent stage (the FIRST stage) ──
|
|
1726
|
+
//
|
|
1727
|
+
// Downloads each SmolSent per-pair JSONL file and streams its lines. Resume is
|
|
1728
|
+
// per-file: a fully-consumed file is recorded in completedFiles
|
|
1729
|
+
// ("smolsent::<name>"); an interrupted file is re-streamed from the top on
|
|
1730
|
+
// resume (re-deposition is idempotent). LOCAL_PATH may hold pre-downloaded
|
|
1731
|
+
// smolsent *.jsonl files.
|
|
1732
|
+
const smolToItems = (row) => {
|
|
1733
|
+
const r = toSmolSentRow(row);
|
|
1734
|
+
return r ? smolSentRowToItems(r) : null;
|
|
1735
|
+
};
|
|
1736
|
+
const trainSmolSent = async () => {
|
|
1737
|
+
if (!SMOLSENT)
|
|
1738
|
+
return;
|
|
1739
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1740
|
+
return;
|
|
1741
|
+
// Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
|
|
1742
|
+
let files;
|
|
1743
|
+
if (LOCAL_PATH) {
|
|
1744
|
+
files = readdirSync(LOCAL_PATH)
|
|
1745
|
+
.filter((f) => /\.jsonl$/i.test(f))
|
|
1746
|
+
.sort()
|
|
1747
|
+
.map((f) => ({
|
|
1748
|
+
id: `${SMOLSENT_ID}::${f}`,
|
|
1749
|
+
name: f,
|
|
1750
|
+
local: join(LOCAL_PATH, f),
|
|
1751
|
+
}));
|
|
1752
|
+
if (SMOLSENT_PAIRS.length) {
|
|
1753
|
+
const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
|
|
1754
|
+
files = files.filter((f) => want.has(f.name.replace(/\.jsonl$/i, "")));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
let paths;
|
|
1759
|
+
try {
|
|
1760
|
+
paths = await listSmolSentFiles();
|
|
1761
|
+
}
|
|
1762
|
+
catch (e) {
|
|
1763
|
+
if (stopRequested || e?.name === "AbortError")
|
|
1764
|
+
return;
|
|
1765
|
+
progress.log(` ${RED}✗${R} SmolSent file listing failed: ${e.message}`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
files = paths.map((path) => ({
|
|
1769
|
+
id: `${SMOLSENT_ID}::${basename(path)}`,
|
|
1770
|
+
name: basename(path),
|
|
1771
|
+
// owner/name and the file path are URL PATH segments — do not encode "/".
|
|
1772
|
+
url: `https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
|
|
1773
|
+
}));
|
|
1774
|
+
}
|
|
1775
|
+
if (files.length === 0) {
|
|
1776
|
+
progress.log(` ${DIM}· no SmolSent files found — skipping${R}`);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const p = await loadProgress(store);
|
|
1780
|
+
const done = new Set(p.completedFiles);
|
|
1781
|
+
const remaining = files.filter((f) => !done.has(f.id));
|
|
1782
|
+
if (remaining.length === 0) {
|
|
1783
|
+
progress.log(` ${DIM}· SmolSent already trained — skipping${R}`);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
state.fileTotal = files.length;
|
|
1787
|
+
progress.log(` ${GRN}✓${R} SmolSent: ${remaining.length}/${files.length} translation file(s) to train`);
|
|
1788
|
+
let idx = 0;
|
|
1789
|
+
for (const f of files) {
|
|
1790
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1791
|
+
break;
|
|
1792
|
+
idx++;
|
|
1793
|
+
if (done.has(f.id))
|
|
1794
|
+
continue;
|
|
1795
|
+
// Acquire (download or reuse), then stream the JSONL.
|
|
1796
|
+
let path = f.local ?? "";
|
|
1797
|
+
let downloaded = false;
|
|
1798
|
+
if (!path) {
|
|
1799
|
+
const got = await acquire(f.url, f.id.replace(/[^A-Za-z0-9._-]+/g, "_"), `SmolSent ${f.name}`);
|
|
1800
|
+
if (!got) {
|
|
1801
|
+
if (stopRequested)
|
|
1802
|
+
break;
|
|
1803
|
+
continue; // a single failed file never aborts the stage
|
|
1804
|
+
}
|
|
1805
|
+
path = got;
|
|
1806
|
+
downloaded = true;
|
|
1807
|
+
}
|
|
1808
|
+
// Accumulate known corpus bytes so the progress bar shows a
|
|
1809
|
+
// meaningful ETA — grows as each file's size is discovered.
|
|
1810
|
+
try {
|
|
1811
|
+
totalCorpusBytes += statSync(path).size;
|
|
1812
|
+
}
|
|
1813
|
+
catch { /* best effort */ }
|
|
1814
|
+
state.activity = "process";
|
|
1815
|
+
state.fileIndex = idx;
|
|
1816
|
+
state.filePath = `SmolSent ${f.name}`;
|
|
1817
|
+
state.fileExamples = 0;
|
|
1818
|
+
tick(true);
|
|
1819
|
+
const p0 = Date.now();
|
|
1820
|
+
let res;
|
|
1821
|
+
try {
|
|
1822
|
+
res = await processJsonl(path, smolToItems, ci, onDeposit, sample, MAX_SMOLSENT_CHARS * 4);
|
|
1823
|
+
}
|
|
1824
|
+
catch (e) {
|
|
1825
|
+
if (stopRequested || e?.name === "AbortError")
|
|
1826
|
+
break;
|
|
1827
|
+
progress.log(` ${RED}✗${R} SmolSent ${f.name} parse failed: ${e.message}`);
|
|
1828
|
+
if (downloaded) {
|
|
1829
|
+
try {
|
|
1830
|
+
unlinkSync(path);
|
|
1831
|
+
}
|
|
1832
|
+
catch { /* best effort */ }
|
|
1833
|
+
}
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
langTally["smolsent"] = (langTally["smolsent"] ?? 0) + res.examples;
|
|
1837
|
+
progress.log(` ${GRN}✓${R} ${f.name.replace(/\.jsonl$/i, "")} ${DIM}[translation]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
1838
|
+
(res.skipped
|
|
1839
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
1840
|
+
: "") +
|
|
1841
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
1842
|
+
if (!res.stopped) {
|
|
1843
|
+
try {
|
|
1844
|
+
totalBytesProcessed += statSync(path).size;
|
|
1845
|
+
}
|
|
1846
|
+
catch { /* best effort */ }
|
|
1847
|
+
if (downloaded) {
|
|
1848
|
+
try {
|
|
1849
|
+
unlinkSync(path);
|
|
1850
|
+
}
|
|
1851
|
+
catch { /* best effort */ }
|
|
1852
|
+
}
|
|
1853
|
+
done.add(f.id);
|
|
1854
|
+
p.completedFiles.push(f.id);
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
await saveProgress(store, {
|
|
1858
|
+
completedFiles: p.completedFiles,
|
|
1859
|
+
depositCount,
|
|
1860
|
+
trainedContentBytes,
|
|
1861
|
+
totalBytesProcessed,
|
|
1862
|
+
totalCorpusBytes,
|
|
1863
|
+
});
|
|
1864
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1865
|
+
}
|
|
1866
|
+
catch { /* best effort — finish() will retry */ }
|
|
1867
|
+
if (res.stopped)
|
|
1868
|
+
break; // cap/signal — leave file un-completed for resume
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
// ── §10b Aya Dataset stage (runs AFTER SmolSent) ──
|
|
1872
|
+
//
|
|
1873
|
+
// Downloads the one train Parquet file and reads it row-group by row-group
|
|
1874
|
+
// (hyparquet + Snappy) — one (inputs → targets) fact per row. Marked complete
|
|
1875
|
+
// only when fully consumed; an interrupted run re-reads from the top on resume
|
|
1876
|
+
// (re-deposition is idempotent). LOCAL_PATH may hold a pre-downloaded *.parquet.
|
|
1877
|
+
const ayaToItems = (row) => {
|
|
1878
|
+
const r = toAyaRow(row);
|
|
1879
|
+
return r ? ayaRowToItems(r) : null;
|
|
1880
|
+
};
|
|
1881
|
+
const trainAya = async () => {
|
|
1882
|
+
if (!AYA)
|
|
1883
|
+
return;
|
|
1884
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1885
|
+
return;
|
|
1886
|
+
const p = await loadProgress(store);
|
|
1887
|
+
if (p.completedFiles.includes(AYA_ID)) {
|
|
1888
|
+
progress.log(` ${DIM}· Aya Dataset already trained — skipping${R}`);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
let path = "", downloaded = false;
|
|
1892
|
+
if (LOCAL_PATH) {
|
|
1893
|
+
const hit = readdirSync(LOCAL_PATH).find((f) => /aya.*\.parquet$/i.test(f) || /\.parquet$/i.test(f));
|
|
1894
|
+
if (!hit) {
|
|
1895
|
+
progress.log(` ${DIM}· no Aya *.parquet in ${LOCAL_PATH} — skipping${R}`);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
path = join(LOCAL_PATH, hit);
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
const got = await acquire(AYA_URL, "aya_train.parquet", "Aya Dataset");
|
|
1902
|
+
if (!got)
|
|
1903
|
+
return;
|
|
1904
|
+
path = got;
|
|
1905
|
+
downloaded = true;
|
|
1906
|
+
}
|
|
2304
1907
|
try {
|
|
2305
|
-
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
totalCorpusBytes,
|
|
2317
|
-
});
|
|
2318
|
-
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2319
|
-
} catch { /* best effort — finish() will retry */ }
|
|
2320
|
-
};
|
|
2321
|
-
// ── §10d General-Knowledge stage (runs AFTER oasst2) ──
|
|
2322
|
-
//
|
|
2323
|
-
// Downloads the single JSON-array file (output.json) and deposits each
|
|
2324
|
-
// {Question, Answer} as one fact. Marked complete only when fully consumed.
|
|
2325
|
-
// LOCAL_PATH may hold a pre-downloaded *.json.
|
|
2326
|
-
const genToItems = (row) => {
|
|
2327
|
-
const r = toGenKnowRow(row);
|
|
2328
|
-
return r ? genKnowRowToItems(r) : null;
|
|
2329
|
-
};
|
|
2330
|
-
const trainGenKnow = async () => {
|
|
2331
|
-
if (!GENKNOW) {
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
const p = await loadProgress(store);
|
|
2338
|
-
if (p.completedFiles.includes(GENKNOW_ID)) {
|
|
2339
|
-
progress.log(
|
|
2340
|
-
` ${DIM}· General-Knowledge already trained — skipping${R}`,
|
|
2341
|
-
);
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
let path = "", downloaded = false;
|
|
2345
|
-
if (LOCAL_PATH) {
|
|
2346
|
-
const hit = readdirSync(LOCAL_PATH).find((f) =>
|
|
2347
|
-
/general.*knowledge.*\.json$/i.test(f) || /output\.json$/i.test(f)
|
|
2348
|
-
);
|
|
2349
|
-
if (!hit) {
|
|
2350
|
-
progress.log(
|
|
2351
|
-
` ${DIM}· no General-Knowledge *.json in ${LOCAL_PATH} — skipping${R}`,
|
|
2352
|
-
);
|
|
2353
|
-
return;
|
|
2354
|
-
}
|
|
2355
|
-
path = join(LOCAL_PATH, hit);
|
|
2356
|
-
} else {
|
|
2357
|
-
const got = await acquire(
|
|
2358
|
-
GENKNOW_URL,
|
|
2359
|
-
"general_knowledge.json",
|
|
2360
|
-
"General-Knowledge",
|
|
2361
|
-
);
|
|
2362
|
-
if (!got) {
|
|
2363
|
-
return;
|
|
2364
|
-
}
|
|
2365
|
-
path = got;
|
|
2366
|
-
downloaded = true;
|
|
2367
|
-
}
|
|
2368
|
-
try {
|
|
2369
|
-
totalCorpusBytes += statSync(path).size;
|
|
2370
|
-
} catch { /* best effort */ }
|
|
2371
|
-
state.fileTotal = 1;
|
|
2372
|
-
state.fileIndex = 1;
|
|
2373
|
-
state.activity = "process";
|
|
2374
|
-
state.filePath = "General-Knowledge";
|
|
2375
|
-
state.fileExamples = 0;
|
|
2376
|
-
tick(true);
|
|
2377
|
-
const p0 = Date.now();
|
|
2378
|
-
let res;
|
|
2379
|
-
try {
|
|
2380
|
-
res = await processJsonArray(path, genToItems, ci, onDeposit, sample);
|
|
2381
|
-
} catch (e) {
|
|
2382
|
-
if (stopRequested || e?.name === "AbortError") {
|
|
2383
|
-
return;
|
|
2384
|
-
}
|
|
2385
|
-
progress.log(
|
|
2386
|
-
` ${RED}✗${R} General-Knowledge processing failed: ${e.message}`,
|
|
2387
|
-
);
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
langTally["genknow"] = (langTally["genknow"] ?? 0) + res.examples;
|
|
2391
|
-
progress.log(
|
|
2392
|
-
` ${GRN}✓${R} General-Knowledge ${DIM}[Q&A facts]${R} → ${
|
|
2393
|
-
int(res.examples)
|
|
2394
|
-
} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2395
|
-
(res.skipped
|
|
2396
|
-
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2397
|
-
: "") +
|
|
2398
|
-
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2399
|
-
);
|
|
2400
|
-
if (!res.stopped) {
|
|
2401
|
-
try {
|
|
2402
|
-
totalBytesProcessed += statSync(path).size;
|
|
2403
|
-
} catch { /* best effort */ }
|
|
2404
|
-
if (downloaded) {
|
|
1908
|
+
totalCorpusBytes += statSync(path).size;
|
|
1909
|
+
}
|
|
1910
|
+
catch { /* best effort */ }
|
|
1911
|
+
state.fileTotal = 1;
|
|
1912
|
+
state.fileIndex = 1;
|
|
1913
|
+
state.activity = "process";
|
|
1914
|
+
state.filePath = "Aya Dataset";
|
|
1915
|
+
state.fileExamples = 0;
|
|
1916
|
+
tick(true);
|
|
1917
|
+
const p0 = Date.now();
|
|
1918
|
+
let res;
|
|
2405
1919
|
try {
|
|
2406
|
-
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
1920
|
+
res = await processParquet(path, ayaToItems, ci, onDeposit, sample);
|
|
1921
|
+
}
|
|
1922
|
+
catch (e) {
|
|
1923
|
+
if (stopRequested || e?.name === "AbortError")
|
|
1924
|
+
return;
|
|
1925
|
+
progress.log(` ${RED}✗${R} Aya processing failed: ${e.message}`);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
langTally["aya"] = (langTally["aya"] ?? 0) + res.examples;
|
|
1929
|
+
progress.log(` ${GRN}✓${R} Aya Dataset ${DIM}[multilingual chat]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
1930
|
+
(res.skipped
|
|
1931
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
1932
|
+
: "") +
|
|
1933
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
1934
|
+
if (!res.stopped) {
|
|
1935
|
+
try {
|
|
1936
|
+
totalBytesProcessed += statSync(path).size;
|
|
1937
|
+
}
|
|
1938
|
+
catch { /* best effort */ }
|
|
1939
|
+
if (downloaded) {
|
|
1940
|
+
try {
|
|
1941
|
+
unlinkSync(path);
|
|
1942
|
+
}
|
|
1943
|
+
catch { /* best effort */ }
|
|
1944
|
+
}
|
|
1945
|
+
p.completedFiles.push(AYA_ID);
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
await saveProgress(store, {
|
|
1949
|
+
completedFiles: p.completedFiles,
|
|
1950
|
+
depositCount,
|
|
1951
|
+
trainedContentBytes,
|
|
1952
|
+
totalBytesProcessed,
|
|
1953
|
+
totalCorpusBytes,
|
|
1954
|
+
});
|
|
1955
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1956
|
+
}
|
|
1957
|
+
catch { /* best effort — finish() will retry */ }
|
|
1958
|
+
};
|
|
1959
|
+
// ── §10c oasst2 stage (multi-turn conversations; runs AFTER Aya; both modes) ──
|
|
1960
|
+
//
|
|
1961
|
+
// Resolves the source (a local *trees*.jsonl.gz in
|
|
1962
|
+
// LOCAL_PATH, else the gzip downloaded to the cache), streams it, and marks
|
|
1963
|
+
// OASST_ID complete only when fully consumed (an interrupted run re-streams
|
|
1964
|
+
// from the top; re-deposition is idempotent). Only multi-turn conversations
|
|
1965
|
+
// are deposited — single Q→A trees are skipped inside processOasst.
|
|
1966
|
+
const trainOasst = async () => {
|
|
1967
|
+
if (!OASST)
|
|
1968
|
+
return;
|
|
1969
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1970
|
+
return;
|
|
1971
|
+
const p = await loadProgress(store);
|
|
1972
|
+
if (p.completedFiles.includes(OASST_ID)) {
|
|
1973
|
+
progress.log(` ${DIM}· oasst2 already trained — skipping${R}`);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
let gzPath = "";
|
|
1977
|
+
let downloaded = false;
|
|
1978
|
+
if (LOCAL_PATH) {
|
|
1979
|
+
const hit = readdirSync(LOCAL_PATH).find((f) => /oasst.*trees.*\.jsonl\.gz$/i.test(f) || /oasst.*\.jsonl\.gz$/i.test(f));
|
|
1980
|
+
if (!hit) {
|
|
1981
|
+
progress.log(` ${DIM}· no oasst2 *trees*.jsonl.gz in ${LOCAL_PATH} — skipping${R}`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
gzPath = join(LOCAL_PATH, hit);
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
const dest = join(CACHE_DIR, "oasst2_ready.trees.jsonl.gz");
|
|
1988
|
+
if (existsSync(dest)) {
|
|
1989
|
+
gzPath = dest; // reuse a copy left by a previous interrupted run
|
|
1990
|
+
progress.log(` ${GRN}✓${R} oasst2 trees ${DIM}(cached)${R}`);
|
|
1991
|
+
}
|
|
1992
|
+
else {
|
|
1993
|
+
let size = 0;
|
|
1994
|
+
try {
|
|
1995
|
+
size = await headSize(OASST_URL);
|
|
1996
|
+
}
|
|
1997
|
+
catch { /* unknown — proceed without a cache-room reservation */ }
|
|
1998
|
+
state.activity = "download";
|
|
1999
|
+
state.filePath = "oasst2 trees";
|
|
2000
|
+
state.fileSize = size;
|
|
2001
|
+
const slot = { done: 0, total: size, t0: Date.now() };
|
|
2002
|
+
dlSlot = slot;
|
|
2003
|
+
tick(true);
|
|
2004
|
+
try {
|
|
2005
|
+
await ensureCacheRoom(size, cacheWarn);
|
|
2006
|
+
slot.t0 = Date.now();
|
|
2007
|
+
await downloadFile(OASST_URL, dest, DOWNLOAD_TRIES, (n, e) => progress.log(` ${YEL}⚠${R} oasst2 download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`), (done, total) => {
|
|
2008
|
+
slot.done = done;
|
|
2009
|
+
if (total > 0)
|
|
2010
|
+
slot.total = total;
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
catch (e) {
|
|
2014
|
+
dlSlot = null;
|
|
2015
|
+
if (stopRequested || e?.name === "AbortError")
|
|
2016
|
+
return;
|
|
2017
|
+
progress.log(` ${RED}✗${R} oasst2 download failed: ${e.message}`);
|
|
2018
|
+
try {
|
|
2019
|
+
unlinkSync(dest);
|
|
2020
|
+
}
|
|
2021
|
+
catch { /* best effort */ }
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
dlSlot = null;
|
|
2025
|
+
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
2026
|
+
const sz = statSync(dest).size;
|
|
2027
|
+
progress.log(` ${CYAN}⬇${R} oasst2 trees ${bytes(sz)} ` +
|
|
2028
|
+
`${DIM}${dur(dlS)} @ ${bytes(sz / dlS)}/s${R}`);
|
|
2029
|
+
gzPath = dest;
|
|
2030
|
+
downloaded = true;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
totalCorpusBytes += statSync(gzPath).size;
|
|
2035
|
+
}
|
|
2036
|
+
catch { /* best effort */ }
|
|
2037
|
+
state.fileTotal = 1;
|
|
2038
|
+
state.fileIndex = 1;
|
|
2039
|
+
// Stream the trees.
|
|
2040
|
+
state.activity = "process";
|
|
2041
|
+
state.filePath = "oasst2 (multi-turn)";
|
|
2042
|
+
state.fileExamples = 0;
|
|
2043
|
+
tick(true);
|
|
2044
|
+
const p0 = Date.now();
|
|
2045
|
+
let result;
|
|
2046
|
+
try {
|
|
2047
|
+
result = await processOasst(gzPath, ci, onDeposit, sample);
|
|
2048
|
+
}
|
|
2049
|
+
catch (e) {
|
|
2050
|
+
if (stopRequested || e?.name === "AbortError")
|
|
2051
|
+
return;
|
|
2052
|
+
progress.log(` ${RED}✗${R} oasst2 processing failed: ${e.message}`);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
const { examples, stopped, skipped, multi } = result;
|
|
2056
|
+
langTally["oasst2"] = (langTally["oasst2"] ?? 0) + examples;
|
|
2057
|
+
progress.log(` ${GRN}✓${R} oasst2 ${DIM}[multi-turn chat]${R} → ${int(examples)} examples from ${int(multi)} conversation(s) ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2058
|
+
(skipped
|
|
2059
|
+
? ` ${YEL}· ${int(skipped)} malformed line(s) skipped${R}`
|
|
2060
|
+
: "") +
|
|
2061
|
+
(stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
2062
|
+
// Only mark complete (and reclaim the cache) when fully consumed.
|
|
2063
|
+
if (!stopped) {
|
|
2064
|
+
try {
|
|
2065
|
+
totalBytesProcessed += statSync(gzPath).size;
|
|
2066
|
+
}
|
|
2067
|
+
catch { /* best effort */ }
|
|
2068
|
+
if (downloaded) {
|
|
2069
|
+
try {
|
|
2070
|
+
unlinkSync(gzPath);
|
|
2071
|
+
}
|
|
2072
|
+
catch { /* best effort */ }
|
|
2073
|
+
}
|
|
2074
|
+
p.completedFiles.push(OASST_ID);
|
|
2075
|
+
}
|
|
2076
|
+
try {
|
|
2077
|
+
await saveProgress(store, {
|
|
2078
|
+
completedFiles: p.completedFiles,
|
|
2079
|
+
depositCount,
|
|
2080
|
+
trainedContentBytes,
|
|
2081
|
+
totalBytesProcessed,
|
|
2082
|
+
totalCorpusBytes,
|
|
2083
|
+
});
|
|
2084
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2085
|
+
}
|
|
2086
|
+
catch { /* best effort — finish() will retry */ }
|
|
2087
|
+
};
|
|
2088
|
+
// ── §10d General-Knowledge stage (runs AFTER oasst2) ──
|
|
2089
|
+
//
|
|
2090
|
+
// Downloads the single JSON-array file (output.json) and deposits each
|
|
2091
|
+
// {Question, Answer} as one fact. Marked complete only when fully consumed.
|
|
2092
|
+
// LOCAL_PATH may hold a pre-downloaded *.json.
|
|
2093
|
+
const genToItems = (row) => {
|
|
2094
|
+
const r = toGenKnowRow(row);
|
|
2095
|
+
return r ? genKnowRowToItems(r) : null;
|
|
2096
|
+
};
|
|
2097
|
+
const trainGenKnow = async () => {
|
|
2098
|
+
if (!GENKNOW)
|
|
2099
|
+
return;
|
|
2100
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
2101
|
+
return;
|
|
2102
|
+
const p = await loadProgress(store);
|
|
2103
|
+
if (p.completedFiles.includes(GENKNOW_ID)) {
|
|
2104
|
+
progress.log(` ${DIM}· General-Knowledge already trained — skipping${R}`);
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
let path = "", downloaded = false;
|
|
2108
|
+
if (LOCAL_PATH) {
|
|
2109
|
+
const hit = readdirSync(LOCAL_PATH).find((f) => /general.*knowledge.*\.json$/i.test(f) || /output\.json$/i.test(f));
|
|
2110
|
+
if (!hit) {
|
|
2111
|
+
progress.log(` ${DIM}· no General-Knowledge *.json in ${LOCAL_PATH} — skipping${R}`);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
path = join(LOCAL_PATH, hit);
|
|
2115
|
+
}
|
|
2116
|
+
else {
|
|
2117
|
+
const got = await acquire(GENKNOW_URL, "general_knowledge.json", "General-Knowledge");
|
|
2118
|
+
if (!got)
|
|
2119
|
+
return;
|
|
2120
|
+
path = got;
|
|
2121
|
+
downloaded = true;
|
|
2122
|
+
}
|
|
2123
|
+
try {
|
|
2124
|
+
totalCorpusBytes += statSync(path).size;
|
|
2125
|
+
}
|
|
2126
|
+
catch { /* best effort */ }
|
|
2127
|
+
state.fileTotal = 1;
|
|
2128
|
+
state.fileIndex = 1;
|
|
2129
|
+
state.activity = "process";
|
|
2130
|
+
state.filePath = "General-Knowledge";
|
|
2131
|
+
state.fileExamples = 0;
|
|
2132
|
+
tick(true);
|
|
2133
|
+
const p0 = Date.now();
|
|
2134
|
+
let res;
|
|
2135
|
+
try {
|
|
2136
|
+
res = await processJsonArray(path, genToItems, ci, onDeposit, sample);
|
|
2137
|
+
}
|
|
2138
|
+
catch (e) {
|
|
2139
|
+
if (stopRequested || e?.name === "AbortError")
|
|
2140
|
+
return;
|
|
2141
|
+
progress.log(` ${RED}✗${R} General-Knowledge processing failed: ${e.message}`);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
langTally["genknow"] = (langTally["genknow"] ?? 0) + res.examples;
|
|
2145
|
+
progress.log(` ${GRN}✓${R} General-Knowledge ${DIM}[Q&A facts]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2146
|
+
(res.skipped
|
|
2147
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2148
|
+
: "") +
|
|
2149
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
2150
|
+
if (!res.stopped) {
|
|
2151
|
+
try {
|
|
2152
|
+
totalBytesProcessed += statSync(path).size;
|
|
2153
|
+
}
|
|
2154
|
+
catch { /* best effort */ }
|
|
2155
|
+
if (downloaded) {
|
|
2156
|
+
try {
|
|
2157
|
+
unlinkSync(path);
|
|
2158
|
+
}
|
|
2159
|
+
catch { /* best effort */ }
|
|
2160
|
+
}
|
|
2161
|
+
p.completedFiles.push(GENKNOW_ID);
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
await saveProgress(store, {
|
|
2165
|
+
completedFiles: p.completedFiles,
|
|
2166
|
+
depositCount,
|
|
2167
|
+
trainedContentBytes,
|
|
2168
|
+
totalBytesProcessed,
|
|
2169
|
+
totalCorpusBytes,
|
|
2170
|
+
});
|
|
2171
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2172
|
+
}
|
|
2173
|
+
catch { /* best effort — finish() will retry */ }
|
|
2174
|
+
};
|
|
2175
|
+
// ── §10 Train the curriculum (resume-aware; one store records all stages) ──
|
|
2176
|
+
//
|
|
2177
|
+
// Every source is paged from an HTTP API (SmolSent, Aya) or a single
|
|
2178
|
+
// downloaded file (oasst2) — there is no per-file ZIP loop. Each stage closure
|
|
2179
|
+
// reads the authoritative completed-set from the store, skips itself when
|
|
2180
|
+
// already done, and persists its own progress, so the whole curriculum resumes
|
|
2181
|
+
// from the store alone. LOCAL_PATH lets oasst2 read a local *.jsonl.gz.
|
|
2182
|
+
tick(true);
|
|
2183
|
+
// ── resume — restore counters and the per-source tally from the store ──
|
|
2184
|
+
const prog = await loadProgress(store);
|
|
2185
|
+
depositCount = prog.depositCount;
|
|
2186
|
+
trainedContentBytes = prog.trainedContentBytes;
|
|
2187
|
+
totalBytesProcessed = prog.totalBytesProcessed;
|
|
2188
|
+
totalCorpusBytes = prog.totalCorpusBytes;
|
|
2189
|
+
rateTrained = trainedContentBytes;
|
|
2190
|
+
rateBytes = totalBytesProcessed;
|
|
2411
2191
|
try {
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
};
|
|
2422
|
-
// ── §10 Train the curriculum (resume-aware; one store records all stages) ──
|
|
2423
|
-
//
|
|
2424
|
-
// Every source is paged from an HTTP API (SmolSent, Aya) or a single
|
|
2425
|
-
// downloaded file (oasst2) — there is no per-file ZIP loop. Each stage closure
|
|
2426
|
-
// reads the authoritative completed-set from the store, skips itself when
|
|
2427
|
-
// already done, and persists its own progress, so the whole curriculum resumes
|
|
2428
|
-
// from the store alone. LOCAL_PATH lets oasst2 read a local *.jsonl.gz.
|
|
2429
|
-
tick(true);
|
|
2430
|
-
// ── resume — restore counters and the per-source tally from the store ──
|
|
2431
|
-
const prog = await loadProgress(store);
|
|
2432
|
-
depositCount = prog.depositCount;
|
|
2433
|
-
trainedContentBytes = prog.trainedContentBytes;
|
|
2434
|
-
totalBytesProcessed = prog.totalBytesProcessed;
|
|
2435
|
-
totalCorpusBytes = prog.totalCorpusBytes;
|
|
2436
|
-
rateTrained = trainedContentBytes;
|
|
2437
|
-
rateBytes = totalBytesProcessed;
|
|
2438
|
-
try {
|
|
2439
|
-
const t = await store.getMeta("train.langTally");
|
|
2440
|
-
if (t) {
|
|
2441
|
-
const parsed = JSON.parse(t);
|
|
2442
|
-
if (parsed && typeof parsed === "object") {
|
|
2443
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
2444
|
-
langTally[k] = Number(v) || 0;
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2192
|
+
const t = await store.getMeta("train.langTally");
|
|
2193
|
+
if (t) {
|
|
2194
|
+
const parsed = JSON.parse(t);
|
|
2195
|
+
if (parsed && typeof parsed === "object") {
|
|
2196
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
2197
|
+
langTally[k] = Number(v) || 0;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2447
2201
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
)
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
await trainOasst();
|
|
2466
|
-
}
|
|
2467
|
-
if (!stopRequested) {
|
|
2468
|
-
await trainGenKnow();
|
|
2469
|
-
}
|
|
2470
|
-
await finish(stopRequested ? stopReason : "done");
|
|
2202
|
+
catch { /* fresh tally */ }
|
|
2203
|
+
if (prog.completedFiles.length > 0) {
|
|
2204
|
+
progress.log(` ${CYAN}↻${R} resuming: ${prog.completedFiles.length} stage-unit(s) done, ` +
|
|
2205
|
+
`${int(depositCount)} examples, ${bytes(trainedContentBytes)} learned`);
|
|
2206
|
+
}
|
|
2207
|
+
// Stage 1 (SmolSent translation facts), 2 (Aya multilingual chat), 3 (oasst2
|
|
2208
|
+
// multi-turn), 4 (General-Knowledge Q&A facts). Each is skipped on a resume
|
|
2209
|
+
// that already finished it.
|
|
2210
|
+
if (!stopRequested)
|
|
2211
|
+
await trainSmolSent();
|
|
2212
|
+
if (!stopRequested)
|
|
2213
|
+
await trainAya();
|
|
2214
|
+
if (!stopRequested)
|
|
2215
|
+
await trainOasst();
|
|
2216
|
+
if (!stopRequested)
|
|
2217
|
+
await trainGenKnow();
|
|
2218
|
+
await finish(stopRequested ? stopReason : "done");
|
|
2471
2219
|
}
|
|
2472
2220
|
// ═══════════════════════════════════════════════════════════════════════
|
|
2473
2221
|
// §11 Entry point — only run when invoked directly, so importing the parser
|
|
2474
2222
|
// functions above (e.g. for tests) never starts training.
|
|
2475
2223
|
// ═══════════════════════════════════════════════════════════════════════
|
|
2476
2224
|
const isMain = import.meta.url === `file://${process.argv[1]}` ||
|
|
2477
|
-
|
|
2225
|
+
process.argv[1]?.endsWith("train_base.js");
|
|
2478
2226
|
if (isMain) {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2227
|
+
main().catch((e) => {
|
|
2228
|
+
process.stderr.write(SHOW);
|
|
2229
|
+
console.error(`\n${RED}fatal:${R}`, e);
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
});
|
|
2484
2232
|
}
|