open-agents-ai 0.39.1 → 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 +51 -12
  2. package/dist/index.js +645 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -109,7 +109,7 @@ Long conversations consume context window tokens. Open Agents uses progressive c
109
109
 
110
110
  ### How It Works
111
111
 
112
- Compaction triggers automatically when estimated token usage reaches 75% of the model's context window. The system:
112
+ Compaction triggers automatically when estimated token usage reaches a tier-proportional threshold of the model's context window. The system:
113
113
 
114
114
  1. **Preserves** the system prompt and initial user task (head messages)
115
115
  2. **Summarizes** middle messages (tool calls, results, exploration) into a structured digest
@@ -131,24 +131,63 @@ Six strategies are available via `/compact <strategy>`:
131
131
 
132
132
  ### Automatic Compaction
133
133
 
134
- Compaction thresholds scale dynamically with model size:
134
+ Compaction thresholds scale **proportionally** with the model's actual context window size:
135
135
 
136
- | Model Tier | Threshold | Recent Messages Kept |
137
- |------------|-----------|---------------------|
138
- | Large (30B+) | 40,000 tokens (or 75% of context) | 12 messages |
139
- | Medium (8-29B) | 24,000 tokens (or 75% of context) | 8 messages |
140
- | Small (≤7B) | 12,000 tokens (or 75% of context) | 4-6 messages |
136
+ | Model Tier | Normal Mode | Deep Context Mode | Recent Messages Kept |
137
+ |------------|-------------|-------------------|---------------------|
138
+ | Large (30B+) | 75% of context window | 85% of context window | 4-12 (normal) / 4-24 (deep) |
139
+ | Medium (8-29B) | 70% of context window | 85% of context window | 4-12 (normal) / 4-24 (deep) |
140
+ | Small (≤7B) | 65% of context window | 85% of context window | 4-12 (normal) / 4-24 (deep) |
141
141
 
142
- ### Status Bar Context Tracking (`Ctx:`)
142
+ For example, a 128K-context large model compacts at ~96K tokens in normal mode (75%) or ~109K tokens in deep mode (85%) — instead of the previous fixed 40K threshold that wasted 69% of available context.
143
143
 
144
- The status bar displays a live `Ctx:` gauge showing estimated context window usage:
144
+ ### Deep Context Mode (`/deep`)
145
+
146
+ Toggle with `/deep` — relaxes compaction so large models leverage more of their context window for complex multi-step reasoning.
147
+
148
+ When deep context is active:
149
+ - **Compaction fires at 85%** of context instead of 65-75% — the model retains much more working memory
150
+ - **Double the recent messages** (up to 24 instead of 12) preserved after compaction
151
+ - **Richer summaries** — compression budget increased from 20% to 30% of context
152
+ - **Larger tool outputs** — cap raised from 8K to 16K chars per tool result
153
+ - **Relaxed output folding** — more head/tail lines preserved (50/25 instead of 20/10 for large models)
154
+
155
+ This mirrors how human cognition works during deep problem-solving: situationally-relevant memories are transiently activated to occupy a larger portion of working memory, with the most relevant details in high-attention positions while supporting context backs them up. LLM attention mechanisms work similarly — earlier relevant context still influences generation even at lower positional weight.
156
+
157
+ Use deep context for:
158
+ - Complex multi-file refactoring or debugging
159
+ - Architecture analysis across many files
160
+ - Long debugging sessions where error context from earlier is critical
161
+ - Tasks where the agent needs to reason about patterns across many files
162
+
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
+
165
+ ### Status Bar Context Tracking (`Ctx:` + `SNR:`)
166
+
167
+ The status bar displays a live `Ctx:` gauge showing estimated context window usage, plus an `SNR:` gauge showing context quality:
145
168
 
146
169
  ```
147
- In: 12,345 | Out: 4,567 | Ctx: 18,000/131,072 86% | Exp: 4.2x
148
- ^^^^^^^^^^^^^^^^^^^^^^^^
149
- 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
150
173
  ```
151
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
+
152
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:
153
192
 
154
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");
@@ -13180,6 +13180,7 @@ Rules:
13180
13180
  taskTimeoutMs: options?.taskTimeoutMs ?? 36e5,
13181
13181
  // 60 min
13182
13182
  compactionThreshold: options?.compactionThreshold ?? 4e4,
13183
+ deepContext: options?.deepContext ?? false,
13183
13184
  dynamicContext: options?.dynamicContext ?? "",
13184
13185
  streamEnabled: options?.streamEnabled ?? false,
13185
13186
  bruteForce: options?.bruteForce ?? true,
@@ -13225,19 +13226,47 @@ Rules:
13225
13226
  /**
13226
13227
  * Compute all context-dependent limits from the current contextWindowSize
13227
13228
  * and modelTier. Returns sensible defaults when contextWindowSize is 0.
13229
+ *
13230
+ * Key insight: LLM attention is most effective when the most relevant content
13231
+ * sits in high-attention positions (start and end of context). Rather than
13232
+ * compacting aggressively and losing detail, we scale thresholds proportionally
13233
+ * to the model's actual context window so larger models can leverage more of
13234
+ * their capacity. The deepContext flag further relaxes limits for complex
13235
+ * multi-step reasoning where earlier context carries critical signal.
13236
+ *
13237
+ * Context utilization targets:
13238
+ * - Small models: compact at ~65% (limited attention span, aggressive helps)
13239
+ * - Medium models: compact at ~70% (balanced)
13240
+ * - Large models: compact at ~75% (plenty of room, let attention work)
13241
+ * - Deep context: compact at ~85% (trust the model's full attention window)
13228
13242
  */
13229
13243
  contextLimits() {
13230
13244
  const ctx = this.options.contextWindowSize;
13231
13245
  const tier = this.options.modelTier ?? "large";
13232
- const compactionThreshold = ctx > 0 ? Math.min(this.options.compactionThreshold, Math.floor(ctx * 0.75)) : this.options.compactionThreshold;
13233
- const keepRecentDivisor = tier === "small" ? 2e3 : tier === "medium" ? 3e3 : 4e3;
13234
- const keepRecent = ctx > 0 ? Math.max(4, Math.min(12, Math.floor(ctx / keepRecentDivisor))) : 12;
13246
+ const deep = this.options.deepContext ?? false;
13247
+ let compactionThreshold;
13248
+ if (ctx > 0) {
13249
+ const factor = deep ? 0.85 : tier === "small" ? 0.65 : tier === "medium" ? 0.7 : 0.75;
13250
+ compactionThreshold = Math.floor(ctx * factor);
13251
+ } else {
13252
+ compactionThreshold = deep ? 8e4 : this.options.compactionThreshold;
13253
+ }
13254
+ const keepRecentMax = deep ? 24 : 12;
13255
+ let keepRecent;
13256
+ if (ctx > 0) {
13257
+ const keepRecentDivisor = tier === "small" ? 2e3 : tier === "medium" ? 3e3 : 4e3;
13258
+ keepRecent = Math.max(4, Math.min(keepRecentMax, Math.floor(ctx / keepRecentDivisor)));
13259
+ } else {
13260
+ keepRecent = deep ? 20 : 12;
13261
+ }
13235
13262
  const maxOutputTokens = ctx > 0 ? Math.min(this.options.maxTokens, Math.max(2048, Math.floor(ctx * 0.25))) : this.options.maxTokens;
13236
- const toolOutputMaxChars = ctx > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctx * 0.5))) : 8e3;
13237
- const foldLineThreshold = tier === "small" ? 30 : tier === "medium" ? 35 : 40;
13238
- const foldHeadLines = tier === "small" ? 12 : tier === "medium" ? 16 : 20;
13239
- const foldTailLines = tier === "small" ? 5 : tier === "medium" ? 8 : 10;
13240
- const maxSummaryChars = ctx > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctx * 0.2))) : 4e3;
13263
+ const toolOutputCeiling = deep ? 16e3 : 8e3;
13264
+ const toolOutputMaxChars = ctx > 0 ? Math.max(2e3, Math.min(toolOutputCeiling, Math.floor(ctx * (deep ? 0.8 : 0.5)))) : toolOutputCeiling;
13265
+ const foldLineThreshold = deep ? tier === "small" ? 50 : tier === "medium" ? 80 : 120 : tier === "small" ? 30 : tier === "medium" ? 35 : 40;
13266
+ const foldHeadLines = deep ? tier === "small" ? 20 : tier === "medium" ? 30 : 50 : tier === "small" ? 12 : tier === "medium" ? 16 : 20;
13267
+ const foldTailLines = deep ? tier === "small" ? 10 : tier === "medium" ? 15 : 25 : tier === "small" ? 5 : tier === "medium" ? 8 : 10;
13268
+ const maxSummaryCeiling = deep ? 16e3 : 8e3;
13269
+ const maxSummaryChars = ctx > 0 ? Math.max(2e3, Math.min(maxSummaryCeiling, Math.floor(ctx * (deep ? 0.3 : 0.2)))) : deep ? 8e3 : 4e3;
13241
13270
  const repetitionWindow = tier === "small" ? 6 : tier === "medium" ? 8 : 10;
13242
13271
  return {
13243
13272
  compactionThreshold,
@@ -14040,8 +14069,8 @@ ${marker}` : marker);
14040
14069
  return;
14041
14070
  try {
14042
14071
  const { mkdirSync: mkdirSync14, writeFileSync: writeFileSync13 } = __require("node:fs");
14043
- const { join: join42 } = __require("node:path");
14044
- 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);
14045
14074
  mkdirSync14(sessionDir, { recursive: true });
14046
14075
  const checkpoint = {
14047
14076
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14054,7 +14083,7 @@ ${marker}` : marker);
14054
14083
  memexEntryCount: this._memexArchive.size,
14055
14084
  fileRegistrySize: this._fileRegistry.size
14056
14085
  };
14057
- writeFileSync13(join42(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14086
+ writeFileSync13(join43(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14058
14087
  } catch {
14059
14088
  }
14060
14089
  }
@@ -15958,8 +15987,8 @@ var init_listen = __esm({
15958
15987
  const nvmBase = join27(homedir8(), ".nvm", "versions", "node");
15959
15988
  if (existsSync19(nvmBase)) {
15960
15989
  try {
15961
- const { readdirSync: readdirSync12 } = await import("node:fs");
15962
- for (const ver of readdirSync12(nvmBase)) {
15990
+ const { readdirSync: readdirSync14 } = await import("node:fs");
15991
+ for (const ver of readdirSync14(nvmBase)) {
15963
15992
  const tcPath = join27(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
15964
15993
  if (existsSync19(join27(tcPath, "dist", "index.js"))) {
15965
15994
  const { createRequire: createRequire4 } = await import("node:module");
@@ -16189,10 +16218,10 @@ transcribe-cli error: ${transcribeCliError}` : "";
16189
16218
  wordTimestamps: false
16190
16219
  });
16191
16220
  if (outputDir) {
16192
- const { basename: basename15 } = await import("node:path");
16221
+ const { basename: basename16 } = await import("node:path");
16193
16222
  const transcriptDir = join27(outputDir, ".oa", "transcripts");
16194
16223
  mkdirSync5(transcriptDir, { recursive: true });
16195
- const outFile = join27(transcriptDir, `${basename15(filePath)}.txt`);
16224
+ const outFile = join27(transcriptDir, `${basename16(filePath)}.txt`);
16196
16225
  writeFileSync5(outFile, result.text, "utf-8");
16197
16226
  }
16198
16227
  return {
@@ -16578,18 +16607,18 @@ function renderTaskAborted() {
16578
16607
  ${c2.yellow("\u26A0")} ${c2.bold("Task aborted by user")}
16579
16608
  `);
16580
16609
  }
16581
- function renderToolCallStart(toolName, args) {
16610
+ function renderToolCallStart(toolName, args, verbose) {
16582
16611
  const icon = TOOL_ICONS[toolName] ?? "\u{1F527}";
16583
16612
  const label = TOOL_LABELS[toolName] ?? toolName;
16584
- const argsSummary = formatToolArgs(toolName, args);
16613
+ const argsSummary = formatToolArgs(toolName, args, verbose);
16585
16614
  const colorFn = _colorsEnabled ? TOOL_COLORS[toolName] ?? c2.dim : (t) => t;
16586
16615
  const emojiPrefix = _emojisEnabled ? `${icon} ` : "";
16587
16616
  process.stdout.write(`
16588
16617
  ${c2.dim("\u23BF")} ${emojiPrefix}${colorFn(c2.bold(label))}${argsSummary ? c2.dim(": ") + argsSummary : ""}
16589
16618
  `);
16590
16619
  }
16591
- function renderToolResult(toolName, success, output) {
16592
- const maxW = getTermWidth() - 10;
16620
+ function renderToolResult(toolName, success, output, verbose) {
16621
+ const maxW = verbose ? Math.max(getTermWidth() - 10, 200) : getTermWidth() - 10;
16593
16622
  const prefix = ` ${c2.dim("\u23BF")} `;
16594
16623
  switch (toolName) {
16595
16624
  case "file_write": {
@@ -16647,18 +16676,47 @@ function renderToolResult(toolName, success, output) {
16647
16676
  `);
16648
16677
  return;
16649
16678
  }
16650
- const maxLines = 6;
16679
+ const maxLines = verbose ? 200 : 6;
16651
16680
  const shown = lines.slice(0, maxLines);
16652
16681
  for (const line of shown) {
16653
- if (isRawJsonDump(line)) {
16682
+ if (isRawJsonDump(line) && !verbose) {
16654
16683
  process.stdout.write(`${prefix}${c2.dim("(content omitted)")}
16655
16684
  `);
16656
16685
  return;
16657
16686
  }
16658
- const cropped = line.length > maxW ? line.slice(0, maxW - 3) + "..." : line;
16659
- const formatted = formatMarkdownLine(cropped);
16660
- 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}
16661
16718
  `);
16719
+ }
16662
16720
  }
16663
16721
  if (lines.length > maxLines) {
16664
16722
  process.stdout.write(`${prefix}${c2.dim(`... ${lines.length - maxLines} more lines`)}
@@ -16914,6 +16972,7 @@ function renderSlashHelp() {
16914
16972
  ["/compact", "Force context compaction now (default strategy)"],
16915
16973
  ["/compact <strategy>", "Compact with strategy: aggressive, decisions, errors, summary, structured"],
16916
16974
  ["/bruteforce", "Toggle brute-force mode (auto re-engage on turn limit)"],
16975
+ ["/deep", "Toggle deep context \u2014 relaxes compaction so large models use 85% of context"],
16917
16976
  ["/tools", "List agent-created custom tools"],
16918
16977
  ["/skills", "List available AIWG skills"],
16919
16978
  ["/skills <keyword>", "Filter skills by name or trigger"],
@@ -16996,8 +17055,8 @@ function renderConfig(config) {
16996
17055
  }
16997
17056
  process.stdout.write("\n");
16998
17057
  }
16999
- function formatToolArgs(toolName, args) {
17000
- const maxArg = Math.max(40, getTermWidth() - 20);
17058
+ function formatToolArgs(toolName, args, verbose) {
17059
+ const maxArg = verbose ? 1e4 : Math.max(40, getTermWidth() - 20);
17001
17060
  switch (toolName) {
17002
17061
  case "file_read":
17003
17062
  case "file_write":
@@ -19451,6 +19510,18 @@ async function handleSlashCommand(input, ctx) {
19451
19510
  renderInfo(`Brute-force mode: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 agent will auto re-engage when turn limit is hit, reassess and try creative strategies" : ""));
19452
19511
  return "handled";
19453
19512
  }
19513
+ case "deep":
19514
+ case "deep-context": {
19515
+ if (!ctx.deepContextToggle) {
19516
+ renderWarning("Deep context not available");
19517
+ return "handled";
19518
+ }
19519
+ const isOn = ctx.deepContextToggle();
19520
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
19521
+ save({ deepContext: isOn });
19522
+ renderInfo(`Deep context mode: ${isOn ? "ON" : "OFF"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 compaction relaxed to 85% of context window. Large models will retain more working memory for complex reasoning." : " \u2014 compaction restored to default thresholds."));
19523
+ return "handled";
19524
+ }
19454
19525
  case "emojis":
19455
19526
  case "emoji": {
19456
19527
  const current = ctx.getEmojis?.() ?? true;
@@ -19871,17 +19942,17 @@ async function handleUpdate(subcommand, ctx) {
19871
19942
  try {
19872
19943
  const { createRequire: createRequire4 } = await import("node:module");
19873
19944
  const { fileURLToPath: fileURLToPath10 } = await import("node:url");
19874
- const { dirname: dirname13, join: join42 } = await import("node:path");
19875
- 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");
19876
19947
  const req = createRequire4(import.meta.url);
19877
19948
  const thisDir = dirname13(fileURLToPath10(import.meta.url));
19878
19949
  const candidates = [
19879
- join42(thisDir, "..", "package.json"),
19880
- join42(thisDir, "..", "..", "package.json"),
19881
- join42(thisDir, "..", "..", "..", "package.json")
19950
+ join43(thisDir, "..", "package.json"),
19951
+ join43(thisDir, "..", "..", "package.json"),
19952
+ join43(thisDir, "..", "..", "..", "package.json")
19882
19953
  ];
19883
19954
  for (const pkgPath of candidates) {
19884
- if (existsSync29(pkgPath)) {
19955
+ if (existsSync30(pkgPath)) {
19885
19956
  const pkg = req(pkgPath);
19886
19957
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
19887
19958
  currentVersion = pkg.version ?? "0.0.0";
@@ -24071,6 +24142,348 @@ DMN state directory: ${this.stateDir}`);
24071
24142
  }
24072
24143
  });
24073
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
+
24074
24487
  // packages/cli/dist/tui/telegram-bridge.js
24075
24488
  function renderTelegramStart(botUsername, adminId) {
24076
24489
  process.stdout.write(`
@@ -24327,7 +24740,7 @@ function themeForTool(toolName) {
24327
24740
  return THEME_DEFAULT;
24328
24741
  }
24329
24742
  }
24330
- 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;
24331
24744
  var init_braille_spinner = __esm({
24332
24745
  "packages/cli/dist/tui/braille-spinner.js"() {
24333
24746
  "use strict";
@@ -24384,10 +24797,16 @@ var init_braille_spinner = __esm({
24384
24797
  ramp: [237, 173, 174, 179, 180, 215, 216, 222, 229],
24385
24798
  speed: 2
24386
24799
  };
24800
+ THEME_DREAM = {
24801
+ ramp: [237, 94, 130, 136, 172, 178, 179, 214, 220],
24802
+ speed: 1
24803
+ };
24387
24804
  DEFAULT_METRICS = {
24388
24805
  contextPct: 0,
24389
24806
  tokenRate: 0,
24390
- isStreaming: false
24807
+ isStreaming: false,
24808
+ snr: 0.5,
24809
+ isDreaming: false
24391
24810
  };
24392
24811
  BrailleSpinner = class {
24393
24812
  frame = 0;
@@ -24439,6 +24858,10 @@ var init_braille_spinner = __esm({
24439
24858
  this._metrics.tokenRate = metrics.tokenRate;
24440
24859
  if (metrics.isStreaming !== void 0)
24441
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;
24442
24865
  }
24443
24866
  /**
24444
24867
  * Render the current animation frame as an ANSI-colored string.
@@ -24450,28 +24873,41 @@ var init_braille_spinner = __esm({
24450
24873
  * used — gentle ripples at low usage, full waves at high usage.
24451
24874
  * - Slinky: a secondary slow sine wave creates organic compression and
24452
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.
24453
24879
  */
24454
24880
  render(width) {
24455
24881
  const cycleLen = WAVE.length;
24456
- const baseSpeed = this.theme.speed;
24457
24882
  const m = this._metrics;
24458
- 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);
24459
24888
  let speed;
24460
24889
  if (m.isStreaming && m.tokenRate > 0) {
24461
24890
  const rateBoost = Math.min(4, m.tokenRate / 8);
24462
24891
  speed = baseSpeed + rateBoost + breathPhase * 0.3;
24892
+ } else if (m.isDreaming) {
24893
+ speed = baseSpeed * 0.6 + breathPhase * 1.2;
24463
24894
  } else {
24464
24895
  speed = baseSpeed + breathPhase * 0.8;
24465
24896
  }
24466
24897
  const pressure = Math.max(0, Math.min(100, m.contextPct)) / 100;
24467
24898
  const densityScale = 0.3 + pressure * 0.7;
24899
+ const snr = m.snr;
24900
+ const entropy = Math.max(0, 1 - snr);
24468
24901
  const slinkyFreq = 0.02 + (m.isStreaming ? 0.01 : 0);
24469
24902
  const slinkyAmp = 1.5 + pressure * 2;
24903
+ const jitterSeed = this.frame * 7919;
24470
24904
  let buf = "";
24471
24905
  let lastColor = -1;
24472
24906
  for (let col = 0; col < width; col++) {
24473
24907
  const slinkyOffset = Math.sin(col * 0.1 + this.frame * slinkyFreq) * slinkyAmp;
24474
- 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;
24475
24911
  const normalizedPhase = (rawPhase % cycleLen + cycleLen) % cycleLen;
24476
24912
  const waveIdx = Math.round(normalizedPhase) % cycleLen;
24477
24913
  let amplitude;
@@ -24489,7 +24925,7 @@ var init_braille_spinner = __esm({
24489
24925
  }
24490
24926
  scaledIdx = Math.max(0, Math.min(cycleLen - 1, scaledIdx));
24491
24927
  const ch = WAVE[scaledIdx];
24492
- const color = this.colorRamp[scaledIdx];
24928
+ const color = activeColorRamp[scaledIdx];
24493
24929
  if (color !== lastColor) {
24494
24930
  buf += `\x1B[38;5;${color}m`;
24495
24931
  lastColor = color;
@@ -24801,6 +25237,13 @@ var init_status_bar = __esm({
24801
25237
  setActiveTool(toolName) {
24802
25238
  this._brailleSpinner.setTool(toolName);
24803
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
+ }
24804
25247
  /** Context window size to display. Can be updated if model changes. */
24805
25248
  setContextWindowSize(size) {
24806
25249
  this.metrics.contextWindowSize = size;
@@ -24827,6 +25270,21 @@ var init_status_bar = __esm({
24827
25270
  recordSpeedTaskEnd() {
24828
25271
  this._speedTracker.taskEnd();
24829
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
+ }
24830
25288
  /** Model capabilities — shown as emoji indicators on the status bar */
24831
25289
  _caps = {
24832
25290
  vision: false,
@@ -25033,6 +25491,14 @@ var init_status_bar = __esm({
25033
25491
  const costStr = m.estimatedCost < 0.01 ? `$${m.estimatedCost.toFixed(4)}` : m.estimatedCost < 1 ? `$${m.estimatedCost.toFixed(3)}` : `$${m.estimatedCost.toFixed(2)}`;
25034
25492
  costLabel = pipe + pastel2(222, "Cost: ") + c2.bold(costStr);
25035
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
+ }
25036
25502
  let speedLabel = "";
25037
25503
  if (this._speedTracker.hasData) {
25038
25504
  const ratio = this._speedTracker.getSpeedRatio();
@@ -25056,7 +25522,7 @@ var init_status_bar = __esm({
25056
25522
  if (this._caps.thinking)
25057
25523
  capParts.push("\u{1F9E0}");
25058
25524
  const capsLabel = capParts.length > 0 ? pipe + pastel2(183, capParts.join(" ")) : "";
25059
- return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
25525
+ return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${snrLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
25060
25526
  }
25061
25527
  // -------------------------------------------------------------------------
25062
25528
  // Private
@@ -25335,11 +25801,11 @@ var init_status_bar = __esm({
25335
25801
  import * as readline2 from "node:readline";
25336
25802
  import { Writable } from "node:stream";
25337
25803
  import { cwd } from "node:process";
25338
- 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";
25339
25805
  import { createRequire as createRequire2 } from "node:module";
25340
25806
  import { fileURLToPath as fileURLToPath8 } from "node:url";
25341
- import { readFileSync as readFileSync20, rmSync as rmSync2 } from "node:fs";
25342
- 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";
25343
25809
  function formatTimeAgo(date) {
25344
25810
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
25345
25811
  if (seconds < 60)
@@ -25358,12 +25824,12 @@ function getVersion() {
25358
25824
  const require2 = createRequire2(import.meta.url);
25359
25825
  const thisDir = dirname11(fileURLToPath8(import.meta.url));
25360
25826
  const candidates = [
25361
- join37(thisDir, "..", "package.json"),
25362
- join37(thisDir, "..", "..", "package.json"),
25363
- join37(thisDir, "..", "..", "..", "package.json")
25827
+ join38(thisDir, "..", "package.json"),
25828
+ join38(thisDir, "..", "..", "package.json"),
25829
+ join38(thisDir, "..", "..", "..", "package.json")
25364
25830
  ];
25365
25831
  for (const pkgPath of candidates) {
25366
- if (existsSync27(pkgPath)) {
25832
+ if (existsSync28(pkgPath)) {
25367
25833
  const pkg = require2(pkgPath);
25368
25834
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
25369
25835
  return pkg.version ?? "0.0.0";
@@ -25374,7 +25840,7 @@ function getVersion() {
25374
25840
  }
25375
25841
  return "0.0.0";
25376
25842
  }
25377
- function adaptTool3(tool) {
25843
+ function adaptTool4(tool) {
25378
25844
  return {
25379
25845
  name: tool.name,
25380
25846
  description: tool.description,
@@ -25470,7 +25936,7 @@ function buildTools(repoRoot, config, contextWindowSize) {
25470
25936
  new AgendaTool(repoRoot)
25471
25937
  ];
25472
25938
  return [
25473
- ...executionTools.map(adaptTool3),
25939
+ ...executionTools.map(adaptTool4),
25474
25940
  createSubAgentTool(config, repoRoot, contextWindowSize),
25475
25941
  createTaskCompleteTool()
25476
25942
  ];
@@ -25522,7 +25988,7 @@ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
25522
25988
  new MemoryReadTool(repoRoot),
25523
25989
  new MemoryWriteTool(repoRoot)
25524
25990
  ];
25525
- subRunner.registerTools(subTools.map(adaptTool3));
25991
+ subRunner.registerTools(subTools.map(adaptTool4));
25526
25992
  subRunner.registerTool(createTaskCompleteTool());
25527
25993
  if (background) {
25528
25994
  const promise = subRunner.run(task, `Working directory: ${repoRoot}`).then((result2) => {
@@ -25552,7 +26018,30 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
25552
26018
  }
25553
26019
  };
25554
26020
  }
25555
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality) {
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) {
25556
26045
  const voiceStyleMap = {
25557
26046
  concise: 1,
25558
26047
  balanced: 3,
@@ -25576,6 +26065,7 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
25576
26065
  taskTimeoutMs: 36e5,
25577
26066
  // 60 minutes — never give up prematurely
25578
26067
  compactionThreshold,
26068
+ deepContext: deepContext ?? false,
25579
26069
  dynamicContext,
25580
26070
  modelTier,
25581
26071
  streamEnabled: stream?.enabled ?? false,
@@ -25686,7 +26176,7 @@ ${entry.fullContent}`
25686
26176
  renderVoiceText(desc);
25687
26177
  voice.speak(desc);
25688
26178
  }
25689
- renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {});
26179
+ renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}, config.verbose);
25690
26180
  });
25691
26181
  break;
25692
26182
  case "tool_result": {
@@ -25702,7 +26192,7 @@ ${entry.fullContent}`
25702
26192
  const toolDurationMs = toolCallStartMs > 0 ? Date.now() - toolCallStartMs : 0;
25703
26193
  toolCallStartMs = 0;
25704
26194
  contentWrite(() => {
25705
- renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? "");
26195
+ renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? "", config.verbose);
25706
26196
  if (config.verbose && toolDurationMs > 0) {
25707
26197
  const durStr = toolDurationMs < 1e3 ? `${toolDurationMs}ms` : `${(toolDurationMs / 1e3).toFixed(1)}s`;
25708
26198
  const sizeStr = resultLen > 0 ? ` | ${resultLen.toLocaleString()} chars (~${Math.ceil(resultLen / 4).toLocaleString()} tokens)` : "";
@@ -25764,6 +26254,8 @@ ${entry.fullContent}`
25764
26254
  break;
25765
26255
  case "compaction":
25766
26256
  contentWrite(() => renderWarning(`Context compacted: ${event.content}`));
26257
+ if (onCompaction)
26258
+ onCompaction();
25767
26259
  break;
25768
26260
  case "status":
25769
26261
  contentWrite(() => renderInfo(event.content ?? ""));
@@ -25924,6 +26416,7 @@ async function startInteractive(config, repoPath) {
25924
26416
  let streamEnabled = savedSettings.stream ?? false;
25925
26417
  let bruteForceEnabled = savedSettings.bruteforce ?? true;
25926
26418
  let currentStyle = PRESET_NAMES.includes(savedSettings.style) ? savedSettings.style : "balanced";
26419
+ let deepContextEnabled = savedSettings.deepContext ?? false;
25927
26420
  if (savedSettings.emojis !== void 0)
25928
26421
  setEmojisEnabled(savedSettings.emojis);
25929
26422
  if (savedSettings.colors !== void 0)
@@ -26067,6 +26560,37 @@ async function startInteractive(config, repoPath) {
26067
26560
  let dreamEngine = null;
26068
26561
  let blessEngine = null;
26069
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
+ }
26070
26594
  let telegramBridge = null;
26071
26595
  let activeTelegramChatId = null;
26072
26596
  let dmnRetriggerTimer = null;
@@ -26096,6 +26620,8 @@ Respond concisely and safely.`;
26096
26620
  return;
26097
26621
  }
26098
26622
  writeContent(() => renderInfo("DMN re-activating..."));
26623
+ statusBar.setProcessing(true);
26624
+ statusBar.setBrailleMetrics({ isDreaming: true });
26099
26625
  try {
26100
26626
  const proposal = await dmnEngine.runCycle((event) => {
26101
26627
  if (event.type === "tool_call") {
@@ -26103,6 +26629,8 @@ Respond concisely and safely.`;
26103
26629
  `));
26104
26630
  }
26105
26631
  });
26632
+ statusBar.setProcessing(false);
26633
+ statusBar.setBrailleMetrics({ isDreaming: false });
26106
26634
  if (proposal) {
26107
26635
  const provenanceNote = proposal.provenance.length > 0 ? `
26108
26636
 
@@ -26118,6 +26646,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26118
26646
  scheduleDMNRetrigger(6e4);
26119
26647
  }
26120
26648
  } catch (err) {
26649
+ statusBar.setProcessing(false);
26650
+ statusBar.setBrailleMetrics({ isDreaming: false });
26121
26651
  writeContent(() => renderWarning(`DMN cycle error: ${err instanceof Error ? err.message : String(err)}`));
26122
26652
  scheduleDMNRetrigger(6e4);
26123
26653
  }
@@ -26175,6 +26705,7 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26175
26705
  "/telegram",
26176
26706
  "/compact",
26177
26707
  "/gc",
26708
+ "/deep",
26178
26709
  "/style",
26179
26710
  "/personality"
26180
26711
  ];
@@ -26336,6 +26867,10 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26336
26867
  bruteForceEnabled = !bruteForceEnabled;
26337
26868
  return bruteForceEnabled;
26338
26869
  },
26870
+ deepContextToggle() {
26871
+ deepContextEnabled = !deepContextEnabled;
26872
+ return deepContextEnabled;
26873
+ },
26339
26874
  setStyle(preset) {
26340
26875
  currentStyle = preset;
26341
26876
  },
@@ -26376,6 +26911,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26376
26911
  }
26377
26912
  dreamEngine = new DreamEngine(currentConfig, repoRoot);
26378
26913
  writeContent(() => renderDreamStart(mode));
26914
+ statusBar.setProcessing(true);
26915
+ statusBar.setBrailleMetrics({ isDreaming: true });
26379
26916
  dreamEngine.start(mode, (event) => {
26380
26917
  if (event.type === "tool_call") {
26381
26918
  writeContent(() => renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}));
@@ -26389,10 +26926,14 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26389
26926
  });
26390
26927
  }
26391
26928
  }).then((state) => {
26929
+ statusBar.setProcessing(false);
26930
+ statusBar.setBrailleMetrics({ isDreaming: false });
26392
26931
  writeContent(() => renderDreamEnd(state));
26393
26932
  dreamEngine = null;
26394
26933
  showPrompt();
26395
26934
  }).catch((err) => {
26935
+ statusBar.setProcessing(false);
26936
+ statusBar.setBrailleMetrics({ isDreaming: false });
26396
26937
  writeContent(() => renderError(`Dream error: ${err instanceof Error ? err.message : String(err)}`));
26397
26938
  dreamEngine = null;
26398
26939
  showPrompt();
@@ -26402,6 +26943,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
26402
26943
  if (dreamEngine?.isActive) {
26403
26944
  dreamEngine.stop();
26404
26945
  dreamEngine = null;
26946
+ statusBar.setProcessing(false);
26947
+ statusBar.setBrailleMetrics({ isDreaming: false });
26405
26948
  }
26406
26949
  },
26407
26950
  isDreaming() {
@@ -26615,8 +27158,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
26615
27158
  return true;
26616
27159
  },
26617
27160
  destroyProject() {
26618
- const oaPath = join37(repoRoot, OA_DIR);
26619
- if (existsSync27(oaPath)) {
27161
+ const oaPath = join38(repoRoot, OA_DIR);
27162
+ if (existsSync28(oaPath)) {
26620
27163
  try {
26621
27164
  rmSync2(oaPath, { recursive: true, force: true });
26622
27165
  writeContent(() => renderInfo(`Removed ${OA_DIR}/ directory.`));
@@ -26875,7 +27418,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
26875
27418
  toolPatternStore: toolPatternStore ?? void 0
26876
27419
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
26877
27420
  lastCompletedSummary = summary;
26878
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle);
27421
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
26879
27422
  activeTask = task;
26880
27423
  showPrompt();
26881
27424
  await task.promise;
@@ -26890,13 +27433,13 @@ Execute this skill now. Follow the behavioral guidance above.`;
26890
27433
  }
26891
27434
  }
26892
27435
  const cleanPath = input.replace(/^['"]|['"]$/g, "").trim();
26893
- const isImage = isImagePath(cleanPath) && existsSync27(resolve23(repoRoot, cleanPath));
26894
- 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));
26895
27438
  if (activeTask) {
26896
27439
  if (isImage) {
26897
27440
  try {
26898
27441
  const imgPath = resolve23(repoRoot, cleanPath);
26899
- const imgBuffer = readFileSync20(imgPath);
27442
+ const imgBuffer = readFileSync21(imgPath);
26900
27443
  const base64 = imgBuffer.toString("base64");
26901
27444
  const ext = extname9(cleanPath).toLowerCase();
26902
27445
  const mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
@@ -26965,6 +27508,14 @@ Summarize or analyze this transcription as appropriate.`;
26965
27508
  const displayText = isImage ? `[Image: ${cleanPath}]` : inputLineCount > 1 ? `[pasted ${inputLineCount} lines]` : fullInput;
26966
27509
  writeContent(() => renderUserMessage(displayText));
26967
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
+ }
26968
27519
  let taskInput = fullInput;
26969
27520
  if (restoredSessionContext) {
26970
27521
  taskInput = `${restoredSessionContext}
@@ -26987,7 +27538,7 @@ NEW TASK: ${fullInput}`;
26987
27538
  toolPatternStore: toolPatternStore ?? void 0
26988
27539
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
26989
27540
  lastCompletedSummary = summary;
26990
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle);
27541
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
26991
27542
  activeTask = task;
26992
27543
  showPrompt();
26993
27544
  await task.promise;
@@ -27016,6 +27567,17 @@ NEW TASK: ${fullInput}`;
27016
27567
  } finally {
27017
27568
  statusBar.setProcessing(false);
27018
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
+ }
27019
27581
  if (activeTask) {
27020
27582
  sessionFilesTouched = Array.from(activeTask.filesTouched);
27021
27583
  sessionToolCallCount = activeTask.toolCallCount;
@@ -27070,6 +27632,8 @@ Respond concisely and safely.`;
27070
27632
  }
27071
27633
  if (dmnEngine) {
27072
27634
  writeContent(() => renderInfo("No queued tasks \u2014 DMN self-reflection activating..."));
27635
+ statusBar.setProcessing(true);
27636
+ statusBar.setBrailleMetrics({ isDreaming: true });
27073
27637
  try {
27074
27638
  const proposal = await dmnEngine.runCycle((event) => {
27075
27639
  if (event.type === "tool_call") {
@@ -27077,6 +27641,8 @@ Respond concisely and safely.`;
27077
27641
  `));
27078
27642
  }
27079
27643
  });
27644
+ statusBar.setProcessing(false);
27645
+ statusBar.setBrailleMetrics({ isDreaming: false });
27080
27646
  if (proposal) {
27081
27647
  const provenanceNote = proposal.provenance.length > 0 ? `
27082
27648
 
@@ -27096,6 +27662,8 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
27096
27662
  scheduleDMNRetrigger(cooldownSec * 1e3);
27097
27663
  }
27098
27664
  } catch (err) {
27665
+ statusBar.setProcessing(false);
27666
+ statusBar.setBrailleMetrics({ isDreaming: false });
27099
27667
  writeContent(() => renderWarning(`DMN cycle error: ${err instanceof Error ? err.message : String(err)}`));
27100
27668
  if (blessEngine?.isActive) {
27101
27669
  scheduleDMNRetrigger(6e4);
@@ -27210,6 +27778,7 @@ var init_interactive = __esm({
27210
27778
  init_dream_engine();
27211
27779
  init_bless_engine();
27212
27780
  init_dmn_engine();
27781
+ init_snr_engine();
27213
27782
  init_telegram_bridge();
27214
27783
  init_status_bar();
27215
27784
  init_dist6();
@@ -27248,7 +27817,7 @@ import { glob } from "glob";
27248
27817
  import ignore from "ignore";
27249
27818
  import { readFile as readFile14, stat as stat4 } from "node:fs/promises";
27250
27819
  import { createHash } from "node:crypto";
27251
- 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";
27252
27821
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
27253
27822
  var init_codebase_indexer = __esm({
27254
27823
  "packages/indexer/dist/codebase-indexer.js"() {
@@ -27292,7 +27861,7 @@ var init_codebase_indexer = __esm({
27292
27861
  const ig = ignore.default();
27293
27862
  if (this.config.respectGitignore) {
27294
27863
  try {
27295
- const gitignoreContent = await readFile14(join38(this.config.rootDir, ".gitignore"), "utf-8");
27864
+ const gitignoreContent = await readFile14(join39(this.config.rootDir, ".gitignore"), "utf-8");
27296
27865
  ig.add(gitignoreContent);
27297
27866
  } catch {
27298
27867
  }
@@ -27307,7 +27876,7 @@ var init_codebase_indexer = __esm({
27307
27876
  for (const relativePath of files) {
27308
27877
  if (ig.ignores(relativePath))
27309
27878
  continue;
27310
- const fullPath = join38(this.config.rootDir, relativePath);
27879
+ const fullPath = join39(this.config.rootDir, relativePath);
27311
27880
  try {
27312
27881
  const fileStat = await stat4(fullPath);
27313
27882
  if (fileStat.size > this.config.maxFileSize)
@@ -27330,7 +27899,7 @@ var init_codebase_indexer = __esm({
27330
27899
  }
27331
27900
  buildTree(files) {
27332
27901
  const root = {
27333
- name: basename14(this.config.rootDir),
27902
+ name: basename15(this.config.rootDir),
27334
27903
  path: this.config.rootDir,
27335
27904
  type: "directory",
27336
27905
  children: []
@@ -27353,7 +27922,7 @@ var init_codebase_indexer = __esm({
27353
27922
  if (!child) {
27354
27923
  child = {
27355
27924
  name: part,
27356
- path: join38(current.path, part),
27925
+ path: join39(current.path, part),
27357
27926
  type: "directory",
27358
27927
  children: []
27359
27928
  };
@@ -27436,13 +28005,13 @@ __export(index_repo_exports, {
27436
28005
  indexRepoCommand: () => indexRepoCommand
27437
28006
  });
27438
28007
  import { resolve as resolve24 } from "node:path";
27439
- import { existsSync as existsSync28, statSync as statSync10 } from "node:fs";
28008
+ import { existsSync as existsSync29, statSync as statSync10 } from "node:fs";
27440
28009
  import { cwd as cwd2 } from "node:process";
27441
28010
  async function indexRepoCommand(opts, _config) {
27442
28011
  const repoRoot = resolve24(opts.repoPath ?? cwd2());
27443
28012
  printHeader("Index Repository");
27444
28013
  printInfo(`Indexing: ${repoRoot}`);
27445
- if (!existsSync28(repoRoot)) {
28014
+ if (!existsSync29(repoRoot)) {
27446
28015
  printError(`Path does not exist: ${repoRoot}`);
27447
28016
  process.exit(1);
27448
28017
  }
@@ -27694,7 +28263,7 @@ var config_exports = {};
27694
28263
  __export(config_exports, {
27695
28264
  configCommand: () => configCommand
27696
28265
  });
27697
- import { join as join39, resolve as resolve25 } from "node:path";
28266
+ import { join as join40, resolve as resolve25 } from "node:path";
27698
28267
  import { homedir as homedir13 } from "node:os";
27699
28268
  import { cwd as cwd3 } from "node:process";
27700
28269
  function redactIfSensitive(key, value) {
@@ -27753,7 +28322,7 @@ function handleShow(opts, config) {
27753
28322
  }
27754
28323
  }
27755
28324
  printSection("Config File");
27756
- printInfo(`~/.open-agents/config.json (${join39(homedir13(), ".open-agents", "config.json")})`);
28325
+ printInfo(`~/.open-agents/config.json (${join40(homedir13(), ".open-agents", "config.json")})`);
27757
28326
  printSection("Priority Chain");
27758
28327
  printInfo(" 1. CLI flags (--model, --backend-url, etc.)");
27759
28328
  printInfo(" 2. Project .oa/settings.json (--local)");
@@ -27792,7 +28361,7 @@ function handleSet(opts, _config) {
27792
28361
  const coerced = coerceForSettings(key, value);
27793
28362
  saveProjectSettings(repoRoot, { [key]: coerced });
27794
28363
  printSuccess(`Project override set: ${key} = ${redactIfSensitive(key, value)}`);
27795
- printInfo(`Saved to ${join39(repoRoot, ".oa", "settings.json")}`);
28364
+ printInfo(`Saved to ${join40(repoRoot, ".oa", "settings.json")}`);
27796
28365
  printInfo("This override applies only when running in this workspace.");
27797
28366
  } catch (err) {
27798
28367
  printError(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);
@@ -28012,7 +28581,7 @@ __export(eval_exports, {
28012
28581
  });
28013
28582
  import { tmpdir as tmpdir7 } from "node:os";
28014
28583
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync12 } from "node:fs";
28015
- import { join as join40 } from "node:path";
28584
+ import { join as join41 } from "node:path";
28016
28585
  async function evalCommand(opts, config) {
28017
28586
  const suiteName = opts.suite ?? "basic";
28018
28587
  const suite = SUITES[suiteName];
@@ -28133,9 +28702,9 @@ async function evalCommand(opts, config) {
28133
28702
  process.exit(failed > 0 ? 1 : 0);
28134
28703
  }
28135
28704
  function createTempEvalRepo() {
28136
- const dir = join40(tmpdir7(), `open-agents-eval-${Date.now()}`);
28705
+ const dir = join41(tmpdir7(), `open-agents-eval-${Date.now()}`);
28137
28706
  mkdirSync13(dir, { recursive: true });
28138
- 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");
28139
28708
  return dir;
28140
28709
  }
28141
28710
  var BASIC_SUITE, FULL_SUITE, SUITES;
@@ -28195,7 +28764,7 @@ init_updater();
28195
28764
  import { parseArgs as nodeParseArgs2 } from "node:util";
28196
28765
  import { createRequire as createRequire3 } from "node:module";
28197
28766
  import { fileURLToPath as fileURLToPath9 } from "node:url";
28198
- import { dirname as dirname12, join as join41 } from "node:path";
28767
+ import { dirname as dirname12, join as join42 } from "node:path";
28199
28768
 
28200
28769
  // packages/cli/dist/cli.js
28201
28770
  import { createInterface } from "node:readline";
@@ -28302,7 +28871,7 @@ init_output();
28302
28871
  function getVersion2() {
28303
28872
  try {
28304
28873
  const require2 = createRequire3(import.meta.url);
28305
- const pkgPath = join41(dirname12(fileURLToPath9(import.meta.url)), "..", "package.json");
28874
+ const pkgPath = join42(dirname12(fileURLToPath9(import.meta.url)), "..", "package.json");
28306
28875
  const pkg = require2(pkgPath);
28307
28876
  return pkg.version;
28308
28877
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.39.1",
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",