Repository navigation
feat(core): bind WorkflowRun identity and workspace lifecycle to the control plane - #68
Conversation
Zpc001
left a comment
There was a problem hiding this comment.
Request changes
改动我逐条核过,方向对、覆盖面也完整,只有一条要改(CI 挂在你自己这份文件上),外加一个 base 的问题。
1. check 挂在这份 head 上
用仓库自己的门禁在本地复现(go1.27.1,main 本身干净):
$ go run ./cmd/checkformat
internal/core/workflow_run_identity_db_test.go ← 本 PR 自己的文件
internal/core/workflow_scheduler.go ← 来自上一个 PR
internal/core/workflow_scheduler_db_test.go
internal/core/workflow_scheduler_test.go
exit status 1
internal/core/workflow_run_identity_db_test.go:154 那行长 INSERT(store.Pool.Exec(\INSERT ...`)拼doc)gofumpt 也不接受。前三个文件是栈里上一个 PR 的,会在它自己那边一起改,这里至少把你自己那份跑一遍 go tool gofumpt -l -extra .`。
2. base 是 main,但分支是叠在上一个 PR 上的
gh pr view 68 显示 base: main,而分支里带着上一个 PR 的提交(workflow_scheduler.go、workflow_condition.go 这些文件的改动都出现在本 PR 的 diff 里)。这样审的时候看不到本 PR 自己的净改动,直接合并本 PR 还会把上一个 PR 的提交一起带进来。建议 base 指向上一个 PR 的分支、按顺序合;如果是有意保持 base=main,那么合并必须严格按顺序,且审的时候请让我知道该看哪几个文件。
核过之后确认没问题的部分
- 六个
issue_run_id IS NULL过滤点全部跟上了。main 上只有六处用"没有 issue_run_id"表示公开 Workspace:internal/core/plugins.go:144、:269、public.go:476、runtime_control.go:129、runtime_permissions.go:23、store.go:597,本 PR 六处都加了workflow_run_id IS NULL(runtime_control.go:129那处是改成OR)。我按这个模式又搜了一遍internal/core,没有漏的。 store.go的list里把 nil 的workflowRunId一起删掉,和既有的issueRunId处理一致,投影不会多出一个 null 字段。createWorkflowRunWorkspace的幂等分支和createRunWorkspace逐字对齐(同样不过滤deleted_at,同样取第一条create_workspace),不是漏写。dispatchRunExecution的(w.S("issueRunId") == runID || w.S("workflowRunId") == runID)、runtimeControlCommand里 IssueRun join 失败后回落到workflow_runs、finishRuntimeMaintenance取 owner 时先 issue 后 workflow——三处都和老路径同构。- 删掉
runtime_controls_maintenance_run_id_fkey的那个理由在代码里有对应:runtime_control_delivery.go现在要求 maintenance 持有者是活的 run(两种都认),这正是多态引用不能做外键之后必须补上的活性检查。多态列上没有 FK、由代码复读,这一点在迁移注释里写清楚了,我认可。 settleRunWorkspace/settleRunWorkspaceDeleted/settleThreadEvents里对着 WorkflowRun 直接return nil:我核过workspaces.observed_state='ready',admission_open=true是由internal/core/control.go:450在操作终态时无条件写的,与 run 种类无关,所以这里只跳过issue_runs自己的簿记,WorkflowRun 的 Workspace 仍会到 ready、能过dispatchRunExecution的检查。不需要改。integration/revision_upgrade_test.go把created_by/project_id从比较文档里减掉,并写明是"形状变化不是数据变化",这是这份迁移该有的处理。
Refs ora-space/circus#141
🤖 Generated with Claude Code
68894e1 to
70d1951
Compare
42e3768 to
43f5505
Compare
Zpc001
left a comment
There was a problem hiding this comment.
复审结论:Request changes
复审对象:head 43f5505(git diff 0d9363d..43f5505)。已在真实 PostgreSQL(127.0.0.1:55432,REQUIRE_POSTGRES=1)下执行 go run ./cmd/checkformat、go tool golangci-lint run、核心 DB 用例(TestWorkflowRunWorkspaceLifecycle|...RequiresProject|TestExecutionWorkOwnerExclusivity|TestWorkflowExecutionWorkDispatchSeam)、集成用例(TestWorkflowRunFreezesCallerProject|...RefusesUnreachableProject|TestRevisionUpgradePreservesPublishedWorkflowSchema)以及 ./internal/contract ./internal/api/...,全部通过。
上一轮的 2 条已确认修复:
阻塞项
1. ClaimWork 对 workflow 工作项返回空 OperationId,整条会话派发链不可用 — internal/controlgrpc/executions.go:31
ClaimWork 仍以 work.S("runId") 作为 OperationId。workflow 的 execution_work 行 run_id 为 NULL(0035 的互斥 CHECK),Object.S 对 nil 返回空串,于是 Controller 拿到 OperationId="";随后 RecordDispatch → clone_dispatch → nodeDispatch → dispatchRunExecution 要求 (run_id=$1 OR workflow_run_id=$1),空串匹配不到任何行,永久 409 dispatch_conflict,工作项无法被领取。
本 PR 的 seam 用例直接调用 nodeDispatch(workflow_run_identity_db_test.go:212),绕过了 ClaimWork,所以没有暴露。这个问题在 #72 把 enqueueExecutionWork 接到真实运行后就会变成 P1;请在 #72(或本 PR)改成 coalesce(run_id, workflow_run_id) 投影,并补一条从 ClaimWork 经 RecordDispatch 到 node_executions 的集成用例。
2. 迁移 0035 缺少仓库要求的文档与专用验证
internal/core/migrations/README.md/README.en.md逐条登记了所有迁移(包括两个 0033 和0035_model_attempt_statistics.sql),但没有0035_workflow_run_identity.sql。仓库已有 0033 双文件编号的先例,应把重复编号、纯前向/可重复、约束形状写清楚。- 没有
TestMigration0035WorkflowRunIdentityFreshAndUpgrade一类用例;目前唯一覆盖升级的是顺带的TestRevisionUpgradePreservesPublishedWorkflowSchema,它只证明迁移能跑完,不证明workspaces/execution_work的 exactly-one CHECK、两个部分唯一索引和 FK 删除在真实数据上是预期的。cloud/AGENTS.md 要求 schema 变更同时提供空库与升级证据,并与文档同一次提交。
3. 描述与证据未收口
- 文件清单过期:描述写
internal/core/workflow_run_workspace.go(实际是run_workspace.go)、internal/core/README*.md(diff 中没有),漏列新增的integration/workflow_run_project_test.go。 - 没有写实现的 ADR 状态(保留
approved还是转implemented),也没有对应的 specs PR;test-cases/cloud/workflow/execution-identity-and-invocation.md仍有 9 处Missing。按 circus 的合并规则,这是合并前必须补的。
非阻塞说明
- 结算钩子的 workflow 分支目前生产不可达:
agent_run_settle.go:38-44、agent_run_release.go:287-293、agent_run_thread.go:96-102需要 workflow run id 才会进入,而control.go:422只读issue_run_id。此外workflow_run_identity_db_test.go的 lifecycle 用例通过测试辅助settleCreateWorkspaceOperation直接写终态,没有走真实的operationStep提交路径,因此“control plane itself drives”的表述偏强。建议要么在 #72 把控制面接上,要么在注释/描述里注明当前只覆盖数据层。 - 测试缺口:
created_by冻结认证身份没有 API 级断言(集成用例只断言了project_id);workspace_run_binding_exclusiveCHECK 与 workflow 绑定的五处可见性过滤没有测试(目前只测了execution_work的互斥 CHECK)。 - “直接运行必须提供 Project”的偏差已在 #141 升级给 owner,代码与 OpenAPI 的说明一致,不需要在本 PR 内定夺。
已核实无问题
迁移纯前向且可重复执行(按完整文件名+校验和记账),exactly-one CHECK 正确,重建的部分索引对 IssueRun 行语义等价;FK 删除可接受,因为 runtime_permit 会重读任一类型的活运行并继续校验 runtimeUsable 与仓库权限;workspace 创建幂等、六处 workflow_run_id IS NULL 过滤、nil projection 移除都没有发现租户/owner 越权。
…rerun Review round on ora-space#68. The migration shipped without the two things every other migration in this directory has: an entry in the READMEs and a fresh/upgrade/ rerun test. `TestMigration0035WorkflowRunIdentityAppliesFreshAndUpgrades` asserts the four columns, both exactly-one-owner CHECKs, both partial unique indexes, and — the one this migration does by deletion rather than addition — that runtime_controls' IssueRun foreign key is gone while the column it constrained stays. A polymorphic run reference cannot be a foreign key, so the liveness check moved into the code that grants on it, and that is worth a test rather than a comment. The migration is pinned to LF at the same time. It is introduced by this PR and no released database has applied it, so this is the last moment the pin is free: after merge the bytes are in a ledger this change could not reach. 0034, which is on main, keeps its platform-dependent newline for exactly that reason, and 0035_model_attempt_statistics.sql is left to the PR that owns it. Co-Authored-By: Claude Code <noreply@anthropic.com>
002d6a5 to
aa1462b
Compare
Rebasing onto the new ora-space#68 head made `resolveWorkflowRunPluginVersion`'s empty -Project branch unreachable: creation now refuses a run that names no Project, so the Project this function reads the space from is always present. The branch and the comment describing it ("a run that named none has no space to read from and freezes nothing") are removed rather than left as a guard for a case the creation gate no longer admits; the value is passed straight to the query. The rest of the function is unchanged: a missing plugin still freezes nothing and is still reported later as `agent_plugin_version_unfrozen`, which remains an owner decision rather than something to change here. Co-Authored-By: Claude Code <noreply@anthropic.com>
…rerun Review round on ora-space#68. The migration shipped without the two things every other migration in this directory has: an entry in the READMEs and a fresh/upgrade/ rerun test. `TestMigration0035WorkflowRunIdentityAppliesFreshAndUpgrades` asserts the four columns, both exactly-one-owner CHECKs, both partial unique indexes, and — the one this migration does by deletion rather than addition — that runtime_controls' IssueRun foreign key is gone while the column it constrained stays. A polymorphic run reference cannot be a foreign key, so the liveness check moved into the code that grants on it, and that is worth a test rather than a comment. The migration is pinned to LF at the same time. It is introduced by this PR and no released database has applied it, so this is the last moment the pin is free: after merge the bytes are in a ledger this change could not reach. 0034, which is on main, keeps its platform-dependent newline for exactly that reason, and 0035_model_attempt_statistics.sql is left to the PR that owns it. Co-Authored-By: Claude Code <noreply@anthropic.com>
aa1462b to
51d31d4
Compare
Rebasing onto the new ora-space#68 head made `resolveWorkflowRunPluginVersion`'s empty -Project branch unreachable: creation now refuses a run that names no Project, so the Project this function reads the space from is always present. The branch and the comment describing it ("a run that named none has no space to read from and freezes nothing") are removed rather than left as a guard for a case the creation gate no longer admits; the value is passed straight to the query. The rest of the function is unchanged: a missing plugin still freezes nothing and is still reported later as `agent_plugin_version_unfrozen`, which remains an owner decision rather than something to change here. Co-Authored-By: Claude Code <noreply@anthropic.com>
Rebase 完成、CI 已绿 — 请复审
冲突与解决、迁移编号的处理、与未改动基线逐条对照的测试证据,都在描述开头的「Rebase 到 有一处值得单独看: 请基于新 head 复审。 |
Zpc001
left a comment
There was a problem hiding this comment.
复审结论:Request changes
复审对象:head 51d31d4。本 PR 自身净改动按 git diff 97d55a3..51d31d4 复核(97d55a3 是把 #67 重放到 main@10fd6dc 后的 tip),栈收敛性也验证过:97d55a3^{tree} 与 git merge-tree --write-tree 10fd6dc 241b294 的树相同。
上一轮的阻塞项都已核对修复:
- ClaimWork 的 run 投影改为
claimedRunId(internal/controlgrpc/executions.go),workflow 工作项不再带出空 runId; - 0035 已补进 migrations README,fresh / upgrade / rerun 均有覆盖(
TestMigration0035WorkflowRunIdentityAppliesFreshAndUpgrades); enqueueWorkflowExecutionWork带workflow_node_id,XOR CHECK 与两个 partial unique 索引齐备;- PR 描述的文件列表与 ADR 状态已更新。
仍有一条要改:projectId 的运行时门禁与 OpenAPI 契约不一致。
1. 契约说 projectId 可省略,实现却无条件 409
createWorkflowRun 通过 resolveWorkflowRunProject 强制要求 projectId,缺失即 409 workflow_run_project_required(internal/core/workflow_run.go:146)。这本身是 D3 的结论,没问题;问题是契约没跟上:
internal/contract/openapi.go:908-911的optionalField仍把projectId列为可省略,api/openapi.json里创建 run 的 schema 因此没有把它放进required,生成的 client 也允许省略;internal/contract/openapi.go:986的描述还写着 “Omit it for a graph with no agent node; a run without one fails honestly at its first agent node.” —— 现在两层都不成立:缺了根本创建不出来,也不会“在第一个 agent 节点才失败”;internal/core/workflow_run.go:144的 doc comment 仍写 “returns the optional Project”,与函数体内 “Naming one is mandatory, not optional.” 自相矛盾。
AGENTS.md 要求 router/internal/contract/api/openapi.json/frontend/src/api 是同一份契约并在同一次改动里同步。建议:把 projectId 从 optionalField 的 /runs 分支移除(其余 /runs 可省字段不动),描述改为「必填,缺失时 409 workflow_run_project_required」,改掉 resolveWorkflowRunProject 的 doc comment,task frontend:generate 重新生成两份产物,并补一条断言(创建时省略 projectId → 409)锁定行为。
非阻塞
- 工作区 settle 钩子对 live WorkflowRun 的放行(返回 nil、不写业务表)有清晰注释,行为正确;但 Thread 历史在 phase-1 不落库这一点,请确认运行 issue 里已由 ADR 或后续 task 承接,避免遗漏。
已验证
Zpc001
left a comment
There was a problem hiding this comment.
复审结论:Approve
复审对象:head 4b4e6b8(上一轮 51d31d4 之后的增量 git diff 51d31d4..4b4e6b8)。
上一轮的阻塞项已按要求修完:
projectId从optionalField的/runs分支移除,创建 run 的 schema 现在required: [projectId];internal/contract/openapi.go与api/openapi.json的描述改为「必填,缺失即 409workflow_run_project_required」;resolveWorkflowRunProject的 doc comment 改为 “the Project”,与函数体的强制门禁一致;api/openapi.json与frontend/src/api已重新生成,没有手改痕迹;- 新增
TestWorkflowRunRouteRequiresProject锁定 required 集合;运行时的 409 断言原本就有(integration/workflow_run_project_test.go的 missing / start→input 两例),这次也覆盖到了; - 前端
useCreateWorkflowRun在缺projectId时于客户端拒绝,并更新了编辑器用例。当前编辑器还没有 Project 选择器,#148 落地前运行按钮按契约拒绝——这与服务端本来的行为一致,不构成回归。
CI 的 check(5m55s)与 frontend(2m43s)通过,mergeable。
提醒:#72 现在基于旧的 51d31d4,已显示 conflicting;请 rebase 到本 head 后重新请求复审。
…rerun Review round on ora-space#68. The migration shipped without the two things every other migration in this directory has: an entry in the READMEs and a fresh/upgrade/ rerun test. `TestMigration0035WorkflowRunIdentityAppliesFreshAndUpgrades` asserts the four columns, both exactly-one-owner CHECKs, both partial unique indexes, and — the one this migration does by deletion rather than addition — that runtime_controls' IssueRun foreign key is gone while the column it constrained stays. A polymorphic run reference cannot be a foreign key, so the liveness check moved into the code that grants on it, and that is worth a test rather than a comment. The migration is pinned to LF at the same time. It is introduced by this PR and no released database has applied it, so this is the last moment the pin is free: after merge the bytes are in a ledger this change could not reach. 0034, which is on main, keeps its platform-dependent newline for exactly that reason, and 0035_model_attempt_statistics.sql is left to the PR that owns it. Co-Authored-By: Claude Code <noreply@anthropic.com>
e89033c to
3dbcd8a
Compare
Rebasing onto the new ora-space#68 head made `resolveWorkflowRunPluginVersion`'s empty -Project branch unreachable: creation now refuses a run that names no Project, so the Project this function reads the space from is always present. The branch and the comment describing it ("a run that named none has no space to read from and freezes nothing") are removed rather than left as a guard for a case the creation gate no longer admits; the value is passed straight to the query. The rest of the function is unchanged: a missing plugin still freezes nothing and is still reported later as `agent_plugin_version_unfrozen`, which remains an owner decision rather than something to change here. Co-Authored-By: Claude Code <noreply@anthropic.com>
EricWvi
left a comment
There was a problem hiding this comment.
main 已合入 cloud#85:docs/ 按领域整体重建为中英双语,文档地图在 docs/README.md,旧的 docs/*.md 已经全部删除。同时 cloud#62 已合入迁移 0038_agent_workspace_blocked_deadline.sql。请先合并或 rebase 最新 main,再做以下适配:
- 迁移编号:main 上最新的迁移已经是
0038。请把本 PR 新增的迁移顺延到合并时 main 最新编号之后,同步改测试里的文件名、测试名和升级基线,并在internal/core/migrations/README(.en).md末尾追加对应小节(两个文件现在和 main 冲突)。(本 PR 新增0038_workflow_run_identity)。 - 文档:WorkflowRun 身份与 Workspace 生命周期绑定到控制面,请更新
docs/workflows/workflows(.en).md;涉及运行 Workspace 和运行时控制的部分,同步检查docs/agent-runs/issue-runs(.en).md与docs/runtime/runtime-control(.en).md。 - 文档规则见
AGENTS.md的 Documentation 一节:中英两版同步修改;只写当前代码能核验的事实;接口字段以api/openapi.json为准;进度和验收记录写在 issue 里,不进仓库。
…control plane specs#99 D3/D8 step 4 (run identity): the WorkflowRun becomes an execution aggregate the control plane accepts, while session execution stays unwired - the scheduler still fails runs on unwired agent nodes until the executor-slot resolution bridge lands, per the slots decision's landing order (cloud behavior is unchanged). - migration 0035: workflow_runs freezes created_by/project_id; workspaces bind workflow_run_id exclusively with issue_run_id; execution_work carries the same optional ownership under an exactly-one constraint with one partial unregistered index per owner; runtime_controls.maintenance_run_id drops its IssueRun FK, since the live run of either kind is validated where it is granted on. - createWorkflowRun freezes created_by from the authenticated Identity. - createWorkflowRunWorkspace/deleteWorkflowRunWorkspace: the WorkflowRun counterpart of the IssueRun workspace lifecycle, keyed on the frozen identity (project and actor required; idempotent; busy semantics shared). - the six public-visibility filters and the document key strip exclude workflow-run workspaces exactly as they exclude issue-run ones. - enqueue, dispatch and runtime permit accept either run kind; the three settlement hooks (workspace settled/deleted, thread events) accept a WorkflowRun and write nothing - the evidence-driven scheduler reads terminal facts directly. Co-Authored-By: Claude Code <noreply@anthropic.com>
specs#99 D3, run identity: workflow_runs.project_id had no production writer at all. Migration 0035 added the column and createWorkflowRunWorkspace already refuses a run without one, but the only creation path that exists — the editor's Run button — never wrote one, so every direct run was identity-incomplete and could only ever fail at its first agent node with workflow_run_project_required. Nothing ever named the Project a run provisions in. - createWorkflowRun accepts an optional projectId from the request body, freezes it in the same INSERT as created_by, and authorizes it through the same `project` helper every project-scoped route uses: an unknown, malformed, other tenant's or archived Project is one 404, and the frozen value is the row's own id rather than the caller's string. No hardcoded or default Project — a workflow document belongs to a tenant, not to a Project, so there is nothing to default to. - A Project already `deleting` is refused (409 resource_unavailable, the rule the rest of the Project API applies). Freezing one would commit a run whose Workspace can never be created — createWorkflowRunWorkspace reads a deleting Project as busy and a deleted one as gone — leaving the run `running` for good, one failed advance per tick and no terminal state. A `provisioning` Project is still accepted: that state is transient, and the scheduler already waits for it the way it waits for any other admission. - Omitting projectId stays legal: a graph with no agent node needs no Workspace, and the editor's Run button has no Project to offer. Such a run stays identity-incomplete and fails honestly at its first agent node rather than guessing. - The route's field list, api/openapi.json and the generated client carry the new optional field. Tests (integration/workflow_run_project_test.go) drive the real HTTP path rather than seeding rows. The freeze test proves the creation transaction wrote the value — read off the row, not off the response, since the run resource carries no project field — and that deleting the Project afterwards cannot change what an already-created run was created against. The refusal test proves no run row survives a rejected Project, and the `deleting` case proves the hang guard. Both were confirmed failing before this change: the old router rejects the field outright (400 unknown_field field:projectId), and the `deleting` case answered 400 instead of 409. Co-Authored-By: Claude Code <noreply@anthropic.com>
Owner ruling on ora-space#141 (2026-10-17): `project_id` is half of a run's frozen identity (specs#99 D3), so a direct run that names no Project is refused at creation rather than persisted. The refusal reuses the code createWorkflowRunWorkspace already raises for the same condition (`workflow_run_project_required`, 409) instead of inventing a second one. - `resolveWorkflowRunProject` requires a non-empty `projectId`. The gate lives on the resolver rather than on its one caller so that every entry which creates a run — the editor today, and the @ confirm / manual run once they are linked to the same creation point — makes one shared decision inside the creation transaction instead of each writing its own "no Project" rule. This is the single creation point the owner asked for; today the INSERT exists only here. - This reverses the previous "omitting it stays legal" contract. That reading treated a Project-less run as merely incomplete and deferred the failure to the run's first agent node. It is not recoverable there: the run can never provision the Workspace those nodes need, so the honest place to report the gap is before the row exists. - A graph that genuinely needs no Workspace still names one. The rule is unconditional, which is the part a "only graphs that provision" reading would get wrong, so a start -> input graph is the case the new test pins. - `createWorkflowRunWorkspace`'s guard is kept: it now guards rows written before this gate, or by a future entry that bypasses the shared creation point. Tests (integration/workflow_run_project_test.go) drive the real HTTP path: - `TestWorkflowRunWithoutProjectIsRefused` — a start -> input graph is refused with `workflow_run_project_required`, leaves no run row, and runs once the caller names a Project, so the refusal is about the missing identity and not about the graph shape. - `TestWorkflowRunRefusesUnreachableProject` — its trailing "omitting stays accepted" case is now the missing-Project refusal, asserted against the same run-count baseline the unreachable-Project table uses. - The tests that start runs legitimately (`TestWorkflowRunLifecycle`, `TestWorkflowRunTenantIsolation`, the admission and snapshot-scope cases) now name a Project through the new `runProject` helper. A missing Project is checked after admission and input validation, so the cases asserting a snapshot, input or slot refusal are unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
`ClaimWork` projected `OperationId` from `run_id` alone. A workflow work item carries `workflow_run_id` and leaves `run_id` NULL by the mutual-exclusion CHECK 0035 added, so the Controller was handed the empty string; the dispatch that echoes it back then matched no row in `dispatchRunExecution` and was refused as `dispatch_conflict` forever. The whole session dispatch chain was dead for workflow runs, and the existing seam test could not see it because it calls `nodeDispatch` directly instead of claiming first. The claim now projects `coalesce(run_id, workflow_run_id)` under its own name. The alias is deliberate: rows are serialized with `row_to_json`, which keeps both copies of a repeated column name, and which one the decoder keeps is not something this query should depend on. New DB test drives the whole chain through the real control actions — `clone_claim` -> `clone_dispatch` -> `node_executions` — and asserts the claim projects the workflow run id and that the dispatch it authorizes binds both the execution and the work item. Against the old projection it fails with the empty string, which is the bug. Co-Authored-By: Claude Code <noreply@anthropic.com>
…rerun Review round on ora-space#68. The migration shipped without the two things every other migration in this directory has: an entry in the READMEs and a fresh/upgrade/ rerun test. `TestMigration0035WorkflowRunIdentityAppliesFreshAndUpgrades` asserts the four columns, both exactly-one-owner CHECKs, both partial unique indexes, and — the one this migration does by deletion rather than addition — that runtime_controls' IssueRun foreign key is gone while the column it constrained stays. A polymorphic run reference cannot be a foreign key, so the liveness check moved into the code that grants on it, and that is worth a test rather than a comment. The migration is pinned to LF at the same time. It is introduced by this PR and no released database has applied it, so this is the last moment the pin is free: after merge the bytes are in a ledger this change could not reach. 0034, which is on main, keeps its platform-dependent newline for exactly that reason, and 0035_model_attempt_statistics.sql is left to the PR that owns it. Co-Authored-By: Claude Code <noreply@anthropic.com>
Rebasing onto main surfaced three conflicts the textual merge could not see: 0037 (cloud#75, already on main) moved a Project's repository out of the projects table into repository_bindings, and this branch still read it where it used to live. - createWorkflowRunWorkspace selected the projects row whole and read defaultBranch from it. That column is gone, so every run Workspace was refused with default_branch_required. It now reads through projectView, exactly as its IssueRun counterpart in the same file does. - TestWorkflowRunRefusesUnreachableProject and the dispatcherDB seed inserted projects(repository_url, default_branch). Both now insert the Project and its Binding together, the idiom every other direct seed in this repository uses. Behavior is unchanged: the same branch name, read from the row that now holds it. Co-Authored-By: Claude Code <noreply@anthropic.com>
The runtime refuses a run that names no Project with 409 `workflow_run_project_required`, but the contract still described the opposite: `projectId` sat in `optionalField`'s `/runs` branch, so the published schema left it out of `required`, and its description promised that a run without one "fails honestly at its first agent node". Neither holds — a run that names none is never created, so it never reaches a node. `resolveWorkflowRunProject`'s own doc comment contradicted itself the same way, calling the Project optional two paragraphs above "Naming one is mandatory, not optional." `projectId` is now required on the run-creation route and nowhere else, the description states the refusal, and the doc comment says what the body does. The same change is carried through `api/openapi.json` and `frontend/src/api`, so the generated client stops offering a request the service always rejects. That makes the editor's create mutation unable to build its body at all: it has no Project to state, and the run form that picks one is not built yet. The mutation refuses such a run before sending it rather than inventing a placeholder Project to fill the field — the service answers the same code, and no run is frozen against a Project nobody chose. `useCreateWorkflowRun` is where that gate lives, so the form that lands later states a Project and the gate opens; the toolbar tests pin the refusal on both sides of the app's real routes. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main now carries 0038_agent_workspace_blocked_deadline.sql (cloud#62), so the run-identity migration moves one further along: 0038_workflow_run_identity.sql becomes 0039, with the test name, upgrade baseline (from 0038), migrations README entries (zh and en) and the eol pin following it. Main keeps its own 0038 section and file untouched. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ership main rebuilt docs/ into bilingual per-domain documents (cloud#85), so the behavior this PR adds is documented there: the workflow run section gains the mandatory projectId and the created_by/project_id frozen at creation, and the run Workspace ownership (workspaces.workflow_run_id, mutually exclusive with issue_run_id) is stated where the Workspace aggregates and the runtime control reservation are described. Both languages change together, per the Documentation rules. Co-Authored-By: Claude Code <noreply@anthropic.com>
3dbcd8a to
73a8189
Compare
Rebasing onto the new ora-space#68 head made `resolveWorkflowRunPluginVersion`'s empty -Project branch unreachable: creation now refuses a run that names no Project, so the Project this function reads the space from is always present. The branch and the comment describing it ("a run that named none has no space to read from and freezes nothing") are removed rather than left as a guard for a case the creation gate no longer admits; the value is passed straight to the query. The rest of the function is unchanged: a missing plugin still freezes nothing and is still reported later as `agent_plugin_version_unfrozen`, which remains an owner decision rather than something to change here. Co-Authored-By: Claude Code <noreply@anthropic.com>
Zpc001
left a comment
There was a problem hiding this comment.
复审结论:Approve
复审对象:head 73a8189(相对 main@4eae91d 的净改动),针对 @EricWvi 2026-10-18 的 request changes 逐条核对:
- 迁移编号顺延为
0039:文件名0039_workflow_run_identity.sql、测试TestMigration0039WorkflowRunIdentityAppliesFreshAndUpgrades(升级基线从 0038)、migrations README 中英两节、.gitattributes的eol=lf均已同步,main 的0038段不动。 - projectId 契约保留:
api/openapi.jsonrequired: ["projectId"],描述写明缺失409 workflow_run_project_required、不可用/已归档/跨空间404、删除中409 resource_unavailable;resolveWorkflowRunProject走统一的 project 授权。 - 文档中英同步:
docs/workflows、docs/runtime/workspaces、docs/runtime/runtime-control、docs/agent-runs/issue-runs四个域的.md/.en.md成对更新,只写当前代码可核验的事实。 - rebase 残留:
integration/revision_upgrade_test.go无重复查询。 - CI:head
73a8189上check+frontend全绿,mergeable。
本 PR 做什么
WorkflowRun 成为控制面承认的执行聚合:运行身份在创建事务里冻结,运行 Workspace 的归属与运行时控制的预留都跟着运行身份走。这是 specs 根决策 D3/D8(
decisions/cloud/workflow/0-cloud-side-scheduler-owns-workflow-execution.md,D1 落地顺序第 4 步「运行身份」)。堆叠关系:本 PR 叠在 #67 之上,base 只能是
main(GitHub 不接受同 fork 分支作 base),合并必须按 #67 → #68 → #72 的顺序。本 PR 自己的净改动是git diff 73a8189^...73a8189与下面「净改动文件」一节。迁移
0039_workflow_run_identity.sqlworkflow_runs.created_by/project_id:运行身份在创建事务里冻结,运行 Workspace 的生命周期读它而不是重新解析「谁按了运行」。workspaces.workflow_run_id(唯一、外键)与issue_run_id的互斥 CHECK:运行 Workspace 可以属于 WorkflowRun。execution_work.run_id变为可空 +workflow_run_id+ 「恰有一个所有者」CHECK;未注册工作项的单列唯一索引拆成每种所有者一条部分索引。runtime_controls.maintenance_run_id去掉issue_runs外键:多态运行引用无法做外键,存活校验移到授予它的代码(runtime_permit在 Node 绑定前重读任一类型的活运行)。纯前向、可重复执行、不回填。运行身份与 Workspace 生命周期
createWorkflowRun从认证身份冻结created_by(不是路径参数),projectId必填:缺失 409workflow_run_project_required,不可用/已归档/跨空间 404,正在删除 409resource_unavailable。工作流文档属于租户不属于 Project,没有可默认的对象。createWorkflowRunWorkspace/deleteWorkflowRunWorkspace:与 IssueRun 同构的生命周期(幂等、busy/quiesce 语义、预声明快照),键在冻结身份上。控制面接受(D8)
enqueueExecutionWork对不是活 IssueRun 的 id 分流到enqueueWorkflowExecutionWork(kind 固定agent_session,D7 把成果交付留后)。ClaimWork投影claimedRunId(coalesce(run_id, workflow_run_id)):workflow 工作项的run_id为 NULL,旧投影交给 Controller 的是空串,回传后永久dispatch_conflict。dispatchRunExecution、runtime_permit、三个结算钩子都接受两种运行;settleSessionEnded仍报错(feat(core): resolve executor slots and run workflow agent sessions #72 是它的提交点)。本轮改动(按 @EricWvi 2026-10-18 的 request changes)
0038)。0039:文件名、TestMigration0039WorkflowRunIdentityAppliesFreshAndUpgrades、升级基线(从 0038 升级)、migrations README 中英两节、.gitattributes的eol=lf固定一起改;main 自己的0038段与文件不动。docs/workflows/workflows(.en).md:新增「运行的身份在创建时冻结」——projectId必填与各拒绝码、冻结的created_by/project_id、运行 Workspace 归属;docs/runtime/workspaces(.en).md:运行 Workspace 恰好属于一次运行(issue_run_id与workflow_run_id互斥);docs/runtime/runtime-control(.en).md:maintenance 按运行身份记录,IssueRun 与 WorkflowRun 都认;docs/agent-runs/issue-runs(.en).md:补交叉引用。ADR 状态
根决策保持
approved:本 PR 落 D3 的冻结身份与 D8,真实执行侧(D1 落地顺序第 3 步)在 #72。specs 侧的证据与错误码同步在 specs#119(按 owner 结论只留证据表,等 #68/#72 合并后按最终 main 逐行重核);ADR 的「不支持的条件配置清单」已由 reviewer 拆到 specs#123。门禁
CI 在 head
73a8189上全绿:check8m59s、frontend2m24s。本地 Go 工具链在 UNC 路径上不可用(
RLock ... Incorrect function),task check无法在本机复跑,以 CI 为准;前几轮在本机的复跑结论(Windows 环境性失败与未改动基线逐条一致)保留在历史评论里。净改动文件
Refs ora-space/circus#141
🤖 Generated with Claude Code