Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

### Fixed

- Automatic Codex OAuth account selection now checks included quota before using paid credits, including cooling and temporarily leased accounts. Unknown quota cannot authorize paid usage.
- Title generation, compaction, branch summaries, side queries, and vision requests retain account rotation and configured model fallback without changing the active chat model. Explicit request keys remain pinned, and remote compaction keeps its local-summary fallback.
- Account switches are reported in the terminal and session events without credential material. Failing or self-removing notice listeners cannot interrupt routing or other listeners.
- Subscription-limit prose triggers credential failover rather than ending the request ([#1769](https://github.com/code-yeongyu/senpi/pull/1769) by [@orientpine](https://github.com/orientpine)).

### Removed

## [2026.9.22-4] - 2026-09-22
Expand Down
14 changes: 14 additions & 0 deletions packages/coding-agent/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,20 @@ if (usage && usage.tokens > 100_000) {
}
```

### ctx.getRetryFallbackSettings()

Returns the resolved retry/fallback policy for the current session, including
session overrides. Auxiliary model requests should use this policy rather than
loading global settings independently. The method is optional for contexts
supplied by older integrations.

```typescript
const policy = ctx.getRetryFallbackSettings?.();
if (policy?.modelFallback) {
// Apply the session's configured fallback policy.
}
```

### ctx.compact()

Trigger compaction without awaiting completion. Use `onComplete` and `onError` for follow-up actions.
Expand Down
20 changes: 20 additions & 0 deletions packages/coding-agent/src/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,26 @@
### Expected merge conflict zones

- `packages/coding-agent/src/modes/print-mode.ts`: navigation and assistant-edit neighbours inside `commandContextActions`.
## 2026-09-20 - Print mode renders account-switch notices

### What changed

- `packages/coding-agent/src/modes/print-mode.ts`: the session subscription adds
`account_failover` (formatted via `formatAccountSwitchNotice`) and
`internal_model_fallback` to the stderr notice handling.

### Why

- Print/json mode is non-interactive; account switches still need a stderr line consistent
with the other model-fallback notices.

### Why an extension could not handle it

- One-shot mode prints engine events outside any extension surface.

### Expected merge conflict zones

- LOW: the two new `else if` branches in the subscription callback.

## 2026-09-20 - Name what the startup timing table measures before the stdin read (senpi#1868 follow-up)

Expand Down
67 changes: 64 additions & 3 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import type {
Api,
AssistantMessage,
AssistantMessageEventStream,
AuthResult,
Context,
ImageContent,
Expand Down Expand Up @@ -109,6 +110,7 @@ import { CompactionLifecycleCoordinator, type CompactionLifecycleState } from ".
import { isTurnStuckOnContextOverflow } from "./compaction/stuck-overflow.ts";
import { isWarmSummaryAnchorValid } from "./compaction/warm-anchor.ts";
import type { CompactionModelSelector } from "./compaction-settings-access.ts";
import { emitAccountSwitch, subscribeAccountSwitch } from "./credential-pool/account-notices.ts";
import { admitCursorHistory, cursorAdmissionBudgetBytes } from "./cursor-history-admission.ts";
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
import { resolveDiscoveredResourcePaths } from "./discovered-resource-scope.ts";
Expand Down Expand Up @@ -203,6 +205,7 @@ import type {
} from "./extensions/types.ts";
import { normalizeToolExposure, RUNTIME_EXTENSION_PATH } from "./extensions/types.ts";
import { shouldWarnHighReasoning } from "./high-reasoning-warning.ts";
import { streamInternalModel } from "./internal-model-request.ts";
import {
isManualContinueSubmission,
MANUAL_CONTINUE_CUSTOM_TYPE,
Expand Down Expand Up @@ -447,6 +450,23 @@ export type AgentSessionEvent =
to: string;
chainConfigured: boolean;
}
| {
type: "account_failover";
provider: string;
from: string;
to: string;
reason: string;
sessionId?: string;
source?: string;
}
| {
type: "internal_model_fallback";
source: string;
from: string;
to: string;
reason: string;
chainKey: string;
}
// Auth login flow (task 13) is additive with event-only completion. The
// login_start command responds immediately, then the OAuth URL and the
// terminal result arrive here, because an interactive browser round-trip
Expand Down Expand Up @@ -876,6 +896,7 @@ export class AgentSession {
// Event subscription state
private _unsubscribeAgent?: () => void;
private _unsubscribeSettingsSource?: () => void;
private _unsubscribeAccountSwitch?: () => void;
private _eventListeners: AgentSessionEventListener[] = [];
private _agentEventQueue: Promise<void> = Promise.resolve();
/**
Expand Down Expand Up @@ -1073,6 +1094,18 @@ export class AgentSession {
this._unsubscribeSettingsSource = this.settingsManager.subscribeToSourceSelection((source) => {
this._emit({ type: "settings_source_selected", ...source });
});
this._unsubscribeAccountSwitch = subscribeAccountSwitch((event) => {
if (event.sessionId !== this.sessionId) return;
this._emit({
type: "account_failover",
provider: event.provider,
from: event.from,
to: event.to,
reason: event.reason,
...(event.sessionId === undefined ? {} : { sessionId: event.sessionId }),
...(event.source === undefined ? {} : { source: event.source }),
});
});
const noModelFallback =
config.resourceLoader.getExtensions().runtime.flagValues.get("no-model-fallback") === true ||
envValue("NO_FALLBACK") === "1";
Expand Down Expand Up @@ -1590,6 +1623,10 @@ export class AgentSession {

private _emit(event: AgentSessionEvent): void {
this._logSessionEvent(event);
if (event.type === "account_failover") {
emitAccountSwitch(event, this._eventListeners);
return;
}
for (const l of this._eventListeners) {
l(event);
}
Expand Down Expand Up @@ -2930,6 +2967,8 @@ export class AgentSession {
this._disconnectFromAgent();
this._unsubscribeSettingsSource?.();
this._unsubscribeSettingsSource = undefined;
this._unsubscribeAccountSwitch?.();
this._unsubscribeAccountSwitch = undefined;
this._unsubscribeWakeSources?.();
this._unsubscribeWakeSources = undefined;
this._eventListeners = [];
Expand Down Expand Up @@ -4286,7 +4325,7 @@ export class AgentSession {
baseOptions: this._buildSessionTitleBaseOptions(),
retry: sessionTitleRetryPolicy(this.settingsManager.getRetrySettings()),
signal: abortController.signal,
streamFn: this.agent.streamFunction,
streamFn: (model, context, options) => this._streamInternalModel(model, context, options, "title"),
});
if (abortController.signal.aborted) {
return;
Expand Down Expand Up @@ -4330,6 +4369,27 @@ export class AgentSession {
};
}

private _streamInternalModel(
model: Model<Api>,
context: Context,
options: SimpleStreamOptions = {},
purpose: string,
): AssistantMessageEventStream | Promise<AssistantMessageEventStream> {
return streamInternalModel(
this._modelRuntime,
model,
context,
{ ...options, purpose, affinitySessionId: this.sessionId },
{
settings: this.settingsManager,
cooldowns: this._selectorCooldowns,
streamFn: this.agent.streamFunction,
notify: (event) => this._emit(event),
agentDir: this._agentDir,
},
);
}

/**
* Internal: Queue a steering message (already expanded, no extension command check).
*/
Expand Down Expand Up @@ -5853,7 +5913,7 @@ export class AgentSession {
signal,
extraBody,
this.thinkingLevel,
this.agent.streamFunction,
(model, context, options) => this._streamInternalModel(model, context, options, "compaction"),
env,
this.agent.transformContext,
this.settingsManager.getRetrySettings(),
Expand Down Expand Up @@ -9210,7 +9270,8 @@ export class AgentSession {
customInstructions,
replaceInstructions,
reserveTokens: branchSummarySettings.reserveTokens,
streamFn: this.agent.streamFunction,
streamFn: (model, context, options) =>
this._streamInternalModel(model, context, options, "branch summary"),
retry: this.settingsManager.getRetrySettings(),
callbacks: this._summarizationRetryCallbacks({
source: "branchSummary",
Expand Down
137 changes: 137 additions & 0 deletions packages/coding-agent/src/core/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,143 @@ The provider id is resolved inside the package before any extension loads, and t

- LOW: the `editAssistantMessage` / `_navigateTree` heads in `agent-session.ts` (one added method and one widened parameter type); the fork-only `edited-user-message.ts`.
- Coverage: `test/suite/tree-edit-user-message.test.ts`.
## 2026-09-20 - Auxiliary requests stream through one internal fallback seam

### What changed

- `packages/coding-agent/src/core/internal-model-request.ts` (new): `streamInternalModel` gives title, compaction, branch summary, /btw, and look_at a per-request fallback lane that never mutates the agent's selected chat model. It strips a pre-resolved Codex OAuth `apiKey` only at the internal seam, drops `apiKey`/`headers`/`extraBody`/`env`/`reasoningEffort` on a cross-provider fallback, and excludes text-only fallback models while the context contains images.
- `packages/coding-agent/src/core/agent-session.ts`: new `_streamInternalModel` helper and the title, blocking compaction, and branch-summary `streamFn` call sites now route through it.
- `packages/coding-agent/src/core/extensions/types.ts` and `runner.ts`: the extension context gains an optional `getRetryFallbackSettings()` so builtins pass session settings into the helper instead of deriving a global `SettingsManager`.
- `compaction/speculative-summary.ts`, `compaction/openai-remote.ts`, `compaction/index.ts`, `btw/index.ts`, and `look-at/runner.ts`: auxiliary lanes use the helper; remote Codex compaction uses the public `ModelRuntime.requestWithCredentialRotation` seam.
- `packages/coding-agent/test/suite/internal-model-request.test.ts` and
`packages/coding-agent/test/suite/codex-auxiliary-routing.test.mjs`: portable
helper and native title/summary/remote-compaction/side-query/vision regressions.
- `packages/coding-agent/test/compaction/before-compact-error-surfacing.test.ts`:
the session fixture now supplies its required `getSessionId` contract.
- When no session fallback policy is provided, it is disabled, or the active
model has no configured fallback chain, the internal helper delegates the
original stream directly, preserving rejection identity, synchronous-throw
semantics, and cancellation rather than relabeling failures through another
lazy error-event layer.

### Why

Auxiliary lanes previously called the runtime directly. Routing them through this seam while keeping their selector local means a resolved credential or provider-specific body can never cross into a different provider, and the failures never re-select the whole session.

### Why an extension could not handle it

Title and branch-summary generation originate inside AgentSession, and native
remote compaction requires the selected credential before its HTTP request.
The shared internal seam covers these core requests and builtins together.

### Expected merge conflict zones

- LOW: the additive import plus `_streamInternalModel` and the three `streamFn` call sites in `agent-session.ts`, the new `internal-model-request.ts`, and the `getRetryFallbackSettings` addition in `extensions/types.ts`/`runner.ts`.

## 2026-09-20 - Codex quota admission precedes paid usage

### What changed

- `packages/coding-agent/src/core/credential-pool/codex-quota.ts`: validates fresh
read-only usage responses and distinguishes included quota, confirmed
exhaustion, and unknown availability. Credits are admitted only after every
normal bucket is confirmed exhausted.
- `packages/coding-agent/src/core/credential-pool/rotation-stream.ts`: checks
the full account inventory, including cooling accounts and accounts leased
by another recovery probe, before filtering generation candidates. Streams
and non-streaming requests share selection, health persistence, and notices.
- `packages/coding-agent/src/core/model-runtime.ts`: lazily loads quota readers,
resolves OAuth through native account-specific refresh, preserves explicit
request-key bypass, carries affinity/purpose, and provides the typed
`requestWithCredentialRotation` HTTP seam. Payload provenance uses the
selected Codex credential, not a stale caller snapshot.
- `packages/coding-agent/src/core/credential-pool/classify.ts`: recognizes
subscription usage-limit prose as an account rate limit. This overlaps the
narrow classification repair proposed in upstream PR #1769.
- Portable regressions:
`packages/coding-agent/test/suite/codex-quota.test.ts`,
`packages/coding-agent/test/suite/codex-runtime-routing.test.ts`, and
`packages/coding-agent/test/suite/account-notices.test.ts`.

### Why

- Generation cooldown and probe ownership are not evidence of exhausted
quota. Omitting those accounts could spend credits while normal quota
remained. Unknown or denied quota must not authorize spending either.
- Explicit keys are a caller contract; only internal request adapters may
discard a previously resolved OAuth snapshot to delegate account selection.

### Why an extension could not handle it

- Account admission, credential refresh, payload provenance, and pre-output
failover belong to ModelRuntime and must also cover noninteractive workers.

### Expected merge conflict zones

- MEDIUM: ModelRuntime request preparation and credential-pool selection.
- LOW: new quota module and portable regressions. Coordinate the classifier
line with PR #1769 rather than submitting two independent copies.

## 2026-09-20 - Account-switch notices are fault-isolated

### What changed

- `packages/coding-agent/src/core/credential-pool/account-notices.ts` (new): the
`emitAccountSwitch` / `subscribeAccountSwitch` / `formatAccountSwitchNotice` surface.
`emitAccountSwitch` wraps each subscriber in its own try/catch and attaches a rejection
handler to async observers. Failures produce a fixed, credential-free diagnostic,
never stop provider selection, never drop later observers, and never surface exception text.
Delivery snapshots the observer collection so a failing observer that removes
itself cannot shift a session listener array and skip its next peer.
- `packages/coding-agent/src/core/agent-session.ts`: each session subscribes at construction
and converts a module-level `account_failover` whose `sessionId` matches into a session
event, forwarding `provider`/`from`/`to`/`reason` plus the optional `sessionId`/`source`.
The `AgentSessionEvent` union gains `account_failover` and `internal_model_fallback`.
`dispose()` unsubscribes the account-switch listener alongside the settings-source listener.

### Why

- The credential-pool selector notifies these listeners from its synchronous path. A throwing
listener previously aborted selection with zero provider attempts and silenced every later
observer; a rejected async observer became an unhandled rejection. The session-scoped
subscription keeps one account switch from leaking across sessions, and dispose keeps the
module-level set from retaining dead sessions.

### Why an extension could not handle it

- The emit runs inside the credential-pool selector before any provider attempt, and the
constructor/dispose subscription in `agent-session.ts` sits below every extension of the
session. Neither seam is reachable from extension code.

### Expected merge conflict zones

- LOW: the new `credential-pool/account-notices.ts` (fork-only) and the additive seams in
`agent-session.ts` - the import block, the `AgentSessionEvent` union entries, the
constructor subscription, and the dispose cleanup.

## 2026-09-21 - Reuse prose subscription-limit classification from PR #1769

### What changed

- `packages/coding-agent/src/core/credential-pool/classify.ts`: recognizes the
narrow usage/session/weekly/daily/hourly limit vocabulary from PR #1769.
- The prerequisite retains that PR's taxonomy and failover regressions.

### Why

- The quota-routing feature needs the same classification when a subscription
limit is reported after preflight. Reusing the existing implementation avoids
a competing expression and keeps its dependency separately reviewable.

### Why an extension could not handle it

- Classification controls the native credential failover runner before an
extension could select a healthy replacement account.

### Expected merge conflict zones

- LOW: the rate-limit expression. This prerequisite incorporates PR #1769's
combined implementation/tests and should be dropped when that PR is integrated.

## 2026-09-20 - A fallback rung too small for the transcript is repaired, not rejected (senpi#1873)

Expand Down
Loading
Loading