mlx-bun 0.0.5 → 0.0.6
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/README.md +11 -2
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/memory-system.md +193 -295
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +42 -0
- package/docs/design/orpo-training.md +13 -0
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
- package/docs/investigations/steel-flash-cce-handoff.md +58 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +44 -5
- package/docs/reference/training.md +9 -5
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/jobs/runner.ts +6 -0
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +13 -0
- package/src/model/flash-attention.ts +5 -3
- package/src/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +213 -44
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +11 -3
- package/src/train/flash-cce.ts +18 -5
- package/src/train/job.ts +41 -4
- package/src/train/loss.ts +10 -4
- package/src/train/trainer.ts +62 -3
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +264 -14
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
package/src/eval/tasks/mmlu.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
// MMLU 5-shot — multi-domain multiple-choice knowledge.
|
|
2
|
-
// optiq/eval/mmlu.py: per-subject dev exemplars, A/B/C/D format,
|
|
3
|
-
//
|
|
1
|
+
// MMLU 5-shot — multi-domain multiple-choice knowledge. Faithful port of
|
|
2
|
+
// optiq/eval/mmlu.py: per-subject dev exemplars, A/B/C/D format, and scoring by
|
|
3
|
+
// **last-token logit argmax over the " A"/" B"/" C"/" D" tokens** on the RAW
|
|
4
|
+
// 5-shot prompt — NO chat template, NO generation. This is the standard
|
|
5
|
+
// (Hendrycks/Unsloth) MMLU methodology; the earlier chat-wrap+generate variant
|
|
6
|
+
// scored ~5 pts low vs optiq's published number, so we match optiq exactly.
|
|
4
7
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// underscores on a small noisy sample, so we drive the chat template: the
|
|
9
|
-
// 5-shot exemplars become the priming context and an explicit instruction
|
|
10
|
-
// asks for ONLY the single letter A/B/C/D. Extraction is robust to chatty
|
|
11
|
-
// formatting (</think> wrappers, "Answer: X", "(X)", "**X**", bare letter).
|
|
12
|
-
// Prompt format, header, exemplar selection, and gold mapping are verbatim.
|
|
8
|
+
// Questions: by DEFAULT we score optiq's EXACT frozen draw (mmlu_optiq_frozen, the
|
|
9
|
+
// same ~969 his published 52.4 used) so base/adapter are directly comparable to his
|
|
10
|
+
// table. MLX_BUN_MMLU_FROZEN=0 falls back to our own sampling of the full 14k set.
|
|
13
11
|
|
|
14
|
-
import {
|
|
12
|
+
import { loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
15
13
|
|
|
16
14
|
interface MmluRow {
|
|
17
15
|
question: string;
|
|
@@ -42,12 +40,8 @@ function formatExample(question: string, choices: string[], answerIdx?: number):
|
|
|
42
40
|
return s;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/** 5-shot prompt: subject header → n demos →
|
|
46
|
-
*
|
|
47
|
-
* Built for the chat template: the header + worked exemplars prime the model,
|
|
48
|
-
* and a trailing instruction pins the output to a single bare letter. The
|
|
49
|
-
* exemplars still close their "Answer: X" line so the model sees the expected
|
|
50
|
-
* shape, and the final question is left open ("Answer:") for the model. */
|
|
43
|
+
/** 5-shot prompt (verbatim optiq _build_prompt): subject header → n demos →
|
|
44
|
+
* open test question ending in "Answer:". Fed raw (no chat template). */
|
|
51
45
|
function buildPrompt(row: MmluRow, devExamples: MmluRow[], nShots: number): string {
|
|
52
46
|
let prompt = `The following are multiple choice questions (with answers) about ${formatSubject(row.subject)}.\n\n`;
|
|
53
47
|
for (const ex of devExamples.slice(0, nShots)) {
|
|
@@ -55,48 +49,46 @@ function buildPrompt(row: MmluRow, devExamples: MmluRow[], nShots: number): stri
|
|
|
55
49
|
prompt += "\n";
|
|
56
50
|
}
|
|
57
51
|
prompt += formatExample(row.question, row.choices);
|
|
58
|
-
prompt += "\n\nRespond with ONLY the single letter (A, B, C, or D) of the correct answer. Do not explain.";
|
|
59
52
|
return prompt;
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const m = text.match(re);
|
|
80
|
-
if (m) return toIdx(m[1]!);
|
|
55
|
+
/** Forward the prompt (prefill + last token) and return the index 0..3 of the
|
|
56
|
+
* highest-logit answer letter — optiq's argmax-on-last-token scoring. */
|
|
57
|
+
function lastTokenArgmax(model: TaskModel["model"], ids: number[], letterIds: number[]): number {
|
|
58
|
+
const cache = model.makeCache();
|
|
59
|
+
try {
|
|
60
|
+
if (ids.length > 1) model.forward(ids.slice(0, -1), cache).dispose(); // prefill (cache only)
|
|
61
|
+
const logits = model.forward([ids[ids.length - 1]!], cache); // [1, 1, V]
|
|
62
|
+
const v = logits.toFloat32();
|
|
63
|
+
logits.dispose();
|
|
64
|
+
let best = 0, bestVal = -Infinity;
|
|
65
|
+
for (let i = 0; i < letterIds.length; i++) {
|
|
66
|
+
const val = v[letterIds[i]!]!;
|
|
67
|
+
if (val > bestVal) { bestVal = val; best = i; }
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
} finally {
|
|
71
|
+
for (const c of cache) c.dispose();
|
|
81
72
|
}
|
|
82
|
-
|
|
83
|
-
// Fallback: first standalone letter (word-boundary, not embedded in a word).
|
|
84
|
-
const standalone = text.match(/\b([A-D])\b/);
|
|
85
|
-
if (standalone) return toIdx(standalone[1]!);
|
|
86
|
-
|
|
87
|
-
return null;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
75
|
export async function evaluateMmlu(
|
|
91
76
|
tm: TaskModel,
|
|
92
|
-
opts: { nSamples?: number; nShots?: number;
|
|
77
|
+
opts: { nSamples?: number; nShots?: number; seed?: number; frozen?: boolean } = {},
|
|
93
78
|
): Promise<MmluResult> {
|
|
94
79
|
const nShots = opts.nShots ?? 5;
|
|
95
|
-
const maxTokens = opts.maxTokens ?? 16;
|
|
96
80
|
const seed = opts.seed ?? 42;
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
// Optiq-parity mode (DEFAULT): score optiq's EXACT frozen draw — the same ~969
|
|
83
|
+
// questions + dev exemplars his published table used — so base/adapter numbers are
|
|
84
|
+
// DIRECTLY comparable to optiq's. Any remaining gap is then pure forward-logit
|
|
85
|
+
// parity, not sampling. Set MLX_BUN_MMLU_FROZEN=0 (or frozen:false) to fall back to
|
|
86
|
+
// our own stratified sampling of the full 14k test set (a DIFFERENT subset: JS RNG
|
|
87
|
+
// ≠ numpy RandomState, so it does not match optiq's questions even at the same seed).
|
|
88
|
+
const useFrozen = opts.frozen ?? (process.env.MLX_BUN_MMLU_FROZEN !== "0");
|
|
89
|
+
|
|
90
|
+
const testRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_frozen" : "mmlu_test");
|
|
91
|
+
const devRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_dev" : "mmlu_dev");
|
|
100
92
|
|
|
101
93
|
// Per-subject dev exemplars for prompt construction.
|
|
102
94
|
const devBySubject = new Map<string, MmluRow[]>();
|
|
@@ -106,38 +98,57 @@ export async function evaluateMmlu(
|
|
|
106
98
|
devBySubject.set(ex.subject, list);
|
|
107
99
|
}
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
testBySubject
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
101
|
+
let sampled: number[];
|
|
102
|
+
if (useFrozen) {
|
|
103
|
+
// Already optiq's exact draw — score ALL of it, in his order, NO resampling
|
|
104
|
+
// (re-sampling here would re-introduce exactly the divergence we're removing).
|
|
105
|
+
sampled = testRows.map((_, i) => i);
|
|
106
|
+
} else {
|
|
107
|
+
// Stratified sample: roughly equal questions per subject (mirrors mmlu.py).
|
|
108
|
+
const testBySubject = new Map<string, number[]>();
|
|
109
|
+
for (let i = 0; i < testRows.length; i++) {
|
|
110
|
+
const subj = testRows[i]!.subject;
|
|
111
|
+
const list = testBySubject.get(subj) ?? [];
|
|
112
|
+
list.push(i);
|
|
113
|
+
testBySubject.set(subj, list);
|
|
114
|
+
}
|
|
115
|
+
const subjects = [...testBySubject.keys()].sort();
|
|
116
|
+
const nSamples = opts.nSamples ?? 1000;
|
|
117
|
+
const perSubject = Math.max(1, Math.floor(nSamples / subjects.length));
|
|
118
|
+
sampled = [];
|
|
119
|
+
for (const subj of subjects) {
|
|
120
|
+
const pool = testBySubject.get(subj)!;
|
|
121
|
+
const take = Math.min(perSubject, pool.length);
|
|
122
|
+
for (const j of sampleIndices(pool.length, take, seed)) sampled.push(pool[j]!);
|
|
123
|
+
}
|
|
124
|
+
if (sampled.length > nSamples) {
|
|
125
|
+
const keep = sampleIndices(sampled.length, nSamples, seed);
|
|
126
|
+
sampled = keep.map((k) => sampled[k]!);
|
|
127
|
+
}
|
|
131
128
|
}
|
|
132
129
|
|
|
130
|
+
// Answer-letter token ids — " A".." D" first token, matching how Hendrycks
|
|
131
|
+
// scores the token after "Answer:". Strip a leading BOS (≈ add_special_tokens=False).
|
|
132
|
+
const bos = tm.tokenizer.bosTokenId;
|
|
133
|
+
const stripBos = (ids: number[]) => (bos !== null && ids[0] === bos ? ids.slice(1) : ids);
|
|
134
|
+
const letterIds = LETTERS.map((L) => {
|
|
135
|
+
const ids = stripBos(tm.tokenizer.encode(` ${L}`));
|
|
136
|
+
return ids[0]!;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Re-assert the mounted adapter for our direct forward()s: generation tasks reset
|
|
140
|
+
// loraState.active to [] when they finish, and MMLU bypasses generate() entirely,
|
|
141
|
+
// so without this the argmax would silently score BASE even with --adapter set.
|
|
142
|
+
if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters;
|
|
143
|
+
|
|
133
144
|
let nCorrect = 0;
|
|
134
145
|
for (let k = 0; k < sampled.length; k++) {
|
|
135
146
|
const item = testRows[sampled[k]!]!;
|
|
136
147
|
const devEx = devBySubject.get(item.subject) ?? [];
|
|
137
|
-
const
|
|
138
|
-
const pred =
|
|
139
|
-
if (pred
|
|
140
|
-
if ((k + 1) %
|
|
148
|
+
const ids = stripBos(tm.tokenizer.encode(buildPrompt(item, devEx, nShots)));
|
|
149
|
+
const pred = lastTokenArgmax(tm.model, ids, letterIds);
|
|
150
|
+
if (pred === item.answer) nCorrect++;
|
|
151
|
+
if ((k + 1) % 25 === 0 || k + 1 === sampled.length)
|
|
141
152
|
process.stderr.write(`\r mmlu ${k + 1}/${sampled.length} acc=${((nCorrect / (k + 1)) * 100).toFixed(1)}%`);
|
|
142
153
|
}
|
|
143
154
|
process.stderr.write("\n");
|
package/src/jobs/runner.ts
CHANGED
|
@@ -149,6 +149,7 @@ interface QueuedSpawn {
|
|
|
149
149
|
jobId: string;
|
|
150
150
|
entry: string;
|
|
151
151
|
bin: string;
|
|
152
|
+
onComplete?: (jobId: string, code: number) => void;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
let gpuLeaseHolder: string | null = null;
|
|
@@ -170,6 +171,9 @@ export interface SubprocessOpts {
|
|
|
170
171
|
entry?: string;
|
|
171
172
|
/** Override the runtime binary (tests). Defaults to "bun". */
|
|
172
173
|
bin?: string;
|
|
174
|
+
/** Called on the server (parent) after the child exits — used to invalidate
|
|
175
|
+
* caches (e.g. the Library) so a finished quantize surfaces immediately. */
|
|
176
|
+
onComplete?: (jobId: string, code: number) => void;
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
/** Create a 'queued' row and either spawn it now (lease free) or leave it
|
|
@@ -188,6 +192,7 @@ export function submitSubprocess(
|
|
|
188
192
|
jobId: row.id,
|
|
189
193
|
entry: opts.entry ?? JOB_ENTRY_PATH,
|
|
190
194
|
bin: opts.bin ?? "bun",
|
|
195
|
+
onComplete: opts.onComplete,
|
|
191
196
|
};
|
|
192
197
|
if (gpuLeaseHolder === null) {
|
|
193
198
|
spawnNow(item);
|
|
@@ -243,6 +248,7 @@ function spawnNow(item: QueuedSpawn): void {
|
|
|
243
248
|
}
|
|
244
249
|
}
|
|
245
250
|
releaseLease(jobId);
|
|
251
|
+
try { item.onComplete?.(jobId, code); } catch {}
|
|
246
252
|
drainQueue();
|
|
247
253
|
})();
|
|
248
254
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis stage 2: CHUNK. ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// Segment each new conversation into topic chunks (a local-model call — this is
|
|
4
|
+
// the segmentation track distilled from gold; eval by boundary/label accuracy).
|
|
5
|
+
// Re-chunk only conversations whose updated_at > chunked_at. Mirrors lucien's
|
|
6
|
+
// chunk-recent.ts. The prompt is the future "chunk" LoRA seam (prompts.ts).
|
|
7
|
+
//
|
|
8
|
+
// See docs/design/memory-system.md → "The nightly pipeline".
|
|
9
|
+
|
|
10
|
+
import type { SynthesisEvent } from "./pipeline";
|
|
11
|
+
|
|
12
|
+
export interface ChunkResult {
|
|
13
|
+
conversations: number;
|
|
14
|
+
chunks: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Conversations → topic chunks (local model). STUB. */
|
|
18
|
+
export async function chunkConversations(
|
|
19
|
+
_model: unknown,
|
|
20
|
+
_onEvent?: (e: SynthesisEvent) => void,
|
|
21
|
+
): Promise<ChunkResult> {
|
|
22
|
+
// TODO(M1): for each conv with updated_at > chunked_at, run the chunk prompt
|
|
23
|
+
// on the local model, write chunks, set chunked_at.
|
|
24
|
+
throw new Error("memory.chunk is not implemented yet (M1)");
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis stage 3: CLUSTER. ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// Assign each new chunk to an existing bucket or propose a new one (local-model
|
|
4
|
+
// call). Buckets are emergent (Meta/Buckets.md), each mapping to a target
|
|
5
|
+
// article. Mirrors lucien's cluster-assign-recent.ts.
|
|
6
|
+
//
|
|
7
|
+
// See docs/design/memory-system.md → "The nightly pipeline".
|
|
8
|
+
|
|
9
|
+
import type { SynthesisEvent } from "./pipeline";
|
|
10
|
+
|
|
11
|
+
export interface ClusterResult {
|
|
12
|
+
assigned: number;
|
|
13
|
+
newBuckets: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Chunks → buckets (local model). STUB. */
|
|
17
|
+
export async function clusterChunks(
|
|
18
|
+
_model: unknown,
|
|
19
|
+
_onEvent?: (e: SynthesisEvent) => void,
|
|
20
|
+
): Promise<ClusterResult> {
|
|
21
|
+
// TODO(M1): for each unbucketed chunk, classify into Meta/Buckets.md taxonomy
|
|
22
|
+
// (or propose a new bucket → new article stem); persist chunk.bucket.
|
|
23
|
+
throw new Error("memory.cluster is not implemented yet (M1)");
|
|
24
|
+
}
|
package/src/memory/db.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis bookkeeping store (bun:sqlite).
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ M1 STUB schema-wise the tables are real, but no stage writes them yet.
|
|
4
|
+
// The read path (vault.ts / tools.ts) does NOT touch this — it's only used by
|
|
5
|
+
// the synthesis pipeline (pipeline.ts), which is itself stubbed.
|
|
6
|
+
//
|
|
7
|
+
// Reuses mlx-bun's existing per-domain sqlite pattern (cf. JobStore in
|
|
8
|
+
// src/jobs/db.ts, EvalDB in src/evaldb.ts): a small class over bun:sqlite with
|
|
9
|
+
// WAL + schema-in-constructor + an additive migrate() hook, living under
|
|
10
|
+
// ~/.cache/mlx-bun/ alongside jobs.sqlite / evals.sqlite / registry.sqlite. It's
|
|
11
|
+
// derived state — rebuildable from the pi transcripts + the vault articles — so
|
|
12
|
+
// it belongs in the rebuildable cache, NOT in the (precious, git-tracked) vault.
|
|
13
|
+
//
|
|
14
|
+
// The schema mirrors lucien's incremental design: per-source watermarks (don't
|
|
15
|
+
// re-ingest), chunked_at > updated_at (re-chunk only changed convs), and a
|
|
16
|
+
// synthesized-chunk ledger (never fold the same chunk into an article twice).
|
|
17
|
+
|
|
18
|
+
import { Database } from "bun:sqlite";
|
|
19
|
+
import { mkdirSync } from "node:fs";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MEMORY_DB = `${process.env.HOME}/.cache/mlx-bun/memory.sqlite`;
|
|
22
|
+
|
|
23
|
+
export interface ConversationRow {
|
|
24
|
+
/** Stable conv id: first 8 hex of the session UUID (the conv:HASH citation). */
|
|
25
|
+
conv: string;
|
|
26
|
+
source: string; // "pi-web" | "pi-terminal"
|
|
27
|
+
title: string;
|
|
28
|
+
/** Full normalized transcript JSON ({role, content}[]). */
|
|
29
|
+
transcript: string;
|
|
30
|
+
updated_at: number;
|
|
31
|
+
chunked_at: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChunkRow {
|
|
35
|
+
id: string;
|
|
36
|
+
conv: string;
|
|
37
|
+
ordinal: number;
|
|
38
|
+
text: string;
|
|
39
|
+
bucket: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BucketRow {
|
|
43
|
+
name: string;
|
|
44
|
+
article: string; // target article stem
|
|
45
|
+
description: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SCHEMA = `
|
|
49
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
50
|
+
conv TEXT PRIMARY KEY,
|
|
51
|
+
source TEXT NOT NULL,
|
|
52
|
+
title TEXT NOT NULL DEFAULT '',
|
|
53
|
+
transcript TEXT NOT NULL,
|
|
54
|
+
updated_at INTEGER NOT NULL,
|
|
55
|
+
chunked_at INTEGER
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
conv TEXT NOT NULL REFERENCES conversations(conv),
|
|
60
|
+
ordinal INTEGER NOT NULL,
|
|
61
|
+
text TEXT NOT NULL,
|
|
62
|
+
bucket TEXT
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_conv ON chunks(conv);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS buckets (
|
|
66
|
+
name TEXT PRIMARY KEY,
|
|
67
|
+
article TEXT NOT NULL,
|
|
68
|
+
description TEXT NOT NULL DEFAULT ''
|
|
69
|
+
);
|
|
70
|
+
-- Per-source ingest watermark: only process what's new since last run.
|
|
71
|
+
CREATE TABLE IF NOT EXISTS watermarks (
|
|
72
|
+
source TEXT PRIMARY KEY,
|
|
73
|
+
cursor TEXT NOT NULL,
|
|
74
|
+
updated_at INTEGER NOT NULL
|
|
75
|
+
);
|
|
76
|
+
-- Idempotency ledger: a (bucket, chunk) pair is folded into an article once.
|
|
77
|
+
CREATE TABLE IF NOT EXISTS synthesized_bucket_chunks (
|
|
78
|
+
bucket TEXT NOT NULL,
|
|
79
|
+
chunk_id TEXT NOT NULL,
|
|
80
|
+
PRIMARY KEY (bucket, chunk_id)
|
|
81
|
+
);
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Synthesis bookkeeping DB. Same shape as JobStore/EvalDB: open in the
|
|
86
|
+
* constructor (WAL on, schema ensured, additive migrate), expose `db`, and a
|
|
87
|
+
* `close()`. Only the synthesis pipeline constructs one — opening it is what
|
|
88
|
+
* *creates* memory.sqlite, so the read path never does. Stage queries land in
|
|
89
|
+
* M1; the tables they expect already exist here.
|
|
90
|
+
*/
|
|
91
|
+
export class MemoryStore {
|
|
92
|
+
readonly db: Database;
|
|
93
|
+
readonly dbPath: string;
|
|
94
|
+
|
|
95
|
+
constructor(dbPath: string = DEFAULT_MEMORY_DB) {
|
|
96
|
+
if (dbPath !== ":memory:") {
|
|
97
|
+
try { mkdirSync(dbPath.slice(0, dbPath.lastIndexOf("/")), { recursive: true }); } catch {}
|
|
98
|
+
}
|
|
99
|
+
this.dbPath = dbPath;
|
|
100
|
+
this.db = new Database(dbPath);
|
|
101
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
102
|
+
this.db.exec(SCHEMA);
|
|
103
|
+
this.migrate();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Additive-only migrations (mirrors JobStore.migrate). Re-opening is a no-op. */
|
|
107
|
+
private migrate(): void {
|
|
108
|
+
// No columns to add today; the hook stays so future fields are additive.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
close(): void {
|
|
112
|
+
this.db.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis stage 1: INGEST. ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// pi sessions (~/.mlx-bun/sessions + ~/.mlx-bun/pi, pi's own JSONL) →
|
|
4
|
+
// memory.sqlite, normalized to {conv, source, title, transcript}, watermark-
|
|
5
|
+
// gated so each run only processes what's new. No web scraping, no Playwright:
|
|
6
|
+
// mlx-bun synthesizes ITS OWN sessions only. Mirrors lucien's ingest-recent.ts.
|
|
7
|
+
//
|
|
8
|
+
// See docs/design/memory-system.md → "Ingestion — mlx-bun sessions only".
|
|
9
|
+
|
|
10
|
+
import type { SynthesisEvent } from "./pipeline";
|
|
11
|
+
|
|
12
|
+
export interface IngestResult {
|
|
13
|
+
scanned: number;
|
|
14
|
+
ingested: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read new pi sessions into memory.sqlite. STUB: returns zero, does nothing. */
|
|
18
|
+
export async function ingestSessions(_onEvent?: (e: SynthesisEvent) => void): Promise<IngestResult> {
|
|
19
|
+
// TODO(M1): SessionManager.listAll() → normalize → upsert into conversations,
|
|
20
|
+
// advancing the per-source watermark. pi's session format is the contract.
|
|
21
|
+
throw new Error("memory.ingest is not implemented yet (M1)");
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis pipeline orchestrator. ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// Ties the stages together: ingest → chunk → cluster → synthesize → wikify →
|
|
4
|
+
// changelog (the lucien nightly chain, reimplemented on the local model). What
|
|
5
|
+
// `mlx-bun memory synthesize` and the nightly launchd job invoke.
|
|
6
|
+
//
|
|
7
|
+
// CURRENTLY A NO-OP: it logs each stage as stubbed and writes nothing, so the
|
|
8
|
+
// scheduled job runs cleanly tonight and every night without touching your
|
|
9
|
+
// vault. When M1 lands, flip runSynthesis to actually call the stage modules
|
|
10
|
+
// (each currently throws "not implemented") and move it onto the src/jobs/*
|
|
11
|
+
// background runner with SSE progress, per the design doc.
|
|
12
|
+
//
|
|
13
|
+
// See docs/design/memory-system.md → "The nightly pipeline".
|
|
14
|
+
|
|
15
|
+
export type SynthesisStage = "ingest" | "chunk" | "cluster" | "synthesize" | "wikify" | "changelog";
|
|
16
|
+
|
|
17
|
+
export interface SynthesisEvent {
|
|
18
|
+
type: "stage" | "log" | "done";
|
|
19
|
+
stage?: SynthesisStage;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SynthesisOptions {
|
|
24
|
+
/** Only synthesize conversations newer than this (ISO date / conv cursor). */
|
|
25
|
+
since?: string;
|
|
26
|
+
/** Override the synthesis model (default e4b). */
|
|
27
|
+
model?: string;
|
|
28
|
+
/** Plan only — never write the vault. */
|
|
29
|
+
dryRun?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SynthesisSummary {
|
|
33
|
+
/** False while stubbed; flip to true when the stages are implemented. */
|
|
34
|
+
implemented: boolean;
|
|
35
|
+
stages: SynthesisStage[];
|
|
36
|
+
note: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STAGES: SynthesisStage[] = ["ingest", "chunk", "cluster", "synthesize", "wikify", "changelog"];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run the synthesis pipeline. STUB: walks the stage list, emits a `stage` event
|
|
43
|
+
* for each (marked skipped), and returns implemented:false. Deterministic and
|
|
44
|
+
* side-effect-free, so it's safe for the nightly job to call today.
|
|
45
|
+
*/
|
|
46
|
+
export async function runSynthesis(
|
|
47
|
+
opts: SynthesisOptions = {},
|
|
48
|
+
onEvent?: (e: SynthesisEvent) => void,
|
|
49
|
+
): Promise<SynthesisSummary> {
|
|
50
|
+
onEvent?.({
|
|
51
|
+
type: "log",
|
|
52
|
+
message: `memory synthesis is not implemented yet (M1)${opts.dryRun ? " · dry-run" : ""} — running as a no-op, nothing will be written.`,
|
|
53
|
+
});
|
|
54
|
+
for (const stage of STAGES) {
|
|
55
|
+
onEvent?.({ type: "stage", stage, message: `${stage}: stubbed (skipped)` });
|
|
56
|
+
}
|
|
57
|
+
onEvent?.({ type: "done", message: "no articles were written (synthesis is stubbed)." });
|
|
58
|
+
return {
|
|
59
|
+
implemented: false,
|
|
60
|
+
stages: STAGES,
|
|
61
|
+
note: "M1 stub — pipeline + scheduling wired; stage modules not implemented.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// mlx-bun memory — per-stage synthesis prompts. ⚠️ M1 STUBS.
|
|
2
|
+
//
|
|
3
|
+
// Each stage's prompt is the seam where a future specialized memory LoRA plugs
|
|
4
|
+
// in (today: base model + prompt; later: adapterScoped per stage). Editorial
|
|
5
|
+
// conventions are NOT duplicated here — they live in the vault's Meta/ pages
|
|
6
|
+
// (Editorial_Guidelines.md, Article_Conventions.md, Buckets.md,
|
|
7
|
+
// Topics_to_Ignore.md) and are read in at synthesis time so the system governs
|
|
8
|
+
// itself in its own substrate.
|
|
9
|
+
//
|
|
10
|
+
// See docs/design/memory-system.md → "Synthesis runs on the local model".
|
|
11
|
+
|
|
12
|
+
export const CHUNK_PROMPT = `TODO(M1): segment a conversation transcript into topic-coherent chunks.`;
|
|
13
|
+
|
|
14
|
+
export const CLUSTER_PROMPT = `TODO(M1): assign a chunk to an existing bucket from Meta/Buckets.md, or propose a new bucket.`;
|
|
15
|
+
|
|
16
|
+
export const SYNTHESIZE_PROMPT = `TODO(M1): integrate new chunks into the bucket's article. Preserve prose and every conv: citation; integrate, don't overwrite; respect the ≥70% word-count floor and the editorial gate.`;
|
|
17
|
+
|
|
18
|
+
/** Placeholder registry so the pipeline can reference prompts by stage name. */
|
|
19
|
+
export const STAGE_PROMPTS: Record<string, string> = {
|
|
20
|
+
chunk: CHUNK_PROMPT,
|
|
21
|
+
cluster: CLUSTER_PROMPT,
|
|
22
|
+
synthesize: SYNTHESIZE_PROMPT,
|
|
23
|
+
};
|