
























11// Raft gateway lifecycle owns the loopback-only wake endpoint and bridge child process.
22import { spawn, type ChildProcess } from "node:child_process";
3-import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3+import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
44import type { EventEmitter } from "node:events";
55import {
66createServer,
@@ -23,9 +23,11 @@ import { RAFT_CHANNEL_ID, type ResolvedRaftAccount } from "./accounts.js";
2323import { dispatchRaftWake } from "./inbound.js";
24242525const BRIDGE_HOST = "127.0.0.1";
26+const ACTIVITY_DRAIN_PATH = "/activity/drain";
2627const HEALTH_PATH = "/health";
2728const WAKE_PATH = "/wake";
2829const WAKE_TOKEN_HEADER = "x-raft-bridge-token";
30+const RAFT_ACTIVITY_DRAIN_SCHEMA = "raft-activity-drain.v1";
2931const MAX_WAKE_BODY_BYTES = 16 * 1024;
3032const WAKE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1000;
3133const WAKE_DEDUPE_MEMORY_MAX_SIZE = 1_000;
@@ -245,8 +247,9 @@ export async function startRaftGatewayAccount(
245247onDiskError: (error) => {
246248ctx.log?.warn?.(`Raft wake dedupe storage failed: ${String(error)}`);
247249},
248- });
250+});
249251const token = (deps.createToken ?? createToken)();
252+const runtimeSession = randomUUID();
250253const sockets = new Set<Socket>();
251254let stopped = false;
252255let bridgeExited: Error | undefined;
@@ -256,6 +259,24 @@ export async function startRaftGatewayAccount(
256259sendJson(response, 200, { ok: true });
257260return;
258261}
262+if (
263+request.method === "GET" &&
264+new URL(request.url ?? "/", `http://${BRIDGE_HOST}`).pathname === ACTIVITY_DRAIN_PATH
265+) {
266+if (!hasMatchingToken(request, token)) {
267+sendJson(response, 401, { error: "unauthorized" });
268+return;
269+}
270+// Raft drains runtime activity after each wake pass. OpenClaw has no
271+// portable Raft activity events to export, but must acknowledge an
272+// empty batch so the bridge's current protocol remains healthy.
273+sendJson(response, 200, {
274+schema: RAFT_ACTIVITY_DRAIN_SCHEMA,
275+events: [],
276+dropped: 0,
277+});
278+return;
279+}
259280if (request.method !== "POST" || request.url !== WAKE_PATH) {
260281sendJson(response, 404, { error: "not found" });
261282return;
@@ -304,7 +325,12 @@ export async function startRaftGatewayAccount(
304325await wakeDedupe.commit(dedupeKey, { namespace: ctx.accountId });
305326return true;
306327});
307-sendJson(response, 202, { accepted: true, ...(dispatched ? {} : { duplicate: true }) });
328+sendJson(response, 202, {
329+ok: true,
330+accepted: true,
331+ runtimeSession,
332+ ...(dispatched ? {} : { duplicate: true }),
333+});
308334})().catch((error: unknown) => {
309335const statusCode = error instanceof WakeRequestError ? error.statusCode : 500;
310336const message = error instanceof WakeRequestError ? error.message : "Internal server error.";
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。