Skip to content

fix(agent): 修复 MEMMY-592 输出位置与完整交付核验(v1.2.6) - #677

Closed
yxyi123 wants to merge 2 commits into
MemTensor:v1.2.6from
yxyi123:fix/memmy-592-output-location-v126
Closed

yxyi123 wants to merge 2 commits into
MemTensor:v1.2.6from
yxyi123:fix/memmy-592-output-location-v126

Conversation

@yxyi123

@yxyi123 yxyi123 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

关联:MEMMY-592;前一版本:#627(目标 v1.2.4)。本 PR 基于 v1.2.6。

任务读取外部目录的参考资料后,产物可能沿用参考资料的保存位置;声明交付 PDF 和 Excel 时,也可能只生成其中一个就回复完成。本次默认使用当前工作区,采用当前用户明确指定的其他输出目录,并在交付前核验已声明的完整清单、生成任务结束状态及新到达的用户要求。

修改内容

  • 用工具参数 output_paths 声明最终产物,在工具执行前校验并累计登记。exec 必须传入(无最终产物时为 []),文件写入、编辑和补丁工具使用可选参数;后续省略或传空数组不能删除已有交付义务。中间脚本、内部 README 不必登记。
  • 文件工具在写入前检查输出位置;最终核验直接使用登记的原始路径,检查存在性、普通文件类型和规范化后的输出目录边界。回复中的链接不会缩小清单,含括号的文件名不经过 Markdown 解析。
  • 同一任务跨父任务后续轮次共享清单;子任务登记汇入父任务,子任务检查自身部分,父任务复核完整清单。修改输出目录时按相对路径迁移义务;已结束任务拒绝旧子任务继续登记。内存清单丢失时保持未确认,要求重新声明。
  • 已登记最终产物的托管 exec 使用实际会话完成状态,文件已出现但后台写入尚未结束时不能宣布完成;状态引用在会话条目清理后仍可读取。父任务同时等待相关子任务结束后再复核。
  • 在结束钩子之后及 GUI/消息总线最终发布边界重新检查。用户新输入到达时丢弃旧完成候选、处理新要求,再检查完整清单;达到迭代上限时如实报告未完成。

保留 v1.2.6 的相对补丁路径约束、队列消息消费/失败重试、子任务计算机使用授权与清理、上下文超限处理和现有 hook 生命周期。没有引入文件计数、固定后台等待预算或持久化完整清单。

验证

最新合并提交 903b9059 已同步 v1.2.6 的 3a5f5c44(#674)。冲突仅涉及 filesystem.ts,保留上游编辑参数的前置校验、类型和说明,同时保留 output_paths 登记及位置检查;新增两项组合回归。

冲突处理后的验证:Agent 类型检查、构建通过;全量 7152 通过、80 跳过;162 项定向测试通过。

以下为此前提交 f5e41c71 的本地验证结果;本次冲突处理没有重新构建 DMG 或重跑真实模型任务:

  • 仓库 typecheck、lint、npm test 通过;Agent 类型检查通过。
  • Agent 全量:7046 通过、80 跳过;迁移相关定向复跑:189 通过。
  • macOS ARM64 CN unsigned DMG 构建通过。从 DMG 复制安装后,11 个本次修改的运行时 JS 模块与编译输出逐字节一致;19 项包内交付集成用例通过,包括缺失第二产物、括号文件名、目录迁移、子任务清单、后台未结束和钩子修改后的复核。
  • 包内集成测试实际生成 PDF 和 Excel,并独立读取内容验证。
  • 真实模型 gpt-5.6-luna 在测试进程显式设置 reasoning_effort=none 后,完成 6 轮请求及 PDF/Excel 生成、内容和交付链接核验;未修改保存的用户配置。
  • 实际安装程序的隔离首次启动、欢迎页显示、重启和 SQLite 测试数据保留检查通过。

验证边界与已知问题

  • 正常退出仍会触发 5 秒清理超时。换用未修改的 v1.2.6 Agent 运行时做诊断对照后,首次与重启也复现。本次未修改桌面退出代码,根因尚未定位;该对照不是另行重建的原始分支 DMG。因此不将整包正常退出计为通过。
  • 当前模型服务在默认参数下曾返回工具调用与推理参数兼容性 HTTP 400。上述真实模型通过结论限定为显式 reasoning_effort=none 的测试条件,不表示默认参数问题已修复。
  • 最终产物由模型声明;这不是 OS 沙箱,不能完整拦截任意脚本中未声明的写入,也不核验报告内容质量。完整交付清单保存在内存中,没有增加持久化恢复机制。

this.bus.outbound.put(response);
this.committedOutputResponses.add(response);
}
if (!pendingGenerators.length && stopReason !== "deliveryPending") {

@origami-chen origami-chen Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

交付失败后清除了清单,用户继续任务时会漏检缺失文件。模型声明生成 report.pdf 和 data.xlsx,实际只生成 PDF,两轮修复后仍缺 Excel。本轮会正确提示“尚未确认交付”,但用户接着说“继续完成刚才的两个文件”时,如果模型直接回复全部完成,缺失的 Excel 就不会再被拦截。补充探针通过真实 processDirect 连续两轮复现了这个结果。
原因是这里在没有运行中的生成任务、且停止原因不是 deliveryPending 时就调用 retire(),交付核验失败的 toolError 也满足条件。retire() 同时清空文件清单和 pendingOutputDelivery 标记,下一轮创建的是没有任何交付义务的新状态。
建议将“本轮停止”和“文件任务结束”分开:核验失败、模型错误或预算耗尽后保留未完成清单,同一任务继续时仍核验全部文件;成功交付或明确取消/开始新任务时再清理。普通用户入口和子任务结果入口共用的发布逻辑需要一起调整,并补上“缺第二文件→修复耗尽→用户继续→仍缺文件不得完成”的跨轮回归,同时验证成功与取消后不会污染新任务。


async selectDirectory(value: string, userInstruction: string): Promise<string> {
if (!this.active) return "Error: this delivery task has ended; its output directory cannot change.";
if (!userInstruction.trim() || !this.shared.requests.some(request => request.includes(userInstruction))) {

@origami-chen origami-chen Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

子任务仍可引用旧用户要求,把输出目录改回已经被替换的地址。用户先要求保存到旧目录,子任务开始执行;随后用户改为新目录,父任务也已经调用工具完成切换。如果子任务此时根据尚未更新的上下文调用 set_output_directory,引用最初的用户原话,当前实现仍会接受,并把父子共享的目录和交付清单一起迁回旧目录。探针中,子任务随后通过 write_file 在旧目录登记并生成文件,父任务的完整核验返回通过,尽管用户最新要求的是新目录。
原因是 requests 累计保存所有用户输入,这里只检查原话是否曾经出现,没有判断该目录要求是否已被更新要求替代。共享状态解决了“子任务读不到新目录”,却仍允许子任务把新状态覆盖回旧状态。
建议为有效目录要求保留用户输入的顺序或版本,并把主任务、注入的新要求和子任务的目录设置统一到这个规则:旧要求不能覆盖更新的目录决定;异步解析结束、提交变更时也要复核版本。补充“子任务模型请求尚未返回→父任务切换目录→子任务返回旧目录工具调用”的回归,确认旧调用被拒绝、清单保留在新目录,只有新目录下的完整产物才能通过父任务核验。

// Changing obligations does not copy files or rewrite already running commands.
if (directory !== previous) {
for (const entry of this.shared.entries.values()) {
entry.path = path.join(directory, path.relative(previous, entry.path));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目录迁移只更新记录中的路径,后续声明可能覆盖仍未完成的清单项。例如先把输出目录设为 workspace/exports,登记 workspace/exports/report.pdf;用户随后改为工作区根目录,按本 PR 的相对路径迁移规则,原交付义务应变成 workspace/report.pdf。如果之后又声明并生成一个 workspace/exports/report.pdf,当前实现会丢掉前一个义务:根目录的报告仍不存在,完整核验却返回通过。两个路径都位于新的允许目录内,因此写入位置检查不能挡住这个问题。
原因是迁移只修改 entry.path,没有同步修改 Map 的键。原记录的键仍是 workspace/exports/report.pdf,值中的路径却变成了 workspace/report.pdf。后续登记前一个路径时,recordFile() 按值找不到匹配记录,再用旧键执行 set(),就覆盖了迁移后的记录及其所属任务信息。
建议迁移时重建以新路径为键的清单,统一登记和查找规则,保留父子任务的归属信息;如路径合并,应合并归属而不是覆盖记录。补充“子目录迁到父目录→再登记旧绝对路径”的回归,确认两个义务都保留,缺少任意一个都不能通过父任务或对应子任务的交付检查。

@yxyi123 yxyi123 closed this Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants