taskplane 0.10.2 → 0.11.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.
- package/README.md +5 -5
- package/extensions/task-runner.ts +58 -18
- package/extensions/taskplane/config-loader.ts +4 -0
- package/extensions/taskplane/config-schema.ts +24 -0
- package/extensions/taskplane/diagnostics.ts +75 -13
- package/extensions/taskplane/engine.ts +300 -0
- package/extensions/taskplane/execution.ts +6 -1
- package/extensions/taskplane/types.ts +17 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ For a single task with full worktree isolation, dashboard, and reviews:
|
|
|
129
129
|
|
|
130
130
|
This uses the same orchestrator infrastructure as a full batch — isolated worktree, orch branch, supervisor, dashboard, inline reviews — but for just one task.
|
|
131
131
|
|
|
132
|
-
> **
|
|
132
|
+
> **Deprecated:** The `/task` command is deprecated and will be removed in a future major version. It does not provide worktree isolation, dashboard, or inline reviews. Use `/orch` for all workflows — including single-task execution.
|
|
133
133
|
|
|
134
134
|
## Commands
|
|
135
135
|
|
|
@@ -137,10 +137,10 @@ This uses the same orchestrator infrastructure as a full batch — isolated work
|
|
|
137
137
|
|
|
138
138
|
| Command | Description |
|
|
139
139
|
|---------|-------------|
|
|
140
|
-
| `/task <path/to/PROMPT.md>` | Execute one task in the current branch/worktree |
|
|
141
|
-
| `/task-status` | Show current task progress |
|
|
142
|
-
| `/task-pause` | Pause after current worker iteration finishes |
|
|
143
|
-
| `/task-resume` | Resume a paused task |
|
|
140
|
+
| `/task <path/to/PROMPT.md>` | ⚠️ **Deprecated.** Execute one task in the current branch/worktree. Use `/orch` instead. |
|
|
141
|
+
| `/task-status` | ⚠️ **Deprecated.** Show current task progress. Use `/orch-status` or dashboard. |
|
|
142
|
+
| `/task-pause` | ⚠️ **Deprecated.** Pause after current worker iteration finishes. Use `/orch-pause`. |
|
|
143
|
+
| `/task-resume` | ⚠️ **Deprecated.** Resume a paused task. Use `/orch-resume`. |
|
|
144
144
|
| `/orch [<areas\|paths\|all>]` | No args: detect state & guide (onboarding, batch planning, etc.); with args: execute tasks via isolated worktrees |
|
|
145
145
|
| `/orch-plan <areas\|paths\|all>` | Preview execution plan without running |
|
|
146
146
|
| `/orch-status` | Show batch progress |
|
|
@@ -2098,9 +2098,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2098
2098
|
|
|
2099
2099
|
// Load reviewer agent definition
|
|
2100
2100
|
const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2101
|
+
// TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
|
|
2102
|
+
const reviewerModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
|
|
2103
|
+
const reviewerModel = reviewerModelFallback
|
|
2104
|
+
? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
|
|
2105
|
+
: (config.reviewer.model
|
|
2106
|
+
|| reviewerDef?.model
|
|
2107
|
+
|| (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
|
|
2104
2108
|
const reviewerPrompt = reviewerDef?.systemPrompt
|
|
2105
2109
|
|| "You are a code reviewer. Read the request and write your review to the specified output file.";
|
|
2106
2110
|
const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
|
|
@@ -2585,9 +2589,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2585
2589
|
const basePrompt = workerDef?.systemPrompt || "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2586
2590
|
const systemPrompt = basePrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
|
|
2587
2591
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2592
|
+
// TP-055: When TASKPLANE_MODEL_FALLBACK=1 is set, skip configured model
|
|
2593
|
+
// and fall back to the session model. This is set by the orchestrator's
|
|
2594
|
+
// model fallback retry when the configured model becomes unavailable.
|
|
2595
|
+
const modelFallbackActive = process.env.TASKPLANE_MODEL_FALLBACK === "1";
|
|
2596
|
+
const model = modelFallbackActive
|
|
2597
|
+
? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
|
|
2598
|
+
: (config.worker.model
|
|
2599
|
+
|| workerDef?.model
|
|
2600
|
+
|| (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
|
|
2591
2601
|
|
|
2592
2602
|
const contextDocsList = task.contextDocs.length > 0
|
|
2593
2603
|
? "\n\nContext docs to read if needed:\n" + task.contextDocs.map(d => `- ${d}`).join("\n")
|
|
@@ -2904,7 +2914,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2904
2914
|
writeFileSync(requestPath, request);
|
|
2905
2915
|
|
|
2906
2916
|
const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
|
|
2907
|
-
|
|
2917
|
+
// TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
|
|
2918
|
+
const reviewerModelFallback2 = process.env.TASKPLANE_MODEL_FALLBACK === "1";
|
|
2919
|
+
const reviewerModel = reviewerModelFallback2
|
|
2920
|
+
? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
|
|
2921
|
+
: (config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
|
|
2908
2922
|
const reviewerPrompt = reviewerDef?.systemPrompt || "You are a code reviewer. Read the request and write your review to the specified output file.";
|
|
2909
2923
|
const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
|
|
2910
2924
|
|
|
@@ -3040,10 +3054,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3040
3054
|
// Determine review model with fallback chain:
|
|
3041
3055
|
// quality_gate.review_model → reviewer.model → agent def → default
|
|
3042
3056
|
const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3057
|
+
// TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
|
|
3058
|
+
const qgModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
|
|
3059
|
+
const reviewModel = qgModelFallback
|
|
3060
|
+
? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
|
|
3061
|
+
: (config.quality_gate.review_model
|
|
3062
|
+
|| config.reviewer.model
|
|
3063
|
+
|| reviewerDef?.model
|
|
3064
|
+
|| (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
|
|
3047
3065
|
|
|
3048
3066
|
const reviewerPrompt = reviewerDef?.systemPrompt
|
|
3049
3067
|
|| "You are a quality gate reviewer. Read the review request and write your JSON verdict to the specified file.";
|
|
@@ -3210,9 +3228,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3210
3228
|
|
|
3211
3229
|
// Use worker model and tools for fix agent (it needs to edit code)
|
|
3212
3230
|
const workerDef = loadAgentDef(ctx.cwd, "task-worker");
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3231
|
+
// TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
|
|
3232
|
+
const fixModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
|
|
3233
|
+
const fixModel = fixModelFallback
|
|
3234
|
+
? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
|
|
3235
|
+
: (config.worker.model
|
|
3236
|
+
|| workerDef?.model
|
|
3237
|
+
|| "anthropic/claude-sonnet-4-20250514");
|
|
3216
3238
|
|
|
3217
3239
|
const basePrompt = workerDef?.systemPrompt
|
|
3218
3240
|
|| "You are a fix agent addressing quality gate findings. Read the feedback and make targeted code fixes.";
|
|
@@ -3405,9 +3427,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
3405
3427
|
}
|
|
3406
3428
|
|
|
3407
3429
|
pi.registerCommand("task", {
|
|
3408
|
-
description: "Start executing a task: /task <path/to/PROMPT.md>",
|
|
3430
|
+
description: "⚠️ [Deprecated] Start executing a task: /task <path/to/PROMPT.md>",
|
|
3409
3431
|
handler: async (args, ctx) => {
|
|
3410
3432
|
widgetCtx = ctx;
|
|
3433
|
+
ctx.ui.notify(
|
|
3434
|
+
"⚠️ /task is deprecated. Use /orch instead — it provides worktree isolation, " +
|
|
3435
|
+
"dashboard, inline reviews, and supervisor monitoring. " +
|
|
3436
|
+
"/task will be removed in a future major version.",
|
|
3437
|
+
"warning",
|
|
3438
|
+
);
|
|
3411
3439
|
const promptPath = args?.trim();
|
|
3412
3440
|
if (!promptPath) {
|
|
3413
3441
|
ctx.ui.notify("Usage: /task <path/to/PROMPT.md>", "error");
|
|
@@ -3425,9 +3453,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3425
3453
|
});
|
|
3426
3454
|
|
|
3427
3455
|
pi.registerCommand("task-status", {
|
|
3428
|
-
description: "Show current task progress",
|
|
3456
|
+
description: "⚠️ [Deprecated] Show current task progress",
|
|
3429
3457
|
handler: async (_args, ctx) => {
|
|
3430
3458
|
widgetCtx = ctx;
|
|
3459
|
+
ctx.ui.notify(
|
|
3460
|
+
"⚠️ /task-status is deprecated. Use the dashboard (`taskplane dashboard`) or `/orch-status` instead.",
|
|
3461
|
+
"warning",
|
|
3462
|
+
);
|
|
3431
3463
|
if (!state.task) {
|
|
3432
3464
|
ctx.ui.notify("No task loaded. Use /task <path/to/PROMPT.md>", "info");
|
|
3433
3465
|
return;
|
|
@@ -3459,9 +3491,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3459
3491
|
});
|
|
3460
3492
|
|
|
3461
3493
|
pi.registerCommand("task-pause", {
|
|
3462
|
-
description: "Pause task after current worker finishes",
|
|
3494
|
+
description: "⚠️ [Deprecated] Pause task after current worker finishes",
|
|
3463
3495
|
handler: async (_args, ctx) => {
|
|
3464
3496
|
widgetCtx = ctx;
|
|
3497
|
+
ctx.ui.notify(
|
|
3498
|
+
"⚠️ /task-pause is deprecated. Use `/orch-pause` instead.",
|
|
3499
|
+
"warning",
|
|
3500
|
+
);
|
|
3465
3501
|
if (state.phase !== "running") {
|
|
3466
3502
|
ctx.ui.notify("No task is running", "warning");
|
|
3467
3503
|
return;
|
|
@@ -3473,9 +3509,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3473
3509
|
});
|
|
3474
3510
|
|
|
3475
3511
|
pi.registerCommand("task-resume", {
|
|
3476
|
-
description: "Resume a paused task",
|
|
3512
|
+
description: "⚠️ [Deprecated] Resume a paused task",
|
|
3477
3513
|
handler: async (_args, ctx) => {
|
|
3478
3514
|
widgetCtx = ctx;
|
|
3515
|
+
ctx.ui.notify(
|
|
3516
|
+
"⚠️ /task-resume is deprecated. Use `/orch-resume` instead.",
|
|
3517
|
+
"warning",
|
|
3518
|
+
);
|
|
3479
3519
|
if (state.phase !== "paused") {
|
|
3480
3520
|
ctx.ui.notify("Task is not paused", "warning");
|
|
3481
3521
|
return;
|
|
@@ -222,6 +222,9 @@ function mapTaskRunnerYaml(raw: any): Partial<TaskRunnerSection> {
|
|
|
222
222
|
// Quality gate (structural — all keys are schema-defined)
|
|
223
223
|
if (raw.quality_gate) result.qualityGate = convertStructuralKeys(raw.quality_gate);
|
|
224
224
|
|
|
225
|
+
// Model fallback (scalar — "inherit" or "fail")
|
|
226
|
+
if (raw.model_fallback) result.modelFallback = raw.model_fallback;
|
|
227
|
+
|
|
225
228
|
return result;
|
|
226
229
|
}
|
|
227
230
|
|
|
@@ -814,6 +817,7 @@ export function toTaskRunnerConfig(config: TaskplaneConfig): import("./types.ts"
|
|
|
814
817
|
task_areas: taskAreas,
|
|
815
818
|
reference_docs: { ...config.taskRunner.referenceDocs },
|
|
816
819
|
...(hasTestingCommands ? { testing_commands: { ...testingCommands } } : {}),
|
|
820
|
+
model_fallback: config.taskRunner.modelFallback ?? "inherit",
|
|
817
821
|
};
|
|
818
822
|
}
|
|
819
823
|
|
|
@@ -170,6 +170,19 @@ export interface SelfDocTarget {
|
|
|
170
170
|
*/
|
|
171
171
|
export type PassThreshold = "no_critical" | "no_important" | "all_clear";
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Model fallback behavior when a configured agent model becomes unavailable mid-batch.
|
|
175
|
+
*
|
|
176
|
+
* - `"inherit"`: Fall back to the session model and retry (default). The task is
|
|
177
|
+
* retried without an explicit --model flag, so pi uses whatever model the
|
|
178
|
+
* session is configured with.
|
|
179
|
+
* - `"fail"`: Fail immediately — the normal failure/retry path handles the error
|
|
180
|
+
* without any model substitution.
|
|
181
|
+
*
|
|
182
|
+
* @since TP-055
|
|
183
|
+
*/
|
|
184
|
+
export type ModelFallbackMode = "inherit" | "fail";
|
|
185
|
+
|
|
173
186
|
/** Quality gate configuration — opt-in post-completion review */
|
|
174
187
|
export interface QualityGateConfig {
|
|
175
188
|
/** Enable quality gate review before .DONE creation (default: false) */
|
|
@@ -222,6 +235,16 @@ export interface TaskRunnerSection {
|
|
|
222
235
|
protectedDocs: string[];
|
|
223
236
|
/** Quality gate configuration — opt-in post-completion review */
|
|
224
237
|
qualityGate: QualityGateConfig;
|
|
238
|
+
/**
|
|
239
|
+
* Model fallback behavior when a configured model becomes unavailable mid-batch.
|
|
240
|
+
*
|
|
241
|
+
* - `"inherit"` (default): Retry the task without an explicit model flag,
|
|
242
|
+
* falling back to the session model.
|
|
243
|
+
* - `"fail"`: Fail immediately without model substitution.
|
|
244
|
+
*
|
|
245
|
+
* @since TP-055
|
|
246
|
+
*/
|
|
247
|
+
modelFallback: ModelFallbackMode;
|
|
225
248
|
}
|
|
226
249
|
|
|
227
250
|
|
|
@@ -515,6 +538,7 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
|
|
|
515
538
|
maxFixCycles: 1,
|
|
516
539
|
passThreshold: "no_critical",
|
|
517
540
|
},
|
|
541
|
+
modelFallback: "inherit",
|
|
518
542
|
};
|
|
519
543
|
|
|
520
544
|
/** Default orchestrator section values */
|
|
@@ -45,6 +45,7 @@ export interface SessionTokenCounts {
|
|
|
45
45
|
* |----------------------|------------------------------------------------------|
|
|
46
46
|
* | `completed` | `.DONE` file found — task finished successfully |
|
|
47
47
|
* | `api_error` | API returned error (auth, rate limit, overload) |
|
|
48
|
+
* | `model_access_error` | Model unavailable (401/403/429, model not found) |
|
|
48
49
|
* | `context_overflow` | Hit context window limit (compactions + high ctx %) |
|
|
49
50
|
* | `wall_clock_timeout` | Killed by task-runner's max_worker_minutes timer |
|
|
50
51
|
* | `process_crash` | Non-zero exit code with no API error indicators |
|
|
@@ -56,6 +57,7 @@ export interface SessionTokenCounts {
|
|
|
56
57
|
export type ExitClassification =
|
|
57
58
|
| "completed"
|
|
58
59
|
| "api_error"
|
|
60
|
+
| "model_access_error"
|
|
59
61
|
| "context_overflow"
|
|
60
62
|
| "wall_clock_timeout"
|
|
61
63
|
| "process_crash"
|
|
@@ -70,6 +72,7 @@ export type ExitClassification =
|
|
|
70
72
|
export const EXIT_CLASSIFICATIONS: readonly ExitClassification[] = [
|
|
71
73
|
"completed",
|
|
72
74
|
"api_error",
|
|
75
|
+
"model_access_error",
|
|
73
76
|
"context_overflow",
|
|
74
77
|
"wall_clock_timeout",
|
|
75
78
|
"process_crash",
|
|
@@ -223,6 +226,51 @@ export interface TaskExitDiagnostic {
|
|
|
223
226
|
*/
|
|
224
227
|
export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
|
|
225
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Patterns that indicate a model access error (as opposed to a generic API error).
|
|
231
|
+
*
|
|
232
|
+
* These patterns match error messages from API providers when:
|
|
233
|
+
* - The model is not found or deprecated
|
|
234
|
+
* - Authentication/authorization fails (HTTP 401/403)
|
|
235
|
+
* - Rate limits are hit specifically for the model (HTTP 429)
|
|
236
|
+
* - API key is expired or invalid
|
|
237
|
+
*
|
|
238
|
+
* The patterns are case-insensitive and tested against the error string.
|
|
239
|
+
*
|
|
240
|
+
* @since TP-055
|
|
241
|
+
*/
|
|
242
|
+
export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
|
|
243
|
+
/\b(?:401|403)\b/, // HTTP auth/forbidden status codes
|
|
244
|
+
/\b429\b/, // HTTP rate limit
|
|
245
|
+
/model[_ ]not[_ ]found/i, // Model not found
|
|
246
|
+
/model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
|
|
247
|
+
/model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
|
|
248
|
+
/api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
|
|
249
|
+
/invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
|
|
250
|
+
/authentication[_ ](?:failed|error|required)/i, // Auth failures
|
|
251
|
+
/authorization[_ ](?:failed|error|denied)/i, // Authz failures
|
|
252
|
+
/access[_ ]denied/i, // Generic access denied
|
|
253
|
+
/permission[_ ]denied/i, // Permission denied
|
|
254
|
+
/quota[_ ]exceeded/i, // Quota exceeded
|
|
255
|
+
/rate[_ ]limit/i, // Rate limit (phrase)
|
|
256
|
+
/insufficient[_ ]quota/i, // Insufficient quota
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Test whether an error message indicates a model access error.
|
|
261
|
+
*
|
|
262
|
+
* Used by `classifyExit()` to distinguish model-specific failures from
|
|
263
|
+
* generic API errors, enabling targeted fallback to the session model.
|
|
264
|
+
*
|
|
265
|
+
* @param errorMessage - Error message to test
|
|
266
|
+
* @returns true if the error matches a model access pattern
|
|
267
|
+
* @since TP-055
|
|
268
|
+
*/
|
|
269
|
+
export function isModelAccessError(errorMessage: string): boolean {
|
|
270
|
+
if (!errorMessage) return false;
|
|
271
|
+
return MODEL_ACCESS_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
|
|
272
|
+
}
|
|
273
|
+
|
|
226
274
|
/**
|
|
227
275
|
* Classify a task session's exit into a deterministic category.
|
|
228
276
|
*
|
|
@@ -232,22 +280,26 @@ export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
|
|
|
232
280
|
*
|
|
233
281
|
* **Classification precedence (highest → lowest):**
|
|
234
282
|
*
|
|
235
|
-
* | Priority | Condition | Result
|
|
236
|
-
*
|
|
237
|
-
* | 1 | `.DONE` file found | `completed`
|
|
238
|
-
* |
|
|
239
|
-
* |
|
|
240
|
-
* |
|
|
241
|
-
* |
|
|
242
|
-
* |
|
|
243
|
-
* |
|
|
244
|
-
* |
|
|
245
|
-
* |
|
|
246
|
-
* |
|
|
283
|
+
* | Priority | Condition | Result |
|
|
284
|
+
* |----------|------------------------------------------------------|----------------------|
|
|
285
|
+
* | 1 | `.DONE` file found | `completed` |
|
|
286
|
+
* | 2a | Retries with model-access error pattern | `model_access_error` |
|
|
287
|
+
* | 2b | Retries present with final retry failed | `api_error` |
|
|
288
|
+
* | 2c | Error message has model-access pattern (no retries) | `model_access_error` |
|
|
289
|
+
* | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
|
|
290
|
+
* | 3b | Task-runner explicitly context-killed | `context_overflow` |
|
|
291
|
+
* | 4 | Timer killed the session | `wall_clock_timeout` |
|
|
292
|
+
* | 5 | Non-zero exit code, no API error | `process_crash` |
|
|
293
|
+
* | 6 | No exit summary file (session vanished) | `session_vanished` |
|
|
294
|
+
* | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
|
|
295
|
+
* | 8 | User manually killed the session | `user_killed` |
|
|
296
|
+
* | 9 | None of the above | `unknown` |
|
|
247
297
|
*
|
|
248
298
|
* **Tie-break rationale:**
|
|
249
299
|
* - `.DONE` always wins because the task succeeded regardless of how messy
|
|
250
300
|
* the session was (retries, compactions, etc.).
|
|
301
|
+
* - `model_access_error` beats generic `api_error` because it's more specific
|
|
302
|
+
* and enables targeted fallback (retry with session model).
|
|
251
303
|
* - `api_error` beats `context_overflow` because API failures are more
|
|
252
304
|
* actionable (auth fix, rate limit backoff).
|
|
253
305
|
* - `wall_clock_timeout` beats `process_crash` because the timer kill
|
|
@@ -269,14 +321,24 @@ export function classifyExit(input: ExitClassificationInput): ExitClassification
|
|
|
269
321
|
return "completed";
|
|
270
322
|
}
|
|
271
323
|
|
|
272
|
-
//
|
|
324
|
+
// 2a. Retries present with model-access error pattern → model_access_error
|
|
325
|
+
// 2b. Retries present with final retry failed → api_error
|
|
273
326
|
if (exitSummary?.retries && exitSummary.retries.length > 0) {
|
|
274
327
|
const lastRetry = exitSummary.retries[exitSummary.retries.length - 1];
|
|
275
328
|
if (!lastRetry.succeeded) {
|
|
329
|
+
// Check if the retry error indicates a model access issue
|
|
330
|
+
if (isModelAccessError(lastRetry.error)) {
|
|
331
|
+
return "model_access_error";
|
|
332
|
+
}
|
|
276
333
|
return "api_error";
|
|
277
334
|
}
|
|
278
335
|
}
|
|
279
336
|
|
|
337
|
+
// 2c. Error message (no retries) indicates model access issue → model_access_error
|
|
338
|
+
if (exitSummary?.error && isModelAccessError(exitSummary.error)) {
|
|
339
|
+
return "model_access_error";
|
|
340
|
+
}
|
|
341
|
+
|
|
280
342
|
// 3. Compactions > 0 AND high context utilization → context_overflow
|
|
281
343
|
if (exitSummary && exitSummary.compactions > 0) {
|
|
282
344
|
const effectivePct = contextPct ?? 0;
|
|
@@ -85,6 +85,7 @@ async function attemptWorkerCrashRetry(
|
|
|
85
85
|
allTaskOutcomes: LaneTaskOutcome[],
|
|
86
86
|
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
87
87
|
stateRoot: string,
|
|
88
|
+
runnerConfig?: TaskRunnerConfig,
|
|
88
89
|
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
89
90
|
if (!batchState.resilience) {
|
|
90
91
|
batchState.resilience = defaultResilienceState();
|
|
@@ -134,6 +135,14 @@ async function attemptWorkerCrashRetry(
|
|
|
134
135
|
continue;
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// model_access_error is handled by attemptModelFallbackRetry() — skip here
|
|
139
|
+
if (classification === "model_access_error") {
|
|
140
|
+
execLog("batch", batchState.batchId,
|
|
141
|
+
`tier0: task ${taskId} classified as model_access_error — deferring to model fallback handler`,
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
// Check retry budget
|
|
138
147
|
const scopeKey = tier0ScopeKey("worker_crash", taskId, waveIdx);
|
|
139
148
|
const currentCount = batchState.resilience.retryCountByScope[scopeKey] ?? 0;
|
|
@@ -333,6 +342,263 @@ async function attemptWorkerCrashRetry(
|
|
|
333
342
|
return { retriedCount, succeededRetries, failedRetries };
|
|
334
343
|
}
|
|
335
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Attempt model fallback retry for tasks that failed with `model_access_error`.
|
|
347
|
+
*
|
|
348
|
+
* When a configured agent model becomes unavailable mid-batch (API key expired,
|
|
349
|
+
* rate limit, model deprecated, provider outage), this function retries the task
|
|
350
|
+
* with the session model by setting `TASKPLANE_MODEL_FALLBACK=1` env var. The
|
|
351
|
+
* task-runner reads this var and omits the explicit `--model` flag, causing pi
|
|
352
|
+
* to use the session's default model.
|
|
353
|
+
*
|
|
354
|
+
* Only runs when `runnerConfig.model_fallback === "inherit"` (the default). When
|
|
355
|
+
* set to `"fail"`, model access errors fall through to normal failure handling.
|
|
356
|
+
*
|
|
357
|
+
* Separate from `attemptWorkerCrashRetry()` because:
|
|
358
|
+
* - Uses a different recovery pattern (`model_fallback` vs `worker_crash`)
|
|
359
|
+
* - Requires env var injection to change the model behavior
|
|
360
|
+
* - Has its own retry budget
|
|
361
|
+
*
|
|
362
|
+
* @since TP-055
|
|
363
|
+
*/
|
|
364
|
+
async function attemptModelFallbackRetry(
|
|
365
|
+
waveResult: WaveExecutionResult,
|
|
366
|
+
waveIdx: number,
|
|
367
|
+
batchState: OrchBatchRuntimeState,
|
|
368
|
+
orchConfig: OrchestratorConfig,
|
|
369
|
+
repoRoot: string,
|
|
370
|
+
workspaceConfig: WorkspaceConfig | null | undefined,
|
|
371
|
+
allTaskOutcomes: LaneTaskOutcome[],
|
|
372
|
+
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
373
|
+
stateRoot: string,
|
|
374
|
+
runnerConfig?: TaskRunnerConfig,
|
|
375
|
+
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
376
|
+
// Short-circuit: if model fallback is disabled, skip entirely
|
|
377
|
+
const modelFallbackMode = runnerConfig?.model_fallback ?? "inherit";
|
|
378
|
+
if (modelFallbackMode !== "inherit") {
|
|
379
|
+
return { retriedCount: 0, succeededRetries: [], failedRetries: [] };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!batchState.resilience) {
|
|
383
|
+
batchState.resilience = defaultResilienceState();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const budget = TIER0_RETRY_BUDGETS.model_fallback;
|
|
387
|
+
const succeededRetries: string[] = [];
|
|
388
|
+
const failedRetries: string[] = [];
|
|
389
|
+
let retriedCount = 0;
|
|
390
|
+
|
|
391
|
+
// Build a map from taskId → lane for re-execution
|
|
392
|
+
const taskToLane = new Map<string, AllocatedLane>();
|
|
393
|
+
for (const lane of waveResult.allocatedLanes) {
|
|
394
|
+
for (const task of lane.tasks) {
|
|
395
|
+
taskToLane.set(task.taskId, lane);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Process only model_access_error tasks
|
|
400
|
+
for (const taskId of [...waveResult.failedTaskIds]) {
|
|
401
|
+
const lane = taskToLane.get(taskId);
|
|
402
|
+
if (!lane) continue;
|
|
403
|
+
|
|
404
|
+
const outcome = allTaskOutcomes.find(o => o.taskId === taskId);
|
|
405
|
+
if (!outcome) continue;
|
|
406
|
+
|
|
407
|
+
const classification = outcome.exitDiagnostic?.classification;
|
|
408
|
+
if (classification !== "model_access_error") continue;
|
|
409
|
+
|
|
410
|
+
// Check retry budget
|
|
411
|
+
const scopeKey = tier0ScopeKey("model_fallback", taskId, waveIdx);
|
|
412
|
+
const currentCount = batchState.resilience.retryCountByScope[scopeKey] ?? 0;
|
|
413
|
+
if (currentCount >= budget.maxRetries) {
|
|
414
|
+
execLog("batch", batchState.batchId,
|
|
415
|
+
`tier0: task ${taskId} model fallback retry budget exhausted (${currentCount}/${budget.maxRetries})`,
|
|
416
|
+
{ scopeKey },
|
|
417
|
+
);
|
|
418
|
+
emitTier0Event(stateRoot, {
|
|
419
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount, budget.maxRetries),
|
|
420
|
+
taskId,
|
|
421
|
+
laneNumber: lane.laneNumber,
|
|
422
|
+
repoId: lane.repoId ?? null,
|
|
423
|
+
classification,
|
|
424
|
+
error: `Model fallback retry budget exhausted for task ${taskId}`,
|
|
425
|
+
scopeKey,
|
|
426
|
+
affectedTaskIds: [taskId],
|
|
427
|
+
suggestion: `Task ${taskId} failed with model_access_error and model fallback retry exhausted. Check API key validity and model availability.`,
|
|
428
|
+
});
|
|
429
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount, budget.maxRetries,
|
|
430
|
+
`Model fallback retry budget exhausted for task ${taskId}`, [taskId],
|
|
431
|
+
`Task ${taskId} failed with model_access_error and model fallback retry exhausted. Check API key validity and model availability.`,
|
|
432
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Increment retry counter
|
|
438
|
+
batchState.resilience.retryCountByScope[scopeKey] = currentCount + 1;
|
|
439
|
+
retriedCount++;
|
|
440
|
+
|
|
441
|
+
const failedModel = outcome.exitDiagnostic?.errorMessage || "configured model";
|
|
442
|
+
execLog("batch", batchState.batchId,
|
|
443
|
+
`tier0: model fallback — retrying task ${taskId} without explicit model (${failedModel} unavailable)`,
|
|
444
|
+
{ scopeKey, classification },
|
|
445
|
+
);
|
|
446
|
+
onNotify(
|
|
447
|
+
`🔄 Model fallback: Retrying task ${taskId} with session model (${failedModel} unavailable)`,
|
|
448
|
+
"info",
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Emit attempt event
|
|
452
|
+
emitTier0Event(stateRoot, {
|
|
453
|
+
...buildTier0EventBase("tier0_recovery_attempt", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
454
|
+
taskId,
|
|
455
|
+
laneNumber: lane.laneNumber,
|
|
456
|
+
repoId: lane.repoId ?? null,
|
|
457
|
+
classification,
|
|
458
|
+
cooldownMs: budget.cooldownMs,
|
|
459
|
+
scopeKey,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Cooldown before retry
|
|
463
|
+
if (budget.cooldownMs > 0) {
|
|
464
|
+
sleepSync(budget.cooldownMs);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Find the specific AllocatedTask
|
|
468
|
+
const allocatedTask = lane.tasks.find(t => t.taskId === taskId);
|
|
469
|
+
if (!allocatedTask) continue;
|
|
470
|
+
|
|
471
|
+
// Re-execute with model fallback env var
|
|
472
|
+
const retryLane: AllocatedLane = {
|
|
473
|
+
...lane,
|
|
474
|
+
tasks: [allocatedTask],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const isWsMode = !!workspaceConfig;
|
|
478
|
+
const wsRoot = workspaceConfig
|
|
479
|
+
? resolve(workspaceConfig.configPath, "..", "..")
|
|
480
|
+
: undefined;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const retryPauseSignal = { paused: false };
|
|
484
|
+
// Pass TASKPLANE_MODEL_FALLBACK=1 as extra env var to signal
|
|
485
|
+
// the task-runner to use the session model instead of configured model.
|
|
486
|
+
const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1" };
|
|
487
|
+
const retryResult = await executeLane(
|
|
488
|
+
retryLane,
|
|
489
|
+
orchConfig,
|
|
490
|
+
repoRoot,
|
|
491
|
+
retryPauseSignal,
|
|
492
|
+
wsRoot,
|
|
493
|
+
isWsMode,
|
|
494
|
+
modelFallbackEnv,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const retryOutcome = retryResult.tasks[0];
|
|
498
|
+
if (retryOutcome && retryOutcome.status === "succeeded") {
|
|
499
|
+
succeededRetries.push(taskId);
|
|
500
|
+
|
|
501
|
+
// Update waveResult: move from failed to succeeded
|
|
502
|
+
const failIdx = waveResult.failedTaskIds.indexOf(taskId);
|
|
503
|
+
if (failIdx !== -1) waveResult.failedTaskIds.splice(failIdx, 1);
|
|
504
|
+
waveResult.succeededTaskIds.push(taskId);
|
|
505
|
+
|
|
506
|
+
// Update lane results
|
|
507
|
+
for (const lr of waveResult.laneResults) {
|
|
508
|
+
const taskIdx = lr.tasks.findIndex(t => t.taskId === taskId);
|
|
509
|
+
if (taskIdx !== -1) {
|
|
510
|
+
lr.tasks[taskIdx] = retryOutcome;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
upsertTaskOutcome(allTaskOutcomes, retryOutcome);
|
|
516
|
+
|
|
517
|
+
execLog("batch", batchState.batchId,
|
|
518
|
+
`tier0: task ${taskId} model fallback retry succeeded`,
|
|
519
|
+
{ scopeKey },
|
|
520
|
+
);
|
|
521
|
+
onNotify(
|
|
522
|
+
`✅ Model fallback: Task ${taskId} succeeded with session model`,
|
|
523
|
+
"info",
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
emitTier0Event(stateRoot, {
|
|
527
|
+
...buildTier0EventBase("tier0_recovery_success", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
528
|
+
taskId,
|
|
529
|
+
laneNumber: lane.laneNumber,
|
|
530
|
+
repoId: lane.repoId ?? null,
|
|
531
|
+
classification,
|
|
532
|
+
resolution: `Task ${taskId} succeeded after falling back to session model`,
|
|
533
|
+
scopeKey,
|
|
534
|
+
});
|
|
535
|
+
} else {
|
|
536
|
+
failedRetries.push(taskId);
|
|
537
|
+
if (retryOutcome) {
|
|
538
|
+
upsertTaskOutcome(allTaskOutcomes, retryOutcome);
|
|
539
|
+
}
|
|
540
|
+
execLog("batch", batchState.batchId,
|
|
541
|
+
`tier0: task ${taskId} model fallback retry failed`,
|
|
542
|
+
{ scopeKey, exitReason: retryOutcome?.exitReason },
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const retryFailError = retryOutcome?.exitReason ?? `Task ${taskId} model fallback retry failed`;
|
|
546
|
+
emitTier0Event(stateRoot, {
|
|
547
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
548
|
+
taskId,
|
|
549
|
+
laneNumber: lane.laneNumber,
|
|
550
|
+
repoId: lane.repoId ?? null,
|
|
551
|
+
classification,
|
|
552
|
+
error: retryFailError,
|
|
553
|
+
scopeKey,
|
|
554
|
+
affectedTaskIds: [taskId],
|
|
555
|
+
suggestion: `Task ${taskId} failed even with session model fallback. Investigate task logs.`,
|
|
556
|
+
});
|
|
557
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries,
|
|
558
|
+
retryFailError, [taskId],
|
|
559
|
+
`Task ${taskId} failed even with session model fallback. Investigate task logs.`,
|
|
560
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
} catch (err: unknown) {
|
|
564
|
+
failedRetries.push(taskId);
|
|
565
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
566
|
+
execLog("batch", batchState.batchId,
|
|
567
|
+
`tier0: task ${taskId} model fallback retry threw error: ${errMsg}`,
|
|
568
|
+
{ scopeKey },
|
|
569
|
+
);
|
|
570
|
+
emitTier0Event(stateRoot, {
|
|
571
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
572
|
+
taskId,
|
|
573
|
+
laneNumber: lane.laneNumber,
|
|
574
|
+
repoId: lane.repoId ?? null,
|
|
575
|
+
classification,
|
|
576
|
+
error: errMsg,
|
|
577
|
+
scopeKey,
|
|
578
|
+
affectedTaskIds: [taskId],
|
|
579
|
+
suggestion: `Model fallback retry for task ${taskId} threw an exception: ${errMsg}`,
|
|
580
|
+
});
|
|
581
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries,
|
|
582
|
+
errMsg, [taskId],
|
|
583
|
+
`Model fallback retry for task ${taskId} threw an exception: ${errMsg}`,
|
|
584
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Recalculate wave-level status if retries changed outcomes
|
|
590
|
+
if (succeededRetries.length > 0) {
|
|
591
|
+
if (waveResult.failedTaskIds.length === 0) {
|
|
592
|
+
waveResult.overallStatus = "succeeded";
|
|
593
|
+
waveResult.stoppedEarly = false;
|
|
594
|
+
} else if (waveResult.succeededTaskIds.length > 0) {
|
|
595
|
+
waveResult.overallStatus = "partial";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { retriedCount, succeededRetries, failedRetries };
|
|
600
|
+
}
|
|
601
|
+
|
|
336
602
|
/**
|
|
337
603
|
* Attempt stale worktree recovery when lane allocation fails with ALLOC_WORKTREE_FAILED.
|
|
338
604
|
*
|
|
@@ -885,6 +1151,40 @@ export async function executeOrchBatch(
|
|
|
885
1151
|
}
|
|
886
1152
|
}
|
|
887
1153
|
|
|
1154
|
+
// ── TP-055: Tier 0 — Model fallback retry ───────────────
|
|
1155
|
+
// Run model fallback BEFORE worker crash retry so that model_access_error
|
|
1156
|
+
// tasks are retried with session model first. Worker crash retry skips
|
|
1157
|
+
// model_access_error tasks (handled here instead).
|
|
1158
|
+
if (waveResult.failedTaskIds.length > 0) {
|
|
1159
|
+
const modelFallbackOutcome = await attemptModelFallbackRetry(
|
|
1160
|
+
waveResult,
|
|
1161
|
+
waveIdx,
|
|
1162
|
+
batchState,
|
|
1163
|
+
orchConfig,
|
|
1164
|
+
repoRoot,
|
|
1165
|
+
workspaceConfig,
|
|
1166
|
+
allTaskOutcomes,
|
|
1167
|
+
onNotify,
|
|
1168
|
+
stateRoot,
|
|
1169
|
+
runnerConfig,
|
|
1170
|
+
);
|
|
1171
|
+
if (modelFallbackOutcome.succeededRetries.length > 0) {
|
|
1172
|
+
// Recompute blocked tasks after model fallback successes
|
|
1173
|
+
if (waveResult.policyApplied === "skip-dependents" && waveResult.failedTaskIds.length > 0) {
|
|
1174
|
+
const recomputed = computeTransitiveDependents(
|
|
1175
|
+
new Set(waveResult.failedTaskIds),
|
|
1176
|
+
depGraph,
|
|
1177
|
+
);
|
|
1178
|
+
waveResult.blockedTaskIds = [...recomputed].sort();
|
|
1179
|
+
} else if (waveResult.failedTaskIds.length === 0) {
|
|
1180
|
+
waveResult.blockedTaskIds = [];
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (modelFallbackOutcome.retriedCount > 0) {
|
|
1184
|
+
persistRuntimeState("tier0-model-fallback", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
888
1188
|
// ── TP-039: Tier 0 — Worker crash retry ─────────────────
|
|
889
1189
|
// Run retry BEFORE accumulating counts and blocked tasks so that
|
|
890
1190
|
// successfully retried tasks don't inflate failedTasks count and
|
|
@@ -683,6 +683,7 @@ export function spawnLaneSession(
|
|
|
683
683
|
config: OrchestratorConfig,
|
|
684
684
|
repoRoot: string,
|
|
685
685
|
workspaceRoot?: string,
|
|
686
|
+
extraEnvVars?: Record<string, string>,
|
|
686
687
|
): void {
|
|
687
688
|
const sessionName = lane.tmuxSessionName;
|
|
688
689
|
const laneId = lane.laneId;
|
|
@@ -706,6 +707,9 @@ export function spawnLaneSession(
|
|
|
706
707
|
|
|
707
708
|
// Build env vars
|
|
708
709
|
const envVars = buildLaneEnvVars(lane, task.task.promptPath, repoRoot, workspaceRoot);
|
|
710
|
+
if (extraEnvVars) {
|
|
711
|
+
Object.assign(envVars, extraEnvVars);
|
|
712
|
+
}
|
|
709
713
|
|
|
710
714
|
// Prepare per-task lane log path for post-mortem diagnostics
|
|
711
715
|
const laneLogPath = resolveLaneLogPath(lane, task);
|
|
@@ -1017,6 +1021,7 @@ export async function executeLane(
|
|
|
1017
1021
|
pauseSignal: { paused: boolean },
|
|
1018
1022
|
workspaceRoot?: string,
|
|
1019
1023
|
isWorkspaceMode?: boolean,
|
|
1024
|
+
extraEnvVars?: Record<string, string>,
|
|
1020
1025
|
): Promise<LaneExecutionResult> {
|
|
1021
1026
|
const laneId = lane.laneId;
|
|
1022
1027
|
const laneStartTime = Date.now();
|
|
@@ -1053,7 +1058,7 @@ export async function executeLane(
|
|
|
1053
1058
|
|
|
1054
1059
|
try {
|
|
1055
1060
|
// Spawn TMUX session
|
|
1056
|
-
spawnLaneSession(lane, task, config, repoRoot, workspaceRoot);
|
|
1061
|
+
spawnLaneSession(lane, task, config, repoRoot, workspaceRoot, extraEnvVars);
|
|
1057
1062
|
|
|
1058
1063
|
// Poll until completion
|
|
1059
1064
|
const pollResult = await pollUntilTaskComplete(
|
|
@@ -134,6 +134,13 @@ export interface TaskRunnerConfig {
|
|
|
134
134
|
reference_docs: Record<string, string>;
|
|
135
135
|
/** Named testing/verification commands (e.g., { test: "npx vitest run" }). Used for baseline fingerprinting (TP-032). */
|
|
136
136
|
testing_commands?: Record<string, string>;
|
|
137
|
+
/**
|
|
138
|
+
* Model fallback behavior when a configured model becomes unavailable mid-batch.
|
|
139
|
+
* - `"inherit"` (default): Retry without explicit model (session model fallback).
|
|
140
|
+
* - `"fail"`: No model substitution — normal failure path.
|
|
141
|
+
* @since TP-055
|
|
142
|
+
*/
|
|
143
|
+
model_fallback?: "inherit" | "fail";
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/** Result of a preflight check */
|
|
@@ -204,6 +211,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
|
|
|
204
211
|
export const DEFAULT_TASK_RUNNER_CONFIG: TaskRunnerConfig = {
|
|
205
212
|
task_areas: {},
|
|
206
213
|
reference_docs: {},
|
|
214
|
+
model_fallback: "inherit",
|
|
207
215
|
};
|
|
208
216
|
|
|
209
217
|
|
|
@@ -1384,7 +1392,8 @@ export const MERGE_FAILURE_CLASSIFICATIONS: readonly MergeFailureClassification[
|
|
|
1384
1392
|
export type Tier0RecoveryPattern =
|
|
1385
1393
|
| "worker_crash"
|
|
1386
1394
|
| "stale_worktree"
|
|
1387
|
-
| "cleanup_gate"
|
|
1395
|
+
| "cleanup_gate"
|
|
1396
|
+
| "model_fallback";
|
|
1388
1397
|
|
|
1389
1398
|
/**
|
|
1390
1399
|
* Exit classifications that are eligible for automatic Tier 0 retry.
|
|
@@ -1398,6 +1407,7 @@ export type Tier0RecoveryPattern =
|
|
|
1398
1407
|
*/
|
|
1399
1408
|
export const TIER0_RETRYABLE_CLASSIFICATIONS: ReadonlySet<string> = new Set([
|
|
1400
1409
|
"api_error",
|
|
1410
|
+
"model_access_error",
|
|
1401
1411
|
"process_crash",
|
|
1402
1412
|
"session_vanished",
|
|
1403
1413
|
]);
|
|
@@ -1444,6 +1454,11 @@ export const TIER0_RETRY_BUDGETS: Readonly<Record<Tier0RecoveryPattern, Tier0Ret
|
|
|
1444
1454
|
cooldownMs: 2_000,
|
|
1445
1455
|
backoffMultiplier: 1.0,
|
|
1446
1456
|
},
|
|
1457
|
+
model_fallback: {
|
|
1458
|
+
maxRetries: 1,
|
|
1459
|
+
cooldownMs: 3_000,
|
|
1460
|
+
backoffMultiplier: 1.0,
|
|
1461
|
+
},
|
|
1447
1462
|
};
|
|
1448
1463
|
|
|
1449
1464
|
/**
|
|
@@ -1457,6 +1472,7 @@ export const TIER0_RETRY_BUDGETS: Readonly<Record<Tier0RecoveryPattern, Tier0Ret
|
|
|
1457
1472
|
* @since TP-039
|
|
1458
1473
|
*/
|
|
1459
1474
|
export type Tier0EscalationPattern = Tier0RecoveryPattern | "merge_timeout";
|
|
1475
|
+
// Note: model_fallback is already included via Tier0RecoveryPattern
|
|
1460
1476
|
|
|
1461
1477
|
/**
|
|
1462
1478
|
* Context payload emitted when Tier 0 retries are exhausted and the
|