@data-club/ai-hub-server 0.0.1
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/auth/argon2.d.ts +72 -0
- package/dist/auth/argon2.d.ts.map +1 -0
- package/dist/auth/argon2.js +145 -0
- package/dist/auth/argon2.js.map +1 -0
- package/dist/auth/encryption.d.ts +52 -0
- package/dist/auth/encryption.d.ts.map +1 -0
- package/dist/auth/encryption.js +98 -0
- package/dist/auth/encryption.js.map +1 -0
- package/dist/auth/jwt.d.ts +133 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +147 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +101 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +161 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/passwordPolicy.d.ts +72 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +88 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/roleRules.d.ts +200 -0
- package/dist/auth/roleRules.d.ts.map +1 -0
- package/dist/auth/roleRules.js +365 -0
- package/dist/auth/roleRules.js.map +1 -0
- package/dist/auth/userView.d.ts +55 -0
- package/dist/auth/userView.d.ts.map +1 -0
- package/dist/auth/userView.js +77 -0
- package/dist/auth/userView.js.map +1 -0
- package/dist/bootstrap.d.ts +104 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +160 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/db/connection.d.ts +39 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +62 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +197 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1902 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +662 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/seed.d.ts +92 -0
- package/dist/db/seed.d.ts.map +1 -0
- package/dist/db/seed.js +160 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/handlers/fetch.d.ts +89 -0
- package/dist/handlers/fetch.d.ts.map +1 -0
- package/dist/handlers/fetch.js +173 -0
- package/dist/handlers/fetch.js.map +1 -0
- package/dist/handlers/nextjs.d.ts +40 -0
- package/dist/handlers/nextjs.d.ts.map +1 -0
- package/dist/handlers/nextjs.js +42 -0
- package/dist/handlers/nextjs.js.map +1 -0
- package/dist/handlers/responses.d.ts +46 -0
- package/dist/handlers/responses.d.ts.map +1 -0
- package/dist/handlers/responses.js +74 -0
- package/dist/handlers/responses.js.map +1 -0
- package/dist/handlers/routeTable.d.ts +21 -0
- package/dist/handlers/routeTable.d.ts.map +1 -0
- package/dist/handlers/routeTable.js +133 -0
- package/dist/handlers/routeTable.js.map +1 -0
- package/dist/handlers/router.d.ts +37 -0
- package/dist/handlers/router.d.ts.map +1 -0
- package/dist/handlers/router.js +82 -0
- package/dist/handlers/router.js.map +1 -0
- package/dist/handlers/sse.d.ts +38 -0
- package/dist/handlers/sse.d.ts.map +1 -0
- package/dist/handlers/sse.js +52 -0
- package/dist/handlers/sse.js.map +1 -0
- package/dist/handlers/types.d.ts +96 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +14 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/letta/client.d.ts +355 -0
- package/dist/letta/client.d.ts.map +1 -0
- package/dist/letta/client.js +529 -0
- package/dist/letta/client.js.map +1 -0
- package/dist/letta/eventTranslator.d.ts +166 -0
- package/dist/letta/eventTranslator.d.ts.map +1 -0
- package/dist/letta/eventTranslator.js +532 -0
- package/dist/letta/eventTranslator.js.map +1 -0
- package/dist/letta/index.d.ts +13 -0
- package/dist/letta/index.d.ts.map +1 -0
- package/dist/letta/index.js +12 -0
- package/dist/letta/index.js.map +1 -0
- package/dist/letta/modelSettings.d.ts +176 -0
- package/dist/letta/modelSettings.d.ts.map +1 -0
- package/dist/letta/modelSettings.js +314 -0
- package/dist/letta/modelSettings.js.map +1 -0
- package/dist/letta/promptWrapping.d.ts +141 -0
- package/dist/letta/promptWrapping.d.ts.map +1 -0
- package/dist/letta/promptWrapping.js +213 -0
- package/dist/letta/promptWrapping.js.map +1 -0
- package/dist/letta/sandbox.d.ts +100 -0
- package/dist/letta/sandbox.d.ts.map +1 -0
- package/dist/letta/sandbox.js +151 -0
- package/dist/letta/sandbox.js.map +1 -0
- package/dist/letta/settingsFileSeed.d.ts +146 -0
- package/dist/letta/settingsFileSeed.d.ts.map +1 -0
- package/dist/letta/settingsFileSeed.js +177 -0
- package/dist/letta/settingsFileSeed.js.map +1 -0
- package/dist/letta/subprocessThrottle.d.ts +151 -0
- package/dist/letta/subprocessThrottle.d.ts.map +1 -0
- package/dist/letta/subprocessThrottle.js +195 -0
- package/dist/letta/subprocessThrottle.js.map +1 -0
- package/dist/letta/tpmRecovery.d.ts +202 -0
- package/dist/letta/tpmRecovery.d.ts.map +1 -0
- package/dist/letta/tpmRecovery.js +314 -0
- package/dist/letta/tpmRecovery.js.map +1 -0
- package/dist/letta/types.d.ts +54 -0
- package/dist/letta/types.d.ts.map +1 -0
- package/dist/letta/types.js +16 -0
- package/dist/letta/types.js.map +1 -0
- package/dist/observability/logger.d.ts +48 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +81 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +92 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/routes/admin/agents.d.ts +63 -0
- package/dist/routes/admin/agents.d.ts.map +1 -0
- package/dist/routes/admin/agents.js +461 -0
- package/dist/routes/admin/agents.js.map +1 -0
- package/dist/routes/admin/rotatingMessages.d.ts +51 -0
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
- package/dist/routes/admin/rotatingMessages.js +292 -0
- package/dist/routes/admin/rotatingMessages.js.map +1 -0
- package/dist/routes/admin/sessions.d.ts +61 -0
- package/dist/routes/admin/sessions.d.ts.map +1 -0
- package/dist/routes/admin/sessions.js +232 -0
- package/dist/routes/admin/sessions.js.map +1 -0
- package/dist/routes/admin/settings.d.ts +110 -0
- package/dist/routes/admin/settings.d.ts.map +1 -0
- package/dist/routes/admin/settings.js +401 -0
- package/dist/routes/admin/settings.js.map +1 -0
- package/dist/routes/admin/users.d.ts +115 -0
- package/dist/routes/admin/users.d.ts.map +1 -0
- package/dist/routes/admin/users.js +374 -0
- package/dist/routes/admin/users.js.map +1 -0
- package/dist/routes/agents.d.ts +30 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +55 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +121 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +422 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/chat.d.ts +97 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +484 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/featureToggles.d.ts +34 -0
- package/dist/routes/featureToggles.d.ts.map +1 -0
- package/dist/routes/featureToggles.js +47 -0
- package/dist/routes/featureToggles.js.map +1 -0
- package/dist/routes/health.d.ts +24 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/metrics.d.ts +35 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +106 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/sessions.d.ts +161 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +1501 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stubs.d.ts +25 -0
- package/dist/routes/stubs.d.ts.map +1 -0
- package/dist/routes/stubs.js +25 -0
- package/dist/routes/stubs.js.map +1 -0
- package/dist/state/cron.d.ts +58 -0
- package/dist/state/cron.d.ts.map +1 -0
- package/dist/state/cron.js +160 -0
- package/dist/state/cron.js.map +1 -0
- package/dist/state/rotationRunner.d.ts +96 -0
- package/dist/state/rotationRunner.d.ts.map +1 -0
- package/dist/state/rotationRunner.js +246 -0
- package/dist/state/rotationRunner.js.map +1 -0
- package/dist/state/rotationScheduler.d.ts +76 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -0
- package/dist/state/rotationScheduler.js +124 -0
- package/dist/state/rotationScheduler.js.map +1 -0
- package/dist/state/turnRegistry.d.ts +95 -0
- package/dist/state/turnRegistry.d.ts.map +1 -0
- package/dist/state/turnRegistry.js +149 -0
- package/dist/state/turnRegistry.js.map +1 -0
- package/dist/state/turnRunner.d.ts +134 -0
- package/dist/state/turnRunner.d.ts.map +1 -0
- package/dist/state/turnRunner.js +476 -0
- package/dist/state/turnRunner.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/argon2.ts +152 -0
- package/src/auth/encryption.ts +110 -0
- package/src/auth/jwt.ts +217 -0
- package/src/auth/middleware.ts +250 -0
- package/src/auth/passwordPolicy.ts +99 -0
- package/src/auth/roleRules.ts +497 -0
- package/src/auth/userView.ts +88 -0
- package/src/bootstrap.ts +254 -0
- package/src/db/connection.ts +75 -0
- package/src/db/migrate.ts +212 -0
- package/src/db/schema.ts +750 -0
- package/src/db/seed.ts +236 -0
- package/src/handlers/fetch.ts +251 -0
- package/src/handlers/nextjs.ts +49 -0
- package/src/handlers/responses.ts +95 -0
- package/src/handlers/routeTable.ts +188 -0
- package/src/handlers/router.ts +96 -0
- package/src/handlers/sse.ts +55 -0
- package/src/handlers/types.ts +104 -0
- package/src/index.ts +60 -0
- package/src/letta/client.ts +860 -0
- package/src/letta/eventTranslator.ts +667 -0
- package/src/letta/index.ts +46 -0
- package/src/letta/lettaCliShim.mjs +40 -0
- package/src/letta/sandbox.ts +167 -0
- package/src/letta/settingsFileSeed.ts +254 -0
- package/src/letta/subprocessThrottle.ts +251 -0
- package/src/letta/types.ts +65 -0
- package/src/observability/logger.ts +103 -0
- package/src/observability/metrics.ts +114 -0
- package/src/routes/admin/agents.ts +574 -0
- package/src/routes/admin/rotatingMessages.ts +351 -0
- package/src/routes/admin/sessions.ts +283 -0
- package/src/routes/admin/settings.ts +463 -0
- package/src/routes/admin/users.ts +455 -0
- package/src/routes/agents.ts +66 -0
- package/src/routes/auth.ts +513 -0
- package/src/routes/chat.ts +534 -0
- package/src/routes/featureToggles.ts +52 -0
- package/src/routes/health.ts +44 -0
- package/src/routes/metrics.ts +111 -0
- package/src/routes/sessions.ts +1878 -0
- package/src/state/cron.ts +179 -0
- package/src/state/rotationRunner.ts +326 -0
- package/src/state/rotationScheduler.ts +164 -0
- package/src/state/turnRegistry.ts +210 -0
- package/src/state/turnRunner.ts +588 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotation runner for **rotating starting messages**.
|
|
3
|
+
*
|
|
4
|
+
* One rotation turns a configured prompt into a fresh, forkable "prepared
|
|
5
|
+
* conversation" (see `rotating_message` in `db/schema.ts`):
|
|
6
|
+
*
|
|
7
|
+
* 1. `createConversation(letta_agent_id)` — a clean conversation.
|
|
8
|
+
* 2. Send `prompt` via the normal streaming path, drained to the terminal
|
|
9
|
+
* `result`. Reuses `streamUserMessage`, so the rotation inherits the same
|
|
10
|
+
* subprocess throttle + toolset-none suppression + model routing as a
|
|
11
|
+
* real chat turn.
|
|
12
|
+
* 3. Find the prompt's `user_message` in the resulting history and delete it,
|
|
13
|
+
* so the conversation opens with the agent's reply.
|
|
14
|
+
* 4. Persist the new conversation id as `current_conversation_id`.
|
|
15
|
+
*
|
|
16
|
+
* **Resilience.** Any failure leaves `current_conversation_id` at its last-good
|
|
17
|
+
* value (sessions keep forking the previous good conversation), records
|
|
18
|
+
* `last_status = 'error'` + `last_error`, and still advances `next_rotation_at`
|
|
19
|
+
* so the next interval retries rather than the tick hammering a broken agent.
|
|
20
|
+
*
|
|
21
|
+
* `computeNextRotation` / `validateScheduleFields` are pure and unit-tested
|
|
22
|
+
* independently of any Letta interaction.
|
|
23
|
+
*
|
|
24
|
+
* @see docs/server/rotating-messages.md
|
|
25
|
+
*/
|
|
26
|
+
import { eq } from "drizzle-orm";
|
|
27
|
+
import { rotatingMessage } from "../db/schema.js";
|
|
28
|
+
import { CronParseError, cronNextRun, parseCron } from "./cron.js";
|
|
29
|
+
/** Smallest allowed `every`-interval (guards against hammering Letta). */
|
|
30
|
+
export const MIN_INTERVAL_SECONDS = 60;
|
|
31
|
+
/** Largest allowed `every`-interval (366 days). */
|
|
32
|
+
export const MAX_INTERVAL_SECONDS = 366 * 24 * 3600;
|
|
33
|
+
/** `"HH:MM"` 24-hour validator for the `daily` preset (UTC). */
|
|
34
|
+
const DAILY_TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
35
|
+
/**
|
|
36
|
+
* Validates a schedule config (used by the admin create/patch handlers).
|
|
37
|
+
* Pure — the only "work" is parsing cron, which throws `CronParseError` on
|
|
38
|
+
* malformed input.
|
|
39
|
+
*
|
|
40
|
+
* @param s - the schedule fields to validate.
|
|
41
|
+
* @returns `{ ok: true }` or `{ ok: false, message }`.
|
|
42
|
+
*/
|
|
43
|
+
export function validateScheduleFields(s) {
|
|
44
|
+
switch (s.scheduleKind) {
|
|
45
|
+
case "every": {
|
|
46
|
+
if (s.intervalSeconds === null ||
|
|
47
|
+
!Number.isInteger(s.intervalSeconds) ||
|
|
48
|
+
s.intervalSeconds < MIN_INTERVAL_SECONDS ||
|
|
49
|
+
s.intervalSeconds > MAX_INTERVAL_SECONDS) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: `interval_seconds must be an integer between ${MIN_INTERVAL_SECONDS} and ${MAX_INTERVAL_SECONDS}.`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { ok: true };
|
|
56
|
+
}
|
|
57
|
+
case "daily": {
|
|
58
|
+
if (!s.dailyTime || !DAILY_TIME_RE.test(s.dailyTime)) {
|
|
59
|
+
return { ok: false, message: 'daily_time must be "HH:MM" (24-hour, UTC).' };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true };
|
|
62
|
+
}
|
|
63
|
+
case "cron": {
|
|
64
|
+
if (!s.cronExpression || s.cronExpression.trim() === "") {
|
|
65
|
+
return { ok: false, message: "cron_expression is required for schedule_kind 'cron'." };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
parseCron(s.cronExpression);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message: err instanceof CronParseError ? err.message : "Invalid cron expression.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Computes the next rotation instant (UTC ISO-8601) strictly after `from`.
|
|
82
|
+
*
|
|
83
|
+
* Assumes the schedule already passed `validateScheduleFields`. `daily` and
|
|
84
|
+
* `cron` are evaluated in UTC (see `cron.ts`).
|
|
85
|
+
*
|
|
86
|
+
* @param s - the schedule fields.
|
|
87
|
+
* @param from - the reference instant.
|
|
88
|
+
* @returns the next rotation time as a UTC ISO string.
|
|
89
|
+
*/
|
|
90
|
+
export function computeNextRotation(s, from) {
|
|
91
|
+
switch (s.scheduleKind) {
|
|
92
|
+
case "every": {
|
|
93
|
+
const secs = s.intervalSeconds ?? MIN_INTERVAL_SECONDS;
|
|
94
|
+
return new Date(from.getTime() + secs * 1000).toISOString();
|
|
95
|
+
}
|
|
96
|
+
case "daily": {
|
|
97
|
+
const [hh, mm] = (s.dailyTime ?? "00:00").split(":").map(Number);
|
|
98
|
+
const next = new Date(from.getTime());
|
|
99
|
+
next.setUTCHours(hh, mm, 0, 0);
|
|
100
|
+
if (next.getTime() <= from.getTime()) {
|
|
101
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
102
|
+
}
|
|
103
|
+
return next.toISOString();
|
|
104
|
+
}
|
|
105
|
+
case "cron":
|
|
106
|
+
return cronNextRun(s.cronExpression ?? "* * * * *", from);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** `computeNextRotation` that never throws — falls back to +1h on a poison row. */
|
|
110
|
+
function safeComputeNextRotation(s, from, logger) {
|
|
111
|
+
try {
|
|
112
|
+
return computeNextRotation(s, from);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
logger.error({ err: err instanceof Error ? err.message : String(err), event: "rotation_next_compute_failed" }, "Failed to compute next rotation time; falling back to +1h.");
|
|
116
|
+
return new Date(from.getTime() + 3600 * 1000).toISOString();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Runs one rotation for `row` and persists the result. Never throws — the
|
|
121
|
+
* outcome is reported via the return value and the row's `last_status`.
|
|
122
|
+
*
|
|
123
|
+
* @param deps - db + Letta client + logger.
|
|
124
|
+
* @param row - the rotating-message row to rotate.
|
|
125
|
+
* @returns the rotation outcome.
|
|
126
|
+
*/
|
|
127
|
+
export async function runRotation(deps, row) {
|
|
128
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
129
|
+
const nowIso = now.toISOString();
|
|
130
|
+
const nextRotationAt = safeComputeNextRotation(row, now, deps.logger);
|
|
131
|
+
const previousConversationId = row.currentConversationId;
|
|
132
|
+
let createdConversationId = null;
|
|
133
|
+
try {
|
|
134
|
+
const conv = await deps.lettaClient.createConversation(row.lettaAgentId);
|
|
135
|
+
createdConversationId = conv.id;
|
|
136
|
+
await sendPromptAndDrain(deps.lettaClient, conv.id, row.lettaAgentId, row.prompt);
|
|
137
|
+
// Delete the prompt's user message so the conversation opens with the
|
|
138
|
+
// agent's reply. listMessages is oldest-first; the first user_message is
|
|
139
|
+
// the prompt we just sent.
|
|
140
|
+
const messages = await deps.lettaClient.listMessages(conv.id);
|
|
141
|
+
const firstUser = messages.find((m) => m.message_type === "user_message");
|
|
142
|
+
if (firstUser?.id) {
|
|
143
|
+
await deps.lettaClient.deleteMessage(row.lettaAgentId, firstUser.id);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
deps.logger.warn({ rotating_message_id: row.id, conversation_id: conv.id, event: "rotation_no_user_message" }, "Rotation produced no user_message to delete; storing conversation as-is.");
|
|
147
|
+
}
|
|
148
|
+
deps.db
|
|
149
|
+
.update(rotatingMessage)
|
|
150
|
+
.set({
|
|
151
|
+
currentConversationId: conv.id,
|
|
152
|
+
lastStatus: "ok",
|
|
153
|
+
lastError: null,
|
|
154
|
+
lastRotatedAt: nowIso,
|
|
155
|
+
nextRotationAt,
|
|
156
|
+
updatedAt: nowIso,
|
|
157
|
+
})
|
|
158
|
+
.where(eq(rotatingMessage.id, row.id))
|
|
159
|
+
.run();
|
|
160
|
+
// Best-effort cleanup of the now-superseded conversation.
|
|
161
|
+
if (previousConversationId && previousConversationId !== conv.id) {
|
|
162
|
+
try {
|
|
163
|
+
await deps.lettaClient.deleteConversation(previousConversationId);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
deps.logger.warn({
|
|
167
|
+
rotating_message_id: row.id,
|
|
168
|
+
conversation_id: previousConversationId,
|
|
169
|
+
err: err instanceof Error ? err.message : String(err),
|
|
170
|
+
event: "rotation_previous_cleanup_failed",
|
|
171
|
+
}, "Failed to delete the previous prepared conversation; leaving it orphaned.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
deps.logger.info({
|
|
175
|
+
rotating_message_id: row.id,
|
|
176
|
+
letta_agent_id: row.lettaAgentId,
|
|
177
|
+
conversation_id: conv.id,
|
|
178
|
+
next_rotation_at: nextRotationAt,
|
|
179
|
+
event: "rotation_ok",
|
|
180
|
+
}, "Rotating starting message refreshed.");
|
|
181
|
+
return { status: "ok", conversationId: conv.id };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
185
|
+
// Best-effort drop of the half-built conversation so failures don't
|
|
186
|
+
// accumulate orphans. The last-good current_conversation_id is untouched.
|
|
187
|
+
if (createdConversationId) {
|
|
188
|
+
try {
|
|
189
|
+
await deps.lettaClient.deleteConversation(createdConversationId);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* best-effort */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
deps.db
|
|
196
|
+
.update(rotatingMessage)
|
|
197
|
+
.set({
|
|
198
|
+
lastStatus: "error",
|
|
199
|
+
lastError: message,
|
|
200
|
+
nextRotationAt,
|
|
201
|
+
updatedAt: nowIso,
|
|
202
|
+
})
|
|
203
|
+
.where(eq(rotatingMessage.id, row.id))
|
|
204
|
+
.run();
|
|
205
|
+
deps.logger.error({
|
|
206
|
+
rotating_message_id: row.id,
|
|
207
|
+
letta_agent_id: row.lettaAgentId,
|
|
208
|
+
err: message,
|
|
209
|
+
next_rotation_at: nextRotationAt,
|
|
210
|
+
event: "rotation_error",
|
|
211
|
+
}, "Rotation failed; keeping the previous prepared conversation.");
|
|
212
|
+
return { status: "error", error: message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Sends `prompt` to `conversationId` and drains the stream to its terminal
|
|
217
|
+
* `result`. Throws when the run produced no terminal result or finished
|
|
218
|
+
* unsuccessfully. Always tears the subprocess down via `handle.close()`.
|
|
219
|
+
*/
|
|
220
|
+
async function sendPromptAndDrain(client, conversationId, lettaAgentId, prompt) {
|
|
221
|
+
const handle = await client.streamUserMessage({
|
|
222
|
+
conversationId,
|
|
223
|
+
lettaAgentId,
|
|
224
|
+
userMessage: prompt,
|
|
225
|
+
});
|
|
226
|
+
let sawResult = false;
|
|
227
|
+
let success = false;
|
|
228
|
+
try {
|
|
229
|
+
for await (const msg of handle.events) {
|
|
230
|
+
if (msg.type === "result") {
|
|
231
|
+
sawResult = true;
|
|
232
|
+
success = msg.success;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
handle.close();
|
|
238
|
+
}
|
|
239
|
+
if (!sawResult) {
|
|
240
|
+
throw new Error("Agent run produced no terminal result.");
|
|
241
|
+
}
|
|
242
|
+
if (!success) {
|
|
243
|
+
throw new Error("Agent run did not complete successfully.");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=rotationRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotationRunner.js","sourceRoot":"","sources":["../../src/state/rotationRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process scheduler for **rotating starting messages**.
|
|
3
|
+
*
|
|
4
|
+
* ai-hub deploys as one long-running Node process (in-process
|
|
5
|
+
* `better-sqlite3` + self-hosted Letta), so a simple tick loop is the right
|
|
6
|
+
* tool — no external cron, queue, or multi-instance coordination needed.
|
|
7
|
+
*
|
|
8
|
+
* Each tick selects non-archived `rotating_message` rows whose
|
|
9
|
+
* `next_rotation_at <= now` and runs them **serially** through `runRotation`.
|
|
10
|
+
* An in-memory `inFlight` set prevents a slow rotation from being re-picked by
|
|
11
|
+
* the next tick. `runRotation` advances `next_rotation_at` itself, so a row is
|
|
12
|
+
* naturally not due again until its interval elapses.
|
|
13
|
+
*
|
|
14
|
+
* The scheduler is started once per process from `createFetchHandler`
|
|
15
|
+
* (`ensureRotationScheduler`) — that's the single construction point both the
|
|
16
|
+
* dev `node:http` runner and the Next.js wrapper go through. It is **skipped
|
|
17
|
+
* under tests** (`VITEST` / `NODE_ENV=test`) and via
|
|
18
|
+
* `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, so the existing server-boot test
|
|
19
|
+
* suite is unaffected.
|
|
20
|
+
*
|
|
21
|
+
* @see docs/server/rotating-messages.md
|
|
22
|
+
*/
|
|
23
|
+
import type { DrizzleDB } from "../db/connection.js";
|
|
24
|
+
import type { LettaClient } from "../letta/client.js";
|
|
25
|
+
import type { Logger } from "../observability/logger.js";
|
|
26
|
+
/** Dependency bundle for the scheduler. */
|
|
27
|
+
export interface RotationSchedulerDeps {
|
|
28
|
+
db: DrizzleDB;
|
|
29
|
+
lettaClient: LettaClient;
|
|
30
|
+
logger: Logger;
|
|
31
|
+
}
|
|
32
|
+
/** Handle for stopping a running scheduler. */
|
|
33
|
+
export interface RotationScheduler {
|
|
34
|
+
/** Stops the tick loop. Idempotent. */
|
|
35
|
+
stop(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Options for `startRotationScheduler`. */
|
|
38
|
+
export interface StartRotationSchedulerOptions {
|
|
39
|
+
/** Tick interval in ms. Default 30000. Tests pass a small value. */
|
|
40
|
+
intervalMs?: number;
|
|
41
|
+
/** Skip the immediate kick-off tick (tests drive `tickOnce` directly). */
|
|
42
|
+
skipInitialTick?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Starts a tick loop and returns a handle. Most callers want
|
|
46
|
+
* `ensureRotationScheduler` (process singleton); this is the testable core.
|
|
47
|
+
*
|
|
48
|
+
* @param deps - db + Letta client + logger.
|
|
49
|
+
* @param opts - tick cadence + initial-tick control.
|
|
50
|
+
* @returns a scheduler handle with `stop()`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function startRotationScheduler(deps: RotationSchedulerDeps, opts?: StartRotationSchedulerOptions): RotationScheduler;
|
|
53
|
+
/**
|
|
54
|
+
* Runs every due, non-archived rotation once, serially. Exported for tests
|
|
55
|
+
* (drive a single tick deterministically without a timer).
|
|
56
|
+
*
|
|
57
|
+
* @param deps - db + Letta client + logger.
|
|
58
|
+
* @param inFlight - shared set guarding against re-entrancy across ticks.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tickOnce(deps: RotationSchedulerDeps, inFlight?: Set<string>): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Starts the rotation scheduler once per process and returns it (or `null`
|
|
63
|
+
* when intentionally disabled). Idempotent — subsequent calls return the
|
|
64
|
+
* already-running instance, so building multiple Fetch handlers in one process
|
|
65
|
+
* never spawns competing loops.
|
|
66
|
+
*
|
|
67
|
+
* Disabled (returns `null`, starts nothing) when:
|
|
68
|
+
* - `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, or
|
|
69
|
+
* - running under tests (`VITEST` set, or `NODE_ENV === "test"`).
|
|
70
|
+
*
|
|
71
|
+
* @param deps - db + Letta client + logger.
|
|
72
|
+
* @param env - env source (defaults to `process.env`).
|
|
73
|
+
* @returns the scheduler handle, or `null` when disabled.
|
|
74
|
+
*/
|
|
75
|
+
export declare function ensureRotationScheduler(deps: RotationSchedulerDeps, env?: Record<string, string | undefined>): RotationScheduler | null;
|
|
76
|
+
//# sourceMappingURL=rotationScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotationScheduler.d.ts","sourceRoot":"","sources":["../../src/state/rotationScheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAOzD,2CAA2C;AAC3C,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,IAAI,IAAI,IAAI,CAAC;CACd;AAED,4CAA4C;AAC5C,MAAM,WAAW,6BAA6B;IAC5C,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,qBAAqB,EAC3B,IAAI,GAAE,6BAAkC,GACvC,iBAAiB,CAmCnB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,qBAAqB,EAC3B,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAa,GAChC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,EAC3B,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,iBAAiB,GAAG,IAAI,CAO1B"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process scheduler for **rotating starting messages**.
|
|
3
|
+
*
|
|
4
|
+
* ai-hub deploys as one long-running Node process (in-process
|
|
5
|
+
* `better-sqlite3` + self-hosted Letta), so a simple tick loop is the right
|
|
6
|
+
* tool — no external cron, queue, or multi-instance coordination needed.
|
|
7
|
+
*
|
|
8
|
+
* Each tick selects non-archived `rotating_message` rows whose
|
|
9
|
+
* `next_rotation_at <= now` and runs them **serially** through `runRotation`.
|
|
10
|
+
* An in-memory `inFlight` set prevents a slow rotation from being re-picked by
|
|
11
|
+
* the next tick. `runRotation` advances `next_rotation_at` itself, so a row is
|
|
12
|
+
* naturally not due again until its interval elapses.
|
|
13
|
+
*
|
|
14
|
+
* The scheduler is started once per process from `createFetchHandler`
|
|
15
|
+
* (`ensureRotationScheduler`) — that's the single construction point both the
|
|
16
|
+
* dev `node:http` runner and the Next.js wrapper go through. It is **skipped
|
|
17
|
+
* under tests** (`VITEST` / `NODE_ENV=test`) and via
|
|
18
|
+
* `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, so the existing server-boot test
|
|
19
|
+
* suite is unaffected.
|
|
20
|
+
*
|
|
21
|
+
* @see docs/server/rotating-messages.md
|
|
22
|
+
*/
|
|
23
|
+
import { and, eq, lte } from "drizzle-orm";
|
|
24
|
+
import { rotatingMessage } from "../db/schema.js";
|
|
25
|
+
import { runRotation } from "./rotationRunner.js";
|
|
26
|
+
/** Default tick cadence. Rotations are hourly/daily, so 30s polling is ample. */
|
|
27
|
+
const DEFAULT_TICK_MS = 30_000;
|
|
28
|
+
/**
|
|
29
|
+
* Starts a tick loop and returns a handle. Most callers want
|
|
30
|
+
* `ensureRotationScheduler` (process singleton); this is the testable core.
|
|
31
|
+
*
|
|
32
|
+
* @param deps - db + Letta client + logger.
|
|
33
|
+
* @param opts - tick cadence + initial-tick control.
|
|
34
|
+
* @returns a scheduler handle with `stop()`.
|
|
35
|
+
*/
|
|
36
|
+
export function startRotationScheduler(deps, opts = {}) {
|
|
37
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_TICK_MS;
|
|
38
|
+
const inFlight = new Set();
|
|
39
|
+
let stopped = false;
|
|
40
|
+
let ticking = false;
|
|
41
|
+
const tick = async () => {
|
|
42
|
+
if (stopped || ticking)
|
|
43
|
+
return;
|
|
44
|
+
ticking = true;
|
|
45
|
+
try {
|
|
46
|
+
await tickOnce(deps, inFlight);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
deps.logger.error({ err: err instanceof Error ? err.message : String(err), event: "rotation_tick_failed" }, "Rotation scheduler tick failed.");
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
ticking = false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
56
|
+
// Never let the scheduler alone keep the process alive.
|
|
57
|
+
if (typeof timer.unref === "function")
|
|
58
|
+
timer.unref();
|
|
59
|
+
if (!opts.skipInitialTick) {
|
|
60
|
+
void tick();
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
stop() {
|
|
64
|
+
stopped = true;
|
|
65
|
+
clearInterval(timer);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs every due, non-archived rotation once, serially. Exported for tests
|
|
71
|
+
* (drive a single tick deterministically without a timer).
|
|
72
|
+
*
|
|
73
|
+
* @param deps - db + Letta client + logger.
|
|
74
|
+
* @param inFlight - shared set guarding against re-entrancy across ticks.
|
|
75
|
+
*/
|
|
76
|
+
export async function tickOnce(deps, inFlight = new Set()) {
|
|
77
|
+
const nowIso = new Date().toISOString();
|
|
78
|
+
const due = deps.db
|
|
79
|
+
.select()
|
|
80
|
+
.from(rotatingMessage)
|
|
81
|
+
.where(and(eq(rotatingMessage.isArchived, false), lte(rotatingMessage.nextRotationAt, nowIso)))
|
|
82
|
+
.all();
|
|
83
|
+
for (const row of due) {
|
|
84
|
+
if (inFlight.has(row.id))
|
|
85
|
+
continue;
|
|
86
|
+
inFlight.add(row.id);
|
|
87
|
+
try {
|
|
88
|
+
await runRotation(deps, row);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
inFlight.delete(row.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
96
|
+
/* Process singleton */
|
|
97
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
98
|
+
let singleton = null;
|
|
99
|
+
/**
|
|
100
|
+
* Starts the rotation scheduler once per process and returns it (or `null`
|
|
101
|
+
* when intentionally disabled). Idempotent — subsequent calls return the
|
|
102
|
+
* already-running instance, so building multiple Fetch handlers in one process
|
|
103
|
+
* never spawns competing loops.
|
|
104
|
+
*
|
|
105
|
+
* Disabled (returns `null`, starts nothing) when:
|
|
106
|
+
* - `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, or
|
|
107
|
+
* - running under tests (`VITEST` set, or `NODE_ENV === "test"`).
|
|
108
|
+
*
|
|
109
|
+
* @param deps - db + Letta client + logger.
|
|
110
|
+
* @param env - env source (defaults to `process.env`).
|
|
111
|
+
* @returns the scheduler handle, or `null` when disabled.
|
|
112
|
+
*/
|
|
113
|
+
export function ensureRotationScheduler(deps, env = process.env) {
|
|
114
|
+
if (singleton)
|
|
115
|
+
return singleton;
|
|
116
|
+
if (env.AI_HUB_DISABLE_ROTATION_SCHEDULER === "1")
|
|
117
|
+
return null;
|
|
118
|
+
if (env.VITEST || env.NODE_ENV === "test")
|
|
119
|
+
return null;
|
|
120
|
+
singleton = startRotationScheduler(deps);
|
|
121
|
+
deps.logger.info({ event: "rotation_scheduler_started" }, "Rotating-message scheduler started.");
|
|
122
|
+
return singleton;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=rotationScheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotationScheduler.js","sourceRoot":"","sources":["../../src/state/rotationScheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory broadcaster for live turn streams.
|
|
3
|
+
*
|
|
4
|
+
* Every active turn has at most one writer (the runner) and zero-or-more
|
|
5
|
+
* subscribers (the original `POST /chat` connection + every
|
|
6
|
+
* `GET /chat/:turn_id/events?from_seq=N` resumption connection still
|
|
7
|
+
* running while the turn is live).
|
|
8
|
+
*
|
|
9
|
+
* Persistence lives in `turn_events` (§11) — the registry only handles the
|
|
10
|
+
* **live attach** part of resumption. The first half of a resumption read
|
|
11
|
+
* (replay from `from_seq`) is a plain DB query.
|
|
12
|
+
*
|
|
13
|
+
* The registry is process-local — this matches the single-customer
|
|
14
|
+
* deployment model (§1). When ai-hub scales to multiple replicas in a
|
|
15
|
+
* v1.x release, this gets a Redis-backed implementation behind the same
|
|
16
|
+
* interface; the wire protocol (§9) and the resumption contract (§6.3.2)
|
|
17
|
+
* stay unchanged.
|
|
18
|
+
*
|
|
19
|
+
* @see SPECIFICATIONS.md §6.3.2 — resumption endpoint
|
|
20
|
+
* @see SPECIFICATIONS.md §6.6 — server-authoritative state machine
|
|
21
|
+
* @see SPECIFICATIONS.md §11 — `turn_events` table
|
|
22
|
+
*/
|
|
23
|
+
import type { AIHubEvent } from "@data-club/ai-hub";
|
|
24
|
+
/** Callback signature for a live subscriber. */
|
|
25
|
+
export type TurnEventListener = (event: AIHubEvent) => void;
|
|
26
|
+
/** Returned by `subscribe()` so the caller can unsubscribe. */
|
|
27
|
+
export type Unsubscribe = () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Begins a new live turn channel.
|
|
30
|
+
*
|
|
31
|
+
* The runner calls this when it inserts the `turn` row, before it starts
|
|
32
|
+
* pumping events. Returns an opaque `TurnPublisher` the runner uses to
|
|
33
|
+
* publish + close.
|
|
34
|
+
*
|
|
35
|
+
* @param turnId - the freshly-created turn id.
|
|
36
|
+
* @returns the publisher handle.
|
|
37
|
+
*/
|
|
38
|
+
export declare function openTurnChannel(turnId: string): TurnPublisher;
|
|
39
|
+
/**
|
|
40
|
+
* Subscribes to a live turn channel.
|
|
41
|
+
*
|
|
42
|
+
* Returns `null` when the turn is not active (either never opened or
|
|
43
|
+
* already closed) — the caller should fall back to replaying persisted
|
|
44
|
+
* events from `turn_events` instead.
|
|
45
|
+
*
|
|
46
|
+
* @param turnId - the turn id to attach to.
|
|
47
|
+
* @param listener - called once per event (with `seq` already assigned).
|
|
48
|
+
* @param onClose - called once when the channel closes (after final event).
|
|
49
|
+
* @returns an unsubscribe function, or `null` when the channel is closed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function subscribeToTurn(turnId: string, listener: TurnEventListener, onClose: () => void): Unsubscribe | null;
|
|
52
|
+
/**
|
|
53
|
+
* Returns `true` when the turn has an open channel right now.
|
|
54
|
+
*
|
|
55
|
+
* The chat handler uses this to disambiguate "turn is in flight" from
|
|
56
|
+
* "turn completed long ago" without having to query the DB twice.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isTurnLive(turnId: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Test-only utility — wipes the registry so tests can isolate from each
|
|
61
|
+
* other. Production code never calls this.
|
|
62
|
+
*/
|
|
63
|
+
export declare function _resetTurnRegistryForTests(): void;
|
|
64
|
+
/**
|
|
65
|
+
* The runner's handle on a live channel. Opaque on purpose — the runner
|
|
66
|
+
* is the only writer.
|
|
67
|
+
*/
|
|
68
|
+
export interface TurnPublisher {
|
|
69
|
+
/**
|
|
70
|
+
* AbortSignal that fires when a separate `POST /chat/:id/cancel`
|
|
71
|
+
* request calls `requestTurnCancellation(turnId)`. The runner listens
|
|
72
|
+
* to this in addition to the originating request's signal.
|
|
73
|
+
*/
|
|
74
|
+
cancelSignal: AbortSignal;
|
|
75
|
+
/** Push one event to every current subscriber. */
|
|
76
|
+
publish(event: AIHubEvent): void;
|
|
77
|
+
/** Mark the channel closed. After this, `subscribeToTurn` returns null. */
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Triggers cancellation of the named turn. The runner is expected to
|
|
82
|
+
* have wired its run to the publisher's `cancelSignal`; firing it from
|
|
83
|
+
* here aborts the upstream Letta call and short-circuits the loop.
|
|
84
|
+
*
|
|
85
|
+
* Idempotent: calling on a closed / unknown turn is a no-op. The
|
|
86
|
+
* `POST /chat/:id/cancel` route's user-facing 200 response covers
|
|
87
|
+
* race-conditions where the turn terminated between the request and
|
|
88
|
+
* this call.
|
|
89
|
+
*
|
|
90
|
+
* @param turnId - the turn to cancel.
|
|
91
|
+
* @returns `true` if the signal was fired, `false` if the channel is
|
|
92
|
+
* closed or unknown.
|
|
93
|
+
*/
|
|
94
|
+
export declare function requestTurnCancellation(turnId: string): boolean;
|
|
95
|
+
//# sourceMappingURL=turnRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnRegistry.d.ts","sourceRoot":"","sources":["../../src/state/turnRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAE5D,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAyCrC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CA0C7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,MAAM,IAAI,GAClB,WAAW,GAAG,IAAI,CAYpB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,YAAY,EAAE,WAAW,CAAC;IAC1B,kDAAkD;IAClD,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK/D"}
|