Files
Cyrene_v2/cmd/cyrene/wire_services.go
T
AskaEth 6e28b38b08
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(memory): 护栏抬高+时间预算闸+试算加性异步 与 D3 入库前审查(耦合两片一起落)
【MT-A:护栏 + 诚实截断 + 试算异步】
- maxTidyCompareItems 400(写死)→2000 并开放为 MEMORY_TIDY_COMPARE_LIMIT;
  MEMORY_TIDY_CANDIDATE_LIMIT 默认 20→200(语义不变,仍每分区)
- 依据是首跑实测:459 条的分区被 400 截断 ⇒ compared(705) < scanned(784),
  候选数(77)由上限而不是规则决定;三处上限的 WARN 都补齐了总数/上限/分区名
- 新增「上限 + 时间预算」双闸(实测 2000 条/分区 ≈234s > 3 分钟墙钟):
  整分区跳过 / 两两比对中途收手,一律如实记 (budget) + WARN
- ?async=1 ⇒ 202 {runId,status};GET …/{runId} 轮询(writeAuth 同档);
  done 的 result 与同步 data 逐字节同形;并发=复用既有 runId、TTL 30min 自完成起算、
  重启即如实 404 tidy_run_not_found;不带 async 时同步路径逐字不变

【MT-B:D3 入库前审查(design/23 §15.1)】
- 新记忆经 Store.Add(仓库里唯一 INSERT INTO memory_items)落库后,由独立记忆管理
  会话(sess-memorytidy)在有界后台队列异步判定 skip/merge/modify/create
- 软合并回写(status + merged_into/merged_from/merge_count,可 revert、绝不改 content)
- 隔离来源(群聊/陌生人)**概不审查**、召回/超时失败**零改写**(fail-safe 结构性成立)
- 写入侧仅 +62.7µs/条(非阻塞入队);默认 ENABLED=true(主人裁定:入库前先审查)

【耦合说明(重要)】两片同包且互相依赖:MT-B 的 preinsert_recall.go 用 MT-A 的
collectTidyPairs 新签名 + tidyBudget ⇒ **各自单独都 build 不过**,故一并落地。
MT-A 为让整包编译,最小幅度适配了 MT-B 的 preinsert_recall.go / preinsert_test.go
(零值预算=行为不变;符号重名改名)。

【我的验收】隔离 worktree(37 文件)build/vet/linux/darwin 全过,切片包全绿;
我亲手打红两个变异:① 去掉 (compare) 截断标记 ⇒ TestTidyCompareLimit_HitIsLoud 红;
② 中和隔离来源守卫 ⇒ TestReviewRestrictedSourceIsNeverTouched 红。
唯一红是 internal/runtime/tool 的两条超时用例(该包无人改)⇒ 主仓库 -count=8 全绿,
判定为负载敏感 flake(当时两个整树验收 + 子代理并发),已记入开发习惯与约定 §7。

.env.example 补 MEMORY_REVIEW_* 四键(MT-B 无该文件权限,由我按其代码语义补写)。
2026-10-06 08:04:12 +08:00

177 lines
5.4 KiB
Go

package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/platform"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink"
)
// wireAuxiliary 组装执行提供者、任务管理器、IoT 管理器、渠道会话管理器、
// 自动化服务与后台思考服务。
func (a *app) wireAuxiliary() error {
provider, err := exec.New(a.cfg.ExecSandbox, a.log)
if err != nil {
return fmt.Errorf("exec provider: %w", err)
}
a.execProv = provider
tmgr, err := task.New(a.cfg, provider, a.log, task.WithBus(a.bus))
if err != nil {
return fmt.Errorf("task manager: %w", err)
}
a.tasks = tmgr
iotMgr, err := iot.New(a.cfg.IoT, a.log, iot.WithBus(a.bus))
if err != nil {
return fmt.Errorf("iot: %w", err)
}
a.iot = iotMgr
sess, err := session.New(context.Background(), a.db, a.log)
if err != nil {
return fmt.Errorf("session manager: %w", err)
}
a.sessions = sess
hist, herr := history.NewStore(context.Background(), a.db, a.log,
history.WithDocumentRefs(documents.NewRefCounter(a.log)))
if herr != nil {
return fmt.Errorf("history store: %w", herr)
}
a.history = hist
automation, err := automations.NewService(automations.Config{}, automations.Deps{
Bus: a.bus,
Loop: a.loop,
Log: logger.WithSource(a.log, "services/automations"),
})
if err != nil {
return fmt.Errorf("automations: %w", err)
}
a.automation = automation
bt, err := backthink.New(backthink.ConfigFromBackground(a.cfg.Background), backthink.Deps{
Bus: a.bus,
Thinker: backthink.NewLoopThinker(a.loop, a.history),
Recent: a.history,
State: backthink.NewIoTStateProvider(iotMgr, a.log),
Exporter: backthink.NewPushOutlet(a.pushOutletConfig()),
DB: a.db,
Log: logger.WithSource(a.log, "services/backthink"),
})
if err != nil {
return fmt.Errorf("backthink: %w", err)
}
a.backthink = bt
a.wireVoice()
a.wireMCP()
a.wireSkills()
a.wireTerminalTool()
a.wireSubagentTool()
a.wireSessionReadTool()
// X1(裁决 15):把外围子系统(IoT)以 KindIoT 放进能力面,并注册只读的
// capability_list 工具。排在最后,是为了让它列出的清单已经包含前面各步
// 注册进来的工具(terminal / subagent / session_read 等)。
a.wireCapabilitySurface()
return nil
}
// pushOutletConfig 把环境变量里的推送出口配置块映射为 backthink 的导出器配置。
// 个推(Getui)凭据只被 config 读取、并不消费:备用 SDK 尚未接入,因此出口在
// 待接入该档位时记日志,而不是伪造一次推送。token 只透传,绝不写日志。
func (a *app) pushOutletConfig() backthink.PushOutletConfig {
po := a.cfg.PushOutlet
return backthink.PushOutletConfig{
Enabled: po.Enabled,
URL: po.URL,
Token: po.Token,
Timeout: time.Duration(po.TimeoutSec) * time.Second,
Log: a.log,
}
}
// close 按依赖顺序关闭每个子系统并释放资源。
func (a *app) close(ctx context.Context) {
// 文档回收循环要先用连接池(每轮一次查询),因此排在关库之前。
a.docGC.stop()
// 用量落库与保留期清理同样要用连接池:先把队列里剩下的样本写完,再停清理。
a.stopUsage()
if a.backthink != nil {
_ = a.backthink.Stop(ctx)
}
// 记忆整理与 backthink 同属后台定时任务:先停它,再关库(它每轮只有
// 查询与候选写入,且 Stop 会等本轮跑完)。
if a.memoryTidy != nil {
_ = a.memoryTidy.Stop(ctx)
}
// 入库前审查与整理同属记忆后台任务:先停它(Stop 会等正在审的那一条跑完),
// 再关库 —— 否则正在写 memory_items 的审查会撞上已关闭的连接池。
if a.memoryReview != nil {
_ = a.memoryReview.Stop(ctx)
}
if a.automation != nil {
_ = a.automation.Stop(ctx)
}
if a.channelBridge != nil {
a.channelBridge.Stop(ctx)
}
if a.scheduler != nil {
_ = a.scheduler.CancelAll(ctx)
}
if a.gw != nil {
a.gw.Close()
}
for _, reg := range a.mcpRegs {
_ = reg.Unregister(ctx)
}
if a.plugins != nil {
a.plugins.Close(ctx)
}
if a.tasks != nil {
_ = a.tasks.Close()
}
if a.vindex != nil {
_ = a.vindex.Close()
}
if a.bus != nil {
_ = a.bus.Close()
}
if a.db != nil {
_ = a.db.Close()
}
a.log.Info("cyrene shutdown complete")
}
// resolveDataDir 返回配置的数据目录(来自 .env / 环境变量,经 cfg.DataDir);
// 只有在没有配置目录时才回落到与操作系统相称的默认值。
func resolveDataDir(cfg *config.Config) string {
if cfg.DataDir != "" {
return cfg.DataDir
}
return platform.DefaultDataDir()
}
// resolveSQLitePath 返回实际使用的 SQLite 路径:当配置里仍是默认值时,
// 把它推导到数据目录下。
func resolveSQLitePath(cfg *config.Config, dataDir string) string {
p := cfg.DB.SQLitePath
if strings.TrimSpace(p) == "" || p == defaultSQLitePath {
return filepath.Join(dataDir, "cyrene.db")
}
return p
}