AskaEth
|
fa573663c1
|
fix(turnreply): 字面 \n 渲染成真换行(Web 镜像与 IM 出站同一份)
主人 2026-10-24 报障:QQ 群最新一条 assistant 消息里 6 处字面 `\n`
(反斜杠 + n,不是真换行)原样发出去,用户看到「F\n多利亚」。
落点选在**写入侧共享层** `turnreply.NormalizeParagraphs`(gateway 的
persistTurnReply 与 channelbridge 的 turnVisibleReply 都调它):
IM 的 deliverReply 拿同一个字符串既落库(= Web 主会话镜像)又发气泡,
Web chat 气泡是 whitespace-pre-wrap ⇒ 在共享层渲染,落库/WS/REST/IM
出站与历史一次性对齐;只在 channelbridge 清洗层改会留下「面板上仍是字面 \n」。
- 新增 internal/runtime/turnreply/literal_breaks.go 的 RenderLiteralLineBreaks:
逐字节扫描,围栏(```)与行内代码(`…` / ``…``)内原样保留,`\n` 与
`\t`/`\r`/`\uXXXX`、行尾孤立 `\` 一律不动;未闭合围栏保守保护到末尾;幂等。
- paragraph.go 在**围栏 skip 之前**调用它 ⇒ 含围栏的消息里围栏外的 `\n` 也渲染。
- 🔴 未改既有语义:只产出换行字符,绝不产出 <br>/<p> —— <br> 仍是气泡分隔符,
字面 \n 只做气泡内换行(一条列表不会被拆成 7 条消息)。
- channelbridge 源码零改动。
验证:真机原文(6 处 \n / 5 个 <p>)修前红(摘掉钩子 3 个测试变红)→ 修后绿;
三处变异(去掉围栏保护 / 去掉 \ 配对 / 把渲染挪到围栏 skip 之后)各自变红后还原复绿。
全树 go build ./... + go vet ./... + go test -count=1 ./... 全绿(73 包)。
|
2026-10-24 19:21:50 +08:00 |
|
AskaEth
|
4ed5925296
|
Merge branch 'feat/memberquiet-cold-shoulder-applies-before' into dev
|
2026-10-15 19:14:00 +08:00 |
|
AskaEth
|
28967d3435
|
refactor(channelbridge): 冷避例外②收窄为「真故障/求救」严格子集
塔 2026-10-15 改裁定:例外给了命中词「穿透 30 分钟冷避」的权力,噪声不再只是多回
一句话,而是直接削弱冷避本身 —— 而实测全表 14 条在 598 条真实 user 消息里只命中
1 次,且那次是假阳性(QQ 机器人卡片文案里的 `bug`,而该群本来就在放这种卡片)。
- 新增 coldShoulderPierceKeywords(具名常量,注释写明为什么比全表严、依据即那条
卡片假阳性)与 hasColdShoulderPierceKeyword:保留 报错/出错/故障/紧急/救命/求助/
帮帮我/urgent,剔出 ASCII `bug`/`error`/`help`(会命中结构化载荷/URL/卡片)与
情绪歧义的 `崩溃`/`坏了`/`坏掉`。
- 例外② 改用该子集;hasHighValueKeyword(基线,ruleLightJudge 用)**一字未改**,
两张表的分工写进注释,并有断言钉住「基线不许被这次收窄波及」。
- 例外矩阵新增三行:机器人卡片含 `bug` ⇒ 不穿透;英文 error/help ⇒ 不穿透;
情绪「我崩溃了」⇒ 不穿透(三行对上一提交均为红)。第 1/2 行继续用保留词 报错,
第 3/4/5/6 行原样保留;另加子集单元护栏(正例六条 / 反例六条)。
- 按项目「文件 ≤500 行」把冷避规则簇拆到 engage_coldshoulder.go(engage.go 528→376)。
红前绿证据:把例外判据临时还原成上一提交的 hasHighValueKeyword 全表(与该提交逐字
等价)⇒ 恰好上述三行变红、其余行与护栏保持绿;已还原(无残留)。
已知代价如实登记在案:用英文写的真求救不再穿透 —— 塔裁定以「宁可少穿透」为准;
全表(highValueKeywords)本身的收窄由塔另排切片,不在本片。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿(73 包);
channelbridge / channel / cmd/cyrene 单独跑绿;darwin/arm64 交叉编译通过。
|
2026-10-15 19:04:48 +08:00 |
|
AskaEth
|
13a0519115
|
feat(channelbridge): 冷避期唯一的放行口——高价值关键词穿透(不解除冷却)
塔 2026-10-15 例外口径定稿落地。M18 主体(冷避优先于「@她必回」)保持不动,
本提交只补冷却期内的例外与它的守卫。
- engage.go:`coldShoulderLocked` 在冷却期内放行**高价值关键词**这一条
(真故障不该被「让她少闲聊」一并压掉),且**不解除冷却**(不动 state / cooldownUntil);
同句既有负面态度又有高价值关键词时**负面态度优先**(仍进冷却、仍静默);
例外与顺序写进函数注释,并写明 owner 车道为什么不需要单独写例外(它在
shouldRespond 第一步就分岔,写在这里是死代码)。文档面 design/18 §5/§9 由塔同步。
- light.go:shouldRespond 的判定顺序与冷避闸注释同步该口径。
- 测试:新增 engage_coldshoulder_exception_test.go(冷却期例外矩阵:真故障放行、
@她/闲聊不穿透、穿透不解除冷却、同句负面态度优先;以及「冷却之外行为与基线一致」
的护栏),并按文件 ≤500 行拆出该文件;engage_coldshoulder_test.go 增共享夹具。
红前绿证据(参照 = 本提交的父 M18 提交):例外矩阵两条「真故障放行」行在我的改动
之前为红(冷避闸会把它们静默),其余三行与基线护栏在父提交上已绿,已如实标注为护栏;
「同句负面态度优先」一条另做变异自证(把例外插到负面态度判定之前 ⇒ 该行立刻变红)。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
channelbridge / channel / cmd/cyrene 单独跑绿;darwin/arm64 交叉编译通过。
|
2026-10-15 19:01:02 +08:00 |
|
AskaEth
|
fe53e029ea
|
fix(channelbridge): 冷避优先于「@她必回」——群成员也能让她少说话
M18(主人 2026-10-14 裁决「给」)。病灶是**顺序**:`shouldRespond` 把
`ruleLightJudge`(指向她本人 / 高价值关键词)排在参与度引擎之前,@她的消息
在规则层就 return true,永远走不到 `engageDecision` 的冷避判定——群成员说
「少说点」只换来一句口头答应,行为零变化(2026-10-14 00:27 群 531082168 报障)。
- light.go:新增冷避闸位次 —— 非主人**群**消息在廉价规则之前先过
`coldShoulderGate`,命中负面态度即进 COOLDOWN、冷却期内一律静默(仍登记)。
边界:私聊不受影响(冷避是群机制)、owner 车道更早分岔不受影响、
与是否装配参与度引擎无关。shouldRespond 的判定顺序写进函数注释。
- engage.go:把冷避判据抽成唯一实现 `coldShoulderLocked`(engageDecision 仍调用
它,作纵深防御),命中时补一条带 source 的 INFO(群 / 发送者 / 冷却到何时)。
- engage.go 词表:依真机原话补「减少文本输出」(覆盖「减少文本输出量」)与
「文本输出调到最少」;刻意不取「减少 / 输出 / 最少」等短泛词(子串匹配会误伤)。
- 测试:新增 engage_coldshoulder_test.go(主断言修前红/修后绿、登记进上下文的
端到端守卫、私聊与 owner 边界护栏、词表误伤护栏、INFO 可见性);
engage_test.go 更新旧口径「@ 打断冷却」为「@ 不得作废冷避」。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿(windows 本机
与 darwin/arm64 交叉编译均通过);channelbridge / channel / cmd/cyrene 单独跑亦绿。
|
2026-10-15 18:55:35 +08:00 |
|
AskaEth
|
6432088586
|
Merge branch 'feat/addrmember-member-lane-mention-judgment' into dev
|
2026-10-14 07:26:34 +08:00 |
|
AskaEth
|
5969e700e1
|
fix(channelbridge): 成员车道「指向她」判据改为 AddressedToMe(@ 别人不再必回)
ruleLightJudge 原判据是 len(msg.Mentions) > 0 —— 只要有任意 @ 就必回,
@ 的是谁不论。2026-10-14 真机病灶:群里 @3889001246、@867587435 两条
都不是 @ 她,她仍逐条回。
改为复用 owner 车道已验证的同一份判据 ownerAddressedToMe
(msg.AddressedToMe || (msg.SelfID == "" && len(msg.Mentions) > 0)):
- 已接线渠道(qq-obv11 填了 SelfID/MentionAll/AddressedToMe):只有 @ 她本人 /
@全体 才必回,@ 别人降级为无结论 → 参与度触发/概率那一档(未触发只登记);
- 未接线渠道(kook/webhook/telegram/weixin_ilink 未填 SelfID):逐字不变,
仍按旧判据必回(「没有信号」≠「没在对我说话」);
- 判据全部来自结构化字段,绝不匹配正文里的 @<数字>。
口径依据 design/18 §2 触发① 的 2026-10-13 修订(消费点在 M13 只接了 owner 车道)。
|
2026-10-14 07:21:07 +08:00 |
|
AskaEth
|
0c2322bb92
|
docs(api): 契约 v0.93 — 审查预算的唯一真源(MEMORY_REVIEW_TIMEOUT_SEC 默认 20→90)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-13 23:28:39 +08:00 |
|
AskaEth
|
9f1f895059
|
Merge branch 'feat/memtimeout-single-source-of-truth-for-re' into dev
|
2026-10-13 23:28:34 +08:00 |
|
AskaEth
|
18ad2b89ab
|
test(memory): 两条审查超时用例搬到正确位置(复审 p2-1,纯搬运)
复审指出 TestEmbeddingTimeoutFitsReviewBudget 的整段文档被夹在「它描述的
函数」与「两条新用例」之间成了孤儿文档,而该函数自己的文档只剩一句、且在
文件里出现两次(一次属于那段孤儿注释,一次紧贴真正的函数)。
只搬不改语义:把 TestReviewTimeoutExplicitEnvValueWins 与
TestReviewTimeoutEffectiveValueIsTheOnlyDefault 整体挪到
TestEmbeddingTimeoutFitsReviewBudget 之后,删掉重复的那句文案与多余的空行,
让那 21 行文档重新紧贴它描述的函数(与 dev 版的相对位置逐字相同)。
纯搬运的证据:拿「排序后的行集合」比对搬运前后(前者取自 HEAD)——
`diff <(git show HEAD:internal/runtime/memory/preinsert_recall_test.go | sort) <(sort internal/runtime/memory/preinsert_recall_test.go)`
只差两行:一句重复文案 + 一个空行,其余逐行一致。
顺带按「主张与落点」把本文件自查了一遍:每段注释都紧贴它描述的符号
(实测文档与函数之间没有空行:`awk '/^\/\// {doc=NR} /^func / {if (NR-doc!=1) print NR}'`),
且文中的位置指代(「那条防线在下面的 …」「下方 cfg.TimeoutSec != 0 那条」)
在搬运后依然成立。
|
2026-10-13 23:21:25 +08:00 |
|
AskaEth
|
42a07cb454
|
fix(config): 单条入库审查的墙钟上限收敛到唯一真源(M16)
生产启动行仍是 timeout=20s —— M15 把 memory 包的 defaultReviewTimeout
抬到 90s 的修复**从未生效**:config 层的 env tag 自己带着 default:"20",
env 层永远产出非零值,于是 memory 侧「≤0 才回落默认」的归一化成了死代码。
修法(二选一中的「① 让 memory 侧成为唯一真源」):去掉 config 侧
MEMORY_REVIEW_TIMEOUT_SEC 的字面默认值,未配置时读出 0 = 「没配」,
由 normalizeReviewConfig 单点落地 90s。
生效值:
.env / env 没写这个键 ⇒ 90s(唯一真源)
写了正数 ⇒ 原样生效(不做任何「保底抬到 90」的加码)
写了 0 / 负数 ⇒ 90s(与项目其它旋钮同一口径:≤0 = 用默认)
新增测试(走真实组装路径,不绕过任何一层):
config.LoadMemoryReviewConfig → ReviewConfig.Timeout →
NewPreInsertReviewer → normalizeReviewConfig → 生效值(= 启动行打印的 timeout=)
断言生效值严格大于「实测最坏召回链 57s + T1 子超时 8s」,并断言启动行打印它。
变异自证(修前红):把 tag 的 default:"20" 加回去 ⇒ 报出
「组装后 timeout = 20s」+「生效值 20s != 唯一真源 1m30s」
原有 TestEmbeddingTimeoutFitsReviewBudget 拦不住这条,因为它比的是**本包常量**,
看不见 env 层;注释里已写明「比常量」与「比组装后生效值」的区别(防后人重蹈)。
|
2026-10-13 23:09:19 +08:00 |
|
AskaEth
|
202cd8535e
|
Merge branch 'feat/embudget-fix-embedding-request-amplifica' into dev
|
2026-10-13 22:59:42 +08:00 |
|
AskaEth
|
9ff0421888
|
Merge branch 'feat/mempanel-memory-tidy-tuning-panel-write' into dev
|
2026-10-13 22:59:38 +08:00 |
|
AskaEth
|
f6c47b0ae1
|
fix(memory): 清掉两处与实现相反的旧说法 + 并发哨兵改成窗口重叠(变异 8/8 红)
review P2 两条。
① 旧说法没删净(同类第 N 次:改了 A,同一主张还留在 B / C)。这次按「**这句主张的所有
落点**」重扫了一遍我这一片里含 Stop / Start / 重启 / 心跳已停 的注释,改掉两处与实现
正面打架的:
- applyTidyConfig 的注释还写着「调用方(ApplyPanelTuning)在 Stop 期间不能持 runMu」
「前提仍是心跳已停」—— 而同文件的文件头刚写过「调优路径上一个 Start/Stop 都没有」,
同一文件里两段话自相矛盾;
- 整理档那条并发哨兵的循环注释还写着「Stop 把 running 置为 false,忘了重启就会静默
停摆」,而该循环里根本没有 Stop/Start(改成「唤醒不是停表」,并把 Stop/Start 那版
明确标成旧版)。
顺带把文件头「所有读点都收进访问器」的说法改准:此外还有一处**同一把锁下**的相等比较
(ApplyPanelTuning 里的 s.cfg == next),它本身不需要访问器。
② 新哨兵是概率性守卫:固定 40 轮时撤回 DryRun 同步只红 4/8(写方那几十次覆写在微秒级
就跑完,读方还在慢端点里打转)⇒ 文档规定的 -count=1 会漏掉约一半回归。改成**两侧各自
跑满同一个 300ms 墙钟窗口**(写窗口与读窗口必然重叠,不靠快慢概率),读方两个活端点各
起两路;写方每轮换一个值(否则 ApplyPanelTuning 会被「配置没变」的早退挡掉,根本不写)。
实测:撤回 DryRun 同步红 **8/8**、撤回 ManualRun 同步红 **8/8**(各 8 次 -race -count=1);
干净 tip `-race -count=5` 全绿、三条哨兵一起 `-count=3` 全绿;该哨兵耗时 0.35s(原 0.18s)。
顺带把 clipJSON 用例注释里那句改准:40 字节处被切掉的是第 13 个汉字的**第三个字节**
(前两个字节被留下),而不是先前写的「第三个字节上」。
|
2026-10-13 22:50:38 +08:00 |
|
AskaEth
|
4df8d6f53a
|
fix(memory): 按字段扫清 s.cfg 的其余无锁读(P1)+ 回显文案按 rune 截断
review P1:applyTidyConfig 在运行期**整体覆写** s.cfg,而我只把调度读收进了持锁
访问器 —— 同一字段还有几处无锁读,其中两处是**活端点**:
- TidyService.DryRun 的开关判定(在取 runMu **之前**)⇒ POST …/tidy/dry-run 与
?async=1 那条会起后台 goroutine 的版本(真机 90s 级);
- TidyService.ManualRun 的开关判定(同样在 runMu 之前)⇒ POST …/tidy/run;
- (原本也漏的)Enabled() 与 Start 的启动日志块。
按**字段**重扫了一遍(不按文件):现在生产代码里对 s.cfg 的读**只剩**三个持锁访问器
内部(enabled / cfgSnapshot / scheduleSnapshot),写点只有 applyTidyConfig 一处;
s.tidier.cfg 的读点全在持 runMu 的轮次 / 试算路径上(含异步版经 DryRun)。
新增第三条并发回归哨兵:真并发跑「试算 + 手动真跑」两个活端点 × 面板保存。
修前红/修后绿:撤回 DryRun 的同步 ⇒ 报「写 applyTidyConfig × 读 DryRun」;撤回
ManualRun 的同步 ⇒ 报「写 applyTidyConfig × 读 ManualRun」;两次变异都逐字还原
(diff 0 差异)。
顺手两条(塔指定):
- 那条并发哨兵的 doc 与实现相反(还写着「正确顺序是 Stop → 写 → Start」),已改成
唤醒机制下的说法 —— 与「注释与实现不符」同类,不留旧说法。
- 非法值的回显文案原按**字节**截断(s[:40]),中文会被切成半截(非法 UTF-8,经
JSON 编码变成 U+FFFD 替换符)。改成按 **rune** 截断,直接复用同包既有的
clipRunes;用例取值刻意让截断点落在多字节字符中间(JSON 引号 + 一个 ASCII 前缀
错开对齐),并做过变异复验:改回按字节 ⇒ 用例红(切出半个汉字)。
|
2026-10-13 22:33:07 +08:00 |
|
AskaEth
|
97c0c1b94a
|
feat(memory): 调优重排改为唤醒通道,不再 Stop/Start(消灭与到期清扫的耦合)
按塔的裁定把「重排心跳」从生命周期操作改成唤醒通道,调优路径上一个 Start/Stop
都没有了。
- tidy_service.go:TidyService 新增 wake(cap 1 + 非阻塞发)与 wakeMu/wakeCancel;
loop 的等待段抽成 waitForTick,三态判定(睡满 / 被调优唤醒 / 停);被唤醒只
重算下一拍、**绝不**跑轮。调度读(nextDelay / dueNow)改走 scheduleSnapshot,
在同一把锁下同取「cfg + lastRun」,于是运行期写 cfg 不必再把后台件停下来。
Start / Stop / wg / running 与 Enabled 早退语义**一个字节没动**。
- tidy_service_tuning.go:ApplyPanelTuning 变成「持 runMu 写 cfg → 发一次唤醒」。
唤醒 = 一次性标记(保证不丢:心跳不在等时标记留在缓冲里)**加上**取消当前那次
等待(保证立刻生效,否则 24h→3h 要等原来那一觉睡完)。
两条冲突就此消失,各配一条可复现断言:
- **面板保存不再触发到期清扫、也不重置清扫 ticker**:新用例在保存前后各造一行已
到期的记忆,保存之后那行必须**还在**、清扫审计计数必须不变;该用例自带活对照
(启动清扫会删掉第一行),因此「行还在」不是量具失灵。若保存走 Stop/Start,清扫
的「启动即扫一轮」会立刻把它删掉。
- **调优路径上不再存在任何 Start 调用**(结构上消灭交叉 Start/Stop):包内生产代码
已 grep 不到任何 `.Start(` / `.Stop(` 调用,Start 与 Stop 各只剩组合根那一处调用点
(`cmd/cyrene/main.go` / `cmd/cyrene/wire_services.go`),M3 注释里那条「Start/Stop
各只调用一次」的不变式因此重新成立;交叉场景由带真心跳的调优用例在 `-race` 下跑过
(不再 panic)。
热生效没退化:原来那条「23h30m → 30m」的断言按新机制重写 —— 用一个「听得懂 ctx
取消」的假等待看两觉的时长(保存前 23h30m、保存当场的第二觉 30m),并补上「唤醒不
跑轮(lastRun 不前移)」「唤醒后 running 仍为 true」。
变异自证(都跑过再还原,`diff` 逐字一致):
- 去掉 `notifyReschedule` 调用 ⇒「被唤醒后的第二觉」等不到 ⇒ 用例红;
- 只留标记、去掉「取消当前等待」那一半 ⇒ 同一条红(两半都必需)。
M3 的守卫 `expire_loop_test.go` **一行未动**:它的假 wait 只认 ctx,父 ctx 取消时
返回 false,在新机制下语义不变(无人唤醒时照样把心跳停住)。
|
2026-10-13 22:17:22 +08:00 |
|
AskaEth
|
bdf3f3edaf
|
docs(embedding,memory): 复核 p2 的三处措辞改正(纯注释,零行为改动)
review-m15 round1 判 p2-1items(fix-then-merge),三条都属「把证据能支撑什么写准」:
1. api.go 原写「最坏也只是这一片失败」与实现语义**相反** —— Embed 的实际语义是
「任一片失败 ⇒ 整条调用失败、其余分片已算完的工作量一并丢弃」。改为与该包
budget.go 里已经写对的那一句对齐,并点明调用方据此降级(记忆一条不丢、判定规则
一字不改,变的是这一条审查的召回质量)。
2. 「尖峰与批次无关」是**过度断言**:原证据只支持「至少存在一个与批次无关的外部/
争用分量」(4 条/片也实测到 7.139s);越界的**频率**是否随批次上升,1/40 vs 2/40
的样本不足以判定 ⇒ 改标为「归因未定」,并明说批上限取 8 的依据始终是硬证据
(16 条/片实测越界 + 真实代码路径 3 次里 2 次越界),**不依赖**任何成因推断
(原文与同文件「16 条越界故不取更大」的理由自相矛盾)。同时补上独立复核窗口的
对照(review-m15:24 次单请求 0 尖峰)说明尖峰强依赖窗口负载。
3. budget.go 的不变式不再被读成延迟保证:明确定性为「**稳态、空闲机器的建模拟合**,
用途是构造批形状」,并点名真正的兜底(90s 审查预算按整条链算 + 任一片失败即降级
的路径 + 已登记的链路韧性遗留);结论句由「8 条/片不越界」改为「**空闲实测下**不
越界、负载下仍偶发越界」;perItemBudget 与 callBudgetShare 注释里「另一半/余量」
的同口径措辞一并改正(加一句:这是建模余量,不是单请求一定不越界的保证)。
另:preinsert_config.go 里「设计稿那一行待同步」已过期 —— 塔同日已把 design/23 §15
同步为 Timeout=90s,改为「已按实测同步」。
验证:go build ./... / go vet ./... 干净;go test -count=1 ./... 全绿;
go test -race -count=1 ./internal/runtime/embedding/... ./internal/runtime/memory/...
全绿;diff 中无非注释行(纯文档改动)。
|
2026-10-13 22:12:11 +08:00 |
|
AskaEth
|
a87fbeb494
|
fix(memory): 面板调优引入的两处运行期写入面收口(-race 修前红 → 修后绿)
一、审查档(preinsert.go / preinsert_gate.go):面板调优会在**运行期**改 cfg 的
两个字段(DisableIngestGate / TempTTL),而读这两个字段的两处不持锁
(gateEnabled 在同步入库闸上、deferDeadline 在第五态 defer 上)。
- 读侧改走新的 tuningSnapshot()(r.mu 由 Mutex 换成 RWMutex,读锁下取整份快照);
- **只**收这两个字段的读点:其余读点读的是构造期冻结的字段,不与写方交手,
故刻意不给入库热路径加无谓的锁;注释写明「将来面板再接管别的字段,那些读点
必须与它一起收」;
- 锁序:写方持锁期间只做字段赋值、不取任何别的锁(normalize/回落都在取锁之前
跑完)⇒ 不引入新锁序。
二、整理档(tidy_service_tuning.go):修掉我自己的那条真竞争 —— 原顺序是
「Stop → Start → 写 cfg」,而新起的循环**立刻**读 s.cfg 重算下一拍,与写入并排
(那一拍还可能按旧配置算出来)。改成「Stop → 写 → Start」。
顺带去掉 Stop 的超时分支:超时意味着 wg 的 Wait 仍在飞,紧接着 Start 的 Add 会
踩上 M3 在 tidy_service.go 明文写下的 WaitGroup 复用纪律(-race 下 panic);`Stop`
期间也不持 runMu,避免与「心跳正要开跑一轮」互相等死。
证据(全部逐条跑过再还原,还原用 diff 逐字校验为 0 差异):
- 审查档:撤回读侧修复 ⇒ 2 处 DATA RACE(写 preinsert_tuning.go 的两个字段 × 读
gateEnabled / deferDeadline);恢复 ⇒ 绿。
- 整理档:把顺序改回「先起后写」⇒ DATA RACE(写 applyTidyConfig × 读 nextDelay);
恢复 ⇒ 绿。
- 新增两条只在 -race 下有意义的回归哨兵(tidy_service_tuning_test.go):审查档真
并发读写;整理档用**真心跳**当读者(不伪造生产不存在的调用方,那个入口在包里
只有心跳 goroutine 会调)。
热路径成本:新增的同步是「一次无竞争 RLock + 整份小结构体快照」,隔离实测约
4ns/次(5e6 次共 22.9ms);入库闸那条测量用例的批均值在 2.62ms → 2.71~2.81ms
之间波动,落在那条用例自身的运行间噪声里(同一份代码连跑两次,纯写入基线就从
39µs 飘到 60µs)。
|
2026-10-13 22:11:41 +08:00 |
|
AskaEth
|
f3fb097630
|
perf(embedding,memory): 批上限 4→8 削掉请求放大,审查预算 20s→90s 覆盖实测链长
实测(2026-10-13,本机空闲,活库 cyrene/user 分区 201 条、均长 34.7 汉字):
- 一条入库审查召回链 = 51 个「4 条」请求 = 23.070 / 21.860 s;旧审查预算 20s
比链长还短 ⇒ 每一条候选满额的审查都超时,且超时后 **一行留痕都不写**
(7 天里 92.1% 的审查 recall_mode 为空、2274 条超时降级的根因)
- 同一条链改成「8 条」= 26 个请求 = 21.435 / 21.597 s:请求数减半,墙钟几乎不变
—— 链路耗时由「条数 × 每条成本」主导,批次只改请求数
- 「16 条」在**真实代码路径**上 3 次里 2 次让单请求越过 5s 单次超时(1.818 /
5.016 超时 / 5.001 超时)⇒ 批上限不越过 8;隔离测量里 12 条 0.996~1.017 s 仍在界内
改动:
- embedding: perItemBudget 500ms→300ms(实测摊销 70~107ms/条的约 3 倍)
⇒ DefaultBatchLimit 由 (5s/2−100ms)/300ms = **8**,同一套推演、两维都成立
(条数 100ms+8×300ms=2.5s;字符 100ms+480×5ms=2.5s,均在超时的一半内)
- embedding: 新增 maxSafeBatchLimit=12(实测外沿)+ 编译期断言(越界即 uint 溢出、
编译失败),与 tidy_config.go 那条同手法
- memory: defaultReviewTimeout 20s→**90s**(> 实测最坏链:空闲 21.6s⇒4.2 倍余量、
与整理链并发 56.7s + T1 8s ⇒ 1.4 倍;不再大的理由=单 worker 有界队列 256 的
最坏积压 256×90s≈6.4h、服务真挂死时第一片 5s 就降级用不到这条预算)
测试(不留过时断言):budget_test.go 加「钉值 8/480 + 两维不等式 + ≤12 外沿 +
16 条必须仍判越界」四层;api_url_test.go 的 reviewBudgetRef 20s→90s;
preinsert_recall_test.go 新增跨包断言「预算 > 实测最坏链 + defaultT1Timeout」。
变异自证:perItemBudget 回 500ms ⇒ 钉值断言红;reviewTimeout 回 20s ⇒ 跨包断言红;
perItemBudget 150ms(⇒16)⇒ 编译失败(constant -4 overflows uint)。三者均已逐字还原。
遗留(如实登记,未修):① 整理轮每轮重嵌整个分区(user 一轮仍 250 个请求),
解法方向是复用已入库的 vec_items 或给整理轮一个 embedding 请求预算;② 链路对单点
慢分片没有韧性 —— 一个分片失败即整条 Embed 失败、丢掉其余分片的工作量,且实测
任何批次形状都会偶发 >5s 尖峰(4 条 7.139s / 8 条 9.711s,与批次无关),方向是
重试/分片级容错,调常量解决不了。
|
2026-10-13 21:59:31 +08:00 |
|
AskaEth
|
e9e7b7dcb2
|
test(memory): memory-tidy.json 写路径(补丁保留其它键 / 值没变不落盘 / 坏文件拒写 / 原子写)
塔批准的 scope 里点名了 internal/config/runtimeconfig/memorytidy_save_test.go,
补上这四个口径的包内单测(网关侧另有端到端用例覆盖三态与审计)。
|
2026-10-13 21:59:18 +08:00 |
|
AskaEth
|
27ee0a8149
|
test(memory): 拆分调优写端点测试(原文件 524 行 > AGENTS §10 的 500 行上限)
守卫用例(非法值不落盘 / 坏文件不覆盖 / 同值不重写 / 热生效两种结果)移到
admin_memory_tuning_guard_test.go,helper 与读侧用例留在 admin_memory_tuning_test.go;
口径不变,两个文件都 ≤300 行。
|
2026-10-13 21:59:18 +08:00 |
|
AskaEth
|
eddf921229
|
feat(memory): 记忆整理调优面板读写(三态 + 热生效 + 面板表单)
主人 2026-10-07 两条口径:「.env 只做兜底用,后端自动创建独立的面板配置文件
之后就走面板配置文件,这些键优先从面板配置上取」+「频率必须调,可以调到 1 小时
或者三小时,或者动态分配(白天短、静默期长)」。
此前 $DATA_DIR/config/memory-tidy.json 只有读路径(装配期读一次 + 启动播种):
主人要改频率只能改文件 + 重启后端。本切片补上「读回来 → 改 → 落盘 → 当场生效」。
后端(读写端点,契约待塔登记 v0.92):
- GET /api/v1/admin/memories/tidy/tuning 读盘上 7 键值 + 取值规则 specs +
缺位时 .env 档的进程生效值(与 /admin/config-facts 同源)
- PUT 同路径:三态补丁(键缺位 = 不改动 / 合法值 = 采用 / 非法值 = 400 且盘上一
个字节都不动)+ null 清除哨兵(删键 ⇒ 回到下一档)+ writeAuth + 审计
memory.tidy.tuning;值一个都没变时不落盘,文件解不开时拒绝覆盖(409)
- 落盘走 runtimeconfig.PatchMemoryTidyTuning(原子写 temp+rename,与
SeedMemoryTidy/SaveWebSearch 同族同手法);补丁语义保留下受管的其它键
(t1Judge/t1PerWindow/autoApplyPerRound 原样保留)
- 热生效:保存后按「面板 > .env > 代码默认」重算并叠到运行期对象;整理档会
重排心跳(否则新周期要等旧周期那一觉睡完),审查档当场改 ingestGate/tempTtlSec。
未接线/失败时如实回 hot:false + hotError,绝不把「落盘了」说成「生效了」
前端(Miuix):
- 记忆管理页新增「记忆整理调优」卡:7 键表单(中文标签、单位、留空⇒跟随谁、
env 档当前值、坏值就地报错并拦住保存、保存后回显从磁盘读回的真值)
- 试算卡的 recallMode 按契约 v0.91 如实渲染:embedding / lexical(已降级)/
mixed(部分分区降级)/ ""(无候选可比)——空串不再渲染成空标签;并把
error.code=tidy_recall_unavailable 单列一句(「尝试过但向量源不可用」,与
「没有候选可比」显式区分)
验证:go test ./internal/{services/gateway,runtime/memory,config/runtimeconfig}
全绿;三态/清除/坏文件拒写/同值不重写/热生效接线各有用例,并逐条做过变异自证;
tsc --noEmit + vite build 通过;两个 esbuild 前端测试 39 + 34 + 41 checks 通过。
|
2026-10-13 21:59:18 +08:00 |
|
AskaEth
|
17c41265e8
|
Merge branch 'feat/memexpire-bounded-expiry-sweep-with-audi' into dev
|
2026-10-13 21:59:01 +08:00 |
|
AskaEth
|
8a274e120d
|
docs(memory): 到期清扫的注释改用符号引用,不再写会漂的行号
review r2(p2-1items)指出:r1 那笔注释里的行号是旧值 —— 我在文件更前面插了
12 行之后 `Stop` 整体下移,而注释里仍写着「`Stop` 在 `tidy_service.go:214` 就放锁,
到 `:219` 才 Wait」(214 是 `Stop` 的文档注释行、219 是 `s.mu.Lock()`)。
行号会漂、符号不会,因此改成符号 + 相对位置:
- 「在同一个 `func (s *TidyService) Stop` 里,`s.mu.Unlock()` 在前、`s.wg.Wait()`
(另一个 goroutine 里)在后,两者之间没有锁」;
- 「`Start` 只在组合根启动期调一次(`cmd/cyrene/main.go` 的
`a.memoryTidy.Start(ctx)`)、`Stop` 只在关停链里调一次(`cmd/cyrene/wire_services.go`
的 `_ = a.memoryTidy.Stop(ctx)`)」。
同一类问题只此一处遗留(`expire.go` 里「照 `preinsert_gc.go:65-69` 的批删范式」
同样改成符号:`ingestReviewSweepBatchSize` / `maxIngestReviewSweepBatches` 那一对
常量)。已 grep 全部改动文件确认再无 `*.go:<行号>` 形式的引用。
(本提交信息刻意不复述任何行号。)
|
2026-10-13 21:54:38 +08:00 |
|
AskaEth
|
69210a8586
|
docs(memory): 改正 Start 里那笔 Add 的并发注释,并补 Enabled=false 回归测试
review r1(p2-1items)指出:`tidy_service.go` 新加的那段注释(以及 2ba0a40 的提交
信息)写着「计数在这把锁里加 …… `Stop` 也持这把锁,这样 Add 与 Stop 的 `wg.Wait`
不可能撞在一起」。**这句不成立**:`Stop` 在 `tidy_service.go:214` 就放锁,到 `:219`
(另一个 goroutine 里)才 `wg.Wait`。锁只串行化 `running` 翻转与两笔 Add,
**不覆盖 Wait**。reviewer 用交叉 Start/Stop 探针在 -race 下复现了
`panic: sync: WaitGroup is reused before previous Wait has returned`,并实测该 panic
在 M3 之前的基线(20051f0)上同样可复现 ⇒ 既有性质,本切片不改行为,只改说法:
- 照真实理由改写注释:锁内加是为了让 `Stop` 的 `running` 判定与这两笔 Add
**原子化**(杜绝「Stop 已看到 running=false 早退」与「Wait 漏掉一笔尚未 Add 的
goroutine」两个窗口);
- 明确写出这把锁**不**覆盖 `Stop` 的 `wg.Wait`,WaitGroup 的复用纪律靠
「`Start`/`Stop` 在进程生命周期各只调一次」维持;
- 并写明那个交叉探针的**当前调用点不可达**(`Start` 只在 `cmd/cyrene/main.go:97`
调、`Stop` 只在 `cmd/cyrene/wire_services.go:212` 调),免得后人看到「有窗口但
不可达」以为是漏改。
同时补上 reviewer 点出的真实缺口:`Enabled=false ⇒ 清扫不跑` 此前只有注释、没有
已提交的回归测试。新增 TestTidyServiceStart_整理关掉时清扫不跑,真跑 Start 后断言
「没有删除发生 + 没有审计 + running 保持 false」三件一起(不是只断言 cfg.Enabled
字段那种假牙)。两条断言各有牙:把 Enabled 闸改成恒 false ⇒ 红在 running 那一条;
在 Enabled 早退分支里偷偷起一个清扫(running 仍为 false)⇒ 红在「到期行却被删了」。
|
2026-10-13 21:46:24 +08:00 |
|
AskaEth
|
44c25eb03b
|
refactor(memory): 到期清扫挂点从 wait 注入缝改为 TidyService.Start
塔裁定:清扫不许挂在注入缝上 —— 包一层 s.wait 会让「清扫是否存活」依赖于
「loop 每一轮都调 s.wait」这个**没有任何契约固定**的假设;将来谁换了等待原语、
或把循环改成 select + 自己的 timer,清扫会**静默消失**(不报错、不红测试,
只是临时期限再也删不掉)。后台件起在 Start 里,读者一眼看到它们同生共死。
- tidy_service.go:Start 里紧跟 go s.loop(cctx) 起 go s.expirySweepLoop(cctx),
wg 计数与既有那一笔同处(同一把锁内加,避免 Add 与 Stop 的 wg.Wait 相撞)。
`if !s.cfg.Enabled { … return }` 仍在其前 ⇒ 语义与改动前逐字相同:整理被关掉
⇒ 清扫不跑(与 preinsert_gc 随审查服务启停同口径)。
- expire_loop.go:删掉 AttachExpirySweep 与对 s.wait 的包裹;文件头写明
「挂点必须是 Start 里那处显式的 go」,防止后人改回注入缝。
- expire_loop_test.go:那条运行时对照改写成 TestTidyServiceStart_清扫随服务起停
—— 真 Start、真 Stop(Stop 不超时即证明清扫随 ctx 退出且登记在同一个 wg)。
- wire_memory_tidy.go:去掉组合根的挂载调用,注释改成「起在 Start 里,组合根零接线」。
- wire_memory_tidy_test.go:把 go s.expirySweepLoop(cctx) 钉进 TestMemoryTidyIsWired
的 seams(摘掉那一行 ⇒ 用例红,已实测)。
|
2026-10-13 21:43:27 +08:00 |
|
AskaEth
|
a017ab6cf8
|
feat(memory): 临时记忆到期物理删除(有界清扫 + 同步清向量 + 删前审计)
design/23 §19 的第 ② / ③ 步:读侧时效闸(M1)只负责「看不见」,本切片负责
「真删掉」。三条护栏照 §19.3:删除是本包内唯一 DELETE 出口、有界(500 行/批 ×
20 批)、删前留审计。
- internal/runtime/memory/expire.go:扫描集合写死为「expires_at ≠ '' ∧
expires_at ≤ now ∧ status='active' ∧ 来源非受限」。空 expires_at(长期/日常
记忆,含 M2 登记的「INSERT 与补写之间中断」窗口)结构性进不了集合;受限来源
复用 nonOwnerSource(与整理/审查同一处判据)。键集游标 (expires_at, rowid)
保证被跳过的行只读一次、扫描位置一定前移。审计串 = id + persona_id + scope +
expires_at + 按 rune 截断(48)的摘要 —— 本仓唯一一处允许携带内容片段的审计。
- internal/runtime/memory/expire_loop.go:清扫循环挂在 TidyService 的生命周期上
(AttachExpirySweep 由组合根调用),随心跳 ctx 跑、随 Stop 停;一轮只写一行
汇总日志,逐条的那一行进审计。
- internal/runtime/memory/vector/delete.go:新增可选删除能力 vector.Deleter +
两个后端实现,删行时同步清向量(M1 reviewer 的 finding:过期向量占 top-K 预算)。
- cmd/cyrene/wire_memory_tidy.go:挂上清扫;顺带把已过时的「这十键」改成「这十一
键」(M2 加了第十一键 tempTtlSec)。
|
2026-10-13 21:43:27 +08:00 |
|
AskaEth
|
80096c8bd1
|
docs(api): 契约 v0.92 — 记忆整理调优键读写端点(B26/B27,切片 M4)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-13 21:38:47 +08:00 |
|
AskaEth
|
20051f0168
|
Merge branch 'feat/ownergate-owner-lane-gate-plus-conversat' into dev
|
2026-10-13 21:18:02 +08:00 |
|
AskaEth
|
8690d129a7
|
Merge branch 'feat/memwrite-defer-fifth-state-plus-temp-ttl' into dev
|
2026-10-13 21:09:47 +08:00 |
|
AskaEth
|
48a5bb89f2
|
fix(config): owner_lane.json 并发落盘不再损坏/丢更新(review p1)
review p1:Set 的固定 tmp 名(path+".tmp")+ 落盘发生在 mu.Unlock() 之后 ⇒
并发 Set 撞同一临时文件把 JSON 写坏(load 退回默认「甲」、主人刚说的「少说点」
白说且无提示),或 rename 完成顺序与新快照先后不一致而丢更新。
修法(两条都取,各司其职):
- 落盘挪进锁内(Set 全程持 s.mu):并发 Set 完全串行 ⇒ 快照与 rename 顺序一致,
不可能丢更新。落盘是一个小 JSON,持锁 I/O 代价可忽略;读侧读内存、不读文件。
- 每次用 os.CreateTemp 新建临时文件(仓库同款先例 internal/user/save.go),
不再撞固定 tmp 名;CreateTemp 以 0600 打开并再 chmod 一次,任一步失败都清掉 tmp。
- 旧 save(快照) 换成 saveLocked()(调用方须持锁,直接 marshal s.cfg)。
测试:新增并发用例 TestOwnerLaneStoreConcurrentSetKeepsAllUpdates(24 轮 × 24 写者),
断言 ① 文件始终是合法 JSON、② 每一次成功的 Set 都能在最终文件读到(不丢更新)。
修前红(原实现在同一用例上):5/5 轮复现,两种失败模式都抓到 ——
"并发落盘把配置写坏了(文件不是合法 JSON):invalid character '\"' after top-level value"
与 "丢更新 —— 键 qq-obv11|g-N-M 不在最终文件里";修后连续 3 次绿,-race 下亦 ok。
Bridge.SetOwnerLanePolicy 无需改动:它在 ownerLaneMu 下设内存后调注入的 store,
串行化由 store 侧保证。
|
2026-10-13 21:07:46 +08:00 |
|
AskaEth
|
c7ea67dcb6
|
Merge branch 'feat/embrecall-fix-embedding-recall-degradati' into dev
|
2026-10-13 21:07:21 +08:00 |
|
AskaEth
|
a3ae98ef2e
|
docs(memory): N2 注释三处更正(示例 / 标点容忍 / 位置锁)+ 逐条实测
应答 review round4 的 p2#1。**只改注释**,行为代码、后缀表 golden、25 条语料、
主语表一字未动。三处都指向**更严格**的表述(无安全影响),且每一处都先实测再下笔
—— 上一轮的教训正是「注释里的例子自己没实测过」。
## ① 示例 `生气得很` 与实现不符 → 换成实测覆盖的示例
原句举例「生气**得很**」,但 `得` **不在**后缀表里:
生气得很 → rest="得很" → rule=""(放行)
与它并列的另两个例子矛盾。换成实测 `rest=""` 的:
心情不错 → rest="" → rule="n2_emotion"
不开心 → rest="" → rule="n2_emotion"
心情很好 → rest="" → rule="n2_emotion"
并把反例留在原位(显式标注「反例」):
生气得很 → rest="得很" → rule=""(放行)
## ② 「任何不在表中的字符 ⇒ 放行」对标点不绝对 → 补齐容忍面
标点与空白是被 `onlyRetentionPunctuation` 收尾清掉的,因此原表述漏了这半边:
心情不错! → rest="" → rule="n2_emotion" ← 实测 defer(标点被容忍)
现改为「任何一个**非标点 / 非空白**的字符只要不在词表里,就会留在余部」。
## ③ 「主语/时间词只允许出现在句首」不准确 → 按实现写「位置锁」
时间词也可**紧跟主语之后**。现按实现写成:
主语只在**位置 0** 被消费一次;时间词只在**紧随其后**被消费一次(无主语时它就是首词)。
实测:
今天好累 → rest="" → rule="n2_emotion"
主人今天好累 → rest="" → rule="n2_emotion"
今天主人好累 → rest="今天主人好累" → saw=false → rule=""(放行)
「指人/指时实体永远不可能落到宾语位置」这条**支柱本身不变**(review round4 已核),
变的只是对**位置**的准确描述。
## ④ 顺手同批修掉的同句不准确:`retentionEmotionTimeWords` 的文档
它与 ③ 是**同一句主张的另一个落点**(「只允许出现在句首的时间词」),留着就等于把同一
处不准确留在盘上。已改成与 ③ 一致的表述并附同一条实测。
(`consumeRetentionPrefixes` 的「反复在句首消费」指的是**当前余部**的开头,是对辅助函数
自身语义的描述,不是文法主张,未动。)
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
TestExtractionRetention 六个用例 69 个子例全 PASS。
|
2026-10-13 21:06:33 +08:00 |
|
AskaEth
|
95f517d61d
|
refactor(embedding): 删掉生产不可达的冷启动 WARN 及其 logger 缝
review round2 的 p2:`api.go` 那句「一次性冷启动放宽被花掉」的 WARN 在生产里
**不可能被人看见** —— 本包默认丢弃日志器,`WithLogger` 全树零调用者,生产走
`embedding.New(cfg)` 不带任何选项。而它的注释写着「这是唯一能审计一次性放宽的
地方」,与实际不符(本项目规则:注释不得说与实现不符的话)。
- 删该 WARN(失败并非静默:四个调用方本来就把错误原文写进各自的 WARN,
路径见 review 召回 / 整理召回 / 向量索引写入 / 检索)。
- 连带删掉 `WithLogger` 与 `log` 字段:WARN 是 `e.log` 的唯一使用者,留着它会
变成一个**空转的公开选项 + 又一次与实现不符的注释**(与上一轮删
`WithRequestTimeout` 同一条理由:不留没人用且未测的缝)。
- `callTimeout` 不再返回 `relaxed`(它只为那条 WARN 存在)。
- `NewAPI` 的注释改为把「本包不记日志、失败沿错误返回」写成**实话**。
行为零变化(生产本就是丢弃日志器),只删死代码与不实注释。
|
2026-10-13 21:01:54 +08:00 |
|
AskaEth
|
4365224c9b
|
docs(memory): N2 安全论证改成真实机制(位置锁 + 四类修饰)+ 两条 tripwire
应答 review round3 的 p2#1(要修的只有这一条)。
## 注释里的论证被证伪,改成真实机制
原句「封闭表里**没有承载内容的词**:没有动词、名词、量词」**字面不成立** ——
reviewer 逐表审过:情绪词表(既是核心也是后缀 token)含 `心情`(名词)、
`生气`/`害怕`/`紧张`(动词)、`压力大`(动宾);否定表里 `没有` 是动词;程度表里
`比较` 可作动词/名词;主语表里 `主人`/`我`/`他` 本身就是**指人实体**。
真正保安全的是**位置**:
1. **位置锁定** —— 主语 / 时间词只允许出现在**句首**、各至多一次 ⇒ `主人`/`我`/`你`/
`他`/`今天` 这些指人/指时实体**在文法里永远不可能落到宾语位置**;
(上一版黑名单正是死在这里:`会想我` 的 `我` 被当成主语剥掉)
2. **核心之后只允许「程度 / 否定 / 情绪 / 语气」四类修饰**,且它们只允许**紧跟**
情绪核心之后 —— 核心必须先被消费,那一段在语义上只能是**情绪的修饰**
(「心情不错」「不开心」),**装不下一个宾语短语**。
「上一版为什么错、黑名单为什么修不完」那一段**保留**(reviewer 确认该部分写得对)。
## 两条 tripwire(回应「后来人照字面加词、没有任何测试会红」)
- `TestExtractionRetention_PersistentFactsAreNeverDeferred`:**25 条**持久事实/习惯
语料(塔给的 6 条 + reviewer 扩展的 19 条,来自 review round3 的对抗构造)逐条断言
**绝不 defer**,钉住**语义后果**;语料是**可增长的**(新事故 → 一条回归断言)。
- `TestExtractionRetention_SuffixTableIsPinned`:把后缀表的**内容**逐 token 钉死
(60 个 token 的 golden),任何增删立刻红。
**为什么它比语料更接近「表的内容性质」**:语料只能覆盖**已经想到的**输入,而这张表
是判据的全部自由度 —— 往后缀里加一个名词,表变了而语料可能一条都不碰;golden 钉的
是「表变了」这个**事实本身**,改表必须显式改 golden ⇒ 变成一次可 review 的
意图声明,而不是一次静默放宽。
两条一起才够:golden 钉「表变了」,语料钉「变了以后语义还对不对」。
## 变异自证(两条 tripwire 分别会红)
变异 A:往后缀语气词表加一个名词「**妈妈**」(语料里没有任何一条会因此变 defer)
```
--- FAIL: TestExtractionRetention_SuffixTableIsPinned (0.00s)
后缀表 token 数 = 61, want 60 —— 表被改过。…
FAIL
```
⇒ golden 抓住了**语料抓不到**的改表。
变异 B:往后缀表加 `会`/`想`/`时`(看起来像无害虚词)
```
--- FAIL: TestExtractionRetention_PersistentFactsAreNeverDeferred (0.00s)
--- FAIL: …/主人心情不好时会不好
extract_retention_test.go:284: 持久事实 "主人心情不好时会不好" 被判成 "n2_emotion" —— 它会进临时层、7 天后被物理删除(不可逆)
--- FAIL: …/主人不开心时会好好想
extract_retention_test.go:284: 持久事实 "主人不开心时会好好想" 被判成 "n2_emotion" —— …
--- FAIL: TestExtractionRetention_SuffixTableIsPinned (…) 后缀表 token 数 = 63, want 60
FAIL
```
⇒ 语料把「会上线的那个后果」直接指了出来。两处变异均已还原。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
TestExtractionRetention 六个用例共 69 个子例全 PASS。
|
2026-10-13 20:57:31 +08:00 |
|
AskaEth
|
6c30ca540a
|
fix(memory): 冷启动放宽改为一次性 + 试算轮区分「没候选」与「向量不可用」
review round1 的 p1/p2 收口(commit 5967272 之后)。
p1:旧实现 `warmed` 只在 2xx 之后才翻 ⇒ 注释里写的「只在第一次成功响应之前」
实际是「服务坏着的全部时间」。reviewer 实测「接受连接但永不回包」时**每次**调用
都等满 30s(fail-fast 变 fail-slow):审查 20s 预算被一次吃光、索引写入拖成
8×30s、开机探针阻塞 30s。
- `warmupLeft` 改为**一次性许可**(CompareAndSwap 取走即失效,成败都算花掉)
⇒ 挂死时只有第一个请求慢一次,此后一律 DefaultRequestTimeout。
- `warmupTimeout` 30s → 8s:与常态 5s 同一量级(≤2×),且严格小于审查总预算
20s(一次冷启动不许吃光整轮);两条界都由 TestDefaultRequestTimeout 守住。
- 放宽被花掉且没成功时留一条 WARN(事后可复核「第一个请求」这个判据)。
- 更正两处自述:api.go 的「不会被 30s 拖住」与 budget.go 的不变式说明(批次形状
永远按稳态超时推导;冷启动只放宽第一个请求的截止时刻)。
- p2:删掉零调用者的 `WithRequestTimeout` —— 单次超时只允许改常量,批次上限与它
同源并由编译期/常量断言复核。
p2:试算轮在向量源不可用时 `RecallMode` 与「没有候选可比」同为空串。
- `recallLedger` 增 `unavailable` 计数 + `noteUnavailable()`(strictRecall 失败
那一笔记一笔);试算汇总行新增 `recall_unavailable_partitions`,**成败都记**。
- 不扩 `recallMode` 取值集合:失败那一路不序列化 result(响应是
500 tidy_recall_unavailable),新取值只会出现在日志里却要动一次契约,不划算。
测试:三种失败形态 × 两次调用的耗时断言(连接被拒 / 500 / 永不回包),
试算轮的「不可用」与「没候选」两态日志断言;两处变异自证(warmupTimeout 调回
30s ⇒ TestDefaultRequestTimeout 红;去掉 noteUnavailable ⇒ 试算汇总行退化成
recall_mode="" + recall_unavailable_partitions=0)。
|
2026-10-13 20:51:10 +08:00 |
|
AskaEth
|
1d49d266f0
|
fix(channel): message_sent 回声升 T2 落行,出站消息 id 不再被丢
塔转 M12 登记的缺口(塔裁定「归你」,文件都在本片 scope 内):她自己发出消息
的回声(message_sent)被当成「帧级链路遥测」一并豁免 self 升档 ⇒ 停在 T1,
只进计数、不落行,`message_id` 被丢,「她最近发了哪些消息」在库里查不到。
- events.go: 把 MessageSent 从 isFrameTelemetry 的豁免里摘出(它不是帧级遥测:
一帧 = 她真发出的一条消息,回声带的是那条出站消息的 id)。
心跳 / 生命周期的豁免**保持不变**(其登记的迁移债注释原样保留)。
- events_mapping_test.go: 两条 MessageSent 断言同步更新
(:52 自身回声命中自己 ⇒ 由 T1 改 T2;:33 无 self 信号仍走基础档 T1,标签改精确)。
- events_test.go: 新增端到端用例(真 SQLite):self 命中 ⇒ 落 1 行、tier=t2、
MessageID=out-msg-42 留在库里、不进上下文、不驱动 loop。
|
2026-10-13 20:50:31 +08:00 |
|
AskaEth
|
537a8b7988
|
feat(channel): owner 车道结构化参与闸与可调旋钮(乙 = C)
主人 2026-10-13 裁决「乙 C」= 默认按甲(群里不对她说就不开口)+ 可调旋钮。
- inbound.go: owner 不再无条件短路参与闸(去掉 `!actor.IsOwner()` 门),
主人群消息改走 shouldRespond 的 owner 车道分支;主人私聊恒回。
- ownerlane.go(新): owner 车道结构化闸 + 判据
`msg.AddressedToMe || (msg.SelfID == "" && len(msg.Mentions) > 0)`
(未接线渠道退回旧判据,保护 kook/webhook 的群 @);指向她本人必回(硬保留);
非指向消息按每群旋钮——默认只进上下文不回(registerSilenced 落隔离会话)。
「少说点」类关键词落成结构化降频配置,带 source 日志。
- channelbridge.go: Options.OwnerLaneFor/OwnerLaneSet 读写接缝 + Bridge 状态。
- engage.go: 冷避词表并入「少说点」(与既有「少说两句」同义)。
- runtimeconfig/channels_ownerlane.go(新): owner_lane.json 落盘面
(默认档 + 每群覆盖,可读可改,面板可写)。
- wire_channels.go: 注入 owner_lane.json 存储($DATA_DIR/config/)。
- 测试:不对她说就不回(进上下文零回复)/ @她必回(即使降频)/ 未接线退回
旧判据 / 降频生效且可恢复 + 频率闸 / 旋钮读写接缝 / 归属不变量闭合 /
陌生人私聊与主人私聊回归。变异自证:恢复 owner 短路 ⇒ 5 条用例红。
|
2026-10-13 20:48:05 +08:00 |
|
AskaEth
|
b1d51d8de2
|
fix(memory): N2 判据改为白名单形态匹配(结构上不再吞实体)
## 上一版为什么没修完(review round2 举证)
上一版把 N2 实现成**黑名单剥离**:`strings.ReplaceAll` 扫整句、把「情绪词 + 时间词 +
主语 + 程度 + 虚词 + 标点」统统删掉,**残差为空才 defer**。它的要害是剥离表里混进了
**本身就能当实体的词** —— 人称词(`我`/`我们`/`他`/`咱`/`自己`)与可作内容的单字
(`好`/`会`/`要`/`想`/`对`/`在`/`有`/`是`/`时`/`得`/`和`)。宾语恰好由这些词构成时,
实体被吞、残差为空 ⇒ **仍判 defer**:
主人不开心时会想我 → residual="" → defer ✗(持久习惯进临时层,7 天后物理删除)
主人难过时会想我们 → residual="" → defer ✗
主人心情不好时会想他 → residual="" → defer ✗
主人不开心时会想和好 → residual="" → defer ✗(`和`+`好` 两个词项各吃掉一半)
主人难过时会想妈妈 → residual="妈妈" → create ← 对照:宾语没被表吃掉
⇒ 这证伪了上一版代码里声称的不变量(「还剩一个实体就必须放行」),也说明**黑名单这
条路走不到头**:「实体」是开放集合(想我/想我们/想他/想和好/想妈妈/听音乐/咬指甲/
养猫/…),永远列不全。
## 新判据:白名单**形态**匹配
「纯情绪句」的**形态是封闭的**,据此改成「整句恰好落在形态里才 defer,其余一律放行」:
[主语]? [时间]? [程度]* 情绪核心 [程度|否定|情绪|语气]* 余部须只剩标点/空白
实现在 `retentionEmotionFormRest`(唯一判据点):
- 主语与时间词**只允许出现在句首**、各至多一次;
- 情绪核心**必须**消费到一个情绪词,否则 sawEmotion=false ⇒ 放行;
- 核心之后**只**允许「程度 / 否定 / 情绪 / 语气」四类修饰(`retentionEmotionSuffixTokens`);
- 最后要求**整句消费干净**(余部只剩标点/空白);
- 各段一律**最长词优先**(否则 `不` 会抢在 `不错` 前、`好` 会拆掉 `好累`)。
## 为什么它在**结构上**不可能再把带实体的句子判成 defer
1. 判 defer 的门槛是**整句被封闭表消费干净**。表里**没有承载内容的词** —— 没有动词、
名词、量词。任何一个形态外的字(`想`/`会`/`时`/`和`/`我`/`妈妈`/`听音乐`)都无法被
消费,必然留在余部 ⇒ 余部非空 ⇒ **放行**。
2. 主语/时间词**限死句首**:`我`/`他`/`我们` 出现在核心之后时**没有**任何词项能吃掉它
(后缀表里没有它们)—— 这正是上一版翻车的地方,现在从结构上排除了。
3. 失败模式从「**未知实体被吞**(危险、不可逆)」翻成「**未知情绪句没被认出**(安全:
多留,有 T1 与整理兜底)」。方向与 design/23 §18.3 的 fail-safe「宁可多留,不可
误弃」一致。
## 配套测试
- 正例清单补 5 条**对抗形态**(宾语由「能当实体的词」构成):`主人不开心时会想我` /
`主人难过时会想我们` / `主人心情不好时会想他` / `主人不开心时会想和好` /
`主人难过时会想妈妈` —— 与上一轮的 5 条(听音乐/咬指甲/养猫/喜欢吃甜的/找朋友聊天)
合计 10 条「带宾语的情绪句」正例;
- `TestExtractionRetention_EmotionWithObjectIsNotDeferred` 改为断言**余部**(未被形态
消费掉的部分)+最终结论:纯情绪 ⇒ 余部为空 ⇒ defer;带宾语 ⇒ 余部非空(
`时会想我` / `时会听音乐` / …)⇒ 放行。**「带宾语 ⇒ 余部非空」这条不变量现在是被
判据本身保证的**,不再是靠一张表的完备性。
## 修前必红 / 修后绿(原样输出)
修前(把 `judgeRetentionEmotion` 换成 commit 979d1cf 的黑名单实现,逐字):
```
--- FAIL: TestExtractionRetention_EmotionWithObjectIsNotDeferred (0.00s)
--- FAIL: …/主人难过时会想我们 extract_retention_test.go:122: judgeExtractionRetention("主人难过时会想我们") = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人心情不好时会想他 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人不开心时会想和好 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人不开心时会想我 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: TestExtractionRetention_PositivesAreNeverDeferred (0.00s)
--- FAIL: …/情绪+代词宾语(想我) 正例 "主人不开心时会想我" 被拦成 "n2_emotion" —— 把真记忆丢进临时层
--- FAIL: …/情绪+代词宾语(想他) … --- FAIL: …/(想我们) … --- FAIL: …/(想和好) …
FAIL
```
修后:`TestExtractionRetention_{UpperCaseASCII,NegativesAreDeferred,
EmotionWithObjectIsNotDeferred,PositivesAreNeverDeferred}` 全 PASS(共 44 个子例)。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
|
2026-10-13 20:47:59 +08:00 |
|
AskaEth
|
041a6ff504
|
Merge branch 'feat/outboundid-capture-platform-message-id-o' into dev
|
2026-10-13 20:47:55 +08:00 |
|
AskaEth
|
b504df01e0
|
feat(channel): 出站消息 id 采集(可选 seam + qq_obv11 两分支取回)
「接她的话」缺的 A 层:平台给她自己那条出站消息分配的 id,此前在解析的那一刻
就被丢掉(HTTP 只校 retcode、WS 写完不等回声、契约层没有 id 出口)。
- internal/channel/adapter/outbound.go(新):SendReceipt/SendOutcome/SendIDStatus
+ 可选接口 OutboundReceiptAdapter + 调用方入口 SendWithReceipt。**不加进
PlatformAdapter** —— 四个未接线适配器与全部桩实现一行不改(向后兼容硬指标);
也不用回调:id 是某一次投递的属性,必须随那次投递返回。
- qq_obv11:HTTP 从响应信封 data.message_id 取;WS 登记 send_msg 的 echo 等待者
(复用 probe.go 的 action 等待表与 routeActionResponse,不另开通道)。取不到
一律空 id + 三态可分辨(reported/absent/unreported),绝不编造。
- send_msg echo 键改用单调计数器(旧 UnixNano 在 Windows 时钟粒度下会撞键,
撞键即拿不到自己的回执)。
- 既有 SendMessage/SendProactive 路径行为不变(wantReceipt=false 不等待)。
验收:go build ./... + go vet ./... + go test ./... 全绿(cmd/cyrene、internal/webui
的 setup 失败是 worktree 缺 dist 的既有环境限制,base 同样失败)。
|
2026-10-13 20:38:12 +08:00 |
|
AskaEth
|
5daba92d0f
|
docs(memory): 十一键措辞收口(seed* + snapshot*)
tempTtlSec 落地后,`memory-tidy.json` 的播种契约就是**十一键**,而三处文件的注释仍
写「十键」。这些是**过时的陈述**(不是风格问题):下一位读者会从「快照契约:十键非
nil」里读出一个错误的口径,而 `memoryTidySeedComplete` 实际校验的正是十一键。
改的只是措辞,一行逻辑没动:
- internal/config/runtimeconfig/memorytidy_seed.go:25,160
- internal/config/runtimeconfig/memorytidy_seed_test.go:19,27,62,82,97,214
- internal/runtime/memory/tidy_snapshot_test.go:14,27,159
(`cmd/cyrene/wire_memory_tidy.go:65` 还有同类的一处「这十键」,该文件属 M3 的
scope,未动 —— 塔已转 M3。)
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
|
2026-10-13 20:38:02 +08:00 |
|
AskaEth
|
979d1cfbb2
|
fix(memory): 留存规则 N2 改按实体判定(持久习惯不再被误弃)
## 改了什么(行为变更)
`judgeRetentionEmotion` 原判据是「含情绪词 ∧ 无阿拉伯数字 ∧ ≤24 rune」—— 它把
design/03 §10.5 对 N2 的定义「**词项 + 无实体**」里的「无实体」这一维**顶替成了
「无数字」**,方向恰好落在 fail-safe 的反面。
反例(review 走 `judgeIngest` 真链路复现):
主人心情不好时会听音乐 → 旧判据 defer / t0_low_value
主人焦虑时会咬指甲 → 旧判据 defer / t0_low_value
这两条是**稳定习惯**(正向清单),判 defer 即 7 天后从所有读路径消失(读侧时效闸,
design/03 §9.6)、随后被后台清扫**物理删除**(design/23 §19)—— **不可逆**,与
mission 文本写死的 fail-safe「宁可多留,不可误弃」相反。
新判据(设计与 mission 允许,不改设计):**剥掉情绪词 / 时间词 / 主语 / 程度词 /
虚词 / 标点之后,余下为空才判 defer**;只要还剩一个实体(听音乐 / 咬指甲 / 养猫
/ 找朋友聊天)就放行。实现是 `retentionResidual` + 一张**长词优先**的剥离表
(长词优先是必须的:先剥「好」会把「好累」拆成光杆「累」而永远剥不掉)。
配套:
- 剥离表新增「不错」(情绪评价,整词)与光杆「累」—— 只靠虚词表里的「不」会把
「心情不错」剥成「错」、把「主人今天真的特别累」剥成「累」而漏判;
- 去掉 N2 专属的 24-rune 上限(判据恒是「无实体」,长度由全局 40-rune 上限兜住);
- 数字不再单独设防:它不在剥离表里 ⇒ 必然留在残差里 ⇒ 一律放行(与旧
hasRetentionDigit 等价,但由同一条判据统一给出)。
## 为什么这是 p1 而不是精度问题
判错的方向不对称:漏判只是噪声留进长期层(有 T1 与整理兜底),误弃是**不可逆的
数据丢失**。因此 N2 宁可漏判,绝不误弃。
## 配套测试(此前这条缝**完全没被覆盖**)
- 正例清单新增 5 条「带宾语的情绪句」:`主人心情不好时会听音乐` /
`主人焦虑时会咬指甲` / `主人心情低落时会养猫` / `主人压力大的时候喜欢吃甜的` /
`主人难过时会去找朋友聊天`(旧正例里没有这一类样本,所以旧缺陷一路全绿);
- 新增 `TestExtractionRetention_EmotionWithObjectIsNotDeferred`:逐条断言**残差**
与最终结论 —— 带宾语 ⇒ 不 defer、纯情绪 ⇒ defer,两者必须给出相反结论;
- 新增反例 `主人今天真的特别累`(纯情绪、无实体)钉住「不是靠长度躲过」。
修前必红 / 修后绿(把 N2 退回旧判据后原样输出):
正例 "主人心情不好时会听音乐" 被拦成 "n2_emotion" —— 那是把真记忆丢进临时层
正例 "主人焦虑时会咬指甲" 被拦成 "n2_emotion" —— …
judgeExtractionRetention("主人心情不好时会听音乐") = "n2_emotion", wantEmotionDefer=false
--- FAIL: TestExtractionRetention_PositivesAreNeverDeferred
--- FAIL: TestExtractionRetention_EmotionWithObjectIsNotDeferred
还原后:TestExtractionRetention_{UpperCaseASCII,EmotionWithObjectIsNotDeferred,
NegativesAreDeferred,PositivesAreNeverDeferred} 全 PASS。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
|
2026-10-13 20:37:57 +08:00 |
|
AskaEth
|
3f266c26b2
|
Merge branch 'feat/speakerattr-attribute-group-speakers-and' into dev
|
2026-10-13 20:37:19 +08:00 |
|
AskaEth
|
e32d85afc3
|
docs(api): 契约 v0.91 —— recallMode 如实上报(新增 mixed/空态语义)
起因:M6 诊断发现旧实现把「向量路径实际失败、回退词项」静默报成 embedding,面板看不出降级。M9 改为上报实际选用的召回路径,取值域扩 mixed(部分分区降级)与 ""(无候选可比)。同步:版本行 + §6 v0.91 条目 + §4.12 的 recallMode 值表;并记录「客户端 5s 超时未改、改的是批上限推演」这条对客户端透明但重要的取舍。
|
2026-10-13 20:37:17 +08:00 |
|
AskaEth
|
91e4dd2fe1
|
fix(channel): 静默登记同源第三方标注 —— 无昵称非主人不再被读成主人
review p1:回帖路径已改用 thirdPartySenderName(非主人恒有标注:昵称 → actor
uid → 「未署名发送者」),但 registerSilenced 仍用 senderDisplayName,非主人
无昵称时写出空串;静默行会被 transcript 读回她的上下文,而受限会话里空标注就是
「主人本人」⇒ 在 senderName 可选且无兜底的渠道(webhook)上,一条没昵称的陌生人
消息被渲染成「[发言归属] 以下发言来自主人(管理员账户)本人」。
- light.go:270:senderDisplayName → thirdPartySenderName,与回帖路径同源。
senderDisplayName 未成死代码:仍被 light.go:223(LightJudge 输入)、
engage.go:284、flowgate.go:183 使用。
- 新增 TestSilencedNamelessMemberIsNotReadAsTheOwner:走真链路(静默登记 → 落库
→ 下一回合播种转录 → loop 归属渲染 → provider 请求),钉住「无昵称的非主人经
静默登记后,读回上下文时不得出现主人标注」。修前必红(写入口径与模型侧两条
断言同时报,含被读成主人的原样请求),修后绿。
验证:go build ./... / go vet ./... / go test -count=1 ./...(全树)全绿。
|
2026-10-13 20:34:04 +08:00 |
|
AskaEth
|
59672720bd
|
fix(memory): embedding 批上限与超时同源 —— 不再「客户端先放弃、服务端继续算」
M9(EMBRECALL)。故障机制(M6 + 本次复测):客户端 5s 单次超时先放弃,服务端仍在
算那一批 ⇒ 本机 llama.cpp 被自己顶住 ⇒ 后续每个请求(含写向量索引的单条请求)
排队到自己的超时 ⇒ 召回全线降级。
- embedding/budget.go(新):把「一次请求的耗时上界 ≤ 单次超时的一半」写成不变式,
批上限(条数 4 / 字符 480)与 5s 超时由**同一个不等式**解出;实测依据与复跑命令
写进注释(空闲态摊销曲线 1→0.19s / 8→1.1s / 12→1.3s / 64→8.5s,约 50ms 固定 +
2.7~3.9ms/字符,n≥32 起有数倍放大 ⇒ 余量取一半)。
- embedding/api.go:Embed 按同一对上限**自己切片**(不变式与调用方无关);冷启动
首次调用单独给 warmupTimeout,拿到第一个成功响应后立刻回常态超时。
- memory/tidy_config.go:tidyEmbedBatch = embedding.DefaultBatchLimit(同源),
并加编译期断言 —— 调回 64 会直接编译失败(constant -60 overflows uint)。
- memory/tidy.go | tidy_recall.go | tidy_dryrun.go:recall_mode 改报**实际发生**
的来源(""=本轮没候选可比 / embedding / lexical / mixed),新增
recall_degraded_partitions;试算轮 RecallMode 同口径(M6 medium finding)。
- 测试:embedding 侧用「实测曲线 + 会睡够时间的假客户端」钉住两态
(批内 < 超时 ⇒ 不降级;旧配对 64 条 ⇒ 必然超时),并断言 abandoned == 0;
memory 侧钉住降级/健康/没候选三态与试算轮的如实上报。
不改 preinsert*、不改 .env、不动判定规则与任何阈值。
|
2026-10-13 20:33:26 +08:00 |
|
AskaEth
|
d4ed043d5c
|
test(memory): 修播种日志用例的偶发假红 + 补 TempTTL 接线缝断言
两件都是**测试面**的补件,不碰被测逻辑。
① 既有 flake(internal/config/runtimeconfig/memorytidy_seed_test.go):
TestSeedMemoryTidy_LogsPathAndReasonWithoutValues 原先用**裸数字**扫**整行**日志找
4242/10800/1800,而同一行合法地打了 `path=<临时目录>`,`t.TempDir()` 的目录名带
随机数十进制 ⇒ 偶发命中(约个位数/10000),表现为假红。
修法取「断言带字段名的形态」:哨兵改成 `=4242` / `=10800` / `=1800` / `=259200`
(slog 的 `key=value` 字段形态),路径里的随机数前面是路径分隔片段而不是 `=`,
因此不受影响。
**为什么不用另一个备选「只扫 keys= 之后的内容」**:那会把断言缩成「keys 列表里没
有值」,任何以别的字段名打出来的值(`interval_sec=4242`、将来新加的字段)都会溜
过去 —— 等于把这条防护改成空壳,而不是修掉它的假红。
顺手把 tempTtlSec 的哨兵值 259200 一并纳入(上一切片给 completeSeedTuning 加了这
个键,漏掉它会让「不打配置值」在这一个键上失去覆盖)。
② 缝断言(cmd/cyrene/wire_memory_review_test.go):
TestMemoryReviewIsWired 的 seams 表补上 `a.memoryTempTTL()` 与 `TempTTL: tempTTL`
—— 上一切片把面板 `tempTtlSec` 接进 ReviewConfig 时,这条缝没有被覆盖,「接线被
摘掉」不会红。
自证:
- ① 前后对比(确定性,把临时目录名强制含 1800):旧形态 FAIL(「日志里出现了配置
值 1800」)、新形态 PASS;另跑 `-count=20` 全绿。
- ② 变异:把 helper 与调用点一起改名 ⇒ `wire_memory_review_test.go💯 找不到
"a.memoryTempTTL()"` FAIL。
|
2026-10-13 20:26:46 +08:00 |
|
AskaEth
|
8ab241f627
|
feat(channel): 群聊发言人归属落进模型上下文(user 侧也产出标注)
裁决 06.4.1 行动项①②落地(主人 2026-10-13 定稿:结构化归属 + system 标注,
不把元数据拼进用户正文)。
- loop/attribution.go:发言人归属从「只认 assistant」扩到 user 侧,按三条
结构事实各出一条边界说明 —— assistant 的角色(她自己 / 别的角色,措辞逐字
不变)、受限会话里带昵称的第三方人员(明标「不是主人」)、以及不带昵称的
主人本人;受限会话里未标注的 assistant 历史按 design/22 §7-4 归到本会话角色。
标注仍只新增 system 消息,正文一字不改。
- channelbridge/inbound_speaker.go(自 inbound.go 拆出):第三方发送者标注车道
thirdPartySenderName —— 主人恒返回空(主人从不被标注),其余昵称缺失时退到
平台 uid 再退到「未署名发送者」,绝不落进「空标注 = 主人」那条车道;
thirdPartyBoundaryPrompt 删掉「只有未标注昵称的消息才是管理员(主人)发言」,
改为「谁在说话以「发言归属」标注为准」。
- channelbridge/source.go:来源行删掉「(非管理员账户=第三方人员,未标注昵称=主人)」。
- 主人本人在群里的消息落库不再写昵称(sender_name 空 = 主人),使转录里同一句话
的归属与当回合一致。
验证:go build ./... / go vet ./... / go test ./...(含全树)全绿;
变异自证:把 user 侧归属恢复成不产出 ⇒ 3 条 loop 用例 + 1 条渠道端到端用例变红。
|
2026-10-13 20:24:21 +08:00 |
|
AskaEth
|
a6f3817061
|
Merge branch 'feat/addressto-add-addressed-to-me-signal-and' into dev
|
2026-10-13 20:23:43 +08:00 |
|