ai-foreman 1.7.24 → 1.7.26
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 +2 -1
- package/dist/adapters/claude.d.ts +15 -0
- package/dist/adapters/claude.js +149 -28
- package/dist/adapters/codex.d.ts +9 -0
- package/dist/adapters/codex.js +77 -8
- package/dist/adapters/fastMode.d.ts +4 -0
- package/dist/adapters/fastMode.js +9 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/agentRun.js +1 -0
- package/dist/branch/runner.js +46 -44
- package/dist/buildAssignment.d.ts +21 -1
- package/dist/buildAssignment.js +215 -2
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +73 -7
- package/dist/cli/start.d.ts +8 -1
- package/dist/cli/start.js +132 -38
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +20 -2
- package/dist/continuity.js +11 -10
- package/dist/foreman.d.ts +8 -0
- package/dist/foreman.js +69 -10
- package/dist/handoffProtocol.d.ts +4 -0
- package/dist/handoffProtocol.js +19 -0
- package/dist/handoffs.d.ts +1 -1
- package/dist/handoffs.js +23 -4
- package/dist/permissions/policy.js +18 -11
- package/dist/permissions/shellForms.d.ts +3 -0
- package/dist/permissions/shellForms.js +203 -0
- package/dist/qaBuilderActivation.d.ts +19 -0
- package/dist/qaBuilderActivation.js +112 -0
- package/dist/qaFailureDelivery.js +1 -1
- package/dist/qaRecovery.js +2 -1
- package/dist/qaReview.d.ts +4 -0
- package/dist/qaReview.js +37 -7
- package/dist/sessionLifecycle.js +1 -1
- package/dist/supervisedStart.js +23 -3
- package/dist/supervisor.js +33 -0
- package/dist/tickets/groupReset.js +100 -96
- package/dist/tickets/reset.js +80 -69
- package/dist/workflowDb.d.ts +16 -1
- package/dist/workflowDb.js +276 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Provider sessions are historical after import. Resume with `rafi build:resume <p
|
|
|
116
116
|
| `-a, --agent <agent>` | `claude` / `codex` | single `rafi-config.yaml` target, otherwise `claude` | Builder agent. Explicit `--agent` always wins. |
|
|
117
117
|
| `-m, --model <model>` | `gpt-5.5` / any supported agent model | agent default | Overrides the builder model. |
|
|
118
118
|
| `--effort <level>` | Claude Code: `low` / `medium` / `high` / `xhigh`<br>Codex: `low` / `medium` / `high` / `xhigh` | agent default | Reasoning level. |
|
|
119
|
-
| `--fast` | flag | off |
|
|
119
|
+
| `--fast` | flag | off | Use native fast mode when the selected model supports it. Otherwise continue at standard speed with the same model and reasoning settings. |
|
|
120
120
|
| `-t, --tickets <path>` | `.md` / `.txt` / `.yaml` | auto-detects standard tracker files | Sends a task file to the builder during preflight planning. |
|
|
121
121
|
| `-y, --yes` | flag | off | Skips preflight confirmation. |
|
|
122
122
|
| `--no-qa` | flag | QA on | Disables per-ticket QA review. |
|
|
@@ -640,6 +640,7 @@ For Claude:
|
|
|
640
640
|
- Bash is denied if it contains an always-escalate substring.
|
|
641
641
|
- Bash is otherwise allowed only for configured prefixes.
|
|
642
642
|
- Every chained segment must start with an `allowBash` prefix.
|
|
643
|
+
- Supported project test forms include literal in-worktree `cd directory &&`, constrained `PYTHONPATH` or test environment assignments, existing project-local virtualenv Python/pytest executables, and `2>&1`. `uv run --offline --no-sync` may wrap an already allowed test command. Every pipeline executable must be allowed. Configure additional test commands under `permissions.allowBash` in `foreman.yaml`; PATH/loader overrides, substitutions, file redirection, and dependency acquisition remain separate decisions.
|
|
643
644
|
- Unknown tools are denied.
|
|
644
645
|
- Unknown Bash commands are denied.
|
|
645
646
|
|
|
@@ -95,9 +95,22 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
95
95
|
private terminalResult?;
|
|
96
96
|
private streamEnded;
|
|
97
97
|
private closed;
|
|
98
|
+
private closePromise?;
|
|
99
|
+
private launchScopeObserved;
|
|
100
|
+
private providerIdentityObserved;
|
|
101
|
+
private toolCwd?;
|
|
102
|
+
private readonly toolCwds;
|
|
103
|
+
private fastModePrepared?;
|
|
104
|
+
private fastModeEnabled;
|
|
105
|
+
private fastModeUnavailable;
|
|
106
|
+
private turnSawWork;
|
|
107
|
+
private prepareFastMode;
|
|
108
|
+
private disableFastModeNotice;
|
|
109
|
+
private disableFastMode;
|
|
98
110
|
static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
|
|
99
111
|
private constructor();
|
|
100
112
|
/** Background loop: consume the SDK message stream until it ends. */
|
|
113
|
+
private providerInputs;
|
|
101
114
|
private pump;
|
|
102
115
|
private handle;
|
|
103
116
|
sendTurn(text: string, policy?: {
|
|
@@ -107,6 +120,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
107
120
|
purpose?: ProviderTurnPurpose;
|
|
108
121
|
}): Promise<TurnResult>;
|
|
109
122
|
private sendTurnInternal;
|
|
123
|
+
private sendTurnAttempt;
|
|
110
124
|
private captureStderr;
|
|
111
125
|
private observationContext;
|
|
112
126
|
private observeToolResults;
|
|
@@ -128,6 +142,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
128
142
|
observeEvents(listener: (event: BuilderEvent) => void): () => void;
|
|
129
143
|
events(): AsyncIterable<BuilderEvent>;
|
|
130
144
|
close(): Promise<void>;
|
|
145
|
+
private closeInternal;
|
|
131
146
|
private failSessionIdentity;
|
|
132
147
|
private observeSession;
|
|
133
148
|
private settlePending;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ProviderPhaseBarrier } from "../providerPhase.js";
|
|
2
2
|
import { OperationDeadline } from "../util/deadline.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
5
5
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
6
6
|
export async function requireClaudeSDK() {
|
|
7
7
|
try {
|
|
@@ -20,6 +20,8 @@ import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
|
20
20
|
import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
|
|
21
21
|
import { createProviderSessionRef, validateProviderSessionScope, canonicalSessionPath } from "../sessionIdentity.js";
|
|
22
22
|
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
23
|
+
import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
|
|
24
|
+
import { handoffSystemInstructions } from "../handoffProtocol.js";
|
|
23
25
|
const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
24
26
|
/**
|
|
25
27
|
* Pure function: build the `options` object passed to `query()`.
|
|
@@ -37,7 +39,9 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
37
39
|
resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
|
|
38
40
|
permissionMode: qaReadOnly ? "default" : "acceptEdits",
|
|
39
41
|
effort: opts.effort,
|
|
40
|
-
|
|
42
|
+
// Start at standard speed even when user/project settings enable fast mode.
|
|
43
|
+
// Enabling it before checking capabilities can silently switch the model.
|
|
44
|
+
settings: { fastMode: false },
|
|
41
45
|
// Read-only roles still need the user's Claude authentication settings (for
|
|
42
46
|
// example, an enterprise apiKeyHelper). Do not load repository-controlled
|
|
43
47
|
// settings in those roles.
|
|
@@ -60,7 +64,7 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
60
64
|
const exactSkills = opts.preloadedSkillContent?.length
|
|
61
65
|
? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
|
|
62
66
|
: undefined;
|
|
63
|
-
const systemAppend = [opts.systemPromptAppend, opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
67
|
+
const systemAppend = [handoffSystemInstructions(opts.sessionRole, opts.systemPromptAppend), opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
64
68
|
if (systemAppend) {
|
|
65
69
|
base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
|
|
66
70
|
}
|
|
@@ -216,6 +220,58 @@ export class ClaudeAdapter {
|
|
|
216
220
|
terminalResult;
|
|
217
221
|
streamEnded = false;
|
|
218
222
|
closed = false;
|
|
223
|
+
closePromise;
|
|
224
|
+
launchScopeObserved = false;
|
|
225
|
+
providerIdentityObserved = false;
|
|
226
|
+
toolCwd;
|
|
227
|
+
toolCwds = new Map();
|
|
228
|
+
fastModePrepared;
|
|
229
|
+
fastModeEnabled = false;
|
|
230
|
+
fastModeUnavailable = false;
|
|
231
|
+
turnSawWork = false;
|
|
232
|
+
async prepareFastMode() {
|
|
233
|
+
if (!this.opts.fast || this.fastModeUnavailable)
|
|
234
|
+
return;
|
|
235
|
+
this.fastModePrepared ??= (async () => {
|
|
236
|
+
// A missing capability is conservative: standard speed, same model.
|
|
237
|
+
let models;
|
|
238
|
+
try {
|
|
239
|
+
const init = await new OperationDeadline("Claude model capabilities", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.initializationResult());
|
|
240
|
+
models = init.models ?? [];
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (!isFastModeUnavailable(String(error)))
|
|
244
|
+
throw error;
|
|
245
|
+
this.disableFastModeNotice();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const model = models.find(item => item.value === (this.opts.model ?? "default"));
|
|
249
|
+
if (model?.supportsFastMode !== true || typeof this.query.applyFlagSettings !== "function") {
|
|
250
|
+
this.disableFastModeNotice();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await new OperationDeadline("Claude fast mode", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: true }));
|
|
255
|
+
this.fastModeEnabled = true;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (!isFastModeUnavailable(String(error)))
|
|
259
|
+
throw error;
|
|
260
|
+
await this.disableFastMode();
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
await this.fastModePrepared;
|
|
264
|
+
}
|
|
265
|
+
disableFastModeNotice() {
|
|
266
|
+
if (!this.fastModeUnavailable)
|
|
267
|
+
reportFastModeUnavailable(this.opts.model);
|
|
268
|
+
this.fastModeUnavailable = true;
|
|
269
|
+
this.fastModeEnabled = false;
|
|
270
|
+
}
|
|
271
|
+
async disableFastMode() {
|
|
272
|
+
await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
|
|
273
|
+
this.disableFastModeNotice();
|
|
274
|
+
}
|
|
219
275
|
static async create(opts) {
|
|
220
276
|
try {
|
|
221
277
|
const runtimeExecutable = opts.runtimeExecutable ?? resolveExecutablePath("claude");
|
|
@@ -269,7 +325,7 @@ export class ClaudeAdapter {
|
|
|
269
325
|
finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
|
|
270
326
|
} : undefined);
|
|
271
327
|
this.query = query({
|
|
272
|
-
prompt: this.
|
|
328
|
+
prompt: this.providerInputs(),
|
|
273
329
|
options: {
|
|
274
330
|
...buildClaudeQueryOptions(opts),
|
|
275
331
|
abortController: this.abort,
|
|
@@ -277,6 +333,10 @@ export class ClaudeAdapter {
|
|
|
277
333
|
PreToolUse: [{ hooks: [async (input) => {
|
|
278
334
|
if (input.hook_event_name !== "PreToolUse")
|
|
279
335
|
return {};
|
|
336
|
+
this.toolCwd = input.cwd;
|
|
337
|
+
this.toolCwds.set(input.tool_use_id, input.cwd);
|
|
338
|
+
if (this.toolCwds.size > 1000)
|
|
339
|
+
this.toolCwds.delete(this.toolCwds.keys().next().value);
|
|
280
340
|
const reason = this.phaseBarrier.denial(input.tool_name);
|
|
281
341
|
return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
|
|
282
342
|
}] }],
|
|
@@ -306,6 +366,7 @@ export class ClaudeAdapter {
|
|
|
306
366
|
const decision = await opts.permission({
|
|
307
367
|
toolName,
|
|
308
368
|
input,
|
|
369
|
+
executionCwd: (requestOptions.toolUseID ? this.toolCwds.get(requestOptions.toolUseID) : undefined) ?? this.toolCwd ?? opts.cwd,
|
|
309
370
|
signal: requestOptions.signal,
|
|
310
371
|
title: requestOptions.title,
|
|
311
372
|
displayName: requestOptions.displayName,
|
|
@@ -334,6 +395,17 @@ export class ClaudeAdapter {
|
|
|
334
395
|
this.pumpDone = this.pump();
|
|
335
396
|
}
|
|
336
397
|
/** Background loop: consume the SDK message stream until it ends. */
|
|
398
|
+
async *providerInputs() {
|
|
399
|
+
for await (const message of this.inbox) {
|
|
400
|
+
// A closed queue can still contain buffered input. Never let the SDK
|
|
401
|
+
// submit it after we recorded a proven pre-submit cancellation/failure.
|
|
402
|
+
if (this.closed || this.terminalResult || this.streamEnded)
|
|
403
|
+
return;
|
|
404
|
+
if (this.pending?.instruction === message.message.content)
|
|
405
|
+
this.pending.submitted = true;
|
|
406
|
+
yield message;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
337
409
|
async pump() {
|
|
338
410
|
try {
|
|
339
411
|
for await (const msg of this.query) {
|
|
@@ -394,10 +466,14 @@ export class ClaudeAdapter {
|
|
|
394
466
|
for (const block of msg.message.content) {
|
|
395
467
|
this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
396
468
|
if (block.type === "text" && block.text) {
|
|
469
|
+
if (msg.error && this.fastModeEnabled && isFastModeUnavailable(block.text) && !this.turnSawWork)
|
|
470
|
+
continue;
|
|
471
|
+
this.turnSawWork = true;
|
|
397
472
|
this.eventQueue.push({ kind: "text", text: block.text, byteCount: Buffer.byteLength(block.text), digest: createHash("sha256").update(block.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
398
473
|
this.opts.observer?.signal(true);
|
|
399
474
|
}
|
|
400
475
|
else if (block.type === "tool_use") {
|
|
476
|
+
this.turnSawWork = true;
|
|
401
477
|
const callId = block.id;
|
|
402
478
|
this.toolCalls.set(callId, { name: block.name, input: block.input });
|
|
403
479
|
if (this.toolCalls.size > 1000)
|
|
@@ -530,7 +606,8 @@ export class ClaudeAdapter {
|
|
|
530
606
|
result.outputTokens = mergedUsage.outputTokens;
|
|
531
607
|
result.usage = { scope: "session-cumulative", inputTokens: mergedUsage.sample.inputTokens, outputTokens: mergedUsage.sample.outputTokens, totalTokens: mergedUsage.sample.totalTokens, costUsd: mergedUsage.sample.authoritativeCostUsd };
|
|
532
608
|
this.cumulativeUsage = mergedUsage.sample;
|
|
533
|
-
|
|
609
|
+
if (!(result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(text)))
|
|
610
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
534
611
|
this.settlePending(result, false);
|
|
535
612
|
this.turnSignals = [];
|
|
536
613
|
this.structuredError = undefined;
|
|
@@ -582,7 +659,17 @@ export class ClaudeAdapter {
|
|
|
582
659
|
this.responseOnlyTurn = false;
|
|
583
660
|
}
|
|
584
661
|
}
|
|
585
|
-
sendTurnInternal(text) {
|
|
662
|
+
async sendTurnInternal(text) {
|
|
663
|
+
if (this.opts.fast)
|
|
664
|
+
await this.prepareFastMode();
|
|
665
|
+
const result = await this.sendTurnAttempt(text);
|
|
666
|
+
if (result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
|
|
667
|
+
await this.disableFastMode();
|
|
668
|
+
return this.sendTurnAttempt(text);
|
|
669
|
+
}
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
sendTurnAttempt(text) {
|
|
586
673
|
if (this.closed)
|
|
587
674
|
return Promise.reject(new Error("builder is closed"));
|
|
588
675
|
if (this.terminalResult || this.streamEnded)
|
|
@@ -593,9 +680,10 @@ export class ClaudeAdapter {
|
|
|
593
680
|
return new Promise((resolve, reject) => {
|
|
594
681
|
this.eventQueue.push({ kind: "activity", state: "starting Claude turn", provider: "claude", model: this.opts.model });
|
|
595
682
|
this.turnSignals = [];
|
|
683
|
+
this.turnSawWork = false;
|
|
596
684
|
this.structuredError = undefined;
|
|
597
685
|
this.apiErrorStatus = undefined;
|
|
598
|
-
const pending = this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
686
|
+
const pending = this.pending = { resolve, reject, instruction: text, submitted: false, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
599
687
|
this.armPendingTurn(pending);
|
|
600
688
|
this.inbox.push({
|
|
601
689
|
type: "user",
|
|
@@ -642,15 +730,16 @@ export class ClaudeAdapter {
|
|
|
642
730
|
// continue the same turn. Preserve that state as unknown until a
|
|
643
731
|
// correlated, explicit terminal result arrives.
|
|
644
732
|
const resultReason = elapsedOnly ? "elapsed_placeholder"
|
|
645
|
-
: failed && /
|
|
646
|
-
: failed && /
|
|
647
|
-
: failed && /
|
|
648
|
-
: failed && /
|
|
649
|
-
: failed &&
|
|
650
|
-
: failed && /\b(?:
|
|
651
|
-
: failed ? "
|
|
652
|
-
: "
|
|
653
|
-
|
|
733
|
+
: failed && /Foreman policy: this action needs human approval\b[\s\S]*\band was NOT performed\./.test(text) ? "host_policy_denied"
|
|
734
|
+
: failed && /timed?\s*out|timeout/i.test(text) ? "timeout"
|
|
735
|
+
: failed && /cancel(?:led|ed)/i.test(text) ? "cancelled"
|
|
736
|
+
: failed && /permission denied/i.test(text) ? "permission_denied"
|
|
737
|
+
: failed && /not found/i.test(text) ? "not_found"
|
|
738
|
+
: failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
|
|
739
|
+
: failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
|
|
740
|
+
: failed ? "unexplained_error"
|
|
741
|
+
: "completed";
|
|
742
|
+
const status = resultReason === "host_policy_denied" ? "denied" : resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
|
|
654
743
|
if (status !== "unknown") {
|
|
655
744
|
this.completedToolCalls.add(callId);
|
|
656
745
|
if (this.completedToolCalls.size > 1000)
|
|
@@ -699,7 +788,8 @@ export class ClaudeAdapter {
|
|
|
699
788
|
});
|
|
700
789
|
const prepare = async () => {
|
|
701
790
|
await this.query.initializationResult();
|
|
702
|
-
|
|
791
|
+
await this.prepareFastMode();
|
|
792
|
+
if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function") {
|
|
703
793
|
const commands = await this.query.supportedCommands();
|
|
704
794
|
// Some SDK/CLI pairs expose init identity only after their first input.
|
|
705
795
|
// Require an advertised built-in local command; never substitute a
|
|
@@ -708,7 +798,10 @@ export class ClaudeAdapter {
|
|
|
708
798
|
this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
|
|
709
799
|
const result = await this.sendTurn("/context", { responseOnly: true });
|
|
710
800
|
this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
|
|
711
|
-
|
|
801
|
+
// SDK total_cost_usd includes earlier conversation cost on resume.
|
|
802
|
+
// The advertised built-in command must report zero model turns;
|
|
803
|
+
// historical cumulative cost does not make this a paid warm-up.
|
|
804
|
+
if (result.isError || result.failure || result.numTurns !== 0 || (result.costUsd > 0 && result.usage?.scope !== "session-cumulative"))
|
|
712
805
|
throw new Error("Claude local context initialization did not complete without model work");
|
|
713
806
|
}
|
|
714
807
|
}
|
|
@@ -803,7 +896,7 @@ export class ClaudeAdapter {
|
|
|
803
896
|
// The SDK can acknowledge initialization while its CLI is still waiting
|
|
804
897
|
// for first input. Sending getContextUsage then deadlocks the control
|
|
805
898
|
// queue, including a later local command. Establish identity first.
|
|
806
|
-
if (!this.
|
|
899
|
+
if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function")
|
|
807
900
|
await this.prepareSession();
|
|
808
901
|
await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
|
|
809
902
|
const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
|
|
@@ -825,10 +918,10 @@ export class ClaudeAdapter {
|
|
|
825
918
|
|| !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
|
|
826
919
|
throw new Error("Claude did not expose an enabled native automatic-compaction threshold after configuration");
|
|
827
920
|
}
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
921
|
+
// A control acknowledgment may disagree with the SDK's subsequent report.
|
|
922
|
+
// Use that reported threshold as the available lifecycle telemetry rather
|
|
923
|
+
// than rejecting the build. This does not prove clamping or independently
|
|
924
|
+
// verify the native trigger's actual enforcement.
|
|
832
925
|
const effectiveThresholdPercent = Math.max(1, Math.min(99, Math.floor(installed.autoCompactThreshold / installed.maxTokens * 100)));
|
|
833
926
|
this.preparedAutoCompactionPolicy = {
|
|
834
927
|
requestedThresholdPercent: threshold,
|
|
@@ -866,20 +959,32 @@ export class ClaudeAdapter {
|
|
|
866
959
|
return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
|
|
867
960
|
if (settings.model === this.opts.model)
|
|
868
961
|
return { ok: true };
|
|
962
|
+
if (this.opts.fast)
|
|
963
|
+
await this.prepareFastMode();
|
|
964
|
+
if (this.fastModeEnabled)
|
|
965
|
+
await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
|
|
966
|
+
this.fastModeEnabled = false;
|
|
869
967
|
const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
|
|
870
968
|
if (result.isError)
|
|
871
969
|
return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
|
|
872
970
|
this.opts.model = settings.model;
|
|
971
|
+
this.fastModePrepared = undefined;
|
|
972
|
+
this.fastModeUnavailable = false;
|
|
973
|
+
if (this.opts.fast)
|
|
974
|
+
await this.prepareFastMode();
|
|
873
975
|
return { ok: true };
|
|
874
976
|
}
|
|
875
977
|
observeEvents(listener) { return this.eventQueue.observe(listener); }
|
|
876
978
|
events() {
|
|
877
979
|
return this.eventQueue;
|
|
878
980
|
}
|
|
879
|
-
|
|
981
|
+
close() { return this.closePromise ??= this.closeInternal(); }
|
|
982
|
+
async closeInternal() {
|
|
880
983
|
if (this.closed)
|
|
881
984
|
return;
|
|
882
985
|
this.closed = true;
|
|
986
|
+
if (this.pending)
|
|
987
|
+
this.settlePending(this.streamFailureResult("Claude turn interrupted; submitted work requires recovery", false));
|
|
883
988
|
this.questionTrace?.finish("closed");
|
|
884
989
|
this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
|
|
885
990
|
this.abort.abort();
|
|
@@ -896,7 +1001,7 @@ export class ClaudeAdapter {
|
|
|
896
1001
|
failSessionIdentity(error) {
|
|
897
1002
|
this.rejectSessionIdentity(error);
|
|
898
1003
|
const result = sessionUnavailableResult(new SessionUnavailableError({
|
|
899
|
-
runtime: "claude", phase: this.pending ? "turn" : "preflight", dispatchState: this.pending ? "unknown" : "not-sent",
|
|
1004
|
+
runtime: "claude", phase: this.pending ? "turn" : "preflight", dispatchState: this.pending?.submitted ? "unknown" : "not-sent",
|
|
900
1005
|
executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: error.message,
|
|
901
1006
|
availability: { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "attach-failed", detail: error.message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}) },
|
|
902
1007
|
}));
|
|
@@ -912,11 +1017,26 @@ export class ClaudeAdapter {
|
|
|
912
1017
|
this.failSessionIdentity(error);
|
|
913
1018
|
throw error;
|
|
914
1019
|
}
|
|
915
|
-
|
|
1020
|
+
const launchCwd = canonicalSessionPath(this.opts.cwd);
|
|
1021
|
+
const currentCwd = observedCwd === undefined ? undefined : canonicalSessionPath(observedCwd);
|
|
1022
|
+
const withinLaunchScope = currentCwd === undefined ? false : (() => {
|
|
1023
|
+
const path = relative(launchCwd, currentCwd);
|
|
1024
|
+
return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`));
|
|
1025
|
+
})();
|
|
1026
|
+
// Establish the launch scope before trusting mutable Bash cwd. Subsequent
|
|
1027
|
+
// messages for the same identity may report a directory inside that scope;
|
|
1028
|
+
// they must not replace the immutable cwd in the durable session reference.
|
|
1029
|
+
const trustedResumeScope = Boolean(this.opts.resumeSessionRef?.validatedAt && canonicalSessionPath(this.opts.resumeSessionRef.cwd) === launchCwd);
|
|
1030
|
+
if (currentCwd !== undefined && (this.launchScopeObserved || trustedResumeScope ? !withinLaunchScope : currentCwd !== launchCwd)) {
|
|
916
1031
|
const error = new Error(`Claude session cwd ${observedCwd} does not match ${this.opts.cwd}`);
|
|
917
1032
|
this.failSessionIdentity(error);
|
|
918
1033
|
throw error;
|
|
919
1034
|
}
|
|
1035
|
+
if (currentCwd === launchCwd)
|
|
1036
|
+
this.launchScopeObserved = true;
|
|
1037
|
+
this.providerIdentityObserved = true;
|
|
1038
|
+
if (currentCwd !== undefined)
|
|
1039
|
+
this.toolCwd = currentCwd;
|
|
920
1040
|
this._sessionId = sessionId;
|
|
921
1041
|
if (this._sessionRef?.validatedAt) {
|
|
922
1042
|
this.resolveSessionIdentity(this._sessionRef);
|
|
@@ -1021,19 +1141,20 @@ export class ClaudeAdapter {
|
|
|
1021
1141
|
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
|
|
1022
1142
|
}
|
|
1023
1143
|
streamFailureResult(message, sessionUnavailable) {
|
|
1144
|
+
const dispatchState = this.pending?.submitted === false ? "not-sent" : "unknown";
|
|
1024
1145
|
if (sessionUnavailable) {
|
|
1025
1146
|
const availability = {
|
|
1026
1147
|
version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "not-found",
|
|
1027
1148
|
detail: message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}),
|
|
1028
1149
|
};
|
|
1029
1150
|
return sessionUnavailableResult(new SessionUnavailableError({
|
|
1030
|
-
runtime: "claude", phase: "turn", dispatchState
|
|
1151
|
+
runtime: "claude", phase: "turn", dispatchState, executable: this.opts.runtimeExecutable ?? "claude",
|
|
1031
1152
|
cwd: this.opts.cwd, diagnostics: message, availability,
|
|
1032
1153
|
}));
|
|
1033
1154
|
}
|
|
1034
1155
|
return {
|
|
1035
1156
|
text: message, isError: true, numTurns: 0, costUsd: 0, costAuthoritative: false,
|
|
1036
|
-
failure: { runtime: "claude", phase: this.opts.runtimePhase ?? "builder", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: message, dispatchState
|
|
1157
|
+
failure: { runtime: "claude", phase: this.opts.runtimePhase ?? "builder", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: message, dispatchState },
|
|
1037
1158
|
};
|
|
1038
1159
|
}
|
|
1039
1160
|
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
28
28
|
private sessionAvailability?;
|
|
29
29
|
private initialized;
|
|
30
30
|
private closed;
|
|
31
|
+
private closePromise?;
|
|
31
32
|
private activeText;
|
|
32
33
|
private usage?;
|
|
33
34
|
private usageRevision;
|
|
@@ -53,6 +54,13 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
53
54
|
private readonly answeredApprovalRequests;
|
|
54
55
|
private activeProviderTurnSpanId?;
|
|
55
56
|
private readonly toolSpans;
|
|
57
|
+
private effectiveModel?;
|
|
58
|
+
private fastPreparedModel?;
|
|
59
|
+
private fastTier?;
|
|
60
|
+
private readonly fastUnavailableModels;
|
|
61
|
+
private turnSawTool;
|
|
62
|
+
private disableFastMode;
|
|
63
|
+
private prepareFastMode;
|
|
56
64
|
constructor(opts: BuilderAdapterOptions);
|
|
57
65
|
contractCapabilities(): {
|
|
58
66
|
sameSessionAcceptance: boolean;
|
|
@@ -89,6 +97,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
89
97
|
observeEvents(listener: (event: BuilderEvent) => void): () => void;
|
|
90
98
|
events(): AsyncIterable<BuilderEvent>;
|
|
91
99
|
close(): Promise<void>;
|
|
100
|
+
private closeInternal;
|
|
92
101
|
private ensureThread;
|
|
93
102
|
private ensureThreadInternal;
|
|
94
103
|
private threadAttached;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -9,6 +9,8 @@ import { BuilderEventQueue, withActivityPhase } from "../activity.js";
|
|
|
9
9
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
10
10
|
import { canonicalSessionPath, createProviderSessionRef, validateProviderSessionScope } from "../sessionIdentity.js";
|
|
11
11
|
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
12
|
+
import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
|
|
13
|
+
import { handoffSystemInstructions } from "../handoffProtocol.js";
|
|
12
14
|
/** Compatibility parser for recorded pre-app-server JSONL fixtures. */
|
|
13
15
|
export function parseCodexLine(raw) {
|
|
14
16
|
const type = raw.type;
|
|
@@ -42,6 +44,7 @@ export class CodexAdapter {
|
|
|
42
44
|
sessionAvailability;
|
|
43
45
|
initialized = false;
|
|
44
46
|
closed = false;
|
|
47
|
+
closePromise;
|
|
45
48
|
activeText = [];
|
|
46
49
|
usage;
|
|
47
50
|
usageRevision = 0;
|
|
@@ -67,6 +70,47 @@ export class CodexAdapter {
|
|
|
67
70
|
answeredApprovalRequests = new Set();
|
|
68
71
|
activeProviderTurnSpanId;
|
|
69
72
|
toolSpans = new Map();
|
|
73
|
+
effectiveModel;
|
|
74
|
+
fastPreparedModel;
|
|
75
|
+
fastTier;
|
|
76
|
+
fastUnavailableModels = new Set();
|
|
77
|
+
turnSawTool = false;
|
|
78
|
+
disableFastMode() {
|
|
79
|
+
const model = this.opts.model ?? this.effectiveModel ?? "default";
|
|
80
|
+
if (!this.fastUnavailableModels.has(model))
|
|
81
|
+
reportFastModeUnavailable(model);
|
|
82
|
+
this.fastUnavailableModels.add(model);
|
|
83
|
+
this.fastTier = undefined;
|
|
84
|
+
}
|
|
85
|
+
async prepareFastMode() {
|
|
86
|
+
const model = this.opts.model ?? this.effectiveModel ?? "default";
|
|
87
|
+
if (!this.opts.fast || this.fastPreparedModel === model)
|
|
88
|
+
return;
|
|
89
|
+
this.fastPreparedModel = model;
|
|
90
|
+
this.fastTier = undefined;
|
|
91
|
+
if (this.fastUnavailableModels.has(model))
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
let cursor;
|
|
95
|
+
for (let page = 0; page < 20; page++) {
|
|
96
|
+
const result = await this.request("model/list", { includeHidden: true, ...(cursor ? { cursor } : {}) });
|
|
97
|
+
const entry = result.data?.find(item => item.model === model || item.id === model);
|
|
98
|
+
if (entry) {
|
|
99
|
+
this.fastTier = entry.serviceTiers?.find(tier => tier.name?.toLowerCase() === "fast" || ["fast", "priority"].includes(tier.id))?.id;
|
|
100
|
+
// Older app-server versions publish only the legacy speed list.
|
|
101
|
+
if (entry.serviceTiers === undefined && entry.additionalSpeedTiers?.includes("fast"))
|
|
102
|
+
this.fastTier = "fast";
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (!result.nextCursor || result.nextCursor === cursor)
|
|
106
|
+
break;
|
|
107
|
+
cursor = result.nextCursor;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch { /* Optional capability discovery never blocks a standard turn. */ }
|
|
111
|
+
if (!this.fastTier)
|
|
112
|
+
this.disableFastMode();
|
|
113
|
+
}
|
|
70
114
|
constructor(opts) {
|
|
71
115
|
this.opts = opts;
|
|
72
116
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
@@ -93,8 +137,6 @@ export class CodexAdapter {
|
|
|
93
137
|
args.push("-m", this.opts.model);
|
|
94
138
|
if (this.opts.effort)
|
|
95
139
|
args.push("-c", `model_reasoning_effort=${this.opts.effort}`);
|
|
96
|
-
else if (this.opts.fast)
|
|
97
|
-
args.push("-c", "model_reasoning_effort=low");
|
|
98
140
|
if (this._sessionId)
|
|
99
141
|
args.push("resume", this._sessionId);
|
|
100
142
|
args.push(instruction);
|
|
@@ -144,23 +186,38 @@ export class CodexAdapter {
|
|
|
144
186
|
const providerInstruction = this.buildInstruction(instruction);
|
|
145
187
|
try {
|
|
146
188
|
await this.ensureThread();
|
|
189
|
+
if (this.opts.fast)
|
|
190
|
+
await this.prepareFastMode();
|
|
147
191
|
const toolsBefore = this.observedToolCalls;
|
|
192
|
+
this.turnSawTool = false;
|
|
148
193
|
this.activeText = [];
|
|
149
194
|
this.lastTurnTokens = undefined;
|
|
150
195
|
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
151
196
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
197
|
+
void completion.catch(() => { });
|
|
152
198
|
turnStartDispatched = true;
|
|
153
|
-
const
|
|
199
|
+
const startParams = {
|
|
154
200
|
threadId: this._sessionId,
|
|
155
201
|
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
156
202
|
cwd: this.opts.cwd,
|
|
157
|
-
model: this.opts.model ?? null,
|
|
158
|
-
effort: this.opts.effort ??
|
|
203
|
+
model: this.opts.model ?? this.effectiveModel ?? null,
|
|
204
|
+
effort: this.opts.effort ?? null,
|
|
205
|
+
...(this.opts.fast ? { serviceTier: this.fastTier ?? "default" } : {}),
|
|
159
206
|
approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
|
|
160
207
|
sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
|
|
161
208
|
? { type: "readOnly", networkAccess: false }
|
|
162
209
|
: { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
|
|
163
|
-
}
|
|
210
|
+
};
|
|
211
|
+
let started;
|
|
212
|
+
try {
|
|
213
|
+
started = await this.request("turn/start", startParams);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (!this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
|
|
217
|
+
throw error;
|
|
218
|
+
this.disableFastMode();
|
|
219
|
+
started = await this.request("turn/start", { ...startParams, serviceTier: "default" });
|
|
220
|
+
}
|
|
164
221
|
this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
|
|
165
222
|
const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
|
|
166
223
|
const turn = params.turn;
|
|
@@ -168,6 +225,10 @@ export class CodexAdapter {
|
|
|
168
225
|
const failed = turn?.status === "failed" || responseOnlyViolation;
|
|
169
226
|
const error = turn?.error;
|
|
170
227
|
const text = this.activeText.join("\n");
|
|
228
|
+
if (failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
|
|
229
|
+
this.disableFastMode();
|
|
230
|
+
return this.sendTurnInternal(instruction, policy);
|
|
231
|
+
}
|
|
171
232
|
const result = {
|
|
172
233
|
text: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
173
234
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
@@ -381,6 +442,8 @@ export class CodexAdapter {
|
|
|
381
442
|
}
|
|
382
443
|
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
383
444
|
async switchSettings(settings) {
|
|
445
|
+
if (settings.model !== this.opts.model || settings.fast !== this.opts.fast)
|
|
446
|
+
this.fastPreparedModel = undefined;
|
|
384
447
|
this.opts.model = settings.model;
|
|
385
448
|
this.opts.effort = settings.effort;
|
|
386
449
|
this.opts.fast = settings.fast;
|
|
@@ -417,7 +480,8 @@ export class CodexAdapter {
|
|
|
417
480
|
}
|
|
418
481
|
observeEvents(listener) { return this.eventQueue.observe(listener); }
|
|
419
482
|
events() { return this.eventQueue; }
|
|
420
|
-
|
|
483
|
+
close() { return this.closePromise ??= this.closeInternal(); }
|
|
484
|
+
async closeInternal() {
|
|
421
485
|
if (this.closed)
|
|
422
486
|
return;
|
|
423
487
|
this.closed = true;
|
|
@@ -475,7 +539,7 @@ export class CodexAdapter {
|
|
|
475
539
|
model: this.opts.model ?? null, approvalPolicy: this.effectiveApprovalPolicy(),
|
|
476
540
|
config: { sandbox_workspace_write: { network_access: this.effectiveNetworkAccess() } },
|
|
477
541
|
sandbox: this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "read-only" : "workspace-write",
|
|
478
|
-
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
542
|
+
developerInstructions: handoffSystemInstructions(this.opts.sessionRole, this.opts.systemPromptAppend) ?? null,
|
|
479
543
|
});
|
|
480
544
|
}
|
|
481
545
|
catch (error) {
|
|
@@ -546,6 +610,7 @@ export class CodexAdapter {
|
|
|
546
610
|
}
|
|
547
611
|
this.sessionAvailability = { version: 1, status: "available", checkedAt: new Date().toISOString(), observedCwd: providerCwd ?? canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
548
612
|
this.threadAttached = true;
|
|
613
|
+
this.effectiveModel = typeof result.model === "string" ? result.model : this.opts.model;
|
|
549
614
|
this.eventQueue.push({ kind: "session-transition", transition: method === "thread/resume" ? "resumed" : "started" });
|
|
550
615
|
}
|
|
551
616
|
threadAttached = false;
|
|
@@ -614,6 +679,7 @@ export class CodexAdapter {
|
|
|
614
679
|
if (item)
|
|
615
680
|
this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "started", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
616
681
|
if (isCodexToolItem(item)) {
|
|
682
|
+
this.turnSawTool = true;
|
|
617
683
|
this.observedToolCalls += 1;
|
|
618
684
|
const callId = codexItemId(item) ?? randomUUID();
|
|
619
685
|
const name = codexToolName(item);
|
|
@@ -633,6 +699,7 @@ export class CodexAdapter {
|
|
|
633
699
|
if (item)
|
|
634
700
|
this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "completed", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
635
701
|
if (isCodexToolItem(item)) {
|
|
702
|
+
this.turnSawTool = true;
|
|
636
703
|
const callId = codexItemId(item);
|
|
637
704
|
const spanId = callId ? this.toolSpans.get(callId) : undefined;
|
|
638
705
|
const status = typeof item?.status === "string" ? item.status : undefined;
|
|
@@ -710,6 +777,8 @@ export class CodexAdapter {
|
|
|
710
777
|
else if (message.method === "error") {
|
|
711
778
|
const error = params.error;
|
|
712
779
|
const reason = String(error?.message ?? params.message ?? "Codex app-server error");
|
|
780
|
+
if (this.fastTier && isFastModeUnavailable(reason) && !this.activeText.length && !this.turnSawTool)
|
|
781
|
+
return;
|
|
713
782
|
if (params.willRetry === true) {
|
|
714
783
|
const retryId = randomUUID();
|
|
715
784
|
this.eventQueue.push(this.opts.observer
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Only explicit speed-option rejection permits a same-session retry. Auth,
|
|
2
|
+
* generic model failures, timeouts, and uncertain transport failures do not. */
|
|
3
|
+
export declare function isFastModeUnavailable(detail: string): boolean;
|
|
4
|
+
export declare function reportFastModeUnavailable(model?: string): void;
|