feat(backthink): 安全域穿透安静时段 + 长静默不拦心跳 + 启动即思考(裁决 1.8/07.4.2/07.4.3/07.4.4/07.4.5)
执行方案阶段 2 · P2-2(`DevDocs/audit/_plan.md`)。四条裁决: - 1.8/07.4.2:`think()` 不再走忽略 importance 的 `disturbAllowed`(已删),**接回 `Decide` 级联**; 安静时段只挡常规推送、**critical 安全域穿透**;`DecisionInput` 增 `ConclusionPush`/`Heartbeat`。 睡眠仍绝对不打扰(裁决只豁免安静时段,此点已在设计与报告写明)。 - 07.4.3:长静默(>2h)分支对**心跳类**放行(常规事件仍需 ≥high),刚聊过天 10min 内扣留不变。 - 07.4.4:`tickLoop` **启动立即先思考一轮** + 拟人化启动消息;启动轮与心跳**同门同限频** (重启风暴不会刷屏),ctx 已取消则跳过。 - 07.4.5:核对一致(`THINK_INTERVAL_SEC` 默认 900s 与 design/20 §0 权威口径一致)⇒ **代码零改动**。 验证:go build 绿;backthink 包 vet/测试绿;`-count=20`(抗 flake)+ `-race` 绿; linux/amd64 + darwin/arm64 交叉编译绿;gofmt 干净;文件 ≤500 行、无 >80 行函数。 变异验证(三条,均带还原证明):① 安静时段改回"全拦" → 安全域穿透断言 FAIL(首次只删 critical 分支时仍绿——`critical >= high` 会从 high 分支漏过,故真实变异必须是"全拦"); ② 删心跳豁免 → 长静默心跳断言 FAIL;③ 删启动思考 → 启动即思考断言 FAIL。 未验证:无真实模型凭据 ⇒ 启动轮问候与 `[我决定:…]` 解析仅经 mock + 真 loop 验证,端到端 LLM 未验。
This commit is contained in:
1 parent
a35bcb8aa9
commit
afef75f640
7 files changed
+468
-176
No files matched your search
@@ -9,9 +9,13 @@
|
||||
// - Asynchronous events on the in-process bus (iot.device_update,
|
||||
// reminder.trigger, system.event) that wake a thinking pass.
|
||||
//
|
||||
// Each pass produces a conclusion, then consults Decide to judge whether the user
|
||||
// should be interrupted at all (event importance / time-of-day / user state such
|
||||
// as heart rate, sleep, work, busy / boredom). When it is worth interrupting the
|
||||
// Each pass produces a conclusion whose structured tail decides whether she
|
||||
// wants to speak at all (design/20 §2 P2), then consults Decide — the importance
|
||||
// cascade restored as the production decision path — to judge whether now is an
|
||||
// acceptable moment (event importance / time-of-day / user state such as heart
|
||||
// rate, sleep, work, busy). A critical safety event punches through work/busy
|
||||
// and quiet hours (裁决 07.4.2), and a heartbeat pass is never held by a
|
||||
// long-silent activity gap (裁决 07.4.3). When it is worth interrupting the
|
||||
// push goes to the active client first and falls back to the IM bridge. The
|
||||
// proactive-push rate gate of DevDocs/design/11 §6 governs the periodic heartbeat
|
||||
// only; an event-driven push bypasses it (owner mandate: "该发就发") subject to
|
||||
@@ -308,10 +312,21 @@ func (b *Backthink) shouldThink(tr Trigger) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// tickLoop is the heartbeat goroutine. It fires one thinking pass per tick and
|
||||
// exits when ctx is cancelled.
|
||||
// tickLoop is the heartbeat goroutine. Per 裁决 07.4.4 (design/12 §1) it thinks
|
||||
// once immediately on process start — producing a humanized startup greeting —
|
||||
// before settling into one thinking pass per tick; it exits when ctx is
|
||||
// cancelled. The accepted side effect is that every process restart wakes one
|
||||
// immediate thinking round.
|
||||
func (b *Backthink) tickLoop(ctx context.Context) {
|
||||
defer b.wg.Done()
|
||||
// 启动即思考:不等满一个心跳周期。已取消的上下文直接退出,不落一条失败的
|
||||
// 启动记录。
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
b.think(ctx, startupTrigger())
|
||||
ticker := time.NewTicker(b.cfg.Interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
@@ -348,24 +363,35 @@ func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
return
|
||||
}
|
||||
|
||||
// P2 (design/20 §2): her own conclusion decides whether to push and to which
|
||||
// channel — not the trigger's importance. parseProactive recovers the decision
|
||||
// robustly and fails safe (malformed/ambiguous → no push).
|
||||
// P2 (design/20 §2): her own conclusion decides whether she wants to speak
|
||||
// and to which channel. parseProactive recovers the decision robustly and
|
||||
// fails safe (malformed/ambiguous → no push).
|
||||
message, channelHint, push := parseProactive(conclusion)
|
||||
|
||||
state := b.currentState(cctx)
|
||||
lastActivity, hasActivity := b.lastUserActivity(cctx)
|
||||
// The "do not disturb" gate (asleep/working/busy/quiet) still applies as a hard
|
||||
// safety; the activity gate (fresh conversation / long idle) softens it; the
|
||||
// frequency gate (PushGate) is applied inside the pusher.
|
||||
shouldPush := push && b.disturbAllowed(at, state) && b.activityAllowed(tr.Import, lastActivity, hasActivity, at)
|
||||
// 裁决 07.4.2:importance 级联恢复为生产决策路径——她的结论回答「想不想说」,
|
||||
// Decide 只判「此刻能不能说」:critical 安全事件穿透工作/忙碌/安静时段,安静
|
||||
// 时段只挡常规推送;心跳类触发(含启动轮)在长期静默段不拦(裁决 07.4.3)。
|
||||
// 频率门(PushGate)仍在推送器内部单独生效。
|
||||
decision := b.Decide(DecisionInput{
|
||||
Trigger: tr.Source,
|
||||
Import: tr.Import,
|
||||
At: at,
|
||||
UserState: state,
|
||||
LastActivity: lastActivity,
|
||||
HasActivity: hasActivity,
|
||||
ConclusionPush: push,
|
||||
Heartbeat: tr.Kind == TriggerHeartbeat,
|
||||
})
|
||||
shouldPush := decision.ShouldPush
|
||||
|
||||
recordConclusion := message
|
||||
if recordConclusion == "" {
|
||||
recordConclusion = conclusion
|
||||
}
|
||||
|
||||
pushRes := PushResult{Route: RouteNone, Detail: b.interruptReason(push, at, state, tr.Import, lastActivity, hasActivity)}
|
||||
pushRes := PushResult{Route: RouteNone, Detail: decision.Reason}
|
||||
if shouldPush {
|
||||
// Same-event dedup: a repeating identical event is suppressed after the
|
||||
// first push (防刷), while a genuinely new event still goes out. Heartbeats
|
||||
|
||||
Reference in new issue
Block a user