diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 170130beb..457185fc0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -195,6 +195,20 @@ impl CodexAppServerStderrSummary { } } +/// 派发 app-server 的收尾与中断任务。 +/// +/// 这个入口会被**没有 tokio runtime 上下文的线程**调用:`cancel_direct_codex_turn` +/// 是同步 Tauri 命令,直接跑在 IPC 回调线程(Windows 上是 WebView2 的 UI 线程); +/// [`CodexThreadLease`] 与 [`CodexTurnGuard`] 的 `Drop` 也在调用方线程上执行。 +/// `tokio::spawn` 在那样的线程上会经 `Handle::current()` panic("there is no reactor +/// running"),而 panic 跨不过 Tauri 的 IPC 回调边界,整个进程会以 `0xC0000409` +/// (FAST_FAIL_FATAL_APP_EXIT)abort——现场就是"点终止,App 闪退"(2026-09-16 的 WER +/// 记录:`genarrative-ai-game-creator-shell.exe`,异常代码 `0xc0000409`,fail-fast +/// 参数 `7`)。一律走 Tauri 的全局异步 runtime:`main` 已把深栈 runtime 装进去。 +fn spawn_codex_app_server_task(task: impl std::future::Future + Send + 'static) { + tauri::async_runtime::spawn(task); +} + struct CodexTurnStartCancellation { inner: Weak, thread_id: String, @@ -256,7 +270,7 @@ impl CodexTurnStartCancellation { }; let connection = CodexAppServerConnection { inner }; let thread_id = self.thread_id.clone(); - tokio::spawn(async move { + spawn_codex_app_server_task(async move { let _ = connection .request( "turn/interrupt", @@ -3566,7 +3580,7 @@ impl Drop for CodexThreadLease { let connection = self.connection.clone(); let key = self.key.clone(); let thread_id = self.thread_id.clone(); - tokio::spawn(async move { + spawn_codex_app_server_task(async move { let mut threads = connection.inner.threads.lock().await; if let Some(entry) = threads.get_mut(&key) { if entry.thread_id == thread_id { @@ -3593,7 +3607,7 @@ impl Drop for CodexTurnGuard { let connection = self.connection.clone(); let thread_id = self.thread_id.clone(); let turn_id = self.turn_id.clone(); - tokio::spawn(async move { + spawn_codex_app_server_task(async move { connection.inner.turns.lock().await.remove(&turn_id); connection.inner.turn_backlog.lock().await.remove(&turn_id); let _ = connection @@ -4489,6 +4503,19 @@ mod tests { assert!(table.select(&key, None).is_err()); } + /// 终止路径会从同步命令线程和 `Drop` 里派发 app-server 任务:那些线程没有 tokio + /// runtime 上下文。`tokio::spawn` 在那里 panic,panic 跨不过 IPC 回调边界就把整个 + /// 进程 abort(0xC0000409,"点终止就闪退")。这条用例把派发入口钉在没有 runtime + /// 上下文的线程上,回退到 `tokio::spawn` 时它会失败。 + #[test] + fn codex_app_server_task_dispatch_needs_no_tokio_runtime_context() { + let joined = std::thread::spawn(|| spawn_codex_app_server_task(async {})); + assert!( + joined.join().is_ok(), + "没有 tokio runtime 上下文的线程也必须能派发 app-server 收尾任务" + ); + } + /// 注册键:前端传的项目路径与回合注册时的路径必须归一化成同一个键(Windows 上 /// `canonicalize` 会带 `\\?\` 前缀,去掉后两边才相等)。 #[test] diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index c701d873b..b28862042 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -10311,7 +10311,6 @@ export function registerProjectAgentStatusTests() { window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); // 项目打开包含异步初始化;以按钮启用为就绪条件,再开始弹窗迟到读取场景。 - await screen.findByText('想做什么游戏?'); await waitFor( () => { expect( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5d8e744dc..0ba04d1c5 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2,6 +2,7 @@ > 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。 > 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。 + ## 2026-09-16 AGC 同 AppData 多窗口共享 Agent Runner - 背景:双击或再次启动 AGC 客户端时报「应用启动失败」,启动日志为 `startup.runner.owner-lock.failed details=AI 游戏创作界面已由同一 AppData 目录中的其他进程运行`。原设计(2026-07-27 / 2026-08-23)要求同一 AppData 只有一个 GUI owner,第二个界面进程在 setup 阶段就失败退出。 @@ -22,6 +23,7 @@ - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs`。**未改** `/api/external/v1` 契约 / OpenAPI / DTO、SpacetimeDB schema、AGC 工具桥行为、前端投影与资源工作台。 - 验证方式:`three_dimensional_game_request_frees_the_engine_choice`、`explicit_flat_presentation_requests_do_not_trigger_three_dimensional_selection`、`named_engine_requests_keep_the_existing_engineering_rule`、`three_dimensional_contract_reports_the_current_project_engine`、`home_three_dimensional_note_keeps_project_creation_available`、`system_prompt_is_bounded_and_declares_direct_runtime`,以及 `agent::direct_tools_mcp` 17 passed;`cargo fmt --check` 通过。本机临时目录 owner ACL 与进程用户不一致,涉及 `init_local_game_project_at` 的既有用例(含未改动模块)在本机无法执行,全量分片与真机 smoke 未在本轮取得。 - 关联文档:[里程碑](../plans/【里程碑】Direct三维请求自选技术栈-2026-09-16.md)、[实施计划](../plans/【实施计划】Direct三维请求自选技术栈-2026-09-16.md)。 + ## 2026-09-16 图标图集自动拆图上限提高到 256 - 背景:AGC 图标图集自动连通域识别在一次生成中识别出 86 个区域,原有 64 片上限在后处理阶段阻断了请求;该上限同时影响 api-server 自动 / 手动切片、SpacetimeDB 批量落库和统一生成结果 item 数量。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 9630581dd..24a21eea8 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5644,3 +5644,20 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **原因**:客户端把两件事压成了一个判据。原生冻结会话(`PlatformSessionSnapshot`)同时承担“身份归属”和“access token 字节比对”,而长回合保活与 401 续期每次都会签发新 token 并推进 native generation 重新安装会话;于是同账号的正常续期被等价成换号,续期窗口内所有在途生成、编辑、上传、确认和下载 operation 全部失配。保活定时器(回合 busy 时每 5 分钟一次)会稳定落在生图窗口内,所以并发越多越必现。另一层在服务端:`/api/auth/refresh` 是严格一次性轮换,且失败时下发清空 refresh cookie 的响应;两个窗口 / 实例并发续期时,输的一方会把赢家刚写入的有效 cookie 删掉。 - **处理(现行口径)**:平台会话统一拆成**身份**(`userId + api origin + identity generation`)与**凭据**(当前 access token)。identity generation 只在登录、切号、登出和新的 GUI authority epoch 推进;同账号续期只更新凭据并推进写入 revision(revision 只用于拒绝迟到写入)。冻结会话校验只比身份,比 token 字节的判据已被取代。刷新失败语义收紧为:只有服务端明确返回 401/403 且经一次收敛重试后仍失败,才清本地会话;网络错误、5xx、网关错误和响应契约异常必须保留会话与 access token。`/api/auth/refresh` 的轮换失败不再下发清 cookie 响应。 - **验证**:AGC `platform_session::tests` 覆盖“同身份 token 轮换后冻结会话仍有效”“换号 / 退出后失效”“迟到 install 被 revision 拒绝”;`appSurface` 前端用例覆盖“续期不推进身份代次且 native 写入 revision 递增”“瞬时刷新失败不清会话”;网站 `src/services/apiClient.test.ts` 覆盖“刷新 401 后收敛重试成功”与“两次都被拒绝才判权威失效”;`api-server` `refresh_session_*` 覆盖“轮换失败不下发清 cookie”。定位同类问题先看冻结会话判据里有没有 token 字节,再看服务端失败响应有没有 `Max-Age=0`。 + +## 2026-09-16 同步 Tauri 命令里 `tokio::spawn`:点「终止」整个客户端 abort 闪退 + +- **现象**:AGC 客户端(陶泥儿 0.1.46 安装包,`C:\Users\<用户>\AppData\Local\陶泥儿\genarrative-ai-game-creator-shell.exe`)在对话回合运行中点输入盒的「终止」后整个 App 直接消失。Windows 事件日志 `Application Error` 1000:异常代码 `0xc0000409`、出错模块就是该 exe、偏移 `0x3395066`;`%LOCALAPPDATA%\CrashDumps` 留下同名 minidump,其 ExceptionStream 为 `NumberParameters=1`、`Param[0]=7`(`FAST_FAIL_FATAL_APP_EXIT`)⇒ Rust `abort()`,既不是栈溢出(`0xC00000FD`)也不是访问违例(`0xC0000005`)。 +- **原因**:`cancel_direct_codex_turn` 是**同步** Tauri 命令,Tauri 把非 `async` 命令直接跑在 IPC 回调线程(Windows 上是 WebView2 的 UI 线程);该线程没有进入任何 tokio runtime 上下文(`main` 只 `tauri::async_runtime::set(handle)` 加泄漏 Runtime,从未在调用线程 `enter()`)。终止链路 `cancel_direct_codex_turn_at → CodexTurnStartCancellation::cancel → maybe_interrupt` 却用 `tokio::spawn` 派发 `turn/interrupt`:在没有上下文的线程上 `tokio::spawn` 经 `Handle::current()` panic("there is no reactor running"),panic 又跨不过 Tauri 的 IPC 回调边界,Rust 只能 abort 整个进程。同一模块的 `CodexThreadLease::drop`、`CodexTurnGuard::drop` 是同一形态。引入点是 2026-09-15 23:37 的 `76bbeb684 重构 DirectProject Agent 深模块`;既有覆盖只有 `#[cfg(unix)]` 的 `#[tokio::test]` 走 Drop 守卫路径,碰不到同步命令线程,所以 CI 全绿。 +- **处理**:`codex_app_server/mod.rs` 新增 `spawn_codex_app_server_task`,统一经 `tauri::async_runtime::spawn` 派发(生产环境就是 `main` 装的深栈 runtime);终止、租约 Drop、回合 Drop 三处一起改走它。判据是「调用点可能位于没有 tokio 上下文的线程」,不是「这个函数看起来像异步」。 +- **验证**:新增 `codex_app_server_task_dispatch_needs_no_tokio_runtime_context`——在 `std::thread::spawn` 出来的无上下文线程里调用派发入口,若回退成 `tokio::spawn` 即失败(实测回退后报 `there is no reactor running, must be called from the context of a Tokio 1.x runtime`)。 +- **排障注意**:Release GUI 没有 panic hook、也没有 stderr,panic 文案不会落盘;`diagnostics/application.log` 只会停在崩溃前最后一行(本次停在 `agent.codex_app_server.remote_control disabled` 之后),所以「应用日志没有异常」不能当作「没有 Rust panic」,要结合 WER 事件、minidump 的 fail-fast 参数与调用线程上下文判断。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`(`install_agent_runtime_async_runtime_with_deep_stack`)、`apps/ai-game-creator-shell/src-tauri/src/commands.rs`(`cancel_direct_codex_turn`)。 + +## 2026-09-16 AGC 壳跑过 `cargo test` 后前端 typecheck 必红:ts-rs 导出把 `generated/*.ts` 重写成另一种形状 + +- **现象**:在 `apps/ai-game-creator-shell/src-tauri` 跑过 `cargo test` 之后,`npm run ai-game-creator-shell:typecheck` 报 10 条类型错(`src/features/project-workspace/resourceReferences.ts:106-112` 的 `string | undefined` 不能赋给 `string | null`;同文件 134 行的对象字面量带 `type: 'message'`,而 `DirectCodexUserMessageItem` 里没有该字段),`npm run ai-game-creator-shell:build` 也死在 `beforeBuildCommand` 的同一条 typecheck 上。 +- **原因**:crate 里的 ts-rs 导出会按**本机依赖版本**重写 `src/features/project-workspace/generated/DirectCodexUser*.ts`:注释头变成 "This file was generated…"、字符串改双引号、`DirectCodexUserMessageItem` 丢掉 `type: 'message'` 判别字段、并多出一个 `DirectCodexUserMessageEnvelope.ts`。仓库里提交的那份是前端真正依赖的形状(前端按带 `type` 的判别联合写),重写后两边就对不上——错在生成器版本漂移,不在前端。 +- **处理(现行口径)**:不要把重写结果当改动提交。跑过 `cargo test` 或构建后先 `git checkout -- apps/ai-game-creator-shell/src/features/project-workspace/generated`,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`,然后才做 typecheck / 打包;绑定与前端形状冲突时以**已提交的绑定 + 前端**为基准排查。 +- **验证**:恢复提交版本后 `npm run ai-game-creator-shell:typecheck` exit 0(`[skill-pack] OK`);保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的未跟踪文件,属同类生成产物。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/`(ts-rs 导出源)、`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/scripts/build-release.mjs`(`beforeBuildCommand`)。