feat(parallel): worker model override for parallel milestone workers
Add parallel.worker_model preference so coordinators can assign a cheaper model to parallel workers (e.g. Haiku for execution) instead of inheriting the coordinator's model. The override is applied via GSD_WORKER_MODEL env var during worker bootstrap. - Add worker_model to ParallelConfig type and validation - Inject GSD_WORKER_MODEL env in spawnWorker when configured - Apply override in bootstrapAutoSession for parallel workers - Document in preferences-reference.md
This commit is contained in:
parent
0a2c9b64c6
commit
5cf86ff490
6 changed files with 50 additions and 11 deletions
|
|
@ -610,6 +610,25 @@ export async function bootstrapAutoSession(
|
|||
};
|
||||
}
|
||||
|
||||
// Apply worker model override from parallel orchestrator (#worker-model).
|
||||
// GSD_WORKER_MODEL is injected by the coordinator when parallel.worker_model
|
||||
// is configured, so parallel milestone workers use a cheaper model than the
|
||||
// coordinator session (e.g. Haiku for execution, Sonnet for planning).
|
||||
const workerModelOverride = process.env.GSD_WORKER_MODEL;
|
||||
if (workerModelOverride && process.env.GSD_PARALLEL_WORKER === "1") {
|
||||
const availableModels = ctx.modelRegistry.getAvailable();
|
||||
const { resolveModelId } = await import("./auto-model-selection.js");
|
||||
const overrideModel = resolveModelId(workerModelOverride, availableModels, ctx.model?.provider);
|
||||
if (overrideModel) {
|
||||
const ok = await pi.setModel(overrideModel, { persist: false });
|
||||
if (ok) {
|
||||
// Update start model so all subsequent units use this as the baseline
|
||||
s.autoModeStartModel = { provider: overrideModel.provider, id: overrideModel.id };
|
||||
ctx.ui.notify(`Worker model override: ${overrideModel.provider}/${overrideModel.id}`, "info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot installed skills
|
||||
if (resolveSkillDiscoveryMode() !== "off") {
|
||||
snapshotSkills();
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
- `budget_ceiling`: number — optional per-parallel-run budget ceiling.
|
||||
- `merge_strategy`: `"per-slice"` or `"per-milestone"` — when to merge worktree results back. Default: `"per-milestone"`.
|
||||
- `auto_merge`: `"auto"`, `"confirm"`, or `"manual"` — merge behavior after completion. `"auto"` merges immediately; `"confirm"` asks first; `"manual"` leaves branches for you. Default: `"confirm"`.
|
||||
- `worker_model`: string — optional model override for parallel milestone workers. When set, workers use this model (e.g. `"claude-haiku-4-5"`) instead of inheriting the coordinator's model. Useful for cost savings on execution-heavy milestones.
|
||||
|
||||
- `verification_commands`: string[] — shell commands to run as verification after task execution (e.g., `["npm test", "npm run lint"]`). Commands run in order; if any fails, the task is marked as needing fixes.
|
||||
|
||||
|
|
|
|||
|
|
@ -548,19 +548,27 @@ export function spawnWorker(
|
|||
|
||||
let child: ChildProcess;
|
||||
try {
|
||||
const workerEnv: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
GSD_MILESTONE_LOCK: milestoneId,
|
||||
// Pass the real project root so workers don't need to re-derive it.
|
||||
// Without this, process.cwd() resolves symlinks and the worktree
|
||||
// path heuristic can match the user-level ~/.gsd instead of the
|
||||
// project .gsd, causing writes to ~ and corrupting user config.
|
||||
GSD_PROJECT_ROOT: basePath,
|
||||
// Prevent workers from spawning their own parallel sessions
|
||||
GSD_PARALLEL_WORKER: "1",
|
||||
};
|
||||
|
||||
// Apply worker model override if configured, so workers use a cheaper
|
||||
// model (e.g. Haiku) rather than inheriting the coordinator's model.
|
||||
if (state.config.worker_model) {
|
||||
workerEnv.GSD_WORKER_MODEL = state.config.worker_model;
|
||||
}
|
||||
|
||||
child = spawn(process.execPath, [binPath, "headless", "--json", "auto"], {
|
||||
cwd: worker.worktreePath,
|
||||
env: {
|
||||
...process.env,
|
||||
GSD_MILESTONE_LOCK: milestoneId,
|
||||
// Pass the real project root so workers don't need to re-derive it.
|
||||
// Without this, process.cwd() resolves symlinks and the worktree
|
||||
// path heuristic can match the user-level ~/.gsd instead of the
|
||||
// project .gsd, causing writes to ~ and corrupting user config.
|
||||
GSD_PROJECT_ROOT: basePath,
|
||||
// Prevent workers from spawning their own parallel sessions
|
||||
GSD_PARALLEL_WORKER: "1",
|
||||
},
|
||||
env: workerEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -492,6 +492,14 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
}
|
||||
}
|
||||
|
||||
if (p.worker_model !== undefined) {
|
||||
if (typeof p.worker_model === "string" && p.worker_model.length > 0) {
|
||||
parallel.worker_model = p.worker_model;
|
||||
} else {
|
||||
errors.push("parallel.worker_model must be a non-empty string");
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(parallel).length > 0) {
|
||||
validated.parallel = parallel as unknown as import("./types.js").ParallelConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -537,5 +537,6 @@ export function resolveParallelConfig(prefs: GSDPreferences | undefined): import
|
|||
budget_ceiling: prefs?.parallel?.budget_ceiling,
|
||||
merge_strategy: prefs?.parallel?.merge_strategy ?? "per-milestone",
|
||||
auto_merge: prefs?.parallel?.auto_merge ?? "confirm",
|
||||
worker_model: prefs?.parallel?.worker_model,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -450,6 +450,8 @@ export interface ParallelConfig {
|
|||
budget_ceiling?: number;
|
||||
merge_strategy: MergeStrategy;
|
||||
auto_merge: AutoMergeMode;
|
||||
/** Optional model override for parallel milestone workers (e.g. "claude-haiku-4-5"). */
|
||||
worker_model?: string;
|
||||
}
|
||||
|
||||
// ─── Reactive Task Execution Types ───────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue