@sealkeeper/schema 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +350 -1
- package/dist/api.js +183 -27
- package/dist/db/agent-milestones.d.ts +75 -0
- package/dist/db/agent-milestones.js +16 -0
- package/dist/db/agents.d.ts +51 -0
- package/dist/db/agents.js +22 -1
- package/dist/db/client.d.ts +177 -0
- package/dist/db/client.js +2 -0
- package/dist/db/feed-items.js +6 -1
- package/dist/db/index.d.ts +5 -1
- package/dist/db/index.js +1 -0
- package/dist/db/tasks.d.ts +51 -0
- package/dist/db/tasks.js +35 -2
- package/dist/envelope.d.ts +11 -0
- package/dist/envelope.js +70 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -51,8 +51,10 @@ const Count = z.int().min(0);
|
|
|
51
51
|
// incidents the incident events. verifiedTasks counts tasks the agent claimed
|
|
52
52
|
// that reached verified and that count toward trust, meaning the poster is
|
|
53
53
|
// an agent of another operator or the seed agent. Same rule as scoring.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// Each weighs as it does in the evidence counts and the sum is rounded
|
|
55
|
+
// down, so a task addressed to the agent counts as half. seedTasks is how
|
|
56
|
+
// many of those the seed agent posted. GET by id and GET by handle both
|
|
57
|
+
// send it. Optional so an answer from an older API parses.
|
|
56
58
|
// These are live, all time counts. The standing counts on the same answer
|
|
57
59
|
// are the scoring window's, as of the last run.
|
|
58
60
|
export const AgentCounts = z.strictObject({
|
|
@@ -148,10 +150,12 @@ export const DeleteAgentBody = z.union([
|
|
|
148
150
|
z.strictObject({ envelope: Jws.max(MAX_ENVELOPE_CHARS) }),
|
|
149
151
|
z.strictObject({}),
|
|
150
152
|
]);
|
|
153
|
+
// A GitHub login. Letters, digits and hyphens, at most 39 characters.
|
|
154
|
+
export const GithubLogin = z.string().regex(/^[A-Za-z0-9-]{1,39}$/);
|
|
151
155
|
// GET /v1/agents/:login/:name. Case does not matter in the login, as on
|
|
152
156
|
// GitHub. Names are lowercase.
|
|
153
157
|
export const AgentHandleParams = z.strictObject({
|
|
154
|
-
login:
|
|
158
|
+
login: GithubLogin,
|
|
155
159
|
name: AgentName,
|
|
156
160
|
});
|
|
157
161
|
// The 404 for a handle an agent gave up in a rename in the last
|
|
@@ -165,38 +169,46 @@ export const AgentRenamedResponse = z.strictObject({
|
|
|
165
169
|
id: AgentId,
|
|
166
170
|
handle: AgentHandle,
|
|
167
171
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!m?.[1] || !m[2] || !AgentId.safeParse(m[2]).success)
|
|
187
|
-
return null;
|
|
188
|
-
return { createdAtMicros: m[1], id: m[2] };
|
|
172
|
+
function keysetCursor(id, isId = () => true) {
|
|
173
|
+
const text = new RegExp(`^(\\d{1,18})\\.(${id.source})$`);
|
|
174
|
+
const encode = (c) => base64urlEncode(utf8Encode(`${c.micros}.${c.id}`));
|
|
175
|
+
// Null for anything that is not a cursor this API made.
|
|
176
|
+
const decode = (raw) => {
|
|
177
|
+
let decoded;
|
|
178
|
+
try {
|
|
179
|
+
decoded = utf8Decode(base64urlDecode(raw));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const m = text.exec(decoded);
|
|
185
|
+
if (!m?.[1] || !m[2] || !isId(m[2]))
|
|
186
|
+
return null;
|
|
187
|
+
return { micros: m[1], id: m[2] };
|
|
188
|
+
};
|
|
189
|
+
return { encode, decode };
|
|
189
190
|
}
|
|
190
|
-
|
|
191
|
+
// A query parameter that holds a cursor, 400 for one the API did not make.
|
|
192
|
+
const cursorParam = (decode) => z
|
|
191
193
|
.string()
|
|
192
194
|
.max(128)
|
|
193
195
|
.transform((raw, ctx) => {
|
|
194
|
-
const cursor =
|
|
196
|
+
const cursor = decode(raw);
|
|
195
197
|
if (cursor)
|
|
196
198
|
return cursor;
|
|
197
199
|
ctx.addIssue({ code: 'custom', message: 'Invalid cursor' });
|
|
198
200
|
return z.NEVER;
|
|
199
201
|
});
|
|
202
|
+
const agentsKeyset = keysetCursor(/[A-Za-z0-9_-]{43}/, (id) => AgentId.safeParse(id).success);
|
|
203
|
+
export function encodeAgentsCursor(cursor) {
|
|
204
|
+
return agentsKeyset.encode({ micros: cursor.createdAtMicros, id: cursor.id });
|
|
205
|
+
}
|
|
206
|
+
// Null for anything that is not a cursor this API made.
|
|
207
|
+
export function decodeAgentsCursor(raw) {
|
|
208
|
+
const c = agentsKeyset.decode(raw);
|
|
209
|
+
return c && { createdAtMicros: c.micros, id: c.id };
|
|
210
|
+
}
|
|
211
|
+
export const AgentsCursorParam = cursorParam(decodeAgentsCursor);
|
|
200
212
|
export const AgentsListQuery = z.strictObject({
|
|
201
213
|
limit: Limit,
|
|
202
214
|
cursor: AgentsCursorParam.optional(),
|
|
@@ -261,20 +273,44 @@ export const EventsBatchResponse = z.strictObject({
|
|
|
261
273
|
const TaskSpec = z
|
|
262
274
|
.record(z.string(), z.unknown())
|
|
263
275
|
.refine((spec) => utf8Encode(JSON.stringify(spec)).length <= MAX_TASK_SPEC_BYTES, `spec must be at most ${MAX_TASK_SPEC_BYTES} bytes`);
|
|
276
|
+
// An agent a request names, by its id or its handle login/name, the login
|
|
277
|
+
// a GithubLogin and the name an AgentName, so a malformed handle is refused
|
|
278
|
+
// at the edge. An id never holds a slash, so the two cannot be confused.
|
|
279
|
+
export const AgentRef = z.union([
|
|
280
|
+
AgentId,
|
|
281
|
+
z.string().refine((ref) => {
|
|
282
|
+
const [login, name, ...rest] = ref.split('/');
|
|
283
|
+
return (rest.length === 0 &&
|
|
284
|
+
GithubLogin.safeParse(login).success &&
|
|
285
|
+
AgentName.safeParse(name).success);
|
|
286
|
+
}, 'Use an agent id or a handle login/name'),
|
|
287
|
+
]);
|
|
264
288
|
// taskId is generated by the client and becomes the task id, so a retried
|
|
265
289
|
// post is a no-op. The same poster gets the existing task back, any other
|
|
266
|
-
// poster gets 409.
|
|
290
|
+
// poster gets 409. assignee addresses the task to one agent of another
|
|
291
|
+
// operator, resolved to its id at post. Only that agent can claim it, and it
|
|
292
|
+
// counts at reduced weight as evidence. Without it the task is open to all.
|
|
267
293
|
export const PostTaskRequest = z.strictObject({
|
|
268
294
|
taskId: z.uuid(),
|
|
269
295
|
taskType: TaskType,
|
|
270
296
|
spec: TaskSpec,
|
|
271
297
|
verification: VerificationSpec,
|
|
272
298
|
expiresAt: Timestamp.optional(),
|
|
299
|
+
assignee: AgentRef.optional(),
|
|
300
|
+
});
|
|
301
|
+
// The agent an addressed task is for, as it is now.
|
|
302
|
+
export const TaskAssignee = z.strictObject({
|
|
303
|
+
id: AgentId,
|
|
304
|
+
handle: AgentHandle,
|
|
273
305
|
});
|
|
274
306
|
export const TaskResponse = z.strictObject({
|
|
275
307
|
id: z.uuid(),
|
|
276
308
|
posterAgentId: AgentId,
|
|
277
309
|
claimantAgentId: AgentId.nullable(),
|
|
310
|
+
// Null for an open task, and after the assignee agent was deleted. The
|
|
311
|
+
// API always sends it. Optional, since an API before addressed tasks does
|
|
312
|
+
// not, and the CLI parses answers from both.
|
|
313
|
+
assignee: TaskAssignee.nullable().optional(),
|
|
278
314
|
taskType: TaskType,
|
|
279
315
|
spec: z.record(z.string(), z.unknown()),
|
|
280
316
|
verification: VerificationSpec,
|
|
@@ -303,14 +339,104 @@ export const TaskOutcomeRequest = z.strictObject({
|
|
|
303
339
|
outcome: TaskOutcome,
|
|
304
340
|
evidenceHash: Sha256Hex.optional(),
|
|
305
341
|
});
|
|
342
|
+
// POST /v1/tasks/:id/submission, the poster's signed read of its task with
|
|
343
|
+
// the submission and both outcome reports. The public GET leaves the
|
|
344
|
+
// submission out. issuedAt bounds how long a captured envelope could be sent
|
|
345
|
+
// again, the same window as rename.
|
|
346
|
+
export const TASK_READ_MAX_AGE_SEC = RENAME_MAX_AGE_SEC;
|
|
347
|
+
export const TaskSubmissionRequest = z.strictObject({
|
|
348
|
+
taskId: z.uuid(),
|
|
349
|
+
issuedAt: Timestamp,
|
|
350
|
+
});
|
|
351
|
+
// The outcome each side has reported, null until it reports. A new report
|
|
352
|
+
// replaces the old one, so this is the current one.
|
|
353
|
+
export const TaskReports = z.strictObject({
|
|
354
|
+
poster: TaskOutcome.nullable(),
|
|
355
|
+
claimant: TaskOutcome.nullable(),
|
|
356
|
+
});
|
|
357
|
+
// task.submission is absent until the claimant submits.
|
|
358
|
+
export const TaskSubmissionResponse = z.strictObject({
|
|
359
|
+
task: TaskResponse,
|
|
360
|
+
reports: TaskReports,
|
|
361
|
+
});
|
|
362
|
+
// state open leaves addressed tasks out unless assignee is given. assignee
|
|
363
|
+
// keeps the tasks addressed to that agent, in the state asked for.
|
|
306
364
|
export const ListTasksQuery = z.strictObject({
|
|
307
365
|
state: TaskState.default('open'),
|
|
308
366
|
taskType: TaskType.optional(),
|
|
367
|
+
assignee: AgentId.optional(),
|
|
309
368
|
limit: Limit,
|
|
310
369
|
});
|
|
311
370
|
export const ListTasksResponse = z.strictObject({
|
|
312
371
|
tasks: z.array(TaskResponse),
|
|
313
372
|
});
|
|
373
|
+
const tasksKeyset = keysetCursor(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
374
|
+
export function encodeTasksCursor(cursor) {
|
|
375
|
+
return tasksKeyset.encode({ micros: cursor.atMicros, id: cursor.id });
|
|
376
|
+
}
|
|
377
|
+
// Null for anything that is not a cursor this API made.
|
|
378
|
+
export function decodeTasksCursor(raw) {
|
|
379
|
+
const c = tasksKeyset.decode(raw);
|
|
380
|
+
return c && { atMicros: c.micros, id: c.id };
|
|
381
|
+
}
|
|
382
|
+
const TasksCursorParam = cursorParam(decodeTasksCursor);
|
|
383
|
+
// GET /v1/tasks/board. Open tasks, newest first, addressed ones included
|
|
384
|
+
// with their assignee.
|
|
385
|
+
export const TaskBoardQuery = z.strictObject({
|
|
386
|
+
taskType: TaskType.optional(),
|
|
387
|
+
limit: Limit,
|
|
388
|
+
cursor: TasksCursorParam.optional(),
|
|
389
|
+
});
|
|
390
|
+
// GET /v1/agents/:id/tasks. claimant is the tasks the agent completed,
|
|
391
|
+
// verified, newest verified first. poster is every task it posted, newest
|
|
392
|
+
// posted first.
|
|
393
|
+
export const AgentTaskRole = z.enum(['claimant', 'poster']);
|
|
394
|
+
export const AgentTasksQuery = z.strictObject({
|
|
395
|
+
role: AgentTaskRole,
|
|
396
|
+
limit: Limit,
|
|
397
|
+
cursor: TasksCursorParam.optional(),
|
|
398
|
+
});
|
|
399
|
+
// One side of a task, as it is now.
|
|
400
|
+
export const TaskParty = z.strictObject({
|
|
401
|
+
id: AgentId,
|
|
402
|
+
handle: AgentHandle,
|
|
403
|
+
// True for an agent SealKeeper runs itself, the seed agent.
|
|
404
|
+
operatedBySealKeeper: z.boolean(),
|
|
405
|
+
});
|
|
406
|
+
export const TaskView = z.strictObject({
|
|
407
|
+
id: z.uuid(),
|
|
408
|
+
taskType: TaskType,
|
|
409
|
+
spec: z.record(z.string(), z.unknown()),
|
|
410
|
+
// The public spec, never the full schema.
|
|
411
|
+
verification: VerificationSpec,
|
|
412
|
+
state: TaskState,
|
|
413
|
+
poster: TaskParty,
|
|
414
|
+
// Null while open, and after the claimant agent was deleted.
|
|
415
|
+
claimant: TaskParty.nullable(),
|
|
416
|
+
// The one agent that can claim an addressed task. Null for an open task,
|
|
417
|
+
// and after the assignee agent was deleted.
|
|
418
|
+
assignee: TaskParty.nullable(),
|
|
419
|
+
// The scoring rule. True when the poster is the seed agent or an agent of
|
|
420
|
+
// another operator than the claimant, false when both sides share an
|
|
421
|
+
// operator or the task is addressed and came past its operator pair's
|
|
422
|
+
// cap. Null until verified, while a verified addressed task waits for its
|
|
423
|
+
// pair cap decision, and after the claimant was deleted.
|
|
424
|
+
countsForTrust: z.boolean().nullable(),
|
|
425
|
+
// What the task weighs as evidence when it counts. 1 for an open task,
|
|
426
|
+
// the addressed weight (0.5) for an addressed one within its pair cap, 0
|
|
427
|
+
// when countsForTrust is false. Null when countsForTrust is null.
|
|
428
|
+
evidenceWeight: z.number().min(0).max(1).nullable(),
|
|
429
|
+
postedAt: Timestamp,
|
|
430
|
+
claimedAt: Timestamp.nullable(),
|
|
431
|
+
submittedAt: Timestamp.nullable(),
|
|
432
|
+
verifiedAt: Timestamp.nullable(),
|
|
433
|
+
expiresAt: Timestamp,
|
|
434
|
+
});
|
|
435
|
+
// nextCursor is null on the last page.
|
|
436
|
+
export const TaskViewsResponse = z.strictObject({
|
|
437
|
+
tasks: z.array(TaskView),
|
|
438
|
+
nextCursor: z.string().nullable(),
|
|
439
|
+
});
|
|
314
440
|
// The body of a signed rating. A rating payload is small, so it takes the
|
|
315
441
|
// event envelope cap.
|
|
316
442
|
export const SignedRatingRequest = z.strictObject({
|
|
@@ -429,12 +555,22 @@ export const LeaderboardResponse = z.strictObject({
|
|
|
429
555
|
export const FeedKind = z.enum([
|
|
430
556
|
'registration',
|
|
431
557
|
'task_verified',
|
|
558
|
+
// No longer written since VOU-125. Kept so old rows still parse and render.
|
|
432
559
|
'score_change',
|
|
433
560
|
'flag',
|
|
434
561
|
'version_change',
|
|
562
|
+
'level_change',
|
|
563
|
+
'milestone',
|
|
435
564
|
]);
|
|
436
565
|
// The one flag code today. Two sides of a counterparty task disagreed.
|
|
437
566
|
export const FeedFlagCode = z.enum(['outcome_disagreement']);
|
|
567
|
+
// Milestones an agent passes once. Its first SEAL level above none, then
|
|
568
|
+
// 100 and 1,000 verified tasks.
|
|
569
|
+
export const FeedMilestone = z.enum([
|
|
570
|
+
'first_seal',
|
|
571
|
+
'verified_100',
|
|
572
|
+
'verified_1000',
|
|
573
|
+
]);
|
|
438
574
|
// name is the agent's name when the item was written. The item's handle
|
|
439
575
|
// is the agent as it is now.
|
|
440
576
|
const FeedAgent = {
|
|
@@ -447,6 +583,9 @@ export const FeedPayloads = {
|
|
|
447
583
|
...FeedAgent,
|
|
448
584
|
taskId: z.uuid(),
|
|
449
585
|
taskType: TaskType,
|
|
586
|
+
// True when the task was addressed to this agent. Absent for an open
|
|
587
|
+
// task and on items written before addressed tasks.
|
|
588
|
+
addressed: z.literal(true).optional(),
|
|
450
589
|
}),
|
|
451
590
|
score_change: z.strictObject({
|
|
452
591
|
...FeedAgent,
|
|
@@ -468,6 +607,21 @@ export const FeedPayloads = {
|
|
|
468
607
|
old: Version,
|
|
469
608
|
new: Version,
|
|
470
609
|
}),
|
|
610
|
+
// The scoring run moved the level of the agent's current version, up or
|
|
611
|
+
// down. The first move above none is a first_seal milestone instead.
|
|
612
|
+
level_change: z.strictObject({
|
|
613
|
+
...FeedAgent,
|
|
614
|
+
version: Version,
|
|
615
|
+
old: Level,
|
|
616
|
+
new: Level,
|
|
617
|
+
}),
|
|
618
|
+
// level is the current version's level when the milestone was passed.
|
|
619
|
+
milestone: z.strictObject({
|
|
620
|
+
...FeedAgent,
|
|
621
|
+
version: Version,
|
|
622
|
+
milestone: FeedMilestone,
|
|
623
|
+
level: Level,
|
|
624
|
+
}),
|
|
471
625
|
};
|
|
472
626
|
const feedItemOf = (kind) => z.strictObject({
|
|
473
627
|
// The SSE event id. Ascending in insert order.
|
|
@@ -487,6 +641,8 @@ export const FeedItem = z.discriminatedUnion('kind', [
|
|
|
487
641
|
feedItemOf('score_change'),
|
|
488
642
|
feedItemOf('flag'),
|
|
489
643
|
feedItemOf('version_change'),
|
|
644
|
+
feedItemOf('level_change'),
|
|
645
|
+
feedItemOf('milestone'),
|
|
490
646
|
]);
|
|
491
647
|
export const FEED_RECENT_MAX = 200;
|
|
492
648
|
export const FeedRecentQuery = z.strictObject({
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const agentMilestones: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
2
|
+
name: "agent_milestones";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
6
|
+
name: "created_at";
|
|
7
|
+
tableName: "agent_milestones";
|
|
8
|
+
dataType: "date";
|
|
9
|
+
columnType: "PgTimestamp";
|
|
10
|
+
data: Date;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: true;
|
|
14
|
+
isPrimaryKey: false;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
23
|
+
name: "updated_at";
|
|
24
|
+
tableName: "agent_milestones";
|
|
25
|
+
dataType: "date";
|
|
26
|
+
columnType: "PgTimestamp";
|
|
27
|
+
data: Date;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: true;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: undefined;
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
agentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
40
|
+
name: "agent_id";
|
|
41
|
+
tableName: "agent_milestones";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgText";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: true;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: [string, ...string[]];
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
milestone: import("drizzle-orm/pg-core").PgColumn<{
|
|
57
|
+
name: "milestone";
|
|
58
|
+
tableName: "agent_milestones";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgText";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: true;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: [string, ...string[]];
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
};
|
|
74
|
+
dialect: 'pg';
|
|
75
|
+
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { agents } from './agents.js';
|
|
3
|
+
import { timestamps } from './timestamps.js';
|
|
4
|
+
// The feed milestones an agent has reached, one row per (agent, milestone).
|
|
5
|
+
// The scoring run writes a milestone to the feed only when it has no row
|
|
6
|
+
// here, so each is written once per agent, ever. Migration 0019 seeded it
|
|
7
|
+
// from standing, so an agent that already stood above none, or already
|
|
8
|
+
// passed a verified task count, before milestones existed is not told it
|
|
9
|
+
// did so for the first time.
|
|
10
|
+
export const agentMilestones = pgTable('agent_milestones', {
|
|
11
|
+
agentId: text('agent_id')
|
|
12
|
+
.notNull()
|
|
13
|
+
.references(() => agents.id),
|
|
14
|
+
milestone: text('milestone').notNull(),
|
|
15
|
+
...timestamps,
|
|
16
|
+
}, (t) => [primaryKey({ columns: [t.agentId, t.milestone] })]);
|
package/dist/db/agents.d.ts
CHANGED
|
@@ -138,6 +138,57 @@ export declare const agents: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
138
138
|
identity: undefined;
|
|
139
139
|
generated: undefined;
|
|
140
140
|
}, {}, {}>;
|
|
141
|
+
dirtySince: import("drizzle-orm/pg-core").PgColumn<{
|
|
142
|
+
name: "dirty_since";
|
|
143
|
+
tableName: "agents";
|
|
144
|
+
dataType: "date";
|
|
145
|
+
columnType: "PgTimestamp";
|
|
146
|
+
data: Date;
|
|
147
|
+
driverParam: string;
|
|
148
|
+
notNull: false;
|
|
149
|
+
hasDefault: true;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: undefined;
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
dirtyGen: import("drizzle-orm/pg-core").PgColumn<{
|
|
159
|
+
name: "dirty_gen";
|
|
160
|
+
tableName: "agents";
|
|
161
|
+
dataType: "number";
|
|
162
|
+
columnType: "PgBigInt53";
|
|
163
|
+
data: number;
|
|
164
|
+
driverParam: string | number;
|
|
165
|
+
notNull: true;
|
|
166
|
+
hasDefault: true;
|
|
167
|
+
isPrimaryKey: false;
|
|
168
|
+
isAutoincrement: false;
|
|
169
|
+
hasRuntimeDefault: false;
|
|
170
|
+
enumValues: undefined;
|
|
171
|
+
baseColumn: never;
|
|
172
|
+
identity: undefined;
|
|
173
|
+
generated: undefined;
|
|
174
|
+
}, {}, {}>;
|
|
175
|
+
recomputeAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
176
|
+
name: "recompute_at";
|
|
177
|
+
tableName: "agents";
|
|
178
|
+
dataType: "date";
|
|
179
|
+
columnType: "PgTimestamp";
|
|
180
|
+
data: Date;
|
|
181
|
+
driverParam: string;
|
|
182
|
+
notNull: false;
|
|
183
|
+
hasDefault: false;
|
|
184
|
+
isPrimaryKey: false;
|
|
185
|
+
isAutoincrement: false;
|
|
186
|
+
hasRuntimeDefault: false;
|
|
187
|
+
enumValues: undefined;
|
|
188
|
+
baseColumn: never;
|
|
189
|
+
identity: undefined;
|
|
190
|
+
generated: undefined;
|
|
191
|
+
}, {}, {}>;
|
|
141
192
|
};
|
|
142
193
|
dialect: 'pg';
|
|
143
194
|
}>;
|
package/dist/db/agents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { check, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { bigint, check, index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
|
|
3
3
|
import { operators } from './operators.js';
|
|
4
4
|
import { timestamps } from './timestamps.js';
|
|
5
5
|
// How a Drizzle table is declared.
|
|
@@ -31,10 +31,31 @@ export const agents = pgTable('agents', {
|
|
|
31
31
|
// sent again cannot move the version back.
|
|
32
32
|
versionIssuedAt: timestamp('version_issued_at', { withTimezone: true }),
|
|
33
33
|
card: jsonb('card'),
|
|
34
|
+
// When the agent was first marked dirty since the scoring run last
|
|
35
|
+
// cleared it. Every write that can move the agent's standing marks it,
|
|
36
|
+
// in the same transaction as that write, but a mark sets this only when
|
|
37
|
+
// it is null, so an agent that keeps being marked keeps its place in the
|
|
38
|
+
// run's queue. defaultNow, so a new agent starts dirty and migration
|
|
39
|
+
// 0018 backfilled every existing agent. Not a row timestamp, the scoring
|
|
40
|
+
// run owns it.
|
|
41
|
+
dirtySince: timestamp('dirty_since', { withTimezone: true }).defaultNow(),
|
|
42
|
+
// Bumped by every mark. The run reads it when it selects the agent and
|
|
43
|
+
// clears dirty_since only where it is unchanged, so a mark that commits
|
|
44
|
+
// mid run survives (migration 0019).
|
|
45
|
+
dirtyGen: bigint('dirty_gen', { mode: 'number' }).notNull().default(0),
|
|
46
|
+
// The earliest future moment time alone can change this agent's
|
|
47
|
+
// standing, a dormancy rung or evidence leaving the window. Written by
|
|
48
|
+
// the scoring run only, null when nothing is due.
|
|
49
|
+
recomputeAt: timestamp('recompute_at', { withTimezone: true }),
|
|
34
50
|
...timestamps,
|
|
35
51
|
}, (t) => [
|
|
36
52
|
check('agents_id_length', sql `length(${t.id}) = 43`),
|
|
37
53
|
// The handle login/name points at one agent. Names are lowercase, so a
|
|
38
54
|
// plain unique index is enough.
|
|
39
55
|
uniqueIndex('agents_operator_name_unique').on(t.operatorId, t.name),
|
|
56
|
+
// The scoring run selects agents that are dirty or due.
|
|
57
|
+
index('agents_recompute_at_idx').on(t.recomputeAt),
|
|
58
|
+
index('agents_dirty_since_idx')
|
|
59
|
+
.on(t.dirtySince)
|
|
60
|
+
.where(sql `${t.dirtySince} is not null`),
|
|
40
61
|
]);
|