@sema-agent/core
Version:
Stateless, task-oriented AI agent core
303 lines (302 loc) • 12.7 kB
JavaScript
import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js";
import { isSessionConflict } from "./session.js";
import { readMember } from "./thrown-value.js";
const DAY_MS = 24 * 60 * 60 * 1000;
import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
export { SESSION_DEFAULT_TTL_DAYS };
export class TtlSessionStore {
retention = "none";
durability;
placements;
listPlaced;
repo;
entries = new Map();
owners = new Map();
pending = new Map();
pendingPlacedDeletes = new Set();
deletingPlaced = new Map();
pinned = new Set();
defaultTtlMs;
evictPolicy;
sweepTimer;
constructor(opts = {}) {
this.repo = opts.repo ?? new InMemorySessionRepo();
const durability = opts.durability ?? (opts.repo === undefined ? "process-local" : undefined);
if (durability !== undefined)
this.durability = durability;
this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
if (opts.placements !== undefined)
this.placements = opts.placements;
const repoListPlaced = this.repo.listPlaced;
if (typeof repoListPlaced === "function") {
this.listPlaced = (kind, o) => repoListPlaced.call(this.repo, kind, o);
}
if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
this.sweepTimer = setInterval(() => this.sweep(), opts.sweepIntervalMs);
this.sweepTimer.unref?.();
}
}
async acquire(sessionId, opts) {
this.sweep();
if (sessionId) {
const inflightPlacedDelete = this.deletingPlaced.get(sessionId);
if (inflightPlacedDelete !== undefined)
await inflightPlacedDelete;
const existing = this.entries.get(sessionId);
if (existing) {
this.assertPlacementAdmission(sessionId, existing.placement, opts);
existing.lastActiveAt = Date.now();
return { session: existing.session, sessionId };
}
if (opts?.requireExisting) {
return { session: await this.openOrCreate(sessionId, true), sessionId };
}
const inflight = this.pending.get(sessionId);
if (inflight) {
const session = await inflight;
this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
return { session, sessionId };
}
const p = this.openOrCreate(sessionId, false, opts?.placement);
this.pending.set(sessionId, p);
try {
const session = await p;
this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
return { session, sessionId };
}
finally {
this.pending.delete(sessionId);
}
}
const session = await this.createAndStore(undefined, opts?.placement);
const meta = await session.getMetadata();
return { session, sessionId: meta.id };
}
assertPlacementAdmission(sessionId, placement, opts) {
if (placement !== undefined && opts?.requireExisting !== true && opts?.placement === undefined) {
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placement.kind} placement partition — a claim-form acquire cannot attach to it (read it with requireExisting, or address the agent through its a* handle)`);
}
if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
const want = opts.placement;
if (placement === undefined) {
throw new SessionError("placement_refused", `session ${sessionId} exists OUTSIDE the ${want.kind} placement partition it was declared into — refusing to run a subagent transcript on it (a concurrent claim-form create won this id, or the repo dropped the tuple)`);
}
if (placement.kind !== want.kind || placement.scope !== want.scope || placement.handle !== want.handle) {
throw new SessionError("placement_refused", `session ${sessionId}'s persisted placement does not match the declared one (persisted ${placement.kind}/${placement.scope ?? "-"}/${placement.handle ?? "-"} vs declared ${want.kind}/${want.scope ?? "-"}/${want.handle ?? "-"}) — refusing a cross-tuple attach`);
}
}
}
async openOrCreate(id, requireExisting = false, placement) {
try {
const session = await this.repo.open({ id, createdAt: "" });
const meta = await session.getMetadata();
if (meta.placement === undefined)
this.pendingPlacedDeletes.delete(id);
this.entries.set(id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
return session;
}
catch (err) {
if (!isNotFound(err) && !isSessionConflict(err)) {
throw err;
}
if (requireExisting && isNotFound(err)) {
throw err;
}
return this.createAndStore(id, placement);
}
}
async createAndStore(id, placement) {
const session = await this.repo.create({ ...(id !== undefined ? { id } : {}), ...(placement !== undefined ? { placement } : {}) });
const meta = await session.getMetadata();
if (meta.placement === undefined)
this.pendingPlacedDeletes.delete(meta.id);
this.entries.set(meta.id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
return session;
}
touch(sessionId) {
const e = this.entries.get(sessionId);
if (e) {
e.lastActiveAt = Date.now();
}
}
noteTaskRun(sessionId, taskId, runId) {
const e = this.entries.get(sessionId);
if (e) {
e.lastTaskId = taskId;
if (runId !== undefined)
e.lastTaskRunId = runId;
else
delete e.lastTaskRunId;
e.lastActiveAt = Date.now();
}
}
async list() {
return [...this.entries.entries()]
.filter(([, e]) => e.placement === undefined)
.map(([sessionId, e]) => ({
sessionId,
lastActiveAt: e.lastActiveAt,
...(e.createdAt !== undefined && e.createdAt !== "" ? { createdAt: e.createdAt } : {}),
...(e.lastTaskId !== undefined ? { lastTaskId: e.lastTaskId } : {}),
...(e.lastTaskRunId !== undefined ? { lastTaskRunId: e.lastTaskRunId } : {}),
...(e.forkedFrom !== undefined ? { forkedFrom: e.forkedFrom } : {}),
}))
.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
}
async fork(sourceId, owner) {
let source;
try {
source = await this.repo.open({ id: sourceId, createdAt: "" });
}
catch (err) {
if (isNotFound(err)) {
return null;
}
throw err;
}
const forkOptions = await settledForkOptions(source);
const forkParams = { ...forkOptions, id: uuidv7() };
if (owner != null) {
forkParams.owner = owner;
}
const forked = await this.repo.fork(await source.getMetadata(), forkParams);
const meta = await forked.getMetadata();
const inherited = this.owners.has(sourceId) ? this.owners.get(sourceId) : undefined;
const forkOwner = owner ?? inherited;
if (forkOwner !== undefined && !this.owners.has(meta.id)) {
this.owners.set(meta.id, forkOwner);
}
this.entries.set(meta.id, {
session: forked,
lastActiveAt: Date.now(),
createdAt: meta.createdAt,
forkedFrom: meta.forkedFrom ?? sourceId,
});
return meta.id;
}
async release(sessionId) {
if (this.pinned.has(sessionId)) {
return;
}
const e = this.entries.get(sessionId);
if (e !== undefined && e.placement === undefined)
this.pendingPlacedDeletes.delete(sessionId);
let placed = e !== undefined ? e.placement !== undefined : this.pendingPlacedDeletes.has(sessionId);
if (!placed && e === undefined && this.placements !== undefined) {
const probe = this.repo.placementOf;
if (typeof probe === "function") {
try {
placed = (await probe.call(this.repo, sessionId)) !== undefined;
}
catch {
placed = false;
}
}
}
if (!e && !placed) {
return;
}
if (placed) {
this.pendingPlacedDeletes.add(sessionId);
this.entries.delete(sessionId);
let settleGate;
this.deletingPlaced.set(sessionId, new Promise((r) => (settleGate = r)));
try {
const meta = e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" };
await this.repo.delete(meta);
}
finally {
this.deletingPlaced.delete(sessionId);
settleGate();
}
this.pendingPlacedDeletes.delete(sessionId);
this.owners.delete(sessionId);
return;
}
this.entries.delete(sessionId);
if (this.evictPolicy === "delete") {
await this.repo.delete(e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" });
this.owners.delete(sessionId);
}
}
forget(sessionId) {
this.entries.delete(sessionId);
}
pin(sessionId) {
this.pinned.add(sessionId);
}
unpin(sessionId) {
this.pinned.delete(sessionId);
}
async ownerOf(sessionId) {
return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined;
}
async register(sessionId, owner) {
if (this.owners.has(sessionId)) {
return;
}
if (owner !== null && typeof owner !== "string") {
throw new TypeError(`SessionStore.register: owner must be a string (owned) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner}`);
}
this.owners.set(sessionId, owner);
}
async incarnationOf(sessionId) {
let session;
try {
session = await this.repo.open({ id: sessionId, createdAt: "" });
}
catch (err) {
if (isNotFound(err))
return undefined;
throw err;
}
return (await session.getMetadata()).incarnation;
}
sweep(now = Date.now()) {
for (const [id, e] of this.entries) {
if (this.pinned.has(id)) {
continue;
}
if (now - e.lastActiveAt > this.defaultTtlMs) {
this.entries.delete(id);
if (this.evictPolicy === "delete" && e.placement === undefined) {
void this.repo.delete({ id, createdAt: "" }).then(() => {
this.owners.delete(id);
}, () => { });
}
}
}
}
get size() {
return this.entries.size;
}
dispose() {
if (this.sweepTimer) {
clearInterval(this.sweepTimer);
}
}
}
async function settledForkOptions(source) {
const storage = source.getStorage();
let id = await storage.getLeafId();
while (id) {
const entry = await storage.getEntry(id);
if (!entry || entry.type !== "message") {
return {};
}
if (entry.message.role === "assistant") {
const hasToolCall = Array.isArray(entry.message.content) &&
entry.message.content.some((c) => c.type === "toolCall");
return hasToolCall && entry.parentId ? { entryId: entry.parentId, position: "at" } : {};
}
if (entry.message.role !== "toolResult") {
return {};
}
id = entry.parentId;
}
return {};
}
function isNotFound(err) {
return readMember(err, "code") === "not_found";
}