pocolobby 0.12.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board-page.js +31 -28
- package/dist/board.js +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli.js +7 -7
- package/dist/console-page.js +110 -68
- package/dist/console.js +1 -1
- package/dist/pace.js +1 -1
- package/dist/protocol.js +1 -1
- package/dist/server.js +4 -4
- package/dist/statusline.js +6 -6
- package/dist/store.js +40 -11
- package/docs/API.md +105 -78
- package/docs/ARCHITECTURE.md +27 -27
- package/docs/LOCAL-SETUP.md +2 -2
- package/docs/OPERATIONS.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/examples/PACING.md +14 -24
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).find(e=>e.trim())??"").trim().slice(0,200)}),"lineOf");async function
|
|
1
|
+
var $e=Object.defineProperty;var R=(u,e)=>$e(u,"name",{value:e,configurable:!0});import{createHash as x,randomBytes as qe,randomUUID as O,timingSafeEqual as Ee}from"node:crypto";import{memoryBriefParts as le,mergeProvenance as K,prepareMemory as me,rankMemory as Ye,rankPrepared as ue}from"./memory.js";import{findSecret as z}from"./secrets.js";import{chmodSync as he,closeSync as be,fsyncSync as Ge,mkdirSync as Ne,mkdtempSync as Je,openSync as Te,readdirSync as Ve,renameSync as Ke,statfsSync as ze,statSync as j,unlinkSync as Q,writeSync as je}from"node:fs";import{tmpdir as Qe}from"node:os";import{dirname as ge,join as X,resolve as B}from"node:path";import{LONG_PROMPT as Ze}from"./prices.js";import{backup as et,DatabaseSync as Re}from"node:sqlite";import{DEFAULT_ATTACHMENT_DISK_RESERVE_BYTES as tt,DEFAULT_ATTACHMENT_GRACE_MS as st,DEFAULT_ATTACHMENT_RETENTION_MS as nt,DEFAULT_LOBBY_ATTACHMENT_BYTES as it,DEFAULT_MAX_IMAGE_BYTES as at,DEFAULT_MAX_TEXT_BYTES as rt,formatBytes as v,kindOf as Z,kindOfBytes as ot,sniffImage as dt}from"./attachments.js";import{addressedOf as P,namedMembers as Le,AccountSettingsSchema as ct,CheckpointSchema as _t,AgentId as $,Email as q,LobbyId as Y,NewLobbyId as ye,RoomId as I,RoomMemberChangesSchema as Et,parsePrincipal as T,qualifyAgent as D,LobbyError as Se,MIN_PUSH_BYTES as pe,MAX_CONTENT_BYTES as lt,MAX_PAGE_BYTES as mt,MAX_SCHEDULE_AHEAD_MS as ee,ProvisionSchema as ut,RESERVED_REQUEST_PREFIX as W,SCHEDULE_PAST_TOLERANCE_MS as Oe,ScheduleSchema as ht,SendSchema as fe,OwnerRequestsSchema as bt,StatusSchema as Nt,UpdateAgentSchema as Tt,CurationFailureSchema as gt,CurationReportSchema as Rt,CodeStatsSchema as Lt,CurationSchema as yt,MEMORY_TEXT_LIMITS as Ae,RecallQuerySchema as St,RememberSchema as pt,canonicalRepeat as De,canonicalSend as te,defaultReplyExpected as Ot,messageDetails as se,DETAIL_FIELDS as Ue,UpdateScheduledSchema as ft,DisplayName as At,Currency as Dt,PacingSettingsSchema as Ut,AgentKind as Ct}from"./protocol.js";import{DEFAULT_PACING as ne,nextPacing as It,weeklyPace as Mt}from"./pace.js";import{BoardAgentsSchema as kt,BoardSourceSchema as vt,MAX_BOARD_SOURCES as Ce,RepoName as Ie,SUMMARY_MODELS as Me,SummarySettingsSchema as Ft,sourceKey as G}from"./board-model.js";import{latestDueOccurrence as Ht,localLabel as Wt,nextOccurrence as ie}from"./recurrence.js";import{SIGN_IN as f}from"./auth.js";import{checkLobbyIcon as wt}from"./icons.js";import{moneyFor as xt}from"./money.js";const F=R(u=>!!Number(u.paused),"resting"),J=R(u=>u.kind==="worker"&&!Number(u.manage_rooms),"paced"),ke=R(u=>u.pacing_lead===null?null:Number(u.pacing_lead),"pacingLead"),ae={image:"POCOLOBBY_ATTACHMENT_MAX_IMAGE_BYTES",text:"POCOLOBBY_ATTACHMENT_MAX_TEXT_BYTES",lobby:"POCOLOBBY_ATTACHMENT_LOBBY_BYTES",reserve:"POCOLOBBY_ATTACHMENT_DISK_RESERVE_BYTES"},M=20,Xt=1e4,Bt=10080*60*1e3,ve=300;function V(u){const e={...u};for(const t of Ue)delete e[t];return u.content.length>ve&&(e.content=`${u.content.slice(0,ve-1)}\u2026`,e.content_truncated=!0),e}R(V,"listed");function re(u){Buffer.byteLength(JSON.stringify(u))-2>lt&&_(400,"content_too_large","Content exceeds 64 KiB once escaped as JSON, where quotes, backslashes, newlines and control characters count two to six bytes; send less or attach it as a file")}R(re,"admitContent");const Pt=5,$t=20,w="scheduled",Fe="cancelled",qt=1440*60*1e3,Yt=1440*60*1e3,Gt='Sent, but reply_to names no message stored now, so its recipients see a reply to nothing they can open. Messages are kept a day: an older one is gone, and the broker cannot tell it from a mistyped ID. If you meant a message from today, find its ID with get_messages(mode="history") and send the reply again under a new client_request_id, with supersedes naming this one; while this one waits for its deliver_at, cancel it with cancel_scheduled instead.',oe=672*60*60*1e3,Jt=2880*60*1e3,Vt=200,de=720*60*60*1e3,He=2160*60*60*1e3,Kt=365*24*60*60*1e3,We=50,zt=.04,we=R(u=>`deliver_at must be at most 30 days ahead, so no later than ${new Date(u+ee).toISOString()}, and at most 5 minutes in the past`,"deliverAtRange"),jt=20,ce=R(u=>({...u.supersedes?{supersedes:String(u.supersedes)}:{},...u.superseded_by?{superseded_by:String(u.superseded_by)}:{}}),"corrections"),xe=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,H=R(u=>x("sha256").update(u).digest("hex"),"hash");function Xe(u,e){return u.reply_expected!==void 0?u.reply_expected:u.kind?!(u.room===void 0&&u.to===T(e).agent_id)&&Ot(u.kind):null}R(Xe,"replyExpectedOf");const Be=R(()=>`pl_${qe(32).toString("base64url")}`,"token"),_=R((u,e,t,n)=>{throw new Se(u,e,t,n)},"fail"),_e=R((u,e)=>`(${u}.room_id IS NOT NULL AND ${u}.cancelled_at IS NULL AND (${u}.deliver_at_ms IS NULL OR ${u}.released_ms IS NOT NULL) AND EXISTS(SELECT 1 FROM room_members rm WHERE rm.lobby_id=${u}.lobby_id AND rm.room_id=${u}.room_id AND rm.agent_id=${e} AND rm.joined_sequence<${u}.sequence))`,"roomReadable"),Qt=R(u=>({id:u.id,from:u.from,to:u.to,room_id:u.room_id,kind:u.kind,created_at:u.created_at,first_line:(u.content.split(`
|
|
2
|
+
`).find(e=>e.trim())??"").trim().slice(0,200)}),"lineOf");async function bs(u,e){B(e)===B(u)&&_(400,"invalid_backup_path","Backup destination must differ from the live database");const t=new Re(u,{readOnly:!0});try{await et(t,e)}finally{t.close()}he(e,384)}R(bs,"backupDatabase");class Ns{static{R(this,"LobbyStore")}db;clock;infoDelay;retries;bootstrapRetry;maxPending;maxAgents;maxLobbies;maxRooms;maxStored;notificationRate;lateAfter;receiptDelay;receiptInterval;maxSchedules;maxScheduled;maxImageBytes;maxTextBytes;maxLobbyAttachments;attachmentRetention;get attachmentRetentionDays(){return this.attachmentRetention/(1440*60*1e3)}attachmentGrace;diskReserve;freeDisk;attachmentsDirectory;constructor(e,t={}){this.clock=t.now??Date.now,this.infoDelay=t.infoDelayMs??1e3,this.retries=[...t.retryDelaysMs??[3e4,12e4,3e5]],this.bootstrapRetry=Math.max(...this.retries),this.maxPending=t.maxPendingBytes??4*1024*1024,this.maxAgents=t.maxAgents??100,this.maxLobbies=t.maxLobbies??100,this.maxRooms=t.maxRoomsPerLobby??100,this.maxStored=t.maxStoredBytes??256*1024*1024,this.notificationRate=t.notificationRatePerMinute??30,this.lateAfter=t.lateAfterMs??6e4,this.receiptDelay=t.receiptDelayMs??2e3,this.receiptInterval=t.receiptIntervalMs??3e4,this.maxSchedules=t.maxSchedulesPerSender??20,this.maxScheduled=t.maxScheduledPerSender??100,this.maxImageBytes=t.maxImageBytes??at,this.maxTextBytes=t.maxTextBytes??rt,this.maxLobbyAttachments=t.maxLobbyAttachmentBytes??it,this.attachmentRetention=t.attachmentRetentionMs??nt,this.attachmentGrace=t.attachmentGraceMs??st,this.diskReserve=t.attachmentDiskReserveBytes??tt,this.freeDisk=t.freeDiskBytes??(s=>{const i=ze(s);return Number(i.bavail)*Number(i.bsize)}),this.attachmentsDirectory=t.attachmentsDir??(e===":memory:"?void 0:X(ge(B(e)),"attachments"));const n=t.maxDatabaseBytes??1024*1024*1024;if(!this.retries.length||this.retries.some(s=>!Number.isFinite(s)||s<0)||[this.infoDelay,this.maxPending,this.maxStored,this.maxAgents,this.lateAfter,this.receiptDelay,this.receiptInterval,this.maxSchedules,this.maxScheduled,this.maxImageBytes,this.maxTextBytes,this.maxLobbyAttachments,this.attachmentRetention,this.attachmentGrace,this.diskReserve].some(s=>!Number.isFinite(s)||s<0))throw new RangeError("Store limits and retry delays must be finite and nonnegative; at least one attempt is required");if(!Number.isSafeInteger(this.notificationRate)||this.notificationRate<1)throw new RangeError("Notification rate must be a positive integer");if([this.maxLobbies,this.maxRooms].some(s=>!Number.isSafeInteger(s)||s<1))throw new RangeError("Lobby and room limits must be positive integers");if(!Number.isSafeInteger(n)||n<512*1024)throw new RangeError("Database page budget must be at least 512 KiB");e!==":memory:"&&(Ne(ge(B(e)),{recursive:!0,mode:448}),be(Te(e,"a",384)),he(e,384)),this.db=new Re(e);try{this.db.exec("PRAGMA foreign_keys=ON; PRAGMA busy_timeout=1000; PRAGMA locking_mode=EXCLUSIVE; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA wal_autocheckpoint=1000;");const s=Number(this.one("PRAGMA page_size").page_size),i=Math.floor(n/s);Number(this.one(`PRAGMA max_page_count=${i}`).max_page_count)>i&&_(507,"storage_capacity","Existing database exceeds its configured page budget");const r=Number(this.one("PRAGMA user_version").user_version);r===0?this.transaction(()=>this.createSchema()):r===M-1?this.transaction(()=>this.upgradeSchema()):r!==M&&_(500,"unsupported_schema",r>M?`This database is schema ${r}, which is newer than this broker (schema ${M}).`:`This database is schema ${r}; this broker opens schema ${M} and upgrades schema ${M-1} only. Bring it to schema ${M-1} first, restore a newer backup, or start a new database.`)}catch(s){throw this.db.close(),s}}createSchema(){this.db.exec(`
|
|
3
3
|
CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
4
4
|
CREATE TABLE accounts (
|
|
5
5
|
account_id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE,
|
|
@@ -26,7 +26,13 @@ var we=Object.defineProperty;var g=(b,e)=>we(b,"name",{value:e,configurable:!0})
|
|
|
26
26
|
work_description TEXT NOT NULL DEFAULT '', machine TEXT NOT NULL DEFAULT '',
|
|
27
27
|
screen INTEGER NOT NULL DEFAULT 0,
|
|
28
28
|
-- When a resting (paused) agent resumes by itself (0.10.1); null rests until resumed.
|
|
29
|
-
resume_at_ms INTEGER
|
|
29
|
+
resume_at_ms INTEGER,
|
|
30
|
+
-- Worker or on_demand, as the owner set it, and a worker's pacing by its account's week
|
|
31
|
+
-- (open or no_new_tasks), since when and the lead that last changed it.
|
|
32
|
+
kind TEXT NOT NULL DEFAULT 'on_demand', pacing TEXT NOT NULL DEFAULT 'open',
|
|
33
|
+
pacing_since_ms INTEGER, pacing_lead REAL,
|
|
34
|
+
-- The account its status line reports from: an email, or agent:<id> when it names none.
|
|
35
|
+
usage_account TEXT
|
|
30
36
|
);
|
|
31
37
|
CREATE UNIQUE INDEX agents_principal_lobby ON agents(agent_id,lobby_id);
|
|
32
38
|
CREATE TABLE runtime_history (
|
|
@@ -159,7 +165,7 @@ var we=Object.defineProperty;var g=(b,e)=>we(b,"name",{value:e,configurable:!0})
|
|
|
159
165
|
lobby_id TEXT PRIMARY KEY REFERENCES lobbies(lobby_id),
|
|
160
166
|
agents TEXT NOT NULL DEFAULT '{}', updated_at TEXT NOT NULL,
|
|
161
167
|
visibility TEXT NOT NULL DEFAULT 'private' CHECK (visibility IN ('private','public')),
|
|
162
|
-
summary_model TEXT NOT NULL DEFAULT '${
|
|
168
|
+
summary_model TEXT NOT NULL DEFAULT '${Me[0]}',
|
|
163
169
|
summary_effort TEXT NOT NULL DEFAULT 'low' CHECK (summary_effort IN ('low','medium','high'))
|
|
164
170
|
);
|
|
165
171
|
CREATE TABLE board_sources (
|
|
@@ -279,8 +285,31 @@ var we=Object.defineProperty;var g=(b,e)=>we(b,"name",{value:e,configurable:!0})
|
|
|
279
285
|
CHECK ((caption IS NULL) <> (error IS NULL))
|
|
280
286
|
);
|
|
281
287
|
CREATE INDEX board_captions_lobby ON board_captions(lobby_id, created_ms);
|
|
282
|
-
|
|
283
|
-
`),this.run("INSERT INTO metadata(key,value) VALUES(?,?)","incarnation",O())}key(e){const t=e.includes("/")?T(e):{lobby_id:this.soleLobby(),agent_id:j.parse(e)};return D(t.lobby_id,t.agent_id)}agentScope(e){const t=T(e);return[t.lobby_id,t.agent_id]}requireLobby(e){const t=$.parse(e);return this.one("SELECT 1 FROM lobbies WHERE lobby_id=?",t)||d(404,"lobby_not_found","Unknown lobby"),t}createLobby(e){const t=Re.parse(e);return this.transaction(()=>(this.insertLobby(t,null),this.audit(null,"create_lobby",t),{lobby_id:t}))}insertLobby(e,t){this.one("SELECT 1 FROM lobbies WHERE lobby_id=? UNION ALL SELECT 1 FROM deleted_lobbies WHERE lobby_id=?",e,e)&&d(409,"lobby_exists","That lobby name is taken, or was used by a deleted lobby"),Number(this.one("SELECT COUNT(*) AS n FROM lobbies").n)>=this.maxLobbies&&d(409,"lobby_capacity","Lobby capacity reached"),this.run("INSERT INTO lobbies(lobby_id,owner_account,created_at) VALUES(?,?,?)",e,t,this.iso())}soleLobby(){const e=this.all("SELECT lobby_id FROM lobbies ORDER BY lobby_id LIMIT 2");return e.length===1?String(e[0].lobby_id):d(400,"lobby_required",e.length?"This broker has several lobbies: name one with ?lobby= (the CLI's --lobby).":"This broker has no lobby yet: create one with POST /v1/admin/lobbies (pocolobby lobby create NAME).")}hasLobby(e){return!!this.one("SELECT 1 FROM lobbies WHERE lobby_id=?",e)}lobbies(){return this.all("SELECT l.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||l.lobby_id) AS display_name,COUNT(a.agent_id) AS agent_count FROM lobbies l LEFT JOIN agents a ON a.lobby_id=l.lobby_id GROUP BY l.lobby_id ORDER BY l.lobby_id").map(e=>({lobby_id:String(e.lobby_id),display_name:e.display_name??null,agent_count:Number(e.agent_count)}))}room(e,t){return I.parse(t),this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",e,t)||(this.one("SELECT 1 FROM archived_rooms WHERE lobby_id=? AND room_id=?",e,t)&&d(409,"room_archived","This room is archived; the lobby owner can restore it"),d(404,"room_not_found","Unknown room")),{lobby_id:e,room_id:t,members:this.all("SELECT agent_id FROM room_members WHERE lobby_id=? AND room_id=? ORDER BY agent_id",e,t).map(s=>T(String(s.agent_id)).agent_id)}}roomManager(e,t){if(typeof e=="object")return this.ownedLobby(e.account_id,t??this.soleLobby());if(e==="owner")return this.requireLobby(t??this.soleLobby());const s=this.agent(e);return(!s.enabled||!s.manage_rooms)&&d(403,"room_management_forbidden","Room-management permission is required"),s.lobby_id}rooms(e,t){this.requireLobby(e);let s;return t!==void 0&&(s=this.agent(t),(!s.enabled||s.lobby_id!==e)&&d(403,"lobby_forbidden","Room discovery is limited to your lobby")),(!s||s.manage_rooms?this.all("SELECT room_id FROM rooms WHERE lobby_id=? ORDER BY room_id",e):this.all("SELECT room_id FROM room_members WHERE lobby_id=? AND agent_id=? ORDER BY room_id",e,this.key(t))).map(n=>this.room(e,String(n.room_id)))}createRoom(e,t,s){return I.parse(t),this.transaction(()=>{const i=this.roomManager(e,s);return this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",i,t)&&d(409,"room_exists","Room already exists"),this.one("SELECT 1 FROM archived_rooms WHERE lobby_id=? AND room_id=?",i,t)&&d(409,"room_archived","An archived room has this name; the lobby owner can restore it"),Number(this.one("SELECT COUNT(*) AS n FROM rooms WHERE lobby_id=?",i).n)>=this.maxRooms&&d(409,"room_capacity","Room capacity reached for this lobby"),this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",i,t),this.audit(e,"create_room",i,t),this.room(i,t)})}setRoomMembers(e,t,s,i){I.parse(t),s.length>100&&d(400,"member_limit","A room supports at most 100 members");const n=[...new Set(s.map(a=>j.parse(a)))];return this.transaction(()=>{const a=this.roomManager(e,i),r=this.room(a,t);for(const o of n)this.row(D(a,o));return this.applyMembers(r,n),this.audit(e,"set_room_members",a,t,{members:n}),this.room(a,t)})}changeRoomMembers(e,t,s,i){I.parse(t);const{add:n=[],remove:a=[]}=ot.parse(s);return this.transaction(()=>{const r=this.roomManager(e,i),o=this.room(r,t);for(const c of[...n,...a])this.row(D(r,c));const _=new Set([...o.members,...n]);for(const c of a)_.delete(c);return _.size>100&&d(400,"member_limit","A room supports at most 100 members"),this.applyMembers(o,[..._]),this.audit(e,"change_room_members",r,t,{add:n,remove:a}),this.room(r,t)})}deleteRoom(e,t,s){return I.parse(t),this.transaction(()=>{const i=this.roomManager(e,s);this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",i,t)||d(404,"room_not_found","Unknown room; it may already have been deleted");const n=this.room(i,t),a=this.holdRoomDeliveries(i,t,"room_deleted"),r=this.cancelRoomSchedules(i,t,"room_deleted");return this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=?",i,t),this.run("DELETE FROM rooms WHERE lobby_id=? AND room_id=?",i,t),this.run("DELETE FROM board_sources WHERE lobby_id=? AND room_id=?",i,t),this.eraseRoomMemory(i,t),this.run("DELETE FROM metadata WHERE key=?",`code_stats:${i}:${t}`),this.audit(e,"delete_room",i,t),{lobby_id:i,room_id:t,deleted:!0,former_members:n.members,held_deliveries:a,cancelled_schedules:r.map(o=>({schedule_id:String(o.id),from:T(String(o.sender)).agent_id}))}})}applyMembers(e,t){for(const s of e.members.filter(i=>!t.includes(i))){const i=D(e.lobby_id,s);this.holdRoomDeliveries(e.lobby_id,e.room_id,"room_membership_revoked",i),this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",e.lobby_id,e.room_id,i)}for(const s of t.filter(i=>!e.members.includes(i)))this.run("INSERT INTO room_members(lobby_id,room_id,agent_id,joined_sequence) VALUES(?,?,?,(SELECT COALESCE(MAX(sequence),0) FROM messages))",e.lobby_id,e.room_id,D(e.lobby_id,s))}holdRoomDeliveries(e,t,s,i){return this.run(`UPDATE deliveries SET held_reason=? WHERE ${i===void 0?"":"agent_id=? AND "}seen_at IS NULL AND (held_reason IS NULL OR held_reason='scheduled') AND message_id IN (SELECT id FROM messages WHERE lobby_id=? AND room_id=?)`,s,...i===void 0?[]:[i],e,t)}statements=new Map;statement(e){let t=this.statements.get(e);return t||(this.statements.size>=1e3&&this.statements.clear(),t=this.db.prepare(e),this.statements.set(e,t)),t}one(e,...t){return this.statement(e).get(...t)}all(e,...t){return this.statement(e).all(...t)}run(e,...t){try{return Number(this.statement(e).run(...t).changes)}catch(s){this.rethrowDatabaseError(s)}}rethrowDatabaseError(e){throw typeof e=="object"&&e!==null&&"errcode"in e&&typeof e.errcode=="number"&&(e.errcode&255)===13&&d(507,"storage_capacity","SQLite storage is full; no new operation was accepted"),e}transaction(e){try{this.db.exec("BEGIN IMMEDIATE");const t=e();return this.db.exec("COMMIT"),t}catch(t){if(this.db.isTransaction)try{this.db.exec("ROLLBACK")}catch{}this.rethrowDatabaseError(t)}}iso(){return new Date(this.clock()).toISOString()}row(e){return e=this.key(e),this.one("SELECT * FROM agents WHERE agent_id=?",e)??d(404,"agent_not_found","Unknown agent")}audit(e,t,s,i=null,n=null,a=null){const r=typeof e=="object"&&e!==null?e.account_id:null,o=typeof e=="string"&&e!=="owner"?this.key(e):null;this.run("INSERT INTO admin_events(agent_id,action,created_at,actor_kind,account_id,lobby_id,target,detail) VALUES(?,?,?,?,?,?,?,?)",o??a,t,this.iso(),r?"account":o?"agent":"operator",r,s,i,n===null?null:JSON.stringify(n))}get incarnation(){return String(this.one("SELECT value FROM metadata WHERE key='incarnation'").value)}close(){this.db.close()}provision(e){const t={...ct.parse(e),lobby_id:$.parse(e.lobby_id??this.soleLobby())},s=D(t.lobby_id,t.agent_id),i=We();return this.transaction(()=>{t.agent_id==="owner"&&d(400,"reserved_identity","owner is reserved for administration"),this.requireLobby(t.lobby_id),this.one("SELECT 1 FROM agents WHERE agent_id=?",s)&&d(409,"agent_exists","Agent already exists"),Number(this.one("SELECT COUNT(*) AS n FROM agents").n)>=this.maxAgents&&d(409,"agent_capacity","Agent capacity reached"),this.run("INSERT INTO agents(agent_id,token_hash,role,work_description,machine,screen,can_broadcast,allowed_recipients,probe_nonce,lobby_id,manage_rooms) VALUES(?,?,?,?,?,?,?,?,?,?,?)",s,k(i),t.role,t.work_description,t.machine,Number(t.screen),Number(t.can_broadcast),t.allowed_recipients===null?null:JSON.stringify(t.allowed_recipients),O(),t.lobby_id,Number(t.manage_rooms)),this.run("INSERT INTO checkpoints(agent_id) VALUES(?)",s),this.audit(null,"provision",t.lobby_id,t.agent_id,null,s)}),{agent:this.agent(s),token:i}}authenticate(e){const t=k(e),s=this.one("SELECT agent_id,token_hash,enabled FROM agents WHERE token_hash=?",t),i=s?.token_hash?String(s.token_hash):"0".repeat(64);return(!de(Buffer.from(t,"hex"),Buffer.from(i,"hex"))||!s||!s.enabled)&&d(401,"unauthorized","Invalid or revoked credential"),String(s.agent_id)}agent(e){e=this.key(e);const t=this.row(e),s=this.one("SELECT MAX(created_ms) AS t FROM messages WHERE sender=? AND schedule_id IS NULL",e)?.t,i=[t.last_agent_seen_at??"",s==null?"":new Date(Number(s)).toISOString()].reduce((a,r)=>r>a?r:a),n=this.one("SELECT COUNT(*) AS n, MAX(CASE WHEN attempts>=? AND next_attempt_at<=? THEN 1 ELSE 0 END) AS attention, COUNT(CASE WHEN notification_attempted_at>=? THEN 1 END) AS notified, MIN(CASE WHEN notification_attempted_at>=? THEN notification_attempted_at END) AS oldest_notified FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",this.retries.length,this.clock(),i,i,e);return{agent_id:T(e).agent_id,lobby_id:String(t.lobby_id),manage_rooms:!!t.manage_rooms,role:String(t.role),work_description:String(t.work_description),machine:String(t.machine),screen:!!t.screen,enabled:!!t.enabled,paused:!!t.paused,resume_at:t.resume_at_ms===null?null:new Date(Number(t.resume_at_ms)).toISOString(),can_broadcast:!!t.can_broadcast,allowed_recipients:t.allowed_recipients===null?null:JSON.parse(String(t.allowed_recipients)),runtime_id:t.runtime_id,generation:Number(t.generation),ready:!!t.ready,last_seen_at:t.last_seen_at,last_agent_seen_at:t.last_agent_seen_at,pending_count:Number(n.n),unseen_notified_count:Number(n.notified),oldest_unseen_notified_at:n.oldest_notified??null,notification_attention_required:!!n.attention||!!(t.runtime_id&&!t.ready&&Number(t.bootstrap_attempts)>=this.retries.length&&Number(t.bootstrap_next_at)<=this.clock()),last_message_at:this.lastMessageAt(e),owner_requests:Number(t.owner_requests)>0?{count:Number(t.owner_requests),text:String(t.owner_requests_text??""),set_at:String(t.owner_requests_at)}:null,status:t.status_text===null?null:{text:String(t.status_text),set_at:String(t.status_set_at)}}}lastMessageAt(e){const{lobby_id:t,agent_id:s}=T(e),i=this.one(`SELECT MAX(t) AS t FROM (
|
|
288
|
+
-- Each weekly window of each account in a lobby, as its agents' status lines report it: the
|
|
289
|
+
-- highest use reported in it, and when it was last reported. An account's week now is the
|
|
290
|
+
-- window reported last.
|
|
291
|
+
CREATE TABLE account_weeks (
|
|
292
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), account TEXT NOT NULL,
|
|
293
|
+
resets_ms INTEGER NOT NULL, used REAL NOT NULL, at_ms INTEGER NOT NULL,
|
|
294
|
+
PRIMARY KEY (lobby_id, account, resets_ms)
|
|
295
|
+
);
|
|
296
|
+
PRAGMA user_version=${M};
|
|
297
|
+
`),this.run("INSERT INTO metadata(key,value) VALUES(?,?)","incarnation",O())}upgradeSchema(){this.db.exec(`
|
|
298
|
+
ALTER TABLE agents ADD COLUMN kind TEXT NOT NULL DEFAULT 'on_demand';
|
|
299
|
+
ALTER TABLE agents ADD COLUMN pacing TEXT NOT NULL DEFAULT 'open';
|
|
300
|
+
ALTER TABLE agents ADD COLUMN pacing_since_ms INTEGER;
|
|
301
|
+
ALTER TABLE agents ADD COLUMN pacing_lead REAL;
|
|
302
|
+
ALTER TABLE agents ADD COLUMN usage_account TEXT;
|
|
303
|
+
-- Each weekly window of each account in a lobby, as its agents' status lines report it: the
|
|
304
|
+
-- highest use reported in it, and when it was last reported. An account's week now is the
|
|
305
|
+
-- window reported last.
|
|
306
|
+
CREATE TABLE account_weeks (
|
|
307
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), account TEXT NOT NULL,
|
|
308
|
+
resets_ms INTEGER NOT NULL, used REAL NOT NULL, at_ms INTEGER NOT NULL,
|
|
309
|
+
PRIMARY KEY (lobby_id, account, resets_ms)
|
|
310
|
+
);
|
|
311
|
+
PRAGMA user_version=${M};
|
|
312
|
+
`)}key(e){const t=e.includes("/")?T(e):{lobby_id:this.soleLobby(),agent_id:$.parse(e)};return D(t.lobby_id,t.agent_id)}agentScope(e){const t=T(e);return[t.lobby_id,t.agent_id]}requireLobby(e){const t=Y.parse(e);return this.one("SELECT 1 FROM lobbies WHERE lobby_id=?",t)||_(404,"lobby_not_found","Unknown lobby"),t}createLobby(e){const t=ye.parse(e);return this.transaction(()=>(this.insertLobby(t,null),this.audit(null,"create_lobby",t),{lobby_id:t}))}insertLobby(e,t){this.one("SELECT 1 FROM lobbies WHERE lobby_id=? UNION ALL SELECT 1 FROM deleted_lobbies WHERE lobby_id=?",e,e)&&_(409,"lobby_exists","That lobby name is taken, or was used by a deleted lobby"),Number(this.one("SELECT COUNT(*) AS n FROM lobbies").n)>=this.maxLobbies&&_(409,"lobby_capacity","Lobby capacity reached"),this.run("INSERT INTO lobbies(lobby_id,owner_account,created_at) VALUES(?,?,?)",e,t,this.iso())}soleLobby(){const e=this.all("SELECT lobby_id FROM lobbies ORDER BY lobby_id LIMIT 2");return e.length===1?String(e[0].lobby_id):_(400,"lobby_required",e.length?"This broker has several lobbies: name one with ?lobby= (the CLI's --lobby).":"This broker has no lobby yet: create one with POST /v1/admin/lobbies (pocolobby lobby create NAME).")}hasLobby(e){return!!this.one("SELECT 1 FROM lobbies WHERE lobby_id=?",e)}lobbies(){return this.all("SELECT l.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||l.lobby_id) AS display_name,COUNT(a.agent_id) AS agent_count FROM lobbies l LEFT JOIN agents a ON a.lobby_id=l.lobby_id GROUP BY l.lobby_id ORDER BY l.lobby_id").map(e=>({lobby_id:String(e.lobby_id),display_name:e.display_name??null,agent_count:Number(e.agent_count)}))}room(e,t){return I.parse(t),this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",e,t)||(this.one("SELECT 1 FROM archived_rooms WHERE lobby_id=? AND room_id=?",e,t)&&_(409,"room_archived","This room is archived; the lobby owner can restore it"),_(404,"room_not_found","Unknown room")),{lobby_id:e,room_id:t,members:this.all("SELECT agent_id FROM room_members WHERE lobby_id=? AND room_id=? ORDER BY agent_id",e,t).map(n=>T(String(n.agent_id)).agent_id)}}roomManager(e,t){if(typeof e=="object")return this.ownedLobby(e.account_id,t??this.soleLobby());if(e==="owner")return this.requireLobby(t??this.soleLobby());const n=this.agent(e);return(!n.enabled||!n.manage_rooms)&&_(403,"room_management_forbidden","Room-management permission is required"),n.lobby_id}rooms(e,t){this.requireLobby(e);let n;return t!==void 0&&(n=this.agent(t),(!n.enabled||n.lobby_id!==e)&&_(403,"lobby_forbidden","Room discovery is limited to your lobby")),(!n||n.manage_rooms?this.all("SELECT room_id FROM rooms WHERE lobby_id=? ORDER BY room_id",e):this.all("SELECT room_id FROM room_members WHERE lobby_id=? AND agent_id=? ORDER BY room_id",e,this.key(t))).map(i=>this.room(e,String(i.room_id)))}createRoom(e,t,n){return I.parse(t),this.transaction(()=>{const s=this.roomManager(e,n);return this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",s,t)&&_(409,"room_exists","Room already exists"),this.one("SELECT 1 FROM archived_rooms WHERE lobby_id=? AND room_id=?",s,t)&&_(409,"room_archived","An archived room has this name; the lobby owner can restore it"),Number(this.one("SELECT COUNT(*) AS n FROM rooms WHERE lobby_id=?",s).n)>=this.maxRooms&&_(409,"room_capacity","Room capacity reached for this lobby"),this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",s,t),this.audit(e,"create_room",s,t),this.room(s,t)})}setRoomMembers(e,t,n,s){I.parse(t),n.length>100&&_(400,"member_limit","A room supports at most 100 members");const i=[...new Set(n.map(a=>$.parse(a)))];return this.transaction(()=>{const a=this.roomManager(e,s),r=this.room(a,t);for(const o of i)this.row(D(a,o));return this.applyMembers(r,i),this.audit(e,"set_room_members",a,t,{members:i}),this.room(a,t)})}changeRoomMembers(e,t,n,s){I.parse(t);const{add:i=[],remove:a=[]}=Et.parse(n);return this.transaction(()=>{const r=this.roomManager(e,s),o=this.room(r,t);for(const d of[...i,...a])this.row(D(r,d));const c=new Set([...o.members,...i]);for(const d of a)c.delete(d);return c.size>100&&_(400,"member_limit","A room supports at most 100 members"),this.applyMembers(o,[...c]),this.audit(e,"change_room_members",r,t,{add:i,remove:a}),this.room(r,t)})}deleteRoom(e,t,n){return I.parse(t),this.transaction(()=>{const s=this.roomManager(e,n);this.one("SELECT 1 FROM rooms WHERE lobby_id=? AND room_id=?",s,t)||_(404,"room_not_found","Unknown room; it may already have been deleted");const i=this.room(s,t),a=this.holdRoomDeliveries(s,t,"room_deleted"),r=this.cancelRoomSchedules(s,t,"room_deleted");return this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=?",s,t),this.run("DELETE FROM rooms WHERE lobby_id=? AND room_id=?",s,t),this.run("DELETE FROM board_sources WHERE lobby_id=? AND room_id=?",s,t),this.eraseRoomMemory(s,t),this.run("DELETE FROM metadata WHERE key=?",`code_stats:${s}:${t}`),this.audit(e,"delete_room",s,t),{lobby_id:s,room_id:t,deleted:!0,former_members:i.members,held_deliveries:a,cancelled_schedules:r.map(o=>({schedule_id:String(o.id),from:T(String(o.sender)).agent_id}))}})}applyMembers(e,t){for(const n of e.members.filter(s=>!t.includes(s))){const s=D(e.lobby_id,n);this.holdRoomDeliveries(e.lobby_id,e.room_id,"room_membership_revoked",s),this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",e.lobby_id,e.room_id,s)}for(const n of t.filter(s=>!e.members.includes(s)))this.run("INSERT INTO room_members(lobby_id,room_id,agent_id,joined_sequence) VALUES(?,?,?,(SELECT COALESCE(MAX(sequence),0) FROM messages))",e.lobby_id,e.room_id,D(e.lobby_id,n))}holdRoomDeliveries(e,t,n,s){return this.run(`UPDATE deliveries SET held_reason=? WHERE ${s===void 0?"":"agent_id=? AND "}seen_at IS NULL AND (held_reason IS NULL OR held_reason='scheduled') AND message_id IN (SELECT id FROM messages WHERE lobby_id=? AND room_id=?)`,n,...s===void 0?[]:[s],e,t)}statements=new Map;statement(e){let t=this.statements.get(e);return t||(this.statements.size>=1e3&&this.statements.clear(),t=this.db.prepare(e),this.statements.set(e,t)),t}one(e,...t){return this.statement(e).get(...t)}all(e,...t){return this.statement(e).all(...t)}run(e,...t){try{return Number(this.statement(e).run(...t).changes)}catch(n){this.rethrowDatabaseError(n)}}rethrowDatabaseError(e){throw typeof e=="object"&&e!==null&&"errcode"in e&&typeof e.errcode=="number"&&(e.errcode&255)===13&&_(507,"storage_capacity","SQLite storage is full; no new operation was accepted"),e}transaction(e){try{this.db.exec("BEGIN IMMEDIATE");const t=e();return this.db.exec("COMMIT"),t}catch(t){if(this.db.isTransaction)try{this.db.exec("ROLLBACK")}catch{}this.rethrowDatabaseError(t)}}iso(){return new Date(this.clock()).toISOString()}row(e){return e=this.key(e),this.one("SELECT * FROM agents WHERE agent_id=?",e)??_(404,"agent_not_found","Unknown agent")}audit(e,t,n,s=null,i=null,a=null){const r=typeof e=="object"&&e!==null?e.account_id:null,o=typeof e=="string"&&e!=="owner"?this.key(e):null;this.run("INSERT INTO admin_events(agent_id,action,created_at,actor_kind,account_id,lobby_id,target,detail) VALUES(?,?,?,?,?,?,?,?)",o??a,t,this.iso(),r?"account":o?"agent":"operator",r,n,s,i===null?null:JSON.stringify(i))}get incarnation(){return String(this.one("SELECT value FROM metadata WHERE key='incarnation'").value)}close(){this.db.close()}provision(e){const t={...ut.parse(e),lobby_id:Y.parse(e.lobby_id??this.soleLobby())},n=D(t.lobby_id,t.agent_id),s=Be();return this.transaction(()=>{t.agent_id==="owner"&&_(400,"reserved_identity","owner is reserved for administration"),this.requireLobby(t.lobby_id),this.one("SELECT 1 FROM agents WHERE agent_id=?",n)&&_(409,"agent_exists","Agent already exists"),Number(this.one("SELECT COUNT(*) AS n FROM agents").n)>=this.maxAgents&&_(409,"agent_capacity","Agent capacity reached"),this.run("INSERT INTO agents(agent_id,token_hash,role,work_description,machine,screen,kind,can_broadcast,allowed_recipients,probe_nonce,lobby_id,manage_rooms) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",n,H(s),t.role,t.work_description,t.machine,Number(t.screen),t.kind,Number(t.can_broadcast),t.allowed_recipients===null?null:JSON.stringify(t.allowed_recipients),O(),t.lobby_id,Number(t.manage_rooms)),this.run("INSERT INTO checkpoints(agent_id) VALUES(?)",n),this.audit(null,"provision",t.lobby_id,t.agent_id,null,n)}),{agent:this.agent(n),token:s}}authenticate(e){const t=H(e),n=this.one("SELECT agent_id,token_hash,enabled FROM agents WHERE token_hash=?",t),s=n?.token_hash?String(n.token_hash):"0".repeat(64);return(!Ee(Buffer.from(t,"hex"),Buffer.from(s,"hex"))||!n||!n.enabled)&&_(401,"unauthorized","Invalid or revoked credential"),String(n.agent_id)}agent(e){e=this.key(e);const t=this.row(e),n=this.one("SELECT MAX(created_ms) AS t FROM messages WHERE sender=? AND schedule_id IS NULL",e)?.t,s=[t.last_agent_seen_at??"",n==null?"":new Date(Number(n)).toISOString()].reduce((a,r)=>r>a?r:a),i=this.one("SELECT COUNT(*) AS n, MAX(CASE WHEN attempts>=? AND next_attempt_at<=? THEN 1 ELSE 0 END) AS attention, COUNT(CASE WHEN notification_attempted_at>=? THEN 1 END) AS notified, MIN(CASE WHEN notification_attempted_at>=? THEN notification_attempted_at END) AS oldest_notified FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",this.retries.length,this.clock(),s,s,e);return{agent_id:T(e).agent_id,lobby_id:String(t.lobby_id),manage_rooms:!!t.manage_rooms,role:String(t.role),work_description:String(t.work_description),machine:String(t.machine),screen:!!t.screen,kind:t.kind,pacing:J(t)&&this.lobbyPacing(String(t.lobby_id)).mode!=="off"?{state:t.pacing,since:t.pacing_since_ms===null?null:new Date(Number(t.pacing_since_ms)).toISOString(),lead:ke(t)}:null,enabled:!!t.enabled,paused:!!t.paused,resume_at:t.resume_at_ms===null?null:new Date(Number(t.resume_at_ms)).toISOString(),can_broadcast:!!t.can_broadcast,allowed_recipients:t.allowed_recipients===null?null:JSON.parse(String(t.allowed_recipients)),runtime_id:t.runtime_id,generation:Number(t.generation),ready:!!t.ready,last_seen_at:t.last_seen_at,last_agent_seen_at:t.last_agent_seen_at,pending_count:Number(i.n),unseen_notified_count:Number(i.notified),oldest_unseen_notified_at:i.oldest_notified??null,notification_attention_required:!!i.attention||!!(t.runtime_id&&!t.ready&&Number(t.bootstrap_attempts)>=this.retries.length&&Number(t.bootstrap_next_at)<=this.clock()),last_message_at:this.lastMessageAt(e),owner_requests:Number(t.owner_requests)>0?{count:Number(t.owner_requests),text:String(t.owner_requests_text??""),set_at:String(t.owner_requests_at)}:null,status:t.status_text===null?null:{text:String(t.status_text),set_at:String(t.status_set_at)}}}lastMessageAt(e){const{lobby_id:t,agent_id:n}=T(e),s=this.one(`SELECT MAX(t) AS t FROM (
|
|
284
313
|
SELECT MAX(created_ms) AS t FROM messages WHERE sender=? AND schedule_id IS NULL
|
|
285
314
|
UNION ALL
|
|
286
315
|
SELECT * FROM (SELECT created_ms FROM messages WHERE lobby_id=? AND target=?
|
|
@@ -288,16 +317,16 @@ var we=Object.defineProperty;var g=(b,e)=>we(b,"name",{value:e,configurable:!0})
|
|
|
288
317
|
UNION ALL
|
|
289
318
|
SELECT * FROM (SELECT released_ms FROM messages WHERE lobby_id=? AND released_ms IS NOT NULL
|
|
290
319
|
AND target=? AND room_id IS NULL AND cancelled_at IS NULL ORDER BY released_ms DESC LIMIT 1)
|
|
291
|
-
)`,e,t,s,t,s);return i?.t===null||i?.t===void 0?null:new Date(Number(i.t)).toISOString()}permissions(e){const t=this.row(e);return{manage_rooms:!!t.manage_rooms,can_broadcast:!!t.can_broadcast}}setOwnerRequests(e,t,s){e=this.key(e);const i=lt.parse(s);return this.transaction(()=>{this.assertRuntime(e,t);const n=i.count===0,a=n?null:i.text.trim(),r=this.row(e),o=Number(r.owner_requests)>0&&r.owner_requests_text===a;return this.run("UPDATE agents SET owner_requests=?,owner_requests_text=?,owner_requests_at=? WHERE agent_id=?",i.count,a,n?null:o?String(r.owner_requests_at):this.iso(),e),this.agent(e).owner_requests})}setStatus(e,t,s){e=this.key(e);const i=mt.parse(s).text.trim()||null;return this.transaction(()=>{this.assertRuntime(e,t);const n=this.row(e);return this.run("UPDATE agents SET status_text=?,status_set_at=? WHERE agent_id=?",i,i===null?null:n.status_text===i?String(n.status_set_at):this.iso(),e),this.agent(e).status})}agents(e){return(e===void 0?this.all("SELECT agent_id FROM agents ORDER BY agent_id"):this.all("SELECT agent_id FROM agents WHERE lobby_id=? ORDER BY agent_id",this.requireLobby(e))).map(s=>this.agent(String(s.agent_id)))}updateAgent(e,t){e=this.key(e);const s=ut.parse(t);return this.transaction(()=>{const i=this.row(e);for(const n of["role","work_description","machine","screen","can_broadcast","manage_rooms","allowed_recipients","enabled","paused"]){if(s[n]===void 0)continue;const a=s[n];this.run(`UPDATE agents SET ${n}=? WHERE agent_id=?`,n==="allowed_recipients"?a===null?null:JSON.stringify(a):typeof a=="boolean"?Number(a):String(a),e)}s.enabled===!1&&i.enabled&&this.resetBinding(e),s.paused!==void 0&&this.run("UPDATE agents SET resume_at_ms=NULL WHERE agent_id=?",e),s.paused===!1&&i.paused&&this.restartNotifications(e),s.role!==void 0&&s.role!==i.role&&this.run("UPDATE agents SET ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=? WHERE agent_id=?",O(),this.clock(),e),this.audit(null,"update_agent",...this.agentScope(e),Object.keys(s),e)}),this.agent(e)}resetBinding(e){e=this.key(e),this.row(e),this.run("UPDATE agents SET runtime_id=NULL,generation=generation+1,ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=?,owner_requests=0,owner_requests_text=NULL,owner_requests_at=NULL,status_text=NULL,status_set_at=NULL WHERE agent_id=?",O(),this.clock(),e),this.resetDeliveryAttempts(e)}resetDeliveryAttempts(e){this.run("UPDATE deliveries SET attempts=0,next_attempt_at=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",this.clock(),e)}bindRuntime(e,t,s){const i=Number(this.one("SELECT COALESCE(MAX(sequence),0) AS n FROM messages").n);this.run("UPDATE agents SET runtime_id=?,generation=?,ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=?,backlog_sequence=?,backlog_at=? WHERE agent_id=?",t,s,O(),this.clock(),i,this.clock(),e)}rotateToken(e){e=this.key(e);const t=We();return this.transaction(()=>{this.resetBinding(e),this.run("UPDATE agents SET token_hash=? WHERE agent_id=?",k(t),e),this.audit(null,"rotate_token",...this.agentScope(e),null,e)}),{token:t}}resetRuntime(e){e=this.key(e),this.transaction(()=>{this.resetBinding(e),this.audit(null,"reset_runtime",...this.agentScope(e),null,e)})}retryNotifications(e){e=this.key(e),this.transaction(()=>{this.row(e),this.restartNotifications(e),this.audit(null,"retry_notifications",...this.agentScope(e),null,e)})}restartNotifications(e){this.resetDeliveryAttempts(e),this.run("UPDATE agents SET bootstrap_attempts=0,bootstrap_next_at=? WHERE agent_id=?",this.clock(),e)}restAgents(e,t,s,i){const n=[...new Set(t.map(a=>j.parse(a)))];return this.transaction(()=>{const a=this.roomManager(e),r=this.key(e),o=s&&i!==void 0?Date.parse(i):null;o!==null&&o<=this.clock()&&d(400,"resume_at_past","resume_at must be in the future");const _=o===null?null:new Date(o).toISOString();for(const c of n){const l=D(a,c),m=this.row(l);m.enabled||d(409,"agent_disabled",`${c} is disabled`),s&&l===r&&d(400,"cannot_rest_self","A coordinator cannot rest itself: nothing could resume it");const N=!!m.paused,R=m.resume_at_ms===null?null:Number(m.resume_at_ms);N===s&&R===o||(this.run("UPDATE agents SET paused=?,resume_at_ms=? WHERE agent_id=?",Number(s),o,l),N&&!s&&this.restartNotifications(l),this.audit(e,s?"rest_agent":"resume_agent",a,c,_===null?null:{resume_at:_}))}return{agents:n.map(c=>({agent_id:c,paused:s,resume_at:_}))}})}resumeDue(){const e=this.all("SELECT agent_id,resume_at_ms FROM agents WHERE paused=1 AND resume_at_ms IS NOT NULL AND resume_at_ms<=? LIMIT 100",this.clock());if(!e.length)return 0;let t=0;return this.transaction(()=>{for(const s of e){const i=String(s.agent_id),n=Number(s.resume_at_ms);if(!this.one("SELECT 1 FROM agents WHERE agent_id=? AND paused=1 AND resume_at_ms=?",i,n))continue;this.run("UPDATE agents SET paused=0,resume_at_ms=NULL WHERE agent_id=?",i),this.restartNotifications(i);const[a,r]=this.agentScope(i);this.audit(null,"resume_agent",a,r,{timer:new Date(n).toISOString()}),t++}}),t}claimRuntime(e,t,s){return e=this.key(e),this.transaction(()=>{let i=this.row(e);return i.enabled||d(403,"agent_disabled","Agent is disabled"),s!==void 0?(i.runtime_id!==t||Number(i.generation)!==s)&&d(409,"stale_runtime","Runtime no longer owns this agent"):i.runtime_id===t?(this.bindRuntime(e,t,Number(i.generation)+1),this.resetDeliveryAttempts(e),this.audit(e,"resume_runtime",...this.agentScope(e)),i=this.row(e)):this.one("SELECT 1 FROM runtime_history WHERE agent_id=? AND runtime_id=?",e,t)?d(409,"stale_runtime","A retired runtime cannot reclaim this identity"):i.runtime_id?d(409,"runtime_conflict","Another runtime owns this agent; owner replacement is required"):(this.bindRuntime(e,t,Math.max(Number(i.generation),1)),this.run("INSERT INTO runtime_history(agent_id,runtime_id) VALUES(?,?)",e,t),i=this.row(e)),{runtime_id:t,generation:Number(i.generation),incarnation:this.incarnation,probe_nonce:String(i.probe_nonce),ready:!!i.ready}})}assertRuntime(e,t){e=this.key(e);const s=this.row(e);(!s.enabled||s.runtime_id!==t.runtime_id||s.generation!==t.generation||t.incarnation!==this.incarnation)&&d(409,"stale_runtime","Runtime no longer owns this agent")}ready(e,t,s){e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t),(this.row(e).probe_nonce!==s.probe_nonce||this.readCheckpoint(e).revision!==s.context_revision)&&d(409,"bootstrap_conflict","Bootstrap context or channel probe is stale"),this.run("UPDATE agents SET ready=1,last_agent_seen_at=? WHERE agent_id=?",this.iso(),e)})}heartbeat(e,t){e=this.key(e),this.assertRuntime(e,t),this.run("UPDATE agents SET last_seen_at=? WHERE agent_id=?",this.iso(),e)}deliveryFacts(e){const t=e.held_reason;return t===W||t===Ie?{state:t,held_reason:null}:t?{state:"held",held_reason:t}:{state:e.seen_at?"seen":"pending",held_reason:null}}message(e,t=!1){const s={id:String(e.id),lobby_id:String(e.lobby_id),room_id:e.room_id,sequence:Number(e.sequence),from:T(String(e.sender)).agent_id,to:String(e.target),priority:e.priority,content:String(e.content),correlation_id:e.correlation_id,reply_to:e.reply_to,created_at:String(e.created_at),kind:e.kind??null,reply_expected:e.reply_expected===null?null:!!e.reply_expected,...e.details===null?{}:JSON.parse(String(e.details)),...re(e)};e.notify_when_seen&&(s.notify_when_seen=!0),e.deliver_at_ms!==null&&(s.deliver_at=new Date(Number(e.deliver_at_ms)).toISOString(),s.delivered_at=e.released_ms===null?null:new Date(Number(e.released_ms)).toISOString(),s.late=!!e.late),e.schedule_id!==null&&(s.schedule_id=String(e.schedule_id)),e.cancelled_at!==null&&(s.cancelled_at=String(e.cancelled_at));const i=this.messageAttachments(String(e.id));if(i.length&&(s.attachments=i),"seen_at"in e&&(!("delivery_recipient_id"in e)||e.delivery_recipient_id!==null)){const n=this.deliveryFacts(e);if(s.agent_seen_at=e.seen_at,s.receipt_seq=e.receipt_seq===null?null:Number(e.receipt_seq),s.delivery_state=n.state,s.held_reason=n.held_reason,n.held_reason&&!t){s.content=`[Held: ${n.held_reason.replaceAll("_"," ")}]`,delete s.attachments;for(const a of fe)delete s[a]}}return s}attachmentInfo(e){return{id:String(e.id),filename:String(e.filename),mime_type:String(e.mime_type),size:Number(e.size),sha256:String(e.sha256),...e.expired_at===null?{}:{expired_at:String(e.expired_at)}}}messageAttachments(e){return this.all("SELECT a.* FROM message_attachments m JOIN attachments a ON a.id=m.attachment_id WHERE m.message_id=? ORDER BY m.position",e).map(t=>this.attachmentInfo(t))}compactAttachments(e){return this.messageAttachments(e).map(({id:t,filename:s,mime_type:i,size:n})=>({id:t,filename:s,mime_type:i,size:n}))}header(e){return{id:String(e.id),from:T(String(e.sender)).agent_id,to:String(e.target),...B(e.details),room_id:e.room_id,priority:e.priority,created_at:String(e.created_at),kind:e.kind??null,...re(e),...(()=>{const t=this.compactAttachments(String(e.id));return t.length?{attachments:t}:{}})()}}recipientDeliveries(e){const t=new Map;if(!e.length)return t;for(const s of this.all("SELECT message_id,agent_id,seen_at,held_reason,notification_attempted_at FROM deliveries WHERE message_id IN (SELECT value FROM json_each(?)) ORDER BY agent_id",JSON.stringify(e))){const i=t.get(String(s.message_id))??[],n=this.deliveryFacts(s);i.push({agent_id:T(String(s.agent_id)).agent_id,delivery_state:n.state,notification_attempted_at:s.notification_attempted_at,agent_seen_at:s.seen_at,held_reason:n.held_reason}),t.set(String(s.message_id),i)}return t}stored(e,t){const s=this.recipientDeliveries([String(e.id)]).get(String(e.id))??[],i=e.reply_to&&!this.one("SELECT 1 FROM messages WHERE id=?",e.reply_to);return{status:"stored",message:this.message(e),recipients:s.map(n=>n.agent_id),deliveries:s,duplicate:t,...i?{warning:xt}:{}}}send(e,t,s){const i=Se.parse(t),n=e==="owner";e=n?D(this.requireLobby(s??this.soleLobby()),"owner"):this.key(e);const a=T(e).lobby_id;return this.transaction(()=>{const r=n?null:this.agent(e);r&&!r.enabled&&d(403,"agent_disabled","Sender is disabled");const o=k(JSON.stringify(Z(i))),_=this.one("SELECT * FROM messages WHERE sender=? AND request_id=?",e,i.client_request_id);if(_)return _.fingerprint!==o&&d(409,"idempotency_conflict",`This client_request_id already stored message ${String(_.id)} at ${String(_.created_at)}, with different arguments. Choose a new client_request_id for a new message`,{original_message_id:_.id,original_created_at:_.created_at}),this.stored(_,!0);i.client_request_id.startsWith(H)&&d(400,"reserved_request_id",`Request IDs starting with ${H} are reserved`),ie(i.content),n&&i.notify_when_seen&&d(400,"receipts_unavailable","The owner has no event stream for read receipts; read history instead"),n&&i.attachments&&d(400,"attachments_unavailable","Only agents attach files: their bridge uploads them before the send");const c=this.clock();let l=null;return i.deliver_at!==void 0&&(l=Date.parse(i.deliver_at),(l<c-ye||l>c+Q)&&d(400,"deliver_at_out_of_range",Fe(c)),l>c&&this.undeliveredCount(e)>=this.maxScheduled&&d(409,"schedule_capacity",`At most ${this.maxScheduled} undelivered scheduled messages per sender`)),this.accept(e,a,r,i,{fingerprint:o,now:c,deliverAt:l})})}undeliveredCount(e){return Number(this.one("SELECT COUNT(*) AS n FROM messages INDEXED BY messages_due WHERE sender=? AND deliver_at_ms IS NOT NULL AND released_ms IS NULL AND cancelled_at IS NULL",e).n)}recipients(e,t,s,i){const n=s===null,a=g(c=>s?.allowed_recipients==null||s.allowed_recipients.includes(c),"allowed");if(i.room!==void 0)return this.room(t,i.room),!n&&!this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",t,i.room,e)&&d(403,"room_membership_required","Room messaging requires membership"),Array.isArray(i.to)?this.namedMembers(e,t,i.room,i.to,a):this.all("SELECT a.agent_id FROM room_members r JOIN agents a ON a.agent_id=r.agent_id WHERE r.lobby_id=? AND r.room_id=? AND a.enabled=1 AND a.agent_id<>? ORDER BY a.agent_id",t,i.room,e).map(c=>String(c.agent_id)).filter(c=>a(T(c).agent_id));if(i.to==="*")return s&&!s.can_broadcast&&d(403,"broadcast_forbidden","Broadcast permission is required"),this.all("SELECT agent_id FROM agents WHERE lobby_id=? AND enabled=1 AND agent_id<>? ORDER BY agent_id",t,e).map(c=>String(c.agent_id)).filter(c=>a(T(c).agent_id));const r=i.to;a(r)||d(403,"recipient_forbidden","Recipient is outside the allowed peer set");const o=D(t,r);return this.row(o).enabled||d(409,"recipient_disabled","Recipient is disabled"),[o]}namedMembers(e,t,s,i,n){const a=[],r=i.map(o=>{const _=D(t,o);_===e&&d(400,"recipient_is_sender","A room message never reaches its sender; leave yourself out of to");const c=this.one("SELECT a.enabled FROM room_members r JOIN agents a ON a.agent_id=r.agent_id WHERE r.lobby_id=? AND r.room_id=? AND r.agent_id=?",t,s,_);return c?c.enabled?n(o)||d(403,"recipient_forbidden",`${o} is outside the allowed peer set`):d(409,"recipient_disabled",`${o} is disabled`):a.push(o),_});return a.length&&d(409,"not_room_member",`${a.join(", ")} ${a.length===1?"is not a member":"are not members"} of room ${s}, so nothing was sent. Leave ${a.length===1?"that name":"them"} out, or message ${a.length===1?"it":"them"} directly; list_rooms shows the members`,{not_members:a}),r}admitSupersedes(e,t,s,i,n,a){ke.test(s)||d(400,"supersedes_not_message_id",`supersedes takes a message's full ID, a UUID such as 695912ea-d6cd-464d-8895-26e3d2af1c74, not ${JSON.stringify(s.slice(0,40))}. Nothing was sent`),n!==null&&n>a&&d(400,"supersedes_scheduled","A correction goes out now: send it without deliver_at, or change a scheduled message with update_scheduled. Nothing was sent");const r=this.one("SELECT * FROM messages WHERE id=? AND lobby_id=? AND sender=?",s,t,e);r||d(404,"supersedes_not_found","supersedes names no stored message of yours. One gone after its day cannot be marked: send the correction without supersedes. Nothing was sent"),r.cancelled_at!==null&&d(409,"supersedes_cancelled","That message was cancelled and reached no one. Nothing was sent"),r.deliver_at_ms!==null&&r.released_ms===null&&d(409,"supersedes_scheduled","That message has not been delivered yet: change it with update_scheduled or cancel it. Nothing was sent"),r.superseded_by!==null&&d(409,"already_superseded",`That message was already superseded by ${String(r.superseded_by)}; supersede that one instead. Nothing was sent`);const o=new Set(i),_=this.all("SELECT agent_id FROM deliveries WHERE message_id=? AND held_reason IS NULL ORDER BY agent_id",s).map(c=>String(c.agent_id)).filter(c=>!o.has(c));_.length&&d(409,"supersedes_audience",`A correction reaches everyone the earlier message reached; add ${_.map(c=>T(c).agent_id).join(", ")}. Nothing was sent`)}admitBytes(e,t,s){const i=g((n,...a)=>Number(this.one(n,...a).n)+s>this.maxStored,"over");i("SELECT COALESCE(SUM(byte_size),0) AS n FROM messages")&&i("SELECT COALESCE(SUM(byte_size),0) AS n FROM messages WHERE lobby_id=?",e)&&d(507,"storage_capacity","This lobby's stored message content capacity is reached");for(const n of t)Number(this.one("SELECT COALESCE(SUM(m.byte_size),0) AS n FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.seen_at IS NULL AND (d.held_reason IS NULL OR d.held_reason='scheduled')",n).n)+s>this.maxPending&&d(409,"inbox_capacity",`Recipient ${n} has insufficient inbox capacity`)}accept(e,t,s,i,n){const{now:a,deliverAt:r}=n,o=n.scheduleId!==void 0,_=this.recipients(e,t,s,i);i.reply_to&&!ke.test(i.reply_to)&&d(400,"reply_to_not_message_id",`reply_to takes a message's full ID, a UUID such as 695912ea-d6cd-464d-8895-26e3d2af1c74, not ${JSON.stringify(i.reply_to.slice(0,40))}. Nothing was sent; send again with the full ID and the same client_request_id`),i.supersedes!==void 0&&this.admitSupersedes(e,t,i.supersedes,_,r,a),i.reply_to&&this.one("SELECT 1 FROM messages WHERE id=?",i.reply_to)&&!this.one(`SELECT 1 FROM messages m WHERE m.id=?1 AND m.lobby_id=?2 AND (?3=1 OR m.sender=?4 OR EXISTS(SELECT 1 FROM deliveries d WHERE d.message_id=m.id AND d.agent_id=?4 AND d.held_reason IS NULL) OR ${oe("m","?4")})`,i.reply_to,t,+(s===null),e)&&d(404,"reply_not_found","Reply target is not accessible"),!o&&Number(this.one("SELECT COUNT(*) AS n FROM messages WHERE sender=? AND created_ms>? AND schedule_id IS NULL",e,a-6e4).n)>=60&&d(429,"send_rate_limit","At most 60 new messages per sender per minute");const c=Te(i.to),l=c?{...ee(i),addressed_to:c}:ee(i),m=Buffer.byteLength(i.content)+(l?Buffer.byteLength(JSON.stringify(l)):0);this.admitBytes(t,_,m);const N=this.resolveAttachments(e,t,i.attachments,a),R=!o&&r!==null&&r>a,E=r===null||R?null:a,u=o&&a-r>this.lateAfter,y=i.kind??null,U=He(i,e),A=O();this.run("INSERT INTO messages(id,sender,request_id,fingerprint,target,priority,content,correlation_id,reply_to,created_at,created_ms,byte_size,lobby_id,room_id,kind,reply_expected,details,notify_when_seen,deliver_at_ms,released_ms,late,schedule_id,supersedes) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",A,e,i.client_request_id,n.fingerprint,Array.isArray(i.to)?"*":i.to,i.priority,i.content,i.correlation_id??null,i.reply_to??null,new Date(a).toISOString(),a,m,t,i.room??null,y,U===null?null:Number(U),l?JSON.stringify(l):null,Number(i.notify_when_seen??!1),r,E,Number(u),n.scheduleId??null,i.supersedes??null),i.supersedes!==void 0&&this.run("UPDATE messages SET superseded_by=? WHERE id=?",A,i.supersedes);for(const h of _)this.run("INSERT INTO deliveries(message_id,agent_id,next_attempt_at,held_reason) VALUES(?,?,?,?)",A,h,R?r:a+(i.priority==="info"?this.infoDelay:0),R?W:null);return this.linkAttachments(e,N,"message",A,a),this.stored(this.one("SELECT * FROM messages WHERE id=?",A),!1)}lobbyAttachmentBytes(e){return Number(this.one("SELECT COALESCE(SUM(size),0) AS n FROM attachment_blobs WHERE sha256 IN (SELECT sha256 FROM attachments WHERE lobby_id=?1 AND expired_at IS NULL UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1)",e).n)}lobbyHolds(e,t){return!!this.one("SELECT 1 FROM attachments WHERE lobby_id=?1 AND sha256=?2 AND expired_at IS NULL UNION ALL SELECT 1 FROM attachment_uploads WHERE lobby_id=?1 AND sha256=?2 LIMIT 1",e,t)}kindLimit(e){return e==="image"?this.maxImageBytes:this.maxTextBytes}tooLarge(e,t,s){return d(413,"attachment_too_large",`${e} is ${v(t)}; this broker accepts ${s} attachments up to ${v(this.kindLimit(s))} (${se[s]}). Nothing was stored.`)}quotaFull(e,t){return d(507,"storage_capacity",`Attachment storage for lobby ${e} is full: ${v(this.lobbyAttachmentBytes(e))} of ${v(this.maxLobbyAttachments)} used and ${v(t)} more needed (${se.lobby}). Nothing was stored; ask the owner, or wait for retention to free space.`)}resolveAttachments(e,t,s,i){if(!s?.length)return[];for(const r of s)r.size>this.kindLimit(z(r.mime_type))&&this.tooLarge(r.filename,r.size,z(r.mime_type));const n=[],a=new Map;for(const r of s){const o=this.one("SELECT * FROM attachments WHERE uploader=? AND sha256=? AND filename=? AND expired_at IS NULL",e,r.sha256,r.filename),_=this.one("SELECT * FROM attachment_blobs WHERE sha256=?",r.sha256);if(!(o??this.one("SELECT 1 FROM attachment_uploads WHERE uploader=? AND sha256=?",e,r.sha256)??this.one("SELECT 1 FROM attachments WHERE uploader=? AND sha256=? AND expired_at IS NULL",e,r.sha256))||!_){a.set(r.sha256,r.size);continue}(Number(_.size)!==r.size||_.kind!==z(r.mime_type)||_.kind==="image"&&_.mime_type!==r.mime_type||o&&o.mime_type!==r.mime_type)&&d(400,"attachment_mismatch",`${r.filename} does not match the uploaded content with that SHA-256 (size or type differs). Nothing was stored.`),n.push(o?String(o.id):r)}if(a.size){const r=[...a].filter(([o])=>!this.lobbyHolds(t,o)).reduce((o,[,_])=>o+_,0);this.lobbyAttachmentBytes(t)+r>this.maxLobbyAttachments&&this.quotaFull(t,r),d(409,"attachment_upload_required",`Upload ${a.size} attached file${a.size===1?"":"s"} first, then repeat this request unchanged. Nothing was stored.`,{missing:[...a.keys()]})}return n.map(r=>{if(typeof r=="string")return r;const o=O();return this.run("INSERT INTO attachments(id,lobby_id,uploader,filename,mime_type,size,sha256,created_at,created_ms) VALUES(?,?,?,?,?,?,?,?,?)",o,t,e,r.filename,r.mime_type,r.size,r.sha256,new Date(i).toISOString(),i),o})}linkAttachments(e,t,s,i,n){t.forEach((a,r)=>{this.run(s==="message"?"INSERT INTO message_attachments(message_id,position,attachment_id) VALUES(?,?,?)":"INSERT INTO schedule_attachments(schedule_id,position,attachment_id) VALUES(?,?,?)",i,r,a),this.run("UPDATE attachments SET last_referenced_ms=MAX(COALESCE(last_referenced_ms,0),?) WHERE id=?",n,a),this.run("DELETE FROM attachment_uploads WHERE uploader=? AND sha256=(SELECT sha256 FROM attachments WHERE id=?)",e,a)})}attachmentsPath(){return this.attachmentsDirectory||(this.attachmentsDirectory=$e(w(Ve(),"pocolobby-attachments-"))),ue(this.attachmentsDirectory,{recursive:!0,mode:448}),this.attachmentsDirectory}blobPath(e){return w(this.attachmentsPath(),e)}uploadAttachment(e,t,s,i){e=this.key(e),/^[0-9a-f]{64}$/u.test(s)||d(400,"invalid_request","Name the upload by its lowercase hex SHA-256"),x("sha256").update(i).digest("hex")!==s&&d(400,"attachment_hash_mismatch","The uploaded bytes do not have that SHA-256");const n=it(i);if(!n)return d(415,"attachment_type","Attach PNG, JPEG, GIF or WebP images, or UTF-8 text documents");const a=n;i.length>this.kindLimit(a)&&this.tooLarge("The upload",i.length,a);const r=a==="image"?nt(i):"text/plain",o=this.clock();return this.transaction(()=>{this.assertRuntime(e,t);const _=String(this.row(e).lobby_id);!this.lobbyHolds(_,s)&&this.lobbyAttachmentBytes(_)+i.length>this.maxLobbyAttachments&&this.quotaFull(_,i.length);const c=this.blobPath(s);return V(c,{throwIfNoEntry:!1})?.size===i.length||(this.freeDisk(this.attachmentsPath())-i.length<this.diskReserve&&d(507,"storage_capacity",`The broker's disk is nearly full: storing ${v(i.length)} would leave less than the ${v(this.diskReserve)} reserved for the database (${se.reserve}). Nothing was stored; the owner must free or extend the volume.`),this.writeBlob(c,i)),this.run("INSERT OR IGNORE INTO attachment_blobs(sha256,size,kind,mime_type,created_at) VALUES(?,?,?,?,?)",s,i.length,a,r,new Date(o).toISOString()),this.run("INSERT INTO attachment_uploads(uploader,sha256,lobby_id,created_ms) VALUES(?,?,?,?) ON CONFLICT(uploader,sha256) DO UPDATE SET created_ms=excluded.created_ms",e,s,_,o),{sha256:s,size:i.length,kind:a,mime_type:r}})}writeBlob(e,t){const s=`${e}.${O()}.tmp`;try{const i=he(s,"wx",384);try{let n=0;for(;n<t.length;)n+=Je(i,t,n,t.length-n);qe(i)}finally{me(i)}Ye(s,e)}catch(i){try{K(s)}catch{}throw i.code==="ENOSPC"&&d(507,"storage_capacity","The broker's disk is full; nothing was stored"),i}}accessibleAttachment(e,t){e=this.key(e);const s=T(e).lobby_id;return this.one(`SELECT a.* FROM attachments a WHERE a.id=?1 AND a.lobby_id=?2 AND (a.uploader=?3 OR EXISTS (SELECT 1 FROM message_attachments m JOIN deliveries d ON d.message_id=m.message_id WHERE m.attachment_id=a.id AND d.agent_id=?3 AND d.held_reason IS NULL) OR EXISTS (SELECT 1 FROM message_attachments m JOIN messages g ON g.id=m.message_id WHERE m.attachment_id=a.id AND ${oe("g","?3")}))`,t,s,e)??d(404,"attachment_not_found","No attachment with that ID is available to you")}attachment(e,t){const s=this.accessibleAttachment(e,t);return{...this.attachmentInfo(s),from:T(String(s.uploader)).agent_id,created_at:String(s.created_at)}}attachmentFile(e,t){const s=this.accessibleAttachment(e,t),i=this.attachmentInfo(s),n=g(()=>d(410,"attachment_expired",`${i.filename} is no longer stored: content is kept for a limited time after its last message. Ask the sender to attach it again.`),"gone");i.expired_at&&n();const a=this.blobPath(i.sha256),r=V(a,{throwIfNoEntry:!1})?.size;return r===void 0?n():{info:i,path:a,size:r}}attachments(e,t={}){const s=this.requireLobby(e),i=t.cursor??0,n=t.limit??50,a=this.all("SELECT * FROM attachments WHERE lobby_id=? AND sequence>? ORDER BY sequence LIMIT ?",s,i,n+1),r=a.slice(0,n);return{items:r.map(o=>({...this.attachmentInfo(o),lobby_id:s,uploader:T(String(o.uploader)).agent_id,created_at:String(o.created_at),last_referenced_at:o.last_referenced_ms===null?null:new Date(Number(o.last_referenced_ms)).toISOString(),messages:this.all("SELECT message_id FROM message_attachments WHERE attachment_id=? ORDER BY rowid",String(o.id)).map(_=>String(_.message_id)),schedules:this.all("SELECT schedule_id FROM schedule_attachments WHERE attachment_id=? ORDER BY rowid",String(o.id)).map(_=>String(_.schedule_id))})),next_cursor:a.length>n?Number(r.at(-1).sequence):null}}attachmentUsage(e){const t=this.requireLobby(e),s=g(n=>Number(this.one(n,t).n),"count");let i=null;try{i={free_bytes:this.freeDisk(this.attachmentsPath()),reserve_bytes:this.diskReserve}}catch{}return{lobby_id:t,used_bytes:this.lobbyAttachmentBytes(t),quota_bytes:this.maxLobbyAttachments,files:s("SELECT COUNT(*) AS n FROM (SELECT sha256 FROM attachments WHERE lobby_id=?1 AND expired_at IS NULL UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1)"),attachments:s("SELECT COUNT(*) AS n FROM attachments WHERE lobby_id=? AND expired_at IS NULL"),expired:s("SELECT COUNT(*) AS n FROM attachments WHERE lobby_id=? AND expired_at IS NOT NULL"),pending_uploads:s("SELECT COUNT(*) AS n FROM attachment_uploads WHERE lobby_id=?"),limits:{max_image_bytes:this.maxImageBytes,max_text_bytes:this.maxTextBytes,retention_days:this.attachmentRetention/(1440*60*1e3),upload_grace_seconds:this.attachmentGrace/1e3},disk:i}}healthy(){this.one("SELECT 1")}pruneNotificationAttempts(e=this.clock()){return this.run("DELETE FROM notification_attempts WHERE created_at<? AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.last_notification_id=notification_attempts.id AND d.seen_at IS NULL)",new Date(e-1440*60*1e3).toISOString())}pruneMessages(){const e=this.clock()-Wt,t=new Date(e).toISOString(),s="SELECT id FROM messages WHERE (cancelled_at IS NULL AND COALESCE(released_ms,created_ms)<?1 AND (deliver_at_ms IS NULL OR released_ms IS NOT NULL)) OR cancelled_at<?2";return!this.one(`${s} LIMIT 1`,e,t)&&!this.one("SELECT 1 FROM schedules WHERE cancelled_at<? LIMIT 1",t)?0:this.transaction(()=>{for(const n of["seen_receipts","message_attachments","deliveries"])this.run(`DELETE FROM ${n} WHERE message_id IN (${s})`,e,t);const i=this.run(`DELETE FROM messages WHERE id IN (${s})`,e,t);return this.run("DELETE FROM schedule_attachments WHERE schedule_id IN (SELECT id FROM schedules WHERE cancelled_at<?)",t),this.run("DELETE FROM schedules WHERE cancelled_at<?",t),i})}pruneAdminEvents(){let e=0;for(const t of this.all("SELECT DISTINCT lobby_id FROM admin_events")){const s=t.lobby_id,i=this.one("SELECT sequence FROM admin_events WHERE lobby_id IS ? ORDER BY sequence DESC LIMIT 1 OFFSET ?",s,Mt);i&&(e+=this.transaction(()=>this.run("DELETE FROM admin_events WHERE lobby_id IS ? AND sequence<=?",s,Number(i.sequence))))}return e}collectAttachments(){const e=this.clock(),t=e-this.attachmentRetention,s=e-this.attachmentGrace,i=new Date(e-vt).toISOString(),n="expired_at<? AND NOT EXISTS (SELECT 1 FROM message_attachments m WHERE m.attachment_id=attachments.id) AND NOT EXISTS (SELECT 1 FROM schedule_attachments s WHERE s.attachment_id=attachments.id)",a="expired_at IS NULL AND COALESCE(last_referenced_ms,created_ms)<? AND NOT EXISTS (SELECT 1 FROM schedule_attachments sa JOIN schedules s ON s.id=sa.schedule_id WHERE sa.attachment_id=attachments.id AND s.cancelled_at IS NULL) AND NOT EXISTS (SELECT 1 FROM message_attachments ma JOIN messages m ON m.id=ma.message_id WHERE ma.attachment_id=attachments.id AND m.deliver_at_ms IS NOT NULL AND m.released_ms IS NULL AND m.cancelled_at IS NULL)",o=this.one(`SELECT 1 FROM attachments WHERE ${a} LIMIT 1`,t)??this.one("SELECT 1 FROM attachment_uploads WHERE created_ms<? LIMIT 1",s)??this.one(`SELECT 1 FROM attachments WHERE ${n} LIMIT 1`,i)?this.transaction(()=>{const m=this.all(`SELECT id FROM attachments WHERE ${a}`,t);for(const E of m)this.run("UPDATE attachments SET expired_at=? WHERE id=?",this.iso(),String(E.id));this.run(`DELETE FROM attachments WHERE ${n}`,i);const N=Number(this.one("SELECT COUNT(*) AS n FROM attachment_uploads WHERE created_ms<?",s).n);this.run("DELETE FROM attachment_uploads WHERE created_ms<?",s);const R=this.all("SELECT sha256 FROM attachment_blobs b WHERE NOT EXISTS (SELECT 1 FROM attachments a WHERE a.sha256=b.sha256 AND a.expired_at IS NULL) AND NOT EXISTS (SELECT 1 FROM attachment_uploads u WHERE u.sha256=b.sha256)").map(E=>String(E.sha256));for(const E of R)this.run("DELETE FROM attachment_blobs WHERE sha256=?",E);return{expired:m.length,uploads:N,unreferenced:R}}):{expired:0,uploads:0,unreferenced:[]};let _=0;if(!this.attachmentsDirectory)return{expired:o.expired,uploads:o.uploads,files:_};const c=this.attachmentsPath(),l=g(m=>/^[0-9a-f]{64}$/u.test(m)&&!!this.one("SELECT 1 FROM attachment_blobs WHERE sha256=?",m),"known");for(const m of Pe(c)){const N=w(c,m);try{const R=V(N),E=o.unreferenced.includes(m)||!l(m)&&Date.now()-R.mtimeMs>this.attachmentGrace;R.isFile()&&E&&(K(N),_++)}catch{}}return{expired:o.expired,uploads:o.uploads,files:_}}messageLines(e,t={}){t.mode!=="history"&&d(400,"headers_need_history","headers work in history mode only");const s=this.messages(e,t);return{items:s.items.map(Pt),next_cursor:s.next_cursor}}messages(e,t={}){e=this.key(e);const s=T(e);s.agent_id!=="owner"?this.row(e):this.requireLobby(s.lobby_id);const i=t.mode??"pending",n=t.cursor??0,a=t.limit??25;(!Number.isSafeInteger(n)||n<0||!Number.isSafeInteger(a)||a<1||a>100)&&d(400,"invalid_pagination","Use a nonnegative cursor and a limit between 1 and 100"),t.query!==void 0&&i!=="history"&&d(400,"query_needs_history","Search works in history mode only"),t.unanswered&&(i!=="history"||s.agent_id==="owner")&&d(400,"unanswered_needs_history","unanswered lists an agent's own history only"),t.awaiting&&(i!=="history"||s.agent_id==="owner")&&d(400,"awaiting_needs_history","awaiting lists an agent's own history only"),t.awaiting&&t.unanswered&&d(400,"awaiting_or_unanswered","Ask for awaiting or unanswered, one at a time"),t.ids!==void 0&&i!=="history"&&d(400,"ids_need_history","ids work in history mode only");const r=t.unanswered?" AND m.sender<>?2 AND m.sender<>(?4 || '/owner') AND d.agent_id=?2 AND d.held_reason IS NULL AND m.reply_expected=1 AND m.superseded_by IS NULL AND NOT EXISTS(SELECT 1 FROM messages r JOIN deliveries rd ON rd.message_id=r.id WHERE r.sender=?2 AND r.reply_to=m.id AND r.cancelled_at IS NULL AND (r.deliver_at_ms IS NULL OR r.released_ms IS NOT NULL) AND rd.agent_id=m.sender AND rd.held_reason IS NULL)":"",o=t.awaiting?" AND m.sender=?2 AND m.reply_expected=1 AND m.superseded_by IS NULL AND m.cancelled_at IS NULL AND (m.deliver_at_ms IS NULL OR m.released_ms IS NOT NULL) AND NOT EXISTS(SELECT 1 FROM messages r JOIN deliveries rd ON rd.message_id=r.id WHERE r.reply_to=m.id AND r.sender<>?2 AND r.cancelled_at IS NULL AND (r.deliver_at_ms IS NULL OR r.released_ms IS NOT NULL) AND rd.agent_id=?2 AND rd.held_reason IS NULL)":"",_=t.ids===void 0?null:JSON.stringify(t.ids.map(L=>L.toLowerCase())),c=g(L=>_===null?"":` AND EXISTS(SELECT 1 FROM json_each(?${L}) j WHERE m.id LIKE j.value || '%')`,"byId"),l=_===null?"":` OR ${oe("m","?2")}`,m=_===null?[]:[_],N=t.query===void 0?null:`%${t.query.replace(/[\\%_]/g,"\\$&")}%`,R="(m.content LIKE ?1 ESCAPE '\\' OR m.details LIKE ?1 ESCAPE '\\' OR EXISTS(SELECT 1 FROM message_attachments x JOIN attachments a ON a.id=x.attachment_id WHERE x.message_id=m.id AND a.filename LIKE ?1 ESCAPE '\\'))";let E;if(i==="history")E=s.agent_id==="owner"?this.all(`SELECT * FROM messages m WHERE m.lobby_id=?2 AND m.sequence>?3${N===null?"":` AND ${R}`}${c(5)} ORDER BY m.sequence LIMIT ?4`,N,s.lobby_id,n,a+1,...m):this.all(`SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason,d.agent_id AS delivery_recipient_id FROM messages m LEFT JOIN deliveries d ON d.message_id=m.id AND d.agent_id=?2 WHERE m.sequence>?3 AND m.lobby_id=?4 AND (m.sender=?2 OR (d.agent_id=?2 AND COALESCE(d.held_reason,'') NOT IN ('scheduled','cancelled') AND (m.deliver_at_ms IS NULL OR m.released_ms IS NOT NULL))${l})${N===null?"":` AND (m.sender=?2 OR d.held_reason IS NULL) AND ${R}`}${r}${o}${c(6)} ORDER BY m.sequence LIMIT ?5`,N,e,n,T(e).lobby_id,a+1,...m);else if(i==="reconciliation"){const L=Math.max(n,this.readCheckpoint(e).covered_receipt_seq);E=this.all("SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.receipt_seq>? ORDER BY d.receipt_seq LIMIT ?",e,L,a+1)}else E=this.all("SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.seen_at IS NULL AND d.held_reason IS NULL AND m.sequence>? ORDER BY m.sequence LIMIT ?",e,n,a+1);const u=E.slice(0,a),y=u.map(L=>this.message(L,t.includeHeldContent));if(i==="history"){const L=new Set(u.filter(S=>s.agent_id==="owner"||S.sender===e).map(S=>String(S.id))),p=this.recipientDeliveries([...L]);for(const S of y)L.has(S.id)&&(S.deliveries=p.get(S.id)??[])}let U=_t,A=0;for(const L of y){const p=Buffer.byteLength(JSON.stringify(L));if(A>0&&p>U)break;A++,U-=p}const h=E.length>A;return{items:y.slice(0,A),next_cursor:h?Number(u[A-1][i==="reconciliation"?"receipt_seq":"sequence"]):null}}acknowledgeInner(e,t){e=this.key(e),t.length>100&&d(400,"ack_limit","At most 100 receipts per operation");const s=[...new Set(t)],i=[];for(const a of s){const r=this.one("SELECT held_reason FROM deliveries WHERE agent_id=? AND message_id=?",e,a);!r&&this.one("SELECT 1 FROM messages WHERE id=?",a)&&d(403,"receipt_forbidden","A receipt can only name messages in your own inbox"),r||i.push(a),r?.held_reason===W&&d(409,"not_yet_delivered",`Message ${a} is scheduled and not yet delivered; acknowledge it once it arrives`)}let n=Number(this.row(e).receipt_seq);for(const a of s){const r=this.one("SELECT d.seen_at,d.held_reason,m.sender,m.notify_when_seen FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.message_id=?",e,a);if(!r||r.seen_at!==null||r.held_reason!==null)continue;const o=this.iso();this.run("UPDATE deliveries SET seen_at=?,receipt_seq=? WHERE agent_id=? AND message_id=?",o,++n,e,a);const _=String(r.sender);r.notify_when_seen&&_!==e&&T(_).agent_id!=="owner"&&this.run("INSERT OR IGNORE INTO seen_receipts(sender,message_id,agent_id,seen_at,seen_ms) VALUES(?,?,?,?,?)",_,a,e,o,this.clock())}return this.run("UPDATE agents SET receipt_seq=?,last_agent_seen_at=? WHERE agent_id=?",n,this.iso(),e),{snapshot:n,unknown:i}}acknowledge(e,t,s){return e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t);const{snapshot:i,unknown:n}=this.acknowledgeInner(e,s);return{receipt_snapshot:i,...this.unknownIds(e,n)}})}unknownIds(e,t){if(!t.length)return{};const s=t.filter(i=>this.one("SELECT 1 FROM notification_attempts WHERE id=? AND agent_id=?",i,e));return{unknown_message_ids:t,...s.length?{note:`${s.join(", ")} ${s.length===1?"is a notification_id":"are notification_ids"}, not message IDs: acknowledge the id of each message you have read, from the event's messages or get_messages.`}:{}}}checkpointView(e){return e=this.key(e),{checkpoint:this.readCheckpoint(e),receipt_snapshot:Number(this.row(e).receipt_seq)}}readCheckpoint(e){e=this.key(e);const t=this.one("SELECT * FROM checkpoints WHERE agent_id=?",e)??d(404,"agent_not_found","Unknown agent");return{revision:Number(t.revision),content:String(t.content),covered_receipt_seq:Number(t.covered_receipt_seq),updated_at:t.updated_at,runtime_id:t.runtime_id}}context(e){e=this.key(e);const t=this.row(e),s=this.readCheckpoint(e),i=Number(t.receipt_seq),n=this.messages(e),a=this.messages(e,{mode:"reconciliation",limit:Ft}),r=Number(this.one("SELECT COUNT(*) AS n FROM deliveries WHERE agent_id=? AND receipt_seq>?",e,s.covered_receipt_seq).n),o=a.next_cursor===null?[]:this.all("SELECT m.id,m.sender,m.target,m.details,m.room_id,m.priority,m.kind,m.created_at,m.supersedes,m.superseded_by,d.seen_at,d.receipt_seq FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.receipt_seq>? ORDER BY d.receipt_seq LIMIT ?",e,a.next_cursor,kt).map(l=>({id:String(l.id),from:T(String(l.sender)).agent_id,to:String(l.target),...B(l.details),room_id:l.room_id,priority:l.priority,kind:l.kind??null,created_at:String(l.created_at),agent_seen_at:l.seen_at,receipt_seq:Number(l.receipt_seq),...re(l)})),_=[],c=g(l=>`${l} acknowledged message${l===1?"":"s"}`,"counted");return r>0&&s.revision===0?_.push(`You have ${c(r)} but have never saved a checkpoint, so every bootstrap lists ${r===1?"it":"them"} again for reconciliation. ${t.ready?"This session already completed its bootstrap, so do not call ready: ready answers only a bootstrap event. After reconciling, call":"After reconciling and calling ready (a checkpoint saved before ready makes ready stale), call"} save_checkpoint with expected_revision 0 and covered_receipt_seq ${i}, keeping unfinished work and uncertain effects in its content.`):r>0&&_.push(`${c(r)} ${r===1?"is":"are"} not covered by checkpoint revision ${s.revision}. ${t.ready?"This session already completed its bootstrap, so do not call ready: ready answers only a bootstrap event. After reconciling, s":"After reconciling and calling ready, s"}ave a checkpoint with covered_receipt_seq ${i} so later bootstraps skip ${r===1?"it":"them"}.`),a.next_cursor!==null&&_.push(`Reconciliation shows the first ${a.items.length} with bodies${o.length?` and headers of ${o.length} more`:""}. Read the rest with get_messages(mode="reconciliation", cursor=${a.next_cursor}) only where you need them.`),n.next_cursor!==null&&_.push(`More pending messages follow; read get_messages(mode="pending", cursor=${n.next_cursor}).`),{agent_id:T(e).agent_id,lobby_id:String(t.lobby_id),runtime_id:t.runtime_id,role:String(t.role),work_description:String(t.work_description),machine:String(t.machine),screen:!!t.screen,memory:this.memoryIndex(e),checkpoint:s,receipt_snapshot:i,pending:n,reconciliation:{...a,total:r,headers:o},scheduled:{schedules:Number(this.one("SELECT COUNT(*) AS n FROM schedules WHERE sender=? AND cancelled_at IS NULL",e).n),messages:this.undeliveredCount(e)},guidance:_}}checkpoint(e,t,s){e=this.key(e);const i=rt.parse(s);return this.transaction(()=>{this.assertRuntime(e,t);const n=this.readCheckpoint(e),a=Number(this.row(e).receipt_seq);n.revision!==i.expected_revision&&d(409,"checkpoint_conflict",`Checkpoint revision changed: the current revision is ${n.revision}, not ${i.expected_revision}. Read it with read_checkpoint before saving over it`),(i.covered_receipt_seq<n.covered_receipt_seq||i.covered_receipt_seq>a)&&d(409,"receipt_snapshot_conflict",`Covered receipts must be an existing, nondecreasing snapshot: covered_receipt_seq from ${n.covered_receipt_seq} to ${a}, not ${i.covered_receipt_seq}`);const r=i.acknowledge_message_ids.some(l=>this.one("SELECT 1 FROM deliveries WHERE agent_id=? AND message_id=? AND seen_at IS NULL AND held_reason IS NULL",e,l));r&&i.covered_receipt_seq!==a&&d(409,"receipt_snapshot_conflict",`Reconcile existing receipts before covering newly acknowledged messages: covered_receipt_seq must be the current snapshot, ${a}`);const{snapshot:o,unknown:_}=this.acknowledgeInner(e,i.acknowledge_message_ids),c=r?o:i.covered_receipt_seq;return this.run("UPDATE checkpoints SET revision=revision+1,content=?,covered_receipt_seq=?,updated_at=?,runtime_id=? WHERE agent_id=?",i.content,c,this.iso(),t.runtime_id,e),{...this.readCheckpoint(e),...this.unknownIds(e,_)}})}nextNotification(e,t,s=ge){e=this.key(e);const i=this.clock(),n=this.one("SELECT paused,ready,bootstrap_attempts,bootstrap_next_at FROM agents WHERE agent_id=?",e);return n&&(Number(n.paused)||(Number(n.ready)?!this.one("SELECT 1 FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts<? AND next_attempt_at<=? LIMIT 1",e,this.retries.length,i):Number(n.bootstrap_attempts)>=this.retries.length||Number(n.bootstrap_next_at)>i))?null:this.transaction(()=>{this.assertRuntime(e,t);const a=this.row(e);if(a.paused)return null;const r=this.clock();if(Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(r-6e4).toISOString()).n)>=this.notificationRate)return null;const _=Number(this.one("SELECT COUNT(*) AS n FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",e).n),c={type:a.ready?"inbox":"bootstrap",notification_id:O(),agent_id:T(e).agent_id,lobby_id:String(a.lobby_id),runtime_id:t.runtime_id,generation:t.generation,messages:[],pending_count:_,truncated:!1};if(a.ready){for(let E=this.retries.length-1;E>=1;E--)this.run("UPDATE deliveries SET attempts=attempts+1,next_attempt_at=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts=? AND next_attempt_at<=? AND EXISTS (SELECT 1 FROM notification_attempts n WHERE n.id=deliveries.last_notification_id AND n.agent_id=deliveries.agent_id AND n.runtime_id=? AND n.generation=? AND n.incarnation=? AND n.notification_attempted_at IS NOT NULL)",r+this.retries[E],e,E,r,t.runtime_id,t.generation,t.incarnation);const l="d.agent_id=? AND d.seen_at IS NULL AND d.held_reason IS NULL AND d.attempts<? AND d.next_attempt_at<=?",m=this.all(`SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason,d.attempts FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE ${l} ORDER BY (m.priority='important') DESC,m.sequence LIMIT 21`,e,this.retries.length,r);if(!m.length)return null;const N=g(E=>Number(E.sequence)<=Number(a.backlog_sequence)&&(E.released_ms===null||Number(E.released_ms)<=Number(a.backlog_at)),"backlog"),R=m.length>20||m.some(E=>Number(E.attempts)>0||N(E));c.messages=R?[]:m.map(E=>this.message(E)),c.truncated=R,Buffer.byteLength(JSON.stringify(c))>s&&(c.messages=[],c.truncated=!0),c.truncated&&(c.covered=m.slice(0,20).map(E=>this.header(E)),c.covered_count=Number(this.one(`SELECT COUNT(*) AS n FROM deliveries d WHERE ${l}`,e,this.retries.length,r).n));for(let E=this.retries.length-1;E>=0;E--)this.run("UPDATE deliveries SET attempts=attempts+1,next_attempt_at=?,last_notification_id=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts=? AND next_attempt_at<=?",r+this.retries[E],c.notification_id,e,E,r)}else{if(Number(a.bootstrap_attempts)>=this.retries.length||Number(a.bootstrap_next_at)>r)return null;c.probe_nonce=String(a.probe_nonce),this.run("UPDATE agents SET bootstrap_attempts=bootstrap_attempts+1,bootstrap_next_at=? WHERE agent_id=?",r+this.bootstrapRetry,e)}return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",c.notification_id,e,t.runtime_id,t.generation,t.incarnation,c.type,this.iso()),c})}recordTransport(e,t,s){e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t);const i=this.one("SELECT * FROM notification_attempts WHERE id=? AND agent_id=? AND runtime_id=? AND generation=? AND incarnation=?",s.notification_id,e,t.runtime_id,t.generation,t.incarnation);i||d(404,"notification_not_found","Notification is not owned by this runtime");const n=s.type==="adapter_received"?"adapter_received_at":"notification_attempted_at";if(i[n]!==null)return;const a=this.iso();this.run(`UPDATE notification_attempts SET ${n}=? WHERE id=?`,a,s.notification_id),n==="notification_attempted_at"&&this.run("UPDATE deliveries SET notification_attempted_at=? WHERE agent_id=? AND last_notification_id=?",a,e,s.notification_id)})}releaseDue(){const e=this.clock();let t=0,s=0;const i=this.all("SELECT id FROM messages WHERE deliver_at_ms IS NOT NULL AND released_ms IS NULL AND cancelled_at IS NULL AND deliver_at_ms<=? ORDER BY deliver_at_ms,sequence LIMIT 100",e).map(a=>String(a.id));i.length&&this.transaction(()=>{for(const a of i)this.release(a,e)&&t++});const n=this.all("SELECT id FROM schedules WHERE cancelled_at IS NULL AND next_run_ms IS NOT NULL AND next_run_ms<=? ORDER BY next_run_ms LIMIT 50",e).map(a=>String(a.id));for(const a of n)this.runSchedule(a,e)&&s++;return{released:t,occurrences:s}}release(e,t){const s=this.one("SELECT * FROM messages WHERE id=? AND released_ms IS NULL AND cancelled_at IS NULL",e);if(!s)return!1;const i=this.one("SELECT enabled FROM agents WHERE agent_id=?",String(s.sender));if(i&&!i.enabled)return this.cancelMessage(e),!1;const n=t-Math.max(Number(s.deliver_at_ms),Number(s.created_ms))>this.lateAfter;return this.run("UPDATE messages SET released_ms=?,late=? WHERE id=?",t,Number(n),e),this.run("UPDATE attachments SET last_referenced_ms=MAX(COALESCE(last_referenced_ms,0),?) WHERE id IN (SELECT attachment_id FROM message_attachments WHERE message_id=?)",t,e),this.run("UPDATE deliveries SET held_reason=NULL,attempts=0,next_attempt_at=? WHERE message_id=? AND held_reason=?",t+(s.priority==="info"?this.infoDelay:0),e,W),!0}cancelMessage(e){this.run("UPDATE messages SET cancelled_at=? WHERE id=?",this.iso(),e),this.run("UPDATE deliveries SET held_reason=? WHERE message_id=? AND held_reason=?",Ie,e,W)}runSchedule(e,t){const s=this.one("SELECT * FROM schedules WHERE id=? AND cancelled_at IS NULL",e);if(!s||s.next_run_ms===null||Number(s.next_run_ms)>t)return!1;const i=this.repeatOf(s),n=Dt(i,Number(s.next_run_ms),t),a=te(i,t),r=new Date(n).toISOString();try{return this.transaction(()=>{const o=String(s.sender),_=this.agent(o);_.enabled||d(403,"agent_disabled","Sender is disabled");const c=Se.parse({client_request_id:`${H}schedule:${e}:${n}`,...JSON.parse(String(s.template))}),l=this.accept(o,String(s.lobby_id),_,c,{fingerprint:k(JSON.stringify(Z(c))),now:t,deliverAt:n,scheduleId:e});return this.run("UPDATE schedules SET next_run_ms=?,occurrence_count=occurrence_count+1,last_occurrence_at=?,last_message_id=?,last_error=NULL WHERE id=?",a,r,l.message.id,e),!0})}catch(o){const _=o instanceof Le?o.code:"internal_error";return this.transaction(()=>this.run("UPDATE schedules SET next_run_ms=?,last_occurrence_at=?,last_error=? WHERE id=? AND cancelled_at IS NULL",a,r,_,e)),!1}}repeatOf(e){return{time_zone:String(e.time_zone),times:JSON.parse(String(e.times)),weekdays:e.weekdays===null?null:JSON.parse(String(e.weekdays))}}schedule(e){const t=JSON.parse(String(e.template)),s=this.repeatOf(e),i=t.kind??null,n=e.next_run_ms===null?null:Number(e.next_run_ms);return{schedule_id:String(e.id),lobby_id:String(e.lobby_id),from:T(String(e.sender)).agent_id,client_request_id:String(e.request_id),to:Array.isArray(t.to)?"*":t.to,...Array.isArray(t.to)?{addressed_to:Te(t.to)}:{},room_id:t.room??null,priority:t.priority,content:t.content,correlation_id:t.correlation_id??null,kind:i,reply_expected:He(t,String(e.sender)),...ee(t),...t.notify_when_seen?{notify_when_seen:!0}:{},...(()=>{const a=this.all("SELECT a.* FROM schedule_attachments s JOIN attachments a ON a.id=s.attachment_id WHERE s.schedule_id=? ORDER BY s.position",String(e.id)).map(r=>this.attachmentInfo(r));return a.length?{attachments:a}:{}})(),repeat:s,created_at:String(e.created_at),next_occurrence_at:n===null?null:new Date(n).toISOString(),next_occurrence_local:n===null?null:Ut(n,s.time_zone),occurrence_count:Number(e.occurrence_count),last_occurrence_at:e.last_occurrence_at,last_message_id:e.last_message_id,last_error:e.last_error,cancelled_at:e.cancelled_at}}createSchedule(e,t,s){e=this.key(e);const i=Et.parse(s),{repeat:n,client_request_id:a,...r}=i,o=Oe(n),_=k(JSON.stringify({...Z({client_request_id:a,...r}),repeat:o}));return this.transaction(()=>{this.assertRuntime(e,t);const c=this.agent(e),l=this.one("SELECT * FROM schedules WHERE sender=? AND request_id=?",e,a);if(l)return l.fingerprint!==_&&d(409,"idempotency_conflict","Retry key was already used for a different request"),{status:"scheduled",schedule:this.schedule(l),duplicate:!0};a.startsWith(H)&&d(400,"reserved_request_id",`Request IDs starting with ${H} are reserved`),ie(i.content),Number(this.one("SELECT COUNT(*) AS n FROM schedules WHERE sender=? AND cancelled_at IS NULL",e).n)>=this.maxSchedules&&d(409,"schedule_capacity",`At most ${this.maxSchedules} active schedules per agent`),this.recipients(e,c.lobby_id,c,r);const m=this.clock(),N=this.resolveAttachments(e,c.lobby_id,r.attachments,m),R=O();return this.run("INSERT INTO schedules(id,sender,lobby_id,request_id,fingerprint,template,time_zone,times,weekdays,created_at,next_run_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?)",R,e,c.lobby_id,a,_,JSON.stringify(r),o.time_zone,JSON.stringify(o.times),o.weekdays===null?null:JSON.stringify(o.weekdays),new Date(m).toISOString(),te(o,m)),this.linkAttachments(e,N,"schedule",R,m),{status:"scheduled",schedule:this.schedule(this.one("SELECT * FROM schedules WHERE id=?",R)),duplicate:!1}})}scheduled(e){e=this.key(e);const t=T(e);return t.agent_id!=="owner"?this.row(e):this.requireLobby(t.lobby_id),{schedules:this.all("SELECT * FROM schedules WHERE sender=? AND cancelled_at IS NULL ORDER BY next_run_ms,created_at",e).map(s=>Y(this.schedule(s))),messages:this.all("SELECT m.*,(SELECT COUNT(*) FROM deliveries d WHERE d.message_id=m.id) AS recipient_count FROM messages m INDEXED BY messages_due WHERE m.sender=? AND m.deliver_at_ms IS NOT NULL AND m.released_ms IS NULL AND m.cancelled_at IS NULL ORDER BY m.deliver_at_ms,m.sequence",e).map(s=>({...Y(this.message(s)),recipients:Number(s.recipient_count)}))}}updateScheduled(e,t,s){e=this.key(e);const i=yt.parse(s);return this.transaction(()=>{this.assertRuntime(e,t),i.content!==void 0&&ie(i.content);const n=this.clock(),a=this.one("SELECT * FROM schedules WHERE id=? AND sender=?",i.id,e);if(a){a.cancelled_at!==null&&d(409,"already_cancelled","The schedule was cancelled; create a new one"),i.deliver_at!==void 0&&d(400,"invalid_request","A schedule repeats: change its repeat, not deliver_at");const m=JSON.parse(String(a.template));i.content!==void 0&&(m.content=i.content);const N=this.repeatOf(a),R=i.repeat?Oe(i.repeat):N,E=JSON.stringify(R)!==JSON.stringify(N);return this.run("UPDATE schedules SET template=?,time_zone=?,times=?,weekdays=?,next_run_ms=? WHERE id=?",JSON.stringify(m),R.time_zone,JSON.stringify(R.times),R.weekdays===null?null:JSON.stringify(R.weekdays),E?te(R,n):a.next_run_ms,i.id),{updated:"schedule",schedule:Y(this.schedule(this.one("SELECT * FROM schedules WHERE id=?",i.id)))}}const r=this.one("SELECT * FROM messages WHERE id=? AND sender=? AND deliver_at_ms IS NOT NULL AND schedule_id IS NULL",i.id,e);r||d(404,"not_found","You have no schedule or scheduled message with that ID"),r.cancelled_at!==null&&d(409,"already_cancelled","The message was cancelled; send a new one"),r.released_ms!==null&&d(409,"already_delivered","The message was already delivered; it cannot be changed, so send a correction"),i.repeat!==void 0&&d(400,"invalid_request","A scheduled message is sent once: change its deliver_at");let o=Number(r.deliver_at_ms);i.deliver_at!==void 0&&(o=Date.parse(i.deliver_at),(o<n-ye||o>n+Q)&&d(400,"deliver_at_out_of_range",Fe(n)));const _=i.content??String(r.content),c=Buffer.byteLength(_)-Buffer.byteLength(String(r.content));c>0&&this.admitBytes(String(r.lobby_id),this.all("SELECT agent_id FROM deliveries WHERE message_id=?",i.id).map(m=>String(m.agent_id)),c),this.run("UPDATE messages SET content=?,byte_size=byte_size+?,deliver_at_ms=? WHERE id=?",_,c,o,i.id);const l=this.one("SELECT m.*,(SELECT COUNT(*) FROM deliveries d WHERE d.message_id=m.id) AS recipient_count FROM messages m WHERE m.id=?",i.id);return{updated:"message",message:{...Y(this.message(l)),recipients:Number(l.recipient_count)}}})}cancelScheduled(e,t,s){return e=this.key(e),this.transaction(()=>{t&&this.assertRuntime(e,t);const i=this.one("SELECT * FROM schedules WHERE id=? AND sender=?",s,e);if(i){const o=i.cancelled_at!==null;return o||this.run("UPDATE schedules SET cancelled_at=?,next_run_ms=NULL WHERE id=?",this.iso(),s),{cancelled:"schedule",schedule:this.schedule(this.one("SELECT * FROM schedules WHERE id=?",s)),already_cancelled:o}}const n=this.one("SELECT * FROM messages WHERE id=? AND sender=? AND deliver_at_ms IS NOT NULL AND schedule_id IS NULL",s,e);n||d(404,"not_found","You have no schedule or scheduled message with that ID");const a=n.cancelled_at!==null;!a&&n.released_ms!==null&&d(409,"already_delivered","The message was already delivered; it cannot be recalled"),a||this.cancelMessage(s);const r=this.one("SELECT * FROM messages WHERE id=?",s);return{cancelled:"message",message:{...this.message(r),deliveries:this.recipientDeliveries([s]).get(s)??[]},already_cancelled:a}})}nextReceipts(e,t){e=this.key(e);const s=this.clock(),i=this.one("SELECT MIN(seen_ms) AS t FROM seen_receipts WHERE sender=? AND state='due'",e);return!i||i.t===null||Number(i.t)>s-this.receiptDelay?null:this.transaction(()=>{this.assertRuntime(e,t);const n=this.row(e);if(!n.ready||n.paused||Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(s-6e4).toISOString()).n)>=this.notificationRate||this.one("SELECT 1 FROM notification_attempts WHERE agent_id=? AND kind='receipts' AND created_at>?",e,new Date(s-this.receiptInterval).toISOString()))return null;this.run("UPDATE seen_receipts SET state='expired' WHERE sender=? AND state='due' AND seen_ms<?",e,s-Ht);const r=this.all("SELECT r.sequence,r.message_id,r.agent_id,r.seen_at,m.kind,m.target,m.details,m.room_id,m.content FROM seen_receipts r JOIN messages m ON m.id=r.message_id WHERE r.sender=? AND r.state='due' ORDER BY r.sequence LIMIT ?",e,$t);if(!r.length)return null;const o=Number(this.one("SELECT COUNT(*) AS n FROM seen_receipts WHERE sender=? AND state='due'",e).n),_={type:"receipts",notification_id:O(),agent_id:T(e).agent_id,lobby_id:String(n.lobby_id),runtime_id:t.runtime_id,generation:t.generation,receipts:r.map(c=>({message_id:String(c.message_id),seen_by:T(String(c.agent_id)).agent_id,seen_at:String(c.seen_at),kind:c.kind??null,to:String(c.target),...B(c.details),room_id:c.room_id,excerpt:[...String(c.content)].slice(0,80).join("")})),remaining:0};for(;_.receipts.length>1&&Buffer.byteLength(JSON.stringify(_))>ge;)_.receipts.pop();_.remaining=o-_.receipts.length;for(const c of _.receipts)this.run("UPDATE seen_receipts SET state='sent',notification_id=? WHERE message_id=? AND agent_id=?",_.notification_id,c.message_id,D(String(n.lobby_id),c.seen_by));return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",_.notification_id,e,t.runtime_id,t.generation,t.incarnation,_.type,this.iso()),_})}openActivityNotice(e,t){e=this.key(e);const s=this.clock();return this.transaction(()=>{this.assertRuntime(e,t);const i=this.row(e);if(!i.ready||i.paused||Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(s-6e4).toISOString()).n)>=this.notificationRate)return null;const a=O();return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",a,e,t.runtime_id,t.generation,t.incarnation,"activity",this.iso()),{notification_id:a,agent_id:T(e).agent_id,lobby_id:String(i.lobby_id)}})}boardLobbies(){return this.all("SELECT DISTINCT s.lobby_id FROM board_sources s WHERE NOT EXISTS (SELECT 1 FROM archived_rooms a WHERE a.lobby_id=s.lobby_id AND a.room_id=s.room_id) ORDER BY s.lobby_id").map(e=>String(e.lobby_id))}boardSources(e){return this.all("SELECT room_id,repo,ref,notes,updated_at FROM board_sources s WHERE lobby_id=? AND NOT EXISTS (SELECT 1 FROM archived_rooms a WHERE a.lobby_id=s.lobby_id AND a.room_id=s.room_id) ORDER BY room_id",e).map(t=>({room_id:String(t.room_id),repo:String(t.repo),ref:String(t.ref),notes:JSON.parse(String(t.notes)),updated_at:String(t.updated_at)}))}boardConfig(e){const t=this.one("SELECT * FROM board_configs WHERE lobby_id=?",e);return{lobby_id:e,agents:t?JSON.parse(String(t.agents)):{},visibility:t?.visibility??"private",summary_model:t?.summary_model??Ue[0],summary_effort:t?.summary_effort??"low",updated_at:t?.updated_at??null,sources:this.boardSources(e)}}setBoardSource(e,t,s,i){const n=ft.parse(s),a=n.repo.toLowerCase();return this.transaction(()=>{const r=this.actorLobby(i,e);this.room(r,I.parse(t)),typeof i=="object"?this.one("SELECT 1 FROM lobby_repos WHERE lobby_id=? AND repo=?",r,a)||d(403,"repo_not_approved","The operator has not approved that repository for this lobby"):this.run("INSERT INTO lobby_repos(lobby_id,repo,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",r,a,this.iso());const o=this.boardSources(r).find(l=>l.room_id===t),_=Number(this.one("SELECT COUNT(*) AS n FROM board_sources WHERE lobby_id=?",r).n);!this.one("SELECT 1 FROM board_sources WHERE lobby_id=? AND room_id=?",r,t)&&_>=Ae&&d(409,"board_source_capacity",`A dashboard reads at most ${Ae} sources`),o&&P(o)!==P({room_id:t,...n})&&this.run("DELETE FROM board_notes WHERE lobby_id=? AND room_id=?",r,t),this.run("INSERT INTO board_sources(lobby_id,room_id,repo,ref,notes,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(lobby_id,room_id) DO UPDATE SET repo=excluded.repo,ref=excluded.ref,notes=excluded.notes,updated_at=excluded.updated_at",r,t,n.repo,n.ref,JSON.stringify(n.notes),this.iso()),this.audit(i,"board_source_set",r,t,{repo:n.repo,ref:n.ref,notes:n.notes})}),this.boardConfig(e)}removeBoardSource(e,t,s){return this.transaction(()=>{const i=this.actorLobby(s,e);this.run("DELETE FROM board_sources WHERE lobby_id=? AND room_id=?",i,t)||d(404,"board_source_not_found","This dashboard reads no notes for that room"),this.audit(s,"board_source_remove",i,t)}),this.boardConfig(e)}updateBoardConfig(e,t){this.run("INSERT INTO board_configs(lobby_id,updated_at) VALUES(?,?) ON CONFLICT(lobby_id) DO NOTHING",e,this.iso());for(const[s,i]of Object.entries(t))this.run(`UPDATE board_configs SET ${s}=?,updated_at=? WHERE lobby_id=?`,i,this.iso(),e)}setBoardAgents(e,t){const s=Ot.parse(t);return this.transaction(()=>{const i=this.actorLobby("owner",e);this.updateBoardConfig(i,{agents:JSON.stringify(s.agents)}),this.audit(null,"board_agents",i)}),this.boardConfig(e)}setSummarySettings(e,t,s){const i=At.parse(s);return this.transaction(()=>{const n=this.actorLobby(e,t);this.updateBoardConfig(n,{summary_model:i.model,summary_effort:i.effort}),this.audit(e,"summary_settings",n,null,i)}),this.boardConfig(t)}setBoardVisibility(e,t,s){return this.transaction(()=>{const i=this.actorLobby(e,t);this.updateBoardConfig(i,{visibility:s}),this.audit(e,"board_visibility",i,s)}),this.boardConfig(t)}boardIcon(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`board_icon:${e}`);if(!t)return null;const s=JSON.parse(String(t.value));return{png:Buffer.from(s.png,"base64"),sha256:s.sha256,set_at:s.set_at}}displayName(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`display_name:${e}`);return t?String(t.value):null}setDisplayName(e,t,s){const i=St.parse(s)||null;return this.transaction(()=>{const n=this.actorLobby(e,t),a=`display_name:${n}`;return i===this.displayName(n)?{lobby_id:n,display_name:i}:(i?this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",a,i):this.run("DELETE FROM metadata WHERE key=?",a),this.audit(e,"display_name",n,i),{lobby_id:n,display_name:i})})}exchangeRates(){const e=this.one("SELECT value FROM metadata WHERE key='exchange_rates'");return e?JSON.parse(String(e.value)):null}saveExchangeRates(e,t){const s=this.exchangeRates(),i={attempted_at:e,date:t?.date??s?.date??null,rates:t?.rates??s?.rates??null};this.run("INSERT INTO metadata(key,value) VALUES('exchange_rates',?1) ON CONFLICT(key) DO UPDATE SET value=?1",JSON.stringify(i))}lobbyCurrency(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`currency:${e}`);return t?String(t.value):"USD"}lobbyMoney(e){return It(this.lobbyCurrency(e),this.exchangeRates())}setLobbyCurrency(e,t,s){const i=pt.parse(s);return this.transaction(()=>{const n=this.actorLobby(e,t);return i!=="USD"&&!this.exchangeRates()?.rates?.[i]&&d(400,"currency_unavailable",`No exchange rate for ${i} is stored on this server`),i===this.lobbyCurrency(n)?{lobby_id:n,currency:i}:(i==="USD"?this.run("DELETE FROM metadata WHERE key=?",`currency:${n}`):this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",`currency:${n}`,i),this.audit(e,"currency",n,i),{lobby_id:n,currency:i})})}setBoardIcon(e,t,s){return s&&Ct(s),this.transaction(()=>{const i=this.actorLobby(e,t),n=`board_icon:${i}`;if(!s)return this.run("DELETE FROM metadata WHERE key=?",n)&&this.audit(e,"board_icon_remove",i),{icon:null};const a=x("sha256").update(s).digest("hex");return this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",n,JSON.stringify({png:s.toString("base64"),sha256:a,set_at:this.iso()})),this.audit(e,"board_icon",i,null,{sha256:a,bytes:s.length}),{icon:{sha256:a,bytes:s.length}}})}boardNoteFacts(e){return new Map(this.all("SELECT room_id,commit_sha,fetched_at,(SELECT COUNT(*) FROM json_each(files)) AS files,(SELECT json_group_array(json_extract(value,'$.path')) FROM json_each(files) WHERE json_extract(value,'$.truncated')) AS cut FROM board_notes WHERE lobby_id=?",e).map(t=>[String(t.room_id),{commit_sha:String(t.commit_sha),fetched_at:String(t.fetched_at),files:Number(t.files),cut:JSON.parse(String(t.cut))}]))}boardNotes(e){return new Map(this.all("SELECT * FROM board_notes WHERE lobby_id=?",e).map(t=>[String(t.room_id),{commit_sha:String(t.commit_sha),files:JSON.parse(String(t.files)),fetched_at:String(t.fetched_at)}]))}saveBoardNotes(e,t,s){return this.transaction(()=>{const i=this.boardSources(e).find(n=>n.room_id===t.room_id);return!i||P(i)!==P(t)?!1:(this.run("INSERT INTO board_notes(lobby_id,room_id,commit_sha,files,fetched_at) VALUES(?,?,?,?,?) ON CONFLICT(lobby_id,room_id) DO UPDATE SET commit_sha=excluded.commit_sha,files=excluded.files,fetched_at=excluded.fetched_at",e,t.room_id,s.commit_sha,JSON.stringify(s.files),s.fetched_at),!0)})}boardMessages(e,t,s,i){return this.all("SELECT sequence,sender,target,details,room_id,kind,created_at,substr(content,1,?) AS content FROM messages WHERE lobby_id=? AND kind IN ('report','assignment') AND created_ms>? AND cancelled_at IS NULL AND (deliver_at_ms IS NULL OR released_ms IS NOT NULL) ORDER BY sequence DESC LIMIT ?",i,e,t,s).map(n=>({sequence:Number(n.sequence),from:T(String(n.sender)).agent_id,to:String(n.target),...B(n.details),room_id:n.room_id,kind:n.kind,created_at:String(n.created_at),content:String(n.content)}))}recordTokens(e,t){return this.transaction(()=>this.countCalls(e,t))}countCalls(e,t){const s=this.key(e),i=T(s).lobby_id,n=this.clock();let a=0;for(const r of t){if(r.at<n-ne||r.at>n+1440*60*1e3||!this.run("INSERT OR IGNORE INTO token_calls(lobby_id,call_id,at_ms) VALUES(?,?,?)",i,r.id,r.at))continue;const o=r.input+r.cache_write_5m+r.cache_write_1h+r.cache_read;this.run(`INSERT INTO token_days(lobby_id,day,agent_id,model,speed,geo,long,calls,input,cache_write_5m,cache_write_1h,cache_read,output)
|
|
320
|
+
)`,e,t,n,t,n);return s?.t===null||s?.t===void 0?null:new Date(Number(s.t)).toISOString()}permissions(e){const t=this.row(e);return{manage_rooms:!!t.manage_rooms,can_broadcast:!!t.can_broadcast}}setOwnerRequests(e,t,n){e=this.key(e);const s=bt.parse(n);return this.transaction(()=>{this.assertRuntime(e,t);const i=s.count===0,a=i?null:s.text.trim(),r=this.row(e),o=Number(r.owner_requests)>0&&r.owner_requests_text===a;return this.run("UPDATE agents SET owner_requests=?,owner_requests_text=?,owner_requests_at=? WHERE agent_id=?",s.count,a,i?null:o?String(r.owner_requests_at):this.iso(),e),this.agent(e).owner_requests})}setStatus(e,t,n){e=this.key(e);const s=Nt.parse(n).text.trim()||null;return this.transaction(()=>{this.assertRuntime(e,t);const i=this.row(e);return this.run("UPDATE agents SET status_text=?,status_set_at=? WHERE agent_id=?",s,s===null?null:i.status_text===s?String(i.status_set_at):this.iso(),e),this.agent(e).status})}agents(e){return(e===void 0?this.all("SELECT agent_id FROM agents ORDER BY agent_id"):this.all("SELECT agent_id FROM agents WHERE lobby_id=? ORDER BY agent_id",this.requireLobby(e))).map(n=>this.agent(String(n.agent_id)))}updateAgent(e,t){e=this.key(e);const n=Tt.parse(t);return this.transaction(()=>{const s=this.row(e);for(const a of["role","work_description","machine","screen","kind","can_broadcast","manage_rooms","allowed_recipients","enabled","paused"]){if(n[a]===void 0)continue;const r=n[a];this.run(`UPDATE agents SET ${a}=? WHERE agent_id=?`,a==="allowed_recipients"?r===null?null:JSON.stringify(r):typeof r=="boolean"?Number(r):String(r),e)}n.enabled===!1&&s.enabled&&this.resetBinding(e),n.paused!==void 0&&this.run("UPDATE agents SET resume_at_ms=NULL WHERE agent_id=?",e);const i=this.row(e);(i.kind!==s.kind||i.manage_rooms!==s.manage_rooms)&&this.paceRole(i),F(s)&&!F(this.row(e))&&this.restartNotifications(e),n.role!==void 0&&n.role!==s.role&&this.run("UPDATE agents SET ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=? WHERE agent_id=?",O(),this.clock(),e),this.audit(null,"update_agent",...this.agentScope(e),Object.keys(n),e)}),this.agent(e)}resetBinding(e){e=this.key(e),this.row(e),this.run("UPDATE agents SET runtime_id=NULL,generation=generation+1,ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=?,owner_requests=0,owner_requests_text=NULL,owner_requests_at=NULL,status_text=NULL,status_set_at=NULL WHERE agent_id=?",O(),this.clock(),e),this.resetDeliveryAttempts(e)}resetDeliveryAttempts(e){this.run("UPDATE deliveries SET attempts=0,next_attempt_at=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",this.clock(),e)}bindRuntime(e,t,n){const s=Number(this.one("SELECT COALESCE(MAX(sequence),0) AS n FROM messages").n);this.run("UPDATE agents SET runtime_id=?,generation=?,ready=0,probe_nonce=?,bootstrap_attempts=0,bootstrap_next_at=?,backlog_sequence=?,backlog_at=? WHERE agent_id=?",t,n,O(),this.clock(),s,this.clock(),e)}rotateToken(e){e=this.key(e);const t=Be();return this.transaction(()=>{this.resetBinding(e),this.run("UPDATE agents SET token_hash=? WHERE agent_id=?",H(t),e),this.audit(null,"rotate_token",...this.agentScope(e),null,e)}),{token:t}}resetRuntime(e){e=this.key(e),this.transaction(()=>{this.resetBinding(e),this.audit(null,"reset_runtime",...this.agentScope(e),null,e)})}retryNotifications(e){e=this.key(e),this.transaction(()=>{this.row(e),this.restartNotifications(e),this.audit(null,"retry_notifications",...this.agentScope(e),null,e)})}restartNotifications(e){this.resetDeliveryAttempts(e),this.run("UPDATE agents SET bootstrap_attempts=0,bootstrap_next_at=? WHERE agent_id=?",this.clock(),e)}restAgents(e,t,n,s){const i=[...new Set(t.map(a=>$.parse(a)))];return this.transaction(()=>{const a=this.roomManager(e),r=this.key(e),o=n&&s!==void 0?Date.parse(s):null;o!==null&&o<=this.clock()&&_(400,"resume_at_past","resume_at must be in the future");const c=o===null?null:new Date(o).toISOString();for(const d of i){const l=D(a,d),m=this.row(l);m.enabled||_(409,"agent_disabled",`${d} is disabled`),n&&l===r&&_(400,"cannot_rest_self","A coordinator cannot rest itself: nothing could resume it");const N=!!m.paused,g=m.resume_at_ms===null?null:Number(m.resume_at_ms);N===n&&g===o||(this.run("UPDATE agents SET paused=?,resume_at_ms=? WHERE agent_id=?",Number(n),o,l),N&&!n&&this.restartNotifications(l),this.audit(e,n?"rest_agent":"resume_agent",a,d,c===null?null:{resume_at:c}))}return{agents:i.map(d=>({agent_id:d,paused:n,resume_at:c}))}})}resumeDue(){const e=this.all("SELECT agent_id,resume_at_ms FROM agents WHERE paused=1 AND resume_at_ms IS NOT NULL AND resume_at_ms<=? LIMIT 100",this.clock());if(!e.length)return 0;let t=0;return this.transaction(()=>{for(const n of e){const s=String(n.agent_id),i=Number(n.resume_at_ms);if(!this.one("SELECT 1 FROM agents WHERE agent_id=? AND paused=1 AND resume_at_ms=?",s,i))continue;this.run("UPDATE agents SET paused=0,resume_at_ms=NULL WHERE agent_id=?",s),this.restartNotifications(s);const[a,r]=this.agentScope(s);this.audit(null,"resume_agent",a,r,{timer:new Date(i).toISOString()}),t++}}),t}claimRuntime(e,t,n){return e=this.key(e),this.transaction(()=>{let s=this.row(e);return s.enabled||_(403,"agent_disabled","Agent is disabled"),n!==void 0?(s.runtime_id!==t||Number(s.generation)!==n)&&_(409,"stale_runtime","Runtime no longer owns this agent"):s.runtime_id===t?(this.bindRuntime(e,t,Number(s.generation)+1),this.resetDeliveryAttempts(e),this.audit(e,"resume_runtime",...this.agentScope(e)),s=this.row(e)):this.one("SELECT 1 FROM runtime_history WHERE agent_id=? AND runtime_id=?",e,t)?_(409,"stale_runtime","A retired runtime cannot reclaim this identity"):s.runtime_id?_(409,"runtime_conflict","Another runtime owns this agent; owner replacement is required"):(this.bindRuntime(e,t,Math.max(Number(s.generation),1)),this.run("INSERT INTO runtime_history(agent_id,runtime_id) VALUES(?,?)",e,t),s=this.row(e)),{runtime_id:t,generation:Number(s.generation),incarnation:this.incarnation,probe_nonce:String(s.probe_nonce),ready:!!s.ready}})}assertRuntime(e,t){e=this.key(e);const n=this.row(e);(!n.enabled||n.runtime_id!==t.runtime_id||n.generation!==t.generation||t.incarnation!==this.incarnation)&&_(409,"stale_runtime","Runtime no longer owns this agent")}ready(e,t,n){e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t),(this.row(e).probe_nonce!==n.probe_nonce||this.readCheckpoint(e).revision!==n.context_revision)&&_(409,"bootstrap_conflict","Bootstrap context or channel probe is stale"),this.run("UPDATE agents SET ready=1,last_agent_seen_at=? WHERE agent_id=?",this.iso(),e)})}heartbeat(e,t){e=this.key(e),this.assertRuntime(e,t),this.run("UPDATE agents SET last_seen_at=? WHERE agent_id=?",this.iso(),e)}deliveryFacts(e){const t=e.held_reason;return t===w||t===Fe?{state:t,held_reason:null}:t?{state:"held",held_reason:t}:{state:e.seen_at?"seen":"pending",held_reason:null}}message(e,t=!1){const n={id:String(e.id),lobby_id:String(e.lobby_id),room_id:e.room_id,sequence:Number(e.sequence),from:T(String(e.sender)).agent_id,to:String(e.target),priority:e.priority,content:String(e.content),correlation_id:e.correlation_id,reply_to:e.reply_to,created_at:String(e.created_at),kind:e.kind??null,reply_expected:e.reply_expected===null?null:!!e.reply_expected,...e.details===null?{}:JSON.parse(String(e.details)),...ce(e)};e.notify_when_seen&&(n.notify_when_seen=!0),e.deliver_at_ms!==null&&(n.deliver_at=new Date(Number(e.deliver_at_ms)).toISOString(),n.delivered_at=e.released_ms===null?null:new Date(Number(e.released_ms)).toISOString(),n.late=!!e.late),e.schedule_id!==null&&(n.schedule_id=String(e.schedule_id)),e.cancelled_at!==null&&(n.cancelled_at=String(e.cancelled_at));const s=this.messageAttachments(String(e.id));if(s.length&&(n.attachments=s),"seen_at"in e&&(!("delivery_recipient_id"in e)||e.delivery_recipient_id!==null)){const i=this.deliveryFacts(e);if(n.agent_seen_at=e.seen_at,n.receipt_seq=e.receipt_seq===null?null:Number(e.receipt_seq),n.delivery_state=i.state,n.held_reason=i.held_reason,i.held_reason&&!t){n.content=`[Held: ${i.held_reason.replaceAll("_"," ")}]`,delete n.attachments;for(const a of Ue)delete n[a]}}return n}attachmentInfo(e){return{id:String(e.id),filename:String(e.filename),mime_type:String(e.mime_type),size:Number(e.size),sha256:String(e.sha256),...e.expired_at===null?{}:{expired_at:String(e.expired_at)}}}messageAttachments(e){return this.all("SELECT a.* FROM message_attachments m JOIN attachments a ON a.id=m.attachment_id WHERE m.message_id=? ORDER BY m.position",e).map(t=>this.attachmentInfo(t))}compactAttachments(e){return this.messageAttachments(e).map(({id:t,filename:n,mime_type:s,size:i})=>({id:t,filename:n,mime_type:s,size:i}))}header(e){return{id:String(e.id),from:T(String(e.sender)).agent_id,to:String(e.target),...P(e.details),room_id:e.room_id,priority:e.priority,created_at:String(e.created_at),kind:e.kind??null,...ce(e),...(()=>{const t=this.compactAttachments(String(e.id));return t.length?{attachments:t}:{}})()}}recipientDeliveries(e){const t=new Map;if(!e.length)return t;for(const n of this.all("SELECT message_id,agent_id,seen_at,held_reason,notification_attempted_at FROM deliveries WHERE message_id IN (SELECT value FROM json_each(?)) ORDER BY agent_id",JSON.stringify(e))){const s=t.get(String(n.message_id))??[],i=this.deliveryFacts(n);s.push({agent_id:T(String(n.agent_id)).agent_id,delivery_state:i.state,notification_attempted_at:n.notification_attempted_at,agent_seen_at:n.seen_at,held_reason:i.held_reason}),t.set(String(n.message_id),s)}return t}stored(e,t){const n=this.recipientDeliveries([String(e.id)]).get(String(e.id))??[],s=e.reply_to&&!this.one("SELECT 1 FROM messages WHERE id=?",e.reply_to);return{status:"stored",message:this.message(e),recipients:n.map(i=>i.agent_id),deliveries:n,duplicate:t,...s?{warning:Gt}:{}}}send(e,t,n){const s=fe.parse(t),i=e==="owner";e=i?D(this.requireLobby(n??this.soleLobby()),"owner"):this.key(e);const a=T(e).lobby_id;return this.transaction(()=>{const r=i?null:this.agent(e);r&&!r.enabled&&_(403,"agent_disabled","Sender is disabled");const o=H(JSON.stringify(te(s))),c=this.one("SELECT * FROM messages WHERE sender=? AND request_id=?",e,s.client_request_id);if(c)return c.fingerprint!==o&&_(409,"idempotency_conflict",`This client_request_id already stored message ${String(c.id)} at ${String(c.created_at)}, with different arguments. Choose a new client_request_id for a new message`,{original_message_id:c.id,original_created_at:c.created_at}),this.stored(c,!0);s.client_request_id.startsWith(W)&&_(400,"reserved_request_id",`Request IDs starting with ${W} are reserved`),re(s.content),i&&s.notify_when_seen&&_(400,"receipts_unavailable","The owner has no event stream for read receipts; read history instead"),i&&s.attachments&&_(400,"attachments_unavailable","Only agents attach files: their bridge uploads them before the send");const d=this.clock();let l=null;return s.deliver_at!==void 0&&(l=Date.parse(s.deliver_at),(l<d-Oe||l>d+ee)&&_(400,"deliver_at_out_of_range",we(d)),l>d&&this.undeliveredCount(e)>=this.maxScheduled&&_(409,"schedule_capacity",`At most ${this.maxScheduled} undelivered scheduled messages per sender`)),this.accept(e,a,r,s,{fingerprint:o,now:d,deliverAt:l})})}admitNewTasks(e,t,n,s){if(!(s.kind==="assignment"||!s.reply_to&&(s.kind===void 0||s.kind==="request"))||this.lobbyPacing(t).mode==="off")return;const a=n.filter(d=>d!==e).map(d=>this.row(d)).filter(d=>J(d)&&d.pacing!=="open");if(!a.length)return;const{open_at:r}=this.lobbyPacing(t),o=a.map(d=>({agent_id:T(String(d.agent_id)).agent_id,pacing:d.pacing,since:d.pacing_since_ms===null?null:new Date(Number(d.pacing_since_ms)).toISOString(),lead:ke(d)})),c=o.map(({agent_id:d,pacing:l,lead:m})=>`${d} (no new tasks${m===null?"":`, at a lead of ${m>0?"+":""}${m}`})`).join(", ");_(409,"no_new_tasks",`No new tasks for ${c}: ${o.length===1?"its account is":"their accounts are"} ahead of the week's line, and ${o.length===1?"it takes":"they take"} new tasks again at a lead of ${r}. Nothing was sent. Give the task to another agent, or, when it continues work already given, send it as a request or a question with reply_to.`,{agents:o,opens_at:r})}undeliveredCount(e){return Number(this.one("SELECT COUNT(*) AS n FROM messages INDEXED BY messages_due WHERE sender=? AND deliver_at_ms IS NOT NULL AND released_ms IS NULL AND cancelled_at IS NULL",e).n)}recipients(e,t,n,s){const i=n===null,a=R(d=>n?.allowed_recipients==null||n.allowed_recipients.includes(d),"allowed");if(s.room!==void 0)return this.room(t,s.room),!i&&!this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",t,s.room,e)&&_(403,"room_membership_required","Room messaging requires membership"),Array.isArray(s.to)?this.namedMembers(e,t,s.room,s.to,a):this.all("SELECT a.agent_id FROM room_members r JOIN agents a ON a.agent_id=r.agent_id WHERE r.lobby_id=? AND r.room_id=? AND a.enabled=1 AND a.agent_id<>? ORDER BY a.agent_id",t,s.room,e).map(d=>String(d.agent_id)).filter(d=>a(T(d).agent_id));if(s.to==="*")return n&&!n.can_broadcast&&_(403,"broadcast_forbidden","Broadcast permission is required"),this.all("SELECT agent_id FROM agents WHERE lobby_id=? AND enabled=1 AND agent_id<>? ORDER BY agent_id",t,e).map(d=>String(d.agent_id)).filter(d=>a(T(d).agent_id));const r=s.to;a(r)||_(403,"recipient_forbidden","Recipient is outside the allowed peer set");const o=D(t,r);return this.row(o).enabled||_(409,"recipient_disabled","Recipient is disabled"),[o]}namedMembers(e,t,n,s,i){const a=[],r=s.map(o=>{const c=D(t,o);c===e&&_(400,"recipient_is_sender","A room message never reaches its sender; leave yourself out of to");const d=this.one("SELECT a.enabled FROM room_members r JOIN agents a ON a.agent_id=r.agent_id WHERE r.lobby_id=? AND r.room_id=? AND r.agent_id=?",t,n,c);return d?d.enabled?i(o)||_(403,"recipient_forbidden",`${o} is outside the allowed peer set`):_(409,"recipient_disabled",`${o} is disabled`):a.push(o),c});return a.length&&_(409,"not_room_member",`${a.join(", ")} ${a.length===1?"is not a member":"are not members"} of room ${n}, so nothing was sent. Leave ${a.length===1?"that name":"them"} out, or message ${a.length===1?"it":"them"} directly; list_rooms shows the members`,{not_members:a}),r}admitSupersedes(e,t,n,s,i,a){xe.test(n)||_(400,"supersedes_not_message_id",`supersedes takes a message's full ID, a UUID such as 695912ea-d6cd-464d-8895-26e3d2af1c74, not ${JSON.stringify(n.slice(0,40))}. Nothing was sent`),i!==null&&i>a&&_(400,"supersedes_scheduled","A correction goes out now: send it without deliver_at, or change a scheduled message with update_scheduled. Nothing was sent");const r=this.one("SELECT * FROM messages WHERE id=? AND lobby_id=? AND sender=?",n,t,e);r||_(404,"supersedes_not_found","supersedes names no stored message of yours. One gone after its day cannot be marked: send the correction without supersedes. Nothing was sent"),r.cancelled_at!==null&&_(409,"supersedes_cancelled","That message was cancelled and reached no one. Nothing was sent"),r.deliver_at_ms!==null&&r.released_ms===null&&_(409,"supersedes_scheduled","That message has not been delivered yet: change it with update_scheduled or cancel it. Nothing was sent"),r.superseded_by!==null&&_(409,"already_superseded",`That message was already superseded by ${String(r.superseded_by)}; supersede that one instead. Nothing was sent`);const o=new Set(s),c=this.all("SELECT agent_id FROM deliveries WHERE message_id=? AND held_reason IS NULL ORDER BY agent_id",n).map(d=>String(d.agent_id)).filter(d=>!o.has(d));c.length&&_(409,"supersedes_audience",`A correction reaches everyone the earlier message reached; add ${c.map(d=>T(d).agent_id).join(", ")}. Nothing was sent`)}admitBytes(e,t,n){const s=R((i,...a)=>Number(this.one(i,...a).n)+n>this.maxStored,"over");s("SELECT COALESCE(SUM(byte_size),0) AS n FROM messages")&&s("SELECT COALESCE(SUM(byte_size),0) AS n FROM messages WHERE lobby_id=?",e)&&_(507,"storage_capacity","This lobby's stored message content capacity is reached");for(const i of t)Number(this.one("SELECT COALESCE(SUM(m.byte_size),0) AS n FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.seen_at IS NULL AND (d.held_reason IS NULL OR d.held_reason='scheduled')",i).n)+n>this.maxPending&&_(409,"inbox_capacity",`Recipient ${i} has insufficient inbox capacity`)}accept(e,t,n,s,i){const{now:a,deliverAt:r}=i,o=i.scheduleId!==void 0,c=this.recipients(e,t,n,s);s.reply_to&&!xe.test(s.reply_to)&&_(400,"reply_to_not_message_id",`reply_to takes a message's full ID, a UUID such as 695912ea-d6cd-464d-8895-26e3d2af1c74, not ${JSON.stringify(s.reply_to.slice(0,40))}. Nothing was sent; send again with the full ID and the same client_request_id`),s.supersedes!==void 0&&this.admitSupersedes(e,t,s.supersedes,c,r,a),s.reply_to&&this.one("SELECT 1 FROM messages WHERE id=?",s.reply_to)&&!this.one(`SELECT 1 FROM messages m WHERE m.id=?1 AND m.lobby_id=?2 AND (?3=1 OR m.sender=?4 OR EXISTS(SELECT 1 FROM deliveries d WHERE d.message_id=m.id AND d.agent_id=?4 AND d.held_reason IS NULL) OR ${_e("m","?4")})`,s.reply_to,t,+(n===null),e)&&_(404,"reply_not_found","Reply target is not accessible"),n!==null&&this.admitNewTasks(e,t,c,s),!o&&Number(this.one("SELECT COUNT(*) AS n FROM messages WHERE sender=? AND created_ms>? AND schedule_id IS NULL",e,a-6e4).n)>=60&&_(429,"send_rate_limit","At most 60 new messages per sender per minute");const d=Le(s.to),l=d?{...se(s),addressed_to:d}:se(s),m=Buffer.byteLength(s.content)+(l?Buffer.byteLength(JSON.stringify(l)):0);this.admitBytes(t,c,m);const N=this.resolveAttachments(e,t,s.attachments,a),g=!o&&r!==null&&r>a,E=r===null||g?null:a,h=o&&a-r>this.lateAfter,y=s.kind??null,U=Xe(s,e),A=O();this.run("INSERT INTO messages(id,sender,request_id,fingerprint,target,priority,content,correlation_id,reply_to,created_at,created_ms,byte_size,lobby_id,room_id,kind,reply_expected,details,notify_when_seen,deliver_at_ms,released_ms,late,schedule_id,supersedes) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",A,e,s.client_request_id,i.fingerprint,Array.isArray(s.to)?"*":s.to,s.priority,s.content,s.correlation_id??null,s.reply_to??null,new Date(a).toISOString(),a,m,t,s.room??null,y,U===null?null:Number(U),l?JSON.stringify(l):null,Number(s.notify_when_seen??!1),r,E,Number(h),i.scheduleId??null,s.supersedes??null),s.supersedes!==void 0&&this.run("UPDATE messages SET superseded_by=? WHERE id=?",A,s.supersedes);for(const b of c)this.run("INSERT INTO deliveries(message_id,agent_id,next_attempt_at,held_reason) VALUES(?,?,?,?)",A,b,g?r:a+(s.priority==="info"?this.infoDelay:0),g?w:null);return this.linkAttachments(e,N,"message",A,a),this.stored(this.one("SELECT * FROM messages WHERE id=?",A),!1)}lobbyAttachmentBytes(e){return Number(this.one("SELECT COALESCE(SUM(size),0) AS n FROM attachment_blobs WHERE sha256 IN (SELECT sha256 FROM attachments WHERE lobby_id=?1 AND expired_at IS NULL UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1)",e).n)}lobbyHolds(e,t){return!!this.one("SELECT 1 FROM attachments WHERE lobby_id=?1 AND sha256=?2 AND expired_at IS NULL UNION ALL SELECT 1 FROM attachment_uploads WHERE lobby_id=?1 AND sha256=?2 LIMIT 1",e,t)}kindLimit(e){return e==="image"?this.maxImageBytes:this.maxTextBytes}tooLarge(e,t,n){return _(413,"attachment_too_large",`${e} is ${v(t)}; this broker accepts ${n} attachments up to ${v(this.kindLimit(n))} (${ae[n]}). Nothing was stored.`)}quotaFull(e,t){return _(507,"storage_capacity",`Attachment storage for lobby ${e} is full: ${v(this.lobbyAttachmentBytes(e))} of ${v(this.maxLobbyAttachments)} used and ${v(t)} more needed (${ae.lobby}). Nothing was stored; ask the owner, or wait for retention to free space.`)}resolveAttachments(e,t,n,s){if(!n?.length)return[];for(const r of n)r.size>this.kindLimit(Z(r.mime_type))&&this.tooLarge(r.filename,r.size,Z(r.mime_type));const i=[],a=new Map;for(const r of n){const o=this.one("SELECT * FROM attachments WHERE uploader=? AND sha256=? AND filename=? AND expired_at IS NULL",e,r.sha256,r.filename),c=this.one("SELECT * FROM attachment_blobs WHERE sha256=?",r.sha256);if(!(o??this.one("SELECT 1 FROM attachment_uploads WHERE uploader=? AND sha256=?",e,r.sha256)??this.one("SELECT 1 FROM attachments WHERE uploader=? AND sha256=? AND expired_at IS NULL",e,r.sha256))||!c){a.set(r.sha256,r.size);continue}(Number(c.size)!==r.size||c.kind!==Z(r.mime_type)||c.kind==="image"&&c.mime_type!==r.mime_type||o&&o.mime_type!==r.mime_type)&&_(400,"attachment_mismatch",`${r.filename} does not match the uploaded content with that SHA-256 (size or type differs). Nothing was stored.`),i.push(o?String(o.id):r)}if(a.size){const r=[...a].filter(([o])=>!this.lobbyHolds(t,o)).reduce((o,[,c])=>o+c,0);this.lobbyAttachmentBytes(t)+r>this.maxLobbyAttachments&&this.quotaFull(t,r),_(409,"attachment_upload_required",`Upload ${a.size} attached file${a.size===1?"":"s"} first, then repeat this request unchanged. Nothing was stored.`,{missing:[...a.keys()]})}return i.map(r=>{if(typeof r=="string")return r;const o=O();return this.run("INSERT INTO attachments(id,lobby_id,uploader,filename,mime_type,size,sha256,created_at,created_ms) VALUES(?,?,?,?,?,?,?,?,?)",o,t,e,r.filename,r.mime_type,r.size,r.sha256,new Date(s).toISOString(),s),o})}linkAttachments(e,t,n,s,i){t.forEach((a,r)=>{this.run(n==="message"?"INSERT INTO message_attachments(message_id,position,attachment_id) VALUES(?,?,?)":"INSERT INTO schedule_attachments(schedule_id,position,attachment_id) VALUES(?,?,?)",s,r,a),this.run("UPDATE attachments SET last_referenced_ms=MAX(COALESCE(last_referenced_ms,0),?) WHERE id=?",i,a),this.run("DELETE FROM attachment_uploads WHERE uploader=? AND sha256=(SELECT sha256 FROM attachments WHERE id=?)",e,a)})}attachmentsPath(){return this.attachmentsDirectory||(this.attachmentsDirectory=Je(X(Qe(),"pocolobby-attachments-"))),Ne(this.attachmentsDirectory,{recursive:!0,mode:448}),this.attachmentsDirectory}blobPath(e){return X(this.attachmentsPath(),e)}uploadAttachment(e,t,n,s){e=this.key(e),/^[0-9a-f]{64}$/u.test(n)||_(400,"invalid_request","Name the upload by its lowercase hex SHA-256"),x("sha256").update(s).digest("hex")!==n&&_(400,"attachment_hash_mismatch","The uploaded bytes do not have that SHA-256");const i=ot(s);if(!i)return _(415,"attachment_type","Attach PNG, JPEG, GIF or WebP images, or UTF-8 text documents");const a=i;s.length>this.kindLimit(a)&&this.tooLarge("The upload",s.length,a);const r=a==="image"?dt(s):"text/plain",o=this.clock();return this.transaction(()=>{this.assertRuntime(e,t);const c=String(this.row(e).lobby_id);!this.lobbyHolds(c,n)&&this.lobbyAttachmentBytes(c)+s.length>this.maxLobbyAttachments&&this.quotaFull(c,s.length);const d=this.blobPath(n);return j(d,{throwIfNoEntry:!1})?.size===s.length||(this.freeDisk(this.attachmentsPath())-s.length<this.diskReserve&&_(507,"storage_capacity",`The broker's disk is nearly full: storing ${v(s.length)} would leave less than the ${v(this.diskReserve)} reserved for the database (${ae.reserve}). Nothing was stored; the owner must free or extend the volume.`),this.writeBlob(d,s)),this.run("INSERT OR IGNORE INTO attachment_blobs(sha256,size,kind,mime_type,created_at) VALUES(?,?,?,?,?)",n,s.length,a,r,new Date(o).toISOString()),this.run("INSERT INTO attachment_uploads(uploader,sha256,lobby_id,created_ms) VALUES(?,?,?,?) ON CONFLICT(uploader,sha256) DO UPDATE SET created_ms=excluded.created_ms",e,n,c,o),{sha256:n,size:s.length,kind:a,mime_type:r}})}writeBlob(e,t){const n=`${e}.${O()}.tmp`;try{const s=Te(n,"wx",384);try{let i=0;for(;i<t.length;)i+=je(s,t,i,t.length-i);Ge(s)}finally{be(s)}Ke(n,e)}catch(s){try{Q(n)}catch{}throw s.code==="ENOSPC"&&_(507,"storage_capacity","The broker's disk is full; nothing was stored"),s}}accessibleAttachment(e,t){e=this.key(e);const n=T(e).lobby_id;return this.one(`SELECT a.* FROM attachments a WHERE a.id=?1 AND a.lobby_id=?2 AND (a.uploader=?3 OR EXISTS (SELECT 1 FROM message_attachments m JOIN deliveries d ON d.message_id=m.message_id WHERE m.attachment_id=a.id AND d.agent_id=?3 AND d.held_reason IS NULL) OR EXISTS (SELECT 1 FROM message_attachments m JOIN messages g ON g.id=m.message_id WHERE m.attachment_id=a.id AND ${_e("g","?3")}))`,t,n,e)??_(404,"attachment_not_found","No attachment with that ID is available to you")}attachment(e,t){const n=this.accessibleAttachment(e,t);return{...this.attachmentInfo(n),from:T(String(n.uploader)).agent_id,created_at:String(n.created_at)}}attachmentFile(e,t){const n=this.accessibleAttachment(e,t),s=this.attachmentInfo(n),i=R(()=>_(410,"attachment_expired",`${s.filename} is no longer stored: content is kept for a limited time after its last message. Ask the sender to attach it again.`),"gone");s.expired_at&&i();const a=this.blobPath(s.sha256),r=j(a,{throwIfNoEntry:!1})?.size;return r===void 0?i():{info:s,path:a,size:r}}attachments(e,t={}){const n=this.requireLobby(e),s=t.cursor??0,i=t.limit??50,a=this.all("SELECT * FROM attachments WHERE lobby_id=? AND sequence>? ORDER BY sequence LIMIT ?",n,s,i+1),r=a.slice(0,i);return{items:r.map(o=>({...this.attachmentInfo(o),lobby_id:n,uploader:T(String(o.uploader)).agent_id,created_at:String(o.created_at),last_referenced_at:o.last_referenced_ms===null?null:new Date(Number(o.last_referenced_ms)).toISOString(),messages:this.all("SELECT message_id FROM message_attachments WHERE attachment_id=? ORDER BY rowid",String(o.id)).map(c=>String(c.message_id)),schedules:this.all("SELECT schedule_id FROM schedule_attachments WHERE attachment_id=? ORDER BY rowid",String(o.id)).map(c=>String(c.schedule_id))})),next_cursor:a.length>i?Number(r.at(-1).sequence):null}}attachmentUsage(e){const t=this.requireLobby(e),n=R(i=>Number(this.one(i,t).n),"count");let s=null;try{s={free_bytes:this.freeDisk(this.attachmentsPath()),reserve_bytes:this.diskReserve}}catch{}return{lobby_id:t,used_bytes:this.lobbyAttachmentBytes(t),quota_bytes:this.maxLobbyAttachments,files:n("SELECT COUNT(*) AS n FROM (SELECT sha256 FROM attachments WHERE lobby_id=?1 AND expired_at IS NULL UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1)"),attachments:n("SELECT COUNT(*) AS n FROM attachments WHERE lobby_id=? AND expired_at IS NULL"),expired:n("SELECT COUNT(*) AS n FROM attachments WHERE lobby_id=? AND expired_at IS NOT NULL"),pending_uploads:n("SELECT COUNT(*) AS n FROM attachment_uploads WHERE lobby_id=?"),limits:{max_image_bytes:this.maxImageBytes,max_text_bytes:this.maxTextBytes,retention_days:this.attachmentRetention/(1440*60*1e3),upload_grace_seconds:this.attachmentGrace/1e3},disk:s}}healthy(){this.one("SELECT 1")}pruneNotificationAttempts(e=this.clock()){return this.run("DELETE FROM notification_attempts WHERE created_at<? AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.last_notification_id=notification_attempts.id AND d.seen_at IS NULL)",new Date(e-1440*60*1e3).toISOString())}pruneMessages(){const e=this.clock()-Yt,t=new Date(e).toISOString(),n="SELECT id FROM messages WHERE (cancelled_at IS NULL AND COALESCE(released_ms,created_ms)<?1 AND (deliver_at_ms IS NULL OR released_ms IS NOT NULL)) OR cancelled_at<?2";return!this.one(`${n} LIMIT 1`,e,t)&&!this.one("SELECT 1 FROM schedules WHERE cancelled_at<? LIMIT 1",t)?0:this.transaction(()=>{for(const i of["seen_receipts","message_attachments","deliveries"])this.run(`DELETE FROM ${i} WHERE message_id IN (${n})`,e,t);const s=this.run(`DELETE FROM messages WHERE id IN (${n})`,e,t);return this.run("DELETE FROM schedule_attachments WHERE schedule_id IN (SELECT id FROM schedules WHERE cancelled_at<?)",t),this.run("DELETE FROM schedules WHERE cancelled_at<?",t),s})}pruneAdminEvents(){let e=0;for(const t of this.all("SELECT DISTINCT lobby_id FROM admin_events")){const n=t.lobby_id,s=this.one("SELECT sequence FROM admin_events WHERE lobby_id IS ? ORDER BY sequence DESC LIMIT 1 OFFSET ?",n,Xt);s&&(e+=this.transaction(()=>this.run("DELETE FROM admin_events WHERE lobby_id IS ? AND sequence<=?",n,Number(s.sequence))))}return e}collectAttachments(){const e=this.clock(),t=e-this.attachmentRetention,n=e-this.attachmentGrace,s=new Date(e-Bt).toISOString(),i="expired_at<? AND NOT EXISTS (SELECT 1 FROM message_attachments m WHERE m.attachment_id=attachments.id) AND NOT EXISTS (SELECT 1 FROM schedule_attachments s WHERE s.attachment_id=attachments.id)",a="expired_at IS NULL AND COALESCE(last_referenced_ms,created_ms)<? AND NOT EXISTS (SELECT 1 FROM schedule_attachments sa JOIN schedules s ON s.id=sa.schedule_id WHERE sa.attachment_id=attachments.id AND s.cancelled_at IS NULL) AND NOT EXISTS (SELECT 1 FROM message_attachments ma JOIN messages m ON m.id=ma.message_id WHERE ma.attachment_id=attachments.id AND m.deliver_at_ms IS NOT NULL AND m.released_ms IS NULL AND m.cancelled_at IS NULL)",o=this.one(`SELECT 1 FROM attachments WHERE ${a} LIMIT 1`,t)??this.one("SELECT 1 FROM attachment_uploads WHERE created_ms<? LIMIT 1",n)??this.one(`SELECT 1 FROM attachments WHERE ${i} LIMIT 1`,s)?this.transaction(()=>{const m=this.all(`SELECT id FROM attachments WHERE ${a}`,t);for(const E of m)this.run("UPDATE attachments SET expired_at=? WHERE id=?",this.iso(),String(E.id));this.run(`DELETE FROM attachments WHERE ${i}`,s);const N=Number(this.one("SELECT COUNT(*) AS n FROM attachment_uploads WHERE created_ms<?",n).n);this.run("DELETE FROM attachment_uploads WHERE created_ms<?",n);const g=this.all("SELECT sha256 FROM attachment_blobs b WHERE NOT EXISTS (SELECT 1 FROM attachments a WHERE a.sha256=b.sha256 AND a.expired_at IS NULL) AND NOT EXISTS (SELECT 1 FROM attachment_uploads u WHERE u.sha256=b.sha256)").map(E=>String(E.sha256));for(const E of g)this.run("DELETE FROM attachment_blobs WHERE sha256=?",E);return{expired:m.length,uploads:N,unreferenced:g}}):{expired:0,uploads:0,unreferenced:[]};let c=0;if(!this.attachmentsDirectory)return{expired:o.expired,uploads:o.uploads,files:c};const d=this.attachmentsPath(),l=R(m=>/^[0-9a-f]{64}$/u.test(m)&&!!this.one("SELECT 1 FROM attachment_blobs WHERE sha256=?",m),"known");for(const m of Ve(d)){const N=X(d,m);try{const g=j(N),E=o.unreferenced.includes(m)||!l(m)&&Date.now()-g.mtimeMs>this.attachmentGrace;g.isFile()&&E&&(Q(N),c++)}catch{}}return{expired:o.expired,uploads:o.uploads,files:c}}messageLines(e,t={}){t.mode!=="history"&&_(400,"headers_need_history","headers work in history mode only");const n=this.messages(e,t);return{items:n.items.map(Qt),next_cursor:n.next_cursor}}messages(e,t={}){e=this.key(e);const n=T(e);n.agent_id!=="owner"?this.row(e):this.requireLobby(n.lobby_id);const s=t.mode??"pending",i=t.cursor??0,a=t.limit??25;(!Number.isSafeInteger(i)||i<0||!Number.isSafeInteger(a)||a<1||a>100)&&_(400,"invalid_pagination","Use a nonnegative cursor and a limit between 1 and 100"),t.query!==void 0&&s!=="history"&&_(400,"query_needs_history","Search works in history mode only"),t.unanswered&&(s!=="history"||n.agent_id==="owner")&&_(400,"unanswered_needs_history","unanswered lists an agent's own history only"),t.awaiting&&(s!=="history"||n.agent_id==="owner")&&_(400,"awaiting_needs_history","awaiting lists an agent's own history only"),t.awaiting&&t.unanswered&&_(400,"awaiting_or_unanswered","Ask for awaiting or unanswered, one at a time"),t.ids!==void 0&&s!=="history"&&_(400,"ids_need_history","ids work in history mode only");const r=t.unanswered?" AND m.sender<>?2 AND m.sender<>(?4 || '/owner') AND d.agent_id=?2 AND d.held_reason IS NULL AND m.reply_expected=1 AND m.superseded_by IS NULL AND NOT EXISTS(SELECT 1 FROM messages r JOIN deliveries rd ON rd.message_id=r.id WHERE r.sender=?2 AND r.reply_to=m.id AND r.cancelled_at IS NULL AND (r.deliver_at_ms IS NULL OR r.released_ms IS NOT NULL) AND rd.agent_id=m.sender AND rd.held_reason IS NULL)":"",o=t.awaiting?" AND m.sender=?2 AND m.reply_expected=1 AND m.superseded_by IS NULL AND m.cancelled_at IS NULL AND (m.deliver_at_ms IS NULL OR m.released_ms IS NOT NULL) AND NOT EXISTS(SELECT 1 FROM messages r JOIN deliveries rd ON rd.message_id=r.id WHERE r.reply_to=m.id AND r.sender<>?2 AND r.cancelled_at IS NULL AND (r.deliver_at_ms IS NULL OR r.released_ms IS NOT NULL) AND rd.agent_id=?2 AND rd.held_reason IS NULL)":"",c=t.ids===void 0?null:JSON.stringify(t.ids.map(L=>L.toLowerCase())),d=R(L=>c===null?"":` AND EXISTS(SELECT 1 FROM json_each(?${L}) j WHERE m.id LIKE j.value || '%')`,"byId"),l=c===null?"":` OR ${_e("m","?2")}`,m=c===null?[]:[c],N=t.query===void 0?null:`%${t.query.replace(/[\\%_]/g,"\\$&")}%`,g="(m.content LIKE ?1 ESCAPE '\\' OR m.details LIKE ?1 ESCAPE '\\' OR EXISTS(SELECT 1 FROM message_attachments x JOIN attachments a ON a.id=x.attachment_id WHERE x.message_id=m.id AND a.filename LIKE ?1 ESCAPE '\\'))";let E;if(s==="history")E=n.agent_id==="owner"?this.all(`SELECT * FROM messages m WHERE m.lobby_id=?2 AND m.sequence>?3${N===null?"":` AND ${g}`}${d(5)} ORDER BY m.sequence LIMIT ?4`,N,n.lobby_id,i,a+1,...m):this.all(`SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason,d.agent_id AS delivery_recipient_id FROM messages m LEFT JOIN deliveries d ON d.message_id=m.id AND d.agent_id=?2 WHERE m.sequence>?3 AND m.lobby_id=?4 AND (m.sender=?2 OR (d.agent_id=?2 AND COALESCE(d.held_reason,'') NOT IN ('scheduled','cancelled') AND (m.deliver_at_ms IS NULL OR m.released_ms IS NOT NULL))${l})${N===null?"":` AND (m.sender=?2 OR d.held_reason IS NULL) AND ${g}`}${r}${o}${d(6)} ORDER BY m.sequence LIMIT ?5`,N,e,i,T(e).lobby_id,a+1,...m);else if(s==="reconciliation"){const L=Math.max(i,this.readCheckpoint(e).covered_receipt_seq);E=this.all("SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.receipt_seq>? ORDER BY d.receipt_seq LIMIT ?",e,L,a+1)}else E=this.all("SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.seen_at IS NULL AND d.held_reason IS NULL AND m.sequence>? ORDER BY m.sequence LIMIT ?",e,i,a+1);const h=E.slice(0,a),y=h.map(L=>this.message(L,t.includeHeldContent));if(s==="history"){const L=new Set(h.filter(S=>n.agent_id==="owner"||S.sender===e).map(S=>String(S.id))),p=this.recipientDeliveries([...L]);for(const S of y)L.has(S.id)&&(S.deliveries=p.get(S.id)??[])}let U=mt,A=0;for(const L of y){const p=Buffer.byteLength(JSON.stringify(L));if(A>0&&p>U)break;A++,U-=p}const b=E.length>A;return{items:y.slice(0,A),next_cursor:b?Number(h[A-1][s==="reconciliation"?"receipt_seq":"sequence"]):null}}acknowledgeInner(e,t){e=this.key(e),t.length>100&&_(400,"ack_limit","At most 100 receipts per operation");const n=[...new Set(t)],s=[];for(const a of n){const r=this.one("SELECT held_reason FROM deliveries WHERE agent_id=? AND message_id=?",e,a);!r&&this.one("SELECT 1 FROM messages WHERE id=?",a)&&_(403,"receipt_forbidden","A receipt can only name messages in your own inbox"),r||s.push(a),r?.held_reason===w&&_(409,"not_yet_delivered",`Message ${a} is scheduled and not yet delivered; acknowledge it once it arrives`)}let i=Number(this.row(e).receipt_seq);for(const a of n){const r=this.one("SELECT d.seen_at,d.held_reason,m.sender,m.notify_when_seen FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.message_id=?",e,a);if(!r||r.seen_at!==null||r.held_reason!==null)continue;const o=this.iso();this.run("UPDATE deliveries SET seen_at=?,receipt_seq=? WHERE agent_id=? AND message_id=?",o,++i,e,a);const c=String(r.sender);r.notify_when_seen&&c!==e&&T(c).agent_id!=="owner"&&this.run("INSERT OR IGNORE INTO seen_receipts(sender,message_id,agent_id,seen_at,seen_ms) VALUES(?,?,?,?,?)",c,a,e,o,this.clock())}return this.run("UPDATE agents SET receipt_seq=?,last_agent_seen_at=? WHERE agent_id=?",i,this.iso(),e),{snapshot:i,unknown:s}}acknowledge(e,t,n){return e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t);const{snapshot:s,unknown:i}=this.acknowledgeInner(e,n);return{receipt_snapshot:s,...this.unknownIds(e,i)}})}unknownIds(e,t){if(!t.length)return{};const n=t.filter(s=>this.one("SELECT 1 FROM notification_attempts WHERE id=? AND agent_id=?",s,e));return{unknown_message_ids:t,...n.length?{note:`${n.join(", ")} ${n.length===1?"is a notification_id":"are notification_ids"}, not message IDs: acknowledge the id of each message you have read, from the event's messages or get_messages.`}:{}}}checkpointView(e){return e=this.key(e),{checkpoint:this.readCheckpoint(e),receipt_snapshot:Number(this.row(e).receipt_seq)}}readCheckpoint(e){e=this.key(e);const t=this.one("SELECT * FROM checkpoints WHERE agent_id=?",e)??_(404,"agent_not_found","Unknown agent");return{revision:Number(t.revision),content:String(t.content),covered_receipt_seq:Number(t.covered_receipt_seq),updated_at:t.updated_at,runtime_id:t.runtime_id}}context(e){e=this.key(e);const t=this.row(e),n=this.readCheckpoint(e),s=Number(t.receipt_seq),i=this.messages(e),a=this.messages(e,{mode:"reconciliation",limit:Pt}),r=Number(this.one("SELECT COUNT(*) AS n FROM deliveries WHERE agent_id=? AND receipt_seq>?",e,n.covered_receipt_seq).n),o=a.next_cursor===null?[]:this.all("SELECT m.id,m.sender,m.target,m.details,m.room_id,m.priority,m.kind,m.created_at,m.supersedes,m.superseded_by,d.seen_at,d.receipt_seq FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE d.agent_id=? AND d.receipt_seq>? ORDER BY d.receipt_seq LIMIT ?",e,a.next_cursor,$t).map(l=>({id:String(l.id),from:T(String(l.sender)).agent_id,to:String(l.target),...P(l.details),room_id:l.room_id,priority:l.priority,kind:l.kind??null,created_at:String(l.created_at),agent_seen_at:l.seen_at,receipt_seq:Number(l.receipt_seq),...ce(l)})),c=[],d=R(l=>`${l} acknowledged message${l===1?"":"s"}`,"counted");return r>0&&n.revision===0?c.push(`You have ${d(r)} but have never saved a checkpoint, so every bootstrap lists ${r===1?"it":"them"} again for reconciliation. ${t.ready?"This session already completed its bootstrap, so do not call ready: ready answers only a bootstrap event. After reconciling, call":"After reconciling and calling ready (a checkpoint saved before ready makes ready stale), call"} save_checkpoint with expected_revision 0 and covered_receipt_seq ${s}, keeping unfinished work and uncertain effects in its content.`):r>0&&c.push(`${d(r)} ${r===1?"is":"are"} not covered by checkpoint revision ${n.revision}. ${t.ready?"This session already completed its bootstrap, so do not call ready: ready answers only a bootstrap event. After reconciling, s":"After reconciling and calling ready, s"}ave a checkpoint with covered_receipt_seq ${s} so later bootstraps skip ${r===1?"it":"them"}.`),a.next_cursor!==null&&c.push(`Reconciliation shows the first ${a.items.length} with bodies${o.length?` and headers of ${o.length} more`:""}. Read the rest with get_messages(mode="reconciliation", cursor=${a.next_cursor}) only where you need them.`),i.next_cursor!==null&&c.push(`More pending messages follow; read get_messages(mode="pending", cursor=${i.next_cursor}).`),{agent_id:T(e).agent_id,lobby_id:String(t.lobby_id),runtime_id:t.runtime_id,role:String(t.role),work_description:String(t.work_description),machine:String(t.machine),screen:!!t.screen,memory:this.memoryIndex(e),checkpoint:n,receipt_snapshot:s,pending:i,reconciliation:{...a,total:r,headers:o},scheduled:{schedules:Number(this.one("SELECT COUNT(*) AS n FROM schedules WHERE sender=? AND cancelled_at IS NULL",e).n),messages:this.undeliveredCount(e)},guidance:c}}checkpoint(e,t,n){e=this.key(e);const s=_t.parse(n);return this.transaction(()=>{this.assertRuntime(e,t);const i=this.readCheckpoint(e),a=Number(this.row(e).receipt_seq);i.revision!==s.expected_revision&&_(409,"checkpoint_conflict",`Checkpoint revision changed: the current revision is ${i.revision}, not ${s.expected_revision}. Read it with read_checkpoint before saving over it`),(s.covered_receipt_seq<i.covered_receipt_seq||s.covered_receipt_seq>a)&&_(409,"receipt_snapshot_conflict",`Covered receipts must be an existing, nondecreasing snapshot: covered_receipt_seq from ${i.covered_receipt_seq} to ${a}, not ${s.covered_receipt_seq}`);const r=s.acknowledge_message_ids.some(l=>this.one("SELECT 1 FROM deliveries WHERE agent_id=? AND message_id=? AND seen_at IS NULL AND held_reason IS NULL",e,l));r&&s.covered_receipt_seq!==a&&_(409,"receipt_snapshot_conflict",`Reconcile existing receipts before covering newly acknowledged messages: covered_receipt_seq must be the current snapshot, ${a}`);const{snapshot:o,unknown:c}=this.acknowledgeInner(e,s.acknowledge_message_ids),d=r?o:s.covered_receipt_seq;return this.run("UPDATE checkpoints SET revision=revision+1,content=?,covered_receipt_seq=?,updated_at=?,runtime_id=? WHERE agent_id=?",s.content,d,this.iso(),t.runtime_id,e),{...this.readCheckpoint(e),...this.unknownIds(e,c)}})}nextNotification(e,t,n=pe){e=this.key(e);const s=this.clock(),i=this.one("SELECT paused,pacing,ready,bootstrap_attempts,bootstrap_next_at FROM agents WHERE agent_id=?",e);return i&&(F(i)||(Number(i.ready)?!this.one("SELECT 1 FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts<? AND next_attempt_at<=? LIMIT 1",e,this.retries.length,s):Number(i.bootstrap_attempts)>=this.retries.length||Number(i.bootstrap_next_at)>s))?null:this.transaction(()=>{this.assertRuntime(e,t);const a=this.row(e);if(F(a))return null;const r=this.clock();if(Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(r-6e4).toISOString()).n)>=this.notificationRate)return null;const c=Number(this.one("SELECT COUNT(*) AS n FROM deliveries WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL",e).n),d={type:a.ready?"inbox":"bootstrap",notification_id:O(),agent_id:T(e).agent_id,lobby_id:String(a.lobby_id),runtime_id:t.runtime_id,generation:t.generation,messages:[],pending_count:c,truncated:!1};if(a.ready){for(let E=this.retries.length-1;E>=1;E--)this.run("UPDATE deliveries SET attempts=attempts+1,next_attempt_at=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts=? AND next_attempt_at<=? AND EXISTS (SELECT 1 FROM notification_attempts n WHERE n.id=deliveries.last_notification_id AND n.agent_id=deliveries.agent_id AND n.runtime_id=? AND n.generation=? AND n.incarnation=? AND n.notification_attempted_at IS NOT NULL)",r+this.retries[E],e,E,r,t.runtime_id,t.generation,t.incarnation);const l="d.agent_id=? AND d.seen_at IS NULL AND d.held_reason IS NULL AND d.attempts<? AND d.next_attempt_at<=?",m=this.all(`SELECT m.*,d.seen_at,d.receipt_seq,d.held_reason,d.attempts FROM deliveries d JOIN messages m ON m.id=d.message_id WHERE ${l} ORDER BY (m.priority='important') DESC,m.sequence LIMIT 21`,e,this.retries.length,r);if(!m.length)return null;const N=R(E=>Number(E.sequence)<=Number(a.backlog_sequence)&&(E.released_ms===null||Number(E.released_ms)<=Number(a.backlog_at)),"backlog"),g=m.length>20||m.some(E=>Number(E.attempts)>0||N(E));d.messages=g?[]:m.map(E=>this.message(E)),d.truncated=g,Buffer.byteLength(JSON.stringify(d))>n&&(d.messages=[],d.truncated=!0),d.truncated&&(d.covered=m.slice(0,20).map(E=>this.header(E)),d.covered_count=Number(this.one(`SELECT COUNT(*) AS n FROM deliveries d WHERE ${l}`,e,this.retries.length,r).n));for(let E=this.retries.length-1;E>=0;E--)this.run("UPDATE deliveries SET attempts=attempts+1,next_attempt_at=?,last_notification_id=? WHERE agent_id=? AND seen_at IS NULL AND held_reason IS NULL AND attempts=? AND next_attempt_at<=?",r+this.retries[E],d.notification_id,e,E,r)}else{if(Number(a.bootstrap_attempts)>=this.retries.length||Number(a.bootstrap_next_at)>r)return null;d.probe_nonce=String(a.probe_nonce),this.run("UPDATE agents SET bootstrap_attempts=bootstrap_attempts+1,bootstrap_next_at=? WHERE agent_id=?",r+this.bootstrapRetry,e)}return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",d.notification_id,e,t.runtime_id,t.generation,t.incarnation,d.type,this.iso()),d})}recordTransport(e,t,n){e=this.key(e),this.transaction(()=>{this.assertRuntime(e,t);const s=this.one("SELECT * FROM notification_attempts WHERE id=? AND agent_id=? AND runtime_id=? AND generation=? AND incarnation=?",n.notification_id,e,t.runtime_id,t.generation,t.incarnation);s||_(404,"notification_not_found","Notification is not owned by this runtime");const i=n.type==="adapter_received"?"adapter_received_at":"notification_attempted_at";if(s[i]!==null)return;const a=this.iso();this.run(`UPDATE notification_attempts SET ${i}=? WHERE id=?`,a,n.notification_id),i==="notification_attempted_at"&&this.run("UPDATE deliveries SET notification_attempted_at=? WHERE agent_id=? AND last_notification_id=?",a,e,n.notification_id)})}releaseDue(){const e=this.clock();let t=0,n=0;const s=this.all("SELECT id FROM messages WHERE deliver_at_ms IS NOT NULL AND released_ms IS NULL AND cancelled_at IS NULL AND deliver_at_ms<=? ORDER BY deliver_at_ms,sequence LIMIT 100",e).map(a=>String(a.id));s.length&&this.transaction(()=>{for(const a of s)this.release(a,e)&&t++});const i=this.all("SELECT id FROM schedules WHERE cancelled_at IS NULL AND next_run_ms IS NOT NULL AND next_run_ms<=? ORDER BY next_run_ms LIMIT 50",e).map(a=>String(a.id));for(const a of i)this.runSchedule(a,e)&&n++;return{released:t,occurrences:n}}release(e,t){const n=this.one("SELECT * FROM messages WHERE id=? AND released_ms IS NULL AND cancelled_at IS NULL",e);if(!n)return!1;const s=this.one("SELECT enabled FROM agents WHERE agent_id=?",String(n.sender));if(s&&!s.enabled)return this.cancelMessage(e),!1;const i=t-Math.max(Number(n.deliver_at_ms),Number(n.created_ms))>this.lateAfter;return this.run("UPDATE messages SET released_ms=?,late=? WHERE id=?",t,Number(i),e),this.run("UPDATE attachments SET last_referenced_ms=MAX(COALESCE(last_referenced_ms,0),?) WHERE id IN (SELECT attachment_id FROM message_attachments WHERE message_id=?)",t,e),this.run("UPDATE deliveries SET held_reason=NULL,attempts=0,next_attempt_at=? WHERE message_id=? AND held_reason=?",t+(n.priority==="info"?this.infoDelay:0),e,w),!0}cancelMessage(e){this.run("UPDATE messages SET cancelled_at=? WHERE id=?",this.iso(),e),this.run("UPDATE deliveries SET held_reason=? WHERE message_id=? AND held_reason=?",Fe,e,w)}runSchedule(e,t){const n=this.one("SELECT * FROM schedules WHERE id=? AND cancelled_at IS NULL",e);if(!n||n.next_run_ms===null||Number(n.next_run_ms)>t)return!1;const s=this.repeatOf(n),i=Ht(s,Number(n.next_run_ms),t),a=ie(s,t),r=new Date(i).toISOString();try{return this.transaction(()=>{const o=String(n.sender),c=this.agent(o);c.enabled||_(403,"agent_disabled","Sender is disabled");const d=fe.parse({client_request_id:`${W}schedule:${e}:${i}`,...JSON.parse(String(n.template))}),l=this.accept(o,String(n.lobby_id),c,d,{fingerprint:H(JSON.stringify(te(d))),now:t,deliverAt:i,scheduleId:e});return this.run("UPDATE schedules SET next_run_ms=?,occurrence_count=occurrence_count+1,last_occurrence_at=?,last_message_id=?,last_error=NULL WHERE id=?",a,r,l.message.id,e),!0})}catch(o){const c=o instanceof Se?o.code:"internal_error";return this.transaction(()=>this.run("UPDATE schedules SET next_run_ms=?,last_occurrence_at=?,last_error=? WHERE id=? AND cancelled_at IS NULL",a,r,c,e)),!1}}repeatOf(e){return{time_zone:String(e.time_zone),times:JSON.parse(String(e.times)),weekdays:e.weekdays===null?null:JSON.parse(String(e.weekdays))}}schedule(e){const t=JSON.parse(String(e.template)),n=this.repeatOf(e),s=t.kind??null,i=e.next_run_ms===null?null:Number(e.next_run_ms);return{schedule_id:String(e.id),lobby_id:String(e.lobby_id),from:T(String(e.sender)).agent_id,client_request_id:String(e.request_id),to:Array.isArray(t.to)?"*":t.to,...Array.isArray(t.to)?{addressed_to:Le(t.to)}:{},room_id:t.room??null,priority:t.priority,content:t.content,correlation_id:t.correlation_id??null,kind:s,reply_expected:Xe(t,String(e.sender)),...se(t),...t.notify_when_seen?{notify_when_seen:!0}:{},...(()=>{const a=this.all("SELECT a.* FROM schedule_attachments s JOIN attachments a ON a.id=s.attachment_id WHERE s.schedule_id=? ORDER BY s.position",String(e.id)).map(r=>this.attachmentInfo(r));return a.length?{attachments:a}:{}})(),repeat:n,created_at:String(e.created_at),next_occurrence_at:i===null?null:new Date(i).toISOString(),next_occurrence_local:i===null?null:Wt(i,n.time_zone),occurrence_count:Number(e.occurrence_count),last_occurrence_at:e.last_occurrence_at,last_message_id:e.last_message_id,last_error:e.last_error,cancelled_at:e.cancelled_at}}createSchedule(e,t,n){e=this.key(e);const s=ht.parse(n),{repeat:i,client_request_id:a,...r}=s,o=De(i),c=H(JSON.stringify({...te({client_request_id:a,...r}),repeat:o}));return this.transaction(()=>{this.assertRuntime(e,t);const d=this.agent(e),l=this.one("SELECT * FROM schedules WHERE sender=? AND request_id=?",e,a);if(l)return l.fingerprint!==c&&_(409,"idempotency_conflict","Retry key was already used for a different request"),{status:"scheduled",schedule:this.schedule(l),duplicate:!0};a.startsWith(W)&&_(400,"reserved_request_id",`Request IDs starting with ${W} are reserved`),re(s.content),Number(this.one("SELECT COUNT(*) AS n FROM schedules WHERE sender=? AND cancelled_at IS NULL",e).n)>=this.maxSchedules&&_(409,"schedule_capacity",`At most ${this.maxSchedules} active schedules per agent`),this.recipients(e,d.lobby_id,d,r);const m=this.clock(),N=this.resolveAttachments(e,d.lobby_id,r.attachments,m),g=O();return this.run("INSERT INTO schedules(id,sender,lobby_id,request_id,fingerprint,template,time_zone,times,weekdays,created_at,next_run_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?)",g,e,d.lobby_id,a,c,JSON.stringify(r),o.time_zone,JSON.stringify(o.times),o.weekdays===null?null:JSON.stringify(o.weekdays),new Date(m).toISOString(),ie(o,m)),this.linkAttachments(e,N,"schedule",g,m),{status:"scheduled",schedule:this.schedule(this.one("SELECT * FROM schedules WHERE id=?",g)),duplicate:!1}})}scheduled(e){e=this.key(e);const t=T(e);return t.agent_id!=="owner"?this.row(e):this.requireLobby(t.lobby_id),{schedules:this.all("SELECT * FROM schedules WHERE sender=? AND cancelled_at IS NULL ORDER BY next_run_ms,created_at",e).map(n=>V(this.schedule(n))),messages:this.all("SELECT m.*,(SELECT COUNT(*) FROM deliveries d WHERE d.message_id=m.id) AS recipient_count FROM messages m INDEXED BY messages_due WHERE m.sender=? AND m.deliver_at_ms IS NOT NULL AND m.released_ms IS NULL AND m.cancelled_at IS NULL ORDER BY m.deliver_at_ms,m.sequence",e).map(n=>({...V(this.message(n)),recipients:Number(n.recipient_count)}))}}updateScheduled(e,t,n){e=this.key(e);const s=ft.parse(n);return this.transaction(()=>{this.assertRuntime(e,t),s.content!==void 0&&re(s.content);const i=this.clock(),a=this.one("SELECT * FROM schedules WHERE id=? AND sender=?",s.id,e);if(a){a.cancelled_at!==null&&_(409,"already_cancelled","The schedule was cancelled; create a new one"),s.deliver_at!==void 0&&_(400,"invalid_request","A schedule repeats: change its repeat, not deliver_at");const m=JSON.parse(String(a.template));s.content!==void 0&&(m.content=s.content);const N=this.repeatOf(a),g=s.repeat?De(s.repeat):N,E=JSON.stringify(g)!==JSON.stringify(N);return this.run("UPDATE schedules SET template=?,time_zone=?,times=?,weekdays=?,next_run_ms=? WHERE id=?",JSON.stringify(m),g.time_zone,JSON.stringify(g.times),g.weekdays===null?null:JSON.stringify(g.weekdays),E?ie(g,i):a.next_run_ms,s.id),{updated:"schedule",schedule:V(this.schedule(this.one("SELECT * FROM schedules WHERE id=?",s.id)))}}const r=this.one("SELECT * FROM messages WHERE id=? AND sender=? AND deliver_at_ms IS NOT NULL AND schedule_id IS NULL",s.id,e);r||_(404,"not_found","You have no schedule or scheduled message with that ID"),r.cancelled_at!==null&&_(409,"already_cancelled","The message was cancelled; send a new one"),r.released_ms!==null&&_(409,"already_delivered","The message was already delivered; it cannot be changed, so send a correction"),s.repeat!==void 0&&_(400,"invalid_request","A scheduled message is sent once: change its deliver_at");let o=Number(r.deliver_at_ms);s.deliver_at!==void 0&&(o=Date.parse(s.deliver_at),(o<i-Oe||o>i+ee)&&_(400,"deliver_at_out_of_range",we(i)));const c=s.content??String(r.content),d=Buffer.byteLength(c)-Buffer.byteLength(String(r.content));d>0&&this.admitBytes(String(r.lobby_id),this.all("SELECT agent_id FROM deliveries WHERE message_id=?",s.id).map(m=>String(m.agent_id)),d),this.run("UPDATE messages SET content=?,byte_size=byte_size+?,deliver_at_ms=? WHERE id=?",c,d,o,s.id);const l=this.one("SELECT m.*,(SELECT COUNT(*) FROM deliveries d WHERE d.message_id=m.id) AS recipient_count FROM messages m WHERE m.id=?",s.id);return{updated:"message",message:{...V(this.message(l)),recipients:Number(l.recipient_count)}}})}cancelScheduled(e,t,n){return e=this.key(e),this.transaction(()=>{t&&this.assertRuntime(e,t);const s=this.one("SELECT * FROM schedules WHERE id=? AND sender=?",n,e);if(s){const o=s.cancelled_at!==null;return o||this.run("UPDATE schedules SET cancelled_at=?,next_run_ms=NULL WHERE id=?",this.iso(),n),{cancelled:"schedule",schedule:this.schedule(this.one("SELECT * FROM schedules WHERE id=?",n)),already_cancelled:o}}const i=this.one("SELECT * FROM messages WHERE id=? AND sender=? AND deliver_at_ms IS NOT NULL AND schedule_id IS NULL",n,e);i||_(404,"not_found","You have no schedule or scheduled message with that ID");const a=i.cancelled_at!==null;!a&&i.released_ms!==null&&_(409,"already_delivered","The message was already delivered; it cannot be recalled"),a||this.cancelMessage(n);const r=this.one("SELECT * FROM messages WHERE id=?",n);return{cancelled:"message",message:{...this.message(r),deliveries:this.recipientDeliveries([n]).get(n)??[]},already_cancelled:a}})}nextReceipts(e,t){e=this.key(e);const n=this.clock(),s=this.one("SELECT MIN(seen_ms) AS t FROM seen_receipts WHERE sender=? AND state='due'",e);return!s||s.t===null||Number(s.t)>n-this.receiptDelay?null:this.transaction(()=>{this.assertRuntime(e,t);const i=this.row(e);if(!i.ready||F(i)||Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(n-6e4).toISOString()).n)>=this.notificationRate||this.one("SELECT 1 FROM notification_attempts WHERE agent_id=? AND kind='receipts' AND created_at>?",e,new Date(n-this.receiptInterval).toISOString()))return null;this.run("UPDATE seen_receipts SET state='expired' WHERE sender=? AND state='due' AND seen_ms<?",e,n-qt);const r=this.all("SELECT r.sequence,r.message_id,r.agent_id,r.seen_at,m.kind,m.target,m.details,m.room_id,m.content FROM seen_receipts r JOIN messages m ON m.id=r.message_id WHERE r.sender=? AND r.state='due' ORDER BY r.sequence LIMIT ?",e,jt);if(!r.length)return null;const o=Number(this.one("SELECT COUNT(*) AS n FROM seen_receipts WHERE sender=? AND state='due'",e).n),c={type:"receipts",notification_id:O(),agent_id:T(e).agent_id,lobby_id:String(i.lobby_id),runtime_id:t.runtime_id,generation:t.generation,receipts:r.map(d=>({message_id:String(d.message_id),seen_by:T(String(d.agent_id)).agent_id,seen_at:String(d.seen_at),kind:d.kind??null,to:String(d.target),...P(d.details),room_id:d.room_id,excerpt:[...String(d.content)].slice(0,80).join("")})),remaining:0};for(;c.receipts.length>1&&Buffer.byteLength(JSON.stringify(c))>pe;)c.receipts.pop();c.remaining=o-c.receipts.length;for(const d of c.receipts)this.run("UPDATE seen_receipts SET state='sent',notification_id=? WHERE message_id=? AND agent_id=?",c.notification_id,d.message_id,D(String(i.lobby_id),d.seen_by));return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",c.notification_id,e,t.runtime_id,t.generation,t.incarnation,c.type,this.iso()),c})}openActivityNotice(e,t){e=this.key(e);const n=this.clock();return this.transaction(()=>{this.assertRuntime(e,t);const s=this.row(e);if(!s.ready||F(s)||Number(this.one("SELECT COUNT(*) AS n FROM notification_attempts WHERE agent_id=? AND created_at>?",e,new Date(n-6e4).toISOString()).n)>=this.notificationRate)return null;const a=O();return this.run("INSERT INTO notification_attempts(id,agent_id,runtime_id,generation,incarnation,kind,created_at) VALUES(?,?,?,?,?,?,?)",a,e,t.runtime_id,t.generation,t.incarnation,"activity",this.iso()),{notification_id:a,agent_id:T(e).agent_id,lobby_id:String(s.lobby_id)}})}boardLobbies(){return this.all("SELECT DISTINCT s.lobby_id FROM board_sources s WHERE NOT EXISTS (SELECT 1 FROM archived_rooms a WHERE a.lobby_id=s.lobby_id AND a.room_id=s.room_id) ORDER BY s.lobby_id").map(e=>String(e.lobby_id))}boardSources(e){return this.all("SELECT room_id,repo,ref,notes,updated_at FROM board_sources s WHERE lobby_id=? AND NOT EXISTS (SELECT 1 FROM archived_rooms a WHERE a.lobby_id=s.lobby_id AND a.room_id=s.room_id) ORDER BY room_id",e).map(t=>({room_id:String(t.room_id),repo:String(t.repo),ref:String(t.ref),notes:JSON.parse(String(t.notes)),updated_at:String(t.updated_at)}))}boardConfig(e){const t=this.one("SELECT * FROM board_configs WHERE lobby_id=?",e);return{lobby_id:e,agents:t?JSON.parse(String(t.agents)):{},visibility:t?.visibility??"private",summary_model:t?.summary_model??Me[0],summary_effort:t?.summary_effort??"low",updated_at:t?.updated_at??null,sources:this.boardSources(e)}}setBoardSource(e,t,n,s){const i=vt.parse(n),a=i.repo.toLowerCase();return this.transaction(()=>{const r=this.actorLobby(s,e);this.room(r,I.parse(t)),typeof s=="object"?this.one("SELECT 1 FROM lobby_repos WHERE lobby_id=? AND repo=?",r,a)||_(403,"repo_not_approved","The operator has not approved that repository for this lobby"):this.run("INSERT INTO lobby_repos(lobby_id,repo,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",r,a,this.iso());const o=this.boardSources(r).find(l=>l.room_id===t),c=Number(this.one("SELECT COUNT(*) AS n FROM board_sources WHERE lobby_id=?",r).n);!this.one("SELECT 1 FROM board_sources WHERE lobby_id=? AND room_id=?",r,t)&&c>=Ce&&_(409,"board_source_capacity",`A dashboard reads at most ${Ce} sources`),o&&G(o)!==G({room_id:t,...i})&&this.run("DELETE FROM board_notes WHERE lobby_id=? AND room_id=?",r,t),this.run("INSERT INTO board_sources(lobby_id,room_id,repo,ref,notes,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(lobby_id,room_id) DO UPDATE SET repo=excluded.repo,ref=excluded.ref,notes=excluded.notes,updated_at=excluded.updated_at",r,t,i.repo,i.ref,JSON.stringify(i.notes),this.iso()),this.audit(s,"board_source_set",r,t,{repo:i.repo,ref:i.ref,notes:i.notes})}),this.boardConfig(e)}removeBoardSource(e,t,n){return this.transaction(()=>{const s=this.actorLobby(n,e);this.run("DELETE FROM board_sources WHERE lobby_id=? AND room_id=?",s,t)||_(404,"board_source_not_found","This dashboard reads no notes for that room"),this.audit(n,"board_source_remove",s,t)}),this.boardConfig(e)}updateBoardConfig(e,t){this.run("INSERT INTO board_configs(lobby_id,updated_at) VALUES(?,?) ON CONFLICT(lobby_id) DO NOTHING",e,this.iso());for(const[n,s]of Object.entries(t))this.run(`UPDATE board_configs SET ${n}=?,updated_at=? WHERE lobby_id=?`,s,this.iso(),e)}setBoardAgents(e,t){const n=kt.parse(t);return this.transaction(()=>{const s=this.actorLobby("owner",e);this.updateBoardConfig(s,{agents:JSON.stringify(n.agents)}),this.audit(null,"board_agents",s)}),this.boardConfig(e)}setSummarySettings(e,t,n){const s=Ft.parse(n);return this.transaction(()=>{const i=this.actorLobby(e,t);this.updateBoardConfig(i,{summary_model:s.model,summary_effort:s.effort}),this.audit(e,"summary_settings",i,null,s)}),this.boardConfig(t)}setBoardVisibility(e,t,n){return this.transaction(()=>{const s=this.actorLobby(e,t);this.updateBoardConfig(s,{visibility:n}),this.audit(e,"board_visibility",s,n)}),this.boardConfig(t)}boardIcon(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`board_icon:${e}`);if(!t)return null;const n=JSON.parse(String(t.value));return{png:Buffer.from(n.png,"base64"),sha256:n.sha256,set_at:n.set_at}}displayName(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`display_name:${e}`);return t?String(t.value):null}setDisplayName(e,t,n){const s=At.parse(n)||null;return this.transaction(()=>{const i=this.actorLobby(e,t),a=`display_name:${i}`;return s===this.displayName(i)?{lobby_id:i,display_name:s}:(s?this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",a,s):this.run("DELETE FROM metadata WHERE key=?",a),this.audit(e,"display_name",i,s),{lobby_id:i,display_name:s})})}exchangeRates(){const e=this.one("SELECT value FROM metadata WHERE key='exchange_rates'");return e?JSON.parse(String(e.value)):null}saveExchangeRates(e,t){const n=this.exchangeRates(),s={attempted_at:e,date:t?.date??n?.date??null,rates:t?.rates??n?.rates??null};this.run("INSERT INTO metadata(key,value) VALUES('exchange_rates',?1) ON CONFLICT(key) DO UPDATE SET value=?1",JSON.stringify(s))}lobbyCurrency(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`currency:${e}`);return t?String(t.value):"USD"}lobbyMoney(e){return xt(this.lobbyCurrency(e),this.exchangeRates())}setLobbyCurrency(e,t,n){const s=Dt.parse(n);return this.transaction(()=>{const i=this.actorLobby(e,t);return s!=="USD"&&!this.exchangeRates()?.rates?.[s]&&_(400,"currency_unavailable",`No exchange rate for ${s} is stored on this server`),s===this.lobbyCurrency(i)?{lobby_id:i,currency:s}:(s==="USD"?this.run("DELETE FROM metadata WHERE key=?",`currency:${i}`):this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",`currency:${i}`,s),this.audit(e,"currency",i,s),{lobby_id:i,currency:s})})}lobbyPacing(e){const t=this.one("SELECT value FROM metadata WHERE key=?",`pacing:${e}`),n=t?JSON.parse(String(t.value)):{};return{mode:n.mode??ne.mode,hold_at:n.hold_at??ne.hold_at,open_at:n.open_at??ne.open_at}}setLobbyPacing(e,t,n){return this.transaction(()=>{const s=this.actorLobby(e,t),i=this.lobbyPacing(s),a=Ut.parse({...i,...n});if(JSON.stringify(a)===JSON.stringify(i))return{lobby_id:s,pacing:a};if(this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",`pacing:${s}`,JSON.stringify(a)),this.audit(e,"pacing_settings",s,null,a),a.mode!==i.mode)for(const r of this.all("SELECT * FROM agents WHERE lobby_id=? AND kind='worker' AND manage_rooms=0",s)){const o=a.mode==="off"?r.pacing==="open"?null:{next:"open",lead:null}:this.nextPacingOf(r,"open",this.clock());o&&this.changePacing(r,o.next,o.lead)}return{lobby_id:s,pacing:a}})}setAgentKind(e,t,n,s){const i=Ct.parse(s),a=$.parse(n);return this.transaction(()=>{const r=this.actorLobby(e,t),o=D(r,a);return this.row(o).kind!==i&&(this.run("UPDATE agents SET kind=? WHERE agent_id=?",i,o),this.paceRole(this.row(o)),this.audit(e,"agent_kind",r,a,{kind:i})),this.agent(o)})}paceRole(e){const t=J(e)?this.lobbyPacing(String(e.lobby_id)).mode==="off"?null:this.nextPacingOf(e,"open",this.clock()):e.pacing==="open"?null:{next:"open",lead:null};t&&this.changePacing(e,t.next,t.lead)}recordWeek(e,t){const n=this.key(e);this.transaction(()=>{const s=this.one("SELECT lobby_id,usage_account FROM agents WHERE agent_id=?",n);if(!s)return;const i=String(s.lobby_id),a=t.account??s.usage_account??`agent:${T(n).agent_id}`,r=this.clock(),o=a!==s.usage_account;o&&this.run("UPDATE agents SET usage_account=? WHERE agent_id=?",a,n);let c=!1;const d=t.seven_day;if(d){const l=Math.round(d.resets_at*1e3);if(l>r){const m=this.weekNow(i,a),N=Math.max(r,m===void 0?0:Number(m.at_ms)+1);this.run("INSERT INTO account_weeks(lobby_id,account,resets_ms,used,at_ms) VALUES(?1,?2,?3,?4,?5) ON CONFLICT(lobby_id,account,resets_ms) DO UPDATE SET used=max(used,?4),at_ms=?5",i,a,l,d.used_percentage,N),c=m!==void 0&&Number(m.resets_ms)!==l}}if(this.lobbyPacing(i).mode!=="off")for(const l of this.all("SELECT * FROM agents WHERE lobby_id=? AND usage_account=? AND kind='worker' AND manage_rooms=0",i,a)){const m=c||o&&l.agent_id===n,N=this.nextPacingOf(l,m?"open":l.pacing,r);N&&this.changePacing(l,N.next,N.lead)}})}paceAll(){const e=this.clock(),t="SELECT * FROM agents WHERE kind='worker' AND manage_rooms=0 AND enabled=1",n=R(i=>this.lobbyPacing(String(i.lobby_id)).mode==="subscription","pacing"),s=this.all(`${t} AND usage_account IS NOT NULL`).filter(i=>n(i)&&this.nextPacingOf(i,i.pacing,e));return s.length?this.transaction(()=>{let i=0;for(const{agent_id:a}of s){const r=this.one(`${t} AND agent_id=?`,String(a)),o=r&&n(r)&&this.nextPacingOf(r,r.pacing,e);!r||!o||(this.changePacing(r,o.next,o.lead),i++)}return i}):0}nextPacingOf(e,t,n){const s=e.usage_account===null?void 0:this.weekNow(String(e.lobby_id),String(e.usage_account)),i=s?Mt(Number(s.used),Number(s.resets_ms)/1e3,n)?.lead??null:null,a=s?It(t,i,this.lobbyPacing(String(e.lobby_id))):t;return a===e.pacing?null:{next:a,lead:i}}changePacing(e,t,n){const s=String(e.agent_id);this.run("UPDATE agents SET pacing=?,pacing_since_ms=?,pacing_lead=? WHERE agent_id=?",t,this.clock(),n,s);const[i,a]=this.agentScope(s);this.audit(null,"pacing",i,a,{state:t,lead:n})}setPacing(e,t,n){return e=this.key(e),this.transaction(()=>{const s=this.row(e);return!J(s)||!Number(s.enabled)||s.pacing===t?!1:(this.changePacing(s,t,n),!0)})}weekNow(e,t){return this.one("SELECT used,resets_ms,at_ms FROM account_weeks WHERE lobby_id=? AND account=? ORDER BY at_ms DESC,resets_ms DESC LIMIT 1",e,t)}accountWeeks(e){const t=new Map;for(const{account:n}of this.all("SELECT DISTINCT account FROM account_weeks WHERE lobby_id=?",e)){const s=this.weekNow(e,String(n));t.set(String(n),{used:Number(s.used),resets_at:Number(s.resets_ms)/1e3,at:Number(s.at_ms)})}return t}agentWeek(e){const t=this.one("SELECT lobby_id,usage_account FROM agents WHERE agent_id=?",this.key(e)),n=t&&t.usage_account!==null?this.weekNow(String(t.lobby_id),String(t.usage_account)):void 0;return n?{used:Number(n.used),resets_at:Number(n.resets_ms)/1e3,at:Number(n.at_ms),account:String(t.usage_account)}:null}pruneAccountWeeks(){const e=this.clock()-6912e5,t="resets_ms<? AND (EXISTS(SELECT 1 FROM account_weeks n WHERE n.lobby_id=account_weeks.lobby_id AND n.account=account_weeks.account AND n.resets_ms>account_weeks.resets_ms) OR NOT EXISTS(SELECT 1 FROM agents a WHERE a.lobby_id=account_weeks.lobby_id AND a.usage_account=account_weeks.account))";return this.one(`SELECT 1 FROM account_weeks WHERE ${t} LIMIT 1`,e)?this.transaction(()=>this.run(`DELETE FROM account_weeks WHERE ${t}`,e)):0}setBoardIcon(e,t,n){return n&&wt(n),this.transaction(()=>{const s=this.actorLobby(e,t),i=`board_icon:${s}`;if(!n)return this.run("DELETE FROM metadata WHERE key=?",i)&&this.audit(e,"board_icon_remove",s),{icon:null};const a=x("sha256").update(n).digest("hex");return this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",i,JSON.stringify({png:n.toString("base64"),sha256:a,set_at:this.iso()})),this.audit(e,"board_icon",s,null,{sha256:a,bytes:n.length}),{icon:{sha256:a,bytes:n.length}}})}boardNoteFacts(e){return new Map(this.all("SELECT room_id,commit_sha,fetched_at,(SELECT COUNT(*) FROM json_each(files)) AS files,(SELECT json_group_array(json_extract(value,'$.path')) FROM json_each(files) WHERE json_extract(value,'$.truncated')) AS cut FROM board_notes WHERE lobby_id=?",e).map(t=>[String(t.room_id),{commit_sha:String(t.commit_sha),fetched_at:String(t.fetched_at),files:Number(t.files),cut:JSON.parse(String(t.cut))}]))}boardNotes(e){return new Map(this.all("SELECT * FROM board_notes WHERE lobby_id=?",e).map(t=>[String(t.room_id),{commit_sha:String(t.commit_sha),files:JSON.parse(String(t.files)),fetched_at:String(t.fetched_at)}]))}saveBoardNotes(e,t,n){return this.transaction(()=>{const s=this.boardSources(e).find(i=>i.room_id===t.room_id);return!s||G(s)!==G(t)?!1:(this.run("INSERT INTO board_notes(lobby_id,room_id,commit_sha,files,fetched_at) VALUES(?,?,?,?,?) ON CONFLICT(lobby_id,room_id) DO UPDATE SET commit_sha=excluded.commit_sha,files=excluded.files,fetched_at=excluded.fetched_at",e,t.room_id,n.commit_sha,JSON.stringify(n.files),n.fetched_at),!0)})}boardMessages(e,t,n,s){return this.all("SELECT sequence,sender,target,details,room_id,kind,created_at,substr(content,1,?) AS content FROM messages WHERE lobby_id=? AND kind IN ('report','assignment') AND created_ms>? AND cancelled_at IS NULL AND (deliver_at_ms IS NULL OR released_ms IS NOT NULL) ORDER BY sequence DESC LIMIT ?",s,e,t,n).map(i=>({sequence:Number(i.sequence),from:T(String(i.sender)).agent_id,to:String(i.target),...P(i.details),room_id:i.room_id,kind:i.kind,created_at:String(i.created_at),content:String(i.content)}))}recordTokens(e,t){return this.transaction(()=>this.countCalls(e,t))}countCalls(e,t){const n=this.key(e),s=T(n).lobby_id,i=this.clock();let a=0;for(const r of t){if(r.at<i-oe||r.at>i+1440*60*1e3||!this.run("INSERT OR IGNORE INTO token_calls(lobby_id,call_id,at_ms) VALUES(?,?,?)",s,r.id,r.at))continue;const o=r.input+r.cache_write_5m+r.cache_write_1h+r.cache_read;this.run(`INSERT INTO token_days(lobby_id,day,agent_id,model,speed,geo,long,calls,input,cache_write_5m,cache_write_1h,cache_read,output)
|
|
292
321
|
VALUES(?,?,?,?,?,?,?,1,?,?,?,?,?)
|
|
293
322
|
ON CONFLICT(lobby_id,day,agent_id,model,speed,geo,long) DO UPDATE SET calls=calls+1,
|
|
294
323
|
input=input+excluded.input, cache_write_5m=cache_write_5m+excluded.cache_write_5m,
|
|
295
324
|
cache_write_1h=cache_write_1h+excluded.cache_write_1h,
|
|
296
|
-
cache_read=cache_read+excluded.cache_read, output=output+excluded.output`,
|
|
297
|
-
${i.evidence}`);r&&d(400,"secret_refused",`This looks like it holds ${r}; nothing was stored. Leave credentials out of shared memory`),i.about!==void 0&&!this.one("SELECT 1 FROM memory_entries WHERE lobby_id=? AND room_id=? AND ref=? AND state='active' UNION ALL SELECT 1 FROM memory_fragments WHERE lobby_id=? AND room_id=? AND id=? AND state='pending'",a,i.room_id,i.about,a,i.room_id,i.about)&&d(404,"memory_entry_not_found",`No active entry or waiting fragment ${i.about} in room ${i.room_id}`);const o=Number(this.one("SELECT COUNT(*) AS n FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='pending'",a,i.room_id).n);o>=Xt&&d(409,"memory_backlog",`Room ${i.room_id} has ${o} fragments waiting for its curator; save this one after its next curation`);const _=O();return this.run("INSERT INTO memory_fragments(id,lobby_id,room_id,author,request_id,kind,text,evidence,about,created_ms) VALUES(?,?,?,?,?,?,?,?,?,?)",_,a,i.room_id,e,i.client_request_id,i.kind,i.text,i.evidence,i.about??null,this.clock()),{fragment:this.fragmentOf(this.one("SELECT * FROM memory_fragments WHERE id=?",_)),duplicate:!1}})}recall(e,t={}){e=this.key(e);const s=Rt.parse(t),i=Number(this.one("SELECT COUNT(*) AS n FROM memory_fragments WHERE author=? AND state='pending'",e).n);if(s.mine)return{entries:[],fragments:this.all("SELECT * FROM memory_fragments WHERE author=? AND created_ms>=? ORDER BY created_ms DESC LIMIT 50",e,this.clock()-ae).map(_=>this.fragmentOf(_)),pending:i};const{lobby:n,rooms:a}=this.memoryRooms(e),r=s.room_id?(this.memoryMember(e,s.room_id),[s.room_id]):a;if(s.ids){const _=[],c=[];for(const l of s.ids){const m=this.one("SELECT * FROM memory_entries WHERE lobby_id=? AND ref=?",n,l);!m||!r.includes(String(m.room_id))?c.push({ref:l,reason:"no such entry in your rooms"}):m.state!=="active"?c.push({ref:l,reason:`archived: ${String(m.reason)}${m.into_ref?` (see ${String(m.into_ref)})`:""}`}):_.push(this.entryOf(m))}return{entries:_,...c.length?{missing:c}:{},pending:i}}const o=r.flatMap(_=>this.activeEntries(n,_)).map(_=>this.entryOf(_));return{entries:Be(o,s.query),pending:i}}memoryIndex(e){const{lobby:t,rooms:s}=this.memoryRooms(e);return s.map(i=>{const n=this.one("SELECT (SELECT revision FROM memory_rooms WHERE lobby_id=?1 AND room_id=?2) AS revision, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active') AS entries, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active' AND kind='gotcha') AS gotchas, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active' AND disputed IS NOT NULL) AS disputed, (SELECT COUNT(*) FROM memory_fragments WHERE lobby_id=?1 AND room_id=?2 AND state='pending') AS pending",t,i);return{room_id:i,revision:Number(n.revision),entries:Number(n.entries),gotchas:Number(n.gotchas),disputed:Number(n.disputed),pending:Number(n.pending)}})}memoryBrief(e,t){e=this.key(e);const{lobby:s,rooms:i}=this.memoryRooms(e);return _e(i.map(n=>({room_id:n,entries:this.activeEntries(s,n).map(a=>this.entryOf(a))}))).parts[t-1]}curationSnapshot(e,t,s=!1){e=this.key(e);const{lobby:i,room:n}=this.memoryCurator(e,t),a=this.activeEntries(i,t),r=a.map(h=>({entry:this.entryOf(h),checked:h.checked_ms===null?-1:Number(h.checked_ms)})),o=s?r.map(({entry:h})=>h.ref):[...r].sort((h,L)=>h.checked-L.checked).slice(0,Math.ceil(r.length*qt)).map(({entry:h})=>h.ref),_=this.all("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? AND state='archived'",i,t).map(h=>({row:h,entry:this.entryOf(h)})),c=r.map(({entry:h})=>h),l=new Set(c.map(h=>h.ref)),m=this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='pending' ORDER BY created_ms",i,t),N=m.slice(0,ve),R=new Set(N.map(h=>String(h.id)));for(let h=!0;h;){h=!1;const L=new Set(N.map(p=>String(p.about)));for(const p of m)!R.has(String(p.id))&&(L.has(String(p.id))||R.has(String(p.about)))&&(N.push(p),R.add(String(p.id)),h=!0)}const E=N.length?ce(c):[],u=N.length?ce(_.map(({entry:h})=>h)):[],y=new Set(o),U=N.map(h=>{const L=this.fragmentOf(h);for(const S of Ee(E,L.text,5))y.add(S.ref);L.about&&y.add(L.about);let p=null;if(L.about){const S=[...a,..._.map(({row:M})=>M)].find(M=>JSON.parse(String(M.sources)).includes(L.about));let C=S?String(S.ref):null;for(let M=0;C&&!l.has(C)&&M<20;M++)C=_.find(({row:xe})=>xe.ref===C)?.row.into_ref??null;C&&l.has(C)&&(y.add(C),p=C)}return{...L,...p?{about_entry:p}:{},archived:Ee(u,L.text,3).map(S=>{const C=_.find(M=>M.entry.ref===S.ref).row;return{ref:S.ref,title:S.title,reason:C.reason??null,archived_at:S.updated_at}})}}),A=n.last_review_ms===null?this.clock()-1440*60*1e3:Number(n.last_review_ms);return{room_id:t,revision:Number(n.revision),next_ref:Number(n.next_ref),last_review_at:n.last_review_ms===null?null:new Date(Number(n.last_review_ms)).toISOString(),lines:c.map(({ref:h,kind:L,title:p,disputed:S})=>({ref:h,kind:L,title:p,disputed:S})),entries:c.filter(h=>y.has(h.ref)),recheck:o,fragments:U,unlanded:this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='refused' AND unlanded=1 AND curated_ms>=? ORDER BY curated_ms DESC LIMIT ?",i,t,this.clock()-ae,ve).map(h=>this.fragmentOf(h)),day:{archived:_.filter(({row:h})=>Number(h.updated_ms)>A).map(({row:h,entry:L})=>({ref:L.ref,title:L.title,reason:h.reason??null,into:h.into_ref??null})),updated:r.filter(({entry:h})=>Date.parse(h.updated_at)>A&&Date.parse(h.created_at)<=A).map(({entry:h})=>({ref:h.ref,title:h.title}))}}}curate(e,t){e=this.key(e);const s=Tt.parse(t);return this.transaction(()=>{const{lobby:i,room:n}=this.memoryCurator(e,s.room_id);Number(n.revision)!==s.revision&&d(409,"memory_revision_conflict",`Room ${s.room_id}'s memory changed since revision ${s.revision} (now ${Number(n.revision)}); read it again`);for(const E of[s.summary,...s.ops.flatMap(u=>Object.values(u).filter(y=>typeof y=="string"))]){const u=J(E);u&&d(400,"secret_refused",`The run's output holds ${u}; nothing was applied`)}const a=this.clock(),r=new Set;let o=Number(n.next_ref);const _=g(E=>{const u=this.one("SELECT * FROM memory_fragments WHERE id=? AND lobby_id=? AND room_id=? AND state='pending'",E,i,s.room_id);return(!u||r.has(E))&&d(409,"memory_fragment_unavailable",`Fragment ${E} is not pending in room ${s.room_id}`),r.add(E),this.run("UPDATE memory_fragments SET state='used',curated_ms=? WHERE id=?",a,E),{author:T(String(u.author)).agent_id,evidence:String(u.evidence)}},"take"),c=g(E=>{const u=this.one("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? AND ref=? AND state='active'",i,s.room_id,E);return u||d(404,"memory_entry_not_found",`Entry ${E} is not active in room ${s.room_id}`),u},"active"),l=g(E=>this.run("INSERT INTO memory_entry_history(entry_id,lobby_id,room_id,row,changed_ms) VALUES(?,?,?,?,?)",String(E.id),i,s.room_id,JSON.stringify(E),a),"keep"),m=g((E,u,y)=>{u.length>pe[E]&&d(400,"memory_text_too_long",`${y}: a ${E} holds at most ${pe[E]} characters, not ${u.length}`)},"textFits"),N={added:0,updated:0,verified:0,archived:0,refused:0};for(const E of s.ops)if(E.op==="add"){const u=`${s.room_id}-${o++}`;m(E.kind,E.text,u),this.run("INSERT INTO memory_entries(id,lobby_id,room_id,ref,kind,title,text,keywords,provenance,sources,checked_commit,checked_ms,created_ms,updated_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)",O(),i,s.room_id,u,E.kind,E.title,E.text,E.keywords,JSON.stringify(G([],E.sources.map(_))),JSON.stringify(E.sources),s.commit,a,a,a),N.added++}else if(E.op==="update"){const u=c(E.entry);l(u);const y=String(u.kind),U=E.text??String(u.text);m(y,U,E.entry),this.run("UPDATE memory_entries SET title=?,text=?,keywords=?,provenance=?,sources=?,disputed=?,checked_commit=?,checked_ms=?,updated_ms=? WHERE id=?",E.title??String(u.title),U,E.keywords??String(u.keywords),JSON.stringify(G(JSON.parse(String(u.provenance)),E.sources.map(_))),JSON.stringify([...JSON.parse(String(u.sources)),...E.sources].slice(-20)),E.resolves?null:u.disputed??null,s.commit,a,a,String(u.id)),N.updated++}else if(E.op==="verify"){const u=c(E.entry);this.run("UPDATE memory_entries SET checked_commit=?,checked_ms=? WHERE id=?",s.commit,a,String(u.id)),N.verified++}else if(E.op==="archive"){const u=c(E.entry);if(E.into!==void 0){E.into===E.entry&&d(400,"memory_merge_invalid",`${E.entry} cannot merge into itself`);const y=c(E.into);y.kind!==u.kind&&d(400,"memory_merge_invalid",`${E.entry} is a ${String(u.kind)}, ${E.into} a ${String(y.kind)}: merge within a kind`),l(y),this.run("UPDATE memory_entries SET provenance=?,disputed=?,updated_ms=? WHERE id=?",JSON.stringify(G(JSON.parse(String(y.provenance)),JSON.parse(String(u.provenance)))),y.disputed??u.disputed??null,a,String(y.id))}l(u),this.run("UPDATE memory_entries SET state='archived',reason=?,into_ref=?,updated_ms=? WHERE id=?",E.reason,E.into??null,a,String(u.id)),N.archived++}else{if(_(E.fragment),E.dispute!==void 0){const u=c(E.dispute);l(u),this.run("UPDATE memory_entries SET disputed=?,updated_ms=? WHERE id=?",E.reason,a,String(u.id))}this.run(`UPDATE memory_fragments SET state='refused',note=?,unlanded=?${E.erase?",text='',evidence=''":""} WHERE id=?`,E.reason,Number(E.unlanded),E.fragment),N.refused++}const R=this.touchMemory(i,s.room_id);return this.run(`UPDATE memory_rooms SET next_ref=?,failures=0${s.review?",last_review_ms=?":""} WHERE lobby_id=? AND room_id=?`,...s.review?[o,a,i,s.room_id]:[o,i,s.room_id]),this.countCuration(e,s.usage),this.run("INSERT INTO memory_curations(id,lobby_id,room_id,curator,revision,commit_sha,review,created_ms,added,updated,verified,archived,refused,summary,usage) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",O(),i,s.room_id,e,R,s.commit,Number(s.review),a,N.added,N.updated,N.verified,N.archived,N.refused,s.summary,s.usage?JSON.stringify(s.usage):null),{revision:R,...N}})}curationFailed(e,t){e=this.key(e);const s=ht.parse(t);return this.transaction(()=>{const{lobby:i}=this.memoryCurator(e,s.room_id);return this.run("UPDATE memory_rooms SET failures=failures+1 WHERE lobby_id=? AND room_id=?",i,s.room_id),this.countCuration(e,s.usage),this.run("INSERT INTO memory_curations(id,lobby_id,room_id,curator,created_ms,error,usage) VALUES(?,?,?,?,?,?,?)",O(),i,s.room_id,e,this.clock(),s.error,s.usage?JSON.stringify(s.usage):null),{failures:Number(this.one("SELECT failures FROM memory_rooms WHERE lobby_id=? AND room_id=?",i,s.room_id).failures)}})}curationReport(e,t){e=this.key(e);const s=bt.parse(t);this.memoryCurator(e,s.room_id);const i=J(s.content);return i&&d(400,"secret_refused",`The report holds ${i}; nothing was sent`),this.send(e,{client_request_id:s.client_request_id,to:s.to,kind:"report",priority:s.priority,content:s.content})}curationStatus(e,t){return this.all("SELECT r.room_id, r.failures, (SELECT COUNT(*) FROM memory_fragments f WHERE f.lobby_id=r.lobby_id AND f.room_id=r.room_id AND f.state='pending') AS pending, (SELECT MIN(created_ms) FROM memory_fragments f WHERE f.lobby_id=r.lobby_id AND f.room_id=r.room_id AND f.state='pending') AS oldest FROM memory_rooms r WHERE r.lobby_id=? AND r.curator=? ORDER BY r.room_id",e,D(e,t)).map(s=>({room_id:String(s.room_id),pending:Number(s.pending),oldest_pending_at:s.oldest===null?null:new Date(Number(s.oldest)).toISOString(),failures:Number(s.failures)}))}setMemoryCurator(e,t,s,i="owner"){return this.transaction(()=>{const n=this.actorLobby(i,e);this.room(n,t);const a=s===null?null:D(n,s);return a!==null&&!this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",n,t,a)&&d(409,"curator_not_member",`${s} is not a member of room ${t}`),this.touchMemory(n,t),this.run("UPDATE memory_rooms SET curator=?,failures=0 WHERE lobby_id=? AND room_id=?",a,n,t),this.audit(i,"set_memory_curator",n,t,{curator:s}),{curator:s}})}memoryOverview(e){const t=this.requireLobby(e),s=this.all(`SELECT room_id FROM memory_rooms WHERE lobby_id=?1
|
|
325
|
+
cache_read=cache_read+excluded.cache_read, output=output+excluded.output`,s,new Date(r.at).toISOString().slice(0,10),n,r.model,r.speed??"",r.geo??"",+(o>Ze),r.input,r.cache_write_5m,r.cache_write_1h,r.cache_read,r.output),a++}return{recorded:a}}countCuration(e,t){!t?.attempt||!t.model||this.countCalls(e,[{id:x("sha256").update(`curation:${t.attempt}`).digest("hex").slice(0,32),at:this.clock(),model:t.model,input:t.input_tokens??0,cache_write_5m:t.cache_write_tokens??0,cache_write_1h:0,cache_read:t.cache_read_tokens??0,output:t.output_tokens??0}])}openTurn(e,t){this.run("INSERT OR IGNORE INTO agent_turns(agent_id,started_ms) VALUES(?,?)",this.key(e),t)}closeTurn(e,t,n){this.run("UPDATE agent_turns SET ended_ms=?,ended_state=? WHERE agent_id=? AND ended_ms IS NULL",t,n,this.key(e))}turns(e,t){const n=this.requireLobby(e);return this.all("SELECT agent_id,started_ms,ended_ms,ended_state FROM agent_turns WHERE agent_id IN (SELECT agent_id FROM agents WHERE lobby_id=?) AND (ended_ms IS NULL OR ended_ms>=?) ORDER BY started_ms",n,t).map(s=>this.turn(s))}latestTurns(){return this.all("SELECT t.agent_id,t.started_ms,t.ended_ms,t.ended_state FROM agent_turns t WHERE t.started_ms=(SELECT MAX(started_ms) FROM agent_turns u WHERE u.agent_id=t.agent_id)").map(e=>this.turn(e))}turn(e){return{agent_id:String(e.agent_id),started_ms:Number(e.started_ms),ended_ms:e.ended_ms===null?null:Number(e.ended_ms),ended_state:e.ended_state??null}}memoryMember(e,t,n="share its memory"){const s=this.agent(e);return s.enabled||_(403,"agent_disabled","This identity is disabled"),this.room(s.lobby_id,t),this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",s.lobby_id,t,this.key(e))||_(403,"room_membership_required",`Only members of room ${t} ${n}; list_rooms shows your rooms`),{agent:s,lobby:s.lobby_id}}memoryCurator(e,t){const{lobby:n}=this.memoryMember(e,t),s=this.one("SELECT * FROM memory_rooms WHERE lobby_id=? AND room_id=?",n,t);return(!s||s.curator!==this.key(e))&&_(403,"memory_curator_required",`Only the curator the owner named for room ${t} curates its memory`),{lobby:n,room:s}}memoryRooms(e){const t=this.agent(e);return{lobby:t.lobby_id,rooms:this.all("SELECT m.room_id FROM room_members m JOIN memory_rooms r ON r.lobby_id=m.lobby_id AND r.room_id=m.room_id WHERE m.lobby_id=? AND m.agent_id=? AND r.curator IS NOT NULL ORDER BY m.room_id",t.lobby_id,this.key(e)).map(n=>String(n.room_id))}}fragmentOf(e){return{id:String(e.id),room_id:String(e.room_id),author:T(String(e.author)).agent_id,kind:String(e.kind),text:String(e.text),evidence:String(e.evidence),about:e.about??null,created_at:new Date(Number(e.created_ms)).toISOString(),state:String(e.state),note:e.note??null}}entryOf(e){return{ref:String(e.ref),room_id:String(e.room_id),kind:String(e.kind),title:String(e.title),text:String(e.text),keywords:String(e.keywords),provenance:JSON.parse(String(e.provenance)),checked_commit:e.checked_commit??null,checked_at:e.checked_ms===null?null:new Date(Number(e.checked_ms)).toISOString(),disputed:e.disputed??null,created_at:new Date(Number(e.created_ms)).toISOString(),updated_at:new Date(Number(e.updated_ms)).toISOString()}}activeEntries(e,t){return this.all("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? AND state='active' ORDER BY length(ref), ref",e,t)}touchMemory(e,t){return this.run("INSERT INTO memory_rooms(lobby_id,room_id,revision) VALUES(?,?,1) ON CONFLICT(lobby_id,room_id) DO UPDATE SET revision=revision+1",e,t),Number(this.one("SELECT revision FROM memory_rooms WHERE lobby_id=? AND room_id=?",e,t).revision)}remember(e,t,n){e=this.key(e);const s=pt.parse(n);return this.transaction(()=>{this.assertRuntime(e,t);const i=this.one("SELECT * FROM memory_fragments WHERE author=? AND request_id=?",e,s.client_request_id);if(i){const d=i.state==="refused"&&i.text==="";return(i.room_id!==s.room_id||i.kind!==s.kind||!d&&(i.text!==s.text||i.evidence!==s.evidence||(i.about??void 0)!==s.about))&&_(409,"idempotency_conflict","This client_request_id already saved a different fragment; choose a new one"),{fragment:this.fragmentOf(i),duplicate:!0}}const{lobby:a}=this.memoryMember(e,s.room_id),r=z(`${s.text}
|
|
326
|
+
${s.evidence}`);r&&_(400,"secret_refused",`This looks like it holds ${r}; nothing was stored. Leave credentials out of shared memory`),s.about!==void 0&&!this.one("SELECT 1 FROM memory_entries WHERE lobby_id=? AND room_id=? AND ref=? AND state='active' UNION ALL SELECT 1 FROM memory_fragments WHERE lobby_id=? AND room_id=? AND id=? AND state='pending'",a,s.room_id,s.about,a,s.room_id,s.about)&&_(404,"memory_entry_not_found",`No active entry or waiting fragment ${s.about} in room ${s.room_id}`);const o=Number(this.one("SELECT COUNT(*) AS n FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='pending'",a,s.room_id).n);o>=Vt&&_(409,"memory_backlog",`Room ${s.room_id} has ${o} fragments waiting for its curator; save this one after its next curation`);const c=O();return this.run("INSERT INTO memory_fragments(id,lobby_id,room_id,author,request_id,kind,text,evidence,about,created_ms) VALUES(?,?,?,?,?,?,?,?,?,?)",c,a,s.room_id,e,s.client_request_id,s.kind,s.text,s.evidence,s.about??null,this.clock()),{fragment:this.fragmentOf(this.one("SELECT * FROM memory_fragments WHERE id=?",c)),duplicate:!1}})}recall(e,t={}){e=this.key(e);const n=St.parse(t),s=Number(this.one("SELECT COUNT(*) AS n FROM memory_fragments WHERE author=? AND state='pending'",e).n);if(n.mine)return{entries:[],fragments:this.all("SELECT * FROM memory_fragments WHERE author=? AND created_ms>=? ORDER BY created_ms DESC LIMIT 50",e,this.clock()-de).map(c=>this.fragmentOf(c)),pending:s};const{lobby:i,rooms:a}=this.memoryRooms(e),r=n.room_id?(this.memoryMember(e,n.room_id),[n.room_id]):a;if(n.ids){const c=[],d=[];for(const l of n.ids){const m=this.one("SELECT * FROM memory_entries WHERE lobby_id=? AND ref=?",i,l);!m||!r.includes(String(m.room_id))?d.push({ref:l,reason:"no such entry in your rooms"}):m.state!=="active"?d.push({ref:l,reason:`archived: ${String(m.reason)}${m.into_ref?` (see ${String(m.into_ref)})`:""}`}):c.push(this.entryOf(m))}return{entries:c,...d.length?{missing:d}:{},pending:s}}const o=r.flatMap(c=>this.activeEntries(i,c)).map(c=>this.entryOf(c));return{entries:Ye(o,n.query),pending:s}}memoryIndex(e){const{lobby:t,rooms:n}=this.memoryRooms(e);return n.map(s=>{const i=this.one("SELECT (SELECT revision FROM memory_rooms WHERE lobby_id=?1 AND room_id=?2) AS revision, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active') AS entries, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active' AND kind='gotcha') AS gotchas, (SELECT COUNT(*) FROM memory_entries WHERE lobby_id=?1 AND room_id=?2 AND state='active' AND disputed IS NOT NULL) AS disputed, (SELECT COUNT(*) FROM memory_fragments WHERE lobby_id=?1 AND room_id=?2 AND state='pending') AS pending",t,s);return{room_id:s,revision:Number(i.revision),entries:Number(i.entries),gotchas:Number(i.gotchas),disputed:Number(i.disputed),pending:Number(i.pending)}})}memoryBrief(e,t){e=this.key(e);const{lobby:n,rooms:s}=this.memoryRooms(e);return le(s.map(i=>({room_id:i,entries:this.activeEntries(n,i).map(a=>this.entryOf(a))}))).parts[t-1]}curationSnapshot(e,t,n=!1){e=this.key(e);const{lobby:s,room:i}=this.memoryCurator(e,t),a=this.activeEntries(s,t),r=a.map(b=>({entry:this.entryOf(b),checked:b.checked_ms===null?-1:Number(b.checked_ms)})),o=n?r.map(({entry:b})=>b.ref):[...r].sort((b,L)=>b.checked-L.checked).slice(0,Math.ceil(r.length*zt)).map(({entry:b})=>b.ref),c=this.all("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? AND state='archived'",s,t).map(b=>({row:b,entry:this.entryOf(b)})),d=r.map(({entry:b})=>b),l=new Set(d.map(b=>b.ref)),m=this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='pending' ORDER BY created_ms",s,t),N=m.slice(0,We),g=new Set(N.map(b=>String(b.id)));for(let b=!0;b;){b=!1;const L=new Set(N.map(p=>String(p.about)));for(const p of m)!g.has(String(p.id))&&(L.has(String(p.id))||g.has(String(p.about)))&&(N.push(p),g.add(String(p.id)),b=!0)}const E=N.length?me(d):[],h=N.length?me(c.map(({entry:b})=>b)):[],y=new Set(o),U=N.map(b=>{const L=this.fragmentOf(b);for(const S of ue(E,L.text,5))y.add(S.ref);L.about&&y.add(L.about);let p=null;if(L.about){const S=[...a,...c.map(({row:k})=>k)].find(k=>JSON.parse(String(k.sources)).includes(L.about));let C=S?String(S.ref):null;for(let k=0;C&&!l.has(C)&&k<20;k++)C=c.find(({row:Pe})=>Pe.ref===C)?.row.into_ref??null;C&&l.has(C)&&(y.add(C),p=C)}return{...L,...p?{about_entry:p}:{},archived:ue(h,L.text,3).map(S=>{const C=c.find(k=>k.entry.ref===S.ref).row;return{ref:S.ref,title:S.title,reason:C.reason??null,archived_at:S.updated_at}})}}),A=i.last_review_ms===null?this.clock()-1440*60*1e3:Number(i.last_review_ms);return{room_id:t,revision:Number(i.revision),next_ref:Number(i.next_ref),last_review_at:i.last_review_ms===null?null:new Date(Number(i.last_review_ms)).toISOString(),lines:d.map(({ref:b,kind:L,title:p,disputed:S})=>({ref:b,kind:L,title:p,disputed:S})),entries:d.filter(b=>y.has(b.ref)),recheck:o,fragments:U,unlanded:this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? AND state='refused' AND unlanded=1 AND curated_ms>=? ORDER BY curated_ms DESC LIMIT ?",s,t,this.clock()-de,We).map(b=>this.fragmentOf(b)),day:{archived:c.filter(({row:b})=>Number(b.updated_ms)>A).map(({row:b,entry:L})=>({ref:L.ref,title:L.title,reason:b.reason??null,into:b.into_ref??null})),updated:r.filter(({entry:b})=>Date.parse(b.updated_at)>A&&Date.parse(b.created_at)<=A).map(({entry:b})=>({ref:b.ref,title:b.title}))}}}curate(e,t){e=this.key(e);const n=yt.parse(t);return this.transaction(()=>{const{lobby:s,room:i}=this.memoryCurator(e,n.room_id);Number(i.revision)!==n.revision&&_(409,"memory_revision_conflict",`Room ${n.room_id}'s memory changed since revision ${n.revision} (now ${Number(i.revision)}); read it again`);for(const E of[n.summary,...n.ops.flatMap(h=>Object.values(h).filter(y=>typeof y=="string"))]){const h=z(E);h&&_(400,"secret_refused",`The run's output holds ${h}; nothing was applied`)}const a=this.clock(),r=new Set;let o=Number(i.next_ref);const c=R(E=>{const h=this.one("SELECT * FROM memory_fragments WHERE id=? AND lobby_id=? AND room_id=? AND state='pending'",E,s,n.room_id);return(!h||r.has(E))&&_(409,"memory_fragment_unavailable",`Fragment ${E} is not pending in room ${n.room_id}`),r.add(E),this.run("UPDATE memory_fragments SET state='used',curated_ms=? WHERE id=?",a,E),{author:T(String(h.author)).agent_id,evidence:String(h.evidence)}},"take"),d=R(E=>{const h=this.one("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? AND ref=? AND state='active'",s,n.room_id,E);return h||_(404,"memory_entry_not_found",`Entry ${E} is not active in room ${n.room_id}`),h},"active"),l=R(E=>this.run("INSERT INTO memory_entry_history(entry_id,lobby_id,room_id,row,changed_ms) VALUES(?,?,?,?,?)",String(E.id),s,n.room_id,JSON.stringify(E),a),"keep"),m=R((E,h,y)=>{h.length>Ae[E]&&_(400,"memory_text_too_long",`${y}: a ${E} holds at most ${Ae[E]} characters, not ${h.length}`)},"textFits"),N={added:0,updated:0,verified:0,archived:0,refused:0};for(const E of n.ops)if(E.op==="add"){const h=`${n.room_id}-${o++}`;m(E.kind,E.text,h),this.run("INSERT INTO memory_entries(id,lobby_id,room_id,ref,kind,title,text,keywords,provenance,sources,checked_commit,checked_ms,created_ms,updated_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)",O(),s,n.room_id,h,E.kind,E.title,E.text,E.keywords,JSON.stringify(K([],E.sources.map(c))),JSON.stringify(E.sources),n.commit,a,a,a),N.added++}else if(E.op==="update"){const h=d(E.entry);l(h);const y=String(h.kind),U=E.text??String(h.text);m(y,U,E.entry),this.run("UPDATE memory_entries SET title=?,text=?,keywords=?,provenance=?,sources=?,disputed=?,checked_commit=?,checked_ms=?,updated_ms=? WHERE id=?",E.title??String(h.title),U,E.keywords??String(h.keywords),JSON.stringify(K(JSON.parse(String(h.provenance)),E.sources.map(c))),JSON.stringify([...JSON.parse(String(h.sources)),...E.sources].slice(-20)),E.resolves?null:h.disputed??null,n.commit,a,a,String(h.id)),N.updated++}else if(E.op==="verify"){const h=d(E.entry);this.run("UPDATE memory_entries SET checked_commit=?,checked_ms=? WHERE id=?",n.commit,a,String(h.id)),N.verified++}else if(E.op==="archive"){const h=d(E.entry);if(E.into!==void 0){E.into===E.entry&&_(400,"memory_merge_invalid",`${E.entry} cannot merge into itself`);const y=d(E.into);y.kind!==h.kind&&_(400,"memory_merge_invalid",`${E.entry} is a ${String(h.kind)}, ${E.into} a ${String(y.kind)}: merge within a kind`),l(y),this.run("UPDATE memory_entries SET provenance=?,disputed=?,updated_ms=? WHERE id=?",JSON.stringify(K(JSON.parse(String(y.provenance)),JSON.parse(String(h.provenance)))),y.disputed??h.disputed??null,a,String(y.id))}l(h),this.run("UPDATE memory_entries SET state='archived',reason=?,into_ref=?,updated_ms=? WHERE id=?",E.reason,E.into??null,a,String(h.id)),N.archived++}else{if(c(E.fragment),E.dispute!==void 0){const h=d(E.dispute);l(h),this.run("UPDATE memory_entries SET disputed=?,updated_ms=? WHERE id=?",E.reason,a,String(h.id))}this.run(`UPDATE memory_fragments SET state='refused',note=?,unlanded=?${E.erase?",text='',evidence=''":""} WHERE id=?`,E.reason,Number(E.unlanded),E.fragment),N.refused++}const g=this.touchMemory(s,n.room_id);return this.run(`UPDATE memory_rooms SET next_ref=?,failures=0${n.review?",last_review_ms=?":""} WHERE lobby_id=? AND room_id=?`,...n.review?[o,a,s,n.room_id]:[o,s,n.room_id]),this.countCuration(e,n.usage),this.run("INSERT INTO memory_curations(id,lobby_id,room_id,curator,revision,commit_sha,review,created_ms,added,updated,verified,archived,refused,summary,usage) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",O(),s,n.room_id,e,g,n.commit,Number(n.review),a,N.added,N.updated,N.verified,N.archived,N.refused,n.summary,n.usage?JSON.stringify(n.usage):null),{revision:g,...N}})}curationFailed(e,t){e=this.key(e);const n=gt.parse(t);return this.transaction(()=>{const{lobby:s}=this.memoryCurator(e,n.room_id);return this.run("UPDATE memory_rooms SET failures=failures+1 WHERE lobby_id=? AND room_id=?",s,n.room_id),this.countCuration(e,n.usage),this.run("INSERT INTO memory_curations(id,lobby_id,room_id,curator,created_ms,error,usage) VALUES(?,?,?,?,?,?,?)",O(),s,n.room_id,e,this.clock(),n.error,n.usage?JSON.stringify(n.usage):null),{failures:Number(this.one("SELECT failures FROM memory_rooms WHERE lobby_id=? AND room_id=?",s,n.room_id).failures)}})}curationReport(e,t){e=this.key(e);const n=Rt.parse(t);this.memoryCurator(e,n.room_id);const s=z(n.content);return s&&_(400,"secret_refused",`The report holds ${s}; nothing was sent`),this.send(e,{client_request_id:n.client_request_id,to:n.to,kind:"report",priority:n.priority,content:n.content})}curationStatus(e,t){return this.all("SELECT r.room_id, r.failures, (SELECT COUNT(*) FROM memory_fragments f WHERE f.lobby_id=r.lobby_id AND f.room_id=r.room_id AND f.state='pending') AS pending, (SELECT MIN(created_ms) FROM memory_fragments f WHERE f.lobby_id=r.lobby_id AND f.room_id=r.room_id AND f.state='pending') AS oldest FROM memory_rooms r WHERE r.lobby_id=? AND r.curator=? ORDER BY r.room_id",e,D(e,t)).map(n=>({room_id:String(n.room_id),pending:Number(n.pending),oldest_pending_at:n.oldest===null?null:new Date(Number(n.oldest)).toISOString(),failures:Number(n.failures)}))}setMemoryCurator(e,t,n,s="owner"){return this.transaction(()=>{const i=this.actorLobby(s,e);this.room(i,t);const a=n===null?null:D(i,n);return a!==null&&!this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",i,t,a)&&_(409,"curator_not_member",`${n} is not a member of room ${t}`),this.touchMemory(i,t),this.run("UPDATE memory_rooms SET curator=?,failures=0 WHERE lobby_id=? AND room_id=?",a,i,t),this.audit(s,"set_memory_curator",i,t,{curator:n}),{curator:n}})}memoryOverview(e){const t=this.requireLobby(e),n=this.all(`SELECT room_id FROM memory_rooms WHERE lobby_id=?1
|
|
298
327
|
UNION SELECT room_id FROM memory_fragments WHERE lobby_id=?1
|
|
299
|
-
UNION SELECT room_id FROM memory_entries WHERE lobby_id=?1 ORDER BY room_id`,t).map(
|
|
328
|
+
UNION SELECT room_id FROM memory_entries WHERE lobby_id=?1 ORDER BY room_id`,t).map(i=>String(i.room_id)),s=this.clock()-10080*60*1e3;return n.map(i=>{const a=this.one("SELECT * FROM memory_rooms WHERE lobby_id=? AND room_id=?",t,i),r=a?.curator?T(String(a.curator)).agent_id:null;return{room_id:i,curator:r,revision:a?Number(a.revision):0,failures:a?Number(a.failures):0,loaded:le([{room_id:i,entries:this.activeEntries(t,i).map(o=>this.entryOf(o))}]).loaded,entries:this.all("SELECT * FROM memory_entries WHERE lobby_id=? AND room_id=? ORDER BY state, length(ref), ref",t,i).map(o=>{const c=this.entryOf(o);return{...c,state:String(o.state),reason:o.reason??null,into:o.into_ref??null,changes:Number(this.one("SELECT COUNT(*) AS n FROM memory_entry_history WHERE entry_id=?",String(o.id)).n),versions:this.all("SELECT row,changed_ms FROM memory_entry_history WHERE entry_id=? ORDER BY changed_ms DESC LIMIT 3",String(o.id)).map(d=>{const l=JSON.parse(String(d.row));return{changed_at:new Date(Number(d.changed_ms)).toISOString(),title:String(l.title),text:String(l.text)}}),curator_only:r!==null&&c.provenance.length>0&&c.provenance.every(d=>d.author===r)}}),fragments:this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? ORDER BY created_ms DESC",t,i).map(o=>this.fragmentOf(o)),authors:this.all("SELECT author, COUNT(*) AS n FROM memory_fragments WHERE lobby_id=? AND room_id=? AND created_ms>=? GROUP BY author ORDER BY n DESC",t,i,s).map(o=>({author:T(String(o.author)).agent_id,fragments:Number(o.n)})),curations:this.all("SELECT * FROM memory_curations WHERE lobby_id=? AND room_id=? ORDER BY created_ms DESC LIMIT 20",t,i).map(o=>({at:new Date(Number(o.created_ms)).toISOString(),curator:T(String(o.curator)).agent_id,revision:o.revision===null?null:Number(o.revision),commit:o.commit_sha??null,review:!!o.review,added:Number(o.added),updated:Number(o.updated),verified:Number(o.verified),archived:Number(o.archived),refused:Number(o.refused),summary:String(o.summary),error:o.error??null,usage:o.usage?JSON.parse(String(o.usage)):null}))}})}eraseMemory(e,t,n,s="owner"){this.transaction(()=>{const i=this.actorLobby(s,e),a=t==="entry"?this.one("SELECT id,room_id FROM memory_entries WHERE ref=? AND lobby_id=?",n,i):this.one("SELECT id,room_id FROM memory_fragments WHERE id=? AND lobby_id=?",n,i);a||_(404,"memory_not_found",`No ${t} ${n} in this lobby`),t==="entry"?(this.run("DELETE FROM memory_entries WHERE id=?",String(a.id)),this.run("DELETE FROM memory_entry_history WHERE entry_id=?",String(a.id))):this.run("DELETE FROM memory_fragments WHERE id=?",String(a.id)),this.touchMemory(i,String(a.room_id)),this.audit(s,`erase_memory_${t}`,i,String(a.room_id),{[t]:n})})}saveCodeStats(e,t){const n=Lt.parse(t);return this.transaction(()=>{const{lobby:s}=this.memoryMember(e,n.room_id,"send its code statistics"),i=new Date(this.clock()).toISOString();return this.run("INSERT INTO metadata(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=?2",`code_stats:${s}:${n.room_id}`,JSON.stringify({...n,received_at:i})),{room_id:n.room_id,received_at:i}})}codeStats(e){return this.all("SELECT m.value FROM rooms r JOIN metadata m ON m.key='code_stats:'||r.lobby_id||':'||r.room_id WHERE r.lobby_id=? ORDER BY r.room_id",e).map(t=>JSON.parse(String(t.value)))}eraseRoomMemory(e,t){for(const n of["memory_rooms","memory_fragments","memory_entries","memory_entry_history","memory_curations"])this.run(`DELETE FROM ${n} WHERE lobby_id=? AND room_id=?`,e,t)}pruneMemory(){const e=this.clock();return this.run("DELETE FROM memory_fragments WHERE state<>'pending' AND curated_ms<?",e-de)+this.run("DELETE FROM memory_entry_history WHERE changed_ms<?",e-He)+this.run("DELETE FROM memory_curations WHERE created_ms<?",e-He)+this.run("DELETE FROM memory_entries WHERE state='archived' AND updated_ms<?",e-Kt)}pruneTurns(){return this.run("DELETE FROM agent_turns WHERE ended_ms IS NOT NULL AND ended_ms<?",this.clock()-Jt)}pruneTokens(){return this.run("DELETE FROM token_calls WHERE at_ms<?",this.clock()-oe-1440*60*1e3)}tokenDays(e,t=""){const n=this.requireLobby(e);return this.all(`SELECT day, model, speed, geo, long, SUM(calls) AS calls, SUM(input) AS input,
|
|
300
329
|
SUM(cache_write_5m) AS cache_write_5m, SUM(cache_write_1h) AS cache_write_1h,
|
|
301
330
|
SUM(cache_read) AS cache_read, SUM(output) AS output
|
|
302
331
|
FROM token_days WHERE lobby_id=? AND day>=? GROUP BY day, model, speed, geo, long
|
|
303
|
-
ORDER BY day, model, speed, geo, long`,s,t).map(i=>({day:String(i.day),model:String(i.model),speed:String(i.speed),geo:String(i.geo),long:Number(i.long)===1,calls:Number(i.calls),input:Number(i.input),cache_write_5m:Number(i.cache_write_5m),cache_write_1h:Number(i.cache_write_1h),cache_read:Number(i.cache_read),output:Number(i.output)}))}clearTokens(e,t,s){const i=new Date(this.clock()-ne).toISOString().slice(0,10);return(!/^\d{4}-\d{2}-\d{2}$/u.test(s)||Number.isNaN(Date.parse(`${s}T00:00:00Z`)))&&d(400,"invalid_date","Give the date as YYYY-MM-DD."),s>i&&d(400,"tokens_too_recent",`Only totals before ${i} can be deleted: the last four weeks can still take late reports.`),this.transaction(()=>{const n=this.actorLobby(e,t),a=this.run("DELETE FROM token_days WHERE lobby_id=? AND day<?",n,s);return this.audit(e,"tokens_clear",n,s,{totals_deleted:a}),{deleted:a}})}summaryRecord(e){return{sequence:Number(e.sequence),lobby_id:String(e.lobby_id),created_at:String(e.created_at),created_ms:Number(e.created_ms),input_hash:String(e.input_hash),sources:JSON.parse(String(e.sources)),model:String(e.model),usage:{input_tokens:Number(e.input_tokens),output_tokens:Number(e.output_tokens),cache_creation_input_tokens:Number(e.cache_creation_input_tokens),cache_read_input_tokens:Number(e.cache_read_input_tokens)},cost_usd:Number(e.cost_usd),summary:e.summary===null?null:JSON.parse(String(e.summary)),error:e.error}}boardSummary(e){const t=this.one("SELECT * FROM board_summaries WHERE lobby_id=? AND summary IS NOT NULL ORDER BY sequence DESC LIMIT 1",e);return t?this.summaryRecord(t):null}boardLastAttempt(e){const t=this.one("SELECT * FROM board_summaries WHERE lobby_id=? ORDER BY sequence DESC LIMIT 1",e);return t?this.summaryRecord(t):null}saveBoardSummary(e){return this.transaction(()=>{this.requireLobby(e.lobby_id);const t=this.clock();return this.run("INSERT INTO board_summaries(lobby_id,created_at,created_ms,input_hash,sources,model,input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens,cost_usd,summary,error) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",e.lobby_id,new Date(t).toISOString(),t,e.input_hash,JSON.stringify(e.sources),e.model,e.usage.input_tokens,e.usage.output_tokens,e.usage.cache_creation_input_tokens,e.usage.cache_read_input_tokens,e.cost_usd,e.summary===null?null:JSON.stringify(e.summary),e.error),this.run("DELETE FROM board_summaries WHERE lobby_id=? AND created_ms<? AND sequence IS NOT (SELECT MAX(sequence) FROM board_summaries WHERE lobby_id=? AND summary IS NOT NULL)",e.lobby_id,t-720*60*60*1e3,e.lobby_id),this.boardLastAttempt(e.lobby_id)})}captionRecord(e){return{sequence:Number(e.sequence),lobby_id:String(e.lobby_id),created_at:String(e.created_at),created_ms:Number(e.created_ms),model:String(e.model),usage:{input_tokens:Number(e.input_tokens),output_tokens:Number(e.output_tokens),cache_creation_input_tokens:Number(e.cache_creation_input_tokens),cache_read_input_tokens:Number(e.cache_read_input_tokens)},cost_usd:Number(e.cost_usd),caption:e.caption,picture:e.picture,error:e.error}}boardCaption(e){const t=this.one("SELECT * FROM board_captions WHERE lobby_id=? AND caption IS NOT NULL ORDER BY sequence DESC LIMIT 1",e);return t?this.captionRecord(t):null}boardLastCaption(e){const t=this.one("SELECT * FROM board_captions WHERE lobby_id=? ORDER BY sequence DESC LIMIT 1",e);return t?this.captionRecord(t):null}saveBoardCaption(e){return this.transaction(()=>{this.requireLobby(e.lobby_id);const t=this.clock();return this.run("INSERT INTO board_captions(lobby_id,created_at,created_ms,model,input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens,cost_usd,caption,picture,error) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",e.lobby_id,new Date(t).toISOString(),t,e.model,e.usage.input_tokens,e.usage.output_tokens,e.usage.cache_creation_input_tokens,e.usage.cache_read_input_tokens,e.cost_usd,e.caption,e.picture,e.error),this.run("DELETE FROM board_captions WHERE lobby_id=? AND created_ms<? AND sequence IS NOT (SELECT MAX(sequence) FROM board_captions WHERE lobby_id=? AND caption IS NOT NULL)",e.lobby_id,t-720*60*60*1e3,e.lobby_id),this.boardLastCaption(e.lobby_id)})}boardUsage(e,t){const s=this.one("SELECT COUNT(*) AS runs, COUNT(error) AS failed, COALESCE(SUM(cost_usd),0) AS cost, COALESCE(SUM(input_tokens+cache_read_input_tokens+cache_creation_input_tokens),0) AS input, COALESCE(SUM(output_tokens),0) AS output FROM (SELECT error,cost_usd,input_tokens,cache_read_input_tokens,cache_creation_input_tokens,output_tokens FROM board_summaries WHERE lobby_id=?1 AND created_ms>=?2 UNION ALL SELECT error,cost_usd,input_tokens,cache_read_input_tokens,cache_creation_input_tokens,output_tokens FROM board_captions WHERE lobby_id=?1 AND created_ms>=?2)",e,t);return{runs:Number(s.runs),failed:Number(s.failed),cost_usd:Number(s.cost),input_tokens:Number(s.input),output_tokens:Number(s.output)}}ownedLobby(e,t){const s=$.safeParse(t).data;return s===void 0||!this.one("SELECT 1 FROM lobbies l JOIN accounts a ON a.account_id=l.owner_account WHERE l.lobby_id=? AND a.account_id=? AND a.disabled_at IS NULL",s,e)?d(404,"lobby_not_found","Unknown lobby"):s}actorLobby(e,t){return typeof e=="object"?this.ownedLobby(e.account_id,t):this.requireLobby(t)}ownsLobby(e,t){try{return this.ownedLobby(e,t),!0}catch{return!1}}ensureAccount(e){const t=this.one("SELECT account_id FROM accounts WHERE email=?",e);if(t)return String(t.account_id);const s=O();return this.run("INSERT INTO accounts(account_id,email,created_at) VALUES(?,?,?)",s,e,this.iso()),s}accountListing(e){const t=String(e.account_id);return{account_id:t,email:String(e.email),may_create_lobbies:!!e.may_create_lobbies,created_at:String(e.created_at),disabled_at:e.disabled_at,owns:this.all("SELECT lobby_id FROM lobbies WHERE owner_account=? ORDER BY lobby_id",t).map(s=>String(s.lobby_id)),views:this.all("SELECT lobby_id FROM board_viewers WHERE account_id=? ORDER BY lobby_id",t).map(s=>String(s.lobby_id))}}accounts(){return this.all("SELECT * FROM accounts ORDER BY email").map(e=>this.accountListing(e))}setAccount(e,t){const s=at.parse(e);return this.transaction(()=>{const i=this.ensureAccount(s.email);if(s.may_create_lobbies!==void 0&&this.run("UPDATE accounts SET may_create_lobbies=? WHERE account_id=?",Number(s.may_create_lobbies),i),s.enabled===!0&&this.run("UPDATE accounts SET disabled_at=NULL WHERE account_id=?",i),s.enabled===!1){this.run("UPDATE accounts SET disabled_at=COALESCE(disabled_at,?) WHERE account_id=?",this.iso(),i);for(const n of["console_sessions","board_sessions","board_handoffs"])this.run(`DELETE FROM ${n} WHERE account_id=?`,i);this.run("DELETE FROM sign_in_codes WHERE email_key=?",t)}return this.audit(null,"account",null,s.email,{enabled:s.enabled,may_create_lobbies:s.may_create_lobbies}),this.accountListing(this.one("SELECT * FROM accounts WHERE account_id=?",i))})}grantLobby(e,t){const s=q.parse(e);return this.transaction(()=>{const i=this.requireLobby(t),n=this.ensureAccount(s),a=this.one("SELECT owner_account FROM lobbies WHERE lobby_id=?",i).owner_account;return a!==null&&a!==n&&d(409,"lobby_owned","Another account owns this lobby; end that ownership first"),this.run("UPDATE lobbies SET owner_account=? WHERE lobby_id=?",n,i),this.audit(null,"grant_lobby",i,s),this.accountListing(this.one("SELECT * FROM accounts WHERE account_id=?",n))})}ungrantLobby(e,t){const s=q.parse(e);return this.transaction(()=>{const i=this.requireLobby(t),n=this.one("SELECT a.* FROM accounts a JOIN lobbies l ON l.owner_account=a.account_id WHERE a.email=? AND l.lobby_id=?",s,i);return n||d(404,"owner_not_found","That account does not own this lobby"),this.run("UPDATE lobbies SET owner_account=NULL WHERE lobby_id=?",i),this.audit(null,"ungrant_lobby",i,s),this.accountListing(n)})}eligibleAccount(e){const t=this.one("SELECT account_id FROM accounts a WHERE email=? AND disabled_at IS NULL AND (may_create_lobbies=1 OR EXISTS (SELECT 1 FROM lobbies l WHERE l.owner_account=a.account_id) OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.account_id=a.account_id))",e);return t?String(t.account_id):null}limitWait(e,t,s){const i=this.one("SELECT window_ms,count FROM sign_in_limits WHERE key=?",e),n=this.clock();return!i||n-Number(i.window_ms)>=s||Number(i.count)<t?0:Math.max(1,Math.ceil((Number(i.window_ms)+s-n)/1e3))}limitCount(e,t){const s=this.clock();this.run("INSERT INTO sign_in_limits(key,window_ms,count) VALUES(?1,?2,1) ON CONFLICT(key) DO UPDATE SET window_ms=CASE WHEN ?2-window_ms>=?3 THEN ?2 ELSE window_ms END, count=CASE WHEN ?2-window_ms>=?3 THEN 1 ELSE count+1 END",e,s,t)}beginSignIn(e,t,s){return this.transaction(()=>{const i=this.limitWait(`send:${t}`,1,f.sendWindowMs)||this.limitWait(`send-hour:${t}`,f.sendsPerAddressPerHour,36e5)||this.limitWait("send:all",f.sendsPerHour,36e5);if(i)return{wait:i,send:!1};if(this.limitCount(`send:${t}`,f.sendWindowMs),this.limitCount(`send-hour:${t}`,36e5),!this.eligibleAccount(e))return{wait:0,send:!1};this.limitCount("send:all",36e5);const n=this.clock();return this.run("INSERT INTO sign_in_codes(email_key,code_mac,created_ms,expires_ms,attempts) VALUES(?,?,?,?,0) ON CONFLICT(email_key) DO UPDATE SET code_mac=excluded.code_mac,created_ms=excluded.created_ms,expires_ms=excluded.expires_ms,attempts=0",t,s,n,n+f.codeMs),{wait:0,send:!0}})}finishSignIn(e,t,s,i){return this.transaction(()=>{const n=`wrong:${t}`,a=this.limitWait(n,f.wrongPerHour,36e5);if(a)return{error:"budget",wait:a};const r=this.clock(),o=this.one("SELECT * FROM sign_in_codes WHERE email_key=?",t),_=g(l=>(this.limitCount(n,36e5),{error:l,wait:0}),"wrong");if(!o||Number(o.expires_ms)<=r)return _("invalid");if(Number(o.attempts)>=f.attempts)return _("attempts");if(!de(Buffer.from(String(o.code_mac),"hex"),Buffer.from(s,"hex")))return this.run("UPDATE sign_in_codes SET attempts=attempts+1 WHERE email_key=?",t),_(Number(o.attempts)+1>=f.attempts?"attempts":"invalid");this.run("DELETE FROM sign_in_codes WHERE email_key=?",t);const c=this.eligibleAccount(e);return c?(this.run("INSERT INTO console_sessions(token_hash,account_id,created_ms,refreshed_ms,expires_ms) VALUES(?,?,?,?,?)",i,c,r,r,r+f.sessionMs),{account_id:c}):{error:"invalid",wait:0}})}consoleSession(e){const t=this.one("SELECT a.account_id,a.email,a.may_create_lobbies,s.refreshed_ms FROM console_sessions s JOIN accounts a ON a.account_id=s.account_id WHERE s.token_hash=? AND s.expires_ms>? AND a.disabled_at IS NULL",e,this.clock());return t?{account_id:String(t.account_id),email:String(t.email),may_create_lobbies:!!t.may_create_lobbies,refresh:this.clock()-Number(t.refreshed_ms)>=864e5}:null}refreshConsoleSession(e){const t=this.clock();this.run("UPDATE console_sessions SET refreshed_ms=?,expires_ms=? WHERE token_hash=?",t,t+f.sessionMs,e)}endConsoleSession(e){this.run("DELETE FROM console_sessions WHERE token_hash=?",e)}consoleLobbies(e){const t=this.lobbyList(e),s=this.all("SELECT v.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||v.lobby_id) AS display_name,COALESCE(c.visibility,'private') AS visibility FROM board_viewers v JOIN accounts a ON a.account_id=v.account_id JOIN lobbies l ON l.lobby_id=v.lobby_id LEFT JOIN board_configs c ON c.lobby_id=v.lobby_id WHERE v.account_id=? AND a.disabled_at IS NULL AND (l.owner_account IS NULL OR l.owner_account<>v.account_id) ORDER BY v.lobby_id",e).map(i=>({lobby_id:String(i.lobby_id),display_name:i.display_name??null,visibility:i.visibility}));return{owned:t,viewing:s}}operatorLobbies(){return this.lobbyList(null)}lobbyList(e){return this.all(`SELECT l.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||l.lobby_id) AS display_name,l.created_at,COALESCE(c.visibility,'private') AS visibility,(SELECT COUNT(*) FROM agents a WHERE a.lobby_id=l.lobby_id AND a.enabled=1) AS agents,(SELECT COUNT(*) FROM rooms r WHERE r.lobby_id=l.lobby_id) AS rooms FROM lobbies l LEFT JOIN board_configs c ON c.lobby_id=l.lobby_id${e===null?"":" WHERE l.owner_account=? AND EXISTS (SELECT 1 FROM accounts o WHERE o.account_id=l.owner_account AND o.disabled_at IS NULL)"} ORDER BY l.lobby_id`,...e===null?[]:[e]).map(t=>({lobby_id:String(t.lobby_id),display_name:t.display_name??null,created_at:t.created_at,visibility:t.visibility,agents:Number(t.agents),rooms:Number(t.rooms)}))}lobbyCreatedAt(e){return this.one("SELECT created_at FROM lobbies WHERE lobby_id=?",e)?.created_at??null}createOwnedLobby(e,t){const s=Re.parse(t.lobby_id),i=t.first_room===void 0?void 0:I.parse(t.first_room);return this.transaction(()=>{this.one("SELECT 1 FROM accounts WHERE account_id=? AND disabled_at IS NULL AND may_create_lobbies=1",e)||d(403,"lobby_creation_forbidden","This account may not create lobbies"),Number(this.one("SELECT COUNT(*) AS n FROM lobbies WHERE owner_account=?",e).n)>=f.lobbiesPerAccount&&d(409,"lobby_allowance",`An account may own at most ${f.lobbiesPerAccount} lobbies`),this.insertLobby(s,e),this.audit({account_id:e},"create_lobby",s),i!==void 0&&(this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",s,i),this.audit({account_id:e},"create_room",s,i))}),this.consoleLobbies(e).owned.find(n=>n.lobby_id===s)}boardVisibility(e){if(!$.safeParse(e).success)return null;const t=this.one("SELECT COALESCE(c.visibility,'private') AS visibility FROM lobbies l LEFT JOIN board_configs c ON c.lobby_id=l.lobby_id WHERE l.lobby_id=?",e);return t?t.visibility:null}mayViewBoard(e,t){return!!this.one("SELECT 1 FROM accounts a JOIN lobbies l ON l.lobby_id=? WHERE a.account_id=? AND a.disabled_at IS NULL AND (l.owner_account=a.account_id OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.lobby_id=l.lobby_id AND v.account_id=a.account_id))",t,e)}createHandoff(e,t,s){return this.transaction(()=>this.mayViewBoard(e,t)?(this.run("INSERT INTO board_handoffs(token_hash,lobby_id,account_id,expires_ms) VALUES(?,?,?,?)",s,t,e,this.clock()+f.handoffMs),!0):!1)}consumeHandoff(e,t,s,i=null){return this.transaction(()=>{const n=this.one("SELECT * FROM board_handoffs WHERE token_hash=? AND lobby_id=?",t,e);if(!n)return!1;this.run("DELETE FROM board_handoffs WHERE token_hash=?",t);const a=this.clock(),r=String(n.account_id);return Number(n.expires_ms)<=a||!this.mayViewBoard(r,e)?!1:(i&&this.run("DELETE FROM board_sessions WHERE token_hash=? AND lobby_id=?",i,e),this.run("INSERT INTO board_sessions(token_hash,lobby_id,account_id,created_ms,expires_ms) VALUES(?,?,?,?,?)",s,e,r,a,a+f.sessionMs),!0)})}boardViewer(e,t){return this.boardSession(e,t)?.email??null}boardSession(e,t){const s=this.clock(),i=this.one("SELECT s.account_id,s.expires_ms,a.email FROM board_sessions s JOIN accounts a ON a.account_id=s.account_id WHERE s.token_hash=? AND s.lobby_id=? AND s.expires_ms>?",t,e,s);return!i||!this.mayViewBoard(String(i.account_id),e)?null:{account_id:String(i.account_id),email:String(i.email),refresh:s+f.sessionMs-Number(i.expires_ms)>=864e5,owner:this.ownsLobby(String(i.account_id),e)}}refreshBoardSession(e){this.run("UPDATE board_sessions SET expires_ms=? WHERE token_hash=?",this.clock()+f.sessionMs,e)}endBoardSession(e){this.run("DELETE FROM board_sessions WHERE token_hash=?",e)}boardSessionCount(e){return Number(this.one("SELECT COUNT(*) AS n FROM board_sessions s JOIN accounts a ON a.account_id=s.account_id JOIN lobbies l ON l.lobby_id=s.lobby_id WHERE s.lobby_id=? AND s.expires_ms>? AND a.disabled_at IS NULL AND (l.owner_account=s.account_id OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.lobby_id=s.lobby_id AND v.account_id=s.account_id))",e,this.clock()).n)}boardViewers(e){return this.all("SELECT a.email,v.added_at,a.disabled_at,(SELECT COUNT(*) FROM board_sessions s WHERE s.lobby_id=v.lobby_id AND s.account_id=v.account_id AND s.expires_ms>?) AS sessions FROM board_viewers v JOIN accounts a ON a.account_id=v.account_id WHERE v.lobby_id=? ORDER BY a.email",this.clock(),e).map(t=>({email:String(t.email),added_at:String(t.added_at),disabled:t.disabled_at!==null,sessions:t.disabled_at===null?Number(t.sessions):0}))}addBoardViewer(e,t,s){const i=q.parse(s);return this.transaction(()=>{const n=this.actorLobby(e,t);Number(this.one("SELECT COUNT(*) AS n FROM board_viewers WHERE lobby_id=?",n).n)>=f.viewersPerLobby&&d(409,"viewer_capacity",`A dashboard lists at most ${f.viewersPerLobby} viewers`),this.run("INSERT INTO board_viewers(lobby_id,account_id,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",n,this.ensureAccount(i),this.iso()),this.audit(e,"viewer_add",n,i)}),this.boardViewers(t)}removeBoardViewer(e,t,s){const i=q.parse(s);return this.transaction(()=>{const n=this.actorLobby(e,t),a=this.one("SELECT account_id FROM accounts WHERE email=?",i);(!a||!this.run("DELETE FROM board_viewers WHERE lobby_id=? AND account_id=?",n,String(a.account_id)))&&d(404,"viewer_not_found","That address is not a viewer of this dashboard"),this.run("DELETE FROM board_sessions WHERE lobby_id=? AND account_id=? AND account_id NOT IN (SELECT owner_account FROM lobbies WHERE lobby_id=? AND owner_account IS NOT NULL)",n,String(a.account_id),n),this.audit(e,"viewer_remove",n,i)}),this.boardViewers(t)}lobbyRepos(e){return this.all("SELECT repo FROM lobby_repos WHERE lobby_id=? ORDER BY repo",this.requireLobby(e)).map(t=>String(t.repo))}approveRepo(e,t){const s=De.parse(t).toLowerCase();return this.transaction(()=>{const i=this.requireLobby(e);this.run("INSERT INTO lobby_repos(lobby_id,repo,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",i,s,this.iso()),this.audit(null,"repo_approve",i,s)}),this.lobbyRepos(e)}removeRepo(e,t){const s=De.parse(t).toLowerCase();return this.transaction(()=>{const i=this.requireLobby(e),n=this.all("SELECT room_id FROM board_sources WHERE lobby_id=? AND lower(repo)=? ORDER BY room_id",i,s).map(a=>String(a.room_id));n.length&&d(409,"repo_in_use",`Rooms still read notes from ${s} (${n.join(", ")}); remove their sources first`,{rooms:n}),this.run("DELETE FROM lobby_repos WHERE lobby_id=? AND repo=?",i,s)||d(404,"repo_not_found","That repository is not approved for this lobby"),this.audit(null,"repo_remove",i,s)}),this.lobbyRepos(e)}archivedRooms(e){return this.all("SELECT room_id,archived_at FROM archived_rooms WHERE lobby_id=? ORDER BY archived_at DESC,room_id",e).map(t=>({room_id:String(t.room_id),archived_at:String(t.archived_at)}))}cancelRoomSchedules(e,t,s){const i=this.all("SELECT id,sender FROM schedules WHERE lobby_id=? AND cancelled_at IS NULL AND json_extract(template,'$.room')=? ORDER BY created_at",e,t);for(const n of i)this.run("UPDATE schedules SET cancelled_at=?,next_run_ms=NULL,last_error=? WHERE id=?",this.iso(),s,String(n.id));return i}archiveRoom(e,t,s){return I.parse(t),this.transaction(()=>{const i=this.roomManager(e,s),n=this.room(i,t),a=this.cancelRoomSchedules(i,t,"room_archived");return this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=?",i,t),this.run("DELETE FROM rooms WHERE lobby_id=? AND room_id=?",i,t),this.run("INSERT INTO archived_rooms(lobby_id,room_id,archived_at) VALUES(?,?,?)",i,t,this.iso()),this.audit(e,"archive_room",i,t,{former_members:n.members}),{lobby_id:i,room_id:t,archived:!0,former_members:n.members,cancelled_schedules:a.map(r=>({schedule_id:String(r.id),from:T(String(r.sender)).agent_id}))}})}restoreRoom(e,t,s){return I.parse(t),this.transaction(()=>{const i=this.roomManager(e,s);return this.run("DELETE FROM archived_rooms WHERE lobby_id=? AND room_id=?",i,t)||d(404,"room_not_found","No archived room has that name"),Number(this.one("SELECT COUNT(*) AS n FROM rooms WHERE lobby_id=?",i).n)>=this.maxRooms&&d(409,"room_capacity","Room capacity reached for this lobby"),this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",i,t),this.audit(e,"restore_room",i,t),this.room(i,t)})}deleteLobby(e,t,s){let i=[];const n=this.transaction(()=>{const a=this.actorLobby(e,t);s!==a&&d(400,"confirmation_mismatch","Type the lobby name exactly to delete it");const r=this.all("SELECT agent_id FROM agents WHERE lobby_id=?",a).map(l=>String(l.agent_id)),o=this.all("SELECT sha256 FROM attachments WHERE lobby_id=?1 UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1",a).map(l=>String(l.sha256)),_="SELECT agent_id FROM agents WHERE lobby_id=?1",c="SELECT id FROM messages WHERE lobby_id=?1";for(const l of[`DELETE FROM seen_receipts WHERE message_id IN (${c}) OR sender IN (${_}) OR agent_id IN (${_})`,`DELETE FROM notification_attempts WHERE agent_id IN (${_})`,`DELETE FROM deliveries WHERE message_id IN (${c}) OR agent_id IN (${_})`,`DELETE FROM message_attachments WHERE message_id IN (${c}) OR attachment_id IN (SELECT id FROM attachments WHERE lobby_id=?1)`,`DELETE FROM schedule_attachments WHERE schedule_id IN (SELECT id FROM schedules WHERE lobby_id=?1 OR sender IN (${_})) OR attachment_id IN (SELECT id FROM attachments WHERE lobby_id=?1)`,`DELETE FROM schedules WHERE lobby_id=?1 OR sender IN (${_})`,"DELETE FROM messages WHERE lobby_id=?1",`DELETE FROM attachment_uploads WHERE lobby_id=?1 OR uploader IN (${_})`,"DELETE FROM attachments WHERE lobby_id=?1",`DELETE FROM checkpoints WHERE agent_id IN (${_})`,`DELETE FROM runtime_history WHERE agent_id IN (${_})`,`DELETE FROM agent_turns WHERE agent_id IN (${_})`,"DELETE FROM room_members WHERE lobby_id=?1","DELETE FROM rooms WHERE lobby_id=?1","DELETE FROM archived_rooms WHERE lobby_id=?1","DELETE FROM board_notes WHERE lobby_id=?1","DELETE FROM board_sources WHERE lobby_id=?1","DELETE FROM board_configs WHERE lobby_id=?1","DELETE FROM board_summaries WHERE lobby_id=?1","DELETE FROM board_captions WHERE lobby_id=?1","DELETE FROM board_sessions WHERE lobby_id=?1","DELETE FROM board_handoffs WHERE lobby_id=?1","DELETE FROM board_viewers WHERE lobby_id=?1","DELETE FROM metadata WHERE key IN ('board_icon:'||?1,'display_name:'||?1,'currency:'||?1) OR (key>'code_stats:'||?1||':' AND key<'code_stats:'||?1||';')","DELETE FROM lobby_repos WHERE lobby_id=?1","DELETE FROM token_calls WHERE lobby_id=?1","DELETE FROM token_days WHERE lobby_id=?1","DELETE FROM memory_rooms WHERE lobby_id=?1","DELETE FROM memory_fragments WHERE lobby_id=?1","DELETE FROM memory_entries WHERE lobby_id=?1","DELETE FROM memory_entry_history WHERE lobby_id=?1","DELETE FROM memory_curations WHERE lobby_id=?1","DELETE FROM agents WHERE lobby_id=?1","DELETE FROM lobbies WHERE lobby_id=?1"])this.run(l,a);i=o.filter(l=>!this.one("SELECT 1 FROM attachments WHERE sha256=?1 AND expired_at IS NULL UNION ALL SELECT 1 FROM attachment_uploads WHERE sha256=?1 LIMIT 1",l));for(const l of i)this.run("DELETE FROM attachment_blobs WHERE sha256=?",l);return this.run("INSERT INTO deleted_lobbies(lobby_id,deleted_at) VALUES(?,?)",a,this.iso()),this.audit(e,"delete_lobby",a,null,{agents:r.length}),{lobby_id:a,agents:r}});if(this.attachmentsDirectory)for(const a of i)try{K(this.blobPath(a))}catch{}return n}activity(e,t=100){return this.all("SELECT e.*,a.email FROM admin_events e LEFT JOIN accounts a ON a.account_id=e.account_id WHERE e.lobby_id=? ORDER BY e.sequence DESC LIMIT ?",e,t).map(s=>({at:String(s.created_at),actor_kind:s.actor_kind??"operator",actor:s.actor_kind==="agent"&&s.agent_id!==null?T(String(s.agent_id)).agent_id:s.actor_kind==="account"?s.email??"a removed account":null,action:String(s.action),target:s.target,detail:s.detail===null?null:JSON.parse(String(s.detail))}))}pruneSignIn(){const e=this.clock(),t=e-36e5;this.one("SELECT 1 FROM sign_in_codes WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM console_sessions WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM board_sessions WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM board_handoffs WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM sign_in_limits WHERE window_ms<=?2 LIMIT 1",e,t)&&this.transaction(()=>{for(const i of["sign_in_codes","console_sessions","board_sessions","board_handoffs"])this.run(`DELETE FROM ${i} WHERE expires_ms<=?`,e);this.run("DELETE FROM sign_in_limits WHERE window_ms<=?",t)})}enterRecovery(){this.transaction(()=>{this.run("UPDATE metadata SET value=? WHERE key='incarnation'",O()),this.run("UPDATE agents SET token_hash=NULL,runtime_id=NULL,generation=generation+1,ready=0,paused=1,resume_at_ms=NULL,probe_nonce=lower(hex(randomblob(16))),bootstrap_attempts=0,bootstrap_next_at=?,owner_requests=0,owner_requests_text=NULL,owner_requests_at=NULL,status_text=NULL,status_set_at=NULL",this.clock()),this.run("UPDATE deliveries SET attempts=0,next_attempt_at=? WHERE seen_at IS NULL",this.clock()),this.run("DELETE FROM console_sessions"),this.run("DELETE FROM board_sessions"),this.run("DELETE FROM board_handoffs"),this.run("DELETE FROM sign_in_codes"),this.run("UPDATE accounts SET disabled_at=? WHERE disabled_at IS NULL",this.iso()),this.audit(null,"enter_recovery",null)})}}export{ds as LobbyStore,Xt as MEMORY_PENDING_LIMIT,Wt as MESSAGE_RETENTION_MS,F as SCHEMA_VERSION,ne as TOKEN_RETENTION_MS,wt as TURN_RETENTION_MS,os as backupDatabase};
|
|
332
|
+
ORDER BY day, model, speed, geo, long`,n,t).map(s=>({day:String(s.day),model:String(s.model),speed:String(s.speed),geo:String(s.geo),long:Number(s.long)===1,calls:Number(s.calls),input:Number(s.input),cache_write_5m:Number(s.cache_write_5m),cache_write_1h:Number(s.cache_write_1h),cache_read:Number(s.cache_read),output:Number(s.output)}))}clearTokens(e,t,n){const s=new Date(this.clock()-oe).toISOString().slice(0,10);return(!/^\d{4}-\d{2}-\d{2}$/u.test(n)||Number.isNaN(Date.parse(`${n}T00:00:00Z`)))&&_(400,"invalid_date","Give the date as YYYY-MM-DD."),n>s&&_(400,"tokens_too_recent",`Only totals before ${s} can be deleted: the last four weeks can still take late reports.`),this.transaction(()=>{const i=this.actorLobby(e,t),a=this.run("DELETE FROM token_days WHERE lobby_id=? AND day<?",i,n);return this.audit(e,"tokens_clear",i,n,{totals_deleted:a}),{deleted:a}})}summaryRecord(e){return{sequence:Number(e.sequence),lobby_id:String(e.lobby_id),created_at:String(e.created_at),created_ms:Number(e.created_ms),input_hash:String(e.input_hash),sources:JSON.parse(String(e.sources)),model:String(e.model),usage:{input_tokens:Number(e.input_tokens),output_tokens:Number(e.output_tokens),cache_creation_input_tokens:Number(e.cache_creation_input_tokens),cache_read_input_tokens:Number(e.cache_read_input_tokens)},cost_usd:Number(e.cost_usd),summary:e.summary===null?null:JSON.parse(String(e.summary)),error:e.error}}boardSummary(e){const t=this.one("SELECT * FROM board_summaries WHERE lobby_id=? AND summary IS NOT NULL ORDER BY sequence DESC LIMIT 1",e);return t?this.summaryRecord(t):null}boardLastAttempt(e){const t=this.one("SELECT * FROM board_summaries WHERE lobby_id=? ORDER BY sequence DESC LIMIT 1",e);return t?this.summaryRecord(t):null}saveBoardSummary(e){return this.transaction(()=>{this.requireLobby(e.lobby_id);const t=this.clock();return this.run("INSERT INTO board_summaries(lobby_id,created_at,created_ms,input_hash,sources,model,input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens,cost_usd,summary,error) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",e.lobby_id,new Date(t).toISOString(),t,e.input_hash,JSON.stringify(e.sources),e.model,e.usage.input_tokens,e.usage.output_tokens,e.usage.cache_creation_input_tokens,e.usage.cache_read_input_tokens,e.cost_usd,e.summary===null?null:JSON.stringify(e.summary),e.error),this.run("DELETE FROM board_summaries WHERE lobby_id=? AND created_ms<? AND sequence IS NOT (SELECT MAX(sequence) FROM board_summaries WHERE lobby_id=? AND summary IS NOT NULL)",e.lobby_id,t-720*60*60*1e3,e.lobby_id),this.boardLastAttempt(e.lobby_id)})}captionRecord(e){return{sequence:Number(e.sequence),lobby_id:String(e.lobby_id),created_at:String(e.created_at),created_ms:Number(e.created_ms),model:String(e.model),usage:{input_tokens:Number(e.input_tokens),output_tokens:Number(e.output_tokens),cache_creation_input_tokens:Number(e.cache_creation_input_tokens),cache_read_input_tokens:Number(e.cache_read_input_tokens)},cost_usd:Number(e.cost_usd),caption:e.caption,picture:e.picture,error:e.error}}boardCaption(e){const t=this.one("SELECT * FROM board_captions WHERE lobby_id=? AND caption IS NOT NULL ORDER BY sequence DESC LIMIT 1",e);return t?this.captionRecord(t):null}boardLastCaption(e){const t=this.one("SELECT * FROM board_captions WHERE lobby_id=? ORDER BY sequence DESC LIMIT 1",e);return t?this.captionRecord(t):null}saveBoardCaption(e){return this.transaction(()=>{this.requireLobby(e.lobby_id);const t=this.clock();return this.run("INSERT INTO board_captions(lobby_id,created_at,created_ms,model,input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens,cost_usd,caption,picture,error) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",e.lobby_id,new Date(t).toISOString(),t,e.model,e.usage.input_tokens,e.usage.output_tokens,e.usage.cache_creation_input_tokens,e.usage.cache_read_input_tokens,e.cost_usd,e.caption,e.picture,e.error),this.run("DELETE FROM board_captions WHERE lobby_id=? AND created_ms<? AND sequence IS NOT (SELECT MAX(sequence) FROM board_captions WHERE lobby_id=? AND caption IS NOT NULL)",e.lobby_id,t-720*60*60*1e3,e.lobby_id),this.boardLastCaption(e.lobby_id)})}boardUsage(e,t){const n=this.one("SELECT COUNT(*) AS runs, COUNT(error) AS failed, COALESCE(SUM(cost_usd),0) AS cost, COALESCE(SUM(input_tokens+cache_read_input_tokens+cache_creation_input_tokens),0) AS input, COALESCE(SUM(output_tokens),0) AS output FROM (SELECT error,cost_usd,input_tokens,cache_read_input_tokens,cache_creation_input_tokens,output_tokens FROM board_summaries WHERE lobby_id=?1 AND created_ms>=?2 UNION ALL SELECT error,cost_usd,input_tokens,cache_read_input_tokens,cache_creation_input_tokens,output_tokens FROM board_captions WHERE lobby_id=?1 AND created_ms>=?2)",e,t);return{runs:Number(n.runs),failed:Number(n.failed),cost_usd:Number(n.cost),input_tokens:Number(n.input),output_tokens:Number(n.output)}}ownedLobby(e,t){const n=Y.safeParse(t).data;return n===void 0||!this.one("SELECT 1 FROM lobbies l JOIN accounts a ON a.account_id=l.owner_account WHERE l.lobby_id=? AND a.account_id=? AND a.disabled_at IS NULL",n,e)?_(404,"lobby_not_found","Unknown lobby"):n}actorLobby(e,t){return typeof e=="object"?this.ownedLobby(e.account_id,t):this.requireLobby(t)}ownsLobby(e,t){try{return this.ownedLobby(e,t),!0}catch{return!1}}ensureAccount(e){const t=this.one("SELECT account_id FROM accounts WHERE email=?",e);if(t)return String(t.account_id);const n=O();return this.run("INSERT INTO accounts(account_id,email,created_at) VALUES(?,?,?)",n,e,this.iso()),n}accountListing(e){const t=String(e.account_id);return{account_id:t,email:String(e.email),may_create_lobbies:!!e.may_create_lobbies,created_at:String(e.created_at),disabled_at:e.disabled_at,owns:this.all("SELECT lobby_id FROM lobbies WHERE owner_account=? ORDER BY lobby_id",t).map(n=>String(n.lobby_id)),views:this.all("SELECT lobby_id FROM board_viewers WHERE account_id=? ORDER BY lobby_id",t).map(n=>String(n.lobby_id))}}accounts(){return this.all("SELECT * FROM accounts ORDER BY email").map(e=>this.accountListing(e))}setAccount(e,t){const n=ct.parse(e);return this.transaction(()=>{const s=this.ensureAccount(n.email);if(n.may_create_lobbies!==void 0&&this.run("UPDATE accounts SET may_create_lobbies=? WHERE account_id=?",Number(n.may_create_lobbies),s),n.enabled===!0&&this.run("UPDATE accounts SET disabled_at=NULL WHERE account_id=?",s),n.enabled===!1){this.run("UPDATE accounts SET disabled_at=COALESCE(disabled_at,?) WHERE account_id=?",this.iso(),s);for(const i of["console_sessions","board_sessions","board_handoffs"])this.run(`DELETE FROM ${i} WHERE account_id=?`,s);this.run("DELETE FROM sign_in_codes WHERE email_key=?",t)}return this.audit(null,"account",null,n.email,{enabled:n.enabled,may_create_lobbies:n.may_create_lobbies}),this.accountListing(this.one("SELECT * FROM accounts WHERE account_id=?",s))})}grantLobby(e,t){const n=q.parse(e);return this.transaction(()=>{const s=this.requireLobby(t),i=this.ensureAccount(n),a=this.one("SELECT owner_account FROM lobbies WHERE lobby_id=?",s).owner_account;return a!==null&&a!==i&&_(409,"lobby_owned","Another account owns this lobby; end that ownership first"),this.run("UPDATE lobbies SET owner_account=? WHERE lobby_id=?",i,s),this.audit(null,"grant_lobby",s,n),this.accountListing(this.one("SELECT * FROM accounts WHERE account_id=?",i))})}ungrantLobby(e,t){const n=q.parse(e);return this.transaction(()=>{const s=this.requireLobby(t),i=this.one("SELECT a.* FROM accounts a JOIN lobbies l ON l.owner_account=a.account_id WHERE a.email=? AND l.lobby_id=?",n,s);return i||_(404,"owner_not_found","That account does not own this lobby"),this.run("UPDATE lobbies SET owner_account=NULL WHERE lobby_id=?",s),this.audit(null,"ungrant_lobby",s,n),this.accountListing(i)})}eligibleAccount(e){const t=this.one("SELECT account_id FROM accounts a WHERE email=? AND disabled_at IS NULL AND (may_create_lobbies=1 OR EXISTS (SELECT 1 FROM lobbies l WHERE l.owner_account=a.account_id) OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.account_id=a.account_id))",e);return t?String(t.account_id):null}limitWait(e,t,n){const s=this.one("SELECT window_ms,count FROM sign_in_limits WHERE key=?",e),i=this.clock();return!s||i-Number(s.window_ms)>=n||Number(s.count)<t?0:Math.max(1,Math.ceil((Number(s.window_ms)+n-i)/1e3))}limitCount(e,t){const n=this.clock();this.run("INSERT INTO sign_in_limits(key,window_ms,count) VALUES(?1,?2,1) ON CONFLICT(key) DO UPDATE SET window_ms=CASE WHEN ?2-window_ms>=?3 THEN ?2 ELSE window_ms END, count=CASE WHEN ?2-window_ms>=?3 THEN 1 ELSE count+1 END",e,n,t)}beginSignIn(e,t,n){return this.transaction(()=>{const s=this.limitWait(`send:${t}`,1,f.sendWindowMs)||this.limitWait(`send-hour:${t}`,f.sendsPerAddressPerHour,36e5)||this.limitWait("send:all",f.sendsPerHour,36e5);if(s)return{wait:s,send:!1};if(this.limitCount(`send:${t}`,f.sendWindowMs),this.limitCount(`send-hour:${t}`,36e5),!this.eligibleAccount(e))return{wait:0,send:!1};this.limitCount("send:all",36e5);const i=this.clock();return this.run("INSERT INTO sign_in_codes(email_key,code_mac,created_ms,expires_ms,attempts) VALUES(?,?,?,?,0) ON CONFLICT(email_key) DO UPDATE SET code_mac=excluded.code_mac,created_ms=excluded.created_ms,expires_ms=excluded.expires_ms,attempts=0",t,n,i,i+f.codeMs),{wait:0,send:!0}})}finishSignIn(e,t,n,s){return this.transaction(()=>{const i=`wrong:${t}`,a=this.limitWait(i,f.wrongPerHour,36e5);if(a)return{error:"budget",wait:a};const r=this.clock(),o=this.one("SELECT * FROM sign_in_codes WHERE email_key=?",t),c=R(l=>(this.limitCount(i,36e5),{error:l,wait:0}),"wrong");if(!o||Number(o.expires_ms)<=r)return c("invalid");if(Number(o.attempts)>=f.attempts)return c("attempts");if(!Ee(Buffer.from(String(o.code_mac),"hex"),Buffer.from(n,"hex")))return this.run("UPDATE sign_in_codes SET attempts=attempts+1 WHERE email_key=?",t),c(Number(o.attempts)+1>=f.attempts?"attempts":"invalid");this.run("DELETE FROM sign_in_codes WHERE email_key=?",t);const d=this.eligibleAccount(e);return d?(this.run("INSERT INTO console_sessions(token_hash,account_id,created_ms,refreshed_ms,expires_ms) VALUES(?,?,?,?,?)",s,d,r,r,r+f.sessionMs),{account_id:d}):{error:"invalid",wait:0}})}consoleSession(e){const t=this.one("SELECT a.account_id,a.email,a.may_create_lobbies,s.refreshed_ms FROM console_sessions s JOIN accounts a ON a.account_id=s.account_id WHERE s.token_hash=? AND s.expires_ms>? AND a.disabled_at IS NULL",e,this.clock());return t?{account_id:String(t.account_id),email:String(t.email),may_create_lobbies:!!t.may_create_lobbies,refresh:this.clock()-Number(t.refreshed_ms)>=864e5}:null}refreshConsoleSession(e){const t=this.clock();this.run("UPDATE console_sessions SET refreshed_ms=?,expires_ms=? WHERE token_hash=?",t,t+f.sessionMs,e)}endConsoleSession(e){this.run("DELETE FROM console_sessions WHERE token_hash=?",e)}consoleLobbies(e){const t=this.lobbyList(e),n=this.all("SELECT v.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||v.lobby_id) AS display_name,COALESCE(c.visibility,'private') AS visibility FROM board_viewers v JOIN accounts a ON a.account_id=v.account_id JOIN lobbies l ON l.lobby_id=v.lobby_id LEFT JOIN board_configs c ON c.lobby_id=v.lobby_id WHERE v.account_id=? AND a.disabled_at IS NULL AND (l.owner_account IS NULL OR l.owner_account<>v.account_id) ORDER BY v.lobby_id",e).map(s=>({lobby_id:String(s.lobby_id),display_name:s.display_name??null,visibility:s.visibility}));return{owned:t,viewing:n}}operatorLobbies(){return this.lobbyList(null)}lobbyList(e){return this.all(`SELECT l.lobby_id,(SELECT value FROM metadata WHERE key='display_name:'||l.lobby_id) AS display_name,l.created_at,COALESCE(c.visibility,'private') AS visibility,(SELECT COUNT(*) FROM agents a WHERE a.lobby_id=l.lobby_id AND a.enabled=1) AS agents,(SELECT COUNT(*) FROM rooms r WHERE r.lobby_id=l.lobby_id) AS rooms FROM lobbies l LEFT JOIN board_configs c ON c.lobby_id=l.lobby_id${e===null?"":" WHERE l.owner_account=? AND EXISTS (SELECT 1 FROM accounts o WHERE o.account_id=l.owner_account AND o.disabled_at IS NULL)"} ORDER BY l.lobby_id`,...e===null?[]:[e]).map(t=>({lobby_id:String(t.lobby_id),display_name:t.display_name??null,created_at:t.created_at,visibility:t.visibility,agents:Number(t.agents),rooms:Number(t.rooms)}))}lobbyCreatedAt(e){return this.one("SELECT created_at FROM lobbies WHERE lobby_id=?",e)?.created_at??null}createOwnedLobby(e,t){const n=ye.parse(t.lobby_id),s=t.first_room===void 0?void 0:I.parse(t.first_room);return this.transaction(()=>{this.one("SELECT 1 FROM accounts WHERE account_id=? AND disabled_at IS NULL AND may_create_lobbies=1",e)||_(403,"lobby_creation_forbidden","This account may not create lobbies"),Number(this.one("SELECT COUNT(*) AS n FROM lobbies WHERE owner_account=?",e).n)>=f.lobbiesPerAccount&&_(409,"lobby_allowance",`An account may own at most ${f.lobbiesPerAccount} lobbies`),this.insertLobby(n,e),this.audit({account_id:e},"create_lobby",n),s!==void 0&&(this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",n,s),this.audit({account_id:e},"create_room",n,s))}),this.consoleLobbies(e).owned.find(i=>i.lobby_id===n)}boardVisibility(e){if(!Y.safeParse(e).success)return null;const t=this.one("SELECT COALESCE(c.visibility,'private') AS visibility FROM lobbies l LEFT JOIN board_configs c ON c.lobby_id=l.lobby_id WHERE l.lobby_id=?",e);return t?t.visibility:null}mayViewBoard(e,t){return!!this.one("SELECT 1 FROM accounts a JOIN lobbies l ON l.lobby_id=? WHERE a.account_id=? AND a.disabled_at IS NULL AND (l.owner_account=a.account_id OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.lobby_id=l.lobby_id AND v.account_id=a.account_id))",t,e)}createHandoff(e,t,n){return this.transaction(()=>this.mayViewBoard(e,t)?(this.run("INSERT INTO board_handoffs(token_hash,lobby_id,account_id,expires_ms) VALUES(?,?,?,?)",n,t,e,this.clock()+f.handoffMs),!0):!1)}consumeHandoff(e,t,n,s=null){return this.transaction(()=>{const i=this.one("SELECT * FROM board_handoffs WHERE token_hash=? AND lobby_id=?",t,e);if(!i)return!1;this.run("DELETE FROM board_handoffs WHERE token_hash=?",t);const a=this.clock(),r=String(i.account_id);return Number(i.expires_ms)<=a||!this.mayViewBoard(r,e)?!1:(s&&this.run("DELETE FROM board_sessions WHERE token_hash=? AND lobby_id=?",s,e),this.run("INSERT INTO board_sessions(token_hash,lobby_id,account_id,created_ms,expires_ms) VALUES(?,?,?,?,?)",n,e,r,a,a+f.sessionMs),!0)})}boardViewer(e,t){return this.boardSession(e,t)?.email??null}boardSession(e,t){const n=this.clock(),s=this.one("SELECT s.account_id,s.expires_ms,a.email FROM board_sessions s JOIN accounts a ON a.account_id=s.account_id WHERE s.token_hash=? AND s.lobby_id=? AND s.expires_ms>?",t,e,n);return!s||!this.mayViewBoard(String(s.account_id),e)?null:{account_id:String(s.account_id),email:String(s.email),refresh:n+f.sessionMs-Number(s.expires_ms)>=864e5,owner:this.ownsLobby(String(s.account_id),e)}}refreshBoardSession(e){this.run("UPDATE board_sessions SET expires_ms=? WHERE token_hash=?",this.clock()+f.sessionMs,e)}endBoardSession(e){this.run("DELETE FROM board_sessions WHERE token_hash=?",e)}boardSessionCount(e){return Number(this.one("SELECT COUNT(*) AS n FROM board_sessions s JOIN accounts a ON a.account_id=s.account_id JOIN lobbies l ON l.lobby_id=s.lobby_id WHERE s.lobby_id=? AND s.expires_ms>? AND a.disabled_at IS NULL AND (l.owner_account=s.account_id OR EXISTS (SELECT 1 FROM board_viewers v WHERE v.lobby_id=s.lobby_id AND v.account_id=s.account_id))",e,this.clock()).n)}boardViewers(e){return this.all("SELECT a.email,v.added_at,a.disabled_at,(SELECT COUNT(*) FROM board_sessions s WHERE s.lobby_id=v.lobby_id AND s.account_id=v.account_id AND s.expires_ms>?) AS sessions FROM board_viewers v JOIN accounts a ON a.account_id=v.account_id WHERE v.lobby_id=? ORDER BY a.email",this.clock(),e).map(t=>({email:String(t.email),added_at:String(t.added_at),disabled:t.disabled_at!==null,sessions:t.disabled_at===null?Number(t.sessions):0}))}addBoardViewer(e,t,n){const s=q.parse(n);return this.transaction(()=>{const i=this.actorLobby(e,t);Number(this.one("SELECT COUNT(*) AS n FROM board_viewers WHERE lobby_id=?",i).n)>=f.viewersPerLobby&&_(409,"viewer_capacity",`A dashboard lists at most ${f.viewersPerLobby} viewers`),this.run("INSERT INTO board_viewers(lobby_id,account_id,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",i,this.ensureAccount(s),this.iso()),this.audit(e,"viewer_add",i,s)}),this.boardViewers(t)}removeBoardViewer(e,t,n){const s=q.parse(n);return this.transaction(()=>{const i=this.actorLobby(e,t),a=this.one("SELECT account_id FROM accounts WHERE email=?",s);(!a||!this.run("DELETE FROM board_viewers WHERE lobby_id=? AND account_id=?",i,String(a.account_id)))&&_(404,"viewer_not_found","That address is not a viewer of this dashboard"),this.run("DELETE FROM board_sessions WHERE lobby_id=? AND account_id=? AND account_id NOT IN (SELECT owner_account FROM lobbies WHERE lobby_id=? AND owner_account IS NOT NULL)",i,String(a.account_id),i),this.audit(e,"viewer_remove",i,s)}),this.boardViewers(t)}lobbyRepos(e){return this.all("SELECT repo FROM lobby_repos WHERE lobby_id=? ORDER BY repo",this.requireLobby(e)).map(t=>String(t.repo))}approveRepo(e,t){const n=Ie.parse(t).toLowerCase();return this.transaction(()=>{const s=this.requireLobby(e);this.run("INSERT INTO lobby_repos(lobby_id,repo,added_at) VALUES(?,?,?) ON CONFLICT DO NOTHING",s,n,this.iso()),this.audit(null,"repo_approve",s,n)}),this.lobbyRepos(e)}removeRepo(e,t){const n=Ie.parse(t).toLowerCase();return this.transaction(()=>{const s=this.requireLobby(e),i=this.all("SELECT room_id FROM board_sources WHERE lobby_id=? AND lower(repo)=? ORDER BY room_id",s,n).map(a=>String(a.room_id));i.length&&_(409,"repo_in_use",`Rooms still read notes from ${n} (${i.join(", ")}); remove their sources first`,{rooms:i}),this.run("DELETE FROM lobby_repos WHERE lobby_id=? AND repo=?",s,n)||_(404,"repo_not_found","That repository is not approved for this lobby"),this.audit(null,"repo_remove",s,n)}),this.lobbyRepos(e)}archivedRooms(e){return this.all("SELECT room_id,archived_at FROM archived_rooms WHERE lobby_id=? ORDER BY archived_at DESC,room_id",e).map(t=>({room_id:String(t.room_id),archived_at:String(t.archived_at)}))}cancelRoomSchedules(e,t,n){const s=this.all("SELECT id,sender FROM schedules WHERE lobby_id=? AND cancelled_at IS NULL AND json_extract(template,'$.room')=? ORDER BY created_at",e,t);for(const i of s)this.run("UPDATE schedules SET cancelled_at=?,next_run_ms=NULL,last_error=? WHERE id=?",this.iso(),n,String(i.id));return s}archiveRoom(e,t,n){return I.parse(t),this.transaction(()=>{const s=this.roomManager(e,n),i=this.room(s,t),a=this.cancelRoomSchedules(s,t,"room_archived");return this.run("DELETE FROM room_members WHERE lobby_id=? AND room_id=?",s,t),this.run("DELETE FROM rooms WHERE lobby_id=? AND room_id=?",s,t),this.run("INSERT INTO archived_rooms(lobby_id,room_id,archived_at) VALUES(?,?,?)",s,t,this.iso()),this.audit(e,"archive_room",s,t,{former_members:i.members}),{lobby_id:s,room_id:t,archived:!0,former_members:i.members,cancelled_schedules:a.map(r=>({schedule_id:String(r.id),from:T(String(r.sender)).agent_id}))}})}restoreRoom(e,t,n){return I.parse(t),this.transaction(()=>{const s=this.roomManager(e,n);return this.run("DELETE FROM archived_rooms WHERE lobby_id=? AND room_id=?",s,t)||_(404,"room_not_found","No archived room has that name"),Number(this.one("SELECT COUNT(*) AS n FROM rooms WHERE lobby_id=?",s).n)>=this.maxRooms&&_(409,"room_capacity","Room capacity reached for this lobby"),this.run("INSERT INTO rooms(lobby_id,room_id) VALUES(?,?)",s,t),this.audit(e,"restore_room",s,t),this.room(s,t)})}deleteLobby(e,t,n){let s=[];const i=this.transaction(()=>{const a=this.actorLobby(e,t);n!==a&&_(400,"confirmation_mismatch","Type the lobby name exactly to delete it");const r=this.all("SELECT agent_id FROM agents WHERE lobby_id=?",a).map(l=>String(l.agent_id)),o=this.all("SELECT sha256 FROM attachments WHERE lobby_id=?1 UNION SELECT sha256 FROM attachment_uploads WHERE lobby_id=?1",a).map(l=>String(l.sha256)),c="SELECT agent_id FROM agents WHERE lobby_id=?1",d="SELECT id FROM messages WHERE lobby_id=?1";for(const l of[`DELETE FROM seen_receipts WHERE message_id IN (${d}) OR sender IN (${c}) OR agent_id IN (${c})`,`DELETE FROM notification_attempts WHERE agent_id IN (${c})`,`DELETE FROM deliveries WHERE message_id IN (${d}) OR agent_id IN (${c})`,`DELETE FROM message_attachments WHERE message_id IN (${d}) OR attachment_id IN (SELECT id FROM attachments WHERE lobby_id=?1)`,`DELETE FROM schedule_attachments WHERE schedule_id IN (SELECT id FROM schedules WHERE lobby_id=?1 OR sender IN (${c})) OR attachment_id IN (SELECT id FROM attachments WHERE lobby_id=?1)`,`DELETE FROM schedules WHERE lobby_id=?1 OR sender IN (${c})`,"DELETE FROM messages WHERE lobby_id=?1",`DELETE FROM attachment_uploads WHERE lobby_id=?1 OR uploader IN (${c})`,"DELETE FROM attachments WHERE lobby_id=?1",`DELETE FROM checkpoints WHERE agent_id IN (${c})`,`DELETE FROM runtime_history WHERE agent_id IN (${c})`,`DELETE FROM agent_turns WHERE agent_id IN (${c})`,"DELETE FROM room_members WHERE lobby_id=?1","DELETE FROM rooms WHERE lobby_id=?1","DELETE FROM archived_rooms WHERE lobby_id=?1","DELETE FROM board_notes WHERE lobby_id=?1","DELETE FROM board_sources WHERE lobby_id=?1","DELETE FROM board_configs WHERE lobby_id=?1","DELETE FROM board_summaries WHERE lobby_id=?1","DELETE FROM board_captions WHERE lobby_id=?1","DELETE FROM board_sessions WHERE lobby_id=?1","DELETE FROM board_handoffs WHERE lobby_id=?1","DELETE FROM board_viewers WHERE lobby_id=?1","DELETE FROM metadata WHERE key IN ('board_icon:'||?1,'display_name:'||?1,'currency:'||?1,'pacing:'||?1) OR (key>'code_stats:'||?1||':' AND key<'code_stats:'||?1||';')","DELETE FROM lobby_repos WHERE lobby_id=?1","DELETE FROM token_calls WHERE lobby_id=?1","DELETE FROM token_days WHERE lobby_id=?1","DELETE FROM memory_rooms WHERE lobby_id=?1","DELETE FROM memory_fragments WHERE lobby_id=?1","DELETE FROM memory_entries WHERE lobby_id=?1","DELETE FROM memory_entry_history WHERE lobby_id=?1","DELETE FROM memory_curations WHERE lobby_id=?1","DELETE FROM account_weeks WHERE lobby_id=?1","DELETE FROM agents WHERE lobby_id=?1","DELETE FROM lobbies WHERE lobby_id=?1"])this.run(l,a);s=o.filter(l=>!this.one("SELECT 1 FROM attachments WHERE sha256=?1 AND expired_at IS NULL UNION ALL SELECT 1 FROM attachment_uploads WHERE sha256=?1 LIMIT 1",l));for(const l of s)this.run("DELETE FROM attachment_blobs WHERE sha256=?",l);return this.run("INSERT INTO deleted_lobbies(lobby_id,deleted_at) VALUES(?,?)",a,this.iso()),this.audit(e,"delete_lobby",a,null,{agents:r.length}),{lobby_id:a,agents:r}});if(this.attachmentsDirectory)for(const a of s)try{Q(this.blobPath(a))}catch{}return i}activity(e,t=100){return this.all("SELECT e.*,a.email FROM admin_events e LEFT JOIN accounts a ON a.account_id=e.account_id WHERE e.lobby_id=? ORDER BY e.sequence DESC LIMIT ?",e,t).map(n=>({at:String(n.created_at),actor_kind:n.actor_kind??"operator",actor:n.actor_kind==="agent"&&n.agent_id!==null?T(String(n.agent_id)).agent_id:n.actor_kind==="account"?n.email??"a removed account":null,action:String(n.action),target:n.target,detail:n.detail===null?null:JSON.parse(String(n.detail))}))}pruneSignIn(){const e=this.clock(),t=e-36e5;this.one("SELECT 1 FROM sign_in_codes WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM console_sessions WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM board_sessions WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM board_handoffs WHERE expires_ms<=?1 UNION ALL SELECT 1 FROM sign_in_limits WHERE window_ms<=?2 LIMIT 1",e,t)&&this.transaction(()=>{for(const s of["sign_in_codes","console_sessions","board_sessions","board_handoffs"])this.run(`DELETE FROM ${s} WHERE expires_ms<=?`,e);this.run("DELETE FROM sign_in_limits WHERE window_ms<=?",t)})}enterRecovery(){this.transaction(()=>{this.run("UPDATE metadata SET value=? WHERE key='incarnation'",O()),this.run("UPDATE agents SET token_hash=NULL,runtime_id=NULL,generation=generation+1,ready=0,paused=1,resume_at_ms=NULL,probe_nonce=lower(hex(randomblob(16))),bootstrap_attempts=0,bootstrap_next_at=?,owner_requests=0,owner_requests_text=NULL,owner_requests_at=NULL,status_text=NULL,status_set_at=NULL",this.clock()),this.run("UPDATE deliveries SET attempts=0,next_attempt_at=? WHERE seen_at IS NULL",this.clock()),this.run("DELETE FROM console_sessions"),this.run("DELETE FROM board_sessions"),this.run("DELETE FROM board_handoffs"),this.run("DELETE FROM sign_in_codes"),this.run("UPDATE accounts SET disabled_at=? WHERE disabled_at IS NULL",this.iso()),this.audit(null,"enter_recovery",null)})}}export{Ns as LobbyStore,Vt as MEMORY_PENDING_LIMIT,Yt as MESSAGE_RETENTION_MS,M as SCHEMA_VERSION,oe as TOKEN_RETENTION_MS,Jt as TURN_RETENTION_MS,bs as backupDatabase};
|