store

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

本文件实现浏览器鉴权所需的两张表:会话(sessions)与一次性 ticket(auth_tickets)。

职责:

  • 凭据哈希:两张表都只存 SHA-256,明文永不落库
  • ticket 的创建与**原子消费**(一次性由一条条件 UPDATE 的原子性保证)
  • 会话的创建、按哈希/按 id 查询、列出、吊销、活跃与续期写入

边界:

  • 不判断会话/ticket 是否「有效」:过期与吊销的判定属业务规则,由 agentd 侧做 (store 是叶子层;且 spec §11 要求把「会话过期」与「会话已吊销」作为不同 原因分别记日志,合并判定就拿不出这个区分)
  • 不生成凭据明文、不设置 cookie、不做任何 HTTP 相关处理
  • 叶子层纪律:本层方法错误 return 前不打日志(避免双份),由调用方带上下文记录。 签发、消费、建立、吊销四个节点的 Info 日志由 agentd 侧(Task 4/6 的调用方)打, 不在本文件任何方法里加日志——这条责任转移是刻意的,不是漏了

本文件是事件镜像两表(mirror_events / mirror_tasks)的持久化实现。

职责:

  • 幂等追加远端事件副本(INSERT OR IGNORE,(task_id, seq) 复合主键即幂等键)
  • 水位(远端 seq 已复制到的最大值)与开区间续拉
  • 任务快照 upsert 与「任务 → 所属机器」的路由索引

边界:

  • **镜像是 replication 不是真相**:权威始终在任务所在机器,本表只是副本, 可随时整表删除、按 from_seq=0 从远端重建——不存在本机独有状态
  • 与 store.go 一致的叶子层纪律:方法错误 return 前不打日志;仅两个高价值 点例外(ListMirrorTasks 的坏行、DeleteMirrorTask 的破坏性操作)

本文件是 project_locations 表(项目 × 本机位置)的持久化实现。

职责:

  • project_locations 的增(CreateProjectLocation)、查(GetProjectLocationByName)、 列(ListProjectLocations)、删(DeleteProjectLocation)
  • 把 SQLite 的主键/UNIQUE 冲突翻译成 ErrProjectDuplicate 哨兵,供上层映射 409

边界:

  • 不算 project_id:那是 internal/projectid 的纯函数,调用方算好后传进来
  • 不判断路径是否存在、是不是 git 仓库——那是 agentd 侧 EnsureRepoUsable 的事
  • 不做名字派生、不做名字去重——那是 agentd/projectadmin.go 的事
  • 与 store.go 一致的叶子层纪律:方法错误 return 前不打日志,由调用方带上下文记录

Package store 是 handoff 的唯一持久化入口,基于 SQLite(modernc.org/sqlite,纯 Go 无 cgo)。

职责:

  • 提供任务(tasks)、事件(events)、工单(tickets)、项目位置(project_locations) 四张表的建表与增删改查
  • 通过 database/sql 连接池支撑单进程多 goroutine 并发访问(WAL + busy_timeout 防 SQLITE_BUSY)
  • CreateTicket 用 INSERT OR IGNORE 实现按 id 幂等创建
  • Open 时顺带把旧 repos 表迁入 project_locations(B62 一次性迁移,见 projects.go)

边界:

  • 不含业务规则,仅保留 UpdateTaskState 对 proto.CanTransit 的一处防护性校验
  • 事件派发、状态变更决策、工单审批逻辑均不在此层
  • 叶子层:方法错误 return 前不打日志(避免双份),由调用方带上下文记录; 仅 Open 成功打 Info、UpdateTaskState 非法迁移打 Warn 两个高价值点例外

Index

Constants

View Source
const VoidAnswer = "__void__"

VoidAnswer 是 VoidPendingTickets 写入的占位答案值。

语义:任务已终结(executor 已不存在)时挂起工单不再可能被回答,作废后 PendingTickets(answer IS NULL)天然不再返回它们——协调者看到的是「无挂起项」 而非可操作的假象;作废原因由调用方(RecoverOnStartup 的 failed 事件)留痕。

Variables

View Source
var ErrBadTransit = errors.New("非法状态迁移")

ErrBadTransit 表示任务状态迁移违反状态机(见 proto.CanTransit)。

View Source
var ErrNotFound = errors.New("记录不存在")

ErrNotFound 表示按 id 查询的记录不存在。

View Source
var ErrProjectDuplicate = errors.New("项目位置冲突(项目、名字或路径已存在)")

ErrProjectDuplicate 表示位置登记冲突,三种成因合用一个哨兵:

  • project_id 已存在 → 这个项目在本机已经有位置了(ADR-0008 只允许一个)
  • name 已被占用 → 引用名撞了
  • path 已被另一个项目指向 → 两个项目声称在同一个目录

为什么合并成一个哨兵:三者在 HTTP 上都是 409,且报文由 agentd 侧按上下文 拼装(它知道是自动登记还是人工登记);分成三个哨兵只会让映射层多两个分支。

Functions

func HashCredential added in v0.3.0

func HashCredential(plain string) string

HashCredential 计算凭据明文的 SHA-256 十六进制串。

参数:

  • plain: 凭据明文(ticket 或 cookie 值)

返回:

  • 64 字符的小写十六进制哈希

注意:

  • 为什么不加盐、不用 bcrypt:输入是 256 位高熵随机串(不是人选的口令), 没有字典攻击面;而查表必须是 O(1) 精确匹配,加盐会退化成逐行比对

Types

type MirrorTask added in v0.3.0

type MirrorTask struct {
	Task      proto.Task
	Target    string // §5.1 透明路由的索引:这条任务该转发给谁
	FetchedAt time.Time
}

MirrorTask 是 mirror_tasks 的一行:远端任务的快照副本 + 路由信息。

type Session added in v0.3.0

type Session struct {
	ID         string
	TokenHash  string
	DeviceName string
	CreatedAt  time.Time
	ExpiresAt  time.Time
	LastSeenAt time.Time
	RevokedAt  *time.Time
}

Session 是一个浏览器会话记录。

注意:

  • TokenHash 是 cookie 明文的 SHA-256;明文不落库,本结构体永远拿不到它
  • RevokedAt 为 nil 表示未吊销;是否「有效」还要看 ExpiresAt,由调用方判定

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 持有 SQLite 数据库连接池,是 store 包对外唯一入口。

func Open

func Open(path string) (*Store, error)

Open 打开(必要时创建)path 处的 SQLite 数据库并建表。

参数:

  • path: 数据库文件路径;父目录需已存在

返回:

  • 可用的 Store;打开或建表失败时返回错误

注意:

  • driver 为 modernc.org/sqlite(纯 Go 实现,无需 cgo)
  • 每次调用幂等:建表均带 IF NOT EXISTS

func (*Store) ActiveTasksByRepoPath

func (s *Store) ActiveTasksByRepoPath(repoPath string) ([]proto.Task, error)

ActiveTasksByRepoPath 返回仓库路径为 repoPath 的全部非终态任务。

参数:

  • repoPath: 仓库绝对路径;空串返回空切片

注意:

  • 与 ActiveTasksByWorkDir 的区别:那个按**工作目录**判定(managed worktree 各不相同),本方法按**仓库**判定——注销一条位置会影响这个项目下的所有 任务,包括从它长出来的 managed worktree
  • 原属 repos.go(B62 迁移后 repos 表删除),项目注销的占用校验仍在用它

func (*Store) ActiveTasksByWorkDir

func (s *Store) ActiveTasksByWorkDir(workDir string) ([]proto.Task, error)

ActiveTasksByWorkDir 返回工作目录为 workDir 的全部非终态任务。

参数:

  • workDir: 工作目录绝对路径(原地模式即仓库路径);空串返回空切片

返回:

  • 非终态任务切片(可能为空),按创建时间倒序
  • 查询失败返回错误(调用方按「查不出来就保守拒发」处置)

注意:

  • 终态清单取自 proto.TerminalStates,避免与状态机定义漂移
  • 空 workDir 直接返回空切片:不查是刻意的,managed 模式每任务一棵新树, 天然不冲突,不需要这个判据
  • WHERE 里对空 work_dir 的兜底是给**旧库历史行**的:早期原地模式的 work_dir 存空串(proto.Task.Workdir() 的回退就是为它们写的),那些任务 同样占着仓库。新派发的任务 work_dir 一定是满的

func (*Store) AnswerTicket

func (s *Store) AnswerTicket(id, answer string) error

AnswerTicket 填写工单答案,工单随即从待办中移出。

参数:

  • id: 工单 ID
  • answer: 人工答复内容

注意:

  • 以 answer IS NULL 为更新条件:工单不存在或已回答(不可重复回答)均返回 ErrNotFound
  • 回答成功后刷新所属任务的 updated_at(子查询取 task_id):answer 落库是任务 活动信号,看门狗以此判定「stalled 之后是否有回复」从而二次告警(P1-15a); 否则「已 stalled → 协调者回答 → executor 仍死」永远不再告警

func (*Store) AppendEvent

func (s *Store) AppendEvent(taskID string, typ proto.EventType, payload any) (proto.Event, error)

AppendEvent 为任务追加一条事件,返回落库后带自增 seq 的完整事件。

参数:

  • taskID: 所属任务 ID(与任务表无外键约束,任务不存在时也可追加)
  • typ: 事件类型
  • payload: 任意可 JSON 序列化的值,序列化后存入 payload 列

返回:

  • 带全局自增 seq 的事件记录

注意:

  • seq 由 AUTOINCREMENT 全局分配,跨任务单调递增,可作为全局时间轴排序依据

func (*Store) AppendMirrorEvent added in v0.3.0

func (s *Store) AppendMirrorEvent(taskID string, ev proto.Event) (inserted bool, err error)

AppendMirrorEvent 幂等追加一条远端事件副本。

参数:

  • taskID: 所属任务 ID
  • ev: 事件(Seq 为**远端库**的自增 seq,原值保留)

返回:

  • inserted: true=本次真的插入了新行;false=该 (task_id, seq) 已存在 (重连补拉重复到达),本次为 no-op
  • err: 数据库错误

注意:

  • INSERT OR IGNORE 的幂等语义就是「重连补拉天然去重」:远端是权威,本机 不重编号,重连凭 seq 续拉即可

func (*Store) Close

func (s *Store) Close() error

Close 关闭数据库连接池。

注意:

  • 关闭后 Store 不可再使用;sql.DB.Close 幂等,重复调用返回 nil

func (*Store) ConsumeAuthTicket added in v0.3.0

func (s *Store) ConsumeAuthTicket(hash string, now time.Time) (string, time.Time, error)

ConsumeAuthTicket 原子认领一张未消费的 ticket,并返回它的设备名与过期时刻。

参数:

  • hash: ticket 明文的 SHA-256
  • now: 认领时刻,写入 consumed_at

返回:

  • deviceName / expiresAt: 该 ticket 的登记信息
  • ErrNotFound: 不存在或已被消费(对调用方是同一个结论:这张票不能用)

注意:

  • 一次性**由 SQL 条件 UPDATE 的原子性保证**,不靠「先查后改」——后者在并发下 会让同一张 ticket 换出两个会话
  • **过期不在这里判**:返回 expiresAt 交给调用方比较。原因是本表的时间以 RFC3339Nano 文本存储,Go 的格式化会裁掉尾部零,导致「整秒」与「带小数秒」 的两个时刻在 SQLite 里按字典序比较时次序反转(`.` 的码位小于 `Z`)。 把比较放到 Go 侧用 time.Time 做,才是无坑的

func (*Store) CountEvents

func (s *Store) CountEvents(taskID string, afterSeq, throughSeq int64) (int, error)

CountEvents 统计任务在 (afterSeq, throughSeq] 区间内的事件条数。

参数:

  • taskID: 任务 ID
  • afterSeq: 区间下界(不含)
  • throughSeq: 区间上界(含)

返回:

  • 区间内事件条数
  • 数据库错误

注意:

  • 用途是 WS 重放截断后的缺口核对:seq 由 AUTOINCREMENT 全局分配,跨任务 交错,单任务的 seq **不连续**,因此无法靠「seq 是否逐格衔接」判断缺口, 只能按区间实际条数核对

func (*Store) CreateAuthTicket added in v0.3.0

func (s *Store) CreateAuthTicket(hash, deviceName string, createdAt, expiresAt time.Time) error

CreateAuthTicket 落库一张一次性 ticket。

参数:

  • hash: ticket 明文的 SHA-256(由调用方算好;本层不接触明文)
  • deviceName: 签发时登记的设备名,纯展示
  • createdAt / expiresAt: 签发与过期时刻

返回:

  • 数据库错误;重复 hash 会因主键冲突报错(概率上不可能,视为真故障)

func (*Store) CreateProjectLocation

func (s *Store) CreateProjectLocation(loc *proto.ProjectLocation) error

CreateProjectLocation 写入一条项目位置。

参数:

  • loc: 位置条目;ProjectID/Name/Path/OriginURL 必须非空,CreatedAt 由调用方给定

返回:

  • 错误:项目/名字/路径任一已存在时返回包装 ErrProjectDuplicate 的错误;其余为写库故障

func (*Store) CreateSession added in v0.3.0

func (s *Store) CreateSession(sess *Session) error

CreateSession 落库一个新会话。

参数:

  • sess: 会话数据;ID 与 TokenHash 必须非空,RevokedAt 被忽略(新会话必未吊销)

返回:

  • 数据库错误

func (*Store) CreateTask

func (s *Store) CreateTask(t *proto.Task) error

CreateTask 写入一个新任务。

参数:

  • t: 任务数据;ID 必须非空且唯一,重复 ID 返回底层主键冲突错误

注意:

  • 状态迁移合法性由 UpdateTaskState 校验,此处仅原样入库,不含业务规则

func (*Store) CreateTicket

func (s *Store) CreateTicket(tk *proto.Ticket) (bool, error)

CreateTicket 幂等创建工单:同 id 重复调用不报错,第二次起 created 为 false。

参数:

  • tk: 工单数据;ID 必须非空

返回:

  • created: true 表示本次为首次创建(id 此前不存在)
  • err: 数据库错误

注意:

  • 通过 INSERT OR IGNORE 实现幂等,与 upsert 不同:已存在时保留旧数据,不覆盖
  • answer/answered_at 一律由 AnswerTicket 写入,入参中的值被忽略

func (*Store) DeleteMirrorTask added in v0.3.0

func (s *Store) DeleteMirrorTask(taskID string) (int, error)

DeleteMirrorTask 删除一条镜像任务及其全部镜像事件。

返回:

  • 被删除的事件条数(任务快照本身不存在时不报错,返回 0)

注意:

  • 这是本包唯一的破坏性镜像操作,成功打 Info(task_id + 事件条数)
  • 两个删放在一个事务里:镜像任务是复制品,删除必须原子,不能留 「快照没了、事件还在」的半截

func (*Store) DeleteProjectLocation

func (s *Store) DeleteProjectLocation(name string) error

DeleteProjectLocation 删除一条位置。

返回:

  • 错误:位置不存在时返回 ErrNotFound(而非静默成功——调用方需要知道自己删错了名字)

注意:

  • 只删登记,**不动磁盘上的仓库**

func (*Store) EventsFrom

func (s *Store) EventsFrom(taskID string, fromSeq int64, limit int) ([]proto.Event, error)

EventsFrom 返回任务 taskID 在 seq 之后的事件,按 seq 升序,最多 limit 条。

语义(重要):事件数超过 limit 时返回**最新**的 limit 条(截断掉最旧的), 保证读到的始终是「离 now 最近」的窗口——attach 的 recent_events 依赖 「最新窗口」,若返回最旧的 limit 条,>limit 的积压会让新事件(如 completed) 永远读不到,违反「不丢新事件」。

注意:**WS 重放不得使用本方法**——截断最旧会让客户端 cursor 越过缺口,缺口 永不补齐;WS 重放请用 EventsFromAsc(截断尾部、可续拉)。

参数:

  • taskID: 任务 ID
  • fromSeq: 起始 seq(不含),传 0 表示从头
  • limit: 返回条数上限

注意:

  • 借助 idx_events_task(task_id, seq) 索引加速按任务的时间线扫描

func (*Store) EventsFromAsc

func (s *Store) EventsFromAsc(taskID string, fromSeq int64, limit int) ([]proto.Event, error)

EventsFromAsc 返回任务 taskID 在 fromSeq 之后的事件,按 seq 升序,最多 limit 条。

与 EventsFrom(最新窗口)语义相反(重要):事件数超过 limit 时截断**窗口尾部**、 保留最旧的 limit 条——客户端 cursor 只前进到「确实收到」的最后一条,被截掉的 尾部缺口可凭更大 from_seq 重连续拉,缺口永远可补齐;EventsFrom 截最旧会让 cursor 越过缺口,缺口永久丢失(见该方法的注意)。

用途:WS 重放(server.handleEvents)必须用本方法从头补起。

参数:

  • taskID: 任务 ID
  • fromSeq: 起始 seq(不含),传 0 表示从头
  • limit: 返回条数上限

注意:

  • 借助 idx_events_task(task_id, seq) 索引加速按任务的时间线扫描

func (*Store) FindReusableGrant

func (s *Store) FindReusableGrant(taskID, fingerprint string) (*proto.Ticket, error)

FindReusableGrant 查同任务、同指纹、已被协调者批准且已送达的 gate 工单。

参数:

  • taskID: 任务 id(复用严格限制在任务内,见 spec §3.4)
  • fingerprint: 权限描述全文的 sha256;空串直接返回无匹配

返回:

  • 命中时返回该工单;无匹配返回 (nil, nil);查询出错返回错误

注意:

  • answer 必须**严格等于** "allow"——gate 的翻译规则就是严格相等, 这里放宽(如 LIKE 'allow%')会让 "allowed once, then never" 之类的 人工笔误变成一张长期通行证
  • delivered_at 必须非空:应答落库但中继失败的工单不构成有效先例, executor 侧那次请求根本没收到批准

func (*Store) GetProjectLocationByName

func (s *Store) GetProjectLocationByName(name string) (proto.ProjectLocation, error)

GetProjectLocationByName 按引用名查询单条位置。

返回:

  • 位置条目;不存在时返回 ErrNotFound

func (*Store) GetTask

func (s *Store) GetTask(id string) (*proto.Task, error)

GetTask 按 id 读取任务;不存在返回 ErrNotFound。

参数:

  • id: 任务 ID

返回:

  • 任务数据;不存在时返回 ErrNotFound

func (*Store) GetTicket

func (s *Store) GetTicket(id string) (*proto.Ticket, error)

GetTicket 按 id 读取工单;不存在返回 ErrNotFound。

参数:

  • id: 工单 ID

返回:

  • 工单数据;不存在时返回 ErrNotFound

func (*Store) LatestEvent

func (s *Store) LatestEvent(taskID string) (*proto.Event, error)

LatestEvent 返回任务最新一条事件(seq 最大)。

返回:

  • 任务无任何事件时返回 ErrNotFound

注意:

  • 看门狗(stalled 判定)与启动恢复用「最新事件时刻」判断任务是否卡住, 单条取最新即可,不必拉全量事件

func (*Store) ListMirrorTasks added in v0.3.0

func (s *Store) ListMirrorTasks() ([]MirrorTask, error)

ListMirrorTasks 返回全部镜像任务快照,按快照时刻降序。

注意:

  • 单行快照解析失败只 Warn 跳过(副本脏了不该让看板挂掉),不使整个列表失败
  • 返回的切片可能为空,恒不为 nil

func (*Store) ListProjectLocations

func (s *Store) ListProjectLocations() ([]proto.ProjectLocation, error)

ListProjectLocations 返回本机全部项目位置,按名字字典序。

注意:

  • 返回的 Status 字段恒为空——实际状态由 agentd 侧现场探测后填充

func (*Store) ListSessions added in v0.3.0

func (s *Store) ListSessions() ([]Session, error)

ListSessions 列出全部会话(含已吊销与已过期),按创建时刻降序。

注意:已吊销的会话也返回——`handoff sessions` 要能看到「这台设备已经被吊销了」, 直接消失反而让人怀疑是不是漏看了

func (*Store) ListTasks

func (s *Store) ListTasks() ([]proto.Task, error)

ListTasks 返回全部任务,按 created_at 降序(最新在前)。

注意:

  • created_at 统一为 UTC RFC3339Nano 文本,字典序即时间序,可直接排序
  • **不填充 Task.Cumulative**。列表页不显示累计消耗,为每一行做一次 SUM 是纯浪费;要拿累计值请用 GetTask。这不是 bug,改之前先想清楚代价。

func (*Store) MarkTicketDelivered

func (s *Store) MarkTicketDelivered(id string) error

MarkTicketDelivered 标记工单应答已送达 executor。

参数:

  • id: 工单 ID

注意:

  • 幂等:已标记的工单重复调用不报错(delivered_at IS NULL 条件不成立即无影响)
  • 只有真正把应答交到 executor 手上(RespondPermission/Send 返回成功)之后 才可调用——这是 RecoverStuck 判断「该不该重投」的唯一依据

func (*Store) MirrorEventsFrom added in v0.3.0

func (s *Store) MirrorEventsFrom(taskID string, fromSeq int64, limit int) ([]proto.Event, error)

MirrorEventsFrom 返回任务 taskID 的镜像事件中 seq 大于 fromSeq 的条目, 按 seq 升序,最多 limit 条。

注意:fromSeq 是**开区间**(seq > fromSeq),与本机 EventsFromAsc 的语义一致; 远端 seq 原值保留,重连凭它续拉。

func (*Store) MirrorTaskTarget added in v0.3.0

func (s *Store) MirrorTaskTarget(taskID string) (string, bool, error)

MirrorTaskTarget 返回任务所属机器(mirror_tasks 的路由索引查询)。

返回:

  • ok=true: target 有效(该任务有镜像记录)
  • ok=false: 任务没有镜像记录(从没发现过它),target 为空串
  • err: 数据库错误

func (*Store) MirrorWatermark added in v0.3.0

func (s *Store) MirrorWatermark(taskID string) (int64, error)

MirrorWatermark 返回某任务镜像事件的当前水位(远端 seq 已复制的最大值)。

返回:任务无任何镜像事件时为 0——「首次订阅从头拉」与「没镜像过」同义。

func (*Store) PendingTickets

func (s *Store) PendingTickets(taskID string) ([]proto.Ticket, error)

PendingTickets 返回任务下所有未回答工单(answer IS NULL),按 created_at 升序。

参数:

  • taskID: 任务 ID

注意:

  • 用于 agent 阻塞等待人工答复的场景;回答过的工单不会出现在结果中

func (*Store) RevokeSession added in v0.3.0

func (s *Store) RevokeSession(id string, at time.Time) error

RevokeSession 吊销一个尚未吊销的会话。

返回:

  • ErrNotFound: 会话不存在**或**已被吊销(两者对调用方是同一个结论:这次吊销没改变什么)

func (*Store) SessionByID added in v0.3.0

func (s *Store) SessionByID(id string) (*Session, error)

SessionByID 按会话 id 查会话;不存在返回 ErrNotFound。

func (*Store) SessionByTokenHash added in v0.3.0

func (s *Store) SessionByTokenHash(hash string) (*Session, error)

SessionByTokenHash 按 cookie 哈希查会话;不存在返回 ErrNotFound。

注意:已过期或已吊销的会话**照样返回**——有效性由调用方判定,这样才能把 「不存在 / 已过期 / 已吊销」记成三种不同的鉴权失败原因

func (*Store) SetEventHook added in v0.3.0

func (s *Store) SetEventHook(fn func(proto.Event))

SetEventHook 注册「事件落库后」的回调。传 nil 可取消。

调用时机:INSERT 成功、proto.Event 组装完成之后,AppendEvent 返回之前, **同步**调用。同步是刻意的——它保证「事件入库顺序 == 钩子观察顺序」, 派生出的帧流才能与事件流对齐。

边界(违反会死锁或自我递归):

  • **钩子内不得回调本 Store 的任何方法**。只允许做不回到数据库的动作, 比如往文件追加一行。
  • 钩子不得长时间阻塞:它跑在 AppendEvent 的调用栈上,会拖慢事件落库。
  • 钩子 panic 由本方法内部 recover:一个可见性副作用不该让已经成功的 事件落库变成失败。

func (*Store) SetTaskField

func (s *Store) SetTaskField(id, field, value string) error

SetTaskField 更新任务白名单内的单个字段(branch/executor_session/plan_summary/done_note)。

参数:

  • id: 任务 ID
  • field: 字段名,仅允许白名单四项
  • value: 新值

注意:

  • 白名单之外(如 id/state)返回错误;任务不存在时不报错(不影响其他行即返回 nil)

func (*Store) SetTaskUsage added in v0.3.0

func (s *Store) SetTaskUsage(id, model string, ctxTokens int, ctxWindow *int) error

SetTaskUsage 一次性更新任务的实际模型名与 context 占用。

参数:

  • id: 任务 ID
  • model: 实际模型名;**空串表示本次不更新该列**(保留既有值)
  • ctxTokens: 当前 context 占用;**0 表示不更新**
  • ctxWindow: 上下文窗口上限;**nil 表示不更新**

为什么空值语义是「不更新」而不是「清空」:用量与模型名往往来自**不同的帧** (grok 的窗口在会话建立时到、占用在每次模型调用后到),若空值等于清空, 后到的那一帧会把先到的那一半抹掉。

注意:

  • 三个参数全为空时是空操作,不打库
  • 任务不存在时不报错(与 SetTaskField 一致,不影响其他行即返回 nil)

func (*Store) TaskCumulative added in v0.3.0

func (s *Store) TaskCumulative(taskID string) (*proto.Cumulative, error)

TaskCumulative 对该任务的全部账目求和,得到累计消耗。

返回:

  • 没有任何账目行时返回 (nil, nil)。**不返回零值结构**——0 会被读成 「一共花了 0」,而真相是「还不知道」
  • 花费状态按四条规则定(known=非 unknown 行的 ticks 之和, missing=unknown 行的条数,est=是否含 estimated 行): missing==0 && !est → reported;missing==0 && est → estimated; missing>0 && known>0 → partial(**下界**);missing>0 && known==0 → unknown

注意:estimated 与 missing 同时成立时按 partial——漏账比不准要紧, 而 partial 的展示(下界)也已经隐含了「别当真」。

**本方法读路径高频,成功不打日志**;只在调用方需要时报错(扫描/遍历出错 由调用方带上下文记录)。

func (*Store) TicketHasEvent

func (s *Store) TicketHasEvent(taskID, ticketID string) (bool, error)

TicketHasEvent 判定某工单的通知事件(permission_request / question)是否已落库。

参数:

  • taskID: 任务 ID(事件按任务分区存储)
  • ticketID: 工单 ID,与事件 payload 里的 ticket_id 精确比对

返回:

  • 是否已有对应通知事件
  • 数据库错误

注意:

  • 用途是「工单已创建但通知事件缺失」的自愈判定(崩溃恰好落在两次写之间): 仅凭工单存在就认定为重放,会把协调者的唤醒事件永久吞掉
  • payload 是 JSON 文本,这里取回后在 Go 侧精确解码比对,不用 LIKE 匹配 (ticket_id 含 `_` 等 LIKE 通配符,字符串匹配会误判)
  • 单任务的问答类事件量级在几十条,全量扫描代价可忽略;且只在重放分支调用

func (*Store) TouchSession added in v0.3.0

func (s *Store) TouchSession(id string, lastSeen, expiresAt time.Time) error

TouchSession 写回会话的最后活跃时刻与过期时刻(滑动续期)。

注意:调用方负责节流——本方法每次调用都真写库

func (*Store) UndeliveredAnswers

func (s *Store) UndeliveredAnswers(taskID string) ([]proto.Ticket, error)

UndeliveredAnswers 返回任务里「已应答但未送达 executor」的工单,按应答时间升序。

参数:

  • taskID: 任务 ID

返回:

  • 待重投的工单列表(可能为空)
  • 数据库错误

注意:

  • 作废工单(answer = VoidAnswer)不在其中:它们是「任务已终结,不会再被回答」 的墓碑,不是待送达的裁决
  • 这是「协调者 reply 得到 502 之后」的可恢复面:列表非空即说明有裁决卡在半路

func (*Store) UpdateProjectLocation added in v0.3.0

func (s *Store) UpdateProjectLocation(name, newName, newPath string) (proto.ProjectLocation, error)

UpdateProjectLocation 改一条位置登记的引用名与/或路径。

参数:

  • name: 当前引用名(定位用)
  • newName: 新引用名;空串表示不改
  • newPath: 新路径;空串表示不改

返回:更新后的记录;

  • ErrNotFound:name 不存在
  • ErrProjectDuplicate:新名字或新路径已被占用

func (*Store) UpdateTaskState

func (s *Store) UpdateTaskState(id string, st proto.TaskState) error

UpdateTaskState 将任务状态迁移到 st,迁移合法性由 proto.CanTransit 校验。

参数:

  • id: 任务 ID;不存在返回 ErrNotFound
  • st: 目标状态

返回:

  • 非法迁移返回 ErrBadTransit;任务不存在返回 ErrNotFound

注意:

  • 非法迁移是本包唯一打 Warn 日志的点:排障时可直接定位谁想从哪迁到哪被拒
  • 写回采用 CAS(WHERE state = 读到的旧状态):若并发写者先变更了状态,本方法返回 ErrBadTransit,调用方应用最新快照重试意图;不会静默覆盖并发迁移

func (*Store) UpsertMirrorTask added in v0.3.0

func (s *Store) UpsertMirrorTask(target string, task proto.Task, fetchedAt time.Time) error

UpsertMirrorTask 覆盖式写入(或更新)一条任务快照。

参数:

  • target: 任务所在机器的名字(本机 cfg.Targets 的键)
  • task: 任务体 JSON,整体序列化进 snapshot 列
  • fetchedAt: 本次快照的时刻(UI 据此显示数据新旧)

注意:

  • upsert 语义:同 task_id 重复调用用新快照覆盖旧快照,不报错

func (*Store) UpsertSpend added in v0.3.0

func (s *Store) UpsertSpend(taskID string, e proto.SpendEntry) error

UpsertSpend 记一条消耗账目;同 (taskID, e.Key) **覆盖**既有行。

参数:

  • taskID: 所属任务
  • e: 账目。三个 token 分项的口径是归一化后的值——**输入不含缓存**、 **缓存输入 = 读缓存 + 写缓存**、**输出含 reasoning**。四家 executor 的 原始字段含义互不相同(codex/grok 的输入含缓存要减,claudecode/opencode 的要加;opencode 的 reasoning 与 output 平行要加,codex/grok 的是子集 不能加),归一化在各 adapter 的 spend.go 里完成,本方法不做换算。

注意:

  • e.Key 为空时直接返回错误——没有键就没有幂等,宁可报错也不写一行永远 去不掉重的账
  • 覆盖而非累加是刻意的,理由见 proto.SpendEntry 的注释
  • **不打成功日志**:频率与 assistant 消息同级,会刷屏;错误由调用方 handleSpend 打(见 Task 2)

func (*Store) VoidPendingTickets

func (s *Store) VoidPendingTickets(taskID string) (int, error)

VoidPendingTickets 把任务全部未回答工单作废(answer 置为 VoidAnswer)。

参数:

  • taskID: 任务 ID

返回:

  • 被作废的工单数(本次更新行数)

注意:

  • 幂等:重复调用第二次起返回 0(已作废的工单不再更新)
  • 不删除工单:request/answered_at 等审计痕迹保留,回答语义上视为已终结; 回答过的工单不受影响
  • 由 agentd 启动恢复(RecoverOnStartup)在判定任务 dead 后调用;hub 侧等待 者随进程消亡不存在,无需清理

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL