涉及源码 :
src/transcript.py、src/session_store.py、src/query_engine.py、src/main.py;与result/03.md、result/05.md中的会话/拒绝语义相连。
1. 「运行史」在运维眼里要解决什么
运维(含 SRE、平台、合规、客服排障)关心的不是「有一段聊天」,而是能否在 不重现当时模型 的前提下回答:
- 谁在什么时间、以什么身份、在什么环境 触发了会话?
- 每一轮 用户输入、助手输出、工具调用请求/结果、拒绝原因 是否可顺序重放?
- 用量与计费 是否与某次请求可对账?
- 失败/越权 能否按
session_id/trace_id跨服务关联? - 数据保留、脱敏、导出、删除 是否有稳定 schema 与生命周期?
下面先还原 claw-code 当前 的 Transcript 与 Session Store 设计,再对照上述问题给出 可运维 数据结构应具备的要素。
2. 当前实现:TranscriptStore(进程内)
python
# 6:23:src/transcript.py
@dataclass
class TranscriptStore:
entries: list[str] = field(default_factory=list)
flushed: bool = False
def append(self, entry: str) -> None:
self.entries.append(entry)
self.flushed = False
def compact(self, keep_last: int = 10) -> None:
if len(self.entries) > keep_last:
self.entries[:] = self.entries[-keep_last:]
def replay(self) -> tuple[str, ...]:
return tuple(self.entries)
def flush(self) -> None:
self.flushed = True
语义:
entries:仅追加字符串 ;在本仓库中与QueryEnginePort.mutable_messages在成功路径上 同步追加同一prompt(见下文submit_message)。flushed:布尔标记 ,append置假,flush置真;不表示「已 fsync 到对象存储」 ,只表示调用过flush_transcript()。compact:尾部截断 ,不是语义摘要;与mutable_messages共用compact_after_turns阈值。
学习点(移植期合理) :极小 API,便于单测与教学。
运维缺口 :无 角色 (user/assistant/tool)、无 时间戳 、无 id 、无 版本 ;无法区分「用户原话」与「系统注入」;截断后 不可恢复 早期轮次。
3. 当前实现:StoredSession 与磁盘 JSON
python
# 8:35:src/session_store.py
@dataclass(frozen=True)
class StoredSession:
session_id: str
messages: tuple[str, ...]
input_tokens: int
output_tokens: int
DEFAULT_SESSION_DIR = Path('.port_sessions')
def save_session(session: StoredSession, directory: Path | None = None) -> Path:
target_dir = directory or DEFAULT_SESSION_DIR
target_dir.mkdir(parents=True, exist_ok=True)
path = target_dir / f'{session.session_id}.json'
path.write_text(json.dumps(asdict(session), indent=2))
return path
def load_session(session_id: str, directory: Path | None = None) -> StoredSession:
target_dir = directory or DEFAULT_SESSION_DIR
data = json.loads((target_dir / f'{session_id}.json').read_text())
return StoredSession(
session_id=data['session_id'],
messages=tuple(data['messages']),
input_tokens=data['input_tokens'],
output_tokens=data['output_tokens'],
)
持久化触发链 :QueryEnginePort.persist_session() → flush_transcript() → save_session(...)。
python
140:150:src/query_engine.py
def persist_session(self) -> str:
self.flush_transcript()
path = save_session(
StoredSession(
session_id=self.session_id,
messages=tuple(self.mutable_messages),
input_tokens=self.total_usage.input_tokens,
output_tokens=self.total_usage.output_tokens,
)
)
return str(path)
hydrate :from_saved_session 把 messages 填回 mutable_messages 与 TranscriptStore.entries,并标记 flushed=True。
运维相关隐患:
DEFAULT_SESSION_DIR为相对路径.port_sessions:依赖 进程当前工作目录 ,不同服务启动方式会导致会话文件散落或「找不到」,生产应 显式传入绝对路径或配置 (save_session已支持directory,但persist_session/load_session未暴露给 CLI 以外的配置层)。- 整文件覆盖写 :无 WAL、无并发锁;多进程写同一
session_id会互相覆盖。 - 无 schema 版本字段:演进字段时难以做迁移。
- 敏感内容 :
messages若为真实用户 prompt,落盘即 PII/密钥风险,需加密、脱敏或存引用 ID 而非原文------当前无任何封装。
4. 与 QueryEnginePort 的状态关系:双缓冲与一致性
成功处理一轮时:
python
# 91:95:src/query_engine.py
self.mutable_messages.append(prompt)
self.transcript_store.append(prompt)
self.permission_denials.extend(denied_tools)
self.total_usage = projected_usage
self.compact_messages_if_needed()
python
129:132:src/query_engine.py
def compact_messages_if_needed(self) -> None:
if len(self.mutable_messages) > self.config.compact_after_turns:
self.mutable_messages[:] = self.mutable_messages[-self.config.compact_after_turns :]
self.transcript_store.compact(self.config.compact_after_turns)
现状 :mutable_messages 与 transcript_store.entries 内容同步截断 ;持久化只写 mutable_messages。设计上两者在内存中应 始终一致 (否则 replay_user_messages 与落盘会分叉------当前实现靠同一套 append/compact 维持)。
未进入运行史的数据(对运维很重要):
- 每轮
TurnResult.output(助手侧摘要/JSON); matched_commands/matched_tools/permission_denials明细;stop_reason;- 工具调用的请求体与返回(成功或失败)。
因此磁盘上的 JSON 不是完整对话录 ,而是 「用户轮次文本 + 伪 token 累计」 的瘦身快照,适合移植演示,不足以单独支撑事故复盘。
5. 流式事件与运行史:协议 vs 持久化
stream_submit_message 在内存中 yield 结构化事件(含 transcript_size),但 这些事件默认不落盘:
python
# 122:127:src/query_engine.py
yield {
'type': 'message_stop',
'usage': {'input_tokens': result.usage.input_tokens, 'output_tokens': result.usage.output_tokens},
'stop_reason': result.stop_reason,
'transcript_size': len(self.transcript_store.entries),
}
可运维做法 :若产品前端消费 SSE,运维侧应把 同一事件流 (或等价日志)追加写入 只增不改的 event log (文件/队列/OLAP),而不是仅依赖最终 StoredSession 快照。
6. 怎样才算「可运维」的数据结构------分层建议
6.1 事件日志(append-only,真相源)
每条记录建议至少包含:
| 字段 | 作用 |
|---|---|
event_id / 单调序号 |
有序、可去重 |
session_id |
会话聚合 |
trace_id |
跨服务 |
ts (RFC3339) |
时间线、与指标对齐 |
type |
user_message / assistant_message / tool_call / tool_result / permission_denial / error ... |
payload |
结构化 JSON(可引用 blob 存大体裁) |
schema_version |
迁移 |
claw-code 的 TurnResult 与流式 dict 是 向该形态过渡的中间层。
6.2 会话快照(derived,可丢可重建)
StoredSession 类角色适合作为 从 event log 物化的缓存 (用于快速 hydrate),而不是 唯一数据源 ;字段可含 last_event_id、checksum、updated_at。
6.3 角色与多说话者
智能体运行史至少 user / assistant / tool 三类;当前仅存 user 侧 prompt 字符串。可运维模型常用 统一 Message 或 Turn 块 ,内含 role 与 content 或 tool_calls[]。
6.4 工具与审计
每次 tool 调用应有 call_id ,与 PermissionDenial、计费、下游日志 同键 (参见 result/05.md)。拒绝、超时、部分成功都应是 独立事件,而非挤在一条字符串里。
6.5 用量与对账
当前 input_tokens/output_tokens 为 词数近似 (UsageSummary.add_turn),运维对账需 API 返回的真实 usage 或 自研 tokenizer ;并记录 model_id、pricing_version。
6.6 生命周期与合规
- Retention:按租户策略 TTL 或法务要求删除。
- PII:字段级加密、密钥轮转、访问审计。
- 导出:GDPR 等「可携带」需稳定 schema。
6.7 运维可操作性
- 可观测性 :
transcript_size、落盘路径、flush 失败率、JSON 写延迟指标。 - 配置:session 目录、是否同步 fsync、单文件大小上限、轮转策略。
- 调试 :
session_id一键拉全链 event(当前仅一条 json 文件)。
7. CLI 与测试提供的「最小闭环」
python
160:169:src/main.py
if args.command == 'flush-transcript':
engine = QueryEnginePort.from_workspace()
engine.submit_message(args.prompt)
path = engine.persist_session()
print(path)
print(f'flushed={engine.transcript_store.flushed}')
return 0
if args.command == 'load-session':
session = load_session(args.session_id)
print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
return 0
test_flush_transcript_cli_runs 断言 flushed=True;test_load_session_cli_runs 经 bootstrap_session 生成文件再 load-session。这是 可回归 的薄层,但 断言内容未覆盖 消息语义完整性或并发。
8. 小结
| 维度 | claw-code 现状 | 可运维目标 |
|---|---|---|
| Transcript | 字符串列表 + flushed + 截断 |
带角色/时间/id 的事件序列或 append-only log |
| Session 落盘 | 单 JSON,用户消息 + 伪用量 | 版本化快照或 log 物化;含助手/工具/拒绝 |
| 路径 | 相对 .port_sessions |
配置化绝对路径、隔离租户 |
| 并发 | 覆盖写 | 单写者或乐观锁 / 外部 store |
| 合规 | 无内建 | 保留、加密、脱敏、导出 |
一句话 :当前 TranscriptStore + StoredSession 把「会话能续上、能演示 persist」跑通了;要 可运维 ,需把运行史从 「几条字符串」 升级为 「可关联、可重放、可审计、可生命周期管理的事件模型」 ,并让磁盘格式成为该模型的 投影 而非全部真相。