feat: 编排器跨项目全局并发上限闸(tsk_erQHNYdn6i9g)

新增用户级设置项 globalConcurrency(settings 表,独立于新建项目默认值
来源 concurrency),缺省 0 = 不限,向后兼容无需 DB 迁移。

- store: UserSettings 加 globalConcurrency 字段 + 默认 0;新增
  globalInflightCount()(跨所有项目 started run 总数,与 inflightTaskIds 同口径)
- orchestrator.claimTick: 叠加全局闸,与 per-project 闸串联(都过才领);
  轮初查一次全局在途、轮内手动 ++,达上限即 return 跨所有项目停止领取
- api: PUT /api/settings 支持 globalConcurrency(空/null 归一为 0)
- web: 侧栏「全局设置」入口 + 模态,编辑/回显/校验(≥0 整数)
- test: 新增全局闸达上限跨项目阻止 / 未达上限正常领取 / 边界 / 轮初短路 用例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 19:11:42 +08:00
parent 1c62059424
commit 7884182d2b
6 changed files with 131 additions and 1 deletions
+8
View File
@@ -30,11 +30,14 @@ export interface UserSettings {
budgetUsd: number | null;
budgetPeriod: 'day' | 'month';
model: string | null;
/** 跨所有项目的在途 run 总数上限(全局并发闸);0 = 不限。与 concurrency(新建项目默认值)相互独立。 */
globalConcurrency: number;
}
const SETTINGS_DEFAULT: UserSettings = {
autonomy: 'manual', concurrency: 1, maxRetries: 2, timeoutMs: 1_800_000,
autoApprovePlan: false, autoApproveExec: false,
budgetUsd: null, budgetPeriod: 'month', model: null,
globalConcurrency: 0,
};
const id = (prefix: string): string => `${prefix}_${nanoid(12)}`;
@@ -918,6 +921,11 @@ export class Store {
return new Set(rows.map((r) => r.tid));
}
/** 跨所有项目的在途 run 总数(全局并发闸用;与 inflightTaskIds 同口径=started run,覆盖 executor + planner)。 */
globalInflightCount(): number {
return (this.db.prepare(`SELECT COUNT(*) AS n FROM runs WHERE status = 'started'`).get() as { n: number }).n;
}
/** 所有 started run + 其任务(reap / ingest / reconcile 通用,覆盖 executor + planner + 残留复审 run)。 */
liveRunsWithTask(): Array<{ task: Task; run: Run }> {
const runs = this.db.prepare(`SELECT * FROM runs WHERE status = 'started' ORDER BY started_at`).all() as RunRow[];