#5821:workspace pool 真正进入“单一付款方”的 credits 运行时
这条业务线解决的不是“在个人套餐里增加一个 Team”。真正的问题是:工作区为席位持有人买了能力之后,成员在这个 workspace 里做的工作应该由谁付款;个人余额、workspace pool、操作发生地和退款对象必须能在同一条数据流里被区分
source_automation: voyager-merged-pr run_date: 2026-08-10 anchor_pr_number: 5821 pr_number: 5821 pr_title: “feat(credits): let a workspace pool pay for work done in it” pr_url: https://github.com/adastralab-ai/voyager/pull/5821 ↗ author: “Shawn Hu / huxwfun” merged_at: “2026-08-10T03:09:07Z” modules:
- backend/go/apps/api/contract_test/credits/e2e/helpers_test.go
- backend/go/apps/api/contract_test/credits/e2e/pool_pays_test.go
- backend/go/apps/api/contract_test/credits/e2e/spend_test.go
- backend/go/internal/agent/service.go
- backend/go/internal/billing/model.go
- backend/go/internal/billing/service.go
- backend/go/internal/brand/reference_task.go
- backend/go/internal/brand/service_import.go
- backend/go/internal/brand/service_integration_test.go
- backend/go/internal/brand/showcase_generate.go
- backend/go/internal/credits/errors.go
- backend/go/internal/credits/model.go
- backend/go/internal/credits/module.go
- backend/go/internal/credits/repo.go
- backend/go/internal/credits/service.go
- backend/go/internal/credits/service_integration_test.go
- backend/go/internal/image/service.go
- backend/go/internal/task/service.go
- backend/go/internal/task/service_integration_test.go
- backend/workers/agent/src/goApi.ts
- backend/workers/agent/src/goApi.test.ts
- packages/site/src/app/(main)/_agent/chat/_components/InsufficientCreditsNotice.tsx files_changed: 22 learning_tags:
- billing
- team-seats
- credit-pool
- single-payer-debit
- refund-attribution
- workspace-scope
- credit-ledger
- postgres-locking
- cross-module-contract
- rollout-compatibility
- integration-testing business_line: “Team 席位、workspace credit pool 与 credits ledger 的付款归属和运行时扣费” related_prs: [5817, 5815, 5771, 5820, 5833] line_stage: “个人 Plan 与 seat type 分离 (#5817) -> PlanResolution/Entitlements 边界 (#5815) -> workspace bucket/ledger schema (#5771) -> seat entitlement 并集 (#5820) -> workspace pool 单一付款方 debit/refund (#5821) -> workspace checkout/webhook 购买入口 (#5833)” open_questions:
- “#5821 的 ResolveFunding 在 task/agent 事务外读取 seat;seat 被撤销后到 DebitTx/DebitAgentTurn 之间没有再次授权校验,余额锁不能替代成员资格校验。”
- “#5833 的 AssignSeat、RevokeSeat、UpdateSeatCount 仍是显式 not implemented;workspace checkout/webhook 也没有创建 pool 或把购买数量转换成 bucket。#5821 的 E2E 只能直接写 workspace_seats 和 credit_buckets fixture。”
- “PoolView.AvailableToMe 当前等于 pool 总余额,SeatMonthlyAllotment、每日 holder 上限和 bucket_id/user_id/created_at 用量索引尚未接入扣费决策。”
- “任务扣费在 pool 不足整笔时整笔回落个人余额;Agent turn 是事后扣费,个人余额为零时允许 pool 用 residue 部分扣到零。两套规则需要产品和账务确认是否长期保持不同。”
- “#5771 的 owner CHECK/FK 仍是 NOT VALID,旧 personal upsert 的 unique conflict target 仍保留;后续 validation 和旧 upsert 清理没有在本线闭合。”
- “workspace_id 已写入个人付款和 pool 付款的 task/agent ledger,但当前索引按 bucket、holder、时间服务 pool usage;workspace 审计/报表查询尚无明确索引契约。”
- “当前测试覆盖真实 Postgres/HTTP 与 Stripe fake,但没有部署级 Stripe checkout -> webhook -> seat assignment -> pool seeding -> debit/refund -> browser gate 链路。” feishu_doc_url: null
业务线概览#
这条业务线解决的不是“在个人套餐里增加一个 Team”。真正的问题是:工作区为席位持有人买了能力之后,成员在这个 workspace 里做的工作应该由谁付款;个人余额、workspace pool、操作发生地和退款对象必须能在同一条数据流里被区分。
当前代码已经把四个问题拆开:
Plan只描述账号自己购买的 free/basic/pro/mega 阶梯;SeatType描述 workspace 买的能力类型。workspace_subscriptions是 workspace 级 Stripe 订阅,workspace_seats是成员获得池子访问权的资格。credit_buckets的 owner 可以是 user 或 workspace;credit_transactions.bucket_id记录真正移动余额的桶,workspace_id记录操作发生地。ResolveFunding(userID, workspaceID)只回答这一次操作可以由哪个余额付款;它不把 personal plan、workspace pool 和可用额度合成一个模糊的“总余额”。
今日锚点 #5821 ↗ 是这条线从 schema 进入运行时的关键一步:它接通了 seat -> workspace pool -> task/agent debit -> ledger -> refund,并把 worker 的 Agent turn gate 改成只接受当前 workspace 的 pool。它没有完成购买、分配席位或创建 pool,因此“代码路径存在”不能推导为“线上用户已经能使用 Team credits”。
今日锚点#
- 标题:feat(credits): let a workspace pool pay for work done in it ↗
- 编号:#5821
- 作者:Shawn Hu /
huxwfun - merge 时间:2026-08-10 03:09:07 UTC(Australia/Melbourne 13:09:07)
- merge commit:
929bad10c614d5e6da7946c961adbafce3219cad - 选择理由:它是 2026-08-10 Melbourne 合并窗口里最能推进既有 Team credits 业务线的未学习 PR,直接实现了上一日 #5771 预留的 bucket owner/ledger schema;同时与当天先后的 #5820、#5833 共享 billing、workspace、credits、Stripe contract 和真实集成测试,而不是标题相似的 UI 或权限机械变更。
演进时间线#
| 阶段 | PR | 改变的层 | 代码证据与意义 |
|---|---|---|---|
| 个人阶梯与席位类型分离 | #5817 ↗,2026-08-10 00:27 Melbourne | TypeSpec/Go model/生成契约 | 删除 PlanTeam;PlanForPriceKey 不再把 team seat price 映射到个人 Plan,改由 SeatTypeForPriceKey 读取。席位不再伪装成个人订阅阶梯。 |
| 计费事实与能力边界收敛 | #5815 ↗,2026-08-10 01:22 Melbourne | billing model/credits | PlanResolution 只保留个人 Plan、price key、billing cycle 和 PlanConfirmed,不再把整行 subscription 透传;能力判断集中到 EntitlementsForUser,为后续 seat entitlement 和 funding resolver 留出窄边界。 |
| 持久化表达 workspace owner | #5771 ↗,2026-08-09 17:20 Melbourne | PostgreSQL migration/index | credit_buckets.workspace_id 作为第二种 owner,并用 XOR check 保证 user/workspace 二选一;ledger 增加 workspace_id,pool partial unique index 和 paying-bucket usage index 先于任何 pool 写入建立。 |
| 席位授予能力 | #5820 ↗,2026-08-10 10:43 Melbourne | billing entitlement/runtime | 个人 entitlements 与仍在 workspace 中的 Basic/Pro seat 按字段求并集;GUEST 不算有效成员,查询失败对能力路径 fail-open 回落个人购买。它解决“能做什么”,不决定“谁付款”。 |
| 单一付款方 debit/refund | #5821 ↗,2026-08-10 13:09 Melbourne | billing/credits/task/agent/worker | 通过 ResolveFunding 按操作 workspace 选择 pool;pool 能覆盖整笔任务才付款,否则整笔由个人余额承担;退款按 debit 行的 bucket 退回,并把 workspace 写入每条 ledger。Agent gate 只看当前 workspace 的 pool。 |
| 购买与 webhook 入口 | #5833 ↗,2026-08-10 19:28 Melbourne | Stripe checkout/webhook/workspace billing | workspace 使用独立 Stripe customer,checkout 在 subscription metadata 写入 workspaceId,webhook 按该 metadata 写 workspace_subscriptions;但 AssignSeat、pool seeding 和 seat count 更新仍未实现。 |
这条线的层次变化可以概括为:先删除错误的 PlanTeam 抽象,再把“能力”与“付款”分开,之后才让 workspace 成为 bucket owner。#5821 是运行时的付款选择层;#5833 是支付对象和 webhook 的入口层,二者并不意味着完整 Team 业务已经闭合。
当前架构与数据流#
flowchart LR
A["Workspace checkout"] --> B["workspace_subscriptions + Stripe subscription metadata"]
B --> C["workspace_seats / active membership"]
C --> D["ResolveFunding(user, workspace)"]
D --> E["Task Submit or Agent turn debit"]
E --> F["Choose one payer: pool or personal"]
F --> G["credit_buckets balance + credit_transactions"]
E --> H["Publish after commit"]
H --> I["Fail -> Refund by debit.bucket_id"]
D --> J["GET /api/credits/me pools"]
J --> K["Agent gate matches current workspace"]plaintext- Workspace billing入口。 #5833 的
WorkspaceCheckout不复用个人 checkout:它为 workspace 使用独立 Stripe customer,先UpsertPendingWorkspaceSubscription写本地行,再创建 Stripe session,并把workspaceId写进subscription_data.metadata。webhook 读取 subscription 本身的 metadata,命中 workspace 分支后用 stale guard 更新workspace_subscriptions;没有 metadata 的个人事件仍走原路径。 - 能力与成员资格。 #5820 的
entitlementsFor以个人 Plan 为基线,再对activeSeats做 entitlement widen。activeSeats只接受 owner/admin/member,不接受 GUEST;seat 查询或 membership 查询失败时,能力路径回落个人 plan。#5821 把底层读取拆成返回 error 的seatsForUser,让 funding 路径不再复用这个 fail-open 语义。 - 付款方解析。
ResolveFunding先解析个人 plan,再严格读取 seat 和 membership;只有 seat 的 workspace 等于本次操作的workspaceID才返回Funding.Pool。seat lookup 失败直接返回 error,而不是把操作错误地记到员工自己的 credits 上。PoolsForUser则返回成员能触达的所有 workspace,供读路径显示。 - 任务提交。 image service 在 source asset、entitlement 和 reference 预检通过后调用
task.Submit。task.Submit在事务外解析 funding,在同一事务里写 task 并调用credits.DebitTx;只有 task/debit 都提交后才执行 queue publish。publish 失败进入Fail,再由Refund按原 debit 的 bucket 恢复余额。 - 任务 debit。
chooseDebitPayer先无锁读取 workspace pool;池子余额不足整笔成本时完全回落 personal,避免一笔操作同时有两个付款方。看起来够付时才锁 pool,并在锁内复核;复核后不足仍回落 personal。余额更新和 ledger insert 在同一事务里完成,workspace_id无论 personal 还是 pool 付款都记录。 - Agent turn。 Agent chat 通过 chat 的 workspace 调用 Go
DebitAgentTurn,不是使用全局当前 workspace。worker 先读取/api/credits/me:个人totalAvailable大于零,或pools中有当前 workspace 且availableToMe > 0,才允许 turn 开始。turn 已经执行后才扣费;个人余额为零时,若 pool 有 residue,会允许 pool 扣到其剩余余额,避免 residue 让 gate 永远放行却不消费。 - Credits read model。
/api/credits/me.totalAvailable仍然只统计个人 bucket;workspace pool 独立放在pools[],不能把两者相加,因为相加后的数字没有明确的可消费 workspace。#5821 当前把PoolView.AvailableToMe设为 pool 总余额,daily holder limit 和 pool reset 仍是后续路径。
关键代码#
1. 先把“个人购买的 Plan”与“席位授予的能力”拆开#
PR #5817,backend/go/internal/billing/model.go:30-91;PR #5815,backend/go/internal/billing/model.go:94-118
// #5817: Plan 只剩个人购买阶梯。
const (
PlanFree = api.PlanFree
PlanBasic = api.PlanBasic
PlanPro = api.PlanPro
PlanMega = api.PlanMega
)
// #5815: credits 只拿它需要的 billing facts。
type PlanResolution struct {
Plan Plan
PriceKey string
Cycle *BillingCycle
PlanConfirmed bool
}plaintextPlanForPriceKey 对 team seat price 返回未识别,SeatTypeForPriceKey 才负责把价格归一到 Basic/Pro seat。这个边界让后续 ResolveFunding 可以同时返回个人 PlanResolution 和 workspace PoolFunding,而不需要让所有 Plan consumer 重新理解 team。
2. bucket owner 使用数据库不变量表达,而不是靠 Go caller 自律#
PR #5771,backend/base/db/migration/000107_credit_buckets_workspace_owner.up.sql:25-33;000108_credit_buckets_workspace_scope_index.up.sql:10-11
ALTER TABLE credit_buckets
ADD COLUMN workspace_id UUID,
ADD CONSTRAINT credit_buckets_one_owner
CHECK ((user_id IS NULL) <> (workspace_id IS NULL)) NOT VALID,
ALTER COLUMN user_id DROP NOT NULL;
CREATE UNIQUE INDEX CONCURRENTLY credit_buckets_workspace_scope
ON credit_buckets(workspace_id, bucket_type) WHERE user_id IS NULL;plaintextXOR check 保证每一行恰好一个 owner;partial unique index 保证每个 workspace 每个 bucket type 只有一个 pool。旧 (user_id, bucket_type) unique constraint 刻意保留,因为滚动发布时旧 binary 仍会发出原来的 ON CONFLICT (user_id, bucket_type)。
3. funding 路径不复用 entitlement 的 fail-open fallback#
PR #5821,backend/go/internal/billing/service.go:205-284
func (s *service) ResolveFunding(ctx context.Context, userID, workspaceID uuid.UUID) (Funding, error) {
personal, err := s.resolveUserPlan(ctx, userID)
if err != nil { return Funding{}, err }
seats, err := s.seatsForUser(ctx, userID)
if err != nil { return Funding{}, errors.Wrap(err, "failed to resolve seats for funding") }
out := Funding{Personal: personal}
for _, seat := range seats {
if seat.WorkspaceID == workspaceID {
out.Pool = &PoolFunding{WorkspaceID: workspaceID}
break
}
}
return out, nil
}plaintext同一个成员查询对两个业务问题采用不同错误策略:entitlement 的查询失败意味着“不要把所有用户的操作都锁死”,所以 activeSeats fail-open;funding 的查询失败意味着“不能确定谁应该付款”,所以必须报错。这个差异是风险边界,不是重复实现。
4. task debit 选择一个 payer,并在必要时才锁 workspace pool#
PR #5821,backend/go/internal/credits/service.go:378-408
candidate, err := s.repo.GetPoolBucketsNoLock(ctx, tx, pool.WorkspaceID)
if err != nil { return nil, err }
if totalBalance(candidate) < poolNeeds {
return personal, nil
}
locked, err := s.repo.LockPoolBuckets(ctx, tx, pool.WorkspaceID)
if err != nil { return nil, err }
if totalBalance(locked) < poolNeeds {
return personal, nil // unlocked read was stale
}
return locked, nilplaintext一笔 task 不拆给公司和员工:pool 能覆盖完整成本才使用 pool,否则完整回落 personal。无锁读避免不可能付款的请求触碰热点 pool;锁内复核避免并发成员把同一余额同时视为可用。FOR UPDATE 持有到事务提交,因此 task row、bucket update、ledger insert 的顺序必须在同一事务内完成。
Agent turn 有明确的不同边界:它是 post-hoc debit,个人余额为零时把 poolNeeds 降为 anyBalance = 1,从而让 pool residue 被消耗;个人还有余额时仍要求 pool 覆盖整笔 turn cost。这解决了 Agent gate 与实际扣费之间的“残余余额永远放行”问题,但也意味着 task 与 Agent 的不足额语义不同。
5. refund 以 debit 行指定的 bucket 为权威,而不是按 spender 重新猜#
PR #5821,backend/go/internal/credits/service.go:605-658;backend/go/internal/credits/repo.go:198-216
for _, d := range debits {
amount := abs(d.Amount)
after, err := s.repo.CreditBucketBalance(txCtx, tx, d.BucketID, amount)
if errors.Is(err, ErrBucketGone) { continue }
if err != nil { return err }
inserts = append(inserts, Transaction{
UserID: userID, BucketID: d.BucketID,
WorkspaceID: d.WorkspaceID,
Amount: amount, BalanceBefore: after - amount, BalanceAfter: after,
Kind: TxnRefund, TaskID: &ref,
})
}plaintext旧路径按 spender 的个人 buckets 找退款目标;pool 付款时当然找不到,于是余额被静默烧掉。新路径沿 debit 的 BucketID 和 WorkspaceID 回退到真实付款方,并用 UPDATE ... balance = balance + ? RETURNING balance 做相对增量,不需要 refund 再锁 pool,避免和 pool debit 形成反向锁序。
6. worker gate 只接受当前 workspace 的 pool#
PR #5821,backend/workers/agent/src/goApi.ts:584-615;对应 UI 为 InsufficientCreditsNotice.tsx:29-71
const workspaceId = requireWorkspaceId(go);
const canPay =
result.data.totalAvailable > 0 ||
result.data.pools.some(
(pool) => pool.workspaceId === workspaceId && pool.availableToMe > 0,
);plaintexttotalAvailable 不再是“账号能在任何地方消费的总额”;它仍是 personal-only。另一个 workspace 的 pool 不能放行当前 chat,否则 turn 可能在当前空间运行、ledger 却没有可用付款方。#5821 的 Go API 单测同时覆盖“只有匹配 workspace pool 放行”和“只有其他 workspace pool 仍拒绝”。
工程取舍#
边界与数据权威#
- 能力和付款分离。 #5815/#5820 让
EntitlementsForUser负责“能做什么”,#5821 的ResolveFunding负责“谁付款”,避免每个 image/task/agent caller 自己从plan或 seat 推断资金来源。 - 三个 ledger 维度不互相替代。
user_id是谁消费,bucket_id是谁的余额移动,workspace_id是在哪里发生;个人 bucket 在 workspace 中付款时也写 workspace,未来才可以做真实用量归因。 - workspace 访问资格来自 seat + membership。
workspace_seats的 active row 单独不够;seatsForUser还要确认 owner/admin/member,排除由共享文件产生的 GUEST membership。
并发、性能与账务一致性#
- pool 只在可能付款时加锁。 无锁候选读减少对 workspace 热点行的锁竞争;锁内重读是必要的,因为候选余额可能在两个读之间被其他成员消耗。
- 不拆 payer。 这牺牲了“先花 pool 再花个人”这种表面上的额度利用率,却保留了一个 task 一个可逆付款对象,refund 不需要跨所有者分配。
- refund 不反向锁 pool。 debit 锁 personal 后再锁 pool;refund 仍锁 spender 以保持 refund idempotency,但用 bucket id 的相对 update 写回 pool,避免两条路径形成锁序倒置。
- 读取模型不做不可消费的总和。 pool 按 workspace 展开,
totalAvailable保持个人值;用户在 workspace 间切换时从同一pools[]找目标,不需要把多个 pool 合并成一个假余额。 - 仍有明显的非原子窗口。 funding 在
task.Submit事务外解析,之后才在事务里写 task/debit;余额锁只保护资金竞争,不会重新证明 seat 仍然有效。这是授权与账务边界之间还没有闭合的窗口。
兼容性与 rollout#
- #5771 用单条
ALTER TABLE增加 nullable workspace owner、外键和 XOR check,并使用lock_timeout = '5s';两个CREATE INDEX CONCURRENTLY各自单独迁移,避免对热表长时间排队。 - 新 FK/check 是
NOT VALID:现存行逻辑上满足约束,不需要在发布锁期间扫描整张表;但后续VALIDATE CONSTRAINT仍是运维责任。 - 旧 personal unique constraint 不立即删除,保证旧 binary 在新 migration 后仍能解析原 conflict target。这个兼容性选择与 #5817 的契约删除不同:已写入数据和正在滚动的 SQL 行为不能靠 reload 修复。
- #5833 的 workspace subscription/customer 和 webhook metadata 是独立的支付对象;
PlanForPriceKey仍不读取 team seat price,seat type 才是购买数量与额度策略的稳定 key。
测试策略#
backend/go/apps/api/contract_test/credits/e2e/pool_pays_test.go:13-184用真实数据库行形状建立 seat、subscription 和 pool,验证 workspace 付款且个人余额不动、pool 退款、pool 不足整笔时不被触碰、pool 与个人余额分开报告、移除 membership 后 pool 不再可达。backend/go/internal/credits/service_integration_test.go:204-357覆盖 Agent turn 的特殊不足额语义、计划未确认时 pool 仍可付款、waived turn 的 COGS 行仍落在个人 bucket,以及 residue 不会持续放行而不扣费。backend/workers/agent/src/goApi.test.ts覆盖当前 workspace 匹配、错误 workspace 不匹配和认证/5xx fallback;UI notice 复用同一个匹配条件,避免 worker 放行但 UI 不给 retry。- #5833 的 checkout/webhook contract test 断言出站 Stripe form 的
subscription_data[metadata][workspaceId]、workspace customer 不带个人userId、workspace row 在 Stripe 前 claim、stale team event 不撤销当前订阅,以及团队 invoice 使用团队通知卡片。 - 这些测试证明了模块边界和若干真实 SQL/HTTP 行为,但不能替代部署级链路:测试 fixture 直接写
workspace_seats/credit_buckets,因为当前 API 仍没有 seat assignment 和 pool seeding。
和最近学习记录的关系#
- #5771:workspace credit pool owner 与 ledger schema 是本次最直接的前置。昨天报告把“下一步必须证明 bucket selection、debit/refund、workspace attribution”列为缺口;#5821 正好补上了这三个 runtime seam,但仍没有补 pool 创建和 seat assignment。
- #5455:insufficient credit telemetry boundary 关注用户遭遇额度不足时如何产生 typed event;#5821 改变的是不足前的 funding/gate 事实,不应把 workspace pool 的 ledger 付款当成前端 encounter event,二者的事实来源不同。
- #5754:平台任务预算终止与自动续跑 是 Agent runtime 的交付状态线。本次只复用其“跨层 contract 要由 worker、Go 和持久化共同闭合”的观察,不把 Agent continuation 与 credits billing 拼成同一业务线。
- #5683 Export Workflow 和 #5520 image delivery 都有“先做边界预检,再进入有副作用事务”的相似工程形态,但没有共享 billing/credits 数据模型,因此不作为本次正式 related PR。
我会怎么吸收#
- 先定义每个字段回答哪个业务问题。 把 money/price、capacity/seat type、owner/bucket、scope/workspace、capability/entitlement 分开,再设计 resolver;不要用一个
Plan或一个workspace_id兼任所有语义。 - 用窄接口把跨模块事实集中到唯一入口。
ResolveFunding和EntitlementsForUser让 task、image、agent 不必复制 seat/membership/plan 判断,也使不同风险场景的 fail-open/fail-closed 策略可被显式评审。 - 把不可逆的账务选择放进单一事务。 task row 与 debit 同事务,publish 在 commit 后;失败按 debit 行 refund,避免队列失败造成“任务已扣费但无法找回付款方”。
- 并发优化不能牺牲复核。 先无锁读减少无意义锁,再在锁内重读确认;如果必须避免锁序倒置,优先用 debit 行作为退款目标和相对增量,而不是为了统一代码强行复用一套锁。
- 测试既要证明真实行为,也要标记 fixture 绕过的产品边界。 真实 Postgres/HTTP contract 能证明 bucket selection,但直接插入 seat/pool 的测试不能证明 checkout 到 assignment 的产品链路,报告和后续计划都应保留这个差异。
边界、风险与未解问题#
代码已经确认的边界#
- #5821 的生产 task 路径把
workspaceID和Funding.Pool带入DebitTx;Agent chat 从 chat 所属 workspace 带入DebitAgentTurn。这两条路径都会把 workspace 写入 debit ledger。 - task debit 的 pool 规则是“能覆盖整笔才用”,而 Agent turn 的 pool 规则在 personal 余额为零时允许消耗 residue;两者都避免同一笔操作同时由两个 owner 付款。
- pool refund 直接使用原 debit 的
BucketID;如果历史 bucket 已被 plan downgrade 删除,ErrBucketGone会跳过该退款,避免把钱错误放入新 bucket。 /api/credits/me的个人totalAvailable与pools[]分离;Agent gate 和 UI retry 都按当前 workspace 精确匹配 pool。- #5833 的 workspace customer、checkout row claim、subscription metadata 路由和 team portal configuration guard 已存在;但 seat 变更接口仍明确返回 not implemented。
仍需确认的风险#
- 资格与扣费之间的竞态。
ResolveFunding在事务外执行,seat/membership 被撤销后,后续 debit 仍可能使用此前解析出的 pool。需要决定以 funding snapshot 为准,还是在事务内重新验证 membership/seat。 - pool 生命周期未闭合。 workspace checkout/webhook 只维护订阅和 seat 商品信息;没有看到把 seat 数量、
SeatMonthlyAllotment、workspace bucket 创建/重置接到同一事务的代码。 - 每日上限未落地。
PoolView.AvailableToMe目前返回 pool total;#5771 的 usage index 尚未被 daily limit 读取路径使用。多成员共享 pool 时,当前实现不能证明每个 holder 的 cap。 - Agent 与 task 的不足额差异。 Agent 的 post-hoc charge 允许个人为空时扣 pool residue,task 则不允许 pool 部分支付;需要产品确认两种行为是否都符合“一个操作一个付款方”的长期账务定义。
- 迁移收尾。
NOT VALIDconstraints 必须验证;旧(user_id, bucket_type)conflict target 何时删除,以及 pool owner 的 down migration 在已有数据下如何安全执行,仍未由本线解决。 - 可观测性。 ledger 有 workspace 维度,但没有看到“pool 被选中/因不足回落个人/因 seat lookup error 拒绝”的结构化业务指标;仅靠余额和 402 很难区分产品行为与故障。
- 部署级验证。 需要真实 Stripe test clock/checkout、webhook 重排、seat assignment、Postgres 并发 debit/refund、Agent reconnect 和浏览器 workspace 切换覆盖,而不只是 fake Stripe 和直接 SQL fixture。
候选说明#
本次按 Australia/Melbourne 的 2026-08-10 合并窗口筛选。当天较有工程信号的候选包括 #5820(seat entitlement)、#5821(workspace pool funding)、#5833(workspace checkout/webhook)、#5837(Agent 读取页面背景)和若干 docs/UI PR。#5821 直接推进前一日 #5771 的未完成 runtime 缺口,且能自然串起 #5820 与 #5833,因此优先于孤立的 UI/文档变更。
日志中已学习过的锚点包括 #5771、#5754、#5683、#5591、#5520、#5503/#5507 等;本次没有重新选择它们。#5771 只作为前置背景和新增阶段对照,不重复把它当作今日锚点。
正式 related PR 限定为 #5817、#5815、#5771、#5820、#5833:它们分别共享 Plan/SeatType 契约、billing resolver、credit bucket/ledger schema、seat entitlement runtime 或 workspace Stripe user flow。六个 PR 的 merge commit 均通过 git merge-base --is-ancestor <commit> origin/main,且当前 Voyager origin/main 为 82497c4398178fa744fd4150869bab88f67c01d4;本次不需要扩大到更早日期。
没有把 #5455、#5754、#5683 等仅有主题相似但不共享代码边界的 PR 塞进正式时间线;它们只在“最近学习记录”中作为对照。#5821 本身有 22 个文件和真实 Postgres/HTTP/worker 测试,因此这条线不是孤立变更,但完整产品链仍被 assignment、pool seeding 和部署级验证阻断。
GitHub 文档#
- 报告文件:
outputs/voyager-daily-pr-study/2026-08-10-pr-5821-workspace-pool-funding.md - 目标仓库:joyehuang/ai-agent-field-notes ↗
- Voyager 锚点:PR #5821 ↗
- Voyager 锚点 merge commit:
929bad10c614d5e6da7946c961adbafce3219cad - Voyager 阅读基线:
origin/main=82497c4398178fa744fd4150869bab88f67c01d4 - 本报告与索引将在本次目标仓库提交中一起持久化;实际目标仓库 commit 以本次外部
study-log.jsonl的githubCommit为准。