@sema-agent/core
Version:
Stateless, task-oriented AI agent core
216 lines (215 loc) • 9.06 kB
JavaScript
import { assertRetentionPolicy } from "./retention-policy.js";
export const MAILBOX_PEER_FROM_MODES = Object.freeze(["bypass", "prompting"]);
export const MAILBOX_PEER_RECORD_KINDS = Object.freeze(["peer_message", "idle_notice", "delivery_notice", "idle_subscription", "engine_frame"]);
export const MAILBOX_PEER_NOTICE_STATES = Object.freeze(["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"]);
export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
const PEER_META_STRING_KEYS = ["fromSession", "fromRoot", "senderKey", "fromScope", "gateReceiptId"];
export function readMailboxPeerMeta(raw) {
if (raw === undefined)
return undefined;
const refuse = (why) => {
throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore.append: peerMeta ${why}`);
};
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
return refuse("must be a plain object");
const proto = Object.getPrototypeOf(raw);
if (proto !== Object.prototype && proto !== null)
return refuse("must be a plain object (a class instance, Date, Map or similar is not)");
const out = {};
for (const key of Reflect.ownKeys(raw)) {
if (typeof key !== "string")
return refuse("carries a symbol-keyed member");
const k = key;
const v = raw[k];
if (v === undefined)
continue;
if (k === "fromMode") {
if (!MAILBOX_PEER_FROM_MODES.includes(v))
return refuse(`fromMode must be one of ${MAILBOX_PEER_FROM_MODES.join("|")}`);
out.fromMode = v;
}
else if (k === "kind") {
if (!MAILBOX_PEER_RECORD_KINDS.includes(v))
return refuse(`kind must be one of ${MAILBOX_PEER_RECORD_KINDS.join("|")}`);
out.kind = v;
}
else if (PEER_META_STRING_KEYS.includes(k)) {
if (typeof v !== "string" || v === "")
return refuse(`${k} must be a non-empty string`);
out[k] = v;
}
else if (k === "notice") {
out.notice = readMailboxPeerNoticeMeta(v, refuse);
}
else if (k === "urgent") {
if (typeof v !== "boolean")
return refuse("urgent must be a boolean");
out.urgent = v;
}
else {
return refuse(`carries an unknown key ${JSON.stringify(k)}`);
}
}
return out;
}
function readMailboxPeerNoticeMeta(raw, refuse) {
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
return refuse("notice must be a plain object");
const proto = Object.getPrototypeOf(raw);
if (proto !== Object.prototype && proto !== null)
return refuse("notice must be a plain object (a class instance, Date, Map or similar is not)");
const out = {};
for (const key of Reflect.ownKeys(raw)) {
if (typeof key !== "string")
return refuse("notice carries a symbol-keyed member");
const v = raw[key];
if (v === undefined)
continue;
if (key === "state") {
if (!MAILBOX_PEER_NOTICE_STATES.includes(v))
return refuse(`notice.state must be one of ${MAILBOX_PEER_NOTICE_STATES.join("|")}`);
out.state = v;
}
else if (key === "recipient" || key === "dropReason" || key === "detail" || key === "target") {
if (typeof v !== "string" || v === "")
return refuse(`notice.${key} must be a non-empty string`);
out[key] = v;
}
else if (key === "refSeq" || key === "finishedAt") {
if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
return refuse(`notice.${key} must be a non-negative finite number`);
out[key] = v;
}
else {
return refuse(`notice carries an unknown key ${JSON.stringify(key)}`);
}
}
if (out.state === undefined)
return refuse("notice.state is required");
return out;
}
export function cloneMailboxPeerMeta(meta) {
return meta === undefined ? undefined : { ...meta, ...(meta.notice !== undefined ? { notice: { ...meta.notice } } : {}) };
}
export const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned";
export class MailboxStoreError extends Error {
code;
constructor(code, message) {
super(message);
this.code = code;
this.name = "MailboxStoreError";
}
}
export function mailboxCrossProcessMountVerdict(store) {
const declared = store.crossProcessSafe;
if (declared === true)
return { ok: true };
const reason = declared === undefined
? "the mailbox backend does not declare cross-process safety (crossProcessSafe is absent): several terminal sessions would share one session box on luck"
: declared === false
? "the mailbox backend declares crossProcessSafe: false (process-local by design): it cannot serve a session box shared across OS processes"
: `the mailbox backend's crossProcessSafe declaration is malformed (${typeof declared}, expected a boolean)`;
return { ok: false, code: MAILBOX_CROSS_PROCESS_UNSAFE_CODE, reason };
}
export function newestSentAt(messages) {
let newest;
for (const m of messages) {
if (!Number.isFinite(m.sentAt))
return undefined;
if (newest === undefined || m.sentAt > newest)
newest = m.sentAt;
}
return newest;
}
export function detachMailboxMessage(m) {
return {
...m,
...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}),
...(m.peerMeta !== undefined ? { peerMeta: cloneMailboxPeerMeta(m.peerMeta) } : {}),
};
}
export class InMemoryMailboxStore {
crossProcessSafe = false;
boxes = new Map();
key(scope, handle) {
if (handle.includes("\u0000")) {
throw new Error(`MailboxStore: unsafe handle ${JSON.stringify(handle)} — a NUL byte makes the composite key ambiguous`);
}
return `${scope}\u0000${handle.toLowerCase()}`;
}
box(scope, handle) {
const k = this.key(scope, handle);
let b = this.boxes.get(k);
if (!b) {
b = { scope, messages: [], nextSeq: 1 };
this.boxes.set(k, b);
}
return b;
}
async append(scope, handle, msg) {
if (scope === undefined || scope === "")
throw new Error("MailboxStore.append: refusing a message without a scope");
const peerMeta = readMailboxPeerMeta(msg.peerMeta);
const b = this.box(scope, handle);
const seq = b.nextSeq++;
b.messages.push({
seq,
...(msg.from !== undefined ? { from: msg.from } : {}),
content: msg.content,
sentAt: msg.sentAt,
...(msg.hopChain !== undefined ? { hopChain: [...msg.hopChain] } : {}),
...(peerMeta !== undefined ? { peerMeta } : {}),
});
return seq;
}
async claimLease(scope, handle, owner, ttlMs, now = Date.now()) {
const b = this.boxes.get(this.key(scope, handle));
if (!b || b.messages.length === 0)
return null;
if (b.lease !== undefined && b.lease.expiresAt > now && b.lease.owner !== owner)
return null;
const maxSeq = b.messages[b.messages.length - 1].seq;
b.lease = { owner, expiresAt: now + ttlMs, maxSeq };
return { messages: b.messages.map((m) => detachMailboxMessage(m)), maxSeq };
}
async ack(scope, handle, owner, upToSeq) {
const b = this.boxes.get(this.key(scope, handle));
if (!b)
return { acked: false };
if (b.lease === undefined || b.lease.owner !== owner)
return { acked: false };
b.messages = b.messages.filter((m) => m.seq > upToSeq);
if (b.lease.maxSeq <= upToSeq)
b.lease = undefined;
return { acked: true };
}
async releaseLease(scope, handle, owner) {
const b = this.boxes.get(this.key(scope, handle));
if (b?.lease?.owner === owner)
b.lease = undefined;
}
async peekCount(scope, handle) {
return this.boxes.get(this.key(scope, handle))?.messages.length ?? 0;
}
async drop(scope, handle) {
this.boxes.delete(this.key(scope, handle));
}
async reap(scope, now, opts) {
assertRetentionPolicy("MailboxStore.reap", opts);
if (opts?.maxAgeMs === undefined)
return 0;
let dropped = 0;
for (const b of this.boxes.values()) {
if (b.scope !== scope)
continue;
const newest = newestSentAt(b.messages);
if (newest !== undefined && newest < now - opts.maxAgeMs) {
b.messages = [];
b.lease = undefined;
dropped++;
}
}
return dropped;
}
}