enhance(#4570): allow disabling planner stall detection (#4585)

* enhance(#4570): allow disabling planner stall detection

* docs(#4570): add changelog fragment

Emitted-Drift-Ack-Growth: plan-phase.md — the explicit opt-out gate covers all five planner and checker spawn classes
Emitted-Drift-Ack-Growth: settings-advanced.md — the toggle prompt and bounded-recovery warning expose the new setting

* chore(#4570): refresh compact-content baseline

* fix(#4570): preserve default-on watchdog fallback

* docs(#4570): qualify the chunked-mode orchestrator rules with the toggle

The two chunked-planning-mode stall-watch imperatives read as
unconditional, with the opt-out stated only in a following bullet. State
the PLANNER_STALL_DETECTION_ENABLED condition inline, matching the three
sites already qualified in plan-phase.md.

* chore(#4570): refresh compact-content baselines against rebased next

* fix(#4570): sync planner stall launcher

Keep the stall-detection helper aligned with the canonical runtime launcher.

* chore(#4570): refresh compact baseline after rebase

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Michel Moreira
2026-09-23 19:46:23 -03:00
committed by GitHub
parent 17bf25a3d2
commit 1e3e1f7cd8
21 changed files with 417 additions and 53 deletions

View File

@@ -162,9 +162,22 @@ const CONFIG_DEFAULTS = {
research_before_questions: _getNestedConfigDefault('workflow', 'research_before_questions'), // #3894
smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
inline_plan_threshold: _getNestedConfigDefault('workflow', 'inline_plan_threshold'), // #3801
planner_stall_detection_enabled: _getNestedConfigDefault('planner', 'stall_detection_enabled'),
max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
};
/**
* Resolve the planner watchdog policy from hand-edited configuration.
* Only a real JSON boolean may override the default; strings/numbers/null
* fail safe to the manifest-owned default-on behavior (#4570).
*/
function resolvePlannerStallDetectionEnabled(value: unknown): boolean {
if (typeof value === 'boolean') return value;
// A missing or skewed manifest must never convert an absent override into
// permission to disable the watchdog. The documented contract is default-on.
return true;
}
/**
* Deep-merge two plain config objects. `overlay` wins on key conflict.
* Explicit `null` in overlay overrides base (null means "unset this key").
@@ -932,6 +945,9 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
phase_naming: get('phase_naming') ?? defaults.phase_naming,
project_code: get('project_code') ?? defaults.project_code,
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(
getNested('planner', 'stall_detection_enabled'),
),
model_overrides: (parsed['model_overrides']) || null,
agent_tools: (parsed['agent_tools']) || null,
models: (parsed['models']) || null,
@@ -1091,6 +1107,9 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
context_window: (globalDefaults['context_window']) ?? defaults.context_window,
subagent_timeout: (globalDefaults['subagent_timeout']) ?? defaults.subagent_timeout,
planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(
(globalDefaults['planner'] as Record<string, unknown> | undefined)?.['stall_detection_enabled'],
),
model_overrides: (globalDefaults['model_overrides']) || null,
models: (globalDefaults['models']) || null,
granularity: (globalDefaults['granularity']) !== undefined ? globalDefaults['granularity'] : null,
@@ -1191,6 +1210,7 @@ export = {
_getNestedConfigDefault,
_getConfigValue,
_getConfigNested,
resolvePlannerStallDetectionEnabled,
_deepMergeConfig,
_warnedUnknownConfigKeys,
_warnedShadowedGlobalKeys,

View File

@@ -17,7 +17,7 @@ import cliExitMod = require('./cli-exit.cjs');
const { ExitError } = cliExitMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import configLoader = require('./config-loader.cjs');
const { CONFIG_DEFAULTS } = configLoader;
const { CONFIG_DEFAULTS, resolvePlannerStallDetectionEnabled } = configLoader;
import { platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import planningWorkspace = require('./planning-workspace.cjs');
@@ -107,6 +107,7 @@ const SCHEMA_DEFAULTS: Record<string, unknown> = {
'context_window': 200000,
'executor.stall_detect_interval_minutes': 5,
'executor.stall_threshold_minutes': 10,
'planner.stall_detection_enabled': CONFIG_DEFAULTS.planner_stall_detection_enabled,
'planner.stall_detect_interval_minutes': 5,
'planner.stall_threshold_minutes': 10,
'git.create_tag': true,
@@ -185,12 +186,15 @@ function resolveSchemaDefault(cwd: string, kp: string): { found: boolean; value:
* Centralizing emission here means masking can't be missed at a call site.
*/
function emitResolvedDefault(kp: string, value: unknown, raw: boolean): void {
const resolvedValue = kp === 'planner.stall_detection_enabled'
? resolvePlannerStallDetectionEnabled(value)
: value;
if (isSecretKey(kp)) {
const masked = maskSecret(value as Parameters<typeof maskSecret>[0]);
const masked = maskSecret(resolvedValue as Parameters<typeof maskSecret>[0]);
output(masked, raw, masked);
return;
}
output(value, raw, String(value));
output(resolvedValue, raw, String(resolvedValue));
}
// ─── Validation helpers ───────────────────────────────────────────────────────
@@ -955,6 +959,14 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
}
}
// Planner watchdog opt-out (#4570) — only a real boolean may change the
// default-on policy. In particular, string "false" must not disable it.
if (kp === 'planner.stall_detection_enabled') {
if (typeof parsedValue !== 'boolean') {
error(`Invalid planner.stall_detection_enabled '${val}'. Must be a boolean (true or false).`);
}
}
// #3086 — git.create_tag: boolean only
if (kp === 'git.create_tag') {
if (typeof parsedValue !== 'boolean') {
@@ -1251,6 +1263,10 @@ function cmdConfigGet(cwd: string, keyPath: string | undefined, raw: boolean, de
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
}
if (kp === 'planner.stall_detection_enabled') {
current = resolvePlannerStallDetectionEnabled(current);
}
// Never echo plaintext for sensitive keys via config-get. Plaintext lives
// in config.json on disk; the CLI surface always shows the masked form.
if (isSecretKey(kp)) {