Skip to content

feat(core): bind WorkflowRun identity and workspace lifecycle to the control plane - #68

Merged
EricWvi merged 9 commits into
ora-space:mainfrom
Kyber5323:feat/141-workflow-run-identity
Oct 19, 2026
Merged

EricWvi merged 9 commits into
ora-space:mainfrom
Kyber5323:feat/141-workflow-run-identity

Conversation

@Kyber5323

@Kyber5323 Kyber5323 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

本 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.sql

  • workflow_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 必填:缺失 409 workflow_run_project_required,不可用/已归档/跨空间 404,正在删除 409 resource_unavailable。工作流文档属于租户不属于 Project,没有可默认的对象。
  • createWorkflowRunWorkspace / deleteWorkflowRunWorkspace:与 IssueRun 同构的生命周期(幂等、busy/quiesce 语义、预声明快照),键在冻结身份上。
  • 六处公开可见性过滤与公开文档键按同一条规则排除运行 Workspace。

控制面接受(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)

  1. rebase 到最新 main(含 cloud#85 的 docs 重建与 cloud#62 的迁移 0038)。
  2. 迁移顺延为 0039:文件名、TestMigration0039WorkflowRunIdentityAppliesFreshAndUpgrades、升级基线(从 0038 升级)、migrations README 中英两节、.gitattributes 的 eol=lf 固定一起改;main 自己的 0038 段与文件不动。
  3. 文档(中英同步,符合 AGENTS.md 的 Documentation 一节):
    • 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 上全绿:check 8m59s、frontend 2m24s。

本地 Go 工具链在 UNC 路径上不可用(RLock ... Incorrect function),task check 无法在本机复跑,以 CI 为准;前几轮在本机的复跑结论(Windows 环境性失败与未改动基线逐条一致)保留在历史评论里。

净改动文件

.gitattributes
api/openapi.json
docs/agent-runs/issue-runs.md            docs/agent-runs/issue-runs.en.md
docs/runtime/runtime-control.md          docs/runtime/runtime-control.en.md
docs/runtime/workspaces.md               docs/runtime/workspaces.en.md
docs/workflows/workflows.md              docs/workflows/workflows.en.md
frontend/src/api/generated.schemas.ts
frontend/src/features/workflows/api.ts
frontend/src/features/workflows/editor/workflow-editor.test.tsx
frontend/src/features/workflows/editor/workflow-run-tools.test.tsx
integration/migration_upgrade_path_test.go
integration/revision_upgrade_test.go
integration/workflow_admission_test.go
integration/workflow_run_project_test.go
integration/workflow_run_test.go
internal/api/router/router.go
internal/contract/openapi.go              internal/contract/openapi_test.go
internal/controlgrpc/executions.go
internal/core/agent_run_release.go        internal/core/agent_run_settle.go
internal/core/agent_run_thread.go         internal/core/execution_work.go
internal/core/migrations/0039_workflow_run_identity.sql
internal/core/migrations/README.md        internal/core/migrations/README.en.md
internal/core/node_execution.go           internal/core/plugins.go
internal/core/public.go                   internal/core/run_workspace.go
internal/core/runtime_control.go          internal/core/runtime_control_delivery.go
internal/core/runtime_permissions.go      internal/core/store.go
internal/core/workflow_run.go             internal/core/workflow_run_identity_db_test.go

Refs ora-space/circus#141

🤖 Generated with Claude Code

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

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

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

复审结论: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. 格式与 lint 干净。
  2. 堆叠关系已写清楚(“合并必须严格按 #67 → #68 → #72 的顺序”),base 用 main 的理由也成立。

阻塞项

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_exclusive CHECK 与 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 越权。

Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
…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>
@Kyber5323
Kyber5323 force-pushed the feat/141-workflow-run-identity branch from 002d6a5 to aa1462b Compare October 17, 2026 10:00
Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
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>
Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
…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>
@Kyber5323
Kyber5323 force-pushed the feat/141-workflow-run-identity branch from aa1462b to 51d31d4 Compare October 17, 2026 11:47
Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
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>
@Kyber5323

Copy link
Copy Markdown
Contributor Author

Rebase 完成、CI 已绿 — 请复审

  • 新 head:51d31d4(原 aa1462b),基线 main@10fd6dc
  • CI:check ✅ + frontend ✅(check 含 go test -race)
  • 与 main 的合并性:git merge-tree --write-tree --name-only 10fd6dc 51d31d4 返回单一 tree,无冲突文件

冲突与解决、迁移编号的处理、与未改动基线逐条对照的测试证据,都在描述开头的「Rebase 到 main@10fd6dc」一节。

有一处值得单独看:0037(#75,已在 main)把 projects.repository_url / default_branch 移进 repository_bindings。本分支的 createWorkflowRunWorkspace 仍从 projects 整行读 defaultBranch,rebase 时不报冲突,但跑起来后每次运行 Workspace 创建都会以 default_branch_required 被拒。已改为读 projectView——读到的还是同一个分支名,只是从现在的持有行读,行为不变。同类问题还有两处测试 seed 用了已被删除的列。

请基于新 head 复审。

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

复审结论: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 承接,避免遗漏。

已验证

  • 栈间合并树干净(68→main、72→68);
  • 真实 PostgreSQL(127.0.0.1:55432,REQUIRE_POSTGRES=1)下 #72 head 上的定向用例通过(#72 包含本 PR 内容):go test ./internal/core -run 'TestWorkflow|TestExecutorSlotMappings|TestMigration|TestControllerExecutionProjection' -count=1 → ok ... 9.369s;
  • CI check(9m27s)与 frontend(3m28s)通过。

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

复审结论:Approve

复审对象:head 4b4e6b8(上一轮 51d31d4 之后的增量 git diff 51d31d4..4b4e6b8)。

上一轮的阻塞项已按要求修完:

  • projectId 从 optionalField 的 /runs 分支移除,创建 run 的 schema 现在 required: [projectId];
  • internal/contract/openapi.go 与 api/openapi.json 的描述改为「必填,缺失即 409 workflow_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 后重新请求复审。

Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
…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>
@Kyber5323
Kyber5323 force-pushed the feat/141-workflow-run-identity branch 2 times, most recently from e89033c to 3dbcd8a Compare October 17, 2026 14:00
Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 17, 2026
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 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.

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 里,不进仓库。

Kyber5323 and others added 2 commits October 19, 2026 09:37
…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>
t and others added 7 commits October 19, 2026 09:37
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>
@Kyber5323
Kyber5323 force-pushed the feat/141-workflow-run-identity branch from 3dbcd8a to 73a8189 Compare October 19, 2026 01:48
Kyber5323 pushed a commit to Kyber5323/cloud that referenced this pull request Oct 19, 2026
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 Zpc001 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.

复审结论:Approve

复审对象:head 73a8189(相对 main@4eae91d 的净改动),针对 @EricWvi 2026-10-18 的 request changes 逐条核对:

  1. 迁移编号顺延为 0039:文件名 0039_workflow_run_identity.sql、测试 TestMigration0039WorkflowRunIdentityAppliesFreshAndUpgrades(升级基线从 0038)、migrations README 中英两节、.gitattributes 的 eol=lf 均已同步,main 的 0038 段不动。
  2. projectId 契约保留:api/openapi.json required: ["projectId"],描述写明缺失 409 workflow_run_project_required、不可用/已归档/跨空间 404、删除中 409 resource_unavailable;resolveWorkflowRunProject 走统一的 project 授权。
  3. 文档中英同步:docs/workflows、docs/runtime/workspaces、docs/runtime/runtime-control、docs/agent-runs/issue-runs 四个域的 .md/.en.md 成对更新,只写当前代码可核验的事实。
  4. rebase 残留:integration/revision_upgrade_test.go 无重复查询。
  5. CI:head 73a8189 上 check + frontend 全绿,mergeable。

Approve。合并顺序仍须 #67 → #68 → #72。

@EricWvi
EricWvi merged commit c9e066f into ora-space:main Oct 19, 2026
2 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.

3 participants