@ai-ecoverse/slicc-agent 5.6.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/dist/agent.js +8 -2
- package/dist/agent.js.map +2 -2
- package/dist/approvals/index.js +62 -29
- package/dist/approvals/index.js.map +2 -2
- package/dist/approvals/record.js.map +1 -1
- package/dist/control.js +2 -1
- package/dist/control.js.map +2 -2
- package/dist/guests/attribution.js +19 -0
- package/dist/guests/attribution.js.map +2 -2
- package/dist/guests/client.js +4 -3
- package/dist/guests/client.js.map +2 -2
- package/dist/guests/guard.js +168 -23
- package/dist/guests/guard.js.map +3 -3
- package/dist/guests/index.js +129 -67
- package/dist/guests/index.js.map +2 -2
- package/dist/guests/review.js +262 -0
- package/dist/guests/review.js.map +7 -0
- package/dist/guests/seats.js.map +2 -2
- package/dist/guests/serve.js +13 -13
- package/dist/guests/serve.js.map +2 -2
- package/dist/host.js +14 -8
- package/dist/host.js.map +2 -2
- package/dist/services.js +2 -0
- package/dist/services.js.map +2 -2
- package/dist/spectrum/agent-port.js +1 -1
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/spectrum/messages.js +50 -6
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/spectrum/proposals.js +7 -3
- package/dist/spectrum/proposals.js.map +2 -2
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/approvals/index.d.ts +5 -2
- package/dist/types/approvals/record.d.ts +2 -0
- package/dist/types/control.d.ts +1 -1
- package/dist/types/guests/attribution.d.ts +16 -0
- package/dist/types/guests/guard.d.ts +29 -2
- package/dist/types/guests/index.d.ts +10 -4
- package/dist/types/guests/review.d.ts +68 -0
- package/dist/types/guests/seats.d.ts +21 -1
- package/dist/types/guests/serve.d.ts +3 -3
- package/dist/types/services.d.ts +5 -2
- package/dist/types/spectrum/messages.d.ts +2 -0
- package/dist/types/spectrum/proposals.d.ts +2 -1
- package/package.json +3 -3
package/dist/guests/index.js
CHANGED
|
@@ -2,9 +2,22 @@ import { replicatedState } from "@earendil-works/chord";
|
|
|
2
2
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
3
3
|
import { InboxDoc } from "@earendil-works/pi-durable";
|
|
4
4
|
import { scrubText } from "../secrets/scrub.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ASKED_KIND,
|
|
7
|
+
GUEST_INPUT_KIND,
|
|
8
|
+
guestContent
|
|
9
|
+
} from "./attribution.js";
|
|
6
10
|
import { GUEST_REQUEST } from "./guard.js";
|
|
7
11
|
import { DEFAULT_GUEST_POLICY } from "./policy.js";
|
|
12
|
+
import {
|
|
13
|
+
endReview,
|
|
14
|
+
GuestReviewsDoc,
|
|
15
|
+
KEPT_SUBMISSIONS,
|
|
16
|
+
liveReviews,
|
|
17
|
+
MAX_QUEUED,
|
|
18
|
+
pruneReviews,
|
|
19
|
+
told
|
|
20
|
+
} from "./review.js";
|
|
8
21
|
import {
|
|
9
22
|
checkDraft,
|
|
10
23
|
cleanLabel,
|
|
@@ -14,7 +27,6 @@ import {
|
|
|
14
27
|
} from "./seats.js";
|
|
15
28
|
const MAX_PROPOSAL = 32e3;
|
|
16
29
|
const MAX_REQUEST = 128;
|
|
17
|
-
const KEPT_DELIVERED = 100;
|
|
18
30
|
const LONGEST_TIMER = 2 ** 31 - 1;
|
|
19
31
|
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
20
32
|
const unanswered = (reason) => ({
|
|
@@ -40,10 +52,10 @@ async function withdrawQueued(harness, agents, record, using) {
|
|
|
40
52
|
);
|
|
41
53
|
for (const id of queued) await harness.abortSubmission(id, using, conversation.id);
|
|
42
54
|
}
|
|
43
|
-
async function deliver(record, text,
|
|
44
|
-
const conversation = await agents.conversation(record.agentId, using);
|
|
55
|
+
async function deliver(record, text, requestId, options, using) {
|
|
56
|
+
const conversation = await options.agents.conversation(record.agentId, using);
|
|
45
57
|
if (!conversation) return unanswered("gone");
|
|
46
|
-
const data = { seat: record.id, name: record.label, approval };
|
|
58
|
+
const data = { seat: record.id, name: record.label, approval: null };
|
|
47
59
|
const submitted = await conversation.submit(
|
|
48
60
|
{
|
|
49
61
|
type: "input",
|
|
@@ -54,7 +66,16 @@ async function deliver(record, text, approval, requestId, agents, using) {
|
|
|
54
66
|
},
|
|
55
67
|
using
|
|
56
68
|
);
|
|
57
|
-
|
|
69
|
+
const id = String(submitted.id);
|
|
70
|
+
await options.harness.commit(async (tx) => {
|
|
71
|
+
const kept = (await tx.doc(GuestsDoc)).seats[record.id];
|
|
72
|
+
if (!kept || kept.submissions?.some((entry) => entry.id === id)) return;
|
|
73
|
+
kept.submissions = [
|
|
74
|
+
...kept.submissions ?? [],
|
|
75
|
+
{ id, approval: null, tools: record.gates.tools }
|
|
76
|
+
].slice(-KEPT_SUBMISSIONS);
|
|
77
|
+
}, using);
|
|
78
|
+
return { state: "submitted", submissionId: id, reason: null };
|
|
58
79
|
}
|
|
59
80
|
async function prepare(record, request) {
|
|
60
81
|
const text = typeof request?.text === "string" ? request.text.trim() : "";
|
|
@@ -63,24 +84,66 @@ async function prepare(record, request) {
|
|
|
63
84
|
const asked = typeof request.requestId === "string" && request.requestId && request.requestId.length <= MAX_REQUEST ? request.requestId : crypto.randomUUID();
|
|
64
85
|
return { text: await scrubText(text), requestId: `${GUEST_REQUEST}${record.id}:${asked}` };
|
|
65
86
|
}
|
|
66
|
-
async function place(record, prepared, options, using) {
|
|
87
|
+
async function place(record, prepared, options, using, at) {
|
|
67
88
|
const gate = record.gates.messages;
|
|
68
|
-
if (gate === "off")
|
|
69
|
-
|
|
70
|
-
if (!options.approvals) return unanswered("unavailable");
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
if (gate === "off") return deliver(record, prepared.text, prepared.requestId, options, using);
|
|
90
|
+
const { reviews } = options;
|
|
91
|
+
if (!reviews || !options.approvals) return unanswered("unavailable");
|
|
92
|
+
const conversation = await options.agents.conversation(record.agentId, using);
|
|
93
|
+
if (!conversation) return unanswered("gone");
|
|
94
|
+
return options.harness.commit(async (tx) => {
|
|
95
|
+
const doc = await tx.doc(GuestReviewsDoc);
|
|
96
|
+
const again = Object.values(doc.reviews).find(
|
|
97
|
+
(review) => review.seat === record.id && review.request === prepared.requestId
|
|
98
|
+
);
|
|
99
|
+
if (again) return { state: "queued", submissionId: null, review: again.id, reason: null };
|
|
100
|
+
if (liveReviews(doc, record.id).length > MAX_QUEUED) return unanswered("overflow");
|
|
101
|
+
const id = `apr-${crypto.randomUUID()}`;
|
|
102
|
+
const task = await tx.createTask(
|
|
103
|
+
reviews.task,
|
|
104
|
+
{ review: id },
|
|
105
|
+
{ ownership: { kind: "conversation" }, conversationId: conversation.id, background: true }
|
|
106
|
+
);
|
|
107
|
+
doc.reviews[id] = {
|
|
108
|
+
id,
|
|
74
109
|
seat: record.id,
|
|
75
110
|
agentId: record.agentId,
|
|
76
|
-
|
|
111
|
+
label: record.label,
|
|
112
|
+
text: prepared.text,
|
|
113
|
+
request: prepared.requestId,
|
|
77
114
|
tier: gate,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
tools: record.gates.tools,
|
|
116
|
+
n: doc.next,
|
|
117
|
+
task: Number(task),
|
|
118
|
+
state: "queued",
|
|
119
|
+
reason: null,
|
|
120
|
+
createdAt: at,
|
|
121
|
+
decidedAt: null,
|
|
122
|
+
submissionId: null,
|
|
123
|
+
ended: false,
|
|
124
|
+
conversation: Number(conversation.id)
|
|
125
|
+
};
|
|
126
|
+
doc.next += 1;
|
|
127
|
+
pruneReviews(doc);
|
|
128
|
+
const asked = {
|
|
129
|
+
seat: record.id,
|
|
130
|
+
name: record.label,
|
|
131
|
+
review: id,
|
|
132
|
+
text: prepared.text,
|
|
133
|
+
at
|
|
134
|
+
};
|
|
135
|
+
await tx.appendEntry(conversation.id, { kind: ASKED_KIND, data: { ...asked } });
|
|
136
|
+
return { state: "queued", submissionId: null, review: id, reason: null };
|
|
137
|
+
}, using);
|
|
138
|
+
}
|
|
139
|
+
function forgotten(harness, id, approval, using) {
|
|
140
|
+
return harness.commit(async (tx) => {
|
|
141
|
+
const record = (await tx.doc(GuestsDoc)).seats[id];
|
|
142
|
+
const kept = record?.alwaysTools?.filter((rule) => rule.approval !== approval);
|
|
143
|
+
if (!record || !kept || kept.length === record.alwaysTools?.length) return { removed: false };
|
|
144
|
+
record.alwaysTools = kept;
|
|
145
|
+
return { removed: true };
|
|
146
|
+
}, using);
|
|
84
147
|
}
|
|
85
148
|
function keyedLine() {
|
|
86
149
|
const locks = /* @__PURE__ */ new Map();
|
|
@@ -94,7 +157,7 @@ function keyedLine() {
|
|
|
94
157
|
return next;
|
|
95
158
|
};
|
|
96
159
|
}
|
|
97
|
-
const pendingIn = (approvals) => (id) => (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === "pending").length;
|
|
160
|
+
const pendingIn = (approvals, reviews) => (id) => (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === "pending").length + liveReviews(reviews.value, id).filter((review) => review.state === "queued").length;
|
|
98
161
|
function shownSeats(records, links, at, pendingOf) {
|
|
99
162
|
return Object.values(records).filter((record) => liveSeat(record, at)).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)).map((record) => ({
|
|
100
163
|
id: record.id,
|
|
@@ -104,7 +167,8 @@ function shownSeats(records, links, at, pendingOf) {
|
|
|
104
167
|
expiresAt: record.expiresAt,
|
|
105
168
|
gates: { ...record.gates },
|
|
106
169
|
connected: (links.get(record.id) ?? 0) > 0,
|
|
107
|
-
pending: pendingOf(record.id)
|
|
170
|
+
pending: pendingOf(record.id),
|
|
171
|
+
alwaysTools: copy(record.alwaysTools ?? [])
|
|
108
172
|
}));
|
|
109
173
|
}
|
|
110
174
|
async function createGuests(options) {
|
|
@@ -112,27 +176,41 @@ async function createGuests(options) {
|
|
|
112
176
|
const now = options.now ?? Date.now;
|
|
113
177
|
const context = options.context ?? BACKGROUND_CONTEXT;
|
|
114
178
|
const guestPolicy = () => options.policy?.current().guests ?? DEFAULT_GUEST_POLICY;
|
|
115
|
-
const records = await harness.commit(
|
|
116
|
-
|
|
179
|
+
const records = await harness.commit(async (tx) => {
|
|
180
|
+
await tx.doc(GuestReviewsDoc);
|
|
181
|
+
return copy((await tx.doc(GuestsDoc)).seats);
|
|
182
|
+
}, context);
|
|
183
|
+
const seatsDoc = await harness.documentState(GuestsDoc, context);
|
|
184
|
+
const reviews = await harness.documentState(
|
|
185
|
+
GuestReviewsDoc,
|
|
117
186
|
context
|
|
118
187
|
);
|
|
119
188
|
const links = /* @__PURE__ */ new Map();
|
|
120
189
|
const ended = /* @__PURE__ */ new Set();
|
|
121
|
-
const gone = /* @__PURE__ */ new
|
|
122
|
-
const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals));
|
|
190
|
+
const gone = /* @__PURE__ */ new Map();
|
|
191
|
+
const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals, reviews));
|
|
123
192
|
const seats = replicatedState(shown());
|
|
124
193
|
let timer;
|
|
125
194
|
const locked = keyedLine();
|
|
126
195
|
const cleanup = (record, using) => locked(record.id, async () => {
|
|
127
196
|
await withdrawQueued(harness, agents, record, using);
|
|
197
|
+
await harness.commit(async (tx) => {
|
|
198
|
+
const doc = await tx.doc(GuestReviewsDoc);
|
|
199
|
+
for (const review of liveReviews(doc, record.id))
|
|
200
|
+
if (review.state === "queued") {
|
|
201
|
+
endReview(review, "withdrawn", "revoked", now());
|
|
202
|
+
await told(tx, review);
|
|
203
|
+
}
|
|
204
|
+
}, using);
|
|
128
205
|
await options.approvals?.withdrawSeat(record.id, using);
|
|
129
206
|
});
|
|
130
207
|
const end = (id, using = context) => {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
208
|
+
const going = gone.get(id);
|
|
209
|
+
if (going !== void 0) return going;
|
|
210
|
+
const cleaning = cleanup(records[id], using);
|
|
211
|
+
gone.set(id, cleaning);
|
|
134
212
|
for (const listener of [...ended]) listener(id);
|
|
135
|
-
return
|
|
213
|
+
return cleaning;
|
|
136
214
|
};
|
|
137
215
|
const publish = () => {
|
|
138
216
|
const at = now();
|
|
@@ -148,51 +226,32 @@ async function createGuests(options) {
|
|
|
148
226
|
timer = setTimeout(publish, Math.min(Math.max(0, next - at), LONGEST_TIMER));
|
|
149
227
|
};
|
|
150
228
|
publish();
|
|
229
|
+
const offSeats = seatsDoc.subscribe((value) => {
|
|
230
|
+
for (const id of Object.keys(records)) delete records[id];
|
|
231
|
+
Object.assign(records, copy(value.seats));
|
|
232
|
+
publish();
|
|
233
|
+
});
|
|
151
234
|
const write = (change) => harness.commit(async (tx) => {
|
|
152
235
|
const doc = await tx.doc(GuestsDoc);
|
|
153
236
|
const changed = change(doc.seats);
|
|
154
237
|
return changed ? copy(changed) : void 0;
|
|
155
238
|
}, context);
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const requestId = `${GUEST_REQUEST}${record.id}:${item.id}`;
|
|
165
|
-
const sent = await deliver(record, text, item.id, requestId, agents, context);
|
|
166
|
-
if (sent.state !== "submitted") return;
|
|
167
|
-
const changed = await write((all) => {
|
|
168
|
-
const kept = all[record.id];
|
|
169
|
-
if (!kept) return void 0;
|
|
170
|
-
kept.delivered = [...kept.delivered ?? [], item.id].slice(-KEPT_DELIVERED);
|
|
171
|
-
return kept;
|
|
172
|
-
});
|
|
173
|
-
if (changed) records[record.id] = changed;
|
|
174
|
-
};
|
|
175
|
-
const approved = (items) => {
|
|
176
|
-
for (const item of items) {
|
|
177
|
-
if (item.kind !== "guest-message" || item.state !== "approved" || !item.seat) continue;
|
|
178
|
-
const record = records[item.seat];
|
|
179
|
-
if (!record || !liveSeat(record, now()) || delivering.has(item.id)) continue;
|
|
180
|
-
if (record.delivered?.includes(item.id)) continue;
|
|
181
|
-
delivering.add(item.id);
|
|
182
|
-
void deliverOne(record.id, item).catch(() => void 0);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const offPending = options.approvals?.state.subscribe((items) => {
|
|
186
|
-
seats.replace(context, shown());
|
|
187
|
-
approved(items);
|
|
239
|
+
const recount = () => seats.replace(context, shown());
|
|
240
|
+
const offPending = options.approvals?.state.subscribe(recount);
|
|
241
|
+
const offReviews = reviews.subscribe(recount);
|
|
242
|
+
options.reviews?.bind({
|
|
243
|
+
approvals: options.approvals,
|
|
244
|
+
agents,
|
|
245
|
+
line: locked,
|
|
246
|
+
alwaysAllowed: () => guestPolicy().always.messages
|
|
188
247
|
});
|
|
189
|
-
approved(options.approvals?.state.value ?? []);
|
|
190
248
|
const current = (guest) => {
|
|
191
249
|
const record = records[guest.id];
|
|
192
250
|
return record && liveSeat(record, now()) && record.epoch === guest.epoch ? record : void 0;
|
|
193
251
|
};
|
|
194
252
|
return {
|
|
195
253
|
seats,
|
|
254
|
+
reviews,
|
|
196
255
|
async open(draft, using) {
|
|
197
256
|
const at = now();
|
|
198
257
|
const problem = checkDraft(draft, at, guestPolicy());
|
|
@@ -227,9 +286,7 @@ async function createGuests(options) {
|
|
|
227
286
|
const changed = await write((all) => {
|
|
228
287
|
const record = all[id];
|
|
229
288
|
if (!record || record.revoked !== null) return void 0;
|
|
230
|
-
record
|
|
231
|
-
record.epoch += 1;
|
|
232
|
-
return record;
|
|
289
|
+
return Object.assign(record, { revoked: at, epoch: record.epoch + 1, alwaysTools: [] });
|
|
233
290
|
});
|
|
234
291
|
if (!changed) return { revoked: false };
|
|
235
292
|
records[id] = changed;
|
|
@@ -238,6 +295,7 @@ async function createGuests(options) {
|
|
|
238
295
|
await ending;
|
|
239
296
|
return { revoked: true };
|
|
240
297
|
},
|
|
298
|
+
forgetAlways: (id, approval, using) => forgotten(harness, id, approval, using),
|
|
241
299
|
admit(id, label) {
|
|
242
300
|
const record = records[id];
|
|
243
301
|
if (!record || !liveSeat(record, now()) || record.label !== cleanLabel(label)) return null;
|
|
@@ -267,13 +325,17 @@ async function createGuests(options) {
|
|
|
267
325
|
if ("state" in prepared) return prepared;
|
|
268
326
|
return locked(record.id, async () => {
|
|
269
327
|
const still = current(guest);
|
|
270
|
-
return still ? place(still, prepared, options, using) : unanswered("revoked");
|
|
328
|
+
return still ? place(still, prepared, options, using, now()) : unanswered("revoked");
|
|
271
329
|
});
|
|
272
330
|
},
|
|
273
331
|
policy: guestPolicy,
|
|
274
332
|
dispose() {
|
|
275
333
|
if (timer) clearTimeout(timer);
|
|
276
334
|
offPending?.();
|
|
335
|
+
offReviews();
|
|
336
|
+
offSeats();
|
|
337
|
+
seatsDoc.dispose();
|
|
338
|
+
reviews.dispose();
|
|
277
339
|
ended.clear();
|
|
278
340
|
}
|
|
279
341
|
};
|
package/dist/guests/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/guests/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { type Harness, InboxDoc } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport type { ApprovalPolicies } from '../approvals/policy.ts';\nimport { scrubText } from '../secrets/scrub.ts';\nimport type { GuestPrincipal } from '../serving.ts';\nimport { GUEST_INPUT_KIND, type GuestInput, guestContent } from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport { DEFAULT_GUEST_POLICY, type GuestPolicy } from './policy.ts';\nimport {\n checkDraft,\n cleanLabel,\n type GuestDraft,\n type GuestSeat,\n GuestsDoc,\n liveSeat,\n MAX_SEAT_MS,\n type SeatRecord,\n} from './seats.ts';\n\nexport type GuestOpened = { seat: GuestSeat; error: null } | { seat: null; error: string };\n\nexport type Proposed =\n | { state: 'submitted'; submissionId: string; reason: null }\n | { state: 'pending'; submissionId: null; approval: string; reason: null }\n | { state: 'unanswered'; submissionId: null; reason: string };\n\nexport interface ProposeRequest {\n text: string;\n requestId: string | null;\n}\n\nexport interface Guests {\n readonly seats: ReplicatedState<GuestSeat[]>;\n open(draft: GuestDraft, context: Context): Promise<GuestOpened>;\n revoke(id: string, context: Context): Promise<{ revoked: boolean }>;\n admit(id: string, label: string): GuestPrincipal | null;\n record(id: string): SeatRecord | undefined;\n current(guest: GuestPrincipal): SeatRecord | undefined;\n joined(id: string): () => void;\n onEnd(listener: (id: string) => void): () => void;\n propose(guest: GuestPrincipal, request: ProposeRequest, context: Context): Promise<Proposed>;\n policy(): GuestPolicy;\n dispose(): void;\n}\n\nexport interface GuestsOptions {\n harness: Pick<Harness, 'commit' | 'abortSubmission'>;\n agents: Pick<Agents, 'state' | 'conversation'>;\n approvals?: Pick<Approvals, 'state' | 'request' | 'withdrawSeat'>;\n policy?: Pick<ApprovalPolicies, 'current'>;\n now?: () => number;\n context?: Context;\n}\n\nexport const MAX_PROPOSAL = 32_000;\nconst MAX_REQUEST = 128;\nconst KEPT_DELIVERED = 100;\nconst LONGEST_TIMER = 2 ** 31 - 1;\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\nconst unanswered = (reason: string): Proposed => ({\n state: 'unanswered',\n submissionId: null,\n reason,\n});\n\nfunction prune(all: Record<string, SeatRecord>, at: number) {\n for (const [id, record] of Object.entries(all)) {\n const over = record.revoked ?? record.expiresAt;\n if (over !== null && at - over > MAX_SEAT_MS) delete all[id];\n }\n}\n\nasync function withdrawQueued(\n harness: GuestsOptions['harness'],\n agents: GuestsOptions['agents'],\n record: SeatRecord,\n using: Context\n) {\n const conversation = await agents.conversation(record.agentId, using);\n if (!conversation) return;\n const queued = await harness.commit(\n async (tx) =>\n (await tx.doc(InboxDoc, conversation.id)).items\n .filter((item) => {\n const entry = item.entry as { kind?: string; data?: { seat?: unknown } } | undefined;\n return entry?.kind === GUEST_INPUT_KIND && entry.data?.seat === record.id;\n })\n .map((item) => item.id),\n using\n );\n for (const id of queued) await harness.abortSubmission(id, using, conversation.id);\n}\n\nasync function deliver(\n record: SeatRecord,\n text: string,\n approval: string | null,\n requestId: string,\n agents: GuestsOptions['agents'],\n using: Context\n): Promise<Proposed> {\n const conversation = await agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n const data: GuestInput = { seat: record.id, name: record.label, approval };\n const submitted = await conversation.submit(\n {\n type: 'input',\n content: guestContent(record.label, text),\n whenBusy: 'followUp',\n entry: { kind: GUEST_INPUT_KIND, data: { ...data } },\n requestId,\n },\n using\n );\n return { state: 'submitted', submissionId: String(submitted.id), reason: null };\n}\n\ntype Prepared = { text: string; requestId: string };\n\nasync function prepare(record: SeatRecord, request: ProposeRequest): Promise<Prepared | Proposed> {\n const text = typeof request?.text === 'string' ? request.text.trim() : '';\n if (!text) return unanswered('empty');\n if (text.length > MAX_PROPOSAL) return unanswered('too-long');\n const asked =\n typeof request.requestId === 'string' &&\n request.requestId &&\n request.requestId.length <= MAX_REQUEST\n ? request.requestId\n : crypto.randomUUID();\n return { text: await scrubText(text), requestId: `${GUEST_REQUEST}${record.id}:${asked}` };\n}\n\nasync function place(\n record: SeatRecord,\n prepared: Prepared,\n options: GuestsOptions,\n using: Context\n): Promise<Proposed> {\n const gate = record.gates.messages;\n if (gate === 'off')\n return deliver(record, prepared.text, null, prepared.requestId, options.agents, using);\n if (!options.approvals) return unanswered('unavailable');\n const item = await options.approvals.request(\n {\n kind: 'guest-message',\n seat: record.id,\n agentId: record.agentId,\n requester: { kind: 'guest', name: record.label, seat: record.id },\n tier: gate,\n detail: { text: prepared.text, attachments: [] },\n },\n using\n );\n if (item.state !== 'pending') return unanswered(item.reason ?? item.state);\n return { state: 'pending', submissionId: null, approval: item.id, reason: null };\n}\n\nfunction keyedLine() {\n const locks = new Map<string, Promise<unknown>>();\n return <T>(id: string, run: () => Promise<T>): Promise<T> => {\n const next = (locks.get(id) ?? Promise.resolve()).then(run, run);\n const settled = next.catch(() => undefined);\n locks.set(id, settled);\n void settled.then(() => {\n if (locks.get(id) === settled) locks.delete(id);\n });\n return next;\n };\n}\n\nconst pendingIn =\n (approvals: GuestsOptions['approvals']) =>\n (id: string): number =>\n (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === 'pending')\n .length;\n\nfunction shownSeats(\n records: Record<string, SeatRecord>,\n links: ReadonlyMap<string, number>,\n at: number,\n pendingOf: (id: string) => number\n): GuestSeat[] {\n return Object.values(records)\n .filter((record) => liveSeat(record, at))\n .sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id))\n .map((record) => ({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n createdAt: record.createdAt,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n connected: (links.get(record.id) ?? 0) > 0,\n pending: pendingOf(record.id),\n }));\n}\n\nexport async function createGuests(options: GuestsOptions): Promise<Guests> {\n const { harness, agents } = options;\n const now = options.now ?? Date.now;\n const context = options.context ?? BACKGROUND_CONTEXT;\n const guestPolicy = () => options.policy?.current().guests ?? DEFAULT_GUEST_POLICY;\n const records: Record<string, SeatRecord> = await harness.commit(\n async (tx) => copy((await tx.doc(GuestsDoc)).seats),\n context\n );\n const links = new Map<string, number>();\n const ended = new Set<(id: string) => void>();\n const gone = new Set<string>();\n const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals));\n const seats = replicatedState<GuestSeat[]>(shown());\n let timer: ReturnType<typeof setTimeout> | undefined;\n const locked = keyedLine();\n const cleanup = (record: SeatRecord, using: Context) =>\n locked(record.id, async () => {\n await withdrawQueued(harness, agents, record, using);\n await options.approvals?.withdrawSeat(record.id, using);\n });\n const end = (id: string, using: Context = context): Promise<void> => {\n const record = records[id];\n if (gone.has(id) || !record) return Promise.resolve();\n gone.add(id);\n for (const listener of [...ended]) listener(id);\n return cleanup(record, using);\n };\n const publish = () => {\n const at = now();\n for (const record of Object.values(records))\n if (!liveSeat(record, at)) void end(record.id).catch(() => undefined);\n seats.replace(context, shown());\n if (timer) clearTimeout(timer);\n timer = undefined;\n const next = Math.min(\n ...Object.values(records)\n .filter((record) => liveSeat(record, at) && record.expiresAt !== null)\n .map((record) => record.expiresAt as number)\n );\n if (Number.isFinite(next))\n timer = setTimeout(publish, Math.min(Math.max(0, next - at), LONGEST_TIMER));\n };\n publish();\n const write = (change: (seats: Record<string, SeatRecord>) => SeatRecord | undefined) =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n const changed = change(doc.seats);\n return changed ? copy(changed) : undefined;\n }, context);\n const delivering = new Set<string>();\n const deliverOne = (seat: string, item: Approval) =>\n locked(seat, async () => {\n const record = records[seat];\n if (!record || !liveSeat(record, now())) return;\n await deliverLocked(record, item);\n });\n const deliverLocked = async (record: SeatRecord, item: Approval) => {\n const text = (item.detail as { text?: string }).text ?? '';\n const requestId = `${GUEST_REQUEST}${record.id}:${item.id}`;\n const sent = await deliver(record, text, item.id, requestId, agents, context);\n if (sent.state !== 'submitted') return;\n const changed = await write((all) => {\n const kept = all[record.id];\n if (!kept) return undefined;\n kept.delivered = [...(kept.delivered ?? []), item.id].slice(-KEPT_DELIVERED);\n return kept;\n });\n if (changed) records[record.id] = changed;\n };\n const approved = (items: readonly Approval[]) => {\n for (const item of items) {\n if (item.kind !== 'guest-message' || item.state !== 'approved' || !item.seat) continue;\n const record = records[item.seat];\n if (!record || !liveSeat(record, now()) || delivering.has(item.id)) continue;\n if (record.delivered?.includes(item.id)) continue;\n delivering.add(item.id);\n void deliverOne(record.id, item).catch(() => undefined);\n }\n };\n const offPending = options.approvals?.state.subscribe((items) => {\n seats.replace(context, shown());\n approved(items);\n });\n approved(options.approvals?.state.value ?? []);\n const current = (guest: GuestPrincipal) => {\n const record = records[guest.id];\n return record && liveSeat(record, now()) && record.epoch === guest.epoch ? record : undefined;\n };\n return {\n seats,\n async open(draft, using) {\n const at = now();\n const problem = checkDraft(draft, at, guestPolicy());\n if (problem) return { seat: null, error: problem };\n if (!agents.state().cones[draft.agentId])\n return { seat: null, error: `There is no cone ${draft.agentId}.` };\n const record: SeatRecord = {\n id: draft.id,\n label: cleanLabel(draft.label),\n agentId: draft.agentId,\n expiresAt: draft.expiresAt,\n gates: { messages: draft.gates.messages, tools: draft.gates.tools },\n createdAt: at,\n epoch: 1,\n revoked: null,\n };\n const kept = await harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n if (doc.seats[record.id]) return null;\n prune(doc.seats, at);\n doc.seats[record.id] = copy(record);\n return copy(doc.seats);\n }, using);\n if (!kept) return { seat: null, error: 'That guest seat already exists.' };\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, kept);\n publish();\n return { seat: shown().find((seat) => seat.id === record.id) as GuestSeat, error: null };\n },\n async revoke(id, using) {\n const at = now();\n const changed = await write((all) => {\n const record = all[id];\n if (!record || record.revoked !== null) return undefined;\n record.revoked = at;\n record.epoch += 1;\n return record;\n });\n if (!changed) return { revoked: false };\n records[id] = changed;\n const ending = end(id, using);\n publish();\n await ending;\n return { revoked: true };\n },\n admit(id, label) {\n const record = records[id];\n if (!record || !liveSeat(record, now()) || record.label !== cleanLabel(label)) return null;\n return { id: record.id, name: record.label, epoch: record.epoch };\n },\n record: (id) => records[id],\n current,\n joined(id) {\n links.set(id, (links.get(id) ?? 0) + 1);\n seats.replace(context, shown());\n let left = false;\n return () => {\n if (left) return;\n left = true;\n links.set(id, (links.get(id) ?? 1) - 1);\n seats.replace(context, shown());\n };\n },\n onEnd(listener) {\n ended.add(listener);\n return () => ended.delete(listener);\n },\n async propose(guest, request, using) {\n const record = current(guest);\n if (!record) return unanswered('revoked');\n const prepared = await prepare(record, request);\n if ('state' in prepared) return prepared;\n return locked(record.id, async () => {\n const still = current(guest);\n return still ? place(still, prepared, options, using) : unanswered('revoked');\n });\n },\n policy: guestPolicy,\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n ended.clear();\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA6C,uBAAuB;AACpE,SAAS,0BAA0B;AACnC,
|
|
4
|
+
"sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { type DocumentState, type Harness, InboxDoc } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approvals } from '../approvals/index.ts';\nimport type { ApprovalPolicies } from '../approvals/policy.ts';\nimport { scrubText } from '../secrets/scrub.ts';\nimport type { GuestPrincipal } from '../serving.ts';\nimport {\n ASKED_KIND,\n GUEST_INPUT_KIND,\n type GuestAsked,\n type GuestInput,\n guestContent,\n} from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport { DEFAULT_GUEST_POLICY, type GuestPolicy } from './policy.ts';\nimport {\n endReview,\n type GuestReviews,\n GuestReviewsDoc,\n KEPT_SUBMISSIONS,\n liveReviews,\n MAX_QUEUED,\n pruneReviews,\n type ReviewsState,\n told,\n} from './review.ts';\nimport {\n checkDraft,\n cleanLabel,\n type GuestDraft,\n type GuestSeat,\n GuestsDoc,\n type GuestsState,\n liveSeat,\n MAX_SEAT_MS,\n type SeatRecord,\n} from './seats.ts';\n\nexport type GuestOpened = { seat: GuestSeat; error: null } | { seat: null; error: string };\n\nexport type Proposed =\n | { state: 'submitted'; submissionId: string; reason: null }\n | { state: 'queued'; submissionId: null; review: string; reason: null }\n | { state: 'unanswered'; submissionId: null; reason: string };\n\nexport interface ProposeRequest {\n text: string;\n requestId: string | null;\n}\n\nexport interface Guests {\n readonly seats: ReplicatedState<GuestSeat[]>;\n readonly reviews: ReplicatedState<ReviewsState | null>;\n open(draft: GuestDraft, context: Context): Promise<GuestOpened>;\n revoke(id: string, context: Context): Promise<{ revoked: boolean }>;\n forgetAlways(id: string, approval: string, context: Context): Promise<{ removed: boolean }>;\n admit(id: string, label: string): GuestPrincipal | null;\n record(id: string): SeatRecord | undefined;\n current(guest: GuestPrincipal): SeatRecord | undefined;\n joined(id: string): () => void;\n onEnd(listener: (id: string) => void): () => void;\n propose(guest: GuestPrincipal, request: ProposeRequest, context: Context): Promise<Proposed>;\n policy(): GuestPolicy;\n dispose(): void;\n}\n\nexport interface GuestsOptions {\n harness: Pick<Harness, 'commit' | 'abortSubmission' | 'documentState'>;\n agents: Pick<Agents, 'state' | 'conversation'>;\n approvals?: Pick<Approvals, 'state' | 'request' | 'withdrawSeat' | 'expire' | 'withdraw'>;\n policy?: Pick<ApprovalPolicies, 'current'>;\n reviews?: Pick<GuestReviews, 'task' | 'bind'>;\n now?: () => number;\n context?: Context;\n}\n\nexport const MAX_PROPOSAL = 32_000;\nconst MAX_REQUEST = 128;\nconst LONGEST_TIMER = 2 ** 31 - 1;\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\nconst unanswered = (reason: string): Proposed => ({\n state: 'unanswered',\n submissionId: null,\n reason,\n});\n\nfunction prune(all: Record<string, SeatRecord>, at: number) {\n for (const [id, record] of Object.entries(all)) {\n const over = record.revoked ?? record.expiresAt;\n if (over !== null && at - over > MAX_SEAT_MS) delete all[id];\n }\n}\n\nasync function withdrawQueued(\n harness: GuestsOptions['harness'],\n agents: GuestsOptions['agents'],\n record: SeatRecord,\n using: Context\n) {\n const conversation = await agents.conversation(record.agentId, using);\n if (!conversation) return;\n const queued = await harness.commit(\n async (tx) =>\n (await tx.doc(InboxDoc, conversation.id)).items\n .filter((item) => {\n const entry = item.entry as { kind?: string; data?: { seat?: unknown } } | undefined;\n return entry?.kind === GUEST_INPUT_KIND && entry.data?.seat === record.id;\n })\n .map((item) => item.id),\n using\n );\n for (const id of queued) await harness.abortSubmission(id, using, conversation.id);\n}\n\nasync function deliver(\n record: SeatRecord,\n text: string,\n requestId: string,\n options: GuestsOptions,\n using: Context\n): Promise<Proposed> {\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n const data: GuestInput = { seat: record.id, name: record.label, approval: null };\n const submitted = await conversation.submit(\n {\n type: 'input',\n content: guestContent(record.label, text),\n whenBusy: 'followUp',\n entry: { kind: GUEST_INPUT_KIND, data: { ...data } },\n requestId,\n },\n using\n );\n const id = String(submitted.id);\n await options.harness.commit(async (tx) => {\n const kept = (await tx.doc(GuestsDoc)).seats[record.id];\n if (!kept || kept.submissions?.some((entry) => entry.id === id)) return;\n kept.submissions = [\n ...(kept.submissions ?? []),\n { id, approval: null, tools: record.gates.tools },\n ].slice(-KEPT_SUBMISSIONS);\n }, using);\n return { state: 'submitted', submissionId: id, reason: null };\n}\n\ntype Prepared = { text: string; requestId: string };\n\nasync function prepare(record: SeatRecord, request: ProposeRequest): Promise<Prepared | Proposed> {\n const text = typeof request?.text === 'string' ? request.text.trim() : '';\n if (!text) return unanswered('empty');\n if (text.length > MAX_PROPOSAL) return unanswered('too-long');\n const asked =\n typeof request.requestId === 'string' &&\n request.requestId &&\n request.requestId.length <= MAX_REQUEST\n ? request.requestId\n : crypto.randomUUID();\n return { text: await scrubText(text), requestId: `${GUEST_REQUEST}${record.id}:${asked}` };\n}\n\nasync function place(\n record: SeatRecord,\n prepared: Prepared,\n options: GuestsOptions,\n using: Context,\n at: number\n): Promise<Proposed> {\n const gate = record.gates.messages;\n if (gate === 'off') return deliver(record, prepared.text, prepared.requestId, options, using);\n const { reviews } = options;\n if (!reviews || !options.approvals) return unanswered('unavailable');\n const conversation = await options.agents.conversation(record.agentId, using);\n if (!conversation) return unanswered('gone');\n return options.harness.commit(async (tx): Promise<Proposed> => {\n const doc = await tx.doc(GuestReviewsDoc);\n const again = Object.values(doc.reviews).find(\n (review) => review.seat === record.id && review.request === prepared.requestId\n );\n if (again) return { state: 'queued', submissionId: null, review: again.id, reason: null };\n if (liveReviews(doc, record.id).length > MAX_QUEUED) return unanswered('overflow');\n const id = `apr-${crypto.randomUUID()}`;\n const task = await tx.createTask(\n reviews.task,\n { review: id },\n { ownership: { kind: 'conversation' }, conversationId: conversation.id, background: true }\n );\n doc.reviews[id] = {\n id,\n seat: record.id,\n agentId: record.agentId,\n label: record.label,\n text: prepared.text,\n request: prepared.requestId,\n tier: gate,\n tools: record.gates.tools,\n n: doc.next,\n task: Number(task),\n state: 'queued',\n reason: null,\n createdAt: at,\n decidedAt: null,\n submissionId: null,\n ended: false,\n conversation: Number(conversation.id),\n };\n doc.next += 1;\n pruneReviews(doc);\n const asked: GuestAsked = {\n seat: record.id,\n name: record.label,\n review: id,\n text: prepared.text,\n at,\n };\n await tx.appendEntry(conversation.id, { kind: ASKED_KIND, data: { ...asked } });\n return { state: 'queued', submissionId: null, review: id, reason: null };\n }, using);\n}\n\nfunction forgotten(\n harness: GuestsOptions['harness'],\n id: string,\n approval: string,\n using: Context\n): Promise<{ removed: boolean }> {\n return harness.commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[id];\n const kept = record?.alwaysTools?.filter((rule) => rule.approval !== approval);\n if (!record || !kept || kept.length === record.alwaysTools?.length) return { removed: false };\n record.alwaysTools = kept;\n return { removed: true };\n }, using);\n}\n\nfunction keyedLine() {\n const locks = new Map<string, Promise<unknown>>();\n return <T>(id: string, run: () => Promise<T>): Promise<T> => {\n const next = (locks.get(id) ?? Promise.resolve()).then(run, run);\n const settled = next.catch(() => undefined);\n locks.set(id, settled);\n void settled.then(() => {\n if (locks.get(id) === settled) locks.delete(id);\n });\n return next;\n };\n}\n\nconst pendingIn =\n (approvals: GuestsOptions['approvals'], reviews: DocumentState<ReviewsState>) =>\n (id: string): number =>\n (approvals?.state.value ?? []).filter((item) => item.seat === id && item.state === 'pending')\n .length + liveReviews(reviews.value, id).filter((review) => review.state === 'queued').length;\n\nfunction shownSeats(\n records: Record<string, SeatRecord>,\n links: ReadonlyMap<string, number>,\n at: number,\n pendingOf: (id: string) => number\n): GuestSeat[] {\n return Object.values(records)\n .filter((record) => liveSeat(record, at))\n .sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id))\n .map((record) => ({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n createdAt: record.createdAt,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n connected: (links.get(record.id) ?? 0) > 0,\n pending: pendingOf(record.id),\n alwaysTools: copy(record.alwaysTools ?? []),\n }));\n}\n\nexport async function createGuests(options: GuestsOptions): Promise<Guests> {\n const { harness, agents } = options;\n const now = options.now ?? Date.now;\n const context = options.context ?? BACKGROUND_CONTEXT;\n const guestPolicy = () => options.policy?.current().guests ?? DEFAULT_GUEST_POLICY;\n const records: Record<string, SeatRecord> = await harness.commit(async (tx) => {\n await tx.doc(GuestReviewsDoc);\n return copy((await tx.doc(GuestsDoc)).seats);\n }, context);\n const seatsDoc = (await harness.documentState(GuestsDoc, context)) as DocumentState<GuestsState>;\n const reviews = (await harness.documentState(\n GuestReviewsDoc,\n context\n )) as DocumentState<ReviewsState>;\n const links = new Map<string, number>();\n const ended = new Set<(id: string) => void>();\n const gone = new Map<string, Promise<void>>();\n const shown = () => shownSeats(records, links, now(), pendingIn(options.approvals, reviews));\n const seats = replicatedState<GuestSeat[]>(shown());\n let timer: ReturnType<typeof setTimeout> | undefined;\n const locked = keyedLine();\n const cleanup = (record: SeatRecord, using: Context) =>\n locked(record.id, async () => {\n await withdrawQueued(harness, agents, record, using);\n await harness.commit(async (tx) => {\n const doc = await tx.doc(GuestReviewsDoc);\n for (const review of liveReviews(doc, record.id))\n if (review.state === 'queued') {\n endReview(review, 'withdrawn', 'revoked', now());\n await told(tx, review);\n }\n }, using);\n await options.approvals?.withdrawSeat(record.id, using);\n });\n const end = (id: string, using: Context = context): Promise<void> => {\n const going = gone.get(id);\n if (going !== undefined) return going;\n const cleaning = cleanup(records[id] as SeatRecord, using);\n gone.set(id, cleaning);\n for (const listener of [...ended]) listener(id);\n return cleaning;\n };\n const publish = () => {\n const at = now();\n for (const record of Object.values(records))\n if (!liveSeat(record, at)) void end(record.id).catch(() => undefined);\n seats.replace(context, shown());\n if (timer) clearTimeout(timer);\n timer = undefined;\n const next = Math.min(\n ...Object.values(records)\n .filter((record) => liveSeat(record, at) && record.expiresAt !== null)\n .map((record) => record.expiresAt as number)\n );\n if (Number.isFinite(next))\n timer = setTimeout(publish, Math.min(Math.max(0, next - at), LONGEST_TIMER));\n };\n publish();\n const offSeats = seatsDoc.subscribe((value) => {\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, copy((value as GuestsState).seats));\n publish();\n });\n const write = (change: (seats: Record<string, SeatRecord>) => SeatRecord | undefined) =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n const changed = change(doc.seats);\n return changed ? copy(changed) : undefined;\n }, context);\n const recount = () => seats.replace(context, shown());\n const offPending = options.approvals?.state.subscribe(recount);\n const offReviews = reviews.subscribe(recount);\n options.reviews?.bind({\n approvals: options.approvals as NonNullable<GuestsOptions['approvals']>,\n agents,\n line: locked,\n alwaysAllowed: () => guestPolicy().always.messages,\n });\n const current = (guest: GuestPrincipal) => {\n const record = records[guest.id];\n return record && liveSeat(record, now()) && record.epoch === guest.epoch ? record : undefined;\n };\n return {\n seats,\n reviews,\n async open(draft, using) {\n const at = now();\n const problem = checkDraft(draft, at, guestPolicy());\n if (problem) return { seat: null, error: problem };\n if (!agents.state().cones[draft.agentId])\n return { seat: null, error: `There is no cone ${draft.agentId}.` };\n const record: SeatRecord = {\n id: draft.id,\n label: cleanLabel(draft.label),\n agentId: draft.agentId,\n expiresAt: draft.expiresAt,\n gates: { messages: draft.gates.messages, tools: draft.gates.tools },\n createdAt: at,\n epoch: 1,\n revoked: null,\n };\n const kept = await harness.commit(async (tx) => {\n const doc = await tx.doc(GuestsDoc);\n if (doc.seats[record.id]) return null;\n prune(doc.seats, at);\n doc.seats[record.id] = copy(record);\n return copy(doc.seats);\n }, using);\n if (!kept) return { seat: null, error: 'That guest seat already exists.' };\n for (const id of Object.keys(records)) delete records[id];\n Object.assign(records, kept);\n publish();\n return { seat: shown().find((seat) => seat.id === record.id) as GuestSeat, error: null };\n },\n async revoke(id, using) {\n const at = now();\n const changed = await write((all) => {\n const record = all[id];\n if (!record || record.revoked !== null) return undefined;\n return Object.assign(record, { revoked: at, epoch: record.epoch + 1, alwaysTools: [] });\n });\n if (!changed) return { revoked: false };\n records[id] = changed;\n const ending = end(id, using);\n publish();\n await ending;\n return { revoked: true };\n },\n forgetAlways: (id, approval, using) => forgotten(harness, id, approval, using),\n admit(id, label) {\n const record = records[id];\n if (!record || !liveSeat(record, now()) || record.label !== cleanLabel(label)) return null;\n return { id: record.id, name: record.label, epoch: record.epoch };\n },\n record: (id) => records[id],\n current,\n joined(id) {\n links.set(id, (links.get(id) ?? 0) + 1);\n seats.replace(context, shown());\n let left = false;\n return () => {\n if (left) return;\n left = true;\n links.set(id, (links.get(id) ?? 1) - 1);\n seats.replace(context, shown());\n };\n },\n onEnd(listener) {\n ended.add(listener);\n return () => ended.delete(listener);\n },\n async propose(guest, request, using) {\n const record = current(guest);\n if (!record) return unanswered('revoked');\n const prepared = await prepare(record, request);\n if ('state' in prepared) return prepared;\n return locked(record.id, async () => {\n const still = current(guest);\n return still ? place(still, prepared, options, using, now()) : unanswered('revoked');\n });\n },\n policy: guestPolicy,\n dispose() {\n if (timer) clearTimeout(timer);\n offPending?.();\n offReviews();\n offSeats();\n seatsDoc.dispose();\n reviews.dispose();\n ended.clear();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|