@dreb/coding-agent 2.32.1 → 2.33.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.
@@ -34,6 +34,7 @@ import { expandPromptTemplate } from "./prompt-templates.js";
34
34
  import { scrubSecrets } from "./secret-scrubber.js";
35
35
  import { isSensitivePath } from "./sensitive-paths.js";
36
36
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
37
+ import { DEFAULT_BG_PARENT_TURN_LIMIT } from "./settings-manager.js";
37
38
  import { createSyntheticSourceInfo } from "./source-info.js";
38
39
  import { buildSystemPrompt } from "./system-prompt.js";
39
40
  import { resolveThinkingDisplay } from "./thinking.js";
@@ -127,10 +128,14 @@ export class AgentSession {
127
128
  _modelRegistry;
128
129
  // Session tasks (in-memory, lost on session end)
129
130
  _tasks = [];
130
- // Background agent turn limiter (Layer D): counts LLM turns while bg agents are running.
131
+ // Background agent turn limiter (Layer D): counts LLM turns that started while bg agents were running.
131
132
  // Reset when a bg agent delivers results. No limit when no bg agents are active.
133
+ // BG_TURN_LIMIT is the default cap; users can retune or disable it via
134
+ // settings (backgroundAgents.parentTurnLimit / parentTurnGuardrail).
132
135
  _bgTurnCounter = 0;
133
- static BG_TURN_LIMIT = 3;
136
+ _bgRunningAtTurnStart = false;
137
+ _bgPauseNotified = false;
138
+ static BG_TURN_LIMIT = DEFAULT_BG_PARENT_TURN_LIMIT;
134
139
  // Sentinel monitor state (Layer B): tracks whether we've already steered for this streaming response
135
140
  _sentinelSteered = false;
136
141
  // Guardrail unsubscribe functions (must be cleaned up on dispose)
@@ -426,30 +431,76 @@ export class AgentSession {
426
431
  }
427
432
  });
428
433
  // Layer D: Turn limiter — cap parent turns while bg agents are running.
429
- // Increment counter on each turn_end while bg agents are active.
434
+ // Count only turns that started with bg agents already running; the launch turn itself is excluded.
430
435
  this._unsubscribeGuardrailCounter = this.agent.subscribe((event) => {
436
+ if (event.type === "turn_start") {
437
+ this._bgRunningAtTurnStart = getRunningBackgroundAgents().length > 0;
438
+ // Re-arm the pause notification for each new run. Within a single paused
439
+ // episode the loop breaks (shouldContinue → false) before turn_start fires,
440
+ // so re-entrant shouldContinue polls stay deduped; a genuinely new run
441
+ // (e.g. the user sends a message to continue) re-arms and re-notifies if it
442
+ // re-pauses, instead of breaking silently.
443
+ this._bgPauseNotified = false;
444
+ return;
445
+ }
431
446
  if (event.type !== "turn_end")
432
447
  return;
433
448
  const bgRunning = getRunningBackgroundAgents();
434
449
  if (bgRunning.length === 0) {
435
- this._bgTurnCounter = 0;
450
+ this._resetBgGuardrailState();
436
451
  return;
437
452
  }
438
- this._bgTurnCounter++;
453
+ if (this._bgRunningAtTurnStart) {
454
+ this._bgTurnCounter++;
455
+ }
439
456
  });
440
457
  // shouldContinue callback — checked before each subsequent LLM call.
441
458
  // Does NOT inject a steer warning — the loop is already stopping, and any
442
459
  // queued warning would go stale (consumed in the next run after bg agents
443
460
  // have already delivered results, making the warning factually wrong).
461
+ //
462
+ // Instead, when the guardrail halts the parent, we emit a frontend/session
463
+ // event (`parent_paused_for_background_agents`) so the TUI and Telegram can surface a
464
+ // friendly, non-error notification. The guardrail can be disabled or its
465
+ // turn limit retuned via settings (backgroundAgents.parentTurnGuardrail /
466
+ // parentTurnLimit).
444
467
  this.agent.setShouldContinue(() => {
445
468
  const bgRunning = getRunningBackgroundAgents();
446
469
  if (bgRunning.length === 0) {
447
- this._bgTurnCounter = 0;
470
+ this._resetBgGuardrailState();
448
471
  return true;
449
472
  }
450
- return this._bgTurnCounter < AgentSession.BG_TURN_LIMIT;
473
+ const { enabled, turnLimit } = this.settingsManager?.getBackgroundAgentGuardrailSettings() ?? {
474
+ enabled: true,
475
+ turnLimit: AgentSession.BG_TURN_LIMIT,
476
+ };
477
+ // Guardrail disabled — advanced opt-out: parent keeps running while bg agents work.
478
+ if (!enabled)
479
+ return true;
480
+ if (this._bgTurnCounter >= turnLimit) {
481
+ if (!this._bgPauseNotified) {
482
+ this._emit({
483
+ type: "parent_paused_for_background_agents",
484
+ runningAgentCount: bgRunning.length,
485
+ turnsUsed: this._bgTurnCounter,
486
+ turnLimit,
487
+ });
488
+ this._bgPauseNotified = true;
489
+ }
490
+ return false;
491
+ }
492
+ return true;
451
493
  });
452
494
  }
495
+ /**
496
+ * Reset the background-agent guardrail counter and the pause-notified flag together.
497
+ * These two fields are one logical unit — they must always reset in lockstep so a new
498
+ * pause episode both restarts the turn budget and re-arms the pause notification.
499
+ */
500
+ _resetBgGuardrailState() {
501
+ this._bgTurnCounter = 0;
502
+ this._bgPauseNotified = false;
503
+ }
453
504
  /**
454
505
  * Handle background agent completion — builds the delivery message and routes
455
506
  * it to the parent agent via the appropriate channel (steer, prompt, or appendMessage).
@@ -503,7 +554,7 @@ export class AgentSession {
503
554
  }
504
555
  else {
505
556
  // Reset bg turn counter on delivery — parent gets fresh turns
506
- this._bgTurnCounter = 0;
557
+ this._resetBgGuardrailState();
507
558
  // Normal completion — deliver and trigger a response
508
559
  // If the agent is already streaming, steer (injects after current tool calls)
509
560
  // instead of followUp (waits until agent would fully stop)