Skip to content

[v2] 增加每项目 Node 驱动与按归属恢复 - #230

Merged
EricWvi merged 5 commits into
ora-space:mainfrom
ElephantGit:feat/774-project-node-driver
Oct 31, 2026
Merged

EricWvi merged 5 commits into
ora-space:mainfrom
ElephantGit:feat/774-project-node-driver

Conversation

@ElephantGit

@ElephantGit ElephantGit commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

为已接收的项目会话增加独立 Agent Driver:每项目共享一个固定 Node,不同 Workspace 使用独立 checkout,首轮启动 Agent,后续轮次提交到原物理会话。历史与终态保存成功后才 ACK;End 等待有效停止确认,正常结束保留工作目录。

连接断开返回不可用,重建前先由 supervisor 确认旧进程树停止。断线会话可单独调用 End 完成旧项目停止和移除,无需另一个 Run 触发;停止未确认时保留目录占用,后续 End 可以重试。初始化、clone 和未派发 Start 的 session 准备失败不永久缓存;控制等待有截止,每次尝试使用新的 execution ID,超时淘汰当前 incarnation,隔离迟到回复。已经派发过的 Agent 不自动重放 prompt。关闭等待遵守调用方 context。

范围仅包含 Driver 与按归属恢复辅助;公共 Service、catalog、main 与页面装配由后续 PR 承接。本 PR 已同步 main 8201254f。

验证:Go 1.27.1、真实 PostgreSQL 17.11 全包 gofmt/vet/race/build;十项新增 fake IPC 回归覆盖完整 Run/Append/End、持久化后 ACK、失败重试、断线停止证明、独立 End 恢复及同 Workspace 后继、控制超时与迟到回复、持久化失败不 ACK、并发关闭截止。独立 End 两项回归在旧实现失败,修复后相关回归 race 连跑十次通过。原生 WSL 的真实 Node/Agent fixture、长历史读回、取消及进程监督恢复均通过。公共装配候选复用本提交的同一驱动,真实 Service/Node 并发、重启读回及正式 CLI fixture-check 也已通过;其 Service 和入口不在本 PR 中。当前界面截图见关联运行。

v2,不涉及 specs。

Refs ora-space/circus#774

@wanglongan587

Copy link
Copy Markdown
Collaborator

独立只读审查(成员,非本运行 reviewer)

审过 commit c554fb4b5869b02a9d4fccd3fd74fca39551a7b4。结论:和 #774 已定的接缝一致(权限、接收、序号、SQL 归调用方,驱动只管物理 Node 和 ACK 门禁),未发现阻塞性正确性缺陷。下面四条都是非阻塞建议,希望在上层装配或下一个 PR 里处理。

已核对

  • 登记/恢复:先 fsync project-runtime.json 再启动;New 先 Recover,未知条目、symlink、登记不一致、socket 路径超限都 fail closed;Cleanup 先逐目录确认停止再删。node.RecoverRuntime 与 StartRuntime 共用同一把锁,活动 runtime 时恢复拒绝。测试覆盖保留 checkout、活动锁拒绝、伪造 marker、symlink 和长路径。
  • ACK 顺序:带 sequence 的 envelope 先写 node-events.jsonl 并 fsync;Agent 事件要 emit 成功才 ACK,失败会标 broken 并请求停止。agent_session_ended 只有在 reason 合法、Node identity 与握手一致时才 markStopped;无效终态不 ACK、不释放目录。
  • 终态映射:只有 end_turn 是 completed + AnswerComplete;max_tokens / max_turn_requests / refusal 记 agent_turn_incomplete,未知原因不泄漏原文。
  • 与集成分支 c52500ed 比对,internal/agentruntime/**.go 和 internal/node/recover_linux.go 逐字一致(只有 README 不同),组合验收确实用的是这份驱动。CI 在 head 上全绿(run 41033668393)。

非阻塞建议

  1. 控制回复没有单次超时(internal/agentruntime/project.go:206-222)。p.control 只等 p.ctx 和 p.dead,install_plugins、clone_repository 都没有自己的截止时间。Node 静默时 start() / prepareCheckout 的协程会挂到项目关闭;Close 也要等自己的 ctx 到期后 p.cancel() 才解得开,并返回 ErrStopUnconfirmed。建议给这两类控制等待加显式超时,让失败可预期。
  2. 会话/checkout 表不回收(internal/agentruntime/session.go:58/67/103、project.go:225-231)。d.sessions、p.agents、p.checkouts 只有插入没有删除;长进程里每个 session 会一直占着 frozen prompt/executor。如果为了 End 幂等不能删,至少在 README 写明「依赖进程重启回收」。
  3. checkout 失败被永久缓存(project.go:225-231)。prepareCheckout 每个 workspace 只建一次 entry,clone 失败后 entry.err 一直复用,同一对话目录在本次进程内再也准备不起来,只能重启。如果这就是「失败即隔离、必须重启」的约定,请在 README 补一句;否则建议失败后允许重建 entry。
  4. 驱动主链路没有自动化覆盖。本 PR 只测了登记/恢复/终态校验,session / begin / submit / observe / End 靠集成分支的真实 Node 测试。合入 main 后这道回归就跑不到了,建议在装配 PR 里补 fake IPC 的驱动测试(或可跳过、但有 assets 时能跑的真实测试),至少覆盖「持久化成功才 ACK」「End 等有效终态」「同一 checkout 串行」。

装配时值得注意的两点(不是本 PR 要改的)

  • 项目 IPC 一旦 transportFailed,该项目 runtime 会保持不可用(fatal + dead),进程内不会重建,End 一直返回 ErrStopUnconfirmed。这和「不释放目录」的保守方向一致,但上层要把它映射成明确的不可用,而不是让调用方无限重试。
  • observe 里 HistorySnapshot 的 Ref.TurnID 在 observation.TurnID 为空且没有 active turn 时会是空串(例如终态之后到达的 thread_preview)。公共层写成 execution.Event 前要先决定空 TurnID 是否合法,避免存储层拒绝。

@EricWvi EricWvi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes before merge (details in the linked run issue):

  1. After the Node transport dies, p.fatal is reported as ErrWorkspaceBusy in session.go, and the dead projectRuntime stays in d.projects, so every new session for that project fails as "workspace busy" until the process restarts. Return ErrUnavailable and drop dead or failed projects from the map so the next session rebuilds them.
  2. Failures are cached forever: project initErr, failed checkouts entries and d.sessions entries with prepareErr are never cleared, so one transient failure (plugin install, clone) disables that project, workspace or session permanently. control() / prepareCheckout also have no per-call timeout, so a single missing Node reply wedges the entry.
  3. Please add a fake-IPC test layer for the driver path (session/begin/submit/observe/End) that covers the recovery cases in 1 and 2.
  4. (Can follow later) reclaim sessions / agents / checkouts entries.

Refs ora-space/circus#774

@ObsisMc ObsisMc left a comment

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.

复审 head 6348e75ab62bd4c9059818d8706b50a3342ce619(基线 main 8201254f)。

owner 上轮四项的核对

  1. 断线后返回 ErrUnavailable,不再报 ErrWorkspaceBusy;Driver.project 发现 fatal 后先 close(supervisor 停止证明),成功才 dropProject 再重建,未确认时保留原条目。✅
  2. initErr 走 close + drop;clone 失败条目从 checkouts 删除;未派发 Start 的 prepareErr session 撤销 owner 预留并移出 d.sessions;control 有两分钟上限,单帧十秒,超时淘汰整个 incarnation,每次尝试用新 execution ID。✅
  3. 八项 fake IPC 测试覆盖 Run/Append/End、持久化后 ACK、失败重试、断线停止证明、控制超时与迟到回复。✅
  4. 回收后置,README 已写明。✅

本地 Go 1.27.1(arm64):go vet 和 go test -race ./internal/agentruntime/ ./internal/node/ 通过;head 上的 server/frontend/e2e CI 也是绿的。

需要修改:断线后单靠 End 永远确认不了停止

已经派发过 Start 的 session,Node 断开后 End(sid) 只会 requestEnd(send 失败)再等 a.stopped;它从不对已死的项目调用 p.close。a.stopped 只有在另一个 Run 走到同一项目的 Driver.project、触发重建时才会被 closeResources 关掉。End 里的 p.close 分支只处理 initErr,不处理 fatal。

复现(基于本 PR 的 harness,stopOK 保持 true,只是断开传输):

done := startIPCRun(h, ctx, h.session("first", "workspace"), "turn", discardIPCEvent)
old := receiveIPC(t, h.created)
nextIPCCommand(t, old, "start_agent_session")
old.Close()
receiveIPC(t, done) // agent_transport_closed
h.driver.End(ctx, "first") // 连续三次都是 ErrStopUnconfirmed,old.stops == 0

现有的 TestDriverIPCDisconnectRequiresStopProofBeforeRebuild 之所以能让 End("first") 最终通过,是靠中间那次 next session 的 Run 触发了重建。

影响(结合 #237 的消费方式):

  • 工作流的下一个节点复用同一个 WorkspaceID 时,executionruntime 先 agent.End(prior),失败就返回 ErrStopUnconfirmed。同项目没有别的 Run 来触发重建,这个工作流就一直卡住,只能重启进程。这正是上轮第 1 项要消除的情况。
  • 公开 End 或删除这条已断线的对话也会一直返回停止未确认,直到同项目有新会话进来。

建议:End 在 a.project.dead 已关闭(或 p.fatal)时,用调用方的 ctx 走 a.project.close(ctx),成功后 d.dropProject(a.project)。closeResources 已经会 markStopped 该项目下所有 agent,所以 End 再检查一次 a.stopped 就能返回 nil;close 失败时仍返回 ErrStopUnconfirmed,保留原条目,不破坏"先有停止证明再重建"的约束。另外把上面的复现补成回归测试。

其余没有发现阻塞问题。改完重新送审后我再复核。

@ObsisMc ObsisMc left a comment

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.

复审 head 594d25dbe25d46baaddead1748d84cb96c1b1835(基线 main 8201254f):上轮的阻塞问题已经解决。

  • 项目 dead/fatal 时,End 用调用方 ctx 走 p.close,由 supervisor 给出停止证明,成功后有条件地 dropProject;停止失败仍返回 ErrStopUnconfirmed,项目和 Workspace owner 都保留,后续 End 可以重试。closeGate 和 closed 标志让它与 Driver.project、Close 并发时保持幂等。
  • 我上轮的复现(stopOK=true、只断开传输、不发起别的 Run)在新 head 上第一次 End 就返回 nil,supervisor 停止 1 次。
  • 新增的两项回归覆盖了停止失败保留占用、单靠 End 重试成功、并发和幂等、同 Workspace 后继成功,以及等待别人关闭时遵守 ctx 截止。
  • 本地 Go 1.27.1:go vet 通过;go test -race -count=10 ./internal/agentruntime/ ./internal/node/ 通过。head 上的 server/frontend/e2e CI 全绿。
  • 改动文件都在 v2-server/ 下,都是交付物;描述写了「v2,不涉及 specs」和 Refs ora-space/circus#774。

非阻塞:正常结束的 session、agent 和 checkout 条目还是保留到 Driver 关闭,README 已写明回收后置,由后续承接。

@EricWvi
EricWvi merged commit bad9e4d into ora-space:main Oct 31, 2026
3 checks passed
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.

4 participants