@agent-team-foundation/first-tree-hub 0.12.9 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bootstrap-BCZC1ki6.mjs → bootstrap-Cya2OoHz.mjs} +7 -7
- package/dist/cli/index.mjs +268 -480
- package/dist/{client-OMwJMCQt-R1T06ZH6.mjs → client-BH4CmUL0-CybE3kuP.mjs} +92 -8
- package/dist/{client-CjGIGddS-BrpazWa3.mjs → client-h4KZ3b9o-CQyibXig.mjs} +3 -3
- package/dist/{dist-CnjqakXS.mjs → dist-C8yStx2L.mjs} +160 -36
- package/dist/drizzle/0041_notifications_dedup_key.sql +29 -0
- package/dist/drizzle/0042_notifications_drop_legacy_types.sql +36 -0
- package/dist/drizzle/0043_onboarding_completed_at.sql +32 -0
- package/dist/drizzle/meta/_journal.json +21 -0
- package/dist/{errors-CF5evtJt-B0NTIVPt.mjs → errors-LPcARA4K-Dbrptiyz.mjs} +2 -1
- package/dist/{feishu-DrnBbl8T.mjs → feishu-D_vnqC6a.mjs} +1 -1
- package/dist/index.mjs +7 -7
- package/dist/invitation-CNv7gfFF-D93KQte0.mjs +4 -0
- package/dist/{invitation-Bg0TRiyx-BsZH4GCS.mjs → invitation-DZO4NX3P-BPxTeHf-.mjs} +2 -2
- package/dist/{saas-connect-CXZhK485.mjs → saas-connect-Bb5LR4y6.mjs} +1499 -716
- package/dist/web/assets/{index-BPMrSv_A.js → index-CJcRUZ8l.js} +1 -1
- package/dist/web/assets/index-DL_9NFkt.js +421 -0
- package/dist/web/assets/index-DaWEZnjh.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/invitation-C299fxkP-KKslbta2.mjs +0 -4
- package/dist/web/assets/index-DxAYxUpz.css +0 -1
- package/dist/web/assets/index-ntmzuk5X.js +0 -421
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { O as withSpan, f as messageAttrs, s as createLogger } from "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, o as ForbiddenError, s as NotFoundError, u as users } from "./errors-
|
|
2
|
+
import { I as extractMentions, M as defaultParticipantMode, at as questionMessageContentSchema, i as AGENT_STATUSES, it as questionAnswerMessageContentSchema, o as AGENT_VISIBILITY, s as CHAT_ENGAGEMENT_STATUSES, ut as scanMentionTokens, x as clientCapabilitiesSchema, y as agentTypeSchema } from "./dist-C8yStx2L.mjs";
|
|
3
|
+
import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, o as ForbiddenError, s as NotFoundError, u as users } from "./errors-LPcARA4K-Dbrptiyz.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { and, desc, eq, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
|
|
6
6
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
7
|
-
//#region ../server/dist/client-
|
|
7
|
+
//#region ../server/dist/client-BH4CmUL0.mjs
|
|
8
8
|
/**
|
|
9
9
|
* Client connections. A client is a single SDK process (AgentRuntime) that may
|
|
10
10
|
* host multiple agents. From the unified-user-token milestone on, a client is
|
|
@@ -237,7 +237,7 @@ function invalidateChatAudience(chatId) {
|
|
|
237
237
|
* - for each row, `peerAgentTypes` is the type of every OTHER participant
|
|
238
238
|
* being inserted in the same call PLUS every EXISTING speaker of
|
|
239
239
|
* the chat. This matters only for `direct` chats; the helper ignores
|
|
240
|
-
* it for `group
|
|
240
|
+
* it for `group`.
|
|
241
241
|
*
|
|
242
242
|
* Writes one INSERT (multi-row) per call.
|
|
243
243
|
*
|
|
@@ -250,7 +250,7 @@ async function addChatParticipants(tx, chatId, participants, options = {}) {
|
|
|
250
250
|
if (participants.length === 0) return;
|
|
251
251
|
const [chat] = await tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
252
252
|
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
253
|
-
if (chat.type !== "direct" && chat.type !== "group"
|
|
253
|
+
if (chat.type !== "direct" && chat.type !== "group") throw new Error(`Unexpected chat type "${chat.type}" for chat "${chatId}"`);
|
|
254
254
|
const chatType = chat.type;
|
|
255
255
|
const agentIds = participants.map((p) => p.agentId);
|
|
256
256
|
const agentRows = await tx.select({
|
|
@@ -1306,6 +1306,7 @@ const pendingQuestions = pgTable("pending_questions", {
|
|
|
1306
1306
|
const INBOX_CHANNEL = "inbox_notifications";
|
|
1307
1307
|
const CONFIG_CHANNEL = "config_changes";
|
|
1308
1308
|
const SESSION_STATE_CHANNEL = "session_state_changes";
|
|
1309
|
+
const SESSION_EVENT_CHANNEL = "session_event_changes";
|
|
1309
1310
|
const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
|
|
1310
1311
|
/**
|
|
1311
1312
|
* Chat-first workspace cross-process kick. Carries `<chatId>:<messageId>`.
|
|
@@ -1314,17 +1315,29 @@ const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
|
|
|
1314
1315
|
* inbox NOTIFY path that only reaches speakers.
|
|
1315
1316
|
*/
|
|
1316
1317
|
const CHAT_MESSAGE_CHANNEL = "chat_message_events";
|
|
1318
|
+
/**
|
|
1319
|
+
* Generic admin-broadcast envelope channel. Producers (e.g. notification.ts)
|
|
1320
|
+
* emit a JSON-stringified `AdminBroadcastPayload`; every server instance
|
|
1321
|
+
* LISTENs and hands the envelope to its local admin-socket fanout. Keeps
|
|
1322
|
+
* cross-instance and single-instance code paths identical from the admin WS
|
|
1323
|
+
* route's perspective.
|
|
1324
|
+
*/
|
|
1325
|
+
const ADMIN_BROADCAST_CHANNEL = "admin_broadcast_envelopes";
|
|
1317
1326
|
function createNotifier(listenClient) {
|
|
1318
1327
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
1319
1328
|
const configChangeHandlers = [];
|
|
1320
1329
|
const sessionStateChangeHandlers = [];
|
|
1330
|
+
const sessionEventHandlers = [];
|
|
1321
1331
|
const runtimeStateChangeHandlers = [];
|
|
1322
1332
|
const chatMessageHandlers = [];
|
|
1333
|
+
const adminBroadcastHandlers = [];
|
|
1323
1334
|
let unlistenInboxFn = null;
|
|
1324
1335
|
let unlistenConfigFn = null;
|
|
1325
1336
|
let unlistenSessionStateFn = null;
|
|
1337
|
+
let unlistenSessionEventFn = null;
|
|
1326
1338
|
let unlistenRuntimeStateFn = null;
|
|
1327
1339
|
let unlistenChatMessageFn = null;
|
|
1340
|
+
let unlistenAdminBroadcastFn = null;
|
|
1328
1341
|
function handleNotification(payload) {
|
|
1329
1342
|
const sepIdx = payload.indexOf(":");
|
|
1330
1343
|
if (sepIdx === -1) return;
|
|
@@ -1374,6 +1387,11 @@ function createNotifier(listenClient) {
|
|
|
1374
1387
|
await listenClient`SELECT pg_notify(${SESSION_STATE_CHANNEL}, ${`${agentId}:${chatId}:${state}:${organizationId}`})`;
|
|
1375
1388
|
} catch {}
|
|
1376
1389
|
},
|
|
1390
|
+
async notifySessionEvent(agentId, chatId, kind, organizationId) {
|
|
1391
|
+
try {
|
|
1392
|
+
await listenClient`SELECT pg_notify(${SESSION_EVENT_CHANNEL}, ${`${agentId}:${chatId}:${kind}:${organizationId}`})`;
|
|
1393
|
+
} catch {}
|
|
1394
|
+
},
|
|
1377
1395
|
async notifyRuntimeStateChange(agentId, state, organizationId) {
|
|
1378
1396
|
try {
|
|
1379
1397
|
await listenClient`SELECT pg_notify(${RUNTIME_STATE_CHANNEL}, ${`${agentId}:${state}:${organizationId}`})`;
|
|
@@ -1384,6 +1402,21 @@ function createNotifier(listenClient) {
|
|
|
1384
1402
|
await listenClient`SELECT pg_notify(${CHAT_MESSAGE_CHANNEL}, ${`${chatId}:${messageId}`})`;
|
|
1385
1403
|
} catch {}
|
|
1386
1404
|
},
|
|
1405
|
+
async notifyAdminBroadcast(payload) {
|
|
1406
|
+
let encoded;
|
|
1407
|
+
try {
|
|
1408
|
+
encoded = JSON.stringify(payload);
|
|
1409
|
+
} catch {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (encoded.length > 7500) {
|
|
1413
|
+
console.error(`[notifier] admin broadcast payload too large (${encoded.length} bytes); refusing to NOTIFY`);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
await listenClient`SELECT pg_notify(${ADMIN_BROADCAST_CHANNEL}, ${encoded})`;
|
|
1418
|
+
} catch {}
|
|
1419
|
+
},
|
|
1387
1420
|
async pushFrameToInbox(inboxId, frame) {
|
|
1388
1421
|
const map = subscriptions.get(inboxId);
|
|
1389
1422
|
if (!map) return 0;
|
|
@@ -1407,12 +1440,18 @@ function createNotifier(listenClient) {
|
|
|
1407
1440
|
onSessionStateChange(handler) {
|
|
1408
1441
|
sessionStateChangeHandlers.push(handler);
|
|
1409
1442
|
},
|
|
1443
|
+
onSessionEvent(handler) {
|
|
1444
|
+
sessionEventHandlers.push(handler);
|
|
1445
|
+
},
|
|
1410
1446
|
onRuntimeStateChange(handler) {
|
|
1411
1447
|
runtimeStateChangeHandlers.push(handler);
|
|
1412
1448
|
},
|
|
1413
1449
|
onChatMessage(handler) {
|
|
1414
1450
|
chatMessageHandlers.push(handler);
|
|
1415
1451
|
},
|
|
1452
|
+
onAdminBroadcast(handler) {
|
|
1453
|
+
adminBroadcastHandlers.push(handler);
|
|
1454
|
+
},
|
|
1416
1455
|
async start() {
|
|
1417
1456
|
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
1418
1457
|
if (payload) handleNotification(payload);
|
|
@@ -1439,6 +1478,27 @@ function createNotifier(listenClient) {
|
|
|
1439
1478
|
}
|
|
1440
1479
|
}
|
|
1441
1480
|
})).unlisten;
|
|
1481
|
+
unlistenSessionEventFn = (await listenClient.listen(SESSION_EVENT_CHANNEL, (payload) => {
|
|
1482
|
+
if (payload) {
|
|
1483
|
+
const firstSep = payload.indexOf(":");
|
|
1484
|
+
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
1485
|
+
const thirdSep = payload.indexOf(":", secondSep + 1);
|
|
1486
|
+
if (firstSep > 0 && secondSep > firstSep && thirdSep > secondSep) {
|
|
1487
|
+
const agentId = payload.slice(0, firstSep);
|
|
1488
|
+
const chatId = payload.slice(firstSep + 1, secondSep);
|
|
1489
|
+
const kind = payload.slice(secondSep + 1, thirdSep);
|
|
1490
|
+
const organizationId = payload.slice(thirdSep + 1);
|
|
1491
|
+
for (const handler of sessionEventHandlers) try {
|
|
1492
|
+
handler({
|
|
1493
|
+
agentId,
|
|
1494
|
+
chatId,
|
|
1495
|
+
kind,
|
|
1496
|
+
organizationId
|
|
1497
|
+
});
|
|
1498
|
+
} catch {}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
})).unlisten;
|
|
1442
1502
|
unlistenRuntimeStateFn = (await listenClient.listen(RUNTIME_STATE_CHANNEL, (payload) => {
|
|
1443
1503
|
if (payload) {
|
|
1444
1504
|
const firstSep = payload.indexOf(":");
|
|
@@ -1468,6 +1528,20 @@ function createNotifier(listenClient) {
|
|
|
1468
1528
|
});
|
|
1469
1529
|
} catch {}
|
|
1470
1530
|
})).unlisten;
|
|
1531
|
+
unlistenAdminBroadcastFn = (await listenClient.listen(ADMIN_BROADCAST_CHANNEL, (payload) => {
|
|
1532
|
+
if (!payload) return;
|
|
1533
|
+
let parsed;
|
|
1534
|
+
try {
|
|
1535
|
+
parsed = JSON.parse(payload);
|
|
1536
|
+
} catch {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
1540
|
+
const envelope = parsed;
|
|
1541
|
+
for (const handler of adminBroadcastHandlers) try {
|
|
1542
|
+
handler(envelope);
|
|
1543
|
+
} catch {}
|
|
1544
|
+
})).unlisten;
|
|
1471
1545
|
},
|
|
1472
1546
|
async stop() {
|
|
1473
1547
|
if (unlistenInboxFn) {
|
|
@@ -1482,6 +1556,10 @@ function createNotifier(listenClient) {
|
|
|
1482
1556
|
await unlistenSessionStateFn();
|
|
1483
1557
|
unlistenSessionStateFn = null;
|
|
1484
1558
|
}
|
|
1559
|
+
if (unlistenSessionEventFn) {
|
|
1560
|
+
await unlistenSessionEventFn();
|
|
1561
|
+
unlistenSessionEventFn = null;
|
|
1562
|
+
}
|
|
1485
1563
|
if (unlistenRuntimeStateFn) {
|
|
1486
1564
|
await unlistenRuntimeStateFn();
|
|
1487
1565
|
unlistenRuntimeStateFn = null;
|
|
@@ -1490,6 +1568,10 @@ function createNotifier(listenClient) {
|
|
|
1490
1568
|
await unlistenChatMessageFn();
|
|
1491
1569
|
unlistenChatMessageFn = null;
|
|
1492
1570
|
}
|
|
1571
|
+
if (unlistenAdminBroadcastFn) {
|
|
1572
|
+
await unlistenAdminBroadcastFn();
|
|
1573
|
+
unlistenAdminBroadcastFn = null;
|
|
1574
|
+
}
|
|
1493
1575
|
}
|
|
1494
1576
|
};
|
|
1495
1577
|
}
|
|
@@ -1700,8 +1782,9 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
|
1700
1782
|
...incomingMeta,
|
|
1701
1783
|
mentions: mergedMentions
|
|
1702
1784
|
} : incomingMeta;
|
|
1785
|
+
const dmAutoProjection = chatType === "direct" ? [...new Set([...mergedMentions, ...participants.filter((p) => p.agentId !== senderId).map((p) => p.agentId)])] : mergedMentions;
|
|
1703
1786
|
if (options.enforceGroupMention && chatType === "group") {
|
|
1704
|
-
if (mergedMentions.filter((id) => id !== senderId).length === 0) throw new BadRequestError("Sending to a group chat requires an explicit @mention. Use `
|
|
1787
|
+
if (mergedMentions.filter((id) => id !== senderId).length === 0) throw new BadRequestError("Sending to a group chat requires an explicit @mention. Use `first-tree-hub chat send <name>` to message a single agent, or @<name> in the content to address one or more group members.");
|
|
1705
1788
|
}
|
|
1706
1789
|
let outboundContent = data.content;
|
|
1707
1790
|
if (options.normalizeMentionsInContent && typeof outboundContent === "string") {
|
|
@@ -1726,6 +1809,7 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
|
1726
1809
|
senderId,
|
|
1727
1810
|
source: data.source ?? null
|
|
1728
1811
|
}, "silent send: empty content after mention strip — no fan-out wake-up");
|
|
1812
|
+
const projectionMentions = isSilentSend ? [] : dmAutoProjection;
|
|
1729
1813
|
const messageId = randomUUID();
|
|
1730
1814
|
const [msg] = await tx.insert(messages).values({
|
|
1731
1815
|
id: messageId,
|
|
@@ -1782,7 +1866,7 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
|
1782
1866
|
chatId,
|
|
1783
1867
|
messageId: msg.id,
|
|
1784
1868
|
senderId,
|
|
1785
|
-
mentionedAgentIds:
|
|
1869
|
+
mentionedAgentIds: projectionMentions,
|
|
1786
1870
|
contentPreview: previewText,
|
|
1787
1871
|
messageCreatedAt: msg.createdAt
|
|
1788
1872
|
});
|
|
@@ -1885,7 +1969,7 @@ async function sendToAgent(db, senderUuid, targetName, data) {
|
|
|
1885
1969
|
}).from(agents).where(eq(agents.uuid, senderUuid)).limit(1);
|
|
1886
1970
|
if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
|
|
1887
1971
|
const [target] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, sender.organizationId), eq(agents.name, targetName), ne(agents.status, "deleted"))).limit(1);
|
|
1888
|
-
if (!target) throw new NotFoundError(`Agent "${targetName}" not found${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(targetName) ? " — `
|
|
1972
|
+
if (!target) throw new NotFoundError(`Agent "${targetName}" not found${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(targetName) ? " — `first-tree-hub chat send` expects an agent NAME, not a uuid. Run `first-tree-hub agent list` to see available names." : ""}`);
|
|
1889
1973
|
const incomingMeta = data.metadata ?? {};
|
|
1890
1974
|
const existingMentionsRaw = incomingMeta.mentions;
|
|
1891
1975
|
const existingMentions = Array.isArray(existingMentionsRaw) ? existingMentionsRaw.filter((m) => typeof m === "string") : [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
2
2
|
import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
3
|
-
import "./dist-
|
|
4
|
-
import "./errors-
|
|
3
|
+
import "./dist-C8yStx2L.mjs";
|
|
4
|
+
import "./errors-LPcARA4K-Dbrptiyz.mjs";
|
|
5
5
|
import "./src-DNBS5Yjj.mjs";
|
|
6
|
-
import { J as listMyPinnedAgents } from "./client-
|
|
6
|
+
import { J as listMyPinnedAgents } from "./client-BH4CmUL0-CybE3kuP.mjs";
|
|
7
7
|
export { listMyPinnedAgents };
|
|
@@ -74,7 +74,7 @@ function scanMentionTokens(content) {
|
|
|
74
74
|
*/
|
|
75
75
|
function defaultParticipantMode(chatType, agentType, peerAgentTypes = []) {
|
|
76
76
|
if (agentType === "human") return "full";
|
|
77
|
-
if (chatType === "group"
|
|
77
|
+
if (chatType === "group") return "mention_only";
|
|
78
78
|
return peerAgentTypes.every((t) => t !== "human") ? "mention_only" : "full";
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
@@ -185,6 +185,7 @@ z.object({
|
|
|
185
185
|
const PROMPT_APPEND_MAX_LENGTH = 32e3;
|
|
186
186
|
const MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
187
187
|
const ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
188
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:/;
|
|
188
189
|
const promptConfigSchema = z.object({ append: z.string().max(PROMPT_APPEND_MAX_LENGTH).default("") });
|
|
189
190
|
const mcpStdioServerSchema = z.object({
|
|
190
191
|
name: z.string().regex(MCP_NAME_PATTERN, "MCP name must match /^[a-z0-9][a-z0-9_-]{0,63}$/i"),
|
|
@@ -214,10 +215,38 @@ const envEntrySchema = z.object({
|
|
|
214
215
|
value: z.string(),
|
|
215
216
|
sensitive: z.boolean().default(false)
|
|
216
217
|
});
|
|
218
|
+
function hasControlCharacters(value) {
|
|
219
|
+
for (let idx = 0; idx < value.length; idx++) {
|
|
220
|
+
const code = value.charCodeAt(idx);
|
|
221
|
+
if (code <= 31 || code === 127) return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
function getRepoLocalPathSafetyError(localPath) {
|
|
226
|
+
if (localPath.length === 0) return "Git repo local path must not be empty";
|
|
227
|
+
if (localPath.trim() !== localPath) return "Git repo local path must not have leading or trailing whitespace";
|
|
228
|
+
if (hasControlCharacters(localPath)) return "Git repo local path must not contain control characters";
|
|
229
|
+
if (localPath.includes("\\")) return "Git repo local path must use forward slashes";
|
|
230
|
+
if (localPath.startsWith("/") || WINDOWS_DRIVE_PATH_PATTERN.test(localPath)) return "Git repo local path must be relative";
|
|
231
|
+
const segments = localPath.split("/");
|
|
232
|
+
for (const segment of segments) {
|
|
233
|
+
if (!segment) return "Git repo local path must not contain empty path segments";
|
|
234
|
+
if (segment === "." || segment === "..") return "Git repo local path must not contain dot segments";
|
|
235
|
+
if (segment.trim() !== segment) return "Git repo local path segments must not have leading or trailing whitespace";
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
217
239
|
const gitRepoSchema = z.object({
|
|
218
240
|
url: z.string().min(1),
|
|
219
241
|
ref: z.string().min(1).optional(),
|
|
220
|
-
localPath: z.string().min(1).
|
|
242
|
+
localPath: z.string().min(1).superRefine((localPath, ctx) => {
|
|
243
|
+
const safetyError = getRepoLocalPathSafetyError(localPath);
|
|
244
|
+
if (!safetyError) return;
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: z.ZodIssueCode.custom,
|
|
247
|
+
message: safetyError
|
|
248
|
+
});
|
|
249
|
+
}).optional()
|
|
221
250
|
});
|
|
222
251
|
/**
|
|
223
252
|
* Untagged base shape — 5 user-tunable fields, no `kind` discriminator.
|
|
@@ -476,6 +505,11 @@ z.object({
|
|
|
476
505
|
});
|
|
477
506
|
const runtimeProviderSchema = z.enum(["claude-code", "codex"]);
|
|
478
507
|
const DEFAULT_RUNTIME_PROVIDER = "claude-code";
|
|
508
|
+
const AGENT_TYPES = {
|
|
509
|
+
HUMAN: "human",
|
|
510
|
+
PERSONAL_ASSISTANT: "personal_assistant",
|
|
511
|
+
AUTONOMOUS_AGENT: "autonomous_agent"
|
|
512
|
+
};
|
|
479
513
|
const agentTypeSchema = z.enum([
|
|
480
514
|
"human",
|
|
481
515
|
"personal_assistant",
|
|
@@ -627,11 +661,7 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
|
|
|
627
661
|
* sneak through `{ source: "github" }` without the required fields.
|
|
628
662
|
*/
|
|
629
663
|
const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
|
|
630
|
-
const chatTypeSchema = z.enum([
|
|
631
|
-
"direct",
|
|
632
|
-
"group",
|
|
633
|
-
"thread"
|
|
634
|
-
]);
|
|
664
|
+
const chatTypeSchema = z.enum(["direct", "group"]);
|
|
635
665
|
/**
|
|
636
666
|
* Per-(chat, user) engagement state. Stored on `chat_user_state` so each
|
|
637
667
|
* user manages their own view independently of structural membership.
|
|
@@ -1167,6 +1197,28 @@ const meChatParticipantSchema = z.object({
|
|
|
1167
1197
|
displayName: z.string(),
|
|
1168
1198
|
type: z.string()
|
|
1169
1199
|
});
|
|
1200
|
+
/**
|
|
1201
|
+
* Live activity hint surfaced in the conversation row's time slot. Derived
|
|
1202
|
+
* server-side from the latest `session_events` row for the chat. See
|
|
1203
|
+
* `MeChatRow.liveActivity` for the lifecycle rules.
|
|
1204
|
+
*
|
|
1205
|
+
* `kind` is intentionally narrower than the full `sessionEventKind` enum:
|
|
1206
|
+
* `turn_end` / `error` produce `liveActivity: null` rather than a live
|
|
1207
|
+
* indicator.
|
|
1208
|
+
*/
|
|
1209
|
+
const liveActivityKindSchema = z.enum([
|
|
1210
|
+
"tool_call",
|
|
1211
|
+
"thinking",
|
|
1212
|
+
"assistant_text"
|
|
1213
|
+
]);
|
|
1214
|
+
const liveActivitySchema = z.object({
|
|
1215
|
+
agentId: z.string(),
|
|
1216
|
+
kind: liveActivityKindSchema,
|
|
1217
|
+
label: z.string(),
|
|
1218
|
+
startedAt: z.string()
|
|
1219
|
+
});
|
|
1220
|
+
/** Stale threshold (ms) past which a `session_events` row stops driving liveActivity. */
|
|
1221
|
+
const LIVE_ACTIVITY_STALE_MS = 6e4;
|
|
1170
1222
|
const meChatRowSchema = z.object({
|
|
1171
1223
|
chatId: z.string(),
|
|
1172
1224
|
type: z.string(),
|
|
@@ -1179,7 +1231,9 @@ const meChatRowSchema = z.object({
|
|
|
1179
1231
|
lastMessagePreview: z.string().nullable(),
|
|
1180
1232
|
unreadMentionCount: z.number().int(),
|
|
1181
1233
|
canReply: z.boolean(),
|
|
1182
|
-
engagementStatus: chatEngagementStatusSchema
|
|
1234
|
+
engagementStatus: chatEngagementStatusSchema,
|
|
1235
|
+
engagedAgentIds: z.array(z.string()),
|
|
1236
|
+
liveActivity: liveActivitySchema.nullable()
|
|
1183
1237
|
});
|
|
1184
1238
|
z.object({
|
|
1185
1239
|
rows: z.array(meChatRowSchema),
|
|
@@ -1287,15 +1341,106 @@ memberSchema.extend({
|
|
|
1287
1341
|
displayName: z.string(),
|
|
1288
1342
|
password: z.string()
|
|
1289
1343
|
});
|
|
1344
|
+
/**
|
|
1345
|
+
* Origin of a normalized webhook event. After the GitHub App ingestion
|
|
1346
|
+
* cutover this is single-form (App installations only); the type stays
|
|
1347
|
+
* a structured object so future non-GitHub sources can be added by
|
|
1348
|
+
* widening it into a discriminated union without churning callers.
|
|
1349
|
+
*/
|
|
1350
|
+
const webhookSourceSchema = z.object({
|
|
1351
|
+
kind: z.literal("github-app-installation"),
|
|
1352
|
+
installationId: z.number().int(),
|
|
1353
|
+
organizationId: z.string().min(1)
|
|
1354
|
+
});
|
|
1355
|
+
const involveReasonSchema = z.enum([
|
|
1356
|
+
"mentioned",
|
|
1357
|
+
"review_requested",
|
|
1358
|
+
"assigned"
|
|
1359
|
+
]);
|
|
1360
|
+
const normalizedEventKindSchema = z.enum([
|
|
1361
|
+
"opened",
|
|
1362
|
+
"edited",
|
|
1363
|
+
"closed",
|
|
1364
|
+
"merged",
|
|
1365
|
+
"reopened",
|
|
1366
|
+
"commented",
|
|
1367
|
+
"review_requested",
|
|
1368
|
+
"reviewed",
|
|
1369
|
+
"review_comment",
|
|
1370
|
+
"synchronized",
|
|
1371
|
+
"commit_commented",
|
|
1372
|
+
"assigned",
|
|
1373
|
+
"other"
|
|
1374
|
+
]);
|
|
1375
|
+
const normalizedEntitySchema = z.object({
|
|
1376
|
+
type: githubEntityTypeSchema,
|
|
1377
|
+
repo: z.string().min(1),
|
|
1378
|
+
key: z.string().min(1),
|
|
1379
|
+
title: z.string().optional(),
|
|
1380
|
+
url: z.string().optional()
|
|
1381
|
+
});
|
|
1382
|
+
const normalizedActorSchema = z.object({
|
|
1383
|
+
githubLogin: z.string().min(1),
|
|
1384
|
+
isBot: z.boolean()
|
|
1385
|
+
});
|
|
1386
|
+
const normalizedInvolveSchema = z.object({
|
|
1387
|
+
githubLogin: z.string().min(1),
|
|
1388
|
+
reason: involveReasonSchema
|
|
1389
|
+
});
|
|
1390
|
+
const normalizedSurfaceSchema = z.object({
|
|
1391
|
+
title: z.string(),
|
|
1392
|
+
body: z.string(),
|
|
1393
|
+
url: z.string()
|
|
1394
|
+
});
|
|
1395
|
+
const normalizedRelatedRefSchema = z.object({
|
|
1396
|
+
type: z.literal("issue"),
|
|
1397
|
+
key: z.string().min(1)
|
|
1398
|
+
});
|
|
1399
|
+
z.object({
|
|
1400
|
+
source: webhookSourceSchema,
|
|
1401
|
+
deliveryId: z.string().nullable(),
|
|
1402
|
+
rawEventType: z.string().min(1),
|
|
1403
|
+
rawAction: z.string().nullable(),
|
|
1404
|
+
entity: normalizedEntitySchema,
|
|
1405
|
+
actor: normalizedActorSchema,
|
|
1406
|
+
kind: normalizedEventKindSchema,
|
|
1407
|
+
involves: z.array(normalizedInvolveSchema),
|
|
1408
|
+
surface: normalizedSurfaceSchema,
|
|
1409
|
+
relatedRefs: z.array(normalizedRelatedRefSchema)
|
|
1410
|
+
});
|
|
1411
|
+
const githubEventCardReasonSchema = z.enum([
|
|
1412
|
+
"mentioned",
|
|
1413
|
+
"review_requested",
|
|
1414
|
+
"assigned",
|
|
1415
|
+
"subscribed"
|
|
1416
|
+
]);
|
|
1417
|
+
z.object({
|
|
1418
|
+
type: z.literal("github_event"),
|
|
1419
|
+
reason: githubEventCardReasonSchema,
|
|
1420
|
+
event: z.string().min(1),
|
|
1421
|
+
action: z.string().nullable(),
|
|
1422
|
+
kind: normalizedEventKindSchema,
|
|
1423
|
+
repository: z.string(),
|
|
1424
|
+
sender: z.string(),
|
|
1425
|
+
title: z.string(),
|
|
1426
|
+
body: z.string(),
|
|
1427
|
+
url: z.string(),
|
|
1428
|
+
entity: z.object({
|
|
1429
|
+
type: githubEntityTypeSchema,
|
|
1430
|
+
key: z.string().min(1),
|
|
1431
|
+
url: z.string().nullable()
|
|
1432
|
+
}),
|
|
1433
|
+
mentionedUser: z.string().optional()
|
|
1434
|
+
});
|
|
1435
|
+
const NOTIFICATION_TYPES = {
|
|
1436
|
+
AGENT_ERROR: "agent_error",
|
|
1437
|
+
AGENT_BLOCKED: "agent_blocked",
|
|
1438
|
+
AGENT_STALE: "agent_stale"
|
|
1439
|
+
};
|
|
1290
1440
|
const notificationTypeSchema = z.enum([
|
|
1291
1441
|
"agent_error",
|
|
1292
|
-
"session_error",
|
|
1293
|
-
"agent_needs_decision",
|
|
1294
1442
|
"agent_blocked",
|
|
1295
|
-
"agent_stale"
|
|
1296
|
-
"agent_disconnected",
|
|
1297
|
-
"agent_connected",
|
|
1298
|
-
"session_completed"
|
|
1443
|
+
"agent_stale"
|
|
1299
1444
|
]);
|
|
1300
1445
|
const notificationSeveritySchema = z.enum([
|
|
1301
1446
|
"high",
|
|
@@ -1430,12 +1575,6 @@ const orgContextTreeOutputSchema = z.object({
|
|
|
1430
1575
|
repo: z.string().optional(),
|
|
1431
1576
|
branch: z.string().optional()
|
|
1432
1577
|
});
|
|
1433
|
-
const orgGithubIntegrationStorageSchema = z.object({ webhookSecretCipher: z.string().optional() });
|
|
1434
|
-
const orgGithubIntegrationInputSchema = z.object({ webhookSecret: z.string().min(1).nullish() });
|
|
1435
|
-
const orgGithubIntegrationOutputSchema = z.object({
|
|
1436
|
-
webhookSecretConfigured: z.boolean(),
|
|
1437
|
-
webhookUrl: z.string()
|
|
1438
|
-
});
|
|
1439
1578
|
const orgSourceReposStorageSchema = z.object({ repos: z.array(z.object({
|
|
1440
1579
|
url: repoUrlSchema,
|
|
1441
1580
|
defaultBranch: z.string().optional()
|
|
@@ -1455,12 +1594,6 @@ const ORG_SETTINGS_NAMESPACES = {
|
|
|
1455
1594
|
output: orgContextTreeOutputSchema,
|
|
1456
1595
|
readPolicy: "member"
|
|
1457
1596
|
},
|
|
1458
|
-
github_integration: {
|
|
1459
|
-
storage: orgGithubIntegrationStorageSchema,
|
|
1460
|
-
input: orgGithubIntegrationInputSchema,
|
|
1461
|
-
output: orgGithubIntegrationOutputSchema,
|
|
1462
|
-
readPolicy: "admin"
|
|
1463
|
-
},
|
|
1464
1597
|
source_repos: {
|
|
1465
1598
|
storage: orgSourceReposStorageSchema,
|
|
1466
1599
|
input: orgSourceReposInputSchema,
|
|
@@ -1678,15 +1811,6 @@ const sessionEventMessageSchema = z.object({
|
|
|
1678
1811
|
chatId: z.string(),
|
|
1679
1812
|
event: sessionEventSchema
|
|
1680
1813
|
});
|
|
1681
|
-
/**
|
|
1682
|
-
* WS control message: client signals that a query completed end-to-end.
|
|
1683
|
-
* Decoupled from `session:event` so the `session_completed` notification
|
|
1684
|
-
* fires on actual result forwarding, not on incidental tool activity.
|
|
1685
|
-
*/
|
|
1686
|
-
const sessionCompletionMessageSchema = z.object({
|
|
1687
|
-
agentId: z.string(),
|
|
1688
|
-
chatId: z.string()
|
|
1689
|
-
});
|
|
1690
1814
|
/** Client → server: list locally-held chatIds; server replies with the subset to drop. */
|
|
1691
1815
|
const sessionReconcileRequestSchema = z.object({
|
|
1692
1816
|
type: z.literal("session:reconcile"),
|
|
@@ -1754,4 +1878,4 @@ z.object({
|
|
|
1754
1878
|
capabilities: serverCapabilitiesSchema.optional()
|
|
1755
1879
|
}).passthrough();
|
|
1756
1880
|
//#endregion
|
|
1757
|
-
export {
|
|
1881
|
+
export { notificationQuerySchema as $, createMemberSchema as A, githubDevCallbackQuerySchema as B, connectTokenExchangeSchema as C, updateChatSchema as Ct, createAgentSchema as D, wsAuthFrameSchema as Dt, createAdapterMappingSchema as E, updateOrganizationSchema as Et, dryRunAgentRuntimeConfigSchema as F, inboxPollQuerySchema as G, imageInlineContentSchema as H, extractMentions as I, isReservedAgentName as J, isOrgSettingNamespace as K, githubAppInstallationClaimBodySchema as L, defaultParticipantMode as M, defaultRuntimeConfigPayload as N, createChatSchema as O, delegateFeishuUserSchema as P, messageSourceSchema as Q, githubAppInstallationPermissionsSchema as R, clientRegisterSchema as S, updateAgentSchema as St, createAdapterConfigSchema as T, updateMemberSchema as Tt, inboxAckFrameSchema as U, githubStartQuerySchema as V, inboxDeliverFrameSchema as W, listMeChatsQuerySchema as X, joinByInvitationSchema as Y, loginSchema as Z, agentPinnedMessageSchema as _, sessionStateMessageSchema as _t, AGENT_TYPES as a, questionMessageContentSchema as at, chatMetadataSchema as b, updateAdapterConfigSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, runtimeStateMessageSchema as ct, NOTIFICATION_TYPES as d, selfServiceFeishuBotSchema as dt, onboardingEventSchema as et, ORG_SETTINGS_NAMESPACES as f, sendMessageSchema as ft, agentBindRequestSchema as g, sessionReconcileRequestSchema as gt, addParticipantSchema as h, sessionEventSchema as ht, AGENT_STATUSES as i, questionAnswerMessageContentSchema as it, createOrgFromMeSchema as j, createMeChatSchema as k, LIVE_ACTIVITY_STALE_MS as l, safeRedirectPath as lt, addMeChatParticipantsSchema as m, sessionEventMessageSchema as mt, AGENT_NAME_REGEX as n, patchChatEngagementSchema as nt, AGENT_VISIBILITY as o, rebindAgentSchema as ot, WS_AUTH_FRAME_TIMEOUT_MS as p, sendToAgentSchema as pt, isRedactedEnvValue as q, AGENT_SELECTOR_HEADER as r, patchOnboardingSchema as rt, CHAT_ENGAGEMENT_STATUSES as s, refreshTokenSchema as st, AGENT_BIND_REJECT_REASONS as t, paginationQuerySchema as tt, MENTION_REGEX as u, scanMentionTokens as ut, agentRuntimeConfigPayloadSchema as v, stripCode as vt, contextTreeSnapshotSchema as w, updateClientCapabilitiesSchema as wt, clientCapabilitiesSchema as x, updateAgentRuntimeConfigSchema as xt, agentTypeSchema as y, submitQuestionAnswerSchema as yt, githubCallbackQuerySchema as z };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- DB-backed deduplication for the notifications table.
|
|
2
|
+
--
|
|
3
|
+
-- Before this migration, the server kept an in-memory Map keyed by
|
|
4
|
+
-- `(agentId, notificationType)` to suppress duplicate notifications within a
|
|
5
|
+
-- five-minute window. That broke in two important ways:
|
|
6
|
+
--
|
|
7
|
+
-- 1. Multi-instance deployments: each server process owned its own Map, so
|
|
8
|
+
-- a notification produced on instance A passed instance B's dedupe gate
|
|
9
|
+
-- unchanged. Operators saw the same row inserted multiple times during
|
|
10
|
+
-- load-balancer failover or rolling restart.
|
|
11
|
+
-- 2. Process restart wiped the Map, re-opening the dedupe window for
|
|
12
|
+
-- whatever events had fired just before.
|
|
13
|
+
--
|
|
14
|
+
-- The new model is purely DB-driven. A producer optionally supplies a
|
|
15
|
+
-- `dedup_key` (e.g. `agent:{uuid}:error`, `chat:{uuid}:completed`). The
|
|
16
|
+
-- partial unique index below scopes uniqueness to `(organization_id,
|
|
17
|
+
-- dedup_key)` *while the prior row is still unread*. Re-emitting the same
|
|
18
|
+
-- key while the previous notification sits unread is a no-op (the
|
|
19
|
+
-- application uses `ON CONFLICT DO NOTHING`); after the user acknowledges
|
|
20
|
+
-- the prior row, a fresh notification can land again.
|
|
21
|
+
--
|
|
22
|
+
-- Producers without a `dedup_key` keep the legacy always-insert behaviour,
|
|
23
|
+
-- so this column is purely additive — no back-fill, no NOT NULL constraint.
|
|
24
|
+
|
|
25
|
+
ALTER TABLE "notifications" ADD COLUMN "dedup_key" text;
|
|
26
|
+
|
|
27
|
+
CREATE UNIQUE INDEX "uq_notifications_org_dedup_unread"
|
|
28
|
+
ON "notifications" ("organization_id", "dedup_key")
|
|
29
|
+
WHERE read = false AND dedup_key IS NOT NULL;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Drop notification rows whose `type` is no longer in the shared schema.
|
|
2
|
+
--
|
|
3
|
+
-- Three types were removed in earlier work but their rows were left behind:
|
|
4
|
+
--
|
|
5
|
+
-- * `agent_disconnected` — producer deleted by PR #348 (the
|
|
6
|
+
-- systemctl-restart spam fix), schema enum entry deleted later. 37 rows
|
|
7
|
+
-- still in dev hub at the time of writing, with no UI label and no
|
|
8
|
+
-- click target.
|
|
9
|
+
-- * `session_completed` — removed end-to-end in this PR (was 56% of recent
|
|
10
|
+
-- notifications, duplicating the conversation list's "latest message"
|
|
11
|
+
-- signal). Without the cleanup, the bell would keep surfacing them
|
|
12
|
+
-- until each org's user marks them all read manually.
|
|
13
|
+
-- * `session_error` — long-dead type: schema entry + UI label existed,
|
|
14
|
+
-- but no producer ever wrote one. Cleanup is defensive — any stray
|
|
15
|
+
-- rows from a hand-written test or prod-DB experiment go.
|
|
16
|
+
--
|
|
17
|
+
-- `notifications.type` is a `text` column (not a PG enum), so this is a
|
|
18
|
+
-- pure data-cleanup migration — no schema change, no type-cast risk.
|
|
19
|
+
--
|
|
20
|
+
-- The unread-only branch handles the transition window for rows still on
|
|
21
|
+
-- the previous per-type dedup key (`agent:{id}:agent_error|blocked|stale`).
|
|
22
|
+
-- They co-exist with new `agent:{id}:fault` rows for the same agent until
|
|
23
|
+
-- one or the other is marked read, which is exactly the redundancy this PR
|
|
24
|
+
-- is fixing — so wipe them too. Read rows on those keys stay (history).
|
|
25
|
+
|
|
26
|
+
DELETE FROM "notifications"
|
|
27
|
+
WHERE "type" IN ('agent_disconnected', 'session_completed', 'session_error');
|
|
28
|
+
|
|
29
|
+
DELETE FROM "notifications"
|
|
30
|
+
WHERE "read" = false
|
|
31
|
+
AND "dedup_key" IN (
|
|
32
|
+
SELECT "dedup_key" FROM "notifications"
|
|
33
|
+
WHERE "dedup_key" LIKE 'agent:%:agent_error'
|
|
34
|
+
OR "dedup_key" LIKE 'agent:%:agent_blocked'
|
|
35
|
+
OR "dedup_key" LIKE 'agent:%:agent_stale'
|
|
36
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- Onboarding terminal-state stamp. Distinct from `onboarding_dismissed_at`:
|
|
2
|
+
--
|
|
3
|
+
-- * `onboarding_dismissed_at` means "the user clicked ✕ on the stepper" —
|
|
4
|
+
-- a UI hide, not a setup-complete signal. The user can resume the
|
|
5
|
+
-- wizard from Settings → Onboarding to land back on whichever step is
|
|
6
|
+
-- still incomplete.
|
|
7
|
+
--
|
|
8
|
+
-- * `onboarding_completed_at` means "the user actually walked Step 3 to
|
|
9
|
+
-- success" (admin Continue, invitee Confirm/Continue). Once set, the
|
|
10
|
+
-- Settings → Onboarding entry point and Resume button disappear
|
|
11
|
+
-- permanently. Subsequent tree / source-repo edits go through Settings
|
|
12
|
+
-- → Team and /agents/:uuid.
|
|
13
|
+
--
|
|
14
|
+
-- The two stamps are intentionally orthogonal: `inferOnboardingStep()`
|
|
15
|
+
-- keeps its existing "infer from current resources" semantics and never
|
|
16
|
+
-- consults this column. This field is UI-gate only.
|
|
17
|
+
--
|
|
18
|
+
-- Backfill: every already-dismissed user is treated as completed. The pre-
|
|
19
|
+
-- column population (mid-2025) had no terminal-state concept, so the only
|
|
20
|
+
-- signal we have is "they hid the stepper" — and the alternative (showing
|
|
21
|
+
-- the Onboarding sidebar entry to every legacy user forever) is worse than
|
|
22
|
+
-- the off-by-one risk that a few users dismissed-without-finishing and
|
|
23
|
+
-- will lose their Resume affordance.
|
|
24
|
+
|
|
25
|
+
ALTER TABLE "users"
|
|
26
|
+
ADD COLUMN IF NOT EXISTS "onboarding_completed_at" timestamp with time zone;
|
|
27
|
+
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
UPDATE "users"
|
|
30
|
+
SET "onboarding_completed_at" = "onboarding_dismissed_at"
|
|
31
|
+
WHERE "onboarding_dismissed_at" IS NOT NULL
|
|
32
|
+
AND "onboarding_completed_at" IS NULL;
|
|
@@ -288,6 +288,27 @@
|
|
|
288
288
|
"when": 1779235200000,
|
|
289
289
|
"tag": "0040_chat_user_state_engagement",
|
|
290
290
|
"breakpoints": true
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"idx": 41,
|
|
294
|
+
"version": "7",
|
|
295
|
+
"when": 1779321600000,
|
|
296
|
+
"tag": "0041_notifications_dedup_key",
|
|
297
|
+
"breakpoints": true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"idx": 42,
|
|
301
|
+
"version": "7",
|
|
302
|
+
"when": 1779408000000,
|
|
303
|
+
"tag": "0042_notifications_drop_legacy_types",
|
|
304
|
+
"breakpoints": true
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"idx": 43,
|
|
308
|
+
"version": "7",
|
|
309
|
+
"when": 1779494400000,
|
|
310
|
+
"tag": "0043_onboarding_completed_at",
|
|
311
|
+
"breakpoints": true
|
|
291
312
|
}
|
|
292
313
|
]
|
|
293
314
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
-
//#region ../server/dist/errors-
|
|
2
|
+
//#region ../server/dist/errors-LPcARA4K.mjs
|
|
3
3
|
/** Organization entity. Agents and chats belong to exactly one organization. */
|
|
4
4
|
const organizations = pgTable("organizations", {
|
|
5
5
|
id: text("id").primaryKey(),
|
|
@@ -20,6 +20,7 @@ const users = pgTable("users", {
|
|
|
20
20
|
avatarUrl: text("avatar_url"),
|
|
21
21
|
status: text("status").notNull().default("active"),
|
|
22
22
|
onboardingDismissedAt: timestamp("onboarding_dismissed_at", { withTimezone: true }),
|
|
23
|
+
onboardingCompletedAt: timestamp("onboarding_completed_at", { withTimezone: true }),
|
|
23
24
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
24
25
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
25
26
|
});
|