@hviana/sema 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/AGENTS.md +43 -4
  2. package/DATASETS.md +160 -0
  3. package/README.md +12 -0
  4. package/dist/example/train_base/cache.d.ts +35 -0
  5. package/dist/example/train_base/cache.js +211 -0
  6. package/dist/example/train_base/config.d.ts +21 -0
  7. package/dist/example/train_base/config.js +94 -0
  8. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  9. package/dist/example/train_base/corpora/aya.js +76 -0
  10. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  11. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  12. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  13. package/dist/example/train_base/corpora/genknow.js +83 -0
  14. package/dist/example/train_base/corpora/index.d.ts +29 -0
  15. package/dist/example/train_base/corpora/index.js +81 -0
  16. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  17. package/dist/example/train_base/corpora/massive.js +98 -0
  18. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  19. package/dist/example/train_base/corpora/oasst2.js +120 -0
  20. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  21. package/dist/example/train_base/corpora/smolsent.js +156 -0
  22. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  23. package/dist/example/train_base/corpora/soda.js +113 -0
  24. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  25. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  26. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  27. package/dist/example/train_base/corpora/wiki2.js +132 -0
  28. package/dist/example/train_base/corpus.d.ts +88 -0
  29. package/dist/example/train_base/corpus.js +65 -0
  30. package/dist/example/train_base/discovery.d.ts +48 -0
  31. package/dist/example/train_base/discovery.js +143 -0
  32. package/dist/example/train_base/http.d.ts +82 -0
  33. package/dist/example/train_base/http.js +219 -0
  34. package/dist/example/train_base/items.d.ts +46 -0
  35. package/dist/example/train_base/items.js +98 -0
  36. package/dist/example/train_base/main.d.ts +4 -0
  37. package/dist/example/train_base/main.js +207 -0
  38. package/dist/example/train_base/progress.d.ts +34 -0
  39. package/dist/example/train_base/progress.js +114 -0
  40. package/dist/example/train_base/readers.d.ts +125 -0
  41. package/dist/example/train_base/readers.js +391 -0
  42. package/dist/example/train_base/runtime.d.ts +115 -0
  43. package/dist/example/train_base/runtime.js +637 -0
  44. package/dist/example/train_base/stage.d.ts +3 -0
  45. package/dist/example/train_base/stage.js +246 -0
  46. package/dist/example/train_base/ui.d.ts +88 -0
  47. package/dist/example/train_base/ui.js +272 -0
  48. package/dist/src/geometry.d.ts +20 -0
  49. package/dist/src/geometry.js +22 -0
  50. package/dist/src/mind/attention.d.ts +6 -0
  51. package/dist/src/mind/attention.js +44 -4
  52. package/dist/src/mind/learning.js +134 -50
  53. package/dist/src/mind/mechanisms/cast.js +45 -1
  54. package/dist/src/mind/mind.d.ts +7 -2
  55. package/dist/src/mind/mind.js +15 -3
  56. package/dist/src/mind/reasoning.js +59 -5
  57. package/dist/src/mind/recognition.js +29 -3
  58. package/dist/src/mind/traverse.d.ts +16 -0
  59. package/dist/src/mind/traverse.js +18 -0
  60. package/dist/src/store-sqlite.d.ts +4 -0
  61. package/dist/src/store-sqlite.js +47 -0
  62. package/dist/src/store.d.ts +7 -0
  63. package/example/train_base/cache.ts +251 -0
  64. package/example/train_base/config.ts +128 -0
  65. package/example/train_base/corpora/aya.ts +106 -0
  66. package/example/train_base/corpora/converted-parquet.ts +64 -0
  67. package/example/train_base/corpora/genknow.ts +114 -0
  68. package/example/train_base/corpora/index.ts +88 -0
  69. package/example/train_base/corpora/massive.ts +111 -0
  70. package/example/train_base/corpora/oasst2.ts +163 -0
  71. package/example/train_base/corpora/smolsent.ts +203 -0
  72. package/example/train_base/corpora/soda.ts +130 -0
  73. package/example/train_base/corpora/taskmaster.ts +217 -0
  74. package/example/train_base/corpora/wiki2.ts +190 -0
  75. package/example/train_base/corpus.ts +150 -0
  76. package/example/train_base/discovery.ts +203 -0
  77. package/example/train_base/http.ts +284 -0
  78. package/example/train_base/items.ts +118 -0
  79. package/example/train_base/main.ts +240 -0
  80. package/example/train_base/progress.ts +149 -0
  81. package/example/train_base/readers.ts +505 -0
  82. package/example/train_base/runtime.ts +894 -0
  83. package/example/train_base/stage.ts +276 -0
  84. package/example/train_base/ui.ts +333 -0
  85. package/jsr.json +1 -1
  86. package/package.json +2 -4
  87. package/src/geometry.ts +23 -0
  88. package/src/mind/attention.ts +54 -1
  89. package/src/mind/learning.ts +137 -43
  90. package/src/mind/mechanisms/cast.ts +48 -1
  91. package/src/mind/mind.ts +13 -2
  92. package/src/mind/reasoning.ts +64 -5
  93. package/src/mind/recognition.ts +29 -3
  94. package/src/mind/traverse.ts +19 -0
  95. package/src/store-sqlite.ts +53 -0
  96. package/src/store.ts +28 -0
  97. package/test/13-conversation.test.mjs +1 -1
  98. package/test/29-counterfactual.test.mjs +43 -6
  99. package/test/77-company-saturation.test.mjs +302 -0
  100. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  101. package/test/84-composed-answer-honesty.test.mjs +137 -0
  102. package/test/85-answered-directly.test.mjs +126 -0
  103. package/test/86-cast-voices-committed.test.mjs +164 -0
  104. package/test/87-codominant-commitment.test.mjs +250 -0
  105. package/test/88-dependency-footprint.test.mjs +99 -0
  106. package/dist/example/train_base.d.ts +0 -93
  107. package/dist/example/train_base.js +0 -2269
  108. package/example/train_base.ts +0 -2735
@@ -0,0 +1,46 @@
1
+ export interface Episode {
2
+ context: string;
3
+ continuation: string;
4
+ }
5
+ export type TrainingItem = string | Episode;
6
+ export declare const isEpisode: (it: TrainingItem) => it is Episode;
7
+ /** One turn of a dialogue, attributed to a speaker. The speaker is only ever
8
+ * used to decide MERGING (see `mergeSpeakerTurns`); it is never deposited. */
9
+ export interface SpeakerTurn {
10
+ speaker: string;
11
+ text: string;
12
+ }
13
+ /** Build the accumulated-context episodes of a turn sequence: each successive
14
+ * turn is the continuation of ALL the turns before it joined together. This is
15
+ * the same cumulative-context shape a multi-turn conversation deposits, so the
16
+ * store learns to continue a growing context.
17
+ *
18
+ * The "\n" below is a CORPUS choice, not a protocol. oasst2 turns are
19
+ * paragraphs, and reading them back with the newlines kept is how this corpus
20
+ * reads naturally; a different corpus may join with nothing, and
21
+ * test/13-conversation.test.mjs does exactly that. Neither has to match the
22
+ * other, because Sema never scans content for turn boundaries — those are
23
+ * offsets the Conversation API carries beside the bytes (see Mind.addTurn's
24
+ * "ON SEPARATORS" note). The newline here is simply part of the text this
25
+ * store learnt, so anything replaying this corpus feeds it back as part of
26
+ * the turn: `addTurn(conv, "\n" + turnText)`. It is not a convention the
27
+ * engine, the API, or the tests have to agree on. */
28
+ export declare function accumulate(turns: string[]): Episode[];
29
+ /** Collapse consecutive same-speaker turns into one, joining with a space, and
30
+ * return the bare texts in order. A turn with no speaker never merges with its
31
+ * neighbour: an unlabelled row is of unknown origin, and joining two of them
32
+ * would invent a contribution that may span two speakers.
33
+ *
34
+ * Load-bearing for corpora that split one contribution across several indexed
35
+ * utterances (an artifact of the collection UI). Left unmerged, the cumulative
36
+ * walk deposits a turn boundary in the middle of one speaker's contribution
37
+ * and teaches it as a hand-off. Measured share of turns absorbed by merging:
38
+ * TM-1 17.7%, TM-2 11.9%, TM-3 0.8%, TM-4 0.0%. */
39
+ export declare function mergeSpeakerTurns(turns: SpeakerTurn[]): string[];
40
+ /** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
41
+ * repeats so a concept never deposits the same form twice. */
42
+ export declare function refineItems(items: TrainingItem[]): TrainingItem[];
43
+ /** Content size of a training item in UTF-8 bytes — the same quantity the
44
+ * scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
45
+ * context plus the continuation, for a bare experience its own text. */
46
+ export declare const itemBytes: (it: TrainingItem) => number;
@@ -0,0 +1,98 @@
1
+ // train_base/items.ts — the REPRESENTATION core: what a training item is, and
2
+ // the three shapes a corpus datum may take.
3
+ //
4
+ // REPRESENTATION POLICY (one datum → one form; no replication):
5
+ // • FACTS are the default. A datum that is a RELATION (translation pair,
6
+ // question → answer) is emitted as a (context → continuation) edge SEMA
7
+ // points at and, by example across the corpus, generalizes from (cf.
8
+ // example/demo.ts).
9
+ // • EXPERIENCES (bare statements) are used only when a fact is NOT possible —
10
+ // content with no natural relational split.
11
+ // • CUMULATIVE CONTINUOUS CONTEXT is used only when truly necessary — genuine
12
+ // MULTI-TURN dialogue, where a turn follows from the whole conversation so
13
+ // far. The fact stages do NOT synthesize a multi-turn walk, which would just
14
+ // replicate the facts (repetition SEMA avoids).
15
+ //
16
+ // Nothing here reads the environment or touches I/O: these are the pure
17
+ // functions every corpus adapter is built out of.
18
+ export const isEpisode = (it) => typeof it !== "string";
19
+ /** Build the accumulated-context episodes of a turn sequence: each successive
20
+ * turn is the continuation of ALL the turns before it joined together. This is
21
+ * the same cumulative-context shape a multi-turn conversation deposits, so the
22
+ * store learns to continue a growing context.
23
+ *
24
+ * The "\n" below is a CORPUS choice, not a protocol. oasst2 turns are
25
+ * paragraphs, and reading them back with the newlines kept is how this corpus
26
+ * reads naturally; a different corpus may join with nothing, and
27
+ * test/13-conversation.test.mjs does exactly that. Neither has to match the
28
+ * other, because Sema never scans content for turn boundaries — those are
29
+ * offsets the Conversation API carries beside the bytes (see Mind.addTurn's
30
+ * "ON SEPARATORS" note). The newline here is simply part of the text this
31
+ * store learnt, so anything replaying this corpus feeds it back as part of
32
+ * the turn: `addTurn(conv, "\n" + turnText)`. It is not a convention the
33
+ * engine, the API, or the tests have to agree on. */
34
+ export function accumulate(turns) {
35
+ const out = [];
36
+ for (let i = 1; i < turns.length; i++) {
37
+ out.push({ context: turns.slice(0, i).join("\n"), continuation: turns[i] });
38
+ }
39
+ return out;
40
+ }
41
+ /** Collapse consecutive same-speaker turns into one, joining with a space, and
42
+ * return the bare texts in order. A turn with no speaker never merges with its
43
+ * neighbour: an unlabelled row is of unknown origin, and joining two of them
44
+ * would invent a contribution that may span two speakers.
45
+ *
46
+ * Load-bearing for corpora that split one contribution across several indexed
47
+ * utterances (an artifact of the collection UI). Left unmerged, the cumulative
48
+ * walk deposits a turn boundary in the middle of one speaker's contribution
49
+ * and teaches it as a hand-off. Measured share of turns absorbed by merging:
50
+ * TM-1 17.7%, TM-2 11.9%, TM-3 0.8%, TM-4 0.0%. */
51
+ export function mergeSpeakerTurns(turns) {
52
+ const out = [];
53
+ let prev = "";
54
+ for (const t of turns) {
55
+ if (out.length > 0 && t.speaker !== "" && t.speaker === prev) {
56
+ out[out.length - 1] += " " + t.text;
57
+ }
58
+ else {
59
+ out.push(t.text);
60
+ }
61
+ prev = t.speaker;
62
+ }
63
+ return out;
64
+ }
65
+ /** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
66
+ * repeats so a concept never deposits the same form twice. */
67
+ export function refineItems(items) {
68
+ const out = [];
69
+ const seen = new Set();
70
+ for (const it of items) {
71
+ if (!isEpisode(it)) {
72
+ const exp = it.trim();
73
+ const key = "E:" + exp;
74
+ if (exp && !seen.has(key)) {
75
+ seen.add(key);
76
+ out.push(exp);
77
+ }
78
+ continue;
79
+ }
80
+ const ctx = it.context.trim();
81
+ const cont = it.continuation.trim();
82
+ if (!ctx || !cont || ctx === cont)
83
+ continue;
84
+ const key = "P:" + ctx + "\u0000" + cont;
85
+ if (seen.has(key))
86
+ continue;
87
+ seen.add(key);
88
+ out.push({ context: ctx, continuation: cont });
89
+ }
90
+ return out;
91
+ }
92
+ const ENC = new TextEncoder();
93
+ /** Content size of a training item in UTF-8 bytes — the same quantity the
94
+ * scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
95
+ * context plus the continuation, for a bare experience its own text. */
96
+ export const itemBytes = (it) => isEpisode(it)
97
+ ? ENC.encode(it.context).length + ENC.encode(it.continuation).length
98
+ : ENC.encode(it).length;
@@ -0,0 +1,4 @@
1
+ export * from "./items.js";
2
+ export * from "./corpora/index.js";
3
+ export { parquetBatchRows } from "./readers.js";
4
+ export { mergeSpeakerTurns as mergeTaskmasterTurns } from "./items.js";
@@ -0,0 +1,207 @@
1
+ //This file uses the Google SMOL dataset, made available under the CC BY 4.0 license.
2
+ //This file uses Aya and oasst2 datasets, made available under the apache-2.0 license.
3
+ //
4
+ //A trained Sema store retains its training text VERBATIM, so distributing a
5
+ //store distributes these corpora and every upstream licence applies to it in
6
+ //full. Read DATASETS.md before adding a corpus here or publishing a store:
7
+ //it carries the per-corpus attribution a distributed store is required to
8
+ //travel with, and the two rules a candidate corpus must pass (no NonCommercial
9
+ //term, no ShareAlike term — checked against what the corpus was BUILT FROM,
10
+ //not merely against the repository's licence tag).
11
+ //This file is a more appropriate training example for Sema.
12
+ //Sema does not learn through repetition;
13
+ //it does not require a massive database.
14
+ //It needs fundamental datasets that teach basic cognitive concepts such as conversation, logic, relationships, behaviors and feelings.
15
+ //The focus is on covering fundamental patterns, not repetition.
16
+ //Tip: ontology-based adapted training datasets could be an interesting path.
17
+ // train_base/main.ts — streaming trainer for the SmolSent + Aya + oasst2 +
18
+ // Taskmaster + 2Wiki + SODA base.
19
+ //
20
+ // Training IS deposition: every source datum is translated into SEMA facts (or,
21
+ // for genuine dialogue, accumulated-context episodes), then stored in one pass.
22
+ // There are no gradients or epochs, and there is no LLM in the loop — the only
23
+ // "model" is the SEMA store itself.
24
+ //
25
+ // Every source here is commercially licensable (cc-by-4.0 / apache-2.0).
26
+ //
27
+ // WHERE THINGS LIVE (this file is the folder's entry point and nothing else):
28
+ //
29
+ // config.ts RUN-level knobs — store, checkpoint cadence, cache ceiling,
30
+ // MAX_MB, the Parquet read budget.
31
+ // items.ts the REPRESENTATION core: what a training item is (fact /
32
+ // experience / accumulated walk) and the policy governing
33
+ // which shape a datum may take.
34
+ // http.ts network policy: wait out throttling, retry what is
35
+ // transient, give up at once on what is not.
36
+ // cache.ts the durable disk cache and the atomic download sink — the
37
+ // sole irreducible Node dependency.
38
+ // readers.ts container formats: newline-delimited JSON (plain or
39
+ // gzipped), JSON arrays, Parquet.
40
+ // discovery.ts where a work-list comes from: an HF repo tree, the
41
+ // auto-converted Parquet branch, a GitHub directory, a local
42
+ // directory.
43
+ // progress.ts the resume record inside the store, and the index passes
44
+ // that keep a checkpoint queryable.
45
+ // ui.ts the live panel, the formatters, the recall box.
46
+ // runtime.ts the RUN: counters, the deposit gate, file acquisition,
47
+ // checkpointing, shutdown.
48
+ // corpus.ts WHAT A CORPUS IS — the contract a corpus declares, and the
49
+ // one rule that turns it into a resume id.
50
+ // stage.ts ONE loop, run once per corpus.
51
+ // corpora/ ONE FILE PER CORPUS: its knobs, its row adapter, its stage
52
+ // descriptor, and the evidence that fixed each default.
53
+ // corpora/index.ts is the curriculum, in order.
54
+ //
55
+ // Adding a corpus is therefore one new file in corpora/ plus its import, its
56
+ // place in CURRICULUM and its re-export in corpora/index.ts — and no change at
57
+ // all to the loop, the readers, or the run.
58
+ //
59
+ // Every source is DOWNLOADED as a file and streamed from disk (never paged
60
+ // row-by-row over an HTTP API — that was slow and rate-limited). Resume is
61
+ // per-file: a fully-consumed file is marked complete; an interrupted one
62
+ // re-reads from the top (re-deposition is idempotent). LOCAL_PATH may hold
63
+ // pre-downloaded files.
64
+ //
65
+ // The store IS the model: memories, training metadata, and the config snapshot
66
+ // all live in {DB_PATH}.sqlite, so a run resumes from the store alone.
67
+ //
68
+ // Built on web standards. All I/O except the durable disk cache uses platform
69
+ // primitives — fetch, WHATWG ReadableStream/WritableStream/TransformStream,
70
+ // DecompressionStream, TextDecoderStream, Blob, AbortController. The sole
71
+ // third-party code is hyparquet (+ its Snappy codec): a DEV dependency of this
72
+ // example, never of the library, and loaded by a dynamic import inside
73
+ // readers.ts the first time a Parquet corpus is read — so a curriculum with no
74
+ // Parquet stage needs it not at all. Consistency guarantees:
75
+ // • Resume from the store alone — completed stage-units, example count,
76
+ // learned-content bytes, and processed-byte total are persisted in
77
+ // {DB_PATH}.sqlite and reloaded.
78
+ // • Atomic cache — a download streams to "<file>.part", is fsync'd, then
79
+ // renamed into place; a file at its final path is, by construction,
80
+ // complete, so an interrupted download can never be mistaken for a cached
81
+ // one.
82
+ // • Bounded cache — a download blocks under the MAX_CACHE_GB ceiling and the
83
+ // fully-processed file is deleted immediately.
84
+ // • Interruptible — Ctrl+C (SIGINT/SIGTERM) aborts in-flight network at once,
85
+ // stops at the next item boundary, writes a final checkpoint, and exits; an
86
+ // un-finished stage-unit is NOT marked complete, so resume re-reads it (re-
87
+ // deposition is idempotent). A second Ctrl+C, or a 60s watchdog, force-exits.
88
+ //
89
+ // Run:
90
+ // npx tsc && node dist/example/train_base/main.js
91
+ // MAX_MB=500 node dist/example/train_base/main.js
92
+ // CHECKPOINT_MB=250 node dist/example/train_base/main.js
93
+ // SMOLSENT_PAIRS=ha_en,zu_en node dist/example/train_base/main.js # a subset of pairs
94
+ // SMOLSENT_DIRECTIONS=both node dist/example/train_base/main.js # also English->foreign
95
+ // SMOLSENT=0 node dist/example/train_base/main.js # skip SmolSent stage
96
+ // AYA=0 node dist/example/train_base/main.js # skip Aya stage
97
+ // OASST=0 node dist/example/train_base/main.js # skip oasst2 stage
98
+ // OASST_MIN_TURNS=6 node dist/example/train_base/main.js # deeper multi-turn only
99
+ // GENKNOW=1 node dist/example/train_base/main.js # General-Knowledge (see DATASETS.md §3.2)
100
+ // PARQUET_BATCH_MB=8 node dist/example/train_base/main.js # smaller Parquet reads on a tight host
101
+ // TASKMASTER=0 node dist/example/train_base/main.js # skip Taskmaster stage
102
+ // TASKMASTER_SETS=TM-3-2020 node dist/example/train_base/main.js # one Taskmaster set
103
+ // WIKI2=0 node dist/example/train_base/main.js # skip 2Wiki triples stage
104
+ // SODA=0 node dist/example/train_base/main.js # skip the SODA stage
105
+ // MASSIVE=1 node dist/example/train_base/main.js # enable MASSIVE (off by default)
106
+ // SODA_MAX_DIALOGS=0 node dist/example/train_base/main.js # lift the SODA budget
107
+ // WIKI2_MAX_ROWS=50000 node dist/example/train_base/main.js # budget the 2Wiki stage
108
+ // LOCAL_PATH=./base node dist/example/train_base/main.js # offline: *.jsonl/.parquet/.jsonl.gz/.json
109
+ // DB_PATH=./data/sema node dist/example/train_base/main.js
110
+ import { CachedIngest, Mind, SQliteStore } from "../../src/index.js";
111
+ import { D, DB_PATH, SEED, SQLITE_CACHE_MB, VECTOR_CACHE_MB, } from "./config.js";
112
+ import { createRuntime } from "./runtime.js";
113
+ import { runStage } from "./stage.js";
114
+ import { sweepPartials } from "./cache.js";
115
+ import { CURRICULUM, enabledLabels, storedLabels } from "./corpora/index.js";
116
+ import { bytes, DIM, dur, num, R, RED, SHOW } from "./ui.js";
117
+ // The parser/representation surface this module used to define itself. Kept
118
+ // exported from here so importing `example/train_base/main.js` still reaches every
119
+ // row adapter (toSmolSentRow, wikiTriplesToItems, …) and the shapes they build.
120
+ export * from "./items.js";
121
+ export * from "./corpora/index.js";
122
+ export { parquetBatchRows } from "./readers.js";
123
+ // The name this helper had while it lived here. It is `mergeSpeakerTurns` now,
124
+ // because SODA merges by the same rule and the Taskmaster-specific name was a
125
+ // lie — but the old name stays reachable so nothing importing it breaks.
126
+ export { mergeSpeakerTurns as mergeTaskmasterTurns } from "./items.js";
127
+ async function main() {
128
+ const store = new SQliteStore({
129
+ path: DB_PATH,
130
+ D,
131
+ vectorCacheMb: VECTOR_CACHE_MB,
132
+ sqliteCacheMb: SQLITE_CACHE_MB,
133
+ });
134
+ // The store IS the model: memories, progress, and metadata all persist in
135
+ // it, so a resumed run just reopens the same store and continues. Guard
136
+ // against a changed D/SEED by comparing against what a previous run recorded.
137
+ const mind = new Mind({ seed: SEED, store });
138
+ // Pre-fill the vector indices' RAM caches with sequential scans (bounded by
139
+ // VECTOR_CACHE_MB). A resumed run over a large store otherwise spends its
140
+ // first minutes warming those caches through random point reads — the
141
+ // ingest hot path is cache-miss bound until then. Seconds, once, up front.
142
+ if (VECTOR_CACHE_MB > 0) {
143
+ const t = Date.now();
144
+ const warmed = await store.warmVectorCaches();
145
+ if (warmed > 0) {
146
+ process.stderr.write(` warmed vector caches: ${num(warmed)} rows in ${dur((Date.now() - t) / 1000)}\n`);
147
+ }
148
+ }
149
+ const ci = new CachedIngest(mind);
150
+ const prevD = await store.getMeta("train.D");
151
+ const prevSeed = await store.getMeta("train.seed");
152
+ if ((prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)) {
153
+ process.stderr.write(`fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
154
+ `requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
155
+ `to start fresh.\n`);
156
+ process.exit(1);
157
+ }
158
+ const dataset = enabledLabels();
159
+ await store.setMeta("train.D", String(D));
160
+ await store.setMeta("train.seed", String(SEED));
161
+ // createdAt is when this STORE was first trained into, so a resume must not
162
+ // overwrite it — the previous behaviour left every store claiming it was
163
+ // created at its most recent resume. When the current run started is a
164
+ // separate, also-useful fact, so it gets its own key rather than displacing
165
+ // this one.
166
+ if ((await store.getMeta("train.createdAt")) === null) {
167
+ await store.setMeta("train.createdAt", new Date().toISOString());
168
+ }
169
+ await store.setMeta("train.startedAt", new Date().toISOString());
170
+ const ctx = createRuntime({ store, mind, ci, title: dataset });
171
+ ctx.tick(true);
172
+ // Reclaim the debris of any download killed mid-transfer. It occupies cache
173
+ // ceiling that nothing else would ever free (see cache.ts sweepPartials).
174
+ const swept = sweepPartials();
175
+ if (swept.files > 0) {
176
+ ctx.progress.log(` ${DIM}· swept ${swept.files} interrupted download(s), ` +
177
+ `${bytes(swept.bytes)} reclaimed${R}`);
178
+ }
179
+ // ── resume — restore counters and the per-source tally from the store ──
180
+ await ctx.restore();
181
+ // What the STORE contains, which is only knowable after the resume: this
182
+ // run's stages plus whatever earlier runs deposited. The panel header above
183
+ // names this RUN's stages, which is a different and also-true fact.
184
+ await store.setMeta("train.dataset", storedLabels(Object.keys(ctx.counters.langTally)));
185
+ // Walk the curriculum. Each stage skips itself on a resume that already
186
+ // finished it, and the walk stops at the first requested stop.
187
+ for (const corpus of CURRICULUM) {
188
+ if (ctx.stopRequested)
189
+ break;
190
+ await runStage(ctx, corpus);
191
+ }
192
+ // Nothing is in flight any more; each stage announces itself as it starts, so
193
+ // this is the one transition that has no next stage to correct it.
194
+ ctx.state.activity = "idle";
195
+ await ctx.finish(ctx.stopRequested ? ctx.stopReason : "done");
196
+ }
197
+ // Only run when invoked directly, so importing the row adapters above (e.g.
198
+ // for a fixture check) never starts training.
199
+ const isMain = import.meta.url === `file://${process.argv[1]}` ||
200
+ process.argv[1]?.endsWith("train_base/main.js");
201
+ if (isMain) {
202
+ main().catch((e) => {
203
+ process.stderr.write(SHOW);
204
+ console.error(`\n${RED}fatal:${R}`, e);
205
+ process.exit(1);
206
+ });
207
+ }
@@ -0,0 +1,34 @@
1
+ import type { Mind, Store } from "../../src/index.js";
2
+ export interface SavedProgress {
3
+ completedFiles: string[];
4
+ depositCount: number;
5
+ trainedContentBytes: number;
6
+ totalBytesProcessed: number;
7
+ totalCorpusBytes: number;
8
+ }
9
+ export declare function loadProgress(store: Store): Promise<SavedProgress>;
10
+ export declare function saveProgress(store: Store, p: SavedProgress): Promise<void>;
11
+ /** Run index maintenance: compact (remove garbage), repair (fill gaps),
12
+ * then refresh the canonical-form index (see below). All three are
13
+ * idempotent — running twice produces the same result as once.
14
+ * Compaction frees index space first; repair then adds back every
15
+ * edge/halo-bearing node whose gist was evicted from the pending cache
16
+ * before it reached the content index, completing the coverage that
17
+ * incremental promotion alone cannot guarantee.
18
+ *
19
+ * repair runs with minParents = 0, NOT the library default of 2. The
20
+ * default repairs only structural BRIDGES (≥2 parents), but this
21
+ * trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
22
+ * structural parents ("The capital of France is Paris." as the dst of a
23
+ * Q→A edge is a root of its own tree, contained in nothing). Those are
24
+ * resonance targets recall depends on — a trained store shipped without
25
+ * them cannot ground statement-shaped queries against its own answers
26
+ * (observed: 33 such roots missing after a full curriculum, including
27
+ * high-traffic conversation replies). minParents = 0 admits every
28
+ * edge/halo bearer; the candidate set is still corpus-of-experiences-
29
+ * sized, so the pass stays cheap.
30
+ *
31
+ * Logs the number of entries removed/added so a run that silently degrades
32
+ * (growing compaction count, or repair never recovering anything) is
33
+ * visible in the training log. */
34
+ export declare function runIndexMaintenance(mind: Mind, log: (msg: string) => void): Promise<void>;
@@ -0,0 +1,114 @@
1
+ // train_base/progress.ts — resume state, and the index passes that keep a
2
+ // checkpointed store queryable.
3
+ //
4
+ // The store IS the model: memories, training metadata and the config snapshot
5
+ // all live in {DB_PATH}.sqlite, so a run resumes from the store alone. The keys
6
+ // below are that resume record, and they are a COMPATIBILITY SURFACE — a store
7
+ // trained by an earlier version must keep resuming, so neither the key names
8
+ // nor the shape of `completedFiles` may drift.
9
+ import { INDEX_MAINTENANCE } from "./config.js";
10
+ import { DIM, GRN, int, R, YEL } from "./ui.js";
11
+ const META_COMPLETED = "train.completedFiles";
12
+ const META_DEPOSITS = "train.depositCount";
13
+ const META_TRAINED_BYTES = "train.trainedContentBytes";
14
+ const META_BYTES = "train.totalBytesProcessed";
15
+ const META_CORPUS_BYTES = "train.totalCorpusBytes";
16
+ export async function loadProgress(store) {
17
+ try {
18
+ const raw = await store.getMeta(META_COMPLETED);
19
+ const deps = await store.getMeta(META_DEPOSITS);
20
+ const b = await store.getMeta(META_BYTES);
21
+ if (raw !== null && deps !== null && b !== null) {
22
+ const completedFiles = JSON.parse(raw);
23
+ if (Array.isArray(completedFiles)) {
24
+ const trained = await store.getMeta(META_TRAINED_BYTES);
25
+ const corpus = await store.getMeta(META_CORPUS_BYTES);
26
+ return {
27
+ completedFiles,
28
+ depositCount: Number(deps) || 0,
29
+ trainedContentBytes: Number(trained) || 0,
30
+ totalBytesProcessed: Number(b) || 0,
31
+ totalCorpusBytes: Number(corpus) || 0,
32
+ };
33
+ }
34
+ }
35
+ }
36
+ catch { /* corrupt/missing — start fresh */ }
37
+ return {
38
+ completedFiles: [],
39
+ depositCount: 0,
40
+ trainedContentBytes: 0,
41
+ totalBytesProcessed: 0,
42
+ totalCorpusBytes: 0,
43
+ };
44
+ }
45
+ export async function saveProgress(store, p) {
46
+ await store.setMeta(META_COMPLETED, JSON.stringify(p.completedFiles));
47
+ await store.setMeta(META_DEPOSITS, String(p.depositCount));
48
+ await store.setMeta(META_TRAINED_BYTES, String(p.trainedContentBytes));
49
+ await store.setMeta(META_BYTES, String(p.totalBytesProcessed));
50
+ await store.setMeta(META_CORPUS_BYTES, String(p.totalCorpusBytes));
51
+ await store.setMeta("train.updatedAt", new Date().toISOString());
52
+ store.commit();
53
+ }
54
+ /** Run index maintenance: compact (remove garbage), repair (fill gaps),
55
+ * then refresh the canonical-form index (see below). All three are
56
+ * idempotent — running twice produces the same result as once.
57
+ * Compaction frees index space first; repair then adds back every
58
+ * edge/halo-bearing node whose gist was evicted from the pending cache
59
+ * before it reached the content index, completing the coverage that
60
+ * incremental promotion alone cannot guarantee.
61
+ *
62
+ * repair runs with minParents = 0, NOT the library default of 2. The
63
+ * default repairs only structural BRIDGES (≥2 parents), but this
64
+ * trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
65
+ * structural parents ("The capital of France is Paris." as the dst of a
66
+ * Q→A edge is a root of its own tree, contained in nothing). Those are
67
+ * resonance targets recall depends on — a trained store shipped without
68
+ * them cannot ground statement-shaped queries against its own answers
69
+ * (observed: 33 such roots missing after a full curriculum, including
70
+ * high-traffic conversation replies). minParents = 0 admits every
71
+ * edge/halo bearer; the candidate set is still corpus-of-experiences-
72
+ * sized, so the pass stays cheap.
73
+ *
74
+ * Logs the number of entries removed/added so a run that silently degrades
75
+ * (growing compaction count, or repair never recovering anything) is
76
+ * visible in the training log. */
77
+ export async function runIndexMaintenance(mind, log) {
78
+ if (!INDEX_MAINTENANCE)
79
+ return;
80
+ try {
81
+ const removed = await mind.store.compactContentIndex();
82
+ if (removed > 0) {
83
+ log(` ${DIM}index compact: removed ${int(removed)} isolated entries${R}`);
84
+ }
85
+ }
86
+ catch (err) {
87
+ log(` ${YEL}⚠ index compact failed${R}: ${err instanceof Error ? err.message : String(err)}`);
88
+ }
89
+ try {
90
+ const added = await mind.repairContentIndex(0);
91
+ if (added > 0) {
92
+ log(` ${GRN}index repair: added ${int(added)} missing resonance targets${R}`);
93
+ }
94
+ }
95
+ catch (err) {
96
+ log(` ${YEL}⚠ index repair failed${R}: ${err instanceof Error ? err.message : String(err)}`);
97
+ }
98
+ // Canonical-form index (src/canon.ts): lets resolution find stored forms
99
+ // across surface variation (case, width, whitespace). Incremental and
100
+ // idempotent by construction — the `canon.upto` meta cursor scans only
101
+ // nodes newer than the last pass, and the (h, id) primary key ignores
102
+ // re-inserted rows — so it composes with the resume model exactly like
103
+ // compact/repair: every checkpoint (and finish) leaves the index
104
+ // covering all content trained so far.
105
+ try {
106
+ const added = await mind.buildCanonIndex();
107
+ if (added > 0) {
108
+ log(` ${GRN}canon index: added ${int(added)} canonical-form entries${R}`);
109
+ }
110
+ }
111
+ catch (err) {
112
+ log(` ${YEL}⚠ canon index build failed${R}: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ }
@@ -0,0 +1,125 @@
1
+ import type { CachedIngest } from "../../src/index.js";
2
+ import { type TrainingItem } from "./items.js";
3
+ /** Turn ONE raw row into deposits, or null/[] when the row carries nothing
4
+ * usable. Pure: no I/O, no counters, no logging. */
5
+ export type RowAdapter = (row: unknown) => TrainingItem[] | null;
6
+ /** What a read produced. `skipped` and `unusable` are deliberately SEPARATE:
7
+ * a line that failed to parse is a defect in the file, while a row the adapter
8
+ * declined is a normal, expected outcome for a corpus being filtered (oasst2
9
+ * drops every single-turn tree by design). Collapsing them — as the two
10
+ * original line readers each did, in opposite directions — makes one of the
11
+ * two log lines a lie. */
12
+ export interface FileResult {
13
+ examples: number;
14
+ rowsUsed: number;
15
+ skipped: number;
16
+ unusable: number;
17
+ stopped: boolean;
18
+ }
19
+ /** Everything a reader needs from the run: where to deposit, what to count,
20
+ * and how to be stopped. */
21
+ export interface ReadContext {
22
+ ci: CachedIngest;
23
+ /** Called once per deposit with its UTF-8 content size. Returns false to
24
+ * stop the read (the MAX_MB cap, or a pending shutdown). */
25
+ onExample: (contentBytes: number) => Promise<boolean>;
26
+ /** Feeds the reservoir behind the checkpoint recall box. */
27
+ sample: (it: TrainingItem) => void;
28
+ signal: AbortSignal;
29
+ /** A stage-level budget. Checked per row and before each Parquet batch is
30
+ * decoded — a budget must STOP the read rather than reject rows: left to
31
+ * reject, a budgeted stage still DECODES every remaining row-group (143,346
32
+ * rows of one 86.7 MB SODA shard) and reports them as "unusable" when
33
+ * nothing was wrong with them, which is a lie in the run log.
34
+ *
35
+ * Measured honestly: on that shard the wall time did NOT improve (2m 35s ->
36
+ * 2m 37s), because a budgeted run is dominated by depositing the rows it DID
37
+ * take, not by scanning past the ones it did not. The win here is a truthful
38
+ * log and the CPU/allocation of ~143k skipped row decodes, not elapsed time.
39
+ * A larger shard past a small budget is where the decode cost would show. */
40
+ shouldStop?: () => boolean;
41
+ /** Rows to SKIP before depositing anything — the position a previous run
42
+ * reached, taken from the durable cursor (see runtime.ts). Resume used to
43
+ * mean "re-read this unit from the top", which was safe but re-deposited
44
+ * everything already stored and counted it a second time; the store then
45
+ * reported up to 77% more examples than it held.
46
+ *
47
+ * Skipping is only sound because the cursor is written in the SAME COMMIT
48
+ * that flushes the deposits it counts, so a row before the cursor is
49
+ * necessarily durable. A skipped row is neither parsed nor counted, so a
50
+ * resumed read's log line describes what THIS read did and nothing else. */
51
+ startRow?: number;
52
+ /** "Row `rows` is FULLY dealt with" — every item it produced is deposited, or
53
+ * it produced none. Called at ROW BOUNDARIES ONLY, and never for a row the
54
+ * read stopped in the middle of.
55
+ *
56
+ * That boundary is the whole point. A checkpoint fires per DEPOSIT, and a row
57
+ * can produce many (2Wiki emits ~5 facts per row, a dialogue one per turn),
58
+ * so a position recorded when a row STARTS would mark it consumed while some
59
+ * of its items were still unwritten — and the resume would skip them. Data
60
+ * loss, silently. Advancing only here means the worst case is re-depositing
61
+ * one row, which is idempotent and counted once. */
62
+ onRowDone?: (rows: number) => void;
63
+ }
64
+ /** A reader: read `filePath`, deposit every row `toItems` accepts. */
65
+ export type Reader = (filePath: string, toItems: RowAdapter, rc: ReadContext) => Promise<FileResult>;
66
+ /** Deposit a row's items: an experience via ingest(text), an episode via
67
+ * ingest(context, continuation). After each, the per-example callback receives
68
+ * the item's UTF-8 content size — the quantity the scaling suite
69
+ * (14-scaling.test.mjs) reports as a constant KB/s — then gates the global
70
+ * example count and checkpointing (returns false to stop). */
71
+ export declare function ingestItems(ci: CachedIngest, items: TrainingItem[], onItem: (contentBytes: number) => Promise<boolean>, sample?: (it: TrainingItem) => void): Promise<boolean>;
72
+ /** Newline-delimited JSON, optionally gzipped.
73
+ *
74
+ * ONE reader serves both the plain JSONL sources and the gzipped oasst2 tree
75
+ * dump: the only difference between them is a `DecompressionStream("gzip")` in
76
+ * the pipeline, and duplicating an 80-line splitter to express that was how
77
+ * the two copies drifted apart in the first place.
78
+ *
79
+ * Lines are split without buffering the whole file OR an unbounded line: a
80
+ * record longer than `maxLineChars` is dropped (counted `skipped`) and the
81
+ * stream continues at the next newline, so a corrupt record can never exhaust
82
+ * memory or abort a good file. */
83
+ export declare const lines: (opts: {
84
+ gzip?: boolean;
85
+ maxLineChars: number;
86
+ }) => Reader;
87
+ /** A whole-file JSON ARRAY of rows. The arrays this reads are small enough
88
+ * (~16 MB) to parse whole; a huge file would be rejected by the cache ceiling
89
+ * long before this. */
90
+ export declare const jsonArray: () => Reader;
91
+ /** How many rows to materialise in one read from a row-group of `rgRows` rows
92
+ * occupying `groupBytes` uncompressed bytes, under a `budgetBytes` target.
93
+ *
94
+ * The group's own footer statistics give the mean row width, so the batch
95
+ * follows the CORPUS's row size rather than the writer's layout: wide rows
96
+ * (SODA carries a whole dialogue per row) batch smaller than narrow ones at
97
+ * the same memory cost. Never exceeds the group — a batch is a subdivision of
98
+ * a group, never a span across two, because `parquetReadObjects` is given an
99
+ * absolute row range and column chunks are per-group. Never returns 0, or the
100
+ * read loop could not advance.
101
+ *
102
+ * A writer that omits `total_byte_size` yields `groupBytes <= 0`; the batch is
103
+ * then the whole group, which is exactly the behaviour this replaced. That
104
+ * fallback is safe for every file we read today (all three report it) and
105
+ * degrades to the old memory profile rather than to a wrong result. */
106
+ export declare function parquetBatchRows(rgRows: number, groupBytes: number, budgetBytes: number): number;
107
+ /** Parquet, read in bounded row batches with hyparquet (+Snappy from
108
+ * hyparquet-compressors) over a web-standard Blob byte source. At most
109
+ * `batchBytes` of source rows are materialised at a time, so neither a
110
+ * multi-hundred-MB file nor a file written as ONE giant row-group loads whole
111
+ * into memory.
112
+ *
113
+ * Batching also makes a single-group file INTERRUPTIBLE: the abort check runs
114
+ * per batch, where before a 1.19M-row group could not be cancelled at all.
115
+ *
116
+ * `columns` PROJECTS the read down to the columns the adapter actually uses.
117
+ * That is not only a memory economy: 2Wiki's `context` column holds the
118
+ * Wikipedia prose the adapter exists to avoid depositing, and naming the
119
+ * columns makes that exclusion structural — the bytes are never decoded at
120
+ * all — in the same way reading only `utterances[].text` structurally excludes
121
+ * Taskmaster's `instructions` scaffolding. Absent ⇒ every column, as before. */
122
+ export declare const parquet: (opts?: {
123
+ batchBytes?: number;
124
+ columns?: string[];
125
+ }) => Reader;