Canonical SQLite 深度解析
Canonical SQLite 模块拆解:从真实源码行号看 agentacct 如何把本地证据、隐私边界和控制面分开。
导入链与契约
canonical store 子页面关注数据层约束。src/agent_chronicle/canonical/sqlite.py:50 到 src/agent_chronicle/canonical/sqlite.py:63 定义 application id、schema version 和 live filename,src/agent_chronicle/canonical/sqlite.py:132 到 src/agent_chronicle/canonical/sqlite.py:153 给迁移步骤计算 checksum。测试 tests/test_canonical_schema.py:12 和 tests/test_canonical_schema.py:49 又把 STRICT、WAL、owner-only 与禁用 raw payload 字段钉住。 tests/test_canonical_schema.py:80,这里再看 tests/test_canonical_schema.py:203
50APPLICATION_ID = 0x41434354 # "ACCT" 51# Version 1 was the spike's unstable DDL (it drifted while the number stood 52# still); version 2 is the first checksum-pinned schema; version 3 admits the 53# 'live' store role for the writer-integration phase; version 4 adds the 54# session_observed_lineage absorb-state table. v1 candidates are rebuilt 55# from a fresh snapshot, never migrated; v2+ stores migrate forward. 56SCHEMA_VERSION = 4 57SCHEMA_BASE_NAME = "canonical-v4" 58APP_VERSION_DEFAULT = "sqlite-truth-spike" 59MINIMUM_SQLITE_VERSION = (3, 37, 0) 60TASK_PUBLIC_ID_PREFIX = "task_" 61LIVE_STORE_FILENAME = "chronicle.sqlite3" 62_MAX_SQLITE_INT = 9_223_372_036_854_775_807 63# Bare directory names, not <name>/state: live state also lives beside state/ 64# (policy.yaml) and the -global root itself serves as a hooks project-dir. 65_FORBIDDEN_STATE_SEQUENCES = ( 66 (".agent-sentinel",), 67 (".agent-sentinel-global",), 68 (".agent-chronicle",), 69 (".agent-chronicle-global",), 70) 71 72_GET_TASK_SQL = "SELECT * FROM rm_task_current WHERE public_task_id = ?" 73_LIST_TASKS_SQL = ( 74 "SELECT * FROM rm_task_current INDEXED BY idx_rm_task_work_recent " 75 "WHERE (sort_activity_at_us, public_task_id) < (?, ?) " 76 "ORDER BY sort_activity_at_us DESC, public_task_id DESC LIMIT ?" 77) 78_LIST_SESSIONS_SQL = ( 79 "SELECT * FROM sessions INDEXED BY idx_sessions_recent " 80 "WHERE (sort_activity_at_us, session_id) < (?, ?) " 81 "ORDER BY sort_activity_at_us DESC, session_id DESC LIMIT ?" 82) 83# Batched point lookups for surface labeling (phase 4.2): each resolves by 84# primary key / a pinned index over an id list bounded by the page the caller 85# just fetched. The placeholder count varies with the batch; the shape does 86# not. 87_SESSIONS_BY_IDS_SQL = "SELECT * FROM sessions WHERE session_id IN ({placeholders})" 88_SOURCES_BY_IDS_SQL = ( 89 "SELECT * FROM source_instances WHERE source_instance_id IN ({placeholders})" 90) 91# At most one row per child: uq_session_edges_one_valid_parent is 92# relation-agnostic (one valid edge per child, whatever its relation), and 93# writers mint the relation from the child's session kind — 'continuation' 94# and 'retry' children carry their single validated lineage edge under those 95# relations, so filtering to relation='parent' would silently drop them. 96_VALID_LINEAGE_EDGES_SQL = ( 97 "SELECT child_session_id, parent_session_id, relation " 98 "FROM session_edges INDEXED BY idx_session_edges_child " 99 "WHERE child_session_id IN ({placeholders}) " 100 "AND validation_state = 'valid'" 101) 102# Bound for every by-ids batch: one page of the widest list surface. 103_BY_IDS_BATCH_LIMIT = 2_000 104_HEALTH_STORE_SQL = ( 105 "SELECT store_uuid, store_role, schema_version, canonical_sequence " 106 "FROM store_metadata WHERE singleton = 1" 107) 108# The single authority for which rm_* read models the minimal rebuild 109# maintains. The rebuild policy (canonical_live) and the health probe 110# (canonical_read) both derive from this tuple — a new read model added to 111# rebuild_minimal_read_models without updating this constant would silently 112# escape staleness detection and never_built synthesis. 113MINIMAL_READ_MODEL_NAMES = ("rm_task_current", "rm_usage_day")
核心实现,逐段拆
▸Schema version 与迁移校验
src/agent_chronicle/canonical/sqlite.py:132 到 src/agent_chronicle/canonical/sqlite.py:190 展示第一层实现。这里不是抽象概念,而是把边界条件写成代码路径:调用方只能沿这些函数和常量进入,异常和漂移被计数或拒绝。 src/agent_chronicle/canonical/sqlite.py:135,这里再看 src/agent_chronicle/canonical/sqlite.py:141,这里再看 src/agent_chronicle/canonical/sqlite.py:188
132@dataclass(frozen=True) 133class SchemaMigration: 134 """One forward schema step from ``from_version`` to ``from_version + 1``. 135 136 Statements run one by one inside a single transaction (never via 137 ``executescript``, which would commit early) and must produce exactly the 138 DDL that compiling the target build's packaged ``schema.sql`` produces — 139 the open-time inventory comparison enforces that equivalence. 140 """ 141 142 from_version: int 143 name: str 144 statements: tuple[str, ...] 145 146 @property 147 def checksum(self) -> str: 148 digest = hashlib.sha256(b"agent-chronicle-schema-migration-v1\x00") 149 for statement in self.statements: 150 payload = statement.encode("utf-8") 151 digest.update(len(payload).to_bytes(8, "big")) 152 digest.update(payload) 153 return digest.hexdigest() 154 155 156# Forward steps keyed by from_version. Version 1 was the spike's unstable 157# DDL, so v1 candidates rebuild rather than migrate. Statements are frozen 158# literals: the registered checksum is validated against every previously 159# migrated store on open, so a step may never be regenerated from the moving 160# schema.sql — the inventory comparison is what proves the literal still 161# produces the packaged DDL. 162SCHEMA_MIGRATIONS: dict[int, SchemaMigration] = { 163 2: SchemaMigration( 164 from_version=2, 165 name="allow-live-store-role", 166 statements=( 167 # SQLite cannot alter a CHECK constraint in place; rebuild the 168 # singleton table around the copied row. The final CREATE must 169 # stay byte-identical to the store_metadata block in schema.sql. 170 "CREATE TABLE store_metadata_role_migration AS " 171 "SELECT singleton, store_uuid, store_role, schema_version, " 172 "canonical_sequence, created_at_us FROM store_metadata", 173 "DROP TABLE store_metadata", 174 ( 175 "CREATE TABLE store_metadata (\n" 176 " singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n" 177 " store_uuid TEXT NOT NULL UNIQUE,\n" 178 " store_role TEXT NOT NULL CHECK (store_role IN ('candidate', 'live')),\n" 179 " schema_version INTEGER NOT NULL CHECK (schema_version >= 1),\n" 180 " canonical_sequence INTEGER NOT NULL DEFAULT 0 CHECK (canonical_sequence >= 0),\n" 181 " created_at_us INTEGER NOT NULL CHECK (created_at_us >= 0)\n" 182 ") STRICT" 183 ), 184 "INSERT INTO store_metadata(singleton, store_uuid, store_role, " 185 "schema_version, canonical_sequence, created_at_us) " 186 "SELECT singleton, store_uuid, store_role, schema_version, " 187 "canonical_sequence, created_at_us FROM store_metadata_role_migration", 188 "DROP TABLE store_metadata_role_migration", 189 ), 190 ),
▸迁移在事务里逐条执行
src/agent_chronicle/canonical/sqlite.py:228 到 src/agent_chronicle/canonical/sqlite.py:316 是第二层实现,重点在失败路径和原子性。页面关注的是这些分支如何避免把未验证输入提升为 authority。 src/agent_chronicle/canonical/sqlite.py:232,这里再看 src/agent_chronicle/canonical/sqlite.py:240,这里再看 src/agent_chronicle/canonical/sqlite.py:313
228def _apply_schema_migrations( 229 connection: sqlite3.Connection, 230 *, 231 from_version: int, 232 app_version: str, 233) -> int: 234 journal_mode = str(connection.execute("PRAGMA journal_mode").fetchone()[0]).lower() 235 if journal_mode != "wal": 236 raise ValueError("candidate database is not in WAL mode; refusing to migrate") 237 metadata_version = connection.execute( 238 "SELECT schema_version FROM store_metadata WHERE singleton = 1" 239 ).fetchone() 240 if metadata_version is None or int(metadata_version[0]) < from_version: 241 raise ValueError("candidate metadata disagrees with the schema version; refusing to migrate") 242 current = from_version 243 while current < SCHEMA_VERSION: 244 step = SCHEMA_MIGRATIONS.get(current) 245 if step is None: 246 raise ValueError( 247 f"candidate schema version {current} has no upgrade path in " 248 f"this build; rebuild the candidate from a fresh snapshot" 249 ) 250 if step.from_version != current: 251 raise ValueError( 252 f"schema migration '{step.name}' is registered for version " 253 f"{current} but declares from_version {step.from_version}" 254 ) 255 target = current + 1 256 try: 257 connection.execute("BEGIN IMMEDIATE") 258 except sqlite3.OperationalError as exc: 259 raise ValueError( 260 f"schema migration '{step.name}' to version {target} could not " 261 f"acquire the write lock; another process may be migrating" 262 ) from exc 263 try: 264 # Re-read versions under the exclusive lock: a concurrent writable 265 # open may have finished this step (or the whole chain) while we 266 # waited. Adopting its committed progress keeps racing opens 267 # idempotent instead of failing on the stamping UNIQUE constraint. 268 observed = int(connection.execute("PRAGMA user_version").fetchone()[0]) 269 row = connection.execute( 270 "SELECT schema_version FROM store_metadata WHERE singleton = 1" 271 ).fetchone() 272 stamped = None if row is None else int(row[0]) 273 if observed != current or stamped != current: 274 if stamped == observed and current < observed <= SCHEMA_VERSION: 275 connection.execute("ROLLBACK") 276 current = observed 277 continue 278 raise ValueError( 279 "candidate metadata disagrees with the schema version; refusing to migrate" 280 ) 281 for statement in step.statements: 282 connection.execute(statement) 283 if not connection.in_transaction: 284 # A statement that is itself COMMIT/ROLLBACK/END would 285 # make the stamping writes below autocommit one by one, 286 # silently breaking per-step atomicity. 287 raise ValueError( 288 f"schema migration '{step.name}' statement ended the " 289 f"transaction; step atomicity cannot be proved" 290 ) 291 connection.execute( 292 "INSERT INTO schema_migrations(version, name, checksum, app_version, applied_at_us) " 293 "VALUES (?, ?, ?, ?, ?)", 294 (target, step.name, step.checksum, app_version, now_us()), 295 ) 296 connection.execute( 297 "UPDATE store_metadata SET schema_version = ? WHERE singleton = 1", 298 (target,), 299 ) 300 connection.execute(f"PRAGMA user_version = {target}") 301 connection.execute("COMMIT") 302 except BaseException as exc: 303 if connection.in_transaction: 304 try: 305 connection.execute("ROLLBACK") 306 except sqlite3.Error: 307 pass 308 if isinstance(exc, sqlite3.Error): 309 # A distinct error class keeps migration failures out of the 310 # generic unreadable-DDL diagnosis in open(). 311 raise ValueError( 312 f"schema migration '{step.name}' to version {target} failed" 313 ) from exc 314 raise 315 current = target 316 return current
关键流程
▸实现判断
它不像普通 sqlite helper 那样自动选默认路径;候选库必须显式创建,迁移也要持有写锁、验证 metadata、逐条执行 DDL 并 stamp schema_migrations。 这条路径还可以和主页面的 src/agent_chronicle/source_discovery.py:37、src/agent_chronicle/capture_runtime.py:41、src/agent_chronicle/control_web.py:50 连起来看:agentacct 的策略是收集证据但不收集敏感正文,展示状态但不泄露执行权柄。
边界 / 失败模式
src/agent_chronicle/canonical/sqlite.py:50附近的入口如果收到不可信输入,只能产出降级或拒绝,不得写入 raw prompt/response。src/agent_chronicle/canonical/sqlite.py:132的分支必须和测试夹具保持同步,避免客户端格式漂移后静默误归因。src/agent_chronicle/canonical/sqlite.py:316附近的失败路径需要持续保留计数或错误码,方便 dashboard 暴露健康状态。