|
| 1 | +# Chat Passive Ratelimit V2 Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Rework passive chat reply policy so direct mentions/private chat always bypass blocking, passive replies and active interjections use willingness-aware rate limiting, and `search_history` supports richer metadata filters while remaining strictly scoped to the current `chat_id`. |
| 6 | + |
| 7 | +**Architecture:** Split reply policy into three layers: intent policy classifies direct/passive/interject decisions and willingness metadata, ratelimit enforces passive/interject budgets with separate hard/soft accounting, and chat generation narrows default context while relying on explicit history tools for broader retrieval. `search_history` becomes the primary metadata-aware history tool, but the handler owns the `chat_id` boundary so tool calls cannot escape the current conversation. |
| 8 | + |
| 9 | +**Tech Stack:** Go, OpenSearch, Redis, GORM/gen query layer, Lark handlers/operators, Ark Responses tool calling, Go test. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Chunk 1: Metadata-Aware History Search |
| 14 | + |
| 15 | +### Task 1: Extend `search_history` request model and tests |
| 16 | + |
| 17 | +**Files:** |
| 18 | +- Create: `internal/application/lark/history/search_test.go` |
| 19 | +- Modify: `internal/application/lark/history/search.go` |
| 20 | +- Modify: `internal/application/lark/handlers/history_search_handler.go` |
| 21 | +- Modify: `internal/application/lark/handlers/tools_test.go` |
| 22 | + |
| 23 | +- [x] **Step 1: Write failing history-search query builder tests** |
| 24 | + |
| 25 | +Add table-driven tests that verify: |
| 26 | +- `HybridSearchRequest.ChatID` is mandatory for tool usage. |
| 27 | +- `user_id`, `user_name`, and `message_type` filters become exact filter clauses. |
| 28 | +- keyword search still builds both keyword and vector clauses. |
| 29 | +- invalid or missing `ChatID` never produces an unscoped query. |
| 30 | + |
| 31 | +- [x] **Step 2: Run test to verify it fails** |
| 32 | + |
| 33 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/history -run 'TestHybridSearch' -count=1` |
| 34 | +Expected: FAIL because metadata filtering and mandatory chat scoping are not implemented. |
| 35 | + |
| 36 | +- [x] **Step 3: Write failing handler/tool tests** |
| 37 | + |
| 38 | +Add tests for: |
| 39 | +- `search_history` tool schema exposes metadata fields such as `user_name` and `message_type`. |
| 40 | +- handler always injects `metaData.ChatID` into the search request. |
| 41 | +- handler rejects empty `metaData.ChatID`. |
| 42 | + |
| 43 | +- [x] **Step 4: Run test to verify it fails** |
| 44 | + |
| 45 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/handlers -run 'Test(SearchHistory|LarkTools)' -count=1` |
| 46 | +Expected: FAIL because the tool does not yet expose the new metadata fields or enforce empty-chat rejection. |
| 47 | + |
| 48 | +- [x] **Step 5: Implement minimal history search changes** |
| 49 | + |
| 50 | +Implement: |
| 51 | +- richer `HybridSearchRequest` metadata fields, |
| 52 | +- a helper that builds the OpenSearch bool query with mandatory `chat_id`, |
| 53 | +- handler-side enforcement that tool searches are scoped to the current chat only. |
| 54 | + |
| 55 | +- [x] **Step 6: Run focused tests** |
| 56 | + |
| 57 | +Run: |
| 58 | +- `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/history -run 'TestHybridSearch' -count=1` |
| 59 | +- `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/handlers -run 'Test(SearchHistory|LarkTools)' -count=1` |
| 60 | +Expected: PASS. |
| 61 | + |
| 62 | +## Chunk 2: Willingness-Aware Passive Reply Policy |
| 63 | + |
| 64 | +### Task 2: Expand intent analysis payload and tests |
| 65 | + |
| 66 | +**Files:** |
| 67 | +- Modify: `internal/application/lark/intentmeta/types.go` |
| 68 | +- Modify: `internal/application/lark/intent/recognizer.go` |
| 69 | +- Modify: `internal/application/lark/intent/recognizer_test.go` |
| 70 | +- Modify: `internal/application/lark/messages/ops/intent_recognize_op_test.go` |
| 71 | + |
| 72 | +- [x] **Step 1: Write failing intent payload tests** |
| 73 | + |
| 74 | +Add tests that cover: |
| 75 | +- parsing/sanitizing `reply_mode`, `user_willingness`, `interrupt_risk`, `needs_history`, `needs_web`, |
| 76 | +- conservative defaults for missing/invalid values, |
| 77 | +- prompt text explicitly describing direct/passive/interject distinctions. |
| 78 | + |
| 79 | +- [x] **Step 2: Run test to verify it fails** |
| 80 | + |
| 81 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/intent ./internal/application/lark/messages/ops -run 'Test(Intent|AnalyzeMessage|IntentRecognize)' -count=1` |
| 82 | +Expected: FAIL because the new fields and prompt guidance do not exist yet. |
| 83 | + |
| 84 | +- [x] **Step 3: Implement minimal intent model changes** |
| 85 | + |
| 86 | +Implement: |
| 87 | +- new enum/type definitions in `intentmeta`, |
| 88 | +- updated recognizer system prompt and JSON parsing, |
| 89 | +- sanitization rules that keep direct/private traffic conservative and passive/interject scores bounded to `0-100`. |
| 90 | + |
| 91 | +- [x] **Step 4: Run focused tests** |
| 92 | + |
| 93 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/intent ./internal/application/lark/messages/ops -run 'Test(Intent|AnalyzeMessage|IntentRecognize)' -count=1` |
| 94 | +Expected: PASS. |
| 95 | + |
| 96 | +### Task 3: Rework passive reply decider and ratelimit accounting |
| 97 | + |
| 98 | +**Files:** |
| 99 | +- Modify: `internal/application/lark/ratelimit/rate_limiter.go` |
| 100 | +- Modify: `internal/application/lark/ratelimit/integration.go` |
| 101 | +- Modify: `internal/application/lark/ratelimit/rate_limiter_test.go` |
| 102 | +- Modify: `internal/application/lark/messages/ops/chat_op.go` |
| 103 | +- Modify: `internal/application/config/accessor.go` |
| 104 | + |
| 105 | +- [x] **Step 1: Write failing decider/ratelimit tests** |
| 106 | + |
| 107 | +Add tests that verify: |
| 108 | +- direct mention traffic is never blocked by `Allow` because it bypasses the decider path, |
| 109 | +- `TriggerTypeMention` contributes to soft load but not hard passive budget, |
| 110 | +- `intent_reply_threshold` and `intent_fallback_rate` come from config accessor rather than hardcoded constants, |
| 111 | +- active interjections cost less than passive replies. |
| 112 | + |
| 113 | +- [x] **Step 2: Run test to verify it fails** |
| 114 | + |
| 115 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/ratelimit ./internal/application/lark/messages/ops -run 'Test(SmartRateLimiter|Decider|ChatMsgOperator)' -count=1` |
| 116 | +Expected: FAIL because hard/soft budget separation and config-driven thresholds are not implemented. |
| 117 | + |
| 118 | +- [x] **Step 3: Implement minimal passive-reply ratelimit changes** |
| 119 | + |
| 120 | +Implement: |
| 121 | +- separate hard/soft weights, |
| 122 | +- willingness-aware score calculation in the decider, |
| 123 | +- config-driven thresholds/fallback rates, |
| 124 | +- chat operator mapping from intent reply mode to passive/interject trigger type. |
| 125 | + |
| 126 | +- [x] **Step 4: Run focused tests** |
| 127 | + |
| 128 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/ratelimit ./internal/application/lark/messages/ops -run 'Test(SmartRateLimiter|Decider|ChatMsgOperator)' -count=1` |
| 129 | +Expected: PASS. |
| 130 | + |
| 131 | +## Chunk 3: Prompt Split And Context Narrowing |
| 132 | + |
| 133 | +### Task 4: Split direct-response vs ambient-response prompt selection |
| 134 | + |
| 135 | +**Files:** |
| 136 | +- Modify: `internal/application/lark/agentruntime/chatflow/standard_plan.go` |
| 137 | +- Modify: `internal/application/lark/agentruntime/chatflow/standard_plan_test.go` |
| 138 | +- Modify: `internal/application/lark/handlers/chat_handler_test.go` |
| 139 | +- Modify: `internal/application/lark/messages/ops/reply_chat_op.go` |
| 140 | +- Modify: `internal/application/lark/messages/ops/chat_op.go` |
| 141 | + |
| 142 | +- [x] **Step 1: Write failing plan-selection tests** |
| 143 | + |
| 144 | +Add tests that verify: |
| 145 | +- direct replies choose the direct-response prompt path, |
| 146 | +- passive/interject replies choose the ambient prompt path, |
| 147 | +- passive prompt path no longer requires broad preloaded history to function. |
| 148 | + |
| 149 | +- [x] **Step 2: Run test to verify it fails** |
| 150 | + |
| 151 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/agentruntime/chatflow ./internal/application/lark/handlers -run 'Test(StandardPlan|GenerateChatSeq|ResolveChatExecutionMode)' -count=1` |
| 152 | +Expected: FAIL because prompt selection is still single-path. |
| 153 | + |
| 154 | +- [x] **Step 3: Implement minimal prompt/context split** |
| 155 | + |
| 156 | +Implement: |
| 157 | +- prompt variant selection in standard chat planning, |
| 158 | +- minimal default context for passive/direct replies, |
| 159 | +- continued use of `search_history` for broader recall instead of mandatory pre-spliced history blocks. |
| 160 | + |
| 161 | +- [x] **Step 4: Run focused tests** |
| 162 | + |
| 163 | +Run: `GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/agentruntime/chatflow ./internal/application/lark/handlers -run 'Test(StandardPlan|GenerateChatSeq|ResolveChatExecutionMode)' -count=1` |
| 164 | +Expected: PASS. |
| 165 | + |
| 166 | +## Chunk 4: Integrated Verification |
| 167 | + |
| 168 | +### Task 5: Run regression suite for touched domains |
| 169 | + |
| 170 | +**Files:** |
| 171 | +- Modify: `docs/superpowers/plans/2026-03-26-chat-passive-ratelimit-v2.md` |
| 172 | + |
| 173 | +- [x] **Step 1: Run the aggregated targeted suite** |
| 174 | + |
| 175 | +Run: |
| 176 | +`GOCACHE=/tmp/betago-gocache go test ./internal/application/lark/history ./internal/application/lark/handlers ./internal/application/lark/intent ./internal/application/lark/messages/ops ./internal/application/lark/ratelimit ./internal/application/lark/agentruntime/chatflow -count=1` |
| 177 | + |
| 178 | +Expected: PASS. |
| 179 | + |
| 180 | +- [x] **Step 2: Update plan checklist and capture follow-up debt** |
| 181 | + |
| 182 | +### Follow-up debt |
| 183 | + |
| 184 | +- `prompt_template_args.prompt_id=5` 仍然存在遗留模板耦合;本次通过“强约束系统提示 + 缩窄上下文”压住了问题,但下一轮最好迁到命名化 prompt config,而不是继续依赖数字 prompt ID。 |
| 185 | +- `search_history` 已支持当前 chat 内的 metadata filter,但还没有进一步拆出 thread slice / recent slice 这类更低成本的专用读取工具。 |
| 186 | +- 主索引现在作为 bot 自我身份判断的事实来源:bot 自己发出的消息保留真实 `user_id`,展示名仍可渲染成“你”;历史搜索和历史行渲染对旧数据里的 `"你"` sender alias 做兼容映射,暂不要求 retriever 同步这套身份元数据。 |
| 187 | +- chat / agentic / continuation 三条 runtime user prompt 现在都会显式注入 `self_open_id` 与 `self_name`;`self_name` 通过带缓存的 `application/v6/applications/:app_id` 读取应用名,失败时回落到 `BaseInfo.RobotName`,便于模型在主索引历史和 mention 元数据里稳定识别“谁是自己”。 |
| 188 | +- 主索引 mention 解析不再把 mention 直接吞掉;历史搜索结果会把 mention 还原成 `@姓名`,命中 bot 自己时渲染成 `@你`,便于模型直接理解“谁在@谁”。 |
| 189 | +- 频控已经拆成 hard budget 和 soft load,但运营侧观测面板还看不到 direct/passive/interject 的拆分指标,后续应补 stats card 或 dashboard。 |
| 190 | +- standard chat 现在默认不预取大段历史/主题摘要,后续可以继续根据 `needs_history / needs_web` 做更细粒度的 runtime 提示或自动 tool bias。 |
| 191 | + |
| 192 | +Record any deferred work, especially: |
| 193 | +- optional new history tools for exact recent chat slices/thread slices, |
| 194 | +- migration from numeric prompt IDs to named prompt configs, |
| 195 | +- stats panel exposure for passive-vs-direct budgets. |
| 196 | + |
| 197 | +- [ ] **Step 3: Commit** |
| 198 | + |
| 199 | +```bash |
| 200 | +git add docs/superpowers/plans/2026-03-26-chat-passive-ratelimit-v2.md \ |
| 201 | + internal/application/lark/history/search.go \ |
| 202 | + internal/application/lark/history/search_test.go \ |
| 203 | + internal/application/lark/handlers/history_search_handler.go \ |
| 204 | + internal/application/lark/handlers/tools_test.go \ |
| 205 | + internal/application/lark/intentmeta/types.go \ |
| 206 | + internal/application/lark/intent/recognizer.go \ |
| 207 | + internal/application/lark/intent/recognizer_test.go \ |
| 208 | + internal/application/lark/messages/ops/chat_op.go \ |
| 209 | + internal/application/lark/messages/ops/intent_recognize_op_test.go \ |
| 210 | + internal/application/lark/ratelimit/integration.go \ |
| 211 | + internal/application/lark/ratelimit/rate_limiter.go \ |
| 212 | + internal/application/lark/ratelimit/rate_limiter_test.go \ |
| 213 | + internal/application/lark/agentruntime/chatflow/standard_plan.go \ |
| 214 | + internal/application/lark/agentruntime/chatflow/standard_plan_test.go \ |
| 215 | + internal/application/lark/handlers/chat_handler_test.go |
| 216 | +git commit -m "feat: add willingness-aware passive chat ratelimit" |
| 217 | +``` |
0 commit comments