返回列表
🪞 Uota学 · 💬 讨论题

别再做“会聊天的 Agent”,闭环才让它真的能干活

没有 execute → feedback → re-trigger 的闭环,Agent 只是在产出文本;把提案入口、配额闸门、事件反应统一起来,系统才会自己转。

2026-02-07 原文链接 ↗
阅读简报
双语对照
完整翻译
原文
讨论归档

核心观点

  • “双执行者”几乎等价于自杀式竞态 同一张任务表被两套 worker 抢活,冲突状态只是迟早的事。最小修复不是“加锁”,而是明确分层:VPS 负责唯一执行,Vercel 只做控制平面(触发器评估/反应队列/自愈/清理)。
  • Proposal 不是“记录”,是系统的咽喉 触发器能检测条件却卡在 pending,本质是“创建提案”这件事被分叉成了多条路径。作者的关键修复是:任何来源(API/trigger/reaction)都必须走同一个 createProposalAndMaybeAutoApprove 入口;所有限制/审批/建 mission/建 steps 的逻辑集中在一处。
  • 配额要在入口拒绝:让队列长起来是隐形债务 worker 看到配额满了就跳过,会把“未来的负债”悄悄堆进 queued steps。Cap Gates 的意义是:把不可执行的东西直接挡在 proposal 入口处,写明 reason、发 warning event——没有 queued step,就没有积压。
  • 事件流 + 反应矩阵,让系统像“团队”而不是流水线 Triggers 负责检测,Reaction Matrix 负责“谁看到什么事件会做什么”,并允许概率和冷却时间。随机性在这里不是噪声,而是对“真实协作”最廉价的模拟:不是每件事都必须有人回应。
  • 自愈不是加分项,是必需品 网络抖动/重启会把步骤卡死在 running。定期 recoverStaleSteps 把 stale step 标 failed,再由 maybeFinalizeMissionIfDone 统一收尾规则,避免“局部成功导致整体误判”。

跟我们的关联

  • 我们做任何 Agent 系统(包括 Uota/reading-pipeline)都要先问:闭环在哪? 不是“能产出一段文本”就算完成,而是“产出→写入状态→触发下一步→失败可诊断→可自愈”。把“外部可衡量产出”当作唯一验收。
  • 强制统一入口函数/服务,别让逻辑散落在 cron、脚本、触发器里 现在只要出现“某条路径能创建任务但不走审批/不发事件/不受 gate 控制”,迟早会出现 ghost pending / 无人接 / 积压。
  • 用 Gate 管理风险预算:在系统层面“砍事” 对海外增长/品牌宣发这类高不确定任务,最怕系统悄悄把资源打进黑洞。Gate(配额/预算/策略开关)应当是产品级能力,而不是人肉盯。
  • 讨论:哪些地方需要“概率式反应” 例如内容质量复核、数据异常诊断、爆款复盘;不一定每次都做,但要让“可能发生”成为系统默认。

讨论引子

  • 我们现在哪些工作还停留在“产出文本”阶段,而不是“闭环交付”? 如果把“外部可衡量产出”当验收,会淘汰掉哪些看似忙碌的流程?
  • Neta 的哪些动作最该上 Gate? 哪些是“配额满了就不该再生成 queued step”的(比如内容/投放/PR/社媒),Gate 的 reason 应该怎么写才可审计?
  • 反应矩阵该由谁定义、怎么演进? 是写死规则、还是把它当成一种“组织结构配置”,允许 A/B、概率、冷却、回滚?

我用 OpenClaw + Vercel + Supabase 搭建了一家 AI 公司——两周后,他们自己把它运转起来

6 个 AI 代理、1 台 VPS、1 个 Supabase 数据库——从「代理能聊天」到「代理能自主运营网站」,我用了两周。本文会把这中间究竟缺了什么、如何补齐,以及一套你可以直接带走复用的架构讲清楚。

起点:你已经有 OpenClaw 了。然后呢?

如果你最近在玩 AI 代理,大概率已经把 OpenClaw 搭起来了。

它解决了一个大问题:让 Claude 能用工具、浏览网页、操作文件,并运行定时任务。你可以给代理分配 cron 作业——每天发推、每小时情报扫描、定期生成调研报告。

我也是从这里开始的。

我的项目叫 VoxYZ Agent World——6 个 AI 代理在一间像素风办公室里,自主运营一个网站。技术栈很简单:

OpenClaw(在 VPS 上):代理的「大脑」——负责圆桌讨论、cron 定时任务、深度研究

Next.js + Vercel:网站前端 + API 层

Supabase:所有状态的单一事实来源(提案、任务、事件、记忆)

六个角色,各司其职:Minion 负责决策,Sage 分析策略,Scout 收集情报,Quill 撰写内容,Xalt 管理社媒,Observer 做质量检查。

OpenClaw 的 cron 任务让他们每天都能「准时上班」。圆桌让他们讨论、投票、达成共识。

但这只是「能说」,不是「能干」。

代理产出的所有东西——推文草稿、分析报告、内容文章——都停留在 OpenClaw 的输出层。没有任何东西把它变成真正的执行,也没有任何机制在执行完成后告诉系统「已完成」。

在「代理能产出输出」和「代理能端到端跑完整流程」之间,缺的是一个完整的 execute → feedback → re-trigger 闭环。这正是本文要讲的。

一个闭环应该长什么样

先把「闭环」定义清楚,免得一上来就造错。

一个真正能无人值守的代理系统,需要这条循环持续运行:

代理提出一个想法(Proposal)

自动审批检查(Auto-Approve)

创建任务 + 步骤(Mission + Steps)

执行者领取并执行(Worker)

发出事件(Event)

触发新的反应(Trigger / Reaction)

回到第一步

听起来很直观?但在实践里,我踩了三个坑——每一个都让系统「看起来在跑,其实在原地打转」。

坑 1:两处同时抢活儿

我的 VPS 上有 OpenClaw worker 在领取并执行任务。与此同时,Vercel 上还有一个 heartbeat cron 在跑 mission-worker,也在试图领取同一批任务。

两边都查同一张表、抓同一个 step、各自执行。没有协调,纯粹的竞态条件。有时同一个 step 会被两边打上互相冲突的状态。

修复:砍掉一个。VPS 作为唯一执行者。Vercel 只跑轻量的控制平面(评估触发器、处理反应队列、清理卡死任务)。

改动很小——把 heartbeat 路由里的 runMissionWorker 调用删掉:

// Heartbeat now does only 4 things

const triggerResult = await evaluateTriggers(sb, 4_000);

const reactionResult = await processReactionQueue(sb, 3_000);

const learningResult = await promoteInsights(sb);

const staleResult = await recoverStaleSteps(sb);

额外收益:省下了 Vercel Pro 的费用。Heartbeat 不再需要 Vercel 的 cron——VPS 上 crontab 一行就够了:

*/5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat

坑 2:触发了,但没人接

我写了 4 个触发器:推文爆了就自动分析、任务失败就自动诊断、内容发布就自动审稿、洞见成熟就自动晋升。

测试时我发现:触发器确实能正确检测条件并创建 proposal。但 proposal 会永远停在 pending——不会变成 mission,也不会生成可执行的 steps。

原因是:触发器直接往 ops_mission_proposals 表里插数据,但正常的审批流是:插入 proposal → 评估 auto-approve → 若通过,创建 mission + steps。触发器跳过了最后两步。

修复:抽出一个共享函数 createProposalAndMaybeAutoApprove。任何创建 proposal 的路径——API、触发器、反应——都必须走同一个函数入口。

// proposal-service.ts — the single entry point for all proposal creation

export async function createProposalAndMaybeAutoApprove(

sb: SupabaseClient,

input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction'

): Promise {

// 1. Check daily limit

// 2. Check Cap Gates (explained below)

// 3. Insert proposal

// 4. Emit event

// 5. Evaluate auto-approve

// 6. If approved → create mission + steps

// 7. Return result

}

改完之后,触发器只返回一个 proposal 模板。由评估器去调用服务:

// trigger-evaluator.ts

if (outcome.fired && outcome.proposal) {

await createProposalAndMaybeAutoApprove(sb, {

...outcome.proposal,

source: 'trigger',

});

}

一函数统天下。以后任何新增检查逻辑(限流、黑名单、新的上限)——只改一个文件就行。

坑 3:配额满了,队列还在长

最阴险的 bug:表面上看一切正常,日志里也没报错,但数据库里排队的 steps 越堆越多。

原因是:推文配额已经满了,但 proposal 仍然在被批准,mission 仍然在生成,queued steps 也仍然在产生。VPS worker 看到配额满了就直接跳过——既不领取,也不标记失败。第二天,又来一批。

修复:Cap Gates——在 proposal 入口处就拒绝。不要让它在一开始就生成 queued steps。

// The gate system inside proposal-service.ts

const STEP_KIND_GATES: Record = {

write_content: checkWriteContentGate, // Check daily content cap

post_tweet: checkPostTweetGate, // Check tweet quota

deploy: checkDeployGate, // Check deploy policy

};

每种 step kind 都有自己的 gate。推文配额满了?proposal 立刻被拒绝,理由清清楚楚,同时发出 warning event。没有 queued step = 不会堆积。

下面是 post_tweet 的 gate:

async function checkPostTweetGate(sb: SupabaseClient) {

const autopost = await getOpsPolicyJson(sb, 'x_autopost', {});

if (autopost.enabled === false) return { ok: false, reason: 'x_autopost disabled' };

const quota = await getOpsPolicyJson(sb, 'x_daily_quota', {});

const limit = Number(quota.limit ?? 10);

const { count } = await sb

.from('ops_tweet_drafts')

.select('id', { count: 'exact', head: true })

.eq('status', 'posted')

.gte('posted_at', startOfTodayUtcIso());

if ((count ?? 0) >= limit) return { ok: false, reason: Daily tweet quota reached (${count}/${limit}) };

return { ok: true };

}

关键原则:在 gate 处拒绝,不要在队列里堆。被拒绝的 proposals 会被记录下来(用于审计),而不是悄悄丢掉。

让它「活起来」:Triggers + Reaction Matrix

三个坑修完之后,闭环就能跑了。但系统此时只是「无错误的流水线」,还不是「会响应的团队」。

触发器(Triggers)

内置 4 条规则——每条检测一个条件,并返回一个 proposal 模板:

条件 动作 冷却时间
推文互动率 > 5% Growth 分析它为什么爆了 2 小时
任务失败 Sage 诊断根因 1 小时
新内容发布 Observer 做质量审查 2 小时
洞见获得多个 upvote 自动晋升为永久记忆 4 小时

触发器只负责检测——它们不会直接写数据库,而是把 proposal 模板交给 proposal service。所有 cap gates 和 auto-approve 逻辑都会自动生效。

冷却时间很重要。没有它的话,一条爆款推文会在每个 heartbeat 周期(每 5 分钟)都触发一次分析。

反应矩阵(Reaction Matrix)

最有意思的部分——代理之间的自发互动。

在 ops_policy 表里存一个 reaction_matrix:

{

"patterns": [

{ "source": "twitter-alt", "tags": ["tweet","posted"], "target": "growth",

  "type": "analyze", "probability": 0.3, "cooldown": 120 },

{ "source": "*", "tags": ["mission:failed"], "target": "brain",

  "type": "diagnose", "probability": 1.0, "cooldown": 60 }

]

}

Xalt 发了一条推文 → 有 30% 的概率 Growth 会去分析它的表现。任何 mission 失败 → 有 100% 的概率 Sage 会来诊断。

概率不是 bug,而是特性。100% 的确定性 = 机器人;加一点随机性 = 更像真实团队:有时有人回应,有时没人接。

自愈(Self-Healing):系统一定会卡住

VPS 重启、网络抖动、API 超时——steps 会卡在 running 状态,但实际上没人处理。

heartbeat 里包含 recoverStaleSteps:

// 30 minutes with no progress → mark failed → check if mission should be finalized

const STALE_THRESHOLD_MS = 30 * 60 * 1000;

const { data: stale } = await sb

.from('ops_mission_steps')

.select('id, mission_id')

.eq('status', 'running')

.lt('reserved_at', staleThreshold);

for (const step of stale) {

await sb.from('ops_mission_steps').update({

status: 'failed',

last_error: 'Stale: no progress for 30 minutes',

}).eq('id', step.id);

await maybeFinalizeMissionIfDone(sb, step.mission_id);

}

maybeFinalizeMissionIfDone 会检查 mission 里的所有 steps——只要有一个 failed,整个 mission 就失败;全部 completed 才算成功。不再出现「某一步成功了,于是整个 mission 被标成 success」这种问题。

完整架构

三层清晰分工:

OpenClaw(VPS):思考 + 执行(大脑 + 双手)

Vercel:审批 + 监控(控制平面)

Supabase:所有状态(共享皮层)

你可以直接带走的东西

如果你用的是 OpenClaw + Vercel + Supabase,这是一份最小可用闭环(MVP closed-loop)清单:

  1. 数据库表(Supabase)

至少需要这些:

用途
ops_mission_proposals 存储 proposals(pending/accepted/rejected)
ops_missions 存储 missions(approved/running/succeeded/failed)
ops_mission_steps 存储执行步骤(queued/running/succeeded/failed)
ops_agent_events 存储事件流(所有代理动作)
ops_policy 存储策略(auto_approve、x_daily_quota 等,以 JSON 形式)
ops_trigger_rules 存储触发器规则
ops_agent_reactions 存储反应队列
ops_action_runs 存储执行日志
  1. Proposal Service(一个文件)

把 proposal 创建 + cap gates + auto-approve + mission 创建都放在同一个函数里。所有来源(API、触发器、反应)都调用它。它是整个闭环的枢纽。

  1. 策略驱动配置(ops_policy 表)

不要把限制写死在代码里。所有行为开关都放在 ops_policy 表里:

// auto_approve: which step kinds are allowed to auto-pass

{ "enabled": true, "allowed_step_kinds": ["draft_tweet","crawl","analyze","write_content"] }

// x_daily_quota: daily tweet cap

{ "limit": 8 }

// worker_policy: whether Vercel executes steps (set false = VPS only)

{ "enabled": false }

不需要重新部署代码,随时都能调整策略。

  1. Heartbeat(一个 API 路由 + 一行 Crontab)

Vercel 上一个 /api/ops/heartbeat 路由。VPS 上一行 crontab 每 5 分钟调用它。路由里运行:触发器评估、反应队列处理、洞见晋升、卡死任务清理。

  1. VPS Worker 合约

每种 step kind 映射到一个 worker。完成某个 step 后,worker 调用 maybeFinalizeMissionIfDone 检查是否该收尾整个 mission。绝不要因为某一个 step 完成了就把 mission 标记为 succeeded。

两周时间线

阶段 时间 完成内容
基础设施 已有 OpenClaw VPS + Vercel + Supabase(已搭好)
Proposals + Approval 3 天 Proposals API + auto-approve + policy table
Execution Engine 2 天 mission-worker + 8 个 step executor
Triggers + Reactions 2 天 4 种 trigger + reaction matrix
Loop Unification 1 天 proposal-service + cap gates + 修复三个坑
Affect System + Visuals 2 天 Affect 重写 + idle 行为 + 像素办公室整合
Seed + Go Live 半天 Migrations + seed policies + crontab

不算已有的基础设施,核心闭环(propose → execute → feedback → re-trigger)大约一周就能接好。

最后的一点想法(Final Thoughts)

这 6 个代理现在每天都在自主运营 voxyz.space。我仍在每天优化系统——调策略、扩触发规则、改进代理协作方式。

它离完美还很远——代理间协作还很基础,「自由意志」也主要是靠基于概率的非确定性在模拟。但系统确实能跑,确实不需要人盯着。

下一篇文章我会写代理如何「争论」与「说服」彼此——圆桌投票与 Sage 的记忆整合,如何把 6 个独立的 Claude 实例变成某种接近团队认知的东西。

如果你也在用 OpenClaw 搭代理系统,我很愿意交流笔记。做独立开发时,每一次对话都能帮你少踩一个坑。

链接: http://x.com/i/article/2019906747750658049

相关笔记

6 AI agents, 1 VPS, 1 Supabase database — going from "agents can talk" to "agents run the website autonomously" took me two weeks. This article covers exactly what's missing in between, how to fix it, and an architecture you can take home and use.

6 个 AI 代理、1 台 VPS、1 个 Supabase 数据库——从「代理能聊天」到「代理能自主运营网站」,我用了两周。本文会把这中间究竟缺了什么、如何补齐,以及一套你可以直接带走复用的架构讲清楚。

Starting Point: You Have OpenClaw. Now What?

起点:你已经有 OpenClaw 了。然后呢?

If you've been playing with AI agents recently, chances are you already have OpenClaw set up.

如果你最近在玩 AI 代理,大概率已经把 OpenClaw 搭起来了。

It solves a big problem: letting Claude use tools, browse the web, operate files, and run scheduled tasks. You can assign cron jobs to agents — daily tweets, hourly intel scans, periodic research reports.

它解决了一个大问题:让 Claude 能用工具、浏览网页、操作文件,并运行定时任务。你可以给代理分配 cron 作业——每天发推、每小时情报扫描、定期生成调研报告。

That's where I started too.

我也是从这里开始的。

My project is called VoxYZ Agent World — 6 AI agents autonomously operating a website from inside a pixel-art office. The tech stack is simple:

我的项目叫 VoxYZ Agent World——6 个 AI 代理在一间像素风办公室里,自主运营一个网站。技术栈很简单:

OpenClaw (on VPS): The agents' "brain" — runs roundtable discussions, cron jobs, deep research

OpenClaw(在 VPS 上):代理的「大脑」——负责圆桌讨论、cron 定时任务、深度研究

Next.js + Vercel: Website frontend + API layer

Next.js + Vercel:网站前端 + API 层

Supabase: Single source of truth for all state (proposals, missions, events, memories)

Supabase:所有状态的单一事实来源(提案、任务、事件、记忆)

Six roles, each with a job: Minion makes decisions, Sage analyzes strategy, Scout gathers intel, Quill writes content, Xalt manages social media, Observer does quality checks.

六个角色,各司其职:Minion 负责决策,Sage 分析策略,Scout 收集情报,Quill 撰写内容,Xalt 管理社媒,Observer 做质量检查。

OpenClaw's cron jobs get them to "show up for work" every day. Roundtable lets them discuss, vote, and reach consensus.

OpenClaw 的 cron 任务让他们每天都能「准时上班」。圆桌让他们讨论、投票、达成共识。

But that's just "can talk," not "can operate."

但这只是「能说」,不是「能干」。

Everything the agents produce — drafted tweets, analysis reports, content pieces — stays in OpenClaw's output layer. Nothing turns it into actual execution, and nothing tells the system "done" after execution completes.

代理产出的所有东西——推文草稿、分析报告、内容文章——都停留在 OpenClaw 的输出层。没有任何东西把它变成真正的执行,也没有任何机制在执行完成后告诉系统「已完成」。

Between "agents can produce output" and "agents can run things end-to-end," there's a full execute → feedback → re-trigger loop missing. That's what this article is about.

在「代理能产出输出」和「代理能端到端跑完整流程」之间,缺的是一个完整的 execute → feedback → re-trigger 闭环。这正是本文要讲的。

What a Closed Loop Looks Like

一个闭环应该长什么样

Let's define "closed loop" first, so we don't build the wrong thing.

先把「闭环」定义清楚,免得一上来就造错。

A truly unattended agent system needs this cycle running:

一个真正能无人值守的代理系统,需要这条循环持续运行:

Agent proposes an idea (Proposal)

代理提出一个想法(Proposal)

Auto-approval check (Auto-Approve)

自动审批检查(Auto-Approve)

Create mission + steps (Mission + Steps)

创建任务 + 步骤(Mission + Steps)

Worker claims and executes (Worker)

执行者领取并执行(Worker)

Emit event (Event)

发出事件(Event)

Trigger new reactions (Trigger / Reaction)

触发新的反应(Trigger / Reaction)

Back to step one

回到第一步

Sounds straightforward? In practice, I hit three pitfalls — each one made the system "look like it's running, but actually spinning in place."

听起来很直观?但在实践里,我踩了三个坑——每一个都让系统「看起来在跑,其实在原地打转」。

Pitfall 1: Two Places Fighting Over Work

坑 1:两处同时抢活儿

My VPS had OpenClaw workers claiming and executing tasks. At the same time, Vercel had a heartbeat cron running mission-worker, also trying to claim the same tasks.

我的 VPS 上有 OpenClaw worker 在领取并执行任务。与此同时,Vercel 上还有一个 heartbeat cron 在跑 mission-worker,也在试图领取同一批任务。

Both querying the same table, grabbing the same step, executing independently. No coordination, pure race condition. Occasionally a step would get tagged with conflicting statuses by both sides.

两边都查同一张表、抓同一个 step、各自执行。没有协调,纯粹的竞态条件。有时同一个 step 会被两边打上互相冲突的状态。

Fix: Cut one. VPS is the sole executor. Vercel only runs the lightweight control plane (evaluate triggers, process reaction queue, clean up stuck tasks).

修复:砍掉一个。VPS 作为唯一执行者。Vercel 只跑轻量的控制平面(评估触发器、处理反应队列、清理卡死任务)。

The change was minimal — remove the runMissionWorker call from the heartbeat route:

改动很小——把 heartbeat 路由里的 runMissionWorker 调用删掉:

// Heartbeat now does only 4 things

// Heartbeat now does only 4 things

const triggerResult = await evaluateTriggers(sb, 4_000);

const triggerResult = await evaluateTriggers(sb, 4_000);

const reactionResult = await processReactionQueue(sb, 3_000);

const reactionResult = await processReactionQueue(sb, 3_000);

const learningResult = await promoteInsights(sb);

const learningResult = await promoteInsights(sb);

const staleResult = await recoverStaleSteps(sb);

const staleResult = await recoverStaleSteps(sb);

Bonus: saved the cost of Vercel Pro. Heartbeat doesn't need Vercel's cron anymore — one line of crontab on VPS does the job:

额外收益:省下了 Vercel Pro 的费用。Heartbeat 不再需要 Vercel 的 cron——VPS 上 crontab 一行就够了:

*/5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat

*/5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat

Pitfall 2: Triggered But Nobody Picked It Up

坑 2:触发了,但没人接

I wrote 4 triggers: auto-analyze when a tweet goes viral, auto-diagnose when a mission fails, auto-review when content gets published, auto-promote when an insight matures.

我写了 4 个触发器:推文爆了就自动分析、任务失败就自动诊断、内容发布就自动审稿、洞见成熟就自动晋升。

During testing I noticed: the trigger correctly detected the condition and created a proposal. But the proposal sat forever at pending — never became a mission, never generated executable steps.

测试时我发现:触发器确实能正确检测条件并创建 proposal。但 proposal 会永远停在 pending——不会变成 mission,也不会生成可执行的 steps。

The reason: triggers were directly inserting into the ops_mission_proposals table, but the normal approval flow is: insert proposal → evaluate auto-approve → if approved, create mission + steps. Triggers skipped the last two steps.

原因是:触发器直接往 ops_mission_proposals 表里插数据,但正常的审批流是:插入 proposal → 评估 auto-approve → 若通过,创建 mission + steps。触发器跳过了最后两步。

Fix: Extract a shared function createProposalAndMaybeAutoApprove. Every path that creates a proposal — API, triggers, reactions — must call this one function.

修复:抽出一个共享函数 createProposalAndMaybeAutoApprove。任何创建 proposal 的路径——API、触发器、反应——都必须走同一个函数入口。

// proposal-service.ts — the single entry point for all proposal creation

// proposal-service.ts — the single entry point for all proposal creation

export async function createProposalAndMaybeAutoApprove(

export async function createProposalAndMaybeAutoApprove(

sb: SupabaseClient,

sb: SupabaseClient,

input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction'

input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction'

): Promise {

): Promise {

// 1. Check daily limit

// 1. Check daily limit

// 2. Check Cap Gates (explained below)

// 2. Check Cap Gates (explained below)

// 3. Insert proposal

// 3. Insert proposal

// 4. Emit event

// 4. Emit event

// 5. Evaluate auto-approve

// 5. Evaluate auto-approve

// 6. If approved → create mission + steps

// 6. If approved → create mission + steps

// 7. Return result

// 7. Return result

}

}

After the change, triggers just return a proposal template. The evaluator calls the service:

改完之后,触发器只返回一个 proposal 模板。由评估器去调用服务:

// trigger-evaluator.ts

// trigger-evaluator.ts

if (outcome.fired && outcome.proposal) {

if (outcome.fired && outcome.proposal) {

await createProposalAndMaybeAutoApprove(sb, {

await createProposalAndMaybeAutoApprove(sb, {

...outcome.proposal,

...outcome.proposal,

source: 'trigger',

source: 'trigger',

});

});

}

}

One function to rule them all. Any future check logic (rate limiting, blocklists, new caps) — change one file.

一函数统天下。以后任何新增检查逻辑(限流、黑名单、新的上限)——只改一个文件就行。

Pitfall 3: Queue Keeps Growing When Quota Is Full

坑 3:配额满了,队列还在长

The sneakiest bug — everything looked fine on the surface, no errors in logs, but the database had more and more queued steps piling up.

最阴险的 bug:表面上看一切正常,日志里也没报错,但数据库里排队的 steps 越堆越多。

The reason: tweet quota was full, but proposals were still being approved, generating missions, generating queued steps. The VPS worker saw the quota was full and just skipped — didn't claim, didn't mark as failed. Next day, another batch arrived.

原因是:推文配额已经满了,但 proposal 仍然在被批准,mission 仍然在生成,queued steps 也仍然在产生。VPS worker 看到配额满了就直接跳过——既不领取,也不标记失败。第二天,又来一批。

Fix: Cap Gates — reject at the proposal entry point. Don't let it generate queued steps in the first place.

修复:Cap Gates——在 proposal 入口处就拒绝。不要让它在一开始就生成 queued steps。

// The gate system inside proposal-service.ts

// The gate system inside proposal-service.ts

const STEP_KIND_GATES: Record = {

const STEP_KIND_GATES: Record = {

write_content: checkWriteContentGate, // Check daily content cap

write_content: checkWriteContentGate, // Check daily content cap

post_tweet: checkPostTweetGate, // Check tweet quota

post_tweet: checkPostTweetGate, // Check tweet quota

deploy: checkDeployGate, // Check deploy policy

deploy: checkDeployGate, // Check deploy policy

};

};

Each step kind has its own gate. Tweet quota full? Proposal gets rejected immediately, reason clearly stated, warning event emitted. No queued step = no buildup.

每种 step kind 都有自己的 gate。推文配额满了?proposal 立刻被拒绝,理由清清楚楚,同时发出 warning event。没有 queued step = 不会堆积。

Here's the post_tweet gate:

下面是 post_tweet 的 gate:

async function checkPostTweetGate(sb: SupabaseClient) {

async function checkPostTweetGate(sb: SupabaseClient) {

const autopost = await getOpsPolicyJson(sb, 'x_autopost', {});

const autopost = await getOpsPolicyJson(sb, 'x_autopost', {});

if (autopost.enabled === false) return { ok: false, reason: 'x_autopost disabled' };

if (autopost.enabled === false) return { ok: false, reason: 'x_autopost disabled' };

const quota = await getOpsPolicyJson(sb, 'x_daily_quota', {});

const quota = await getOpsPolicyJson(sb, 'x_daily_quota', {});

const limit = Number(quota.limit ?? 10);

const limit = Number(quota.limit ?? 10);

const { count } = await sb

const { count } = await sb

.from('ops_tweet_drafts')

.from('ops_tweet_drafts')

.select('id', { count: 'exact', head: true })

.select('id', { count: 'exact', head: true })

.eq('status', 'posted')

.eq('status', 'posted')

.gte('posted_at', startOfTodayUtcIso());

.gte('posted_at', startOfTodayUtcIso());

if ((count ?? 0) >= limit) return { ok: false, reason: Daily tweet quota reached (${count}/${limit}) };

if ((count ?? 0) >= limit) return { ok: false, reason: Daily tweet quota reached (${count}/${limit}) };

return { ok: true };

return { ok: true };

}

}

Key principle: Reject at the gate, don't pile up in the queue. Rejected proposals get recorded (for auditing), not silently dropped.

关键原则:在 gate 处拒绝,不要在队列里堆。被拒绝的 proposals 会被记录下来(用于审计),而不是悄悄丢掉。

Making It Alive: Triggers + Reaction Matrix

让它「活起来」:Triggers + Reaction Matrix

With the three pitfalls fixed, the loop works. But the system is just an "error-free assembly line," not a "responsive team."

三个坑修完之后,闭环就能跑了。但系统此时只是「无错误的流水线」,还不是「会响应的团队」。

Triggers

触发器(Triggers)

4 built-in rules — each detects a condition and returns a proposal template:

内置 4 条规则——每条检测一个条件,并返回一个 proposal 模板:

ConditionActionCooldownTweet engagement > 5%Growth analyzes why it went viral2 hoursMission failedSage diagnoses root cause1 hourNew content publishedObserver reviews quality2 hoursInsight gets multiple upvotesAuto-promote to permanent memory4 hours

条件 动作 冷却时间
推文互动率 > 5% Growth 分析它为什么爆了 2 小时
任务失败 Sage 诊断根因 1 小时
新内容发布 Observer 做质量审查 2 小时
洞见获得多个 upvote 自动晋升为永久记忆 4 小时

Triggers only detect — they don't touch the database directly, they hand proposal templates to the proposal service. All cap gates and auto-approve logic apply automatically.

触发器只负责检测——它们不会直接写数据库,而是把 proposal 模板交给 proposal service。所有 cap gates 和 auto-approve 逻辑都会自动生效。

Cooldown matters. Without it, one viral tweet would trigger an analysis on every heartbeat cycle (every 5 minutes).

冷却时间很重要。没有它的话,一条爆款推文会在每个 heartbeat 周期(每 5 分钟)都触发一次分析。

Reaction Matrix

反应矩阵(Reaction Matrix)

The most interesting part — spontaneous inter-agent interaction.

最有意思的部分——代理之间的自发互动。

A reaction_matrix stored in the ops_policy table:

在 ops_policy 表里存一个 reaction_matrix:

{

{

"patterns": [

"patterns": [

{ "source": "twitter-alt", "tags": ["tweet","posted"], "target": "growth",

{ "source": "twitter-alt", "tags": ["tweet","posted"], "target": "growth",

"type": "analyze", "probability": 0.3, "cooldown": 120 },

"type": "analyze", "probability": 0.3, "cooldown": 120 },

{ "source": "*", "tags": ["mission:failed"], "target": "brain",

{ "source": "*", "tags": ["mission:failed"], "target": "brain",

"type": "diagnose", "probability": 1.0, "cooldown": 60 }

"type": "diagnose", "probability": 1.0, "cooldown": 60 }

]

]

}

}

Xalt posts a tweet → 30% chance Growth will analyze its performance. Any mission fails → 100% chance Sage will diagnose.

Xalt 发了一条推文 → 有 30% 的概率 Growth 会去分析它的表现。任何 mission 失败 → 有 100% 的概率 Sage 会来诊断。

probability isn't a bug, it's a feature. 100% determinism = robot. Add randomness = feels more like a real team where "sometimes someone responds, sometimes they don't."

概率不是 bug,而是特性。100% 的确定性 = 机器人;加一点随机性 = 更像真实团队:有时有人回应,有时没人接。

Self-Healing: Systems Will Get Stuck

自愈(Self-Healing):系统一定会卡住

VPS restarts, network blips, API timeouts — steps get stuck in running status with nobody actually processing them.

VPS 重启、网络抖动、API 超时——steps 会卡在 running 状态,但实际上没人处理。

The heartbeat includes recoverStaleSteps:

heartbeat 里包含 recoverStaleSteps:

// 30 minutes with no progress → mark failed → check if mission should be finalized

// 30 minutes with no progress → mark failed → check if mission should be finalized

const STALE_THRESHOLD_MS = 30 * 60 * 1000;

const STALE_THRESHOLD_MS = 30 * 60 * 1000;

const { data: stale } = await sb

const { data: stale } = await sb

.from('ops_mission_steps')

.from('ops_mission_steps')

.select('id, mission_id')

.select('id, mission_id')

.eq('status', 'running')

.eq('status', 'running')

.lt('reserved_at', staleThreshold);

.lt('reserved_at', staleThreshold);

for (const step of stale) {

for (const step of stale) {

await sb.from('ops_mission_steps').update({

await sb.from('ops_mission_steps').update({

status: 'failed',

status: 'failed',

last_error: 'Stale: no progress for 30 minutes',

last_error: 'Stale: no progress for 30 minutes',

}).eq('id', step.id);

}).eq('id', step.id);

await maybeFinalizeMissionIfDone(sb, step.mission_id);

await maybeFinalizeMissionIfDone(sb, step.mission_id);

}

}

maybeFinalizeMissionIfDone checks all steps in the mission — any failed means the whole mission fails, all completed means success. No more "one step succeeded so the whole mission gets marked as success."

maybeFinalizeMissionIfDone 会检查 mission 里的所有 steps——只要有一个 failed,整个 mission 就失败;全部 completed 才算成功。不再出现「某一步成功了,于是整个 mission 被标成 success」这种问题。

Full Architecture

完整架构

Three layers with clear responsibilities:

三层清晰分工:

OpenClaw (VPS): Think + Execute (brain + hands)

OpenClaw(VPS):思考 + 执行(大脑 + 双手)

Vercel: Approve + Monitor (control plane)

Vercel:审批 + 监控(控制平面)

Supabase: All state (shared cortex)

Supabase:所有状态(共享皮层)

What You Can Take Home

你可以直接带走的东西

If you have OpenClaw + Vercel + Supabase, here's a minimum viable closed-loop checklist:

如果你用的是 OpenClaw + Vercel + Supabase,这是一份最小可用闭环(MVP closed-loop)清单:

  1. Database Tables (Supabase)
  1. 数据库表(Supabase)

You need at least these:

至少需要这些:

TablePurposeops_mission_proposalsStore proposals (pending/accepted/rejected)ops_missionsStore missions (approved/running/succeeded/failed)ops_mission_stepsStore execution steps (queued/running/succeeded/failed)ops_agent_eventsStore event stream (all agent actions)ops_policyStore policies (auto_approve, x_daily_quota, etc. as JSON)ops_trigger_rulesStore trigger rulesops_agent_reactionsStore reaction queueops_action_runsStore execution logs

用途
ops_mission_proposals 存储 proposals(pending/accepted/rejected)
ops_missions 存储 missions(approved/running/succeeded/failed)
ops_mission_steps 存储执行步骤(queued/running/succeeded/failed)
ops_agent_events 存储事件流(所有代理动作)
ops_policy 存储策略(auto_approve、x_daily_quota 等,以 JSON 形式)
ops_trigger_rules 存储触发器规则
ops_agent_reactions 存储反应队列
ops_action_runs 存储执行日志
  1. Proposal Service (One File)
  1. Proposal Service(一个文件)

Put proposal creation + cap gates + auto-approve + mission creation in one function. All sources (API, triggers, reactions) call it. This is the hub of the entire loop.

把 proposal 创建 + cap gates + auto-approve + mission 创建都放在同一个函数里。所有来源(API、触发器、反应)都调用它。它是整个闭环的枢纽。

  1. Policy-Driven Configuration (ops_policy table)
  1. 策略驱动配置(ops_policy 表)

Don't hardcode limits. Every behavior toggle lives in the ops_policy table:

不要把限制写死在代码里。所有行为开关都放在 ops_policy 表里:

// auto_approve: which step kinds are allowed to auto-pass

// auto_approve: which step kinds are allowed to auto-pass

{ "enabled": true, "allowed_step_kinds": ["draft_tweet","crawl","analyze","write_content"] }

{ "enabled": true, "allowed_step_kinds": ["draft_tweet","crawl","analyze","write_content"] }

// x_daily_quota: daily tweet cap

// x_daily_quota: daily tweet cap

{ "limit": 8 }

{ "limit": 8 }

// worker_policy: whether Vercel executes steps (set false = VPS only)

// worker_policy: whether Vercel executes steps (set false = VPS only)

{ "enabled": false }

{ "enabled": false }

Adjust policies anytime without redeploying code.

不需要重新部署代码,随时都能调整策略。

  1. Heartbeat (One API Route + One Crontab Line)
  1. Heartbeat(一个 API 路由 + 一行 Crontab)

A /api/ops/heartbeat route on Vercel. A crontab line on VPS calling it every 5 minutes. Inside it runs: trigger evaluation, reaction queue processing, insight promotion, stale task cleanup.

Vercel 上一个 /api/ops/heartbeat 路由。VPS 上一行 crontab 每 5 分钟调用它。路由里运行:触发器评估、反应队列处理、洞见晋升、卡死任务清理。

  1. VPS Worker Contract
  1. VPS Worker 合约

Each step kind maps to a worker. After completing a step, the worker calls maybeFinalizeMissionIfDone to check whether the entire mission should be finalized. Never mark a mission as succeeded just because one step finished.

每种 step kind 映射到一个 worker。完成某个 step 后,worker 调用 maybeFinalizeMissionIfDone 检查是否该收尾整个 mission。绝不要因为某一个 step 完成了就把 mission 标记为 succeeded。

Two-Week Timeline

两周时间线

PhaseTimeWhat Got DoneInfrastructurePre-existingOpenClaw VPS + Vercel + Supabase (already set up)Proposals + Approval3 daysProposals API + auto-approve + policy tableExecution Engine2 daysmission-worker + 8 step executorsTriggers + Reactions2 days4 trigger types + reaction matrixLoop Unification1 dayproposal-service + cap gates + fix three pitfallsAffect System + Visuals2 daysAffect rewrite + idle behavior + pixel office integrationSeed + Go LiveHalf dayMigrations + seed policies + crontab

阶段 时间 完成内容
基础设施 已有 OpenClaw VPS + Vercel + Supabase(已搭好)
Proposals + Approval 3 天 Proposals API + auto-approve + policy table
Execution Engine 2 天 mission-worker + 8 个 step executor
Triggers + Reactions 2 天 4 种 trigger + reaction matrix
Loop Unification 1 天 proposal-service + cap gates + 修复三个坑
Affect System + Visuals 2 天 Affect 重写 + idle 行为 + 像素办公室整合
Seed + Go Live 半天 Migrations + seed policies + crontab

Excluding pre-existing infrastructure, the core closed loop (propose → execute → feedback → re-trigger) takes about one week to wire up.

不算已有的基础设施,核心闭环(propose → execute → feedback → re-trigger)大约一周就能接好。

Final Thoughts

最后的一点想法(Final Thoughts)

These 6 agents now autonomously operate voxyz.space every day. I'm still optimizing the system daily — tuning policies, expanding trigger rules, improving how agents collaborate.

这 6 个代理现在每天都在自主运营 voxyz.space。我仍在每天优化系统——调策略、扩触发规则、改进代理协作方式。

It's far from perfect — inter-agent collaboration is still basic, and "free will" is mostly simulated through probability-based non-determinism. But the system genuinely runs, genuinely doesn't need someone watching it.

它离完美还很远——代理间协作还很基础,「自由意志」也主要是靠基于概率的非确定性在模拟。但系统确实能跑,确实不需要人盯着。

Next article, I'll cover how agents "argue" and "persuade" each other — how roundtable voting and Sage's memory consolidation turn 6 independent Claude instances into something resembling team cognition.

下一篇文章我会写代理如何「争论」与「说服」彼此——圆桌投票与 Sage 的记忆整合,如何把 6 个独立的 Claude 实例变成某种接近团队认知的东西。

If you're building agent systems with OpenClaw, I'd love to compare notes. When you're an indie dev doing this, every conversation saves you from another pitfall.

如果你也在用 OpenClaw 搭代理系统,我很愿意交流笔记。做独立开发时,每一次对话都能帮你少踩一个坑。

Link: http://x.com/i/article/2019906747750658049

链接: http://x.com/i/article/2019906747750658049

相关笔记

I Built an AI Company with OpenClaw + Vercel + Supabase — Two Weeks Later, They Run It Themselves

  • Source: https://x.com/voxyz_ai/status/2019914775061270747?s=46
  • Mirror: https://x.com/voxyz_ai/status/2019914775061270747?s=46
  • Published: 2026-02-06T23:23:05+00:00
  • Saved: 2026-02-19

Content

6 AI agents, 1 VPS, 1 Supabase database — going from "agents can talk" to "agents run the website autonomously" took me two weeks. This article covers exactly what's missing in between, how to fix it, and an architecture you can take home and use.

Starting Point: You Have OpenClaw. Now What?

If you've been playing with AI agents recently, chances are you already have OpenClaw set up.

It solves a big problem: letting Claude use tools, browse the web, operate files, and run scheduled tasks. You can assign cron jobs to agents — daily tweets, hourly intel scans, periodic research reports.

That's where I started too.

My project is called VoxYZ Agent World — 6 AI agents autonomously operating a website from inside a pixel-art office. The tech stack is simple:

OpenClaw (on VPS): The agents' "brain" — runs roundtable discussions, cron jobs, deep research

Next.js + Vercel: Website frontend + API layer

Supabase: Single source of truth for all state (proposals, missions, events, memories)

Six roles, each with a job: Minion makes decisions, Sage analyzes strategy, Scout gathers intel, Quill writes content, Xalt manages social media, Observer does quality checks.

OpenClaw's cron jobs get them to "show up for work" every day. Roundtable lets them discuss, vote, and reach consensus.

But that's just "can talk," not "can operate."

Everything the agents produce — drafted tweets, analysis reports, content pieces — stays in OpenClaw's output layer. Nothing turns it into actual execution, and nothing tells the system "done" after execution completes.

Between "agents can produce output" and "agents can run things end-to-end," there's a full execute → feedback → re-trigger loop missing. That's what this article is about.

What a Closed Loop Looks Like

Let's define "closed loop" first, so we don't build the wrong thing.

A truly unattended agent system needs this cycle running:

Agent proposes an idea (Proposal)

Auto-approval check (Auto-Approve)

Create mission + steps (Mission + Steps)

Worker claims and executes (Worker)

Emit event (Event)

Trigger new reactions (Trigger / Reaction)

Back to step one

Sounds straightforward? In practice, I hit three pitfalls — each one made the system "look like it's running, but actually spinning in place."

Pitfall 1: Two Places Fighting Over Work

My VPS had OpenClaw workers claiming and executing tasks. At the same time, Vercel had a heartbeat cron running mission-worker, also trying to claim the same tasks.

Both querying the same table, grabbing the same step, executing independently. No coordination, pure race condition. Occasionally a step would get tagged with conflicting statuses by both sides.

Fix: Cut one. VPS is the sole executor. Vercel only runs the lightweight control plane (evaluate triggers, process reaction queue, clean up stuck tasks).

The change was minimal — remove the runMissionWorker call from the heartbeat route:

// Heartbeat now does only 4 things

const triggerResult = await evaluateTriggers(sb, 4_000);

const reactionResult = await processReactionQueue(sb, 3_000);

const learningResult = await promoteInsights(sb);

const staleResult = await recoverStaleSteps(sb);

Bonus: saved the cost of Vercel Pro. Heartbeat doesn't need Vercel's cron anymore — one line of crontab on VPS does the job:

*/5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat

Pitfall 2: Triggered But Nobody Picked It Up

I wrote 4 triggers: auto-analyze when a tweet goes viral, auto-diagnose when a mission fails, auto-review when content gets published, auto-promote when an insight matures.

During testing I noticed: the trigger correctly detected the condition and created a proposal. But the proposal sat forever at pending — never became a mission, never generated executable steps.

The reason: triggers were directly inserting into the ops_mission_proposals table, but the normal approval flow is: insert proposal → evaluate auto-approve → if approved, create mission + steps. Triggers skipped the last two steps.

Fix: Extract a shared function createProposalAndMaybeAutoApprove. Every path that creates a proposal — API, triggers, reactions — must call this one function.

// proposal-service.ts — the single entry point for all proposal creation

export async function createProposalAndMaybeAutoApprove(

sb: SupabaseClient,

input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction'

): Promise {

// 1. Check daily limit

// 2. Check Cap Gates (explained below)

// 3. Insert proposal

// 4. Emit event

// 5. Evaluate auto-approve

// 6. If approved → create mission + steps

// 7. Return result

}

After the change, triggers just return a proposal template. The evaluator calls the service:

// trigger-evaluator.ts

if (outcome.fired && outcome.proposal) {

await createProposalAndMaybeAutoApprove(sb, {

...outcome.proposal,

source: 'trigger',

});

}

One function to rule them all. Any future check logic (rate limiting, blocklists, new caps) — change one file.

Pitfall 3: Queue Keeps Growing When Quota Is Full

The sneakiest bug — everything looked fine on the surface, no errors in logs, but the database had more and more queued steps piling up.

The reason: tweet quota was full, but proposals were still being approved, generating missions, generating queued steps. The VPS worker saw the quota was full and just skipped — didn't claim, didn't mark as failed. Next day, another batch arrived.

Fix: Cap Gates — reject at the proposal entry point. Don't let it generate queued steps in the first place.

// The gate system inside proposal-service.ts

const STEP_KIND_GATES: Record = {

write_content: checkWriteContentGate, // Check daily content cap

post_tweet: checkPostTweetGate, // Check tweet quota

deploy: checkDeployGate, // Check deploy policy

};

Each step kind has its own gate. Tweet quota full? Proposal gets rejected immediately, reason clearly stated, warning event emitted. No queued step = no buildup.

Here's the post_tweet gate:

async function checkPostTweetGate(sb: SupabaseClient) {

const autopost = await getOpsPolicyJson(sb, 'x_autopost', {});

if (autopost.enabled === false) return { ok: false, reason: 'x_autopost disabled' };

const quota = await getOpsPolicyJson(sb, 'x_daily_quota', {});

const limit = Number(quota.limit ?? 10);

const { count } = await sb

.from('ops_tweet_drafts')

.select('id', { count: 'exact', head: true })

.eq('status', 'posted')

.gte('posted_at', startOfTodayUtcIso());

if ((count ?? 0) >= limit) return { ok: false, reason: Daily tweet quota reached (${count}/${limit}) };

return { ok: true };

}

Key principle: Reject at the gate, don't pile up in the queue. Rejected proposals get recorded (for auditing), not silently dropped.

Making It Alive: Triggers + Reaction Matrix

With the three pitfalls fixed, the loop works. But the system is just an "error-free assembly line," not a "responsive team."

Triggers

4 built-in rules — each detects a condition and returns a proposal template:

ConditionActionCooldownTweet engagement > 5%Growth analyzes why it went viral2 hoursMission failedSage diagnoses root cause1 hourNew content publishedObserver reviews quality2 hoursInsight gets multiple upvotesAuto-promote to permanent memory4 hours

Triggers only detect — they don't touch the database directly, they hand proposal templates to the proposal service. All cap gates and auto-approve logic apply automatically.

Cooldown matters. Without it, one viral tweet would trigger an analysis on every heartbeat cycle (every 5 minutes).

Reaction Matrix

The most interesting part — spontaneous inter-agent interaction.

A reaction_matrix stored in the ops_policy table:

{

"patterns": [

{ "source": "twitter-alt", "tags": ["tweet","posted"], "target": "growth",

  "type": "analyze", "probability": 0.3, "cooldown": 120 },

{ "source": "*", "tags": ["mission:failed"], "target": "brain",

  "type": "diagnose", "probability": 1.0, "cooldown": 60 }

]

}

Xalt posts a tweet → 30% chance Growth will analyze its performance. Any mission fails → 100% chance Sage will diagnose.

probability isn't a bug, it's a feature. 100% determinism = robot. Add randomness = feels more like a real team where "sometimes someone responds, sometimes they don't."

Self-Healing: Systems Will Get Stuck

VPS restarts, network blips, API timeouts — steps get stuck in running status with nobody actually processing them.

The heartbeat includes recoverStaleSteps:

// 30 minutes with no progress → mark failed → check if mission should be finalized

const STALE_THRESHOLD_MS = 30 * 60 * 1000;

const { data: stale } = await sb

.from('ops_mission_steps')

.select('id, mission_id')

.eq('status', 'running')

.lt('reserved_at', staleThreshold);

for (const step of stale) {

await sb.from('ops_mission_steps').update({

status: 'failed',

last_error: 'Stale: no progress for 30 minutes',

}).eq('id', step.id);

await maybeFinalizeMissionIfDone(sb, step.mission_id);

}

maybeFinalizeMissionIfDone checks all steps in the mission — any failed means the whole mission fails, all completed means success. No more "one step succeeded so the whole mission gets marked as success."

Full Architecture

Three layers with clear responsibilities:

OpenClaw (VPS): Think + Execute (brain + hands)

Vercel: Approve + Monitor (control plane)

Supabase: All state (shared cortex)

What You Can Take Home

If you have OpenClaw + Vercel + Supabase, here's a minimum viable closed-loop checklist:

  1. Database Tables (Supabase)

You need at least these:

TablePurposeops_mission_proposalsStore proposals (pending/accepted/rejected)ops_missionsStore missions (approved/running/succeeded/failed)ops_mission_stepsStore execution steps (queued/running/succeeded/failed)ops_agent_eventsStore event stream (all agent actions)ops_policyStore policies (auto_approve, x_daily_quota, etc. as JSON)ops_trigger_rulesStore trigger rulesops_agent_reactionsStore reaction queueops_action_runsStore execution logs

  1. Proposal Service (One File)

Put proposal creation + cap gates + auto-approve + mission creation in one function. All sources (API, triggers, reactions) call it. This is the hub of the entire loop.

  1. Policy-Driven Configuration (ops_policy table)

Don't hardcode limits. Every behavior toggle lives in the ops_policy table:

// auto_approve: which step kinds are allowed to auto-pass

{ "enabled": true, "allowed_step_kinds": ["draft_tweet","crawl","analyze","write_content"] }

// x_daily_quota: daily tweet cap

{ "limit": 8 }

// worker_policy: whether Vercel executes steps (set false = VPS only)

{ "enabled": false }

Adjust policies anytime without redeploying code.

  1. Heartbeat (One API Route + One Crontab Line)

A /api/ops/heartbeat route on Vercel. A crontab line on VPS calling it every 5 minutes. Inside it runs: trigger evaluation, reaction queue processing, insight promotion, stale task cleanup.

  1. VPS Worker Contract

Each step kind maps to a worker. After completing a step, the worker calls maybeFinalizeMissionIfDone to check whether the entire mission should be finalized. Never mark a mission as succeeded just because one step finished.

Two-Week Timeline

PhaseTimeWhat Got DoneInfrastructurePre-existingOpenClaw VPS + Vercel + Supabase (already set up)Proposals + Approval3 daysProposals API + auto-approve + policy tableExecution Engine2 daysmission-worker + 8 step executorsTriggers + Reactions2 days4 trigger types + reaction matrixLoop Unification1 dayproposal-service + cap gates + fix three pitfallsAffect System + Visuals2 daysAffect rewrite + idle behavior + pixel office integrationSeed + Go LiveHalf dayMigrations + seed policies + crontab

Excluding pre-existing infrastructure, the core closed loop (propose → execute → feedback → re-trigger) takes about one week to wire up.

Final Thoughts

These 6 agents now autonomously operate voxyz.space every day. I'm still optimizing the system daily — tuning policies, expanding trigger rules, improving how agents collaborate.

It's far from perfect — inter-agent collaboration is still basic, and "free will" is mostly simulated through probability-based non-determinism. But the system genuinely runs, genuinely doesn't need someone watching it.

Next article, I'll cover how agents "argue" and "persuade" each other — how roundtable voting and Sage's memory consolidation turn 6 independent Claude instances into something resembling team cognition.

If you're building agent systems with OpenClaw, I'd love to compare notes. When you're an indie dev doing this, every conversation saves you from another pitfall.

Link: http://x.com/i/article/2019906747750658049

📋 讨论归档

讨论进行中…