chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-07-01 00:00:29 +08:00
parent 8e4b20f15d
commit ddd97398e7
10 changed files with 251 additions and 37 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules/
__pycache__/
sessions/
logs/
attachments/

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"updatedAt": "2026-06-28T07:29:33.938Z",
"updatedAt": "2026-06-30T15:42:26.421Z",
"replies": []
}

72
findings.md Normal file
View File

@@ -0,0 +1,72 @@
# cc-web Codex hooks 验证发现
## Initial Position
当前需验证的核心边界:
- Codex hooks 官方上属于 Codex/app-server 的生命周期能力。
- cc-web 作为平台客户端,理论上不应硬编码执行某个 skill 的 hooks。
- 需要分别验证官方文档、cc-web 实现、本地环境三个层面,避免把“配置发现”“运行时注入”“技能行为”混成一个问题。
## Findings
### 官方文档背景线程
- Codex hooks 默认开启,可用 `[features] hooks = false` 关闭。
- hooks 从 active config layers 旁边的 `hooks.json``config.toml` 内联 `[hooks]` 发现。
- 常见位置包括 `~/.codex/hooks.json``~/.codex/config.toml``<repo>/.codex/hooks.json``<repo>/.codex/config.toml`
- 项目级 hooks 依赖项目 `.codex/` layer trust未信任项目时仍可加载用户/系统 hooks。
- 非 managed command hooks 需要 review/trust定义变化后按 hash 重新 review。
- 未找到官方文档支持 `thread/start.config.hooks.*` 作为运行时 hooks 注入机制。
- `externalAgentConfig/import``HOOKS` 是 external-agent artifacts 迁移导入,不是每轮运行时 hook 注入。
### 六项断点阶段结论
- 断点 1 `CODEX_HOME/HOME`:当前 local 模式下通。运行中 app-server 看到 `HOME=/home/hdzx``CODEX_HOME` 未设置,能按默认读取 `/home/hdzx/.codex`。custom 模式会把 `CODEX_HOME` 指向 `config/codex-runtime-home`,但当前未启用。
- 断点 2 `thread cwd`:通。目标会话 `00a7cbc2-d0c3-457f-a262-aa5a5859fa54``cwd=/home/cc-web``thread/start``thread/resume` 都使用同一个 `threadParams.cwd`
- 断点 3 项目 `.codex` trust通。`/home/hdzx/.codex/config.toml` 存在 `[projects."/home/cc-web"] trust_level = "trusted"`
- 断点 5 `[features].hooks`未发现关闭。用户级和项目级配置、cc-web thread config、app-server 启动参数均未发现 `hooks = false`
- 断点 4 command hook review/trust不通。app-server 只读 `hooks/list` 返回当前 `/home/cc-web/.codex/hooks.json` 的 7 条 command hook 全部 `trustStatus: "untrusted"`
- 断点 6 marker hook不再需要作为前置验证。既然 `hooks/list` 已确认 command hooks 未 trust按官方语义这些非 managed command hooks 会被跳过marker 只有在 trust 后仍不触发时才需要。
### Command Hook Trust 明细
当前项目 7 条 hook 均来自 `/home/cc-web/.codex/hooks.json`,均 `enabled: true``source: project`,但均 `trustStatus: "untrusted"`
- `preToolUse`: `sha256:a3f36079079ee92b6d39f0ca263b0d23fadafdd4bd2eae8ef3982af4e446fd8b`
- `permissionRequest`: `sha256:db03c3a6c225bdb10010a5b2101d8fcb986d1ee5b2838746d8f003e952d2d08e`
- `postToolUse`: `sha256:7e6a12765c74be11e44fcfcb8f4e6534f138913fc76c539993f62b6e03da6e7a`
- `preCompact`: `sha256:73ec586017f031e8b216256e2eab41a7de8b45395643ce4461c76f212ad9a0ca`
- `sessionStart`: `sha256:b7e002f1913670e919eb42947a8b5dd7c5a9b448b07d1b0aa86bcded841109c0`
- `userPromptSubmit`: `sha256:cd4a71b110e2fdeae8eb47b47bae3e4786baa20b841f35032267cf6914567409`
- `stop`: `sha256:cd543e4852e7bc63704908c599ba9cf0bfc6662b7bd8fe2fea7738cd59d641b7`
### cc-web Codex App 接入链路线程
- 当前 `local` 模式下cc-web 不主动阻断 Codex App hooks 加载app-server 继承 `process.env`,不剥离 `HOME` / `CODEX_HOME``thread/start``thread/resume` 都传入会话 `cwd`
- `codexAppThreadConfig()` 当前只组装 `mcp_servers.*`,没有 hooks/trust 注入;这符合“不走未文档化 `thread/start.config.hooks.*` 主路径”的判断。
- 当前本机 `config/codex.json``mode: "local"`,所以 `custom` 模式的 `CODEX_HOME` 隔离风险未激活。
- 风险:`custom` 模式会将 `CODEX_HOME` 指到 `config/codex-runtime-home`,该目录目前只写认证/model provider 配置,不复制 hooks 配置;如果用户级 hooks 依赖 `~/.codex/hooks.json`custom 模式可能导致用户级 hooks 不加载。
- cc-web 当前没有 Codex App hooks/trust 诊断 UI 或日志;已有日志能看到 app-server 初始化、collaborationMode、MCP startup但不能直接展示 hook 是否发现、是否跳过、trustStatus 是什么。
- 建议:增加只读 hooks 诊断能力,展示 app-server `hooks/list` 的 source/sourcePath/currentHash/trustStatuscustom 模式增加 `CODEX_HOME` 隔离提示或继承策略。
### hapi 对齐线程
- hapi 的 app-server 路径主要依赖 JSON-RPC 事件流、`thread/start.config["mcp_servers.hapi"]``turn/start.collaborationMode`,未看到 app-server 路径主动注入 hooks。
- hapi 的 hooks 主要用于本地 Codex CLI 路径,通过 `-c hooks.SessionStart=...` 做 transcript/session 发现;这不是 app-server `thread/start.config.hooks.*` 机制。
- hapi 因此不能作为“cc-web 应该把 hooks 注入 thread/start”的证据它反而支持MCP 走 `thread/start.config.mcp_servers.*`hooks 不走未文档化的 runtime 注入。
- hapi 也不能推翻官方 hooks 机制或本机 app-server `hooks/list` 结果;它只能说明 hapi 当前产品路径没有依赖 app-server hooks。
- 对 cc-web 的结论应拆开:
- 运行 hooks 的主权仍属于 Codex/app-server 原生 hooks 机制。
- cc-web 不应模拟执行 hooks也不应发明 `thread/start.config.hooks.*` 主路径。
- cc-web 应补的是诊断/信任入口:展示 app-server `hooks/list` 的 trust 状态,并引导用户 review/trust。
### 本地 planning-with-files hooks 条件线程
- 本地 `planning-with-files` 脚本和配置本身可用:`.codex/hooks.json` 配置了 `SessionStart``UserPromptSubmit``PreToolUse``PermissionRequest``PostToolUse``PreCompact``Stop`
- 当前项目根存在 `task_plan.md``findings.md``progress.md`,因此当前时刻满足 root active plan 条件;但这些文件是本轮验证中新建的,不能倒推出目标会话发生时也存在 active plan。
- 手动 smoke test 通过:`sh .codex/hooks/user-prompt-submit.sh` 能输出 `[planning-with-files] ACTIVE PLAN``pre_tool_use.py` / `post_tool_use.py` adapter 能输出预期 JSON 或 progress 提醒。
- 无 active plan 时 hook 会静默;`.planning/sessions/` 存在但当前 session 未 attached 时也会静默。
- 多数 `.sh` hook 脚本没有 `+x`,但 `.codex/hooks.json` 使用 `sh script` / `python3 script` 调用,因此执行位不是当前阻断点。
- 与断点 4 合并后的结论:本地脚本没坏,当前阻断点在 Codex command hook trust7 条项目 command hook 均为 `untrusted`,所以正常 app-server 路径会跳过它们。
- marker 方案仍可用于 trust 后二次验证;但在 hooks 未 trust 前marker 不出现只能证明 trust 拦截,不再是定位根因的必要步骤。

View File

@@ -170,13 +170,14 @@ function createCodexRolloutStore(deps) {
return walkFiles(codexSessionsDir, []).filter((filePath) => filePath.endsWith('.jsonl')).sort().reverse();
}
function getImportedCodexThreadIds() {
function getImportedCodexThreadIds(agent = 'codex') {
const field = agent === 'codexapp' ? 'codexAppThreadId' : 'codexThreadId';
const imported = new Set();
try {
for (const f of fs.readdirSync(sessionsDir).filter((name) => name.endsWith('.json'))) {
try {
const session = normalizeSession(JSON.parse(fs.readFileSync(path.join(sessionsDir, f), 'utf8')));
if (session.codexThreadId) imported.add(session.codexThreadId);
if (session[field]) imported.add(session[field]);
} catch {}
}
} catch {}

20
progress.md Normal file
View File

@@ -0,0 +1,20 @@
# cc-web Codex hooks 验证进度
## Log
- 2026-06-30T00:00:00+08:00 使用 `planning-with-files` 建立本次验证计划。
- 2026-06-30T00:00:00+08:00 确认根目录此前没有 `task_plan.md``findings.md``progress.md`;工作区存在用户已有未提交改动,主线程不会触碰。
- 2026-06-30T00:00:00+08:00 先创建 4 个背景子线程官方文档、cc-web 链路、hapi 对齐、本地 hooks 条件。
- 2026-06-30T00:00:00+08:00 根据用户纠正,收敛到 6 个精准断点,并分别创建子线程验证。
- 2026-06-30T00:00:00+08:00 官方文档背景线程已返回:确认 hooks 属于官方 config layer 发现机制,未确认 `thread/start.config.hooks.*` 是稳定注入路径。
- 2026-06-30T00:00:00+08:00 断点 1、2、3、5 已有子线程/主线程证据:当前通或未关闭;断点 4、6 仍是关键未闭环点。
- 2026-06-30T00:00:00+08:00 断点 4 已返回直接证据app-server `hooks/list` 显示项目 7 条 command hook 全部 untrusted。marker 验证不再是定位根因的必要步骤。
- 2026-06-30T00:00:00+08:00 cc-web 接入链路线程已纳入local 模式不阻断 hookscustom 模式存在 CODEX_HOME 隔离风险;当前缺少 hooks/trust 诊断入口。
- 2026-06-30T00:00:00+08:00 hapi 对齐线程已纳入hapi app-server 路径没用 hooks 注入,支持“不走 thread/start.config.hooks.*”;但不能否定 Codex 原生 hooks/list/trust 机制。
- 2026-06-30T00:00:00+08:00 本地 planning-with-files 条件线程已纳入:脚本和 active-plan 检测本身可用;当前阻断点仍是 command hook untrusted。
- 2026-06-30T00:00:00+08:00 断点 3 返回后完成总线收敛:项目根 `/home/cc-web` 已 trusted但这不等于 `.codex/hooks.json` 里的每条 command hook 已通过 hash trust。最终结论锁定为 command hook trust 阻断。
- 2026-06-30T00:00:00+08:00 断点 4 正式返回并确认app-server 只读 `hooks/list` 对当前 `/home/cc-web/.codex/hooks.json` 返回 7 条 command hook全部 `enabled=true``source=project``trustStatus=untrusted`。这是本轮最直接根因证据。
- 2026-06-30T00:00:00+08:00 断点 5 正式返回并确认:用户级 `[features]` 未设置 `hooks=false`,项目 `.codex/config.toml``[features]`cc-web thread config/app-server 参数/环境变量均未发现关闭 hooks 的配置。hooks feature 开关不是阻断点。
- 2026-06-30T00:00:00+08:00 断点 6 隔离 marker 验证返回:临时 app-server 能通过 `hooks/list` 发现 `/tmp` 项目 hook状态为 `enabled=true/source=project/trustStatus=untrusted``turn/start` 可触发但 marker 不出现、无 hook started/completed 通知。结论是“加载但未 trust”不是“未加载 hooks”。
- 2026-06-30T00:00:00+08:00 修复入口核验:本机 Codex CLI 0.140.0 帮助中没有公开 `hooks trust` 子命令,只有 `--dangerously-bypass-hook-trust`app-server schema 暴露 `hooks/list``HookTrustStatus`,未发现公开 `hooks/trust` 请求。产品修复第一阶段应先做诊断/引导,不直接发明 trust 写入。
- 2026-06-30T00:00:00+08:00 用户执行 Codex hook review 后复核:`/home/hdzx/.codex/config.toml` 新增 `[hooks.state]` 下 7 条 `/home/cc-web/.codex/hooks.json:<event>:0:0``trusted_hash`;按 schema 用 `hooks/list { cwds: ["/home/cc-web"] }` 复查7 条 project hooks 均返回 `trustStatus=trusted`

View File

@@ -4057,13 +4057,10 @@
});
}
if (importSessionBtn) {
if (isCodexAppAgent(currentAgent)) {
importSessionBtn.textContent = 'Codex App 暂不支持导入';
importSessionBtn.disabled = true;
} else {
importSessionBtn.textContent = currentAgent === 'codex' ? '导入本地 Codex 会话' : '导入本地 Claude 会话';
importSessionBtn.disabled = false;
}
importSessionBtn.textContent = isCodexLikeAgent(currentAgent)
? `导入本地 ${AGENT_LABELS[currentAgent]} 会话`
: '导入本地 Claude 会话';
importSessionBtn.disabled = false;
}
updateReloadMcpButtonUI();
}
@@ -8297,9 +8294,7 @@
});
importSessionBtn.addEventListener('click', () => {
newChatDropdown.hidden = true;
if (isCodexAppAgent(currentAgent)) {
appendError('Codex App 模式暂不支持导入本地会话。');
} else if (currentAgent === 'codex') {
if (isCodexLikeAgent(currentAgent)) {
showImportCodexSessionModal();
} else {
showImportSessionModal();
@@ -9970,7 +9965,15 @@
}
function showImportCodexSessionModal() {
if (currentAgent !== 'codex') return;
if (!isCodexLikeAgent(currentAgent)) return;
const importAgent = currentAgent;
const label = AGENT_LABELS[importAgent] || 'Codex';
const contextTitle = importAgent === 'codexapp'
? '从 Codex App rollout 历史导入'
: '从 Codex rollout 历史导入';
const contextCopy = importAgent === 'codexapp'
? '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复对话文本、工具调用和 token 统计,并绑定 Codex App 线程用于后续续接。'
: '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。';
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'import-codex-session-overlay';
@@ -9978,12 +9981,12 @@
overlay.innerHTML = `
<div class="modal-panel modal-panel-wide">
<div class="modal-header">
<span class="modal-title">导入本地 Codex 会话</span>
<span class="modal-title">导入本地 ${escapeHtml(label)} 会话</span>
<button class="modal-close-btn" id="ics-close-btn">✕</button>
</div>
<div class="modal-body" id="ics-body">
${buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。')}
<div class="modal-loading">正在加载 Codex 本地历史…</div>
${buildAgentContextCard(importAgent, contextTitle, contextCopy)}
<div class="modal-loading">正在加载 ${escapeHtml(label)} 本地历史…</div>
</div>
</div>
`;
@@ -10002,11 +10005,11 @@
const body = overlay.querySelector('#ics-body');
if (!body) return;
if (!items || items.length === 0) {
body.innerHTML = `${buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。')}<div class="modal-empty">未找到本地 Codex 会话</div>`;
body.innerHTML = `${buildAgentContextCard(importAgent, contextTitle, contextCopy)}<div class="modal-empty">未找到本地 ${escapeHtml(label)} 会话</div>`;
return;
}
body.innerHTML = buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。');
body.innerHTML = buildAgentContextCard(importAgent, contextTitle, contextCopy);
items.forEach((sess) => {
const item = document.createElement('div');
item.className = 'import-item';
@@ -10050,11 +10053,11 @@
btn.textContent = sess.alreadyImported ? '重新导入' : '导入';
btn.addEventListener('click', () => {
const confirmed = sess.alreadyImported
? confirm('已导入过此 Codex 会话,重新导入将覆盖已有内容。确认继续?')
: confirm('将解析本地 Codex rollout 历史并导入当前 Web 视图。确认继续?');
? confirm(`已导入过此 ${label} 会话,重新导入将覆盖已有内容。确认继续?`)
: confirm(`将解析本地 ${label} rollout 历史并导入当前 Web 视图。确认继续?`);
if (!confirmed) return;
close();
send({ type: 'import_codex_session', threadId: sess.threadId, rolloutPath: sess.rolloutPath });
send({ type: 'import_codex_session', agent: importAgent, threadId: sess.threadId, rolloutPath: sess.rolloutPath });
});
item.appendChild(info);
@@ -10063,7 +10066,7 @@
});
};
send({ type: 'list_codex_sessions' });
send({ type: 'list_codex_sessions', agent: importAgent });
}
// --- Helpers ---

View File

@@ -313,16 +313,22 @@ function createFakeClaudeHistory(homeDir) {
return { sessionId, projectDir: 'tmp-project', filePath };
}
function createFakeCodexHistory(homeDir) {
function createFakeCodexHistory(homeDir, options = {}) {
const sessionsDir = path.join(homeDir, '.codex', 'sessions', '2026', '03', '12');
mkdirp(sessionsDir);
const threadId = 'codex-import-thread';
const rolloutPath = path.join(sessionsDir, 'rollout-2026-03-12T00-00-00-codex-import-thread.jsonl');
const threadId = options.threadId || 'codex-import-thread';
const cwd = options.cwd || '/tmp/project-b';
const userText = options.userText || 'Codex import prompt';
const answerText = options.answerText || 'Codex import answer';
const source = options.source || 'exec';
const cliVersion = options.cliVersion || '0.114.0';
const fileStamp = options.fileStamp || '2026-03-12T00-00-00';
const rolloutPath = path.join(sessionsDir, `rollout-${fileStamp}-${threadId}.jsonl`);
const rolloutLines = [
JSON.stringify({
timestamp: '2026-03-12T00:00:00.000Z',
type: 'session_meta',
payload: { id: threadId, cwd: '/tmp/project-b', cli_version: '0.114.0', source: 'exec' },
payload: { id: threadId, cwd, cli_version: cliVersion, source },
}),
JSON.stringify({
timestamp: '2026-03-12T00:00:00.100Z',
@@ -336,7 +342,7 @@ function createFakeCodexHistory(homeDir) {
JSON.stringify({
timestamp: '2026-03-12T00:00:01.000Z',
type: 'event_msg',
payload: { type: 'user_message', message: 'Codex import prompt' },
payload: { type: 'user_message', message: userText },
}),
JSON.stringify({
timestamp: '2026-03-12T00:00:02.000Z',
@@ -344,7 +350,7 @@ function createFakeCodexHistory(homeDir) {
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'Codex import answer' }],
content: [{ type: 'output_text', text: answerText }],
},
}),
JSON.stringify({
@@ -419,7 +425,7 @@ function createFakeCodexHistory(homeDir) {
estimated_bytes INTEGER NOT NULL DEFAULT 0
);
INSERT INTO threads (id, rollout_path, created_at, updated_at, source, model_provider, cwd, title, sandbox_policy, approval_mode, cli_version)
VALUES ('${threadId}', '${rolloutPath.replace(/'/g, "''")}', 1, 2, 'exec', 'OpenAI', '/tmp/project-b', 'Codex import prompt', '{}', 'never', '0.114.0');
VALUES ('${threadId}', '${rolloutPath.replace(/'/g, "''")}', 1, 2, '${source}', 'OpenAI', '${cwd.replace(/'/g, "''")}', '${userText.replace(/'/g, "''")}', '{}', 'never', '${cliVersion}');
INSERT INTO logs (ts, ts_nanos, level, target, thread_id) VALUES (1, 0, 'INFO', 'test', '${threadId}');
`);
@@ -500,6 +506,15 @@ function assertFrontendGenerationControlsContract() {
!source.includes(staleDefaultApprovalWarning),
'Frontend should not show the stale default-mode approval warning after Codex App approvals are supported'
);
assert(
!source.includes('Codex App 暂不支持导入') && !source.includes('Codex App 模式暂不支持导入'),
'Frontend should not disable Codex App native session import'
);
assert(
source.includes("send({ type: 'list_codex_sessions', agent: importAgent })") &&
source.includes("send({ type: 'import_codex_session', agent: importAgent"),
'Frontend Codex import modal should pass the selected Codex-like agent'
);
}
function assertFrontendComposerMcpContract() {
@@ -674,6 +689,14 @@ async function main() {
createFakeClaudeHistory(homeDir);
createFakeCodexConfig(homeDir);
const codexFixture = createFakeCodexHistory(homeDir);
const codexAppImportFixture = createFakeCodexHistory(homeDir, {
threadId: 'codexapp-import-thread',
cwd: '/tmp/project-c',
userText: 'Codex App import prompt',
answerText: 'Codex App import answer',
source: 'vscode',
fileStamp: '2026-03-12T00-00-10',
});
const port = await getFreePort();
const password = 'Regression!234';
@@ -1870,6 +1893,48 @@ async function main() {
assert(importedCodex.messages?.[0]?.content === 'Codex import prompt', 'Codex import kept wrapper instructions');
assert(importedCodex.totalUsage?.inputTokens === 20, 'Codex import usage parse failed');
ws.send(JSON.stringify({ type: 'list_codex_sessions', agent: 'codexapp' }));
const codexAppImportSessions = await nextMessage(messages, ws, (msg) => msg.type === 'codex_sessions');
const codexAppImportItem = codexAppImportSessions.sessions.find((item) => item.threadId === codexAppImportFixture.threadId);
assert(codexAppImportItem, 'Codex App session listing failed');
assert(codexAppImportItem.agent === 'codexapp', 'Codex App import listing should echo target agent');
assert(codexAppImportItem.alreadyImported === false, 'Codex App import should not reuse old Codex imported state');
ws.send(JSON.stringify({
type: 'import_codex_session',
agent: 'codexapp',
threadId: codexAppImportItem.threadId,
rolloutPath: codexAppImportItem.rolloutPath,
}));
const importedCodexApp = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.title === 'Codex App import prompt');
assert(importedCodexApp.messages?.[0]?.content === 'Codex App import prompt', 'Codex App import parsed wrong first message');
assert(importedCodexApp.totalUsage?.inputTokens === 20, 'Codex App import usage parse failed');
const importedCodexAppPath = path.join(sessionsDir, `${importedCodexApp.sessionId}.json`);
const storedImportedCodexApp = JSON.parse(fs.readFileSync(importedCodexAppPath, 'utf8'));
assert(storedImportedCodexApp.agent === 'codexapp', 'Codex App import should persist codexapp agent');
assert(storedImportedCodexApp.codexAppThreadId === codexAppImportFixture.threadId, 'Codex App import should persist codexAppThreadId');
assert(!storedImportedCodexApp.codexThreadId, 'Codex App import should not persist legacy codexThreadId');
ws.send(JSON.stringify({ type: 'list_codex_sessions', agent: 'codexapp' }));
const codexAppImportSessionsAfter = await nextMessage(messages, ws, (msg) => msg.type === 'codex_sessions');
const codexAppImportItemAfter = codexAppImportSessionsAfter.sessions.find((item) => item.threadId === codexAppImportFixture.threadId);
assert(codexAppImportItemAfter?.alreadyImported === true, 'Codex App import listing should mark codexAppThreadId as imported');
ws.send(JSON.stringify({ type: 'delete_session', sessionId: importedCodexApp.sessionId }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && !msg.sessions.some((s) => s.id === importedCodexApp.sessionId));
assert(!fs.existsSync(importedCodexAppPath), 'Deleting Codex App imported session did not remove cc-web session JSON');
assert(fs.existsSync(codexAppImportFixture.rolloutPath), 'Deleting Codex App imported session should keep rollout history for recovery');
ws.send(JSON.stringify({
type: 'import_codex_session',
agent: 'codexapp',
threadId: codexAppImportFixture.threadId,
rolloutPath: codexAppImportFixture.rolloutPath,
}));
const restoredCodexApp = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.title === 'Codex App import prompt');
assert(restoredCodexApp.sessionId !== importedCodexApp.sessionId, 'Codex App deleted session should be recreated from rollout history');
assert(restoredCodexApp.messages?.[0]?.content === 'Codex App import prompt', 'Codex App re-import should restore messages after cc-web deletion');
const importedSessionId = importedCodex.sessionId;
ws.send(JSON.stringify({ type: 'delete_session', sessionId: importedSessionId }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && !msg.sessions.some((s) => s.id === importedSessionId));

View File

@@ -6314,7 +6314,7 @@ wss.on('connection', (ws, req) => {
handleImportNativeSession(ws, msg);
break;
case 'list_codex_sessions':
handleListCodexSessions(ws);
handleListCodexSessions(ws, msg);
break;
case 'import_codex_session':
handleImportCodexSession(ws, msg);
@@ -9878,8 +9878,13 @@ function handleImportNativeSession(ws, msg) {
sendSessionList(ws);
}
function handleListCodexSessions(ws) {
const imported = getImportedCodexThreadIds();
function resolveCodexImportAgent(value) {
return value === 'codexapp' ? 'codexapp' : 'codex';
}
function handleListCodexSessions(ws, msg = {}) {
const importAgent = resolveCodexImportAgent(msg?.agent);
const imported = getImportedCodexThreadIds(importAgent);
const items = [];
const seen = new Set();
for (const filePath of getCodexRolloutFiles()) {
@@ -9896,6 +9901,7 @@ function handleListCodexSessions(ws) {
cliVersion: parsed.meta.cliVersion || '',
source: parsed.meta.source || '',
rolloutPath: filePath,
agent: importAgent,
alreadyImported: imported.has(parsed.meta.threadId),
});
}
@@ -9904,6 +9910,7 @@ function handleListCodexSessions(ws) {
function handleImportCodexSession(ws, msg) {
const threadId = String(msg?.threadId || '').trim();
const importAgent = resolveCodexImportAgent(msg?.agent);
if (!threadId) {
return wsSend(ws, { type: 'error', message: '缺少 threadId' });
}
@@ -9932,7 +9939,8 @@ function handleImportCodexSession(ws, msg) {
for (const f of fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'))) {
try {
const s = normalizeSession(JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8')));
if (s.codexThreadId === threadId) { existingSession = s; break; }
const runtimeThreadId = importAgent === 'codexapp' ? s.codexAppThreadId : s.codexThreadId;
if (runtimeThreadId === threadId) { existingSession = s; break; }
} catch {}
}
} catch {}
@@ -9944,10 +9952,11 @@ function handleImportCodexSession(ws, msg) {
created: existingSession?.created || new Date().toISOString(),
updated: new Date().toISOString(),
pinnedAt: existingSession?.pinnedAt || null,
agent: 'codex',
agent: importAgent,
claudeSessionId: null,
codexThreadId: threadId,
importedFrom: 'codex',
codexThreadId: importAgent === 'codex' ? threadId : null,
codexAppThreadId: importAgent === 'codexapp' ? threadId : null,
importedFrom: importAgent,
importedRolloutPath: parsed.filePath,
model: existingSession?.model || null,
permissionMode: existingSession?.permissionMode || 'yolo',

43
task_plan.md Normal file
View File

@@ -0,0 +1,43 @@
# cc-web Codex hooks 验证计划
## Goal
验证 cc-web 的 Codex App 模式下Codex hooks 应由谁加载、当前链路是否让 app-server 发现项目/全局 hooks以及 `planning-with-files` hook 没体现效果的真实原因。
## Status
- [x] 建立验证计划
- [x] 官方文档验证:确认 app-server/hooks 官方支持边界
- [x] cc-web 实现链路验证:确认 cwd、CODEX_HOME、thread params、config 注入逻辑
- [x] 本地环境/触发条件验证:确认 hook 文件、active plan、trust/feature 开关等条件
- [x] 汇总结论:明确是否需要 cc-web 改造,以及改造点
## Focused Breakpoints
- [x] app-server 进程看到的 `CODEX_HOME` / `HOME` 是否正确
- [x] `thread/start` / `thread/resume``cwd` 是否为 `/home/cc-web`
- [x] `/home/cc-web/.codex` 项目层是否被 Codex trust
- [x] `.codex/hooks.json` 里的 command hook 是否已 review/trust
- [x] `[features].hooks` 是否被关闭
- [x] 最小 marker hook 是否能证明 app-server 触发 hooks
## Final Synthesis
- 当前目标会话没看到 `planning-with-files` hook 效果,主因是项目 hooks 的 command hash 未被 Codex trust`hooks/list` 已显示 7 条 command hook 全部 `untrusted`
- `HOME/CODEX_HOME``thread cwd`、项目根 trust、`[features].hooks` 这些前置条件在当前 local 模式下都不是阻断点。
- `planning-with-files` 脚本本身有效;手动 smoke test 可输出 active plan。它的效果被 Codex command hook trust 层挡住,而不是 skill 逻辑坏。
- cc-web 不应自行模拟执行 hooks也不应把 `thread/start.config.hooks.*` 当主路径;应让 Codex app-server 原生 hooks/list/trust 机制工作,并补诊断/信任入口。
## Questions
1. `thread/start.config.hooks.*` 是否有官方文档支持,还是不应作为主路径?
2. cc-web 是否应该自行执行 hooks还是只保证 app-server 能看到官方 config layers
3. 当前项目 hooks 没生效,更可能是哪一层断了?
4. 如何用最小可复现实验证明 app-server 端是否加载并执行 hooks
## Constraints
- 不修改业务代码。
- 不覆盖用户已有未提交改动。
- 代码理解优先使用 `codebase-memory-mcp`,不用 graphify。
- 官方 Codex 行为优先参考 OpenAI 官方文档。