Files
openclaw/src/gateway/server-restart-sentinel.ts

108 lines
3.9 KiB
TypeScript

import { resolveSessionAgentId } from "../agents/agent-scope.js";
import { resolveAnnounceTargetFromKey } from "../agents/tools/sessions-send-helpers.js";
import { normalizeChannelId } from "../channels/plugins/index.js";
import type { CliDeps } from "../cli/deps.js";
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
import { parseSessionThreadInfo } from "../config/sessions/delivery-info.js";
import { deliverOutboundPayloads } from "../infra/outbound/deliver.js";
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
import {
consumeRestartSentinel,
formatRestartSentinelMessage,
summarizeRestartSentinel,
} from "../infra/restart-sentinel.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { deliveryContextFromSession, mergeDeliveryContext } from "../utils/delivery-context.js";
import { loadSessionEntry } from "./session-utils.js";
export async function scheduleRestartSentinelWake(_params: { deps: CliDeps }) {
const sentinel = await consumeRestartSentinel();
if (!sentinel) {
return;
}
const payload = sentinel.payload;
const sessionKey = payload.sessionKey?.trim();
const message = formatRestartSentinelMessage(payload);
const summary = summarizeRestartSentinel(payload);
if (!sessionKey) {
const mainSessionKey = resolveMainSessionKeyFromConfig();
enqueueSystemEvent(message, { sessionKey: mainSessionKey });
return;
}
const { baseSessionKey, threadId: sessionThreadId } = parseSessionThreadInfo(sessionKey);
const { cfg, entry } = loadSessionEntry(sessionKey);
const parsedTarget = resolveAnnounceTargetFromKey(baseSessionKey ?? sessionKey);
// Prefer delivery context from sentinel (captured at restart) over session store
// Handles race condition where store wasn't flushed before restart
const sentinelContext = payload.deliveryContext;
let sessionDeliveryContext = deliveryContextFromSession(entry);
if (!sessionDeliveryContext && baseSessionKey && baseSessionKey !== sessionKey) {
const { entry: baseEntry } = loadSessionEntry(baseSessionKey);
sessionDeliveryContext = deliveryContextFromSession(baseEntry);
}
const origin = mergeDeliveryContext(
sentinelContext,
mergeDeliveryContext(sessionDeliveryContext, parsedTarget ?? undefined),
);
const channelRaw = origin?.channel;
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
const to = origin?.to;
if (!channel || !to) {
enqueueSystemEvent(message, { sessionKey });
return;
}
const resolved = resolveOutboundTarget({
channel,
to,
cfg,
accountId: origin?.accountId,
mode: "implicit",
});
if (!resolved.ok) {
enqueueSystemEvent(message, { sessionKey });
return;
}
const threadId =
payload.threadId ??
parsedTarget?.threadId ?? // From resolveAnnounceTargetFromKey (extracts :topic:N)
sessionThreadId ??
(origin?.threadId != null ? String(origin.threadId) : undefined);
// Slack uses replyToId (thread_ts) for threading, not threadId.
// The reply path does this mapping but deliverOutboundPayloads does not,
// so we must convert here to ensure post-restart notifications land in
// the originating Slack thread. See #17716.
const isSlack = channel === "slack";
const replyToId = isSlack && threadId != null && threadId !== "" ? String(threadId) : undefined;
const resolvedThreadId = isSlack ? undefined : threadId;
try {
await deliverOutboundPayloads({
cfg,
channel,
to: resolved.to,
accountId: origin?.accountId,
replyToId,
threadId: resolvedThreadId,
payloads: [{ text: message }],
agentId: resolveSessionAgentId({ sessionKey, config: cfg }),
bestEffort: true,
sessionKey,
});
} catch (err) {
enqueueSystemEvent(`${summary}\n${String(err)}`, { sessionKey });
}
}
export function shouldWakeFromRestartSentinel() {
return !process.env.VITEST && process.env.NODE_ENV !== "test";
}