@dieulc/workflow 0.9.1 → 0.10.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 +82 -15
- package/edit-policy.ts +11 -64
- package/index.ts +1136 -200
- package/package.json +10 -6
- package/permissions.ts +393 -93
- package/provider-retarget.ts +258 -0
- package/review-handoff.ts +125 -0
- package/review-recovery.ts +65 -0
- package/review-runtime.ts +318 -48
- package/review-tools.ts +303 -6
- package/review.ts +321 -4
- package/shell-text.ts +542 -0
- package/utils.ts +42 -0
package/review.ts
CHANGED
|
@@ -64,6 +64,15 @@ export const REVIEW_SUBMITTED_ENTRY_TYPE = "workflow-review-submitted";
|
|
|
64
64
|
// ── Step 2: Review Mode configuration ────────────────────────────────
|
|
65
65
|
|
|
66
66
|
export type ReviewFallbackOnError = "skip" | "block";
|
|
67
|
+
export type ReviewProviderRecovery = "fallback" | "off";
|
|
68
|
+
export type ReviewChunkedSubmit = "auto" | "off";
|
|
69
|
+
|
|
70
|
+
/** A reviewer model reference used by the recovery chain or an explicit fallback. */
|
|
71
|
+
export interface ReviewFallbackModel {
|
|
72
|
+
provider: string;
|
|
73
|
+
id: string;
|
|
74
|
+
thinking: string;
|
|
75
|
+
}
|
|
67
76
|
|
|
68
77
|
export interface ReviewModeConfig {
|
|
69
78
|
/** Master toggle. When false every Review Mode code path is a no-op. */
|
|
@@ -76,6 +85,22 @@ export interface ReviewModeConfig {
|
|
|
76
85
|
verify: boolean;
|
|
77
86
|
/** What to do if the reviewer model is unavailable or the review fails. */
|
|
78
87
|
fallbackOnError: ReviewFallbackOnError;
|
|
88
|
+
/**
|
|
89
|
+
* Provider recovery: after a recoverable stream failure, try the fallback
|
|
90
|
+
* chain instead of failing the round immediately. `off` restores the old
|
|
91
|
+
* single-model behaviour.
|
|
92
|
+
*/
|
|
93
|
+
providerRecovery: ReviewProviderRecovery;
|
|
94
|
+
/** SDK retries per reviewer model before the chain advances (0-2). */
|
|
95
|
+
providerRetries: number;
|
|
96
|
+
/** Explicit first fallback; the runtime still dedupes and clamps it. */
|
|
97
|
+
fallbackModel?: ReviewFallbackModel;
|
|
98
|
+
/** Chunked rewrite submission for long plans (default automatic by size). */
|
|
99
|
+
chunkedSubmit: ReviewChunkedSubmit;
|
|
100
|
+
/** Max characters per `review_plan_chunk`; also sets the auto threshold. */
|
|
101
|
+
chunkChars: number;
|
|
102
|
+
/** Overall recovery budget; 0 uses the round deadline. */
|
|
103
|
+
recoveryBudgetMs: number;
|
|
79
104
|
/** Max `review_explore` subagents per review round (cost ceiling). */
|
|
80
105
|
exploreBudget: number;
|
|
81
106
|
/**
|
|
@@ -103,6 +128,11 @@ export const DEFAULT_REVIEW_MODE_CONFIG: Readonly<ReviewModeConfig> = {
|
|
|
103
128
|
passes: 2,
|
|
104
129
|
verify: true,
|
|
105
130
|
fallbackOnError: "skip",
|
|
131
|
+
providerRecovery: "fallback",
|
|
132
|
+
providerRetries: 1,
|
|
133
|
+
chunkedSubmit: "auto",
|
|
134
|
+
chunkChars: 6_000,
|
|
135
|
+
recoveryBudgetMs: 0,
|
|
106
136
|
exploreBudget: 1,
|
|
107
137
|
timeoutMs: 300_000,
|
|
108
138
|
contextChars: 15_000,
|
|
@@ -125,6 +155,83 @@ const CONTEXT_ENTRIES_MAX = 120;
|
|
|
125
155
|
const CONTEXT_TOOL_CHARS_MIN = 200;
|
|
126
156
|
const CONTEXT_TOOL_CHARS_MAX = 4_000;
|
|
127
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Build the automatic fallback list: the parent session's model first (it just
|
|
160
|
+
* planned successfully), then the base reviewer and planner roles from
|
|
161
|
+
* roles.json (ignoring the active profile). The caller supplies the clamp so
|
|
162
|
+
* this stays independent of Pi's model registry.
|
|
163
|
+
*/
|
|
164
|
+
export function buildReviewerFallbacks(input: {
|
|
165
|
+
parentModel?: ReviewFallbackModel;
|
|
166
|
+
baseReviewer?: ReviewFallbackModel;
|
|
167
|
+
basePlanner?: ReviewFallbackModel;
|
|
168
|
+
clampThinking?: (model: ReviewFallbackModel) => string;
|
|
169
|
+
}): ReviewFallbackModel[] {
|
|
170
|
+
const out: ReviewFallbackModel[] = [];
|
|
171
|
+
const seen = new Set<string>();
|
|
172
|
+
const push = (candidate: ReviewFallbackModel | undefined): void => {
|
|
173
|
+
if (!candidate) return;
|
|
174
|
+
const provider = candidate.provider.trim();
|
|
175
|
+
const id = candidate.id.trim();
|
|
176
|
+
if (!provider || !id) return;
|
|
177
|
+
const key = `${provider}/${id}`;
|
|
178
|
+
if (seen.has(key)) return;
|
|
179
|
+
seen.add(key);
|
|
180
|
+
let thinking = candidate.thinking.trim() || "high";
|
|
181
|
+
if (input.clampThinking) {
|
|
182
|
+
try {
|
|
183
|
+
thinking = input.clampThinking({ ...candidate, provider, id }) || thinking;
|
|
184
|
+
} catch {
|
|
185
|
+
/* a registry miss keeps the stored level */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
out.push({ provider, id, thinking });
|
|
189
|
+
};
|
|
190
|
+
push(input.parentModel);
|
|
191
|
+
push(input.baseReviewer);
|
|
192
|
+
push(input.basePlanner);
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The bounded attempt chain for one review round: the selected reviewer model,
|
|
198
|
+
* an explicit fallback, then the automatic fallback list. Duplicate
|
|
199
|
+
* provider/id pairs are rejected so a retry never repeats the same model.
|
|
200
|
+
*/
|
|
201
|
+
export function buildReviewerAttempts(input: {
|
|
202
|
+
base?: ReviewFallbackModel;
|
|
203
|
+
explicitFallback?: ReviewFallbackModel;
|
|
204
|
+
fallbacks?: readonly ReviewFallbackModel[];
|
|
205
|
+
max?: number;
|
|
206
|
+
}): ReviewFallbackModel[] {
|
|
207
|
+
const out: ReviewFallbackModel[] = [];
|
|
208
|
+
const seen = new Set<string>();
|
|
209
|
+
const push = (candidate: ReviewFallbackModel | undefined): void => {
|
|
210
|
+
if (!candidate) return;
|
|
211
|
+
const provider = candidate.provider.trim();
|
|
212
|
+
const id = candidate.id.trim();
|
|
213
|
+
if (!provider || !id) return;
|
|
214
|
+
const key = `${provider}/${id}`;
|
|
215
|
+
if (seen.has(key)) return;
|
|
216
|
+
seen.add(key);
|
|
217
|
+
out.push({
|
|
218
|
+
provider,
|
|
219
|
+
id,
|
|
220
|
+
thinking: candidate.thinking.trim() || "high",
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
push(input.base);
|
|
224
|
+
push(input.explicitFallback);
|
|
225
|
+
for (const model of input.fallbacks ?? []) push(model);
|
|
226
|
+
return out.slice(0, Math.max(1, Math.trunc(input.max ?? 4)));
|
|
227
|
+
}
|
|
228
|
+
const PROVIDER_RETRIES_MIN = 0;
|
|
229
|
+
const PROVIDER_RETRIES_MAX = 2;
|
|
230
|
+
const CHUNK_CHARS_MIN = 1_000;
|
|
231
|
+
const CHUNK_CHARS_MAX = 20_000;
|
|
232
|
+
const RECOVERY_BUDGET_MIN = 0;
|
|
233
|
+
const RECOVERY_BUDGET_MAX = 3_600_000;
|
|
234
|
+
|
|
128
235
|
/** A JSON object parsed from an untrusted source (settings file, fragment). */
|
|
129
236
|
export type JsonObject = Record<string, unknown>;
|
|
130
237
|
|
|
@@ -149,6 +256,19 @@ function asBool(value: unknown, fallback: boolean): boolean {
|
|
|
149
256
|
return typeof value === "boolean" ? value : fallback;
|
|
150
257
|
}
|
|
151
258
|
|
|
259
|
+
function sanitizeFallbackModel(value: unknown): ReviewFallbackModel | undefined {
|
|
260
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
261
|
+
const raw = value as Record<string, unknown>;
|
|
262
|
+
const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
|
|
263
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
264
|
+
if (!provider || !id) return undefined;
|
|
265
|
+
const thinking =
|
|
266
|
+
typeof raw.thinking === "string" && raw.thinking.trim()
|
|
267
|
+
? raw.thinking.trim()
|
|
268
|
+
: "high";
|
|
269
|
+
return { provider, id, thinking };
|
|
270
|
+
}
|
|
271
|
+
|
|
152
272
|
/**
|
|
153
273
|
* Coerce an untrusted settings fragment into a complete ReviewModeConfig.
|
|
154
274
|
* Accepts an object, or the boolean shorthand `true`/`false` for
|
|
@@ -168,12 +288,42 @@ export function coerceReviewModeConfig(
|
|
|
168
288
|
r.fallbackOnError === "block" || r.fallbackOnError === "skip"
|
|
169
289
|
? r.fallbackOnError
|
|
170
290
|
: base.fallbackOnError;
|
|
291
|
+
const providerRecovery: ReviewProviderRecovery =
|
|
292
|
+
r.providerRecovery === "off" || r.providerRecovery === "fallback"
|
|
293
|
+
? r.providerRecovery
|
|
294
|
+
: base.providerRecovery;
|
|
295
|
+
const chunkedSubmit: ReviewChunkedSubmit =
|
|
296
|
+
r.chunkedSubmit === "off" || r.chunkedSubmit === "auto"
|
|
297
|
+
? r.chunkedSubmit
|
|
298
|
+
: base.chunkedSubmit;
|
|
299
|
+
const fallbackModel = sanitizeFallbackModel(r.fallbackModel);
|
|
171
300
|
return {
|
|
172
301
|
enabled: asBool(r.enabled, base.enabled),
|
|
173
302
|
rounds: clampInt(r.rounds, ROUNDS_MIN, ROUNDS_MAX, base.rounds),
|
|
174
303
|
passes: clampInt(r.passes, PASSES_MIN, PASSES_MAX, base.passes),
|
|
175
304
|
verify: asBool(r.verify, base.verify),
|
|
176
305
|
fallbackOnError,
|
|
306
|
+
providerRecovery,
|
|
307
|
+
providerRetries: clampInt(
|
|
308
|
+
r.providerRetries,
|
|
309
|
+
PROVIDER_RETRIES_MIN,
|
|
310
|
+
PROVIDER_RETRIES_MAX,
|
|
311
|
+
base.providerRetries,
|
|
312
|
+
),
|
|
313
|
+
...(fallbackModel ? { fallbackModel } : {}),
|
|
314
|
+
chunkedSubmit,
|
|
315
|
+
chunkChars: clampInt(
|
|
316
|
+
r.chunkChars,
|
|
317
|
+
CHUNK_CHARS_MIN,
|
|
318
|
+
CHUNK_CHARS_MAX,
|
|
319
|
+
base.chunkChars,
|
|
320
|
+
),
|
|
321
|
+
recoveryBudgetMs: clampInt(
|
|
322
|
+
r.recoveryBudgetMs,
|
|
323
|
+
RECOVERY_BUDGET_MIN,
|
|
324
|
+
RECOVERY_BUDGET_MAX,
|
|
325
|
+
base.recoveryBudgetMs,
|
|
326
|
+
),
|
|
177
327
|
exploreBudget: clampInt(
|
|
178
328
|
r.exploreBudget,
|
|
179
329
|
EXPLORE_MIN,
|
|
@@ -478,6 +628,22 @@ export function formatReviewStatusLines(input: ReviewStatusInput): string[] {
|
|
|
478
628
|
c.timeoutMs,
|
|
479
629
|
input.timeoutSource,
|
|
480
630
|
)}`,
|
|
631
|
+
` recovery : ${c.providerRecovery}${
|
|
632
|
+
c.providerRecovery === "fallback"
|
|
633
|
+
? ` (${c.providerRetries} SDK retry/model; budget ${
|
|
634
|
+
c.recoveryBudgetMs === 0
|
|
635
|
+
? "round deadline"
|
|
636
|
+
: `${Math.round(c.recoveryBudgetMs / 1000)}s`
|
|
637
|
+
}${
|
|
638
|
+
c.fallbackModel
|
|
639
|
+
? `; explicit ${c.fallbackModel.provider}/${c.fallbackModel.id} (${c.fallbackModel.thinking})`
|
|
640
|
+
: ""
|
|
641
|
+
})`
|
|
642
|
+
: ""
|
|
643
|
+
}`,
|
|
644
|
+
` chunked submit : ${c.chunkedSubmit}${
|
|
645
|
+
c.chunkedSubmit === "auto" ? ` (${c.chunkChars} chars/chunk)` : ""
|
|
646
|
+
}`,
|
|
481
647
|
];
|
|
482
648
|
if (
|
|
483
649
|
input.timeoutSource !== "default" &&
|
|
@@ -682,6 +848,10 @@ export interface ReviewState {
|
|
|
682
848
|
rewrittenPlan?: string;
|
|
683
849
|
/** Reviewer passes completed in this round. */
|
|
684
850
|
passes: ReviewPassResult[];
|
|
851
|
+
/** Recovery attempts made in this round, in order (model labels). */
|
|
852
|
+
recoveryAttempts?: string[];
|
|
853
|
+
/** Last provider error text captured during the round. */
|
|
854
|
+
providerError?: string;
|
|
685
855
|
/** Number of `review_explore` subagents used this round. */
|
|
686
856
|
exploreUsed: number;
|
|
687
857
|
/** Input tokens reported by the child session, when available. */
|
|
@@ -1440,9 +1610,11 @@ export function reviewEscapeAction(input: {
|
|
|
1440
1610
|
export function reviewFooterSegment(
|
|
1441
1611
|
state: ReviewState | undefined,
|
|
1442
1612
|
paused: boolean,
|
|
1613
|
+
/** A round owns the turn before its first state event (cold boot). */
|
|
1614
|
+
starting = false,
|
|
1443
1615
|
): string {
|
|
1444
1616
|
if (paused) return "🔍 review paused — /review-resume to continue";
|
|
1445
|
-
if (!state) return "";
|
|
1617
|
+
if (!state) return starting ? "🔍 review · starting" : "";
|
|
1446
1618
|
return reviewStateSummary(state);
|
|
1447
1619
|
}
|
|
1448
1620
|
|
|
@@ -1460,14 +1632,97 @@ export function reviewStatusVisible(input: {
|
|
|
1460
1632
|
roundInFlight?: boolean;
|
|
1461
1633
|
roundActive?: boolean;
|
|
1462
1634
|
paused?: boolean;
|
|
1635
|
+
/**
|
|
1636
|
+
* The retained round state's phase, when one exists. A retained *terminal*
|
|
1637
|
+
* phase must not make the line visible during a new round's cold boot
|
|
1638
|
+
* (`roundActive` flips before the first live phase event).
|
|
1639
|
+
*/
|
|
1640
|
+
phase?: ReviewPhase;
|
|
1641
|
+
}): boolean {
|
|
1642
|
+
if (input.paused === true) return true;
|
|
1643
|
+
if (input.roundInFlight !== true && input.roundActive !== true) return false;
|
|
1644
|
+
// `undefined` means the round has not emitted a state yet; there is nothing
|
|
1645
|
+
// stale to paint and the lock/working message still describe the round.
|
|
1646
|
+
return input.phase === undefined || reviewIsActive(input.phase);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* The text for the live review status surface, or `""` when nothing is live.
|
|
1651
|
+
*
|
|
1652
|
+
* Single source for the footer segment and the review widget: a retained
|
|
1653
|
+
* `reviewState` is used for `/review-status` and the summary band, but it is
|
|
1654
|
+
* never painted as a live line once its round is over. While paused it shows
|
|
1655
|
+
* the paused form (never the retained terminal summary).
|
|
1656
|
+
*/
|
|
1657
|
+
export function reviewStatusText(input: {
|
|
1658
|
+
state: ReviewState | undefined;
|
|
1659
|
+
roundInFlight?: boolean;
|
|
1660
|
+
roundActive?: boolean;
|
|
1661
|
+
paused?: boolean;
|
|
1662
|
+
}): string {
|
|
1663
|
+
const visible = reviewStatusVisible({
|
|
1664
|
+
roundInFlight: input.roundInFlight,
|
|
1665
|
+
roundActive: input.roundActive,
|
|
1666
|
+
paused: input.paused,
|
|
1667
|
+
phase: input.state?.phase,
|
|
1668
|
+
});
|
|
1669
|
+
return visible
|
|
1670
|
+
? reviewFooterSegment(
|
|
1671
|
+
input.state,
|
|
1672
|
+
input.paused === true,
|
|
1673
|
+
input.roundInFlight === true,
|
|
1674
|
+
)
|
|
1675
|
+
: "";
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Whether a pause snapshot belongs to the current review cycle.
|
|
1680
|
+
*
|
|
1681
|
+
* Pauses are in-memory and cycle-tagged. A snapshot from an older cycle must
|
|
1682
|
+
* never keep the review line/lock alive or be resumed after new work started.
|
|
1683
|
+
*/
|
|
1684
|
+
export function reviewPauseIsCurrent(input: {
|
|
1685
|
+
pauseCycle: number | undefined;
|
|
1686
|
+
currentCycle: number;
|
|
1463
1687
|
}): boolean {
|
|
1464
1688
|
return (
|
|
1465
|
-
input.
|
|
1466
|
-
input.
|
|
1467
|
-
input.paused === true
|
|
1689
|
+
typeof input.pauseCycle === "number" &&
|
|
1690
|
+
input.pauseCycle === input.currentCycle
|
|
1468
1691
|
);
|
|
1469
1692
|
}
|
|
1470
1693
|
|
|
1694
|
+
/** Result of consuming one review round from the cycle budget. */
|
|
1695
|
+
export interface ReviewRoundBudget {
|
|
1696
|
+
/** The round number this attempt would use (1-based). */
|
|
1697
|
+
round: number;
|
|
1698
|
+
/** False when the cycle's `rounds` cap has already been consumed. */
|
|
1699
|
+
allowed: boolean;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* Consume one round from a cycle's budget.
|
|
1704
|
+
*
|
|
1705
|
+
* A new cycle starts at round 1 regardless of the previous cycle's count; this
|
|
1706
|
+
* is the fix for the session-wide counter that made a second plan skip review
|
|
1707
|
+
* with `round limit (1) reached`. Within a cycle the existing cap applies.
|
|
1708
|
+
*/
|
|
1709
|
+
export function nextReviewRound(input: {
|
|
1710
|
+
round: number;
|
|
1711
|
+
maxRounds: number;
|
|
1712
|
+
newCycle?: boolean;
|
|
1713
|
+
}): ReviewRoundBudget {
|
|
1714
|
+
const maxRaw = Number.isFinite(input.maxRounds)
|
|
1715
|
+
? Math.trunc(input.maxRounds)
|
|
1716
|
+
: 1;
|
|
1717
|
+
const max = Math.max(1, maxRaw);
|
|
1718
|
+
const roundRaw = Number.isFinite(input.round)
|
|
1719
|
+
? Math.trunc(input.round)
|
|
1720
|
+
: 0;
|
|
1721
|
+
const current = Math.max(0, roundRaw);
|
|
1722
|
+
const round = (input.newCycle === true ? 0 : current) + 1;
|
|
1723
|
+
return { round, allowed: round <= max };
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1471
1726
|
// ── Mode-switch lock (plan 2026-10-25) ───────────────────────────────
|
|
1472
1727
|
//
|
|
1473
1728
|
// While a review owns the turn, mode transitions are refused: switching to
|
|
@@ -2109,6 +2364,12 @@ export interface CreateReviewSessionOptions {
|
|
|
2109
2364
|
reviewPrompt: string;
|
|
2110
2365
|
/** Reviewer-only tools, built by the caller. */
|
|
2111
2366
|
customTools: unknown[];
|
|
2367
|
+
/**
|
|
2368
|
+
* SDK retries per reviewer model (0-2). Applied through a runtime-only child
|
|
2369
|
+
* SettingsManager override so the recovery chain advances after one doomed
|
|
2370
|
+
* long request instead of spending the parent's 3-retry budget first.
|
|
2371
|
+
*/
|
|
2372
|
+
providerRetries?: number;
|
|
2112
2373
|
/**
|
|
2113
2374
|
* Parent-side provider-registration surface (structural, feature-detected),
|
|
2114
2375
|
* found with `findParentProviderSurface(ctx)` — the session's model registry
|
|
@@ -2169,6 +2430,53 @@ function readActiveToolNames(session: unknown): string[] | undefined {
|
|
|
2169
2430
|
}
|
|
2170
2431
|
}
|
|
2171
2432
|
|
|
2433
|
+
function clampProviderRetries(value: number | undefined): number {
|
|
2434
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 1;
|
|
2435
|
+
return Math.min(2, Math.max(0, Math.trunc(value)));
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* Build the child's runtime-only SettingsManager so its SDK retry budget is
|
|
2440
|
+
* bounded. `applyOverrides` is in-memory: the shared settings.json is never
|
|
2441
|
+
* written. Feature-detected for older Pi runtimes; a failure here means the
|
|
2442
|
+
* child keeps the parent's settings and the chain simply waits that budget out.
|
|
2443
|
+
*/
|
|
2444
|
+
function createChildSettingsManager(
|
|
2445
|
+
sdk: unknown,
|
|
2446
|
+
cwd: string,
|
|
2447
|
+
agentDir: string,
|
|
2448
|
+
providerRetries: number | undefined,
|
|
2449
|
+
): unknown | undefined {
|
|
2450
|
+
try {
|
|
2451
|
+
const holder = sdk as {
|
|
2452
|
+
SettingsManager?: { create?: (...args: unknown[]) => unknown };
|
|
2453
|
+
};
|
|
2454
|
+
const SettingsManager = holder?.SettingsManager;
|
|
2455
|
+
const create = SettingsManager?.create;
|
|
2456
|
+
if (typeof create !== "function") return undefined;
|
|
2457
|
+
const manager = create.call(SettingsManager, cwd, agentDir) as {
|
|
2458
|
+
getRetrySettings?: () => Record<string, unknown>;
|
|
2459
|
+
applyOverrides?: (overrides: unknown) => void;
|
|
2460
|
+
};
|
|
2461
|
+
if (!manager || typeof manager !== "object") return undefined;
|
|
2462
|
+
if (typeof manager.applyOverrides !== "function") return manager;
|
|
2463
|
+
const base =
|
|
2464
|
+
typeof manager.getRetrySettings === "function"
|
|
2465
|
+
? manager.getRetrySettings()
|
|
2466
|
+
: { enabled: true, maxRetries: 3, baseDelayMs: 2_000, maxAgentDelayMs: 60_000 };
|
|
2467
|
+
manager.applyOverrides({
|
|
2468
|
+
retry: {
|
|
2469
|
+
...base,
|
|
2470
|
+
enabled: true,
|
|
2471
|
+
maxRetries: clampProviderRetries(providerRetries),
|
|
2472
|
+
},
|
|
2473
|
+
});
|
|
2474
|
+
return manager;
|
|
2475
|
+
} catch {
|
|
2476
|
+
return undefined;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2172
2480
|
function wrapAgentSession(
|
|
2173
2481
|
raw: unknown,
|
|
2174
2482
|
modelLabel: string,
|
|
@@ -2344,6 +2652,14 @@ export async function createReviewSession(
|
|
|
2344
2652
|
// custom tools too, so a built-ins-only list would silently remove
|
|
2345
2653
|
// `review_submit_plan` and make every review fail (see REVIEW_BUILTIN_TOOLS).
|
|
2346
2654
|
const expectedTools = reviewToolAllowlist(options.customTools);
|
|
2655
|
+
// The child gets its own retry budget. The shared settings file is not
|
|
2656
|
+
// touched: applyOverrides is runtime-only.
|
|
2657
|
+
const settingsManager = createChildSettingsManager(
|
|
2658
|
+
sdk,
|
|
2659
|
+
options.cwd,
|
|
2660
|
+
options.agentDir,
|
|
2661
|
+
options.providerRetries,
|
|
2662
|
+
);
|
|
2347
2663
|
|
|
2348
2664
|
process.env[REVIEW_CHILD_MARKER_ENV] = "1";
|
|
2349
2665
|
let created: { session?: unknown; modelFallbackMessage?: string };
|
|
@@ -2358,6 +2674,7 @@ export async function createReviewSession(
|
|
|
2358
2674
|
customTools: options.customTools as never,
|
|
2359
2675
|
resourceLoader: loader,
|
|
2360
2676
|
sessionManager,
|
|
2677
|
+
...(settingsManager ? { settingsManager } : {}),
|
|
2361
2678
|
sessionStartEvent: { type: "session_start", reason: "startup" },
|
|
2362
2679
|
} as never);
|
|
2363
2680
|
} finally {
|