openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
899 lines (898 loc) • 35.2 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { n as sliceUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import { u as normalizeMainKey } from "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js";
import "./io-bdCzpGWJ.js";
import { i as loadOrCreateProcessDeviceIdentity } from "./device-identity-J83pn_rP.js";
import { En as validateNodePresenceActivityPayload, pn as validateNodeHostStatsPayload } from "./src-BiL5aQto.js";
import { x as runWithGatewayIndependentRootWorkContinuation } from "./gateway-work-admission-R1IpuDim.js";
import { D as NODE_PRESENCE_ALIVE_REASONS } from "./nodes-D1M6Vssk.js";
import { i as deleteMediaBuffer } from "./store-Ph57PZ9P.js";
import { n as deliveryContextFromSession } from "./delivery-context.shared-CXmRgetN.js";
import { l as requestHeartbeat } from "./heartbeat-wake-dlWl8dXS.js";
import { a as enqueueSystemEvent, h as withSystemEventOwner } from "./system-events-C03jsM0j.js";
import "./session-accessor-YsytfDtG.js";
import { b as upsertSessionEntryCore } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { a as normalizeChannelId } from "./registry-Cz6cv4VC.js";
import "./plugins-Cozj1Enf.js";
import { c as resolveSystemMainSessionTarget } from "./main-session-Br0F9dzh.js";
import { l as resolveAgentHarnessSessionContextError } from "./agent-harness-session-key-BOz3yx0-.js";
import { t as buildOutboundSessionContext } from "./session-context-CpdGMrv6.js";
import { n as resolveSessionModelRef } from "./session-model-ref-CPZiclLt.js";
import { r as resolveGatewayModelSupportsImages } from "./session-utils-model-BYAclq2V.js";
import { i as scopedHeartbeatWakeOptionsForPolicy, n as resolveEventSessionRoutingPolicy, t as resolveEventSessionKeyForPolicy } from "./event-session-routing-CJOO5dMS.js";
import { r as loadGatewaySessionEntry } from "./session-utils-store-CInT2loy.js";
import "./session-utils-Cai0_C6U.js";
import { n as sendDurableMessageBatchCore } from "./send-Dk4bo3m5.js";
import "./runtime-zD0iWK04.js";
import { a as resolveOutboundTarget } from "./targets-CP7C4mlG.js";
import { t as NODE_HOST_STATS_EVENT } from "./node-host-stats-BMvjvr90.js";
import { y as updatePairedDevicePresence } from "./device-pairing-DFIQp3ZY.js";
import { t as createOutboundSendDeps } from "./outbound-send-deps-CzQHPhLv.js";
import { r as agentCommandFromIngress } from "./agent-command-DK8Hh-Mq.js";
import { t as formatForLog } from "./ws-log-Dkg9wKNU.js";
import "./agent-W7bgjwyp.js";
import { n as resolveChatAttachmentMaxBytes } from "./chat-attachment-policy-F0KcWQR6.js";
import { c as persistInboundImagesForTranscript, s as parseMessageWithAttachments, t as INLINE_IMAGE_DURABLE_OMISSION_MARKER } from "./chat-attachments-DAJvw83V.js";
import { t as normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize-BA7mKleS.js";
import "./push-apns-n6D4554g.js";
import { h as registerApnsRegistration, t as ApnsRegistrationPairingChangedError } from "./push-apns-store-cDZzCfro.js";
import { randomUUID } from "node:crypto";
//#region src/shared/node-presence.ts
/** Gateway event name used by node hosts to refresh their last-seen presence. */
const NODE_PRESENCE_ALIVE_EVENT = "node.presence.alive";
/** Gateway event name used by interactive nodes to report recent local input. */
const NODE_PRESENCE_ACTIVITY_EVENT = "node.presence.activity";
const NODE_PRESENCE_ALIVE_REASON_SET = new Set(Object.values(NODE_PRESENCE_ALIVE_REASONS));
/** Normalizes untrusted presence trigger values, defaulting unknown input to background. */
function normalizeNodePresenceAliveReason(value) {
const normalized = normalizeOptionalString(value)?.toLowerCase();
if (normalized && NODE_PRESENCE_ALIVE_REASON_SET.has(normalized)) return normalized;
return "background";
}
//#endregion
//#region src/gateway/server-node-events.ts
function resolveDefaultServerNodeEventDependencies() {
return {
agentCommandFromIngress,
ApnsRegistrationPairingChangedError,
buildOutboundSessionContext,
createOutboundSendDeps,
defaultRuntime,
deleteMediaBuffer,
enqueueSystemEvent,
formatForLog,
getRuntimeConfig,
INLINE_IMAGE_DURABLE_OMISSION_MARKER,
loadOrCreateProcessDeviceIdentity,
loadSessionEntry: loadGatewaySessionEntry,
normalizeChannelId,
normalizeMainKey,
normalizeRpcAttachmentsToChatAttachments,
parseMessageWithAttachments,
persistInboundImagesForTranscript,
registerApnsRegistration,
requestHeartbeat,
resolveChatAttachmentMaxBytes,
resolveGatewayModelSupportsImages,
resolveOutboundTarget,
resolveSessionAgentId,
resolveSessionModelRef,
resolveSystemMainSessionTarget,
sendDurableMessageBatchCore,
updatePairedDevicePresence,
upsertSessionEntryCore,
withSystemEventOwner
};
}
const MAX_EXEC_EVENT_OUTPUT_CHARS = 180;
const MAX_NOTIFICATION_EVENT_TEXT_CHARS = 120;
const VOICE_TRANSCRIPT_DEDUPE_WINDOW_MS = 1500;
const MAX_RECENT_VOICE_TRANSCRIPTS = 200;
const EXEC_FINISHED_RUN_DEDUPE_WINDOW_MS = 6e5;
const MAX_RECENT_EXEC_FINISHED_RUNS = 2e3;
const NODE_PRESENCE_PERSIST_MIN_INTERVAL_MS = 6e4;
const MAX_RECENT_NODE_PRESENCE_KEYS = 1024;
const recentVoiceTranscripts = /* @__PURE__ */ new Map();
const pendingVoiceTranscriptReservations = /* @__PURE__ */ new Map();
const recentExecFinishedRuns = /* @__PURE__ */ new Map();
const recentNodePresencePersistAt = /* @__PURE__ */ new Map();
function normalizeFiniteInteger(value) {
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : null;
}
function dispatchNodeAgentCommand(ctx, nodeId, input, dependencies, isConnectionCurrent, onAdmissionRejected) {
runWithGatewayIndependentRootWorkContinuation(async () => {
if (isConnectionCurrent && !await isConnectionCurrent()) {
await onAdmissionRejected?.();
return;
}
await dependencies.agentCommandFromIngress(input, dependencies.defaultRuntime, ctx.deps);
}, "node-events:agent-turn").catch((err) => {
ctx.logGateway.warn(`agent failed node=${nodeId}: ${dependencies.formatForLog(err)}`);
});
}
function resolveVoiceTranscriptFingerprint(obj, text) {
const eventId = normalizeOptionalString(obj.eventId) ?? normalizeOptionalString(obj.providerEventId) ?? normalizeOptionalString(obj.transcriptId);
if (eventId) return `event:${eventId}`;
const callId = normalizeOptionalString(obj.providerCallId) ?? normalizeOptionalString(obj.callId);
const sequence = normalizeFiniteInteger(obj.sequence) ?? normalizeFiniteInteger(obj.seq);
if (callId && sequence !== null) return `call-seq:${callId}:${sequence}`;
const eventTimestamp = normalizeFiniteInteger(obj.timestamp) ?? normalizeFiniteInteger(obj.ts) ?? normalizeFiniteInteger(obj.eventTimestamp);
if (callId && eventTimestamp !== null) return `call-ts:${callId}:${eventTimestamp}`;
if (eventTimestamp !== null) return `timestamp:${eventTimestamp}|text:${text}`;
return `text:${text}`;
}
function shouldDropDuplicateVoiceTranscript(params) {
const previous = recentVoiceTranscripts.get(params.sessionKey);
if (previous && previous.fingerprint === params.fingerprint && params.now - previous.ts <= VOICE_TRANSCRIPT_DEDUPE_WINDOW_MS) return true;
recentVoiceTranscripts.set(params.sessionKey, {
fingerprint: params.fingerprint,
ts: params.now
});
if (recentVoiceTranscripts.size > MAX_RECENT_VOICE_TRANSCRIPTS) {
const cutoff = params.now - VOICE_TRANSCRIPT_DEDUPE_WINDOW_MS * 2;
for (const [key, value] of recentVoiceTranscripts) {
if (value.ts < cutoff) recentVoiceTranscripts.delete(key);
if (recentVoiceTranscripts.size <= MAX_RECENT_VOICE_TRANSCRIPTS) break;
}
pruneMapToMaxSize(recentVoiceTranscripts, MAX_RECENT_VOICE_TRANSCRIPTS);
}
return false;
}
function reserveVoiceTranscript(params) {
let resolveDecision = () => {};
let rejectDecision = () => {};
const decision = new Promise((resolve, reject) => {
resolveDecision = resolve;
rejectDecision = reject;
});
const reservation = {
fingerprint: params.fingerprint,
receivedAt: params.receivedAt,
status: "pending",
resolve: resolveDecision,
rejectDecision,
decision
};
const queue = pendingVoiceTranscriptReservations.get(params.sessionKey) ?? [];
queue.push(reservation);
pendingVoiceTranscriptReservations.set(params.sessionKey, queue);
const drain = () => {
while (queue[0]?.status === "rejected") {
const next = queue.shift();
if (!next) break;
next.resolve(null);
}
const next = queue[0];
if (!next) {
pendingVoiceTranscriptReservations.delete(params.sessionKey);
return;
}
if (next.status !== "ready") return;
next.status = "checking";
(async () => {
try {
const admission = (next.isConnectionCurrent ? await next.isConnectionCurrent() : true) && !shouldDropDuplicateVoiceTranscript({
sessionKey: params.sessionKey,
fingerprint: next.fingerprint,
now: next.receivedAt
}) && next.start ? { work: next.start() } : null;
queue.shift();
next.resolve(admission);
} catch (err) {
queue.shift();
next.rejectDecision(err);
}
drain();
})();
};
const settle = (status) => {
if (reservation.status !== "pending") return;
reservation.status = status;
drain();
};
return {
admit: ({ isConnectionCurrent, start }) => {
reservation.isConnectionCurrent = isConnectionCurrent;
reservation.start = start;
settle("ready");
return reservation.decision;
},
reject: () => settle("rejected")
};
}
function dispatchReservedVoiceAgentCommand(params) {
runWithGatewayIndependentRootWorkContinuation(async () => {
if (params.isConnectionCurrent && !await params.isConnectionCurrent()) {
params.reservation.reject();
return;
}
const admission = await params.reservation.admit({
isConnectionCurrent: params.isConnectionCurrent,
start: () => {
params.onStart();
return params.dependencies.agentCommandFromIngress(params.input, params.dependencies.defaultRuntime, params.ctx.deps);
}
});
if (!admission) return;
await admission.work;
}, "node-events:voice-turn").catch((err) => {
params.reservation.reject();
params.ctx.logGateway.warn(`agent failed node=${params.nodeId}: ${params.dependencies.formatForLog(err)}`);
});
}
function shouldDropDuplicateExecFinished(params) {
const fingerprint = `${params.sessionKey}::${params.runId}`;
const previousTs = recentExecFinishedRuns.get(fingerprint);
if (typeof previousTs === "number" && params.now - previousTs <= EXEC_FINISHED_RUN_DEDUPE_WINDOW_MS) return true;
recentExecFinishedRuns.set(fingerprint, params.now);
if (recentExecFinishedRuns.size > MAX_RECENT_EXEC_FINISHED_RUNS) {
const cutoff = params.now - EXEC_FINISHED_RUN_DEDUPE_WINDOW_MS;
for (const [key, ts] of recentExecFinishedRuns) {
if (ts < cutoff) recentExecFinishedRuns.delete(key);
if (recentExecFinishedRuns.size <= MAX_RECENT_EXEC_FINISHED_RUNS) break;
}
pruneMapToMaxSize(recentExecFinishedRuns, MAX_RECENT_EXEC_FINISHED_RUNS);
}
return false;
}
function pruneBoundedTimestampMap(map, params) {
if (map.size <= params.maxEntries) return;
const cutoff = params.now - params.ttlMs;
for (const [key, ts] of map) {
if (ts < cutoff) map.delete(key);
if (map.size <= params.maxEntries) return;
}
pruneMapToMaxSize(map, params.maxEntries);
}
function compactExecEventOutput(raw) {
const normalized = raw.replace(/\s+/g, " ").trim();
if (!normalized) return "";
if (normalized.length <= MAX_EXEC_EVENT_OUTPUT_CHARS) return normalized;
return `${sliceUtf16Safe(normalized, 0, Math.max(1, 179))}…`;
}
function compactNotificationEventText(raw) {
const normalized = raw.replace(/\s+/g, " ").trim();
if (!normalized) return "";
if (normalized.length <= MAX_NOTIFICATION_EVENT_TEXT_CHARS) return normalized;
return `${sliceUtf16Safe(normalized, 0, Math.max(1, 119))}…`;
}
async function touchSessionStore(params) {
const { storePath } = params;
if (!storePath) return;
await params.dependencies.upsertSessionEntryCore({
sessionKey: params.canonicalKey,
storePath
}, {
sessionId: params.sessionId,
updatedAt: params.now,
thinkingLevel: params.entry?.thinkingLevel,
fastMode: params.entry?.fastMode,
verboseLevel: params.entry?.verboseLevel,
reasoningLevel: params.entry?.reasoningLevel,
systemSent: params.entry?.systemSent,
sendPolicy: params.entry?.sendPolicy,
delivery: params.entry?.delivery
});
}
function queueSessionStoreTouch(params) {
runWithGatewayIndependentRootWorkContinuation(async () => {
if (params.isConnectionCurrent && !await params.isConnectionCurrent()) return;
await touchSessionStore({
storePath: params.storePath,
canonicalKey: params.canonicalKey,
entry: params.entry,
sessionId: params.sessionId,
now: params.now,
dependencies: params.dependencies
});
}, "node-events:voice-persist").catch((err) => {
params.ctx.logGateway.warn("voice session-store update failed: " + params.dependencies.formatForLog(err));
});
}
async function isNodeEventConnectionCurrent(opts) {
if (!opts?.isConnectionCurrent) return true;
try {
return await opts.isConnectionCurrent();
} catch {
return false;
}
}
function pairingChangedResult(event) {
return {
ok: true,
event,
handled: false,
reason: "pairing_changed"
};
}
async function cleanupNodeEventMedia(ids, ctx, dependencies) {
for (const id of ids) try {
await dependencies.deleteMediaBuffer(id);
} catch (cleanupErr) {
ctx.logGateway.warn(`Failed to cleanup orphaned media ${id}: ${formatErrorMessage(cleanupErr)}`);
}
}
function parseSessionKeyFromPayloadJSON(payloadJSON) {
let payload;
try {
payload = JSON.parse(payloadJSON);
} catch {
return null;
}
if (typeof payload !== "object" || payload === null) return null;
const sessionKey = normalizeOptionalString(payload.sessionKey) ?? "";
return sessionKey.length > 0 ? sessionKey : null;
}
function parsePayloadObject(payloadJSON) {
if (!payloadJSON) return null;
let payload;
try {
payload = JSON.parse(payloadJSON);
} catch {
return null;
}
return typeof payload === "object" && payload !== null ? payload : null;
}
async function sendReceiptAck(params) {
const resolved = params.dependencies.resolveOutboundTarget({
channel: params.channel,
to: params.to,
cfg: params.cfg,
mode: "explicit"
});
if (!resolved.ok) throw new Error(String(resolved.error));
const session = params.dependencies.buildOutboundSessionContext({
cfg: params.cfg,
sessionKey: params.sessionKey
});
const send = await params.dependencies.sendDurableMessageBatchCore({
cfg: params.cfg,
channel: params.channel,
to: resolved.to,
payloads: [{ text: params.text }],
session,
bestEffort: true,
durability: "best_effort",
deps: params.dependencies.createOutboundSendDeps(params.deps)
});
if (send.status === "failed") throw send.error;
}
const handleNodeEvent = async (ctx, nodeId, evt, opts, dependencies = resolveDefaultServerNodeEventDependencies()) => {
const { ApnsRegistrationPairingChangedError, enqueueSystemEvent, formatForLog, getRuntimeConfig, INLINE_IMAGE_DURABLE_OMISSION_MARKER, loadOrCreateProcessDeviceIdentity, loadSessionEntry, normalizeChannelId, normalizeMainKey, normalizeRpcAttachmentsToChatAttachments, parseMessageWithAttachments, persistInboundImagesForTranscript, registerApnsRegistration, requestHeartbeat, resolveChatAttachmentMaxBytes, resolveGatewayModelSupportsImages, resolveSessionAgentId, resolveSessionModelRef, resolveSystemMainSessionTarget, updatePairedDevicePresence, withSystemEventOwner } = dependencies;
if (!await isNodeEventConnectionCurrent(opts)) return pairingChangedResult(evt.event);
switch (evt.event) {
case "voice.transcript": {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj) return;
const text = normalizeOptionalString(obj.text) ?? "";
if (!text) return;
if (text.length > 2e4) return;
const sessionKeyRaw = normalizeOptionalString(obj.sessionKey) ?? "";
const rawMainKey = normalizeMainKey(getRuntimeConfig().session?.mainKey);
const { storePath, entry, canonicalKey } = loadSessionEntry(sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey);
if (resolveAgentHarnessSessionContextError(canonicalKey, entry)) return;
const receivedAt = Date.now();
const fingerprint = resolveVoiceTranscriptFingerprint(obj, text);
const sessionId = entry?.sessionId ?? randomUUID();
const runId = randomUUID();
const transcriptReservation = reserveVoiceTranscript({
sessionKey: canonicalKey,
fingerprint,
receivedAt
});
dispatchReservedVoiceAgentCommand({
ctx,
nodeId,
dependencies,
input: {
runId,
message: text,
sessionId,
sessionKey: canonicalKey,
thinking: "low",
deliver: false,
messageChannel: "node",
inputProvenance: {
kind: "external_user",
sourceChannel: "voice",
sourceTool: "gateway.voice.transcript"
},
allowModelOverride: false
},
reservation: transcriptReservation,
isConnectionCurrent: opts?.isConnectionCurrent,
onStart: () => {
queueSessionStoreTouch({
ctx,
dependencies,
storePath,
canonicalKey,
entry,
sessionId,
now: receivedAt,
isConnectionCurrent: opts?.isConnectionCurrent
});
ctx.addChatRun(runId, {
sessionKey: canonicalKey,
clientRunId: runId
});
}
});
return;
}
case "agent.request": {
if (!evt.payloadJSON) return;
let link;
try {
link = JSON.parse(evt.payloadJSON);
} catch {
return;
}
const sessionKeyRaw = (link?.sessionKey ?? "").trim();
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : `node-${nodeId}`;
const cfg = getRuntimeConfig();
const { storePath, entry, canonicalKey } = loadSessionEntry(sessionKey);
if (resolveAgentHarnessSessionContextError(canonicalKey, entry)) return;
let message = (link?.message ?? "").trim();
let transcriptMessage = message;
const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(link?.attachments ?? void 0);
let images = [];
let imageOrder = [];
let offloadedRefs = [];
if (!message && normalizedAttachments.length === 0) return;
if (message.length > 2e4) return;
if (normalizedAttachments.length > 0) {
const sessionAgentId = resolveSessionAgentId({
sessionKey,
config: cfg
});
const modelRef = resolveSessionModelRef(cfg, entry, sessionAgentId);
const supportsInlineImages = await resolveGatewayModelSupportsImages({
loadGatewayModelCatalog: ctx.loadGatewayModelCatalog,
loadGatewayModelCatalogSnapshot: ctx.loadGatewayModelCatalogSnapshot,
agentId: sessionAgentId,
provider: modelRef.provider,
model: modelRef.model
});
if (!await isNodeEventConnectionCurrent(opts)) return pairingChangedResult(evt.event);
try {
const parsed = await parseMessageWithAttachments(message, normalizedAttachments, {
maxBytes: resolveChatAttachmentMaxBytes(cfg),
log: ctx.logGateway,
supportsInlineImages,
acceptNonImage: false
});
if (!await isNodeEventConnectionCurrent(opts)) {
await cleanupNodeEventMedia((parsed.offloadedRefs ?? []).map((ref) => ref.id), ctx, dependencies);
return pairingChangedResult(evt.event);
}
message = parsed.message.trim();
images = parsed.images;
imageOrder = parsed.imageOrder;
offloadedRefs = parsed.offloadedRefs;
if (message.length > 2e4) {
ctx.logGateway.warn(`agent.request message exceeds limit after attachment parsing (length=${message.length})`);
if (parsed.offloadedRefs && parsed.offloadedRefs.length > 0) await cleanupNodeEventMedia(parsed.offloadedRefs.map((ref) => ref.id), ctx, dependencies);
return;
}
} catch (err) {
ctx.logGateway.warn(`agent.request attachment parse failed: ${formatErrorMessage(err)}`);
return;
}
}
if (!message && images.length === 0) return;
let channel = normalizeChannelId(normalizeOptionalString(link?.channel) ?? "") ?? void 0;
let to = normalizeOptionalString(link?.to);
const deliverRequested = Boolean(link?.deliver);
const wantsReceipt = Boolean(link?.receipt);
const receiptText = normalizeOptionalString(link?.receiptText) || "Just received your iOS share + request, working on it.";
const now = Date.now();
const sessionId = entry?.sessionId ?? randomUUID();
if (!await isNodeEventConnectionCurrent(opts)) {
await cleanupNodeEventMedia((offloadedRefs ?? []).map((ref) => ref.id), ctx, dependencies);
return pairingChangedResult(evt.event);
}
await touchSessionStore({
storePath,
canonicalKey,
entry,
sessionId,
now,
dependencies
});
if (!await isNodeEventConnectionCurrent(opts)) {
await cleanupNodeEventMedia((offloadedRefs ?? []).map((ref) => ref.id), ctx, dependencies);
return pairingChangedResult(evt.event);
}
if (deliverRequested && (!channel || !to)) {
const entryContext = deliveryContextFromSession(entry);
const entryChannel = entryContext?.channel ? normalizeChannelId(entryContext.channel) : void 0;
const entryTo = normalizeOptionalString(entryContext?.to) ?? "";
if (!channel && entryChannel) channel = entryChannel;
if (!to && entryTo) to = entryTo;
}
const deliver = deliverRequested && Boolean(channel && to);
const deliveryChannel = deliver ? channel : void 0;
const deliveryTo = deliver ? to : void 0;
if (deliverRequested && !deliver) ctx.logGateway.warn(`agent delivery disabled node=${nodeId}: missing session delivery route (channel=${channel ?? "-"} to=${to ?? "-"})`);
if (!await isNodeEventConnectionCurrent(opts)) {
await cleanupNodeEventMedia((offloadedRefs ?? []).map((ref) => ref.id), ctx, dependencies);
return pairingChangedResult(evt.event);
}
const persistedTranscriptMedia = await persistInboundImagesForTranscript({
images,
offloadedRefs,
log: ctx.logGateway,
logContext: "agent.request"
});
if (!await isNodeEventConnectionCurrent(opts)) {
await cleanupNodeEventMedia(persistedTranscriptMedia.entries.map((media) => media.id), ctx, dependencies);
return pairingChangedResult(evt.event);
}
if (persistedTranscriptMedia.omission === "inline-image-save-failed") transcriptMessage = [transcriptMessage, INLINE_IMAGE_DURABLE_OMISSION_MARKER].filter(Boolean).join("\n");
const transcriptMedia = persistedTranscriptMedia.entries.map((media) => media.fact);
if (wantsReceipt && deliveryChannel && deliveryTo) runWithGatewayIndependentRootWorkContinuation(async () => {
if (!await isNodeEventConnectionCurrent(opts)) return;
await sendReceiptAck({
cfg,
deps: ctx.deps,
dependencies,
sessionKey: canonicalKey,
channel: deliveryChannel,
to: deliveryTo,
text: receiptText
});
}, "node-events:delivery").catch((err) => {
ctx.logGateway.warn(`agent receipt failed node=${nodeId}: ${formatForLog(err)}`);
});
else if (wantsReceipt) ctx.logGateway.warn(`agent receipt skipped node=${nodeId}: missing delivery route (channel=${deliveryChannel ?? "-"} to=${deliveryTo ?? "-"})`);
dispatchNodeAgentCommand(ctx, nodeId, {
runId: sessionId,
message,
images,
imageOrder,
...transcriptMedia.length > 0 || persistedTranscriptMedia.omission === "inline-image-save-failed" ? {
transcriptMessage,
...transcriptMedia.length > 0 ? { transcriptMedia } : {}
} : {},
sessionId,
sessionKey: canonicalKey,
thinking: link?.thinking ?? void 0,
deliver,
to: deliveryTo,
channel: deliveryChannel,
timeout: typeof link?.timeoutSeconds === "number" ? link.timeoutSeconds.toString() : void 0,
messageChannel: "node",
allowModelOverride: false
}, dependencies, opts?.isConnectionCurrent, () => cleanupNodeEventMedia(persistedTranscriptMedia.entries.map((media) => media.id), ctx, dependencies));
return;
}
case "notifications.changed": {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj) return;
const change = normalizeOptionalString(obj.change) ? normalizeLowercaseStringOrEmpty(obj.change) : void 0;
if (change !== "posted" && change !== "removed") return;
const keyRaw = normalizeOptionalString(obj.key);
if (!keyRaw) return;
const key = keyRaw;
const requestedSessionKey = normalizeOptionalString(obj.sessionKey);
let target;
try {
target = requestedSessionKey ? { sessionKey: requestedSessionKey } : resolveSystemMainSessionTarget(getRuntimeConfig());
} catch (error) {
ctx.logGateway.warn(`notification event not delivered node=${nodeId}: ${formatErrorMessage(error)}`);
return;
}
const sessionKeyRaw = target.sessionKey;
const { canonicalKey: sessionKey, entry } = loadSessionEntry(sessionKeyRaw);
if (resolveAgentHarnessSessionContextError(sessionKey, entry)) return;
const packageName = normalizeOptionalString(obj.packageName) ?? null;
const title = compactNotificationEventText(normalizeOptionalString(obj.title) ?? "");
const text = compactNotificationEventText(normalizeOptionalString(obj.text) ?? "");
let summary = `Notification ${change} (node=${nodeId} key=${key}`;
if (packageName) summary += ` package=${packageName}`;
summary += ")";
if (change === "posted") {
const messageParts = [title, text].filter(Boolean);
if (messageParts.length > 0) summary += `: ${messageParts.join(" - ")}`;
}
const eventOptions = {
sessionKey,
contextKey: `notification:${keyRaw}`
};
if (enqueueSystemEvent(summary, target.agentId ? withSystemEventOwner(eventOptions, target.agentId) : eventOptions)) requestHeartbeat({
source: "notifications-event",
intent: "event",
reason: "notifications-event",
...target.agentId ? { agentId: target.agentId } : {},
sessionKey
});
return;
}
case "chat.subscribe": {
if (!evt.payloadJSON) return;
const sessionKey = parseSessionKeyFromPayloadJSON(evt.payloadJSON);
if (!sessionKey) return;
const { canonicalKey } = loadSessionEntry(sessionKey);
await ctx.nodeSubscribe(nodeId, canonicalKey, opts?.connId);
return;
}
case "chat.unsubscribe": {
if (!evt.payloadJSON) return;
const sessionKey = parseSessionKeyFromPayloadJSON(evt.payloadJSON);
if (!sessionKey) return;
const { canonicalKey } = loadSessionEntry(sessionKey);
await ctx.nodeUnsubscribe(nodeId, canonicalKey, opts?.connId);
return;
}
case "exec.started":
case "exec.finished":
case "exec.denied": {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj) return;
const sessionKeyRaw = normalizeOptionalString(obj.sessionKey) ?? `node-${nodeId}`;
if (!sessionKeyRaw) return;
const { canonicalKey: sessionKey } = loadSessionEntry(sessionKeyRaw);
const cfg = getRuntimeConfig();
const runId = normalizeOptionalString(obj.runId) ?? "";
if (!ctx.authorizeNodeSystemRunEvent({
nodeId,
connId: opts?.connId,
...runId ? { runId } : {},
sessionKey: sessionKeyRaw,
terminal: evt.event === "exec.finished" || evt.event === "exec.denied"
})) return {
ok: true,
event: evt.event,
handled: false,
reason: "unmatched_exec_event"
};
if (!(cfg.tools?.exec?.notifyOnExit !== false)) return;
if (obj.suppressNotifyOnExit === true) return;
if (evt.event === "exec.denied") return;
const command = normalizeOptionalString(obj.command) ?? "";
const exitCode = typeof obj.exitCode === "number" && Number.isFinite(obj.exitCode) ? obj.exitCode : void 0;
const timedOut = obj.timedOut === true;
const output = normalizeOptionalString(obj.output) ?? "";
const reason = (normalizeOptionalString(obj.reason) ?? "").replace(/[()]/g, "");
let text;
if (evt.event === "exec.started") {
text = `Exec started (node=${nodeId}${runId ? ` id=${runId}` : ""})`;
if (command) text += `: ${command}`;
} else if (evt.event === "exec.finished") {
const exitLabel = timedOut ? "timeout" : `code ${exitCode ?? "?"}`;
const compactOutput = compactExecEventOutput(output);
if (!(timedOut || exitCode !== 0 || compactOutput.length > 0)) return;
if (runId && shouldDropDuplicateExecFinished({
sessionKey,
runId,
now: Date.now()
})) return;
text = `Exec finished (node=${nodeId}${runId ? ` id=${runId}` : ""}, ${exitLabel})`;
if (compactOutput) text += `\n${compactOutput}`;
} else {
text = `Exec denied (node=${nodeId}${runId ? ` id=${runId}` : ""}${reason ? `, ${reason}` : ""})`;
if (command) text += `: ${command}`;
}
const eventRouting = resolveEventSessionRoutingPolicy({
cfg,
sessionKey
});
if (enqueueSystemEvent(text, {
sessionKey: resolveEventSessionKeyForPolicy(sessionKey, eventRouting),
contextKey: runId ? `exec:${runId}` : "exec"
})) requestHeartbeat(scopedHeartbeatWakeOptionsForPolicy(sessionKey, {
source: "exec-event",
intent: "event",
reason: "exec-event",
coalesceMs: 0
}, eventRouting));
return;
}
case "push.apns.register": {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj) return;
const transport = normalizeLowercaseStringOrEmpty(obj.transport) || "direct";
const topic = typeof obj.topic === "string" ? obj.topic : "";
const environment = obj.environment;
try {
const expectedPairingGeneration = await opts?.resolveApnsRegistrationGeneration?.();
if (!expectedPairingGeneration) {
ctx.logGateway.warn(`push apns register rejected node=${nodeId}: stale or invalidated pairing session`);
return pairingChangedResult(evt.event);
}
if (transport === "relay") {
const gatewayDeviceId = normalizeOptionalString(obj.gatewayDeviceId) ?? "";
const currentGatewayDeviceId = loadOrCreateProcessDeviceIdentity().deviceId;
if (!gatewayDeviceId || gatewayDeviceId !== currentGatewayDeviceId) {
ctx.logGateway.warn(`push relay register rejected node=${nodeId}: gateway identity mismatch`);
return;
}
await registerApnsRegistration({
nodeId,
transport: "relay",
relayHandle: typeof obj.relayHandle === "string" ? obj.relayHandle : "",
sendGrant: typeof obj.sendGrant === "string" ? obj.sendGrant : "",
installationId: typeof obj.installationId === "string" ? obj.installationId : "",
topic,
environment,
distribution: obj.distribution,
relayOrigin: obj.relayOrigin,
tokenDebugSuffix: obj.tokenDebugSuffix,
expectedPairingGeneration
});
} else await registerApnsRegistration({
nodeId,
transport: "direct",
token: typeof obj.token === "string" ? obj.token : "",
topic,
environment,
expectedPairingGeneration
});
} catch (err) {
if (err instanceof ApnsRegistrationPairingChangedError) {
ctx.logGateway.warn(`push apns register rejected node=${nodeId}: stale or invalidated pairing session`);
return pairingChangedResult(evt.event);
}
ctx.logGateway.warn(`push apns register failed node=${nodeId}: ${formatForLog(err)}`);
}
return;
}
case NODE_HOST_STATS_EVENT: {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj || !validateNodeHostStatsPayload(obj)) return {
ok: true,
event: evt.event,
handled: false,
reason: "invalid_payload"
};
const hostStats = ctx.updateNodeHostStats?.({
nodeId,
connId: opts?.connId,
stats: obj
});
if (!hostStats) return {
ok: true,
event: evt.event,
handled: false,
reason: "stale_connection"
};
ctx.broadcast("node.hostStats", {
nodeId,
hostStats
}, { dropIfSlow: true });
return {
ok: true,
event: evt.event,
handled: true,
reason: "updated"
};
}
case NODE_PRESENCE_ACTIVITY_EVENT: {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj || !validateNodePresenceActivityPayload(obj)) return {
ok: true,
event: evt.event,
handled: false,
reason: "invalid_payload"
};
if ("action" in obj) {
const cleared = ctx.clearNodePresenceActivity?.({
nodeId,
connId: opts?.connId
});
if (cleared === null || cleared === void 0) return {
ok: true,
event: evt.event,
handled: false,
reason: "stale_connection"
};
if (cleared) ctx.broadcast("node.presence", {
nodeId,
lastActiveAtMs: null,
presenceUpdatedAtMs: null
}, { dropIfSlow: true });
return {
ok: true,
event: evt.event,
handled: true,
reason: cleared ? "cleared" : "already_clear"
};
}
if (opts?.presenceAllowed !== true) return {
ok: true,
event: evt.event,
handled: false,
reason: "permission_required"
};
const updated = ctx.updateNodePresenceActivity?.({
nodeId,
connId: opts.connId,
idleSeconds: obj.idleSeconds,
...obj.saturated === true ? { saturated: true } : {}
});
if (!updated) return {
ok: true,
event: evt.event,
handled: false,
reason: "stale_connection"
};
ctx.broadcast("node.presence", {
nodeId,
...updated
}, { dropIfSlow: true });
return {
ok: true,
event: evt.event,
handled: true,
reason: "updated"
};
}
case NODE_PRESENCE_ALIVE_EVENT: {
const obj = parsePayloadObject(evt.payloadJSON);
if (!obj) return {
ok: true,
event: evt.event,
handled: false,
reason: "invalid_payload"
};
const deviceId = normalizeOptionalString(opts?.deviceId);
if (!deviceId) return {
ok: true,
event: evt.event,
handled: false,
reason: "missing_device_identity"
};
const pairingGeneration = opts?.pairingGeneration;
if (!pairingGeneration || pairingGeneration.nodeId !== deviceId) return pairingChangedResult(evt.event);
const now = Date.now();
const presenceOwnerKey = `${deviceId}\0${pairingGeneration.key}`;
if (now - (recentNodePresencePersistAt.get(presenceOwnerKey) ?? 0) < NODE_PRESENCE_PERSIST_MIN_INTERVAL_MS) return {
ok: true,
event: evt.event,
handled: true,
reason: "throttled"
};
const lastSeenReason = normalizeNodePresenceAliveReason(obj.trigger);
try {
if (!await updatePairedDevicePresence(deviceId, {
lastSeenAtMs: now,
lastSeenReason
}, pairingGeneration)) return pairingChangedResult(evt.event);
recentNodePresencePersistAt.set(presenceOwnerKey, now);
pruneBoundedTimestampMap(recentNodePresencePersistAt, {
now,
ttlMs: NODE_PRESENCE_PERSIST_MIN_INTERVAL_MS * 10,
maxEntries: MAX_RECENT_NODE_PRESENCE_KEYS
});
return {
ok: true,
event: evt.event,
handled: true,
reason: "persisted"
};
} catch (err) {
ctx.logGateway.warn(`node presence alive failed node=${nodeId}: ${formatForLog(err)}`);
return {
ok: true,
event: evt.event,
handled: false,
reason: "persist_failed"
};
}
}
default: return {
ok: true,
event: evt.event,
handled: false,
reason: "unsupported_event"
};
}
};
//#endregion
export { handleNodeEvent };