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
4 changes: 2 additions & 2 deletions docs/product/codex-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@

ChatGPT 登录的 Codex 订阅额度耗尽时,Desktop 在 Renderer 中用两道账号级布尔门禁用 Composer 提交:账号额度门和 reserve `hardBlocked`。API Key 登录不经过这两道门。它们是界面上的订阅额度预检,不是协议限制;外部 Harness 的 `turn/start` 由 Host 路由,不会发到官方后端。

因此在单个 Composer 选中外部 Agent、Adapter 就绪且没有 codexhost 自身的提交阻塞时,`renderer-codex-usage-gate.ts` 只把该 Composer 对这两道门的订阅快照投影为 `false`,继续走原生提交链路。不写账号、atom 或额度查询缓存,Codex 额度横幅保持显示;其他 Composer、Codex 路径和空输入、附件、运行中等其他原生限制不受影响。外部 Harness 的真实额度与错误由其自身处理。切回 Codex、Composer 移除或扩展卸载时恢复实时原生结果。
因此在单个 Composer 选中外部 Agent、Adapter 就绪且没有 codexhost 自身的提交阻塞时,`renderer-codex-usage-gate.ts` 只把该 Composer 对这两道门的订阅快照投影为 `false`,继续走原生提交链路。不写账号、atom 或额度查询缓存;旁路生效期间只隐藏该 Composer 内的 Codex 额度横幅,恢复原生状态时还原其显示样式。其他 Composer、Codex 路径和空输入、附件、运行中等其他原生限制不受影响。外部 Harness 的真实额度与错误由其自身处理。切回 Codex、Composer 移除或扩展卸载时恢复实时原生结果。

门按其 selector 实际读取的字段识别,不依赖压缩名或 hook 序号。兼容直接 `[store, atom]` 订阅及 Desktop 26.928 的 readonly signal adapter / lazy snapshot wrapper:以成对的订阅 effect 和原生快照一致性验证 wrapper,限量重放 readonly 布尔 selector 的依赖以识别间接 reserve 门;拒绝循环依赖、追踪型 render、混合门和快照不匹配。释放时分别恢复 subscriber 与 React instance 原来的 getter,不把 wrapper 替换成另一个函数。无法唯一识别时保留原生限制,并在 Agent 控件悬停提示中说明。升级后的诊断步骤见 [Desktop 更新兼容性诊断手册](../operations/codex-desktop-upgrade-diagnosis-playbook.md#检查-codex-额度门)。
门按其 selector 实际读取的字段识别,不依赖压缩名或 hook 序号。兼容直接 `[store, atom]` 订阅及 Desktop 26.928 的 readonly signal adapter / lazy snapshot wrapper:以成对的订阅 effect 和原生快照一致性验证 wrapper,限量重放 readonly 布尔 selector 的依赖以识别间接 reserve 门;拒绝循环依赖、追踪型 render、混合门和快照不匹配。Desktop 26.1002 的 Account 门可读取 reserve `active` 作为豁免,并同时由 submit owner 和编辑器到 owner 之间的子组件订阅;这些订阅按组件分别投影。每次重试从 React 当前已提交树重新发现作用域,包括 alternate 切换后才挂载的子组件;无法确认当前作用域时恢复原生限制。释放时分别恢复 subscriber 与 React instance 原来的 getter,不把 wrapper 替换成另一个函数。无法唯一识别时保留原生限制,并在 Agent 控件悬停提示中说明。升级后的诊断步骤见 [Desktop 更新兼容性诊断手册](../operations/codex-desktop-upgrade-diagnosis-playbook.md#检查-codex-额度门)。

## 其他 Harness 的只读账号额度

Expand Down
108 changes: 108 additions & 0 deletions packages/renderer-extension/src/renderer-codex-usage-banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Hides Desktop's Codex usage-limit banner in one external Harness Composer
* while its usage gate is bypassed. That banner (for example "Codex usage
* limit reached ... Upgrade / Reset usage") describes the Codex path only, so
* it is misleading once the Composer submits to another Harness.
*
* The banner is identified by the props of the component Desktop renders it
* with (`banner`, `rateLimitStatus`, `sendBlocked`), searched only inside the
* submit owner's subtree. Only the inline `display` of its outermost host
* elements is changed and restored; React state, atoms and the banner's own
* dismissal bookkeeping are untouched, so the Codex Harness keeps the banner.
*/
type RecordValue = Record<string, unknown>;
type Fiber = { memoizedProps?: unknown; child?: unknown; sibling?: unknown; stateNode?: unknown };

const MAX_FIBERS = 20_000;

function isRecord(value: unknown): value is RecordValue {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isUsageBanner(props: unknown): boolean {
return (
isRecord(props) &&
"banner" in props &&
"rateLimitStatus" in props &&
typeof props.sendBlocked === "boolean"
);
}

function isElement(value: unknown): value is HTMLElement {
return (
isRecord(value) &&
typeof (value as { setAttribute?: unknown }).setAttribute === "function" &&
isRecord((value as { style?: unknown }).style)
);
}

/** Outermost host elements rendered by `fiber`, without descending past them. */
function hostElements(fiber: Fiber, budget: { remaining: number }): HTMLElement[] {
const result: HTMLElement[] = [];
const visit = (node: unknown): void => {
for (let current = node; isRecord(current); current = (current as Fiber).sibling) {
if (budget.remaining-- <= 0) return;
const { stateNode } = current as Fiber;
if (isElement(stateNode)) result.push(stateNode);
else visit((current as Fiber).child);
}
};
visit(fiber.child);
return result;
}

/** Usage banner host elements inside `owner`'s committed subtree. */
export function findCodexUsageBannerElements(owner: unknown): HTMLElement[] {
if (!isRecord(owner)) return [];
const budget = { remaining: MAX_FIBERS };
const found: HTMLElement[] = [];
const stack: unknown[] = [(owner as Fiber).child];
while (stack.length > 0 && budget.remaining > 0) {
const node = stack.pop();
if (!isRecord(node)) continue;
budget.remaining -= 1;
const fiber = node as Fiber;
stack.push(fiber.sibling);
if (isUsageBanner(fiber.memoizedProps)) found.push(...hostElements(fiber, budget));
else stack.push(fiber.child);
}
return budget.remaining > 0 ? found : [];
}

const HIDDEN_ATTRIBUTE = "data-codexhost-usage-banner-hidden";

export interface RendererCodexUsageBanner {
/** Hide the banners currently inside `owner`; restore any no longer there. */
hide(owner: unknown): void;
restore(): void;
}

export function createRendererCodexUsageBanner(): RendererCodexUsageBanner {
const hidden = new Map<HTMLElement, { value: string; priority: string }>();
const show = (element: HTMLElement): void => {
const previous = hidden.get(element);
hidden.delete(element);
if (!previous || element.getAttribute(HIDDEN_ATTRIBUTE) === null) return;
element.removeAttribute(HIDDEN_ATTRIBUTE);
if (previous.value) element.style.setProperty("display", previous.value, previous.priority);
else element.style.removeProperty("display");
};
return {
hide(owner) {
const current = new Set(findCodexUsageBannerElements(owner));
for (const element of [...hidden.keys()]) if (!current.has(element)) show(element);
for (const element of current) {
if (hidden.has(element)) continue;
hidden.set(element, {
value: element.style.getPropertyValue("display"),
priority: element.style.getPropertyPriority("display"),
});
element.setAttribute(HIDDEN_ATTRIBUTE, "");
element.style.setProperty("display", "none", "important");
}
},
restore() {
for (const element of [...hidden.keys()]) show(element);
},
};
}
Loading
Loading