@sema-agent/core
Version:
Stateless, task-oriented AI agent core
262 lines (261 loc) • 9.1 kB
JavaScript
import { PEER_HELD_QUEUE_CAP, PEER_HELD_REVIEW_CAUSES } from "./peer-notices.js";
import { peerSessionBoxHandle } from "./peer-directory.js";
export const realPeerClock = {
now: () => Date.now(),
setTimeout: (fn, ms) => {
const h = setTimeout(fn, ms);
h.unref?.();
return h;
},
clearTimeout: (h) => clearTimeout(h),
};
const crashHandlers = new WeakMap();
export const PEER_HELD_DECISION_MEMORY = PEER_HELD_QUEUE_CAP * 4;
export function createPeerHeldQueue(key, opts = {}) {
const cap = opts.cap ?? PEER_HELD_QUEUE_CAP;
const clock = opts.clock ?? realPeerClock;
const entries = [];
const deadlines = new Map();
const decisions = new Map();
let cfg;
const sinkOrThrow = () => {
if (cfg === undefined)
throw new Error("PeerHeldQueue: not configured — bind a run (configure) before holding messages");
return cfg.sink;
};
const safe = (f) => {
try {
f?.();
}
catch {
}
};
const clearDeadline = (seq) => {
const h = deadlines.get(seq);
if (h !== undefined) {
clock.clearTimeout(h);
deadlines.delete(seq);
}
};
const remove = (entry) => {
const i = entries.indexOf(entry);
if (i !== -1)
entries.splice(i, 1);
clearDeadline(entry.seq);
};
const settleOut = (entry, settlement, reason, receipt) => {
const sink = sinkOrThrow();
remove(entry);
if (settlement !== "delivered") {
decisions.set(entry.seq, settlement);
while (decisions.size > PEER_HELD_DECISION_MEMORY) {
const oldest = decisions.keys().next();
if (oldest.done === true)
break;
decisions.delete(oldest.value);
}
}
if (receipt !== undefined)
safe(() => sink.receipt(entry, receipt));
safe(() => sink.onSettled?.(entry, settlement, reason, entries.length));
};
const armDeadline = (entry) => {
if (cfg === undefined || cfg.dialogExpiryMs === null || deadlines.has(entry.seq))
return;
if (entry.released === true)
return;
if (!PEER_HELD_REVIEW_CAUSES.includes(entry.cause))
return;
const h = clock.setTimeout(() => {
deadlines.delete(entry.seq);
if (!entries.includes(entry))
return;
rejudge();
if (!entries.includes(entry))
return;
settleOut(entry, "expired", "deadline", "expired");
}, cfg.dialogExpiryMs);
deadlines.set(entry.seq, h);
};
const tryDeliver = (entry, reason) => {
const sink = sinkOrThrow();
if (sink.inject === undefined)
return "no_leg";
try {
if (sink.inject(entry) === "deferred")
return "no_leg";
}
catch {
settleOut(entry, "dropped", "inject_failed", "dropped");
return "dropped";
}
settleOut(entry, "delivered", reason, "delivered");
return "delivered";
};
const reconcile = (present) => {
const sink = sinkOrThrow();
let retired = 0;
for (const entry of [...entries]) {
if (present.has(entry.seq))
continue;
remove(entry);
retired += 1;
safe(() => sink.onSettled?.(entry, "expired", "superseded", entries.length));
}
return retired;
};
const rejudge = () => {
const sink = sinkOrThrow();
let released = 0;
let refused = 0;
const reannounce = [];
for (const entry of [...entries]) {
let verdict;
try {
verdict = sink.judge(entry);
}
catch {
continue;
}
if (verdict.verdict === "refuse") {
settleOut(entry, "refused", "rejudge", "refused");
refused += 1;
continue;
}
if (verdict.verdict === "deliver" || entry.released === true) {
const r = tryDeliver(entry, entry.released === true ? "approved" : "rejudge");
if (r === "no_leg")
entry.released = true;
else if (r === "delivered")
released += 1;
continue;
}
if (verdict.cause !== entry.cause) {
entry.cause = verdict.cause;
reannounce.push([entry, verdict.cause]);
clearDeadline(entry.seq);
armDeadline(entry);
}
}
for (const [entry, cause] of reannounce)
safe(() => sink.onHeld?.(entry, entries.length, cause, true));
return { released, refused, held: entries.length };
};
const queue = {
key,
configure(next) {
cfg = next;
for (const entry of entries) {
if (next.dialogExpiryMs === null)
clearDeadline(entry.seq);
else
armDeadline(entry);
}
},
detachLeg() {
if (cfg === undefined)
return;
const { inject: _inject, ...rest } = cfg.sink;
void _inject;
cfg = { ...cfg, sink: rest };
},
configured: () => cfg !== undefined,
hold(input) {
const sink = sinkOrThrow();
if (entries.some((e) => e.box === input.box && e.seq === input.seq))
return;
const entry = { ...input, heldAt: clock.now() };
if (entries.length >= cap) {
const oldest = entries[0];
settleOut(oldest, "expired", "evicted", "expired");
}
entries.push(entry);
armDeadline(entry);
safe(() => sink.onHeld?.(entry, entries.length, entry.cause, false));
safe(() => sink.receipt(entry, "held"));
},
rejudge,
reconcile,
settle(seq, decision) {
const sink = sinkOrThrow();
const entry = entries.find((e) => e.seq === seq);
if (entry === undefined)
return "gone";
if (decision === "deny") {
settleOut(entry, "denied", "denied", "denied");
return "denied";
}
if (decision === "cancel") {
settleOut(entry, "expired", "cancelled", "expired");
return "expired";
}
let verdict;
try {
verdict = sink.judge(entry);
}
catch {
entry.released = true;
return "released_pending";
}
if (verdict.verdict === "refuse") {
settleOut(entry, "refused", "approved", "refused");
return "dropped_by_policy";
}
const r = tryDeliver(entry, "approved");
if (r === "no_leg") {
entry.released = true;
clearDeadline(entry.seq);
return "released_pending";
}
return r;
},
list: () => entries.map((e) => ({ ...e, ...(e.hopChain !== undefined ? { hopChain: [...e.hopChain] } : {}), ...(e.peerMeta !== undefined ? { peerMeta: { ...e.peerMeta } } : {}) })),
size: () => entries.length,
decisions: () => decisions,
forgetDecisions(seqs) {
for (const seq of seqs)
decisions.delete(seq);
},
shutdown() {
if (cfg === undefined) {
entries.length = 0;
for (const h of deadlines.values())
clock.clearTimeout(h);
deadlines.clear();
return;
}
for (const entry of [...entries])
settleOut(entry, "expired", "shutdown", "expired");
},
};
crashHandlers.set(queue, () => {
decisions.clear();
entries.length = 0;
for (const h of deadlines.values())
clock.clearTimeout(h);
deadlines.clear();
});
return queue;
}
const heldQueues = new Map();
export function peerHeldQueueKey(scope, sessionId, box) {
const own = peerSessionBoxHandle(sessionId);
return JSON.stringify(box === undefined || box === own ? [scope, own] : [scope, own, box]);
}
export function peerHeldQueueFor(scope, sessionId, opts = {}) {
const key = peerHeldQueueKey(scope, sessionId, opts.box);
let q = heldQueues.get(key);
if (q === undefined) {
q = createPeerHeldQueue(key, opts);
heldQueues.set(key, q);
}
return q;
}
export function peerHeldQueueIfAny(scope, sessionId, box) {
return heldQueues.get(peerHeldQueueKey(scope, sessionId, box));
}
export function resetPeerHeldQueuesForTests() {
for (const q of heldQueues.values())
crashHandlers.get(q)?.();
heldQueues.clear();
}