taskplane 0.10.2 → 0.12.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/dashboard/server.cjs +4 -0
- 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 +335 -13
- package/extensions/taskplane/execution.ts +6 -1
- package/extensions/taskplane/merge.ts +358 -2
- package/extensions/taskplane/supervisor-primer.md +30 -0
- package/extensions/taskplane/supervisor.ts +23 -0
- package/extensions/taskplane/types.ts +123 -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 |
|
package/dashboard/server.cjs
CHANGED
|
@@ -437,6 +437,10 @@ function loadTelemetryData(batchState) {
|
|
|
437
437
|
for (const event of events) {
|
|
438
438
|
switch (event.type) {
|
|
439
439
|
case "message_end": {
|
|
440
|
+
// A successful message_end means any prior retry resolved.
|
|
441
|
+
// Clear retryActive to prevent stale retry badges from persisting
|
|
442
|
+
// across batches or after transient API errors recover.
|
|
443
|
+
acc.retryActive = false;
|
|
440
444
|
const usage = event.message?.usage;
|
|
441
445
|
if (usage) {
|
|
442
446
|
acc.inputTokens += usage.input || 0;
|
|
@@ -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;
|