sloptimize 0.3.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/bin/sloptimize.mjs +346 -0
- package/docs/DESIGN-mecharoyale-v0.md +32 -0
- package/docs/INTEGRATION.md +230 -0
- package/docs/JITTER-AND-FOOTPRINTS.md +236 -0
- package/docs/SPEC-attach.md +176 -0
- package/docs/SPEC.md +845 -0
- package/docs/USAGE.md +227 -0
- package/hooks/hooks.json +16 -0
- package/mcp/server.mjs +127 -0
- package/package.json +64 -0
- package/skills/install/SKILL.md +143 -0
- package/skills/sloptimize/SKILL.md +74 -0
- package/src/attach.mjs +197 -0
- package/src/census.js +193 -0
- package/src/classify.js +70 -0
- package/src/footprint.js +170 -0
- package/src/history.js +273 -0
- package/src/index.js +8 -0
- package/src/inject-body.js +152 -0
- package/src/motion.js +345 -0
- package/src/panel.js +530 -0
- package/src/proposals.mjs +268 -0
- package/src/recorder.js +235 -0
- package/src/watch.mjs +242 -0
package/src/watch.mjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// watch.mjs — the push channel (SPEC §8.1.1): perf.jsonl → agent wake events
|
|
3
|
+
// ============================================================
|
|
4
|
+
// The recorder is already the auto-detector; this is the other half of
|
|
5
|
+
// "auto identifies bottleneck, sends signal to claude code, I just play".
|
|
6
|
+
// A byte cursor over each watched ledger, polled on an interval, printing
|
|
7
|
+
// ONE line per record an agent should act on — and nothing else, because
|
|
8
|
+
// every line is a conversation message to whoever armed the watch
|
|
9
|
+
// (Claude Code's Monitor primitive, INTEGRATION.md §5).
|
|
10
|
+
//
|
|
11
|
+
// Wakes: any usermark · an auto hitch ≥ minHitchMs (default 100) · a
|
|
12
|
+
// gpu-settle that hit its cap without settling · a warm run whose worst
|
|
13
|
+
// batch ≥ minHitchMs (with its per-batch builds) · any gpu-stall
|
|
14
|
+
// record · a coordinate jitter (a snap or an oscillation of the unit or the
|
|
15
|
+
// camera, SPEC §3.6) · the feed going quiet (> staleMin without a record)
|
|
16
|
+
// and coming back. Silent: heartbeats, arm-probes, sub-threshold hitches,
|
|
17
|
+
// a jitter whose verdict is long-frame-catch-up (the stall it rode already
|
|
18
|
+
// woke as a hitch) or one explained as the passenger of another track.
|
|
19
|
+
//
|
|
20
|
+
// Starts at EOF: the ledger's history is the report's business, not a wake
|
|
21
|
+
// storm at arm time. Cursor is in memory, per watcher — two sessions
|
|
22
|
+
// watching the same directory each see every record (USAGE.md "who gets
|
|
23
|
+
// told? all of them"); nothing on disk to race over.
|
|
24
|
+
import { existsSync, openSync, readSync, closeSync, fstatSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { footprintOf } from './footprint.js';
|
|
27
|
+
|
|
28
|
+
const DEFAULTS = { minHitchMs: 100, staleMin: 45, now: () => Date.now() };
|
|
29
|
+
|
|
30
|
+
/** The line an agent is woken with for one record, or null when the record
|
|
31
|
+
* is not worth a wake. Pure; the classification vocabulary is the
|
|
32
|
+
* recorder's own, forwarded verbatim (SPEC §8.1.1: the schema is a PUSH
|
|
33
|
+
* CONTRACT, fields surface directly). */
|
|
34
|
+
export function wakeLine(rec, dir, opts = {}) {
|
|
35
|
+
// A robot's session is not an incident: headless verifies and preview
|
|
36
|
+
// health checks arm the recorder and post real-looking records, and a
|
|
37
|
+
// 16-minute cadence of them woke the agent all day (2026-08-28). The
|
|
38
|
+
// report still lists them; the wake channel is for a human's session.
|
|
39
|
+
if (rec.automated === true) return null;
|
|
40
|
+
const minHitchMs = opts.minHitchMs ?? DEFAULTS.minHitchMs;
|
|
41
|
+
const where = `[${dir}]`;
|
|
42
|
+
// The footprint (SPEC §3.7) and how many times this cause has been seen on
|
|
43
|
+
// this ledger — the watcher's count when it keeps one, else nothing: the
|
|
44
|
+
// number is a fact about the ledger, never a guess.
|
|
45
|
+
const fp = footprintOf(rec);
|
|
46
|
+
const seen = fp && opts.counts ? opts.counts.get(fp.id) : undefined;
|
|
47
|
+
const fpTxt = fp ? ` fp=${fp.id}${seen ? ` ×${seen}` : ''}` : '';
|
|
48
|
+
const ctx = [rec.phase && `phase=${rec.phase}`, rec.ctx && `ctx=${rec.ctx}`, rec.build && `build=${rec.build}`].filter(Boolean).join(' ') + fpTxt;
|
|
49
|
+
const cls = (c) => c?.[0] ? `${c[0].guess} (${c[0].evidence})` : 'unclassified';
|
|
50
|
+
switch (rec.type) {
|
|
51
|
+
case 'usermark': {
|
|
52
|
+
const w = rec.worstFrames?.[0];
|
|
53
|
+
return `sloptimize ★ usermark "${rec.note ?? 'Ctrl+F12'}" @ ${rec.at}: window ${rec.window?.frames}f median ${rec.window?.medianMs}ms; worst ${w?.frameMs}ms → ${cls(w?.classification)} ${ctx} ${where}`;
|
|
54
|
+
}
|
|
55
|
+
case 'hitch':
|
|
56
|
+
if (!(rec.frameMs >= minHitchMs)) return null;
|
|
57
|
+
return `sloptimize ⚡ hitch ${round(rec.frameMs)}ms (median ${rec.medianMs}ms, ${round(rec.insideRenderMs)}ms in render) @ ${rec.at} → ${cls(rec.classification)}${mints(rec)} ${ctx} ${where}`;
|
|
58
|
+
case 'gpu-settle':
|
|
59
|
+
// A settled wait is the verification channel (the freeze stayed behind
|
|
60
|
+
// the cover) — the report's business. Only a cap hit is an incident.
|
|
61
|
+
if (rec.settled) return null;
|
|
62
|
+
return `sloptimize ⏳ gpu-settle ${rec.tag} ${rec.ms}ms NOT settled (cap hit) @ ${rec.at} ${ctx} ${where}`;
|
|
63
|
+
case 'warm': {
|
|
64
|
+
// A warm run whose worst batch crossed the hitch bar: the sweep IS the
|
|
65
|
+
// freeze, and the per-batch builds say whether one build cost that or
|
|
66
|
+
// the batcher packed many into one task.
|
|
67
|
+
if (!(rec.worstBatchMs >= minHitchMs)) return null;
|
|
68
|
+
// A warm in a hidden/unfocused tab is not an incident: no frame was
|
|
69
|
+
// drawn around it, and its wall clock is the browser's throttling.
|
|
70
|
+
if (rec.hidden === true) return null;
|
|
71
|
+
const built = Array.isArray(rec.batchBuilt) && rec.batchBuilt.length ? ` builds/batch=[${rec.batchBuilt.join(',')}]` : '';
|
|
72
|
+
return `sloptimize 🔥 warm ${rec.tag} (${rec.kind}, budget ${rec.budgetMs ?? 'atomic'}ms): ${rec.keys} key(s) in ${rec.batches} batch(es), worst ${round(rec.worstBatchMs)}ms${built}${rec.costliest ? ` — ${rec.costliest}` : ''} @ ${rec.at} ${ctx} ${where}`;
|
|
73
|
+
}
|
|
74
|
+
case 'gpu-stall':
|
|
75
|
+
return `sloptimize ⏳ gpu-stall ${rec.queueDoneMs}ms @ ${rec.at} → ${cls(rec.classification)} ${ctx} ${where}`;
|
|
76
|
+
case 'jitter': {
|
|
77
|
+
// The unit/camera landed off its own trajectory (SPEC §3.6). Not an
|
|
78
|
+
// incident of its own when the stall it rode already woke (the catch-up
|
|
79
|
+
// verdict), nor when it merely followed the track it is attached to —
|
|
80
|
+
// that track's record wakes, and names it as coincident.
|
|
81
|
+
const g = rec.classification?.[0]?.guess;
|
|
82
|
+
if (g === 'long-frame-catch-up' || g === 'follows-track') return null;
|
|
83
|
+
const shape = rec.kind === 'oscillation'
|
|
84
|
+
? `oscillation ×${rec.frames} over ${rec.durationMs}ms, amplitude ${rec.amplitude}`
|
|
85
|
+
: `snap ${rec.units} (jump [${(rec.jump ?? []).join(', ')}], expected ${rec.travelUnits} of travel at ${rec.speed}/s in a ${rec.dtMs}ms frame)`;
|
|
86
|
+
const with_ = Array.isArray(rec.coincident) && rec.coincident.length ? ` with=${rec.coincident.join(',')}` : '';
|
|
87
|
+
return `sloptimize ↯ jitter ${rec.track} ${shape}${with_} @ ${rec.at} → ${cls(rec.classification)} ${ctx} ${where}`;
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The mints a hitch carries, each with WHY it minted when the record says
|
|
95
|
+
* (`changed`: the cache-key parts that moved since that material's previous
|
|
96
|
+
* mint; `[]` = same key compiled again; absent = first mint). */
|
|
97
|
+
function mints(rec) {
|
|
98
|
+
if (!Array.isArray(rec.mints) || rec.mints.length === 0) return '';
|
|
99
|
+
const one = (m) => {
|
|
100
|
+
const why = Array.isArray(m.changed) ? (m.changed.length ? ` changed:${m.changed.join(',')}` : ' re-minted:same-key') : '';
|
|
101
|
+
return `${m.material}@${m.object}${why}`;
|
|
102
|
+
};
|
|
103
|
+
return ` mints=[${rec.mints.map(one).join('; ')}]`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function round(n) { return typeof n === 'number' ? +n.toFixed(1) : n; }
|
|
107
|
+
|
|
108
|
+
/** One ledger's byte cursor. Reads only what was appended since last time,
|
|
109
|
+
* holds an unterminated tail until its newline lands (the sink appends a
|
|
110
|
+
* whole batch per post, but a poll can still land mid-write), and resets
|
|
111
|
+
* when the file shrinks (rotated or recreated). */
|
|
112
|
+
function ledgerCursor(path) {
|
|
113
|
+
// Positioned at CREATION, not at the first poll: anything appended between
|
|
114
|
+
// arming and the first tick is new and must wake. A ledger that does not
|
|
115
|
+
// exist yet starts at 0 so its first record wakes too.
|
|
116
|
+
let { size: offset, mtimeMs } = fileStat(path);
|
|
117
|
+
let tail = '';
|
|
118
|
+
return function readNew() {
|
|
119
|
+
if (!existsSync(path)) { offset = 0; mtimeMs = 0; tail = ''; return []; }
|
|
120
|
+
const fd = openSync(path, 'r');
|
|
121
|
+
try {
|
|
122
|
+
const st = fstatSync(fd);
|
|
123
|
+
const size = st.size;
|
|
124
|
+
// Shrunk: rotated or recreated. Same size but touched: rewritten — an
|
|
125
|
+
// append always grows the file, so this can only be a rewrite (a
|
|
126
|
+
// rotation that happened to land on the same byte count). Stated
|
|
127
|
+
// limit: a same-size rewrite inside the same mtime tick as the last
|
|
128
|
+
// read is invisible to stat — not a shape an append-only ledger has.
|
|
129
|
+
if (size < offset || (size === offset && offset > 0 && st.mtimeMs !== mtimeMs)) { offset = 0; tail = ''; }
|
|
130
|
+
mtimeMs = st.mtimeMs;
|
|
131
|
+
if (size === offset) return [];
|
|
132
|
+
const buf = Buffer.alloc(size - offset);
|
|
133
|
+
const n = readSync(fd, buf, 0, buf.length, offset);
|
|
134
|
+
offset += n;
|
|
135
|
+
const chunk = tail + buf.toString('utf8', 0, n);
|
|
136
|
+
const parts = chunk.split('\n');
|
|
137
|
+
tail = parts.pop();
|
|
138
|
+
return parts.filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
139
|
+
} finally { closeSync(fd); }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A watcher over one or more `.sloptimize` directories. `poll()` returns
|
|
144
|
+
* the wake lines since the previous poll — the CLI prints them, a test
|
|
145
|
+
* asserts on them. */
|
|
146
|
+
export function createWatcher(dirs, opts = {}) {
|
|
147
|
+
const o = { ...DEFAULTS, ...opts };
|
|
148
|
+
const ledgers = dirs.map((dir) => ({
|
|
149
|
+
dir,
|
|
150
|
+
read: ledgerCursor(join(dir, 'perf.jsonl')),
|
|
151
|
+
lastAt: 0, // ms of the newest record seen (any type)
|
|
152
|
+
dark: false,
|
|
153
|
+
// Occurrences per footprint on this ledger so far (SPEC §3.7): seeded from
|
|
154
|
+
// the file at arm, advanced per record — the `×N` on every wake line.
|
|
155
|
+
counts: footprintCounts(join(dir, 'perf.jsonl')),
|
|
156
|
+
}));
|
|
157
|
+
// Seed liveness from what is already on disk so a ledger that died before
|
|
158
|
+
// the watch was armed still reports quiet — once.
|
|
159
|
+
for (const l of ledgers) l.lastAt = lastRecordTime(join(l.dir, 'perf.jsonl'));
|
|
160
|
+
|
|
161
|
+
function poll() {
|
|
162
|
+
const lines = [];
|
|
163
|
+
for (const l of ledgers) {
|
|
164
|
+
let recs;
|
|
165
|
+
try { recs = l.read(); } catch { recs = []; } // a transient read error is not a wake
|
|
166
|
+
for (const r of recs) {
|
|
167
|
+
const t = Date.parse(r.at);
|
|
168
|
+
if (t > l.lastAt) l.lastAt = t;
|
|
169
|
+
const fp = footprintOf(r);
|
|
170
|
+
if (fp) l.counts.set(fp.id, (l.counts.get(fp.id) ?? 0) + 1);
|
|
171
|
+
const w = wakeLine(r, l.dir, { ...o, counts: l.counts });
|
|
172
|
+
if (w) lines.push(w);
|
|
173
|
+
}
|
|
174
|
+
if (l.lastAt > 0) {
|
|
175
|
+
const ageMin = (o.now() - l.lastAt) / 60_000;
|
|
176
|
+
if (ageMin > o.staleMin && !l.dark) {
|
|
177
|
+
l.dark = true;
|
|
178
|
+
lines.push(`sloptimize ◌ feed quiet ${Math.round(ageMin)}min [${l.dir}] — session over, or the feed went dark (ingest disarmed?)`);
|
|
179
|
+
} else if (ageMin <= o.staleMin && l.dark) {
|
|
180
|
+
l.dark = false;
|
|
181
|
+
lines.push(`sloptimize ◉ feed live again [${l.dir}]`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
return { poll };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Occurrences per footprint id over a whole ledger — read once at arm. A
|
|
191
|
+
* ledger is megabytes at most; the count is what makes "this again" a
|
|
192
|
+
* number on the wake line instead of a feeling. */
|
|
193
|
+
function footprintCounts(path) {
|
|
194
|
+
const counts = new Map();
|
|
195
|
+
if (!existsSync(path)) return counts;
|
|
196
|
+
let text;
|
|
197
|
+
try { text = readFileSync(path, 'utf8'); } catch { return counts; }
|
|
198
|
+
for (const line of text.split('\n')) {
|
|
199
|
+
if (!line) continue;
|
|
200
|
+
let r; try { r = JSON.parse(line); } catch { continue; }
|
|
201
|
+
const fp = footprintOf(r);
|
|
202
|
+
if (fp) counts.set(fp.id, (counts.get(fp.id) ?? 0) + 1);
|
|
203
|
+
}
|
|
204
|
+
return counts;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fileStat(path) {
|
|
208
|
+
if (!existsSync(path)) return { size: 0, mtimeMs: 0 };
|
|
209
|
+
const fd = openSync(path, 'r');
|
|
210
|
+
try { const st = fstatSync(fd); return { size: st.size, mtimeMs: st.mtimeMs }; } finally { closeSync(fd); }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Timestamp (ms) of the last parseable record in a ledger, 0 if none.
|
|
214
|
+
* Reads only the final few KB — the ledger can be megabytes. */
|
|
215
|
+
function lastRecordTime(path) {
|
|
216
|
+
if (!existsSync(path)) return 0;
|
|
217
|
+
const fd = openSync(path, 'r');
|
|
218
|
+
try {
|
|
219
|
+
const size = fstatSync(fd).size;
|
|
220
|
+
const span = Math.min(size, 64 * 1024);
|
|
221
|
+
const buf = Buffer.alloc(span);
|
|
222
|
+
readSync(fd, buf, 0, span, size - span);
|
|
223
|
+
const lines = buf.toString('utf8').split('\n').filter(Boolean);
|
|
224
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
225
|
+
try { const t = Date.parse(JSON.parse(lines[i]).at); if (t > 0) return t; } catch { /* partial or foreign line */ }
|
|
226
|
+
}
|
|
227
|
+
return 0;
|
|
228
|
+
} finally { closeSync(fd); }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The CLI loop: poll every `intervalMs`, print each wake line, never exit
|
|
232
|
+
* (Monitor semantics: exit ends the watch). Flushes per line — stdout is
|
|
233
|
+
* the event stream. */
|
|
234
|
+
export async function runWatch(dirs, opts = {}) {
|
|
235
|
+
const intervalMs = opts.intervalMs ?? 20_000;
|
|
236
|
+
const w = createWatcher(dirs, opts);
|
|
237
|
+
process.stdout.write(`sloptimize watch armed: ${dirs.join(', ')} (every ${Math.round(intervalMs / 1000)}s; hitches ≥${opts.minHitchMs ?? DEFAULTS.minHitchMs}ms, every usermark)\n`);
|
|
238
|
+
for (;;) {
|
|
239
|
+
for (const line of w.poll()) process.stdout.write(line + '\n');
|
|
240
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
241
|
+
}
|
|
242
|
+
}
|