fix(teacher): 区分读取额度与上下文失效
This commit is contained in:
@@ -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.
|
||||
@@ -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。
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user