agentacctacct
§ 00 · Source-level Observatory

agentacct 本地 Agent 工作账本

agentacct 是一个 local-first 的 coding-agent 工作账本:它读取 Codex、Claude Code、Hermes、OpenCode 等本地使用证据,却刻意不保存 prompt、response 或工具参数;再把使用、工作事件、证据链接和受控执行投影到只在本机暴露的 dashboard。

★ 375
GitHub Stars
26K+
LOC
190
Files
today
Last commit
local logs ──▶ evidence rows ──▶ canonical SQLite ──▶ localhost dashboard ▲ │ │ └──── privacy guardrails ◀──── control projection
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

agentacct 解决的是 coding-agent 时代一个很具体的问题:团队开了很多 Codex、Claude Code、OpenCode 或自建代理后,谁做了什么、花了多少 token、哪些工作有机器证据,往往散落在不同客户端日志里。源码入口 src/agent_chronicle/source_discovery.py:37 先把这些客户端按来源探测出来,src/agent_chronicle/usage_truth.py:49 又把每种来源的可信等级写成表,而不是把所有数字都当成账单真相。src/agent_chronicle/source_discovery.py:37,这里再看 src/agent_chronicle/source_discovery.py:48,这里再看 src/agent_chronicle/source_discovery.py:58,这里再看 src/agent_chronicle/source_discovery.py:62,这里再看 src/agent_chronicle/source_discovery.py:65,这里再看 src/agent_chronicle/source_discovery.py:86,这里再看 src/agent_chronicle/source_discovery.py:95,这里再看 src/agent_chronicle/source_discovery.py:126,这里再看 src/agent_chronicle/source_discovery.py:140,这里再看 src/agent_chronicle/source_discovery.py:149

更值得拆的是它的克制:src/agent_chronicle/capture_runtime.py:18 明确 raw hook input 不回显,src/agent_chronicle/control_web.py:50 对控制台投影做脱敏,src/agent_chronicle/canonical/sqlite.py:50 用 application id、schema version 和 owner-only 文件权限约束候选库。这个项目不是另一个自动调度器,而是试图把本地证据、成本估算和可控执行分层。src/agent_chronicle/log_evidence.py:54,这里再看 src/agent_chronicle/log_evidence.py:62,这里再看 src/agent_chronicle/log_evidence.py:74,这里再看 src/agent_chronicle/log_evidence.py:113,这里再看 src/agent_chronicle/log_evidence.py:124,这里再看 src/agent_chronicle/log_evidence.py:133,这里再看 src/agent_chronicle/log_evidence.py:141,这里再看 src/agent_chronicle/log_evidence.py:159,这里再看 src/agent_chronicle/log_evidence.py:167,这里再看 src/agent_chronicle/log_evidence.py:189

▸核心数据

架构模式
本地日志发现 → evidence/event 归一化 → SQLite canonical model → FastAPI/HTML 只读或受控执行投影。
核心抽象
UsageSourceDiscovery、UsageTruthRow、log-evidenced event id、CanonicalStore、ControlProjection。
性能/精度数据
候选库使用 WAL、STRICT table、busy_timeout;UI 默认限制 recent activity、attention strip 和 advanced evidence rows。
下游依赖
Codex/Claude Code/OpenCode/Hermes/OpenClaw 本地日志、MCP workflow events、host hook capture、OTLP/Paperclip/Entire connectors。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

本地日志发现与隐私边界

第一层不是导入,而是发现。src/agent_chronicle/source_discovery.py:45 返回六类来源的状态,src/agent_chronicle/source_discovery.py:58 和 src/agent_chronicle/source_discovery.py:86 分别处理 Claude Code 与 Codex,重要的是它只证明“来源存在”和“可导入路径”,不把未解析内容包装成确定成本。tests/test_source_discovery.py:63,这里再看 tests/test_source_discovery.py:82,这里再看 tests/test_source_discovery.py:118,这里再看 tests/test_source_discovery.py:141,这里再看 tests/test_source_discovery.py:151,这里再看 tests/test_source_discovery.py:172,这里再看 tests/test_source_discovery.py:179,这里再看 tests/test_source_discovery.py:201

▸系统拓扑

client homes ── scan paths ──▶ UsageSourceDiscovery │ │ │ ├─ status: found/missing/error │ ├─ confidence: client_reported/unknown ▼ ▼ import-local command ◀── notes/importer gate

▸模块清单

模块分工很清楚:src/agent_chronicle/source_discovery.py:37 管发现,src/agent_chronicle/log_evidence.py:113 管从客户端日志里抽取可信 event id,src/agent_chronicle/canonical/sqlite.py:228 管候选库迁移,src/agent_chronicle/control_web.py:50 管 dashboard 投影脱敏。src/agent_chronicle/canonical/sqlite.py:50,这里再看 src/agent_chronicle/canonical/sqlite.py:56,这里再看 src/agent_chronicle/canonical/sqlite.py:61,这里再看 src/agent_chronicle/canonical/sqlite.py:162,这里再看 src/agent_chronicle/canonical/sqlite.py:228,这里再看 src/agent_chronicle/canonical/sqlite.py:257,这里再看 src/agent_chronicle/canonical/sqlite.py:281,这里再看 src/agent_chronicle/canonical/sqlite.py:291,这里再看 src/agent_chronicle/canonical/sqlite.py:300,这里再看 src/agent_chronicle/canonical/sqlite.py:303

source_discovery
只返回状态、路径、session_count、confidence 和 importer 命令,避免把文件存在误当成完整账单。
log_evidence
只允许五个 creation tools 的输出捐赠 event id,read tools 即使形似也拒绝。
canonical/sqlite
候选库必须显式路径、WAL、STRICT schema、迁移校验和,避免悄悄污染 live store。

▸关键数据结构

src/agent_chronicle/source_discovery.py · L18-35Python
18@dataclass(frozen=True)
19class UsageSourceDiscovery:
20    client: str
21    display_name: str
22    status: SourceStatus
23    evidence: str
24    paths: list[str]
25    file_count: int
26    session_count: int | None
27    latest_updated_at: int | None
28    usage_confidence: str
29    cost_confidence: str
30    importer: str | None
31    notes: list[str]
32
33    def to_dict(self) -> dict[str, object]:
34        return asdict(self)
35
深度阅读 · 日志证据绑定如何拒绝伪造 event id
§ 02 · CORE MODULES

证据绑定与 canonical truth model

▸Usage source discovery:发现但不越权

src/agent_chronicle/source_discovery.py:86 的 Codex 分支同时看 state_5.sqlite 和 rollout JSONL,src/agent_chronicle/source_discovery.py:128 的注释说明 rollout identity 可以形成 session observation,但 usage truth 仍交给 ingestion health。这个设计把“有 session”与“有可信 token/cost”拆开。src/agent_chronicle/control_web.py:50,这里再看 src/agent_chronicle/control_web.py:57,这里再看 src/agent_chronicle/control_web.py:62,这里再看 src/agent_chronicle/control_web.py:77,这里再看 src/agent_chronicle/control_web.py:100,这里再看 src/agent_chronicle/control_web.py:130,这里再看 src/agent_chronicle/control_web.py:150,这里再看 src/agent_chronicle/control_web.py:172,这里再看 src/agent_chronicle/control_web.py:181,这里再看 src/agent_chronicle/control_web.py:208

src/agent_chronicle/source_discovery.py · L86-153Python
86def _discover_codex_source(codex_home: Path | None) -> UsageSourceDiscovery:
87    source_plan = resolve_usage_source_paths("codex", explicit_home=codex_home)
88    homes = list(source_plan.homes)
89    session_roots = [path for home in homes for path in (home / "sessions", home / "archived_sessions")]
90    active_session_roots = [home / "sessions" for home in homes]
91    files_by_home = [
92        _dedupe_paths(_matching_files(root, ["rollout-*.jsonl"]))
93        for root in active_session_roots
94    ]
95    files = _dedupe_paths(path for home_files in files_by_home for path in home_files)
96    db_paths_by_home = [
97        home / "state_5.sqlite" if _is_file(home / "state_5.sqlite") else None
98        for home in homes
99    ]
100    db_paths = [path for path in db_paths_by_home if path is not None]
101    db_session_ids_by_home = [
102        _sqlite_text_values(db_path, "threads", "id") if db_path is not None else set()
103        for db_path in db_paths_by_home
104    ]
105    db_usage_counts = [
106        _sqlite_count(db_path, "threads", where="tokens_used > 0") if db_path is not None else 0
107        for db_path in db_paths_by_home
108    ]
109    db_error_count = sum(
110        session_ids is None or usage_count is None
111        for session_ids, usage_count in zip(db_session_ids_by_home, db_usage_counts, strict=True)
112    )
113    db_usage_count = sum(count or 0 for count in db_usage_counts)
114    rollout_session_ids_by_home = [
115        _codex_rollout_session_ids(home_files, root=root)
116        for home_files, root in zip(files_by_home, active_session_roots, strict=True)
117    ]
118    session_count = sum(
119        len((db_session_ids or set()) | rollout_session_ids)
120        for db_session_ids, rollout_session_ids in zip(
121            db_session_ids_by_home,
122            rollout_session_ids_by_home,
123            strict=True,
124        )
125    )
126    paths = _existing_or_attempted_paths([*db_paths, *session_roots])
127    latest = _latest_mtime([*files, *db_paths])
128    # Rollout identity now produces an honest session observation even when a
129    # state-db row or token_count event is absent. Source discovery is only a
130    # presence scan, so a rollout file is enough to expose the import path;
131    # parse/usage truth remains the ingestion health endpoint's job.
132    found = session_count > 0
133    notes = ["rollout session identity and token_count events; local cost usually unavailable"]
134    if db_error_count:
135        notes.append(
136            f"{db_error_count} Codex state database(s) could not be read; inspect or repair state_5.sqlite"
137        )
138    if source_plan.omitted_home_count:
139        notes.append(f"source path plan omitted {source_plan.omitted_home_count} path(s) after its safety limit")
140    return UsageSourceDiscovery(
141        client="codex",
142        display_name="Codex",
143        status="error" if db_error_count else "found" if found else "missing",
144        evidence="state-db+rollout-jsonl",
145        paths=paths,
146        file_count=len(files),
147        session_count=session_count or None,
148        latest_updated_at=latest,
149        usage_confidence=USAGE_CLIENT_REPORTED if db_usage_count else USAGE_UNKNOWN,
150        cost_confidence=COST_UNKNOWN,
151        importer="agentacct usage import-local --client codex" if found else None,
152        notes=notes,
153    )

▸Log evidence:allowlist-first 的事件配对

src/agent_chronicle/log_evidence.py:54 固定五个 creation tools,src/agent_chronicle/log_evidence.py:113 严格检查单个 created event shape,src/agent_chronicle/log_evidence.py:141 和 src/agent_chronicle/log_evidence.py:189 又分别处理 Codex namespace 与新 MCP invocation 形态。src/agent_chronicle/capture_runtime.py:11,这里再看 src/agent_chronicle/capture_runtime.py:18,这里再看 src/agent_chronicle/capture_runtime.py:20,这里再看 src/agent_chronicle/capture_runtime.py:33,这里再看 src/agent_chronicle/capture_runtime.py:41,这里再看 src/agent_chronicle/capture_runtime.py:53,这里再看 src/agent_chronicle/capture_runtime.py:73,这里再看 src/agent_chronicle/capture_runtime.py:85,这里再看 src/agent_chronicle/capture_runtime.py:96,这里再看 src/agent_chronicle/capture_runtime.py:99

src/agent_chronicle/log_evidence.py · L54-138Python
54# The ONLY tools whose responses may donate an evidenced event id. Every one
55# of them returns exactly one created event under a top-level "event" key
56# (mcp.py); read tools (sentinel_list_events, sentinel_get_event_summary,
57# sentinel_list_runs, sentinel_get_report, ...) echo foreign ids and must
58# never appear here.
59# frozen: the sentinel_* tool names survive the Agent Chronicle rename —
60# historical stores/logs/files carry them forever, and three shipped
61# instruction surfaces plus real user CLAUDE.md/AGENTS.md files quote them.
62SENTINEL_CREATION_TOOLS = frozenset(
63    {
64        "sentinel_record_event",
65        "sentinel_attach_client_context",
66        "sentinel_record_section",
67        "sentinel_record_agent_usage_debug",
68        "sentinel_record_machine_check",
69    }
70)
71
72# \Z (not $) so a trailing newline is rejected: 'evt_deadbeef\n' must NOT
73# pass at any of the .match() call sites ($ matches before a final newline).
74EVIDENCED_EVENT_ID_RE = re.compile(r"^evt_[0-9a-f]{6,64}\Z")
75
76# Real max observed in one session: 157 distinct created ids. The cap bounds
77# hostile/huge sessions; the true total is always preserved alongside so
78# overflow is visible, never silent.
79EVIDENCED_EVENT_IDS_CAP = 200
80
81# Codex fork rollouts can begin by replaying the parent's transcript, including
82# old MCP creation responses.  A small clock-skew allowance keeps near-boundary
83# facts joinable while still rejecting the impossible case: an event recorded
84# well before the alleged donor session existed.
85CODEX_REPLAY_CLOCK_SKEW_SECONDS = 5.0
86CODEX_REPLAY_REJECTION_REASON = "codex_event_before_session_start"
87CODEX_REPLAY_TIME_UNPROVEN_REASON = "codex_replay_timestamps_unproven"
88SOURCE_NAMESPACE_CONFLICT_REASON = "source_namespace_conflict"
89CROSS_KIND_DONOR_IDENTITY_CONFLICT_REASON = "cross_kind_donor_identity_conflict"
90CANONICAL_EVENT_IDENTITY_CONFLICT_REASON = "canonical_event_identity_conflict"
91
92# The documented MCP registration names (every current setup path writes
93# "agentacct"). Claude Code transcripts carry the registration key verbatim in
94# `mcp__<server>__<tool>`; Codex normalizes the config key to underscores in the
95# rollout `namespace` field (`mcp__agent_chronicle` / `mcp__agent_sentinel`; the
96# hyphen-free `agentacct` normalizes to itself). A custom registration name
97# loses evidence links (skipped-but-counted, never guessed) — documented in
98# INSTALL.md.
99#
100# ALL THREE names are accepted FOREVER: `agentacct` is what new installs now
101# register, and the pre-rename `agent-chronicle` / `agent-sentinel` keys stay
102# accepted because historical client logs carry them forever and pre-rename
103# registrations keep producing them in new sessions — dropping an old key would
104# silently unlink every pre-rename session on the next import.
105SENTINEL_SERVER_KEY = "agentacct"  # the name new installs register
106ACCEPTED_SERVER_KEYS = frozenset({"agentacct", "agent-chronicle", "agent-sentinel"})
107_CODEX_ACCEPTED_NAMESPACES = frozenset(
108    {"agentacct", "agent-chronicle", "agent_chronicle", "agent-sentinel", "agent_sentinel"}
109)
110_CLAUDE_TOOL_PREFIX = "mcp__"
111
112
113def extract_created_event_id(text: str) -> str | None:
114    """Strict single-created-event shape check shared by every wire adapter.
115
116    Accepts exactly the creation-tool payload shape: a JSON dict with a
117    top-level ``"event"`` dict whose ``event_id`` is a well-formed evt id.
118    Rejects (belt-and-braces — the tool-name allowlist already excludes read
119    tools): list-shaped payloads carrying ``"events"``/``"runs"`` lists,
120    error texts, truncated/torn JSON, outcome-only machine_check payloads,
121    and any non-dict shape.
122    """
123
124    try:
125        payload = json.loads(text)
126    except (json.JSONDecodeError, ValueError):
127        return None
128    if not isinstance(payload, dict):
129        return None
130    event = payload.get("event")
131    if not isinstance(event, dict):
132        return None
133    if any(isinstance(payload.get(key), list) for key in ("events", "runs")):
134        return None
135    event_id = event.get("event_id")
136    if not isinstance(event_id, str) or not EVIDENCED_EVENT_ID_RE.match(event_id):
137        return None
138    return event_id
深度阅读 · Canonical SQLite 如何保证候选库可审计
§ 03 · KEY FLOW

从采集到控制台的读写流

写入路径和展示路径都在强调 fail closed。src/agent_chronicle/capture_runtime.py:41 捕获 host hook payload,异常时返回 fail_open receipt 但不让 host hook 失败;src/agent_chronicle/control_web.py:172 又把外部 Codex/Claude 标为 observed_only,避免 dashboard 暗示它能控制未由 agentacct 启动的进程。src/agent_chronicle/source_discovery.py:37,这里再看 src/agent_chronicle/source_discovery.py:48,这里再看 src/agent_chronicle/source_discovery.py:58,这里再看 src/agent_chronicle/source_discovery.py:62,这里再看 src/agent_chronicle/source_discovery.py:65,这里再看 src/agent_chronicle/source_discovery.py:86,这里再看 src/agent_chronicle/source_discovery.py:95,这里再看 src/agent_chronicle/source_discovery.py:126,这里再看 src/agent_chronicle/source_discovery.py:140,这里再看 src/agent_chronicle/source_discovery.py:149

▸Usage evidence pipeline

client logs ─▶ source discovery ─▶ import-local ─▶ usage truth rows │ │ │ └─ no raw text └─ confidence tags └─ health state separate

▸Owned execution control pipeline

register workspace/agent ─▶ create contract ─▶ pending attempt ─▶ launch approval ─▶ sanitized projection │ │ │ └──── fixed argv ────┴──── idempotency ───┴──── no process secrets in HTML
深度阅读 · Local control surface 为什么只展示决策状态
§ 04 · TESTS & VALIDATION

测试把安全声明钉到行为上

测试覆盖不是装饰。tests/test_source_discovery.py:63 验证 found/missing/importer/confidence,tests/test_log_evidenced_linking.py:188 钉住五个 creation tools 的配对规则,tests/test_canonical_schema.py:49 明确 schema 不得出现 prompt/response/tool_results 字段,tests/test_control_api.py:81 验证控制 API 不泄露路径、argv 或 nonce。

▸测试覆盖

下面这段测试证明 source discovery 把 Codex、Claude、Hermes、OpenClaw 和 OpenCode 分别报告,不会因某个来源缺失就制造成功。它还让 tests/test_source_discovery.py:82 到 tests/test_source_discovery.py:91 的断言覆盖 session_count、cost_confidence 与 importer。tests/test_source_discovery.py:63,这里再看 tests/test_source_discovery.py:82,这里再看 tests/test_source_discovery.py:118,这里再看 tests/test_source_discovery.py:141,这里再看 tests/test_source_discovery.py:151,这里再看 tests/test_source_discovery.py:172,这里再看 tests/test_source_discovery.py:179,这里再看 tests/test_source_discovery.py:201

tests/test_source_discovery.py · L63-91Python
63def test_discover_usage_sources_reports_found_and_pending_sources(tmp_path):
64    codex_home = _make_codex_source(tmp_path)
65    claude_home = _make_claude_source(tmp_path)
66    hermes_home = _make_hermes_source(tmp_path)
67    openclaw_home = tmp_path / "openclaw-home"
68    openclaw_home.mkdir()
69    (openclaw_home / "session.jsonl").write_text("{}", encoding="utf-8")
70
71    sources = {
72        source.client: source
73        for source in discover_usage_sources(
74            codex_home=codex_home,
75            claude_home=claude_home,
76            hermes_home=hermes_home,
77            openclaw_home=openclaw_home,
78            opencode_home=tmp_path / "missing-opencode",
79        )
80    }
81
82    assert sources["codex"].status == "found"
83    assert sources["codex"].session_count == 2
84    assert sources["codex"].importer == "agentacct usage import-local --client codex"
85    assert sources["claude-code"].status == "found"
86    assert sources["hermes"].status == "found"
87    assert sources["hermes"].cost_confidence == "client_reported"
88    assert sources["openclaw"].status == "found"
89    assert sources["openclaw"].usage_confidence == "unknown"
90    assert sources["openclaw"].importer == "agentacct usage import-local --client openclaw"
91    assert sources["opencode"].status == "missing"

▸已知边界 / 失败模式

  • Codex state DB 可读但 tokens_used 全为 0 时,tests/test_source_discovery.py:179 仍只标 usage_confidence unknown,不假装有用量真相。
  • Read tool 输出即使包含 event_id,也在 tests/test_log_evidenced_linking.py:222 被证明不能捐赠证据,避免 foreign id 污染。
  • 候选 SQLite 路径若落在 live agentacct 或 Codex state 下,tests/test_canonical_schema.py:203 期望直接拒绝创建。
  • 控制 API 的 JSON 投影在 tests/test_control_api.py:81 检查不得包含 store_dir、argv_template、process_group_id、nonce 等执行权柄。

▸捕获 receipt 不回显原始 payload

src/agent_chronicle/capture_runtime.py:11 到 src/agent_chronicle/capture_runtime.py:38 把 capture 结果转成 body-safe receipt,明确记录 src/agent_chronicle/capture_runtime.py:34 的 metadata_only 和 src/agent_chronicle/capture_runtime.py:35 的 raw_payload_returned=false;这让 hook 集成可以反馈状态,却不把原始提示、响应或工具内容带回页面。

src/agent_chronicle/capture_runtime.py · L11-38Python
11def capture_write_result_payload(
12    result: CaptureWriteResult,
13    *,
14    vendor: str,
15    host_event: str,
16    enabled: bool = True,
17) -> dict[str, Any]:
18    """Return a body-safe receipt; raw hook input is never echoed."""
19
20    return {
21        "schema_version": "agent-chronicle.capture-receipt.v1",
22        "enabled": enabled,
23        "vendor": vendor,
24        "host_event": host_event,
25        "ok": result.ok,
26        "fail_open": result.fail_open,
27        "attempted_count": result.attempted_count,
28        "stored_count": result.stored_count,
29        "ignored_reason": result.capture.ignored_reason or None,
30        "warnings": list(result.capture.warnings),
31        "write_errors": list(result.write_errors),
32        "observations": [observation.to_dict() for observation in result.capture.observations],
33        "privacy": {
34            "capture_mode": "metadata_only",
35            "raw_payload_returned": False,
36            "hook_exit_policy": "always_zero",
37        },
38    }

▸控制台投影只保留决策状态

src/agent_chronicle/control_web.py:50 到 src/agent_chronicle/control_web.py:178 生成 dashboard payload,但 src/agent_chronicle/control_web.py:172 到 src/agent_chronicle/control_web.py:177 明确区分 owned_execution 与 observed_only;这也是 tests/test_control_api.py:81 不允许路径、argv 和进程权柄泄露的原因。

src/agent_chronicle/control_web.py · L150-178Python
150    active_execution = {"pending", "launching", "running", "cancel_requested"}
151    summary = {
152        "task_count": len(tasks),
153        "attempt_count": len(attempts),
154        "active_attempt_count": sum(row["execution_state"] in active_execution for row in attempts),
155        "running_attempt_count": sum(row["execution_state"] in {"launching", "running", "cancel_requested"} for row in attempts),
156        "pending_approval_count": sum(row["state"] == "pending" for row in approvals),
157    }
158    return {
159        "schema_version": CONTROL_WEB_SCHEMA_VERSION,
160        "summary": summary,
161        "tasks": tasks,
162        "contracts": contracts,
163        "agents": agents,
164        "workspaces": workspaces,
165        "attempts": attempts,
166        "approvals": approvals,
167        "budget_policies": budgets,
168        "schedules": schedules,
169        "events": events,
170        "issues": [{"code": item.code, "line_number": item.line_number} for item in projection.issues],
171        "observed_tasks": observed,
172        "authority_boundary": {
173            "owned_execution": "controllable",
174            "external_codex": "observed_only",
175            "external_claude_code": "observed_only",
176            "external_orchestrators": "observed_only",
177        },
178    }
§ 05 · HIGHLIGHTS

为什么它值得进入工具箱

隐私边界明确
代码多次声明不保存 prompt、response、tool arguments、stdout/stderr;这不是 README 承诺,而是 runtime receipt、schema test 和 projection sanitizer 的共同约束。
证据等级诚实
UsageTruthRow 把 client-reported、provider-reported、unknown 等等级固定下来,避免 dashboard 把估算成本包装成真实账单。
迁移可审计
SQLite migration 使用冻结字面量和 checksum,迁移时逐条执行并检查 transaction,不靠动态 schema 生成。
控制面窄化
dashboard 只控制 agentacct 自己启动的 subprocess,外部 Codex/Claude 都是 observed_only,减少误操作面。
§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

最大风险是项目很新且表面较大:src/agent_chronicle/api.py:19 到 src/agent_chronicle/api.py:169 的 import 扇入显示它已经聚合大量模块,长期维护需要防止 API 层变成中心泥球。另一个风险是本地日志格式由上游客户端决定,src/agent_chronicle/log_evidence.py:189 也能看到它必须追踪 Codex wire shape 变化。

客户端 wire drift
Codex/Claude/OpenCode 日志格式一变,证据配对和 usage import 都需要测试先行更新。
成本不是账单
本地 token/cost 字段不能代表 provider invoice,产品必须持续展示 confidence。
控制面需要用户理解
owned execution 与 observed-only 的区别如果 UI 没讲清,用户可能误以为它能接管现有 agent 进程。
候选库/legacy 双轨复杂
canonical spike 与 legacy reader 并存期间,读写路径和 cutover 策略会增加维护成本。
诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。