open-agents-ai 0.40.0 → 0.41.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 (3) hide show
  1. package/README.md +21 -5
  2. package/dist/index.js +588 -68
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -162,16 +162,32 @@ Use deep context for:
162
162
 
163
163
  The setting persists to `.oa/settings.json`. Deep context is particularly valuable for models with 64K+ context windows (Qwen3.5-122B, Llama 3.1 70B, etc.) where the default thresholds were leaving significant capacity unused.
164
164
 
165
- ### Status Bar Context Tracking (`Ctx:`)
165
+ ### Status Bar Context Tracking (`Ctx:` + `SNR:`)
166
166
 
167
- The status bar displays a live `Ctx:` gauge showing estimated context window usage:
167
+ The status bar displays a live `Ctx:` gauge showing estimated context window usage, plus an `SNR:` gauge showing context quality:
168
168
 
169
169
  ```
170
- In: 12,345 | Out: 4,567 | Ctx: 18,000/131,072 86% | Exp: 4.2x
171
- ^^^^^^^^^^^^^^^^^^^^^^^^
172
- Estimated tokens used / total context window
170
+ In: 12,345 | Out: 4,567 | Ctx: 18,000/131,072 86% | SNR: 72% d'2.1 | Exp: 4.2x
171
+ ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
172
+ Context window usage Signal-to-Noise Ratio
173
173
  ```
174
174
 
175
+ **SNR (Signal-to-Noise Ratio)** — measures how much of the agent's memory context is relevant to the current task vs noise. Inspired by neuroscience signal detection theory:
176
+
177
+ - **d-prime (d')**: psychophysics metric measuring separation between signal and noise distributions. d' >= 2.0 = excellent discrimination, d' ≈ 1.0 = moderate, d' <= 0.5 = noisy
178
+ - **Signal**: memory entries with high keyword overlap to the current task (PFC gating analogy)
179
+ - **Noise**: entries with low relevance or high redundancy (dentate gyrus pattern separation)
180
+ - **Sparsity**: how much of the context is unique vs redundant (sparse distributed memory)
181
+
182
+ The SNR formula combines three components:
183
+ - 50% **signal proportion** (relevant entries / total entries)
184
+ - 30% **d-prime quality** (normalized to 0-1 from the 0-3 d' range)
185
+ - 20% **sparsity** (1 - average pairwise n-gram overlap)
186
+
187
+ Color coding: green (>=70%), yellow (40-70%), red (<40%). SNR is evaluated at task start and task completion. In deep context mode with `/deep`, parallel evaluator agents (PFC Relevance Evaluator + Dentate Gyrus Noise Detector) can run a full consensus-based evaluation.
188
+
189
+ Research basis: d-prime from signal detection theory (Green & Swets 1966), hippocampal pattern separation (Yassa & Stark 2011), PFC gating (Miller & Cohen 2001), biased competition (Desimone & Duncan 1995), multi-agent debate (Du et al., arXiv:2305.14325).
190
+
175
191
  This gauge reflects the **post-compaction** token count — when compaction fires, the `Ctx:` value drops to match the actual compressed message history. The compaction warning message shows the before/after:
176
192
 
177
193
  ```
package/dist/index.js CHANGED
@@ -6133,8 +6133,8 @@ async function loadTranscribeCli() {
6133
6133
  const nvmBase = join15(homedir6(), ".nvm", "versions", "node");
6134
6134
  if (existsSync12(nvmBase)) {
6135
6135
  try {
6136
- const { readdirSync: readdirSync12 } = await import("node:fs");
6137
- for (const ver of readdirSync12(nvmBase)) {
6136
+ const { readdirSync: readdirSync14 } = await import("node:fs");
6137
+ for (const ver of readdirSync14(nvmBase)) {
6138
6138
  const tcPath = join15(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
6139
6139
  if (existsSync12(join15(tcPath, "dist", "index.js"))) {
6140
6140
  const { createRequire: createRequire4 } = await import("node:module");
@@ -14069,8 +14069,8 @@ ${marker}` : marker);
14069
14069
  return;
14070
14070
  try {
14071
14071
  const { mkdirSync: mkdirSync14, writeFileSync: writeFileSync13 } = __require("node:fs");
14072
- const { join: join42 } = __require("node:path");
14073
- const sessionDir = join42(this._workingDirectory, ".oa", "session", this._sessionId);
14072
+ const { join: join43 } = __require("node:path");
14073
+ const sessionDir = join43(this._workingDirectory, ".oa", "session", this._sessionId);
14074
14074
  mkdirSync14(sessionDir, { recursive: true });
14075
14075
  const checkpoint = {
14076
14076
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14083,7 +14083,7 @@ ${marker}` : marker);
14083
14083
  memexEntryCount: this._memexArchive.size,
14084
14084
  fileRegistrySize: this._fileRegistry.size
14085
14085
  };
14086
- writeFileSync13(join42(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14086
+ writeFileSync13(join43(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14087
14087
  } catch {
14088
14088
  }
14089
14089
  }
@@ -15987,8 +15987,8 @@ var init_listen = __esm({
15987
15987
  const nvmBase = join27(homedir8(), ".nvm", "versions", "node");
15988
15988
  if (existsSync19(nvmBase)) {
15989
15989
  try {
15990
- const { readdirSync: readdirSync12 } = await import("node:fs");
15991
- for (const ver of readdirSync12(nvmBase)) {
15990
+ const { readdirSync: readdirSync14 } = await import("node:fs");
15991
+ for (const ver of readdirSync14(nvmBase)) {
15992
15992
  const tcPath = join27(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
15993
15993
  if (existsSync19(join27(tcPath, "dist", "index.js"))) {
15994
15994
  const { createRequire: createRequire4 } = await import("node:module");
@@ -16218,10 +16218,10 @@ transcribe-cli error: ${transcribeCliError}` : "";
16218
16218
  wordTimestamps: false
16219
16219
  });
16220
16220
  if (outputDir) {
16221
- const { basename: basename15 } = await import("node:path");
16221
+ const { basename: basename16 } = await import("node:path");
16222
16222
  const transcriptDir = join27(outputDir, ".oa", "transcripts");
16223
16223
  mkdirSync5(transcriptDir, { recursive: true });
16224
- const outFile = join27(transcriptDir, `${basename15(filePath)}.txt`);
16224
+ const outFile = join27(transcriptDir, `${basename16(filePath)}.txt`);
16225
16225
  writeFileSync5(outFile, result.text, "utf-8");
16226
16226
  }
16227
16227
  return {
@@ -16607,18 +16607,18 @@ function renderTaskAborted() {
16607
16607
  ${c2.yellow("\u26A0")} ${c2.bold("Task aborted by user")}
16608
16608
  `);
16609
16609
  }
16610
- function renderToolCallStart(toolName, args) {
16610
+ function renderToolCallStart(toolName, args, verbose) {
16611
16611
  const icon = TOOL_ICONS[toolName] ?? "\u{1F527}";
16612
16612
  const label = TOOL_LABELS[toolName] ?? toolName;
16613
- const argsSummary = formatToolArgs(toolName, args);
16613
+ const argsSummary = formatToolArgs(toolName, args, verbose);
16614
16614
  const colorFn = _colorsEnabled ? TOOL_COLORS[toolName] ?? c2.dim : (t) => t;
16615
16615
  const emojiPrefix = _emojisEnabled ? `${icon} ` : "";
16616
16616
  process.stdout.write(`
16617
16617
  ${c2.dim("\u23BF")} ${emojiPrefix}${colorFn(c2.bold(label))}${argsSummary ? c2.dim(": ") + argsSummary : ""}
16618
16618
  `);
16619
16619
  }
16620
- function renderToolResult(toolName, success, output) {
16621
- const maxW = getTermWidth() - 10;
16620
+ function renderToolResult(toolName, success, output, verbose) {
16621
+ const maxW = verbose ? Math.max(getTermWidth() - 10, 200) : getTermWidth() - 10;
16622
16622
  const prefix = ` ${c2.dim("\u23BF")} `;
16623
16623
  switch (toolName) {
16624
16624
  case "file_write": {
@@ -16676,18 +16676,47 @@ function renderToolResult(toolName, success, output) {
16676
16676
  `);
16677
16677
  return;
16678
16678
  }
16679
- const maxLines = 6;
16679
+ const maxLines = verbose ? 200 : 6;
16680
16680
  const shown = lines.slice(0, maxLines);
16681
16681
  for (const line of shown) {
16682
- if (isRawJsonDump(line)) {
16682
+ if (isRawJsonDump(line) && !verbose) {
16683
16683
  process.stdout.write(`${prefix}${c2.dim("(content omitted)")}
16684
16684
  `);
16685
16685
  return;
16686
16686
  }
16687
- const cropped = line.length > maxW ? line.slice(0, maxW - 3) + "..." : line;
16688
- const formatted = formatMarkdownLine(cropped);
16689
- process.stdout.write(`${prefix}${formatted === cropped ? highlightToolOutput(cropped) : formatted}
16687
+ if (verbose) {
16688
+ const termW = getTermWidth() - 10;
16689
+ if (line.length > termW) {
16690
+ let remaining = line;
16691
+ let first = true;
16692
+ while (remaining.length > 0) {
16693
+ if (remaining.length <= termW) {
16694
+ const formatted2 = formatMarkdownLine(remaining);
16695
+ process.stdout.write(`${first ? prefix : prefix + " "}${formatted2 === remaining ? highlightToolOutput(remaining) : formatted2}
16696
+ `);
16697
+ break;
16698
+ }
16699
+ let breakAt = remaining.lastIndexOf(" ", termW);
16700
+ if (breakAt < termW * 0.3)
16701
+ breakAt = termW;
16702
+ const chunk = remaining.slice(0, breakAt);
16703
+ remaining = remaining.slice(breakAt).trimStart();
16704
+ const formatted = formatMarkdownLine(chunk);
16705
+ process.stdout.write(`${first ? prefix : prefix + " "}${formatted === chunk ? highlightToolOutput(chunk) : formatted}
16706
+ `);
16707
+ first = false;
16708
+ }
16709
+ } else {
16710
+ const formatted = formatMarkdownLine(line);
16711
+ process.stdout.write(`${prefix}${formatted === line ? highlightToolOutput(line) : formatted}
16712
+ `);
16713
+ }
16714
+ } else {
16715
+ const cropped = line.length > maxW ? line.slice(0, maxW - 3) + "..." : line;
16716
+ const formatted = formatMarkdownLine(cropped);
16717
+ process.stdout.write(`${prefix}${formatted === cropped ? highlightToolOutput(cropped) : formatted}
16690
16718
  `);
16719
+ }
16691
16720
  }
16692
16721
  if (lines.length > maxLines) {
16693
16722
  process.stdout.write(`${prefix}${c2.dim(`... ${lines.length - maxLines} more lines`)}
@@ -17026,8 +17055,8 @@ function renderConfig(config) {
17026
17055
  }
17027
17056
  process.stdout.write("\n");
17028
17057
  }
17029
- function formatToolArgs(toolName, args) {
17030
- const maxArg = Math.max(40, getTermWidth() - 20);
17058
+ function formatToolArgs(toolName, args, verbose) {
17059
+ const maxArg = verbose ? 1e4 : Math.max(40, getTermWidth() - 20);
17031
17060
  switch (toolName) {
17032
17061
  case "file_read":
17033
17062
  case "file_write":
@@ -19913,17 +19942,17 @@ async function handleUpdate(subcommand, ctx) {
19913
19942
  try {
19914
19943
  const { createRequire: createRequire4 } = await import("node:module");
19915
19944
  const { fileURLToPath: fileURLToPath10 } = await import("node:url");
19916
- const { dirname: dirname13, join: join42 } = await import("node:path");
19917
- const { existsSync: existsSync29 } = await import("node:fs");
19945
+ const { dirname: dirname13, join: join43 } = await import("node:path");
19946
+ const { existsSync: existsSync30 } = await import("node:fs");
19918
19947
  const req = createRequire4(import.meta.url);
19919
19948
  const thisDir = dirname13(fileURLToPath10(import.meta.url));
19920
19949
  const candidates = [
19921
- join42(thisDir, "..", "package.json"),
19922
- join42(thisDir, "..", "..", "package.json"),
19923
- join42(thisDir, "..", "..", "..", "package.json")
19950
+ join43(thisDir, "..", "package.json"),
19951
+ join43(thisDir, "..", "..", "package.json"),
19952
+ join43(thisDir, "..", "..", "..", "package.json")
19924
19953
  ];
19925
19954
  for (const pkgPath of candidates) {
19926
- if (existsSync29(pkgPath)) {
19955
+ if (existsSync30(pkgPath)) {
19927
19956
  const pkg = req(pkgPath);
19928
19957
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
19929
19958
  currentVersion = pkg.version ?? "0.0.0";
@@ -24113,6 +24142,348 @@ DMN state directory: ${this.stateDir}`);
24113
24142
  }
24114
24143
  });
24115
24144
 
24145
+ // packages/cli/dist/tui/snr-engine.js
24146
+ import { existsSync as existsSync27, readdirSync as readdirSync12, readFileSync as readFileSync20 } from "node:fs";
24147
+ import { join as join37, basename as basename14 } from "node:path";
24148
+ function computeDPrime(signalScores, noiseScores) {
24149
+ if (signalScores.length === 0 || noiseScores.length === 0)
24150
+ return 0;
24151
+ const mean = (arr) => arr.reduce((s, v) => s + v, 0) / arr.length;
24152
+ const variance = (arr, mu) => arr.reduce((s, v) => s + (v - mu) ** 2, 0) / Math.max(1, arr.length - 1);
24153
+ const muSignal = mean(signalScores);
24154
+ const muNoise = mean(noiseScores);
24155
+ const varSignal = variance(signalScores, muSignal);
24156
+ const varNoise = variance(noiseScores, muNoise);
24157
+ const pooledStd = Math.sqrt((varSignal + varNoise) / 2);
24158
+ if (pooledStd === 0)
24159
+ return muSignal > muNoise ? 3 : 0;
24160
+ return (muSignal - muNoise) / pooledStd;
24161
+ }
24162
+ function computeSparsity(entries) {
24163
+ if (entries.length <= 1)
24164
+ return 1;
24165
+ const ngramSets = entries.map((e) => {
24166
+ const words = e.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
24167
+ const ngrams = /* @__PURE__ */ new Set();
24168
+ for (let i = 0; i < words.length - 1; i++) {
24169
+ ngrams.add(`${words[i]} ${words[i + 1]}`);
24170
+ }
24171
+ return ngrams;
24172
+ });
24173
+ let totalPairs = 0;
24174
+ let totalJaccard = 0;
24175
+ for (let i = 0; i < ngramSets.length; i++) {
24176
+ for (let j = i + 1; j < ngramSets.length; j++) {
24177
+ const a = ngramSets[i];
24178
+ const b = ngramSets[j];
24179
+ if (a.size === 0 && b.size === 0)
24180
+ continue;
24181
+ const intersection = new Set([...a].filter((x) => b.has(x)));
24182
+ const union = /* @__PURE__ */ new Set([...a, ...b]);
24183
+ totalJaccard += union.size > 0 ? intersection.size / union.size : 0;
24184
+ totalPairs++;
24185
+ }
24186
+ }
24187
+ if (totalPairs === 0)
24188
+ return 1;
24189
+ const avgOverlap = totalJaccard / totalPairs;
24190
+ return Math.max(0, Math.min(1, 1 - avgOverlap));
24191
+ }
24192
+ function adaptTool3(tool) {
24193
+ return {
24194
+ name: tool.name,
24195
+ description: tool.description,
24196
+ parameters: tool.parameters,
24197
+ async execute(args) {
24198
+ const result = await tool.execute(args);
24199
+ return { success: result.success, output: result.output, error: result.error };
24200
+ }
24201
+ };
24202
+ }
24203
+ function renderSNRUpdate(score) {
24204
+ const pct = Math.round(score.ratio * 100);
24205
+ const color = pct >= 70 ? c2.green : pct >= 40 ? c2.yellow : c2.red;
24206
+ const capWarn = score.capacityWarning ? c2.red(" [CAPACITY]") : "";
24207
+ process.stdout.write(` ${color("\u25C8")} SNR: ${color(`${pct}%`)} (d'=${score.dPrime.toFixed(1)}, signal=${score.signalCount}/${score.entriesEvaluated}, sparsity=${Math.round(score.sparsity * 100)}%) [${score.evaluators.join("+")}]${capWarn}
24208
+ `);
24209
+ }
24210
+ var SNREngine;
24211
+ var init_snr_engine = __esm({
24212
+ "packages/cli/dist/tui/snr-engine.js"() {
24213
+ "use strict";
24214
+ init_dist5();
24215
+ init_dist2();
24216
+ init_project_context();
24217
+ init_render();
24218
+ SNREngine = class {
24219
+ config;
24220
+ repoRoot;
24221
+ lastScore = null;
24222
+ evaluationCount = 0;
24223
+ constructor(config, repoRoot) {
24224
+ this.config = config;
24225
+ this.repoRoot = repoRoot;
24226
+ }
24227
+ get score() {
24228
+ return this.lastScore;
24229
+ }
24230
+ /**
24231
+ * Quick local-only SNR estimation (no LLM calls, fast).
24232
+ * @param contextSlots — total context window tokens (for Hopfield capacity warning)
24233
+ */
24234
+ computeLocalSNR(currentTask, contextEntries, contextSlots) {
24235
+ if (contextEntries.length === 0) {
24236
+ return this.makeScore(1, 3, 0, 0, 0, 1, ["local-empty"]);
24237
+ }
24238
+ const taskTerms = new Set(currentTask.toLowerCase().split(/\s+/).filter((w) => w.length > 3).map((w) => w.replace(/[^a-z0-9]/g, "")));
24239
+ const scores = contextEntries.map((entry) => {
24240
+ const entryTerms = entry.toLowerCase().split(/\s+/).filter((w) => w.length > 3).map((w) => w.replace(/[^a-z0-9]/g, ""));
24241
+ const matchCount = entryTerms.filter((t) => taskTerms.has(t)).length;
24242
+ const score2 = entryTerms.length > 0 ? matchCount / Math.sqrt(entryTerms.length) : 0;
24243
+ return { entry, score: score2 };
24244
+ });
24245
+ const allScores = scores.map((s) => s.score);
24246
+ const mean = allScores.reduce((a, b) => a + b, 0) / allScores.length;
24247
+ const std = Math.sqrt(allScores.reduce((a, v) => a + (v - mean) ** 2, 0) / allScores.length);
24248
+ const threshold = mean + 0.5 * std;
24249
+ const signalScores = allScores.filter((s) => s >= threshold);
24250
+ const noiseScores = allScores.filter((s) => s < threshold);
24251
+ const dPrime = computeDPrime(signalScores.length > 0 ? signalScores : [mean], noiseScores.length > 0 ? noiseScores : [0]);
24252
+ const sparsity = computeSparsity(contextEntries);
24253
+ const signalProportion = signalScores.length / allScores.length;
24254
+ const dPrimeNorm = Math.min(1, Math.max(0, dPrime / 3));
24255
+ const ratio = 0.5 * signalProportion + 0.3 * dPrimeNorm + 0.2 * sparsity;
24256
+ const capacityWarning = contextSlots ? contextEntries.length > Math.floor(contextSlots * 0.138) : false;
24257
+ const score = this.makeScore(ratio, dPrime, contextEntries.length, signalScores.length, noiseScores.length, sparsity, ["local-keyword"], capacityWarning);
24258
+ this.lastScore = score;
24259
+ this.evaluationCount++;
24260
+ return score;
24261
+ }
24262
+ /**
24263
+ * Full SNR evaluation using parallel LLM evaluator agents.
24264
+ * Mirrors PFC gating + multi-agent debate (Du et al. 2023).
24265
+ *
24266
+ * Spawns 2 lightweight agents with different evaluation perspectives:
24267
+ * - "Relevance Evaluator" — scores entries by task relevance (PFC role)
24268
+ * - "Noise Detector" — identifies redundant/stale/irrelevant entries (DG role)
24269
+ *
24270
+ * Their consensus determines the final SNR.
24271
+ */
24272
+ async evaluateWithAgents(currentTask, memoryTopics, onEvent) {
24273
+ const entries = this.loadMemoryEntries(memoryTopics);
24274
+ if (entries.length === 0) {
24275
+ const score2 = this.makeScore(1, 3, 0, 0, 0, 1, ["agents-empty"]);
24276
+ this.lastScore = score2;
24277
+ return score2;
24278
+ }
24279
+ const entrySummaries = entries.map((e, i) => `[${i}] topic=${e.topic} key=${e.key}: ${e.value.slice(0, 200)}${e.value.length > 200 ? "..." : ""}`);
24280
+ const entriesBlock = entrySummaries.join("\n");
24281
+ const [relevanceResult, noiseResult] = await Promise.allSettled([
24282
+ this.runEvaluatorAgent("relevance", `You are a Prefrontal Cortex Gating Evaluator. Your role is to assess which
24283
+ memory entries are RELEVANT to the current task.
24284
+
24285
+ CURRENT TASK: ${currentTask}
24286
+
24287
+ MEMORY ENTRIES TO EVALUATE:
24288
+ ${entriesBlock}
24289
+
24290
+ For each entry, output a JSON array of objects:
24291
+ [{"index": 0, "score": 0.8, "reason": "directly related to task"}, ...]
24292
+
24293
+ Score 0-1 where:
24294
+ - 1.0 = directly relevant to the current task
24295
+ - 0.7 = supporting context (useful background)
24296
+ - 0.4 = marginally relevant
24297
+ - 0.1 = not relevant to current task
24298
+
24299
+ Call task_complete with the JSON array when done.`, onEvent),
24300
+ this.runEvaluatorAgent("noise", `You are a Dentate Gyrus Pattern Separation Evaluator. Your role is to identify
24301
+ NOISE in the memory context \u2014 entries that are redundant, stale, or interfering.
24302
+
24303
+ CURRENT TASK: ${currentTask}
24304
+
24305
+ MEMORY ENTRIES TO EVALUATE:
24306
+ ${entriesBlock}
24307
+
24308
+ For each entry, output a JSON array of objects:
24309
+ [{"index": 0, "noise_score": 0.2, "reason": "unique, not redundant"}, ...]
24310
+
24311
+ Noise score 0-1 where:
24312
+ - 1.0 = pure noise (completely redundant, stale, or interfering with task)
24313
+ - 0.7 = mostly noise (outdated or largely redundant with another entry)
24314
+ - 0.4 = moderate noise (some overlap with other entries)
24315
+ - 0.1 = clean signal (unique, fresh, non-interfering)
24316
+
24317
+ Look for: duplicate information, outdated facts, entries that contradict
24318
+ newer entries, entries about completely unrelated topics.
24319
+
24320
+ Call task_complete with the JSON array when done.`, onEvent)
24321
+ ]);
24322
+ const relevanceScores = this.parseEvaluatorResult(relevanceResult.status === "fulfilled" ? relevanceResult.value : "", entries.length, 0.5);
24323
+ const noiseScores = this.parseEvaluatorResult(noiseResult.status === "fulfilled" ? noiseResult.value : "", entries.length, 0.5);
24324
+ const combinedScores = entries.map((_, i) => {
24325
+ const rel = relevanceScores[i] ?? 0.5;
24326
+ const noise = noiseScores[i] ?? 0.5;
24327
+ const votes = [rel, 1 - noise].sort((a, b) => a - b);
24328
+ return (votes[0] + votes[1]) / 2;
24329
+ });
24330
+ const mean = combinedScores.reduce((a, b) => a + b, 0) / combinedScores.length;
24331
+ const std = Math.sqrt(combinedScores.reduce((a, v) => a + (v - mean) ** 2, 0) / combinedScores.length);
24332
+ const threshold = Math.max(0.3, mean);
24333
+ const signalEntries = combinedScores.filter((s) => s >= threshold);
24334
+ const noiseEntries = combinedScores.filter((s) => s < threshold);
24335
+ const dPrime = computeDPrime(signalEntries.length > 0 ? signalEntries : [mean], noiseEntries.length > 0 ? noiseEntries : [0.1]);
24336
+ const sparsity = computeSparsity(entries.map((e) => e.value));
24337
+ const signalProportion = signalEntries.length / combinedScores.length;
24338
+ const dPrimeNorm = Math.min(1, Math.max(0, dPrime / 3));
24339
+ const ratio = 0.5 * signalProportion + 0.3 * dPrimeNorm + 0.2 * sparsity;
24340
+ const evaluators = [
24341
+ relevanceResult.status === "fulfilled" ? "relevance-agent" : "relevance-failed",
24342
+ noiseResult.status === "fulfilled" ? "noise-agent" : "noise-failed"
24343
+ ];
24344
+ const score = this.makeScore(ratio, dPrime, entries.length, signalEntries.length, noiseEntries.length, sparsity, evaluators);
24345
+ this.lastScore = score;
24346
+ this.evaluationCount++;
24347
+ return score;
24348
+ }
24349
+ // ── Evaluator agent ──────────────────────────────────────────────────
24350
+ async runEvaluatorAgent(name, prompt, onEvent) {
24351
+ const backend = new OllamaAgenticBackend(this.config.backendUrl, this.config.model, this.config.apiKey);
24352
+ const modelTier = getModelTier(this.config.model);
24353
+ const runner = new AgenticRunner(backend, {
24354
+ maxTurns: 5,
24355
+ // Evaluators are very focused — 5 turns max
24356
+ maxTokens: 4096,
24357
+ temperature: 0,
24358
+ // Deterministic scoring
24359
+ requestTimeoutMs: this.config.timeoutMs,
24360
+ taskTimeoutMs: this.config.timeoutMs,
24361
+ compactionThreshold: modelTier === "small" ? 8e3 : 16e3,
24362
+ modelTier
24363
+ });
24364
+ const tools = [
24365
+ new MemoryReadTool(this.repoRoot),
24366
+ new MemorySearchTool(this.repoRoot)
24367
+ ];
24368
+ runner.registerTools([
24369
+ ...tools.map(adaptTool3),
24370
+ {
24371
+ name: "task_complete",
24372
+ description: "Signal evaluation is complete with your scored results.",
24373
+ parameters: {
24374
+ type: "object",
24375
+ properties: {
24376
+ summary: { type: "string", description: "JSON array of scored entries" }
24377
+ },
24378
+ required: ["summary"]
24379
+ },
24380
+ async execute(args) {
24381
+ return { success: true, output: args["summary"] || "[]" };
24382
+ }
24383
+ }
24384
+ ]);
24385
+ if (onEvent)
24386
+ runner.onEvent(onEvent);
24387
+ const result = await runner.run(prompt, `SNR Evaluator (${name}). Working directory: ${this.repoRoot}`);
24388
+ return result.summary || "[]";
24389
+ }
24390
+ // ── Memory loading ───────────────────────────────────────────────────
24391
+ loadMemoryEntries(topics) {
24392
+ const entries = [];
24393
+ const dirs = [
24394
+ join37(this.repoRoot, ".oa", "memory"),
24395
+ join37(this.repoRoot, ".open-agents", "memory")
24396
+ ];
24397
+ for (const dir of dirs) {
24398
+ if (!existsSync27(dir))
24399
+ continue;
24400
+ try {
24401
+ const files = readdirSync12(dir).filter((f) => f.endsWith(".json"));
24402
+ for (const f of files) {
24403
+ const topic = basename14(f, ".json");
24404
+ if (topics.length > 0 && !topics.includes(topic))
24405
+ continue;
24406
+ try {
24407
+ const data = JSON.parse(readFileSync20(join37(dir, f), "utf-8"));
24408
+ for (const [key, val] of Object.entries(data)) {
24409
+ const value = typeof val === "object" && val !== null && "value" in val ? String(val.value) : String(val);
24410
+ entries.push({ topic, key, value });
24411
+ }
24412
+ } catch {
24413
+ }
24414
+ }
24415
+ } catch {
24416
+ }
24417
+ }
24418
+ return entries;
24419
+ }
24420
+ // ── Result parsing ───────────────────────────────────────────────────
24421
+ parseEvaluatorResult(summary, entryCount, defaultScore) {
24422
+ const scores = new Array(entryCount).fill(defaultScore);
24423
+ try {
24424
+ const jsonMatch = summary.match(/\[[\s\S]*\]/);
24425
+ if (!jsonMatch)
24426
+ return scores;
24427
+ const parsed = JSON.parse(jsonMatch[0]);
24428
+ for (const item of parsed) {
24429
+ const idx = typeof item.index === "number" ? item.index : -1;
24430
+ const score = typeof item.score === "number" ? item.score : typeof item.noise_score === "number" ? item.noise_score : defaultScore;
24431
+ if (idx >= 0 && idx < entryCount) {
24432
+ scores[idx] = Math.max(0, Math.min(1, score));
24433
+ }
24434
+ }
24435
+ } catch {
24436
+ }
24437
+ return scores;
24438
+ }
24439
+ // ── Memory pruning recommendations ──────────────────────────────────
24440
+ /**
24441
+ * Identify low-SNR memory entries that should be pruned.
24442
+ * Mirrors synaptic downscaling during slow-wave sleep — weak connections
24443
+ * (low-relevance, high-redundancy entries) are depotentiated.
24444
+ *
24445
+ * Returns topics+keys of entries scoring below the noise threshold.
24446
+ * Does NOT delete anything — the caller decides what to do with the list.
24447
+ */
24448
+ getPruningCandidates(currentTask, noiseThreshold = 0.3) {
24449
+ const entries = this.loadMemoryEntries([]);
24450
+ if (entries.length === 0)
24451
+ return [];
24452
+ const taskTerms = new Set(currentTask.toLowerCase().split(/\s+/).filter((w) => w.length > 3).map((w) => w.replace(/[^a-z0-9]/g, "")));
24453
+ const candidates = [];
24454
+ for (const entry of entries) {
24455
+ const entryTerms = entry.value.toLowerCase().split(/\s+/).filter((w) => w.length > 3).map((w) => w.replace(/[^a-z0-9]/g, ""));
24456
+ const matchCount = entryTerms.filter((t) => taskTerms.has(t)).length;
24457
+ const relevance = entryTerms.length > 0 ? matchCount / Math.sqrt(entryTerms.length) : 0;
24458
+ if (relevance < noiseThreshold) {
24459
+ candidates.push({
24460
+ topic: entry.topic,
24461
+ key: entry.key,
24462
+ score: relevance,
24463
+ reason: relevance === 0 ? "no keyword overlap with current task" : `low relevance (${(relevance * 100).toFixed(0)}% < ${(noiseThreshold * 100).toFixed(0)}% threshold)`
24464
+ });
24465
+ }
24466
+ }
24467
+ return candidates.sort((a, b) => a.score - b.score);
24468
+ }
24469
+ // ── Helpers ──────────────────────────────────────────────────────────
24470
+ makeScore(ratio, dPrime, total, signal, noise, sparsity, evaluators, capacityWarning = false) {
24471
+ return {
24472
+ ratio: Math.max(0, Math.min(1, ratio)),
24473
+ dPrime: Math.max(0, dPrime),
24474
+ entriesEvaluated: total,
24475
+ signalCount: signal,
24476
+ noiseCount: noise,
24477
+ sparsity: Math.max(0, Math.min(1, sparsity)),
24478
+ evaluatedAt: (/* @__PURE__ */ new Date()).toISOString(),
24479
+ evaluators,
24480
+ capacityWarning
24481
+ };
24482
+ }
24483
+ };
24484
+ }
24485
+ });
24486
+
24116
24487
  // packages/cli/dist/tui/telegram-bridge.js
24117
24488
  function renderTelegramStart(botUsername, adminId) {
24118
24489
  process.stdout.write(`
@@ -24369,7 +24740,7 @@ function themeForTool(toolName) {
24369
24740
  return THEME_DEFAULT;
24370
24741
  }
24371
24742
  }
24372
- var DENSITY, WAVE, THEME_DEFAULT, THEME_FILE, THEME_SHELL, THEME_WEB, THEME_SEARCH, THEME_MEMORY, THEME_SKILL, THEME_TOOL_CREATE, DEFAULT_METRICS, BrailleSpinner;
24743
+ var DENSITY, WAVE, THEME_DEFAULT, THEME_FILE, THEME_SHELL, THEME_WEB, THEME_SEARCH, THEME_MEMORY, THEME_SKILL, THEME_TOOL_CREATE, THEME_DREAM, DEFAULT_METRICS, BrailleSpinner;
24373
24744
  var init_braille_spinner = __esm({
24374
24745
  "packages/cli/dist/tui/braille-spinner.js"() {
24375
24746
  "use strict";
@@ -24426,10 +24797,16 @@ var init_braille_spinner = __esm({
24426
24797
  ramp: [237, 173, 174, 179, 180, 215, 216, 222, 229],
24427
24798
  speed: 2
24428
24799
  };
24800
+ THEME_DREAM = {
24801
+ ramp: [237, 94, 130, 136, 172, 178, 179, 214, 220],
24802
+ speed: 1
24803
+ };
24429
24804
  DEFAULT_METRICS = {
24430
24805
  contextPct: 0,
24431
24806
  tokenRate: 0,
24432
- isStreaming: false
24807
+ isStreaming: false,
24808
+ snr: 0.5,
24809
+ isDreaming: false
24433
24810
  };
24434
24811
  BrailleSpinner = class {
24435
24812
  frame = 0;
@@ -24481,6 +24858,10 @@ var init_braille_spinner = __esm({
24481
24858
  this._metrics.tokenRate = metrics.tokenRate;
24482
24859
  if (metrics.isStreaming !== void 0)
24483
24860
  this._metrics.isStreaming = metrics.isStreaming;
24861
+ if (metrics.snr !== void 0)
24862
+ this._metrics.snr = Math.max(0, Math.min(1, metrics.snr));
24863
+ if (metrics.isDreaming !== void 0)
24864
+ this._metrics.isDreaming = metrics.isDreaming;
24484
24865
  }
24485
24866
  /**
24486
24867
  * Render the current animation frame as an ANSI-colored string.
@@ -24492,28 +24873,41 @@ var init_braille_spinner = __esm({
24492
24873
  * used — gentle ripples at low usage, full waves at high usage.
24493
24874
  * - Slinky: a secondary slow sine wave creates organic compression and
24494
24875
  * expansion across columns, making the wave feel elastic and alive.
24876
+ * - SNR entropy: low SNR injects phase jitter per-column (noisy neural signal),
24877
+ * high SNR produces smooth coherent waves (clean signal propagation).
24878
+ * - Dream mode: slower, deeper breathing with warm amber color override.
24495
24879
  */
24496
24880
  render(width) {
24497
24881
  const cycleLen = WAVE.length;
24498
- const baseSpeed = this.theme.speed;
24499
24882
  const m = this._metrics;
24500
- const breathPhase = Math.sin(this.frame * 0.08);
24883
+ const activeTheme = m.isDreaming ? THEME_DREAM : this.theme;
24884
+ const activeColorRamp = m.isDreaming ? buildColorRamp(THEME_DREAM.ramp) : this.colorRamp;
24885
+ const baseSpeed = activeTheme.speed;
24886
+ const breathRate = m.isDreaming ? 0.04 : 0.08;
24887
+ const breathPhase = Math.sin(this.frame * breathRate);
24501
24888
  let speed;
24502
24889
  if (m.isStreaming && m.tokenRate > 0) {
24503
24890
  const rateBoost = Math.min(4, m.tokenRate / 8);
24504
24891
  speed = baseSpeed + rateBoost + breathPhase * 0.3;
24892
+ } else if (m.isDreaming) {
24893
+ speed = baseSpeed * 0.6 + breathPhase * 1.2;
24505
24894
  } else {
24506
24895
  speed = baseSpeed + breathPhase * 0.8;
24507
24896
  }
24508
24897
  const pressure = Math.max(0, Math.min(100, m.contextPct)) / 100;
24509
24898
  const densityScale = 0.3 + pressure * 0.7;
24899
+ const snr = m.snr;
24900
+ const entropy = Math.max(0, 1 - snr);
24510
24901
  const slinkyFreq = 0.02 + (m.isStreaming ? 0.01 : 0);
24511
24902
  const slinkyAmp = 1.5 + pressure * 2;
24903
+ const jitterSeed = this.frame * 7919;
24512
24904
  let buf = "";
24513
24905
  let lastColor = -1;
24514
24906
  for (let col = 0; col < width; col++) {
24515
24907
  const slinkyOffset = Math.sin(col * 0.1 + this.frame * slinkyFreq) * slinkyAmp;
24516
- const rawPhase = col * speed + this.frame + slinkyOffset;
24908
+ const jitterHash = Math.sin((col * 127 + jitterSeed) * 1e-3) * 2 + Math.cos((col * 311 + jitterSeed) * 7e-4) * 1.5;
24909
+ const snrJitter = jitterHash * entropy * 3;
24910
+ const rawPhase = col * speed + this.frame + slinkyOffset + snrJitter;
24517
24911
  const normalizedPhase = (rawPhase % cycleLen + cycleLen) % cycleLen;
24518
24912
  const waveIdx = Math.round(normalizedPhase) % cycleLen;
24519
24913
  let amplitude;
@@ -24531,7 +24925,7 @@ var init_braille_spinner = __esm({
24531
24925
  }
24532
24926
  scaledIdx = Math.max(0, Math.min(cycleLen - 1, scaledIdx));
24533
24927
  const ch = WAVE[scaledIdx];
24534
- const color = this.colorRamp[scaledIdx];
24928
+ const color = activeColorRamp[scaledIdx];
24535
24929
  if (color !== lastColor) {
24536
24930
  buf += `\x1B[38;5;${color}m`;
24537
24931
  lastColor = color;
@@ -24843,6 +25237,13 @@ var init_status_bar = __esm({
24843
25237
  setActiveTool(toolName) {
24844
25238
  this._brailleSpinner.setTool(toolName);
24845
25239
  }
25240
+ /**
25241
+ * Forward metrics directly to the braille spinner.
25242
+ * Used for SNR, dream mode, and other neural activity indicators.
25243
+ */
25244
+ setBrailleMetrics(metrics) {
25245
+ this._brailleSpinner.setMetrics(metrics);
25246
+ }
24846
25247
  /** Context window size to display. Can be updated if model changes. */
24847
25248
  setContextWindowSize(size) {
24848
25249
  this.metrics.contextWindowSize = size;
@@ -24869,6 +25270,21 @@ var init_status_bar = __esm({
24869
25270
  recordSpeedTaskEnd() {
24870
25271
  this._speedTracker.taskEnd();
24871
25272
  }
25273
+ /** SNR (Signal-to-Noise Ratio) from context quality evaluation */
25274
+ _snr = null;
25275
+ /** Update the SNR gauge with a new evaluation result */
25276
+ setSNR(ratio, dPrime, capacityWarning) {
25277
+ this._snr = { ratio, dPrime, capacityWarning };
25278
+ this._brailleSpinner.setMetrics({ snr: ratio });
25279
+ if (this.active)
25280
+ this.renderFooterPreserveCursor();
25281
+ }
25282
+ /** Clear the SNR gauge (e.g., when starting a fresh task) */
25283
+ clearSNR() {
25284
+ this._snr = null;
25285
+ if (this.active)
25286
+ this.renderFooterPreserveCursor();
25287
+ }
24872
25288
  /** Model capabilities — shown as emoji indicators on the status bar */
24873
25289
  _caps = {
24874
25290
  vision: false,
@@ -25075,6 +25491,14 @@ var init_status_bar = __esm({
25075
25491
  const costStr = m.estimatedCost < 0.01 ? `$${m.estimatedCost.toFixed(4)}` : m.estimatedCost < 1 ? `$${m.estimatedCost.toFixed(3)}` : `$${m.estimatedCost.toFixed(2)}`;
25076
25492
  costLabel = pipe + pastel2(222, "Cost: ") + c2.bold(costStr);
25077
25493
  }
25494
+ let snrLabel = "";
25495
+ if (this._snr) {
25496
+ const snrPct = Math.round(this._snr.ratio * 100);
25497
+ const snrColor = snrPct >= 70 ? c2.green : snrPct >= 40 ? c2.yellow : c2.red;
25498
+ const dPrimeStr = this._snr.dPrime.toFixed(1);
25499
+ const capStr = this._snr.capacityWarning ? c2.red(" !CAP") : "";
25500
+ snrLabel = pipe + pastel2(183, "SNR: ") + snrColor(c2.bold(`${snrPct}%`)) + c2.dim(` d'${dPrimeStr}`) + capStr;
25501
+ }
25078
25502
  let speedLabel = "";
25079
25503
  if (this._speedTracker.hasData) {
25080
25504
  const ratio = this._speedTracker.getSpeedRatio();
@@ -25098,7 +25522,7 @@ var init_status_bar = __esm({
25098
25522
  if (this._caps.thinking)
25099
25523
  capParts.push("\u{1F9E0}");
25100
25524
  const capsLabel = capParts.length > 0 ? pipe + pastel2(183, capParts.join(" ")) : "";
25101
- return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
25525
+ return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${snrLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
25102
25526
  }
25103
25527
  // -------------------------------------------------------------------------
25104
25528
  // Private
@@ -25377,11 +25801,11 @@ var init_status_bar = __esm({
25377
25801
  import * as readline2 from "node:readline";
25378
25802
  import { Writable } from "node:stream";
25379
25803
  import { cwd } from "node:process";
25380
- import { resolve as resolve23, join as join37, dirname as dirname11, extname as extname9 } from "node:path";
25804
+ import { resolve as resolve23, join as join38, dirname as dirname11, extname as extname9 } from "node:path";
25381
25805
  import { createRequire as createRequire2 } from "node:module";
25382
25806
  import { fileURLToPath as fileURLToPath8 } from "node:url";
25383
- import { readFileSync as readFileSync20, rmSync as rmSync2 } from "node:fs";
25384
- import { existsSync as existsSync27 } from "node:fs";
25807
+ import { readFileSync as readFileSync21, rmSync as rmSync2, readdirSync as readdirSync13 } from "node:fs";
25808
+ import { existsSync as existsSync28 } from "node:fs";
25385
25809
  function formatTimeAgo(date) {
25386
25810
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
25387
25811
  if (seconds < 60)
@@ -25400,12 +25824,12 @@ function getVersion() {
25400
25824
  const require2 = createRequire2(import.meta.url);
25401
25825
  const thisDir = dirname11(fileURLToPath8(import.meta.url));
25402
25826
  const candidates = [
25403
- join37(thisDir, "..", "package.json"),
25404
- join37(thisDir, "..", "..", "package.json"),
25405
- join37(thisDir, "..", "..", "..", "package.json")
25827
+ join38(thisDir, "..", "package.json"),
25828
+ join38(thisDir, "..", "..", "package.json"),
25829
+ join38(thisDir, "..", "..", "..", "package.json")
25406
25830
  ];
25407
25831
  for (const pkgPath of candidates) {
25408
- if (existsSync27(pkgPath)) {
25832
+ if (existsSync28(pkgPath)) {
25409
25833
  const pkg = require2(pkgPath);
25410
25834
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
25411
25835
  return pkg.version ?? "0.0.0";
@@ -25416,7 +25840,7 @@ function getVersion() {
25416
25840
  }
25417
25841
  return "0.0.0";
25418
25842
  }
25419
- function adaptTool3(tool) {
25843
+ function adaptTool4(tool) {
25420
25844
  return {
25421
25845
  name: tool.name,
25422
25846
  description: tool.description,
@@ -25512,7 +25936,7 @@ function buildTools(repoRoot, config, contextWindowSize) {
25512
25936
  new AgendaTool(repoRoot)
25513
25937
  ];
25514
25938
  return [
25515
- ...executionTools.map(adaptTool3),
25939
+ ...executionTools.map(adaptTool4),
25516
25940
  createSubAgentTool(config, repoRoot, contextWindowSize),
25517
25941
  createTaskCompleteTool()
25518
25942
  ];
@@ -25564,7 +25988,7 @@ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
25564
25988
  new MemoryReadTool(repoRoot),
25565
25989
  new MemoryWriteTool(repoRoot)
25566
25990
  ];
25567
- subRunner.registerTools(subTools.map(adaptTool3));
25991
+ subRunner.registerTools(subTools.map(adaptTool4));
25568
25992
  subRunner.registerTool(createTaskCompleteTool());
25569
25993
  if (background) {
25570
25994
  const promise = subRunner.run(task, `Working directory: ${repoRoot}`).then((result2) => {
@@ -25594,7 +26018,30 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
25594
26018
  }
25595
26019
  };
25596
26020
  }
25597
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext) {
26021
+ function gatherMemorySnippets(root) {
26022
+ const snippets = [];
26023
+ const dirs = [
26024
+ join38(root, ".oa", "memory"),
26025
+ join38(root, ".open-agents", "memory")
26026
+ ];
26027
+ for (const dir of dirs) {
26028
+ if (!existsSync28(dir))
26029
+ continue;
26030
+ try {
26031
+ for (const f of readdirSync13(dir).filter((f2) => f2.endsWith(".json"))) {
26032
+ const data = JSON.parse(readFileSync21(join38(dir, f), "utf-8"));
26033
+ for (const val of Object.values(data)) {
26034
+ const v = typeof val === "object" && val !== null && "value" in val ? String(val.value) : String(val);
26035
+ if (v.length > 10)
26036
+ snippets.push(v);
26037
+ }
26038
+ }
26039
+ } catch {
26040
+ }
26041
+ }
26042
+ return snippets;
26043
+ }
26044
+ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext, onCompaction) {
25598
26045
  const voiceStyleMap = {
25599
26046
  concise: 1,
25600
26047
  balanced: 3,
@@ -25729,7 +26176,7 @@ ${entry.fullContent}`
25729
26176
  renderVoiceText(desc);
25730
26177
  voice.speak(desc);
25731
26178
  }
25732
- renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {});
26179
+ renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}, config.verbose);
25733
26180
  });
25734
26181
  break;
25735
26182
  case "tool_result": {
@@ -25745,7 +26192,7 @@ ${entry.fullContent}`
25745
26192
  const toolDurationMs = toolCallStartMs > 0 ? Date.now() - toolCallStartMs : 0;
25746
26193
  toolCallStartMs = 0;
25747
26194
  contentWrite(() => {
25748
- renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? "");
26195
+ renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? "", config.verbose);
25749
26196
  if (config.verbose && toolDurationMs > 0) {
25750
26197
  const durStr = toolDurationMs < 1e3 ? `${toolDurationMs}ms` : `${(toolDurationMs / 1e3).toFixed(1)}s`;
25751
26198
  const sizeStr = resultLen > 0 ? ` | ${resultLen.toLocaleString()} chars (~${Math.ceil(resultLen / 4).toLocaleString()} tokens)` : "";
@@ -25807,6 +26254,8 @@ ${entry.fullContent}`
25807
26254
  break;
25808
26255
  case "compaction":
25809
26256
  contentWrite(() => renderWarning(`Context compacted: ${event.content}`));
26257
+ if (onCompaction)
26258
+ onCompaction();
25810
26259
  break;
25811
26260
  case "status":
25812
26261
  contentWrite(() => renderInfo(event.content ?? ""));
@@ -26111,6 +26560,37 @@ async function startInteractive(config, repoPath) {
26111
26560
  let dreamEngine = null;
26112
26561
  let blessEngine = null;
26113
26562
  let dmnEngine = null;
26563
+ const snrEngine = new SNREngine(config, repoRoot);
26564
+ let pendingSNREval = null;
26565
+ function scheduleAgentSNREval(taskPrompt) {
26566
+ if (pendingSNREval)
26567
+ return;
26568
+ pendingSNREval = (async () => {
26569
+ try {
26570
+ const score = await snrEngine.evaluateWithAgents(taskPrompt, []);
26571
+ statusBar.setSNR(score.ratio, score.dPrime, score.capacityWarning);
26572
+ renderSNRUpdate(score);
26573
+ } catch {
26574
+ } finally {
26575
+ pendingSNREval = null;
26576
+ }
26577
+ })();
26578
+ }
26579
+ function compactionSNRCallback() {
26580
+ if (!deepContextEnabled)
26581
+ return void 0;
26582
+ return () => {
26583
+ try {
26584
+ const memSnippets = gatherMemorySnippets(repoRoot);
26585
+ if (memSnippets.length > 0 && lastSubmittedPrompt) {
26586
+ const snr = snrEngine.computeLocalSNR(lastSubmittedPrompt, memSnippets, resolvedContextWindowSize);
26587
+ statusBar.setSNR(snr.ratio, snr.dPrime, snr.capacityWarning);
26588
+ scheduleAgentSNREval(lastSubmittedPrompt);
26589
+ }
26590
+ } catch {
26591
+ }
26592
+ };
26593
+ }
26114
26594
  let telegramBridge = null;
26115
26595
  let activeTelegramChatId = null;
26116
26596
  let dmnRetriggerTimer = null;
@@ -26140,6 +26620,8 @@ Respond concisely and safely.`;
26140
26620
  return;
26141
26621
  }
26142
26622
  writeContent(() => renderInfo("DMN re-activating..."));
26623
+ statusBar.setProcessing(true);
26624
+ statusBar.setBrailleMetrics({ isDreaming: true });
26143
26625
  try {
26144
26626
  const proposal = await dmnEngine.runCycle((event) => {
26145
26627
  if (event.type === "tool_call") {
@@ -26147,6 +26629,8 @@ Respond concisely and safely.`;
26147
26629
  `));
26148
26630
  }
26149
26631
  });
26632
+ statusBar.setProcessing(false);
26633
+ statusBar.setBrailleMetrics({ isDreaming: false });
26150
26634
  if (proposal) {
26151
26635
  const provenanceNote = proposal.provenance.length > 0 ? `
26152
26636
 
@@ -26162,6 +26646,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26162
26646
  scheduleDMNRetrigger(6e4);
26163
26647
  }
26164
26648
  } catch (err) {
26649
+ statusBar.setProcessing(false);
26650
+ statusBar.setBrailleMetrics({ isDreaming: false });
26165
26651
  writeContent(() => renderWarning(`DMN cycle error: ${err instanceof Error ? err.message : String(err)}`));
26166
26652
  scheduleDMNRetrigger(6e4);
26167
26653
  }
@@ -26425,6 +26911,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26425
26911
  }
26426
26912
  dreamEngine = new DreamEngine(currentConfig, repoRoot);
26427
26913
  writeContent(() => renderDreamStart(mode));
26914
+ statusBar.setProcessing(true);
26915
+ statusBar.setBrailleMetrics({ isDreaming: true });
26428
26916
  dreamEngine.start(mode, (event) => {
26429
26917
  if (event.type === "tool_call") {
26430
26918
  writeContent(() => renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}));
@@ -26438,10 +26926,14 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26438
26926
  });
26439
26927
  }
26440
26928
  }).then((state) => {
26929
+ statusBar.setProcessing(false);
26930
+ statusBar.setBrailleMetrics({ isDreaming: false });
26441
26931
  writeContent(() => renderDreamEnd(state));
26442
26932
  dreamEngine = null;
26443
26933
  showPrompt();
26444
26934
  }).catch((err) => {
26935
+ statusBar.setProcessing(false);
26936
+ statusBar.setBrailleMetrics({ isDreaming: false });
26445
26937
  writeContent(() => renderError(`Dream error: ${err instanceof Error ? err.message : String(err)}`));
26446
26938
  dreamEngine = null;
26447
26939
  showPrompt();
@@ -26451,6 +26943,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26451
26943
  if (dreamEngine?.isActive) {
26452
26944
  dreamEngine.stop();
26453
26945
  dreamEngine = null;
26946
+ statusBar.setProcessing(false);
26947
+ statusBar.setBrailleMetrics({ isDreaming: false });
26454
26948
  }
26455
26949
  },
26456
26950
  isDreaming() {
@@ -26664,8 +27158,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
26664
27158
  return true;
26665
27159
  },
26666
27160
  destroyProject() {
26667
- const oaPath = join37(repoRoot, OA_DIR);
26668
- if (existsSync27(oaPath)) {
27161
+ const oaPath = join38(repoRoot, OA_DIR);
27162
+ if (existsSync28(oaPath)) {
26669
27163
  try {
26670
27164
  rmSync2(oaPath, { recursive: true, force: true });
26671
27165
  writeContent(() => renderInfo(`Removed ${OA_DIR}/ directory.`));
@@ -26924,7 +27418,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
26924
27418
  toolPatternStore: toolPatternStore ?? void 0
26925
27419
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
26926
27420
  lastCompletedSummary = summary;
26927
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled);
27421
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
26928
27422
  activeTask = task;
26929
27423
  showPrompt();
26930
27424
  await task.promise;
@@ -26939,13 +27433,13 @@ Execute this skill now. Follow the behavioral guidance above.`;
26939
27433
  }
26940
27434
  }
26941
27435
  const cleanPath = input.replace(/^['"]|['"]$/g, "").trim();
26942
- const isImage = isImagePath(cleanPath) && existsSync27(resolve23(repoRoot, cleanPath));
26943
- const isMedia = !isImage && isTranscribablePath(cleanPath) && existsSync27(resolve23(repoRoot, cleanPath));
27436
+ const isImage = isImagePath(cleanPath) && existsSync28(resolve23(repoRoot, cleanPath));
27437
+ const isMedia = !isImage && isTranscribablePath(cleanPath) && existsSync28(resolve23(repoRoot, cleanPath));
26944
27438
  if (activeTask) {
26945
27439
  if (isImage) {
26946
27440
  try {
26947
27441
  const imgPath = resolve23(repoRoot, cleanPath);
26948
- const imgBuffer = readFileSync20(imgPath);
27442
+ const imgBuffer = readFileSync21(imgPath);
26949
27443
  const base64 = imgBuffer.toString("base64");
26950
27444
  const ext = extname9(cleanPath).toLowerCase();
26951
27445
  const mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
@@ -27014,6 +27508,14 @@ Summarize or analyze this transcription as appropriate.`;
27014
27508
  const displayText = isImage ? `[Image: ${cleanPath}]` : inputLineCount > 1 ? `[pasted ${inputLineCount} lines]` : fullInput;
27015
27509
  writeContent(() => renderUserMessage(displayText));
27016
27510
  lastSubmittedPrompt = fullInput;
27511
+ try {
27512
+ const memSnippets = gatherMemorySnippets(repoRoot);
27513
+ if (memSnippets.length > 0) {
27514
+ const snr = snrEngine.computeLocalSNR(fullInput, memSnippets, resolvedContextWindowSize);
27515
+ statusBar.setSNR(snr.ratio, snr.dPrime, snr.capacityWarning);
27516
+ }
27517
+ } catch {
27518
+ }
27017
27519
  let taskInput = fullInput;
27018
27520
  if (restoredSessionContext) {
27019
27521
  taskInput = `${restoredSessionContext}
@@ -27036,7 +27538,7 @@ NEW TASK: ${fullInput}`;
27036
27538
  toolPatternStore: toolPatternStore ?? void 0
27037
27539
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
27038
27540
  lastCompletedSummary = summary;
27039
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled);
27541
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
27040
27542
  activeTask = task;
27041
27543
  showPrompt();
27042
27544
  await task.promise;
@@ -27065,6 +27567,17 @@ NEW TASK: ${fullInput}`;
27065
27567
  } finally {
27066
27568
  statusBar.setProcessing(false);
27067
27569
  statusBar.recordSpeedTaskEnd();
27570
+ try {
27571
+ const memSnippets = gatherMemorySnippets(repoRoot);
27572
+ if (memSnippets.length > 0 && lastSubmittedPrompt) {
27573
+ const snr = snrEngine.computeLocalSNR(lastSubmittedPrompt, memSnippets, resolvedContextWindowSize);
27574
+ statusBar.setSNR(snr.ratio, snr.dPrime, snr.capacityWarning);
27575
+ }
27576
+ } catch {
27577
+ }
27578
+ if (deepContextEnabled && lastSubmittedPrompt) {
27579
+ scheduleAgentSNREval(lastSubmittedPrompt);
27580
+ }
27068
27581
  if (activeTask) {
27069
27582
  sessionFilesTouched = Array.from(activeTask.filesTouched);
27070
27583
  sessionToolCallCount = activeTask.toolCallCount;
@@ -27119,6 +27632,8 @@ Respond concisely and safely.`;
27119
27632
  }
27120
27633
  if (dmnEngine) {
27121
27634
  writeContent(() => renderInfo("No queued tasks \u2014 DMN self-reflection activating..."));
27635
+ statusBar.setProcessing(true);
27636
+ statusBar.setBrailleMetrics({ isDreaming: true });
27122
27637
  try {
27123
27638
  const proposal = await dmnEngine.runCycle((event) => {
27124
27639
  if (event.type === "tool_call") {
@@ -27126,6 +27641,8 @@ Respond concisely and safely.`;
27126
27641
  `));
27127
27642
  }
27128
27643
  });
27644
+ statusBar.setProcessing(false);
27645
+ statusBar.setBrailleMetrics({ isDreaming: false });
27129
27646
  if (proposal) {
27130
27647
  const provenanceNote = proposal.provenance.length > 0 ? `
27131
27648
 
@@ -27145,6 +27662,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
27145
27662
  scheduleDMNRetrigger(cooldownSec * 1e3);
27146
27663
  }
27147
27664
  } catch (err) {
27665
+ statusBar.setProcessing(false);
27666
+ statusBar.setBrailleMetrics({ isDreaming: false });
27148
27667
  writeContent(() => renderWarning(`DMN cycle error: ${err instanceof Error ? err.message : String(err)}`));
27149
27668
  if (blessEngine?.isActive) {
27150
27669
  scheduleDMNRetrigger(6e4);
@@ -27259,6 +27778,7 @@ var init_interactive = __esm({
27259
27778
  init_dream_engine();
27260
27779
  init_bless_engine();
27261
27780
  init_dmn_engine();
27781
+ init_snr_engine();
27262
27782
  init_telegram_bridge();
27263
27783
  init_status_bar();
27264
27784
  init_dist6();
@@ -27297,7 +27817,7 @@ import { glob } from "glob";
27297
27817
  import ignore from "ignore";
27298
27818
  import { readFile as readFile14, stat as stat4 } from "node:fs/promises";
27299
27819
  import { createHash } from "node:crypto";
27300
- import { join as join38, relative as relative3, extname as extname10, basename as basename14 } from "node:path";
27820
+ import { join as join39, relative as relative3, extname as extname10, basename as basename15 } from "node:path";
27301
27821
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
27302
27822
  var init_codebase_indexer = __esm({
27303
27823
  "packages/indexer/dist/codebase-indexer.js"() {
@@ -27341,7 +27861,7 @@ var init_codebase_indexer = __esm({
27341
27861
  const ig = ignore.default();
27342
27862
  if (this.config.respectGitignore) {
27343
27863
  try {
27344
- const gitignoreContent = await readFile14(join38(this.config.rootDir, ".gitignore"), "utf-8");
27864
+ const gitignoreContent = await readFile14(join39(this.config.rootDir, ".gitignore"), "utf-8");
27345
27865
  ig.add(gitignoreContent);
27346
27866
  } catch {
27347
27867
  }
@@ -27356,7 +27876,7 @@ var init_codebase_indexer = __esm({
27356
27876
  for (const relativePath of files) {
27357
27877
  if (ig.ignores(relativePath))
27358
27878
  continue;
27359
- const fullPath = join38(this.config.rootDir, relativePath);
27879
+ const fullPath = join39(this.config.rootDir, relativePath);
27360
27880
  try {
27361
27881
  const fileStat = await stat4(fullPath);
27362
27882
  if (fileStat.size > this.config.maxFileSize)
@@ -27379,7 +27899,7 @@ var init_codebase_indexer = __esm({
27379
27899
  }
27380
27900
  buildTree(files) {
27381
27901
  const root = {
27382
- name: basename14(this.config.rootDir),
27902
+ name: basename15(this.config.rootDir),
27383
27903
  path: this.config.rootDir,
27384
27904
  type: "directory",
27385
27905
  children: []
@@ -27402,7 +27922,7 @@ var init_codebase_indexer = __esm({
27402
27922
  if (!child) {
27403
27923
  child = {
27404
27924
  name: part,
27405
- path: join38(current.path, part),
27925
+ path: join39(current.path, part),
27406
27926
  type: "directory",
27407
27927
  children: []
27408
27928
  };
@@ -27485,13 +28005,13 @@ __export(index_repo_exports, {
27485
28005
  indexRepoCommand: () => indexRepoCommand
27486
28006
  });
27487
28007
  import { resolve as resolve24 } from "node:path";
27488
- import { existsSync as existsSync28, statSync as statSync10 } from "node:fs";
28008
+ import { existsSync as existsSync29, statSync as statSync10 } from "node:fs";
27489
28009
  import { cwd as cwd2 } from "node:process";
27490
28010
  async function indexRepoCommand(opts, _config) {
27491
28011
  const repoRoot = resolve24(opts.repoPath ?? cwd2());
27492
28012
  printHeader("Index Repository");
27493
28013
  printInfo(`Indexing: ${repoRoot}`);
27494
- if (!existsSync28(repoRoot)) {
28014
+ if (!existsSync29(repoRoot)) {
27495
28015
  printError(`Path does not exist: ${repoRoot}`);
27496
28016
  process.exit(1);
27497
28017
  }
@@ -27743,7 +28263,7 @@ var config_exports = {};
27743
28263
  __export(config_exports, {
27744
28264
  configCommand: () => configCommand
27745
28265
  });
27746
- import { join as join39, resolve as resolve25 } from "node:path";
28266
+ import { join as join40, resolve as resolve25 } from "node:path";
27747
28267
  import { homedir as homedir13 } from "node:os";
27748
28268
  import { cwd as cwd3 } from "node:process";
27749
28269
  function redactIfSensitive(key, value) {
@@ -27802,7 +28322,7 @@ function handleShow(opts, config) {
27802
28322
  }
27803
28323
  }
27804
28324
  printSection("Config File");
27805
- printInfo(`~/.open-agents/config.json (${join39(homedir13(), ".open-agents", "config.json")})`);
28325
+ printInfo(`~/.open-agents/config.json (${join40(homedir13(), ".open-agents", "config.json")})`);
27806
28326
  printSection("Priority Chain");
27807
28327
  printInfo(" 1. CLI flags (--model, --backend-url, etc.)");
27808
28328
  printInfo(" 2. Project .oa/settings.json (--local)");
@@ -27841,7 +28361,7 @@ function handleSet(opts, _config) {
27841
28361
  const coerced = coerceForSettings(key, value);
27842
28362
  saveProjectSettings(repoRoot, { [key]: coerced });
27843
28363
  printSuccess(`Project override set: ${key} = ${redactIfSensitive(key, value)}`);
27844
- printInfo(`Saved to ${join39(repoRoot, ".oa", "settings.json")}`);
28364
+ printInfo(`Saved to ${join40(repoRoot, ".oa", "settings.json")}`);
27845
28365
  printInfo("This override applies only when running in this workspace.");
27846
28366
  } catch (err) {
27847
28367
  printError(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);
@@ -28061,7 +28581,7 @@ __export(eval_exports, {
28061
28581
  });
28062
28582
  import { tmpdir as tmpdir7 } from "node:os";
28063
28583
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync12 } from "node:fs";
28064
- import { join as join40 } from "node:path";
28584
+ import { join as join41 } from "node:path";
28065
28585
  async function evalCommand(opts, config) {
28066
28586
  const suiteName = opts.suite ?? "basic";
28067
28587
  const suite = SUITES[suiteName];
@@ -28182,9 +28702,9 @@ async function evalCommand(opts, config) {
28182
28702
  process.exit(failed > 0 ? 1 : 0);
28183
28703
  }
28184
28704
  function createTempEvalRepo() {
28185
- const dir = join40(tmpdir7(), `open-agents-eval-${Date.now()}`);
28705
+ const dir = join41(tmpdir7(), `open-agents-eval-${Date.now()}`);
28186
28706
  mkdirSync13(dir, { recursive: true });
28187
- writeFileSync12(join40(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
28707
+ writeFileSync12(join41(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
28188
28708
  return dir;
28189
28709
  }
28190
28710
  var BASIC_SUITE, FULL_SUITE, SUITES;
@@ -28244,7 +28764,7 @@ init_updater();
28244
28764
  import { parseArgs as nodeParseArgs2 } from "node:util";
28245
28765
  import { createRequire as createRequire3 } from "node:module";
28246
28766
  import { fileURLToPath as fileURLToPath9 } from "node:url";
28247
- import { dirname as dirname12, join as join41 } from "node:path";
28767
+ import { dirname as dirname12, join as join42 } from "node:path";
28248
28768
 
28249
28769
  // packages/cli/dist/cli.js
28250
28770
  import { createInterface } from "node:readline";
@@ -28351,7 +28871,7 @@ init_output();
28351
28871
  function getVersion2() {
28352
28872
  try {
28353
28873
  const require2 = createRequire3(import.meta.url);
28354
- const pkgPath = join41(dirname12(fileURLToPath9(import.meta.url)), "..", "package.json");
28874
+ const pkgPath = join42(dirname12(fileURLToPath9(import.meta.url)), "..", "package.json");
28355
28875
  const pkg = require2(pkgPath);
28356
28876
  return pkg.version;
28357
28877
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",