@bubblebrain-ai/bubble 0.0.6 → 0.0.7

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.
@@ -3,21 +3,16 @@ import type { TaskType } from "./task-classifier.js";
3
3
  export interface GovernorDecision {
4
4
  blockedResult?: ToolResult;
5
5
  }
6
- type WorkPhase = "explore" | "modify" | "verify";
7
6
  export declare class ExecutionGovernor {
8
- private taskType;
9
7
  private budget;
10
8
  private history;
11
9
  private totalSteps;
12
10
  private searchSteps;
13
11
  private readSteps;
14
- private explorationStepsWithoutWrite;
15
- private searchFrozen;
16
- private explorationFrozen;
17
- private phase;
18
- private codeChanged;
12
+ private mutationVersion;
19
13
  private reminderQueue;
20
14
  private warnedFamilies;
15
+ private warnedSignatures;
21
16
  private softTotalWarned;
22
17
  private softSearchWarned;
23
18
  private softReadWarned;
@@ -29,16 +24,13 @@ export declare class ExecutionGovernor {
29
24
  readSteps: number;
30
25
  searchFrozen: boolean;
31
26
  explorationFrozen: boolean;
32
- phase: WorkPhase;
27
+ phase: "observe";
33
28
  };
34
29
  filterToolDefinitions(toolDefinitions: ToolRegistryEntry[]): ToolRegistryEntry[];
35
30
  beforeToolCall(toolCall: ParsedToolCall): GovernorDecision;
36
31
  afterToolResult(toolCall: ParsedToolCall, result: ToolResult): void;
37
32
  private trailingNoProgressCount;
38
- private freezeSearch;
39
- private enterModifyPhase;
40
- private isModificationTask;
41
- private historyCount;
33
+ private hasCurrentMutationObservation;
34
+ private warnOnce;
42
35
  private maybeWarnOnSoftBudgets;
43
36
  }
44
- export {};
@@ -1,118 +1,84 @@
1
1
  import { analyzeToolIntent } from "./tool-intent.js";
2
- import { buildExplorationFreezeReminder, buildInvestigationReminder, buildLoopWarningReminder, buildSearchFreezeReminder } from "../prompt/reminders.js";
2
+ import { buildInvestigationReminder, buildLoopWarningReminder } from "../prompt/reminders.js";
3
3
  const BUDGETS = {
4
4
  security_investigation: {
5
5
  softTotalSteps: 14,
6
6
  softSearchSteps: 6,
7
7
  softReadSteps: 8,
8
- maxExplorationStepsWithoutWrite: 12,
9
- maxNoProgressReadExactRepeats: 2,
10
- maxNoProgressExactRepeats: 2,
11
- maxNoProgressFamilyRepeats: 3,
8
+ warningExactRepeats: 2,
12
9
  warningFamilyRepeats: 2,
13
10
  },
14
11
  code_search: {
15
12
  softTotalSteps: 16,
16
13
  softSearchSteps: 8,
17
14
  softReadSteps: 10,
18
- maxExplorationStepsWithoutWrite: 14,
19
- maxNoProgressReadExactRepeats: 2,
20
- maxNoProgressExactRepeats: 3,
21
- maxNoProgressFamilyRepeats: 4,
15
+ warningExactRepeats: 2,
22
16
  warningFamilyRepeats: 3,
23
17
  },
24
18
  debugging: {
25
19
  softTotalSteps: 18,
26
20
  softSearchSteps: 8,
27
21
  softReadSteps: 7,
28
- maxExplorationStepsWithoutWrite: 10,
29
- maxNoProgressReadExactRepeats: 1,
30
- maxNoProgressExactRepeats: 3,
31
- maxNoProgressFamilyRepeats: 4,
22
+ warningExactRepeats: 1,
32
23
  warningFamilyRepeats: 3,
33
24
  },
34
25
  implementation: {
35
26
  softTotalSteps: 18,
36
27
  softSearchSteps: 8,
37
28
  softReadSteps: 6,
38
- maxExplorationStepsWithoutWrite: 8,
39
- maxNoProgressReadExactRepeats: 1,
40
- maxNoProgressExactRepeats: 3,
41
- maxNoProgressFamilyRepeats: 4,
29
+ warningExactRepeats: 1,
42
30
  warningFamilyRepeats: 3,
43
31
  },
44
32
  code_review: {
45
33
  softTotalSteps: 14,
46
34
  softSearchSteps: 6,
47
35
  softReadSteps: 8,
48
- maxExplorationStepsWithoutWrite: 12,
49
- maxNoProgressReadExactRepeats: 2,
50
- maxNoProgressExactRepeats: 3,
51
- maxNoProgressFamilyRepeats: 4,
36
+ warningExactRepeats: 2,
52
37
  warningFamilyRepeats: 3,
53
38
  },
54
39
  code_explanation: {
55
40
  softTotalSteps: 12,
56
41
  softSearchSteps: 6,
57
42
  softReadSteps: 8,
58
- maxExplorationStepsWithoutWrite: 12,
59
- maxNoProgressReadExactRepeats: 2,
60
- maxNoProgressExactRepeats: 3,
61
- maxNoProgressFamilyRepeats: 4,
43
+ warningExactRepeats: 2,
62
44
  warningFamilyRepeats: 3,
63
45
  },
64
46
  repo_orientation: {
65
47
  softTotalSteps: 12,
66
48
  softSearchSteps: 6,
67
49
  softReadSteps: 8,
68
- maxExplorationStepsWithoutWrite: 12,
69
- maxNoProgressReadExactRepeats: 2,
70
- maxNoProgressExactRepeats: 3,
71
- maxNoProgressFamilyRepeats: 4,
50
+ warningExactRepeats: 2,
72
51
  warningFamilyRepeats: 3,
73
52
  },
74
53
  product_discussion: {
75
54
  softTotalSteps: 10,
76
55
  softSearchSteps: 4,
77
56
  softReadSteps: 4,
78
- maxExplorationStepsWithoutWrite: 8,
79
- maxNoProgressReadExactRepeats: 2,
80
- maxNoProgressExactRepeats: 2,
81
- maxNoProgressFamilyRepeats: 3,
57
+ warningExactRepeats: 2,
82
58
  warningFamilyRepeats: 2,
83
59
  },
84
60
  general: {
85
61
  softTotalSteps: 18,
86
62
  softSearchSteps: 8,
87
63
  softReadSteps: 10,
88
- maxExplorationStepsWithoutWrite: 14,
89
- maxNoProgressReadExactRepeats: 2,
90
- maxNoProgressExactRepeats: 3,
91
- maxNoProgressFamilyRepeats: 4,
64
+ warningExactRepeats: 2,
92
65
  warningFamilyRepeats: 3,
93
66
  },
94
67
  };
95
- const SEARCH_TOOLS_DISABLED = new Set(["grep", "web_search", "web_fetch"]);
96
- const EXPLORATION_TOOLS_DISABLED = new Set(["read", "glob", "grep", "web_search", "web_fetch", "spawn_agent", "wait_agent", "send_input", "tool_search"]);
97
68
  export class ExecutionGovernor {
98
- taskType;
99
69
  budget;
100
70
  history = [];
101
71
  totalSteps = 0;
102
72
  searchSteps = 0;
103
73
  readSteps = 0;
104
- explorationStepsWithoutWrite = 0;
105
- searchFrozen = false;
106
- explorationFrozen = false;
107
- phase = "explore";
108
- codeChanged = false;
74
+ mutationVersion = 0;
109
75
  reminderQueue = [];
110
76
  warnedFamilies = new Set();
77
+ warnedSignatures = new Set();
111
78
  softTotalWarned = false;
112
79
  softSearchWarned = false;
113
80
  softReadWarned = false;
114
81
  constructor(taskType) {
115
- this.taskType = taskType;
116
82
  this.budget = BUDGETS[taskType];
117
83
  if (taskType === "security_investigation") {
118
84
  this.reminderQueue.push(buildInvestigationReminder());
@@ -128,62 +94,33 @@ export class ExecutionGovernor {
128
94
  totalSteps: this.totalSteps,
129
95
  searchSteps: this.searchSteps,
130
96
  readSteps: this.readSteps,
131
- searchFrozen: this.searchFrozen,
132
- explorationFrozen: this.explorationFrozen,
133
- phase: this.phase,
97
+ searchFrozen: false,
98
+ explorationFrozen: false,
99
+ phase: "observe",
134
100
  };
135
101
  }
136
102
  filterToolDefinitions(toolDefinitions) {
137
- let filtered = toolDefinitions;
138
- if (this.explorationFrozen) {
139
- filtered = filtered.filter((tool) => !EXPLORATION_TOOLS_DISABLED.has(tool.name));
140
- }
141
- else if (this.searchFrozen) {
142
- filtered = filtered.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
143
- }
144
- return filtered;
103
+ return toolDefinitions;
145
104
  }
146
105
  beforeToolCall(toolCall) {
147
106
  const intent = analyzeToolIntent(toolCall);
148
- if (this.explorationFrozen && isExplorationIntent(intent)) {
149
- return {
150
- blockedResult: blockedResult("Exploration blocked: this implementation task already has enough context. Use edit/write, verify an existing change, or explain the blocker.", "blocked", "Exploration frozen because tool calls stopped producing task progress.", metadataKindForFamily(intent.family)),
151
- };
152
- }
153
- if (this.isModificationTask() && !this.codeChanged && intent.family === "read") {
107
+ if (intent.family === "read") {
154
108
  const signature = intent.read?.signature;
155
- if (signature && this.historyCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressReadExactRepeats) {
156
- this.enterModifyPhase(`Repeated the same file range without making progress: ${signature}`);
157
- return {
158
- blockedResult: blockedResult("Read blocked: this file range was already read. You have enough context to make the requested change; use edit/write now or explain the blocker.", "blocked", "Repeated identical read before modification.", "read"),
159
- };
109
+ if (signature && this.hasCurrentMutationObservation((entry) => entry.signature === signature)) {
110
+ this.warnOnce(`read:${signature}`, "This exact file range was already read since the last successful edit/write. If the content is still available and nothing changed, use the prior result; otherwise it is okay to re-read to recover context or verify a change.");
160
111
  }
161
112
  }
162
113
  if (intent.family === "search") {
163
- if (this.searchFrozen) {
164
- return {
165
- blockedResult: blockedResult("Search blocked: repeated low-yield searching is now frozen for this task.", "blocked", "Search frozen due to repeated low-yield searching.", "search"),
166
- };
167
- }
168
114
  const signature = intent.search?.signature;
169
115
  const familyKey = intent.search?.familyKey;
170
- if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressExactRepeats) {
171
- this.freezeSearch(`Repeated the same search signature without new evidence: ${signature}`);
172
- return {
173
- blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress.", "search"),
174
- };
116
+ if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.warningExactRepeats) {
117
+ this.warnOnce(`search:${signature}`, "This search is very similar to one you already ran and it did not produce new evidence. Change the query/path, follow a concrete lead, or summarize the strongest findings.");
175
118
  }
176
119
  if (familyKey) {
177
120
  const familyNoProgress = this.trailingNoProgressCount((entry) => entry.familyKey === familyKey);
178
- if (familyNoProgress >= this.budget.maxNoProgressFamilyRepeats) {
179
- this.freezeSearch(`Repeated the same search family without new evidence: ${familyKey}`);
180
- return {
181
- blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress.", "search"),
182
- };
183
- }
184
121
  if (familyNoProgress >= this.budget.warningFamilyRepeats && !this.warnedFamilies.has(familyKey)) {
185
122
  this.warnedFamilies.add(familyKey);
186
- this.reminderQueue.push(buildLoopWarningReminder("Repeated searches are yielding little new evidence. Change your hypothesis, narrow the path, or summarize current findings instead of repeating variants."));
123
+ this.reminderQueue.push(buildLoopWarningReminder("Repeated searches in the same family are yielding little new evidence. Change your hypothesis, narrow the path, follow a specific file lead, or summarize current findings instead of repeating variants."));
187
124
  }
188
125
  }
189
126
  }
@@ -194,9 +131,6 @@ export class ExecutionGovernor {
194
131
  if (intent.family === "read") {
195
132
  this.readSteps += 1;
196
133
  }
197
- if (isExplorationIntent(intent) && !this.codeChanged) {
198
- this.explorationStepsWithoutWrite += 1;
199
- }
200
134
  this.maybeWarnOnSoftBudgets(intent.family === "search", intent.family === "read");
201
135
  return {};
202
136
  }
@@ -211,23 +145,19 @@ export class ExecutionGovernor {
211
145
  signature: intent.search?.signature ?? intent.read?.signature,
212
146
  familyKey: intent.search?.familyKey ?? intent.read?.familyKey,
213
147
  progress,
148
+ mutationVersion: this.mutationVersion,
214
149
  });
215
150
  if (isSuccessfulWriteIntent(intent, result)) {
216
- this.codeChanged = true;
217
- this.phase = "verify";
218
- return;
219
- }
220
- if (this.isModificationTask()
221
- && !this.codeChanged
222
- && isExplorationIntent(intent)
223
- && this.explorationStepsWithoutWrite >= this.budget.maxExplorationStepsWithoutWrite) {
224
- this.enterModifyPhase(`Used ${this.explorationStepsWithoutWrite} exploration tools without editing files.`);
151
+ this.mutationVersion += 1;
225
152
  }
226
153
  }
227
154
  trailingNoProgressCount(predicate) {
228
155
  let count = 0;
229
156
  for (let index = this.history.length - 1; index >= 0; index--) {
230
157
  const entry = this.history[index];
158
+ if (entry.mutationVersion !== this.mutationVersion) {
159
+ break;
160
+ }
231
161
  if (!predicate(entry)) {
232
162
  break;
233
163
  }
@@ -238,27 +168,15 @@ export class ExecutionGovernor {
238
168
  }
239
169
  return count;
240
170
  }
241
- freezeSearch(reason) {
242
- if (this.searchFrozen) {
243
- return;
244
- }
245
- this.searchFrozen = true;
246
- this.reminderQueue.push(buildSearchFreezeReminder(reason));
171
+ hasCurrentMutationObservation(predicate) {
172
+ return this.history.some((entry) => entry.mutationVersion === this.mutationVersion && predicate(entry));
247
173
  }
248
- enterModifyPhase(reason) {
249
- if (this.explorationFrozen) {
174
+ warnOnce(key, reason) {
175
+ if (this.warnedSignatures.has(key)) {
250
176
  return;
251
177
  }
252
- this.phase = "modify";
253
- this.explorationFrozen = true;
254
- this.searchFrozen = true;
255
- this.reminderQueue.push(buildExplorationFreezeReminder(reason));
256
- }
257
- isModificationTask() {
258
- return this.taskType === "implementation" || this.taskType === "debugging";
259
- }
260
- historyCount(predicate) {
261
- return this.history.reduce((count, entry) => count + (predicate(entry) ? 1 : 0), 0);
178
+ this.warnedSignatures.add(key);
179
+ this.reminderQueue.push(buildLoopWarningReminder(reason));
262
180
  }
263
181
  maybeWarnOnSoftBudgets(isSearchStep, isReadStep) {
264
182
  if (!this.softTotalWarned && this.totalSteps >= this.budget.softTotalSteps) {
@@ -275,28 +193,12 @@ export class ExecutionGovernor {
275
193
  }
276
194
  }
277
195
  }
278
- function isExplorationIntent(intent) {
279
- return intent.family === "search" || intent.family === "read" || intent.family === "web";
280
- }
281
196
  function isSuccessfulWriteIntent(intent, result) {
282
197
  if (result.isError || result.status === "blocked" || result.status === "command_error") {
283
198
  return false;
284
199
  }
285
200
  return intent.family === "write" || intent.family === "edit" || result.metadata?.kind === "write" || result.metadata?.kind === "edit";
286
201
  }
287
- function metadataKindForFamily(family) {
288
- switch (family) {
289
- case "search":
290
- case "read":
291
- case "write":
292
- case "edit":
293
- case "shell":
294
- case "web":
295
- return family;
296
- default:
297
- return "security";
298
- }
299
- }
300
202
  function inferProgress(intent, result) {
301
203
  if (result.status === "blocked" || result.status === "timeout" || result.status === "command_error") {
302
204
  return false;
@@ -314,14 +216,3 @@ function inferProgress(intent, result) {
314
216
  }
315
217
  return !result.isError;
316
218
  }
317
- function blockedResult(content, status, reason, kind = "security") {
318
- return {
319
- content,
320
- isError: true,
321
- status,
322
- metadata: {
323
- kind,
324
- reason,
325
- },
326
- };
327
- }
package/dist/main.js CHANGED
@@ -125,9 +125,8 @@ async function main() {
125
125
  const { registry: slashRegistry } = await import("./slash-commands/index.js");
126
126
  slashRegistry.addDynamicSource(() => mcpManager.getPromptCommands());
127
127
  }
128
- // Signal-based shutdown for Ctrl-C / kill. For /quit the command handler
129
- // shuts MCP down directly process.once("exit", ...)
130
- // runs synchronously and can't await async work, so relying on it is a trap.
128
+ // Signal-based shutdown for Ctrl-C / kill. Normal /quit cleanup happens after
129
+ // the TUI renderer has been destroyed, avoiding native teardown races.
131
130
  const shutdownMcp = async () => {
132
131
  try {
133
132
  await mcpManager.shutdown();
@@ -261,6 +260,18 @@ async function main() {
261
260
  // Codex-style memory runs at startup over historical rollouts. Exit should
262
261
  // not perform an ad-hoc extraction of the just-finished session.
263
262
  };
263
+ const shutdownRuntime = async () => {
264
+ const results = await Promise.allSettled([
265
+ flushMemory(),
266
+ shutdownMcp(),
267
+ lspService.shutdown(),
268
+ ]);
269
+ for (const result of results) {
270
+ if (result.status === "rejected") {
271
+ // Shutdown is best-effort; never turn exit into a fatal error.
272
+ }
273
+ }
274
+ };
264
275
  const runMemoryCompaction = async () => formatMemoryStartupResult(await runMemoryStartupPipeline({
265
276
  cwd: args.cwd,
266
277
  complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
@@ -296,49 +307,53 @@ async function main() {
296
307
  console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
297
308
  }
298
309
  }
299
- // Print mode: single prompt, then exit
300
- if (args.print || args.prompt) {
301
- const prompt = args.prompt || (await readPipedStdin()) || "";
302
- if (!prompt) {
303
- console.error(chalk.red("Error: No prompt provided."));
304
- process.exit(1);
305
- }
306
- for await (const event of agent.run(prompt, args.cwd)) {
307
- if (event.type === "text_delta") {
308
- process.stdout.write(event.content);
309
- }
310
- else if (event.type === "tool_start") {
311
- console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
310
+ try {
311
+ // Print mode: single prompt, then exit
312
+ if (args.print || args.prompt) {
313
+ const prompt = args.prompt || (await readPipedStdin()) || "";
314
+ if (!prompt) {
315
+ console.error(chalk.red("Error: No prompt provided."));
316
+ process.exit(1);
312
317
  }
313
- else if (event.type === "tool_end") {
314
- const color = event.result.isError ? chalk.red : chalk.dim;
315
- console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
318
+ for await (const event of agent.run(prompt, args.cwd)) {
319
+ if (event.type === "text_delta") {
320
+ process.stdout.write(event.content);
321
+ }
322
+ else if (event.type === "tool_start") {
323
+ console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
324
+ }
325
+ else if (event.type === "tool_end") {
326
+ const color = event.result.isError ? chalk.red : chalk.dim;
327
+ console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
328
+ }
316
329
  }
330
+ console.log();
331
+ return;
317
332
  }
318
- console.log();
319
- return;
333
+ // Interactive mode: OpenTUI uses Bun native FFI, matching opencode's TUI stack.
334
+ const { runTui } = await import("./tui/run.js");
335
+ await runTui(agent, args, {
336
+ sessionManager,
337
+ createProvider,
338
+ registry,
339
+ skillRegistry,
340
+ planHandlerRef,
341
+ approvalHandlerRef,
342
+ questionController,
343
+ bashAllowlist,
344
+ settingsManager,
345
+ lspService,
346
+ mcpManager,
347
+ theme: userConfig.getTheme(),
348
+ flushMemory,
349
+ runMemoryCompaction,
350
+ runMemorySummary,
351
+ runMemoryRefresh,
352
+ });
353
+ }
354
+ finally {
355
+ await shutdownRuntime();
320
356
  }
321
- // Interactive mode: OpenTUI uses Bun native FFI, matching opencode's TUI stack.
322
- const { runTui } = await import("./tui/run.js");
323
- await runTui(agent, args, {
324
- sessionManager,
325
- createProvider,
326
- registry,
327
- skillRegistry,
328
- planHandlerRef,
329
- approvalHandlerRef,
330
- questionController,
331
- bashAllowlist,
332
- settingsManager,
333
- lspService,
334
- mcpManager,
335
- theme: userConfig.getTheme(),
336
- flushMemory,
337
- runMemoryCompaction,
338
- runMemorySummary,
339
- runMemoryRefresh,
340
- });
341
- await flushMemory();
342
357
  }
343
358
  async function readPipedStdin() {
344
359
  if (process.stdin.isTTY)
@@ -36,18 +36,15 @@ export function createDefaultHooks() {
36
36
  },
37
37
  beforeModelCall(ctx) {
38
38
  ctx.agent.compactResidentHistory();
39
- if (ctx.state.governor) {
40
- ctx.toolEntries = ctx.state.governor.filterToolDefinitions(ctx.toolEntries);
41
- }
42
39
  if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker && ctx.state.governor) {
43
40
  const coverage = ctx.state.evidenceTracker.snapshot();
44
41
  const phase = resolveWorkflowPhase({
45
42
  coreCoverageComplete: ctx.state.evidenceTracker.isCoreCoverageComplete(),
46
- searchFrozen: ctx.state.governor.snapshot().searchFrozen,
43
+ searchFrozen: false,
47
44
  });
48
45
  ctx.state.workflowPhase = phase;
49
46
  const summary = formatCoverageSummary(coverage);
50
- const key = `${phase}:${ctx.state.evidenceTracker.key()}:${ctx.state.governor.snapshot().searchFrozen ? "1" : "0"}`;
47
+ const key = `${phase}:${ctx.state.evidenceTracker.key()}:0`;
51
48
  if (ctx.state.workflowKey !== key) {
52
49
  ctx.state.workflowKey = key;
53
50
  ctx.queueReminder(buildWorkflowPhaseReminder({
@@ -66,10 +63,7 @@ export function createDefaultHooks() {
66
63
  beforeToolCall(ctx) {
67
64
  const arbitration = arbitrateToolCall(ctx.toolCall);
68
65
  ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
69
- const decision = ctx.state.governor?.beforeToolCall(ctx.toolCall);
70
- if (decision?.blockedResult) {
71
- ctx.blockToolCall(decision.blockedResult);
72
- }
66
+ ctx.state.governor?.beforeToolCall(ctx.toolCall);
73
67
  },
74
68
  afterToolCall(ctx) {
75
69
  if (ctx.toolCall.arbiterNote) {
@@ -149,12 +143,6 @@ export function createDefaultHooks() {
149
143
  ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
150
144
  return;
151
145
  }
152
- const allSearchResultsWereLowSignal = ctx.toolCalls.length > 0
153
- && ctx.toolCalls.every((toolCall) => ["glob", "grep", "bash", "web_search", "web_fetch"].includes(toolCall.name))
154
- && ctx.toolResults.every((result) => result.status === "no_match" || result.status === "blocked");
155
- if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
156
- ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
157
- }
158
146
  // Verification reminders intentionally removed. See afterToolCall.
159
147
  },
160
148
  afterTurn() {
@@ -41,7 +41,8 @@ export declare function buildTaskSummaryReminder(): string;
41
41
  export declare function buildEditRetryEscalationReminder(reason: string): string;
42
42
  /**
43
43
  * Fired the FIRST time the model re-reads a file it already read in this turn.
44
- * Soft — does not freeze the tool. Just prevents a 3rd / 4th re-read.
44
+ * Soft — does not freeze the tool. The model may still re-read when context was
45
+ * pruned, the requested range changed, or a later mutation needs verification.
45
46
  */
46
47
  export declare function buildRedundantReadReminder(path: string): string;
47
48
  /**
@@ -200,12 +200,13 @@ Stop retrying the same call. Pick one of:
200
200
  }
201
201
  /**
202
202
  * Fired the FIRST time the model re-reads a file it already read in this turn.
203
- * Soft — does not freeze the tool. Just prevents a 3rd / 4th re-read.
203
+ * Soft — does not freeze the tool. The model may still re-read when context was
204
+ * pruned, the requested range changed, or a later mutation needs verification.
204
205
  */
205
206
  export function buildRedundantReadReminder(path) {
206
207
  return wrapInSystemReminder(`
207
- You already read ${path} earlier in this turn. Use the content already in context rather than re-reading.
208
- Only re-read this path if a subsequent tool call (edit/write/bash) modified it since.
208
+ You already read ${path} earlier in this turn. If that content is still available and nothing changed, rely on it rather than re-reading.
209
+ It is okay to re-read when you need to recover pruned context, inspect a different range, or verify a later edit/write/bash change.
209
210
  `);
210
211
  }
211
212
  /**
@@ -274,24 +274,7 @@ const builtinSlashCommandEntries = [
274
274
  name: "quit",
275
275
  description: "Exit the application",
276
276
  async handler(args, ctx) {
277
- // Shut MCP stdio children down first; their stdout/stderr listeners
278
- // otherwise hold the Node event loop open even after ink unmounts.
279
- try {
280
- await ctx.mcpManager?.shutdown();
281
- }
282
- catch {
283
- // ignore — we're quitting anyway
284
- }
285
- try {
286
- await ctx.flushMemory?.();
287
- }
288
- catch {
289
- // memory shutdown hooks are best-effort during exit
290
- }
291
277
  ctx.exit();
292
- // Belt-and-braces: if anything else (raw-mode tty handle, pending
293
- // timer, etc.) still holds the loop, force-exit shortly after.
294
- setTimeout(() => process.exit(0), 100).unref();
295
278
  },
296
279
  },
297
280
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {