@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,55 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { rawRows, type Database } from "./database";
|
|
3
|
+
import { fromPostgresLosslessJson } from "./lossless-json";
|
|
4
|
+
import { historyCallId, historyItemType } from "./session-tool-call-settlement";
|
|
5
|
+
|
|
6
|
+
/** Receipt proof metadata only. Canonical model history is never projected here. */
|
|
7
|
+
export async function readSessionHistoryToolIdentityPage(
|
|
8
|
+
tx: Pick<Database, "execute">,
|
|
9
|
+
scope: { workspaceId: string; sessionId: string; turnId: string },
|
|
10
|
+
before: number | null,
|
|
11
|
+
limit: number,
|
|
12
|
+
): Promise<Array<{ position: number; callId: string | null; type: string | null }>> {
|
|
13
|
+
const rows = await rawRows<{
|
|
14
|
+
position: string;
|
|
15
|
+
item: Record<string, unknown>;
|
|
16
|
+
itemCodecVersion: number | null;
|
|
17
|
+
}>(
|
|
18
|
+
tx,
|
|
19
|
+
sql`
|
|
20
|
+
select page.position, page.item_codec_version as "itemCodecVersion",
|
|
21
|
+
jsonb_build_object(
|
|
22
|
+
'type', identity.type, 'callId', identity."callId", 'call_id', identity.call_id,
|
|
23
|
+
'id', identity.id, 'providerData', jsonb_build_object(
|
|
24
|
+
'callId', identity."providerData"->'callId',
|
|
25
|
+
'call_id', identity."providerData"->'call_id'
|
|
26
|
+
)
|
|
27
|
+
) as item
|
|
28
|
+
from (
|
|
29
|
+
select position, item, item_codec_version from session_history_items
|
|
30
|
+
where workspace_id = ${scope.workspaceId}::uuid
|
|
31
|
+
and session_id = ${scope.sessionId}::uuid and turn_id = ${scope.turnId}::uuid
|
|
32
|
+
${before === null ? sql`` : sql`and position < ${before}`}
|
|
33
|
+
order by position desc limit ${limit}
|
|
34
|
+
) page
|
|
35
|
+
cross join lateral jsonb_to_record(
|
|
36
|
+
case when jsonb_typeof(page.item) in ('object', 'null') then page.item else '{}'::jsonb end
|
|
37
|
+
) as identity(type jsonb, "callId" jsonb, call_id jsonb, id jsonb, "providerData" jsonb)
|
|
38
|
+
order by page.position desc
|
|
39
|
+
`,
|
|
40
|
+
);
|
|
41
|
+
// The synchronized JSONB mirror has the same field values as item_ordered.
|
|
42
|
+
// Decode only these fields with the original row's codec, then use the same
|
|
43
|
+
// identity precedence/type checks as the full-history reader. jsonb_to_record
|
|
44
|
+
// extracts them in one pass rather than detoasting a large output per field.
|
|
45
|
+
// A malformed JSON null still rejects the read; other primitive/array rows
|
|
46
|
+
// have no named identities, just as with the previous JavaScript lookup.
|
|
47
|
+
return rows.map((row) => {
|
|
48
|
+
const item = fromPostgresLosslessJson(row.item, row.itemCodecVersion);
|
|
49
|
+
return {
|
|
50
|
+
position: Number(row.position),
|
|
51
|
+
callId: historyCallId(item),
|
|
52
|
+
type: historyItemType(item),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { and,
|
|
2
|
+
import { and, eq, sql, type SQL } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type {
|
|
5
5
|
SessionMessageSearchRequest,
|
|
@@ -35,6 +35,19 @@ const Cursor = z
|
|
|
35
35
|
})
|
|
36
36
|
.strict();
|
|
37
37
|
type Position = z.infer<typeof Position>;
|
|
38
|
+
type CandidateRow = {
|
|
39
|
+
visited: number;
|
|
40
|
+
rank: number | null;
|
|
41
|
+
sessionId: string | null;
|
|
42
|
+
eventId: string | null;
|
|
43
|
+
sequence: number | null;
|
|
44
|
+
turnId: string | null;
|
|
45
|
+
type: "user.message" | "agent.message.completed" | null;
|
|
46
|
+
sessionTitle: string | null;
|
|
47
|
+
messageId: unknown;
|
|
48
|
+
codec: number | null;
|
|
49
|
+
smallText: unknown;
|
|
50
|
+
};
|
|
38
51
|
|
|
39
52
|
export class SessionMessageSearchCursorError extends Error {
|
|
40
53
|
constructor() {
|
|
@@ -50,6 +63,82 @@ export function sessionMessageLiteralPattern(query: string): RegExp {
|
|
|
50
63
|
return new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iu");
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
// Highest code point with a Unicode case mapping (Adlam ends at U+1E943).
|
|
67
|
+
const LAST_CASED_CODE_POINT = 0x1ffff;
|
|
68
|
+
let caseClasses: ReadonlyMap<number, readonly number[]> | undefined;
|
|
69
|
+
|
|
70
|
+
/** Every code point that `sessionMessageLiteralPattern` treats as the same
|
|
71
|
+
* character, keyed by each member. Characters absent from the map only match
|
|
72
|
+
* themselves. Built once from the runtime's own case mappings and verified
|
|
73
|
+
* against its `iu` matching, so the database filter agrees with the exact
|
|
74
|
+
* JavaScript matcher (including K/k/U+212A, s/S/U+017F and the Greek sigmas).
|
|
75
|
+
*/
|
|
76
|
+
export function sessionMessageCaseClasses(): ReadonlyMap<number, readonly number[]> {
|
|
77
|
+
if (caseClasses) return caseClasses;
|
|
78
|
+
const edges = new Map<number, Set<number>>();
|
|
79
|
+
const link = (a: number, b: number) => {
|
|
80
|
+
let set = edges.get(a);
|
|
81
|
+
if (!set) edges.set(a, (set = new Set()));
|
|
82
|
+
set.add(b);
|
|
83
|
+
};
|
|
84
|
+
for (let cp = 0; cp <= LAST_CASED_CODE_POINT; cp++) {
|
|
85
|
+
if (cp >= 0xd800 && cp <= 0xdfff) continue;
|
|
86
|
+
const char = String.fromCodePoint(cp);
|
|
87
|
+
for (const mapped of [char.toLowerCase(), char.toUpperCase()]) {
|
|
88
|
+
if (mapped === char) continue;
|
|
89
|
+
const target = mapped.codePointAt(0)!;
|
|
90
|
+
if (String.fromCodePoint(target) !== mapped) continue;
|
|
91
|
+
if (!sessionMessageLiteralPattern(char).test(mapped)) continue;
|
|
92
|
+
link(cp, target);
|
|
93
|
+
link(target, cp);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const classes = new Map<number, readonly number[]>();
|
|
97
|
+
for (const start of edges.keys()) {
|
|
98
|
+
if (classes.has(start)) continue;
|
|
99
|
+
const members = new Set([start]);
|
|
100
|
+
for (const member of members) for (const next of edges.get(member) ?? []) members.add(next);
|
|
101
|
+
const sorted = [...members].sort((a, b) => a - b);
|
|
102
|
+
for (const member of sorted) classes.set(member, sorted);
|
|
103
|
+
}
|
|
104
|
+
return (caseClasses = classes);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A PostgreSQL advanced regular expression matching exactly the strings that
|
|
108
|
+
* contain `query` under `sessionMessageLiteralPattern`. Every character is a
|
|
109
|
+
* \u/\U escape, so no character is special and the database collation plays
|
|
110
|
+
* no part. Null when the query holds a character PostgreSQL text cannot store
|
|
111
|
+
* (NUL or a lone surrogate): only losslessly encoded text can contain it.
|
|
112
|
+
*/
|
|
113
|
+
export function sessionMessagePostgresPattern(query: string): string | null {
|
|
114
|
+
const escape = (cp: number) =>
|
|
115
|
+
cp <= 0xffff
|
|
116
|
+
? `\\u${cp.toString(16).padStart(4, "0")}`
|
|
117
|
+
: `\\U${cp.toString(16).padStart(8, "0")}`;
|
|
118
|
+
let pattern = "";
|
|
119
|
+
for (const char of query) {
|
|
120
|
+
const cp = char.codePointAt(0)!;
|
|
121
|
+
if (cp === 0 || (cp >= 0xd800 && cp <= 0xdfff)) return null;
|
|
122
|
+
const members = sessionMessageCaseClasses().get(cp);
|
|
123
|
+
pattern += members ? `[${members.map(escape).join("")}]` : escape(cp);
|
|
124
|
+
}
|
|
125
|
+
return pattern;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Database tests over stored message text: `matched` is exact for ordinary
|
|
129
|
+
* text; `encoded` marks losslessly encoded strings, which only the decoding
|
|
130
|
+
* reader can inspect and so always remain candidates. */
|
|
131
|
+
export function sessionMessageTextMatchSql(
|
|
132
|
+
query: string,
|
|
133
|
+
text: SQL,
|
|
134
|
+
): { matched: SQL; encoded: SQL } {
|
|
135
|
+
const pattern = sessionMessagePostgresPattern(query);
|
|
136
|
+
return {
|
|
137
|
+
matched: pattern === null ? sql`false` : sql`(${text} ~ ${pattern})`,
|
|
138
|
+
encoded: sql`(left(${text}, ${LOSSLESS_JSON_STRING_PREFIX.length}) = ${LOSSLESS_JSON_STRING_PREFIX})`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
53
142
|
export function sessionMessageSearchSnippet(text: string, start: number, length: number) {
|
|
54
143
|
let left = Math.max(0, start - 120);
|
|
55
144
|
let right = Math.min(text.length, start + length + 120);
|
|
@@ -76,7 +165,6 @@ export async function scanSessionMessages(
|
|
|
76
165
|
signal?: AbortSignal,
|
|
77
166
|
): Promise<SessionMessageSearchResponse> {
|
|
78
167
|
signal?.throwIfAborted();
|
|
79
|
-
const started = performance.now();
|
|
80
168
|
const binding = createHash("sha256")
|
|
81
169
|
.update(
|
|
82
170
|
JSON.stringify([
|
|
@@ -127,6 +215,13 @@ export async function scanSessionMessages(
|
|
|
127
215
|
? sql`${e.sessionId} > ${position.sessionId}::uuid`
|
|
128
216
|
: sql`(${e.sessionId} > ${position.sessionId}::uuid or (${e.sessionId} = ${position.sessionId}::uuid and ${position.done ? sql`${e.sequence} > ${position.sequence}` : sql`${e.sequence} >= ${position.sequence}`}))`
|
|
129
217
|
: sql`true`;
|
|
218
|
+
// One statement checks every remaining message's text in PostgreSQL and
|
|
219
|
+
// returns only the first candidates that contain the literal. The cheap
|
|
220
|
+
// list/visibility predicates and the text test run once per message; the
|
|
221
|
+
// correlated canonical-copy and unclaimed-prompt checks run only for
|
|
222
|
+
// candidates, lazily in key order, until the batch is full. A no-hit scan
|
|
223
|
+
// finishes in one request instead of one request per 33 messages.
|
|
224
|
+
//
|
|
130
225
|
// Correlate the exact durable message identity, not turn status or latest
|
|
131
226
|
// model context. The latest full completion for a provider message wins,
|
|
132
227
|
// so an earlier shorter completion cannot hide later retained text. Same-
|
|
@@ -135,27 +230,15 @@ export async function scanSessionMessages(
|
|
|
135
230
|
// Suppression requires the competing copy to retain searchable string text
|
|
136
231
|
// itself: a later text-less completion (failed/empty retry sharing the
|
|
137
232
|
// provider message id) must never hide the earlier complete text.
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
>`case when octet_length(${schema.sessions.title}) <= 2048 then ${schema.sessions.title} else null end`,
|
|
148
|
-
messageId: sql<unknown>`case when octet_length((${e.payload}->'messageId')::text) <= 2048 then ${e.payload}->'messageId' else null end`,
|
|
149
|
-
codec: e.payloadCodecVersion,
|
|
150
|
-
smallText: sql<unknown>`case when octet_length(${e.payload}->>'text') <= 8192 then ${e.payload}->'text' else null end`,
|
|
151
|
-
})
|
|
152
|
-
.from(e)
|
|
153
|
-
.innerJoin(
|
|
154
|
-
schema.sessions,
|
|
155
|
-
and(eq(schema.sessions.workspaceId, e.workspaceId), eq(schema.sessions.id, e.sessionId)),
|
|
156
|
-
)
|
|
157
|
-
.where(
|
|
158
|
-
and(
|
|
233
|
+
const textMatch = sessionMessageTextMatchSql(request.query, sql`${e.payload}->>'text'`);
|
|
234
|
+
const rows: CandidateRow[] = await db.execute<CandidateRow>(sql`
|
|
235
|
+
with eligible as materialized (
|
|
236
|
+
select ${e.id} as id, ${e.sessionId} as session_id, ${e.sequence} as sequence,
|
|
237
|
+
${textMatch.matched} as matched, ${textMatch.encoded} as encoded
|
|
238
|
+
from ${e}
|
|
239
|
+
inner join ${schema.sessions}
|
|
240
|
+
on ${schema.sessions.workspaceId} = ${e.workspaceId} and ${schema.sessions.id} = ${e.sessionId}
|
|
241
|
+
where ${and(
|
|
159
242
|
eq(e.workspaceId, workspaceId),
|
|
160
243
|
...filters,
|
|
161
244
|
boundary,
|
|
@@ -164,8 +247,41 @@ export async function scanSessionMessages(
|
|
|
164
247
|
sql`(${e.turnAssociation} is null or ${e.turnAssociation} = 'current')`,
|
|
165
248
|
sql`${e.duplicateOfEventId} is null`,
|
|
166
249
|
sql`jsonb_typeof(${e.payload}->'text') = 'string'`,
|
|
167
|
-
|
|
168
|
-
|
|
250
|
+
)}
|
|
251
|
+
), visited as (
|
|
252
|
+
select count(*)::int as visited from eligible
|
|
253
|
+
)
|
|
254
|
+
select visited.visited, candidate.*
|
|
255
|
+
from visited
|
|
256
|
+
left join lateral (
|
|
257
|
+
select * from (
|
|
258
|
+
select valid.*${
|
|
259
|
+
request.groupBy === "session"
|
|
260
|
+
? sql`, (count(*) filter (where valid.definite) over (partition by valid."sessionId"
|
|
261
|
+
order by valid.sequence rows between unbounded preceding and 1 preceding))::int as earlier`
|
|
262
|
+
: sql``
|
|
263
|
+
}
|
|
264
|
+
from (
|
|
265
|
+
select ranked.rank, ranked.session_id as "sessionId", ${e.id} as "eventId",
|
|
266
|
+
ranked.sequence as sequence, ${e.turnId} as "turnId", ${e.type} as type,
|
|
267
|
+
ranked.matched and not ranked.encoded as definite,
|
|
268
|
+
case when octet_length(${schema.sessions.title}) <= 2048 then ${schema.sessions.title} else null end as "sessionTitle",
|
|
269
|
+
case when octet_length((${e.payload}->'messageId')::text) <= 2048 then ${e.payload}->'messageId' else null end as "messageId",
|
|
270
|
+
${e.payloadCodecVersion} as codec,
|
|
271
|
+
case when octet_length(${e.payload}->>'text') <= 8192 then ${e.payload}->'text' else null end as "smallText"
|
|
272
|
+
from (
|
|
273
|
+
select eligible.id, eligible.session_id, eligible.sequence, eligible.matched, eligible.encoded,
|
|
274
|
+
(row_number() over (order by eligible.session_id, eligible.sequence))::int as rank
|
|
275
|
+
from eligible
|
|
276
|
+
order by eligible.session_id, eligible.sequence
|
|
277
|
+
offset 0
|
|
278
|
+
) ranked
|
|
279
|
+
inner join ${e} on ${e.workspaceId} = ${workspaceId} and ${e.id} = ranked.id
|
|
280
|
+
inner join ${schema.sessions}
|
|
281
|
+
on ${schema.sessions.workspaceId} = ${e.workspaceId} and ${schema.sessions.id} = ${e.sessionId}
|
|
282
|
+
where (ranked.matched or ranked.encoded)
|
|
283
|
+
and ${excludeUnclaimedHumanPromptEventFilter(workspaceId)}
|
|
284
|
+
and (${e.type} <> 'agent.message.completed' or not exists (
|
|
169
285
|
select 1 from ${schema.sessionEvents} other_message
|
|
170
286
|
where other_message.workspace_id = ${workspaceId} and other_message.session_id = ${e.sessionId}
|
|
171
287
|
and other_message.turn_id = ${e.turnId}
|
|
@@ -181,15 +297,46 @@ export async function scanSessionMessages(
|
|
|
181
297
|
and other_message.sequence < ${e.sequence}
|
|
182
298
|
and other_message.payload->'text' = ${e.payload}->'text'
|
|
183
299
|
and (other_message.payload_codec_version is not distinct from ${e.payloadCodecVersion}
|
|
184
|
-
or left(${e.payload}->>'text', ${LOSSLESS_JSON_STRING_PREFIX.length}) <> ${LOSSLESS_JSON_STRING_PREFIX})))))
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
or left(${e.payload}->>'text', ${LOSSLESS_JSON_STRING_PREFIX.length}) <> ${LOSSLESS_JSON_STRING_PREFIX})))))
|
|
301
|
+
order by ranked.session_id, ranked.sequence
|
|
302
|
+
offset 0
|
|
303
|
+
) valid
|
|
304
|
+
) grouped
|
|
305
|
+
-- Grouping needs only each session's first certain hit. Encoded text
|
|
306
|
+
-- stays a candidate because only the decoding reader can confirm it.
|
|
307
|
+
where ${request.groupBy === "session" ? sql`grouped.earlier = 0` : sql`true`}
|
|
308
|
+
order by grouped."sessionId", grouped.sequence
|
|
309
|
+
limit 33
|
|
310
|
+
) candidate on true
|
|
311
|
+
order by candidate.rank
|
|
312
|
+
`);
|
|
313
|
+
const visited = rows[0]?.visited ?? 0;
|
|
314
|
+
const identities = rows.flatMap((row: CandidateRow) =>
|
|
315
|
+
row.rank === null ||
|
|
316
|
+
row.sessionId === null ||
|
|
317
|
+
row.eventId === null ||
|
|
318
|
+
row.sequence === null ||
|
|
319
|
+
row.type === null
|
|
320
|
+
? []
|
|
321
|
+
: [
|
|
322
|
+
{
|
|
323
|
+
...row,
|
|
324
|
+
rank: row.rank,
|
|
325
|
+
sessionId: row.sessionId,
|
|
326
|
+
eventId: row.eventId,
|
|
327
|
+
sequence: row.sequence,
|
|
328
|
+
type: row.type,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
);
|
|
332
|
+
// The cursor's message being resumed mid-text is the first visited row
|
|
333
|
+
// again; it was already counted by the request that started it.
|
|
334
|
+
const visitedBefore = scanned - (position && !position.done ? 1 : 0);
|
|
335
|
+
const budgetStarted = performance.now();
|
|
189
336
|
let identityIndex = 0;
|
|
190
337
|
for (let window = 0; window < 32 && matches.length < (request.limit ?? 20); window++) {
|
|
191
338
|
signal?.throwIfAborted();
|
|
192
|
-
if (window > 0 && performance.now() -
|
|
339
|
+
if (window > 0 && performance.now() - budgetStarted >= 1_500) break;
|
|
193
340
|
const identity = identities[identityIndex];
|
|
194
341
|
if (!identity) {
|
|
195
342
|
// Grouping can drain a full candidate batch in fewer than 32 windows.
|
|
@@ -203,7 +350,7 @@ export async function scanSessionMessages(
|
|
|
203
350
|
position.sessionId === identity.sessionId &&
|
|
204
351
|
position.sequence === identity.sequence;
|
|
205
352
|
if (!continuing) {
|
|
206
|
-
scanned
|
|
353
|
+
scanned = visitedBefore + identity.rank;
|
|
207
354
|
position = {
|
|
208
355
|
sessionId: identity.sessionId,
|
|
209
356
|
sequence: identity.sequence,
|
|
@@ -326,6 +473,7 @@ export async function scanSessionMessages(
|
|
|
326
473
|
};
|
|
327
474
|
}
|
|
328
475
|
if (identityIndex >= identities.length && identities.length < 33) exhausted = true;
|
|
476
|
+
if (exhausted) scanned = visitedBefore + visited;
|
|
329
477
|
}
|
|
330
478
|
signal?.throwIfAborted();
|
|
331
479
|
return {
|
|
@@ -339,7 +487,14 @@ export async function scanSessionMessages(
|
|
|
339
487
|
exhausted || !position
|
|
340
488
|
? null
|
|
341
489
|
: Buffer.from(
|
|
342
|
-
JSON.stringify({
|
|
490
|
+
JSON.stringify({
|
|
491
|
+
version: 1,
|
|
492
|
+
binding,
|
|
493
|
+
position,
|
|
494
|
+
scanned,
|
|
495
|
+
matched,
|
|
496
|
+
occurrences,
|
|
497
|
+
}),
|
|
343
498
|
).toString("base64url"),
|
|
344
499
|
};
|
|
345
500
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Two scopes, each with its own authority and nothing borrowed:
|
|
11
11
|
* - `organization_administrator`: the organization context (no workspace),
|
|
12
|
-
*
|
|
12
|
+
* a shared organization membership lifecycle lock, the caller's subject and the
|
|
13
13
|
* organization administrator check (the predicate the connection policies
|
|
14
14
|
* use), in every transaction. Only a shared, organization-managed
|
|
15
15
|
* connection (not one a workspace manages) of the provider's registered
|
|
@@ -200,15 +200,17 @@ export const subscriptionCoreAdministeredConnections = memoByProvider(
|
|
|
200
200
|
{ accountId: scope.accountId, workspaceId: null },
|
|
201
201
|
async (tx) => {
|
|
202
202
|
// The organization membership lifecycle key, before the
|
|
203
|
-
// administrator check,
|
|
204
|
-
//
|
|
205
|
-
//
|
|
203
|
+
// administrator check, shared between inspections of independent
|
|
204
|
+
// connections. Lifecycle writers remain exclusive, so a demotion
|
|
205
|
+
// cannot commit while this transaction acts on its result. The
|
|
206
|
+
// bounded wait refuses a busy organization rather than queueing
|
|
207
|
+
// indefinitely.
|
|
206
208
|
await tx.execute(
|
|
207
209
|
sql`select set_config('lock_timeout', ${ORGANIZATION_MEMBERSHIP_LOCK_TIMEOUT}, true)`,
|
|
208
210
|
);
|
|
209
211
|
try {
|
|
210
212
|
await tx.execute(
|
|
211
|
-
sql`select
|
|
213
|
+
sql`select pg_advisory_xact_lock_shared(hashtextextended(${`organization-membership:${scope.accountId}`}, 0))`,
|
|
212
214
|
);
|
|
213
215
|
} catch (error) {
|
|
214
216
|
if (nestedPostgresSqlState(error) === "55P03") throw new NotVisible();
|
|
@@ -44,11 +44,11 @@ export function blockedSubscriptionWindows(
|
|
|
44
44
|
provider: Provider,
|
|
45
45
|
usage: ReturnType<typeof projectSubscriptionCoreAccountUsage>,
|
|
46
46
|
now: number,
|
|
47
|
-
): Partial<Record<Window, number>> {
|
|
48
|
-
const blocked: Partial<Record<Window, number>> = {};
|
|
47
|
+
): Partial<Record<Window, number | null>> {
|
|
48
|
+
const blocked: Partial<Record<Window, number | null>> = {};
|
|
49
49
|
if (!usage) return blocked;
|
|
50
50
|
const block = (id: Window, until: number) => {
|
|
51
|
-
if (until > now) blocked[id] = Math.max(blocked[id] ?? 0, until);
|
|
51
|
+
if (until > now && blocked[id] !== null) blocked[id] = Math.max(blocked[id] ?? 0, until);
|
|
52
52
|
};
|
|
53
53
|
const windows = usage.windows.flatMap((window) => {
|
|
54
54
|
const id = windowId(provider, window.id);
|
|
@@ -56,8 +56,10 @@ export function blockedSubscriptionWindows(
|
|
|
56
56
|
return id === null ? [] : [{ id, resetsAt, status: window.status }];
|
|
57
57
|
});
|
|
58
58
|
for (const window of windows) {
|
|
59
|
-
if (window.status === "exhausted"
|
|
60
|
-
|
|
59
|
+
if (window.status === "exhausted") {
|
|
60
|
+
if (window.resetsAt === null) blocked[window.id] = null;
|
|
61
|
+
else block(window.id, window.resetsAt);
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
const until = epoch(usage.exhaustedUntil);
|
|
63
65
|
if (until !== null && until > now) {
|
|
@@ -11,7 +11,7 @@ import { parseVerifiedAttemptToolCatalog } from "@opengeni/codemode";
|
|
|
11
11
|
import { and, desc, eq, lt, or, sql } from "drizzle-orm";
|
|
12
12
|
import { isDeepStrictEqual } from "node:util";
|
|
13
13
|
import type { Database } from "./database";
|
|
14
|
-
import { withRlsContext, withWorkspaceRls } from "./database";
|
|
14
|
+
import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./database";
|
|
15
15
|
import * as schema from "./schema";
|
|
16
16
|
import { hydrateStoredAttemptToolCatalog } from "./session-content-blobs";
|
|
17
17
|
import { sameSitePublicationRequest, sitePublicationRequest } from "./site-publication-request";
|
|
@@ -185,6 +185,50 @@ export class WorkspaceArtifactNotFoundError extends Error {
|
|
|
185
185
|
readonly name = "WorkspaceArtifactNotFoundError";
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Records that a person opened a Site in the console, for the order of their
|
|
190
|
+
* own Site rows. Only a Site this viewer can see is recorded; the row grants
|
|
191
|
+
* nothing. Repeated opens only move the time forward.
|
|
192
|
+
*/
|
|
193
|
+
export async function recordSiteOpened(
|
|
194
|
+
db: Database,
|
|
195
|
+
input: {
|
|
196
|
+
workspaceId: string;
|
|
197
|
+
subjectId: string;
|
|
198
|
+
viewer: WorkspaceArtifactViewer;
|
|
199
|
+
siteId: string;
|
|
200
|
+
},
|
|
201
|
+
): Promise<{ openedAt: string }> {
|
|
202
|
+
return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scoped) => {
|
|
203
|
+
const [site] = await scoped
|
|
204
|
+
.select({ accountId: schema.workspaceArtifacts.accountId })
|
|
205
|
+
.from(schema.workspaceArtifacts)
|
|
206
|
+
.where(
|
|
207
|
+
and(
|
|
208
|
+
eq(schema.workspaceArtifacts.workspaceId, input.workspaceId),
|
|
209
|
+
eq(schema.workspaceArtifacts.id, input.siteId),
|
|
210
|
+
visibleTo(input.viewer),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
.limit(1);
|
|
214
|
+
if (!site) throw new WorkspaceArtifactNotFoundError("Site not found");
|
|
215
|
+
const [row] = await scoped
|
|
216
|
+
.insert(schema.siteOpens)
|
|
217
|
+
.values({
|
|
218
|
+
accountId: site.accountId,
|
|
219
|
+
workspaceId: input.workspaceId,
|
|
220
|
+
subjectId: input.subjectId,
|
|
221
|
+
siteId: input.siteId,
|
|
222
|
+
})
|
|
223
|
+
.onConflictDoUpdate({
|
|
224
|
+
target: [schema.siteOpens.workspaceId, schema.siteOpens.subjectId, schema.siteOpens.siteId],
|
|
225
|
+
set: { openedAt: sql`greatest(${schema.siteOpens.openedAt}, now())` },
|
|
226
|
+
})
|
|
227
|
+
.returning({ openedAt: schema.siteOpens.openedAt });
|
|
228
|
+
return { openedAt: row!.openedAt.toISOString() };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
188
232
|
export class WorkspaceArtifactConflictError extends Error {
|
|
189
233
|
readonly name = "WorkspaceArtifactConflictError";
|
|
190
234
|
constructor(
|