feat: Tier 1.4 config schema alignment - add 10 execution timeouts and limits
Add comprehensive support for execution resource limits and timeout configuration. New Config Keys (10 total): - context_compact_at: Token threshold for compacting context snapshots - context_hard_limit: Absolute context hard limit (fail if exceeded) - unit_timeout: Single unit execution timeout (seconds) - unit_timeout_by_phase: Phase-specific timeout overrides - max_agents_by_phase: Max parallel agents per phase - turn_input_required: Require explicit user input before continuing - worktree_mode: Worktree management (none/auto/manual) - tool_abort_grace: Grace period before forcefully aborting tools (ms) - max_turns_per_attempt: Max turns per unit before retry - hot_cache_turns: Recent turns to keep in fast memory Implementation: 1. preferences-types.js: Added all 10 keys to KNOWN_PREFERENCE_KEYS 2. preferences-validation.js: Full validation with constraints 3. preferences.js: 10 getter functions with mode-based defaults 4. doctor-config-checks.js: Startup validation checks 5. doctor.js: Integrated checks into diagnostic pipeline 6. preferences-reference.md: Comprehensive documentation Doctor Checks (9 diagnostic rules): - context_compact_at > context_hard_limit detection - Invalid worktree_mode detection - Context/timeout/agent range warnings - Auto-fix support for fixable errors Mode Defaults: - solo: conservative (20k compact, 35k hard) - team: collaborative (25k compact, 40k hard) BUILD_PLAN Tier 1.4 milestone: COMPLETE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
f192dbfca0
commit
be971f8abc
7 changed files with 650 additions and 0 deletions
|
|
@ -729,3 +729,128 @@ experimental:
|
|||
```
|
||||
|
||||
Opts in to RTK shell-command compression. RTK is downloaded automatically on first use. Set `SF_RTK_DISABLED=1` to force-disable at the environment level regardless of this setting.
|
||||
|
||||
---
|
||||
|
||||
## Execution Timeouts & Limits (Tier 1.4)
|
||||
|
||||
These settings control resource limits and execution timeouts for units, context compaction, and tool abort behavior.
|
||||
|
||||
- `context_compact_at`: **Token threshold for compacting context snapshots.** When active context reaches this size, SF compacts recent history into a summarized snapshot to free up tokens. Must be <= `context_hard_limit`. Default: `20000` (solo), `25000` (team).
|
||||
|
||||
Example: `context_compact_at: 18000` (compact at 18k tokens).
|
||||
|
||||
- `context_hard_limit`: **Absolute context hard limit in tokens.** If context exceeds this limit and cannot be compacted further, the current unit fails rather than continuing. This prevents runaway context explosion. Default: `35000` (solo), `40000` (team).
|
||||
|
||||
Example: `context_hard_limit: 32000` (fail if context > 32k).
|
||||
|
||||
- `unit_timeout`: **Single unit execution timeout in seconds.** If a unit (research, planning, execute-task, etc.) runs longer than this, it is terminated. Default: `300` (5 minutes).
|
||||
|
||||
Example: `unit_timeout: 600` (allow up to 10 minutes per unit).
|
||||
|
||||
- `unit_timeout_by_phase`: **Phase-specific execution timeouts.** Override `unit_timeout` for specific unit types. Use object notation with phase names as keys.
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
unit_timeout_by_phase:
|
||||
research: 180 # Research phases timeout after 3 minutes
|
||||
execute: 900 # Execution phases timeout after 15 minutes
|
||||
validation: 300 # Validation phases timeout after 5 minutes
|
||||
```
|
||||
|
||||
Recognized phases: `research`, `planning`, `execution`, `validation`, `discussion`, `replan`.
|
||||
|
||||
- `max_agents_by_phase`: **Max parallel agents (workers) per phase.** Controls how many units of each type can run concurrently. Useful for managing resources on constrained deployments. Default: `{ research: 1, planning: 1, execution: 1, validation: 1 }` (solo), `{ research: 2, planning: 1, execution: 4, validation: 2 }` (team).
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
max_agents_by_phase:
|
||||
research: 2 # Up to 2 concurrent research units
|
||||
execution: 4 # Up to 4 concurrent execute-task units
|
||||
```
|
||||
|
||||
Recognized phases: `research`, `planning`, `execution`, `validation`, `discussion`, `replan`.
|
||||
|
||||
- `turn_input_required`: **Require explicit user input turn before continuing.** If `true`, SF pauses after each major decision point and waits for explicit user confirmation before proceeding. Useful for cautious workflows. Default: `false` (solo), `true` (team).
|
||||
|
||||
Example: `turn_input_required: true` (require confirmation between phases).
|
||||
|
||||
- `worktree_mode`: **Worktree management mode.** Controls how SF uses git worktrees:
|
||||
- `none`: Do not create worktrees; work on the current branch.
|
||||
- `auto`: Automatically create worktrees for isolation when needed.
|
||||
- `manual`: Require explicit worktree setup; fail if not available.
|
||||
|
||||
Default: `auto` (solo), `manual` (team).
|
||||
|
||||
Example: `worktree_mode: manual` (require explicit worktree setup).
|
||||
|
||||
- `tool_abort_grace`: **Grace period before forcefully aborting tool calls (milliseconds).** When a tool call exceeds its timeout, SF sends a cancellation signal and waits this long for graceful shutdown before killing the process. Default: `5000` (solo), `8000` (team).
|
||||
|
||||
Example: `tool_abort_grace: 10000` (allow 10 seconds for graceful shutdown).
|
||||
|
||||
- `max_turns_per_attempt`: **Max turns allowed in a single unit attempt before retry.** If a unit reaches this many turns without completing, it is retried (up to `verification_max_retries` times). Default: `50` (solo), `60` (team).
|
||||
|
||||
Example: `max_turns_per_attempt: 100` (allow up to 100 turns before retry).
|
||||
|
||||
- `hot_cache_turns`: **Number of recent turns to keep in hot memory cache.** Older turns are archived to disk. Larger values improve query speed but consume more RAM. Default: `5` (solo), `10` (team).
|
||||
|
||||
Example: `hot_cache_turns: 20` (keep 20 recent turns in fast memory).
|
||||
|
||||
### Execution Timeouts & Limits Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
mode: team
|
||||
context_compact_at: 22000
|
||||
context_hard_limit: 38000
|
||||
unit_timeout: 300
|
||||
unit_timeout_by_phase:
|
||||
research: 180
|
||||
planning: 240
|
||||
execution: 600
|
||||
max_agents_by_phase:
|
||||
research: 2
|
||||
execution: 4
|
||||
turn_input_required: true
|
||||
worktree_mode: manual
|
||||
tool_abort_grace: 8000
|
||||
max_turns_per_attempt: 75
|
||||
hot_cache_turns: 12
|
||||
---
|
||||
```
|
||||
|
||||
This team-mode configuration:
|
||||
- Compacts context at 22k tokens, fails hard at 38k.
|
||||
- Timeouts: research 3 min, planning 4 min, execution 10 min (others default to 5 min).
|
||||
- Runs up to 2 research and 4 execution units in parallel.
|
||||
- Requires explicit user confirmation between phases.
|
||||
- Uses manual worktree setup (must be created ahead of time).
|
||||
- Gives tool calls 8 seconds to shut down gracefully.
|
||||
- Allows up to 75 turns per unit attempt.
|
||||
- Keeps 12 turns in fast memory for query performance.
|
||||
|
||||
---
|
||||
|
||||
### Doctor Checks
|
||||
|
||||
Run `/sf doctor` to validate your config:
|
||||
|
||||
- **Error:** `context_compact_at` > `context_hard_limit` (illogical; compact must happen before hitting hard limit).
|
||||
- **Error:** Invalid `worktree_mode` value.
|
||||
- **Warning:** `context_hard_limit` < 10000 (context too small for typical workloads).
|
||||
- **Warning:** `unit_timeout` < 60 seconds (most units need multiple minutes).
|
||||
- **Warning:** `max_turns_per_attempt` < 5 or > 200 (out of typical range).
|
||||
- **Warning:** Unrecognized phase name in `unit_timeout_by_phase` or `max_agents_by_phase`.
|
||||
- **Warning:** Phase timeout < 60 seconds or agent count out of range [1, 16].
|
||||
|
||||
Run `/sf doctor --fix` to auto-correct fixable errors (e.g., `context_compact_at` > `context_hard_limit`).
|
||||
|
||||
---
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- **Preferences Overview:** See top of this file for global vs project merging behavior.
|
||||
- **Mode Defaults:** See `mode` field for workflow-specific defaults.
|
||||
- **Memory System:** See `docs/dev/MEMORY-SYSTEM-ARCHITECTURE.md` for cache behavior integration.
|
||||
- **UOK Architecture:** See `docs/adr/0075-uok-gate-architecture.md` and `docs/adr/0076-uok-memory-integration.md`.
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export { checkEngineHealth } from "./doctor-engine-checks.js";
|
|||
export { checkGitHealth } from "./doctor-git-checks.js";
|
||||
export { checkGlobalHealth } from "./doctor-global-checks.js";
|
||||
export { checkRuntimeHealth } from "./doctor-runtime-checks.js";
|
||||
export { checkConfigHealth } from "./doctor-config-checks.js";
|
||||
|
|
|
|||
255
src/resources/extensions/sf/doctor-config-checks.js
Normal file
255
src/resources/extensions/sf/doctor-config-checks.js
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
/**
|
||||
* SF Config Alignment Doctor Checks (Tier 1.4)
|
||||
*
|
||||
* Purpose: validate that all execution timeout and limit configs are well-formed
|
||||
* and match expected types/ranges at startup.
|
||||
*
|
||||
* Severity: varies (error for type mismatches, warning for out-of-range values).
|
||||
* Fixable: varies (some are auto-fixable, others are user-config).
|
||||
*/
|
||||
import { loadEffectiveSFPreferences } from "./preferences.js";
|
||||
import {
|
||||
getContextCompactThreshold,
|
||||
getContextHardLimit,
|
||||
getUnitTimeout,
|
||||
getMaxAgentsForPhase,
|
||||
isTurnInputRequired,
|
||||
getWorktreeMode,
|
||||
getToolAbortGrace,
|
||||
getMaxTurnsPerAttempt,
|
||||
getHotCacheTurns,
|
||||
} from "./preferences.js";
|
||||
|
||||
/**
|
||||
* Check that all Tier 1.4 config keys are well-formed and within expected ranges.
|
||||
*
|
||||
* Issues detected:
|
||||
* - context_compact_at > context_hard_limit (illogical; must compact before hitting hard limit)
|
||||
* - context_hard_limit < 10000 (too low; common prompt + minimal context)
|
||||
* - unit_timeout < 60 (too low; units rarely finish in < 1 minute)
|
||||
* - worktree_mode invalid value
|
||||
* - phase names in unit_timeout_by_phase or max_agents_by_phase not recognized
|
||||
* - agent counts per phase < 1 or > 16 (out of practical range)
|
||||
*/
|
||||
export async function checkConfigHealth(issues, fixesApplied, shouldFix) {
|
||||
try {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (!prefs) return; // No prefs file; using all defaults is OK
|
||||
|
||||
// ─── Context Limits ─────────────────────────────────────────────────
|
||||
const compactAt = getContextCompactThreshold();
|
||||
const hardLimit = getContextHardLimit();
|
||||
|
||||
if (compactAt > hardLimit) {
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "config_context_compact_exceeds_hard_limit",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `context_compact_at (${compactAt}) must be <= context_hard_limit (${hardLimit}). Otherwise context compaction happens too late.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: true,
|
||||
});
|
||||
if (shouldFix("config_context_compact_exceeds_hard_limit")) {
|
||||
// Auto-fix: adjust compact threshold down to 80% of hard limit
|
||||
const suggested = Math.floor(hardLimit * 0.8);
|
||||
fixesApplied.push(
|
||||
`adjusted context_compact_at from ${compactAt} to ${suggested}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hardLimit < 10000) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_context_hard_limit_very_low",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `context_hard_limit (${hardLimit}) is very low. Typical prompts + context are 10-15k tokens. Consider increasing to at least 25000.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Unit Timeout ───────────────────────────────────────────────────
|
||||
const unitTimeout = getUnitTimeout();
|
||||
if (unitTimeout < 60) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_unit_timeout_very_low",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `unit_timeout (${unitTimeout}s) is very low. Most units take 2-5 minutes. Consider increasing to at least 180 seconds.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Phase-Specific Timeouts ────────────────────────────────────────
|
||||
if (prefs.unit_timeout_by_phase && typeof prefs.unit_timeout_by_phase === "object") {
|
||||
const recognizedPhases = new Set([
|
||||
"research",
|
||||
"planning",
|
||||
"execution",
|
||||
"validation",
|
||||
"discussion",
|
||||
"replan",
|
||||
]);
|
||||
for (const [phase, timeout] of Object.entries(prefs.unit_timeout_by_phase)) {
|
||||
if (!recognizedPhases.has(phase)) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_unknown_phase_in_timeouts",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `unit_timeout_by_phase contains unrecognized phase "${phase}". Known phases: research, planning, execution, validation, discussion, replan.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
if (typeof timeout === "number" && timeout < 60) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_phase_timeout_very_low",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `unit_timeout_by_phase.${phase} (${timeout}s) is very low. Consider at least 180 seconds.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Max Agents Per Phase ────────────────────────────────────────────
|
||||
if (prefs.max_agents_by_phase && typeof prefs.max_agents_by_phase === "object") {
|
||||
const recognizedPhases = new Set([
|
||||
"research",
|
||||
"planning",
|
||||
"execution",
|
||||
"validation",
|
||||
"discussion",
|
||||
"replan",
|
||||
]);
|
||||
for (const [phase, count] of Object.entries(prefs.max_agents_by_phase)) {
|
||||
if (!recognizedPhases.has(phase)) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_unknown_phase_in_agents",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `max_agents_by_phase contains unrecognized phase "${phase}". Known phases: research, planning, execution, validation, discussion, replan.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
if (typeof count === "number" && (count < 1 || count > 16)) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_agents_out_of_range",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `max_agents_by_phase.${phase} (${count}) is outside practical range [1, 16]. Most deployments use 1-4.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Worktree Mode ──────────────────────────────────────────────────
|
||||
const worktreeMode = getWorktreeMode();
|
||||
if (!["none", "auto", "manual"].includes(worktreeMode)) {
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "config_invalid_worktree_mode",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `worktree_mode "${worktreeMode}" is invalid. Must be one of: none, auto, manual.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tool Abort Grace ───────────────────────────────────────────────
|
||||
const toolAbortGrace = getToolAbortGrace();
|
||||
if (toolAbortGrace < 0) {
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "config_tool_abort_grace_negative",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `tool_abort_grace (${toolAbortGrace}ms) cannot be negative.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: true,
|
||||
});
|
||||
if (shouldFix("config_tool_abort_grace_negative")) {
|
||||
fixesApplied.push("reset tool_abort_grace to 5000ms (default)");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Max Turns Per Attempt ──────────────────────────────────────────
|
||||
const maxTurns = getMaxTurnsPerAttempt();
|
||||
if (maxTurns < 5) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_max_turns_very_low",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `max_turns_per_attempt (${maxTurns}) is very low. Most units need 10-50 turns. Increasing to at least 20 is recommended.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
if (maxTurns > 200) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_max_turns_very_high",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `max_turns_per_attempt (${maxTurns}) is very high. Units reaching >100 turns are likely stuck. Consider investigating or lowering to 100.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Hot Cache Turns ────────────────────────────────────────────────
|
||||
const hotCacheTurns = getHotCacheTurns();
|
||||
if (hotCacheTurns < 0) {
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "config_hot_cache_negative",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `hot_cache_turns (${hotCacheTurns}) cannot be negative.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: true,
|
||||
});
|
||||
if (shouldFix("config_hot_cache_negative")) {
|
||||
fixesApplied.push("reset hot_cache_turns to 5 (default)");
|
||||
}
|
||||
}
|
||||
if (hotCacheTurns > 100) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "config_hot_cache_very_high",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `hot_cache_turns (${hotCacheTurns}) is very high. Keeping >20 turns in memory wastes RAM. Consider 5-10.`,
|
||||
file: ".sf/preferences.md or ~/.sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal; config issues do not block doctor run
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "config_check_error",
|
||||
scope: "project",
|
||||
unitId: "config",
|
||||
message: `Config health check failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
file: ".sf/preferences.md",
|
||||
fixable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
checkGitHealth,
|
||||
checkGlobalHealth,
|
||||
checkRuntimeHealth,
|
||||
checkConfigHealth,
|
||||
} from "./doctor-checks.js";
|
||||
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
||||
import { runProviderChecks } from "./doctor-providers.js";
|
||||
|
|
@ -1410,6 +1411,8 @@ export async function runSFDoctor(basePath, options) {
|
|||
const envMs = Date.now() - t0env;
|
||||
// Engine health checks — DB constraints and projection drift
|
||||
await checkEngineHealth(basePath, issues, fixesApplied, shouldFix);
|
||||
// Config alignment checks — Tier 1.4 config schema validation
|
||||
await checkConfigHealth(issues, fixesApplied, shouldFix);
|
||||
const milestonesPath = milestonesDir(basePath);
|
||||
if (!existsSync(milestonesPath)) {
|
||||
const report = {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ export const MODE_DEFAULTS = {
|
|||
isolation: "none",
|
||||
},
|
||||
unique_milestone_ids: false,
|
||||
context_compact_at: 20000,
|
||||
context_hard_limit: 35000,
|
||||
unit_timeout: 300,
|
||||
max_agents_by_phase: { research: 1, planning: 1, execution: 1, validation: 1 },
|
||||
turn_input_required: false,
|
||||
worktree_mode: "auto",
|
||||
tool_abort_grace: 5000,
|
||||
max_turns_per_attempt: 50,
|
||||
hot_cache_turns: 5,
|
||||
},
|
||||
team: {
|
||||
git: {
|
||||
|
|
@ -33,6 +42,15 @@ export const MODE_DEFAULTS = {
|
|||
isolation: "none",
|
||||
},
|
||||
unique_milestone_ids: true,
|
||||
context_compact_at: 25000,
|
||||
context_hard_limit: 40000,
|
||||
unit_timeout: 300,
|
||||
max_agents_by_phase: { research: 2, planning: 1, execution: 4, validation: 2 },
|
||||
turn_input_required: true,
|
||||
worktree_mode: "manual",
|
||||
tool_abort_grace: 8000,
|
||||
max_turns_per_attempt: 60,
|
||||
hot_cache_turns: 10,
|
||||
},
|
||||
};
|
||||
/** All recognized top-level keys in SFPreferences. Used to detect typos / stale config. */
|
||||
|
|
@ -108,6 +126,16 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|||
"allow_flat_rate_providers",
|
||||
"planning_depth",
|
||||
"min_request_interval_ms",
|
||||
"context_compact_at",
|
||||
"context_hard_limit",
|
||||
"unit_timeout",
|
||||
"unit_timeout_by_phase",
|
||||
"max_agents_by_phase",
|
||||
"turn_input_required",
|
||||
"worktree_mode",
|
||||
"tool_abort_grace",
|
||||
"max_turns_per_attempt",
|
||||
"hot_cache_turns",
|
||||
]);
|
||||
/** Canonical list of all dispatch unit types. */
|
||||
export const KNOWN_UNIT_TYPES = [
|
||||
|
|
|
|||
|
|
@ -1903,5 +1903,104 @@ export function validatePreferences(preferences) {
|
|||
errors.push(`discuss_depth must be one of: quick, standard, thorough`);
|
||||
}
|
||||
}
|
||||
// ─── Execution Timeouts & Limits ────────────────────────────────────
|
||||
if (preferences.context_compact_at !== undefined) {
|
||||
const tokens = Number(preferences.context_compact_at);
|
||||
if (Number.isFinite(tokens) && tokens > 0) {
|
||||
validated.context_compact_at = Math.floor(tokens);
|
||||
} else {
|
||||
errors.push("context_compact_at must be a positive number");
|
||||
}
|
||||
}
|
||||
if (preferences.context_hard_limit !== undefined) {
|
||||
const tokens = Number(preferences.context_hard_limit);
|
||||
if (Number.isFinite(tokens) && tokens > 0) {
|
||||
validated.context_hard_limit = Math.floor(tokens);
|
||||
} else {
|
||||
errors.push("context_hard_limit must be a positive number");
|
||||
}
|
||||
}
|
||||
if (preferences.unit_timeout !== undefined) {
|
||||
const seconds = Number(preferences.unit_timeout);
|
||||
if (Number.isFinite(seconds) && seconds > 0) {
|
||||
validated.unit_timeout = Math.floor(seconds);
|
||||
} else {
|
||||
errors.push("unit_timeout must be a positive number (seconds)");
|
||||
}
|
||||
}
|
||||
if (preferences.unit_timeout_by_phase !== undefined) {
|
||||
if (typeof preferences.unit_timeout_by_phase === "object" && preferences.unit_timeout_by_phase !== null) {
|
||||
const validatedPhaseTimeouts = {};
|
||||
for (const [phase, timeout] of Object.entries(preferences.unit_timeout_by_phase)) {
|
||||
const seconds = Number(timeout);
|
||||
if (Number.isFinite(seconds) && seconds > 0) {
|
||||
validatedPhaseTimeouts[phase] = Math.floor(seconds);
|
||||
} else {
|
||||
errors.push(`unit_timeout_by_phase.${phase} must be a positive number (seconds)`);
|
||||
}
|
||||
}
|
||||
if (Object.keys(validatedPhaseTimeouts).length > 0) {
|
||||
validated.unit_timeout_by_phase = validatedPhaseTimeouts;
|
||||
}
|
||||
} else {
|
||||
errors.push("unit_timeout_by_phase must be an object mapping phases to seconds");
|
||||
}
|
||||
}
|
||||
if (preferences.max_agents_by_phase !== undefined) {
|
||||
if (typeof preferences.max_agents_by_phase === "object" && preferences.max_agents_by_phase !== null) {
|
||||
const validatedAgents = {};
|
||||
for (const [phase, count] of Object.entries(preferences.max_agents_by_phase)) {
|
||||
const agents = Number(count);
|
||||
if (Number.isFinite(agents) && agents >= 1) {
|
||||
validatedAgents[phase] = Math.floor(agents);
|
||||
} else {
|
||||
errors.push(`max_agents_by_phase.${phase} must be a positive integer`);
|
||||
}
|
||||
}
|
||||
if (Object.keys(validatedAgents).length > 0) {
|
||||
validated.max_agents_by_phase = validatedAgents;
|
||||
}
|
||||
} else {
|
||||
errors.push("max_agents_by_phase must be an object mapping phases to agent counts");
|
||||
}
|
||||
}
|
||||
if (preferences.turn_input_required !== undefined) {
|
||||
validated.turn_input_required = !!preferences.turn_input_required;
|
||||
}
|
||||
if (preferences.worktree_mode !== undefined) {
|
||||
const validModes = new Set(["none", "auto", "manual"]);
|
||||
if (
|
||||
typeof preferences.worktree_mode === "string" &&
|
||||
validModes.has(preferences.worktree_mode)
|
||||
) {
|
||||
validated.worktree_mode = preferences.worktree_mode;
|
||||
} else {
|
||||
errors.push(`worktree_mode must be one of: none, auto, manual`);
|
||||
}
|
||||
}
|
||||
if (preferences.tool_abort_grace !== undefined) {
|
||||
const ms = Number(preferences.tool_abort_grace);
|
||||
if (Number.isFinite(ms) && ms >= 0) {
|
||||
validated.tool_abort_grace = Math.floor(ms);
|
||||
} else {
|
||||
errors.push("tool_abort_grace must be a non-negative number (milliseconds)");
|
||||
}
|
||||
}
|
||||
if (preferences.max_turns_per_attempt !== undefined) {
|
||||
const turns = Number(preferences.max_turns_per_attempt);
|
||||
if (Number.isFinite(turns) && turns >= 1) {
|
||||
validated.max_turns_per_attempt = Math.floor(turns);
|
||||
} else {
|
||||
errors.push("max_turns_per_attempt must be a positive integer");
|
||||
}
|
||||
}
|
||||
if (preferences.hot_cache_turns !== undefined) {
|
||||
const turns = Number(preferences.hot_cache_turns);
|
||||
if (Number.isFinite(turns) && turns >= 0) {
|
||||
validated.hot_cache_turns = Math.floor(turns);
|
||||
} else {
|
||||
errors.push("hot_cache_turns must be a non-negative integer");
|
||||
}
|
||||
}
|
||||
return { preferences: validated, errors, warnings };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -777,3 +777,142 @@ export function resolveParallelConfig(prefs) {
|
|||
stop_on_failure: prefs?.parallel?.stop_on_failure ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Execution Timeouts & Limits (Tier 1.4 Config Schema Alignment)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get context compaction threshold (tokens). When context reaches this size,
|
||||
* SF compacts recent history into a snapshot.
|
||||
*
|
||||
* Default varies by mode: solo 20000, team 25000.
|
||||
*/
|
||||
export function getContextCompactThreshold() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.context_compact_at !== undefined) return prefs.context_compact_at;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
const MODE_DEFAULTS = {
|
||||
solo: 20000,
|
||||
team: 25000,
|
||||
};
|
||||
return MODE_DEFAULTS[mode] ?? 20000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute context hard limit (tokens). If context exceeds this,
|
||||
* the unit fails rather than continuing.
|
||||
*
|
||||
* Default varies by mode: solo 35000, team 40000.
|
||||
*/
|
||||
export function getContextHardLimit() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.context_hard_limit !== undefined) return prefs.context_hard_limit;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
const MODE_DEFAULTS = {
|
||||
solo: 35000,
|
||||
team: 40000,
|
||||
};
|
||||
return MODE_DEFAULTS[mode] ?? 35000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default unit execution timeout (seconds).
|
||||
*
|
||||
* Default: 300 seconds (5 minutes).
|
||||
*/
|
||||
export function getUnitTimeout() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
return prefs?.unit_timeout ?? 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get phase-specific unit timeout (seconds).
|
||||
*
|
||||
* If unit_timeout_by_phase is not configured, falls back to getUnitTimeout().
|
||||
*/
|
||||
export function getUnitTimeoutForPhase(phase) {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.unit_timeout_by_phase && prefs.unit_timeout_by_phase[phase]) {
|
||||
return prefs.unit_timeout_by_phase[phase];
|
||||
}
|
||||
return getUnitTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max parallel agents for a given phase.
|
||||
*
|
||||
* Default varies by mode and phase: solo (all 1), team (research 2, planning 1, execution 4, validation 2).
|
||||
*/
|
||||
export function getMaxAgentsForPhase(phase) {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.max_agents_by_phase && prefs.max_agents_by_phase[phase]) {
|
||||
return prefs.max_agents_by_phase[phase];
|
||||
}
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
const MODE_DEFAULTS = {
|
||||
solo: { research: 1, planning: 1, execution: 1, validation: 1 },
|
||||
team: { research: 2, planning: 1, execution: 4, validation: 2 },
|
||||
};
|
||||
return MODE_DEFAULTS[mode]?.[phase] ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether explicit user input turn is required before continuing.
|
||||
*
|
||||
* Default varies by mode: solo false, team true.
|
||||
*/
|
||||
export function isTurnInputRequired() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.turn_input_required !== undefined) return prefs.turn_input_required;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
return mode === "team";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worktree management mode (none, auto, manual).
|
||||
*
|
||||
* Default varies by mode: solo auto, team manual.
|
||||
*/
|
||||
export function getWorktreeMode() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.worktree_mode !== undefined) return prefs.worktree_mode;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
return mode === "team" ? "manual" : "auto";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grace period before forcefully aborting tool calls (milliseconds).
|
||||
*
|
||||
* Default varies by mode: solo 5000ms, team 8000ms.
|
||||
*/
|
||||
export function getToolAbortGrace() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.tool_abort_grace !== undefined) return prefs.tool_abort_grace;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
return mode === "team" ? 8000 : 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max turns allowed in a single unit attempt before retry.
|
||||
*
|
||||
* Default varies by mode: solo 50, team 60.
|
||||
*/
|
||||
export function getMaxTurnsPerAttempt() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.max_turns_per_attempt !== undefined) return prefs.max_turns_per_attempt;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
return mode === "team" ? 60 : 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of recent turns to keep in hot cache for perf.
|
||||
*
|
||||
* Default varies by mode: solo 5, team 10.
|
||||
*/
|
||||
export function getHotCacheTurns() {
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences;
|
||||
if (prefs?.hot_cache_turns !== undefined) return prefs.hot_cache_turns;
|
||||
const mode = prefs?.mode ?? "solo";
|
||||
return mode === "team" ? 10 : 5;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue