Merge branch 'codex/20260831-fix-history-quota-errors-6c2a91e4-fix-history-quota-errors-6c2a91e4'

This commit is contained in:
inman
2026-09-01 11:46:28 +08:00
14 changed files with 480 additions and 49 deletions

View File

@@ -6,8 +6,8 @@ import type {
ConversationToolNode,
PublicUsage,
} from '../contracts';
import { isAIGatewayUserContextMissing } from '../../../shared/ai-gateway-error-details';
import type { PiRpcEvent } from './rpc-client';
import { projectPiProviderFailure } from './provider-failure';
import { subagentDetailsOfResult } from '../subagent-protocol';
import {
isProductToolName,
@@ -45,17 +45,6 @@ function retryFailure(message: string) {
};
}
function providerFailure(message: unknown) {
if (typeof message === 'string' && isAIGatewayUserContextMissing(message)) {
return {
code: 'CODING_PROVIDER_AUTH_REQUIRED' as const,
message: '模型服务身份上下文无效,请重试;若仍失败请重新登录。',
recoverable: true,
};
}
return retryFailure('模型服务请求失败,请稍后重试。');
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
@@ -294,7 +283,7 @@ export class PiEventProjector {
...run,
status: 'error',
terminalReason: 'failed',
error: providerFailure(event.finalError),
error: projectPiProviderFailure(event.finalError),
},
}];
}
@@ -485,7 +474,7 @@ export class PiEventProjector {
...snapshot.run,
status: 'error',
terminalReason: 'failed',
error: providerFailure(assistant.errorMessage),
error: projectPiProviderFailure(assistant.errorMessage),
},
}];
}

View File

@@ -0,0 +1,25 @@
import type { CodingRuntimePublicError } from '../contracts';
import { isAIGatewayUserContextMissing } from '../../../shared/ai-gateway-error-details';
import { getAIGatewayErrorKind } from '../../../shared/ai-gateway-error-kind';
export function projectPiProviderFailure(message: unknown): CodingRuntimePublicError {
if (typeof message === 'string' && isAIGatewayUserContextMissing(message)) {
return {
code: 'CODING_PROVIDER_AUTH_REQUIRED',
message: '模型服务身份上下文无效,请重试;若仍失败请重新登录。',
recoverable: true,
};
}
if (typeof message === 'string' && getAIGatewayErrorKind(message) === 'quota_exhausted') {
return {
code: 'CODING_PROVIDER_QUOTA_EXHAUSTED',
message: '词元点数余额不足,请充值后重试。',
recoverable: false,
};
}
return {
code: 'CODING_RUNTIME_START_FAILED',
message: '模型服务请求失败,请稍后重试。',
recoverable: true,
};
}

View File

@@ -14,6 +14,7 @@ import type {
} from './event-projector';
import { isProductToolName, productToolDetailsOfResult } from '../product-tool-protocol';
import { subagentDetailsOfResult } from '../subagent-protocol';
import { projectPiProviderFailure } from './provider-failure';
export interface PiSessionSnapshotInput {
snapshot: ConversationSnapshot;
@@ -106,23 +107,6 @@ function activePath(entriesValue: unknown, leafId: unknown): SessionEntryRecord[
return path.reverse();
}
function retainedTail(path: SessionEntryRecord[]): SessionEntryRecord[] {
const compactionIndex = path.findLastIndex((entry) => entry.type === 'compaction');
if (compactionIndex < 0) return path;
const compaction = path[compactionIndex];
const firstKeptEntryId = compaction.firstKeptEntryId;
if (typeof firstKeptEntryId !== 'string') unreadable();
const firstKeptIndex = path.findIndex(
(entry, index) => index < compactionIndex && entry.id === firstKeptEntryId,
);
if (firstKeptIndex < 0) unreadable();
return [
compaction,
...path.slice(firstKeptIndex, compactionIndex),
...path.slice(compactionIndex + 1),
];
}
async function contentBlocks(
messageId: string,
content: unknown,
@@ -265,6 +249,19 @@ async function projectEntries(
...(stopReason ? { stopReason } : {}),
};
nodes.push(node);
if (message.stopReason === 'error') {
const failure = projectPiProviderFailure(message.errorMessage);
if (failure.code === 'CODING_PROVIDER_AUTH_REQUIRED'
|| failure.code === 'CODING_PROVIDER_QUOTA_EXHAUSTED') {
nodes.push({
kind: 'notice',
id: `${messageId}:provider-error`,
code: failure.code,
level: 'error',
message: failure.message,
});
}
}
if (!Array.isArray(message.content)) continue;
for (const value of message.content) {
const block = asRecord(value);
@@ -310,6 +307,16 @@ function reconcileLiveIds(
liveNodes: ConversationNode[],
): ConversationNode[] {
const used = new Set<string>();
const durableCompactions = durableNodes.filter((node) => node.kind === 'compaction');
const liveCompactions = liveNodes.filter((node) => node.kind === 'compaction');
const compactionMatches = new Map<string, Extract<ConversationNode, { kind: 'compaction' }>>();
for (
let durableIndex = durableCompactions.length - 1, liveIndex = liveCompactions.length - 1;
durableIndex >= 0 && liveIndex >= 0;
durableIndex -= 1, liveIndex -= 1
) {
compactionMatches.set(durableCompactions[durableIndex].id, liveCompactions[liveIndex]);
}
return durableNodes.map((node) => {
if (node.kind === 'message') {
const live = liveNodes.find((candidate) => candidate.kind === 'message'
@@ -338,10 +345,8 @@ function reconcileLiveIds(
};
}
if (node.kind === 'compaction') {
const live = liveNodes.findLast(
(candidate) => candidate.kind === 'compaction' && !used.has(candidate.id),
);
if (!live || live.kind !== 'compaction') return node;
const live = compactionMatches.get(node.id);
if (!live) return node;
used.add(live.id);
return { ...node, id: live.id, runId: live.runId };
}
@@ -375,7 +380,7 @@ export async function projectPiSessionSnapshot(
const response = asRecord(input.entries);
const state = asRecord(input.state);
if (!response || !state) unreadable();
const path = retainedTail(activePath(response.entries, response.leafId));
const path = activePath(response.entries, response.leafId);
const durableNodes = await projectEntries(path, input);
const nodes = reconcileLiveIds(durableNodes, input.snapshot.nodes);
return {