@hviana/sema 0.5.9 → 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.
- package/AGENTS.md +20 -4
- package/DATASETS.md +12 -11
- package/dist/example/train_base/cache.d.ts +35 -0
- package/dist/example/train_base/cache.js +211 -0
- package/dist/example/train_base/config.d.ts +21 -0
- package/dist/example/train_base/config.js +94 -0
- package/dist/example/train_base/corpora/aya.d.ts +19 -0
- package/dist/example/train_base/corpora/aya.js +76 -0
- package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
- package/dist/example/train_base/corpora/converted-parquet.js +44 -0
- package/dist/example/train_base/corpora/genknow.d.ts +14 -0
- package/dist/example/train_base/corpora/genknow.js +83 -0
- package/dist/example/train_base/corpora/index.d.ts +29 -0
- package/dist/example/train_base/corpora/index.js +81 -0
- package/dist/example/train_base/corpora/massive.d.ts +7 -0
- package/dist/example/train_base/corpora/massive.js +98 -0
- package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
- package/dist/example/train_base/corpora/oasst2.js +120 -0
- package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
- package/dist/example/train_base/corpora/smolsent.js +156 -0
- package/dist/example/train_base/corpora/soda.d.ts +12 -0
- package/dist/example/train_base/corpora/soda.js +113 -0
- package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
- package/dist/example/train_base/corpora/taskmaster.js +144 -0
- package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
- package/dist/example/train_base/corpora/wiki2.js +132 -0
- package/dist/example/train_base/corpus.d.ts +88 -0
- package/dist/example/train_base/corpus.js +65 -0
- package/dist/example/train_base/discovery.d.ts +48 -0
- package/dist/example/train_base/discovery.js +143 -0
- package/dist/example/train_base/http.d.ts +82 -0
- package/dist/example/train_base/http.js +219 -0
- package/dist/example/train_base/items.d.ts +46 -0
- package/dist/example/train_base/items.js +98 -0
- package/dist/example/train_base/main.d.ts +4 -0
- package/dist/example/train_base/main.js +207 -0
- package/dist/example/train_base/progress.d.ts +34 -0
- package/dist/example/train_base/progress.js +114 -0
- package/dist/example/train_base/readers.d.ts +125 -0
- package/dist/example/train_base/readers.js +391 -0
- package/dist/example/train_base/runtime.d.ts +115 -0
- package/dist/example/train_base/runtime.js +637 -0
- package/dist/example/train_base/stage.d.ts +3 -0
- package/dist/example/train_base/stage.js +246 -0
- package/dist/example/train_base/ui.d.ts +88 -0
- package/dist/example/train_base/ui.js +272 -0
- package/dist/src/mind/mind.d.ts +1 -1
- package/dist/src/mind/mind.js +1 -1
- package/example/train_base/cache.ts +251 -0
- package/example/train_base/config.ts +128 -0
- package/example/train_base/corpora/aya.ts +106 -0
- package/example/train_base/corpora/converted-parquet.ts +64 -0
- package/example/train_base/corpora/genknow.ts +114 -0
- package/example/train_base/corpora/index.ts +88 -0
- package/example/train_base/corpora/massive.ts +111 -0
- package/example/train_base/corpora/oasst2.ts +163 -0
- package/example/train_base/corpora/smolsent.ts +203 -0
- package/example/train_base/corpora/soda.ts +130 -0
- package/example/train_base/corpora/taskmaster.ts +217 -0
- package/example/train_base/corpora/wiki2.ts +190 -0
- package/example/train_base/corpus.ts +150 -0
- package/example/train_base/discovery.ts +203 -0
- package/example/train_base/http.ts +284 -0
- package/example/train_base/items.ts +118 -0
- package/example/train_base/main.ts +240 -0
- package/example/train_base/progress.ts +149 -0
- package/example/train_base/readers.ts +505 -0
- package/example/train_base/runtime.ts +894 -0
- package/example/train_base/stage.ts +276 -0
- package/example/train_base/ui.ts +333 -0
- package/jsr.json +1 -1
- package/package.json +2 -4
- package/src/mind/mind.ts +1 -1
- package/test/13-conversation.test.mjs +1 -1
- package/test/84-composed-answer-honesty.test.mjs +2 -1
- package/test/88-dependency-footprint.test.mjs +99 -0
- package/dist/example/train_base.d.ts +0 -163
- package/dist/example/train_base.js +0 -3220
- package/example/train_base.ts +0 -3882
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
|
|
19
|
+
export interface Episode {
|
|
20
|
+
context: string;
|
|
21
|
+
continuation: string;
|
|
22
|
+
}
|
|
23
|
+
export type TrainingItem = string | Episode;
|
|
24
|
+
|
|
25
|
+
export const isEpisode = (it: TrainingItem): it is Episode =>
|
|
26
|
+
typeof it !== "string";
|
|
27
|
+
|
|
28
|
+
/** One turn of a dialogue, attributed to a speaker. The speaker is only ever
|
|
29
|
+
* used to decide MERGING (see `mergeSpeakerTurns`); it is never deposited. */
|
|
30
|
+
export interface SpeakerTurn {
|
|
31
|
+
// Upper-cased by the adapters, so USER/ASSISTANT (TM-1/2) and user/assistant
|
|
32
|
+
// (TM-3/4) compare equal.
|
|
33
|
+
speaker: string;
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build the accumulated-context episodes of a turn sequence: each successive
|
|
38
|
+
* turn is the continuation of ALL the turns before it joined together. This is
|
|
39
|
+
* the same cumulative-context shape a multi-turn conversation deposits, so the
|
|
40
|
+
* store learns to continue a growing context.
|
|
41
|
+
*
|
|
42
|
+
* The "\n" below is a CORPUS choice, not a protocol. oasst2 turns are
|
|
43
|
+
* paragraphs, and reading them back with the newlines kept is how this corpus
|
|
44
|
+
* reads naturally; a different corpus may join with nothing, and
|
|
45
|
+
* test/13-conversation.test.mjs does exactly that. Neither has to match the
|
|
46
|
+
* other, because Sema never scans content for turn boundaries — those are
|
|
47
|
+
* offsets the Conversation API carries beside the bytes (see Mind.addTurn's
|
|
48
|
+
* "ON SEPARATORS" note). The newline here is simply part of the text this
|
|
49
|
+
* store learnt, so anything replaying this corpus feeds it back as part of
|
|
50
|
+
* the turn: `addTurn(conv, "\n" + turnText)`. It is not a convention the
|
|
51
|
+
* engine, the API, or the tests have to agree on. */
|
|
52
|
+
export function accumulate(turns: string[]): Episode[] {
|
|
53
|
+
const out: Episode[] = [];
|
|
54
|
+
for (let i = 1; i < turns.length; i++) {
|
|
55
|
+
out.push({ context: turns.slice(0, i).join("\n"), continuation: turns[i] });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Collapse consecutive same-speaker turns into one, joining with a space, and
|
|
61
|
+
* return the bare texts in order. A turn with no speaker never merges with its
|
|
62
|
+
* neighbour: an unlabelled row is of unknown origin, and joining two of them
|
|
63
|
+
* would invent a contribution that may span two speakers.
|
|
64
|
+
*
|
|
65
|
+
* Load-bearing for corpora that split one contribution across several indexed
|
|
66
|
+
* utterances (an artifact of the collection UI). Left unmerged, the cumulative
|
|
67
|
+
* walk deposits a turn boundary in the middle of one speaker's contribution
|
|
68
|
+
* and teaches it as a hand-off. Measured share of turns absorbed by merging:
|
|
69
|
+
* TM-1 17.7%, TM-2 11.9%, TM-3 0.8%, TM-4 0.0%. */
|
|
70
|
+
export function mergeSpeakerTurns(turns: SpeakerTurn[]): string[] {
|
|
71
|
+
const out: string[] = [];
|
|
72
|
+
let prev = "";
|
|
73
|
+
for (const t of turns) {
|
|
74
|
+
if (out.length > 0 && t.speaker !== "" && t.speaker === prev) {
|
|
75
|
+
out[out.length - 1] += " " + t.text;
|
|
76
|
+
} else {
|
|
77
|
+
out.push(t.text);
|
|
78
|
+
}
|
|
79
|
+
prev = t.speaker;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
|
|
85
|
+
* repeats so a concept never deposits the same form twice. */
|
|
86
|
+
export function refineItems(items: TrainingItem[]): TrainingItem[] {
|
|
87
|
+
const out: TrainingItem[] = [];
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
for (const it of items) {
|
|
90
|
+
if (!isEpisode(it)) {
|
|
91
|
+
const exp = it.trim();
|
|
92
|
+
const key = "E:" + exp;
|
|
93
|
+
if (exp && !seen.has(key)) {
|
|
94
|
+
seen.add(key);
|
|
95
|
+
out.push(exp);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const ctx = it.context.trim();
|
|
100
|
+
const cont = it.continuation.trim();
|
|
101
|
+
if (!ctx || !cont || ctx === cont) continue;
|
|
102
|
+
const key = "P:" + ctx + "\u0000" + cont;
|
|
103
|
+
if (seen.has(key)) continue;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
out.push({ context: ctx, continuation: cont });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ENC = new TextEncoder();
|
|
111
|
+
|
|
112
|
+
/** Content size of a training item in UTF-8 bytes — the same quantity the
|
|
113
|
+
* scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
|
|
114
|
+
* context plus the continuation, for a bare experience its own text. */
|
|
115
|
+
export const itemBytes = (it: TrainingItem): number =>
|
|
116
|
+
isEpisode(it)
|
|
117
|
+
? ENC.encode(it.context).length + ENC.encode(it.continuation).length
|
|
118
|
+
: ENC.encode(it).length;
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
|
|
12
|
+
//This file is a more appropriate training example for Sema.
|
|
13
|
+
//Sema does not learn through repetition;
|
|
14
|
+
//it does not require a massive database.
|
|
15
|
+
//It needs fundamental datasets that teach basic cognitive concepts such as conversation, logic, relationships, behaviors and feelings.
|
|
16
|
+
//The focus is on covering fundamental patterns, not repetition.
|
|
17
|
+
//Tip: ontology-based adapted training datasets could be an interesting path.
|
|
18
|
+
|
|
19
|
+
// train_base/main.ts — streaming trainer for the SmolSent + Aya + oasst2 +
|
|
20
|
+
// Taskmaster + 2Wiki + SODA base.
|
|
21
|
+
//
|
|
22
|
+
// Training IS deposition: every source datum is translated into SEMA facts (or,
|
|
23
|
+
// for genuine dialogue, accumulated-context episodes), then stored in one pass.
|
|
24
|
+
// There are no gradients or epochs, and there is no LLM in the loop — the only
|
|
25
|
+
// "model" is the SEMA store itself.
|
|
26
|
+
//
|
|
27
|
+
// Every source here is commercially licensable (cc-by-4.0 / apache-2.0).
|
|
28
|
+
//
|
|
29
|
+
// WHERE THINGS LIVE (this file is the folder's entry point and nothing else):
|
|
30
|
+
//
|
|
31
|
+
// config.ts RUN-level knobs — store, checkpoint cadence, cache ceiling,
|
|
32
|
+
// MAX_MB, the Parquet read budget.
|
|
33
|
+
// items.ts the REPRESENTATION core: what a training item is (fact /
|
|
34
|
+
// experience / accumulated walk) and the policy governing
|
|
35
|
+
// which shape a datum may take.
|
|
36
|
+
// http.ts network policy: wait out throttling, retry what is
|
|
37
|
+
// transient, give up at once on what is not.
|
|
38
|
+
// cache.ts the durable disk cache and the atomic download sink — the
|
|
39
|
+
// sole irreducible Node dependency.
|
|
40
|
+
// readers.ts container formats: newline-delimited JSON (plain or
|
|
41
|
+
// gzipped), JSON arrays, Parquet.
|
|
42
|
+
// discovery.ts where a work-list comes from: an HF repo tree, the
|
|
43
|
+
// auto-converted Parquet branch, a GitHub directory, a local
|
|
44
|
+
// directory.
|
|
45
|
+
// progress.ts the resume record inside the store, and the index passes
|
|
46
|
+
// that keep a checkpoint queryable.
|
|
47
|
+
// ui.ts the live panel, the formatters, the recall box.
|
|
48
|
+
// runtime.ts the RUN: counters, the deposit gate, file acquisition,
|
|
49
|
+
// checkpointing, shutdown.
|
|
50
|
+
// corpus.ts WHAT A CORPUS IS — the contract a corpus declares, and the
|
|
51
|
+
// one rule that turns it into a resume id.
|
|
52
|
+
// stage.ts ONE loop, run once per corpus.
|
|
53
|
+
// corpora/ ONE FILE PER CORPUS: its knobs, its row adapter, its stage
|
|
54
|
+
// descriptor, and the evidence that fixed each default.
|
|
55
|
+
// corpora/index.ts is the curriculum, in order.
|
|
56
|
+
//
|
|
57
|
+
// Adding a corpus is therefore one new file in corpora/ plus its import, its
|
|
58
|
+
// place in CURRICULUM and its re-export in corpora/index.ts — and no change at
|
|
59
|
+
// all to the loop, the readers, or the run.
|
|
60
|
+
//
|
|
61
|
+
// Every source is DOWNLOADED as a file and streamed from disk (never paged
|
|
62
|
+
// row-by-row over an HTTP API — that was slow and rate-limited). Resume is
|
|
63
|
+
// per-file: a fully-consumed file is marked complete; an interrupted one
|
|
64
|
+
// re-reads from the top (re-deposition is idempotent). LOCAL_PATH may hold
|
|
65
|
+
// pre-downloaded files.
|
|
66
|
+
//
|
|
67
|
+
// The store IS the model: memories, training metadata, and the config snapshot
|
|
68
|
+
// all live in {DB_PATH}.sqlite, so a run resumes from the store alone.
|
|
69
|
+
//
|
|
70
|
+
// Built on web standards. All I/O except the durable disk cache uses platform
|
|
71
|
+
// primitives — fetch, WHATWG ReadableStream/WritableStream/TransformStream,
|
|
72
|
+
// DecompressionStream, TextDecoderStream, Blob, AbortController. The sole
|
|
73
|
+
// third-party code is hyparquet (+ its Snappy codec): a DEV dependency of this
|
|
74
|
+
// example, never of the library, and loaded by a dynamic import inside
|
|
75
|
+
// readers.ts the first time a Parquet corpus is read — so a curriculum with no
|
|
76
|
+
// Parquet stage needs it not at all. Consistency guarantees:
|
|
77
|
+
// • Resume from the store alone — completed stage-units, example count,
|
|
78
|
+
// learned-content bytes, and processed-byte total are persisted in
|
|
79
|
+
// {DB_PATH}.sqlite and reloaded.
|
|
80
|
+
// • Atomic cache — a download streams to "<file>.part", is fsync'd, then
|
|
81
|
+
// renamed into place; a file at its final path is, by construction,
|
|
82
|
+
// complete, so an interrupted download can never be mistaken for a cached
|
|
83
|
+
// one.
|
|
84
|
+
// • Bounded cache — a download blocks under the MAX_CACHE_GB ceiling and the
|
|
85
|
+
// fully-processed file is deleted immediately.
|
|
86
|
+
// • Interruptible — Ctrl+C (SIGINT/SIGTERM) aborts in-flight network at once,
|
|
87
|
+
// stops at the next item boundary, writes a final checkpoint, and exits; an
|
|
88
|
+
// un-finished stage-unit is NOT marked complete, so resume re-reads it (re-
|
|
89
|
+
// deposition is idempotent). A second Ctrl+C, or a 60s watchdog, force-exits.
|
|
90
|
+
//
|
|
91
|
+
// Run:
|
|
92
|
+
// npx tsc && node dist/example/train_base/main.js
|
|
93
|
+
// MAX_MB=500 node dist/example/train_base/main.js
|
|
94
|
+
// CHECKPOINT_MB=250 node dist/example/train_base/main.js
|
|
95
|
+
// SMOLSENT_PAIRS=ha_en,zu_en node dist/example/train_base/main.js # a subset of pairs
|
|
96
|
+
// SMOLSENT_DIRECTIONS=both node dist/example/train_base/main.js # also English->foreign
|
|
97
|
+
// SMOLSENT=0 node dist/example/train_base/main.js # skip SmolSent stage
|
|
98
|
+
// AYA=0 node dist/example/train_base/main.js # skip Aya stage
|
|
99
|
+
// OASST=0 node dist/example/train_base/main.js # skip oasst2 stage
|
|
100
|
+
// OASST_MIN_TURNS=6 node dist/example/train_base/main.js # deeper multi-turn only
|
|
101
|
+
// GENKNOW=1 node dist/example/train_base/main.js # General-Knowledge (see DATASETS.md §3.2)
|
|
102
|
+
// PARQUET_BATCH_MB=8 node dist/example/train_base/main.js # smaller Parquet reads on a tight host
|
|
103
|
+
// TASKMASTER=0 node dist/example/train_base/main.js # skip Taskmaster stage
|
|
104
|
+
// TASKMASTER_SETS=TM-3-2020 node dist/example/train_base/main.js # one Taskmaster set
|
|
105
|
+
// WIKI2=0 node dist/example/train_base/main.js # skip 2Wiki triples stage
|
|
106
|
+
// SODA=0 node dist/example/train_base/main.js # skip the SODA stage
|
|
107
|
+
// MASSIVE=1 node dist/example/train_base/main.js # enable MASSIVE (off by default)
|
|
108
|
+
// SODA_MAX_DIALOGS=0 node dist/example/train_base/main.js # lift the SODA budget
|
|
109
|
+
// WIKI2_MAX_ROWS=50000 node dist/example/train_base/main.js # budget the 2Wiki stage
|
|
110
|
+
// LOCAL_PATH=./base node dist/example/train_base/main.js # offline: *.jsonl/.parquet/.jsonl.gz/.json
|
|
111
|
+
// DB_PATH=./data/sema node dist/example/train_base/main.js
|
|
112
|
+
|
|
113
|
+
import { CachedIngest, Mind, SQliteStore } from "../../src/index.js";
|
|
114
|
+
import {
|
|
115
|
+
D,
|
|
116
|
+
DB_PATH,
|
|
117
|
+
SEED,
|
|
118
|
+
SQLITE_CACHE_MB,
|
|
119
|
+
VECTOR_CACHE_MB,
|
|
120
|
+
} from "./config.js";
|
|
121
|
+
import { createRuntime } from "./runtime.js";
|
|
122
|
+
import { runStage } from "./stage.js";
|
|
123
|
+
import { sweepPartials } from "./cache.js";
|
|
124
|
+
import { CURRICULUM, enabledLabels, storedLabels } from "./corpora/index.js";
|
|
125
|
+
import { bytes, DIM, dur, num, R, RED, SHOW } from "./ui.js";
|
|
126
|
+
|
|
127
|
+
// The parser/representation surface this module used to define itself. Kept
|
|
128
|
+
// exported from here so importing `example/train_base/main.js` still reaches every
|
|
129
|
+
// row adapter (toSmolSentRow, wikiTriplesToItems, …) and the shapes they build.
|
|
130
|
+
export * from "./items.js";
|
|
131
|
+
export * from "./corpora/index.js";
|
|
132
|
+
export { parquetBatchRows } from "./readers.js";
|
|
133
|
+
// The name this helper had while it lived here. It is `mergeSpeakerTurns` now,
|
|
134
|
+
// because SODA merges by the same rule and the Taskmaster-specific name was a
|
|
135
|
+
// lie — but the old name stays reachable so nothing importing it breaks.
|
|
136
|
+
export { mergeSpeakerTurns as mergeTaskmasterTurns } from "./items.js";
|
|
137
|
+
|
|
138
|
+
async function main(): Promise<void> {
|
|
139
|
+
const store = new SQliteStore({
|
|
140
|
+
path: DB_PATH,
|
|
141
|
+
D,
|
|
142
|
+
vectorCacheMb: VECTOR_CACHE_MB,
|
|
143
|
+
sqliteCacheMb: SQLITE_CACHE_MB,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// The store IS the model: memories, progress, and metadata all persist in
|
|
147
|
+
// it, so a resumed run just reopens the same store and continues. Guard
|
|
148
|
+
// against a changed D/SEED by comparing against what a previous run recorded.
|
|
149
|
+
const mind = new Mind({ seed: SEED, store });
|
|
150
|
+
|
|
151
|
+
// Pre-fill the vector indices' RAM caches with sequential scans (bounded by
|
|
152
|
+
// VECTOR_CACHE_MB). A resumed run over a large store otherwise spends its
|
|
153
|
+
// first minutes warming those caches through random point reads — the
|
|
154
|
+
// ingest hot path is cache-miss bound until then. Seconds, once, up front.
|
|
155
|
+
if (VECTOR_CACHE_MB > 0) {
|
|
156
|
+
const t = Date.now();
|
|
157
|
+
const warmed = await store.warmVectorCaches();
|
|
158
|
+
if (warmed > 0) {
|
|
159
|
+
process.stderr.write(
|
|
160
|
+
` warmed vector caches: ${num(warmed)} rows in ${
|
|
161
|
+
dur((Date.now() - t) / 1000)
|
|
162
|
+
}\n`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const ci = new CachedIngest(mind);
|
|
167
|
+
const prevD = await store.getMeta("train.D");
|
|
168
|
+
const prevSeed = await store.getMeta("train.seed");
|
|
169
|
+
if (
|
|
170
|
+
(prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)
|
|
171
|
+
) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
|
|
174
|
+
`requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
|
|
175
|
+
`to start fresh.\n`,
|
|
176
|
+
);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dataset = enabledLabels();
|
|
181
|
+
await store.setMeta("train.D", String(D));
|
|
182
|
+
await store.setMeta("train.seed", String(SEED));
|
|
183
|
+
// createdAt is when this STORE was first trained into, so a resume must not
|
|
184
|
+
// overwrite it — the previous behaviour left every store claiming it was
|
|
185
|
+
// created at its most recent resume. When the current run started is a
|
|
186
|
+
// separate, also-useful fact, so it gets its own key rather than displacing
|
|
187
|
+
// this one.
|
|
188
|
+
if ((await store.getMeta("train.createdAt")) === null) {
|
|
189
|
+
await store.setMeta("train.createdAt", new Date().toISOString());
|
|
190
|
+
}
|
|
191
|
+
await store.setMeta("train.startedAt", new Date().toISOString());
|
|
192
|
+
|
|
193
|
+
const ctx = createRuntime({ store, mind, ci, title: dataset });
|
|
194
|
+
ctx.tick(true);
|
|
195
|
+
|
|
196
|
+
// Reclaim the debris of any download killed mid-transfer. It occupies cache
|
|
197
|
+
// ceiling that nothing else would ever free (see cache.ts sweepPartials).
|
|
198
|
+
const swept = sweepPartials();
|
|
199
|
+
if (swept.files > 0) {
|
|
200
|
+
ctx.progress.log(
|
|
201
|
+
` ${DIM}· swept ${swept.files} interrupted download(s), ` +
|
|
202
|
+
`${bytes(swept.bytes)} reclaimed${R}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── resume — restore counters and the per-source tally from the store ──
|
|
207
|
+
await ctx.restore();
|
|
208
|
+
|
|
209
|
+
// What the STORE contains, which is only knowable after the resume: this
|
|
210
|
+
// run's stages plus whatever earlier runs deposited. The panel header above
|
|
211
|
+
// names this RUN's stages, which is a different and also-true fact.
|
|
212
|
+
await store.setMeta(
|
|
213
|
+
"train.dataset",
|
|
214
|
+
storedLabels(Object.keys(ctx.counters.langTally)),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Walk the curriculum. Each stage skips itself on a resume that already
|
|
218
|
+
// finished it, and the walk stops at the first requested stop.
|
|
219
|
+
for (const corpus of CURRICULUM) {
|
|
220
|
+
if (ctx.stopRequested) break;
|
|
221
|
+
await runStage(ctx, corpus);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Nothing is in flight any more; each stage announces itself as it starts, so
|
|
225
|
+
// this is the one transition that has no next stage to correct it.
|
|
226
|
+
ctx.state.activity = "idle";
|
|
227
|
+
await ctx.finish(ctx.stopRequested ? ctx.stopReason : "done");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Only run when invoked directly, so importing the row adapters above (e.g.
|
|
231
|
+
// for a fixture check) never starts training.
|
|
232
|
+
const isMain = import.meta.url === `file://${process.argv[1]}` ||
|
|
233
|
+
process.argv[1]?.endsWith("train_base/main.js");
|
|
234
|
+
if (isMain) {
|
|
235
|
+
main().catch((e) => {
|
|
236
|
+
process.stderr.write(SHOW);
|
|
237
|
+
console.error(`\n${RED}fatal:${R}`, e);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
|
|
10
|
+
import type { Mind, Store } from "../../src/index.js";
|
|
11
|
+
import { INDEX_MAINTENANCE } from "./config.js";
|
|
12
|
+
import { DIM, GRN, int, R, YEL } from "./ui.js";
|
|
13
|
+
|
|
14
|
+
const META_COMPLETED = "train.completedFiles";
|
|
15
|
+
const META_DEPOSITS = "train.depositCount";
|
|
16
|
+
const META_TRAINED_BYTES = "train.trainedContentBytes";
|
|
17
|
+
const META_BYTES = "train.totalBytesProcessed";
|
|
18
|
+
const META_CORPUS_BYTES = "train.totalCorpusBytes";
|
|
19
|
+
|
|
20
|
+
export interface SavedProgress {
|
|
21
|
+
completedFiles: string[];
|
|
22
|
+
depositCount: number;
|
|
23
|
+
trainedContentBytes: number;
|
|
24
|
+
totalBytesProcessed: number;
|
|
25
|
+
totalCorpusBytes: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadProgress(store: Store): Promise<SavedProgress> {
|
|
29
|
+
try {
|
|
30
|
+
const raw = await store.getMeta(META_COMPLETED);
|
|
31
|
+
const deps = await store.getMeta(META_DEPOSITS);
|
|
32
|
+
const b = await store.getMeta(META_BYTES);
|
|
33
|
+
if (raw !== null && deps !== null && b !== null) {
|
|
34
|
+
const completedFiles = JSON.parse(raw);
|
|
35
|
+
if (Array.isArray(completedFiles)) {
|
|
36
|
+
const trained = await store.getMeta(META_TRAINED_BYTES);
|
|
37
|
+
const corpus = await store.getMeta(META_CORPUS_BYTES);
|
|
38
|
+
return {
|
|
39
|
+
completedFiles,
|
|
40
|
+
depositCount: Number(deps) || 0,
|
|
41
|
+
trainedContentBytes: Number(trained) || 0,
|
|
42
|
+
totalBytesProcessed: Number(b) || 0,
|
|
43
|
+
totalCorpusBytes: Number(corpus) || 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch { /* corrupt/missing — start fresh */ }
|
|
48
|
+
return {
|
|
49
|
+
completedFiles: [],
|
|
50
|
+
depositCount: 0,
|
|
51
|
+
trainedContentBytes: 0,
|
|
52
|
+
totalBytesProcessed: 0,
|
|
53
|
+
totalCorpusBytes: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function saveProgress(
|
|
58
|
+
store: Store,
|
|
59
|
+
p: SavedProgress,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
await store.setMeta(META_COMPLETED, JSON.stringify(p.completedFiles));
|
|
62
|
+
await store.setMeta(META_DEPOSITS, String(p.depositCount));
|
|
63
|
+
await store.setMeta(META_TRAINED_BYTES, String(p.trainedContentBytes));
|
|
64
|
+
await store.setMeta(META_BYTES, String(p.totalBytesProcessed));
|
|
65
|
+
await store.setMeta(META_CORPUS_BYTES, String(p.totalCorpusBytes));
|
|
66
|
+
await store.setMeta("train.updatedAt", new Date().toISOString());
|
|
67
|
+
store.commit();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run index maintenance: compact (remove garbage), repair (fill gaps),
|
|
71
|
+
* then refresh the canonical-form index (see below). All three are
|
|
72
|
+
* idempotent — running twice produces the same result as once.
|
|
73
|
+
* Compaction frees index space first; repair then adds back every
|
|
74
|
+
* edge/halo-bearing node whose gist was evicted from the pending cache
|
|
75
|
+
* before it reached the content index, completing the coverage that
|
|
76
|
+
* incremental promotion alone cannot guarantee.
|
|
77
|
+
*
|
|
78
|
+
* repair runs with minParents = 0, NOT the library default of 2. The
|
|
79
|
+
* default repairs only structural BRIDGES (≥2 parents), but this
|
|
80
|
+
* trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
|
|
81
|
+
* structural parents ("The capital of France is Paris." as the dst of a
|
|
82
|
+
* Q→A edge is a root of its own tree, contained in nothing). Those are
|
|
83
|
+
* resonance targets recall depends on — a trained store shipped without
|
|
84
|
+
* them cannot ground statement-shaped queries against its own answers
|
|
85
|
+
* (observed: 33 such roots missing after a full curriculum, including
|
|
86
|
+
* high-traffic conversation replies). minParents = 0 admits every
|
|
87
|
+
* edge/halo bearer; the candidate set is still corpus-of-experiences-
|
|
88
|
+
* sized, so the pass stays cheap.
|
|
89
|
+
*
|
|
90
|
+
* Logs the number of entries removed/added so a run that silently degrades
|
|
91
|
+
* (growing compaction count, or repair never recovering anything) is
|
|
92
|
+
* visible in the training log. */
|
|
93
|
+
export async function runIndexMaintenance(
|
|
94
|
+
mind: Mind,
|
|
95
|
+
log: (msg: string) => void,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
if (!INDEX_MAINTENANCE) return;
|
|
98
|
+
try {
|
|
99
|
+
const removed = await mind.store.compactContentIndex();
|
|
100
|
+
if (removed > 0) {
|
|
101
|
+
log(
|
|
102
|
+
` ${DIM}index compact: removed ${int(removed)} isolated entries${R}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
log(
|
|
107
|
+
` ${YEL}⚠ index compact failed${R}: ${
|
|
108
|
+
err instanceof Error ? err.message : String(err)
|
|
109
|
+
}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const added = await mind.repairContentIndex(0);
|
|
114
|
+
if (added > 0) {
|
|
115
|
+
log(
|
|
116
|
+
` ${GRN}index repair: added ${
|
|
117
|
+
int(added)
|
|
118
|
+
} missing resonance targets${R}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
log(
|
|
123
|
+
` ${YEL}⚠ index repair failed${R}: ${
|
|
124
|
+
err instanceof Error ? err.message : String(err)
|
|
125
|
+
}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
// Canonical-form index (src/canon.ts): lets resolution find stored forms
|
|
129
|
+
// across surface variation (case, width, whitespace). Incremental and
|
|
130
|
+
// idempotent by construction — the `canon.upto` meta cursor scans only
|
|
131
|
+
// nodes newer than the last pass, and the (h, id) primary key ignores
|
|
132
|
+
// re-inserted rows — so it composes with the resume model exactly like
|
|
133
|
+
// compact/repair: every checkpoint (and finish) leaves the index
|
|
134
|
+
// covering all content trained so far.
|
|
135
|
+
try {
|
|
136
|
+
const added = await mind.buildCanonIndex();
|
|
137
|
+
if (added > 0) {
|
|
138
|
+
log(
|
|
139
|
+
` ${GRN}canon index: added ${int(added)} canonical-form entries${R}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log(
|
|
144
|
+
` ${YEL}⚠ canon index build failed${R}: ${
|
|
145
|
+
err instanceof Error ? err.message : String(err)
|
|
146
|
+
}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|