pocolobby 0.12.2
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/activity.js +1 -0
- package/dist/adapters/claude.js +7 -0
- package/dist/attachments.js +2 -0
- package/dist/auth.js +4 -0
- package/dist/bin.js +4 -0
- package/dist/board-caption.js +19 -0
- package/dist/board-github.js +1 -0
- package/dist/board-model.js +1 -0
- package/dist/board-page.js +418 -0
- package/dist/board-summary.js +22 -0
- package/dist/board.js +1 -0
- package/dist/catalog.js +1 -0
- package/dist/cli.js +123 -0
- package/dist/client.js +1 -0
- package/dist/code-stats.js +3 -0
- package/dist/config.js +3 -0
- package/dist/console-page.js +467 -0
- package/dist/console.js +1 -0
- package/dist/exchange-rates.js +1 -0
- package/dist/hook.js +1 -0
- package/dist/html.js +1 -0
- package/dist/icons.js +4 -0
- package/dist/launch.js +2 -0
- package/dist/local-files.js +1 -0
- package/dist/lock.js +1 -0
- package/dist/mail.js +4 -0
- package/dist/mascots.js +6 -0
- package/dist/memory-curate.js +59 -0
- package/dist/memory.js +5 -0
- package/dist/money.js +1 -0
- package/dist/node-version.js +1 -0
- package/dist/pace.js +1 -0
- package/dist/png.js +1 -0
- package/dist/presentation.js +1 -0
- package/dist/prices.js +1 -0
- package/dist/protocol.js +1 -0
- package/dist/recurrence.js +1 -0
- package/dist/secrets.js +1 -0
- package/dist/server.js +8 -0
- package/dist/statusline.js +7 -0
- package/dist/store.js +303 -0
- package/dist/transcripts.js +2 -0
- package/docs/AGENT-SETUP-PROMPT.md +72 -0
- package/docs/API.md +1097 -0
- package/docs/ARCHITECTURE.md +410 -0
- package/docs/CLAUDE-SETUP.md +166 -0
- package/docs/INTEGRATION-PLAN.md +122 -0
- package/docs/LOCAL-SETUP.md +356 -0
- package/docs/MEMORY.md +142 -0
- package/docs/OPERATIONS.md +320 -0
- package/docs/QUICKSTART.md +111 -0
- package/docs/RELEASING.md +40 -0
- package/docs/SELF-HOSTING.md +236 -0
- package/docs/UPGRADES-WITHOUT-AGENT-RESTARTS.md +112 -0
- package/examples/CLAUDE.md +50 -0
- package/examples/PACING.md +33 -0
- package/examples/POCOLOBBY.md +69 -0
- package/examples/fly.toml +42 -0
- package/package.json +68 -0
- package/site/agents/gonk-server.webp +0 -0
- package/site/agents/luke-sleep.webp +0 -0
- package/site/agents/mouse-empty.webp +0 -0
- package/site/agents/mouse-envelope.webp +0 -0
- package/site/agents/poco-bell.webp +0 -0
- package/site/agents/vader-asleep.webp +0 -0
- package/site/agents/vader-crate.webp +0 -0
- package/site/agents/vader-deckchair.webp +0 -0
- package/site/agents/vader-point.webp +0 -0
- package/site/agents/vader-thumbs.webp +0 -0
- package/site/icons/apple-touch-icon.png +0 -0
- package/site/icons/favicon-32.png +0 -0
- package/site/icons/favicon.ico +0 -0
- package/site/icons/favicon.svg +12 -0
- package/site/index.html +204 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
var we=Object.defineProperty;var g=(b,e)=>we(b,"name",{value:e,configurable:!0});import{createHash as x,randomBytes as Xe,randomUUID as O,timingSafeEqual as de}from"node:crypto";import{memoryBriefParts as _e,mergeProvenance as G,prepareMemory as ce,rankMemory as Be,rankPrepared as Ee}from"./memory.js";import{findSecret as J}from"./secrets.js";import{chmodSync as le,closeSync as me,fsyncSync as qe,mkdirSync as ue,mkdtempSync as $e,openSync as he,readdirSync as Pe,renameSync as Ye,statfsSync as Ge,statSync as V,unlinkSync as K,writeSync as Je}from"node:fs";import{tmpdir as Ve}from"node:os";import{dirname as be,join as w,resolve as X}from"node:path";import{LONG_PROMPT as Ke}from"./prices.js";import{backup as ze,DatabaseSync as Ne}from"node:sqlite";import{DEFAULT_ATTACHMENT_DISK_RESERVE_BYTES as je,DEFAULT_ATTACHMENT_GRACE_MS as Qe,DEFAULT_ATTACHMENT_RETENTION_MS as Ze,DEFAULT_LOBBY_ATTACHMENT_BYTES as et,DEFAULT_MAX_IMAGE_BYTES as tt,DEFAULT_MAX_TEXT_BYTES as st,formatBytes as v,kindOf as z,kindOfBytes as it,sniffImage as nt}from"./attachments.js";import{addressedOf as B,namedMembers as Te,AccountSettingsSchema as at,CheckpointSchema as rt,AgentId as j,Email as q,LobbyId as $,NewLobbyId as Re,RoomId as I,RoomMemberChangesSchema as ot,parsePrincipal as T,qualifyAgent as D,LobbyError as Le,MIN_PUSH_BYTES as ge,MAX_CONTENT_BYTES as dt,MAX_PAGE_BYTES as _t,MAX_SCHEDULE_AHEAD_MS as Q,ProvisionSchema as ct,RESERVED_REQUEST_PREFIX as H,SCHEDULE_PAST_TOLERANCE_MS as ye,ScheduleSchema as Et,SendSchema as Se,OwnerRequestsSchema as lt,StatusSchema as mt,UpdateAgentSchema as ut,CurationFailureSchema as ht,CurationReportSchema as bt,CodeStatsSchema as Nt,CurationSchema as Tt,MEMORY_TEXT_LIMITS as pe,RecallQuerySchema as Rt,RememberSchema as Lt,canonicalRepeat as Oe,canonicalSend as Z,defaultReplyExpected as gt,messageDetails as ee,DETAIL_FIELDS as fe,UpdateScheduledSchema as yt,DisplayName as St,Currency as pt}from"./protocol.js";import{BoardAgentsSchema as Ot,BoardSourceSchema as ft,MAX_BOARD_SOURCES as Ae,RepoName as De,SUMMARY_MODELS as Ue,SummarySettingsSchema as At,sourceKey as P}from"./board-model.js";import{latestDueOccurrence as Dt,localLabel as Ut,nextOccurrence as te}from"./recurrence.js";import{SIGN_IN as f}from"./auth.js";import{checkLobbyIcon as Ct}from"./icons.js";import{moneyFor as It}from"./money.js";const se={image:"POCOLOBBY_ATTACHMENT_MAX_IMAGE_BYTES",text:"POCOLOBBY_ATTACHMENT_MAX_TEXT_BYTES",lobby:"POCOLOBBY_ATTACHMENT_LOBBY_BYTES",reserve:"POCOLOBBY_ATTACHMENT_DISK_RESERVE_BYTES"},F=19,Mt=1e4,vt=10080*60*1e3,Ce=300;function Y(b){const e={...b};for(const t of fe)delete e[t];return b.content.length>Ce&&(e.content=`${b.content.slice(0,Ce-1)}\u2026`,e.content_truncated=!0),e}g(Y,"listed");function ie(b){Buffer.byteLength(JSON.stringify(b))-2>dt&&d(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")}g(ie,"admitContent");const Ft=5,kt=20,W="scheduled",Ie="cancelled",Ht=1440*60*1e3,Wt=1440*60*1e3,xt='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.',ne=672*60*60*1e3,wt=2880*60*1e3,Xt=200,ae=720*60*60*1e3,Me=2160*60*60*1e3,Bt=365*24*60*60*1e3,ve=50,qt=.04,Fe=g(b=>`deliver_at must be at most 30 days ahead, so no later than ${new Date(b+Q).toISOString()}, and at most 5 minutes in the past`,"deliverAtRange"),$t=20,re=g(b=>({...b.supersedes?{supersedes:String(b.supersedes)}:{},...b.superseded_by?{superseded_by:String(b.superseded_by)}:{}}),"corrections"),ke=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,k=g(b=>x("sha256").update(b).digest("hex"),"hash");function He(b,e){return b.reply_expected!==void 0?b.reply_expected:b.kind?!(b.room===void 0&&b.to===T(e).agent_id)&>(b.kind):null}g(He,"replyExpectedOf");const We=g(()=>`pl_${Xe(32).toString("base64url")}`,"token"),d=g((b,e,t,s)=>{throw new Le(b,e,t,s)},"fail"),oe=g((b,e)=>`(${b}.room_id IS NOT NULL AND ${b}.cancelled_at IS NULL AND (${b}.deliver_at_ms IS NULL OR ${b}.released_ms IS NOT NULL) AND EXISTS(SELECT 1 FROM room_members rm WHERE rm.lobby_id=${b}.lobby_id AND rm.room_id=${b}.room_id AND rm.agent_id=${e} AND rm.joined_sequence<${b}.sequence))`,"roomReadable"),Pt=g(b=>({id:b.id,from:b.from,to:b.to,room_id:b.room_id,kind:b.kind,created_at:b.created_at,first_line:(b.content.split(`
|
|
2
|
+
`).find(e=>e.trim())??"").trim().slice(0,200)}),"lineOf");async function os(b,e){X(e)===X(b)&&d(400,"invalid_backup_path","Backup destination must differ from the live database");const t=new Ne(b,{readOnly:!0});try{await ze(t,e)}finally{t.close()}le(e,384)}g(os,"backupDatabase");class ds{static{g(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??tt,this.maxTextBytes=t.maxTextBytes??st,this.maxLobbyAttachments=t.maxLobbyAttachmentBytes??et,this.attachmentRetention=t.attachmentRetentionMs??Ze,this.attachmentGrace=t.attachmentGraceMs??Qe,this.diskReserve=t.attachmentDiskReserveBytes??je,this.freeDisk=t.freeDiskBytes??(i=>{const n=Ge(i);return Number(n.bavail)*Number(n.bsize)}),this.attachmentsDirectory=t.attachmentsDir??(e===":memory:"?void 0:w(be(X(e)),"attachments"));const s=t.maxDatabaseBytes??1024*1024*1024;if(!this.retries.length||this.retries.some(i=>!Number.isFinite(i)||i<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(i=>!Number.isFinite(i)||i<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(i=>!Number.isSafeInteger(i)||i<1))throw new RangeError("Lobby and room limits must be positive integers");if(!Number.isSafeInteger(s)||s<512*1024)throw new RangeError("Database page budget must be at least 512 KiB");e!==":memory:"&&(ue(be(X(e)),{recursive:!0,mode:448}),me(he(e,"a",384)),le(e,384)),this.db=new Ne(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 i=Number(this.one("PRAGMA page_size").page_size),n=Math.floor(s/i);Number(this.one(`PRAGMA max_page_count=${n}`).max_page_count)>n&&d(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!==F&&d(500,"unsupported_schema",r>F?`This database is schema ${r}, which is newer than this broker (schema ${F}).`:`This database is schema ${r}; this broker opens schema ${F} only. Upgrade it with the release that brought schema ${F}, restore a newer backup, or start a new database.`)}catch(i){throw this.db.close(),i}}createSchema(){this.db.exec(`
|
|
3
|
+
CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
4
|
+
CREATE TABLE accounts (
|
|
5
|
+
account_id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE,
|
|
6
|
+
may_create_lobbies INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, disabled_at TEXT
|
|
7
|
+
);
|
|
8
|
+
CREATE TABLE lobbies (
|
|
9
|
+
lobby_id TEXT PRIMARY KEY, owner_account TEXT REFERENCES accounts(account_id), created_at TEXT
|
|
10
|
+
);
|
|
11
|
+
CREATE INDEX lobbies_owner ON lobbies(owner_account);
|
|
12
|
+
CREATE TABLE deleted_lobbies (lobby_id TEXT PRIMARY KEY, deleted_at TEXT NOT NULL);
|
|
13
|
+
CREATE TABLE agents (
|
|
14
|
+
agent_id TEXT PRIMARY KEY, token_hash TEXT UNIQUE, role TEXT NOT NULL,
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1, paused INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
can_broadcast INTEGER NOT NULL, allowed_recipients TEXT,
|
|
17
|
+
runtime_id TEXT, generation INTEGER NOT NULL DEFAULT 0, ready INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
probe_nonce TEXT NOT NULL, bootstrap_attempts INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
bootstrap_next_at INTEGER NOT NULL DEFAULT 0, backlog_sequence INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
last_seen_at TEXT, last_agent_seen_at TEXT, receipt_seq INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
22
|
+
manage_rooms INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
-- The one-line status it sets for its peers (0.11.0), and since when.
|
|
24
|
+
status_text TEXT, status_set_at TEXT, backlog_at INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
owner_requests INTEGER NOT NULL DEFAULT 0, owner_requests_text TEXT, owner_requests_at TEXT,
|
|
26
|
+
work_description TEXT NOT NULL DEFAULT '', machine TEXT NOT NULL DEFAULT '',
|
|
27
|
+
screen INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
-- When a resting (paused) agent resumes by itself (0.10.1); null rests until resumed.
|
|
29
|
+
resume_at_ms INTEGER
|
|
30
|
+
);
|
|
31
|
+
CREATE UNIQUE INDEX agents_principal_lobby ON agents(agent_id,lobby_id);
|
|
32
|
+
CREATE TABLE runtime_history (
|
|
33
|
+
agent_id TEXT NOT NULL REFERENCES agents(agent_id), runtime_id TEXT NOT NULL,
|
|
34
|
+
PRIMARY KEY (agent_id, runtime_id)
|
|
35
|
+
);
|
|
36
|
+
CREATE TABLE checkpoints (
|
|
37
|
+
agent_id TEXT PRIMARY KEY REFERENCES agents(agent_id), revision INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
content TEXT NOT NULL DEFAULT '', covered_receipt_seq INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
updated_at TEXT, runtime_id TEXT
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE rooms (
|
|
42
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), room_id TEXT NOT NULL,
|
|
43
|
+
PRIMARY KEY(lobby_id,room_id)
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE room_members (
|
|
46
|
+
lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, agent_id TEXT NOT NULL,
|
|
47
|
+
-- The last message sequence when it joined (0.11.0): by ID it reads the room's later ones.
|
|
48
|
+
joined_sequence INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
PRIMARY KEY(lobby_id,room_id,agent_id),
|
|
50
|
+
FOREIGN KEY(lobby_id,room_id) REFERENCES rooms(lobby_id,room_id),
|
|
51
|
+
FOREIGN KEY(agent_id,lobby_id) REFERENCES agents(agent_id,lobby_id)
|
|
52
|
+
);
|
|
53
|
+
CREATE TABLE archived_rooms (
|
|
54
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), room_id TEXT NOT NULL,
|
|
55
|
+
archived_at TEXT NOT NULL, PRIMARY KEY (lobby_id, room_id)
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE messages (
|
|
58
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE,
|
|
59
|
+
sender TEXT NOT NULL, request_id TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
60
|
+
target TEXT NOT NULL, priority TEXT NOT NULL, content TEXT NOT NULL,
|
|
61
|
+
correlation_id TEXT, reply_to TEXT, created_at TEXT NOT NULL, created_ms INTEGER NOT NULL,
|
|
62
|
+
byte_size INTEGER NOT NULL,
|
|
63
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
64
|
+
room_id TEXT, details TEXT, notify_when_seen INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
deliver_at_ms INTEGER, released_ms INTEGER, late INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
cancelled_at TEXT, schedule_id TEXT,
|
|
67
|
+
kind TEXT
|
|
68
|
+
CHECK (kind IS NULL OR kind IN ('request','notice','assignment','report','question')),
|
|
69
|
+
reply_expected INTEGER CHECK (reply_expected IS NULL OR reply_expected IN (0,1)),
|
|
70
|
+
supersedes TEXT, superseded_by TEXT,
|
|
71
|
+
UNIQUE (sender, request_id)
|
|
72
|
+
);
|
|
73
|
+
CREATE INDEX messages_sender_time ON messages(sender, created_ms);
|
|
74
|
+
CREATE INDEX messages_lobby_sequence ON messages(lobby_id,sequence);
|
|
75
|
+
CREATE INDEX messages_due ON messages(deliver_at_ms)
|
|
76
|
+
WHERE deliver_at_ms IS NOT NULL AND released_ms IS NULL AND cancelled_at IS NULL;
|
|
77
|
+
CREATE UNIQUE INDEX messages_occurrence ON messages(schedule_id, deliver_at_ms)
|
|
78
|
+
WHERE schedule_id IS NOT NULL;
|
|
79
|
+
CREATE INDEX messages_bytes ON messages(byte_size);
|
|
80
|
+
CREATE INDEX messages_lobby_time ON messages(lobby_id,created_ms);
|
|
81
|
+
CREATE INDEX messages_lobby_released ON messages(lobby_id,released_ms)
|
|
82
|
+
WHERE released_ms IS NOT NULL;
|
|
83
|
+
CREATE TABLE deliveries (
|
|
84
|
+
message_id TEXT NOT NULL REFERENCES messages(id), agent_id TEXT NOT NULL REFERENCES agents(agent_id),
|
|
85
|
+
seen_at TEXT, receipt_seq INTEGER, attempts INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
next_attempt_at INTEGER NOT NULL, last_notification_id TEXT, held_reason TEXT,
|
|
87
|
+
notification_attempted_at TEXT,
|
|
88
|
+
PRIMARY KEY (message_id, agent_id), UNIQUE (agent_id, receipt_seq)
|
|
89
|
+
);
|
|
90
|
+
CREATE INDEX deliveries_pending ON deliveries(agent_id, seen_at, next_attempt_at);
|
|
91
|
+
CREATE INDEX deliveries_notification ON deliveries(last_notification_id);
|
|
92
|
+
CREATE TABLE notification_attempts (
|
|
93
|
+
id TEXT PRIMARY KEY, agent_id TEXT NOT NULL REFERENCES agents(agent_id), runtime_id TEXT NOT NULL,
|
|
94
|
+
generation INTEGER NOT NULL, incarnation TEXT NOT NULL, kind TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
95
|
+
adapter_received_at TEXT, notification_attempted_at TEXT
|
|
96
|
+
);
|
|
97
|
+
CREATE INDEX notification_attempts_agent_time ON notification_attempts(agent_id,created_at);
|
|
98
|
+
CREATE TABLE seen_receipts (
|
|
99
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
sender TEXT NOT NULL REFERENCES agents(agent_id),
|
|
101
|
+
message_id TEXT NOT NULL REFERENCES messages(id),
|
|
102
|
+
agent_id TEXT NOT NULL REFERENCES agents(agent_id),
|
|
103
|
+
seen_at TEXT NOT NULL, seen_ms INTEGER NOT NULL,
|
|
104
|
+
state TEXT NOT NULL DEFAULT 'due' CHECK (state IN ('due','sent','expired')),
|
|
105
|
+
notification_id TEXT, UNIQUE (message_id, agent_id)
|
|
106
|
+
);
|
|
107
|
+
CREATE INDEX seen_receipts_due ON seen_receipts(sender, sequence) WHERE state='due';
|
|
108
|
+
CREATE TABLE schedules (
|
|
109
|
+
id TEXT PRIMARY KEY, sender TEXT NOT NULL REFERENCES agents(agent_id),
|
|
110
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), request_id TEXT NOT NULL,
|
|
111
|
+
fingerprint TEXT NOT NULL, template TEXT NOT NULL, time_zone TEXT NOT NULL,
|
|
112
|
+
times TEXT NOT NULL, weekdays TEXT, created_at TEXT NOT NULL, next_run_ms INTEGER,
|
|
113
|
+
cancelled_at TEXT, occurrence_count INTEGER NOT NULL DEFAULT 0, last_occurrence_at TEXT,
|
|
114
|
+
last_message_id TEXT, last_error TEXT, UNIQUE (sender, request_id)
|
|
115
|
+
);
|
|
116
|
+
CREATE INDEX schedules_due ON schedules(next_run_ms) WHERE cancelled_at IS NULL;
|
|
117
|
+
CREATE TABLE attachment_blobs (
|
|
118
|
+
sha256 TEXT PRIMARY KEY, size INTEGER NOT NULL,
|
|
119
|
+
kind TEXT NOT NULL CHECK (kind IN ('image','text')), mime_type TEXT NOT NULL,
|
|
120
|
+
created_at TEXT NOT NULL
|
|
121
|
+
);
|
|
122
|
+
CREATE TABLE attachments (
|
|
123
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE,
|
|
124
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
125
|
+
uploader TEXT NOT NULL REFERENCES agents(agent_id),
|
|
126
|
+
filename TEXT NOT NULL, mime_type TEXT NOT NULL, size INTEGER NOT NULL,
|
|
127
|
+
sha256 TEXT NOT NULL, created_at TEXT NOT NULL, created_ms INTEGER NOT NULL,
|
|
128
|
+
last_referenced_ms INTEGER, expired_at TEXT
|
|
129
|
+
);
|
|
130
|
+
CREATE UNIQUE INDEX attachments_identity ON attachments(uploader,sha256,filename)
|
|
131
|
+
WHERE expired_at IS NULL;
|
|
132
|
+
CREATE INDEX attachments_lobby ON attachments(lobby_id,sequence);
|
|
133
|
+
CREATE INDEX attachments_content ON attachments(sha256) WHERE expired_at IS NULL;
|
|
134
|
+
CREATE TABLE message_attachments (
|
|
135
|
+
message_id TEXT NOT NULL REFERENCES messages(id), position INTEGER NOT NULL,
|
|
136
|
+
attachment_id TEXT NOT NULL REFERENCES attachments(id),
|
|
137
|
+
PRIMARY KEY (message_id, position)
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX message_attachments_attachment ON message_attachments(attachment_id);
|
|
140
|
+
CREATE TABLE schedule_attachments (
|
|
141
|
+
schedule_id TEXT NOT NULL REFERENCES schedules(id), position INTEGER NOT NULL,
|
|
142
|
+
attachment_id TEXT NOT NULL REFERENCES attachments(id),
|
|
143
|
+
PRIMARY KEY (schedule_id, position)
|
|
144
|
+
);
|
|
145
|
+
CREATE INDEX schedule_attachments_attachment ON schedule_attachments(attachment_id);
|
|
146
|
+
CREATE TABLE attachment_uploads (
|
|
147
|
+
uploader TEXT NOT NULL REFERENCES agents(agent_id), sha256 TEXT NOT NULL,
|
|
148
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), created_ms INTEGER NOT NULL,
|
|
149
|
+
PRIMARY KEY (uploader, sha256)
|
|
150
|
+
);
|
|
151
|
+
CREATE INDEX attachment_uploads_content ON attachment_uploads(sha256);
|
|
152
|
+
CREATE TABLE admin_events (
|
|
153
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT, action TEXT NOT NULL,
|
|
154
|
+
created_at TEXT NOT NULL, actor_kind TEXT, account_id TEXT, lobby_id TEXT, target TEXT,
|
|
155
|
+
detail TEXT
|
|
156
|
+
);
|
|
157
|
+
CREATE INDEX admin_events_lobby ON admin_events(lobby_id, sequence);
|
|
158
|
+
CREATE TABLE board_configs (
|
|
159
|
+
lobby_id TEXT PRIMARY KEY REFERENCES lobbies(lobby_id),
|
|
160
|
+
agents TEXT NOT NULL DEFAULT '{}', updated_at TEXT NOT NULL,
|
|
161
|
+
visibility TEXT NOT NULL DEFAULT 'private' CHECK (visibility IN ('private','public')),
|
|
162
|
+
summary_model TEXT NOT NULL DEFAULT '${Ue[0]}',
|
|
163
|
+
summary_effort TEXT NOT NULL DEFAULT 'low' CHECK (summary_effort IN ('low','medium','high'))
|
|
164
|
+
);
|
|
165
|
+
CREATE TABLE board_sources (
|
|
166
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), room_id TEXT NOT NULL,
|
|
167
|
+
repo TEXT NOT NULL, ref TEXT NOT NULL, notes TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
168
|
+
PRIMARY KEY (lobby_id, room_id)
|
|
169
|
+
);
|
|
170
|
+
CREATE TABLE board_notes (
|
|
171
|
+
lobby_id TEXT NOT NULL, room_id TEXT NOT NULL,
|
|
172
|
+
commit_sha TEXT NOT NULL, files TEXT NOT NULL, fetched_at TEXT NOT NULL,
|
|
173
|
+
PRIMARY KEY (lobby_id, room_id),
|
|
174
|
+
FOREIGN KEY (lobby_id, room_id) REFERENCES board_sources(lobby_id, room_id) ON DELETE CASCADE
|
|
175
|
+
);
|
|
176
|
+
CREATE TABLE board_summaries (
|
|
177
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
178
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
179
|
+
created_at TEXT NOT NULL, created_ms INTEGER NOT NULL,
|
|
180
|
+
input_hash TEXT NOT NULL, sources TEXT NOT NULL, model TEXT NOT NULL,
|
|
181
|
+
input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL,
|
|
182
|
+
cache_creation_input_tokens INTEGER NOT NULL, cache_read_input_tokens INTEGER NOT NULL,
|
|
183
|
+
cost_usd REAL NOT NULL, summary TEXT, error TEXT,
|
|
184
|
+
CHECK ((summary IS NULL) <> (error IS NULL))
|
|
185
|
+
);
|
|
186
|
+
CREATE INDEX board_summaries_lobby ON board_summaries(lobby_id, sequence);
|
|
187
|
+
CREATE TABLE lobby_repos (
|
|
188
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id), repo TEXT NOT NULL,
|
|
189
|
+
added_at TEXT NOT NULL, PRIMARY KEY (lobby_id, repo)
|
|
190
|
+
);
|
|
191
|
+
CREATE TABLE board_viewers (
|
|
192
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
193
|
+
account_id TEXT NOT NULL REFERENCES accounts(account_id),
|
|
194
|
+
added_at TEXT NOT NULL, PRIMARY KEY (lobby_id, account_id)
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX board_viewers_account ON board_viewers(account_id);
|
|
197
|
+
CREATE TABLE board_sessions (
|
|
198
|
+
token_hash TEXT PRIMARY KEY, lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
199
|
+
account_id TEXT NOT NULL REFERENCES accounts(account_id),
|
|
200
|
+
created_ms INTEGER NOT NULL, expires_ms INTEGER NOT NULL
|
|
201
|
+
);
|
|
202
|
+
CREATE INDEX board_sessions_lobby ON board_sessions(lobby_id);
|
|
203
|
+
CREATE INDEX board_sessions_account ON board_sessions(account_id);
|
|
204
|
+
CREATE TABLE board_handoffs (
|
|
205
|
+
token_hash TEXT PRIMARY KEY, lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
206
|
+
account_id TEXT NOT NULL REFERENCES accounts(account_id), expires_ms INTEGER NOT NULL
|
|
207
|
+
);
|
|
208
|
+
CREATE TABLE console_sessions (
|
|
209
|
+
token_hash TEXT PRIMARY KEY, account_id TEXT NOT NULL REFERENCES accounts(account_id),
|
|
210
|
+
created_ms INTEGER NOT NULL, refreshed_ms INTEGER NOT NULL, expires_ms INTEGER NOT NULL
|
|
211
|
+
);
|
|
212
|
+
CREATE INDEX console_sessions_account ON console_sessions(account_id);
|
|
213
|
+
CREATE TABLE sign_in_codes (
|
|
214
|
+
email_key TEXT PRIMARY KEY, code_mac TEXT NOT NULL, created_ms INTEGER NOT NULL,
|
|
215
|
+
expires_ms INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0
|
|
216
|
+
);
|
|
217
|
+
CREATE TABLE sign_in_limits (
|
|
218
|
+
key TEXT PRIMARY KEY, window_ms INTEGER NOT NULL, count INTEGER NOT NULL
|
|
219
|
+
);
|
|
220
|
+
CREATE TABLE token_calls (
|
|
221
|
+
lobby_id TEXT NOT NULL, call_id TEXT NOT NULL, at_ms INTEGER NOT NULL,
|
|
222
|
+
PRIMARY KEY (lobby_id, call_id)
|
|
223
|
+
);
|
|
224
|
+
CREATE INDEX token_calls_time ON token_calls(at_ms);
|
|
225
|
+
CREATE TABLE token_days (
|
|
226
|
+
lobby_id TEXT NOT NULL, day TEXT NOT NULL, agent_id TEXT NOT NULL, model TEXT NOT NULL,
|
|
227
|
+
speed TEXT NOT NULL DEFAULT '', geo TEXT NOT NULL DEFAULT '', long INTEGER NOT NULL,
|
|
228
|
+
calls INTEGER NOT NULL, input INTEGER NOT NULL, cache_write_5m INTEGER NOT NULL,
|
|
229
|
+
cache_write_1h INTEGER NOT NULL, cache_read INTEGER NOT NULL, output INTEGER NOT NULL,
|
|
230
|
+
PRIMARY KEY (lobby_id, day, agent_id, model, speed, geo, long)
|
|
231
|
+
);
|
|
232
|
+
CREATE TABLE agent_turns (
|
|
233
|
+
agent_id TEXT NOT NULL, started_ms INTEGER NOT NULL, ended_ms INTEGER, ended_state TEXT,
|
|
234
|
+
PRIMARY KEY (agent_id, started_ms)
|
|
235
|
+
);
|
|
236
|
+
CREATE INDEX agent_turns_ended ON agent_turns(ended_ms);
|
|
237
|
+
CREATE TABLE memory_rooms (
|
|
238
|
+
lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, curator TEXT,
|
|
239
|
+
revision INTEGER NOT NULL DEFAULT 0, next_ref INTEGER NOT NULL DEFAULT 1,
|
|
240
|
+
last_review_ms INTEGER, failures INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
PRIMARY KEY (lobby_id, room_id)
|
|
242
|
+
);
|
|
243
|
+
CREATE TABLE memory_fragments (
|
|
244
|
+
id TEXT PRIMARY KEY, lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, author TEXT NOT NULL,
|
|
245
|
+
request_id TEXT NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL, evidence TEXT NOT NULL,
|
|
246
|
+
about TEXT, created_ms INTEGER NOT NULL, state TEXT NOT NULL DEFAULT 'pending', note TEXT,
|
|
247
|
+
unlanded INTEGER NOT NULL DEFAULT 0, curated_ms INTEGER, UNIQUE (author, request_id)
|
|
248
|
+
);
|
|
249
|
+
CREATE INDEX memory_fragments_room ON memory_fragments(lobby_id, room_id, state, created_ms);
|
|
250
|
+
CREATE TABLE memory_entries (
|
|
251
|
+
id TEXT PRIMARY KEY, lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, ref TEXT NOT NULL,
|
|
252
|
+
kind TEXT NOT NULL, title TEXT NOT NULL, text TEXT NOT NULL, keywords TEXT NOT NULL,
|
|
253
|
+
provenance TEXT NOT NULL, sources TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'active',
|
|
254
|
+
reason TEXT, into_ref TEXT, disputed TEXT, checked_commit TEXT, checked_ms INTEGER,
|
|
255
|
+
created_ms INTEGER NOT NULL, updated_ms INTEGER NOT NULL, UNIQUE (lobby_id, ref)
|
|
256
|
+
);
|
|
257
|
+
CREATE INDEX memory_entries_room ON memory_entries(lobby_id, room_id, state, updated_ms);
|
|
258
|
+
CREATE TABLE memory_entry_history (
|
|
259
|
+
entry_id TEXT NOT NULL, lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, row TEXT NOT NULL,
|
|
260
|
+
changed_ms INTEGER NOT NULL
|
|
261
|
+
);
|
|
262
|
+
CREATE INDEX memory_entry_history_entry ON memory_entry_history(entry_id, changed_ms);
|
|
263
|
+
CREATE TABLE memory_curations (
|
|
264
|
+
id TEXT PRIMARY KEY, lobby_id TEXT NOT NULL, room_id TEXT NOT NULL, curator TEXT NOT NULL,
|
|
265
|
+
revision INTEGER, commit_sha TEXT, review INTEGER NOT NULL DEFAULT 0,
|
|
266
|
+
created_ms INTEGER NOT NULL, added INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
updated INTEGER NOT NULL DEFAULT 0, verified INTEGER NOT NULL DEFAULT 0,
|
|
268
|
+
archived INTEGER NOT NULL DEFAULT 0, refused INTEGER NOT NULL DEFAULT 0,
|
|
269
|
+
summary TEXT NOT NULL DEFAULT '', error TEXT, usage TEXT
|
|
270
|
+
);
|
|
271
|
+
CREATE INDEX memory_curations_room ON memory_curations(lobby_id, room_id, created_ms);
|
|
272
|
+
CREATE TABLE board_captions (
|
|
273
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
274
|
+
lobby_id TEXT NOT NULL REFERENCES lobbies(lobby_id),
|
|
275
|
+
created_at TEXT NOT NULL, created_ms INTEGER NOT NULL, model TEXT NOT NULL,
|
|
276
|
+
input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL,
|
|
277
|
+
cache_creation_input_tokens INTEGER NOT NULL, cache_read_input_tokens INTEGER NOT NULL,
|
|
278
|
+
cost_usd REAL NOT NULL, caption TEXT, picture TEXT, error TEXT,
|
|
279
|
+
CHECK ((caption IS NULL) <> (error IS NULL))
|
|
280
|
+
);
|
|
281
|
+
CREATE INDEX board_captions_lobby ON board_captions(lobby_id, created_ms);
|
|
282
|
+
PRAGMA user_version=${F};
|
|
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 (
|
|
284
|
+
SELECT MAX(created_ms) AS t FROM messages WHERE sender=? AND schedule_id IS NULL
|
|
285
|
+
UNION ALL
|
|
286
|
+
SELECT * FROM (SELECT created_ms FROM messages WHERE lobby_id=? AND target=?
|
|
287
|
+
AND room_id IS NULL AND deliver_at_ms IS NULL ORDER BY created_ms DESC LIMIT 1)
|
|
288
|
+
UNION ALL
|
|
289
|
+
SELECT * FROM (SELECT released_ms FROM messages WHERE lobby_id=? AND released_ms IS NOT NULL
|
|
290
|
+
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)
|
|
292
|
+
VALUES(?,?,?,?,?,?,?,1,?,?,?,?,?)
|
|
293
|
+
ON CONFLICT(lobby_id,day,agent_id,model,speed,geo,long) DO UPDATE SET calls=calls+1,
|
|
294
|
+
input=input+excluded.input, cache_write_5m=cache_write_5m+excluded.cache_write_5m,
|
|
295
|
+
cache_write_1h=cache_write_1h+excluded.cache_write_1h,
|
|
296
|
+
cache_read=cache_read+excluded.cache_read, output=output+excluded.output`,i,new Date(r.at).toISOString().slice(0,10),s,r.model,r.speed??"",r.geo??"",+(o>Ke),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,s){this.run("UPDATE agent_turns SET ended_ms=?,ended_state=? WHERE agent_id=? AND ended_ms IS NULL",t,s,this.key(e))}turns(e,t){const s=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",s,t).map(i=>this.turn(i))}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,s="share its memory"){const i=this.agent(e);return i.enabled||d(403,"agent_disabled","This identity is disabled"),this.room(i.lobby_id,t),this.one("SELECT 1 FROM room_members WHERE lobby_id=? AND room_id=? AND agent_id=?",i.lobby_id,t,this.key(e))||d(403,"room_membership_required",`Only members of room ${t} ${s}; list_rooms shows your rooms`),{agent:i,lobby:i.lobby_id}}memoryCurator(e,t){const{lobby:s}=this.memoryMember(e,t),i=this.one("SELECT * FROM memory_rooms WHERE lobby_id=? AND room_id=?",s,t);return(!i||i.curator!==this.key(e))&&d(403,"memory_curator_required",`Only the curator the owner named for room ${t} curates its memory`),{lobby:s,room:i}}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(s=>String(s.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,s){e=this.key(e);const i=Lt.parse(s);return this.transaction(()=>{this.assertRuntime(e,t);const n=this.one("SELECT * FROM memory_fragments WHERE author=? AND request_id=?",e,i.client_request_id);if(n){const c=n.state==="refused"&&n.text==="";return(n.room_id!==i.room_id||n.kind!==i.kind||!c&&(n.text!==i.text||n.evidence!==i.evidence||(n.about??void 0)!==i.about))&&d(409,"idempotency_conflict","This client_request_id already saved a different fragment; choose a new one"),{fragment:this.fragmentOf(n),duplicate:!0}}const{lobby:a}=this.memoryMember(e,i.room_id),r=J(`${i.text}
|
|
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
|
|
298
|
+
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(n=>String(n.room_id)),i=this.clock()-10080*60*1e3;return s.map(n=>{const a=this.one("SELECT * FROM memory_rooms WHERE lobby_id=? AND room_id=?",t,n),r=a?.curator?T(String(a.curator)).agent_id:null;return{room_id:n,curator:r,revision:a?Number(a.revision):0,failures:a?Number(a.failures):0,loaded:_e([{room_id:n,entries:this.activeEntries(t,n).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,n).map(o=>{const _=this.entryOf(o);return{..._,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(c=>{const l=JSON.parse(String(c.row));return{changed_at:new Date(Number(c.changed_ms)).toISOString(),title:String(l.title),text:String(l.text)}}),curator_only:r!==null&&_.provenance.length>0&&_.provenance.every(c=>c.author===r)}}),fragments:this.all("SELECT * FROM memory_fragments WHERE lobby_id=? AND room_id=? ORDER BY created_ms DESC",t,n).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,n,i).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,n).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,s,i="owner"){this.transaction(()=>{const n=this.actorLobby(i,e),a=t==="entry"?this.one("SELECT id,room_id FROM memory_entries WHERE ref=? AND lobby_id=?",s,n):this.one("SELECT id,room_id FROM memory_fragments WHERE id=? AND lobby_id=?",s,n);a||d(404,"memory_not_found",`No ${t} ${s} 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(n,String(a.room_id)),this.audit(i,`erase_memory_${t}`,n,String(a.room_id),{[t]:s})})}saveCodeStats(e,t){const s=Nt.parse(t);return this.transaction(()=>{const{lobby:i}=this.memoryMember(e,s.room_id,"send its code statistics"),n=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:${i}:${s.room_id}`,JSON.stringify({...s,received_at:n})),{room_id:s.room_id,received_at:n}})}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 s of["memory_rooms","memory_fragments","memory_entries","memory_entry_history","memory_curations"])this.run(`DELETE FROM ${s} 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-ae)+this.run("DELETE FROM memory_entry_history WHERE changed_ms<?",e-Me)+this.run("DELETE FROM memory_curations WHERE created_ms<?",e-Me)+this.run("DELETE FROM memory_entries WHERE state='archived' AND updated_ms<?",e-Bt)}pruneTurns(){return this.run("DELETE FROM agent_turns WHERE ended_ms IS NOT NULL AND ended_ms<?",this.clock()-wt)}pruneTokens(){return this.run("DELETE FROM token_calls WHERE at_ms<?",this.clock()-ne-1440*60*1e3)}tokenDays(e,t=""){const s=this.requireLobby(e);return this.all(`SELECT day, model, speed, geo, long, SUM(calls) AS calls, SUM(input) AS input,
|
|
300
|
+
SUM(cache_write_5m) AS cache_write_5m, SUM(cache_write_1h) AS cache_write_1h,
|
|
301
|
+
SUM(cache_read) AS cache_read, SUM(output) AS output
|
|
302
|
+
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};
|