feat: vendor and persist Codex fast mode

This commit is contained in:
叶林立
2026-08-20 00:06:39 +08:00
parent bd7ff978e9
commit d3bf562189
13 changed files with 804 additions and 20 deletions
+16 -3
View File
@@ -40,8 +40,12 @@
- 上游来源:<https://github.com/ogulcancelik/pi-extensions/tree/main/packages/pi-minimal-footer>
- 初始导入快照:`77bd1e175003cd08e6d05d9e7fed695f86ae87b7``@ogulcancelik/pi-minimal-footer@0.1.10`)。
- 该目录从明确 commit 的上游源码导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`
- `pi-extension-codex-fast-mode/`:为符合条件的 Codex Responses 请求设置 priority service tier,并把新会话默认值跨会话持久化。
- 上游来源:<https://github.com/Firstp1ck/pi-coding-agent-forge/tree/main/pi-extension-codex-fast-mode>
- 初始导入快照:`f1d0efd24a7f4ae99d19e10c5f4c3770a3bdd845``@firstpick/pi-extension-codex-fast-mode@0.1.1`)。
- 该目录从上游源码仓库导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`
- `pi-condense@2.9.1`:总结已完成的工具调用批次,以短 stub 替换历史原始输出,并通过 `context_tree_query` 按需恢复;组合包在用户尚未配置 `contextPrune.enabled` 时默认开启。
- 根包还固定安装 `@tavily/pi-extension@0.1.2``@keenable/pi-search@0.1.2``pi-hashline-edit@0.8.3``typescript-language-server@5.3.0``typescript@6.0.3``pi-context-view``@firstpick/pi-extension-codex-fast-mode``@gotgenes/pi-permission-system``pi-minimal-footer``pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖。
- 根包还固定安装 `@tavily/pi-extension@0.1.2``@keenable/pi-search@0.1.2``pi-hashline-edit@0.8.3``typescript-language-server@5.3.0``typescript@6.0.3``pi-context-view``@firstpick/pi-extension-codex-fast-mode``@gotgenes/pi-permission-system``pi-minimal-footer``pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖。
- `install.sh`:先安装根组合包,再交互检查 Hippo Memory CLI、Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Hippo 项目目录与 `hippo init` 始终由用户自行决定。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。
- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。
- `uninstall.sh`:先移除根组合包;若检测到 Hippo Memory CLI,再明确询问是否卸载确认属于 npm 全局安装的 `hippo-memory`,默认保留且始终不删除 `.hippo/` 或用户记忆数据。其他共享终端环境和机器工具不卸载。
@@ -67,7 +71,7 @@
- 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。
- `pi-context-view` 只观察上下文占用,不参与压缩策略。
- `pi-condense` 负责压缩已经进入会话的历史工具结果,与 Context Mode 的输入隔离职责互补;`extensions/condense.ts` 只在 `settings.json` 尚无 `contextPrune.enabled` 时写入 `true`,必须保留用户显式设置的 `false`,配置无效时不得覆盖原文件。其他参数沿用上游默认,包括 `agent-message` 触发模式和 skill 路径保护。
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier,由 `/fast-mode` 在会话内控制
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier`/fast-mode on|off` 同时更新当前分支记录与 Pi agent 目录中的 owner-only 全局默认值;新会话继承全局值,已有分支记录优先,配置缺失或无效时回退为关闭
- 权限策略默认允许常规工具,允许 FFF 工具;拒绝 Bash 直搜和敏感凭据路径;Git 非只读操作、包管理、外部目录、文件/系统/网络高风险操作与普通 MCP 调用先由 `pi-permission-system` 判为 `ask`
- `pi-permission-auto-review` 不是独立 `tool_call` gate,而是 `pi-permission-system` authorizer chain 中名为 `auto-review` 的链路;只复核权限基线产生的 `ask`,不会重复处理已 `allow` 或已 `deny` 的请求。
- reviewer 返回 `allow` 时自动批准、返回 `deny` 时直接拒绝,配置、模型、认证、超时或响应异常时必须 `defer` 到正常人工提示。`pi-permission-system` 的 delegation envelope 继续禁止 authorizer 自动批准全部 `path` 请求;`external_directory` 对内置只读路径工具(`read``find``grep``ls`)接受 reviewer 的 `allow`,写入、编辑、Bash、未知工具和其他外部访问仍转人工。
@@ -88,7 +92,7 @@
- 优先在目标扩展目录内完成改动;不要让一个扩展依赖另一个扩展的未公开内部实现。
- 保留原项目的 `LICENSE`、版权信息和必要的来源说明。
- 扩展运行目录中的 `config.json`、日志、构建产物、覆盖率目录和依赖目录属于本地状态,不应提交;`config/pi-permission-system.json``config/pi-hashline-edit.json``config/lsp.json` 分别是权限链、Hashline 默认行为和 LSP 后端的组合包权威源配置,必须提交并维护。
- 外部 Pi 扩展依赖必须在根 `package.json` 中使用精确版本,并更新根 `package-lock.json`;不要用仓库级 `.pi/settings.json` 代替组合包依赖。
- 外部 Pi 扩展依赖必须在根 `package.json` 中使用精确版本,自维护 Fast mode 等扩展必须使用本地 `file:` 依赖,并更新根 `package-lock.json`;不要用仓库级 `.pi/settings.json` 代替组合包依赖。
- 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前 `better-sqlite3@12.11.1` 仍由 Context Mode 使用并保持精确放行。官方 Hippo CLI 是独立机器级 npm 包,不得因此向根包批量批准其他 install scripts。
- Pi 核心包只作为宿主 peer dependencies,不得在组合包内再安装或打包一套 Pi runtime;保留根 `.npmrc` 的 peer 安装策略。
- `pi-tool-search/` 的根依赖必须保持 `file:./pi-tool-search`,组合包直接加载 `pi-tool-search/extensions/index.ts`;同步上游时先核对本地增量动态工具改造,禁止用旧 npm 入口或上游旧宿主实现直接覆盖。
@@ -141,6 +145,15 @@
- CLI 版本权威源是 `config/hippo-memory-version`;修改它时必须同步上游源码快照、README、AGENTS、安装/升级 mock 测试和扩展加载验证。
- 扩展依赖 PATH 中的全局 `hippo`。CLI 缺失时必须 fail-safe,不得阻止其他扩展加载;项目未执行 `hippo init` 时不得擅自创建 `.hippo/`
## `pi-extension-codex-fast-mode` 开发约定
- 扩展入口是 `pi-extension-codex-fast-mode/index.ts`,测试保留在该目录的 `tests/`,来源记录是 `UPSTREAM.md`
- 同步上游必须从明确 commit 移植并保留 `LICENSE`、作者和来源;不得导入嵌套 `.git``node_modules` 或构建产物。
- 全局默认配置必须位于 Pi agent 目录的 `extensions/pi-extension-codex-fast-mode/config.json`,使用原子写入和 owner-only 权限;无效或不可读配置必须安全回退为关闭且不得静默覆盖。
- 新会话使用全局默认值,已有分支的最新有效 custom entry 必须继续优先;修改持久化、分支恢复、provider/API 资格或请求 payload 时必须同步 README、技术文档和测试。
- 根包必须通过 `file:./pi-extension-codex-fast-mode` 提供依赖并直接加载仓库源码,不得同时安装或加载 npm 预编译入口。
- 修改该扩展后至少运行包内 `npm test``npm run check``npm run smoke``npm pack --dry-run --json`;加载或依赖入口变化还要运行根扩展联合加载测试和实际 packed tarball 的隔离安装验证。
## `pi-tool-search` 开发约定
+3 -4
View File
@@ -9,7 +9,7 @@
- `@keenable/pi-search@0.1.2`:提供紧凑的 `keenable_search` / `keenable_fetch`,并加载其搜索 skillAPI key 可选。
- `pi-mcp-adapter@2.26.0`:通过单个共享 adapter 实例接入机器现有的 CodeGraph 和 Exa 托管 MCP,避免重复注册 Pi 的全局 MCP flag 与命令。
- `pi-context-view@0.4.2`:查看上下文占用。
- `@firstpick/pi-extension-codex-fast-mode@0.1.1`为 Codex provider 提供会话 `/fast-mode`
- 本仓库维护的 `pi-extension-codex-fast-mode`:基于 `@firstpick/pi-extension-codex-fast-mode@0.1.1`为 Codex provider 提供会话持久化的 `/fast-mode`
- `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。
- `hippo-memory-pi/`:从官方 `hippo-memory v1.33.0` 源码导入的 Pi Extension,提供项目记忆注入、错误捕获和 sleep consolidation。
- `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程压缩。
@@ -102,7 +102,7 @@ PI_PACKAGE_SOURCE="$PWD" ./install.sh
PI_PACKAGE_SOURCE="git:git@bitbucket.org:siakitem/my-pi.git" ./update.sh
```
`package.json` 对外部依赖使用精确版本,对本仓库维护的包使用 `file:` 路径;`package-lock.json` 固定完整依赖树。不要再用项目级 `.pi/settings.json` 重复安装这些扩展,否则同一扩展可能被加载两次。
`package.json` 对外部依赖使用精确版本,对本仓库维护的包(包括 Codex Fast mode使用 `file:` 路径;`package-lock.json` 固定完整依赖树。不要再用项目级 `.pi/settings.json` 重复安装这些扩展,否则同一扩展可能被加载两次。
Hippo 官方 Pi Extension 存在于上游 Git 仓库的 `extensions/pi-extension/`,但没有包含在
`hippo-memory@1.33.0` npm tarball 中。本仓库从官方 `v1.33.0` /
@@ -313,5 +313,4 @@ Hippo CLI:从升级后的组合包读取固定版本,先通过 npm 确认版
### Codex fast mode
该扩展只在 `openai-codex` provider 的 Responses 请求上加入
`service_tier: "priority"`。使用 `/fast-mode on|off|status` 控制当前会话,不影响
其他 provider。
`service_tier: "priority"`。使用 `/fast-mode on|off` 修改当前分支状态时,也会把全局默认值原子写入 Pi agent 目录下的 `extensions/pi-extension-codex-fast-mode/config.json`;新会话继承该默认值,恢复已有会话时仍由分支内最新记录优先。`/fast-mode status` 同时显示当前分支状态和新会话默认值。配置目录与文件分别使用 `0700` / `0600` 权限;配置缺失或无效时安全回退为关闭。其他 provider 不受影响
+14 -11
View File
@@ -9,7 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@ff-labs/pi-fff": "0.10.5",
"@firstpick/pi-extension-codex-fast-mode": "0.1.1",
"@firstpick/pi-extension-codex-fast-mode": "file:./pi-extension-codex-fast-mode",
"@gotgenes/pi-permission-system": "file:./pi-permission-system",
"@keenable/pi-search": "0.1.2",
"@ogulcancelik/pi-codex-compaction": "0.1.3",
@@ -2263,16 +2263,8 @@
}
},
"node_modules/@firstpick/pi-extension-codex-fast-mode": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@firstpick/pi-extension-codex-fast-mode/-/pi-extension-codex-fast-mode-0.1.1.tgz",
"integrity": "sha512-P/+chadW+mDDwKgy6v3lvhtJVwFKuDEmROnk4ei2oJBkZJbjgNSVGV2ZgEaRoOACmbmvwaaXPnG5WId5kgTPnA==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*"
}
"resolved": "pi-extension-codex-fast-mode",
"link": true
},
"node_modules/@gotgenes/pi-permission-system": {
"resolved": "pi-permission-system",
@@ -7025,6 +7017,17 @@
"zod": "^3.25.28 || ^4"
}
},
"pi-extension-codex-fast-mode": {
"name": "@firstpick/pi-extension-codex-fast-mode",
"version": "0.1.1",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*"
}
},
"pi-minimal-footer": {
"name": "@ogulcancelik/pi-minimal-footer",
"version": "0.1.10-my-pi.1",
+3 -2
View File
@@ -11,6 +11,7 @@
"files": [
"extensions",
"hippo-memory-pi",
"pi-extension-codex-fast-mode",
"pi-rtk-optimizer",
"pi-minimal-footer",
"pi-tool-search/extensions",
@@ -49,7 +50,7 @@
"./node_modules/context-mode/build/adapters/pi/extension.js",
"./extensions/lsp.ts",
"./node_modules/pi-context-view/src/index.ts",
"./node_modules/@firstpick/pi-extension-codex-fast-mode/index.ts",
"./pi-extension-codex-fast-mode/index.ts",
"./hippo-memory-pi/index.ts",
"./node_modules/@ogulcancelik/pi-codex-compaction/index.ts",
"./pi-minimal-footer/index.ts",
@@ -66,7 +67,7 @@
},
"dependencies": {
"@ff-labs/pi-fff": "0.10.5",
"@firstpick/pi-extension-codex-fast-mode": "0.1.1",
"@firstpick/pi-extension-codex-fast-mode": "file:./pi-extension-codex-fast-mode",
"@gotgenes/pi-permission-system": "file:./pi-permission-system",
"@keenable/pi-search": "0.1.2",
"@ogulcancelik/pi-codex-compaction": "0.1.3",
@@ -0,0 +1,36 @@
# Development guide: Codex Fast Mode for Pi
Contributor implementation and validation notes for the locally maintained fork.
[Back to README](README.md) · [Technical reference](TECHNICAL.md) · [Upstream provenance](UPSTREAM.md)
## Validation
```bash
npm test
npm run check
npm run smoke
npm pack --dry-run --json
```
Tests cover command grammar and completions, branch restoration, cross-session defaults, owner-only atomic config writes, invalid-config fallback, persistence failures, busy mutation guards, status publication, exact provider/API isolation, malformed payload passthrough, non-mutation, and tier overwrite. They make no authenticated or paid model requests.
## State model
The effective state is resolved in this order:
1. Read the global default from `<agentDir>/extensions/pi-extension-codex-fast-mode/config.json`; missing or invalid data defaults to off.
2. Reconstruct the latest valid `codex-fast-mode` custom entry on the active session branch.
3. Use the branch entry when present; otherwise use the global default.
A successful mode command appends a session entry when the branch value changes and atomically writes the global default even when the branch already has the requested value. This allows an explicit `/fast-mode on` or `/fast-mode off` to repair or recreate global configuration.
If the global write fails, the branch state still changes and the user receives a warning. Concurrent Pi processes use atomic replacement; the last successful command determines the default for future sessions.
## Request isolation
When enabled, the extension shallow-copies only a plain serialized payload whose provider is exactly `openai-codex` and API is exactly `openai-codex-responses`, then writes `service_tier: "priority"`. It preserves all other top-level fields and does not mutate the original payload.
## Maintenance
This directory is maintained directly in `my-pi`, is not a submodule, and must not acquire a nested `.git`, `node_modules`, or generated build output. Compare local changes with the snapshot recorded in [UPSTREAM.md](UPSTREAM.md) before porting later upstream work.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Firstpick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
# Codex Fast Mode for Pi
A locally maintained fork of `@firstpick/pi-extension-codex-fast-mode@0.1.1` that adds a cross-session Fast mode default.
## Behavior
- `/fast-mode on` and `/fast-mode off` update the current session and the global default for future sessions.
- A new session inherits the global default.
- A resumed session branch keeps its own latest recorded setting, overriding the global default.
- Only `openai-codex` requests using `openai-codex-responses` receive `service_tier: "priority"`.
- Other providers, APIs, payload fields, and authentication remain unchanged.
The global preference is stored at:
```text
<Pi agent dir>/extensions/pi-extension-codex-fast-mode/config.json
```
Normally this is `~/.pi/agent/extensions/pi-extension-codex-fast-mode/config.json`. The directory and file are restricted to owner-only permissions (`0700` and `0600`). Missing, unreadable, or invalid configuration fails safely to Fast mode off.
## Commands
- `/fast-mode` toggles Fast mode and saves the new global default.
- `/fast-mode on` enables it and makes new sessions default to on.
- `/fast-mode off` disables it and makes new sessions default to off.
- `/fast-mode status` reports the active branch setting and the default for new sessions.
Mode changes are rejected while Pi is busy; status remains available.
## Installation in my-pi
The root bundle loads this directory directly and declares it as a local `file:` dependency. Do not also install the upstream npm package, or Pi may load the command twice.
## Source and technical details
See [UPSTREAM.md](UPSTREAM.md) for provenance, [TECHNICAL.md](TECHNICAL.md) for runtime behavior, and [DEVELOPMENT.md](DEVELOPMENT.md) for validation. The upstream MIT license is retained in [LICENSE](LICENSE).
+44
View File
@@ -0,0 +1,44 @@
# Technical reference: Codex Fast Mode for Pi
Advanced behavior, configuration, compatibility, and troubleshooting for the locally maintained fork.
[Back to README](README.md) · [Development guide](DEVELOPMENT.md) · [Upstream provenance](UPSTREAM.md)
## Commands
- `/fast-mode` — toggle the active branch and global default.
- `/fast-mode on` — enable the active branch and make future sessions default to on.
- `/fast-mode off` — disable the active branch and make future sessions default to off.
- `/fast-mode status` — show both the active branch setting and global default.
Mutation is refused while Pi is busy. Status remains available.
## Persistence
The extension stores the global default in:
```text
<Pi agent dir>/extensions/pi-extension-codex-fast-mode/config.json
```
The file contains a single boolean setting:
```json
{
"enabled": true
}
```
Writes use a temporary file followed by atomic rename. The extension enforces `0700` on the containing directory and `0600` on the file. Missing configuration defaults to off. Malformed or unreadable configuration also fails to off and emits a warning without overwriting the bad file automatically.
Pi custom session entries remain branch-scoped. On session start or tree navigation, the latest valid branch entry overrides the global default. Consequently, resumed historical branches retain their own decision while genuinely new sessions inherit the latest global setting.
## Eligibility and credit use
Fast mode applies only when the selected model has provider `openai-codex` and API `openai-codex-responses`. For eligible plain-object payloads, the extension returns a shallow copy with `service_tier: "priority"`; unsupported providers, APIs, disabled state, and malformed payloads pass through unchanged.
Fast mode expresses request intent and cannot guarantee that the upstream service accepts or provides priority processing. OpenAI controls account and model eligibility, credit multipliers, and current service terms.
## Privacy and limitations
The extension does not inspect credentials, change authentication, choose a model, or make a separate network request. The only additional local data is the boolean global preference. If multiple Pi processes change the setting concurrently, the last successful atomic write becomes the default for future sessions.
+13
View File
@@ -0,0 +1,13 @@
# Upstream provenance
- Upstream repository: <https://github.com/Firstp1ck/pi-coding-agent-forge/tree/main/pi-extension-codex-fast-mode>
- Imported package: `@firstpick/pi-extension-codex-fast-mode@0.1.1`
- Initial source snapshot: `f1d0efd24a7f4ae99d19e10c5f4c3770a3bdd845` (2026-08-18)
- Published npm source commit: `239829d747deae4d9c0f039421ab29440f40f1a2`; the only later upstream change in this directory at the imported snapshot was the package version bump from `0.1.0` to `0.1.1`.
- License: MIT; the upstream `LICENSE` file is retained unchanged.
## Local maintenance
This directory was imported from the upstream Git repository and is maintained directly in `my-pi`. It is not a submodule and does not retain upstream `.git` state, dependencies, or build output.
Local changes add an owner-only, atomically written global Fast mode preference. New sessions inherit that preference, while existing Pi custom session entries continue to preserve branch-specific history. The root bundle loads this source directly and uses a local `file:` dependency instead of the npm package.
+261
View File
@@ -0,0 +1,261 @@
import { chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { dirname, join } from "node:path";
import {
getAgentDir,
type ExtensionAPI,
type ExtensionCommandContext,
type ExtensionContext,
} from "@earendil-works/pi-coding-agent";
export const FAST_MODE_STATUS_KEY = "codex-fast-mode";
export const FAST_MODE_STATE_ENTRY_TYPE = "codex-fast-mode";
export const FAST_MODE_SERVICE_TIER = "priority";
export const FAST_MODE_CONFIG_DIRECTORY = "pi-extension-codex-fast-mode";
const OWNER_ONLY_DIRECTORY_MODE = 0o700;
const OWNER_ONLY_FILE_MODE = 0o600;
export type FastModeState = {
enabled: boolean;
};
export type FastModeModel = {
provider?: unknown;
api?: unknown;
};
export type FastModeCommand = "toggle" | "on" | "off" | "status" | "invalid";
export type FastModeConfigLoadResult = {
state: FastModeState;
warning?: string;
};
export type FastModeConfigSaveResult =
| { success: true }
| { success: false; error: string };
export type FastModeExtensionDependencies = {
configPath?: string;
loadGlobalState?: (path: string) => FastModeConfigLoadResult;
saveGlobalState?: (path: string, state: FastModeState) => FastModeConfigSaveResult;
};
/** Returns true only for object records that can safely receive a shallow request rewrite. */
export function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== "object") return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
/** Fast mode is limited to Pi's subscription-backed Codex Responses provider. */
export function isFastModeEligibleModel(model: FastModeModel | undefined): boolean {
return model?.provider === "openai-codex" && model.api === "openai-codex-responses";
}
/**
* Applies Fast-mode request intent without mutating the provider's serialized payload.
* Undefined is intentional: Pi then retains the original payload unchanged.
*/
export function transformFastModeRequest(
enabled: boolean,
model: FastModeModel | undefined,
payload: unknown,
): Record<string, unknown> | undefined {
if (!enabled || !isFastModeEligibleModel(model) || !isPlainObject(payload)) return undefined;
return { ...payload, service_tier: FAST_MODE_SERVICE_TIER };
}
/** Resolves the owner-only global preference file beneath Pi's agent directory. */
export function getFastModeConfigPath(agentDir: string): string {
return join(agentDir, "extensions", FAST_MODE_CONFIG_DIRECTORY, "config.json");
}
/** Reads the cross-session default. Missing or invalid configuration fails safely to off. */
export function loadGlobalFastModeState(path: string): FastModeConfigLoadResult {
try {
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
if (!isPlainObject(parsed) || typeof parsed.enabled !== "boolean") {
return {
state: { enabled: false },
warning: `Ignoring invalid Fast mode config at ${path}; expected { "enabled": boolean }.`,
};
}
return { state: { enabled: parsed.enabled } };
} catch (error) {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
return { state: { enabled: false } };
}
return {
state: { enabled: false },
warning: `Could not read Fast mode config at ${path}: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/** Atomically writes the cross-session default with owner-only permissions. */
export function saveGlobalFastModeState(path: string, state: FastModeState): FastModeConfigSaveResult {
const directory = dirname(path);
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
try {
mkdirSync(directory, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
chmodSync(directory, OWNER_ONLY_DIRECTORY_MODE);
writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, {
encoding: "utf8",
flag: "wx",
mode: OWNER_ONLY_FILE_MODE,
});
chmodSync(temporaryPath, OWNER_ONLY_FILE_MODE);
renameSync(temporaryPath, path);
chmodSync(path, OWNER_ONLY_FILE_MODE);
return { success: true };
} catch (error) {
try {
unlinkSync(temporaryPath);
} catch {
// The temporary file may not have been created.
}
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/** Reconstructs the latest branch snapshot, falling back to the cross-session default. */
export function reconstructFastModeState(
entries: readonly unknown[],
defaultEnabled = false,
): FastModeState {
let enabled = defaultEnabled;
for (const entry of entries) {
if (!isPlainObject(entry)) continue;
if (entry.type !== "custom" || entry.customType !== FAST_MODE_STATE_ENTRY_TYPE) continue;
if (!isPlainObject(entry.data) || typeof entry.data.enabled !== "boolean") continue;
enabled = entry.data.enabled;
}
return { enabled };
}
export function parseFastModeCommand(args: string): FastModeCommand {
const normalized = args.trim().toLowerCase();
if (!normalized) return "toggle";
if (normalized === "on" || normalized === "off" || normalized === "status") return normalized;
return "invalid";
}
export function fastModeArgumentCompletions(prefix: string) {
const normalized = prefix.trim().toLowerCase();
return ["on", "off", "status"]
.filter((value) => value.startsWith(normalized))
.map((value) => ({ value, label: value }));
}
function isBusy(ctx: Pick<ExtensionCommandContext, "isIdle" | "hasPendingMessages">): boolean {
return !ctx.isIdle() || ctx.hasPendingMessages();
}
function publishStatus(ctx: Pick<ExtensionContext, "ui">, enabled: boolean): void {
ctx.ui.setStatus(FAST_MODE_STATUS_KEY, enabled ? "on" : "off");
}
function formatStatus(enabled: boolean, globalEnabled: boolean): string {
const state = enabled ? "on" : "off";
const globalState = globalEnabled ? "on" : "off";
return `Fast mode: ${state}. New sessions default to ${globalState}. It only requests priority service for openai-codex/openai-codex-responses.`;
}
export default function codexFastModeExtension(
pi: ExtensionAPI,
dependencies: FastModeExtensionDependencies = {},
): void {
const configPath = dependencies.configPath ?? getFastModeConfigPath(getAgentDir());
const loadGlobalState = dependencies.loadGlobalState ?? loadGlobalFastModeState;
const saveGlobalState = dependencies.saveGlobalState ?? saveGlobalFastModeState;
let enabled = false;
let globalEnabled = false;
let lastConfigWarning: string | undefined;
const restoreState = (ctx: ExtensionContext): void => {
const loaded = loadGlobalState(configPath);
globalEnabled = loaded.state.enabled;
enabled = reconstructFastModeState(ctx.sessionManager.getBranch(), globalEnabled).enabled;
publishStatus(ctx, enabled);
if (loaded.warning && loaded.warning !== lastConfigWarning) {
ctx.ui.notify(loaded.warning, "warning");
}
lastConfigWarning = loaded.warning;
};
const persistSessionState = (): void => {
pi.appendEntry<FastModeState>(FAST_MODE_STATE_ENTRY_TYPE, { enabled });
};
const setEnabled = (ctx: ExtensionCommandContext, nextEnabled: boolean): void => {
const stateChanged = enabled !== nextEnabled;
const saveResult = saveGlobalState(configPath, { enabled: nextEnabled });
if (saveResult.success) {
globalEnabled = nextEnabled;
}
if (stateChanged) {
enabled = nextEnabled;
persistSessionState();
}
publishStatus(ctx, enabled);
if (stateChanged) {
ctx.ui.notify(enabled
? "Fast mode enabled. Supported Codex requests will use priority service, and new sessions will inherit this setting."
: "Fast mode disabled. Supported Codex requests will keep their existing service tier, and new sessions will inherit this setting.", "info");
} else {
ctx.ui.notify(`Fast mode is already ${enabled ? "on" : "off"}. The global default was refreshed.`, "info");
}
if (!saveResult.success) {
ctx.ui.notify(`The current session was updated, but the Fast mode global default could not be saved: ${saveResult.error}`, "warning");
}
};
pi.on("session_start", (_event, ctx) => {
restoreState(ctx);
});
pi.on("session_tree", (_event, ctx) => {
restoreState(ctx);
});
pi.on("before_provider_request", (event, ctx) => {
return transformFastModeRequest(enabled, ctx.model, event.payload);
});
pi.registerCommand("fast-mode", {
description: "Toggle persistent Codex subscription Fast mode. Usage: /fast-mode [on|off|status]",
getArgumentCompletions: fastModeArgumentCompletions,
handler: async (args, ctx) => {
const command = parseFastModeCommand(args);
if (command === "status") {
publishStatus(ctx, enabled);
ctx.ui.notify(formatStatus(enabled, globalEnabled), "info");
return;
}
if (command === "invalid") {
ctx.ui.notify("Usage: /fast-mode [on|off|status]", "warning");
return;
}
if (isBusy(ctx)) {
ctx.ui.notify("Fast mode cannot be changed while the session is busy. Run /fast-mode status to inspect it.", "warning");
return;
}
setEnabled(ctx, command === "toggle" ? !enabled : command === "on");
},
});
}
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@firstpick/pi-extension-codex-fast-mode",
"version": "0.1.1",
"description": "Persistent Codex subscription Fast mode for Pi requests.",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/Firstp1ck/pi-coding-agent-forge/tree/main/pi-extension-codex-fast-mode#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/Firstp1ck/pi-coding-agent-forge.git",
"directory": "pi-extension-codex-fast-mode"
},
"keywords": [
"pi-package",
"pi",
"pi-coding-agent",
"extension",
"openai-codex",
"fast-mode"
],
"pi": {
"extensions": [
"./index.ts"
]
},
"scripts": {
"test": "node --experimental-strip-types --test tests/*.test.ts",
"check": "node --experimental-strip-types --check index.ts",
"smoke": "node --experimental-strip-types --input-type=module -e \"import('./index.ts')\""
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*"
},
"files": [
"index.ts",
"README.md",
"DEVELOPMENT.md",
"TECHNICAL.md",
"UPSTREAM.md",
"LICENSE"
],
"engines": {
"node": ">=22.19.0"
}
}
@@ -0,0 +1,299 @@
import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
import codexFastModeExtension, {
FAST_MODE_SERVICE_TIER,
FAST_MODE_STATE_ENTRY_TYPE,
FAST_MODE_STATUS_KEY,
fastModeArgumentCompletions,
getFastModeConfigPath,
isFastModeEligibleModel,
isPlainObject,
loadGlobalFastModeState,
parseFastModeCommand,
reconstructFastModeState,
saveGlobalFastModeState,
transformFastModeRequest,
type FastModeState,
} from "../index.ts";
type StatusUpdate = { key: string; value: string | undefined };
type Notification = { message: string; level: string };
type Handler = (event: any, ctx: any) => unknown;
function createHarness(options: {
entries?: unknown[];
busy?: boolean;
pending?: boolean;
globalEnabled?: boolean;
saveError?: string;
} = {}) {
const handlers = new Map<string, Handler>();
const commands = new Map<string, any>();
const entries = options.entries ?? [];
const statusUpdates: StatusUpdate[] = [];
const notifications: Notification[] = [];
const appendCalls: Array<{ customType: string; data: unknown }> = [];
const loadCalls: string[] = [];
const saveCalls: Array<{ path: string; state: FastModeState }> = [];
let busy = options.busy ?? false;
let pending = options.pending ?? false;
let globalEnabled = options.globalEnabled ?? false;
const context = {
model: { provider: "openai-codex", api: "openai-codex-responses" },
sessionManager: { getBranch: () => entries },
isIdle: () => !busy,
hasPendingMessages: () => pending,
ui: {
setStatus(key: string, value: string | undefined) {
statusUpdates.push({ key, value });
},
notify(message: string, level: string) {
notifications.push({ message, level });
},
},
} as unknown as ExtensionCommandContext;
codexFastModeExtension({
on(event: string, handler: Handler) {
handlers.set(event, handler);
},
registerCommand(name: string, command: unknown) {
commands.set(name, command);
},
appendEntry(customType: string, data: unknown) {
appendCalls.push({ customType, data });
entries.push({ type: "custom", customType, data });
},
} as unknown as ExtensionAPI, {
configPath: "/test-agent/extensions/pi-extension-codex-fast-mode/config.json",
loadGlobalState(path) {
loadCalls.push(path);
return { state: { enabled: globalEnabled } };
},
saveGlobalState(path, state) {
saveCalls.push({ path, state });
if (options.saveError) return { success: false, error: options.saveError };
globalEnabled = state.enabled;
return { success: true };
},
});
return {
appendCalls,
commands,
context,
entries,
handlers,
loadCalls,
notifications,
saveCalls,
setBusy(value: boolean) { busy = value; },
setPending(value: boolean) { pending = value; },
statusUpdates,
};
}
test("plain-object guard excludes malformed payloads", () => {
assert.equal(isPlainObject({}), true);
assert.equal(isPlainObject(Object.create(null)), true);
assert.equal(isPlainObject([]), false);
assert.equal(isPlainObject(new Date()), false);
assert.equal(isPlainObject(null), false);
assert.equal(isPlainObject("payload"), false);
});
test("provider eligibility is exact", () => {
assert.equal(isFastModeEligibleModel({ provider: "openai-codex", api: "openai-codex-responses" }), true);
assert.equal(isFastModeEligibleModel({ provider: "openai", api: "openai-codex-responses" }), false);
assert.equal(isFastModeEligibleModel({ provider: "openai-codex", api: "openai-responses" }), false);
assert.equal(isFastModeEligibleModel(undefined), false);
});
test("request transformer is isolated, non-mutating, and overwrites only service_tier", () => {
const payload = { model: "gpt-5.6-codex", service_tier: "default", nested: { preserved: true } };
const transformed = transformFastModeRequest(true, { provider: "openai-codex", api: "openai-codex-responses" }, payload);
assert.deepEqual(transformed, {
model: "gpt-5.6-codex",
service_tier: FAST_MODE_SERVICE_TIER,
nested: { preserved: true },
});
assert.notStrictEqual(transformed, payload);
assert.strictEqual(transformed?.nested, payload.nested);
assert.equal(payload.service_tier, "default");
assert.equal(transformFastModeRequest(false, { provider: "openai-codex", api: "openai-codex-responses" }, payload), undefined);
assert.equal(transformFastModeRequest(true, { provider: "openai", api: "openai-codex-responses" }, payload), undefined);
assert.equal(transformFastModeRequest(true, { provider: "openai-codex", api: "openai-codex-responses" }, []), undefined);
});
test("branch reconstruction prefers the latest valid snapshot over the global default", () => {
assert.deepEqual(reconstructFastModeState([], true), { enabled: true });
assert.deepEqual(reconstructFastModeState([
{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } },
{ type: "custom", customType: "other", data: { enabled: false } },
{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: "invalid" } },
{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } },
], true), { enabled: false });
});
test("global config round-trips atomically with owner-only permissions", () => {
const agentDir = mkdtempSync(join(tmpdir(), "fast-mode-test-"));
const configPath = getFastModeConfigPath(agentDir);
try {
assert.deepEqual(loadGlobalFastModeState(configPath), { state: { enabled: false } });
assert.deepEqual(saveGlobalFastModeState(configPath, { enabled: true }), { success: true });
assert.deepEqual(loadGlobalFastModeState(configPath), { state: { enabled: true } });
assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { enabled: true });
assert.equal(statSync(configPath).mode & 0o777, 0o600);
assert.equal(statSync(join(agentDir, "extensions", "pi-extension-codex-fast-mode")).mode & 0o777, 0o700);
} finally {
rmSync(agentDir, { recursive: true, force: true });
}
});
test("invalid or unreadable global config fails safely to off", () => {
const directory = mkdtempSync(join(tmpdir(), "fast-mode-invalid-"));
const configPath = join(directory, "config.json");
try {
writeFileSync(configPath, "{ not-json", "utf8");
const invalid = loadGlobalFastModeState(configPath);
assert.deepEqual(invalid.state, { enabled: false });
assert.match(invalid.warning ?? "", /Could not read/u);
writeFileSync(configPath, JSON.stringify({ enabled: "yes" }), "utf8");
const wrongType = loadGlobalFastModeState(configPath);
assert.deepEqual(wrongType.state, { enabled: false });
assert.match(wrongType.warning ?? "", /expected/u);
chmodSync(configPath, 0o600);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("command grammar and completions are deterministic", () => {
assert.equal(parseFastModeCommand(""), "toggle");
assert.equal(parseFastModeCommand(" ON "), "on");
assert.equal(parseFastModeCommand("off"), "off");
assert.equal(parseFastModeCommand("status"), "status");
assert.equal(parseFastModeCommand("on now"), "invalid");
assert.deepEqual(fastModeArgumentCompletions("o"), [
{ value: "on", label: "on" },
{ value: "off", label: "off" },
]);
assert.deepEqual(fastModeArgumentCompletions("status"), [{ value: "status", label: "status" }]);
});
test("a new session inherits the global default while a branch snapshot takes precedence", () => {
const inherited = createHarness({ globalEnabled: true });
const inheritedStart = inherited.handlers.get("session_start");
assert.ok(inheritedStart);
inheritedStart!({}, inherited.context as unknown as ExtensionContext);
assert.deepEqual(inherited.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "on" });
const overridden = createHarness({
globalEnabled: true,
entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } }],
});
const overriddenStart = overridden.handlers.get("session_start");
assert.ok(overriddenStart);
overriddenStart!({}, overridden.context as unknown as ExtensionContext);
assert.deepEqual(overridden.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
});
test("extension restores state, publishes status, and persists session and global mutations", async () => {
const harness = createHarness({
entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }],
});
const sessionStart = harness.handlers.get("session_start");
const request = harness.handlers.get("before_provider_request");
const command = harness.commands.get("fast-mode");
assert.ok(sessionStart && request && command);
sessionStart!({}, harness.context as unknown as ExtensionContext);
assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "on" });
assert.deepEqual(request!({ payload: { service_tier: "standard", keep: true } }, harness.context), {
service_tier: FAST_MODE_SERVICE_TIER,
keep: true,
});
await command.handler("off", harness.context);
assert.deepEqual(harness.appendCalls, [{ customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } }]);
assert.deepEqual(harness.saveCalls.map((call) => call.state), [{ enabled: false }]);
assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
assert.equal(request!({ payload: { keep: true } }, harness.context), undefined);
await command.handler("status", harness.context);
assert.match(harness.notifications.at(-1)?.message ?? "", /Fast mode: off.*New sessions default to off/u);
assert.equal(harness.notifications.at(-1)?.level, "info");
});
test("an explicit no-op mode command still refreshes the global default", async () => {
const harness = createHarness({ globalEnabled: true });
const sessionStart = harness.handlers.get("session_start");
const command = harness.commands.get("fast-mode");
assert.ok(sessionStart && command);
sessionStart!({}, harness.context as unknown as ExtensionContext);
await command.handler("on", harness.context);
assert.equal(harness.appendCalls.length, 0);
assert.deepEqual(harness.saveCalls.map((call) => call.state), [{ enabled: true }]);
});
test("a global save failure leaves the session usable and reports the persistence failure", async () => {
const harness = createHarness({ saveError: "read-only filesystem" });
const sessionStart = harness.handlers.get("session_start");
const command = harness.commands.get("fast-mode");
assert.ok(sessionStart && command);
sessionStart!({}, harness.context as unknown as ExtensionContext);
await command.handler("on", harness.context);
assert.deepEqual(harness.appendCalls, [{ customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }]);
assert.equal(harness.notifications.at(-1)?.level, "warning");
assert.match(harness.notifications.at(-1)?.message ?? "", /could not be saved.*read-only filesystem/u);
});
test("extension rejects every mutation while busy but leaves status readable", async () => {
const harness = createHarness({ busy: true });
const sessionStart = harness.handlers.get("session_start");
const command = harness.commands.get("fast-mode");
assert.ok(sessionStart && command);
sessionStart!({}, harness.context as unknown as ExtensionContext);
await command.handler("on", harness.context);
await command.handler("", harness.context);
await command.handler("off", harness.context);
assert.equal(harness.appendCalls.length, 0);
assert.equal(harness.saveCalls.length, 0);
assert.equal(harness.notifications.filter((item) => item.level === "warning").length, 3);
await command.handler("status", harness.context);
assert.match(harness.notifications.at(-1)?.message ?? "", /Fast mode: off/u);
assert.equal(harness.notifications.at(-1)?.level, "info");
harness.setBusy(false);
harness.setPending(true);
await command.handler("on", harness.context);
assert.equal(harness.appendCalls.length, 0);
assert.equal(harness.saveCalls.length, 0);
});
test("tree navigation reconstructs the active branch state", () => {
const harness = createHarness({
globalEnabled: true,
entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }],
});
const sessionStart = harness.handlers.get("session_start");
const sessionTree = harness.handlers.get("session_tree");
assert.ok(sessionStart && sessionTree);
sessionStart!({}, harness.context as unknown as ExtensionContext);
harness.entries.push({ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } });
sessionTree!({}, harness.context as unknown as ExtensionContext);
assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
});
+13
View File
@@ -12,6 +12,7 @@ const hasPi = spawnSync("pi", ["--version"], { encoding: "utf8" }).status === 0;
test("all package extensions load together without global registration conflicts", { skip: !hasPi }, async () => {
const packageJson = JSON.parse(await readFile(join(repositoryRoot, "package.json"), "utf8")) as {
dependencies: Record<string, string>;
files: string[];
pi: { extensions: string[] };
};
assert.ok(
@@ -22,6 +23,18 @@ test("all package extensions load together without global registration conflicts
packageJson.pi.extensions.includes("./pi-minimal-footer/index.ts"),
"the bundle must load the locally maintained minimal footer",
);
assert.ok(
packageJson.pi.extensions.includes("./pi-extension-codex-fast-mode/index.ts"),
"the bundle must load the locally maintained Codex Fast mode extension",
);
assert.equal(
packageJson.dependencies["@firstpick/pi-extension-codex-fast-mode"],
"file:./pi-extension-codex-fast-mode",
);
assert.ok(
packageJson.files.includes("pi-extension-codex-fast-mode"),
"the packed bundle must include the locally maintained Codex Fast mode source",
);
assert.equal(packageJson.dependencies["@ogulcancelik/pi-minimal-footer"], "file:./pi-minimal-footer");
assert.equal(
packageJson.pi.extensions.some((entry) => entry.includes("pi-hermes-memory") || entry.includes("the-forge-flow")),