Skip to content

feat: add AI-led team workspace with conversational setup - #4376

Open
KashiwaByte wants to merge 3 commits into
loopx-project:mainfrom
KashiwaByte:codex/team-continuous-goals
Open

KashiwaByte wants to merge 3 commits into
loopx-project:mainfrom
KashiwaByte:codex/team-continuous-goals

Conversation

@KashiwaByte

Copy link
Copy Markdown

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.js
  • python -m loopx.cli canary premerge --from-git-diff (13 selected checks passed, including public-boundary validation)
  • Live local Codex dialogue produced contextual retention/activity/key-work suggestions and preserved unknown metric baselines
  • Browser-verified Goal and member conversational entry points

Signed-off-by: KashiwaByte <471314513@qq.com>
Signed-off-by: KashiwaByte <471314513@qq.com>
Signed-off-by: KashiwaByte <471314513@qq.com>
@KashiwaByte
KashiwaByte force-pushed the codex/team-continuous-goals branch from 4aac19c to 6426983 Compare September 14, 2026 10:59

@steven-kid steven-kid 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: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 修复。

具体改动

  1. server.py 的本地 HTTP host 与路由、web/app.js 的 dialogue/commit/render,以及 index/style:覆盖概览、Goal、成员、请求、反馈、文件、监测和设置。写路由要求本地 Host、Origin 校验及特定 JSON 请求头;草稿选项只是建议,提交仍需明确动作。静态和 artifact 路径有 containment 检查。页面是中文独立产品面,未加入自动安装的核心入口。
  2. service.py::Workspace.tick/executeloopx_client.py::LoopX:常驻轮询 canonical guard/todos,经 claim 后执行及验收,再通过 CLI 写回;ready journal 在模型调用前回放。人类 supplement/execute/judge 分开,观察证据不能直接成为 judge 决策,纠正按范围记录。监测无变化不调用模型,有变化才分析;快照把真实 todos 与 host 验收标记组合呈现。
  3. service.py::Workspace.askmodel.py::Codex.ask/cancel:本地每日调用计数和 enabled/revision 门禁、模型锁、read-only/workspace-write sandbox、严格结果 schema。问题位于 timeout 的进程组清理分支,详见下一节。
  4. store.py::Store 与 service 的 apply/replay:SQLite WAL、FULL 同步和记录锁保存 host 配置、成员、请求、runs、observations、lessons 等。新增多个可影响调度的 phase/boolean/revision;本轮未完整证明它们在外部 canonical 更新、所有 crash 点及恢复时的派生/冲突语义,不能将“不是 Todo 数据库”直接当作没有重复 authority 的证据。
  5. 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.

@steven-kid

Copy link
Copy Markdown
Collaborator

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.

@huangruiteng

Copy link
Copy Markdown
Collaborator

我感觉这个思路是ok的,loopx仓库里有篇doc,讲了attached和managed两种模式,我感觉可以呼应上那个doc,直接在当前loopx的前端上做一个原型

@huangruiteng

Copy link
Copy Markdown
Collaborator

建议让agent找到那篇doc,调研一下当前的loopx,写篇rfc,然后这个pr作为那个rfc的step 1,最简的原型

@huangruiteng

Copy link
Copy Markdown
Collaborator

按上面的建议,那篇 doc(attached / managed 两种模式)已经整理成 RFC 并合入 main 了:

它把原来只在桌面执行前端提案里的 Mode A / Mode B,抽成一份与前端无关的契约:每个
Agent 会话绑定只带一个显式、持久化、可回读的执行模式(managed_runtime /
attached_host),模式绝不从散文、提示词、能力探测或传输推断,也绝不隐式变化;两种
模式下 LoopX 都独占工作事实;一个绑定至多一个活跃执行器;未声明的投递能力失败关闭。
第 6 节是规范性归属地图:桌面产品流程归桌面 RFC,服务生命周期归单属主守护进程 RFC,
延续归管家 RFC,Turn 事务归 loopx_turn_v0,宿主模式选择归 host_mode_plan_v0

这个 PR 与 RFC 的对应关系:本 PR 就是 RFC 交付计划里的 M1(Step 1 最小原型)。

对照 RFC 第 5 节的宿主接入契约,这个原型已经天然落在契约上:

  • 模式:它属于 managed_runtime——执行、验证由宿主自己启动并监督,LoopX 不是运行时;
  • 工作事实:Todo 生命周期、quota、monitor 观察仍交给已交付的 LoopX CLI,SQLite 只放
    宿主配置、交互记录与可重放的结果日志,不是竞争状态源(接入契约第 7 条);
  • 对话式建档:Goal / 成员先出可编辑草稿,缺失信息只问最关键的一处,选择项不预选、
    不当成已确认事实,需要用户单独确认才写 Goal / 成员记录(不变量 6 与接入契约第 5 条);
  • 独立验证:任务结果由只读 Codex 调用单独验证,只有验证过的结果才 journal 后回写
    (接入契约第 6 条);
  • 可卸载的 opt-in 预览:不注册新的内建能力,不引入常驻服务(接入契约第 9 条)。

合并前需要处理的既有发现(也是 RFC M1 的进入门槛之一)

steven-kidpackages/team-workspace/model.py:91–98 报的 P1 仍然有效:超时清理只在
父进程仍存活时发 SIGKILL,一个抗 SIGTERM 的子进程可能在 ask 已报告超时、句柄已清空
之后继续写入。这在会话契约里不只是清理缺陷,而是"第二执行器"风险——用户会认为这次
执行已经结束。RFC 的 M1 进入门槛直接写了这一点:复用 loopx.extensions.process_runtime
里既有的 run_capped_process 有界进程语义,并补一个超时/取消后"后代进程确实不再产生
效果"的测试。

几点按 RFC 需要明确的

  • 本 PR 保持"M1 可选预览"边界即可:RFC 第 13 节把"预览宿主是否可以在满足全部接入行
    之前绑定执行会话"列为待决事项 4,建议是至少先具备模式回读、单执行器、对话不是回执
    三项;提升为受支持的前端面是待决事项 5(M4),不随本 PR 自动发生。
  • 请在原型里显式声明并回读它的模式与绑定身份(agent_id / executor_endpoint_id /
    host_surface),这样它才不是一份隐式契约。
  • RFC 没有批准自动模式迁移、自动恢复会话、常驻服务或任何新的权限/凭据边界;这些仍
    需要单独决策。

English. The attached/managed mode doc is now a merged RFC:
agent-session-execution-modes-v0
(中文镜像,
PR #4394). It defines one explicit,
persisted, read-back execution mode per Agent session binding (managed_runtime or
attached_host), LoopX-owned work truth in both modes, one active executor per
binding, and fail-closed capability gating, plus a normative ownership map against
the Desktop, daemon, manager, continuation, Turn, and host-mode-plan documents.

This PR is the M1 Step 1 minimal prototype in that RFC's delivery plan. Its
shape already matches the admission contract: managed-mode execution, LoopX-owned
Todo/quota/monitor state with replays-only local journals, drafts plus explicit
confirmation instead of conversation-as-receipt, independent verification before
journaling and writeback, and an opt-in preview that registers no new built-in
capability. The RFC's M1 entry gate names the open review finding on
packages/team-workspace/model.py:91–98 (SIGKILL only while the parent is alive) as
the item to resolve by reusing the existing bounded-process runtime helper, since a
surviving child writing after a reported timeout is a second-executor risk rather
than only a cleanup defect.

Please keep the preview boundary: declare and read back the prototype's mode and
binding identity instead of leaving it implicit, and treat preview admission
(Section 13 item 4) and promotion to a supported frontend (item 5 / M4) as separate
decisions. The RFC does not approve automatic mode migration, automatic session
resume, resident services, or any new permission or credential boundary.

@huangruiteng huangruiteng 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.

如上

@huangruiteng huangruiteng 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.

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.

  1. No declared execution mode. The package never declares or persists
    managed_runtime / attached_host, while
    packages/team-workspace/loopx_client.py:53 registers an Agent and server.py
    runs a resident scheduler thread whose Workspace.tick actually 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.
  2. Executor is undeclared and unreadback. Registration passes only --agent-id,
    and the executor is a hard-coded local codex (server.py --codex-bin defaults to
    codex; model.py::Codex.ask spawns codex 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.
  3. Single executor relies on the claim lease only. Reusing canonical todo claim
    (loopx_client.py:88-90) together with the host's fcntl single-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.
  4. 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/verified is a second state machine that LoopX readback
    does not declare.
  5. Readback boundary. /api/state returns 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.js monitorCardm.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.
  6. First-screen review gate. The PR adds a new product first screen
    (web/index.html sidebar and the "新建业务目标" primary CTA). Per the repository
    first-screen gate, the owner previews that screen and confirms it against
    docs/development/design.md before 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 项处理后再复审合并。

  1. 未声明执行模式。 全 PR 没有 managed_runtime / attached_host 的声明与持久化读回,
    packages/team-workspace/loopx_client.py:53 注册 Agent、server.py 以常驻调度线程 +
    Workspace.tick 实际执行。按契约,未声明的宿主可以读 LoopX 状态,但不能占用执行中的会话
    绑定。bootstrap 时传 --codex-app-heartbeat noloopx_client.py:51-53)能避免竞争,
    但不等于模式声明;声明必须显式并可从读回验证。
  2. 执行器未声明、未读回。 注册只传 --agent-id;执行器是硬编码的本地 codex
    server.py --codex-bin 默认 codexmodel.py::Codex.askcodex exec)。因此无法区分
    operator 凭据执行器与个人 CLI 登录执行器,也没有复用已发布的 managed executor 读回
    (executor kind / available / 类型化 reason / fail-closed)。
  3. 单执行器只靠 claim 兜底。 走 canonical todo claimloopx_client.py:88-90)加宿主
    fcntl 单实例锁方向正确;但若同一 Agent 之后被 goal heartbeat 或 loopx turn 绑定,就只剩
    claim 租约仲裁,缺少契约要求的绑定声明与类型化失败。
  4. 宿主私有 ingress。 草稿→显式确认→CLI 写入是对的;但人类请求、反馈与模型解释出的后续
    任务存在宿主 SQLite(store.py/api/goal/feedback)并驱动执行,
    awaiting/submitted/gap/verified 是第二套状态机,LoopX 读回没有声明这条 ingress。
  5. 读回边界。 /api/state 整体返回宿主机行(settingsjobsartifacts
    native_dashboard_url),错误只有自由文本 {'error': ...},没有稳定错误码;监测卡片渲染
    本机文件路径(web/app.jsmonitorCardm.path)。本地回环页面可以这样展示,但该
    快照不是 public-safe 投影,不能直接镜像进 LoopX / Lark 读回。
  6. 前端首屏评审门。 本 PR 新增产品首屏(web/index.html 侧栏与「新建业务目标」主 CTA);
    按仓库首屏门禁,合并前需 owner 预览首屏并对照 docs/development/design.md 确认。

保留:canonical CLI 复用、确认后才创建、人类判断与观察分离、可选 package、loopback-only 边界。

@huangruiteng huangruiteng 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.

动机

长周期 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 jsonloopx,把返回投影成 RuntimeError),没有复制 quota/Todo/Goal 的状态规则,也没有第二份持久化权威:store.py 只保存 workspace 自己的草稿与会话。Codex 执行被限制在 model.py 的一个适配器里,固定 --sandbox workspace-write|read-onlyapproval_policy="never"--ephemeral,并主动清掉 CODEX_THREAD_ID,避免污染宿主的 lane 绑定。由于是纯增量、无 feature gate,关闭方式就是使用与否,隔离性是默认成立的。

具体改动

关键代码讲解

  1. model.py:77-108ask()codex exec 跑一轮受限执行,schema 约束输出,超时后调 cancel();这是本 PR 唯一会创建子进程的地方,也是唯一的阻塞项所在。
  2. server.py:本地 HTTP 服务。写入要求 JSON content-type,Host/Origin 做 loopback 校验(server.py:77 静态文件、server.py:145 artifact 路径都用 is_relative_to 做包含性校验),没有把 workspace 目录暴露成任意路径读取器。
  3. loopx_client.py:30,122):统一 CLI 桥。所有 LoopX 操作走真实 CLI,失败转成带原文的类型化 RuntimeError;quota 花费带上 goal/agent/turn-instance/todo 与 --scan-path,即交付结算仍由 LoopX 记账。
  4. service.py:对话 → 草稿 → 确认 → 写入的编排层;task():157)建 Todo 时只带标题与 acceptance 说明,这一步与主干当前的 quota 交付契约要求不一致(见下)。
  5. 测试与前端:test_workspace.py(真实 CLI 端到端)、test_http.pytest_dialogue.pyweb/ 三件套;node --check 覆盖前端语法。

对主干的风险

两个 exact head 阻塞项,都是可复现的,不是文字判断:

其一(P1,超时后进程组回收不完整):model.py:69-101cancel() 只对进程组发 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:122quota spend-slot ... --execute,主干守卫返回 quota spend requires an explicit Todo delivery contract; declare repository/write requirements or mark the Todo as explicit non-deliveryslot_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.

@huangruiteng

Copy link
Copy Markdown
Collaborator

按 RFC 框架重做这份 review(对齐 #4394 / agent-session-execution-modes-v0

我先承认:上一条 review 是我自己那套取证套路,没有按你在本 PR 的建议来写。你的建议是把本 PR 当成 agent-session-execution-modes-v0中文镜像,PR #4394,merge commit d15bbde25)交付计划里的 M1(Step 1 最小原型)来评,并且必须:① 用 RFC 第 5 节宿主接入契约逐条对账;② 把 M1 进入门槛(model.py 进程清理 P1)当作硬性前置;③ 让原型显式声明并回读模式与绑定身份,不要留成隐式契约;④ 守住"M1 预览"边界,预览准入(第 13 节待决事项 4)与提升为受支持前端(事项 5 / M4)分开决策。下面按这四条给出核对结果(全部在 exact head 642698356ab9ee5a977b095773ba6d9d915200b1 上实测)。

一、第 5 节接入契约逐条对账

已成立的部分(有代码位置为证):

  • 第 3/7 条(LoopX 独占工作事实、宿主本地存储非权威):loopx_client.py 是唯一的 LoopX 通道(python -m loopx.cli --format json 子进程),store.py 只存宿主配置、交互与结果日志,service.py::snapshot 的 Goal 行直接叠加 self.loop.contract(...)
  • 第 5 条(对话在用户确认前只是提案):test_dialogue.py 断言提示词含"不要把建议当成用户确认的事实",Goal/成员记录必须单独确认后才写。
  • 第 6 条(结果先独立验证再 journal):执行后由只读调用(service.py:151 write=False)独立验证,只有 verification.sufficient 的 run 才在投影里标成 host_verified
  • 第 9 条(opt-in、可移除、无 schema 迁移):13 个文件全部新增、0 删除,未注册能力、未改任何既有模块,删掉包即回到原状。
  • 第 2 条部分成立:loopx_client.py:53 通过 register-agent --goal-id <goal> --agent-id team-workspace-ai --execute 注册了真实 Agent。

尚未成立、需要在本 PR 内补上的部分:

  1. 模式声明与回读缺失(第 1、8 条,也是待决事项 4 的预览最低门槛)。packages/team-workspace/ 全量搜索 managed_runtime|attached_host|execution_mode|host_surface|executor_endpoint零命中/healthz 只回 applicationscheduler_alive/api/state 也不含模式或绑定身份,错误文案是中文散文(如 '仅允许本机访问')而不是稳定错误码。这正是你要求"显式声明并回读模式与绑定身份(agent_id / executor_endpoint_id / host_surface)"的那一项:按当前形态,它的模式仍是隐式契约。
  2. executor_endpoint_id 缺失(第 2 条)。 只注册了 Agent 身份 team-workspace-ai,没有与 Agent 身份相区分的执行器端点标识,因此"哪台/哪个执行器绑定了这个 Agent"无法回读。
  3. "每个绑定至多一个活跃执行器"目前只是单进程约束(第 3 条)。 现有保障是 service.pymodel_lock 加上 server.py:221-227 的数据目录 flock(注释写的是"prevents two schedulers from executing the same Goal")——这防的是同一数据目录同机双开,不是"同一 Agent 绑定"。换一个 --data 目录或换端口启动第二个预览进程,仍可对同一个已注册 Agent 起第二个执行器;RFC 第 3 条明确包含"重启、崩溃、手动重启之后"也不行。
  4. 常驻调度与 M1 的"不得新增第二个 scheduler"(第 12 节)需要显式表态。 server.py 会启动常驻线程 team-goal-scheduler(daemon)并在 /healthz 暴露存活状态;M1 的行文是"must not add uncalled schema builders, a second scheduler, or a new runtime authority",你在评论里也把"常驻服务"列为 RFC 未批准项。请明确它是"仅存在于操作员手动启动的预览进程内、不随 LoopX 自启、不构成 LoopX 级调度权威",或把常驻调度挪出本 PR。

二、M1 进入门槛(进程清理 P1)我独立复现了,支持作为合并前置

steven-kidmodel.py:91–98 的发现一致:cancel() 只对进程组发 SIGTERM(model.py:69-75),只有当 proc.wait(timeout=5) 自身超时才补 killpg(SIGKILL)model.py:96-100)。合成二进制实测(exact head):

host: raise after 2.0s -> RuntimeError: 本轮 Codex 超时;保留任务,下轮先检查已有结果
host: descendant side effect after timeout report: True      # 标记文件在报超时 3.5s 后仍被写入
reuse: timed_out = timeout
reuse: descendant side effect after timeout: False           # 既有 owner 无迟到副作用
P1 reproduced (host leaks, reused owner does not): True

同一个二进制换用仓库既有 owner loopx.extensions.process_runtime.run_capped_process 就没有迟到写入,其内部通过 _terminate_posix_process_group 在宽限期后无条件升级 SIGKILL。所以 M1 门槛的最小修法就是复用该 owner(或照它升级),并补一个"超时/取消后后代不再产生效果"的测试——这正是 RFC M1 行里写的进入条件。

三、另外两项与本 PR 可合并性直接相关的实测结论

  • 自报验证在 exact head 上不成立。 PR 描述里的 python -B packages/team-workspace/test_workspace.py(9 passed)实测为 Ran 9 tests ... FAILED (errors=3)pytest packages/team-workspace3 failed, 12 passed,失败点都在 loopx_client.py:122quota spend-slot ... --execute:守卫返回 quota spend requires an explicit Todo delivery contract; declare repository/write requirements or mark the Todo as explicit non-delivery。该守卫来自已合并的 fix(quota): make unknown workspace causality repairable #3519,且在本 PR 的 merge base 上就已存在,属新代码未满足当前主干策略(service.py:157 建 Todo 时只写了 acceptance note),需要声明 repository/write 需求或显式标记非交付。
  • 边界与隔离面是好的。 server.py 仅回环(Host 白名单 + Origin 校验),写操作要求 JSON 与 X-Team-Workspace 头,静态与 artifact 路径用 is_relative_to 限制,artifact 一律以 octet-stream 下载并带 CSP/nosniff;test_http.py(4 passed)与 test_dialogue.py(2 passed)在 head 上绿。对既有模块零改动,所以"feature-off parity"这一类 M1 退出证据在当前形态下其实是构造性成立的。

四、请守住的边界

本 PR 保持 M1 可选预览即可:不注册新内建能力、不自启、不做模式自动迁移、不做会话自动恢复、不新增权限/凭据边界;预览准入(第 13 节事项 4)与"提升为受支持前端"(事项 5 / M4)是两次独立决策,不随本 PR 自动发生。

综合:M1 的两项硬前置(进程清理复用既有 owner、模式与绑定身份显式声明并回读)加上自报验证转绿,是我这里解除阻塞的全部条件;这三项修好后在新 head 上复审。本轮不做合并动作。


English. Re-reviewing this PR as RFC M1 (Step 1 minimal prototype) per your guidance on #4394. Against Section 5's admission contract: LoopX-owned work truth, host-local storage as a journal only, conversation-as-proposal, independent verification before journaling, and opt-in/removable parity all hold with code evidence; an Agent is registered (loopx_client.py:53), but no mode or binding identity is declared or read back anywhere (zero hits for managed_runtime|attached_host|execution_mode|host_surface|executor_endpoint under packages/team-workspace/, and /healthz / /api/state expose neither mode nor stable error codes), and there is no executor_endpoint_id distinct from the Agent identity. "One active executor per binding" is currently only a per-process lock plus a data-directory flock (server.py:221-227), so a second preview process with a different --data can still start a second executor for the same registered Agent. The resident team-goal-scheduler thread also needs an explicit statement against M1's "must not add … a second scheduler" and your not-approved resident-service note. The M1 entry gate is confirmed: I independently reproduced the process-cleanup P1 at this exact head (a SIGTERM-ignoring descendant wrote its marker 3.5s after the timeout report, while the existing run_capped_process owner left no late write), so the fix is to reuse that owner plus a descendant-effect test. Separately, the PR's own claimed validation is red at this head (FAILED (errors=3)) because the Todos created in service.py:157 do not satisfy main's delivery-contract guard. Clear those three items and re-run on a new head; no merge this round.

@huangruiteng

Copy link
Copy Markdown
Collaborator

按你在本 PR 的 comment 建议重新对齐(本 PR = RFC step 1 最小原型,呼应 attached / managed 两种模式)

上一条 RFC 对照已经按这个口径发过(agent-session-execution-modes-v0,PR #4394 的 M1)。这里只补"你的建议里我还没回应的一半"和最小修法,不重复已经发过的内容。

1. "直接在当前 LoopX 前端上做原型"这一点目前没有对上。 本 PR 的前端是它自带的独立页面:packages/team-workspace/web/{index.html,app.js,style.css},自建侧栏 + 「新建业务目标」主 CTA + 自带样式,由 server.py 自己托管在 loopback 端口上。它没有挂进现有 LoopX 前端的任何承载点,也没有说明与桌面前端提案的归属关系——按 RFC 第 6 节的规范性归属地图,桌面产品流程归桌面 RFC。所以要落实你那句建议,要么把 workspace 视图做成现有前端里一个可选面,要么在 PR 描述里显式写成"独立预览面 + 迁移到桌面前端的路径"。现在的形态是第三套入口,且没有迁移说明。

2. 首屏评审门仍然适用。 新增首屏(侧栏导航 + 主 CTA)落在仓库 First-Screen Review Gate 里:合并前需要 owner 看过第一屏预览、并对齐 docs/development/design.md,这一步在 PR 里还没有证据。

3. 解除阻塞的最小清单(与我上一条一致,不扩张):

  • 进程清理复用既有 owner:loopx.extensions.process_runtime.run_capped_process(或照它无条件升级 SIGKILL),并补一个"超时/取消后后代不再产生效果"的测试;
  • 显式声明并回读执行模式与绑定身份(managed_runtime / attached_hostagent_idexecutor_endpoint_idhost_surface),不要留成隐式契约;
  • 自报验证在新 head 上转绿(service.py 建 Todo 时未满足主干交付契约)。

边界不变: 本 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 huangruiteng 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.

动机

这份 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 定位成 M1managed_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 上读代码 + 实测):

已经落上契约的部分

  1. 第 3/7 条:loopx_client.py 是唯一 LoopX 通道;store.py 只持宿主本地状态;service.py::snapshot 的 Goal 行直接叠加 self.loop.contract(...)
  2. 第 5 条:草稿 → 单独确认 → 才写 Goal/成员记录,test_dialogue.py(2 passed)覆盖。
  3. 第 6 条:执行后由只读调用独立验证(service.py:151write=False),只有 verification.sufficient 的 run 才在投影里标成 host_verified
  4. 第 9 条:13 个文件全部新增、0 删除,未改既有模块,删包即回滚。
  5. 第 2 条部分成立:loopx_client.py:53register-agent --goal-id <goal> --agent-id team-workspace-ai --execute 注册了真实 Agent。

尚未落上契约 / 尚未对上你建议的部分

  1. 模式与绑定身份没有声明,也无法回读(第 1、8 条,也是待决事项 4 的预览最低门槛)。packages/team-workspace/ 全量搜索 managed_runtimeattached_hostexecutor_endpoint_idhost_surface 均无命中;RFC 要求"每个会话绑定只带一个显式、持久化、可回读的执行模式,绝不从散文或能力探测推断"。当前这层关系只存在于代码结构里,属于隐式契约。
  2. "直接在当前 LoopX 前端上做原型"这一点目前没对上。 本 PR 的前端是自带的独立页面 packages/team-workspace/web/{index.html,app.js,style.css}:自建侧栏 + 「新建业务目标」主 CTA + 自带样式,由 server.py 自己托管在 loopback 端口上。它没有挂进现有 LoopX 前端的任何承载点,也没有说明与桌面前端提案的归属关系——按 RFC 第 6 节的规范性归属地图,桌面产品流程归桌面 RFC。现在的形态是第三套入口且无迁移说明。要落实你这句话,要么把 workspace 视图做成现有前端里一个可选面,要么在 PR 描述里显式写成"独立预览面 + 迁移到桌面前端的路径"。
  3. 首屏评审门仍然适用。 新增首屏(侧栏导航 + 主 CTA)落在仓库 First-Screen Review Gate 内:合并前需要 owner 看过第一屏预览并对齐 docs/development/design.md,本 PR 还没有这一步的证据。
  4. 关键实现位置: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-101cancel() 只对进程组发 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:122quota spend-slot ... --execute,主干守卫返回 quota spend requires an explicit Todo delivery contractslot_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_hostagent_idexecutor_endpoint_idhost_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.

This branch has not been deployed

No deployments
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.

3 participants