@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.9
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/CHANGELOG.md +51 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-8yy8h3bc.md} +51 -1
- package/dist/cli.js +4443 -4444
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/task/executor.d.ts +4 -0
- package/dist/types/tools/browser/launch.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +2 -0
- package/package.json +13 -13
- package/scripts/omp +11 -2
- package/src/async/job-manager.ts +26 -0
- package/src/cli/stats-cli.ts +3 -3
- package/src/cli/update-cli.ts +2 -56
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents.ts +71 -3
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/hindsight/state.ts +2 -22
- package/src/internal-urls/memory-protocol.ts +29 -0
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/modes/acp/acp-agent.ts +2 -1
- package/src/modes/components/agent-hub.ts +36 -20
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/controllers/event-controller.ts +159 -2
- package/src/modes/controllers/extension-ui-controller.ts +2 -1
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/registry/agent-lifecycle.ts +21 -3
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +156 -186
- package/src/session/session-paths.ts +68 -121
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +234 -16
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +105 -29
- package/src/task/isolation-runner.ts +19 -1
- package/src/task/structured-subagent.ts +17 -2
- package/src/tiny/worker.ts +1 -1
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/browser/launch.ts +37 -9
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +13 -3
- package/src/tools/read.ts +6 -0
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/utils/changelog.ts +5 -4
- package/src/utils/late-cleanup.ts +17 -0
- package/src/vibe/runtime.ts +197 -53
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/mojeek.ts +1 -1
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +1 -1
|
@@ -58,7 +58,6 @@ import type {
|
|
|
58
58
|
ImageContent,
|
|
59
59
|
Message,
|
|
60
60
|
Model,
|
|
61
|
-
ModelUsageHealth,
|
|
62
61
|
OAuthAccountIdentity,
|
|
63
62
|
ProviderSessionState,
|
|
64
63
|
ResetCreditAccountStatus,
|
|
@@ -101,7 +100,7 @@ import { type AdvisorConfig, type AdvisorRuntimeStatus, loadAdvisorTranscriptCos
|
|
|
101
100
|
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
102
101
|
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
103
102
|
import type { ModelRegistry } from "../config/model-registry";
|
|
104
|
-
import {
|
|
103
|
+
import type { ResolvedModelRoleValue } from "../config/model-resolver";
|
|
105
104
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
106
105
|
import { buildServiceTierByFamily } from "../config/service-tier";
|
|
107
106
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
@@ -230,7 +229,7 @@ import type {
|
|
|
230
229
|
SessionHandoffOptions,
|
|
231
230
|
SessionOAuthAccountList,
|
|
232
231
|
SessionStats,
|
|
233
|
-
|
|
232
|
+
UsageFallbackConfirmer,
|
|
234
233
|
} from "./agent-session-types";
|
|
235
234
|
import {
|
|
236
235
|
ASYNC_INLINE_RESULT_MAX_CHARS,
|
|
@@ -312,7 +311,6 @@ import {
|
|
|
312
311
|
queueChipText,
|
|
313
312
|
toRestoredQueuedMessage,
|
|
314
313
|
} from "./queued-messages";
|
|
315
|
-
import { formatRetryFallbackSelector, type RetryFallbackSelector } from "./retry-fallback-chains";
|
|
316
314
|
import { type AdvisorStats, SessionAdvisors, type SessionAdvisorsHost } from "./session-advisors";
|
|
317
315
|
import type { BuildSessionContextOptions, SessionContext } from "./session-context";
|
|
318
316
|
import { getRestorableSessionModels } from "./session-context";
|
|
@@ -485,6 +483,7 @@ export class AgentSession {
|
|
|
485
483
|
|
|
486
484
|
// Retry state
|
|
487
485
|
readonly #recovery: TurnRecovery;
|
|
486
|
+
#textOutputCommitted = true;
|
|
488
487
|
#planModeReminderCount = 0;
|
|
489
488
|
#planModeReminderAwaitingProgress = false;
|
|
490
489
|
readonly #todo: TodoTracker;
|
|
@@ -558,9 +557,13 @@ export class AgentSession {
|
|
|
558
557
|
|
|
559
558
|
// Model registry for API key resolution
|
|
560
559
|
#modelRegistry: ModelRegistry;
|
|
561
|
-
#usageFallbackConfirmer:
|
|
562
|
-
#usageReserveApprovedSelector: string | undefined;
|
|
560
|
+
#usageFallbackConfirmer: UsageFallbackConfirmer | undefined;
|
|
563
561
|
#usagePreflightAbortControllers = new Set<AbortController>();
|
|
562
|
+
#queuedMessageDrainBlocked = false;
|
|
563
|
+
#usagePreflightReadyForNextModelCall = false;
|
|
564
|
+
#usagePreflightReadyModel: Model | undefined;
|
|
565
|
+
#detachUsageBeforeQueueDequeue: (() => void) | undefined;
|
|
566
|
+
#detachUsageBeforeModelCall: (() => void) | undefined;
|
|
564
567
|
|
|
565
568
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
566
569
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
@@ -768,8 +771,10 @@ export class AgentSession {
|
|
|
768
771
|
!this.#canAutoContinueForFollowUp()
|
|
769
772
|
? [...this.agent.peekFollowUpQueue()]
|
|
770
773
|
: [];
|
|
774
|
+
const parkedQueueDrainBlocked = parkedFollowUps.length > 0 && this.#queuedMessageDrainBlocked;
|
|
771
775
|
if (parkedFollowUps.length > 0) {
|
|
772
776
|
this.agent.replaceQueues([...this.agent.peekSteeringQueue()], []);
|
|
777
|
+
if (parkedQueueDrainBlocked) this.#queuedMessageDrainBlocked = false;
|
|
773
778
|
}
|
|
774
779
|
let finishObservation: ((error?: unknown) => void | Promise<void>) | undefined;
|
|
775
780
|
try {
|
|
@@ -805,6 +810,7 @@ export class AgentSession {
|
|
|
805
810
|
[...this.agent.peekSteeringQueue()],
|
|
806
811
|
[...parkedFollowUps, ...this.agent.peekFollowUpQueue()],
|
|
807
812
|
);
|
|
813
|
+
this.#queuedMessageDrainBlocked ||= parkedQueueDrainBlocked;
|
|
808
814
|
}
|
|
809
815
|
this.#endInFlight(async () => {
|
|
810
816
|
try {
|
|
@@ -830,6 +836,7 @@ export class AgentSession {
|
|
|
830
836
|
steering.filter(m => !isAdvisorCard(m)),
|
|
831
837
|
followUp.filter(m => !isAdvisorCard(m)),
|
|
832
838
|
);
|
|
839
|
+
this.#reconcileQueuedMessageDrain();
|
|
833
840
|
return cards;
|
|
834
841
|
}
|
|
835
842
|
|
|
@@ -1025,6 +1032,7 @@ export class AgentSession {
|
|
|
1025
1032
|
modelRegistry: this.#modelRegistry,
|
|
1026
1033
|
configWarnings: this.configWarnings,
|
|
1027
1034
|
model: () => this.model,
|
|
1035
|
+
textOutputCommitted: () => this.#textOutputCommitted,
|
|
1028
1036
|
thinkingLevel: () => this.thinkingLevel,
|
|
1029
1037
|
configuredThinkingLevel: () => this.configuredThinkingLevel(),
|
|
1030
1038
|
setThinkingLevel: level => this.setThinkingLevel(level),
|
|
@@ -1050,6 +1058,31 @@ export class AgentSession {
|
|
|
1050
1058
|
withBashBranchTransition: operation => this.#bash.withBranchTransition(operation),
|
|
1051
1059
|
};
|
|
1052
1060
|
this.#recovery = new TurnRecovery(recoveryHost, { initialRetryFallback: config.initialRetryFallback });
|
|
1061
|
+
this.#detachUsageBeforeQueueDequeue = this.agent.addBeforeQueuedMessageDequeueHook(async signal => {
|
|
1062
|
+
if (
|
|
1063
|
+
!this.settings.get("retry.usageAwareFallback") ||
|
|
1064
|
+
(this.#usagePreflightReadyForNextModelCall && this.#usagePreflightReadyModel === this.model)
|
|
1065
|
+
) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (!(await this.#runQueuedUsageAwarePreflight(signal))) {
|
|
1069
|
+
signal?.throwIfAborted();
|
|
1070
|
+
throw new DOMException("Usage preflight cancelled", "AbortError");
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
this.#detachUsageBeforeModelCall = this.agent.addBeforeModelCallHook(async signal => {
|
|
1074
|
+
if (!this.settings.get("retry.usageAwareFallback")) return;
|
|
1075
|
+
if (this.#usagePreflightReadyForNextModelCall) {
|
|
1076
|
+
const checkedModel = this.#usagePreflightReadyModel;
|
|
1077
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
1078
|
+
this.#usagePreflightReadyModel = undefined;
|
|
1079
|
+
if (checkedModel === this.model) return;
|
|
1080
|
+
}
|
|
1081
|
+
if (!(await this.#runUsageAwarePreflight(signal))) {
|
|
1082
|
+
signal?.throwIfAborted();
|
|
1083
|
+
throw new DOMException("Usage preflight cancelled", "AbortError");
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1053
1086
|
const statsHost: SessionStatsTrackerHost = {
|
|
1054
1087
|
session: this,
|
|
1055
1088
|
agent: this.agent,
|
|
@@ -2658,14 +2691,21 @@ export class AgentSession {
|
|
|
2658
2691
|
// outside the session transcript (issue #6177).
|
|
2659
2692
|
logProviderTurnError(msg);
|
|
2660
2693
|
|
|
2661
|
-
// Invalidate GitHub Copilot credentials on auth failure
|
|
2662
|
-
// aren't reused on the next request
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2694
|
+
// Invalidate GitHub Copilot credentials on a hard auth failure (401, or an
|
|
2695
|
+
// expired/revoked token) so stale tokens aren't reused on the next request.
|
|
2696
|
+
// Account usage caps and concurrency caps leave the credential valid: the
|
|
2697
|
+
// former rotates until its reset window, while the latter is retried after
|
|
2698
|
+
// a short backoff without touching the credential pool.
|
|
2699
|
+
if (msg.stopReason === "error" && msg.provider === "github-copilot") {
|
|
2700
|
+
const errorId = AIError.classifyMessage(msg);
|
|
2701
|
+
const isConcurrencyCap = AIError.parseRateLimitReason(msg.errorMessage ?? "") === "CONCURRENT_LIMIT";
|
|
2702
|
+
if (
|
|
2703
|
+
AIError.is(errorId, AIError.Flag.AuthFailed) &&
|
|
2704
|
+
!AIError.is(errorId, AIError.Flag.UsageLimit) &&
|
|
2705
|
+
!isConcurrencyCap
|
|
2706
|
+
) {
|
|
2707
|
+
await this.#modelRegistry.authStorage.remove("github-copilot");
|
|
2708
|
+
}
|
|
2669
2709
|
}
|
|
2670
2710
|
|
|
2671
2711
|
if (this.#maintenance.skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
@@ -2967,19 +3007,17 @@ export class AgentSession {
|
|
|
2967
3007
|
this.#beginInFlight();
|
|
2968
3008
|
try {
|
|
2969
3009
|
await this.#recovery.maybeRestoreRetryFallbackPrimary();
|
|
2970
|
-
if (
|
|
2971
|
-
this.settings.get("retry.modelFallback") &&
|
|
2972
|
-
this.settings.get("retry.usageAwareFallback") &&
|
|
2973
|
-
!(await this.#runUsageAwarePreflight())
|
|
2974
|
-
) {
|
|
2975
|
-
this.#skipAgentContinue("session-unavailable", options);
|
|
2976
|
-
return;
|
|
2977
|
-
}
|
|
2978
3010
|
if (signal.aborted || this.#isDisposed) {
|
|
2979
3011
|
this.#skipAgentContinue("post-restore-unavailable", options);
|
|
2980
3012
|
return;
|
|
2981
3013
|
}
|
|
2982
|
-
|
|
3014
|
+
if (this.settings.get("retry.usageAwareFallback")) {
|
|
3015
|
+
if (!(await this.#runQueuedUsageAwarePreflight(signal))) {
|
|
3016
|
+
this.#skipAgentContinue("session-unavailable", options);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
await this.agent.continue(signal);
|
|
2983
3021
|
} catch (error) {
|
|
2984
3022
|
logger.warn("agent.continue failed after scheduling", {
|
|
2985
3023
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -2987,6 +3025,7 @@ export class AgentSession {
|
|
|
2987
3025
|
});
|
|
2988
3026
|
options?.onError?.(error);
|
|
2989
3027
|
} finally {
|
|
3028
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
2990
3029
|
this.#endInFlight();
|
|
2991
3030
|
}
|
|
2992
3031
|
},
|
|
@@ -3605,6 +3644,12 @@ export class AgentSession {
|
|
|
3605
3644
|
*/
|
|
3606
3645
|
beginDispose(): void {
|
|
3607
3646
|
this.#isDisposed = true;
|
|
3647
|
+
this.#queuedMessageDrainBlocked = false;
|
|
3648
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
3649
|
+
this.#detachUsageBeforeQueueDequeue?.();
|
|
3650
|
+
this.#detachUsageBeforeQueueDequeue = undefined;
|
|
3651
|
+
this.#detachUsageBeforeModelCall?.();
|
|
3652
|
+
this.#detachUsageBeforeModelCall = undefined;
|
|
3608
3653
|
this.#memory.cancelLocalMemoryStartup();
|
|
3609
3654
|
this.#titleGenerationAbortController.abort();
|
|
3610
3655
|
this.#abortAutolearnCapture();
|
|
@@ -3926,174 +3971,69 @@ export class AgentSession {
|
|
|
3926
3971
|
}
|
|
3927
3972
|
|
|
3928
3973
|
/** Install the interactive decision surface for reserve-triggered model changes. */
|
|
3929
|
-
setUsageFallbackConfirmer(
|
|
3930
|
-
confirmer: ((confirmation: UsageFallbackConfirmation) => Promise<boolean>) | undefined,
|
|
3931
|
-
): void {
|
|
3974
|
+
setUsageFallbackConfirmer(confirmer: UsageFallbackConfirmer | undefined): void {
|
|
3932
3975
|
this.#usageFallbackConfirmer = confirmer;
|
|
3933
3976
|
}
|
|
3934
3977
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
const controller = new AbortController();
|
|
3938
|
-
this.#usagePreflightAbortControllers.add(controller);
|
|
3939
|
-
try {
|
|
3940
|
-
await this.#maybeApplyUsageAwareFallback(controller.signal);
|
|
3941
|
-
return !controller.signal.aborted && this.#promptGeneration === generation;
|
|
3942
|
-
} catch (error) {
|
|
3943
|
-
if (controller.signal.aborted || this.#promptGeneration !== generation) return false;
|
|
3944
|
-
throw error;
|
|
3945
|
-
} finally {
|
|
3946
|
-
this.#usagePreflightAbortControllers.delete(controller);
|
|
3947
|
-
}
|
|
3978
|
+
#allowQueuedMessageDrainRetry(): void {
|
|
3979
|
+
this.#queuedMessageDrainBlocked = false;
|
|
3948
3980
|
}
|
|
3949
3981
|
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
const aborted = Promise.withResolvers<boolean>();
|
|
3954
|
-
const onAbort = () => aborted.resolve(false);
|
|
3955
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
3956
|
-
try {
|
|
3957
|
-
return await Promise.race([confirmer(confirmation), aborted.promise]);
|
|
3958
|
-
} finally {
|
|
3959
|
-
signal.removeEventListener("abort", onAbort);
|
|
3982
|
+
#reconcileQueuedMessageDrain(): void {
|
|
3983
|
+
if (!this.agent.hasQueuedMessages()) {
|
|
3984
|
+
this.#queuedMessageDrainBlocked = false;
|
|
3960
3985
|
}
|
|
3961
3986
|
}
|
|
3962
3987
|
|
|
3963
|
-
async #
|
|
3964
|
-
if (!this.settings.get("retry.modelFallback") || !this.settings.get("retry.usageAwareFallback")) return;
|
|
3965
|
-
const currentModel = this.model;
|
|
3966
|
-
if (!currentModel) return;
|
|
3967
|
-
const currentSelector = formatRetryFallbackSelector(currentModel, this.thinkingLevel);
|
|
3968
|
-
let health: ModelUsageHealth;
|
|
3988
|
+
async #runQueuedUsageAwarePreflight(signal?: AbortSignal): Promise<boolean> {
|
|
3969
3989
|
try {
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
signal,
|
|
3976
|
-
});
|
|
3990
|
+
const allowed = await this.#runUsageAwarePreflight(signal);
|
|
3991
|
+
this.#usagePreflightReadyForNextModelCall = allowed;
|
|
3992
|
+
this.#usagePreflightReadyModel = allowed ? this.model : undefined;
|
|
3993
|
+
this.#queuedMessageDrainBlocked = !allowed && this.agent.hasQueuedMessages();
|
|
3994
|
+
return allowed;
|
|
3977
3995
|
} catch (error) {
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
model: currentModel.id,
|
|
3981
|
-
error: String(error),
|
|
3982
|
-
});
|
|
3983
|
-
return;
|
|
3996
|
+
this.#queuedMessageDrainBlocked = this.agent.hasQueuedMessages();
|
|
3997
|
+
throw error;
|
|
3984
3998
|
}
|
|
3985
|
-
|
|
3999
|
+
}
|
|
3986
4000
|
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
if (!role) return;
|
|
4010
|
-
let fallback: { selector: RetryFallbackSelector; apiKey: string } | undefined;
|
|
4011
|
-
for (const candidate of this.#recovery.findRetryFallbackCandidates(role, currentSelector, currentModel)) {
|
|
4012
|
-
if (this.#recovery.isRetryFallbackSelectorSuppressed(candidate)) continue;
|
|
4013
|
-
const resolved = resolveModelOverride([candidate.raw], this.#modelRegistry, this.settings);
|
|
4014
|
-
const candidateModel = resolved.model ?? this.#modelRegistry.find(candidate.provider, candidate.id);
|
|
4015
|
-
if (!candidateModel) continue;
|
|
4016
|
-
if (!this.#modelRegistry.hasConfiguredAuth(candidateModel)) continue;
|
|
4017
|
-
try {
|
|
4018
|
-
const candidateHealth = await this.#modelRegistry.authStorage.getModelUsageHealth(candidateModel.provider, {
|
|
4019
|
-
modelId: candidateModel.id,
|
|
4020
|
-
sessionId: this.sessionId,
|
|
4021
|
-
baseUrl: candidateModel.baseUrl,
|
|
4022
|
-
reserveFraction: this.settings.get("retry.usageReservePct") / 100,
|
|
4023
|
-
signal,
|
|
4024
|
-
});
|
|
4025
|
-
if (signal.aborted) return;
|
|
4026
|
-
if (candidateHealth.state === "depleted" || candidateHealth.state === "reserve") continue;
|
|
4027
|
-
if (candidateHealth.state === "healthy") {
|
|
4028
|
-
const selected = candidateHealth.accounts.find(account => account.selected);
|
|
4029
|
-
if (
|
|
4030
|
-
selected &&
|
|
4031
|
-
selected.state !== "healthy" &&
|
|
4032
|
-
candidateHealth.accounts.some(account => account.state === "healthy")
|
|
4033
|
-
) {
|
|
4034
|
-
this.#modelRegistry.authStorage.releaseSessionCredentialForReselection(
|
|
4035
|
-
candidateModel.provider,
|
|
4036
|
-
this.sessionId,
|
|
4037
|
-
);
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
} catch {
|
|
4041
|
-
if (signal.aborted) return;
|
|
4042
|
-
// Unknown usage fails open for an otherwise valid fallback.
|
|
4043
|
-
}
|
|
4044
|
-
if (signal.aborted) return;
|
|
4045
|
-
let apiKey: string | undefined;
|
|
4046
|
-
try {
|
|
4047
|
-
apiKey = await this.#modelRegistry.getApiKey(candidateModel, this.sessionId, { signal });
|
|
4048
|
-
} catch {
|
|
4049
|
-
if (signal.aborted) return;
|
|
4050
|
-
continue;
|
|
4051
|
-
}
|
|
4052
|
-
if (signal.aborted) return;
|
|
4053
|
-
if (!apiKey) continue;
|
|
4054
|
-
fallback = { selector: candidate, apiKey };
|
|
4055
|
-
break;
|
|
4056
|
-
}
|
|
4057
|
-
if (!fallback) return;
|
|
4058
|
-
|
|
4059
|
-
if (health.state === "reserve") {
|
|
4060
|
-
if (reservePolicy === "confirm" && this.#usageFallbackConfirmer) {
|
|
4061
|
-
if (this.#usageReserveApprovedSelector === currentSelector) return;
|
|
4062
|
-
const selected = health.accounts.find(account => account.selected);
|
|
4063
|
-
const remainingFraction =
|
|
4064
|
-
selected?.remainingFraction ??
|
|
4065
|
-
health.accounts.reduce<number | undefined>(
|
|
4066
|
-
(minimum, account) =>
|
|
4067
|
-
account.remainingFraction === undefined
|
|
4068
|
-
? minimum
|
|
4069
|
-
: minimum === undefined
|
|
4070
|
-
? account.remainingFraction
|
|
4071
|
-
: Math.min(minimum, account.remainingFraction),
|
|
4072
|
-
undefined,
|
|
4001
|
+
async #runUsageAwarePreflightForNextModelCall(signal?: AbortSignal): Promise<boolean> {
|
|
4002
|
+
const allowed = await this.#runUsageAwarePreflight(signal);
|
|
4003
|
+
this.#usagePreflightReadyForNextModelCall = allowed;
|
|
4004
|
+
this.#usagePreflightReadyModel = allowed ? this.model : undefined;
|
|
4005
|
+
return allowed;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
async #runUsageAwarePreflight(signal?: AbortSignal): Promise<boolean> {
|
|
4009
|
+
if (signal?.aborted) return false;
|
|
4010
|
+
const generation = this.#promptGeneration;
|
|
4011
|
+
|
|
4012
|
+
const controller = new AbortController();
|
|
4013
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
4014
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4015
|
+
this.#usagePreflightAbortControllers.add(controller);
|
|
4016
|
+
try {
|
|
4017
|
+
while (true) {
|
|
4018
|
+
const model = this.model;
|
|
4019
|
+
try {
|
|
4020
|
+
const fallbackCommitted = await this.#recovery.maybeApplyUsageAwareFallback(
|
|
4021
|
+
controller.signal,
|
|
4022
|
+
this.#usageFallbackConfirmer,
|
|
4073
4023
|
);
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
);
|
|
4082
|
-
if (signal.aborted) return;
|
|
4083
|
-
if (!shouldFallback) {
|
|
4084
|
-
this.#usageReserveApprovedSelector = currentSelector;
|
|
4085
|
-
return;
|
|
4024
|
+
if (fallbackCommitted) return true;
|
|
4025
|
+
if (controller.signal.aborted || this.#promptGeneration !== generation) return false;
|
|
4026
|
+
if (this.model === model || modelsAreEqual(this.model, model)) return true;
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (controller.signal.aborted || this.#promptGeneration !== generation) return false;
|
|
4029
|
+
if (this.model !== model && !modelsAreEqual(this.model, model)) continue;
|
|
4030
|
+
throw error;
|
|
4086
4031
|
}
|
|
4087
4032
|
}
|
|
4033
|
+
} finally {
|
|
4034
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4035
|
+
this.#usagePreflightAbortControllers.delete(controller);
|
|
4088
4036
|
}
|
|
4089
|
-
|
|
4090
|
-
if (signal.aborted) return;
|
|
4091
|
-
this.#usageReserveApprovedSelector = undefined;
|
|
4092
|
-
await this.#recovery.applyRetryFallbackCandidate(role, fallback.selector, currentSelector, {
|
|
4093
|
-
pinFallback: true,
|
|
4094
|
-
apiKey: fallback.apiKey,
|
|
4095
|
-
signal,
|
|
4096
|
-
});
|
|
4097
4037
|
}
|
|
4098
4038
|
|
|
4099
4039
|
/** Effective thinking level applied to the agent (the resolved level when `auto`). */
|
|
@@ -4184,6 +4124,11 @@ export class AgentSession {
|
|
|
4184
4124
|
return this.agent.state.systemPrompt;
|
|
4185
4125
|
}
|
|
4186
4126
|
|
|
4127
|
+
/** Marks streamed text as committed or buffered for turn-recovery replay decisions. */
|
|
4128
|
+
setTextOutputCommitted(committed: boolean): void {
|
|
4129
|
+
this.#textOutputCommitted = committed;
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4187
4132
|
/** Current retry attempt (0 if not retrying) */
|
|
4188
4133
|
get retryAttempt(): number {
|
|
4189
4134
|
return this.#recovery.attempt;
|
|
@@ -5026,7 +4971,7 @@ export class AgentSession {
|
|
|
5026
4971
|
if (this.isStreaming) {
|
|
5027
4972
|
const streamingBehavior = options?.streamingBehavior;
|
|
5028
4973
|
if (!streamingBehavior) throw new AgentBusyError();
|
|
5029
|
-
|
|
4974
|
+
|
|
5030
4975
|
// Steer/follow-up the keyword notices BEFORE the queued user message so the
|
|
5031
4976
|
// model reads the steering notice ahead of the prompt it modifies.
|
|
5032
4977
|
for (const notice of keywordNotices) {
|
|
@@ -5121,7 +5066,7 @@ export class AgentSession {
|
|
|
5121
5066
|
if (options?.queueOnly) {
|
|
5122
5067
|
const streamingBehavior = options?.streamingBehavior;
|
|
5123
5068
|
if (!streamingBehavior) throw new AgentBusyError();
|
|
5124
|
-
|
|
5069
|
+
|
|
5125
5070
|
for (const notice of keywordNotices) {
|
|
5126
5071
|
await this.#queueCustomMessage(notice, streamingBehavior);
|
|
5127
5072
|
}
|
|
@@ -5131,7 +5076,7 @@ export class AgentSession {
|
|
|
5131
5076
|
if (this.isStreaming) {
|
|
5132
5077
|
const streamingBehavior = options?.streamingBehavior;
|
|
5133
5078
|
if (!streamingBehavior) throw new AgentBusyError();
|
|
5134
|
-
|
|
5079
|
+
|
|
5135
5080
|
for (const notice of keywordNotices) {
|
|
5136
5081
|
await this.#queueCustomMessage(notice, streamingBehavior);
|
|
5137
5082
|
}
|
|
@@ -5168,7 +5113,7 @@ export class AgentSession {
|
|
|
5168
5113
|
const generation = this.#promptGeneration;
|
|
5169
5114
|
try {
|
|
5170
5115
|
await this.#recovery.maybeRestoreRetryFallbackPrimary();
|
|
5171
|
-
if (!(await this.#
|
|
5116
|
+
if (!(await this.#runUsageAwarePreflightForNextModelCall())) return;
|
|
5172
5117
|
// Flush any pending bash messages before the new prompt
|
|
5173
5118
|
await this.#bash.flushPending();
|
|
5174
5119
|
this.#eval.flushPending();
|
|
@@ -5379,6 +5324,7 @@ export class AgentSession {
|
|
|
5379
5324
|
await this.#waitForPostPromptRecovery(generation);
|
|
5380
5325
|
}
|
|
5381
5326
|
} finally {
|
|
5327
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
5382
5328
|
this.#endInFlight();
|
|
5383
5329
|
}
|
|
5384
5330
|
}
|
|
@@ -5544,7 +5490,6 @@ export class AgentSession {
|
|
|
5544
5490
|
}
|
|
5545
5491
|
|
|
5546
5492
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
5547
|
-
if (!(await this.#runUsageAwarePreflight())) return;
|
|
5548
5493
|
await this.#queueUserMessage(expandedText, images, "steer");
|
|
5549
5494
|
}
|
|
5550
5495
|
|
|
@@ -5562,7 +5507,6 @@ export class AgentSession {
|
|
|
5562
5507
|
|
|
5563
5508
|
const expandedText =
|
|
5564
5509
|
options?.expandPromptTemplates === false ? text : expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
5565
|
-
if (!(await this.#runUsageAwarePreflight())) return;
|
|
5566
5510
|
if (!options?.synthetic) {
|
|
5567
5511
|
await this.#queueUserMessage(expandedText, images, "followUp");
|
|
5568
5512
|
return;
|
|
@@ -5579,6 +5523,7 @@ export class AgentSession {
|
|
|
5579
5523
|
const imageDescriptionNotice = normalizedImages?.length
|
|
5580
5524
|
? await this.#buildImageDescriptionNotice(normalizedImages)
|
|
5581
5525
|
: undefined;
|
|
5526
|
+
this.#allowQueuedMessageDrainRetry();
|
|
5582
5527
|
if (imageDescriptionNotice) this.agent.followUp(imageDescriptionNotice);
|
|
5583
5528
|
this.agent.followUp({
|
|
5584
5529
|
role: "developer",
|
|
@@ -5608,6 +5553,7 @@ export class AgentSession {
|
|
|
5608
5553
|
const imageDescriptionNotice = normalizedImages?.length
|
|
5609
5554
|
? await this.#buildImageDescriptionNotice(normalizedImages)
|
|
5610
5555
|
: undefined;
|
|
5556
|
+
this.#allowQueuedMessageDrainRetry();
|
|
5611
5557
|
if (mode === "followUp") {
|
|
5612
5558
|
if (imageDescriptionNotice) this.agent.followUp(imageDescriptionNotice);
|
|
5613
5559
|
this.agent.followUp({
|
|
@@ -5634,7 +5580,12 @@ export class AgentSession {
|
|
|
5634
5580
|
}
|
|
5635
5581
|
|
|
5636
5582
|
#scheduleQueuedMessageDrain(): void {
|
|
5637
|
-
if (
|
|
5583
|
+
if (
|
|
5584
|
+
this.#queuedMessageDrainScheduled ||
|
|
5585
|
+
this.#queuedMessageDrainBlocked ||
|
|
5586
|
+
!this.#canAutoContinueForFollowUp() ||
|
|
5587
|
+
!this.agent.hasQueuedMessages()
|
|
5588
|
+
) {
|
|
5638
5589
|
return;
|
|
5639
5590
|
}
|
|
5640
5591
|
this.#queuedMessageDrainScheduled = true;
|
|
@@ -5648,6 +5599,7 @@ export class AgentSession {
|
|
|
5648
5599
|
},
|
|
5649
5600
|
onError: () => {
|
|
5650
5601
|
this.#queuedMessageDrainScheduled = false;
|
|
5602
|
+
this.#queuedMessageDrainBlocked = this.agent.hasQueuedMessages();
|
|
5651
5603
|
},
|
|
5652
5604
|
});
|
|
5653
5605
|
}
|
|
@@ -5784,7 +5736,7 @@ export class AgentSession {
|
|
|
5784
5736
|
): Promise<void> {
|
|
5785
5737
|
this.#beginInFlight();
|
|
5786
5738
|
try {
|
|
5787
|
-
if (!(await this.#
|
|
5739
|
+
if (!(await this.#runUsageAwarePreflightForNextModelCall())) return;
|
|
5788
5740
|
const acceptTerminalEmptyStop = options?.acceptTerminalEmptyStop === true;
|
|
5789
5741
|
if (acceptTerminalEmptyStop) {
|
|
5790
5742
|
this.#resetPromptMaintenanceState();
|
|
@@ -5793,6 +5745,7 @@ export class AgentSession {
|
|
|
5793
5745
|
await this.agent.prompt(message);
|
|
5794
5746
|
await this.#waitForPostPromptRecovery();
|
|
5795
5747
|
} finally {
|
|
5748
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
5796
5749
|
this.#recovery.setAcceptTerminalEmptyStop(false);
|
|
5797
5750
|
this.#endInFlight();
|
|
5798
5751
|
}
|
|
@@ -5824,6 +5777,7 @@ export class AgentSession {
|
|
|
5824
5777
|
timestamp: Date.now(),
|
|
5825
5778
|
};
|
|
5826
5779
|
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
5780
|
+
this.#allowQueuedMessageDrainRetry();
|
|
5827
5781
|
if (deliverAs === "followUp") {
|
|
5828
5782
|
this.agent.followUp(normalizedAppMessage);
|
|
5829
5783
|
} else {
|
|
@@ -5880,7 +5834,7 @@ export class AgentSession {
|
|
|
5880
5834
|
this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
|
|
5881
5835
|
return false;
|
|
5882
5836
|
}
|
|
5883
|
-
|
|
5837
|
+
this.#allowQueuedMessageDrainRetry();
|
|
5884
5838
|
|
|
5885
5839
|
if (options?.deliverAs === "followUp") {
|
|
5886
5840
|
this.agent.followUp(normalizedAppMessage);
|
|
@@ -5963,8 +5917,6 @@ export class AgentSession {
|
|
|
5963
5917
|
if (images.length === 0) images = undefined;
|
|
5964
5918
|
}
|
|
5965
5919
|
|
|
5966
|
-
if (options?.deliverAs && !(await this.#runUsageAwarePreflight())) return;
|
|
5967
|
-
|
|
5968
5920
|
if (options?.deliverAs === "followUp") {
|
|
5969
5921
|
await this.#queueUserMessage(text, images, "followUp");
|
|
5970
5922
|
return;
|
|
@@ -6004,6 +5956,7 @@ export class AgentSession {
|
|
|
6004
5956
|
? isAdvisorCard
|
|
6005
5957
|
: m => !isUserQueuedMessage(m) && !isHiddenUserCompanion(m);
|
|
6006
5958
|
this.agent.replaceQueues(steeringAll.filter(keep), followUpAll.filter(keep));
|
|
5959
|
+
this.#reconcileQueuedMessageDrain();
|
|
6007
5960
|
return { steering, followUp };
|
|
6008
5961
|
}
|
|
6009
5962
|
|
|
@@ -6053,12 +6006,14 @@ export class AgentSession {
|
|
|
6053
6006
|
if (fromSteer >= 0) {
|
|
6054
6007
|
const removed = steering[fromSteer];
|
|
6055
6008
|
this.agent.replaceQueues(removeWithCompanions(steering, fromSteer), followUp.slice());
|
|
6009
|
+
this.#reconcileQueuedMessageDrain();
|
|
6056
6010
|
return toRestoredQueuedMessage(removed);
|
|
6057
6011
|
}
|
|
6058
6012
|
const fromFollowUp = lastUserIndex(followUp);
|
|
6059
6013
|
if (fromFollowUp >= 0) {
|
|
6060
6014
|
const removed = followUp[fromFollowUp];
|
|
6061
6015
|
this.agent.replaceQueues(steering.slice(), removeWithCompanions(followUp, fromFollowUp));
|
|
6016
|
+
this.#reconcileQueuedMessageDrain();
|
|
6062
6017
|
return toRestoredQueuedMessage(removed);
|
|
6063
6018
|
}
|
|
6064
6019
|
return undefined;
|
|
@@ -6348,6 +6303,8 @@ export class AgentSession {
|
|
|
6348
6303
|
await this.#memory.resetContextForNewTranscript();
|
|
6349
6304
|
this.#pendingNextTurnMessages = [];
|
|
6350
6305
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6306
|
+
this.#queuedMessageDrainBlocked = false;
|
|
6307
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
6351
6308
|
|
|
6352
6309
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
|
|
6353
6310
|
this.sessionManager.appendServiceTierChange(this.#models.serviceTierEntry());
|
|
@@ -7372,6 +7329,9 @@ export class AgentSession {
|
|
|
7372
7329
|
const previousFollowUpMessages = [...this.agent.peekFollowUpQueue()];
|
|
7373
7330
|
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
7374
7331
|
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
7332
|
+
const previousQueuedMessageDrainBlocked = this.#queuedMessageDrainBlocked;
|
|
7333
|
+
const previousUsagePreflightReadyForNextModelCall = this.#usagePreflightReadyForNextModelCall;
|
|
7334
|
+
const previousUsagePreflightReadyModel = this.#usagePreflightReadyModel;
|
|
7375
7335
|
const previousModel = this.model;
|
|
7376
7336
|
const previousThinkingLevel = this.thinkingLevel;
|
|
7377
7337
|
const previousAutoThinking = this.isAutoThinking;
|
|
@@ -7396,6 +7356,9 @@ export class AgentSession {
|
|
|
7396
7356
|
this.agent.clearAllQueues();
|
|
7397
7357
|
this.#pendingNextTurnMessages = [];
|
|
7398
7358
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7359
|
+
this.#queuedMessageDrainBlocked = false;
|
|
7360
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
7361
|
+
this.#usagePreflightReadyModel = undefined;
|
|
7399
7362
|
|
|
7400
7363
|
try {
|
|
7401
7364
|
if (switchingToDifferentSession) {
|
|
@@ -7564,6 +7527,9 @@ export class AgentSession {
|
|
|
7564
7527
|
this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
|
|
7565
7528
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
7566
7529
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
7530
|
+
this.#queuedMessageDrainBlocked = previousQueuedMessageDrainBlocked;
|
|
7531
|
+
this.#usagePreflightReadyForNextModelCall = previousUsagePreflightReadyForNextModelCall;
|
|
7532
|
+
this.#usagePreflightReadyModel = previousUsagePreflightReadyModel;
|
|
7567
7533
|
this.#inheritedProviderPromptCacheKey = previousInheritedProviderPromptCacheKey;
|
|
7568
7534
|
this.#checkpointState = previousCheckpointState;
|
|
7569
7535
|
this.#pendingRewindReport = previousPendingRewindReport;
|
|
@@ -7652,6 +7618,8 @@ export class AgentSession {
|
|
|
7652
7618
|
// Clear pending messages (bound to old session state)
|
|
7653
7619
|
this.#pendingNextTurnMessages = [];
|
|
7654
7620
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7621
|
+
this.#queuedMessageDrainBlocked = false;
|
|
7622
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
7655
7623
|
|
|
7656
7624
|
await this.#bash.flushPending();
|
|
7657
7625
|
// Flush pending writes before branching
|
|
@@ -7779,6 +7747,8 @@ export class AgentSession {
|
|
|
7779
7747
|
this.#pendingNextTurnMessages = [];
|
|
7780
7748
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7781
7749
|
this.agent.replaceQueues([], []);
|
|
7750
|
+
this.#queuedMessageDrainBlocked = false;
|
|
7751
|
+
this.#usagePreflightReadyForNextModelCall = false;
|
|
7782
7752
|
await this.#bash.flushPending();
|
|
7783
7753
|
await this.sessionManager.flush();
|
|
7784
7754
|
const bashTransition = this.#bash.beginSessionTransition();
|