mlx-bun 0.0.4 → 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 +175 -93
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/adapters-end-to-end.md +50 -46
- package/docs/design/compat-cli-surface-design.md +10 -8
- package/docs/design/docs-reorg-plan.md +14 -1
- package/docs/design/hlg-sampling.md +12 -2
- package/docs/design/memory-system.md +193 -295
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +340 -0
- package/docs/design/orpo-training.md +1212 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/runtime-isolation.md +1 -1
- package/docs/design/segmented-backward-training.md +58 -39
- package/docs/design/tauri-desktop-app.md +127 -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/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
- package/docs/investigations/starter-model-port-handoff.md +32 -13
- package/docs/investigations/steel-flash-cce-handoff.md +489 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/planning/IDEAS.md +29 -0
- package/docs/planning/PRODUCT_ROADMAP.md +147 -27
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/journal.md +161 -0
- package/docs/reference/distribution.md +61 -5
- package/docs/reference/library-api.md +29 -14
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +130 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +296 -0
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/ifeval.ts +138 -0
- 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/eval/ultrafeedback.ts +60 -0
- package/src/jobs/runner.ts +6 -0
- package/src/lora.ts +12 -7
- 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 +31 -4
- package/src/model/flash-attention.ts +37 -19
- package/src/model/fused-geglu-kernel.ts +111 -0
- package/src/model/gemma4-base.ts +39 -1
- package/src/model/gemma4.ts +196 -21
- package/src/model/minicpm5.ts +44 -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 +216 -47
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +29 -6
- package/src/train/flash-cce.ts +1175 -0
- package/src/train/job.ts +64 -3
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +947 -0
- package/src/train/optimizer.ts +10 -3
- package/src/train/prefix-shared.ts +435 -0
- package/src/train/rank.ts +36 -0
- package/src/train/segmented.ts +1148 -3
- package/src/train/steel-qmm-header.ts +1461 -0
- package/src/train/trainer.ts +570 -54
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +285 -19
- 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/{docs → src/assets}/curve-designer.html +0 -0
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
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// mlx-bun memory — nightly scheduling via launchd. ✅ REAL (the pipeline it
|
|
2
|
+
// runs is the M1 stub, but install/remove/status here are fully functional).
|
|
3
|
+
//
|
|
4
|
+
// mlx-bun is macOS/Apple-silicon-only by identity, so launchd is the native
|
|
5
|
+
// choice over crontab: it survives reboot, runs jobs missed while asleep on
|
|
6
|
+
// next wake, and gives clean install/uninstall. `memory schedule` writes
|
|
7
|
+
// ~/Library/LaunchAgents/com.mlx-bun.memory.plist (a StartCalendarInterval that
|
|
8
|
+
// runs `mlx-bun memory synthesize`) and loads it; `unschedule` unloads + removes
|
|
9
|
+
// it. Today that job is a no-op (synthesis is stubbed) — harmless; when M1 lands
|
|
10
|
+
// the same schedule starts producing articles with no reconfiguration.
|
|
11
|
+
//
|
|
12
|
+
// See docs/design/memory-system.md → "Scheduling (launchd)".
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
export const LAUNCHD_LABEL = "com.mlx-bun.memory";
|
|
21
|
+
|
|
22
|
+
export function plistPath(): string {
|
|
23
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function logsDir(): string {
|
|
27
|
+
return join(homedir(), ".mlx-bun", "logs");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve how to invoke mlx-bun from launchd's minimal environment. Prefer the
|
|
32
|
+
* installed binary on PATH; fall back to the current dev invocation
|
|
33
|
+
* (`<bun> <cli.ts>`). Wrapped in a login shell at call time so PATH/bun resolve.
|
|
34
|
+
*/
|
|
35
|
+
function mlxBunCommand(): string {
|
|
36
|
+
const onPath = Bun.which("mlx-bun");
|
|
37
|
+
if (onPath) return shQuote(onPath);
|
|
38
|
+
// Dev: process.execPath is bun, argv[1] is .../src/cli.ts.
|
|
39
|
+
return `${shQuote(process.execPath)} ${shQuote(process.argv[1] ?? "")}`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shQuote(s: string): string {
|
|
43
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function xmlEscape(s: string): string {
|
|
47
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ScheduleOptions {
|
|
51
|
+
/** Local wall-clock time, "HH:MM" 24h. Default 03:00. */
|
|
52
|
+
at?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parse "HH:MM" → {hour, minute}; defaults to 03:00 on bad/empty input. */
|
|
56
|
+
export function parseAt(at?: string): { hour: number; minute: number } {
|
|
57
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec((at ?? "").trim());
|
|
58
|
+
if (!m) return { hour: 3, minute: 0 };
|
|
59
|
+
const hour = Math.min(23, Math.max(0, Number(m[1])));
|
|
60
|
+
const minute = Math.min(59, Math.max(0, Number(m[2])));
|
|
61
|
+
return { hour, minute };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Pure: build the launchd plist XML for the nightly synthesis job. */
|
|
65
|
+
export function buildPlist(opts: ScheduleOptions = {}): string {
|
|
66
|
+
const { hour, minute } = parseAt(opts.at);
|
|
67
|
+
// Login shell so the user's PATH (bun, mlx-bun) resolves under launchd.
|
|
68
|
+
const cmd = `exec ${mlxBunCommand()} memory synthesize`;
|
|
69
|
+
const out = join(logsDir(), "memory-synthesis.out.log");
|
|
70
|
+
const err = join(logsDir(), "memory-synthesis.err.log");
|
|
71
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
72
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
73
|
+
<plist version="1.0">
|
|
74
|
+
<dict>
|
|
75
|
+
<key>Label</key>
|
|
76
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
77
|
+
|
|
78
|
+
<key>ProgramArguments</key>
|
|
79
|
+
<array>
|
|
80
|
+
<string>/bin/zsh</string>
|
|
81
|
+
<string>-lc</string>
|
|
82
|
+
<string>${xmlEscape(cmd)}</string>
|
|
83
|
+
</array>
|
|
84
|
+
|
|
85
|
+
<!-- Local wall-clock; if asleep at the time, launchd runs on next wake. -->
|
|
86
|
+
<key>StartCalendarInterval</key>
|
|
87
|
+
<dict>
|
|
88
|
+
<key>Hour</key>
|
|
89
|
+
<integer>${hour}</integer>
|
|
90
|
+
<key>Minute</key>
|
|
91
|
+
<integer>${minute}</integer>
|
|
92
|
+
</dict>
|
|
93
|
+
|
|
94
|
+
<key>RunAtLoad</key>
|
|
95
|
+
<false/>
|
|
96
|
+
|
|
97
|
+
<key>StandardOutPath</key>
|
|
98
|
+
<string>${xmlEscape(out)}</string>
|
|
99
|
+
<key>StandardErrorPath</key>
|
|
100
|
+
<string>${xmlEscape(err)}</string>
|
|
101
|
+
|
|
102
|
+
<key>ProcessType</key>
|
|
103
|
+
<string>Background</string>
|
|
104
|
+
</dict>
|
|
105
|
+
</plist>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function launchctl(args: string[]): Promise<boolean> {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const proc = spawn("launchctl", args, { stdio: "ignore" });
|
|
112
|
+
proc.on("exit", (code) => resolve(code === 0));
|
|
113
|
+
proc.on("error", () => resolve(false));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface InstallResult {
|
|
118
|
+
plistPath: string;
|
|
119
|
+
at: { hour: number; minute: number };
|
|
120
|
+
loaded: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Write the plist and (re)load it into launchd. */
|
|
124
|
+
export async function installSchedule(opts: ScheduleOptions = {}): Promise<InstallResult> {
|
|
125
|
+
const path = plistPath();
|
|
126
|
+
await mkdir(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
127
|
+
await mkdir(logsDir(), { recursive: true });
|
|
128
|
+
await writeFile(path, buildPlist(opts));
|
|
129
|
+
// Reload: unload first (ignore failure if not loaded), then load -w.
|
|
130
|
+
await launchctl(["unload", path]);
|
|
131
|
+
const loaded = await launchctl(["load", "-w", path]);
|
|
132
|
+
return { plistPath: path, at: parseAt(opts.at), loaded };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Unload and remove the launchd agent. Returns true if a plist was removed. */
|
|
136
|
+
export async function removeSchedule(): Promise<boolean> {
|
|
137
|
+
const path = plistPath();
|
|
138
|
+
if (!existsSync(path)) return false;
|
|
139
|
+
await launchctl(["unload", "-w", path]);
|
|
140
|
+
await rm(path, { force: true });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ScheduleStatus {
|
|
145
|
+
installed: boolean;
|
|
146
|
+
plistPath: string;
|
|
147
|
+
/** Whether launchctl currently lists the agent (best-effort). */
|
|
148
|
+
loaded: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function scheduleStatus(): Promise<ScheduleStatus> {
|
|
152
|
+
const path = plistPath();
|
|
153
|
+
const installed = existsSync(path);
|
|
154
|
+
let loaded = false;
|
|
155
|
+
if (installed) {
|
|
156
|
+
loaded = await new Promise<boolean>((resolve) => {
|
|
157
|
+
const proc = spawn("launchctl", ["list", LAUNCHD_LABEL], { stdio: "ignore" });
|
|
158
|
+
proc.on("exit", (code) => resolve(code === 0));
|
|
159
|
+
proc.on("error", () => resolve(false));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return { installed, plistPath: path, loaded };
|
|
163
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis stage 4: SYNTHESIZE. ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// For each bucket touched this run, integrate its new chunks into the bucket's
|
|
4
|
+
// article (local-model call) — create-or-update, conservatively. The hard,
|
|
5
|
+
// proven gate is inherited from lucien: preserve prose, ≥70% word-count floor,
|
|
6
|
+
// every original conv: citation survives, integrate rather than overwrite. A
|
|
7
|
+
// weak local pass is gated to NO-OP rather than allowed to corrupt the vault.
|
|
8
|
+
// Mirrors lucien's synthesize-dispatch.ts / synthesize-update.ts.
|
|
9
|
+
//
|
|
10
|
+
// See docs/design/memory-system.md → "Synthesis runs on the local model".
|
|
11
|
+
|
|
12
|
+
import type { SynthesisEvent } from "./pipeline";
|
|
13
|
+
|
|
14
|
+
export interface SynthesizeResult {
|
|
15
|
+
articlesCreated: number;
|
|
16
|
+
articlesUpdated: number;
|
|
17
|
+
skippedByGate: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Bucket → article create/update + safety gate (local model). STUB. */
|
|
21
|
+
export async function synthesizeBuckets(
|
|
22
|
+
_model: unknown,
|
|
23
|
+
_onEvent?: (e: SynthesisEvent) => void,
|
|
24
|
+
): Promise<SynthesizeResult> {
|
|
25
|
+
// TODO(M1): per touched bucket, feed unsynthesized chunks + current article
|
|
26
|
+
// through the synthesize prompt; apply the conservative gate; write the
|
|
27
|
+
// article; record (bucket, chunk_id) in synthesized_bucket_chunks.
|
|
28
|
+
throw new Error("memory.synthesize is not implemented yet (M1)");
|
|
29
|
+
}
|