P0:消除 Agora 启动阻塞与 read_file 截断假完成
| 项目 |
内容 |
| 产品 |
my-agent(MA) |
| 严重级别 |
P0 / release blocker |
| 当前候选 |
MA 0.3.0(PR #40 8e110a5)+ Agora 0.2.0(Agora PR #22 cb4cb6d) |
| 关联 |
#39、#41、PR #40、zimoos/agora#22 |
| 范围 |
一个父 Issue,包含启动性能与文件读取可信度两个必须同时完成的工作流 |
Problem
当前最终候选存在两个直接破坏首次体验和用户信任的问题:
- 启动阻塞:执行
ma 后,TUI 必须等待 Agora 二进制启动、MCP 握手和 Provider ready,用户会面对约 5~10 秒空白。
- 文件读取假完成:
read_file 虽然先返回完整文件,但 MA 在写入模型历史前统一截为 4000 字符。模型尝试分页时,字符串形式的 offset 又被文件服务静默忽略,导致重复读取第一页,最后仍可能声称“已完整查看”。
这不是两个孤立的小 Bug:前者让用户误判产品无法启动,后者让 Agent 在代码工作中给出没有证据的结论。两者都必须在正式交付前解决,不能用增加 spinner、扩大字符上限或提示词约束冒充完成。
Evidence
A. 启动性能
同一台 Apple Silicon Mac、同一候选包的串行实测:
| 场景 |
耗时 |
最终 ma 到首个可见 TUI frame |
5347ms、6619ms |
| 当前配置冷 bootstrap |
最高 8453ms |
| 仅 Agora、无普通 MCP |
4328ms、4630ms |
| 远程 Provider + 5 个普通 MCP |
135ms、145ms |
| MA 空壳基线 |
10ms |
packaged Agora MCP initialize |
4278ms、4541ms |
同代码源码 runtime initialize |
242ms、433ms |
Agora 初始化的分步结果:tools/list 1~2ms、resources/list 1ms、doctor 10~11ms、runtime_capabilities 1ms、models_list 9ms。慢点集中在 packaged runtime 启动,不在 35B 模型加载,也不在能力查询。
当前代码路径:
src/cli/index.tsx 在 render(<App />) 前执行 await bootstrap(...)。
src/agent.ts#createAgent 在返回 Agent 前执行 await providerRuntime.ready?.()。
src/provider/agora.ts#start 同步等待 MCP initialize 和完整能力握手。
- Agora
scripts/build_native_release.py 同时传入 --onefile --standalone;产物包含 NUITKA_ONEFILE_* 标记并按进程解包到临时目录。
Nuitka 官方文档明确说明:onefile 会在目标机器运行前自解包,默认使用每次唯一且随后删除的临时目录;--mode=onefile 已自动包含 standalone。npm 本身已经完成安装解包,因此这里继续使用 onefile 只增加重复启动成本:
B. read_file 截断与重复读取
真实 Agora 会话的脱敏证据:
src/agent/compact.ts 对普通工具结果设置 TOOL_RESULT_MAX_CHARS = 4000,保留 75% 头部和 25% 尾部。
- 一个 198 行 JS 文件进入模型历史时丢失 1380 字符;一个 212 行 JS 文件丢失 2324 字符。
- 模型后续分别发出
offset: "100" 和 offset: "95"。
servers/fs-mcp.ts 只接受 typeof offset === "number";字符串会被静默降级成 offset = 1。
- 两个文件的第二次工具结果分别与第一次 SHA-256 完全相同,证明没有翻页,只是重新读取第一页。
- 最终回答仍声称“已经完整查看”,属于确定性假完成。
这里还有两层必须区分的截断:
- TUI 工具结果预览约 400 字符、progress 预览约 90 字符,属于展示折叠。
- 写入模型历史的 4000 字符上限属于真实信息丢失,模型确实看不到中间内容。
fs-mcp 已经返回 offset/limit/totalLines/start/end/complete/nextOffset/hash structured content,但该证据目前没有进入模型可见的分页说明,也没有进入 Agent 的读取覆盖账本。
Architecture Direction
1. 启动源真相:TUI 生命周期与 Provider 生命周期解耦
启动必须拆成两个阶段:
同步读取配置/会话
↓
立即渲染完整 TUI shell(可输入、显示启动状态)
↓
后台并行连接普通 MCP + 启动 Agora MCP
↓
能力握手完成后原地 hydrate Agent/Memory/Model 状态
固定状态机:
shell_visible → connecting → ready
↘ degraded / failed(TUI 不退出)
要求:
runChat() 不得在首屏前等待完整 bootstrap()。
- 启动期间输入框允许编辑;用户提交的第一条消息最多排队一条,ready 后自动发送。失败时保留草稿。
- Provider/MCP 失败不得
process.exit(1);TUI 提供重试、doctor 详情和切换远程 Provider。
- 普通 MCP 使用
Promise.allSettled 并行连接,同时保持配置顺序和独立失败状态。
- Agora 仍必须执行真实 initialize、工具清单、协议、manifest、签名和 capability 验证;缓存不得跳过信任握手。
- 模型权重仍只在第一次 chat 时加载,启动阶段不得预加载 35B 模型。
2. Agora 发布源真相:npm 安装目录就是可复用的 standalone runtime
将 Agora 平台包从 Nuitka onefile 改为 Nuitka standalone 编译目录:
- 用户入口仍只有
agora Mach-O launcher。
- 平台包内部可包含多个编译后的 Mach-O、
.so/.dylib 和明确 allowlist 的运行数据,但不得包含任何 .py、.pyc、.js、source map、源码路径、私钥或 debug symbols。
- launcher 直接
execv 已安装的 standalone runtime,不做每进程临时解包。
- manifest 覆盖 standalone runtime tree 的全部可加载文件;验证每个 hash,并拒绝未列出的可执行/源码型文件。
- 对 runtime tree 内所有 Mach-O 递归执行 strip、Developer ID Hardened Runtime 签名和 release 审计。
- 保留现有 host protocol、native ABI、capability、notarization、Gatekeeper 和 npm integrity 机制。
- 产物变化必须发布新 semver;禁止复用已有版本制造同版本不同 hash。
--onefile-tempdir-spec 持久缓存只能作为实验对照,不能作为最终方案:它仍让首次启动承担解包成本,并引入缓存失效、权限和篡改边界。
3. 文件读取源真相:由 Read Receipt 证明覆盖范围
不得通过无限扩大 TOOL_RESULT_MAX_CHARS 解决。建立 provider 无关的 FileReadLedger,唯一可信输入是 read_file 的 structured content:
canonical_path
file_hash
covered_ranges[]
total_lines
complete
next_offset / next_cursor
每个模型可见页面必须包含受控正文和机器可读页脚,例如:
[read_file page] lines 1-80/198 · complete=false · next_offset=81 · hash=ab12…
规则:
- 默认读取改为受字符预算约束的分页,单页正文目标不超过 3200 字符,为页脚和下一轮推理留空间。
- 保留
offset/limit 兼容;正整数字符串必须规范化为 number,其他非法值必须返回 typed error,禁止静默回到第一页。
- 对超长单行增加
next_cursor(包含行/列位置或等价 opaque cursor),保证 minified 文件也不会静默丢中段。
ToolExecutionResult.structuredContent 必须进入 FileReadLedger;当前只进入 UI event/返回对象但在 Agent 主循环被丢弃的路径必须接通。
- read_file 的模型历史使用“已受限页面 + receipt”,不再经过会切掉中段的通用 head/tail compaction;其他工具继续保留通用压缩策略。
- 相同
path + hash + range/cursor 在同一任务重复时,不再把同一正文二次写入上下文;返回 duplicate_page 和正确下一位置。
- 文件 hash 变化时废弃旧覆盖范围,明确提示文件已变化。
exec cat/sed/head/tail 的文本不计入“完整读取”证据;完整代码审阅必须来自可验证 read receipt。
4. 完成源真相:没有覆盖证据就不能声称完整读取
复用 CompletionObligationAudit:
- 当用户要求“完整/全部审阅文件或项目”,增加
file_read_coverage obligation。
- 当候选最终回答主动声称“完整看过/fully read/已阅读全部代码”,若本任务存在未完成页面,也触发同一 gate。
- gate 返回未覆盖文件和下一位置,让 Agent 继续分页;达到有界补救次数仍不完整时,必须诚实报告“部分读取”,不能输出成功口径。
- TUI 明确区分
预览折叠、页面未读完 和 Provider 输出截断,不能都显示成同一个“截断”。
Reuse / Mature Solution Plan
优先复用现有实现:
- MA:
BootstrapResult、Ink App/store、provider:attempt/retry events、MCP client、structuredContent 管道、CompletionObligationAudit、PTY/visual helper、Context Usage。
- 文件工具:现有
offset/limit/totalLines/complete/nextOffset/hash 及分页重建测试。
- Agora:C++ launcher、manifest/hash verifier、native core、tarball source audit、strip/sign/notary/Gatekeeper/release evidence 流程。
- 打包方式:使用 Nuitka 官方 standalone 模式,不引入自定义解包器或常驻 daemon。
Implementation Checklist
Workstream A — Agora 与 MA 启动
Agora 仓库
MA 仓库
Workstream B — 文件分页与真实性
Validation
1. 启动性能测试
- 使用真实 npm candidate、真实 MCP stdio、同一参考 Mac,串行执行,禁止并发测量干扰。
- 至少执行 5 次 clean-install/cold run 和 30 次 warm run。
- PTY 分别记录:process spawn、首个完整 TUI frame、输入可编辑、Agora initialize、provider ready。
- 同时跑 remote Provider + 5 MCP 对照组。
- 保存原始 JSON timing artifact,不只粘贴平均值。
2. Agora 包验证
strings 不得出现 NUITKA_ONEFILE_*。
- npm tarball 清单不得包含
.py/.pyc/.js/.map、源码路径、私钥或 debug symbols。
- 修改任意 runtime 文件后 launcher 必须拒绝启动。
- 删除任意 manifest 文件、加入未允许可执行文件、协议/ABI 不匹配都必须失败。
- candidate 与正式 release runner 使用同一 layout 和同一审计逻辑。
3. 文件读取单元/契约测试
- 198/212 行普通源码可通过连续 receipt 无缝重建,覆盖范围无重叠、无缺口、hash 一致。
offset: 100 与 offset: "100" 行为一致;"abc"、0、负数、NaN 明确失败。
- 单行超过 20K 字符的文件通过 cursor 完整重建。
- 同页重复调用不重复写入模型上下文,并返回下一页。
- 文件中途修改后旧 ledger 作废。
- Context compact/session resume 后 ledger 语义保持,正文不无限膨胀。
4. 真实 Agent E2E
- 使用真实 packaged Agora + 本地白名单模型执行“完整审阅多文件项目”。
- Trace 必须证明:所有目标文件 page coverage 完成;没有相同 hash/range 重复;最终回答前无未读页。
- 再用一个远程 OpenAI-compatible Provider 执行同一 fixture,证明不是 Agora 特判。
- 增加反例:故意让模型给出字符串 offset、重复页和提前“完整看过”,MA 必须规范化、去重或阻止完成。
5. 全量回归
- MA build、全量 tests、
MA_RUN_PTY_TESTS=1、visual、真实 provider/tool calling、Context Usage。
- Agora 全量 tests、native release pipeline、packaged MCP、签名/篡改审计。
- skip 不算 PTY/packaged 验收通过。
Acceptance Criteria
启动
文件读取
Redlines
Acceptance Mapping
| Acceptance criterion |
Code or system path |
Validation/proof |
Old path status |
Notes |
| TUI ≤500ms 可见 |
src/cli/index.tsx, Startup Coordinator, Ink store |
5 cold + 30 warm PTY timing JSON |
pre-render full bootstrap removed |
完整 shell,不是外部 spinner |
| 输入排队且失败不丢 |
startup state/store, App hydration |
PTY ready/failure/retry cases |
process-exit startup removed |
最多一条待发送消息 |
| Agora initialize 达标 |
Agora standalone runtime、launcher |
packaged MCP cold/warm p95 |
onefile removed |
不加载模型权重 |
| 无源码且可验证 |
Agora build/release/manifest/signing |
tarball audit、codesign、篡改测试 |
source ban unchanged; manifest expanded |
多二进制允许,语言源码禁止 |
| MCP 并行且有序 |
src/index.ts, connection projection |
slow/failed/success mixed contract test |
sequential connect removed |
单个 MCP 失败不阻塞首屏 |
| Read Page 契约 |
servers/fs-mcp.ts, MCP types |
page reconstruction + long-line tests |
full-file default bypassed |
bounded chars + receipt |
| 参数不静默降级 |
fs argument normalization |
number/string/invalid matrix |
silent fallback removed |
错误必须可自愈 |
| 读取覆盖账本 |
ToolExecutor, agent.ts, new FileReadLedger |
range merge/hash change/resume tests |
structuredContent discard removed |
正文可 compact,证据保留 |
| 重复页不污染上下文 |
FileReadLedger/tool policy |
identical hash/range regression |
repeated-success path removed |
返回 next position |
| 禁止假完整 |
completion-obligations.ts, candidate final audit |
explicit request + spontaneous claim tests |
text-only success removed |
失败时报告部分读取 |
| Provider 一致性 |
Agora + OpenAI-compatible request loop |
同 fixture 双 Provider real E2E |
unchanged by design |
不是 Agora 特判 |
| Context Usage 无回归 |
agent.getContextUsage(), StatusBar |
宽/窄 PTY + compact/resume |
unchanged by design |
分页不清空 context |
Parent Scope Status
Non-goals
- 不在启动时预加载本地大模型。
- 不引入常驻 daemon、账号登录、机器绑定或联网授权。
- 不把 Agora 改回向用户分发 Python/JavaScript 源码;standalone 包仍必须是编译产物。
- 不取消通用工具结果压缩,也不把整个大文件一次性塞入上下文。
- 不修改 Agora Memory v2、MemoryPatch、内化和热拔插语义。
- 不用 onefile 持久缓存、splash screen 或单纯调大 timeout 作为关闭证据。
P0:消除 Agora 启动阻塞与
read_file截断假完成0.3.0(PR #408e110a5)+ Agora0.2.0(Agora PR #22cb4cb6d)Problem
当前最终候选存在两个直接破坏首次体验和用户信任的问题:
ma后,TUI 必须等待 Agora 二进制启动、MCP 握手和 Provider ready,用户会面对约 5~10 秒空白。read_file虽然先返回完整文件,但 MA 在写入模型历史前统一截为 4000 字符。模型尝试分页时,字符串形式的offset又被文件服务静默忽略,导致重复读取第一页,最后仍可能声称“已完整查看”。这不是两个孤立的小 Bug:前者让用户误判产品无法启动,后者让 Agent 在代码工作中给出没有证据的结论。两者都必须在正式交付前解决,不能用增加 spinner、扩大字符上限或提示词约束冒充完成。
Evidence
A. 启动性能
同一台 Apple Silicon Mac、同一候选包的串行实测:
ma到首个可见 TUI frameinitializeinitializeAgora 初始化的分步结果:
tools/list1~2ms、resources/list1ms、doctor10~11ms、runtime_capabilities1ms、models_list9ms。慢点集中在 packaged runtime 启动,不在 35B 模型加载,也不在能力查询。当前代码路径:
src/cli/index.tsx在render(<App />)前执行await bootstrap(...)。src/agent.ts#createAgent在返回 Agent 前执行await providerRuntime.ready?.()。src/provider/agora.ts#start同步等待 MCP initialize 和完整能力握手。scripts/build_native_release.py同时传入--onefile --standalone;产物包含NUITKA_ONEFILE_*标记并按进程解包到临时目录。Nuitka 官方文档明确说明:onefile 会在目标机器运行前自解包,默认使用每次唯一且随后删除的临时目录;
--mode=onefile已自动包含 standalone。npm 本身已经完成安装解包,因此这里继续使用 onefile 只增加重复启动成本:B.
read_file截断与重复读取真实 Agora 会话的脱敏证据:
src/agent/compact.ts对普通工具结果设置TOOL_RESULT_MAX_CHARS = 4000,保留 75% 头部和 25% 尾部。offset: "100"和offset: "95"。servers/fs-mcp.ts只接受typeof offset === "number";字符串会被静默降级成offset = 1。这里还有两层必须区分的截断:
fs-mcp已经返回offset/limit/totalLines/start/end/complete/nextOffset/hashstructured content,但该证据目前没有进入模型可见的分页说明,也没有进入 Agent 的读取覆盖账本。Architecture Direction
1. 启动源真相:TUI 生命周期与 Provider 生命周期解耦
启动必须拆成两个阶段:
固定状态机:
要求:
runChat()不得在首屏前等待完整bootstrap()。process.exit(1);TUI 提供重试、doctor 详情和切换远程 Provider。Promise.allSettled并行连接,同时保持配置顺序和独立失败状态。2. Agora 发布源真相:npm 安装目录就是可复用的 standalone runtime
将 Agora 平台包从 Nuitka onefile 改为 Nuitka standalone 编译目录:
agoraMach-O launcher。.so/.dylib和明确 allowlist 的运行数据,但不得包含任何.py、.pyc、.js、source map、源码路径、私钥或 debug symbols。execv已安装的 standalone runtime,不做每进程临时解包。--onefile-tempdir-spec持久缓存只能作为实验对照,不能作为最终方案:它仍让首次启动承担解包成本,并引入缓存失效、权限和篡改边界。3. 文件读取源真相:由 Read Receipt 证明覆盖范围
不得通过无限扩大
TOOL_RESULT_MAX_CHARS解决。建立 provider 无关的FileReadLedger,唯一可信输入是read_file的 structured content:每个模型可见页面必须包含受控正文和机器可读页脚,例如:
规则:
offset/limit兼容;正整数字符串必须规范化为 number,其他非法值必须返回 typed error,禁止静默回到第一页。next_cursor(包含行/列位置或等价 opaque cursor),保证 minified 文件也不会静默丢中段。ToolExecutionResult.structuredContent必须进入FileReadLedger;当前只进入 UI event/返回对象但在 Agent 主循环被丢弃的路径必须接通。path + hash + range/cursor在同一任务重复时,不再把同一正文二次写入上下文;返回duplicate_page和正确下一位置。exec cat/sed/head/tail的文本不计入“完整读取”证据;完整代码审阅必须来自可验证 read receipt。4. 完成源真相:没有覆盖证据就不能声称完整读取
复用
CompletionObligationAudit:file_read_coverageobligation。预览折叠、页面未读完和Provider 输出截断,不能都显示成同一个“截断”。Reuse / Mature Solution Plan
优先复用现有实现:
BootstrapResult、Ink App/store、provider:attempt/retry events、MCP client、structuredContent管道、CompletionObligationAudit、PTY/visual helper、Context Usage。offset/limit/totalLines/complete/nextOffset/hash及分页重建测试。Implementation Checklist
Workstream A — Agora 与 MA 启动
Agora 仓库
scripts/build_native_release.py从 onefile 改为 standalone-only 构建。libexec/runtime路径。doctor与 MCP initialize 冷/热启动性能门。MA 仓库
bootstrap拆成 shell bootstrap 与 async runtime hydration;保留 CLI、E2E 和 benchmark 可复用入口。await providerRuntime.ready?.();Agent chat 在首次提交边界等待 ready。连接中/ready/失败,不向 transcript 插入启动噪声。Workstream B — 文件分页与真实性
read_file offset/limit增加统一正整数规范化;非法参数 typed error,删除静默默认。ToolExecutionResult.structuredContent接入新的FileReadLedger。execute_command读取文本不构成完整读取凭证,并补回归测试。Validation
1. 启动性能测试
2. Agora 包验证
strings不得出现NUITKA_ONEFILE_*。.py/.pyc/.js/.map、源码路径、私钥或 debug symbols。3. 文件读取单元/契约测试
offset: 100与offset: "100"行为一致;"abc"、0、负数、NaN 明确失败。4. 真实 Agent E2E
5. 全量回归
MA_RUN_PTY_TESTS=1、visual、真实 provider/tool calling、Context Usage。Acceptance Criteria
启动
文件读取
Redlines
strings中无NUITKA_ONEFILE_*,构建脚本主路径无--onefile。runChat首屏路径不再await完整 runtime bootstrap;createAgent的 ready 等待不再阻塞 render。read_file非法offset/limit不得回退默认值。read_file不得再由通用 head/tail compaction 静默切掉中段。Acceptance Mapping
src/cli/index.tsx, Startup Coordinator, Ink storesrc/index.ts, connection projectionservers/fs-mcp.ts, MCP typesToolExecutor,agent.ts, new FileReadLedgercompletion-obligations.ts, candidate final auditagent.getContextUsage(), StatusBarParent Scope Status
Non-goals