openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
707 lines (706 loc) • 27.4 kB
JavaScript
import { C as resolveExpiresAtMsFromDurationMs, O as resolveNonNegativeIntegerOption, S as resolveDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { v as resolveSessionAgentId } from "./agent-scope-MrLta7Pq.js";
import { h as stringifyRouteThreadId } from "./channel-route-D7X5briz.js";
import { i as mergeDeliveryContext, n as deliveryContextFromSession } from "./delivery-context.shared-CpAdEETO.js";
import { t as INTERNAL_MESSAGE_CHANNEL } from "./message-channel-constants-CgI32lqD.js";
import { A as parseSessionThreadInfo } from "./store-load-C4uq6Lrq.js";
import { a as normalizeChannelId, t as getChannelPlugin } from "./registry-MkxNn1Ue.js";
import "./plugins-t2ejWcVy.js";
import "./message-channel-BiOeMu0l.js";
import { u as resolveMainSessionKeyFromConfig } from "./sessions-D6zZvoxX.js";
import { o as requestHeartbeat } from "./heartbeat-wake-YhOjrxkC.js";
import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js";
import { u as loadSessionEntry } from "./session-utils-CHNCuY8a.js";
import { c as resolveRestartSentinelPath, i as formatRestartSentinelMessage, l as summarizeRestartSentinel, n as finalizeUpdateRestartSentinelRunningVersion, o as readRestartSentinel, s as removeRestartSentinelFile } from "./restart-sentinel-BFAG1Tav.js";
import { a as generateSecureUuid } from "./secure-random-Ds4AFLgz.js";
import { t as buildOutboundSessionContext } from "./session-context-Bz96-75Y.js";
import { _ as loadDeliveryQueueEntry, b as upsertDeliveryQueueEntry, c as enqueueDelivery, g as loadDeliveryQueueEntries, h as deleteDeliveryQueueEntry, l as failDelivery, s as ackDelivery, v as moveDeliveryQueueEntryToFailed, y as updateDeliveryQueueEntry } from "./delivery-queue-Ds6M9589.js";
import { t as sendDurableMessageBatch } from "./send-DWoTcOuO.js";
import "./runtime-BiQQNgz2.js";
import { i as resolveOutboundTarget } from "./targets-C4BNalx6.js";
import { t as finalizeInboundContext } from "./inbound-context-CZx-NgvC.js";
import { t as dispatchReplyWithBufferedBlockDispatcher } from "./provider-dispatcher-CkTw0sGQ.js";
import { n as dispatchAssembledChannelTurn } from "./kernel-BdraY_qZ.js";
import { t as recordInboundSession } from "./session-Do0yFgU8.js";
import "./get-reply-run-queue-DFpyekY2.js";
import { t as runStartupTasks } from "./startup-tasks-BrQYuflC.js";
import { a as isPendingControlPlaneUpdateRestartSentinel } from "./update-control-plane-sentinel-CGsG9zuq.js";
import { createHash } from "node:crypto";
//#region src/infra/session-delivery-queue-storage.ts
const QUEUE_NAME = "session";
function buildEntryId(idempotencyKey) {
if (!idempotencyKey) return generateSecureUuid();
return createHash("sha256").update(idempotencyKey).digest("hex");
}
function queuedSessionDeliveryMetadata(entry) {
const route = entry.kind === "agentTurn" ? entry.route : void 0;
return {
entryKind: entry.kind,
sessionKey: entry.sessionKey,
channel: route?.channel ?? entry.deliveryContext?.channel,
target: route?.to ?? entry.deliveryContext?.to,
accountId: route?.accountId ?? entry.deliveryContext?.accountId
};
}
/** Enqueue a session delivery and return its durable id. */
async function enqueueSessionDelivery(params, stateDir) {
const id = buildEntryId(params.idempotencyKey);
if (params.idempotencyKey && loadDeliveryQueueEntry(QUEUE_NAME, id, stateDir)) return id;
const entry = {
...params,
id,
enqueuedAt: Date.now(),
retryCount: 0
};
upsertDeliveryQueueEntry({
queueName: QUEUE_NAME,
entry,
metadata: queuedSessionDeliveryMetadata(entry),
stateDir
});
return id;
}
/** Acknowledge a successfully delivered session entry. */
async function ackSessionDelivery(id, stateDir) {
deleteDeliveryQueueEntry(QUEUE_NAME, id, stateDir);
}
/** Record a failed delivery attempt and increment retry metadata. */
async function failSessionDelivery(id, error, stateDir) {
updateDeliveryQueueEntry(QUEUE_NAME, id, stateDir, (entry) => {
const queued = entry;
return {
...queued,
retryCount: queued.retryCount + 1,
lastAttemptAt: Date.now(),
lastError: error
};
});
}
/** Load one pending session delivery by durable id. */
async function loadPendingSessionDelivery(id, stateDir) {
return loadDeliveryQueueEntry(QUEUE_NAME, id, stateDir);
}
/** Load all pending session deliveries in retry order. */
async function loadPendingSessionDeliveries(stateDir) {
return loadDeliveryQueueEntries(QUEUE_NAME, stateDir);
}
/** Move an exhausted session delivery out of the pending queue. */
async function moveSessionDeliveryToFailed(id, stateDir) {
moveDeliveryQueueEntryToFailed(QUEUE_NAME, id, stateDir);
}
//#endregion
//#region src/infra/session-delivery-queue-recovery.ts
const MAX_SESSION_DELIVERY_RETRIES = 5;
const BACKOFF_MS = [
5e3,
25e3,
12e4,
6e5
];
const drainInProgress = /* @__PURE__ */ new Map();
const entriesInProgress = /* @__PURE__ */ new Set();
function getErrnoCode(err) {
return err && typeof err === "object" && "code" in err ? String(err.code) : null;
}
function createEmptyRecoverySummary() {
return {
recovered: 0,
failed: 0,
skippedMaxRetries: 0,
deferredBackoff: 0
};
}
function claimRecoveryEntry(entryId) {
if (entriesInProgress.has(entryId)) return false;
entriesInProgress.add(entryId);
return true;
}
function releaseRecoveryEntry(entryId) {
entriesInProgress.delete(entryId);
}
function computeSessionDeliveryBackoffMs(retryCount) {
if (retryCount <= 0) return 0;
return BACKOFF_MS[Math.min(retryCount - 1, BACKOFF_MS.length - 1)] ?? BACKOFF_MS.at(-1) ?? 0;
}
function resolveSessionDeliveryMaxRetries(entry) {
return entry.maxRetries ?? MAX_SESSION_DELIVERY_RETRIES;
}
function resolveSessionDeliveryRecoveryDeadlineMs(maxRecoveryMs) {
const durationMs = resolveNonNegativeIntegerOption(maxRecoveryMs, 6e4);
if (durationMs <= 0) return resolveDateTimestampMs(Date.now());
return resolveExpiresAtMsFromDurationMs(durationMs) ?? resolveDateTimestampMs(Date.now());
}
function isSessionDeliveryEligibleForRetry(entry, now) {
const backoff = computeSessionDeliveryBackoffMs(entry.retryCount);
if (backoff <= 0) return { eligible: true };
if (entry.retryCount === 0 && entry.lastAttemptAt === void 0) return { eligible: true };
const nextEligibleAt = (typeof entry.lastAttemptAt === "number" && entry.lastAttemptAt > 0 ? entry.lastAttemptAt : entry.enqueuedAt) + backoff;
if (now >= nextEligibleAt) return { eligible: true };
return {
eligible: false,
remainingBackoffMs: nextEligibleAt - now
};
}
async function drainQueuedEntry(opts) {
const { entry } = opts;
try {
await opts.deliver(entry);
await ackSessionDelivery(entry.id, opts.stateDir);
opts.onRecovered?.(entry);
return "recovered";
} catch (err) {
const errMsg = formatErrorMessage(err);
opts.onFailed?.(entry, errMsg);
try {
await failSessionDelivery(entry.id, errMsg, opts.stateDir);
return "failed";
} catch (failErr) {
if (getErrnoCode(failErr) === "ENOENT") return "already-gone";
return "failed";
}
}
}
/** Drain matching queued session deliveries with retry/backoff protection. */
async function drainPendingSessionDeliveries(opts) {
if (drainInProgress.get(opts.drainKey)) {
opts.log.info(`${opts.logLabel}: already in progress for ${opts.drainKey}, skipping`);
return;
}
drainInProgress.set(opts.drainKey, true);
try {
const matchingEntries = (await loadPendingSessionDeliveries(opts.stateDir)).filter((entry) => opts.selectEntry(entry, Date.now()).match).toSorted((a, b) => a.enqueuedAt - b.enqueuedAt);
for (const entry of matchingEntries) {
if (!claimRecoveryEntry(entry.id)) {
opts.log.info(`${opts.logLabel}: entry ${entry.id} is already being recovered`);
continue;
}
try {
const currentEntry = await loadPendingSessionDelivery(entry.id, opts.stateDir);
if (!currentEntry) continue;
const currentDecision = opts.selectEntry(currentEntry, Date.now());
if (!currentDecision.match) continue;
if (currentEntry.retryCount >= resolveSessionDeliveryMaxRetries(currentEntry)) {
try {
await moveSessionDeliveryToFailed(currentEntry.id, opts.stateDir);
} catch (err) {
if (getErrnoCode(err) !== "ENOENT") throw err;
}
opts.log.warn(`${opts.logLabel}: entry ${currentEntry.id} exceeded max retries and was moved to failed/`);
continue;
}
if (!currentDecision.bypassBackoff) {
const retryEligibility = isSessionDeliveryEligibleForRetry(currentEntry, Date.now());
if (!retryEligibility.eligible) {
opts.log.info(`${opts.logLabel}: entry ${currentEntry.id} not ready for retry yet — backoff ${retryEligibility.remainingBackoffMs}ms remaining`);
continue;
}
}
await drainQueuedEntry({
entry: currentEntry,
deliver: opts.deliver,
stateDir: opts.stateDir,
onFailed: (failedEntry, errMsg) => {
opts.log.warn(`${opts.logLabel}: retry failed for entry ${failedEntry.id}: ${errMsg}`);
}
});
} finally {
releaseRecoveryEntry(entry.id);
}
}
} finally {
drainInProgress.delete(opts.drainKey);
}
}
/** Replay pending session deliveries until the recovery budget is exhausted. */
async function recoverPendingSessionDeliveries(opts) {
const pending = (await loadPendingSessionDeliveries(opts.stateDir)).filter((entry) => opts.maxEnqueuedAt == null || entry.enqueuedAt <= opts.maxEnqueuedAt);
if (pending.length === 0) return createEmptyRecoverySummary();
pending.sort((a, b) => a.enqueuedAt - b.enqueuedAt);
const summary = createEmptyRecoverySummary();
const deadline = resolveSessionDeliveryRecoveryDeadlineMs(opts.maxRecoveryMs);
for (const entry of pending) {
if (Date.now() >= deadline) {
opts.log.warn("Session delivery recovery time budget exceeded — remaining entries deferred");
break;
}
if (!claimRecoveryEntry(entry.id)) continue;
try {
const currentEntry = await loadPendingSessionDelivery(entry.id, opts.stateDir);
if (!currentEntry) continue;
if (opts.maxEnqueuedAt != null && currentEntry.enqueuedAt > opts.maxEnqueuedAt) continue;
if (currentEntry.retryCount >= resolveSessionDeliveryMaxRetries(currentEntry)) {
summary.skippedMaxRetries += 1;
try {
await moveSessionDeliveryToFailed(currentEntry.id, opts.stateDir);
} catch (err) {
if (getErrnoCode(err) !== "ENOENT") throw err;
}
continue;
}
if (!isSessionDeliveryEligibleForRetry(currentEntry, Date.now()).eligible) {
summary.deferredBackoff += 1;
continue;
}
if (await drainQueuedEntry({
entry: currentEntry,
deliver: opts.deliver,
stateDir: opts.stateDir,
onRecovered: () => {
summary.recovered += 1;
},
onFailed: (_failedEntry, errMsg) => {
summary.failed += 1;
opts.log.warn(`Session delivery retry failed: ${errMsg}`);
}
}) === "recovered") opts.log.info(`Recovered session delivery ${currentEntry.id}`);
} finally {
releaseRecoveryEntry(entry.id);
}
}
return summary;
}
//#endregion
//#region src/gateway/server-restart-sentinel.ts
const log = createSubsystemLogger("gateway/restart-sentinel");
const OUTBOUND_RETRY_DELAY_MS = 1e3;
const OUTBOUND_MAX_ATTEMPTS = 45;
const RESTART_CONTINUATION_BUSY_RETRY_DELAY_MS = process.env.VITEST ? 1 : 6e3;
const RESTART_CONTINUATION_BUSY_MAX_ATTEMPTS = 20;
const CONTROL_PLANE_UPDATE_PENDING_RETRY_DELAY_MS = process.env.VITEST ? 1 : 2e3;
const CONTROL_PLANE_UPDATE_PENDING_MAX_ATTEMPTS = 900;
const RESTART_CONTINUATION_BUSY_RETRY_ERROR = "restart continuation deferred because previous run is still shutting down";
let latestUpdateRestartSentinel = null;
function cloneRestartSentinelPayload(payload) {
if (!payload) return null;
return structuredClone(payload);
}
function hasRoutableDeliveryContext(context) {
return Boolean(context?.channel && context?.to);
}
function enqueueRestartSentinelWake(message, sessionKey, deliveryContext) {
enqueueSystemEvent(message, {
sessionKey,
...deliveryContext ? { deliveryContext } : {}
});
requestHeartbeat({
source: "restart-sentinel",
intent: "immediate",
reason: "wake",
sessionKey
});
}
async function waitForOutboundRetry(delayMs) {
await new Promise((resolve) => {
setTimeout(resolve, delayMs).unref?.();
});
}
async function deliverRestartSentinelNotice(params) {
const payloads = [{ text: params.message }];
const queueId = await enqueueDelivery({
channel: params.channel,
to: params.to,
accountId: params.accountId,
replyToId: params.replyToId,
threadId: params.threadId,
payloads,
bestEffort: false
}).catch(() => null);
for (let attempt = 1; attempt <= OUTBOUND_MAX_ATTEMPTS; attempt += 1) try {
const send = await sendDurableMessageBatch({
cfg: params.cfg,
channel: params.channel,
to: params.to,
accountId: params.accountId,
replyToId: params.replyToId,
threadId: params.threadId,
payloads,
session: params.session,
deps: params.deps,
bestEffort: false,
skipQueue: true
});
if (send.status === "failed" || send.status === "partial_failed") throw send.error;
if ((send.status === "sent" ? send.results : []).length > 0) {
if (queueId) await ackDelivery(queueId).catch(() => {});
return;
}
throw new Error("outbound delivery returned no results");
} catch (err) {
const retrying = attempt < OUTBOUND_MAX_ATTEMPTS;
const suffix = retrying ? `; retrying in ${OUTBOUND_RETRY_DELAY_MS}ms` : "";
log.warn(`${params.summary}: outbound delivery failed${suffix}: ${String(err)}`, {
channel: params.channel,
to: params.to,
sessionKey: params.sessionKey,
attempt,
maxAttempts: OUTBOUND_MAX_ATTEMPTS
});
if (!retrying) {
if (queueId) await failDelivery(queueId, formatErrorMessage(err)).catch(() => void 0);
return;
}
await waitForOutboundRetry(OUTBOUND_RETRY_DELAY_MS);
}
}
function buildRestartContinuationMessageId(params) {
return `restart-sentinel:${params.sessionKey}:${params.kind}:${params.ts}`;
}
function resolveRestartContinuationRoute(params) {
if (!params.channel || !params.to) return;
return {
channel: params.channel,
to: params.to,
...params.accountId ? { accountId: params.accountId } : {},
...params.replyToId ? { replyToId: params.replyToId } : {},
...params.threadId ? { threadId: params.threadId } : {},
chatType: params.chatType
};
}
function isRestartContinuationBusyPayload(payload) {
return typeof payload.text === "string" && payload.text.trim() === "⚠️ Previous run is still shutting down. Please try again in a moment.";
}
function isRestartContinuationBusyRetry(entry) {
return entry?.lastError === RESTART_CONTINUATION_BUSY_RETRY_ERROR;
}
function resolveQueuedRestartContinuationMessageId(entry) {
if (isRestartContinuationBusyRetry(entry) && entry.retryCount > 0) return `${entry.messageId}:retry:${entry.retryCount}`;
return entry.messageId;
}
function resolveQueuedSessionDeliveryContext(entry) {
if (entry.kind === "agentTurn" && entry.route) return {
channel: entry.route.channel,
to: entry.route.to,
...entry.route.accountId ? { accountId: entry.route.accountId } : {},
...entry.route.threadId ? { threadId: entry.route.threadId } : {}
};
return entry.deliveryContext;
}
async function deliverQueuedSessionDelivery(params) {
const { cfg, entry, storePath, canonicalKey } = loadSessionEntry(params.entry.sessionKey);
const queuedDeliveryContext = resolveQueuedSessionDeliveryContext(params.entry);
if (params.entry.kind === "systemEvent") {
enqueueRestartSentinelWake(params.entry.text, canonicalKey, queuedDeliveryContext);
return;
}
if (params.entry.expectedSessionId && (!entry?.sessionId || entry.sessionId !== params.entry.expectedSessionId)) {
log.warn("restart continuation skipped: session changed", {
sessionKey: canonicalKey,
queueId: params.entry.id,
expectedSessionId: params.entry.expectedSessionId,
actualSessionId: entry?.sessionId ?? null
});
enqueueRestartSentinelWake(params.entry.message, canonicalKey, queuedDeliveryContext);
return;
}
if (!params.entry.route) {
enqueueRestartSentinelWake(params.entry.message, canonicalKey, queuedDeliveryContext);
return;
}
const route = params.entry.route;
const messageId = resolveQueuedRestartContinuationMessageId(params.entry);
const userMessage = params.entry.message.trim();
const agentId = resolveSessionAgentId({
sessionKey: canonicalKey,
config: cfg
});
let dispatchError;
const ctxPayload = finalizeInboundContext({
Body: userMessage,
BodyForAgent: userMessage,
BodyForCommands: "",
RawBody: userMessage,
CommandBody: "",
SessionKey: canonicalKey,
AccountId: route.accountId,
MessageSid: messageId,
Timestamp: Date.now(),
InputProvenance: {
kind: "internal_system",
sourceChannel: route.channel,
sourceTool: "restart-sentinel"
},
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
ChatType: route.chatType,
CommandAuthorized: true,
GatewayClientScopes: ["operator.admin"],
ReplyToId: route.replyToId,
OriginatingChannel: route.channel,
OriginatingTo: route.to,
ExplicitDeliverRoute: false,
MessageThreadId: route.threadId
}, {
forceBodyForCommands: true,
forceChatType: true
});
await dispatchAssembledChannelTurn({
cfg,
channel: route.channel,
accountId: route.accountId,
agentId,
routeSessionKey: canonicalKey,
storePath,
ctxPayload,
recordInboundSession,
dispatchReplyWithBufferedBlockDispatcher,
replyOptions: { sourceReplyDeliveryMode: "message_tool_only" },
delivery: {
preparePayload: (payload) => {
if (isRestartContinuationBusyPayload(payload)) throw new Error(RESTART_CONTINUATION_BUSY_RETRY_ERROR);
return payload;
},
durable: false,
deliver: async () => ({ visibleReplySent: false }),
onError: (err, info) => {
dispatchError ??= err;
log.warn(`restart continuation dispatch failed during ${info.kind}: ${String(err)}`, { sessionKey: canonicalKey });
}
},
record: { onRecordError: (err) => {
log.warn(`restart continuation failed to record inbound session metadata: ${String(err)}`, { sessionKey: canonicalKey });
} }
});
if (dispatchError) throw toLintErrorObject(dispatchError, "Non-Error thrown");
}
function buildQueuedRestartContinuation(params) {
const idempotencyKey = buildRestartContinuationMessageId({
sessionKey: params.sessionKey,
kind: params.continuation.kind,
ts: params.ts
});
if (params.continuation.kind === "systemEvent") return {
kind: "systemEvent",
sessionKey: params.sessionKey,
text: params.continuation.text,
...params.deliveryContext ? { deliveryContext: params.deliveryContext } : {},
idempotencyKey,
maxRetries: RESTART_CONTINUATION_BUSY_MAX_ATTEMPTS
};
return {
kind: "agentTurn",
sessionKey: params.sessionKey,
message: params.continuation.message,
messageId: idempotencyKey,
...params.expectedSessionId ? { expectedSessionId: params.expectedSessionId } : {},
maxRetries: RESTART_CONTINUATION_BUSY_MAX_ATTEMPTS,
...params.route ? { route: params.route } : {},
...params.deliveryContext ? { deliveryContext: params.deliveryContext } : {},
idempotencyKey
};
}
async function drainRestartContinuationQueue(params) {
for (let attempt = 1; attempt <= RESTART_CONTINUATION_BUSY_MAX_ATTEMPTS; attempt += 1) {
await drainPendingSessionDeliveries({
drainKey: `restart-continuation:${params.entryId}`,
logLabel: "restart continuation",
log: params.log,
deliver: (entry) => deliverQueuedSessionDelivery({
deps: params.deps,
entry
}),
selectEntry: (entry) => ({
match: entry.id === params.entryId,
bypassBackoff: true
})
});
if (!isRestartContinuationBusyRetry(await loadPendingSessionDelivery(params.entryId))) return;
if (attempt >= RESTART_CONTINUATION_BUSY_MAX_ATTEMPTS) return;
params.log.info(`restart continuation: entry ${params.entryId} still waiting for the previous run to clear; retrying in ${RESTART_CONTINUATION_BUSY_RETRY_DELAY_MS}ms`);
await waitForOutboundRetry(RESTART_CONTINUATION_BUSY_RETRY_DELAY_MS);
}
}
async function recoverPendingRestartContinuationDeliveries(params) {
await recoverPendingSessionDeliveries({
deliver: (entry) => deliverQueuedSessionDelivery({
deps: params.deps,
entry
}),
log: params.log ?? log,
maxEnqueuedAt: params.maxEnqueuedAt
});
}
async function loadRestartSentinelStartupTask(params) {
const sentinel = await readRestartSentinel();
if (!sentinel) return null;
const sentinelPath = resolveRestartSentinelPath();
const payload = sentinel.payload;
if (payload.kind === "update") recordLatestUpdateRestartSentinel(payload);
const sessionKey = payload.sessionKey?.trim();
const message = formatRestartSentinelMessage(payload);
const summary = summarizeRestartSentinel(payload);
const wakeDeliveryContext = mergeDeliveryContext(payload.threadId != null ? {
...payload.deliveryContext,
threadId: payload.threadId
} : payload.deliveryContext, void 0);
const run = async () => {
if (isPendingControlPlaneUpdateRestartSentinel(payload)) {
const attempt = params.attempt ?? 0;
if (attempt < CONTROL_PLANE_UPDATE_PENDING_MAX_ATTEMPTS) {
setTimeout(() => {
scheduleRestartSentinelWakeAttempt({
deps: params.deps,
attempt: attempt + 1
}).catch((err) => {
log.warn(`restart sentinel pending update retry failed: ${formatErrorMessage(err)}`);
});
}, CONTROL_PLANE_UPDATE_PENDING_RETRY_DELAY_MS).unref?.();
return {
status: "skipped",
reason: "update-restart-pending"
};
}
log.warn(`${summary}: update restart sentinel remained pending after retry window`, {
sessionKey,
reason: payload.stats?.reason ?? null
});
}
if (!sessionKey) {
const mainSessionKey = resolveMainSessionKeyFromConfig();
enqueueSystemEvent(message, { sessionKey: mainSessionKey });
if (payload.continuation) log.warn(`${summary}: continuation skipped: restart sentinel sessionKey unavailable`, {
sessionKey: mainSessionKey,
continuationKind: payload.continuation.kind
});
await removeRestartSentinelFile(sentinelPath);
return { status: "ran" };
}
const { baseSessionKey, threadId: sessionThreadId } = parseSessionThreadInfo(sessionKey);
const { cfg, entry, canonicalKey } = loadSessionEntry(sessionKey);
const sentinelContext = payload.deliveryContext;
let sessionDeliveryContext = deliveryContextFromSession(entry);
let chatType = entry?.origin?.chatType ?? "direct";
if (!hasRoutableDeliveryContext(sessionDeliveryContext) && baseSessionKey && baseSessionKey !== sessionKey) {
const { entry: baseEntry } = loadSessionEntry(baseSessionKey);
chatType = entry?.origin?.chatType ?? baseEntry?.origin?.chatType ?? "direct";
sessionDeliveryContext = mergeDeliveryContext(sessionDeliveryContext, deliveryContextFromSession(baseEntry));
}
const origin = mergeDeliveryContext(sentinelContext, sessionDeliveryContext);
const channelRaw = origin?.channel;
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
const to = origin?.to;
const threadId = payload.threadId ?? sessionThreadId ?? (origin?.threadId != null ? stringifyRouteThreadId(origin.threadId) : void 0);
let resolvedTo;
let replyToId;
let resolvedThreadId = threadId;
let continuationQueueId;
let continuationRoute;
if (channel && to) {
const resolved = resolveOutboundTarget({
channel,
to,
cfg,
accountId: origin?.accountId,
mode: "implicit"
});
if (resolved.ok) {
resolvedTo = resolved.to;
const replyTransport = getChannelPlugin(channel)?.threading?.resolveReplyTransport?.({
cfg,
accountId: origin?.accountId,
threadId
}) ?? null;
replyToId = replyTransport?.replyToId ?? void 0;
resolvedThreadId = replyTransport && Object.hasOwn(replyTransport, "threadId") ? replyTransport.threadId != null ? stringifyRouteThreadId(replyTransport.threadId) : void 0 : threadId;
}
}
if (payload.continuation) {
continuationRoute = resolveRestartContinuationRoute({
channel: channel ?? void 0,
to: resolvedTo,
accountId: origin?.accountId,
replyToId,
threadId: resolvedThreadId,
chatType
});
continuationQueueId = await enqueueSessionDelivery(buildQueuedRestartContinuation({
sessionKey: canonicalKey,
continuation: payload.continuation,
ts: payload.ts,
route: continuationRoute,
expectedSessionId: entry?.sessionId,
deliveryContext: resolvedTo && channel ? {
channel,
to: resolvedTo,
...origin?.accountId ? { accountId: origin.accountId } : {},
...resolvedThreadId ? { threadId: resolvedThreadId } : {}
} : wakeDeliveryContext
}));
}
await removeRestartSentinelFile(sentinelPath);
if (!(payload.continuation?.kind === "agentTurn" && continuationRoute !== void 0)) enqueueRestartSentinelWake(message, sessionKey, wakeDeliveryContext);
if (resolvedTo && channel) {
const outboundSession = buildOutboundSessionContext({
cfg,
sessionKey: canonicalKey
});
await deliverRestartSentinelNotice({
deps: params.deps,
cfg,
sessionKey: canonicalKey,
summary,
message,
channel,
to: resolvedTo,
accountId: origin?.accountId,
replyToId,
threadId: resolvedThreadId,
session: outboundSession
});
}
if (continuationQueueId) await drainRestartContinuationQueue({
deps: params.deps,
entryId: continuationQueueId,
log
});
return { status: "ran" };
};
return {
source: "restart-sentinel",
...sessionKey ? { sessionKey } : {},
run
};
}
async function scheduleRestartSentinelWakeAttempt(params) {
const task = await loadRestartSentinelStartupTask(params);
if (!task) return;
await runStartupTasks({
tasks: [task],
log
});
}
async function scheduleRestartSentinelWake(params) {
await scheduleRestartSentinelWakeAttempt({
...params,
attempt: 0
});
}
function shouldWakeFromRestartSentinel() {
return !process.env.VITEST && true;
}
async function refreshLatestUpdateRestartSentinel() {
const current = await readRestartSentinel();
if (current?.payload.kind === "update" && isPendingControlPlaneUpdateRestartSentinel(current.payload)) {
latestUpdateRestartSentinel = cloneRestartSentinelPayload(current.payload);
return cloneRestartSentinelPayload(latestUpdateRestartSentinel);
}
const sentinel = await finalizeUpdateRestartSentinelRunningVersion() ?? current;
if (sentinel?.payload.kind === "update") latestUpdateRestartSentinel = cloneRestartSentinelPayload(sentinel.payload);
return cloneRestartSentinelPayload(latestUpdateRestartSentinel);
}
function getLatestUpdateRestartSentinel() {
return cloneRestartSentinelPayload(latestUpdateRestartSentinel);
}
function recordLatestUpdateRestartSentinel(payload) {
latestUpdateRestartSentinel = cloneRestartSentinelPayload(payload);
}
function toLintErrorObject(value, fallbackMessage) {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
const error = new Error(fallbackMessage, { cause: value });
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
return error;
}
//#endregion
export { scheduleRestartSentinelWake as a, refreshLatestUpdateRestartSentinel as i, recordLatestUpdateRestartSentinel as n, shouldWakeFromRestartSentinel as o, recoverPendingRestartContinuationDeliveries as r, getLatestUpdateRestartSentinel as t };