From 60c20f996e8428d4dfbd5efb92792d3900722af5 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Sat, 26 Sep 2026 18:03:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(teacher):=20=E5=8C=BA=E5=88=86=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E9=A2=9D=E5=BA=A6=E4=B8=8E=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260926-teacher-read-expired-087d82f7.md | 44 ++++++++++++++++ README.md | 1 + electron/coding-teacher/cloud-runner.ts | 16 +++--- tests/unit/coding-teacher-cloud.test.ts | 52 +++++++++++++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260926-teacher-read-expired-087d82f7.md diff --git a/.project-docs/30-worklog/tasks/20260926-teacher-read-expired-087d82f7.md b/.project-docs/30-worklog/tasks/20260926-teacher-read-expired-087d82f7.md new file mode 100644 index 00000000..9a4e4e14 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260926-teacher-read-expired-087d82f7.md @@ -0,0 +1,44 @@ +# Task: Diagnose expired consultation read requests after repeated tools + +## Identity + +- Task ID: 20260926-teacher-read-expired-087d82f7 +- Mode: Feature +- Branch: codex/20260926-teacher-read-expired-087d82f7-teacher-read-expired +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260926-teacher-read-expired-087d82f7 +- Base commit: 287dcd303d3c99665e95eca3345c4ee3270a855f +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Diagnose the reported read-request-expired failure after 48 tool activities in installed Makelore 1.6.8. Establish the actual trigger and a red regression before changing the responsible boundary. + +## Intent And Constraints + +- Concurrent and Planning Gates Passed through official check/start/status in this isolated feature worktree. Source base matches current main; entry, own record, AGENTS, relevant teacher decisions/domain/architecture/evidence and recent peer scopes were loaded, with unchanged earlier context reused. Packaging 1.6.8 is a separate completed scope. Historical placeholder peers have no concrete dependency here. +- Installed 1.6.8 contains the prior run_busy retry. Its expired-read guard currently conflates context mismatch, malformed/oversized calls and the six-round limit. Trigger still needs runtime evidence; do not presume expiry from the UI count alone. +- One fresh read-only reviewer explicitly authorized by the user. No additional subagents, primary/peer writes, credentials in evidence, live paid calls, deployment, unapproved cleanup or silent change to the accepted six-batch contract. Plan: inspect bounded local request metadata, construct a red test, repair the demonstrated cause, run targeted validation and document remaining rollout limits. + +## Outcome + +- Installed 1.6.8 includes the previous run_busy retry. Read-only history inspection found 48 activities across seven cloud Runs but 25 unique tool-call IDs; prior batches reappear in continuation Runs. The first six batches have later completed results and a seventh batch of new calls appears before the local error. Local history does not retain the cloud model request body, so the exact production provider/deployment cause remains unverified. +- A deterministic valid-context seventh-batch replay produced the exact reported expired-context error. Main now retains the six-batch protection and cancellation but distinguishes teacher_read_limit, teacher_context_expired and teacher_protocol_invalid; six-batch completion returns the actual answer. README documents this behavior. +- The paired Yuxi task 20260926-teacher-read-round-yx-8be372c1 adds explicit tool_choice none for final-answer calls after consuming the sixth batch. Its real HTTP/PG/Redis/worker/SDK test reproduces a seventh interrupt when the fixture provider continues historical tool names without explicit none, and completes after the fix. A cooperative-provider six-batch control passed before the fix; the round counter itself is not proven broken. +- Fresh independent review 20260926-teacher-read-review-b7d3e91a passed with no material findings. Reviewer independently reran six relevant client cases and the six-batch Yuxi HTTP/PG/worker case. No merge, deployment or installed-client update performed. + +## Verification + +- Red: pnpm exec vitest run tests/unit/coding-teacher-cloud.test.ts -t 'exhausted read rounds' --maxWorkers=1: failed with teacher_context_expired on a valid seventh batch before the guard split. +- Green: pnpm exec vitest run tests/unit/coding-teacher-cloud.test.ts tests/unit/coding-teacher-read-tools.test.ts tests/unit/coding-teacher.test.ts --maxWorkers=1: 130 passed. Corrected table rows to wrap empty/oversized arrays as objects; focused malformed-batch replay: 3 passed. +- pnpm run typecheck and pnpm run build:vite: passed. Build warnings concern existing chunk sizes, mixed imports and Browserslist age. +- git diff --check: passed. No live paid model request, production trace capture or desktop acceptance run. + +## Follow-ups + +- Deploy the paired Yuxi worker fix and update the client error projection before live acceptance. Existing failed questions are not automatically replayed or billed. +- Tool activity presentation currently counts the same call per Run and can retain interruption-related failed status before a continuation completes; report this observed presentation issue separately. It was not changed in this bounded completion repair. + +## Promotion Candidates + +- Target: integrated consultation runtime boundary. Proposal: six valid local batches still complete normally, while protocol-invalid batches, stale context and exhausted read quota have separate error semantics. Evidence: deterministic red/green and paired worker test. Impact: future diagnosis must not infer context expiry from quota exhaustion or UI tool count. No semantic conflict or additional product-direction approval needed. diff --git a/README.md b/README.md index 101b3f8e..5ff0cbeb 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p - 想法板区分已留下、智能体建议和暂放内容,可由学生采纳、暂放、选择先想哪项。“把想法理一理”明确转换为智能体归纳的结构图,并可“回去补充想法”。“只聊天”暂停整理,“先这些”暂时结束并保留未确定内容,“接着改”恢复原工具;这些操作只影响智能体讨论,不创建项目分支、不执行作品修改。状态、版本和历史快照按当前账号/项目/话题保存。 - Main 校验结构、引用和内容长度,拒绝未经学生选择的类型变更与旧版本更新。结构化回复在完整校验后一次应用;生成中显示简短处理状态,停止、失败或无效组件保留上一份内容。普通 Markdown 链接、JSON 对象/数组和代码示例保持正文,仅讨论协议的顶层字段或专用围栏进入组件解析。解析失败仍保留完整的可读 reply,并将本次原始回答保存为默认折叠、字面显示的“查看收到的原始内容”,不把它再次加入模型上下文;既有丢失原文的历史不能恢复。 - 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按运行/调用身份合并名称和状态,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。 +- 云端咨询最多执行六批本地读取。配套 Yuxi 在最后一批结果返回后以禁止工具调用的模型请求生成答案;Main 仍拒绝第七批读取,并分别提示读取达到上限、上下文失效或读取请求格式无效。 - 智能体人设、职责、提示词和 Skills 由发布配置决定。Main 不追加固定教学基线、不生成“小麦”、不清空所选智能体的 Skills,只添加真实工具能力、上下文边界和本轮界面协议。智能体名称叫“老师”“朋友”或“代码顾问”不改变调用路径或权限。 - 每轮格式由 Main 的对应意图协议决定;`discussion.ts` 为支持组件的请求注入唯一 `{reply, quickReplies, tool}` 协议。运营教学补充不另写字段协议或要求始终纯文字。工具内讨论保留类型、稳定 ID、未修改内容和采纳状态;暂停/未进入时 `tool:null`,没有实质变化时也可保留原内容。结构图、流程和对照目前没有独立的采纳/来源字段,待定、建议与预测只能在展示文字中明确,不能据此推导已确认共识。 - 当前输入是按预算截取的所选操作对话已完成分支文本、明确引用、咨询历史和适用的当前组件,并支持上述只读原文恢复;没有跨全部操作会话自动读取,也未向操作 Agent 注入咨询共识。“先这些”仍仅结束本轮整理,保留各条目状态,不确认全部候选、不自动制作。提示词不宣称这些系统能力已接通,也不声称看过未提供的画面或试玩过作品。Yuxi 话题经云端原生执行,旧版本地智能体由 Main 调用计费网关;均不创建本地 Pi 编程 worker。 diff --git a/electron/coding-teacher/cloud-runner.ts b/electron/coding-teacher/cloud-runner.ts index 3f319c66..01d2e225 100644 --- a/electron/coding-teacher/cloud-runner.ts +++ b/electron/coding-teacher/cloud-runner.ts @@ -300,18 +300,20 @@ export function prepareCloudTeacher( 'teacher_interaction_unsupported', '智能体请求了当前面板不支持的交互,请联系运营调整该智能体。' ); - if ( - pending.context_id !== requestId || - !Array.isArray(pending.calls) || - !pending.calls.length || - pending.calls.length > 32 || - ++rounds > 6 - ) + if (pending.context_id !== requestId) throw new TeacherError( 409, 'teacher_context_expired', '智能体读取请求已失效,请重新提问。' ); + if (!Array.isArray(pending.calls) || !pending.calls.length || pending.calls.length > 32) + throw new TeacherError(502, 'teacher_protocol_invalid', '智能体读取请求格式无效,请重试。'); + if (++rounds > 6) + throw new TeacherError( + 409, + 'teacher_read_limit', + '智能体已达到本次读取上限,但尚未完成回答。请缩小检查范围后重试。' + ); onProgress('正在读取项目与会话…'); const results = []; for (const raw of pending.calls) { diff --git a/tests/unit/coding-teacher-cloud.test.ts b/tests/unit/coding-teacher-cloud.test.ts index 73089041..d0ae8f2f 100644 --- a/tests/unit/coding-teacher-cloud.test.ts +++ b/tests/unit/coding-teacher-cloud.test.ts @@ -440,6 +440,58 @@ it.each(['方法老师', '朋友', '代码顾问'])('returns all three local rea expect(f.progress).toHaveBeenCalledWith('正在读取项目与会话…'); }); +it.each([false, true])('finishes six batches and rejects a seventh as a read limit (extra batch: %s)', async extraBatch => { + const f = await fixture(); + const returned: unknown[] = []; + const transport: TeacherCloudTransport = { + events: vi.fn(), + json: vi.fn(async (url, body) => { + if (url === '/questions') return { request_id: 'question', run_id: 'run-0' }; + if (url.endsWith('/cancel')) return { status: 'cancelled' }; + if (url.endsWith('/tool-results')) { + returned.push(body); + return { run_id: `run-${returned.length}` }; + } + if (!extraBatch && returned.length === 6) return { status: 'completed', output: '已根据六批资料完成检查。' }; + return { status: 'interrupted', interrupt: { + source: 'client_read_tools', context_id: requestId, + calls: [{ tool_call_id: `read-${returned.length}`, name: 'read_project_file', arguments: { path: 'src/game.ts' } }], + } }; + }), + }; + const run = prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport); + const text = vi.fn(); + const result = run.run([{ role: 'user', content: '检查项目' }], new AbortController().signal, text); + if (extraBatch) { + await expect(result).rejects.toMatchObject({ code: 'teacher_read_limit', message: expect.stringContaining('读取上限') }); + expect(transport.json).toHaveBeenCalledWith('/questions/' + requestId + '/cancel', {}, expect.anything()); + } else { + await result; + expect(text).toHaveBeenCalledWith('已根据六批资料完成检查。'); + expect(transport.json).not.toHaveBeenCalledWith('/questions/' + requestId + '/cancel', expect.anything(), expect.anything()); + } + expect(returned).toHaveLength(6); +}); + +it.each([ + { label: 'missing', calls: undefined }, + { label: 'empty', calls: [] }, + { label: 'oversized', calls: Array.from({ length: 33 }, () => ({})) }, +])('rejects $label read batches as protocol errors', async ({ calls }) => { + const f = await fixture(); + const transport: TeacherCloudTransport = { + events: vi.fn(), + json: vi.fn(async url => { + if (url === '/questions') return { request_id: 'question', run_id: 'one' }; + if (url.endsWith('/cancel')) return { status: 'cancelled' }; + return { status: 'interrupted', interrupt: { source: 'client_read_tools', context_id: requestId, calls } }; + }), + }; + const run = prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport); + await expect(run.run([], new AbortController().signal, vi.fn())).rejects.toMatchObject({ code: 'teacher_protocol_invalid' }); + expect(transport.json).not.toHaveBeenCalledWith(expect.stringContaining('/tool-results'), expect.anything(), expect.anything()); +}); + it('rejects a stale context before reading or returning any project data', async () => { const f = await fixture(); const transport: TeacherCloudTransport = {