homerailRail
§ · Nested detail page

审计证据、校验和与会话恢复 深度解析

HomeRail 不只保存模型最终文本:它分离记录 transcript、工具事件和会话快照,并在中断恢复时删除未完成工具调用,避免重复上下文污染。

§ 00 · IMPORT CHAIN

导入链与契约

PromptRunner 启动时先在 sources/worker/src/prompt-runner.ts:167、sources/worker/src/prompt-runner.ts:168、sources/worker/src/prompt-runner.ts:169、sources/worker/src/prompt-runner.ts:174、sources/worker/src/prompt-runner.ts:175、sources/worker/src/prompt-runner.ts:177 创建 DAG 工具与两个审计 writer;sources/worker/src/prompt-runner.ts:182、sources/worker/src/prompt-runner.ts:183、sources/worker/src/prompt-runner.ts:185、sources/worker/src/prompt-runner.ts:186、sources/worker/src/prompt-runner.ts:188、sources/worker/src/prompt-runner.ts:190 立即写入 prompt_start 和裁剪后的任务预览。审计失败被视为非致命,但 session transcript 仍有另一条 best-effort 写入路径,因此运行控制与证据采集不会互相锁死。

src/homerail_worker/src/audit/index.ts · L32-46TypeScript
32export interface AuditWriters {
33  transcript: TranscriptWriter;
34  toolEvents: ToolEventWriter;
35}
36
37export function createAuditWriters(
38  runId: string,
39  baseDir?: string,
40): AuditWriters {
41  assertSafeToolEventRunId(runId);
42  return {
43    transcript: createTranscriptWriter(runId, baseDir),
44    toolEvents: createToolEventWriter(runId, baseDir),
45  };
46}
§ 01 · CORE IMPL

核心实现,逐段拆

▸Transcript 追加 JSONL,关闭时生成 sidecar 校验和

Transcript writer 在 sources/worker/src/audit/transcript-writer.ts:19、sources/worker/src/audit/transcript-writer.ts:23、sources/worker/src/audit/transcript-writer.ts:24、sources/worker/src/audit/transcript-writer.ts:25、sources/worker/src/audit/transcript-writer.ts:26、sources/worker/src/audit/transcript-writer.ts:27 校验 runId、创建目录并以 append 模式打开文件;每条记录由 sources/worker/src/audit/transcript-writer.ts:29、sources/worker/src/audit/transcript-writer.ts:30、sources/worker/src/audit/transcript-writer.ts:31、sources/worker/src/audit/transcript-writer.ts:32、sources/worker/src/audit/transcript-writer.ts:39、sources/worker/src/audit/transcript-writer.ts:41 加上时间戳后写入,close 回调在 sources/worker/src/audit/transcript-writer.ts:42、sources/worker/src/audit/transcript-writer.ts:43、sources/worker/src/audit/transcript-writer.ts:45、sources/worker/src/audit/transcript-writer.ts:46 生成校验和 sidecar。这样长运行不必把全文留在内存,同时结束后可以检查文件是否被截断或篡改。

src/homerail_worker/src/audit/transcript-writer.ts · L19-48TypeScript
19export function createTranscriptWriter(
20  runId: string,
21  baseDir?: string,
22): TranscriptWriter {
23  const safeRunId = assertSafeRunId(runId);
24  const dir = baseDir ?? homerailPath("audit", "transcripts");
25  mkdirSync(dir, { recursive: true });
26  const filePath = join(dir, `${safeRunId}.jsonl`);
27  const stream: Writable = createWriteStream(filePath, { flags: "a" });
28
29  return {
30    write(entry: Record<string, unknown>) {
31      const line = JSON.stringify({ ...entry, ts: Date.now() }) + "\n";
32      stream.write(line);
33    },
34    flush() {
35      return new Promise<void>((resolve, reject) => {
36        stream.write("", (err) => (err ? reject(err) : resolve()));
37      });
38    },
39    close() {
40      return new Promise<void>((resolve, reject) => {
41        stream.end(() => {
42          writeTranscriptChecksum(filePath);
43          resolve();
44        });
45        stream.on("error", reject);
46      });
47    },
48  };

▸Session 快照去除密钥,中断在工具调用中则剔除半成品消息

会话快照的数据契约在 sources/worker/src/session/session-store.ts:24、sources/worker/src/session/session-store.ts:25、sources/worker/src/session/session-store.ts:26、sources/worker/src/session/session-store.ts:27、sources/worker/src/session/session-store.ts:29、sources/worker/src/session/session-store.ts:33 保存 run、node、消息、工具调用状态和去密配置;写盘前 sources/worker/src/session/session-store.ts:79、sources/worker/src/session/session-store.ts:80、sources/worker/src/session/session-store.ts:81、sources/worker/src/session/session-store.ts:82、sources/worker/src/session/session-store.ts:83、sources/worker/src/session/session-store.ts:87 只保留 provider、model 与 workspace,不持久化 API key。恢复时 sources/worker/src/session/resume.ts:16、sources/worker/src/session/resume.ts:17、sources/worker/src/session/resume.ts:20、sources/worker/src/session/resume.ts:25、sources/worker/src/session/resume.ts:29、sources/worker/src/session/resume.ts:30 判断是否中断于工具调用,并按 pendingToolCallId 同时删除未完成的 tool_use 与可能的局部 tool_result。

src/homerail_worker/src/session/resume.ts · L16-44TypeScript
16export function resumeSession(runId: string, baseDir?: string): ResumeResult | null {
17  const state = loadSession(runId, baseDir);
18  if (!state) return null;
19
20  const wasInterrupted = state.toolCallState.inFlight;
21
22  // If interrupted mid-tool, strip the in-flight tool_use from the
23  // conversation so the agent can re-execute cleanly.
24  let messages = state.messages;
25  if (wasInterrupted && state.toolCallState.pendingToolCallId) {
26    // Remove any tool_result that might have been partially written for
27    // the pending call, and keep everything up to but not including the
28    // pending assistant message.
29    const pendingId = state.toolCallState.pendingToolCallId;
30    messages = messages.filter(
31      (m) => m.tool_call_id !== pendingId && !m.tool_calls?.some((tc) => tc.id === pendingId),
32    );
33  }
34
35  return {
36    context: {
37      ...state.agentConfig,
38      apiKey: "",
39      baseUrl: "",
40    },
41    messages,
42    wasInterrupted,
43  };
44}
§ 02 · KEY FLOW

关键流程

prompt_start→transcript.jsonl+tool-events/run.jsonl→ closechecksum sidecar
session.json→ crash → sanitize pending call →resume context

工具事件单独按 runId 分片。sources/worker/src/audit/tool-event-writer.ts:19、sources/worker/src/audit/tool-event-writer.ts:20、sources/worker/src/audit/tool-event-writer.ts:23、sources/worker/src/audit/tool-event-writer.ts:24、sources/worker/src/audit/tool-event-writer.ts:35、sources/worker/src/audit/tool-event-writer.ts:36 先阻断路径穿越并计算文件位置,sources/worker/src/audit/tool-event-writer.ts:43、sources/worker/src/audit/tool-event-writer.ts:44、sources/worker/src/audit/tool-event-writer.ts:45、sources/worker/src/audit/tool-event-writer.ts:47、sources/worker/src/audit/tool-event-writer.ts:50、sources/worker/src/audit/tool-event-writer.ts:52 再把事件补上 run_id 和时间戳后追加。读取逻辑在 sources/worker/src/audit/tool-event-writer.ts:69、sources/worker/src/audit/tool-event-writer.ts:71、sources/worker/src/audit/tool-event-writer.ts:72、sources/worker/src/audit/tool-event-writer.ts:74、sources/worker/src/audit/tool-event-writer.ts:75、sources/worker/src/audit/tool-event-writer.ts:77 同时兼容新版分片文件和旧版全局文件,并按时间排序。

▸完整性检查必须成为上层策略,而不是假设

sources/worker/src/audit/index.ts:63、sources/worker/src/audit/index.ts:67、sources/worker/src/audit/index.ts:68、sources/worker/src/audit/index.ts:71、sources/worker/src/audit/index.ts:74、sources/worker/src/audit/index.ts:77 检查 transcript 是否存在并计算当前 hash,sources/worker/src/audit/index.ts:79、sources/worker/src/audit/index.ts:80、sources/worker/src/audit/index.ts:81、sources/worker/src/audit/index.ts:84、sources/worker/src/audit/index.ts:85、sources/worker/src/audit/index.ts:89 再验证 sidecar 和 tool-events。返回值是 complete 加 missing 列表,而 PromptRunner 的审计写入本身是 best-effort;高合规部署应在工作流末端显式调用这一检查,并把缺证据当成失败或人工复核。

§ 03 · EDGE CASES

边界 / 失败模式

  • runId 含斜杠、反斜杠或非白名单字符会被拒绝,防止审计路径逃逸。
  • 审计 writer 创建失败不会阻断 Agent;因此业务成功与证据完整必须分别判定。
  • 旧版 tool-events.jsonl 仍会被读取,并按 run_id 过滤后与新版分片合并。
  • 工具调用中断时删除未完成调用,可能导致外部副作用已发生但对话中没有结果;工具本身仍需幂等键。
Sub-page · 2026-07-10 Source · xiaotianfotos/homerail ↑ homerail 主页面