Skip to content
Merged
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
11 changes: 11 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Fixed

- Classified zero-output Devin `invalid_argument` trailers as context overflow when the serialized message history is already large, routing cumulative tool-output payload failures through context maintenance—including artifact-backed shake rescue—instead of retrying the same rejected history.
## [17.0.5] - 2026-07-18

### Changed
Expand All @@ -18,6 +21,14 @@
- Fixed tool request failures (HTTP 400) on local grammar-constrained OpenAI-compatible backends (such as llama.cpp, LM Studio, and vLLM) by widening bare boolean subschemas into a value-accepting primitive union.
- Fixed custom OAuth Anthropic-compatible endpoints receiving generated Claude Code fingerprint headers even when explicit header overrides were provided.
- Fixed active sessions for plan-gated OpenAI Codex models (Sol/Luna) silently re-routing to sibling OAuth accounts when usage headroom changed, ensuring session stickiness is preserved as long as the preferred credential remains usable and eligible.
- Retry one transient OpenAI Responses stream truncation before replay-unsafe output, preventing recoverable transport truncations from surfacing as failed turns ([#5908](https://github.com/can1357/oh-my-pi/issues/5908)).
- Kept native Kimi Code K3 thinking enabled for named function selection by using generic required tool choice.
- Fixed `/login moonshot` validating China-platform API keys against the international host instead of honoring `MOONSHOT_BASE_URL` ([#5981](https://github.com/can1357/oh-my-pi/issues/5981)).
- Fixed Anthropic session credential stickiness suppressing usage-based re-ranking indefinitely: the session pin skipped ranking for up to 30 days (or process lifetime) even after the ≤1h prompt cache it protects was no longer guaranteed warm. The Anthropic skip is now gated on time since the session's last resolve (`ANTHROPIC_SESSION_STICKY_CACHE_WARM_MS`, 1h), while providers without a verified cache lifetime retain their existing stickiness. When ranking runs, the pinned account is only a tie-break rather than an absolute front-of-queue override, restoring proactive multi-account load balancing after long idle ([#5966](https://github.com/can1357/oh-my-pi/issues/5966)).
- Fixed clockless Anthropic usage windows outranking clocked sibling credentials by scoring their headroom over the full window duration ([#5960](https://github.com/can1357/oh-my-pi/issues/5960)).
- Fixed local grammar-constrained OpenAI-compatible backends (llama.cpp, LM Studio, vLLM) rejecting every tool request with `400 "Unable to generate parser for this template ... Unrecognized schema: true"`. The `task` tool's open `outputSchema` field normalizes to a bare boolean `true` subschema (issue #1179), which llama.cpp's JSON-schema→GBNF converter cannot compile. The chat-completions encoder now widens bare boolean subschemas into a value-accepting primitive union (and preserves closed-object `additionalProperties: false`) for these backends via the new `grammar` tool-schema flavor ([#5914](https://github.com/can1357/oh-my-pi/issues/5914)).
- Fixed custom OAuth Anthropic-compatible endpoints with explicit header overrides still receiving generated Claude Code fingerprint headers instead. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888))
- Fixed plan-gated OpenAI Codex models (Sol/Luna) silently re-routing an active session to a sibling OAuth account when usage headroom changed: `AuthStorage.#resolveOAuthSelection` now promotes the session-preferred credential back to the front of the ranked candidates whenever it is still usable, unblocked, and plan-eligible, so a session stays sticky unless the sticky account is blocked, exhausted, or ineligible for the current model ([#5203](https://github.com/can1357/oh-my-pi/issues/5203)).

## [17.0.4] - 2026-07-18

Expand Down
83 changes: 75 additions & 8 deletions packages/ai/src/providers/devin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ const CONNECT_END_STREAM_FLAG = 0x02;
* fails fast instead of consuming memory.
*/
const MAX_CONNECT_FRAME_PAYLOAD = 16 * 1024 * 1024;
/**
* Recovery heuristic for opaque Devin `invalid_argument` trailers. This is not
* asserted to be the backend's hard limit: small requests can hit the same
* intermittent error, while compactable message history this large is likely
* to benefit from the existing context-overflow maintenance path.
*/
const LARGE_HISTORY_RECOVERY_BYTES = 512 * 1024;

export const streamDevin: StreamFunction<"devin-agent"> = (
model: Model<"devin-agent">,
Expand Down Expand Up @@ -157,10 +164,14 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
const auth = await fetchDevinAuthMetadata(apiKey, baseUrl, fetchImpl, options?.signal);
const chatBaseUrl = auth.baseUrl ?? baseUrl;
const request = buildDevinChatRequest(model, context, options, apiKey, auth.userJwt);
logger.debug("devin: sending chat request", { model: model.id, tools: context.tools?.length ?? 0 });

const reqBytes = toBinary(GetChatMessageRequestSchema, request);
const gz = gzipSync(reqBytes);
logger.debug("devin: sending chat request", {
model: model.id,
tools: context.tools?.length ?? 0,
requestBytes: reqBytes.byteLength,
compressedBytes: gz.byteLength,
});
const frame = Buffer.alloc(5 + gz.length);
frame[0] = CONNECT_COMPRESSED_FLAG;
frame.writeUInt32BE(gz.length, 1);
Expand Down Expand Up @@ -223,7 +234,53 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
if (flag & CONNECT_END_STREAM_FLAG) {
const trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;
const trailerError = readConnectTrailerError(trailerBytes.toString("utf8").trim());
if (trailerError) throw new AIError.ValidationError(trailerError);
if (trailerError) {
const error = new AIError.ValidationError(trailerError.formatted);
if (
firstTokenTime === undefined &&
trailerError.code.toLowerCase() === "invalid_argument" &&
/\binternal error\b/i.test(trailerError.message)
) {
// The full protobuf also contains the system prompt and tool
// schemas, which history maintenance cannot shrink. Re-encode
// only the repeated history field before choosing recovery.
let activeTailCount = 0;
const lastRole = context.messages.at(-1)?.role;
if (lastRole === "user" || lastRole === "developer") {
activeTailCount = 1;
// A trailing developer message can accompany the current user
// prompt. Earlier user-role records may instead be flushed
// execution history and must remain eligible for compaction.
if (lastRole === "developer") {

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 Exclude all trailing user active-turn messages

The fresh remaining active-tail case is when the same prompt ends with another user-role companion: this backward scan is skipped unless the final role is developer, but #promptWithMessage can append same-turn messages after the prompt (e.g. file mentions after messages.push(message) in packages/coding-agent/src/session/agent-session.ts:8876-8892), and image file mentions convert to role: "user" in packages/coding-agent/src/session/messages.ts:845-872. For a huge current user prompt followed by a small user companion, the huge live prompt is counted in shrinkablePrompts, so the opaque Devin rejection is mislabeled as ContextOverflow and sent through compaction even though that active tail cannot be shrunk.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Investigated against the existing regression suite. The history contains indistinguishable consecutive user-role records: flushed prior execution history must remain compactable, while same-turn user companions must be excluded. Without an active-turn boundary in the Message/Context contract, excluding every trailing user record breaks the prior-history regression and misclassifies large flushed execution output. The current implementation preserves that safety invariant; this finding needs an explicit active-turn boundary before it can be implemented without regressing the earlier P1/P2 fixes.

for (let i = context.messages.length - 2; i >= 0; i--) {
const role = context.messages[i].role;
if (role !== "user" && role !== "developer") break;
activeTailCount++;
}
}
}
const shrinkablePrompts =
activeTailCount > 0
? request.chatMessagePrompts.slice(0, -activeTailCount)
: request.chatMessagePrompts;
const historyBytes = toBinary(
GetChatMessageRequestSchema,
create(GetChatMessageRequestSchema, {
chatMessagePrompts: shrinkablePrompts,
}),
).byteLength;
if (historyBytes >= LARGE_HISTORY_RECOVERY_BYTES) {
AIError.attach(error, AIError.create(AIError.Flag.ContextOverflow));
logger.warn("devin: treating large-history invalid_argument as context overflow", {
model: model.id,
historyBytes,
requestBytes: reqBytes.byteLength,
compressedBytes: gz.byteLength,
});
}
}
throw error;
}
continue;
}

Expand Down Expand Up @@ -569,12 +626,18 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
return prompts;
}

interface ConnectTrailerError {
code: string;
message: string;
formatted: string;
}

/**
* Parse a Connect end-of-stream JSON trailer and return a human-readable error
* string when it carries `{ error: { code, message } }`, else `null`. The trailer
* is untrusted server output, so the shape is checked with guards rather than asserted.
* Parse a Connect end-of-stream JSON trailer and return its structured error
* when it carries `{ error: { code, message } }`, else `null`. The trailer is
* untrusted server output, so the shape is checked with guards rather than asserted.
*/
function readConnectTrailerError(text: string): string | null {
function readConnectTrailerError(text: string): ConnectTrailerError | null {
if (text.length === 0) return null;
let parsed: unknown;
try {
Expand All @@ -588,5 +651,9 @@ function readConnectTrailerError(text: string): string | null {
const code = "code" in err && typeof err.code === "string" ? err.code : "";
const message = "message" in err && typeof err.message === "string" ? err.message : "";
if (!code && !message) return null;
return `Devin stream error${code ? ` ${code}` : ""}: ${message}`;
return {
code,
message,
formatted: `Devin stream error${code ? ` ${code}` : ""}: ${message}`,
};
}
Loading
Loading