diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index aca14fac6..57ccaf972 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -5628,6 +5628,7 @@ export function App({ PROJECT_SUPERVISOR_AGENT_ID, sessionId, submissionRunProfile, + 'project-supervisor-game-chat', ) : null; let autoPreviewAfterRevision = 0; diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index 65a2a05d4..32e2714c7 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -725,6 +725,7 @@ export function matchingAgentRuntimeForSteer( requestedRunProfile: NonNullable< AgentRuntimeState['runProfile'] > = 'standard', + requestedSource?: string, ) { if (!sessionId) { return null; @@ -735,6 +736,7 @@ export function matchingAgentRuntimeForSteer( runtime?.agentId === agentId && runtime.sessionId === sessionId && (runtime.runProfile ?? 'standard') === requestedRunProfile && + (!requestedSource || runtime.source === requestedSource) && isAgentRuntimeSteerableState(runtime), ) ?? null ); @@ -834,6 +836,7 @@ export async function submitProjectSupervisorRuntimeTask({ PROJECT_SUPERVISOR_AGENT_ID, sessionId, runProfile, + source, ); if (steerRuntime) { const steer = await invoke( @@ -1330,7 +1333,9 @@ export function projectSupervisorCollaboratingAgentRuntimes( const runtimesByAgentId = new Map(); for (const runtime of Object.values(runtimeByAgentId)) { const isVisibleChildSource = - ['agent-delegate', 'agent-delegate-retry'].includes(runtime?.source ?? '') || + ['agent-delegate', 'agent-delegate-retry'].includes( + runtime?.source ?? '', + ) || (supervisorRuntime.source === 'project-supervisor-game-chat' && runtime?.source === 'agent-ready-task-scheduler'); if ( diff --git a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts index 9a9721f8d..367cca150 100644 --- a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts +++ b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts @@ -82,7 +82,8 @@ describe('Game Chat stream identity and source', () => { eventPath: 'events.jsonl', } satisfies AgentRuntimeResult; const invoke = vi.fn( - async (_command: string, _args?: Record) => runtimeResult, + async (_command: string, _args?: Record) => + runtimeResult, ); await submitProjectSupervisorRuntimeTask({ @@ -116,6 +117,7 @@ describe('Game Chat stream identity and source', () => { ...runtimeResult.state, runId: 'steer-run', runProfile: 'autonomous-game-build', + source: 'project-supervisor-game-chat', status: 'running', phase: 'planning', } satisfies AgentRuntimeState; @@ -157,6 +159,24 @@ describe('Game Chat stream identity and source', () => { runProfile: 'autonomous-game-build', }); expect(invoke.mock.calls[0]?.[1]).not.toHaveProperty('source'); + + invoke.mockClear(); + await submitProjectSupervisorRuntimeTask({ + invoke, + projectPath: '/tmp/game-chat', + sessionId: 'session-provider-retry', + prompt: 'continue from the app', + runtime: { + ...steerRuntime, + source: 'project-supervisor-cli', + }, + runProfile: 'autonomous-game-build', + source: 'project-supervisor-game-chat', + }); + expect(invoke).toHaveBeenCalledWith( + 'start_game_creator_supervisor_runtime_task', + expect.objectContaining({ source: 'project-supervisor-game-chat' }), + ); }); test('game-chat hydration keeps identical text from a different runtime response identity', () => { @@ -178,10 +198,9 @@ describe('Game Chat stream identity and source', () => { }, ]; - expect(mergeGameChatRuntimeResponseMessagesIntoHistory(history, pending)).toEqual([ - pending[0], - history[0], - ]); + expect( + mergeGameChatRuntimeResponseMessagesIntoHistory(history, pending), + ).toEqual([pending[0], history[0]]); expect( mergeGameChatRuntimeResponseMessagesIntoHistory(history, [ { ...pending[0], messageId: 'persisted-message-1' }, diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index 7555d2a6d..71d427a97 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -471,9 +471,7 @@ function createProjectSupervisorRuntimeHarness({ currentProjectMessages.push({ schemaVersion: 'game-creator-conversation.v1', ...message, - ...(args?.messageId - ? { messageId: String(args.messageId) } - : {}), + ...(args?.messageId ? { messageId: String(args.messageId) } : {}), updatedAt: typeof message.updatedAt === 'number' ? message.updatedAt @@ -517,6 +515,7 @@ function createProjectSupervisorRuntimeHarness({ currentRuntime = runtimeState({ runId, runProfile: args?.runProfile, + source: args?.source, status: 'running', phase: 'planning', currentTask: String(args?.task ?? ''), 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 2a508e90f..41d5254b6 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 @@ -92,7 +92,7 @@ function gameChatRuntimeEvent({ runId, source: agentId === 'project-supervisor' - ? 'project-supervisor' + ? 'project-supervisor-game-chat' : 'agent-delegate', eventId, eventType, @@ -114,7 +114,7 @@ function gameChatRuntimeState( taskId: 'project-supervisor', sessionId: 'game-chat-supervisor-session', runId: 'game-chat-supervisor-run', - source: 'project-supervisor', + source: 'project-supervisor-game-chat', status: 'running', phase: 'execution', currentTask: '生成首版游戏', diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 3c6a256d2..aa64acfac 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3772,6 +3772,7 @@ - Provider action 安全持久化补充:pending / provider action 的泄密检测不能因裸自然语言短语 `api key` 直接拒绝,否则 `agent.delegate` 中“不要暴露 External Editor API Key”等安全约束会被误报并阻断首批协作。赋值形式只允许完整匹配受控的“未配置 / 不可用 / 禁止读取”等状态或固定无密钥降级说明,不能用 `starts_with` 放行 `none-but-secret`、`not configured; actual value ...` 等安全前缀后的凭据;`**API Key**:`、`` `API Key`: ``、`API Key(生产):` 等装饰或限定标签也必须识别为赋值。结构化字段标记 `apiKey / api_key`、`Authorization / Cookie`、`token / Bearer` 以及已知 secret token 形状仍必须检测并失败关闭。 - Windows retry 扫描补充:`Path::strip_prefix(root)` 在 Windows 上得到的相对 `Path` 转字符串后使用反斜杠,不能直接传给只接受 portable `/` 的 Runtime JSON sidecar 读取器;否则 Runner 重启或显式 `--agent-resume` 扫描已到期 retry 时会报“项目文件路径不能包含反斜杠”,任务持续停在 `waiting-for-provider-retry`。目录扫描应按路径组件重组成 `/` 分隔的 UTF-8 相对路径,不要放宽全局路径校验。 - 恢复交互:`needs-reconciliation` 即使没有 `pendingToolAction`,也必须提供显式“已核对,结束旧任务”;它只取消旧 run,不直接 retry。若取消后仍有 pending task,由 Runner 自动继续;只有队列为空且旧 run 已取消时,才允许创建新的 retry run,避免重复执行同一用户输入。自主构建 Supervisor 的 retry 不能改写为普通 `agent-background-task` source,必须从已验证的原 Run Profile 绑定恢复 `project-supervisor-gui / project-supervisor-cli` 可信来源;不得只信可追加的 task journal。 +- Steer source:前端选择可 steer Runtime 时不能只比较 Agent、Session 和 Run Profile,还必须在调用方声明 source 时精确比较持久 `source`。例如 game-chat 只能 steer `project-supervisor-game-chat`,不能把同 Session/Profile 的 `project-supervisor-cli` run 当成目标;source 不一致时应按当前入口新建或排队自己的 run,不能先调用后端再把“steer source 与当前 Run 不一致”暴露给用户。 - 验证:前端回归同时覆盖零历史、无 Session 的初始空态、无 active Session 索引但存在持久 `needs-reconciliation` 总控 Runtime 的恢复展示,以及“先取消、队列为空后才重试”;真实 Windows 运行全部 tool-plan handoff 测试,确保相对句柄 rename、覆盖安装、回读和清理均通过。Responses 回归覆盖 system / user / assistant 文本分别序列化,并保留 user `input_text + input_image`;Runtime 回归覆盖“无效计划 → repair transport 等待 → steer → 新 cursor 再修复”,断言 cursor `0 / 1` 各有一条审计且不冲突。 ## 固定画布产物返工不能变成任意覆盖,design-foundation 不能越权修程序