AskaEth
|
f37fe05aee
|
Merge branch 'feat/m42-logchannel-channel-and-tool-url-plus' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 14:03:12 +08:00 |
|
AskaEth
|
abbd79a3b1
|
Merge branch 'feat/m43-logupstreamadopt-adopt-reviewed-comm' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 14:02:22 +08:00 |
|
AskaEth
|
4b4173dd42
|
fix(channel): M41 r1 复审三条 —— telegram 构造分支、QQ 改 MaskQuery、删两处死上限
P2-1 telegram doJSON 的**请求构造**失败分支:http.NewRequestWithContext 解析失败的
err 同样是 *url.Error(里面是整条 /bot<token>/… 地址),而 poll.go 的 long poll
failed 会原样落日志。改成与传输层分支同形:trace.HostOf(endpoint) + callCause。
P2-2 qq_obv11 connectClient 的成功 INFO 从 maskAccessTokenQuery 换成
trace.MaskQuery:后者认得 access%5Ftoken / token / api_key / 签名参数 / userinfo,
且保留 EscapedPath(路径可读)与不截断。maskAccessTokenQuery 仍被
sanitizeConnectReason 使用,未成死代码。
P2-3 删掉 maxEnvelopeMessageRunes / maxDescriptionRunes(各 200):trace.RedactText
内部已 truncate 到 maxSummaryRunes=80,这两层永不命中,注释口径与实际不符。改为
直接 return trace.RedactText(压平空白),注释写明 80 rune 由 RedactText 负责。
用例(全部变异打红,逐条验证):telegram 构造分支 token、QQ 八形态凭据矩阵
(含四种 maskAccessTokenQuery 漏掉的形态 + 路径可读反向断言)、两处 80 rune 上限。
|
2026-10-26 13:58:19 +08:00 |
|
AskaEth
|
9e88605cc8
|
fix(voice): CloneVoice 的 200-with-message 泄漏收口到 dashscopeOKBodyError
M41 同族的最后一条缺口:HTTP 200 的响应里用顶层 message 报业务失败,而这句话
是上游自由文本 —— create_voice 的 input.url 是参考音频的公开地址(可能是预签名
URL,带凭据),上游报错常把它原样回显。
- ERROR 只留「打到了、但 200 是个业务失败」这个形状事实 + 体积摘要;
message 原文不进 error,code/type 若真的带回来也只以令牌形态出镜
- errors.Is(err, ErrProviderFailed) 不变(熔断口径不动)
- 用例断言失败仍被识别、且 error 文本里搜不到哨兵正文
|
2026-10-26 13:50:10 +08:00 |
|
AskaEth
|
0f9251b983
|
fix(channel): 渠道适配器与出站工具的上游响应体/带凭据 URL 不再进日志与错误(M41)
接 M38 的 18 条真泄漏里「渠道适配器 + 出站工具」这一族(#10 #11 #12 #13 #15 #16)。
只动日志与 error 文字:发送/接收、解析口径、重试次数一个都没改。
- httpclient.do:非 2xx 不再把 512B 响应体原文拼进 error,改 trace.UpstreamError
(状态/体积/sha256 前缀)+ 信封 code/message(RedactText + 200 rune);error 里的
target 改 trace.HostOf,并剥掉 *url.Error 外壳(它带整条地址)
- telegram doJSON:4xx 只解 error_code/description(清洗 + 截断),体其余字段不拼;
传输错误只留来源 —— Bot API 把 token 写在地址路径里
- weixin_ilink doJSON:4096B 体 ⇒ trace.UpstreamError,rawURL ⇒ mediafetch.HostOf;
uploadOnce 剥掉预签名上传地址(encrypted_query_param),重试日志补来源主机
- qq_obv11 connectClient 成功 INFO:改走同包已有的 maskAccessTokenQuery
(OneBot 惯例把 ?access_token= 写进 remote_url)
- plugins/tools/io http 拦截日志:改记 trace.HostOf + 原因码,不记原 URL、也不记错误
原文(解析失败时它包着 *url.Error)
每条都有能变异打红的用例:QQ/iLink 预签名地址的哨兵、三处 4xx 体哨兵、
user:pass@ 与 query 凭据;八处变异逐条验证变红。
|
2026-10-26 13:47:56 +08:00 |
|
AskaEth
|
66dda084cd
|
fix(runtime): 上游错误体统一走 trace.UpstreamError,正文不进 error
M41 的「LLM / embedding / 视觉 上游错误体」一族(M38 #5 #6 #7 #8 #9 #14):
非 2xx 时把响应体截断后拼进 error,而 error 会被 chat run / loop step /
retrieve_fallback 打日志,等于把她的提问、记忆正文、要念的台词写进日志。
- model:openai/anthropic 两处 upstreamError 收口到 upstream.go 的
providerUpstreamError;anthropic 流内 error 事件的 evt.Error.Message
(原本无上限)整段丢弃,只保留令牌形状的 type
- embedding:readErrorBody 删除,改走 helper(input 就是她的记忆正文)
- vision:错误路径不再沿用取图的 50 MiB 上限,改为 4 KiB 有界读且不拼原文
- voice:classifyDashscopeStatus 只解 code/request_id,正文只留在内存里做
分类判据;错误体读取同样改为 4 KiB 有界
- 测试:五个上游各一条「日志/error 里搜不到哨兵正文」用例,且能变异打红
|
2026-10-26 13:44:32 +08:00 |
|
AskaEth
|
a697fa0a1e
|
Merge branch 'feat/m35-ttsemo-emotiontts-provider-with-emot' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 13:44:00 +08:00 |
|
AskaEth
|
48b3dae5ed
|
Merge branch 'feat/m37-memtier-retention-tier-column-and-de' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 13:42:20 +08:00 |
|
AskaEth
|
b90811fae4
|
docs(voice): 收口「没会话/没绑定就走 default」的镜像注释(r4)
唯一 finding:audio.go 的 speechRequest.SessionID 注释原本写「省略它时本口没有会话
概念 ⇒ 走 emotionTTS 的 default 分支」——**反了**。真实链路是
effectivePersona(ctx,"") → sessionPersona("") 为空**不是终点** → Effective =
defaultPersonaID() → 常规安装必有全局默认人格 ⇒ personaId 非空 ⇒ 走 personas 映射。
照那句话改的人会写出 `if req.SessionID == "" { personaID = "" }`,正是 r2 修掉的病。
改为:省略时按**全局默认人格**选发音人(会话绑定 → 全局默认人格 → 两者都没有才落到
default 字符),并显式写了"不要写成 if SessionID == \"\" { personaID = \"\" }"。
顺手同根三处(不留第四轮):
- tts/config.go 的 Config.Default 注释:成因表「会话无绑定 / 读取失败 / 该出口本身
没有会话」已过时 ⇒ 改为「空串的唯一合法成因是连全局默认人格都没有」,并写明不是
"会话未绑定"、也不是"出站口没有会话"。
- runtimeconfig/voicetts.go 的 Default 字段注释与文件头同族措辞同步;
- selection.go 的 WithPersonaID / Resolve 分支表、service.go 的分支小标把
「解析不到」统一改成「连全局默认人格都没有」;
- voice_emotiontts_test.go 的 TestSpeechEndpointNoSessionUsesDefault 注释同步(该用例
没接人格库,所以确实落到 default —— 但成因写对了);
- wire_voice_tts_test.go nit:改成「已废弃的全局 `character` 不生效」(墓碑键仍在)。
纯注释:`git diff` 里非注释改动行为空。go build/vet 通过;四包 go test -count=1 全绿。
|
2026-10-26 13:40:00 +08:00 |
|
AskaEth
|
46a6da13e0
|
Merge branch 'feat/m40-logtrace-span-write-side-redaction-p' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 13:36:31 +08:00 |
|
AskaEth
|
066319f9c6
|
fix(memory): 复审 r2 返工 —— 真实「记住 X」路径钉 long,断言改为能打红
复审 r2 = p2-2items 全在「RememberManual 钉 long」那半,两条指向同一件事:
**我以为修好的人工路径实际没修**。逐条:
P2-1 上一版断言是**空断言**:夹具正文「记住她喜欢喝咖啡」含「记住」⇒
extract_retention.go 的护栏① 直接放行 ⇒ 默认落点本来就是 long,因此删掉
store.go 的 pin 仍然绿。
→ extract_test.go 的 TestStore_RememberManual_正常 改为一对语料:
「我正在测通道」(N7,规则真会判 temp)+「记住她喜欢喝咖啡」(正文保留契约),
并同时断言档位、`ExpiresAt` 零值、以及**落库列** `(retention_tier, expires_at)`。
P2-2 pin 钉在零生产调用方的方法上:真实路径是 ManualExtractor.Extract(无可用
LLM 出口时的生产回退)→ persistMemories → AddScoped → Add,而 rememberDirective
**先剥掉「记住」前缀** ⇒ 护栏① 永远看不到它。改前实测:记住今天下雨了→temp/n6、
记住我正在测通道→temp/n7、记住现在几点了→temp/n4(均 +7 天)。
→ extract.go 的 ManualExtractor.Extract 在**产出侧**显式置
`RetentionTier: RetentionLong`(并在注释里写清「不能靠护栏①」的原因)。
store.go 的 RememberManual pin 保留(两条独立的契约点)。
→ 新增 TestManualExtractorItemsStayLongTerm:四条正文过 Extract →
复刻 persistMemories 的溯源覆盖 → AddScoped 落库 → 断言**落库列**
`(long, 空串)`。
**变异证据(删掉两处 pin 后实测)**:
--- FAIL: TestManualExtractorItemsStayLongTerm
extract_test.go:167: 「记住今天下雨了」全链落库 =
(tier="temp", expires_at="2026-11-02T05:34:20.844624200Z"), want (long, 空串)
--- FAIL: TestStore_RememberManual_正常
extract_test.go:99: RememberManual("我正在测通道") 的档位 = "temp", want "long"
恢复 pin 后两条均 PASS。
回归:go build ./... + go vet ./... + go test -count=1 ./internal/runtime/memory/...
./internal/db/... ./cmd/cyrene/ 全绿;go test -race ./internal/runtime/memory/ 绿。
|
2026-10-26 13:36:15 +08:00 |
|
AskaEth
|
1c2944fcf4
|
docs(voice): 收回 r2 修掉的旧语义注释 + 让关闭态变异真能打红(r3 P2-1/P2-2)
P2-1(两处注释在描述已被推翻的旧语义,会把 r2 的修复写回去):
- voice.go 的 voiceReplySink 文档:原写「personaID = env.PersonaID,会话无绑定时为空」
⇒ 实际传的是 turnPersona.Effective,**未绑定 ≠ 空**(有全局默认人格时非空)。
改为写明「生效人格(会话绑定 → 全局默认人格;两者都没有才是空)」,并显式指出
它不是 env.PersonaID、把空当"没有角色"会让主会话落到 default(正是 r2 修掉的)。
- voice.go 的 synthesizeVoiceReply:原写「空 personaID = 会话无绑定 ⇒ 走 default」
⇒ 反了。改为「空 = 连全局默认人格都没有 ⇒ 才走 voice-tts.json 的 default」。
- 同族 format 残留措辞:client_test.go 的「非法格式的回落」(该行为已不存在)、
cmd/cyrene/wire_voice_tts_test.go 的「角色/格式/超时按文件来」都改写。
- nit:voice.go 的 `env, turnPersona := ...` 遮蔽同包类型名,改成 `tp`。
P2-2(我上轮那条「摘掉 PrimaryActive 闸 ⇒ 用例红」不成立,复审实测仍绿):
根因是 audioStubVoice 不实现 PrimaryActive,拦住 persona 读取的只有类型断言。
按塔选的第二种收口(把测试做强):
- audioStubVoice 补 `PrimaryActive() bool { return true }`(它是"生效中的 persona
voice"替身),并在注释里说明它存在的理由;
- 关闭态用例改用新替身 inactivePersonaVoice(实现 personaVoice 但报 false),
于是 PrimaryActive 成为关闭态唯一的闸;
- 重跑变异(摘掉 `&& pv.PrimaryActive()`)**实测打红**:
「未生效的 personaVoice 却查了 1 次会话人格(关闭态行为变了)」。
未动:功能面、错误码、500 状态码、白名单、session 可读、隐私/format/Stats/redactURL。
go build ./... + go vet ./... + go test -count=1 -race 覆盖 tts/runtimeconfig/gateway/
cmd/cyrene 全绿。
|
2026-10-26 13:32:04 +08:00 |
|
AskaEth
|
542bac677d
|
fix(trace): UpstreamError 默认不带 error.message,要带必须显式 opt-in
复审 P2 收口。上一条留的漏法(不是理论,是构造得出的):
`{"error":{"code":"invalid_request_error","message":"Invalid value: <用户原文>"}}`
—— 默认保留前 128 rune,那 128 rune 就会**原样**进 error 与日志。此前的用例只钉了
input/echo 这类 sibling 字段,没钉到 message 这一面;而 M41(embedding input 是她的
记忆正文、DashScope TTS input 是她要念的话)与 M42(渠道响应体)都把这个 helper 当
模板,默认带 message 等于把洞复制到十几处。
改法(默认安全,放宽必须写出来):
- `UpstreamError(upstream, status, contentType, body, opts ...UpstreamOption)`:
最常用的四参调用**天然是最安全的那个**;默认只留 Upstream/Status/ContentType/
BodyLen/BodySHA256/Code/Type;
- 新增 `WithUpstreamMessage()` 作为显式开关:带 message 时**必须**过 RedactText
(凭据形状打码 + 压平空白)再截到 maxUpstreamMessageRunes,并在注释里写明
「只有在你确认该上游不会回显用户内容时才可用」;
- 文件头写清取舍:**宁可少一条人话提示,也不让她的原话进日志**;排障靠
status + code + type + bodyLen + sha256 通常足够分类(400 还是 429、是
model_not_found 还是 context_length_exceeded、是不是同一份报文),细节要看时
报文在调用点上下文里还在。
测试(可变异打红):
- 新增 TestUpstreamError_DefaultDropsMessage:message 里塞哨兵的 400 ⇒ 默认构造下
error 与 slog 输出都搜不到哨兵,Message 为空,而 status/code/type/bodyLen/sha256
仍可辨;Attrs() 也不旁路带出;
- 新增 TestUpstreamError_OptInMessageIsSanitized:opt-in 后 message 被截到 128 rune
且凭据被打码(超长 + 带 token 的 message);
- 既有 TestUpstreamError_DropsEchoedBody 补一条「默认 Message 为空」;
- 变异验证:把默认构造改回带 message ⇒ 前两条变红;让 opt-in 跳过 RedactText ⇒
opt-in 那条变红。
回归:go build ./... + go vet ./... + go test -count=1 ./... 全绿(backthink 的两条
2s 定时用例在满并发下偶发超时,单元重跑与整仓重跑均绿,与本改动无关:该包不引用
trace);trace 包另跑 -race 绿。只动 upstream.go 与其测试,白名单/session/loop/
orchestration 未动。
|
2026-10-26 13:31:11 +08:00 |
|
AskaEth
|
765ef167bd
|
fix(memory): 复审 p2 返工 —— SQL 空串字面量、两处过期注释、人工路径档位
复审 p2-1items(tip cc6904b)要求的三处,逐条:
1) SQL 双引号改单引号(唯一返工项):
- preinsert_store.go 的 revertMergedSQL:`merged_into = ""` → `''`。今天功能等价,
但靠的是 SQLite「双引号标识符解析不到 ⇒ 回退成字符串字面量」的 legacy 行为;
这是一条**改数据**的语句,还被 RevertMerged 与整理侧回滚共用,不该依赖回退。
- retention_migration_test.go 的 `WHERE retention_tier = ""` 一并统一成 `''`,
并在原位留下「为什么」。
2) 两处过期注释:
- retention_schema.go:「反过来(先自举、后迁移)也成立」是**假结论**,改为
「生产顺序成立;反向顺序在 0019 处即失败(duplicate column name: status,既有问题)」。
- store.go 的 ExpiresAt:「本字段目前只写不读」已半过期(Add 现在会把最终落库
时刻回填到返回值),改成「读回语义是半开的」:Add 返回的条目有值,从库里读回的
恒为零值。
3) 人工路径(塔裁定「人工动作默认应为 long」):包内的 `RememberManual`(「记住 X」)
显式钉 `RetentionTier = long`,留存规则因此不许把人工明确要求记住的东西降成临时层
(extract_test.go 加断言)。面板网关那条(adminAddMemory 直调 Add、不传档位)
按塔的口径不在本片、留给后续切片 —— 本片对它的唯一影响见回报。
不动(按塔的裁定):preinsert.go 514 行(dev 既有 tech debt)、
design/03 §10.5 与 design/23 §18 的章节(塔的账)、gateway 人工路径。
回归:go build ./... + go vet ./... + go test -count=1 ./internal/runtime/memory/... ./internal/db/...
全绿,内存包 -race 亦绿。
|
2026-10-26 13:24:35 +08:00 |
|
AskaEth
|
e684061e56
|
fix(voice): 主/子系统会话的发音人跟「生效人格」走(r2 P2-1)+ 错误码可区分
P2-1(实质):主会话(web)的 persona_id 恒为空且无重绑路径,而"空"的真实语义是
「继承全局默认人格」(loop/persona.go 回退 Options.PersonaID;前端 lib/persona.ts 的
bound ?? defaultPersona(personas) 是同一口径)。此前只有 TTS 把空读成"没有生效人格"
⇒ 落到 voice-tts.json 的 default:主会话聊天是 furina、声音却是 default 那个人,
换全局人格后也不跟声 —— 正是主人抱怨的那件事,且发生在唯一能给普通聊天出声的路径。
修法:
- turnEnvResolver 新增 resolvePersona/effectivePersona(绑定 → 全局默认人格 → ""),
默认人格扫描抽成 defaultPersonaIDFrom 一处(*Server.defaultPersonaID 转调它);
- ownerWithPersona 复用**同一次**会话读取同时给出 loop 用的 binding 与呈现用的
effective —— WS 语音回合为选音色不新增任何 I/O;
- REST:现有闸内改用 effectivePersona;WS:voice.go 那次 env 解析处取一次传给 sink。
因此 `default` 只剩"连默认人格都没有"这一种用途。
- service.go 那句"换人格后用老角色声音不可能发生"收回为限定说法:本包无缓存、无兜底
角色、未映射即拒绝,但**调用方必须传生效人格**(网关两处已修)。
回归锁:voice_persona_inherit_test.go 复现复审用例(名字不同、两条口各一条)——
未绑定会话 + 全局默认 furina + default 指向 cyrene ⇒ 必须用 furina 的声音;
变异(effectivePersona 退回「空 = 无」)实测两条都红,报文与复审一致。
建议 3(顺手做):persona 未映射时状态码仍 500(服务端配置缺失),但错误码换成
tts_persona_unmapped / tts_no_character,不再与一般合成失败混成一个码;
用例 TestSpeechEndpointPersonaUnmappedErrorCode 钉住。
P2-2(nit):voicetts.go 开头删掉「格式、」两字(该键已删除)。
go build ./... + go vet ./... + go test(含 -race)全绿。
|
2026-10-26 13:21:44 +08:00 |
|
AskaEth
|
6d0df6a825
|
fix(trace): 白名单放行 session —— 归属标识可读,正文边界不动
主人 2026-10-26 裁定:session 是**业务标识**,不是正文。它是排障时把一条 stderr
日志对回某个会话的唯一线索;而 /admin/flow/graph 本来就按会话返回、连 sessionId
都给了,图里多一个 session 不构成新的暴露。因此把 session 加进共享白名单
(safePropKeys,走 redactValue 的普通清洗),不再被写侧折成 Digest。
「不放宽白名单」仍是原则:本次只放行这一个键,正文的边界一个字没动 ——
- trace 侧新增 TestJSONRecorder_KeepsSessionReadableDropsBody,**一条用例同时钉
两件相反的事**:JSONL 里 `"session":"web#lane-2"` 原文可读;同一 span 里的
result / text 哨兵正文仍然搜不到,且只在 omitted 里留下体积与指纹。
- loop 侧真循环用例补一句断言:落盘里 `"session":"sess"` 可读,而工具结果哨兵
同时仍不可见。
- 变异验证:把 session 从白名单摘掉,上述两处都会变红(session 落到 omitted)。
回归:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
trace/loop/orchestration 另跑 -race 全绿。
|
2026-10-26 13:20:45 +08:00 |
|
AskaEth
|
003d12a712
|
feat(trace): M40 span 写侧脱敏 + 共享上游错误收敛 helper
stderr 的 JSONL 是无主人在场的落盘面,却在此前原样落工具结果(result)、
子代理回复(text/summary)与上游报文,等于一次 tail 就能拷走整段对话。
本切片交付两件地基:
1) 写侧脱敏(trace.JSONRecorder)
- 新增 trace/span_props.go:safePropKeys 白名单与 projectSpanProps 投影,
读侧(safeSpanProps)与写侧共用同一张表、同一个投影函数;
- WriteSpan 落盘前经 trace.SanitizeRecord 投影:白名单键清洗,表外键只留
Digest(bytes/runes/sha256 前缀,收进 omitted),事件先 RedactEvent 再把
正文键折成 Digest。落到磁盘的只有元数据,正文一个字节都不留;
- 新增 trace/digest.go:Digest/DigestOf/DigestOfValue,给「脱敏后仍要能诊断」
一个构造上不含正文的取值形态(体积 + 指纹),两条投影都按类型放行它。
2) 共享上游错误体收敛 helper(给 M41/M42,禁止三家各写一份)
- trace.UpstreamError(upstream, status, contentType, body):只保留 status /
contentType / bodyLen / body sha256 前缀 / 上游 JSON 的 error.code /
error.type / 截断 128 rune 的 error.message,其余一律丢弃,响应体原文绝不
进 error 或日志;*UpstreamErrorInfo 提供 Error()/Attrs()/Unwrap()(errors.Is
可判 ErrUpstream 走降级链);
- trace.HostOf / trace.MaskQuery:日志里的 URL 只回来源,凭据类 query 与
fragment 值打码为 ***,user:pass@ 整段打码。
3) 剥掉三处正文上 span
- loop:tool span 不再放 result,改放 result_summary(与 tool_execution.end
帧同源、已脱敏截断)与 result_digest;
- orchestration:subagent span 不再放 text,subagent.done 事件不再放 summary,
改记 status/steps/chars/sha256;
- automations:AddRuleByName 不再 %v 整个 payload,改记 subject/type/bytes/sha256。
测试:以哨兵正文在落盘字节上断言(JSONL / slog 输出),并分别对「编排层收到的
原始 span」与「写侧投影」两层各钉一遍;UpstreamError 用回显 input 的 422 报文
与 HTML 错误页各钉一条;解析出/未解析出 JSON 都不回原文。以上均做过变异验证
(去掉保护即变红)。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
trace/loop/orchestration 另跑 -race 全绿。
|
2026-10-26 13:17:42 +08:00 |
|
AskaEth
|
cc6904b1b4
|
feat(memory): retention_tier 档位列 + defer 与档位合并为同一判定(M37)
主人 2026-10-26 对「记忆分型」裁定「两列」:kind(关于什么)保留,新增
retention_tier(留多久 / 干什么用)。本片把这条轴从列、判定、写入、读侧过滤
一路打通,并把「判第五态 defer」与「判 retention_tier=temp」合并成一个判定。
结构(迁移 0026 + 自举前滚):
- memory_items 新增 retention_tier TEXT NOT NULL DEFAULT ''(空串 = 尚未分型,
历史行与受限来源行),索引 idx_memory_items_retention_tier;up/down 均实跑过
(在隔离的生产库副本上 migrate → 建 Store → down → 再 up,5073 行零丢失)。
- NewStore 幂等前滚该列 + 建索引(ensureRetentionSchema),与迁移同形由测试钉住。
判定(retention.go,全仓唯一入口):
- RetentionTier 闭集 temp/daily/work/long/knowledge + 空串未分型;与 kind 正交。
- resolveRetention 的优先级:调用方显式档位 > 显式有效期(⇒ temp)> 受限来源
不分型 > 留存规则 N1~N7 ⇒ temp > 默认 long。判据靠元数据,规则不许推翻显式档位。
- tempDeadline 是「档位 → expires_at」的唯一换算:temp ⇔ 有效期非空,两列同一笔
INSERT 落下(M2 登记过的「INSERT 与补写之间的短窗口」就此关闭)。
合并(defer ⇔ temp):
- judgeIngest 不再跑留存规则,只读 Add 落定的 item.RetentionTier;规则调用点因此
全仓唯一(源码级断言钉住)。
- 判定不再依赖同步闸是否开启:闸未接/关着时档位与有效期照样落库。
- 异步层对已是 temp 的行不重复改写(避免 TTL 随审查延迟漂移)。
- IngestRuleVersion d3.3 → d3.4(判据作用范围变了,按既有硬规矩升版)。
读侧:
- 四条列表读路径的 SELECT 与 scanItem 读出 retention_tier(14 列);Get 同样读回。
- 新增 ListByPersonaTiers / ListScopeTiers / ListTiers / ListProjectTiers(可选档位
过滤,缺省不过滤 ⇒ 历史行照常返回;显式传空串只取未分型;非法档位报错)。
原方法签名逐字不变 —— 网关 MemoryStore 接口钉着它们,可变参数会编译不过。
- store.go 的读路径拆到 store_list.go,守 500 行上限。
回归:go build ./... + go vet ./... + go test ./... 全绿(内存包另有 -race)。
|
2026-10-26 13:09:30 +08:00 |
|
AskaEth
|
8820b0b316
|
fix(voice): 修 r1 复审四条(正文进日志的隐私泄漏 / format 假开关 / Stats 无消费者 / redactURL 兜底)
上一版把 r1 的三条 finding 静默漏了(我在交付前没读 inbox),本提交逐条补齐:
1. 🔴 隐私:upstreamStatus 不再读也不记响应体
FastAPI 的 422 会把请求体里的 input 整段回显,原来「错误体前 512 字节进 WARN」
等于把她的原话写进日志。现在只记 status / content_type / response_bytes
(字节数优先取 Content-Length,缺失时有界排空取上界,读到即丢)。
新用例 TestSpeechRejectLogOmitsRequestText 用**回显正文**的假上游抓它;
变异(把 body 塞回 WARN)实测打红。
2. format 键彻底删除(假开关)
上游只拿 response_format 改自报 media_type,产物恒 .wav 无转码,我方只按字节
嗅探裁决 ⇒ wav/mp3/ogg 交付字节与响应头完全相同。删 tts.Config.Format 与
VoiceTTSTuning.Format,encodeSpeech 硬编码 requestFormat="wav"(不可配置)。
用例钉住请求体恒为 wav;变异(改成 mp3)打红。旧文件里残留的 format 键被忽略。
3. Stats 接进 degrade 的 WARN(attempts/failures)
否则统计在生产日志里没有消费者。TestServiceDegradeLogsCounters 钉住;
变异(去掉两个字段)打红。
4. redactURL 兜底含 "@" 整体打码
解析失败/无 host 时原来原样返回,user:pass@ 会漏凭据。TestRedactURLCredentials
覆盖合法 URL 去 userinfo、非法含 @ 打码;变异(退回原样返回)打红。
另:关闭态零行为变化的**当前 tip 重跑**——
- handleSpeech 的 persona 解析改为只在语音服务实现 personaVoice 且 PrimaryActive
时才查库;未接 emotionTTS(既有链/stub)时零额外 I/O、ctx 不改、音频字节相同。
用例 TestSpeechEndpointDisabledVoiceDoesNotReadPersona(含对照组证明闸不是恒关);
变异(去掉闸)打红。
- voice.go 在 dev 基线里本就有 env.PersonaID 解析(无人格解析新增 I/O),本次未改。
Config.Normalized 去掉未使用的 logger 参数。go build/vet/test(含 -race)全绿;
真机 live 复测:cyrene 264104 B、furina 221194 B。
|
2026-10-26 13:06:39 +08:00 |
|
AskaEth
|
efc25c6a7f
|
feat(voice): emotionTTS 发音人绑定会话人格(不再全局定死一个 character)
主人 2026-10-26 逐字:「TTS 的人物选择是要绑定后端的角色的,不然会出现以后换了
别的角色(人格配置),还是用老角色的 TTS 角色」——上一版把 character 写在面板文件
里当全局值,换人格后声音不变,是真缺陷。
配置形状(单一真源 = $DATA_DIR/config/voice-tts.json):
{"enabled":true,
"personas":{"<personaId>":"<characterId>"},
"default":"<characterId>", // 只服务「解析不到 personaId」
"format":"wav","timeoutSec":60}
- 旧的全局 `character` 键**废弃并被忽略**(启动 WARN 提示改用 personas);
Config.Normalized 不给任何角色兜底(代码里写死默认角色=病害本身);
- configs/personas/*.yaml 不参与(塔裁定:避免启动期静态配置与面板两处漂移);
- 零新 .env 键。
选角四条分支(Config.Resolve,纯函数、无缓存):
① persona 有映射 ⇒ 用它;
② personaId 为空/解析不到 + 配了 default ⇒ 用 default;
③ personaId 为空 + 没配 default ⇒ 不合成(ErrNoCharacter,上游 0 请求);
④ persona 无映射(含映射值为空)⇒ 拒绝(ErrPersonaUnmapped),
**绝不**拿 default 或别的角色顶 —— 主人担心的病害本身。
两条既有出站口各自按当次请求解析 persona(不在 SetVoice 时算一次缓存成全局):
- WS voice_output:用本回合的 env.PersonaID(runVoiceTurn/runVoiceTurnDirect),
经 voiceReplySink 传给 synthesizeVoiceReply,再 WithPersonaID 交给语音面;
- POST /api/v1/audio/speech:请求体新增**可选** sessionId(纯增量),
经 turnEnvResolver.sessionPersona 解析;省略 ⇒ 走 default 分支。
Client.Speech 改为**每次传入 character**(内部不缓存发音人);Provider 适配同样按
ctx 的 personaId 现算。选不到发音人时**不回落既有链**(回落=用另一个声音说话),
上游拒绝(404/500)仍按既有口径回落既有链+WARN。
测试:四条分支各自可变异打红,断言打在「上游请求体里的 voice」与帧/响应字节上;
含「同一进程先后两个 persona,第二次绝不沿用第一次 voice」的回归锁,
以及两条出站口的端到端 persona 切换。四条变异(未映射回落 default / 客户端缓存
voice / REST 不看会话 / 选不到时回落底链)均已实测打红后还原。
go build ./... / go vet ./... / go test -race 全绿;真机 live 证据:
cyrene(char_84fb88a3)=254630 B、furina(char_5314489e)=213392 B。
|
2026-10-26 12:57:35 +08:00 |
|
AskaEth
|
240b59708d
|
docs(contract): 矩阵补 B28-B31 与计数改正(14+31+40=85,纠正 104 的重复计入)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 12:48:53 +08:00 |
|
AskaEth
|
3a2711a7e0
|
feat(voice): emotionTTS 接成一级发音人(UTF-8 请求体 + 失败回落既有链)
侦察结论(真机 + 上游源码):
- POST /api/analyze_emotion ⇒ {primary,complex,intensity};primary 闭集
实测 平/喜/惊/哀/怒/厌/惧/急,intensity ∈ Low/Medium/High。
- POST /api/synthesize 收 emo_vector(float[])+emo_alpha,但还要 ref_audio_filename
且只回 {"audio_url"}(服务端落盘),不适合做运行时直出。
- POST /v1/audio/speech 内部先 LLM 分析情绪 + 匹配参考音,再把情绪向量以
[EMO:{vector}|{alpha}]base64:... 注入 voice 交给 indexTTS2 ⇒ **情绪确实影响
语气,但由服务端从待念文本自推**,我方没有可映射的参数。
- 上游把下游载荷的 speed 硬编码 1.0(core/utils/llm_provider.py),客户端
speed 是空转 ⇒ 本切片不暴露 speed,也不暴露 emotion/tone(不造假开关)。
实现:
- internal/runtime/voice/tts:Client(唯一 HTTP 出口,json.Encoder 产出 UTF-8
请求体、超时/非 200/非音频/超限各自可分类)+ 字节嗅探庭裁 + Service(emotionTTS
在前、失败回落既有链,永不上抛成回复失败)+ Provider 适配(可作链的一级)。
- internal/config/runtimeconfig/voicetts.go:$DATA_DIR/config/voice-tts.json
(enabled/baseUrl/character/format/timeoutSec),零新 .env 键。
- cmd/cyrene/wire_voice_tts.go + wireFrontend 的唯一注入点:两条既有出站口
(POST /api/v1/audio/speech 与 WS voice_output)同时用上;不新建端点/帧族。
- 严守 voice 入站闭集(不扩 Source/Status),出站音频不挂 voice DTO(塔裁决 B)。
测试:中文 UTF-8 请求体、非 200、超时、非音频、超限、未启用、空文本、嗅探表、
降级回落、未启用透传、两条出站口的字节级实测、面板配置解析、组合根映射;
三条变异(去中文 / 嗅探恒真 / 去掉回落)各自打红。真机 live 用例(build tag
emotiontts_live)实测 278,036 字节 WAV。
|
2026-10-26 12:46:57 +08:00 |
|
AskaEth
|
4c79728553
|
docs(contract): v0.94 MCP 管理面与工具 origin + v0.95 渠道静默窗口,含写门不对称与呈现隔离口径
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-26 12:46:25 +08:00 |
|
AskaEth
|
a895754d55
|
Merge branch 'feat/m34-memdedup-dedupe-group-action-plus-re' into dev
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-25 00:52:28 +08:00 |
|
AskaEth
|
01aff71bc1
|
fix(memory): dedupe_group 数值门改三态(归一后同值可合 / 同量纲不同值冲突 / 量纲不可比保守拒),修 r3 复审两项
r3 复审指出我第一版中文数字门两个方向都反了(规格本身被复审自己收回重写):
- 项 2(过严):归一后**同值**、中文与 ASCII 混写的两条合不了
(`一小时` vs `60 分钟`、`十五分钟` vs `15 分钟`)。根因是判定仍以 `sameNumbers`
(只抽 ASCII token)为闸:`{}` vs `{60}` 不等 ⇒ 直接否决,归一化没机会生效。
- 项 3(过松):**量纲不可比**(`一小时` vs `一杯`)被当成"不算冲突"⇒ 会合并,
与「比不了单位就不许合并」相反。
修法:数值门从「布尔冲突」改成**三态**,并取代 `sameNumbers` 作为 dedupe 的数值判据:
| 结果 | 含义 | 合并 |
|---|---|---|
| tidyNumberSame | 归一后同值(含中英混写、单位换算:一小时 ≡ 60 分钟) | ✅ |
| tidyNumberConflict | 同量纲、值不相等(含单位不可互换:两杯 vs 三瓶) | ❌ |
| tidyNumberIncomparable | 量纲不可比 / 存活行没有同量纲数量可比(一小时 vs 一杯) | ❌ 保守拒绝 |
配套:
- 数量抽取同时支持 **ASCII 与中文数字 + 单位**(`60 分钟` / `15分钟` / `十五分钟` /
`半个小时` / `一万两千行`),并新增 `字符`/`字节` 两个文字量单位;
- **裸数字 token(无单位)**仍按 `sameNumbers` 的集合相等口径判(`0023` vs `0024`
这类靠它拦住),并有 parity 用例钉住「与 extractTidyFacts 的 nums 逐项一致」;
- 三态里 `incomparable` 只**收紧**,不放宽任何既有门;`sameNumbers` 函数本身未改
(judgePair/judgeIngest 共用,另处 blast radius,已登记建议)。
回归用例(三态各一条 + 修前红/修后绿,均实测):
- TestDedupeGroupNumberGateThreeStates 四个子用例(两条同值含混写、同量纲不同值、量纲不可比),
同时断言判定与簇结果;
· 变异 = 退回 `sameNumbers`(项 2 旧行为)⇒ 两条「同值」子用例红;
· 变异 = 不可比当放行(项 3 旧行为)⇒ 「量纲不可比」子用例红。
- TestDedupeGroupRejectsConflictingChineseNumbers / RejectsNonConvertibleUnits /
SameChineseNumberStillMerges(r2 三项保持绿)+ TestTidyQuantitiesNumberParsing +
TestScanTidyNumbersBareTokensMatchExistingFactNums(裸 token 口径不变)。
语料实测(生产库只读 mode=ro,active 3667,999 对已过否定/取值/相似度门的成员对):
三态分布 same=942 / conflict=25 / incomparable=32;dedupe 产出
`250 簇/495 行` → **248 簇/489 行(13.3%)**。incomparable 那 32 对是「四行歌词 vs
一段歌词」「一个项目进度代号 vs 项目进度代号为「w49」」这类:按「比不了就不许合并」
一律拒绝,聚合上只少减 6 行(簇成员互有重叠)。
验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
|
2026-10-25 00:49:44 +08:00 |
|
AskaEth
|
7700a7eda0
|
fix(memory): dedupe_group 补中文数字门(量纲/单位归一),修 r2 复审 p2
r2 复审 p2:`sameNumbers` 只抽 ASCII 数字 token,而本语料的数字主要写成中文
(一小时 / 十五分钟 / 第二杯 / 三千次轮回)⇒「午睡一小时 vs 十五分钟」
「咖啡不续第二杯 vs 不超过一杯」这类**数值冲突**被当成同一事实合并。
修法(新文件 internal/runtime/memory/tidy_dedupe_numbers.go):
- 抽出「**中文数字 + 单位**」对(一~九/两/半/十/百/千/万,含十五、二十四、一万两千、
半个小时),按**量纲归一**后比较:时间=秒(分钟 60 / 小时 3600 / 天 86400 /
周 604800 / 个月 2592000 / 年 31536000)、长度=米、文字量=个、计数=个;
- 只认**后面紧跟已知单位**的数字:`一起 / 一直 / 一些 / 一样 / 第二天(序数位置)`
这类含数字的习语一律不产生数量 —— 不做成「有中文数字就不合并」;
- 判据是**冲突**而非差异:存活行在同量纲里没有对应数量 ⇒ 不算冲突(被吸收行多出
的细节);等值(含单位换算后等值,如「半小时」=1800 秒)⇒ 照常合并;
值不等且单位换算不到一起 ⇒ 按冲突处理(复审口径:宁可不合并);
- 只**增加否决**,不放宽任何既有门;ASCII 路径仍由 sameNumbers 先判,一字未动;
`sameNumbers` 本身也未改(judgePair/judgeIngest 共用它,属于另一处 blast radius,
已在回报里登记为后续建议)。
语料实测(生产库只读 mode=ro,active 3667):999 对里中文数字门挡掉 **17 对**,
其中真冲突:午睡十五分钟 vs 一小时 ×3、咖啡一杯 vs 第二杯 ×4、
「三千次轮回」vs「三千万次轮回」×1(ASCII 路径完全看不见这条);
其余 9 对是「单位不可换算」的保守拒绝(半碗 vs 一个包/一杯、一次 vs 两圈)。
dedupe 产出 255 簇/505 行 → **250 簇/495 行(13.5%)**。
回归用例(去掉这道门即红,已实测):
- TestDedupeGroupRejectsConflictingChineseNumbers(复审给的两对真实语料,脱敏保留原意)
- TestDedupeGroupRejectsNonConvertibleUnits(两杯 vs 三瓶 ⇒ 保守不合并)
- TestDedupeGroupSameChineseNumberStillMerges(同值/换算后同值 ⇒ 照常合并,门没退化成「有数字就不合」)
- TestTidyQuantitiesChineseNumerals(解析口径:一小时=3600s、十五分钟=900s、半小时=1800s、
一万两千行=12000、第二天/一起/一直 不产生数量)
验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
|
2026-10-25 00:41:16 +08:00 |
|
AskaEth
|
54e32304f2
|
fix(memory): dedupe_group 补取值门 + 临时层双击复核;闸补 3 类长期事实安全阀(M34 r1 复审)
r1 复审 p1-1(会毁数据):tag_set 簇的安全门只有数值与否定,**专有名词/引号取值不比**,
生产语料实测把 `用户的昵称是「叶酱」。` 并进 `主人的昵称是「喵喵七号」…` 的存活行,
而同库另有 4 行写着「喵喵七号已作废、应称叶酱」⇒ 一次 apply 丢掉当前事实。
修法(结构性,不是补丁):
- 新增取值门 tidy_dedupe_values.go:被吸收行的**引号取值**(「」『』“”‘’")与
**拉丁 token**(≥2 字母,含 h3R3 这类混排)必须在存活行正文中一个不少;
方向单向(被吸收 ⊆ 存活),与软合并语义同向(存活行多出的取值不会丢)。
补齐 design/23 §5.1-3 要求的「专有名词 / 数值 / 否定词」三件套里缺的第一件。
- tag_set 簇也过相似度下限(dedupeGroupMinSimilarity 两档统一)。取值 0.10 而非更高:
语料实测 tag_set 合法改写(`用户可能需要提醒吃早饭…` 0.047、`叶酱有工作…` 0.109)
的余弦本来就在 0.05~0.4 —— 抬高它只会删掉合法去重,不会多挡住危险对。
实测(503 对):0.10→保留 98%、0.20→86%、0.30→63%、0.40→40%、0.50→20%。
- 回归用例:TestDedupeGroupRejectsSameTagDifferentValue(脱敏真实语料 + 同值对照组)、
TestDedupeGroupLatinTokenIsAlsoAValue、TestDedupeGroupNeverLosesAValue(簇级不变式)、
TestDedupeGroupTagSetAlsoNeedsSimilarityFloor。去掉取值门 ⇒ 前三条红;把 tag 相似度
归零 ⇒ 第四条红。
r1 复审 p2-1(数据安全):元信息查询失败时**该分区本轮不分簇**(旧实现降级成 nil map,
`temporary` 恒 false ⇒ 绕开「临时行绝不进簇」红线);apply 侧补 `expires_at` 复核
(候选久置/生成后变临时 ⇒ 整簇 skip,tidySkipTemporaryRow)。用例:
TestDedupeInPartitionSkipsWhenMetaUnavailable、TestTidyApplyDedupeGroupSkipsTemporaryRows。
r1 复审 p2-2(闸误杀被低报):补三类安全阀 ——「以后/从此/往后/今后」= 自此刻起永久成立、
「换成/变成/改成/搬进」= 状态已改变且新状态持续、N7 的进行中标记同样必须句首 12 字内;
「宿舍」进长期事实词表;顺手修 `<=` 的 off-by-one(改为 `<`)。
实测(生产库只读 mode=ro,active 3667):
- dedupe:281 簇/594 行 → **255 簇/505 行(13.8%)**;tag_set 240 / prefix12 15;
取值门挡 76 对、相似度门挡 12 对;簇大小 2:155 3:48 4:26 5:8 6:7 7:4 8:2 9:2 18:1 20:2
(两个 20 行簇与 18 行簇的取值集合均为单一「喵喵七号」⇒ 大簇=同一事实的重写)。
- 闸:改前 2 (0.05%) → 改后 **75 (2.05%)**(n6 53 / n7 20 / n1 2);含锚点词的 27 条
逐条人读均为「约定明天中午见 / 下周留个午后」类到期即失效的安排。
- 达标代价:+目前/现在 当锚点 → 2.13%;再叠一次性事件动词表(曾/发来/分享…)→ 6.74%
且新增 172 条里 56 条是长期身份/里程碑(「i love you 文件」17 条、「海报就是助手本人」
10 条…)⇒ 仍到不了 8%,不许为凑区间放宽。
验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
|
2026-10-25 00:29:02 +08:00 |
|
AskaEth
|
dfac06bbeb
|
docs(agents): 记下 internal/webui/dist/.gitkeep 占位不可删(干净 checkout 可构建的前提)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
|
2026-10-25 00:09:56 +08:00 |
|
AskaEth
|
6a3f3a329a
|
feat(memory): dedupe_group 候选动作 + 留存闸组合口径与 N6/N7 时点规则(M34)
M27 体检(DevDocs/audit/记忆库价值体检-2026-10-24.md)两条根治项:
1) 新增 `dedupe_group` 候选动作(不放大 containNone):
- 簇只认「同 scope+kind+完全相同 tag 集」或「同 scope+kind+前 12 字相同」
(后者附加词项余弦 ≥0.70 与同 kind 收窄);
- 每簇保留超集:内容最长 > merge_count 大 > created_at 早 > id;
- 安全门与既有合并路径同源:受限来源/绝对时刻/时效状态/低置信一律不进簇,
数值与否定与存活行不一致的成员被逐条剔出;临时层行(expires_at 非空)
两个方向都不进簇(存活行被清扫会给被吸收行留悬空 merged_into);
- 已被 archive/merge 候选覆盖的条目不重复出第二条待办(抑制);
- 落库形态与 merge 同构:source_ids 多条 + target_id,应用为整簇原子的软合并,
回滚复用 revertTidyMerge(逐条复原);
- 批量应用按**行**计上限 200 行/批(maxTidyApplyRowsPerBatch),超限如实 truncated;
- 不进自动档(人工/批量点);试算(DryRun)零写库给出簇数与可减少行数。
2) 留存闸改组合口径(治根)+ N6/N7:
- 护栏②从「含习惯/约定词就一票放行」改为「锚点词 **且** 含具体约束
(数字/禁忌/频次规律)」;无约束的锚点交给更窄的规则判;
- N1/N3/N4/N5 这几条词项规则改为「无锚点时才开火」(`主人喜欢说晚安`
这类偏好不再因寒暄词被判临时);
- 新增 N6(指示性时点,句首前 12 字内)与 N7(进行中状态),安全阀:习惯词、
硬性长期事实词、数值/约束、寒暄/情绪词一律按回去;
- 命中即第五态 defer,`expires_at = 判定时刻 + tempTtlSec`(默认 7 天,
已落在 $DATA_DIR/config/memory-tidy.json,零新 .env 键);**只开新增侧**,
存量一条不补写(用例钉住)。
契约(加性,不改既有字段语义):tidy dry-run/run 回执加 dedupeGroups /
dedupeRowsReducible / dedupeTruncated;批量应用回执加 deduped / rowsAffected。
实测(生产库只读 mode=ro,active 3667):
- 闸:改前 2 条(0.05%) → 改后 89 条(2.43%):n6 58 / n7 29 / n1 2;
89 条里 30 条含锚点词,逐条人读均为「约定明天中午见」「约定下周留个午后」
这类到期即失效的安排(唯一可议:1 条把「中午互动的日常」一起带进临时层)。
- dedupe_group:281 簇 / 可减 594 行(16.2% of active);tag_set 265 / prefix12 16;
簇大小 2:170 3:48 4:27 5:10 6:10 7:7 8:2 9:2 10:1。
验收:go build ./... + go vet ./... + go test ./... 全绿。
|
2026-10-25 00:08:46 +08:00 |
|
AskaEth
|
210c8b999f
|
Merge branch 'feat/m33-testhealth-fix-test-double-races-and' into dev
|
2026-10-24 23:41:40 +08:00 |
|
AskaEth
|
f56d74c363
|
test(channelbridge): 准入三条用例改按「这条消息续出的臂」断言(M33 r2 复审 p2)
TestCoalesceSystemNoticeDoesNotJoinBatch 仍在用位置索引 d[1]:waitTimerCalls 只保证
「至少 2 次请求」,不保证顺序 —— 加压下会拿到 m1/m2 那次 2s 的续臂(实测 -race
-count=200 有 2/200 假红,空载测不出来)。
改成与兄弟用例同一口径,但把相位做成确定的:新增 sendSettledAndDuration ——
投递后等 worker 把这条收进本批、**为它**续好臂(按请求序号认领),再返回那次续臂的
时长。仅用 latestDuration 仍有缝:worker 可能在帧/通知之后才补上一条消息的续臂,
最新那次就成了 2s。三条准入用例(action 帧 / 系统通知 / 聊天消息对照组)统一改用它。
断言口径未变:仍然断言「系统通知/action 帧不重置静默窗口(剩余 500ms)」、
「聊天消息重置成完整 2s」;waitTimerCalls 语义、请求次数均未改。
证据:-race -count=400 连跑 3 次(1200 次)全绿、DATA RACE=0;
兄弟两条 -count=200 全绿;整包 -race -count=3 全绿。
|
2026-10-24 23:36:41 +08:00 |
|
AskaEth
|
1ae9158250
|
test(channelbridge): 窗口夹具按截止时刻推钟 + 去掉无法证实的替身锁(M33 r1 复审)
P1-1(channelbridge 的 -race 门仍红,红在用例不确定性而不是竞态):
- coalesceTimers 改成**截止时刻**语义:每次 after(d) 记 at = 注入时钟 + d,
推钟只把时钟推进到「当前最早的截止时刻」再触发它。旧写法按 FIFO 逐个触发并
按请求时长累加,实测 12 条消息内攒到 5 个废弃定时器把时钟推 +10s(窗口 2s、
绝对上限 6s)⇒ 循环头走「到点直接发车、不 takeAll」的分支,队列尾部落到下一批。
- 推钟前强制「车道排空」这条不变量(pump/advanceAndFire/waitIdle):
时钟只表示时间过去,它一转就可能跳过 takeAll,因此先等 worker 把货收进本批。
- sendSettled:投递后按**序号**等「这次投递之后新挂的定时器」(旧的都已被 worker
放弃、再没人监听),供「投递与推钟交替」的用例(每 500ms 一条的绝对上限用例)。
- fireLatest/advanceAndFire 带 done 谓词重试,避免「登记与触发之间续臂」导致
推了钟却没人醒;waitIdle 一边等一边推窗口(worker 停在窗口里时永远走不到退休)。
- 用例侧只补相位与等待,不动断言:TwelveBurst 读历史前等 assistant 行落库;
准入用例断言「最新一次续臂」(原来按下标取 d[1],隐含臂数假设);
stale 用例两条都投完再推时钟(车道不会在两条之间退休)。
P2-1(替身锁无法用变异证实)选 A:删掉 stubChatter/stubSessions 的锁与访问器,
退回原字段访问,并写清理由 —— 21 个用例全走同步便捷路径
(b.process → processFrom → runBatch → Loop.Run),DEV HEAD 上 -race 数十次跑
(含把锁删掉的变异)DATA RACE 恒为 0;若将来从车道 worker 触碰这些字段必须加锁
并补 -race 用例。
证据:go test -race -count=3 ./internal/services/channelbridge/ 连跑 6 轮
18/18 全绿、DATA RACE=0;coalesce 子集 -count=80 全绿;整包 -race -count=20 全绿;
全树 go build/vet/test -count=1 全绿。
|
2026-10-24 23:18:45 +08:00 |
|
AskaEth
|
7d9b2132bb
|
test(testhealth): 替身竞态加锁 + 墙钟断言硬度(M33)
四处测试健康债务,只改 *_test.go,不动生产代码、不降并发:
- channelbridge:stubChatter / stubSessions 的跨 goroutine 字段一律经 mu 访问
(Run/GetOrCreate 在车道 worker 上写、测试 goroutine 读,两者无 happens-before;
负向断言「等一会儿再看循环有没有被驱动」尤其如此)。读写走访问器,Messages()
返回副本;t.Parallel 与并发度不变。
- plugins/tools/io:claimProbeTimeout 2s(真实时间预算)→ 60s,并由 claimProbeWait
按 t.Deadline 收缩。探针只等 goroutine 被调度(无时间语义),放大余量把
「机器快慢」从断言里去掉,死锁仍以断言失败暴露。
- backthink:TestSelfWakeGate_WindowExpiry 窗口 120ms → 6s(给 waitFor 的 4s 上界
留 2s 余量),到期等待改为按首次受理时刻推算 window+250ms,不再盲睡 3×窗口。
另修 TestSelfWakeStatus_MatchesReceiptAndTracksPendingSlot:worker 先 take 再进
Think,卡 Think 并不占待跑槽 —— 改为「上一轮把 worker 占住再断言 Pending」。
- memory:墙钟实测用例显式给闸 5s 预算(measureGateBudget),不再因默认 150ms
超时 fail-open 打炸「每轮都真扫真判」前置断言;并把 GateFailed 纳入该断言。
|
2026-10-24 22:24:14 +08:00 |
|
AskaEth
|
20cf3e0a93
|
Merge branch 'feat/m32-subagentbackfill-deliver-subagent-re' into dev
|
2026-10-24 22:11:16 +08:00 |
|
AskaEth
|
da6a365222
|
tower: snapshot of uncommitted base checkout changes (worktree wt-33)
|
2026-10-24 22:03:13 +08:00 |
|
AskaEth
|
3acc81cec8
|
feat(gateway): 子代理结果回填父会话(系统注记)+ 记忆抽取排除(M32)
断点(M29 勘测定论):后台批次结算后 `subagent.result` 发了、hub 也把它中继成
WS 帧,但**没有任何代码把结果回填父会话** —— `chat_messages` 无行、模型下一轮
看不到,于是「她拍了个子代理出去,但没有下文」。
回填拆成互不冒充的两条面(人格=身份红线:子代理结论绝不写成她的人格发言):
- 可观测/历史面(新文件 subagent_backfill.go):独立订阅者(不塞进 hub)订阅
`subagent.result`,写父会话一条 `role=system` 注记(内容以 `[子代理结果]` 开头
并点名 `sub-<id>`),随后走既有 `message_persisted` 扇出 ⇒ 刷新/重连后仍在;
- 模型面:同一行经转录播种成为下一回合的**系统注记**(`model.RoleSystem`),
不是「她说过的话」。
落库口径:角色恒为 system(不冒充 user/assistant)、PersonaID 与 TurnID 留空
(注记不属于任何角色、也不属于任何一路回合)。
记忆抽取排除(新文件 subagent_backfill_extract.go):`WithoutSubagentNotes` 装饰
`memory.Extractor`,判据是元数据(role=system + 注记标记,与 loop 的 summaryMarker
同一手法),组合根经 `loopMemoryExtractor` 套上 ⇒ 子代理结论绝不进记忆库;LLM
提取器的异步能力原样保留,压缩摘要(同样是 system)不被误剔。
可观测补齐(M29 §7):`SpawnTool.await` 发布成功补带 source 的 INFO,`b==nil` 与
「无结果」的静默 return 改为 WARN,发布失败 WARN 带上 count/session;回填订阅者
顺手读 `bus.Dropped()`,涨了就记一条 WARN(总线丢事件不再是黑洞)。
测试(均可变异打红):父会话恰一条 system 行且零伪造助手发言;注记/摘要/批次正文
有界且逐代理点名;不可用载荷与落库失败各留 WARN 且不广播;Start 真订阅总线;
结果不进记忆抽取(同步/异步/端到端接缝);组合根真装配(真 bus 发布 → 父会话多
一行)。回归:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
不做:不改 `subagent_result` 帧形状、不加进 frameReachesAll、不新开路由/帧族、
不注入人设、不改 Docs(契约归一建议报塔)。
|
2026-10-24 22:02:11 +08:00 |
|
AskaEth
|
bd49080bd1
|
tower: snapshot of uncommitted base checkout changes (worktree wt-32)
|
2026-10-24 21:51:07 +08:00 |
|
AskaEth
|
0fe88a44c0
|
Merge branch 'feat/m23-coalesce-silent-window-batching-and' into dev
|
2026-10-24 21:32:29 +08:00 |
|
AskaEth
|
4c9cabce1b
|
fix(channelbridge): 窗口准入落到车道头 + lane.go 按职责拆分(M23 复审 r1 P2)
P2-1(行为缺陷):窗口准入此前只在 prepareInbound 里算 `solo`,而 prepare 发生在
**窗口等待之后** —— 平台帧(action / 系统通知 / 我们自己的 message_sent 回执)照样
进窗、照样把滑动计时重置:一条回执就能把这一批的静默期一直续住(群友说了三句话,
她因为「撤回事件」迟迟不开口)。这与任务书「窗口只收 chat/image/voice/file」相反。
- `laneWindowAdmits(msg)` 成为**唯一准入判据**,只看结构化事实(ContentType +
isSystemNoticeMessage,绝不解析正文);
- `splitWindowItems` 在窗口等待阶段把到达的条目分成「进窗口」与「不进窗口」两半;
- 非准入帧**不建窗口、不重置计时**(首条即到达即发车),窗口中途到达的只收进 solos,
这一批发车后**原样回插队首**各自单跑(保序、不合并、不丢弃);
- prepareInbound 里的 `solo` 保留为第二道保险(适配器将来把平台事件标成 text 时,
仍会在组批前被切出去)。
P2-2(硬约束):lane.go 682 行 → 按职责纯搬移成 5 个文件(最大 246 行):
lane.go(车道类型与注册表)/ lane_queue.go(入队与取队列)/
lane_worker.go(单消费者与回合槽)/ lane_drop.go(丢弃与停机诚实)/
lane_owner.go(主人私聊判据与车道键)。语义零改动:去注释后的声明集合
(34 个顶层声明,含函数体/字段/常量值)逐字节全等;既有用例一条未改。
顺带:把两处「IsOwner() && !IsGroupChat()」的注释改成代码真正用的判据
(ownerPrivateLane = !msg.IsGroup() && actor.IsOwner()),并记下这处 P3 分叉。
验证:全树 build/vet/test 全绿(73 包 ok);三包 -race 全绿;
新增 4 条准入用例在「准入失效」变异下三条必红、对照组保持绿。
|
2026-10-24 21:28:26 +08:00 |
|
AskaEth
|
ca8577f05b
|
feat(channelbridge): 群聊静默窗口合并与回合中续跑(M23)
主人 2026-10-23/24:「要让群聊聊天很自然,很流畅的做法」。群里「一鼓作气打三句」
此前会让她回三次;本片在**每会话车道头部**加一段静默等待,把这几句合成**一轮**回答
(批内每条仍是独立的一条消息,design/15 §2.1 不许拼成一条)。
四条不可走样:
- 🔴 主人私聊**结构性**无窗口:车道分 `newDirectLane`(无 batcher 字段、拿不到定时器
工厂)与 `newBatchLane`,类型由 actor 元数据(IsOwner() && !IsGroupChat())决定,
配置无权翻转;
- 🔴 窗口内零丢失:唯一容器是车道队列(takeAll 单消费者取出),合并阶段无新增丢弃点
(CoalescedDroppedTotal 恒 0);超单批上限的**尾部回插队首**保序、锁内、发车前;
- 🔴 一条批 = 一个 turnID = 一次 loop.Run = 一条助手回复;
- 🔴 不改「回什么」:闸门仍逐条跑(prepareInbound 逐字搬运 M22 的 processFrom),
被静默的消息进不了批。
实现落点:
- lane.go:direct/batch 两种车道 + front(回插队首)+ takeAll + 停机诚实登记;
- coalesce.go:生效参数解析、滑动窗口 + 绝对上限等待、发车原因与计数;
- coalesce_lane.go:攒批 → 拆分 → 陈旧度/闸门 → 发车;
- coalesce_prepare.go / coalesce_batch.go:processFrom 拆成 prepareInbound + runBatch;
- runtimeconfig/channels_coalesce.go:$DATA_DIR/config/coalesce.json(零新 .env 键);
- gateway/admin_coalesce.go:GET/PUT /api/v1/admin/channels/coalesce(三态补丁 +
writeAuth + 审计 channels.coalesce + 热生效),routes.go 两行注册(塔已批);
- 前端:渠道总览页一张卡(六个旋钮;主人私聊只给只读说明,不给开关)。
M22 既有用例仍在(lane_test.go 夹具显式关窗以钉住车道本身);stranger_test.go 的那条
按塔裁定 #4 改写:「已有回合在跑 ⇒ 只登记不回」变成排队回答(留 WARN,绝不静默)。
回归:go build/vet/test ./... 全绿;三包 -race 全绿;前端 tsc --noEmit + npm run build
+ 渲染测试 36 项通过。不 push、不重启后端、未改 .env 与 Docs/。
|
2026-10-24 21:11:22 +08:00 |
|
AskaEth
|
679c8abf1d
|
Merge branch 'feat/m30-mcporigin-stamp-tool-origin-and-grou' into dev
|
2026-10-24 20:36:47 +08:00 |
|
AskaEth
|
c608dd4afb
|
fix(gateway): 卸载拒绝话术按接入路径分流(.env 接入的 server 不再指向 MCP 页)
复审 M30-p2-1(r1):`plugin_uninstall_scope` 的 400 一律让主人「去『MCP 服务器』页
卸载」,但 `.env` 的 MCP_SERVERS_JSON 启动期接入的 server **不经**插件管理器、
不在 plugins.json 里,MCP 页(= Manager.Statuses()/List())根本看不到它 ⇒ 那条指引
对该路径是假指引。
- 新增 `plugin.(Manager) MCPServerName`:`mcp:<server>` 注册键 → server 名的**唯一**
解析口(前缀只在 manager.go 出现一次),网关不再自己拼前缀。
- `serverScopedUninstall` 改为 `*Server` 方法,文案经 `uninstallPointer` 分流:
· 管理器托管(落在 plugins.json,判据 `Manager.List()`)⇒ 保留「请在『MCP 服务器』
页启停或卸载该 server」;
· 否则 ⇒ 明说该 server 由 .env 的 MCP_SERVERS_JSON 在启动时接入、面板不管理它,
并给出可操作步骤(改 .env 里那一项:删掉或设 enabled=false,改完需重启生效)。
接入路径判据来自**管理器持久清单**,不是名字前缀/transport 推断。
- `mcp:<server>` 登记条目同样分流(此前也一律指向 MCP 页)。
测试(gateway):plugins.json 路径断言含「MCP 服务器」页字样且不提 MCP_SERVERS_JSON;
.env 路径(工具与登记条目各一条)断言含 MCP_SERVERS_JSON 且**不含**「MCP 服务器」
字样;三条均断言 400 + `plugin_uninstall_scope` 不变、被拒后条目仍在。
修前红:两条 .env 用例(见回报);修后绿。回归 go build/vet/test 全绿。
|
2026-10-24 20:32:36 +08:00 |
|
AskaEth
|
86aef7b454
|
feat(capability): 工具按上游 MCP server 分节(来源章 + 呈现隔离)
主人 2026-10-24:「能否将每个 MCP 服务器所承载的插件和工具按上游(来源)服务器分类
并与其他插件相对隔离?」
归属信息此前后端根本没有(GET /admin/plugins 每条只有 name/kind/description/两个
布尔),而 `mcp:` 是每台 server 一条的 KindMCP 条目的注册键,**不是**工具的命名空间
(工具名由 server 原样给出,连前缀都没有)⇒ 只能靠注册时的元数据,不能解析名字。
后端(唯一汇点盖章):
- 新增 pkg/plugins/sdk/origin.go:Origin{Kind,Server,Transport} + 可选接口
OriginAware(没实现 = 宿主内置,老工具零改动)+ OriginOf(零值视为无来源)。
- registry.Capability 加 Origin 字段;RegisterTool 盖章(覆盖热装与 .env
MCP_SERVERS_JSON 两条接入路径的合流处),ReplaceTool **同样重新盖章**(它逐字段
重建条目,漏了就是「热替换把来源抹掉」)。
- mcp.mcpTool 实现 Origin()(server 名与传输都在 cfg 上),transport 用新的
transportNameOf 报**实际会用的**形态(stdio/sse/http),与 newTransport 同一推断。
- 网关 PluginSummary 加可选 origin(omitempty):零值 ⇒ 键缺席,不是空对象。
- 卸载守卫:对单把 MCP 工具或 `mcp:<server>` 登记条目直接 400
(plugin_uninstall_scope)并指向「MCP 服务器」页 —— 原来会绕过插件管理器裸删,
server 的 names 账本不同步、重连后工具复活。判据是来源章,不是名字前缀。
前端(呈现分组):
- admin/modules/pluginOrigin.ts:纯函数按 origin.server 分节(按 server 名排序),
transport 未报告时标题省略括号,缺 server 名不冒充内置。
- Plugins.tsx:工具段两级(`来自 fs(stdio)· N 把工具` + 宿主内置),MCP 工具行去掉
「卸载」改为指向 MCP 页;`kind:"mcp"` 登记条目与未知 kind(iot)各有段如实显示,
不再静默丢弃(design/09 §5.2.1 红线③)。
零影响(有断言):tool_policies 表/策略写路径未改;ToolSchemas 不发来源(测试断言
模型面既无 origin 键也无 server 名);停用 server ⇒ 条目连来源一起消失,无孤儿。
测试:registry 盖章/保章(两条均已实测变异变红)/内置零值/改策略不清章;mcp 注册
路径盖章 + transport 推断表;网关读面缺席与存在、热装整链、卸载守卫;前端 esbuild+cjs
逻辑测试 12 项。验收:go build/vet/test 全绿,frontend `npm run build` 通过。
|
2026-10-24 20:21:22 +08:00 |
|
AskaEth
|
fd8a62edf5
|
Merge branch 'feat/lanequeue-decouple-inbound-turns-and-add' into dev
|
2026-10-24 20:17:17 +08:00 |
|
AskaEth
|
e3e8878ead
|
fix(channelbridge): 车道键与会话键同源 + 主人私聊溢出区保序(M22 复审)
复审 r1 的 P1-1 被证实:车道键(a.Name()+ConversationKey,含频道与账号维度)与会话键
(platform \x00 channelID)是两套没有共同来源的判据,于是 kook 同一 guild 的两个 text
channel、以及同平台两个账号对同一对端,都会得到「一个 session、两条车道、两个回合并发」
—— 两条 run 共享 sessionID,checkpoint 行 id 互相覆盖 / flow 桶被先收尾者删掉。
- P1-1 车道键与会话键同源:`internal/channel/session` 导出 ChannelKey / Key(channelID
与原缓存键派生**原样**导出,manager 内部改为走同一个 sessionKey,语义一字未改);
`laneKey` 现在逐字是 `session.Key(msg)`(主人私聊仍是 `main|web` 这个目的地键),
**去掉 a.Name() 维度**。同平台多账号对同一对端因此共用一条车道(它们是同一个 session),
出站路由不受影响:回复取每条消息自己的 platform/ChannelID 与它自己的适配器。
- P2-1 溢出区保序:`spill` 非空后新条目一律追加 spill、不再回写 `ch`,修掉复审实测的
`[m1 m2 m3 m5 m4]` 超车(现为到达顺序)。
- P2-2 口径修正:lane_test 文件头改指 `TestLaneRejectsNewestWhenFull`(语义是「门前拒收
最新、队列不动」);laneKey 注释改写「多账号必须共用一条车道」并写明取舍。
- P2-3 生产可见性:丢弃 WARN 带上 `dropped_stale_total` / `dropped_overflow_total` 累计
计数(不新开端点);主人私聊积压改成按倍率 16/32/64… 告警,不再只响一次。
断言:TestLaneKeyNeverCollidesAcrossSessions(表驱动,含 kook c1/c2@同 guild、
telegram conf0/conf1@同对端两行 + 不同群仍并行的控制行)、
TestLaneOwnerPrivateSpillKeepsArrivalOrder(逐条放行,钉住到达顺序)、
internal/channel/session/keys_test.go(键派生与会话缓存键形状)。
|
2026-10-24 20:08:28 +08:00 |
|
AskaEth
|
7ea2850f18
|
Merge branch 'feat/m24-flowgate-move-review-off-admission-p' into dev
|
2026-10-24 20:00:57 +08:00 |
|
AskaEth
|
187eb327d6
|
Merge branch 'feat/mcppanel-mcp-server-management-page-and' into dev
|
2026-10-24 19:58:37 +08:00 |
|
AskaEth
|
cfb8700b61
|
Merge branch 'feat/m26-webuidist-make-go-build-work-in-clea' into dev
|
2026-10-24 19:54:10 +08:00 |
|
AskaEth
|
2142de92f2
|
fix(plugin): 拒收 env/args 的非字符串值 + 保留键收成内置 id 集合 + 启用落盘失败文案
r1 复审 P2-1 + 两条顺手件。
P2-1(必修):`actionStringMap` / `actionStringList` 之前对非字符串值 `continue`,于是
前端 kv 控件里最自然的写法 `{"PORT":8080}` 会被**静默丢掉**、安装仍报 `installed:true`。
现在非字符串值、空键、非数组一律 400 invalid_config,文案只点名**键与类型**(不回显值);
盘上零变更、注册表零工具、快照零行(测试同时断言)。
顺手件 1:`isReservedPanelKey` 从「只挡 mcp」改成内置提供者闭集(mcp / web-search /
browser,写成 switch,新增内置页时漏补一眼可见)。之前一台名为 web-search 的 server 被
卸载时,`forgetPanels → MarkUninstalled` 会把内置「搜索提供商」页在重启前整页摘掉。
顺手件 2:启用「已连上但启用态未落盘」的文案改成事实(原来那句「它仍处于停用状态」与
事实相反)。为此新增 `ErrPluginStatePersist` 哨兵,把「磁盘没记住」与「连不上」分开;
`state=connected` 的派生口径不动。停用失败文案同时补上「server 仍在运行」。
测试:三条新用例,均已验「修前红 → 修后绿」(见 review-request 的取证)。
|
2026-10-24 19:53:38 +08:00 |
|
AskaEth
|
a25e0c8870
|
test(channelbridge): 锁住审查解析的接线(BOM 韧性必须在生产侧生效)
- 新增 TestParseKeepIndices_ToleratesBOMWiring:喂对端**原样字节**(\xef\xbb\xbf + JSON 数组)
给生产路径的 parseKeepIndices,必须解析成功且升序;同时断言夹具真的以 BOM 开头、且裸
json.Unmarshal 在它上面仍然失败 —— 把委派改回裸 json.Unmarshal 即红。
- 新增 TestParseKeepIndices_StillFiltersAfterDelegation:委派之后过滤口径不变(越界/负数/
重复编号丢弃、结果升序)。
- 新增 TestNewLLMReviewer_ToleratesBOMOutput:走整条生产路径(NewLLMReviewer 产出的
Reviewer),回带 BOM 时给出保留编号而不是退回 FIFO。
- 注释里写明 M24 已确认的语义代价:审查移出准入路径后 review_pruned 不再从门口丢消息
(41–74 条/日 会进流水线),而 rate_limited 门口丢与 FIFO 上限修剪一字不改(由既有用例钉住)。
只加用例与必要注释(含一个为断言所需的 encoding/json import),未改动既有用例的任何断言。
|
2026-10-24 19:49:58 +08:00 |
|