@opengeni/db 4.4.1 → 4.5.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/canonical-human-identities.js +3 -3
- package/dist/capability-reconciliation.js +2 -2
- package/dist/{chunk-T7GDRZOH.js → chunk-4LVV4R4F.js} +2 -2
- package/dist/{chunk-NQWVBQFK.js → chunk-575FUJE2.js} +3 -3
- package/dist/{chunk-K2XOMTAG.js → chunk-7LFHUC35.js} +2 -2
- package/dist/{chunk-JC43DLHB.js → chunk-7P67EF5K.js} +5 -5
- package/dist/{chunk-XWANFAN4.js → chunk-BH2HBICR.js} +4 -4
- package/dist/{chunk-PB73JQOI.js → chunk-C3FI3DJ5.js} +2 -2
- package/dist/{chunk-LLQLCFCT.js → chunk-GSZZDYPV.js} +2 -2
- package/dist/{chunk-B4DWUU4Y.js → chunk-GYAQYECK.js} +2 -2
- package/dist/{chunk-GYHXV2LR.js → chunk-K2YN562I.js} +2 -2
- package/dist/{chunk-6UJX2OXO.js → chunk-KMBU4KDY.js} +25 -1
- package/dist/chunk-KMBU4KDY.js.map +1 -0
- package/dist/{chunk-JN2DIKGW.js → chunk-M6TNKUGE.js} +3 -1
- package/dist/chunk-M6TNKUGE.js.map +1 -0
- package/dist/{chunk-SSCUP23X.js → chunk-MSAWMGWJ.js} +6 -6
- package/dist/{chunk-T5RZ3HWS.js → chunk-OFQDECMZ.js} +4 -4
- package/dist/{chunk-7UA4WUGK.js → chunk-RRZO5XRA.js} +2671 -2335
- package/dist/{chunk-7UA4WUGK.js.map → chunk-RRZO5XRA.js.map} +1 -1
- package/dist/{chunk-72JNABPO.js → chunk-TIF52FMJ.js} +3 -3
- package/dist/{chunk-67VNEM5N.js → chunk-TLCTS7CZ.js} +4 -4
- package/dist/{chunk-VFQI3CDL.js → chunk-VXCIFQX7.js} +2 -2
- package/dist/{chunk-5H6FHFHD.js → chunk-XX5M3TJJ.js} +2 -2
- package/dist/{chunk-LUO43IZE.js → chunk-YOFYG5ZI.js} +2 -2
- package/dist/connect-attempts.js +3 -3
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +21 -0
- package/dist/index.js +25 -19
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.js +19 -19
- package/dist/organization-integration-policy.js +3 -3
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +6 -6
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +93 -0
- package/dist/schema.js +3 -1
- package/dist/session-background-commands.js +5 -5
- package/dist/session-command-output.js +5 -5
- package/dist/session-event-slices.js +3 -3
- package/dist/session-history-tool-identities.d.ts +11 -0
- package/dist/session-mcp-credential-rotation.js +6 -6
- package/dist/session-message-search.d.ts +21 -0
- package/dist/session-tenancy.js +6 -6
- package/dist/{subscription-core-codex-SUO2TISD.js → subscription-core-codex-L7NPFWAH.js} +6 -6
- package/dist/{subscription-core-codex-compat-SDHM4XG4.js → subscription-core-codex-compat-AK2UMLE3.js} +8 -8
- package/dist/subscription-usage-pool.d.ts +1 -1
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +13 -0
- package/dist/workspace-tool-defaults.js +5 -5
- package/drizzle/0907_subscription_connection_one_owner.sql +339 -209
- package/drizzle/0908_site_opens.sql +33 -0
- package/package.json +9 -9
- package/src/index.ts +304 -44
- package/src/runtime-posture.ts +2 -0
- package/src/schema.ts +24 -0
- package/src/session-history-tool-identities.ts +55 -0
- package/src/session-message-search.ts +188 -33
- package/src/subscription-core/administered-connections.ts +7 -5
- package/src/subscription-usage-pool.ts +7 -5
- package/src/workspace-artifacts.ts +45 -1
- package/dist/chunk-6UJX2OXO.js.map +0 -1
- package/dist/chunk-JN2DIKGW.js.map +0 -1
- /package/dist/{chunk-T7GDRZOH.js.map → chunk-4LVV4R4F.js.map} +0 -0
- /package/dist/{chunk-NQWVBQFK.js.map → chunk-575FUJE2.js.map} +0 -0
- /package/dist/{chunk-K2XOMTAG.js.map → chunk-7LFHUC35.js.map} +0 -0
- /package/dist/{chunk-JC43DLHB.js.map → chunk-7P67EF5K.js.map} +0 -0
- /package/dist/{chunk-XWANFAN4.js.map → chunk-BH2HBICR.js.map} +0 -0
- /package/dist/{chunk-PB73JQOI.js.map → chunk-C3FI3DJ5.js.map} +0 -0
- /package/dist/{chunk-LLQLCFCT.js.map → chunk-GSZZDYPV.js.map} +0 -0
- /package/dist/{chunk-B4DWUU4Y.js.map → chunk-GYAQYECK.js.map} +0 -0
- /package/dist/{chunk-GYHXV2LR.js.map → chunk-K2YN562I.js.map} +0 -0
- /package/dist/{chunk-SSCUP23X.js.map → chunk-MSAWMGWJ.js.map} +0 -0
- /package/dist/{chunk-T5RZ3HWS.js.map → chunk-OFQDECMZ.js.map} +0 -0
- /package/dist/{chunk-72JNABPO.js.map → chunk-TIF52FMJ.js.map} +0 -0
- /package/dist/{chunk-67VNEM5N.js.map → chunk-TLCTS7CZ.js.map} +0 -0
- /package/dist/{chunk-VFQI3CDL.js.map → chunk-VXCIFQX7.js.map} +0 -0
- /package/dist/{chunk-5H6FHFHD.js.map → chunk-XX5M3TJJ.js.map} +0 -0
- /package/dist/{chunk-LUO43IZE.js.map → chunk-YOFYG5ZI.js.map} +0 -0
- /package/dist/{subscription-core-codex-SUO2TISD.js.map → subscription-core-codex-L7NPFWAH.js.map} +0 -0
- /package/dist/{subscription-core-codex-compat-SDHM4XG4.js.map → subscription-core-codex-compat-AK2UMLE3.js.map} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- When each person last opened a Site in the console. It orders that
|
|
3
|
+
-- person's Site rows in the sidebar like session activity and grants
|
|
4
|
+
-- nothing: the Site's own audience still decides who can see it. New code
|
|
5
|
+
-- owns the table; old binaries never read it.
|
|
6
|
+
CREATE TABLE site_opens (
|
|
7
|
+
account_id uuid NOT NULL,
|
|
8
|
+
workspace_id uuid NOT NULL,
|
|
9
|
+
subject_id text NOT NULL CHECK (length(subject_id) BETWEEN 1 AND 512),
|
|
10
|
+
site_id uuid NOT NULL,
|
|
11
|
+
opened_at timestamptz NOT NULL DEFAULT now(),
|
|
12
|
+
PRIMARY KEY (workspace_id, subject_id, site_id),
|
|
13
|
+
FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
|
|
14
|
+
FOREIGN KEY (workspace_id, site_id) REFERENCES workspace_artifacts(workspace_id, id) ON DELETE CASCADE
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX site_opens_site_idx ON site_opens(workspace_id, site_id);
|
|
17
|
+
ALTER TABLE site_opens ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
ALTER TABLE site_opens FORCE ROW LEVEL SECURITY;
|
|
19
|
+
-- Personal like session pins: only the opening subject reads or writes a row.
|
|
20
|
+
CREATE POLICY workspace_isolation ON site_opens
|
|
21
|
+
USING (
|
|
22
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
23
|
+
AND subject_id = opengeni_private.current_subject_id()
|
|
24
|
+
)
|
|
25
|
+
WITH CHECK (
|
|
26
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
27
|
+
AND subject_id = opengeni_private.current_subject_id()
|
|
28
|
+
);
|
|
29
|
+
DO $$ BEGIN
|
|
30
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
31
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON site_opens TO opengeni_app;
|
|
32
|
+
END IF;
|
|
33
|
+
END $$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Opengeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -121,14 +121,14 @@
|
|
|
121
121
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@opengeni/codemode": "4.
|
|
125
|
-
"@opengeni/codex": "4.
|
|
126
|
-
"@opengeni/config": "4.
|
|
127
|
-
"@opengeni/contracts": "4.
|
|
128
|
-
"@opengeni/network": "4.
|
|
129
|
-
"@opengeni/observability": "4.
|
|
130
|
-
"@opengeni/subscriptions": "4.
|
|
131
|
-
"@opengeni/xai-subscription": "4.
|
|
124
|
+
"@opengeni/codemode": "4.5.0",
|
|
125
|
+
"@opengeni/codex": "4.5.0",
|
|
126
|
+
"@opengeni/config": "4.5.0",
|
|
127
|
+
"@opengeni/contracts": "4.5.0",
|
|
128
|
+
"@opengeni/network": "4.5.0",
|
|
129
|
+
"@opengeni/observability": "4.5.0",
|
|
130
|
+
"@opengeni/subscriptions": "4.5.0",
|
|
131
|
+
"@opengeni/xai-subscription": "4.5.0",
|
|
132
132
|
"drizzle-orm": "^0.45.2",
|
|
133
133
|
"postgres": "^3.4.7"
|
|
134
134
|
},
|
package/src/index.ts
CHANGED
|
@@ -783,6 +783,7 @@ import type {
|
|
|
783
783
|
SessionMemoryScope,
|
|
784
784
|
SessionListResponse,
|
|
785
785
|
SessionListTotals,
|
|
786
|
+
SiteConversationGroup,
|
|
786
787
|
SessionListEntryResponse,
|
|
787
788
|
SessionTenancyPublicProjection,
|
|
788
789
|
SessionEvent,
|
|
@@ -1057,6 +1058,7 @@ import {
|
|
|
1057
1058
|
historyItemType,
|
|
1058
1059
|
TOOL_RESULT_TYPE_BY_CALL_TYPE,
|
|
1059
1060
|
} from "./session-tool-call-settlement";
|
|
1061
|
+
import { readSessionHistoryToolIdentityPage } from "./session-history-tool-identities";
|
|
1060
1062
|
import {
|
|
1061
1063
|
assertAgentCommandAuthorityInTransaction,
|
|
1062
1064
|
canonicalSessionCommandHash,
|
|
@@ -29996,6 +29998,37 @@ function wellFormedDriverText(value: unknown): unknown {
|
|
|
29996
29998
|
return Array.isArray(value) ? value.map(wellFormedDriverText) : value;
|
|
29997
29999
|
}
|
|
29998
30000
|
|
|
30001
|
+
/**
|
|
30002
|
+
* A new Site conversation is filed in the project of the chat that created the
|
|
30003
|
+
* Site (the tree of its first version's session), so the Site's sidebar row and
|
|
30004
|
+
* its conversations sit together. Without a Site origin, or a creating chat,
|
|
30005
|
+
* the session stays unfiled.
|
|
30006
|
+
*/
|
|
30007
|
+
async function siteHomeChannelId(
|
|
30008
|
+
tx: Database,
|
|
30009
|
+
workspaceId: string,
|
|
30010
|
+
metadata: Record<string, unknown>,
|
|
30011
|
+
): Promise<string | null> {
|
|
30012
|
+
const origin = metadata._opengeniSiteOrigin;
|
|
30013
|
+
const siteId =
|
|
30014
|
+
origin && typeof origin === "object" ? (origin as { siteId?: unknown }).siteId : undefined;
|
|
30015
|
+
if (typeof siteId !== "string" || !z.string().uuid().safeParse(siteId).success) return null;
|
|
30016
|
+
const [home] = await rawRows<{ channelId: string | null }>(
|
|
30017
|
+
tx,
|
|
30018
|
+
sql`
|
|
30019
|
+
select root.channel_id::text as "channelId"
|
|
30020
|
+
from ${schema.workspaceArtifactVersions} version
|
|
30021
|
+
join ${schema.sessions} source
|
|
30022
|
+
on source.workspace_id = version.workspace_id and source.id = version.source_session_id
|
|
30023
|
+
join ${schema.sessions} root
|
|
30024
|
+
on root.workspace_id = source.workspace_id and root.id = source.root_session_id
|
|
30025
|
+
where version.workspace_id = ${workspaceId} and version.artifact_id = ${siteId}::uuid
|
|
30026
|
+
and version.revision = 1 and root.site_access_root_id is null
|
|
30027
|
+
limit 1`,
|
|
30028
|
+
);
|
|
30029
|
+
return home?.channelId ?? null;
|
|
30030
|
+
}
|
|
30031
|
+
|
|
29999
30032
|
async function createSessionInTransaction(
|
|
30000
30033
|
tx: SessionActivityDatabase,
|
|
30001
30034
|
input: SessionCreateInput,
|
|
@@ -30285,6 +30318,10 @@ async function createSessionInTransaction(
|
|
|
30285
30318
|
input.workspaceId,
|
|
30286
30319
|
parseAcceptedMcpAccountBindings(input.mcpAccountBindings),
|
|
30287
30320
|
);
|
|
30321
|
+
const channelId =
|
|
30322
|
+
input.channelId === undefined && !input.parentSessionId
|
|
30323
|
+
? await siteHomeChannelId(tx as unknown as Database, input.workspaceId, sessionMetadata)
|
|
30324
|
+
: (input.channelId ?? null);
|
|
30288
30325
|
try {
|
|
30289
30326
|
const insertValues: typeof schema.sessions.$inferInsert = withLosslessContentWriteVersion(
|
|
30290
30327
|
{
|
|
@@ -30321,7 +30358,7 @@ async function createSessionInTransaction(
|
|
|
30321
30358
|
variableSetId,
|
|
30322
30359
|
rigId: input.rigId ?? null,
|
|
30323
30360
|
rigVersionId: input.rigVersionId ?? null,
|
|
30324
|
-
channelId
|
|
30361
|
+
channelId,
|
|
30325
30362
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
30326
30363
|
firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
|
|
30327
30364
|
initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
@@ -32021,6 +32058,13 @@ export type SessionListFilterOptions = {
|
|
|
32021
32058
|
siteAccessId?: string;
|
|
32022
32059
|
/** Sessions created through this published Site, independent of project. */
|
|
32023
32060
|
originSiteId?: string;
|
|
32061
|
+
/**
|
|
32062
|
+
* Ordinary discovery omits conversations that run inside a Site. `include`
|
|
32063
|
+
* lists them as ordinary rows (search, pins, branches); `group` keeps them
|
|
32064
|
+
* out of root rows but counts them in totals and returns `siteGroups`.
|
|
32065
|
+
* Presentation only: native ownership and Site RLS still decide access.
|
|
32066
|
+
*/
|
|
32067
|
+
siteConversations?: "include" | "group";
|
|
32024
32068
|
/** Exact workspace project; null selects unfiled sessions. */
|
|
32025
32069
|
channelId?: string | null;
|
|
32026
32070
|
/** Exact frozen creator identity. */
|
|
@@ -33037,12 +33081,33 @@ async function sessionListTotalsInScope(
|
|
|
33037
33081
|
db: Database,
|
|
33038
33082
|
workspaceId: string,
|
|
33039
33083
|
options: ListSessionsForSubjectOptions,
|
|
33040
|
-
|
|
33084
|
+
withSiteRows = false,
|
|
33085
|
+
): Promise<SessionListTotals & { sites: SiteConversationGroup[]; sitesTruncated: boolean }> {
|
|
33041
33086
|
const archiveStatus = options.archiveStatus ?? (options.archivedOnly ? "archived" : "active");
|
|
33087
|
+
// A grouped rail counts Site conversations in its project totals and gets
|
|
33088
|
+
// their Site rows here; its ordinary rows still omit them.
|
|
33089
|
+
const siteConversations = options.siteConversations ? "include" : undefined;
|
|
33090
|
+
// Sites this person opened join the rows only in the plain view: a root
|
|
33091
|
+
// filter (Mine, search, creator, dates, archived content, another person's
|
|
33092
|
+
// scope) or an agent caller lists only Sites its listed chats created.
|
|
33093
|
+
const withOpenedSites =
|
|
33094
|
+
withSiteRows &&
|
|
33095
|
+
!options.needsYouOnly &&
|
|
33096
|
+
!options.mineOnly &&
|
|
33097
|
+
!options.search?.trim() &&
|
|
33098
|
+
!options.contentArchivedOnly &&
|
|
33099
|
+
!options.scopeSubjectId &&
|
|
33100
|
+
!options.createdBy &&
|
|
33101
|
+
!options.updatedFrom &&
|
|
33102
|
+
!options.updatedBefore &&
|
|
33103
|
+
!options.createdFrom &&
|
|
33104
|
+
!options.createdBefore &&
|
|
33105
|
+
!options.authorizationScope?.agentAccessViewer;
|
|
33042
33106
|
const rootFilters = and(
|
|
33043
33107
|
eq(schema.sessions.workspaceId, workspaceId),
|
|
33044
33108
|
...sessionFilters({
|
|
33045
33109
|
...options,
|
|
33110
|
+
...(siteConversations ? { siteConversations } : {}),
|
|
33046
33111
|
parentSessionId: null,
|
|
33047
33112
|
needsYouOnly: false,
|
|
33048
33113
|
archiveStatus: "all",
|
|
@@ -33051,7 +33116,11 @@ async function sessionListTotalsInScope(
|
|
|
33051
33116
|
? []
|
|
33052
33117
|
: [sql`coalesce(root_archive.archived, false) = ${archiveStatus === "archived"}`]),
|
|
33053
33118
|
);
|
|
33054
|
-
const rows = await rawRows<{
|
|
33119
|
+
const rows = await rawRows<{
|
|
33120
|
+
needsYouCount: number;
|
|
33121
|
+
groups: SessionListTotals["groups"];
|
|
33122
|
+
sites: SiteConversationGroup[] | null;
|
|
33123
|
+
}>(
|
|
33055
33124
|
db,
|
|
33056
33125
|
sql`
|
|
33057
33126
|
-- Inline this subject's state so each lateral lookup can use the session
|
|
@@ -33072,6 +33141,11 @@ async function sessionListTotalsInScope(
|
|
|
33072
33141
|
${schema.sessions.activeTurnId} as active_turn_id,
|
|
33073
33142
|
${schema.sessions.inputWaitUntil} as input_wait_until,
|
|
33074
33143
|
${schema.sessions.inputWaitTurnId} as input_wait_turn_id,
|
|
33144
|
+
${schema.sessions.siteId} as site_id,
|
|
33145
|
+
${schema.sessions.siteAccessRootId} is not null as site_conversation,
|
|
33146
|
+
${schema.sessions.updatedAt} as updated_at,
|
|
33147
|
+
${schema.sessions.createdAt} as created_at,
|
|
33148
|
+
${schema.sessions.metadata}->'_opengeniSiteOrigin'->>'title' as origin_title,
|
|
33075
33149
|
${sessionRelatedListScope(options.authorizationScope)} as related,
|
|
33076
33150
|
coalesce(root_personal.archived, false) as archived,
|
|
33077
33151
|
coalesce(root_personal.pinned, false) as pinned,
|
|
@@ -33142,6 +33216,9 @@ async function sessionListTotalsInScope(
|
|
|
33142
33216
|
from roots root left join attention_roots attention on attention.root_id = root.id
|
|
33143
33217
|
), measured as (
|
|
33144
33218
|
select nodes.*,
|
|
33219
|
+
root.site_conversation as root_site_conversation, root.site_id as root_site_id,
|
|
33220
|
+
root.updated_at as root_updated_at, root.created_at as root_created_at,
|
|
33221
|
+
root.origin_title as root_origin_title,
|
|
33145
33222
|
cursor.last_meaningful_sequence > nodes.acknowledged_sequence as unread,
|
|
33146
33223
|
nodes.actively_working as active_work,
|
|
33147
33224
|
case when nodes.status = 'requires_action' then (
|
|
@@ -33198,21 +33275,150 @@ async function sessionListTotalsInScope(
|
|
|
33198
33275
|
count(*) filter (where unread)::int as unread,
|
|
33199
33276
|
count(*) filter (where active_work)::int as "activeWork"
|
|
33200
33277
|
from measured group by channel_id
|
|
33278
|
+
), site_conv as (
|
|
33279
|
+
-- Site conversations nest under their Site's row in their own project.
|
|
33280
|
+
-- Pinned ones live with pins.
|
|
33281
|
+
select root.*, root.channel_id as row_channel_id
|
|
33282
|
+
from roots root
|
|
33283
|
+
where ${
|
|
33284
|
+
withSiteRows
|
|
33285
|
+
? sql`root.site_conversation and root.site_id is not null and not root.pinned
|
|
33286
|
+
${
|
|
33287
|
+
options.needsYouOnly
|
|
33288
|
+
? sql`and root.id in (select id from root_attention where needs_you)`
|
|
33289
|
+
: sql``
|
|
33290
|
+
}`
|
|
33291
|
+
: sql`false`
|
|
33292
|
+
}
|
|
33293
|
+
), site_conv_totals as (
|
|
33294
|
+
select site_id, row_channel_id as channel_id, count(*)::int as conversations,
|
|
33295
|
+
max(updated_at) as updated_at, max(created_at) as created_at,
|
|
33296
|
+
(array_agg(origin_title order by updated_at desc, id desc)
|
|
33297
|
+
filter (where origin_title is not null))[1] as origin_title
|
|
33298
|
+
from site_conv group by site_id, row_channel_id
|
|
33299
|
+
), site_home as (
|
|
33300
|
+
-- A Site sits in the project of the chat that created it (the tree of
|
|
33301
|
+
-- its first version's session), as far as this viewer can see that
|
|
33302
|
+
-- chat; new Site conversations are filed there too. A Site without
|
|
33303
|
+
-- conversations here is listed when this viewer lists its creating
|
|
33304
|
+
-- chat, or opened it. Candidates are the live Sites in this viewer's
|
|
33305
|
+
-- audience (one row each); revision 1 is a unique-index lookup.
|
|
33306
|
+
select artifact.id as site_id, creator.channel_id
|
|
33307
|
+
from ${schema.workspaceArtifacts} artifact
|
|
33308
|
+
join lateral (
|
|
33309
|
+
select version.source_session_id from ${schema.workspaceArtifactVersions} version
|
|
33310
|
+
where version.workspace_id = ${workspaceId} and version.artifact_id = artifact.id
|
|
33311
|
+
and version.revision = 1
|
|
33312
|
+
) first_version on true
|
|
33313
|
+
left join ${schema.sessions} source
|
|
33314
|
+
on source.workspace_id = ${workspaceId} and source.id = first_version.source_session_id
|
|
33315
|
+
left join ${schema.sessions} creator
|
|
33316
|
+
on creator.workspace_id = ${workspaceId} and creator.id = source.root_session_id
|
|
33317
|
+
and creator.site_access_root_id is null
|
|
33318
|
+
where ${withSiteRows && !options.needsYouOnly ? sql`true` : sql`false`}
|
|
33319
|
+
and artifact.workspace_id = ${workspaceId} and artifact.status = 'active'
|
|
33320
|
+
and (artifact.visibility = 'workspace_shared'
|
|
33321
|
+
or artifact.owner_subject_id = ${options.subjectId})
|
|
33322
|
+
and artifact.id not in (select site_id from site_conv)
|
|
33323
|
+
and (exists (select 1 from roots listed where listed.id = creator.id)
|
|
33324
|
+
${
|
|
33325
|
+
withOpenedSites
|
|
33326
|
+
? sql`or exists (
|
|
33327
|
+
select 1 from ${schema.siteOpens} opened
|
|
33328
|
+
where opened.workspace_id = ${workspaceId}
|
|
33329
|
+
and opened.subject_id = ${options.subjectId} and opened.site_id = artifact.id)`
|
|
33330
|
+
: sql``
|
|
33331
|
+
})
|
|
33332
|
+
), site_keys as (
|
|
33333
|
+
select * from site_conv_totals
|
|
33334
|
+
union all
|
|
33335
|
+
select site_id, channel_id, 0, null, null, null from site_home
|
|
33336
|
+
), site_rows as (
|
|
33337
|
+
-- One row per Site and project. The title is the Site's current one when
|
|
33338
|
+
-- its audience includes this viewer (the catalog's rule; RLS only
|
|
33339
|
+
-- isolates the workspace), else the creation-time label. A row without
|
|
33340
|
+
-- conversations needs a live Site the viewer can see. Activity is the
|
|
33341
|
+
-- latest of its conversations, its own changes and this person opening
|
|
33342
|
+
-- it. One extra row reports truncation.
|
|
33343
|
+
select key.site_id as "siteId", key.channel_id as "channelId",
|
|
33344
|
+
coalesce(site.title, key.origin_title, '') as title,
|
|
33345
|
+
key.conversations,
|
|
33346
|
+
greatest(key.updated_at, site.updated_at, opened.opened_at) as "updatedAt",
|
|
33347
|
+
coalesce(key.created_at, site.created_at) as "createdAt"
|
|
33348
|
+
from site_keys key
|
|
33349
|
+
left join lateral (
|
|
33350
|
+
select artifact.title, artifact.status, artifact.created_at, artifact.updated_at
|
|
33351
|
+
from ${schema.workspaceArtifacts} artifact
|
|
33352
|
+
where artifact.workspace_id = ${workspaceId} and artifact.id = key.site_id
|
|
33353
|
+
and (artifact.visibility = 'workspace_shared'
|
|
33354
|
+
or artifact.owner_subject_id = ${options.subjectId})
|
|
33355
|
+
) site on true
|
|
33356
|
+
left join lateral (
|
|
33357
|
+
select opened.opened_at from ${schema.siteOpens} opened
|
|
33358
|
+
where opened.workspace_id = ${workspaceId} and opened.subject_id = ${options.subjectId}
|
|
33359
|
+
and opened.site_id = key.site_id
|
|
33360
|
+
) opened on true
|
|
33361
|
+
where (key.conversations > 0 or site.status = 'active')
|
|
33362
|
+
${options.channelId !== undefined ? sql`and key.channel_id is not distinct from ${options.channelId}::uuid` : sql``}
|
|
33363
|
+
order by 5 desc, 1, 2
|
|
33364
|
+
limit ${SITE_CONVERSATION_GROUP_LIMIT + 1}
|
|
33365
|
+
), site_status as (
|
|
33366
|
+
-- Status counts follow project totals: active, unpinned trees only.
|
|
33367
|
+
select conv.site_id, conv.row_channel_id, count(*)::int as total,
|
|
33368
|
+
count(*) filter (where node.status = 'requires_action')::int as attention,
|
|
33369
|
+
min(node.attention_since) as "attentionSince",
|
|
33370
|
+
count(*) filter (where node.status = 'failed' and node.unread)::int as failed,
|
|
33371
|
+
count(*) filter (where node.pause_revision is null and
|
|
33372
|
+
(node.status in ('running', 'recovering') or node.input_wait))::int as active,
|
|
33373
|
+
count(*) filter (where node.pause_revision is null and
|
|
33374
|
+
node.status in ('queued', 'waiting_capacity'))::int as queued,
|
|
33375
|
+
count(*) filter (where node.pause_revision is null and node.input_wait)::int as waiting,
|
|
33376
|
+
count(*) filter (where node.unread)::int as unread,
|
|
33377
|
+
count(*) filter (where node.active_work)::int as "activeWork"
|
|
33378
|
+
from measured node join site_conv conv on conv.id = node.root_id
|
|
33379
|
+
group by conv.site_id, conv.row_channel_id
|
|
33380
|
+
), site_grouped as (
|
|
33381
|
+
select site_rows.*, coalesce(status.total, 0) as total,
|
|
33382
|
+
coalesce(status.attention, 0) as attention, status."attentionSince",
|
|
33383
|
+
coalesce(status.failed, 0) as failed, coalesce(status.active, 0) as active,
|
|
33384
|
+
coalesce(status.queued, 0) as queued, coalesce(status.waiting, 0) as waiting,
|
|
33385
|
+
coalesce(status.unread, 0) as unread, coalesce(status."activeWork", 0) as "activeWork"
|
|
33386
|
+
from site_rows left join site_status status
|
|
33387
|
+
on status.site_id = site_rows."siteId"
|
|
33388
|
+
and status.row_channel_id is not distinct from site_rows."channelId"
|
|
33201
33389
|
)
|
|
33202
33390
|
select (select count(*)::int from root_attention where needs_you) as "needsYouCount",
|
|
33203
|
-
coalesce((select jsonb_agg(to_jsonb(grouped)) from grouped), '[]'::jsonb) as groups
|
|
33391
|
+
coalesce((select jsonb_agg(to_jsonb(grouped)) from grouped), '[]'::jsonb) as groups,
|
|
33392
|
+
coalesce((select jsonb_agg(to_jsonb(site_grouped)
|
|
33393
|
+
order by site_grouped."updatedAt" desc, site_grouped."siteId", site_grouped."channelId")
|
|
33394
|
+
from site_grouped), '[]'::jsonb) as sites
|
|
33204
33395
|
`,
|
|
33205
33396
|
);
|
|
33206
33397
|
const result = rows[0];
|
|
33398
|
+
const iso = (value: string | null) => (value ? new Date(value).toISOString() : null);
|
|
33399
|
+
const sites = result?.sites ?? [];
|
|
33207
33400
|
return {
|
|
33208
33401
|
needsYouCount: Number(result?.needsYouCount ?? 0),
|
|
33209
33402
|
groups: (result?.groups ?? []).map((group) => ({
|
|
33210
33403
|
...group,
|
|
33211
|
-
attentionSince:
|
|
33404
|
+
attentionSince: iso(group.attentionSince),
|
|
33212
33405
|
})),
|
|
33406
|
+
sites: sites.slice(0, SITE_CONVERSATION_GROUP_LIMIT).map((site) => ({
|
|
33407
|
+
...site,
|
|
33408
|
+
updatedAt: iso(site.updatedAt)!,
|
|
33409
|
+
createdAt: iso(site.createdAt)!,
|
|
33410
|
+
attentionSince: iso(site.attentionSince),
|
|
33411
|
+
})),
|
|
33412
|
+
sitesTruncated: sites.length > SITE_CONVERSATION_GROUP_LIMIT,
|
|
33213
33413
|
};
|
|
33214
33414
|
}
|
|
33215
33415
|
|
|
33416
|
+
/**
|
|
33417
|
+
* Bound on Site rows per read. Rows sort by activity, so Sites nobody touched
|
|
33418
|
+
* for a long time fall past it; the Artifacts page lists every Site.
|
|
33419
|
+
*/
|
|
33420
|
+
export const SITE_CONVERSATION_GROUP_LIMIT = 200;
|
|
33421
|
+
|
|
33216
33422
|
function sessionFilters(
|
|
33217
33423
|
options: Pick<
|
|
33218
33424
|
ListSessionsForSubjectOptions,
|
|
@@ -33225,6 +33431,7 @@ function sessionFilters(
|
|
|
33225
33431
|
| "channelId"
|
|
33226
33432
|
| "originSiteId"
|
|
33227
33433
|
| "siteAccessId"
|
|
33434
|
+
| "siteConversations"
|
|
33228
33435
|
| "createdBy"
|
|
33229
33436
|
| "updatedFrom"
|
|
33230
33437
|
| "updatedBefore"
|
|
@@ -33258,9 +33465,14 @@ function sessionFilters(
|
|
|
33258
33465
|
eq(schema.sessions.siteId, options.originSiteId),
|
|
33259
33466
|
sql`${schema.sessions.siteAccessRootId} is not null`,
|
|
33260
33467
|
);
|
|
33261
|
-
} else
|
|
33468
|
+
} else if (
|
|
33469
|
+
options.siteConversations !== "include" ||
|
|
33470
|
+
options.authorizationScope?.agentAccessViewer
|
|
33471
|
+
) {
|
|
33262
33472
|
// App-internal execution is not ordinary workspace conversation discovery.
|
|
33263
33473
|
// This is presentation scope only: native ownership and Site RLS still apply.
|
|
33474
|
+
// The console rail opts in explicitly (`include` or `group`); agent
|
|
33475
|
+
// callers keep their own-root bound so the option never widens discovery.
|
|
33264
33476
|
const ownRoot = options.authorizationScope?.agentAccessViewer?.callerRootSessionId;
|
|
33265
33477
|
filters.push(
|
|
33266
33478
|
ownRoot
|
|
@@ -33511,6 +33723,7 @@ function sessionListFilterIdentity(options: SessionListFilterOptions): string {
|
|
|
33511
33723
|
!hasChannel &&
|
|
33512
33724
|
!options.originSiteId &&
|
|
33513
33725
|
!options.siteAccessId &&
|
|
33726
|
+
options.siteConversations !== "include" &&
|
|
33514
33727
|
!options.createdBy &&
|
|
33515
33728
|
!options.updatedFrom &&
|
|
33516
33729
|
!options.updatedBefore &&
|
|
@@ -33526,6 +33739,8 @@ function sessionListFilterIdentity(options: SessionListFilterOptions): string {
|
|
|
33526
33739
|
return JSON.stringify([
|
|
33527
33740
|
...(options.siteAccessId ? [["siteAccess", options.siteAccessId]] : []),
|
|
33528
33741
|
...(options.originSiteId ? [["originSite", options.originSiteId]] : []),
|
|
33742
|
+
// `group` returns the same rows as the default; only `include` changes them.
|
|
33743
|
+
...(options.siteConversations === "include" ? [["siteConversations", "include"]] : []),
|
|
33529
33744
|
hasChannel ? ["channel", options.channelId] : null,
|
|
33530
33745
|
options.createdBy ? ["creator", options.createdBy.kind, options.createdBy.subjectId] : null,
|
|
33531
33746
|
options.updatedFrom ? ["updatedFrom", options.updatedFrom.toISOString()] : null,
|
|
@@ -33883,7 +34098,27 @@ export async function listVisibleSessionInvalidationIds(
|
|
|
33883
34098
|
sessionIds: string[],
|
|
33884
34099
|
authority: SessionListReadAuthority,
|
|
33885
34100
|
): Promise<string[]> {
|
|
33886
|
-
|
|
34101
|
+
return (await listVisibleSessionInvalidationHints(db, workspaceId, sessionIds, authority))
|
|
34102
|
+
.sessionIds;
|
|
34103
|
+
}
|
|
34104
|
+
|
|
34105
|
+
/**
|
|
34106
|
+
* Ordinary IDs plus, separately, the viewer's own visible Site conversations.
|
|
34107
|
+
* A rail with Site rows refreshes their summaries for the latter without
|
|
34108
|
+
* treating every unknown Site conversation as a full list invalidation.
|
|
34109
|
+
*/
|
|
34110
|
+
export async function listVisibleSessionInvalidationHints(
|
|
34111
|
+
db: Database,
|
|
34112
|
+
workspaceId: string,
|
|
34113
|
+
sessionIds: string[],
|
|
34114
|
+
authority: SessionListReadAuthority,
|
|
34115
|
+
): Promise<{ sessionIds: string[]; siteSessionIds: string[] }> {
|
|
34116
|
+
if (!sessionIds.length) return { sessionIds: [], siteSessionIds: [] };
|
|
34117
|
+
// Exactly the rows ordinary discovery omits (an agent keeps its own root).
|
|
34118
|
+
const ownRoot = authority.authorizationScope?.agentAccessViewer?.callerRootSessionId;
|
|
34119
|
+
const siteOnly = ownRoot
|
|
34120
|
+
? sql<boolean>`(${schema.sessions.siteAccessRootId} is not null and ${schema.sessions.rootSessionId} is distinct from ${ownRoot}::uuid)`
|
|
34121
|
+
: sql<boolean>`${schema.sessions.siteAccessRootId} is not null`;
|
|
33887
34122
|
return await withWorkspaceSubjectRls(
|
|
33888
34123
|
db,
|
|
33889
34124
|
workspaceId,
|
|
@@ -33891,16 +34126,24 @@ export async function listVisibleSessionInvalidationIds(
|
|
|
33891
34126
|
async (tx) => {
|
|
33892
34127
|
await requireSessionListReadAuthorityInScope(tx, workspaceId, authority);
|
|
33893
34128
|
const rows = await tx
|
|
33894
|
-
.select({
|
|
34129
|
+
.select({
|
|
34130
|
+
id: schema.sessions.id,
|
|
34131
|
+
site: siteOnly,
|
|
34132
|
+
})
|
|
33895
34133
|
.from(schema.sessions)
|
|
33896
34134
|
.where(
|
|
33897
34135
|
and(
|
|
33898
34136
|
eq(schema.sessions.workspaceId, workspaceId),
|
|
33899
34137
|
inArray(schema.sessions.id, sessionIds),
|
|
33900
|
-
|
|
34138
|
+
// Hints carry no content. Site conversations use the same viewer
|
|
34139
|
+
// authorization (and agent bound) as `siteConversations=include`.
|
|
34140
|
+
...sessionFilters({ ...authority, archiveStatus: "all", siteConversations: "include" }),
|
|
33901
34141
|
),
|
|
33902
34142
|
);
|
|
33903
|
-
return
|
|
34143
|
+
return {
|
|
34144
|
+
sessionIds: rows.filter((row) => !row.site).map((row) => row.id),
|
|
34145
|
+
siteSessionIds: rows.filter((row) => row.site).map((row) => row.id),
|
|
34146
|
+
};
|
|
33904
34147
|
},
|
|
33905
34148
|
{ isolationLevel: "read committed" },
|
|
33906
34149
|
);
|
|
@@ -33931,16 +34174,43 @@ async function readSessionListForSubject(
|
|
|
33931
34174
|
}
|
|
33932
34175
|
if (options.includeTotals && options.parentSessionId !== null && !options.pinsOnly)
|
|
33933
34176
|
throw new SessionListCursorError("Attention totals require root or global pin pages");
|
|
33934
|
-
const
|
|
33935
|
-
|
|
33936
|
-
|
|
33937
|
-
|
|
33938
|
-
|
|
33939
|
-
|
|
33940
|
-
|
|
33941
|
-
)
|
|
33942
|
-
|
|
34177
|
+
const groupSites = options.siteConversations === "group";
|
|
34178
|
+
if (
|
|
34179
|
+
groupSites &&
|
|
34180
|
+
(options.parentSessionId !== null ||
|
|
34181
|
+
options.pinsOnly ||
|
|
34182
|
+
archiveMode === "archived" ||
|
|
34183
|
+
options.originSiteId ||
|
|
34184
|
+
options.siteAccessId)
|
|
34185
|
+
)
|
|
34186
|
+
throw new SessionListCursorError("Site rows require an ordinary, non-archived root page");
|
|
34187
|
+
// Site rows are a complete section like pins: read once, with page one.
|
|
34188
|
+
const siteGroupsRequested = groupSites && !options.cursor;
|
|
34189
|
+
const completeTotals =
|
|
34190
|
+
options.includeTotals || siteGroupsRequested
|
|
34191
|
+
? await sessionListTotalsInScope(
|
|
34192
|
+
tx,
|
|
34193
|
+
workspaceId,
|
|
34194
|
+
options.pinsOnly
|
|
34195
|
+
? { ...options, archiveStatus: "active", parentSessionId: null }
|
|
34196
|
+
: options,
|
|
34197
|
+
siteGroupsRequested,
|
|
34198
|
+
)
|
|
34199
|
+
: undefined;
|
|
34200
|
+
const totals =
|
|
34201
|
+
options.includeTotals && completeTotals
|
|
34202
|
+
? { needsYouCount: completeTotals.needsYouCount, groups: completeTotals.groups }
|
|
34203
|
+
: undefined;
|
|
34204
|
+
const siteGroups = siteGroupsRequested ? (completeTotals?.sites ?? []) : undefined;
|
|
34205
|
+
const siteGroupsTruncated = siteGroupsRequested && completeTotals?.sitesTruncated === true;
|
|
33943
34206
|
const filters = [eq(schema.sessions.workspaceId, workspaceId), ...sessionFilters(options)];
|
|
34207
|
+
// A pinned Site conversation is a personal shortcut like any other pin.
|
|
34208
|
+
const pinnedFilters = groupSites
|
|
34209
|
+
? [
|
|
34210
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
34211
|
+
...sessionFilters({ ...options, siteConversations: "include" }),
|
|
34212
|
+
]
|
|
34213
|
+
: filters;
|
|
33944
34214
|
const ordinaryPinFilter =
|
|
33945
34215
|
archiveMode === "archived"
|
|
33946
34216
|
? sql`true`
|
|
@@ -34270,7 +34540,7 @@ async function readSessionListForSubject(
|
|
|
34270
34540
|
eq(schema.sessionPins.workspaceId, workspaceId),
|
|
34271
34541
|
eq(schema.sessionPins.subjectId, options.subjectId),
|
|
34272
34542
|
eq(schema.sessionPins.pinned, true),
|
|
34273
|
-
...
|
|
34543
|
+
...pinnedFilters,
|
|
34274
34544
|
),
|
|
34275
34545
|
)
|
|
34276
34546
|
.orderBy(desc(schema.sessionPins.pinnedAt), desc(schema.sessions.id))
|
|
@@ -34403,6 +34673,9 @@ async function readSessionListForSubject(
|
|
|
34403
34673
|
...(options.needsYouOnly ? { needsYouOnly: true as const } : {}),
|
|
34404
34674
|
...(options.contentArchivedOnly ? { contentArchivedOnly: true as const } : {}),
|
|
34405
34675
|
...(options.mineOnly ? { mineOnly: true as const } : {}),
|
|
34676
|
+
...(options.siteConversations ? { siteConversations: options.siteConversations } : {}),
|
|
34677
|
+
...(siteGroups ? { siteGroups } : {}),
|
|
34678
|
+
...(siteGroupsTruncated ? { siteGroupsTruncated: true } : {}),
|
|
34406
34679
|
pinned: pinnedRows.map((row) => sessionListEntry(mapListSession(row))),
|
|
34407
34680
|
pinnedTruncated,
|
|
34408
34681
|
sessions: pageRows.map((row) => sessionListEntry(mapListSession(row))),
|
|
@@ -34415,6 +34688,9 @@ async function readSessionListForSubject(
|
|
|
34415
34688
|
...(options.needsYouOnly ? { needsYouOnly: true as const } : {}),
|
|
34416
34689
|
...(options.contentArchivedOnly ? { contentArchivedOnly: true as const } : {}),
|
|
34417
34690
|
...(options.mineOnly ? { mineOnly: true as const } : {}),
|
|
34691
|
+
...(options.siteConversations ? { siteConversations: options.siteConversations } : {}),
|
|
34692
|
+
...(siteGroups ? { siteGroups } : {}),
|
|
34693
|
+
...(siteGroupsTruncated ? { siteGroupsTruncated: true } : {}),
|
|
34418
34694
|
pinned: pinnedRows.map(mapListSession),
|
|
34419
34695
|
pinnedTruncated,
|
|
34420
34696
|
sessions: pageRows.map(mapListSession),
|
|
@@ -40878,7 +41154,7 @@ const DURABLE_TOOL_CALL_SCAN_PAGE = 32;
|
|
|
40878
41154
|
* calls still read the whole turn before staying pending.
|
|
40879
41155
|
*/
|
|
40880
41156
|
async function durableToolCallPairIdsTx(
|
|
40881
|
-
tx: Pick<Database, "
|
|
41157
|
+
tx: Pick<Database, "execute">,
|
|
40882
41158
|
scope: { workspaceId: string; sessionId: string; turnId: string },
|
|
40883
41159
|
pending: Array<{ id: string; callId: string; callType: string }>,
|
|
40884
41160
|
): Promise<string[]> {
|
|
@@ -40892,32 +41168,16 @@ async function durableToolCallPairIdsTx(
|
|
|
40892
41168
|
const durable: string[] = [];
|
|
40893
41169
|
let before: number | null = null;
|
|
40894
41170
|
while (unproven.size > 0) {
|
|
40895
|
-
const page = await
|
|
40896
|
-
|
|
40897
|
-
|
|
40898
|
-
|
|
40899
|
-
|
|
40900
|
-
|
|
40901
|
-
.from(schema.sessionHistoryItems)
|
|
40902
|
-
.where(
|
|
40903
|
-
and(
|
|
40904
|
-
eq(schema.sessionHistoryItems.workspaceId, scope.workspaceId),
|
|
40905
|
-
eq(schema.sessionHistoryItems.sessionId, scope.sessionId),
|
|
40906
|
-
eq(schema.sessionHistoryItems.turnId, scope.turnId),
|
|
40907
|
-
before === null ? undefined : lt(schema.sessionHistoryItems.position, before),
|
|
40908
|
-
),
|
|
40909
|
-
)
|
|
40910
|
-
.orderBy(desc(schema.sessionHistoryItems.position))
|
|
40911
|
-
.limit(DURABLE_TOOL_CALL_SCAN_PAGE);
|
|
41171
|
+
const page = await readSessionHistoryToolIdentityPage(
|
|
41172
|
+
tx,
|
|
41173
|
+
scope,
|
|
41174
|
+
before,
|
|
41175
|
+
DURABLE_TOOL_CALL_SCAN_PAGE,
|
|
41176
|
+
);
|
|
40912
41177
|
// Positions are unique per session, so each row is strictly older than
|
|
40913
41178
|
// every row already seen.
|
|
40914
41179
|
for (const row of page) {
|
|
40915
|
-
const
|
|
40916
|
-
string,
|
|
40917
|
-
unknown
|
|
40918
|
-
>;
|
|
40919
|
-
const callId = historyCallId(item);
|
|
40920
|
-
const type = historyItemType(item);
|
|
41180
|
+
const { callId, type } = row;
|
|
40921
41181
|
if (callId === null || type === null) continue;
|
|
40922
41182
|
const call = unproven.get(callId);
|
|
40923
41183
|
if (!call) continue;
|
package/src/runtime-posture.ts
CHANGED
|
@@ -1974,6 +1974,7 @@ export const FORCE_RLS_TABLES = [
|
|
|
1974
1974
|
"site_hostname_holds",
|
|
1975
1975
|
"site_hostnames",
|
|
1976
1976
|
"site_limit_profiles",
|
|
1977
|
+
"site_opens",
|
|
1977
1978
|
"site_platform_brake",
|
|
1978
1979
|
"site_platform_storage",
|
|
1979
1980
|
"site_platform_totals",
|
|
@@ -2276,6 +2277,7 @@ export const RUNTIME_FULL_DML_TABLES = [
|
|
|
2276
2277
|
"site_data_workspace_validation_windows",
|
|
2277
2278
|
"site_hostnames",
|
|
2278
2279
|
"site_limit_profiles",
|
|
2280
|
+
"site_opens",
|
|
2279
2281
|
"site_platform_brake",
|
|
2280
2282
|
"site_platform_storage",
|
|
2281
2283
|
"site_platform_totals",
|
package/src/schema.ts
CHANGED
|
@@ -316,6 +316,30 @@ export const siteSessionAccess = pgTable(
|
|
|
316
316
|
],
|
|
317
317
|
);
|
|
318
318
|
|
|
319
|
+
/** When each person last opened a Site (migration 0908); personal ordering only. */
|
|
320
|
+
export const siteOpens = pgTable(
|
|
321
|
+
"site_opens",
|
|
322
|
+
{
|
|
323
|
+
accountId: uuid("account_id").notNull(),
|
|
324
|
+
workspaceId: uuid("workspace_id").notNull(),
|
|
325
|
+
subjectId: text("subject_id").notNull(),
|
|
326
|
+
siteId: uuid("site_id").notNull(),
|
|
327
|
+
openedAt: timestamp("opened_at", { withTimezone: true }).notNull().defaultNow(),
|
|
328
|
+
},
|
|
329
|
+
(table) => [
|
|
330
|
+
primaryKey({ columns: [table.workspaceId, table.subjectId, table.siteId] }),
|
|
331
|
+
foreignKey({
|
|
332
|
+
columns: [table.workspaceId, table.accountId],
|
|
333
|
+
foreignColumns: [workspaces.id, workspaces.accountId],
|
|
334
|
+
}).onDelete("cascade"),
|
|
335
|
+
foreignKey({
|
|
336
|
+
columns: [table.workspaceId, table.siteId],
|
|
337
|
+
foreignColumns: [workspaceArtifacts.workspaceId, workspaceArtifacts.id],
|
|
338
|
+
}).onDelete("cascade"),
|
|
339
|
+
index("site_opens_site_idx").on(table.workspaceId, table.siteId),
|
|
340
|
+
],
|
|
341
|
+
);
|
|
342
|
+
|
|
319
343
|
export const workspaceArtifactUploads = pgTable(
|
|
320
344
|
"workspace_artifact_uploads",
|
|
321
345
|
{
|