修复 DirectProject 空历史注入导致新项目第一条消息失败
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / Backend tests (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 2m13s
Project CI / Native shell tests (push) Successful in 6m19s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m1s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m23s
Project CI / AI game creator shell web tests (push) Successful in 1m41s
Project CI / Repository checks (push) Successful in 2m16s
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / Backend tests (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 2m13s
Project CI / Native shell tests (push) Successful in 6m19s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m1s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m23s
Project CI / AI game creator shell web tests (push) Successful in 1m41s
Project CI / Repository checks (push) Successful in 2m16s
- build_direct_project_history_injection_params 在历史为空时返回 Ok(None),调用方跳过 thread/inject_items(codex app-server 0.155.1 起拒绝 items: []) - 新增两条单测:空历史不得构造载荷、有历史仍构造 1 条;cargo test direct_project_history_wire 3 passed、cargo fmt --check 通过 - 复验:真 AGC CLI → 真 app-server → loopback Responses 夹具从「4 秒失败、requests=[]」变为「requests=3、markers.ready=true、exit=0」 - 记录第二层未解决问题:同一用例账本仍落 phase=interrupted(app-server 收尾连接终止),两条运行时验收继续未勾选 - decision-log 与 pitfalls 记录缺陷、修复与证据;DirectProject 里程碑补本轮核对
This commit is contained in:
+55
-3
@@ -76,12 +76,20 @@ fn compact_history_images(value: &mut Value, remaining_bytes: &mut usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造 `thread/inject_items` 载荷;**历史为空时返回 `None`**,调用方不得发空载荷。
|
||||
///
|
||||
/// codex app-server 0.155.1 起把 `items: []` 当协议错误拒绝(`items must not be empty`),
|
||||
/// 而 CLI / 宿主探针路径(`--direct-codex-chat`)在全新项目上没有前端先写用户条目的步骤,
|
||||
/// 于是「第一次对话」会直接失败并报「执行通道中断」。空历史本来就没有可注入的内容。
|
||||
pub(super) fn build_direct_project_history_injection_params(
|
||||
history_root: &Path,
|
||||
thread_id: &str,
|
||||
) -> Result<Value, platform_llm::LlmError> {
|
||||
) -> Result<Option<Value>, platform_llm::LlmError> {
|
||||
let canonical_items = read_direct_project_history_items_at(history_root)
|
||||
.map_err(platform_llm::LlmError::InvalidRequest)?;
|
||||
if canonical_items.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut remaining_image_bytes = DIRECT_PROJECT_HISTORY_IMAGE_TOTAL_MAX_BYTES;
|
||||
let items = canonical_items
|
||||
.iter()
|
||||
@@ -100,13 +108,57 @@ pub(super) fn build_direct_project_history_injection_params(
|
||||
if let Some(error) = direct_project_history_injection_oversize_error(¶ms, payload_bytes) {
|
||||
return Err(platform_llm::LlmError::InvalidRequest(error));
|
||||
}
|
||||
Ok(params)
|
||||
Ok(Some(params))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{compact_history_images, DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT};
|
||||
use super::{
|
||||
build_direct_project_history_injection_params, compact_history_images,
|
||||
DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
|
||||
// 回归:全新项目没有 `.agent/conversations/project.jsonl`,历史为空时**不得**构造
|
||||
// `items: []` 载荷——codex app-server 0.155.1 起会以 `items must not be empty` 拒绝,
|
||||
// 表现为 CLI / 宿主探针的第一次对话直接「执行通道中断」。
|
||||
#[test]
|
||||
fn empty_history_produces_no_injection_payload() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
let params = build_direct_project_history_injection_params(root.path(), "thread-empty")
|
||||
.expect("build params for empty history");
|
||||
assert!(params.is_none(), "空历史不得构造 thread/inject_items 载荷");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_history_still_produces_an_injection_payload() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
let conversations = root.path().join(".agent/conversations");
|
||||
fs::create_dir_all(&conversations).expect("create conversations dir");
|
||||
fs::write(
|
||||
conversations.join("project.jsonl"),
|
||||
concat!(
|
||||
"{\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"user\",",
|
||||
"\"id\":\"codex-item-1\",\"content\":[{\"type\":\"input_text\",\"text\":\"先做一版\"}]}}\n"
|
||||
),
|
||||
)
|
||||
.expect("write history");
|
||||
let params = build_direct_project_history_injection_params(root.path(), "thread-history")
|
||||
.expect("build params for existing history")
|
||||
.expect("有历史时必须构造载荷");
|
||||
assert_eq!(
|
||||
params.get("threadId").and_then(|v| v.as_str()),
|
||||
Some("thread-history")
|
||||
);
|
||||
assert_eq!(
|
||||
params
|
||||
.get("items")
|
||||
.and_then(|value| value.as_array())
|
||||
.map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_image_budget_omits_only_wire_preview_when_exhausted() {
|
||||
|
||||
@@ -3377,19 +3377,22 @@ impl CodexAppServerConnection {
|
||||
}
|
||||
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
if thread_created {
|
||||
let params =
|
||||
match build_direct_project_history_injection_params(history_root, &thread_id) {
|
||||
Ok(params) => params,
|
||||
Err(error) => {
|
||||
match build_direct_project_history_injection_params(history_root, &thread_id) {
|
||||
// 空历史没有可注入内容:app-server 会拒绝 `items: []`(items must not be empty),
|
||||
// 新项目的第一次对话就发生在这里,因此必须跳过而不是发空载荷。
|
||||
Ok(None) => {}
|
||||
Ok(Some(params)) => {
|
||||
if let Err(error) = self.request("thread/inject_items", params).await {
|
||||
self.release_thread(snapshot, &thread_id).await;
|
||||
return Err(error.into());
|
||||
return Err(DirectTurnRunFailure::from(
|
||||
platform_llm::LlmError::Transport(error),
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(error) = self.request("thread/inject_items", params).await {
|
||||
self.release_thread(snapshot, &thread_id).await;
|
||||
return Err(DirectTurnRunFailure::from(
|
||||
platform_llm::LlmError::Transport(error),
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
self.release_thread(snapshot, &thread_id).await;
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,3 +73,12 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
|
||||
- 运行时(待补):真实 app-server 会话下的新回合、杀进程重开、页面重进、分页与终止。
|
||||
- 边界:订阅过期、回执竞态、不可显示切片、无 `turn.completed` 的残回合、工具输出超长截断与脱敏。
|
||||
- 环境注意:`rehype-highlight` 已装齐后 `ChatMarkdownMessage` / `AgentMessageContent` 转绿;Node 26 下 vitest 的 jsdom 用例需要 `--localstorage-file` 才能拿到 `window.localStorage`(`clientApi.test.ts` / `chatPromptPolish.test.tsx`),已记入 `docs/project-memory/shared-memory/pitfalls.md`。
|
||||
|
||||
## 本轮核对(2026-09-28,真实 app-server 运行时:先把路修通)
|
||||
|
||||
为了给两条运行时验收(杀进程重进、进程存活期间重进)建可复跑的证路,本轮先用既有夹具 `apps/ai-game-creator-shell/scripts/direct-execution-production-fixture.mjs`(真 AGC CLI → 真 app-server → loopback Responses fixture,无账号、无付费 Provider)跑通基线,结果先挖出一个**真实缺陷**并修掉:
|
||||
|
||||
- **缺陷**:全新项目的第一次对话直接失败。夹具 `--cases completed` 下 AGC 进程 4 秒退出、`requests=[]`,stderr 只有 `Codex app-server JSON-RPC 失败:items must not be empty`。根因是 `thread_created` 时无条件发 `thread/inject_items`,而空项目没有 `.agent/conversations/project.jsonl`,载荷成了 `items: []`;codex app-server 0.155.1 起把空数组当协议错误。GUI 路径没暴露是因为前端会先写用户消息,CLI / 无前端宿主是裸的。
|
||||
- **修复**:`build_direct_project_history_injection_params()` 在历史为空时返回 `Ok(None)`,调用方跳过注入;新增单测「空历史不得构造载荷」「有历史仍构造 1 条」。`cargo test … direct_project_history_wire`(3 passed)与 `cargo fmt --check` 通过。
|
||||
- **复验**:同一条命令下夹具从「4 秒失败」变成「走到 loopback Provider:`requests=3`、`markers.ready=true`、AGC exit=0」,说明第一层已经修通。
|
||||
- **仍未完成(第二层)**:同一用例仍断言失败,执行账本落在 `phase=interrupted`(期望 `completed`),stderr 为 `agent.runner.failed: Codex app-server 连接终止 … stderrBytes=1242`,CLI 回执是「执行通道已断开,不能自动重放未确认操作」。现象指向 CLI 收尾阶段的关闭竞态,需要先拿到 app-server 的 stderr 原文(当前只落字节数与 sha256)。两条运行时验收**继续未勾选**。
|
||||
|
||||
@@ -9711,6 +9711,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(Windows 侧也要能核产物内部身份):新增 `scripts/pe-version-info.mjs`(纯 Node 解析 PE `RT_VERSION`,无第三方依赖),核对脚本下载 Windows 安装包后断言 `ProductName` 与 `FileVersion` 等于本渠道身份与本轮版本。理由与 mac 侧一致:文件名可以改,PE 版本资源是产物自己写的;两侧对称以后「产物里的渠道身份」不再只靠源码级用例。实测 `dev-win` 0.1.155 → `陶泥儿开发版`/`0.1.155`,`release-win` 0.1.150 → `陶泥儿 Release`/`0.1.150`。
|
||||
- 事实(同日稍后):`dev-win` 的 0.1.155(commit `3702e0f8e`)在 14:26:13 发布完成,总号、渠道清单与旧协议指针三者对齐;该构建的源码早于 `assertArtifactVersionMatches()`,所以那条守卫仍待下一次真实渠道构建验证。
|
||||
- 决策(旧协议指针也要线上核对):`agc/latest.json` 是全局单对象,只属于 dev 的 Windows 系统;核对任何非 dev 的 Windows 渠道时,脚本断言它仍指向 `/agc/dev-win/…`。本轮 `release-win` 通过(指针指向 `dev-win/0.1.155` 的安装包),把「release 不能写 dev 桥」这条从单测扩到线上状态。
|
||||
|
||||
## 2026-09-28 DirectProject 空历史不再发 `thread/inject_items`
|
||||
|
||||
- 背景:用 `direct-execution-production-fixture.mjs`(真 AGC CLI → 真 app-server → loopback Responses fixture)跑基线时,全新项目的第一次对话直接失败:`Codex app-server JSON-RPC 失败:items must not be empty`,AGC 进程 4 秒退出、loopback Provider 一次都没被调用。根因是 `thread_created` 时无条件发 `thread/inject_items`,空项目没有 `.agent/conversations/project.jsonl`,载荷成了 `items: []`;codex app-server 0.155.1 起拒绝空数组。GUI 路径因为前端先写用户消息才没暴露,CLI / 无前端宿主没有这一步。
|
||||
- 决策:`build_direct_project_history_injection_params()` 在历史为空时返回 `Ok(None)`,调用方跳过注入(空历史本来就没有可注入内容);有历史时行为不变(仍按原载荷、原大小前置校验)。
|
||||
- 验证:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_project_history_wire` → 3 passed(新增「空历史不得构造载荷」「有历史仍构造 1 条」);同一条夹具命令从「4 秒失败、`requests=[]`」变成「`requests=3`、`markers.ready=true`、AGC exit=0」——但账本仍落 `interrupted`(app-server 收尾连接终止,`stderrBytes=1242`),是另一层问题,未在本次一并修。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/{direct_project_history_wire.rs,mod.rs}`、`docs/project-memory/shared-memory/pitfalls.md`、DirectProject 里程碑。
|
||||
- 决策(口径回归):macOS 现行契约是 arm64 单架构(2026-09-21 决策),里程碑里「两个 macOS 平台键指向 universal 产物」的旧文字按现行决策改写;不得据此重新切回 universal,除非按该决策给出的恢复路径补齐按架构的 Node 运行时。
|
||||
- 影响范围:`apps/ai-game-creator-shell/scripts/build-macos-ci.mjs`、新增 `apps/ai-game-creator-shell/scripts/macos-release-identity.mjs` 与其 `.test.mjs`、`apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs`、`jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`、`scripts/check-agc-update-channel-manifests.mjs`、两份里程碑与本文件、pitfalls。
|
||||
- 验证:`node --test apps/ai-game-creator-shell/scripts/*.test.mjs` → **110 passed / 0 failed**(其中定向批次 `macos-release-identity / prepare-macos-codex / verify-updater-signature / build-release / cargo-features` 60 passed;新增的 mac 身份回归用例直接喂线上那份 0.1.139 release 身份 plist,必须抛错;新增的产物版本守卫用例喂 `_0.1.153_` 残留安装包,必须抛错);`npm run check:production-ops`、`check:encoding`、`check:doc-index`、prettier、eslint、`git diff --check` 通过;只读核对对线上 `dev-win` 全 PASS(含 158 MiB 产物下载验签与旧协议 sha256 一致),对线上 `dev-mac` 精确报出上面两条 FAIL。
|
||||
|
||||
@@ -6106,3 +6106,11 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **判据**:只读核对要**打开产物看身份**,不能只看「地址存在 + 签名匹配」。`npm run check:agc-update-channel-manifests`(`AGC_UPDATE_VERIFY_DOWNLOAD=1`)现在会解出 mac 包的 `Info.plist`、以及 Windows 安装包的 PE `RT_VERSION`(`scripts/pe-version-info.mjs`),断言「产物内版本 == 清单版本」且「产物内产品名/identifier == 本渠道身份」,并额外断言**不同渠道的更新包不得字节相同**;本轮对线上取样得到三条 FAIL,正是这个缺陷。字节级佐证:`dev-mac/0.1.142/陶泥儿 Release.app.tar.gz` 与 `release-mac/0.1.139/陶泥儿 Release.app.tar.gz` 的 sha256 完全相同(`1dfc9deb79f7…`),说明 dev 分区里放的就是 release 那一次构建的产物。
|
||||
- **处理(2026-09-28 已修)**:构建前按后缀清空 `macos/` 下的 `*.app.tar.gz`、`*.app.tar.gz.sig`、`*.dmg`、`*.dmg.sha256`;构建后读 `.app/Contents/Info.plist` 断言版本/identifier/产品名;生成清单后再断言 `release.artifact` 就是本轮那一个。守卫在 `apps/ai-game-creator-shell/scripts/macos-release-identity.mjs`,回归用例直接用线上那份 0.1.139 release 身份包(`node --test` 59 passed)。
|
||||
- **教训**:凡是「按目录扫描挑产物」的发布步骤,都要么先清空同类产物、要么按本轮预期路径断言;只删「本轮要写的名字」等于把上一轮的坏包留在候选集里。现在共享入口 `generateUpdateManifest()` 也补了与平台无关的 `assertArtifactVersionMatches()`(文件名带 `_0.1.153_` 这类版本段时必须是本轮版本),所以即使将来某个流水线不再 `git clean -fdx`,旧安装包也会被拒绝而不是被发出去。还有一条更一般的:核对线上清单时,先看 decision-log 的现行口径(这里 macOS 已是 arm64 单架构),别拿过期里程碑文字当契约。
|
||||
|
||||
## 2026-09-28 DirectProject 空历史注入被新版 app-server 拒绝:新项目第一条消息直接「执行通道中断」
|
||||
|
||||
- **现象**:用 `apps/ai-game-creator-shell/scripts/direct-execution-production-fixture.mjs`(真 AGC CLI → 真 app-server → loopback Responses fixture,无账号/无付费 Provider)跑 `--cases completed` 时,AGC 进程 4 秒就退出、`requests=[]`,stderr 只有 `Codex app-server JSON-RPC 失败:items must not be empty` + `agent.runner.failed`,夹具因此报「AGC did not reach the loopback fixture」。
|
||||
- **根因**:`codex_app_server/mod.rs` 在 `thread_created` 时无条件调用 `thread/inject_items`,载荷由 `build_direct_project_history_injection_params()` 从 `.agent/conversations/project.jsonl` 构造;**全新项目该文件不存在 → `items: []`**,而 codex app-server 0.155.1 起把空数组当协议错误拒绝。GUI 路径之所以没暴露:前端会先调 `append_direct_project_conversation_message` 把用户消息写进历史,注入时至少有 1 条;`--direct-codex-chat` 这类无前端宿主(以及任何直接调用 `run_direct_game_creator_turn_at` 的夹具/CLI)没有这一步。
|
||||
- **处理(2026-09-28 已修)**:`build_direct_project_history_injection_params()` 在历史为空时返回 `Ok(None)`,调用方跳过 `thread/inject_items`;新增两条单测(空历史不得构造载荷、有历史仍构造 1 条)。
|
||||
- **复验**:同一条夹具命令下,AGC 进程从「4 秒失败、`requests=[]`」变成「走到 loopback Provider、`requests=3`、`markers.ready=true`、exit=0」——第一层缺陷确实修掉了。
|
||||
- **仍未解决(第二层,另一个问题)**:同一个夹具的 `completed` 用例仍断言失败,原因是执行账本 `phase=interrupted`(期望 `completed`),stderr 为 `agent.runner.failed: Codex app-server 连接终止 … exitStatus=unknown;stderrBytes=1242`,CLI 回执文本也是「执行通道已断开,不能自动重放未确认操作」。看起来是 CLI 收尾阶段的关闭竞态(账本被记成宿主终止),与空历史注入不是同一件事;要动它需要先抓 app-server 的 stderr 内容(应用目前只记字节数与摘要,不落原文)。
|
||||
|
||||
Reference in New Issue
Block a user