@otto-code/brain 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/bench/context-corpus.js +3 -3
  2. package/dist/bench/corpus.js +2 -2
  3. package/dist/bench/curated-repos.js +3 -3
  4. package/dist/bench/health.d.ts +1 -1
  5. package/dist/bench/health.js +2 -2
  6. package/dist/bench/tasks.js +2 -2
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/commands/bench.d.ts +1 -1
  10. package/dist/commands/bench.js +27 -4
  11. package/dist/commands/calibrate.d.ts +1 -1
  12. package/dist/commands/calibrate.js +5 -2
  13. package/dist/commands/catalog.d.ts +1 -1
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/lifecycle.js +1 -1
  16. package/dist/commands/pull.d.ts +1 -1
  17. package/dist/commands/pull.js +9 -4
  18. package/dist/commands/report.d.ts +1 -1
  19. package/dist/commands/rescore.d.ts +1 -1
  20. package/dist/commands/rescore.js +1 -1
  21. package/dist/commands/runtime.d.ts +1 -1
  22. package/dist/commands/scan.d.ts +1 -1
  23. package/dist/commands/scan.js +6 -1
  24. package/dist/commands/search.d.ts +1 -1
  25. package/dist/commands/share.js +2 -2
  26. package/dist/commands/sweep.d.ts +2 -2
  27. package/dist/commands/sweep.js +22 -8
  28. package/dist/commands/ui.d.ts +1 -1
  29. package/dist/config/index.d.ts +2 -1
  30. package/dist/config/index.js +2 -1
  31. package/dist/config/otto-home.js +1 -1
  32. package/dist/config/paths.d.ts +1 -0
  33. package/dist/config/paths.js +4 -0
  34. package/dist/config/profile-edit.d.ts +94 -0
  35. package/dist/config/profile-edit.js +269 -0
  36. package/dist/config/profiles.d.ts +2 -2
  37. package/dist/config/profiles.js +1 -1
  38. package/dist/config/schema.d.ts +4 -4
  39. package/dist/config/schema.js +6 -6
  40. package/dist/config/store.d.ts +1 -1
  41. package/dist/config/store.js +1 -1
  42. package/dist/models/download.js +1 -1
  43. package/dist/models/enrich.d.ts +2 -2
  44. package/dist/models/index.d.ts +1 -1
  45. package/dist/models/index.js +1 -1
  46. package/dist/models/pick.d.ts +9 -0
  47. package/dist/models/pick.js +24 -1
  48. package/dist/ops/report.js +28 -28
  49. package/dist/ops/results.d.ts +128 -9
  50. package/dist/ops/results.js +77 -5
  51. package/dist/output/render.js +1 -1
  52. package/dist/output/types.d.ts +1 -1
  53. package/dist/runtime/args.d.ts +1 -1
  54. package/dist/runtime/args.js +1 -1
  55. package/dist/runtime/managed.js +4 -4
  56. package/dist/service/activity.d.ts +83 -0
  57. package/dist/service/activity.js +216 -0
  58. package/dist/service/host-api.d.ts +132 -0
  59. package/dist/service/host-api.js +397 -0
  60. package/dist/service/http-util.d.ts +27 -0
  61. package/dist/service/http-util.js +72 -0
  62. package/dist/service/model-selector.d.ts +2 -2
  63. package/dist/service/model-selector.js +6 -6
  64. package/dist/service/router.d.ts +28 -4
  65. package/dist/service/router.js +128 -94
  66. package/dist/service/scheduler.d.ts +2 -2
  67. package/dist/service/scheduler.js +1 -1
  68. package/dist/service/serve.d.ts +2 -2
  69. package/dist/service/serve.js +51 -8
  70. package/dist/service/supervisor.d.ts +6 -0
  71. package/dist/service/supervisor.js +2 -0
  72. package/dist/service/tailscale.js +1 -1
  73. package/dist/service/tls.d.ts +4 -4
  74. package/dist/service/tls.js +3 -3
  75. package/dist/sysmon.d.ts +20 -4
  76. package/dist/sysmon.js +42 -18
  77. package/dist/tui/app.d.ts +12 -2
  78. package/dist/tui/app.js +46 -22
  79. package/dist/vram.d.ts +8 -1
  80. package/dist/vram.js +6 -3
  81. package/package.json +1 -1
@@ -133,7 +133,7 @@ function padTo(body, budgetChars, seed) {
133
133
  }
134
134
  /** Build one spec part covering `stages`, padded to `budgetChars`. */
135
135
  function specPart(partIndex, stages, budgetChars) {
136
- const heading = `# Pipeline specification part ${String(partIndex).padStart(2, "0")}\n\n` +
136
+ const heading = `# Pipeline specification - part ${String(partIndex).padStart(2, "0")}\n\n` +
137
137
  `This part defines the rule for stages ${stages[0].index}–${stages[stages.length - 1].index}. ` +
138
138
  `Each stage's module in the working copy is a passthrough placeholder; implement its \`apply(x)\` ` +
139
139
  `exactly as written here. All arithmetic is bounded modulo 1000 and returns an integer.\n`;
@@ -176,8 +176,8 @@ export function generateContextCorpus({ targetTokens }) {
176
176
  "`python -m unittest test_pipeline`.\n";
177
177
  files["docs/OVERVIEW.md"] =
178
178
  "# Specification overview\n\nThe pipeline applies each stage's `apply(x)` in order, starting " +
179
- `from ${PIPELINE_START}. There are ${stageCount} stages. The rule for each stage its ` +
180
- "operation and operand is defined in the numbered parts under `docs/spec/`. The code carries " +
179
+ `from ${PIPELINE_START}. There are ${stageCount} stages. The rule for each stage - its ` +
180
+ "operation and operand - is defined in the numbered parts under `docs/spec/`. The code carries " +
181
181
  "no hint of the operands; you must read the spec.\n";
182
182
  for (let p = 0; p < partCount; p += 1) {
183
183
  const slice = stages.slice(p * partStride, (p + 1) * partStride);
@@ -193,7 +193,7 @@ Rules are applied in **descending priority** (highest \`priority\` first). Two
193
193
  rules with equal priority may be applied in any order; the specification is
194
194
  written so that the result does not depend on the order of equal-priority rules.
195
195
 
196
- ### Base amount the load-bearing rule
196
+ ### Base amount - the load-bearing rule
197
197
 
198
198
  Every rule's discount is computed against the **original subtotal**, NOT against
199
199
  the running total after earlier rules. Discounts do **not** compound. A 30% rule
@@ -203,7 +203,7 @@ and a 40% rule on a $100.00 order remove \`$30.00 + $40.00 = $70.00\`, they do
203
203
  This is the single most common mistake when re-implementing the engine: applying
204
204
  each discount to the amount left by the previous one. Do not do that.
205
205
 
206
- ### Combined cap the other load-bearing rule
206
+ ### Combined cap - the other load-bearing rule
207
207
 
208
208
  The **sum** of all rule discounts is capped at **50% of the original subtotal**.
209
209
  Once the accumulated discount reaches half the subtotal, no further discount is
@@ -19,7 +19,7 @@ export const CURATED_REPOS = [
19
19
  workspaceDir: "packages/protocol",
20
20
  ref: "origin/main",
21
21
  maxTasks: 3,
22
- note: "schema/validation workspace fast suite, no heavy deps",
22
+ note: "schema/validation workspace - fast suite, no heavy deps",
23
23
  },
24
24
  {
25
25
  name: "brain",
@@ -27,7 +27,7 @@ export const CURATED_REPOS = [
27
27
  workspaceDir: "packages/brain",
28
28
  ref: "origin/main",
29
29
  maxTasks: 3,
30
- note: "the brain package itself pure-logic tests (vram, gguf, bench)",
30
+ note: "the brain package itself - pure-logic tests (vram, gguf, bench)",
31
31
  },
32
32
  ];
33
33
  /** Look up a curated preset by its handle. */
@@ -36,6 +36,6 @@ export function findCuratedRepo(name) {
36
36
  }
37
37
  /** A one-per-line listing of the available presets, for help/error text. */
38
38
  export function describeCuratedRepos() {
39
- return CURATED_REPOS.map((r) => ` ${r.name.padEnd(10)} ${r.workspace} ${r.note}`).join("\n");
39
+ return CURATED_REPOS.map((r) => ` ${r.name.padEnd(10)} ${r.workspace} - ${r.note}`).join("\n");
40
40
  }
41
41
  //# sourceMappingURL=curated-repos.js.map
@@ -42,7 +42,7 @@ export declare function summarize(samples: HealthSamples): HealthSummary;
42
42
  /**
43
43
  * Begin sampling; returns a handle with stop() that returns the summary.
44
44
  * Sampling failures (no GPU, nvidia-smi missing) degrade to empty aggregates
45
- * rather than throwing health is diagnostic, never a reason to fail a bench.
45
+ * rather than throwing - health is diagnostic, never a reason to fail a bench.
46
46
  */
47
47
  export declare function start({ intervalMs }?: {
48
48
  intervalMs?: number;
@@ -5,7 +5,7 @@ import { createCpuSampler } from "../sysmon.js";
5
5
  * System-health sampling for a benchmark run.
6
6
  *
7
7
  * A score is only trustworthy if the machine was in a comparable state when it
8
- * was measured a thermal-throttled or power-capped GPU produces a slower,
8
+ * was measured - a thermal-throttled or power-capped GPU produces a slower,
9
9
  * lower run that says nothing about the model. So we sample GPU/CPU/RAM through
10
10
  * the run and summarise it alongside the result, and flag when nvidia-smi itself
11
11
  * reports a thermal or power slowdown (more reliable than inferring one from a
@@ -81,7 +81,7 @@ export function summarize(samples) {
81
81
  /**
82
82
  * Begin sampling; returns a handle with stop() that returns the summary.
83
83
  * Sampling failures (no GPU, nvidia-smi missing) degrade to empty aggregates
84
- * rather than throwing health is diagnostic, never a reason to fail a bench.
84
+ * rather than throwing - health is diagnostic, never a reason to fail a bench.
85
85
  */
86
86
  export function start({ intervalMs = 1000 } = {}) {
87
87
  const samples = { gpu: [], cpu: [], ram: [] };
@@ -1037,7 +1037,7 @@ const contextStressTask = {
1037
1037
  * so the depth task stops probing rather than zeroing the whole category.
1038
1038
  */
1039
1039
  function isContextLimitError(message) {
1040
- // Specific to a context-length rejection a bare "context" would misclassify
1040
+ // Specific to a context-length rejection - a bare "context" would misclassify
1041
1041
  // any unrelated 500 whose body happens to mention the word.
1042
1042
  return /exceed|n_ctx|context (?:length|window|size)|too (?:many|long|large)|larger than|prompt is too long/i.test(message);
1043
1043
  }
@@ -1166,7 +1166,7 @@ const contextDepthTask = {
1166
1166
  };
1167
1167
  // ---------------------------------------------------------------- concurrency
1168
1168
  /**
1169
- * Aggregate throughput with several requests in flight at once the load a
1169
+ * Aggregate throughput with several requests in flight at once - the load a
1170
1170
  * shared host actually sees when multiple chats or a model's own sub-agents hit
1171
1171
  * it together. Reports tokens/sec in (prompt) and out (generation) summed across
1172
1172
  * the concurrent requests; the score is how many returned real content, which
package/dist/cli.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * a group factory (`createBrainCommand`) that adds straight into the main `otto`
4
4
  * program, and a `registerBrainCommands` that mounts the same verbs on a
5
5
  * standalone root program (for `bin/otto-brain` on a server without the full CLI).
6
- * Handlers stay pure they return typed results wrapped by withOutput.
6
+ * Handlers stay pure - they return typed results wrapped by withOutput.
7
7
  */
8
8
  import { Command } from "commander";
9
9
  /** Mount every brain verb on the given command (a root program or the group). */
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * a group factory (`createBrainCommand`) that adds straight into the main `otto`
4
4
  * program, and a `registerBrainCommands` that mounts the same verbs on a
5
5
  * standalone root program (for `bin/otto-brain` on a server without the full CLI).
6
- * Handlers stay pure they return typed results wrapped by withOutput.
6
+ * Handlers stay pure - they return typed results wrapped by withOutput.
7
7
  */
8
8
  import { Command } from "commander";
9
9
  import { addBenchOptions, runBenchCommand } from "./commands/bench.js";
@@ -15,6 +15,6 @@ interface BenchOptions {
15
15
  curated?: string;
16
16
  }
17
17
  export declare function addBenchOptions(cmd: Command): Command;
18
- export declare function runBenchCommand(options: BenchOptions, _command: Command): Promise<void>;
18
+ export declare function runBenchCommand(options: BenchOptions, command: Command): Promise<void>;
19
19
  export {};
20
20
  //# sourceMappingURL=bench.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain bench` score a model's agentic coding ability on this machine.
2
+ * `otto brain bench` - score a model's agentic coding ability on this machine.
3
3
  * Either benchmarks an endpoint that is already serving (`--endpoint`) or loads
4
4
  * each requested model itself. A long streaming run, so it prints a formatted
5
5
  * report directly rather than going through the output layer.
@@ -11,6 +11,7 @@ import { query as queryGpu } from "../gpu.js";
11
11
  import { pickModel, scanModels } from "../models/index.js";
12
12
  import { CommandError } from "../output/types.js";
13
13
  import { resolveRuntime } from "../runtime/index.js";
14
+ import { withActivity } from "../service/activity.js";
14
15
  import { createRouter, Telemetry } from "../service/router.js";
15
16
  import { Supervisor } from "../service/supervisor.js";
16
17
  import * as bench from "../bench/index.js";
@@ -45,7 +46,14 @@ export function addBenchOptions(cmd) {
45
46
  .option("--repo-max <n>", "max mined tasks to run", "5")
46
47
  .option("--curated <name>", "run a curated mined-repo preset (needs --repo-dir); pass an unknown name to list presets");
47
48
  }
48
- export async function runBenchCommand(options, _command) {
49
+ export async function runBenchCommand(options, command) {
50
+ // Announced so the Brain rail can show the host as busy. A benchmark loads and
51
+ // unloads models and drives completions through them for minutes at a time,
52
+ // and it is the one op most likely to be running while somebody wonders why
53
+ // their prompt is slow.
54
+ return withActivity("benchmark", { target: options.model ?? options.endpoint ?? null }, () => runBenchSuite(options, command));
55
+ }
56
+ async function runBenchSuite(options, _command) {
49
57
  const config = loadBrainConfig();
50
58
  const execute = options.execute !== false;
51
59
  const depths = options.depths
@@ -158,18 +166,27 @@ export async function runBenchCommand(options, _command) {
158
166
  for (const needle of needles) {
159
167
  const model = pickModel(catalog, needle);
160
168
  let profile = forModel(store, model, config.defaults);
169
+ // Both are recorded with the run: the fit is what says whether the profile
170
+ // that ran is the profile that was configured, and the calibration is what
171
+ // says whether the fit's own VRAM figures were measured or guessed. A score
172
+ // read without them is a score nobody can diagnose.
173
+ const calibration = getCalibration(store, model, profile);
174
+ let fit = null;
161
175
  const gpu = await queryGpu();
162
176
  if (gpu) {
163
- const fit = vram.fitToBudget({
177
+ fit = vram.fitToBudget({
164
178
  model,
165
179
  profile,
166
- calibration: getCalibration(store, model, profile),
180
+ calibration,
167
181
  totalVramBytes: gpu.totalBytes,
168
182
  });
169
183
  if (!fit.adjusted && !fit.budget.fits) {
170
184
  process.stderr.write(`\nskipping ${model.displayName}: ${fit.reason}\n`);
171
185
  continue;
172
186
  }
187
+ if (fit.adjusted) {
188
+ process.stderr.write(`\n${model.displayName}: ${fit.reason}\n`);
189
+ }
173
190
  profile = fit.profile;
174
191
  }
175
192
  process.stdout.write(`\n${"=".repeat(74)}\n${model.displayName}\n${"=".repeat(74)}\n`);
@@ -205,6 +222,12 @@ export async function runBenchCommand(options, _command) {
205
222
  gpu,
206
223
  runtime: runtimeLabel,
207
224
  archiveId,
225
+ // Straight off the supervisor rather than rebuilt here: this is the argv
226
+ // the child was spawned with, so it cannot drift from what actually ran.
227
+ args: supervisor.args,
228
+ fit,
229
+ calibration,
230
+ suite: { execute, concurrency, depths: depths ?? null, only, mined: Boolean(repoTasks) },
208
231
  });
209
232
  process.stderr.write(` saved to results/${path.basename(file)}\n`);
210
233
  entries.push({ modelName: model.displayName, report });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain calibrate` measure real KV bytes/token for a model and persist it,
2
+ * `otto brain calibrate` - measure real KV bytes/token for a model and persist it,
3
3
  * so the VRAM budget uses a measured figure instead of the (over-estimating)
4
4
  * theoretical formula.
5
5
  */
@@ -4,6 +4,7 @@ import { pickModel, scanModels } from "../models/index.js";
4
4
  import { CommandError } from "../output/types.js";
5
5
  import { calibrate } from "../ops/calibrate.js";
6
6
  import { resolveRuntime } from "../runtime/index.js";
7
+ import { withActivity } from "../service/activity.js";
7
8
  import * as vram from "../vram.js";
8
9
  const calibrateSchema = {
9
10
  idField: "model",
@@ -34,7 +35,9 @@ export async function runCalibrateCommand(options, _command) {
34
35
  const catalog = scanModels(config);
35
36
  const model = pickModel(catalog, options.model ?? store.lastModelId ?? undefined);
36
37
  const profile = forModel(store, model, config.defaults);
37
- const measurement = await calibrate({
38
+ // Announced so the Brain rail can show the host as busy: a calibrate loads the
39
+ // model at several context sizes and will make anything else queue behind it.
40
+ const measurement = await withActivity("calibrate", { target: model.displayName }, () => calibrate({
38
41
  runtime,
39
42
  model,
40
43
  profile,
@@ -46,7 +49,7 @@ export async function runCalibrateCommand(options, _command) {
46
49
  if (p.phase === "skip")
47
50
  process.stderr.write(` skipped ${p.contextSize}: ${p.reason}\n`);
48
51
  },
49
- });
52
+ }));
50
53
  putCalibration(store, model, profile, measurement);
51
54
  saveProfilesStore(store);
52
55
  const gpu = await queryGpu();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain catalog` list the downloadable model catalog, each entry
2
+ * `otto brain catalog` - list the downloadable model catalog, each entry
3
3
  * annotated with whether it is already installed. The installed flag reuses the
4
4
  * authoritative catalog↔model join (enrichWithCatalog sets `catalogId` back-
5
5
  * references on scanned models), so it agrees with `scan`/`pull` and works with
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain config show|set` inspect and edit the persisted config at
2
+ * `otto brain config show|set` - inspect and edit the persisted config at
3
3
  * $OTTO_HOME/otto-brain/config.json. Opt-in flags (`enabled`, `autoStart`) live
4
4
  * here; the Otto daemon reads them to decide whether to manage the brain.
5
5
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Service lifecycle commands. `serve` runs the brain in the foreground; `start`
3
3
  * launches it detached (the shape the Otto daemon uses to supervise a managed
4
- * child); `stop`/`status` operate on the pid file. All honor the opt-in config
4
+ * child); `stop`/`status` operate on the pid file. All honor the opt-in config -
5
5
  * they are always explicit user actions, never auto-started.
6
6
  */
7
7
  import { spawn } from "node:child_process";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain pull <model>` download a model from the catalog into the managed
2
+ * `otto brain pull <model>` - download a model from the catalog into the managed
3
3
  * models directory, using only Node's fetch (no external downloader). The catalog
4
4
  * is the same one seeded from docs/candidate-models.md. `--list-quants` shows what
5
5
  * quantizations the repo offers and `--quant <label>` downloads a specific one.
@@ -2,6 +2,7 @@ import { loadBrainConfig, loadCatalog } from "../config/index.js";
2
2
  import { downloadRepoFiles, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
3
3
  import { CommandError } from "../output/types.js";
4
4
  import { formatBytes } from "../models/scan.js";
5
+ import { withActivity } from "../service/activity.js";
5
6
  const pullSchema = {
6
7
  idField: "model",
7
8
  columns: [
@@ -78,19 +79,20 @@ export async function runPullCommand(modelArg, options, _command) {
78
79
  const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
79
80
  const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
80
81
  let lastPct = -1;
81
- const written = await downloadRepoFiles({
82
+ const written = await withActivity("download", { target: `${model.name} (${choice.quant})` }, (activity) => downloadRepoFiles({
82
83
  repo: model.hfRepo,
83
84
  files,
84
85
  destRoot: managedModelsDir(config),
85
86
  token,
86
87
  onProgress: (p) => {
88
+ activity.update(total ? p.receivedBytes / total : null);
87
89
  const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
88
90
  if (pct !== lastPct && pct % 5 === 0) {
89
91
  lastPct = pct;
90
92
  process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
91
93
  }
92
94
  },
93
- });
95
+ }));
94
96
  process.stderr.write("\n");
95
97
  return {
96
98
  type: "single",
@@ -105,7 +107,9 @@ export async function runPullCommand(modelArg, options, _command) {
105
107
  }
106
108
  const destRoot = managedModelsDir(config);
107
109
  let lastPct = -1;
108
- const localPath = await pullModel({
110
+ // Announced so the Brain rail can show the download: this is the longest-
111
+ // running thing the brain does, and the only one measured in tens of GiB.
112
+ const localPath = await withActivity("download", { target: model.name }, (activity) => pullModel({
109
113
  model,
110
114
  destRoot,
111
115
  file: options.file,
@@ -113,13 +117,14 @@ export async function runPullCommand(modelArg, options, _command) {
113
117
  onProgress: (p) => {
114
118
  if (!p.totalBytes)
115
119
  return;
120
+ activity.update(p.receivedBytes / p.totalBytes);
116
121
  const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
117
122
  if (pct !== lastPct && pct % 5 === 0) {
118
123
  lastPct = pct;
119
124
  process.stderr.write(` ${model.name}: ${pct}%\r`);
120
125
  }
121
126
  },
122
- });
127
+ }));
123
128
  process.stderr.write("\n");
124
129
  return {
125
130
  type: "single",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain report` write the HTML comparison report and print a per-model
2
+ * `otto brain report` - write the HTML comparison report and print a per-model
3
3
  * score table (newest run per model+config).
4
4
  */
5
5
  import type { Command } from "commander";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain rescore` re-grade archived benchmark transcripts with the current
2
+ * `otto brain rescore` - re-grade archived benchmark transcripts with the current
3
3
  * scorer, no GPU used. Surfaces scorer changes against what was recorded live.
4
4
  */
5
5
  import type { Command } from "commander";
@@ -34,7 +34,7 @@ export async function runRescoreCommand(options, _command) {
34
34
  }
35
35
  const before = record?.tasks.find((t) => t.id === task.taskId);
36
36
  const now = `${(task.score * 100).toFixed(0)}%`;
37
- const then = before ? `${(before.score * 100).toFixed(0)}%` : "";
37
+ const then = before ? `${(before.score * 100).toFixed(0)}%` : "-";
38
38
  const moved = before && Math.abs(before.score - task.score) > 0.005;
39
39
  if (moved)
40
40
  changed += 1;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain runtime install|list` manage the self-contained llama.cpp runtime.
2
+ * `otto brain runtime install|list` - manage the self-contained llama.cpp runtime.
3
3
  * `install` downloads a pinned build into $OTTO_HOME/otto-brain/runtimes so the
4
4
  * tool needs no other software; `list` shows every runtime found (managed first,
5
5
  * then LM Studio).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain scan` list detected models with their arch, quant, size, native
2
+ * `otto brain scan` - list detected models with their arch, quant, size, native
3
3
  * context, vision, calibration state, and source. Returns a typed list the output
4
4
  * layer renders as a table (default), json, or yaml.
5
5
  */
@@ -1,5 +1,6 @@
1
1
  import { getCalibration, forModel, loadBrainConfig, loadProfilesStore } from "../config/index.js";
2
2
  import { formatBytes, scanModels } from "../models/index.js";
3
+ import { withActivity } from "../service/activity.js";
3
4
  export const scanSchema = {
4
5
  idField: "model",
5
6
  columns: [
@@ -27,7 +28,11 @@ export function addScanOptions(cmd) {
27
28
  export async function runScanCommand(options, _command) {
28
29
  const config = loadBrainConfig();
29
30
  const store = loadProfilesStore();
30
- const catalog = scanModels(config, process.env, { withMetadata: options.metadata !== false });
31
+ // Announced because a metadata scan opens and reads the GGUF header of every
32
+ // model on disk, which on a large library is seconds of I/O, not milliseconds.
33
+ // Only the explicit `scan` command announces: the other commands call
34
+ // `scanModels` too, and each announcing would fight over the one record.
35
+ const catalog = await withActivity("scan", {}, async () => scanModels(config, process.env, { withMetadata: options.metadata !== false }));
31
36
  const rows = catalog.map((model) => {
32
37
  const profile = forModel(store, model, config.defaults);
33
38
  const md = model.metadata ?? {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain search <query>` and `otto brain repo-quants <repo>` Hugging Face
2
+ * `otto brain search <query>` and `otto brain repo-quants <repo>` - Hugging Face
3
3
  * model discovery. These wrap the surface-agnostic primitives in models/hf.ts so
4
4
  * the same code serves the TUI (in-process) and the Otto app (daemon shells out
5
5
  * to `--json`, mirroring `catalog`/`scan`).
@@ -1,9 +1,9 @@
1
1
  /**
2
- * `otto brain share` opt a brain into being reachable (and optionally
2
+ * `otto brain share` - opt a brain into being reachable (and optionally
3
3
  * configurable) by other Otto hosts. Off by default: a brain binds loopback and
4
4
  * is invisible to the network until its owner runs this. Sets the bind, the
5
5
  * access level (open on a trusted network, or a bearer key), HTTPS, and whether
6
- * key holders may reconfigure it. Writes $OTTO_HOME/otto-brain/config.json the
6
+ * key holders may reconfigure it. Writes $OTTO_HOME/otto-brain/config.json - the
7
7
  * same fields the Otto app's Sharing UI sets, so the two stay in sync.
8
8
  */
9
9
  import { randomBytes } from "node:crypto";
@@ -1,12 +1,12 @@
1
1
  /**
2
- * `otto brain sweep` find the reasoning budget that yields the best output, and
2
+ * `otto brain sweep` - find the reasoning budget that yields the best output, and
3
3
  * save it to the model's profile. Guards against the thinking-model failure where
4
4
  * an unrestricted budget returns pure reasoning and no content.
5
5
  */
6
6
  import type { Command } from "commander";
7
7
  import type { AnyCommandResult } from "../output/index.js";
8
8
  export interface SweepRow {
9
- budget: number | string;
9
+ budget: number;
10
10
  content: string;
11
11
  files: string;
12
12
  tokensPerSec: string;
@@ -1,12 +1,20 @@
1
- import { forModel, loadBrainConfig, loadProfilesStore, put, saveProfilesStore, } from "../config/index.js";
1
+ import { forModel, formatReasoningBudget, loadBrainConfig, loadProfilesStore, put, saveProfilesStore, } from "../config/index.js";
2
2
  import { pickModel, scanModels } from "../models/index.js";
3
3
  import { CommandError } from "../output/types.js";
4
- import { sweep } from "../ops/sweep.js";
4
+ import { DEFAULT_BUDGETS, sweep } from "../ops/sweep.js";
5
5
  import { resolveRuntime } from "../runtime/index.js";
6
+ import { withActivity } from "../service/activity.js";
6
7
  const sweepSchema = {
7
8
  idField: "budget",
8
9
  columns: [
9
- { header: "BUDGET", field: "budget", width: 8, align: "right" },
10
+ // Labelled only for the table. `budget` stays the raw number on the row, so
11
+ // json/yaml and --quiet still emit -1 for machine consumers.
12
+ {
13
+ header: "BUDGET",
14
+ field: (row) => formatReasoningBudget(row.budget),
15
+ width: 8,
16
+ align: "right",
17
+ },
10
18
  { header: "CONTENT", field: "content", width: 9, align: "right" },
11
19
  { header: "FILES", field: "files", width: 6, align: "right" },
12
20
  { header: "TOK/S", field: "tokensPerSec", width: 7, align: "right" },
@@ -37,20 +45,26 @@ export async function runSweepCommand(options, _command) {
37
45
  const budgets = options.budgets
38
46
  ? options.budgets.split(",").map((s) => Number(s.trim()))
39
47
  : undefined;
40
- const report = await sweep({
48
+ // Progress lines stack, so pad to the widest label this run will print rather
49
+ // than a fixed width that "unrestricted" would overflow.
50
+ const labelWidth = Math.max(...(budgets ?? DEFAULT_BUDGETS).map((b) => formatReasoningBudget(b).length));
51
+ const label = (budget) => formatReasoningBudget(budget).padStart(labelWidth);
52
+ // Announced so the Brain rail can show the host as busy: a sweep reloads the
53
+ // model once per budget and owns the machine for the duration.
54
+ const report = await withActivity("sweep", { target: model.displayName }, () => sweep({
41
55
  runtime,
42
56
  model,
43
57
  profile,
44
58
  budgets,
45
59
  onProgress: (p) => {
46
60
  if (p.phase === "loading")
47
- process.stderr.write(` budget ${String(p.budget).padStart(5)}: loading…\n`);
61
+ process.stderr.write(` budget ${label(p.budget)}: loading…\n`);
48
62
  if (p.phase === "done")
49
- process.stderr.write(` budget ${String(p.budget).padStart(5)}: done\n`);
63
+ process.stderr.write(` budget ${label(p.budget)}: done\n`);
50
64
  if (p.phase === "failed")
51
- process.stderr.write(` budget ${String(p.budget).padStart(5)}: failed ${p.error}\n`);
65
+ process.stderr.write(` budget ${label(p.budget)}: failed ${p.error}\n`);
52
66
  },
53
- });
67
+ }));
54
68
  if (report.recommended !== null && report.recommended !== undefined) {
55
69
  profile.reasoningBudget = report.recommended;
56
70
  put(store, model, profile);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `otto brain ui` (and the bare `otto brain`) the full-screen interactive TUI,
2
+ * `otto brain ui` (and the bare `otto brain`) - the full-screen interactive TUI,
3
3
  * the tool's signature experience. Gated on a TTY; falls back with guidance when
4
4
  * piped. Bypasses the output layer since it owns the whole screen.
5
5
  */
@@ -4,6 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
4
4
  export { resolveBrainPaths, packageRoot, type BrainPaths } from "./paths.js";
5
5
  export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
6
6
  export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
7
- export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
7
+ export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
8
+ export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, type CalibrationInfo, type CalibrationState, type ProfileFieldDescriptor, type ProfileWarning, } from "./profile-edit.js";
8
9
  export * from "./schema.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
4
4
  export { resolveBrainPaths, packageRoot } from "./paths.js";
5
5
  export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
6
6
  export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
7
- export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
7
+ export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
8
+ export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, } from "./profile-edit.js";
8
9
  export * from "./schema.js";
9
10
  //# sourceMappingURL=index.js.map
@@ -2,7 +2,7 @@
2
2
  * Resolves $OTTO_HOME exactly as the Otto daemon does (see
3
3
  * packages/server/src/server/otto-home.ts): env override, else `~/.otto`, tilde
4
4
  * expanded, made private. Sharing this rule is what lets otto-brain's config sit
5
- * next to Otto's and follow it into the dev home whenever the dev scripts export
5
+ * next to Otto's - and follow it into the dev home whenever the dev scripts export
6
6
  * OTTO_HOME. There is deliberately no dev-path logic here; that lives in shell.
7
7
  */
8
8
  import { homedir } from "node:os";
@@ -7,6 +7,7 @@ export interface BrainPaths {
7
7
  modelsDir: string;
8
8
  runtimesDir: string;
9
9
  pidFile: string;
10
+ activityFile: string;
10
11
  logFile: string;
11
12
  resultsDir: string;
12
13
  }
@@ -18,6 +18,10 @@ export function resolveBrainPaths(env = process.env) {
18
18
  modelsDir: path.join(root, "models"),
19
19
  runtimesDir: path.join(root, "runtimes"),
20
20
  pidFile: path.join(root, "otto-brain.pid"),
21
+ // Which long-running op currently owns the host. A file rather than service
22
+ // state because calibrate/sweep/bench run as their own CLI processes and the
23
+ // service - which is what answers /__host/status - never sees them otherwise.
24
+ activityFile: path.join(root, "otto-brain.activity"),
21
25
  logFile: path.join(root, "otto-brain.log"),
22
26
  resultsDir: path.join(root, "results"),
23
27
  };