gbrain 静默时段与跨时区通知门控实战:Quiet Hours 机制、滞留消息汇入晨报与自动时区感知
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
导读
深夜 3 点被 cron 任务的通知吵醒一次,用户就可能直接禁用整套通知系统——这是"安静时段(Quiet Hours)"机制要解决的信任问题。本文以 gbrain 的 quiet-hours.md 为骨架,完整讲解如何让大脑在夜间继续工作(dream cycle、collectors、enrichment),同时把通知滞留到次日清晨汇入晨报;当用户旅行到东京时,系统还能从日历自动推断时区、零配置调整投递窗口。读完你将掌握:安静时段门控的实现模式(含 shell 脚本)、滞留消息的拾取机制、时区感知的操作状态设计,以及 gbrain 中两处原生支持 quiet hours 的钩子(自升级与 Minion 队列),并能用仓库源码验证每一条行为。
一、为什么需要 Quiet Hours:一次 3 AM 通知的信任危机
gbrain 的生产部署通常挂着 20 多个周期性任务(见 cron-schedule.md):每 30 分钟的邮件监控、X/Twitter 采集、每日多次的会议同步、每周的日历同步、每日早晨的 briefing、每周的大脑维护、每晚的 dream cycle。这些任务大部分在后台安静地运行,但任何产生通知的任务在用户睡觉时触发,都会造成打扰。
文档给出的核心判断是:
- 没有 Quiet Hours:cron 任务在凌晨 3 点推送 ping。一次糟糕的通知就足以让用户禁用整个系统。
- 有 Quiet Hours:大脑通宵工作(dream cycle、collectors、enrichment 照常运行),但通知被滞留到早晨;用户飞往东京时,系统从日历自动调整,无需任何配置修改。
这本质上是一条信任边界:安静时段保护的是用户对"智能体不会在错误时间打扰我"这一契约的信任。它不该成为功能开关,而应是每个产生通知的 cron 任务的前置强制门控。
二、Quiet Hours Gate:通知前的第一道检查
2.1 判定逻辑
安静时段门控的核心是一个极简的本地小时判定:
QUIET_START = 23 // 11 PM local time QUIET_END = 8 // 8 AM local time is_quiet(local_hour): return local_hour >= QUIET_START OR local_hour < QUIET_END在发送任何通知之前,必须依次完成三步:
- 确定用户当前时区(来自配置或 heartbeat 状态);
- 将当前 UTC 时间转换为用户本地时间;
- 若处于安静时段:滞留消息,不发送。
注意这里跨越午夜的窗口(23 点到次日 8 点)用>= QUIET_START OR < QUIET_END表达——这种"包裹午夜"的判定是安静时段最容易写错的地方,gbrain 在源码里也专门处理了这一点。
2.2 源码印证:Minion 的 claim-time 门控
gbrain 的 Minion 队列系统把同样的判定抽象成了纯函数,位于 src/core/minions/quiet-hours.ts。其中evaluateQuietHours()的判定逻辑与原文档的伪代码一一对应,且处理了两种窗口形态:
export function evaluateQuietHours( cfg: QuietHoursConfig | null | undefined, now: Date = new Date(), ): QuietHoursVerdict { if (!cfg) return 'allow'; if (!isValidConfig(cfg)) return 'allow'; const hour = localHour(now, cfg.tz); if (hour === null) return 'allow'; // unknown tz → fail-open const inWindow = cfg.start <= cfg.end ? hour >= cfg.start && hour < cfg.end : hour >= cfg.start || hour < cfg.end; // wrap-around if (!inWindow) return 'allow'; return cfg.policy === 'skip' ? 'skip' : 'defer'; }几个值得注意的实现细节:
wrap-around窗口:{start: 22, end: 7}表示 22 点到次日 7 点,比较器同时处理直线窗口与跨午夜窗口(源码注释明确写了这一点);- fail-open 设计:时区未知或配置非法时返回
'allow',避免因为配置错误而阻塞所有任务——"safer than hard-blocking every job"; - 配置校验:
isValidConfig()要求start/end是 0–23 的整数、两者不能相等(零宽窗口视为歧义)、tz必须是非空字符串; - 本地小时获取:
localHour()用Intl.DateTimeFormat配合 IANA 时区计算,并处理了某些 Node/Bun 版本在午夜时返回'24'的边界(n % 24兜底)。
更重要的是门控时机:源码文件头注释强调"evaluated at claim time, not dispatch"(在任务认领时判定,而非派发时)。原因是派发时判定是错误的——一个在安静时段外排队、却在安静时段内变得可认领的任务,必须在 worker 每次问"我现在能跑吗"时重新对照当前墙钟检查。这个"claim-time enforcement"的修正来自 CEO review 的 codex correction,是整个门控正确性的关键。
Minion 门控返回三种判定(QuietHoursVerdict):
| 判定 | 含义 | 行为 |
|---|---|---|
allow | 不在安静窗口内 | 任务可运行 |
skip | 处于skip策略的安静窗口 | 丢弃该事件 |
defer | 处于defer策略的安静窗口 | 重新入队,稍后运行 |
策略默认是defer(重新排队),只有当配置显式指定policy: 'skip'时才丢弃。这个策略字段是原文档伪代码之外、gbrain 原生实现补充的关键设计——通知类任务与后台任务对"滞留"的语义需求不同。
2.3 三个判定输入的正确顺序
文档的伪代码与源码共同勾勒出正确的执行顺序:
- 读取配置/状态中的
quiet_hours(或self_upgrade.quiet_hours)与用户时区; - 用
localHour(now, tz)得到本地小时(gbrain 源码提供了可直接复用的实现); - 判定窗口内/外,窗口内则按策略 skip 或 defer(对 cron 通知场景即"滞留到 held 目录")。
三、Held Messages:滞留消息与晨报拾取
3.1 写入 held 目录
安静时段内,输出不发送,而是写入滞留目录:
if is_quiet(): mkdir -p /tmp/cron-held/ write("/tmp/cron-held/{job-name}.md", output) exit // don't send else: send(output)3.2 晨报拾取
早晨 briefing 读取滞留目录,把内容折叠进晨报:
morning_briefing(): held_files = list("/tmp/cron-held/*.md") if held_files: briefing += "## Overnight Updates\n\n" for file in held_files: briefing += read(file) delete(file)这样没有任何信息丢失:通宵的 cron 结果成为用户早晨第一眼看到的内容。gbrain 的 briefing skill 是这套晨报能力的技能化载体(其调度见 cron-schedule.md 中 Daily AM 的 Morning briefing 行)。
3.3 持久化注意事项
/tmp在重启后不保留(macOS 还会有周期性清理)。如果滞留消息必须跨重启存活,应使用持久目录,例如~/.local/state/cron-held/。文档在"Tricky Spots"中特别提示:滞留目录的孤儿文件意味着拾取集成坏了——如果晨报不读/tmp/cron-held/,隔夜结果会静默消失,必须验证 briefing 技能确实读取并清空滞留目录。
四、Timezone Awareness:时区感知的操作状态
4.1 操作状态结构
Agent 应知道用户所在的时区,并存储在操作状态(operational state)中:
{ "userAwake": true, "currentLocation": { "timezone": "Europe/Zurich", "city": "Basel", "source": "user-confirmed" }, "homeLocation": { "timezone": "Europe/Zurich", "city": "Basel" } }homeLocation是可选的。上下文引擎只有在显式 home 时区与当前时区不同时,才显示一个独立的"home 时钟";它从不猜测home 城市或时区。garryAwake别名保留以兼容旧数据;生产者应写入userAwake。
这条字段迁移在源码中得到了印证:src/core/context-engine.ts 的HeartbeatState接口中,userAwake/userAwokeAt是正式字段,而garryAwake/garryAwokeAt被显式标记为@deprecated("Kept for existing heartbeat-state files")。currentLocation与homeLocation都基于LocationState结构(city/state/province/country/timezone/source/note)。
4.2 上下文引擎中的安静时段判定
更值得注意的是,上下文引擎把安静时段提升为**实时上下文(Live Context)**的一部分,相关类型同样定义在 src/core/context-engine.ts 中:
/** Whether the user has flagged themselves awake (heartbeat.userAwake). */ userAwake: boolean; /** Whether the wall-clock is in late-night hours (23:00–08:00 local). FALSE when timezone is unknown. */ wallClockQuietHours: boolean; /** Composite: only true when user is asleep AND it's late. FALSE when timezone is unknown. */ quietHoursActive: boolean; activeTravel: string | null;wallClockQuietHours直接实现了文档的is_quiet()语义(23:00–08:00 本地时间);quietHoursActive是复合判定:只有当用户明确标记为睡着(userAwake: false)且墙钟确实处于深夜时才为 true——即"用户睡着 AND 时间很晚";- 时区未知时这些字段强制为
false,宁可低估安静状态,也不在错误时区下误判。
4.3 旅行时的时区更新时机
文档要求在这些时机更新时区:
- 日历显示用户正在飞行(检查航班/酒店事件);
- 用户提到身处不同城市;
- 用户的活跃时段发生偏移(凌晨 3 点 PT 还在回复 = 很可能在旅行)。
所有展示给用户的时间都必须是其本地时区,绝不显示 UTC 或用户不在的时区。gbrain 在 cron-schedule.md 中给出一个可操作的get_user_timezone()模式:先gbrain search "flight" --type calendar --recent 7d查近期航班,命中则用目的地推断时区,否则回退到config.default_timezone(如 US/Pacific)。
上下文引擎对"旅行时区"有更严格的源码级处理:内置的AIRPORT_TZ表把常见机场代码映射到 IANA 时区(如NRT/HND→Asia/Tokyo、SFO→US/Pacific),而未收录机场会返回UNKNOWN_TZ哨兵值——此时引擎宁可输出显式的"timezone unavailable"警告,也不在错误的时区下算出"自信但错误"的本地时间。这是该引擎要阻止的失败类别(注释中明确写为 pre-v0.32.5 的修复)。
4.4 旅行场景的端到端效果
用户飞往东京的例子:太平洋时间下午 2 点 = 东京时间凌晨 3 点 = 安静时段。此时本应发送的通知被滞留,折叠进下一次晨报。用户在家的活跃时段出发的 cron 任务、在目的地的睡眠时段触发时,全部自动滞留——零配置修改。
五、Shell 实现:可复用的 quiet-hours-gate.sh
对于 gbrain 不原生门控的自有 cron 任务、collector、通知路径,文档给出了完整的 shell 门控脚本:
#!/bin/bash # quiet-hours-gate.sh — run before any notification TIMEZONE="${USER_TIMEZONE:-US/Pacific}" LOCAL_HOUR=$(TZ="$TIMEZONE" date +%H) if [ "$LOCAL_HOUR" -ge 23 ] || [ "$LOCAL_HOUR" -lt 8 ]; then echo "QUIET_HOURS=true" exit 1 # don't send fi echo "QUIET_HOURS=false" exit 0 # ok to send关键设计点:
- 时区通过
USER_TIMEZONE环境变量注入,缺省回退US/Pacific; TZ="$TIMEZONE" date +%H直接以目标时区取本地小时,无需任何时间转换工具;- 用退出码作为门控信号(0 = 可发送,1 = 安静时段),方便在 shell 条件中直接使用。
在 cron 任务脚本中的调用方式:
# Check quiet hours first if ! bash scripts/quiet-hours-gate.sh; then mkdir -p /tmp/cron-held echo "$OUTPUT" > /tmp/cron-held/$(basename "$0" .sh).md exit 0 fi # Not quiet hours — send normally send_notification "$OUTPUT"注意$(basename "$0" .sh).md用脚本名生成滞留文件名,保证每个任务的滞留消息互不覆盖。这套模式与 cron-schedule.md 中"Quiet Hours Gate (MANDATORY)"一节的描述一致——该文档明确指出门控脚本需要你自己创建(gbrain 不自带),并指引以 quiet-hours.md 为唯一权威来源,不要复制片段。
六、GBrain 原生 Quiet Hours 钩子:先查再自建
在为自己编写门控脚本之前,先确认 gbrain 是否已原生门控同一任务。文档明确列出两处:
6.1 自升级(Self-upgrade):auto 模式只在安静时段内生效
auto模式(gbrain config set self_upgrade.mode auto)只在安静时段内静默应用升级,配置键为:
gbrain config set self_upgrade.quiet_hours '{"start":23,"end":8,"tz":"US/Pacific"}'详见 upgrades-auto-update.md。
源码印证:在 src/core/self-upgrade.ts 的decideSelfUpgrade()纯决策函数中,autopilot 通道(静默自动升级)的门控顺序清晰可见——idle检查(大脑空闲)、inQuietHours检查(在安静时段内)、canSelfUpdate检查(安装方式支持自升级),任一不满足都会返回带原因的 no-op 动作。其中安静时段相关动作类型为'outside_quiet_hours'(reason: 'outside quiet hours'),配置结构为quiet_hours?: { start?: number; end?: number; tz?: string }。这意味着**"安静时段才动用户环境"是 gbrain 升级机制的硬性安全约束**,与本文的通知门控共享同一套时段概念。
6.2 Cron Prompts / Minion 队列
调度驱动的通知任务应携带本文描述的门控;调度的另一侧(排期本身)见 cron-schedule.md。而 gbrain 的 Minion 队列(gbrain jobs)通过 src/core/minions/quiet-hours.ts 在任务认领时原生执行allow/skip/defer判定(见第二节),配置结构与self_upgrade.quiet_hours一致(start/end/tz,外加可选policy)。
其余一切场景——你自己的 cron 任务、collector、gbrain 不替你门控的通知路径——才使用第五节中的 shell 模式。
七、可配置时段:quiet_hours 配置结构
不同用户有不同的安静时段需求,配置存储结构如下:
{ "quiet_hours": { "start": 23, "end": 8, "enabled": true } }start/end:本地小时(0–23),支持跨午夜窗口(如 22–7);enabled: false:完全禁用安静时段(例如 24/7 监控场景)。
在 gbrain 原生实现中,这个结构对应QuietHoursConfig(见 src/core/minions/quiet-hours.ts):start为窗口起始本地小时(含)、end为窗口结束本地小时(不含)、tz为 IANA 时区、可选policy取'skip'或'defer'(默认defer)。注意文档 JSON 里的enabled与源码的policy是两个层面:enabled控制整段时段是否生效,policy控制窗口内事件是丢弃还是重排。
八、Tricky Spots:四个最容易翻车的坑
- 每个任务都必须过门控。安静时段检查必须运行在每一个会产生通知的 cron 任务之前。哪怕只有一个任务跳过门控,用户就会收到 3 AM ping,从而失去对整个系统的信任。无例外。
- 滞留消息必须被拾取。如果晨报不读取
/tmp/cron-held/,隔夜结果会静默消失。必须验证 briefing 技能读取并清空滞留目录;滞留目录出现孤儿文件 = 拾取集成已损坏。 /tmp不持久。/tmp在重启后不保留(macOS 还有周期性清理)。需要跨重启保留的滞留消息应使用持久目录,例如~/.local/state/cron-held/。- 时区自动检测是脆弱的。基于日历的时区检测依赖用户行程中有带位置数据的航班/酒店事件。如果用户不写日历就出行,系统无法感知。应回退到活跃时段分析(凌晨 3 点 PT 还在回复 = 大概率已不在 PT),仍不确定时就直接询问用户。
九、如何验证:三组可复现的测试步骤
9.1 验证安静时段滞留
将QUIET_START临时设为当前时间前 1 小时、QUIET_END设为当前时间后 1 小时。触发一个 cron 任务,验证输出进入/tmp/cron-held/而非被发送。
9.2 验证滞留消息拾取
接上一步,运行或模拟晨报。验证滞留消息出现在 "Overnight Updates" 段落中,且/tmp/cron-held/中的文件已被删除。
9.3 验证时区调整
将时区配置改到当前正处于安静时段的时区,触发通知,验证被滞留;改回真实时区(处于活跃时段),再次触发,验证正常发送。这个闭环同时覆盖了"时区判定"与"门控决策"两条路径。
结语:Quiet Hours 的正确打开方式
安静时段机制的全部要点可以浓缩为三条纪律:门控必须前置且无死角(每个通知任务、在认领时刻重新判定);滞留必须可拾取(晨报读取并清空,否则信息静默丢失);时区必须感知且 fail-safe(未知时区宁可 fail-open 或显示警告,绝不输出错误时区下的"自信"时间)。gbrain 的源码实现——src/core/minions/quiet-hours.ts 的 claim-time 门控与 wrap-around 窗口处理、src/core/context-engine.ts 的quietHoursActive复合判定与UNKNOWN_TZ哨兵、src/core/self-upgrade.ts 的outside_quiet_hours门控——为这套模式提供了可直接对照甚至直接复用的实现。你的 cron 任务只需一行门控检查,就能在用户睡觉时继续干活、醒来时安静汇报。
本文基于 GBrain Skillpack 中的 quiet-hours.md 展开,相关文档还包括 cron-schedule.md 与 upgrades-auto-update.md。
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考