openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
353 lines (352 loc) • 14 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { t as isPromiseLike } from "./promise-like-D7-l5Fsp.js";
import { t as isPluginJsonValue } from "./host-hook-json-BdcyDerH.js";
import { h as getActivePluginSessionExtensionRegistry, u as getActivePluginRegistry } from "./runtime-BL4wZfTq.js";
import { t as buildPluginAgentTurnPrepareContext } from "./host-hooks-sVMn958c.js";
import { t as normalizeSessionEntrySlotKey } from "./session-entry-slot-keys-DWTKggcI.js";
import "./session-accessor-YsytfDtG.js";
import { a as resolveSessionEntryAccessTarget, c as updateResolvedSessionEntry } from "./session-accessor.entry-CQ2WPpfn.js";
import { randomUUID } from "node:crypto";
//#region src/plugins/host-hook-state.ts
const log = createSubsystemLogger("plugins/host-hook-state");
const PROJECTION_FAILED = Symbol("plugin-session-extension-projection-failed");
const MAX_PLUGIN_NEXT_TURN_INJECTION_TEXT_LENGTH = 32768;
const MAX_PLUGIN_NEXT_TURN_INJECTION_IDEMPOTENCY_KEY_LENGTH = 512;
const MAX_PLUGIN_NEXT_TURN_INJECTIONS_PER_SESSION = 32;
function normalizeNamespace(value) {
return value.trim();
}
function copyJsonValue(value) {
return structuredClone(value);
}
function isPluginNextTurnInjectionPlacement(value) {
return value === "prepend_context" || value === "append_context";
}
function isPluginNextTurnInjectionRecord(value) {
if (!value || typeof value !== "object") return false;
const candidate = value;
return typeof candidate.id === "string" && typeof candidate.pluginId === "string" && typeof candidate.text === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && isPluginNextTurnInjectionPlacement(candidate.placement) && (candidate.ttlMs === void 0 || typeof candidate.ttlMs === "number" && Number.isFinite(candidate.ttlMs) && candidate.ttlMs >= 0) && (candidate.idempotencyKey === void 0 || typeof candidate.idempotencyKey === "string");
}
function isExpired(entry, now) {
if (!isPluginNextTurnInjectionRecord(entry)) return true;
return typeof entry.ttlMs === "number" && entry.ttlMs >= 0 && now - entry.createdAt > entry.ttlMs;
}
function isPluginPromptInjectionEnabled(cfg, pluginId) {
return (cfg.plugins?.entries?.[pluginId])?.hooks?.allowPromptInjection !== false;
}
function toPluginNextTurnInjectionRecord(params) {
return {
id: params.injection.idempotencyKey?.trim() || randomUUID(),
pluginId: params.pluginId,
pluginName: params.pluginName,
text: params.injection.text,
idempotencyKey: params.injection.idempotencyKey?.trim() || void 0,
placement: params.injection.placement ?? "prepend_context",
ttlMs: params.injection.ttlMs,
createdAt: params.now,
metadata: params.injection.metadata
};
}
async function enqueuePluginNextTurnInjection(params) {
if (typeof params.injection.sessionKey !== "string") return {
enqueued: false,
id: "",
sessionKey: ""
};
const sessionKey = params.injection.sessionKey.trim();
if (!sessionKey) return {
enqueued: false,
id: "",
sessionKey
};
if (typeof params.injection.text !== "string") return {
enqueued: false,
id: "",
sessionKey
};
const text = params.injection.text.trim();
if (!text) return {
enqueued: false,
id: "",
sessionKey
};
if (text.length > MAX_PLUGIN_NEXT_TURN_INJECTION_TEXT_LENGTH) return {
enqueued: false,
id: "",
sessionKey
};
if (params.injection.metadata !== void 0 && !isPluginJsonValue(params.injection.metadata)) return {
enqueued: false,
id: "",
sessionKey
};
if (params.injection.idempotencyKey !== void 0 && (typeof params.injection.idempotencyKey !== "string" || params.injection.idempotencyKey.trim().length === 0 || params.injection.idempotencyKey.length > MAX_PLUGIN_NEXT_TURN_INJECTION_IDEMPOTENCY_KEY_LENGTH)) return {
enqueued: false,
id: "",
sessionKey
};
if (params.injection.placement !== void 0 && !isPluginNextTurnInjectionPlacement(params.injection.placement)) return {
enqueued: false,
id: "",
sessionKey
};
if (params.injection.ttlMs !== void 0 && (!Number.isFinite(params.injection.ttlMs) || params.injection.ttlMs < 0)) return {
enqueued: false,
id: "",
sessionKey
};
const now = params.now ?? Date.now();
const record = toPluginNextTurnInjectionRecord({
pluginId: params.pluginId,
pluginName: params.pluginName,
injection: {
...params.injection,
sessionKey,
text
},
now
});
const scope = {
cfg: params.cfg,
sessionKey,
agentId: params.injection.agentId
};
const updated = await updateResolvedSessionEntry(scope, (entry) => {
let enqueued = false;
let resultId = record.id;
const injections = { ...entry.pluginNextTurnInjections };
const rawExisting = injections[params.pluginId];
const existing = (Array.isArray(rawExisting) ? [...rawExisting] : []).filter((candidate) => !isExpired(candidate, now));
const duplicate = record.idempotencyKey ? existing.find((candidate) => candidate.idempotencyKey === record.idempotencyKey) : void 0;
if (duplicate) {
resultId = duplicate.id;
injections[params.pluginId] = existing;
entry.pluginNextTurnInjections = injections;
return {
enqueued,
id: resultId
};
}
if (existing.length >= MAX_PLUGIN_NEXT_TURN_INJECTIONS_PER_SESSION) {
injections[params.pluginId] = existing;
entry.pluginNextTurnInjections = injections;
return {
enqueued,
id: resultId
};
}
injections[params.pluginId] = [...existing, record];
entry.pluginNextTurnInjections = injections;
entry.updatedAt = now;
enqueued = true;
return {
enqueued,
id: resultId
};
});
if (!updated.found) return {
enqueued: false,
id: "",
sessionKey
};
return {
...updated.result,
sessionKey: updated.canonicalKey
};
}
async function drainPluginNextTurnInjections(params) {
const sessionKey = params.sessionKey?.trim();
if (!sessionKey) return [];
const scope = {
cfg: params.cfg,
sessionKey,
agentId: params.agentId
};
const target = resolveSessionEntryAccessTarget(scope);
if (!target.entry) return [];
if (!target.entry.pluginNextTurnInjections || Object.keys(target.entry.pluginNextTurnInjections).length === 0) return [];
const now = params.now ?? Date.now();
const updated = await updateResolvedSessionEntry(scope, (entry) => {
if (!entry?.pluginNextTurnInjections) return [];
const activePluginIds = new Set((getActivePluginRegistry()?.plugins ?? []).filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id));
const drained = [];
for (const [pluginId, entries] of Object.entries(entry.pluginNextTurnInjections)) {
if (!activePluginIds.has(pluginId) || !isPluginPromptInjectionEnabled(params.cfg, pluginId)) continue;
if (!Array.isArray(entries)) continue;
const liveEntries = entries.filter((candidate) => !isExpired(candidate, now));
drained.push(...liveEntries);
}
drained.sort((left, right) => left.createdAt - right.createdAt);
delete entry.pluginNextTurnInjections;
if (drained.length > 0) entry.updatedAt = now;
return drained;
});
return updated.found ? updated.result : [];
}
async function drainPluginNextTurnInjectionContext(params) {
const queuedInjections = await drainPluginNextTurnInjections(params);
return {
queuedInjections,
...buildPluginAgentTurnPrepareContext({ queuedInjections })
};
}
function getPluginSessionExtensionStateSync(params) {
const pluginId = params.pluginId.trim();
const sessionKey = normalizeOptionalString(params.sessionKey);
if (!pluginId || !sessionKey) return;
const value = resolveSessionEntryAccessTarget({
cfg: params.cfg,
sessionKey,
agentId: params.agentId
}).entry?.pluginExtensions?.[pluginId];
return value ? copyJsonValue(value) : void 0;
}
async function patchPluginSessionExtension(params) {
const namespace = normalizeNamespace(params.namespace);
const pluginId = params.pluginId.trim();
if (!pluginId || !namespace) return {
ok: false,
error: "pluginId and namespace are required"
};
if (params.unset === true && params.value !== void 0) return {
ok: false,
error: "plugin session extension cannot specify both unset and value"
};
if (params.value !== void 0 && !isPluginJsonValue(params.value)) return {
ok: false,
error: "plugin session extension value must be JSON-compatible"
};
if (params.unset !== true && params.value === void 0) return {
ok: false,
error: "plugin session extension value is required unless unset is true"
};
const nextPluginValue = params.value;
const registration = (getActivePluginSessionExtensionRegistry()?.sessionExtensions ?? []).find((entry) => entry.pluginId === pluginId && entry.extension.namespace === namespace);
if (!registration) return {
ok: false,
error: `unknown plugin session extension: ${pluginId}/${namespace}`
};
const rawSlotKey = normalizeOptionalString(registration.extension.sessionEntrySlotKey);
const normalizedSlotKey = rawSlotKey ? normalizeSessionEntrySlotKey(rawSlotKey) : void 0;
if (normalizedSlotKey?.ok === false) log.warn(`plugin session extension slot promotion skipped for ${pluginId}/${namespace}: ${normalizedSlotKey.error}`);
const slotKey = normalizedSlotKey?.ok === true ? normalizedSlotKey.key : void 0;
const updated = await updateResolvedSessionEntry({
cfg: params.cfg,
sessionKey: params.sessionKey,
agentId: params.agentId
}, (entry, context) => {
params.assertCurrent?.();
const entryRecord = entry;
const pluginExtensions = { ...entry.pluginExtensions };
const pluginState = { ...pluginExtensions[pluginId] };
if (params.unset === true) delete pluginState[namespace];
else pluginState[namespace] = copyJsonValue(nextPluginValue);
if (Object.keys(pluginState).length > 0) pluginExtensions[pluginId] = pluginState;
else delete pluginExtensions[pluginId];
if (Object.keys(pluginExtensions).length > 0) entry.pluginExtensions = pluginExtensions;
else delete entry.pluginExtensions;
const storedSlotKeys = { ...entry.pluginExtensionSlotKeys };
const pluginSlotKeys = { ...storedSlotKeys[pluginId] };
const previousSlotKey = normalizeSessionEntrySlotKey(pluginSlotKeys[namespace]);
if (previousSlotKey.ok && previousSlotKey.key !== slotKey) delete entryRecord[previousSlotKey.key];
if (slotKey && params.unset !== true) pluginSlotKeys[namespace] = slotKey;
else delete pluginSlotKeys[namespace];
if (Object.keys(pluginSlotKeys).length > 0) storedSlotKeys[pluginId] = pluginSlotKeys;
else delete storedSlotKeys[pluginId];
if (Object.keys(storedSlotKeys).length > 0) entry.pluginExtensionSlotKeys = storedSlotKeys;
else delete entry.pluginExtensionSlotKeys;
if (slotKey) {
const projected = projectSessionExtensionValueForSlot({
registration,
sessionKey: context.canonicalKey,
sessionId: entry.sessionId,
nextValue: params.unset === true ? void 0 : nextPluginValue
});
if (projected === void 0) delete entryRecord[slotKey];
else entryRecord[slotKey] = projected;
}
entry.updatedAt = Date.now();
return pluginState[namespace];
});
if (!updated.found) return {
ok: false,
error: `unknown session key: ${params.sessionKey}`
};
return {
ok: true,
key: updated.canonicalKey,
value: updated.result
};
}
/**
* Resolve the value that should be mirrored to `SessionEntry[slotKey]` for a
* promoted session-extension namespace. Failures are swallowed so a
* misbehaving projector cannot block the primary patch from being persisted.
*/
function projectSessionExtensionValueForSlot(params) {
if (params.nextValue === void 0) return;
const projected = projectSessionExtensionValue({
pluginId: params.registration.pluginId,
namespace: params.registration.extension.namespace,
project: params.registration.extension.project,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
state: params.nextValue
});
if (projected === PROJECTION_FAILED) return;
if (isPromiseLike(projected)) {
discardUnexpectedPromiseProjection(projected);
return;
}
if (projected === void 0 || !isPluginJsonValue(projected)) return;
return copyJsonValue(projected);
}
function collectPluginSessionExtensionProjections(params) {
const extensions = getActivePluginSessionExtensionRegistry()?.sessionExtensions ?? [];
if (extensions.length === 0) return [];
const projections = [];
for (const registration of extensions) {
const state = params.entry.pluginExtensions?.[registration.pluginId]?.[registration.extension.namespace];
if (state === void 0) continue;
const projected = projectSessionExtensionValue({
pluginId: registration.pluginId,
namespace: registration.extension.namespace,
project: registration.extension.project,
sessionKey: params.sessionKey,
sessionId: params.entry.sessionId,
state
});
if (projected === PROJECTION_FAILED) continue;
if (isPromiseLike(projected)) {
discardUnexpectedPromiseProjection(projected);
continue;
}
if (projected !== void 0 && isPluginJsonValue(projected)) projections.push({
pluginId: registration.pluginId,
namespace: registration.extension.namespace,
value: copyJsonValue(projected)
});
}
return projections;
}
function discardUnexpectedPromiseProjection(value) {
Promise.resolve(value).catch(() => void 0);
}
function projectSessionExtensionValue(params) {
try {
return params.project ? params.project({
sessionKey: params.sessionKey,
sessionId: params.sessionId,
state: params.state
}) : params.state;
} catch (error) {
log.warn(`plugin session extension projection failed: plugin=${params.pluginId} namespace=${params.namespace} error=${String(error)}`);
return PROJECTION_FAILED;
}
}
function projectPluginSessionExtensionsSync(params) {
return collectPluginSessionExtensionProjections(params);
}
//#endregion
export { projectPluginSessionExtensionsSync as a, patchPluginSessionExtension as i, enqueuePluginNextTurnInjection as n, getPluginSessionExtensionStateSync as r, drainPluginNextTurnInjectionContext as t };