Skip to content

refactor(agent-core-v2): plug llm credentials in through request config - #3682

Merged
7Sageer merged 30 commits into
mainfrom
refactor/human-connection-credentials
Sep 10, 2026
Merged

refactor(agent-core-v2): plug llm credentials in through request config#3682
7Sageer merged 30 commits into
mainfrom
refactor/human-connection-credentials

Conversation

@7Sageer

@7Sageer 7Sageer commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

N/A — internal refactor, no linked issue.

Problem

OAuth handling in the human layer lived in withAuth/withAuthUpload requester decorators wrapping generate: they resolved credentials per call, intercepted the llm.failed.remote event stream, swallowed the first 401, and silently retried once. The same policy existed in two parallel copies (event-based for generate, throw-based for upload), the upload copy's canRecover could never fire on raw SDK errors (so the upload 401 retry was dead code), and the silent retry was invisible to the event contract — no llm.retrying/llm.recovering ever fires for an auth refresh. A second auth home (AuthProvider/StaticAuthProvider) also sat in llm-adapter, so credential logic was split across layers.

What changed

Auth becomes part of the request: LlmRequestConfig.credentials is the credential contribution point (resolve / canRecover / invalidate), and the attempt loops — not a decorator — own resolution and recovery.

  • human/credentials/ is the single home for credential providers: staticCredentials(apiKey) and oauthCredentials(getToken) (force-refresh on invalidate, 401 detection via the shared errorStatusCode in llm/errors.ts), plus the applyCredential / resolveModelCredentials helpers. kimi-oauth drops withAuth/withAuthUpload/CredentialSource; it is now a one-line adapter over oauthCredentials.
  • llm-adapter's AuthProvider/StaticAuthProvider/ProviderRequestAuth are gone: Model.authProvider becomes Model.credentials, built by the catalog through the factories above. ModelRequesterImpl is pure transport — it resolves credentials per attempt and never retries.
  • The llm machine's request actor (machine path) resolves config.credentials into a fully-credentialed model before each attempt (zero ticks when resolution is synchronous, so wire event ordering is unchanged), and the turn state machine owns 401 recovery: a recoverable 401 emits llm.recovering {strategy:'credentials', action:'refresh'}, calls credentials.invalidate(), and re-enters thinking (once per step, via the existing appliedRecoveries reset). The state machine can now drive OAuth natively.
  • Direct callers outside the state machines — catalog ping/generate (the new IModelCatalog.generate backing the klient facade), full compaction, media upload — write the same single-retry recovery out inline at the call site, sharing the state machine's provider instance and honoring abort. No wrapper layer: the control flow reads linearly where it happens.
  • Pre-gate failure handling: when credential resolution rejects before the first request (e.g. login expired), the loop settles the queued or notification-seeded turn through the full lifecycle (beginActiveTurn + endTurn, with failure events and queue progression) instead of hanging the submitter's ready/result/settled(). The raw error rides llm.failed.remote as rawError, so coded errors like auth.login_required reach the UI intact, and recovering now closes the current machine step exactly like retrying (balanced step.begin/step.end).

Verified: agent-core-v2 suite 6426 tests green (new loop-level regressions for pre-gate settle of queued and notification-seeded turns, coded-error identity, and recovering step balance — each shown to fail without its fix), repo typecheck green, lint clean.

Architecture diagram (dot source)
digraph authflow {
  rankdir=LR;
  node [shape=box, style="rounded,filled", fillcolor=white, fontname="Helvetica", fontsize=11];
  edge [fontname="Helvetica", fontsize=9];

  subgraph cluster_before {
    label="before";
    dec [label="withAuth / withAuthUpload decorators\n+ llm-adapter AuthProvider", fillcolor=mistyrose];
    swallow [shape=note, label="intercept llm.failed.remote,\nswallow first 401, silent retry\n(two parallel copies)"];
    dec -> swallow [style=dashed, arrowhead=none];
  }

  subgraph cluster_after {
    label="after: credentials as part of the request";
    cred [label="human/credentials\nstaticCredentials / oauthCredentials", fillcolor=honeydew];
    actor [label="llm machine request actor\n(resolve per attempt)"];
    turn [label="turn machine\ncredentials branch"];
    direct [label="direct paths: ping / generate /\ncompaction / media upload\n(inline single-retry)"];
    mri [label="ModelRequesterImpl\npure transport, no retry"];
    rec [shape=note, label="recoverable 401 -> invalidate()\nllm.recovering(credentials/refresh)\nre-enter thinking"];
    cred -> actor;
    cred -> direct;
    actor -> turn [label="llm.failed.remote"];
    turn -> rec [style=dashed, arrowhead=none];
    turn -> actor [label="re-enter"];
    actor -> mri;
    direct -> mri;
  }

  dec -> cred [color=gray50, style=dotted, label="replaced by"];
}

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…ai formats

The requester already forwarded LlmRequestConfig.toolMessageConversion
into formatRequest, but the openai chat and openai-responses formats
only read the trait hook, so the explicit request config was silently
ignored. Resolve the mode as request config, then trait default, then
the protocol default, and pass the resolved value into lowering.
…ialects and provider connection

The ProtocolTrait interface bundled endpoint/headers connection config,
model capability, message/params conversion hooks, and error
classification into one bag, and every format received the whole bag
whether or not it consumed each hook — hooks a protocol ignores were
accepted by the type system and silently dead at runtime.

Split it by consumer:

- ProviderConnection (protocol/connection.ts): endpoint env
  declaration and default headers, still resolved per request inside
  generate.
- Per-protocol typed dialects (OpenAIDialect, OpenAIResponsesDialect,
  AnthropicDialect, GoogleGenAIDialect): only the customization points
  each protocol actually consumes; data-shaped hooks (reasoningKey,
  toolCallIdPolicy, toolMessageConversion, strictThinkingValidation)
  are data fields, and message/history hooks use the protocol wire
  types instead of Record<string, unknown>.
- convertError moves to a requester option, capability to a provider
  variant field.

Dialects are bound when the format is created (createOpenAIFormat and
siblings), so FormatRequestInput and the stream parser carry request
data only; ProtocolTrait is deleted. The thinking hook returns the
kwargs and the preserveThinking flag together as ThinkingApplication,
and the kimi dialect emits the final thinking params directly instead
of routing them through an extra_body flatten in buildParams. The
llm-adapter layer is migrated to the same {connection, dialect,
convertError} assembly.
- move CONTEXT_MANAGEMENT_BETA into anthropic/contract and have the kimi
  trait import wire types from each protocol's contract.ts instead of
  format/lower; export the four contract modules from human/index.ts and
  guard the boundary in check-import-boundaries
- rename protocol/trait.ts to protocol/thinking.ts and move TraitContext
  to protocol/base.ts; clean up the remaining dialect-era test variable
- type extractUsage as OpenAIRawUsage / OpenAIResponsesRawUsage instead of
  Record<string, unknown> and drop the requester-side casts
… dead format types

- the four requesters spread LlmRequestConfig into the plan input instead
  of hand-enumerating fields, so a new cross-protocol config field cannot
  be silently dropped per protocol
- ProtocolFormat loses the phantom TRequest/TResponse type parameters;
  the dead OpenAIRawResponse/AnthropicRawResponse/GoogleRawChunk contract
  types go with them
- the thinking test now names the extra_body flatten behavior explicitly
… requester pipeline

- drop the four format barrels (and openai/reasoning-key) from
  human/index.ts; each base's public seam is now exactly
  contract / trait / requester
- stop re-exporting contract wire types from format/lower modules so the
  neutral vocabulary has a single home; move the *LoweredMessage staging
  types out of contract into the owning format module
- guard the seam in check-import-boundaries: only llm/requester/bases
  code and tests may import format/lower/patterns/reasoning-key
Replace the withAuth/withAuthUpload requester decorators with a
credential contribution point on LlmRequestConfig: the llm machine's
request actor (machine path) and ModelRequesterImpl (direct path)
resolve credentials per attempt, and a recoverable 401 is recovered by
invalidating and re-resolving — emitted as llm.recovering on the turn
machine's credentials branch.
@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a0ec899

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3effd9bae0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

? input.config
: {
...input.config,
model: await resolveModelCredentials(input.config.model, input.config.credentials),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate credential-resolution failures from the request actor

When credentials.resolve() rejects—for example because OAuth refresh, credential storage, or login resolution fails—the rejection occurs inside this detached void async function. A callback actor cannot propagate that promise rejection to the invocation's onError, so the turn remains in thinking indefinitely and the rejection may surface only as an unhandled promise rejection. Use an actor whose returned promise is supervised, or catch this rejection and emit/forward a terminal failure.

Useful? React with 👍 / 👎.

A rejection inside the llm machine's detached request actor (credential
resolution, message resolvers, or a throwing requester) could not reach
the machine, leaving the turn stuck in thinking. Convert it to an
llm.failed.remote event so the turn lands in its failed state.
@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: fe5b4b96c4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bb2f049d1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +160 to +162
const credential = await this.model.credentials.resolve();
await requester.generate(
{ ...config, model: applyCredential(resolved.model, credential) },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore credential recovery for direct model requests

When an OAuth token is revoked while still cached, direct consumers such as ModelCatalog.ping() receive the first 401 from this request and fail without invalidating or retrying the credential. The turn machine now supplies that recovery for agent turns, but ping() calls ModelRequesterImpl directly, and the previous withAuth wrapper performed the missing forced refresh; wrap this direct request path with the new single-recovery helper or implement equivalent recovery here.

Useful? React with 👍 / 👎.

Comment on lines +67 to +68
credentials.invalidate?.();
return attempt();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip credential retries after cancellation

When an operation is canceled as its first attempt returns a recoverable 401, this helper still invalidates credentials and unconditionally starts a second attempt. This affects the media uploader at mediaToolsRegistrar.ts: ModelRequesterImpl.uploadVideo() resolves credentials before passing its signal to the uploader, so a canceled upload can force an OAuth refresh and potentially begin another upload; the removed credential wrapper explicitly suppressed retries when its signal was aborted.

Useful? React with 👍 / 👎.

Comment on lines +214 to +216
currentCredentials(): LlmCredentialProvider | undefined {
if (!this.profile.hasProvider()) return undefined;
return this.modelCatalog.get(this.profile.resolveModelContext().modelAlias).credentials;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve credentials from the turn's snapshotted model

If the profile model is changed while a turn is already running, prepareTurnConfig() keeps that turn on its previously snapshotted model, but this method returns credentials for the newly current profile model. A 401 from the old model therefore causes the turn recovery branch to invalidate the new model's provider, while the retried request resolves the old provider's unchanged cached token and fails again; credential lookup needs to use the same turn/model context as resolveRequest().

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7ffce0623

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

usage = undefined;
finish = undefined;
messageId = undefined;
const credential = await this.model.credentials.resolve();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore recovery for facade model requests

When an OAuth token is revoked while cached, global.models.generate() still reaches ModelRequesterImpl.request() directly through packages/klient/src/transports/memory/dispatcher.ts:475-498; this one-shot credential resolution sends the stale token, and the resulting 401 is returned without invalidating or retrying, so subsequent facade requests keep failing until another flow refreshes the token. Fresh evidence beyond the prior comment is that the current dispatcher route remains unwrapped while recovery was added only to specific callers such as catalog ping; apply the single credential-recovery loop to this direct route or the requester boundary.

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bb61141cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

: credential instanceof Promise
? {
...input.config,
model: applyCredential(input.config.model, await credential),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the pending turn before resolving credentials

When the first request for a queued turn uses OAuth and credential resolution rejects (for example, login is required or token refresh fails), this await is caught and converted to llm.failed.remote before requester.generate() runs. In the agent-core-v2 loop, createMachineRequester.generate() is where gate() binds pendingMachineTurn; without that call, loopService.ts:764-768 merely clears the pending marker on turnSettled and never removes or settles the queued reservation, leaving the submitter's ready/result promises and settled() hanging. Fresh evidence beyond the earlier comment is that the new catch now terminates the machine, but this ordering still bypasses the binding gate; bind the pending turn before credential resolution or explicitly settle its reservation on this failure path.

Useful? React with 👍 / 👎.

Remove the attemptWithCredentialRecovery/streamWithCredentialRecovery
helpers from human/credentials; the single-retry recovery is now written
out at each direct call site (catalog ping/generate, full compaction,
media upload) so the control flow reads linearly without a wrapper hop.
The credentials module keeps only the provider factories and the
credential application helpers; recoverability and invalidation stay on
the LlmCredentialProvider itself.
…s before gating

When the llm machine fails before requester.generate() runs — for
example when OAuth credential resolution rejects — the loop cleared the
pending machine turn marker on turnSettled without ever binding or
settling the queued reservation, leaving the submitter's ready/result
promises and settled() hanging. The loop now settles the unbound
reservation explicitly: cancelled on abort, failed otherwise, mirroring
the evaluateSettle error propagation.
@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc59aecffb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +452 to +456
const error = outcome.error ?? new Error2(ErrorCodes.INTERNAL, 'Turn ended before first step');
reservation.controller.abort(error);
reservation.turn.state = 'failed';
reservation.ready.reject(error);
reservation.result.resolve({ type: 'failed', steps: 0, error });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish failures for turns settled before gating

When credential resolution rejects before the gate for a normal user prompt, this direct settlement resolves turn.result without passing through endTurn(), which is the path that dispatches TurnEnded and AgentErrorEvent. AgentPromptService therefore emits only prompt.completed with a generic failed reason and no error payload, so clients never receive the OAuth/login error needed to explain or recover from the accepted prompt's failure. Fresh evidence beyond the earlier hanging report is this newly added settlement path; it should publish the equivalent failure lifecycle events or bind the turn before failing it.

Useful? React with 👍 / 👎.

… turn lifecycle

The direct settlement resolved turn.result without endTurn(), so no
TurnPrompt/TurnStarted/TurnEnded/AgentErrorEvent was published and the
next queued reservation was never launched. The unbound reservation is
now bound via beginActiveTurn and ended via endTurn (failed, or
cancelled on abort), chained onto afterChain when the previous turn is
still settling, so pre-gate failures get the same lifecycle events and
queue progression as any other failed turn.
@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

1 similar comment
@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 426e0ba757

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

pending: { readonly id: number; readonly queueItemId?: string },
outcome: { readonly outcome: MachineTurnOutcome; readonly error?: unknown },
): void {
if (pending.queueItemId === undefined) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Settle notification-backed turns after pre-gate failures

When an idle notify() carrying a context message starts a turn and credential resolution fails before gate(), the pending machine turn has no queueItemId, so this return clears pendingMachineTurn without consuming or dropping the nudge. The nudge remains marked sentToMachine and hasPendingRequests() stays true, while the machine is already idle and will not resend it; consequently settled() hangs until another prompt arrives or the caller explicitly drops the notification. Handle notification-backed pending turns here as well as queued reservations.

Useful? React with 👍 / 👎.

},
);
} catch (error) {
sendBack({ type: 'llm.failed.remote', error: toLlmErrorMessage(error) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve coded credential errors

When credentials.resolve() rejects with the Error2 produced by ModelOAuthTokenAdapter for a missing or revoked login, this conversion discards its auth.login_required code and details before the pre-gate settlement path receives it. endTurn() then serializes the plain LLM-message object as an internal error (even reducing its message to [object Object]), so the TUI check in apps/kimi-code/src/tui/controllers/session-event-handler.ts:988-989 cannot show the login-required recovery notice. Fresh evidence beyond the earlier lifecycle comment is that the turn now emits failure events, but those events still contain the lossy converted error; retain the original error alongside the LLM event or restore its coded payload when settling.

Useful? React with 👍 / 👎.

Comment on lines +590 to +591
target: 'thinking',
reenter: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close the current step before credential recovery

When an OAuth-backed request emits llm.sent and then returns a recoverable 401, this re-entry emits recovering, but AgentLoopService.projectMachineEvent() handles only retrying by closing and clearing the current MachineStepState. The next attempt therefore passes through gate() with the old step still active, overwrites it with a newly numbered step, and leaves the original step.begin without a matching step.end; the recovered request is also counted as an extra turn step and can be rejected by a low maxStepsPerTurn. Close/reset the loop step for credential recovery just as the retry path does before re-entering thinking.

Useful? React with 👍 / 👎.

… coded credential errors, close steps on recovering

- Notification-seeded machine turns that fail before gating now bind a
  seeded reservation and end through endTurn like queued ones, instead
  of leaving the nudge pending and settled() hanging
- The llm machine's request actor attaches the raw error to
  llm.failed.remote and the turn machine prefers it for its terminal
  failure, so coded errors (e.g. auth.login_required) survive to
  endTurn's error payload
- projectMachineEvent now closes the current machine step on recovering
  exactly as it does on retrying, keeping step.begin/step.end balanced
  when credential recovery re-enters the request
@7Hanrui

7Hanrui commented Sep 9, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: b9be6d35c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…ional Model.credentials, abort-safe request actor

- oauthCredentials.invalidate() now starts the forced refresh eagerly and
  hands the pending token to the next resolve(), so the force signal can no
  longer leak into or be consumed by unrelated resolves
- Model.credentials becomes optional, dropping the {} as never /
  staticCredentials(undefined) filler in test fakes
- the llm machine request actor swallows abort outcomes instead of
  reporting llm.failed.remote, so an aborted turn settles as aborted
  rather than failed
…credential-recovery executors, single-proposal failure triage
@7Hanrui

7Hanrui commented Sep 10, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d78c485838

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +442 to +445
const seeded = this.nudges.slice(this.nudgeCursor).find(
(nudge) => !nudge.dropped && nudge.contextMessage !== undefined && nudge.contextMessage.content.length > 0,
);
if (seeded === undefined) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Settle pre-gate failures for empty notifications

When an idle turn is started by loop.notify() without a message and credential resolution fails before gate(), this return leaves the corresponding nudge unconsumed after pendingMachineTurn is cleared. Real callers use this form in features/externalHooks/agent/agentExternalHooksService.ts:258 and features/goal/goalService.ts:607; because hasPendingRequests() continues to see the nudge while the machine has already drained its notification, settled() hangs until unrelated work arrives. Fresh evidence beyond the prior context-notification report is that the current branch explicitly excludes empty nudges even though these message-less call sites exist.

Useful? React with 👍 / 👎.

Comment on lines +110 to +111
const model = await resolveModelCredentials(resolved.model, this.model.credentials);
return uploader(video, { model, signal: options?.signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry video uploads from the media resolver

When an OAuth token is rejected while still cached, uploadVideo() now performs only one credential resolution and one upload attempt. The wrapper added in mediaToolsRegistrar.ts covers the read-media tool, but agent/media/mediaResolverService.ts:258-280 creates this requester-backed uploader directly while preprocessing user-supplied videos and explicitly rethrows authentication failures; that path therefore neither invalidates nor retries the stale credential, so the user's video request fails until another operation refreshes the token. Apply the shared single-recovery executor at this remaining caller or retain recovery at the upload boundary.

Useful? React with 👍 / 👎.

…lure, recover credentials in media resolver uploads
@7Hanrui

7Hanrui commented Sep 10, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7086d06b2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

signal: input.signal,
});
try {
const credential = input.config.credentials?.resolve();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid consuming refreshed credentials before the real request

When oauthCredentials wraps a token source that returns the refreshed token only for { force: true }, invalidate() reserves that token for the next resolve(), but this call consumes it into config before createMachineRequester.generate() discards _config; ModelRequesterImpl.runRequest() then resolves the provider again normally. The credential-recovery attempt can therefore send the stale token and repeat the 401 despite a successful refresh. Pass the resolved configuration through the machine requester or otherwise ensure credentials are resolved exactly once per attempt.

AGENTS.md reference: packages/agent-core-v2/src/human/llm/AGENTS.md:L1-L1

Useful? React with 👍 / 👎.

Keep the per-protocol typed trait structure and port main's changes onto it:

- llm/protocol/trait.ts (deleted here, widened on main with
  acceptedImageMimes): the hook now lives on AnthropicTrait, resolved in
  planAnthropicRequest and threaded through lowerAnthropicRequest;
  kimiAnthropicTrait supplies the Kimi set via providerImagePolicy
- openai/format.ts: fold the reasoning_details round-trip into the
  requester-composed stream parser; the explicit reasoning key reaches the
  parser through OpenAIStreamParserOptions
- protocolAdapterRegistry.resolveCapability: drop the removed explain/trace
  stack, keep the definition-level capability hook
- adjust thinking, anthropic-lower, and sessionMediaStore tests to the
  requester pipeline shapes
Resolve the overlap with the protocol trait refactor (#3641) and the
event-sourced agent store (#3691):

- engine.ts: keep both additions at the machine wiring site — the
  turn-aware credential provider feeding input.request.credentials, and
  the journal-backed AgentEventStore now required by AgentInput
- docs/{en,zh}/llm.md: unify the request lifecycle paragraph — the
  credential resolution flow alongside the requester plan* composition,
  and the turn-side emptyResponseError / recovery-chain wording that
  matches the merged code
@7Sageer
7Sageer marked this pull request as ready for review September 10, 2026 12:21
@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@a0ec899
npx https://pkg.pr.new/@moonshot-ai/kimi-code@a0ec899

commit: a0ec899

Resolve the overlap with the llm-machine removal (#3710):

- the machine's createRequestActor is gone; the credential resolution and
  the abort-guarded error boundary move into llm/requester/actor.ts, kept
  synchronous on the credential-less path so the turn startup cascade keeps
  its ordering
- docs/{en,zh}/llm.md: unify principles and the request lifecycle with the
  turn-driven orchestration — credential resolution in the request actor,
  the recovery strategy chain and empty-response judgment in the turn
- turn.test.ts: drive the request actor through a harness machine instead
  of the deleted llm machine
@7Sageer
7Sageer merged commit 18f77ef into main Sep 10, 2026
15 checks passed
@7Sageer
7Sageer deleted the refactor/human-connection-credentials branch September 10, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants