feat: add AI-led team workspace with conversational setup - #4376
KashiwaByte wants to merge 3 commits into
Conversation
Signed-off-by: KashiwaByte <471314513@qq.com>
Signed-off-by: KashiwaByte <471314513@qq.com>
Signed-off-by: KashiwaByte <471314513@qq.com>
4aac19c to
6426983
Compare
steven-kid
left a comment
There was a problem hiding this comment.
动机
评审 head:642698356ab9ee5a977b095773ba6d9d915200b1;base:a112bdda729942fe465e197278b5a2bbde14003b。结论:REQUEST_CHANGES。
该 PR 希望把长期业务目标、AI 执行、成员贡献和反馈放到一个可对话创建的本地工作区。原有 LoopX 已有 Goal、Todo、quota、monitor、Chat 和进程管理,因此真正新增的价值应是团队交互宿主及人类贡献界面,而不是第二套调度和执行正确性契约。选择可选 package、通过 CLI 使用 canonical 状态是合理起点;但不能仅凭“宿主元数据”这个名称认定所有 SQLite phase/revision/启停状态都无需与原有 owner 比较。
我先比较了不新增宿主、扩展现有边界与完整新增宿主三种选择。仅保留现状不能交付这里的团队交互;但进程终止已经有可复用的正确实现,不需要独立发明。下述真实反例说明新的执行封装当前没有兑现 bounded execution 的关键保证。
改动思路
HTTP 与静态页面提供对话草稿,用户确认后才创建 Goal/成员。LoopX 客户端用现有 CLI 创建、claim、complete、quota、lifecycle、monitor;Workspace 负责计划、执行、独立模型验收、人类请求、反馈、监测和常驻循环;SQLite 记录宿主元数据、反馈及回放 journal。AirJelly 默认关闭,启用后只读取明确选定实例的时间线。
正向测试使用真实临时 LoopX 状态,合成模型先写文件、再提供验收结构,最终真实 Todo complete;重复 apply_execution 不再调用模型或花第二个 quota slot。这里能证明宿主到 canonical CLI 的转移,不能证明真实模型确实独立验收,因为 sufficient 正是 fixture 提供的值。
负向则直接执行新 Codex.ask 的真实 subprocess 边界:模型进程超时后,parent 的退出不足以证明整个进程组已停止。这个 owner 缺陷发生在结果回放和下轮验收之前,不能由 SQLite journal 修复。
具体改动
server.py的本地 HTTP host 与路由、web/app.js的 dialogue/commit/render,以及 index/style:覆盖概览、Goal、成员、请求、反馈、文件、监测和设置。写路由要求本地 Host、Origin 校验及特定 JSON 请求头;草稿选项只是建议,提交仍需明确动作。静态和 artifact 路径有 containment 检查。页面是中文独立产品面,未加入自动安装的核心入口。service.py::Workspace.tick/execute与loopx_client.py::LoopX:常驻轮询 canonical guard/todos,经 claim 后执行及验收,再通过 CLI 写回;ready journal 在模型调用前回放。人类 supplement/execute/judge 分开,观察证据不能直接成为 judge 决策,纠正按范围记录。监测无变化不调用模型,有变化才分析;快照把真实 todos 与 host 验收标记组合呈现。service.py::Workspace.ask、model.py::Codex.ask/cancel:本地每日调用计数和 enabled/revision 门禁、模型锁、read-only/workspace-write sandbox、严格结果 schema。问题位于 timeout 的进程组清理分支,详见下一节。store.py::Store与 service 的 apply/replay:SQLite WAL、FULL 同步和记录锁保存 host 配置、成员、请求、runs、observations、lessons 等。新增多个可影响调度的 phase/boolean/revision;本轮未完整证明它们在外部 canonical 更新、所有 crash 点及恢复时的派生/冲突语义,不能将“不是 Todo 数据库”直接当作没有重复 authority 的证据。airjelly.py::AirJelly.runtime/events:检查唯一活跃实例、runtime 身份及 capability,固定 loopback 请求且拒绝 redirect,限制时间窗并将观察与显式判断区分。README 描述启动、私有目录、模型限制和回放;三个测试文件分别覆盖真实 CLI 集成、HTTP 边界及合成对话。13 个新增文件 +1764/-0 均纳入评审,未修改核心文件;不因行数本身拒绝,但进程执行和恢复机制应优先复用。
对主干的风险
[P1] 超时后仍有子进程继续执行。 packages/team-workspace/model.py:91–98:向进程组 SIGTERM 后,只在 proc.wait(timeout=5) 再次超时时才 SIGKILL。若父进程及时退出、同组子进程忽略 SIGTERM,wait 成功就跳过 SIGKILL;finally 又清空 self.process。宿主已经报告“本轮超时”,遗留子进程仍能改变 workspace,与下轮检查/重试并发。
独立复现使用无网络的合成可执行程序:parent fork 子进程,子进程忽略 SIGTERM 并在第 3 秒写一个临时 marker,parent 保持运行;对真实 Codex.ask(..., write=True) 设置 2 秒 timeout。结果:ask 报超时后 marker 仍出现。对同一程序执行仓库既有 loopx/extensions/process_runtime.py::run_capped_process,marker 不出现;该实现即使 leader 已退出也会继续清理进程组。探针最后清理了自身创建的子进程。首次更短超时探针没有观察到问题,延长启动窗口后复现;结论基于后者的实际写入,而非 kill(0) 或父进程状态猜测。
最小修复:复用既有进程树终止语义或抽取共享终止 owner,TERM 宽限后确认/强制清理整个组,不能把 leader 已退出作为完成条件;同时覆盖显式 cancel/暂停。补充一个真实 subprocess 回归,断言超时返回后子进程不能再产生副作用。保持有界输出、退出状态及清理的责任一致,避免再增加一个仅看 parent 的判定。
验证:三个 package suite 15 tests passed(隔离 HOME,真实临时 LoopX 状态,模型为 fixture),Node syntax check 和 diff check 通过。这些测试未覆盖上述后代进程边界,因此全绿与反例不矛盾。没有独立运行真实 Codex、真实 AirJelly 或完整浏览器旅程;也未完成 quota/本地门禁并发、所有 journal crash 点和外部 canonical 状态切换的矩阵。这些标为未验证,不据此编造第二个 bug 或声称完整 authority/default-off 证明。核心默认文件未变并不自动证明启动此宿主后的双向状态一致。
我的整体评价
交互目标清楚,CLI 复用、明确创建、人类判断与观察分离等方向值得保留。但实际子进程能够越过宣称的执行时限,属于阻塞问题。请先修复/复用进程管理边界,再补齐宿主 state 与 canonical owner 的冲突、恢复和暂停证据;复审应覆盖整个新宿主,不能只重新跑 15 个 fixture 测试。当前不批准、不合并。
English verdict: REQUEST_CHANGES at 6426983. P1 in packages/team-workspace/model.py:91–98: timeout cleanup sends SIGKILL only if the parent remains alive. A SIGTERM-resistant child can keep writing after ask has reported timeout and cleared its process handle. A real synthetic subprocess reproduced the late write; the same fixture under existing loopx.extensions.process_runtime.run_capped_process produced no late write. Reuse the established process-tree cleanup semantics and test descendant effects after timeout/cancel. All 15 package tests, Node syntax and diff checks passed, but model fixtures do not cover this boundary. Full external-state/replay/quota consistency, live Codex/AirJelly and browser validation remain unverified. No merge performed.
|
English verdict: REQUEST_CHANGES at 6426983. P1 in packages/team-workspace/model.py:91–98: timeout cleanup sends SIGKILL only if the parent remains alive. A SIGTERM-resistant child can keep writing after ask has reported timeout and cleared its process handle. A real synthetic subprocess reproduced the late write; the same fixture under existing loopx.extensions.process_runtime.run_capped_process produced no late write. Reuse the established process-tree cleanup semantics and test descendant effects after timeout/cancel. All 15 package tests, Node syntax and diff checks passed, but model fixtures do not cover this boundary. Full external-state/replay/quota consistency, live Codex/AirJelly and browser validation remain unverified. No merge performed. |
|
我感觉这个思路是ok的,loopx仓库里有篇doc,讲了attached和managed两种模式,我感觉可以呼应上那个doc,直接在当前loopx的前端上做一个原型 |
|
建议让agent找到那篇doc,调研一下当前的loopx,写篇rfc,然后这个pr作为那个rfc的step 1,最简的原型 |
|
按上面的建议,那篇 doc(attached / managed 两种模式)已经整理成 RFC 并合入
它把原来只在桌面执行前端提案里的 Mode A / Mode B,抽成一份与前端无关的契约:每个 这个 PR 与 RFC 的对应关系:本 PR 就是 RFC 交付计划里的 M1(Step 1 最小原型)。 对照 RFC 第 5 节的宿主接入契约,这个原型已经天然落在契约上:
合并前需要处理的既有发现(也是 RFC M1 的进入门槛之一)
几点按 RFC 需要明确的
English. The attached/managed mode doc is now a merged RFC: This PR is the M1 Step 1 minimal prototype in that RFC's delivery plan. Its Please keep the preview boundary: declare and read back the prototype's mode and |
huangruiteng
left a comment
There was a problem hiding this comment.
Host admission contract review — agent-session-execution-modes-v0
Reviewed head: 642698356ab9ee5a977b095773ba6d9d915200b1.
The existing REQUEST_CHANGES already covers the process-tree P1 and the host-state
authority risk. This comment adds only the host admission lens from
docs/architecture/rfcs/agent-session-execution-modes-v0.md,
and does not repeat those findings.
Verdict: hold. The direction is right and worth landing as the RFC's step 1.
Do not merge until items 1, 2, 4, 5 and 6 below are addressed.
- No declared execution mode. The package never declares or persists
managed_runtime/attached_host, while
packages/team-workspace/loopx_client.py:53registers an Agent andserver.py
runs a resident scheduler thread whoseWorkspace.tickactually executes. Under
the host contract an undeclared host may read LoopX state but may not hold an
executing session binding. Disabling the goal heartbeat at bootstrap
(loopx_client.py:51-53) mitigates the race but is not a declared mode; the
declaration has to be explicit and readable back. - Executor is undeclared and unreadback. Registration passes only
--agent-id,
and the executor is a hard-coded localcodex(server.py --codex-bindefaults to
codex;model.py::Codex.askspawnscodex exec). Nothing distinguishes an
operator-credential executor from an individual CLI login, and the shipped managed
executor readback (executor kind, availability, typed reason, fail-closed) is not
reused. - Single executor relies on the claim lease only. Reusing canonical
todo claim
(loopx_client.py:88-90) together with the host'sfcntlsingle-instance lock is
the right direction, but if the same Agent is later bound by the goal heartbeat or
loopx turn, only the claim lease arbitrates. The contract asks for a declared
binding plus a typed failure. - Host-private ingress. Draft → explicit confirm → CLI write is correct. However
human requests, feedback and model-interpreted follow-ups live in the host SQLite
store (store.py,/api/goal/feedback) and drive execution, so
awaiting/submitted/gap/verifiedis a second state machine that LoopX readback
does not declare. - Readback boundary.
/api/statereturns host rows wholesale (settings,
jobs,artifacts,native_dashboard_url), and every error path is free text
({'error': ...}) with no stable code. The monitor card renders a local filesystem
path (web/app.jsmonitorCard→m.path). That is acceptable for a
loopback-only page, but the snapshot is not a public-safe projection and must not
be mirrored into LoopX or Lark readback as-is. - First-screen review gate. The PR adds a new product first screen
(web/index.htmlsidebar and the "新建业务目标" primary CTA). Per the repository
first-screen gate, the owner previews that screen and confirms it against
docs/development/design.mdbefore merge.
Retained as correct: canonical CLI reuse, create-only-after-confirmation, the split
between human judgement and observation, the optional package shape, and the
loopback-only boundary.
中文
宿主接入契约评审 — agent-session-execution-modes-v0
复核 head:642698356ab9ee5a977b095773ba6d9d915200b1。已有 REQUEST_CHANGES 指出了进程树清理
P1 与宿主状态 authority 风险;本条只补充宿主准入契约口径,不重复既有发现。
结论:hold。 方向正确,值得作为 RFC step 1 落地;下列 1、2、4、5、6 项处理后再复审合并。
- 未声明执行模式。 全 PR 没有
managed_runtime/attached_host的声明与持久化读回,
而packages/team-workspace/loopx_client.py:53注册 Agent、server.py以常驻调度线程 +
Workspace.tick实际执行。按契约,未声明的宿主可以读 LoopX 状态,但不能占用执行中的会话
绑定。bootstrap 时传--codex-app-heartbeat no(loopx_client.py:51-53)能避免竞争,
但不等于模式声明;声明必须显式并可从读回验证。 - 执行器未声明、未读回。 注册只传
--agent-id;执行器是硬编码的本地codex
(server.py --codex-bin默认codex、model.py::Codex.ask调codex exec)。因此无法区分
operator 凭据执行器与个人 CLI 登录执行器,也没有复用已发布的 managed executor 读回
(executor kind / available / 类型化 reason / fail-closed)。 - 单执行器只靠 claim 兜底。 走 canonical
todo claim(loopx_client.py:88-90)加宿主
fcntl单实例锁方向正确;但若同一 Agent 之后被 goal heartbeat 或loopx turn绑定,就只剩
claim 租约仲裁,缺少契约要求的绑定声明与类型化失败。 - 宿主私有 ingress。 草稿→显式确认→CLI 写入是对的;但人类请求、反馈与模型解释出的后续
任务存在宿主 SQLite(store.py、/api/goal/feedback)并驱动执行,
awaiting/submitted/gap/verified是第二套状态机,LoopX 读回没有声明这条 ingress。 - 读回边界。
/api/state整体返回宿主机行(settings、jobs、artifacts、
native_dashboard_url),错误只有自由文本{'error': ...},没有稳定错误码;监测卡片渲染
本机文件路径(web/app.js的monitorCard→m.path)。本地回环页面可以这样展示,但该
快照不是 public-safe 投影,不能直接镜像进 LoopX / Lark 读回。 - 前端首屏评审门。 本 PR 新增产品首屏(
web/index.html侧栏与「新建业务目标」主 CTA);
按仓库首屏门禁,合并前需 owner 预览首屏并对照docs/development/design.md确认。
保留:canonical CLI 复用、确认后才创建、人类判断与观察分离、可选 package、loopback-only 边界。
huangruiteng
left a comment
There was a problem hiding this comment.
动机
长周期 Goal 目前只有 CLI 与原生 Goal chat 两个入口,用户想把"一条业务方向"或"某个同事"用自然语言描述出来时,缺少一个可以来回确认的中间层:要么直接写进 Goal/Todo,要么把判断留在对话里。本 PR 增加一个可选的本地 team workspace:把自然语言对话整理成可 review 的草稿,只追问最高价值的缺失信息,给出上下文相关的 GenUI 选项同时保留自由输入,并在用户单独确认后才写入 Goal/成员记录;执行、独立验证、人类补料/执行/判断请求、有范围的纠正记忆、可选 AirJelly 证据读取与常驻调度都挂在这一层,而 Goal、Todo、quota、monitor、生命周期状态仍由 LoopX 作为唯一权威。没有任何生产模块被改动,整个 workspace 是新增的独立包(13 文件 +1764/-0)。
改动思路
放置选择是清楚的:这是"可独立分发、可选安装"的能力,而不是核心 control plane 契约,因此落在 packages/team-workspace/,也不需要为它新增 capability。LoopX 侧只通过 CLI 边界被调用(loopx_client.py 以 --format json 调 loopx,把返回投影成 RuntimeError),没有复制 quota/Todo/Goal 的状态规则,也没有第二份持久化权威:store.py 只保存 workspace 自己的草稿与会话。Codex 执行被限制在 model.py 的一个适配器里,固定 --sandbox workspace-write|read-only、approval_policy="never"、--ephemeral,并主动清掉 CODEX_THREAD_ID,避免污染宿主的 lane 绑定。由于是纯增量、无 feature gate,关闭方式就是使用与否,隔离性是默认成立的。
具体改动
关键代码讲解
model.py:77-108:ask()用codex exec跑一轮受限执行,schema 约束输出,超时后调cancel();这是本 PR 唯一会创建子进程的地方,也是唯一的阻塞项所在。server.py:本地 HTTP 服务。写入要求 JSON content-type,Host/Origin 做 loopback 校验(server.py:77静态文件、server.py:145artifact 路径都用is_relative_to做包含性校验),没有把 workspace 目录暴露成任意路径读取器。loopx_client.py:30,122):统一 CLI 桥。所有 LoopX 操作走真实 CLI,失败转成带原文的类型化RuntimeError;quota 花费带上 goal/agent/turn-instance/todo 与--scan-path,即交付结算仍由 LoopX 记账。service.py:对话 → 草稿 → 确认 → 写入的编排层;task()(:157)建 Todo 时只带标题与 acceptance 说明,这一步与主干当前的 quota 交付契约要求不一致(见下)。- 测试与前端:
test_workspace.py(真实 CLI 端到端)、test_http.py、test_dialogue.py与web/三件套;node --check覆盖前端语法。
对主干的风险
两个 exact head 阻塞项,都是可复现的,不是文字判断:
其一(P1,超时后进程组回收不完整):model.py:69-101 的 cancel() 只对进程组发 SIGTERM,只有在 proc.wait(timeout=5) 自身超时后才会补 os.killpg(..., SIGKILL)。只要直接子进程在宽限期内退出,忽略 SIGTERM 的后代进程就能活下来继续写。我用一个合成二进制在 exact head 上实测:宿主在 2.0s 抛 RuntimeError('本轮 Codex 超时…'),标记文件在其后 3.5s 仍然被写成功(host: descendant side effect after timeout report: True);同一个二进制改用仓库既有 owner loopx.extensions.process_runtime.run_capped_process,结果是 failure_kind=timeout 且迟到副作用为 False。也就是说"已报告超时"之后 workspace 目录仍可能被写入,而这正是该模块承诺要禁止的。仓库已经有正确的 owner:_terminate_posix_process_group 在宽限期后无条件 killpg(SIGKILL)。最小修复是照它升级一次 SIGKILL(或直接复用该 owner),而不是只在自己 wait 超时后才补刀。
其二(P1,自报验证在 head 上不可复现):PR 描述写的 python -B packages/team-workspace/test_workspace.py(9 passed)在 exact head 上得到 Ran 9 tests ... FAILED (errors=3),pytest 运行同样 3 failed, 12 passed。失败点都在 loopx_client.py:122 的 quota spend-slot ... --execute,主干守卫返回 quota spend requires an explicit Todo delivery contract; declare repository/write requirements or mark the Todo as explicit non-delivery(slot_accounting.py:467)。该守卫来自已合并的 #3519,且已经在本 PR 的 merge base 上,所以这不是"分支落后主干"造成的差异,而是新代码还没有满足当前主干策略:service.py 建 Todo 时只写了 acceptance note。修复方向是在同一次交付里声明 repository/write 需求或显式标记非交付,再在新 head 上重跑这条自报命令。
其余风险面我已经独立看过:服务只监听本地并做 Host/Origin 校验,静态与 artifact 路径都有包含性校验;外呼只有 Codex CLI 与可选 AirJelly;未发现凭据、内部链接、本地绝对路径或 raw 证据被写入仓库;对现有模块零改动,默认行为不变。函数式的验证成本值得注意:test_workspace.py 会真的调用 loopx 并 --execute 花 quota(我这次单跑约 98s),因此它更适合被当作端到端 smoke 而不是默认单测入口。
我的整体评价
可观测语义对比是明确的:改动前仓库里不存在任何 team workspace 入口(13 个文件均为新增),改动后用户可以从自然语言得到可确认的草稿与受限执行,而 Goal/Todo/quota/monitor/生命周期仍然只有一个权威(LoopX CLI)。体量与问题相称,也没有把设计可能性提前变成生产结构。所以我对架构与隔离性没有阻塞意见,但 exact head 存在两个 P1:超时后的进程组回收会让"已报超时"之后仍有迟到写入(仓库已有可复用的正确 owner),以及 PR 自报的验证命令在 head 上失败(Todo 交付契约未满足当前主干策略)。建议两项一起修好后在新 head 上复审,本轮不做合并动作。
English verdict: REQUEST_CHANGES at exact head 6426983 — the incremental packages/team-workspace/ design is sound (LoopX stays the sole Goal/Todo/quota/lifecycle authority, loopback-only server with Host/Origin checks and is_relative_to containment, no changes to existing modules), but two P1 blockers reproduce at the head: (1) model.py:69-101 sends SIGTERM to the process group and only escalates to SIGKILL when its own 5s wait times out, so a SIGTERM-ignoring descendant still wrote its marker 3.5s after ask() reported timeout (the repo's existing loopx.extensions.process_runtime.run_capped_process owner terminates the same binary's descendant group correctly), and (2) the PR's own claimed validation does not reproduce — python -B packages/team-workspace/test_workspace.py yields FAILED (errors=3) (pytest: 3 failed, 12 passed) because the Todos created in service.py do not satisfy main's quota delivery-contract guard from #3519, which already exists at the PR merge base. Repair both, rebase, and re-run the claimed commands on the new head.
按 RFC 框架重做这份 review(对齐 #4394 /
|
按你在本 PR 的 comment 建议重新对齐(本 PR = RFC step 1 最小原型,呼应 attached / managed 两种模式)上一条 RFC 对照已经按这个口径发过( 1. "直接在当前 LoopX 前端上做原型"这一点目前没有对上。 本 PR 的前端是它自带的独立页面: 2. 首屏评审门仍然适用。 新增首屏(侧栏导航 + 主 CTA)落在仓库 First-Screen Review Gate 里:合并前需要 owner 看过第一屏预览、并对齐 3. 解除阻塞的最小清单(与我上一条一致,不扩张):
边界不变: 本 PR 保持"M1 可选预览"——不注册新内建能力、不自启、不自动迁移模式、不自动恢复会话、不新增权限/凭据边界;预览准入(RFC 第 13 节事项 4)与"提升为受支持前端"(事项 5 / M4)是两次独立决策,不随本 PR 自动发生。本轮不做合并动作。 English verdict: still REQUEST_CHANGES at this head, re-anchored to your suggestion. This PR is the RFC's M1 step 1 prototype, and the parts that already hold (LoopX-owned work truth, draft-then-confirm, independent verification before journaling, opt-in incremental package) I confirmed by reading the code. Two new evidence-backed gaps: the prototype ships its own standalone loopback page instead of mounting on the current LoopX front-end and gives no migration note, and the new first screen still needs the owner preview gate. Unblock stays: reuse the bounded-process owner plus a descendant-effect test, declare and read back mode and binding identity, and make the PR's own validation green at the new head. |
huangruiteng
left a comment
There was a problem hiding this comment.
动机
这份 review 按你在本 PR 的 comment 建议重写:先找到仓库里讲 attached / managed 两种模式的那篇 doc,调研当前 LoopX,写成 RFC,然后**把本 PR 当作那份 RFC 的 step 1(最简原型)**来评。那篇 doc 已整理为 agent-session-execution-modes-v0(中文镜像,PR #4394,merge commit d15bbde25)并入 main。所以下面用 RFC 第 5 节的宿主接入契约逐条对账,并把本 PR 定位成 M1:managed_runtime 方向上的一个可选预览,而不是受支持前端。
需求侧没有争议:长周期 Goal 目前只有 CLI 与原生 Goal chat 两个入口,用户把"一条业务方向"或"某个同事"用自然语言说出来时,缺少一个可以来回确认的中间层——要么直接写进 Goal/Todo,要么判断只留在对话里。本 PR 增加一个可选本地 team workspace,把方向整理成可 review 的草稿、只追问最高价值的缺失信息、在用户单独确认后才写入 Goal/成员记录,并在同一个包里给出受限执行 + 独立验证 + 常驻调度的原型;Goal、Todo、quota、monitor、生命周期仍由 LoopX 独占。评审 head:642698356ab9ee5a977b095773ba6d9d915200b1(13 文件 +1764/-0)。
同 head 上已有一条我自己的取证式 review(07:43,CHANGES_REQUESTED)。这条是按你建议重发的正式 review 记录:口径换成 RFC M1 对账,结论不变。
改动思路
- 归属正确:这是"可独立分发、可选安装"的能力,没有核心 caller 契约,所以落在
packages/team-workspace/而不是loopx/capabilities/;没有注册 capability/extension,没有改动任何既有模块。 - LoopX 独占工作事实(RFC 第 3/7 条):
loopx_client.py是唯一通道,全部走python -m loopx.cli --format json子进程;store.py只存宿主配置、交互记录与结果日志,不构成第二状态源。 - 确认前只是提案(第 5 条):
service.py先出可编辑草稿,缺失信息只追问一处,选项不预选;test_dialogue.py断言提示词里含"不要把建议当成用户确认的事实"。 - 执行受控:
model.py是唯一会创建子进程的地方,固定codex exec --sandbox workspace-write|read-only、-c approval_policy=never、--ephemeral,输出走 schema,并清掉CODEX_THREAD_ID,避免污染宿主 lane 绑定。 - 默认关闭(第 9 条):不注册能力、不自启、不自动迁移模式、不自动恢复会话;删除这个包即回到原状。
具体改动
RFC 第 5 节逐条对账(在 exact head 上读代码 + 实测):
已经落上契约的部分
- 第 3/7 条:
loopx_client.py是唯一 LoopX 通道;store.py只持宿主本地状态;service.py::snapshot的 Goal 行直接叠加self.loop.contract(...)。 - 第 5 条:草稿 → 单独确认 → 才写 Goal/成员记录,
test_dialogue.py(2 passed)覆盖。 - 第 6 条:执行后由只读调用独立验证(
service.py:151,write=False),只有verification.sufficient的 run 才在投影里标成host_verified。 - 第 9 条:13 个文件全部新增、0 删除,未改既有模块,删包即回滚。
- 第 2 条部分成立:
loopx_client.py:53用register-agent --goal-id <goal> --agent-id team-workspace-ai --execute注册了真实 Agent。
尚未落上契约 / 尚未对上你建议的部分
- 模式与绑定身份没有声明,也无法回读(第 1、8 条,也是待决事项 4 的预览最低门槛)。 在
packages/team-workspace/全量搜索managed_runtime、attached_host、executor_endpoint_id、host_surface均无命中;RFC 要求"每个会话绑定只带一个显式、持久化、可回读的执行模式,绝不从散文或能力探测推断"。当前这层关系只存在于代码结构里,属于隐式契约。 - "直接在当前 LoopX 前端上做原型"这一点目前没对上。 本 PR 的前端是自带的独立页面
packages/team-workspace/web/{index.html,app.js,style.css}:自建侧栏 + 「新建业务目标」主 CTA + 自带样式,由server.py自己托管在 loopback 端口上。它没有挂进现有 LoopX 前端的任何承载点,也没有说明与桌面前端提案的归属关系——按 RFC 第 6 节的规范性归属地图,桌面产品流程归桌面 RFC。现在的形态是第三套入口且无迁移说明。要落实你这句话,要么把 workspace 视图做成现有前端里一个可选面,要么在 PR 描述里显式写成"独立预览面 + 迁移到桌面前端的路径"。 - 首屏评审门仍然适用。 新增首屏(侧栏导航 + 主 CTA)落在仓库 First-Screen Review Gate 内:合并前需要 owner 看过第一屏预览并对齐
docs/development/design.md,本 PR 还没有这一步的证据。 - 关键实现位置:
model.py(Codex 适配器与进程监督)、server.py(loopback + Host/Origin 校验、is_relative_to包含性校验、写入要求 JSON +X-Team-Workspace)、loopx_client.py:30,122(CLI 桥与结算)、service.py:157-165(建 Todo)。
对主干的风险
阻塞项 1(P1,超时/取消后进程组回收不完整)。 steven-kid 在原 head 上报的这条仍然成立,我在 exact head 上独立复现:model.py:69-101 的 cancel() 只对进程组发 SIGTERM,只有 proc.wait(timeout=5) 自己超时后才补 os.killpg(..., SIGKILL);只要直接子进程在宽限期内退出,忽略 SIGTERM 的后代就会存活并继续写。合成二进制实测:宿主在 2.0s 抛 RuntimeError(本轮超时),标记文件在其后 3.5s 仍被写成功(host: descendant side effect after timeout report: True);同一二进制换用仓库既有 owner loopx.extensions.process_runtime.run_capped_process 则是 failure_kind=timeout 且迟到副作用为 False。按 RFC 的会话契约,这不只是清理缺陷:ask 已报告超时、句柄已清空,而 workspace 目录还会在下一次 turn 被复用,等于"已结束的 turn 仍在产生效果"。最小修复是照既有 owner 在宽限期后无条件升级 SIGKILL(或直接复用该 owner),并补一个"超时/取消后后代不再产生副作用"的测试。
阻塞项 2(P1,PR 自报验证在 exact head 上不可复现)。 PR 描述里的 python -B packages/team-workspace/test_workspace.py(声称 9 passed)在 head 上得到 FAILED (errors=3),pytest packages/team-workspace -q 同样 3 failed, 12 passed;失败点都是 loopx_client.py:122 的 quota spend-slot ... --execute,主干守卫返回 quota spend requires an explicit Todo delivery contract(slot_accounting.py:467,来自已合并的 #3519)。该守卫已在本 PR 的 merge base 上,所以不是"分支落后主干",而是 service.py:160 建 Todo 时只写了 acceptance note,没有声明 repository/write 需求或显式非交付。修法就是在同一交付里补声明,再在新 head 重跑自报命令。
边界与其余风险。 本 PR 应保持"M1 可选预览":不注册新内建能力、不自启、不自动迁移模式、不自动恢复会话、不新增权限/凭据边界;预览准入(RFC 第 13 节事项 4)与"提升为受支持前端"(事项 5 / M4)是两次独立决策,不随本 PR 自动发生。其余核对过:服务只监听本地并做 Host/Origin 校验,静态与 artifact 路径都有包含性校验,artifact 不以可执行 HTML 形式返回;test_http.py(4 passed)与 test_dialogue.py(2 passed)在 head 上绿;未发现凭据、内部链接、本地绝对路径或 raw 证据进入仓库;对既有模块零改动,默认行为不变。另外 test_workspace.py 会真的调 loopx 并 --execute 花 quota(单跑约 98s),更适合端到端 smoke 而非默认单测入口。
我的整体评价
REQUEST_CHANGES,但方向本身按你的判断是成立的:本 PR 作为 RFC agent-session-execution-modes-v0 的 M1(step 1 最简原型)是对的选择,LoopX 独占工作事实、草稿-确认-写入、独立验证后回写、opt-in 可移除这四点已经在代码里成立,我也逐条核实过。要解除阻塞只需三件事,不扩张范围:① 进程清理复用既有 owner(loopx.extensions.process_runtime)并补后代不再产生副作用的测试;② 显式声明并回读执行模式与绑定身份(managed_runtime/attached_host、agent_id、executor_endpoint_id、host_surface),不要留成隐式契约;③ 补上 Todo 交付契约声明,让 PR 自报的验证在新 head 上转绿。另外请把"独立预览面 + 迁移到桌面前端的路径"写进 PR 描述,并安排一次首屏预览给 owner 过门。本轮不做合并动作。
English verdict: REQUEST_CHANGES at 642698356ab9ee5a977b095773ba6d9d915200b1. Re-anchored to your comment suggestion: this PR is the M1 (step-1 minimal prototype) of the attached/managed agent-session-execution-modes-v0 RFC (#4394), so I checked it clause by clause against RFC section 5. What already holds: LoopX remains the only owner of Goal/Todo/quota/monitor/lifecycle state (single CLI bridge, workspace-local store), drafts are never treated as confirmed facts until a separate confirmation, every execution is independently verified by a read-only call before it is journaled, and the package is purely additive and removable. Three things still block: the timeout path in packages/team-workspace/model.py:69-101 does not reap SIGTERM-resistant descendants (reproduced at this head; the existing owner loopx.extensions.process_runtime.run_capped_process does not leak the late write), the PR's own validation command fails at the exact head because the Todo has no delivery contract (3 errors / 3 failed, guard already present at the merge base), and the RFC's mode/binding identity is neither declared nor readable (no occurrence of managed_runtime, attached_host, executor_endpoint_id or host_surface in the package). Your "prototype on the current LoopX front-end" suggestion is also not met yet: the prototype ships its own standalone loopback page with no migration note, and the new first screen still needs the owner preview gate. No merge performed this round.
Adds an optional local team workspace for long-horizon LoopX Goals. A user can describe a broad business direction or a teammate in natural language; Codex turns the conversation into a reviewable draft, asks only for the highest-value missing detail, and offers contextual GenUI choices while preserving free-text input.
The workspace keeps LoopX as the canonical owner of Goal, Todo, quota, monitor, and lifecycle state. It adds bounded Codex execution and independent verification, human requests for supplement/execute/judge work, scoped correction memory, optional AirJelly evidence reads, resident scheduling, and links back to the native LoopX Goal chat. Suggested choices are never preselected or treated as confirmed facts, and Goal/member records are written only after a separate user confirmation.
Validation:
python -B packages/team-workspace/test_workspace.py(9 passed)python -B packages/team-workspace/test_http.py(4 passed)python -B packages/team-workspace/test_dialogue.py(2 passed)node --check packages/team-workspace/web/app.jspython -m loopx.cli canary premerge --from-git-diff(13 selected checks passed, including public-boundary validation)