@opengeni/db 3.4.0-canary.2 → 3.6.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.d.ts +20 -0
- package/dist/canonical-human-identities.js +7 -3
- package/dist/{chunk-ZWRIPCTR.js → chunk-2NT476HD.js} +2 -2
- package/dist/{chunk-PYMY3AJW.js → chunk-744DFC65.js} +202 -7
- package/dist/chunk-744DFC65.js.map +1 -0
- package/dist/{chunk-O6FTELKH.js → chunk-CDMMSUWJ.js} +3 -3
- package/dist/chunk-G3732M3T.js +299 -0
- package/dist/chunk-G3732M3T.js.map +1 -0
- package/dist/{chunk-TRBOWUG3.js → chunk-IW6O6G7R.js} +2 -2
- package/dist/{chunk-HY3F5Q4E.js → chunk-Q5YUGQVK.js} +3 -3
- package/dist/{chunk-V3TRUXFJ.js → chunk-UGDVSQ6U.js} +72 -19
- package/dist/chunk-UGDVSQ6U.js.map +1 -0
- package/dist/{chunk-C2AFP7SB.js → chunk-UJ2EW3QI.js} +6664 -6051
- package/dist/chunk-UJ2EW3QI.js.map +1 -0
- package/dist/{chunk-H7MXYG7R.js → chunk-WW7AJ5IC.js} +2 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +44 -6
- package/dist/index.js +2914 -1535
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-set-schema.d.ts +1399 -0
- package/dist/managed-auth-session-sets.d.ts +145 -0
- package/dist/managed-auth-session-sets.js +50 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/organization-recovery-schema.d.ts +2144 -0
- package/dist/organization-recovery.d.ts +186 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +7 -4
- package/dist/schema.d.ts +9 -6
- package/dist/schema.js +43 -1
- package/dist/session-background-commands.js +3 -3
- package/dist/session-control.d.ts +5 -3
- package/dist/session-queue-commands.d.ts +2 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/work-claims-schema.d.ts +975 -0
- package/dist/work-claims.d.ts +83 -0
- package/drizzle/0362_managed_auth_session_sets.sql +2347 -0
- package/drizzle/0363_organization_recovery_custody.sql +2418 -0
- package/drizzle/0364_workspace_learning_policy_snapshot_lock_order.sql +221 -0
- package/drizzle/0365_permission_scoped_work_claims.sql +1306 -0
- package/drizzle/0366_session_discovery_title_search_index.sql +5 -0
- package/drizzle/0367_session_discovery_goal_search_index.sql +5 -0
- package/drizzle/0368_session_discovery_claim_search_index.sql +6 -0
- package/package.json +10 -6
- package/src/canonical-human-identities.ts +105 -27
- package/src/index.ts +1471 -433
- package/src/managed-auth-session-set-schema.ts +176 -0
- package/src/managed-auth-session-sets.ts +430 -0
- package/src/organization-recovery-schema.ts +277 -0
- package/src/organization-recovery.ts +424 -0
- package/src/provision-roles.ts +67 -0
- package/src/runtime-posture.ts +144 -6
- package/src/schema.ts +3 -0
- package/src/session-control.ts +255 -16
- package/src/session-queue-commands.ts +10 -1
- package/src/session-tool-call-settlement.ts +6 -1
- package/src/work-claims-schema.ts +148 -0
- package/src/work-claims.ts +200 -0
- package/dist/chunk-C2AFP7SB.js.map +0 -1
- package/dist/chunk-PYMY3AJW.js.map +0 -1
- package/dist/chunk-V3TRUXFJ.js.map +0 -1
- /package/dist/{chunk-ZWRIPCTR.js.map → chunk-2NT476HD.js.map} +0 -0
- /package/dist/{chunk-O6FTELKH.js.map → chunk-CDMMSUWJ.js.map} +0 -0
- /package/dist/{chunk-TRBOWUG3.js.map → chunk-IW6O6G7R.js.map} +0 -0
- /package/dist/{chunk-HY3F5Q4E.js.map → chunk-Q5YUGQVK.js.map} +0 -0
- /package/dist/{chunk-H7MXYG7R.js.map → chunk-WW7AJ5IC.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -20,9 +20,21 @@ import {
|
|
|
20
20
|
SessionGoalSnapshot,
|
|
21
21
|
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
22
22
|
ScheduledTaskRunAcceptedExecution,
|
|
23
|
+
WORK_CLAIM_DISCOVERY_DEFAULT_LIMIT,
|
|
24
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
25
|
+
WORK_DISCOVERY_QUERY_MAX_CHARS,
|
|
26
|
+
WORK_DISCOVERY_RECENT_HOURS_MAX,
|
|
27
|
+
WorkClaimDiscoverySummary as WorkClaimDiscoverySummarySchema,
|
|
28
|
+
WorkDiscoveryMatch as WorkDiscoveryMatchSchema,
|
|
29
|
+
WorkClaimNamespace,
|
|
30
|
+
WorkClaimSubjectType,
|
|
31
|
+
WorkClaimCanonicalKey,
|
|
32
|
+
SessionStatus as SessionStatusSchema,
|
|
23
33
|
SessionGoalRootConstraintsWrite,
|
|
24
34
|
normalizeSessionGoalRootConstraints,
|
|
25
35
|
normalizeAutomaticSessionTitle,
|
|
36
|
+
normalizeWorkClaimCanonicalKey,
|
|
37
|
+
normalizeWorkClaimNamespace,
|
|
26
38
|
sessionVisibilityToPublic,
|
|
27
39
|
sessionGoalUtf8Bytes,
|
|
28
40
|
} from "@opengeni/contracts";
|
|
@@ -154,6 +166,9 @@ import type {
|
|
|
154
166
|
WorkspaceArchiveDescriptor,
|
|
155
167
|
ModalCheckpointProviderBinding,
|
|
156
168
|
SandboxProviderContinuityRecovery,
|
|
169
|
+
WorkClaimDiscoverySummary,
|
|
170
|
+
WorkClaimSubjectFilter,
|
|
171
|
+
WorkDiscoveryProjection,
|
|
157
172
|
} from "@opengeni/contracts";
|
|
158
173
|
import {
|
|
159
174
|
closePrivateSessionCreateCapability,
|
|
@@ -273,6 +288,7 @@ import {
|
|
|
273
288
|
sql,
|
|
274
289
|
type SQL,
|
|
275
290
|
} from "drizzle-orm";
|
|
291
|
+
import type { PgTransactionConfig } from "drizzle-orm/pg-core";
|
|
276
292
|
import {
|
|
277
293
|
creatorColumns,
|
|
278
294
|
frozenInitiatorForCommandActor,
|
|
@@ -426,6 +442,7 @@ export * from "./company-brain-governed-writes";
|
|
|
426
442
|
export * from "./company-brain-context-selection";
|
|
427
443
|
export * from "./knowledge-source-sync";
|
|
428
444
|
export * from "./task-notes";
|
|
445
|
+
export * from "./work-claims";
|
|
429
446
|
export * from "./managed-human-provisioning";
|
|
430
447
|
export * from "./managed-user-setup";
|
|
431
448
|
export * from "./organization-membership-backfill";
|
|
@@ -1508,6 +1525,7 @@ export async function ensureManagedAccessForUser(
|
|
|
1508
1525
|
name: string;
|
|
1509
1526
|
emailVerified?: boolean;
|
|
1510
1527
|
provisionFallbackOrganization?: boolean;
|
|
1528
|
+
bindPendingInvitations?: boolean;
|
|
1511
1529
|
},
|
|
1512
1530
|
): Promise<AccessContext> {
|
|
1513
1531
|
return (await ensureManagedAccessForUserWithOrganizationMemberships(db, input)).accessContext;
|
|
@@ -1653,6 +1671,7 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1653
1671
|
name: string;
|
|
1654
1672
|
emailVerified?: boolean;
|
|
1655
1673
|
provisionFallbackOrganization?: boolean;
|
|
1674
|
+
bindPendingInvitations?: boolean;
|
|
1656
1675
|
},
|
|
1657
1676
|
): Promise<ManagedAccessProvisioningResult> {
|
|
1658
1677
|
const subjectId = `user:${input.userId}`;
|
|
@@ -1660,7 +1679,7 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1660
1679
|
return await db.transaction(async (tx) => {
|
|
1661
1680
|
const txDb = tx as unknown as Database;
|
|
1662
1681
|
await setSubjectRlsContext(txDb, subjectId);
|
|
1663
|
-
if (input.emailVerified === true) {
|
|
1682
|
+
if (input.emailVerified === true && input.bindPendingInvitations !== false) {
|
|
1664
1683
|
await rawRows(
|
|
1665
1684
|
txDb,
|
|
1666
1685
|
sql`select bind_pending_organization_invitations_for_verified_email(
|
|
@@ -16176,6 +16195,43 @@ export async function listVariableSets(
|
|
|
16176
16195
|
});
|
|
16177
16196
|
}
|
|
16178
16197
|
|
|
16198
|
+
export async function resolveVariableSetAttachments(
|
|
16199
|
+
db: Database,
|
|
16200
|
+
context: VariableSetAccessContext,
|
|
16201
|
+
variableSetIds: readonly string[],
|
|
16202
|
+
): Promise<Array<Pick<VariableSet, "id" | "scope">>> {
|
|
16203
|
+
if (variableSetIds.length === 0) return [];
|
|
16204
|
+
// Drizzle expands a bare JavaScript array into a parameter list rather than
|
|
16205
|
+
// one PostgreSQL array value. Build the bounded requested relation directly.
|
|
16206
|
+
const requestedValues = sql.join(
|
|
16207
|
+
variableSetIds.map(
|
|
16208
|
+
(variableSetId, index) => sql`(${variableSetId}::uuid, ${index + 1}::integer)`,
|
|
16209
|
+
),
|
|
16210
|
+
sql`, `,
|
|
16211
|
+
);
|
|
16212
|
+
return await withRlsContext(db, context, async (scopedDb) => {
|
|
16213
|
+
await setSubjectRlsContext(scopedDb, context.subjectId);
|
|
16214
|
+
return await rawRows<Pick<VariableSet, "id" | "scope">>(
|
|
16215
|
+
scopedDb,
|
|
16216
|
+
sql`with requested(variable_set_id, ordinal) as (
|
|
16217
|
+
values ${requestedValues}
|
|
16218
|
+
)
|
|
16219
|
+
select
|
|
16220
|
+
resolved.value->>'id' as id,
|
|
16221
|
+
resolved.value->>'scope' as scope
|
|
16222
|
+
from requested
|
|
16223
|
+
cross join lateral list_scoped_variable_sets(
|
|
16224
|
+
${context.accountId}::uuid,
|
|
16225
|
+
${context.workspaceId}::uuid,
|
|
16226
|
+
requested.variable_set_id,
|
|
16227
|
+
null,
|
|
16228
|
+
null
|
|
16229
|
+
) resolved(value)
|
|
16230
|
+
order by requested.ordinal`,
|
|
16231
|
+
);
|
|
16232
|
+
});
|
|
16233
|
+
}
|
|
16234
|
+
|
|
16179
16235
|
export async function getVariableSet(
|
|
16180
16236
|
db: Database,
|
|
16181
16237
|
context: VariableSetAccessContext,
|
|
@@ -29002,6 +29058,43 @@ async function lockSessionPersonalStateExclusive(
|
|
|
29002
29058
|
);
|
|
29003
29059
|
}
|
|
29004
29060
|
|
|
29061
|
+
async function requireSessionPersonalStateWriteAuthority(
|
|
29062
|
+
db: Database,
|
|
29063
|
+
input: {
|
|
29064
|
+
workspaceId: string;
|
|
29065
|
+
subjectId: string;
|
|
29066
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
29067
|
+
},
|
|
29068
|
+
): Promise<void> {
|
|
29069
|
+
const [membership] = await db
|
|
29070
|
+
.select({ id: schema.workspaceMemberships.id })
|
|
29071
|
+
.from(schema.workspaceMemberships)
|
|
29072
|
+
.where(
|
|
29073
|
+
and(
|
|
29074
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
29075
|
+
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
29076
|
+
),
|
|
29077
|
+
)
|
|
29078
|
+
.limit(1);
|
|
29079
|
+
// Managed personal-workspace owners intentionally have no membership row.
|
|
29080
|
+
// The caller may assert this exception only from canonical managed-cookie
|
|
29081
|
+
// authentication; the live organization-membership pointer remains the
|
|
29082
|
+
// database authority and keeps suspended/foreign subjects denied.
|
|
29083
|
+
if (
|
|
29084
|
+
!membership &&
|
|
29085
|
+
!(
|
|
29086
|
+
input.personalWorkspaceOwnerException === true &&
|
|
29087
|
+
(await subjectHasLiveWorkspaceAuthorityInScope(db, {
|
|
29088
|
+
accountId: await accountIdInRlsScope(db),
|
|
29089
|
+
workspaceId: input.workspaceId,
|
|
29090
|
+
subjectId: input.subjectId,
|
|
29091
|
+
}))
|
|
29092
|
+
)
|
|
29093
|
+
) {
|
|
29094
|
+
throw new SessionPinAccessError();
|
|
29095
|
+
}
|
|
29096
|
+
}
|
|
29097
|
+
|
|
29005
29098
|
type SessionPinRow = Pick<
|
|
29006
29099
|
typeof schema.sessionPins.$inferSelect,
|
|
29007
29100
|
| "pinned"
|
|
@@ -30213,36 +30306,7 @@ export async function setSessionPin(
|
|
|
30213
30306
|
// serializes with member removal before changing personal state. A stale
|
|
30214
30307
|
// API grant cannot recreate a pin after removal commits.
|
|
30215
30308
|
await lockSessionPersonalStateExclusive(tx, input.workspaceId, input.subjectId);
|
|
30216
|
-
|
|
30217
|
-
.select({ id: schema.workspaceMemberships.id })
|
|
30218
|
-
.from(schema.workspaceMemberships)
|
|
30219
|
-
.where(
|
|
30220
|
-
and(
|
|
30221
|
-
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
30222
|
-
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
30223
|
-
),
|
|
30224
|
-
)
|
|
30225
|
-
.limit(1);
|
|
30226
|
-
// The owner of a managed personal workspace never has a membership row
|
|
30227
|
-
// there (migration 0219 raises on one), so a bare probe would deny every
|
|
30228
|
-
// pin inside their own workspace. Defer to the canonical resolver, on
|
|
30229
|
-
// this same transaction handle so the exclusive personal-state fence
|
|
30230
|
-
// above still serializes it with member removal. Gated on the caller's
|
|
30231
|
-
// canonical managed-cookie assertion; see
|
|
30232
|
-
// PersonalWorkspaceOwnerException.
|
|
30233
|
-
if (
|
|
30234
|
-
!membership &&
|
|
30235
|
-
!(
|
|
30236
|
-
input.personalWorkspaceOwnerException === true &&
|
|
30237
|
-
(await subjectHasLiveWorkspaceAuthorityInScope(tx, {
|
|
30238
|
-
accountId: await accountIdInRlsScope(tx),
|
|
30239
|
-
workspaceId: input.workspaceId,
|
|
30240
|
-
subjectId: input.subjectId,
|
|
30241
|
-
}))
|
|
30242
|
-
)
|
|
30243
|
-
) {
|
|
30244
|
-
throw new SessionPinAccessError();
|
|
30245
|
-
}
|
|
30309
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30246
30310
|
const [session] = await tx
|
|
30247
30311
|
.select()
|
|
30248
30312
|
.from(schema.sessions)
|
|
@@ -30371,6 +30435,8 @@ export async function setSessionAttention(
|
|
|
30371
30435
|
acknowledgedThroughSequence?: number | undefined;
|
|
30372
30436
|
activelyWorking?: boolean | undefined;
|
|
30373
30437
|
expectedVersion?: number | undefined;
|
|
30438
|
+
/** See {@link PersonalWorkspaceOwnerException}. Absent means "no exception". */
|
|
30439
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
30374
30440
|
},
|
|
30375
30441
|
): Promise<Session | null> {
|
|
30376
30442
|
return await withWorkspaceSubjectRls(
|
|
@@ -30380,17 +30446,7 @@ export async function setSessionAttention(
|
|
|
30380
30446
|
async (scopedDb) =>
|
|
30381
30447
|
await scopedDb.transaction(async (tx) => {
|
|
30382
30448
|
await lockSessionPersonalStateExclusive(tx, input.workspaceId, input.subjectId);
|
|
30383
|
-
|
|
30384
|
-
.select({ id: schema.workspaceMemberships.id })
|
|
30385
|
-
.from(schema.workspaceMemberships)
|
|
30386
|
-
.where(
|
|
30387
|
-
and(
|
|
30388
|
-
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
30389
|
-
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
30390
|
-
),
|
|
30391
|
-
)
|
|
30392
|
-
.limit(1);
|
|
30393
|
-
if (!membership) throw new SessionPinAccessError();
|
|
30449
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30394
30450
|
|
|
30395
30451
|
const [session] = await tx
|
|
30396
30452
|
.select()
|
|
@@ -30542,6 +30598,8 @@ export async function setSessionArchive(
|
|
|
30542
30598
|
sessionId: string;
|
|
30543
30599
|
archived: boolean;
|
|
30544
30600
|
expectedVersion?: number | undefined;
|
|
30601
|
+
/** See {@link PersonalWorkspaceOwnerException}. Absent means "no exception". */
|
|
30602
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
30545
30603
|
},
|
|
30546
30604
|
): Promise<Session | null> {
|
|
30547
30605
|
return await withWorkspaceSubjectRls(
|
|
@@ -30551,17 +30609,7 @@ export async function setSessionArchive(
|
|
|
30551
30609
|
async (scopedDb) =>
|
|
30552
30610
|
await scopedDb.transaction(async (tx) => {
|
|
30553
30611
|
await lockSessionPersonalStateExclusive(tx, input.workspaceId, input.subjectId);
|
|
30554
|
-
|
|
30555
|
-
.select({ id: schema.workspaceMemberships.id })
|
|
30556
|
-
.from(schema.workspaceMemberships)
|
|
30557
|
-
.where(
|
|
30558
|
-
and(
|
|
30559
|
-
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
30560
|
-
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
30561
|
-
),
|
|
30562
|
-
)
|
|
30563
|
-
.limit(1);
|
|
30564
|
-
if (!membership) throw new SessionPinAccessError();
|
|
30612
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30565
30613
|
|
|
30566
30614
|
const [session] = await tx
|
|
30567
30615
|
.select()
|
|
@@ -30792,9 +30840,11 @@ export async function getLatestSessionModelForSubject(
|
|
|
30792
30840
|
});
|
|
30793
30841
|
}
|
|
30794
30842
|
|
|
30795
|
-
export type SessionDiscoveryOrderBy = "createdAt" | "updatedAt";
|
|
30843
|
+
export type SessionDiscoveryOrderBy = "createdAt" | "updatedAt" | "relevance";
|
|
30796
30844
|
export type SessionDiscoveryCursor = {
|
|
30797
30845
|
orderBy: SessionDiscoveryOrderBy;
|
|
30846
|
+
/** Stable rank class for relevance order; null for chronological order. */
|
|
30847
|
+
sortRank: number | null;
|
|
30798
30848
|
/** Decimal database activity revision; zero is the legacy/created-order bucket. */
|
|
30799
30849
|
sortRevision: string;
|
|
30800
30850
|
/** Exact PostgreSQL timestamp text (including microseconds), not a JS Date. */
|
|
@@ -30806,6 +30856,8 @@ export type SessionDiscoveryCursor = {
|
|
|
30806
30856
|
snapshotRevision: string;
|
|
30807
30857
|
/** Incremental revision scope is cursor-bound and cannot change on continuation. */
|
|
30808
30858
|
updatedAfter: string | null;
|
|
30859
|
+
/** SHA-256 of normalized relevance filters; null for chronological order. */
|
|
30860
|
+
filterHash: string | null;
|
|
30809
30861
|
};
|
|
30810
30862
|
export const SESSION_DISCOVERY_GOAL_MAX_CHARS = 600;
|
|
30811
30863
|
export const SESSION_DISCOVERY_MESSAGE_MAX_CHARS = 600;
|
|
@@ -30830,12 +30882,15 @@ export type SessionDiscoverySummary = {
|
|
|
30830
30882
|
preview: string | null;
|
|
30831
30883
|
previewOriginalChars: number | null;
|
|
30832
30884
|
} | null;
|
|
30885
|
+
workDiscovery: WorkDiscoveryProjection;
|
|
30833
30886
|
createdAt: string;
|
|
30834
30887
|
updatedAt: string;
|
|
30835
30888
|
/** Internal exact revision key; omitted from the MCP row projection. */
|
|
30836
30889
|
sortRevision: string;
|
|
30837
30890
|
/** Internal exact keyset timestamp; omitted from the MCP row projection. */
|
|
30838
30891
|
sortAt: string;
|
|
30892
|
+
/** Internal stable relevance class; omitted from public row projections. */
|
|
30893
|
+
sortRank: number | null;
|
|
30839
30894
|
};
|
|
30840
30895
|
|
|
30841
30896
|
function projectSessionDiscoveryControlForRelatedAccess(
|
|
@@ -30894,6 +30949,700 @@ async function readWorkspaceSessionActivityRevision(
|
|
|
30894
30949
|
return normalizeSessionActivityRevision(revision, "snapshot revision");
|
|
30895
30950
|
}
|
|
30896
30951
|
|
|
30952
|
+
const SESSION_DISCOVERY_QUERY_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
30953
|
+
|
|
30954
|
+
type NormalizedSessionDiscoveryFilters = {
|
|
30955
|
+
query: string | null;
|
|
30956
|
+
statuses: SessionStatus[];
|
|
30957
|
+
activeOnly: boolean;
|
|
30958
|
+
recentHours: number | null;
|
|
30959
|
+
rootSessionId: string | null;
|
|
30960
|
+
parentSessionId: string | null | undefined;
|
|
30961
|
+
subject: WorkClaimSubjectFilter | null;
|
|
30962
|
+
};
|
|
30963
|
+
|
|
30964
|
+
type SessionDiscoveryPageRow = {
|
|
30965
|
+
id: string | null;
|
|
30966
|
+
title: string | null;
|
|
30967
|
+
titleOriginalChars: number | string | null;
|
|
30968
|
+
parentSessionId: string | null;
|
|
30969
|
+
rootSessionId: string | null;
|
|
30970
|
+
nestedAgentDepth: number | string | null;
|
|
30971
|
+
status: string | null;
|
|
30972
|
+
createdAt: Date | string | null;
|
|
30973
|
+
updatedAt: Date | string | null;
|
|
30974
|
+
sortRevision: string | null;
|
|
30975
|
+
sortAt: string | null;
|
|
30976
|
+
sortRank: number | string | null;
|
|
30977
|
+
matchClass: string | null;
|
|
30978
|
+
matchedField: string | null;
|
|
30979
|
+
scoreBand: string | null;
|
|
30980
|
+
matchedClaimId: string | null;
|
|
30981
|
+
total: number | string;
|
|
30982
|
+
};
|
|
30983
|
+
|
|
30984
|
+
type SessionDiscoveryClaimRow = {
|
|
30985
|
+
sessionId: string;
|
|
30986
|
+
id: string;
|
|
30987
|
+
subjectNamespace: string;
|
|
30988
|
+
subjectType: string;
|
|
30989
|
+
canonicalKey: string;
|
|
30990
|
+
displayLabel: string | null;
|
|
30991
|
+
role: string;
|
|
30992
|
+
state: string;
|
|
30993
|
+
revision: number | string;
|
|
30994
|
+
provenance: string;
|
|
30995
|
+
versionKind: string | null;
|
|
30996
|
+
versionValue: string | null;
|
|
30997
|
+
observedAt: Date | string;
|
|
30998
|
+
updatedAt: Date | string;
|
|
30999
|
+
settledAt: Date | string | null;
|
|
31000
|
+
availableCount: number | string;
|
|
31001
|
+
};
|
|
31002
|
+
|
|
31003
|
+
function normalizeSessionDiscoveryQuery(value: string | undefined): string | null {
|
|
31004
|
+
if (value === undefined) return null;
|
|
31005
|
+
const canonical = value.normalize("NFKC");
|
|
31006
|
+
if (SESSION_DISCOVERY_QUERY_CONTROL_CHARACTERS.test(canonical)) {
|
|
31007
|
+
throw new Error("sessions_list query must not contain control characters");
|
|
31008
|
+
}
|
|
31009
|
+
const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
31010
|
+
if (!normalized) return null;
|
|
31011
|
+
if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS) {
|
|
31012
|
+
throw new Error(
|
|
31013
|
+
`sessions_list query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS} characters`,
|
|
31014
|
+
);
|
|
31015
|
+
}
|
|
31016
|
+
return normalized;
|
|
31017
|
+
}
|
|
31018
|
+
|
|
31019
|
+
function normalizeSessionDiscoverySubject(
|
|
31020
|
+
value: WorkClaimSubjectFilter | undefined,
|
|
31021
|
+
): WorkClaimSubjectFilter | null {
|
|
31022
|
+
if (!value) return null;
|
|
31023
|
+
return {
|
|
31024
|
+
namespace: WorkClaimNamespace.parse(normalizeWorkClaimNamespace(value.namespace)),
|
|
31025
|
+
type: WorkClaimSubjectType.parse(value.type),
|
|
31026
|
+
canonicalKey: WorkClaimCanonicalKey.parse(normalizeWorkClaimCanonicalKey(value.canonicalKey)),
|
|
31027
|
+
};
|
|
31028
|
+
}
|
|
31029
|
+
|
|
31030
|
+
function normalizeSessionDiscoveryFilters(options: {
|
|
31031
|
+
query?: string;
|
|
31032
|
+
search?: string;
|
|
31033
|
+
statuses?: SessionStatus[];
|
|
31034
|
+
activeOnly?: boolean;
|
|
31035
|
+
recentHours?: number;
|
|
31036
|
+
rootSessionId?: string;
|
|
31037
|
+
parentSessionId?: string | null;
|
|
31038
|
+
subject?: WorkClaimSubjectFilter;
|
|
31039
|
+
}): NormalizedSessionDiscoveryFilters {
|
|
31040
|
+
const query = normalizeSessionDiscoveryQuery(options.query ?? options.search);
|
|
31041
|
+
if (
|
|
31042
|
+
options.query !== undefined &&
|
|
31043
|
+
options.search !== undefined &&
|
|
31044
|
+
normalizeSessionDiscoveryQuery(options.query) !== normalizeSessionDiscoveryQuery(options.search)
|
|
31045
|
+
) {
|
|
31046
|
+
throw new Error("sessions_list query and legacy search filter do not match");
|
|
31047
|
+
}
|
|
31048
|
+
const statuses = [...new Set(options.statuses ?? [])]
|
|
31049
|
+
.map((status) => SessionStatusSchema.parse(status))
|
|
31050
|
+
.sort();
|
|
31051
|
+
const recentHours = options.recentHours ?? null;
|
|
31052
|
+
if (
|
|
31053
|
+
recentHours !== null &&
|
|
31054
|
+
(!Number.isSafeInteger(recentHours) ||
|
|
31055
|
+
recentHours < 1 ||
|
|
31056
|
+
recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX)
|
|
31057
|
+
) {
|
|
31058
|
+
throw new Error(
|
|
31059
|
+
`sessions_list recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX}`,
|
|
31060
|
+
);
|
|
31061
|
+
}
|
|
31062
|
+
const subject = normalizeSessionDiscoverySubject(options.subject);
|
|
31063
|
+
if (query && subject) {
|
|
31064
|
+
throw new Error("sessions_list query cannot be combined with an exact subject filter");
|
|
31065
|
+
}
|
|
31066
|
+
return {
|
|
31067
|
+
query,
|
|
31068
|
+
statuses,
|
|
31069
|
+
activeOnly: options.activeOnly === true,
|
|
31070
|
+
recentHours,
|
|
31071
|
+
rootSessionId: options.rootSessionId ?? null,
|
|
31072
|
+
parentSessionId: Object.prototype.hasOwnProperty.call(options, "parentSessionId")
|
|
31073
|
+
? (options.parentSessionId ?? null)
|
|
31074
|
+
: undefined,
|
|
31075
|
+
subject,
|
|
31076
|
+
};
|
|
31077
|
+
}
|
|
31078
|
+
|
|
31079
|
+
function sessionDiscoveryFilterHash(filters: NormalizedSessionDiscoveryFilters): string {
|
|
31080
|
+
return createHash("sha256")
|
|
31081
|
+
.update(
|
|
31082
|
+
JSON.stringify({
|
|
31083
|
+
query: filters.query,
|
|
31084
|
+
statuses: filters.statuses,
|
|
31085
|
+
activeOnly: filters.activeOnly,
|
|
31086
|
+
recentHours: filters.recentHours,
|
|
31087
|
+
rootSessionId: filters.rootSessionId,
|
|
31088
|
+
parentSessionId:
|
|
31089
|
+
filters.parentSessionId === undefined ? { any: true } : filters.parentSessionId,
|
|
31090
|
+
subject: filters.subject,
|
|
31091
|
+
}),
|
|
31092
|
+
)
|
|
31093
|
+
.digest("hex");
|
|
31094
|
+
}
|
|
31095
|
+
|
|
31096
|
+
function sessionDiscoverySubjectDigest(subject: WorkClaimSubjectFilter): string {
|
|
31097
|
+
return createHash("sha256")
|
|
31098
|
+
.update(`${subject.namespace}\u001f${subject.type}\u001f${subject.canonicalKey}`, "utf8")
|
|
31099
|
+
.digest("hex");
|
|
31100
|
+
}
|
|
31101
|
+
|
|
31102
|
+
function sessionDiscoveryIso(value: Date | string): string {
|
|
31103
|
+
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
31104
|
+
}
|
|
31105
|
+
|
|
31106
|
+
async function selectSessionDiscoveryPageRows(
|
|
31107
|
+
db: Database,
|
|
31108
|
+
workspaceId: string,
|
|
31109
|
+
options: {
|
|
31110
|
+
limit: number;
|
|
31111
|
+
orderBy: SessionDiscoveryOrderBy;
|
|
31112
|
+
cursor?: SessionDiscoveryCursor;
|
|
31113
|
+
snapshotAt: string;
|
|
31114
|
+
snapshotRevision: string;
|
|
31115
|
+
updatedAfter: string | null;
|
|
31116
|
+
subjectId?: string;
|
|
31117
|
+
authorizationScope?: SessionAuthorizationListScope;
|
|
31118
|
+
filters: NormalizedSessionDiscoveryFilters;
|
|
31119
|
+
},
|
|
31120
|
+
): Promise<{ rows: SessionDiscoveryPageRow[]; total: number }> {
|
|
31121
|
+
const authorizationFilters: SQL[] = [eq(schema.sessions.workspaceId, workspaceId)];
|
|
31122
|
+
if (options.subjectId) {
|
|
31123
|
+
authorizationFilters.push(sql`not exists (
|
|
31124
|
+
select 1
|
|
31125
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
31126
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
31127
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
31128
|
+
and private_slack_interaction.visibility = 'private'
|
|
31129
|
+
and private_slack_interaction.owning_subject_id <> ${options.subjectId}
|
|
31130
|
+
)`);
|
|
31131
|
+
} else {
|
|
31132
|
+
// Missing subject context must fail closed for Slack-private roots. Public
|
|
31133
|
+
// and ordinary workspace-shared discovery remains available to internal
|
|
31134
|
+
// callers that do not carry a human subject.
|
|
31135
|
+
authorizationFilters.push(sql`not exists (
|
|
31136
|
+
select 1
|
|
31137
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
31138
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
31139
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
31140
|
+
and private_slack_interaction.visibility = 'private'
|
|
31141
|
+
)`);
|
|
31142
|
+
}
|
|
31143
|
+
if (options.authorizationScope) {
|
|
31144
|
+
authorizationFilters.push(sessionAuthorizationScopeFilter(options.authorizationScope));
|
|
31145
|
+
}
|
|
31146
|
+
if (options.filters.statuses.length > 0) {
|
|
31147
|
+
authorizationFilters.push(inArray(schema.sessions.status, options.filters.statuses));
|
|
31148
|
+
}
|
|
31149
|
+
if (options.filters.activeOnly) {
|
|
31150
|
+
authorizationFilters.push(notInArray(schema.sessions.status, ["failed", "cancelled"]));
|
|
31151
|
+
}
|
|
31152
|
+
if (options.filters.recentHours !== null) {
|
|
31153
|
+
authorizationFilters.push(
|
|
31154
|
+
sql`${schema.sessions.updatedAt} >= ${options.snapshotAt}::text::timestamptz
|
|
31155
|
+
- (${options.filters.recentHours}::integer * interval '1 hour')`,
|
|
31156
|
+
);
|
|
31157
|
+
}
|
|
31158
|
+
if (options.filters.rootSessionId) {
|
|
31159
|
+
authorizationFilters.push(eq(schema.sessions.rootSessionId, options.filters.rootSessionId));
|
|
31160
|
+
}
|
|
31161
|
+
if (options.filters.parentSessionId !== undefined) {
|
|
31162
|
+
authorizationFilters.push(
|
|
31163
|
+
options.filters.parentSessionId === null
|
|
31164
|
+
? isNull(schema.sessions.parentSessionId)
|
|
31165
|
+
: eq(schema.sessions.parentSessionId, options.filters.parentSessionId),
|
|
31166
|
+
);
|
|
31167
|
+
}
|
|
31168
|
+
authorizationFilters.push(
|
|
31169
|
+
options.orderBy === "createdAt"
|
|
31170
|
+
? sql`${schema.sessions.createdAt} <= ${options.snapshotAt}::text::timestamptz`
|
|
31171
|
+
: sql`${schema.sessions.activityRevision} <= ${options.snapshotRevision}::text::bigint`,
|
|
31172
|
+
);
|
|
31173
|
+
if (options.updatedAfter !== null) {
|
|
31174
|
+
authorizationFilters.push(
|
|
31175
|
+
sql`${schema.sessions.activityRevision} > ${options.updatedAfter}::text::bigint`,
|
|
31176
|
+
);
|
|
31177
|
+
}
|
|
31178
|
+
const authorizedPredicate = and(...authorizationFilters) ?? sql`false`;
|
|
31179
|
+
const claimStatePredicate =
|
|
31180
|
+
options.filters.activeOnly || options.filters.recentHours === null
|
|
31181
|
+
? sql`${schema.sessionWorkClaims.state} = 'active'`
|
|
31182
|
+
: sql`(${schema.sessionWorkClaims.state} = 'active' or
|
|
31183
|
+
${schema.sessionWorkClaims.updatedAt} >= ${options.snapshotAt}::text::timestamptz
|
|
31184
|
+
- (${options.filters.recentHours}::integer * interval '1 hour'))`;
|
|
31185
|
+
const subjectDigest = options.filters.subject
|
|
31186
|
+
? sessionDiscoverySubjectDigest(options.filters.subject)
|
|
31187
|
+
: null;
|
|
31188
|
+
const subjectScopePredicate = options.filters.subject
|
|
31189
|
+
? sql`exists (
|
|
31190
|
+
select 1
|
|
31191
|
+
from ${schema.sessionWorkClaims}
|
|
31192
|
+
where ${schema.sessionWorkClaims.workspaceId} = ${workspaceId}
|
|
31193
|
+
and ${schema.sessionWorkClaims.sessionId} = authorized_sessions.id
|
|
31194
|
+
and ${schema.sessionWorkClaims.subjectNamespace} = ${options.filters.subject.namespace}
|
|
31195
|
+
and ${schema.sessionWorkClaims.subjectType} = ${options.filters.subject.type}
|
|
31196
|
+
and ${schema.sessionWorkClaims.subjectDigest} = ${subjectDigest}
|
|
31197
|
+
and ${claimStatePredicate}
|
|
31198
|
+
)`
|
|
31199
|
+
: sql`true`;
|
|
31200
|
+
|
|
31201
|
+
const rankingCtes = options.filters.subject
|
|
31202
|
+
? sql`,
|
|
31203
|
+
match_candidates as materialized (
|
|
31204
|
+
select
|
|
31205
|
+
subject_scoped_sessions.id as session_id,
|
|
31206
|
+
0::integer as match_rank,
|
|
31207
|
+
'exact_subject'::text as match_class,
|
|
31208
|
+
'subject'::text as matched_field,
|
|
31209
|
+
'exact'::text as score_band,
|
|
31210
|
+
${schema.sessionWorkClaims.id} as matched_claim_id,
|
|
31211
|
+
${schema.sessionWorkClaims.updatedAt} as matched_claim_updated_at
|
|
31212
|
+
from subject_scoped_sessions
|
|
31213
|
+
join ${schema.sessionWorkClaims}
|
|
31214
|
+
on ${schema.sessionWorkClaims.workspaceId} = ${workspaceId}
|
|
31215
|
+
and ${schema.sessionWorkClaims.sessionId} = subject_scoped_sessions.id
|
|
31216
|
+
and ${schema.sessionWorkClaims.subjectNamespace} = ${options.filters.subject.namespace}
|
|
31217
|
+
and ${schema.sessionWorkClaims.subjectType} = ${options.filters.subject.type}
|
|
31218
|
+
and ${schema.sessionWorkClaims.subjectDigest} = ${subjectDigest}
|
|
31219
|
+
and ${claimStatePredicate}
|
|
31220
|
+
),
|
|
31221
|
+
best_matches as materialized (
|
|
31222
|
+
select distinct on (session_id)
|
|
31223
|
+
session_id, match_rank, match_class, matched_field, score_band, matched_claim_id
|
|
31224
|
+
from match_candidates
|
|
31225
|
+
order by session_id, match_rank, matched_claim_updated_at desc, matched_claim_id desc
|
|
31226
|
+
)`
|
|
31227
|
+
: options.filters.query
|
|
31228
|
+
? sql`,
|
|
31229
|
+
search_input as materialized (
|
|
31230
|
+
select
|
|
31231
|
+
lower(${options.filters.query}::text) as normalized_query,
|
|
31232
|
+
websearch_to_tsquery('simple', ${options.filters.query}::text) as ts_query
|
|
31233
|
+
),
|
|
31234
|
+
match_candidates as materialized (
|
|
31235
|
+
select
|
|
31236
|
+
subject_scoped_sessions.id as session_id,
|
|
31237
|
+
case
|
|
31238
|
+
when lower(search_session.title) = search_input.normalized_query then 10
|
|
31239
|
+
when starts_with(lower(search_session.title), search_input.normalized_query) then 11
|
|
31240
|
+
else 12
|
|
31241
|
+
end::integer as match_rank,
|
|
31242
|
+
'title'::text as match_class,
|
|
31243
|
+
'title'::text as matched_field,
|
|
31244
|
+
case
|
|
31245
|
+
when lower(search_session.title) = search_input.normalized_query then 'exact'
|
|
31246
|
+
else 'strong'
|
|
31247
|
+
end::text as score_band,
|
|
31248
|
+
null::uuid as matched_claim_id,
|
|
31249
|
+
null::timestamptz as matched_claim_updated_at
|
|
31250
|
+
from subject_scoped_sessions
|
|
31251
|
+
join ${schema.sessions} search_session
|
|
31252
|
+
on search_session.workspace_id = ${workspaceId}
|
|
31253
|
+
and search_session.id = subject_scoped_sessions.id
|
|
31254
|
+
cross join search_input
|
|
31255
|
+
where search_session.title is not null
|
|
31256
|
+
and (
|
|
31257
|
+
lower(search_session.title) = search_input.normalized_query
|
|
31258
|
+
or starts_with(lower(search_session.title), search_input.normalized_query)
|
|
31259
|
+
or to_tsvector('simple', search_session.title) @@ search_input.ts_query
|
|
31260
|
+
)
|
|
31261
|
+
|
|
31262
|
+
union all
|
|
31263
|
+
|
|
31264
|
+
select
|
|
31265
|
+
subject_scoped_sessions.id as session_id,
|
|
31266
|
+
case
|
|
31267
|
+
when lower(${schema.sessionGoals.text}) = search_input.normalized_query then 20
|
|
31268
|
+
when starts_with(
|
|
31269
|
+
lower(${schema.sessionGoals.text}), search_input.normalized_query
|
|
31270
|
+
) then 21
|
|
31271
|
+
else 22
|
|
31272
|
+
end::integer as match_rank,
|
|
31273
|
+
'goal'::text as match_class,
|
|
31274
|
+
'goal'::text as matched_field,
|
|
31275
|
+
case
|
|
31276
|
+
when lower(${schema.sessionGoals.text}) = search_input.normalized_query then 'exact'
|
|
31277
|
+
else 'strong'
|
|
31278
|
+
end::text as score_band,
|
|
31279
|
+
null::uuid as matched_claim_id,
|
|
31280
|
+
null::timestamptz as matched_claim_updated_at
|
|
31281
|
+
from subject_scoped_sessions
|
|
31282
|
+
join ${schema.sessionGoals}
|
|
31283
|
+
on ${schema.sessionGoals.workspaceId} = ${workspaceId}
|
|
31284
|
+
and ${schema.sessionGoals.sessionId} = subject_scoped_sessions.id
|
|
31285
|
+
and ${schema.sessionGoals.status} = 'active'
|
|
31286
|
+
cross join search_input
|
|
31287
|
+
where lower(${schema.sessionGoals.text}) = search_input.normalized_query
|
|
31288
|
+
or starts_with(lower(${schema.sessionGoals.text}), search_input.normalized_query)
|
|
31289
|
+
or to_tsvector('simple', ${schema.sessionGoals.text}) @@ search_input.ts_query
|
|
31290
|
+
|
|
31291
|
+
union all
|
|
31292
|
+
|
|
31293
|
+
select
|
|
31294
|
+
subject_scoped_sessions.id as session_id,
|
|
31295
|
+
(
|
|
31296
|
+
case when ${schema.sessionWorkClaims.state} = 'active' then 30 else 40 end
|
|
31297
|
+
+ case
|
|
31298
|
+
when lower(${schema.sessionWorkClaims.canonicalKey}) = search_input.normalized_query
|
|
31299
|
+
or lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')) = search_input.normalized_query then 0
|
|
31300
|
+
when starts_with(
|
|
31301
|
+
lower(${schema.sessionWorkClaims.canonicalKey}),
|
|
31302
|
+
search_input.normalized_query
|
|
31303
|
+
) or starts_with(
|
|
31304
|
+
lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')),
|
|
31305
|
+
search_input.normalized_query
|
|
31306
|
+
) then 1
|
|
31307
|
+
else 2
|
|
31308
|
+
end
|
|
31309
|
+
)::integer as match_rank,
|
|
31310
|
+
'fuzzy'::text as match_class,
|
|
31311
|
+
case
|
|
31312
|
+
when lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')) = search_input.normalized_query
|
|
31313
|
+
or starts_with(
|
|
31314
|
+
lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')),
|
|
31315
|
+
search_input.normalized_query
|
|
31316
|
+
)
|
|
31317
|
+
or to_tsvector('simple', coalesce(${schema.sessionWorkClaims.displayLabel}, '')) @@ search_input.ts_query
|
|
31318
|
+
then 'claim_label'
|
|
31319
|
+
else 'claim_key'
|
|
31320
|
+
end::text as matched_field,
|
|
31321
|
+
case
|
|
31322
|
+
when lower(${schema.sessionWorkClaims.canonicalKey}) = search_input.normalized_query
|
|
31323
|
+
or lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')) = search_input.normalized_query then 'exact'
|
|
31324
|
+
when starts_with(
|
|
31325
|
+
lower(${schema.sessionWorkClaims.canonicalKey}), search_input.normalized_query
|
|
31326
|
+
) or starts_with(
|
|
31327
|
+
lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')),
|
|
31328
|
+
search_input.normalized_query
|
|
31329
|
+
) then 'strong'
|
|
31330
|
+
else 'related'
|
|
31331
|
+
end::text as score_band,
|
|
31332
|
+
${schema.sessionWorkClaims.id} as matched_claim_id,
|
|
31333
|
+
${schema.sessionWorkClaims.updatedAt} as matched_claim_updated_at
|
|
31334
|
+
from subject_scoped_sessions
|
|
31335
|
+
join ${schema.sessionWorkClaims}
|
|
31336
|
+
on ${schema.sessionWorkClaims.workspaceId} = ${workspaceId}
|
|
31337
|
+
and ${schema.sessionWorkClaims.sessionId} = subject_scoped_sessions.id
|
|
31338
|
+
and ${claimStatePredicate}
|
|
31339
|
+
cross join search_input
|
|
31340
|
+
where lower(${schema.sessionWorkClaims.canonicalKey}) = search_input.normalized_query
|
|
31341
|
+
or starts_with(
|
|
31342
|
+
lower(${schema.sessionWorkClaims.canonicalKey}),
|
|
31343
|
+
search_input.normalized_query
|
|
31344
|
+
)
|
|
31345
|
+
or to_tsvector(
|
|
31346
|
+
'simple',
|
|
31347
|
+
${schema.sessionWorkClaims.canonicalKey} || ' '
|
|
31348
|
+
|| coalesce(${schema.sessionWorkClaims.displayLabel}, '')
|
|
31349
|
+
) @@ search_input.ts_query
|
|
31350
|
+
or lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')) = search_input.normalized_query
|
|
31351
|
+
or starts_with(
|
|
31352
|
+
lower(coalesce(${schema.sessionWorkClaims.displayLabel}, '')),
|
|
31353
|
+
search_input.normalized_query
|
|
31354
|
+
)
|
|
31355
|
+
),
|
|
31356
|
+
best_matches as materialized (
|
|
31357
|
+
select distinct on (session_id)
|
|
31358
|
+
session_id, match_rank, match_class, matched_field, score_band, matched_claim_id
|
|
31359
|
+
from match_candidates
|
|
31360
|
+
order by session_id, match_rank, matched_claim_updated_at desc nulls last,
|
|
31361
|
+
matched_claim_id desc nulls last
|
|
31362
|
+
)`
|
|
31363
|
+
: sql``;
|
|
31364
|
+
const rankedCte =
|
|
31365
|
+
options.orderBy === "relevance"
|
|
31366
|
+
? sql`,
|
|
31367
|
+
ranked_sessions as materialized (
|
|
31368
|
+
select
|
|
31369
|
+
subject_scoped_sessions.*,
|
|
31370
|
+
best_matches.match_rank,
|
|
31371
|
+
best_matches.match_class,
|
|
31372
|
+
best_matches.matched_field,
|
|
31373
|
+
best_matches.score_band,
|
|
31374
|
+
best_matches.matched_claim_id
|
|
31375
|
+
from subject_scoped_sessions
|
|
31376
|
+
join best_matches on best_matches.session_id = subject_scoped_sessions.id
|
|
31377
|
+
)`
|
|
31378
|
+
: sql`,
|
|
31379
|
+
ranked_sessions as materialized (
|
|
31380
|
+
select
|
|
31381
|
+
subject_scoped_sessions.*,
|
|
31382
|
+
null::integer as match_rank,
|
|
31383
|
+
null::text as match_class,
|
|
31384
|
+
null::text as matched_field,
|
|
31385
|
+
null::text as score_band,
|
|
31386
|
+
null::uuid as matched_claim_id
|
|
31387
|
+
from subject_scoped_sessions
|
|
31388
|
+
)`;
|
|
31389
|
+
const cursorPredicate = !options.cursor
|
|
31390
|
+
? sql`true`
|
|
31391
|
+
: options.orderBy === "relevance"
|
|
31392
|
+
? sql`(
|
|
31393
|
+
ranked_sessions.match_rank > ${options.cursor.sortRank}::integer
|
|
31394
|
+
or (
|
|
31395
|
+
ranked_sessions.match_rank = ${options.cursor.sortRank}::integer
|
|
31396
|
+
and (
|
|
31397
|
+
ranked_sessions.activity_revision < ${options.cursor.sortRevision}::text::bigint
|
|
31398
|
+
or (
|
|
31399
|
+
ranked_sessions.activity_revision = ${options.cursor.sortRevision}::text::bigint
|
|
31400
|
+
and (
|
|
31401
|
+
ranked_sessions.updated_at < ${options.cursor.sortAt}::text::timestamptz
|
|
31402
|
+
or (
|
|
31403
|
+
ranked_sessions.updated_at = ${options.cursor.sortAt}::text::timestamptz
|
|
31404
|
+
and ranked_sessions.id < ${options.cursor.id}::uuid
|
|
31405
|
+
)
|
|
31406
|
+
)
|
|
31407
|
+
)
|
|
31408
|
+
)
|
|
31409
|
+
)
|
|
31410
|
+
)`
|
|
31411
|
+
: options.orderBy === "updatedAt"
|
|
31412
|
+
? sql`(
|
|
31413
|
+
ranked_sessions.activity_revision < ${options.cursor.sortRevision}::text::bigint
|
|
31414
|
+
or (
|
|
31415
|
+
ranked_sessions.activity_revision = ${options.cursor.sortRevision}::text::bigint
|
|
31416
|
+
and (
|
|
31417
|
+
ranked_sessions.updated_at < ${options.cursor.sortAt}::text::timestamptz
|
|
31418
|
+
or (
|
|
31419
|
+
ranked_sessions.updated_at = ${options.cursor.sortAt}::text::timestamptz
|
|
31420
|
+
and ranked_sessions.id < ${options.cursor.id}::uuid
|
|
31421
|
+
)
|
|
31422
|
+
)
|
|
31423
|
+
)
|
|
31424
|
+
)`
|
|
31425
|
+
: sql`(
|
|
31426
|
+
ranked_sessions.created_at < ${options.cursor.sortAt}::text::timestamptz
|
|
31427
|
+
or (
|
|
31428
|
+
ranked_sessions.created_at = ${options.cursor.sortAt}::text::timestamptz
|
|
31429
|
+
and ranked_sessions.id < ${options.cursor.id}::uuid
|
|
31430
|
+
)
|
|
31431
|
+
)`;
|
|
31432
|
+
const pageOrder =
|
|
31433
|
+
options.orderBy === "relevance"
|
|
31434
|
+
? sql`ranked_sessions.match_rank asc, ranked_sessions.activity_revision desc,
|
|
31435
|
+
ranked_sessions.updated_at desc, ranked_sessions.id desc`
|
|
31436
|
+
: options.orderBy === "updatedAt"
|
|
31437
|
+
? sql`ranked_sessions.activity_revision desc, ranked_sessions.updated_at desc,
|
|
31438
|
+
ranked_sessions.id desc`
|
|
31439
|
+
: sql`ranked_sessions.created_at desc, ranked_sessions.id desc`;
|
|
31440
|
+
const outerOrder =
|
|
31441
|
+
options.orderBy === "relevance"
|
|
31442
|
+
? sql`page."sortRank" asc nulls last, page."sortRevision"::bigint desc nulls last,
|
|
31443
|
+
page."sortAt"::timestamptz desc nulls last, page.id desc nulls last`
|
|
31444
|
+
: options.orderBy === "updatedAt"
|
|
31445
|
+
? sql`page."sortRevision"::bigint desc nulls last,
|
|
31446
|
+
page."sortAt"::timestamptz desc nulls last, page.id desc nulls last`
|
|
31447
|
+
: sql`page."sortAt"::timestamptz desc nulls last, page.id desc nulls last`;
|
|
31448
|
+
const sortRevision =
|
|
31449
|
+
options.orderBy === "createdAt" ? sql`'0'::text` : sql`ranked_sessions.activity_revision::text`;
|
|
31450
|
+
const sortAt =
|
|
31451
|
+
options.orderBy === "createdAt"
|
|
31452
|
+
? sql`to_char(ranked_sessions.created_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
|
|
31453
|
+
: sql`to_char(ranked_sessions.updated_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`;
|
|
31454
|
+
|
|
31455
|
+
const result = await rawRows<SessionDiscoveryPageRow>(
|
|
31456
|
+
db,
|
|
31457
|
+
sql`
|
|
31458
|
+
with authorized_sessions as materialized (
|
|
31459
|
+
select
|
|
31460
|
+
${schema.sessions.id} as id,
|
|
31461
|
+
${schema.sessions.title} as title,
|
|
31462
|
+
${schema.sessions.parentSessionId} as parent_session_id,
|
|
31463
|
+
${schema.sessions.rootSessionId} as root_session_id,
|
|
31464
|
+
${schema.sessions.nestedAgentDepth} as nested_agent_depth,
|
|
31465
|
+
${schema.sessions.status} as status,
|
|
31466
|
+
${schema.sessions.createdAt} as created_at,
|
|
31467
|
+
${schema.sessions.updatedAt} as updated_at,
|
|
31468
|
+
${schema.sessions.activityRevision} as activity_revision
|
|
31469
|
+
from ${schema.sessions}
|
|
31470
|
+
where ${authorizedPredicate}
|
|
31471
|
+
),
|
|
31472
|
+
subject_scoped_sessions as materialized (
|
|
31473
|
+
select * from authorized_sessions where ${subjectScopePredicate}
|
|
31474
|
+
)
|
|
31475
|
+
${rankingCtes}
|
|
31476
|
+
${rankedCte}
|
|
31477
|
+
select
|
|
31478
|
+
page.id,
|
|
31479
|
+
page.title,
|
|
31480
|
+
page."titleOriginalChars",
|
|
31481
|
+
page."parentSessionId",
|
|
31482
|
+
page."rootSessionId",
|
|
31483
|
+
page."nestedAgentDepth",
|
|
31484
|
+
page.status,
|
|
31485
|
+
page."createdAt",
|
|
31486
|
+
page."updatedAt",
|
|
31487
|
+
page."sortRevision",
|
|
31488
|
+
page."sortAt",
|
|
31489
|
+
page."sortRank",
|
|
31490
|
+
page."matchClass",
|
|
31491
|
+
page."matchedField",
|
|
31492
|
+
page."scoreBand",
|
|
31493
|
+
page."matchedClaimId",
|
|
31494
|
+
totals.total
|
|
31495
|
+
from (select count(*)::integer as total from ranked_sessions) totals
|
|
31496
|
+
left join lateral (
|
|
31497
|
+
select
|
|
31498
|
+
ranked_sessions.id,
|
|
31499
|
+
left(ranked_sessions.title, ${SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS}) as title,
|
|
31500
|
+
char_length(ranked_sessions.title)::integer as "titleOriginalChars",
|
|
31501
|
+
ranked_sessions.parent_session_id as "parentSessionId",
|
|
31502
|
+
ranked_sessions.root_session_id as "rootSessionId",
|
|
31503
|
+
ranked_sessions.nested_agent_depth as "nestedAgentDepth",
|
|
31504
|
+
ranked_sessions.status,
|
|
31505
|
+
ranked_sessions.created_at as "createdAt",
|
|
31506
|
+
ranked_sessions.updated_at as "updatedAt",
|
|
31507
|
+
${sortRevision} as "sortRevision",
|
|
31508
|
+
${sortAt} as "sortAt",
|
|
31509
|
+
ranked_sessions.match_rank as "sortRank",
|
|
31510
|
+
ranked_sessions.match_class as "matchClass",
|
|
31511
|
+
ranked_sessions.matched_field as "matchedField",
|
|
31512
|
+
ranked_sessions.score_band as "scoreBand",
|
|
31513
|
+
ranked_sessions.matched_claim_id as "matchedClaimId"
|
|
31514
|
+
from ranked_sessions
|
|
31515
|
+
where ${cursorPredicate}
|
|
31516
|
+
order by ${pageOrder}
|
|
31517
|
+
limit ${options.limit + 1}
|
|
31518
|
+
) page on true
|
|
31519
|
+
order by ${outerOrder}
|
|
31520
|
+
`,
|
|
31521
|
+
);
|
|
31522
|
+
const total = Number(result[0]?.total ?? 0);
|
|
31523
|
+
return { rows: result.filter((row) => row.id !== null), total };
|
|
31524
|
+
}
|
|
31525
|
+
|
|
31526
|
+
async function sessionDiscoveryClaimsForSessions(
|
|
31527
|
+
db: Database,
|
|
31528
|
+
workspaceId: string,
|
|
31529
|
+
sessionIds: string[],
|
|
31530
|
+
options: {
|
|
31531
|
+
snapshotAt: string;
|
|
31532
|
+
activeOnly: boolean;
|
|
31533
|
+
recentHours: number | null;
|
|
31534
|
+
limit: number;
|
|
31535
|
+
matchedClaimIds: ReadonlyMap<string, string | null>;
|
|
31536
|
+
},
|
|
31537
|
+
): Promise<Map<string, { claims: WorkClaimDiscoverySummary[]; truncated: boolean }>> {
|
|
31538
|
+
if (sessionIds.length === 0 || options.limit < 1) return new Map();
|
|
31539
|
+
const claimStatePredicate =
|
|
31540
|
+
options.activeOnly || options.recentHours === null
|
|
31541
|
+
? sql`${schema.sessionWorkClaims.state} = 'active'`
|
|
31542
|
+
: sql`(${schema.sessionWorkClaims.state} = 'active' or
|
|
31543
|
+
${schema.sessionWorkClaims.updatedAt} >= ${options.snapshotAt}::text::timestamptz
|
|
31544
|
+
- (${options.recentHours}::integer * interval '1 hour'))`;
|
|
31545
|
+
const rows = await rawRows<SessionDiscoveryClaimRow>(
|
|
31546
|
+
db,
|
|
31547
|
+
sql`
|
|
31548
|
+
with requested_sessions(session_id, matched_claim_id) as (
|
|
31549
|
+
values ${sql.join(
|
|
31550
|
+
sessionIds.map(
|
|
31551
|
+
(sessionId) =>
|
|
31552
|
+
sql`(${sessionId}::uuid, ${options.matchedClaimIds.get(sessionId) ?? null}::uuid)`,
|
|
31553
|
+
),
|
|
31554
|
+
sql`, `,
|
|
31555
|
+
)}
|
|
31556
|
+
)
|
|
31557
|
+
select
|
|
31558
|
+
requested_sessions.session_id as "sessionId",
|
|
31559
|
+
claim.id,
|
|
31560
|
+
claim.subject_namespace as "subjectNamespace",
|
|
31561
|
+
claim.subject_type as "subjectType",
|
|
31562
|
+
claim.canonical_key as "canonicalKey",
|
|
31563
|
+
claim.display_label as "displayLabel",
|
|
31564
|
+
claim.role,
|
|
31565
|
+
claim.state,
|
|
31566
|
+
claim.revision,
|
|
31567
|
+
claim.provenance,
|
|
31568
|
+
claim.version_kind as "versionKind",
|
|
31569
|
+
claim.version_value as "versionValue",
|
|
31570
|
+
claim.observed_at as "observedAt",
|
|
31571
|
+
claim.updated_at as "updatedAt",
|
|
31572
|
+
claim.settled_at as "settledAt",
|
|
31573
|
+
claim.available_count as "availableCount"
|
|
31574
|
+
from requested_sessions
|
|
31575
|
+
cross join lateral (
|
|
31576
|
+
select
|
|
31577
|
+
${schema.sessionWorkClaims.id} as id,
|
|
31578
|
+
${schema.sessionWorkClaims.subjectNamespace} as subject_namespace,
|
|
31579
|
+
${schema.sessionWorkClaims.subjectType} as subject_type,
|
|
31580
|
+
${schema.sessionWorkClaims.canonicalKey} as canonical_key,
|
|
31581
|
+
${schema.sessionWorkClaims.displayLabel} as display_label,
|
|
31582
|
+
${schema.sessionWorkClaims.role} as role,
|
|
31583
|
+
${schema.sessionWorkClaims.state} as state,
|
|
31584
|
+
${schema.sessionWorkClaims.revision} as revision,
|
|
31585
|
+
${schema.sessionWorkClaims.provenance} as provenance,
|
|
31586
|
+
${schema.sessionWorkClaims.versionKind} as version_kind,
|
|
31587
|
+
${schema.sessionWorkClaims.versionValue} as version_value,
|
|
31588
|
+
${schema.sessionWorkClaims.observedAt} as observed_at,
|
|
31589
|
+
${schema.sessionWorkClaims.updatedAt} as updated_at,
|
|
31590
|
+
${schema.sessionWorkClaims.settledAt} as settled_at,
|
|
31591
|
+
count(*) over()::integer as available_count
|
|
31592
|
+
from ${schema.sessionWorkClaims}
|
|
31593
|
+
where ${schema.sessionWorkClaims.workspaceId} = ${workspaceId}
|
|
31594
|
+
and ${schema.sessionWorkClaims.sessionId} = requested_sessions.session_id
|
|
31595
|
+
and ${claimStatePredicate}
|
|
31596
|
+
order by
|
|
31597
|
+
(
|
|
31598
|
+
requested_sessions.matched_claim_id is not null
|
|
31599
|
+
and ${schema.sessionWorkClaims.id} = requested_sessions.matched_claim_id
|
|
31600
|
+
) desc,
|
|
31601
|
+
(${schema.sessionWorkClaims.state} = 'active') desc,
|
|
31602
|
+
${schema.sessionWorkClaims.updatedAt} desc,
|
|
31603
|
+
${schema.sessionWorkClaims.id} desc
|
|
31604
|
+
limit ${options.limit}
|
|
31605
|
+
) claim
|
|
31606
|
+
order by requested_sessions.session_id,
|
|
31607
|
+
(
|
|
31608
|
+
requested_sessions.matched_claim_id is not null
|
|
31609
|
+
and claim.id = requested_sessions.matched_claim_id
|
|
31610
|
+
) desc,
|
|
31611
|
+
(claim.state = 'active') desc, claim.updated_at desc, claim.id desc
|
|
31612
|
+
`,
|
|
31613
|
+
);
|
|
31614
|
+
const grouped = new Map<string, { claims: WorkClaimDiscoverySummary[]; truncated: boolean }>();
|
|
31615
|
+
for (const row of rows) {
|
|
31616
|
+
const entry = grouped.get(row.sessionId) ?? { claims: [], truncated: false };
|
|
31617
|
+
entry.claims.push(
|
|
31618
|
+
WorkClaimDiscoverySummarySchema.parse({
|
|
31619
|
+
id: row.id,
|
|
31620
|
+
sessionId: row.sessionId,
|
|
31621
|
+
subject: {
|
|
31622
|
+
namespace: row.subjectNamespace,
|
|
31623
|
+
type: row.subjectType,
|
|
31624
|
+
canonicalKey: row.canonicalKey,
|
|
31625
|
+
displayLabel: row.displayLabel,
|
|
31626
|
+
},
|
|
31627
|
+
role: row.role,
|
|
31628
|
+
state: row.state,
|
|
31629
|
+
revision: Number(row.revision),
|
|
31630
|
+
provenance: row.provenance,
|
|
31631
|
+
version:
|
|
31632
|
+
row.versionKind === null || row.versionValue === null
|
|
31633
|
+
? null
|
|
31634
|
+
: { kind: row.versionKind, value: row.versionValue },
|
|
31635
|
+
observedAt: sessionDiscoveryIso(row.observedAt),
|
|
31636
|
+
updatedAt: sessionDiscoveryIso(row.updatedAt),
|
|
31637
|
+
settledAt: row.settledAt === null ? null : sessionDiscoveryIso(row.settledAt),
|
|
31638
|
+
}),
|
|
31639
|
+
);
|
|
31640
|
+
entry.truncated = Number(row.availableCount) > options.limit;
|
|
31641
|
+
grouped.set(row.sessionId, entry);
|
|
31642
|
+
}
|
|
31643
|
+
return grouped;
|
|
31644
|
+
}
|
|
31645
|
+
|
|
30897
31646
|
/**
|
|
30898
31647
|
* Excludes a human/API `user.message` whose accepted turn was never claimed.
|
|
30899
31648
|
* Such a prompt was never model input: it is waiting work (represented by
|
|
@@ -30957,7 +31706,17 @@ export async function listSessionDiscoverySummaries(
|
|
|
30957
31706
|
orderBy?: SessionDiscoveryOrderBy;
|
|
30958
31707
|
updatedAfter?: string;
|
|
30959
31708
|
parentSessionId?: string | null;
|
|
31709
|
+
rootSessionId?: string;
|
|
31710
|
+
query?: string;
|
|
31711
|
+
/** @deprecated use query; retained for the existing topology route. */
|
|
30960
31712
|
search?: string;
|
|
31713
|
+
statuses?: SessionStatus[];
|
|
31714
|
+
activeOnly?: boolean;
|
|
31715
|
+
recentHours?: number;
|
|
31716
|
+
subject?: WorkClaimSubjectFilter;
|
|
31717
|
+
claimLimit?: number;
|
|
31718
|
+
/** Skip advisory claim reads for an operator-disabled discovery rollout. */
|
|
31719
|
+
includeWorkDiscovery?: boolean;
|
|
30961
31720
|
subjectId?: string;
|
|
30962
31721
|
authorizationScope?: SessionAuthorizationListScope;
|
|
30963
31722
|
},
|
|
@@ -30971,330 +31730,306 @@ export async function listSessionDiscoverySummaries(
|
|
|
30971
31730
|
snapshotRevision: string;
|
|
30972
31731
|
updatedThrough: string | null;
|
|
30973
31732
|
updatedAfter: string | null;
|
|
31733
|
+
/** Internal relevance cursor binding; omitted from row projections. */
|
|
31734
|
+
filterHash: string | null;
|
|
30974
31735
|
}> {
|
|
30975
31736
|
const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
|
|
30976
|
-
const
|
|
30977
|
-
|
|
30978
|
-
|
|
30979
|
-
|
|
30980
|
-
|
|
30981
|
-
|
|
30982
|
-
|
|
30983
|
-
|
|
30984
|
-
|
|
30985
|
-
|
|
30986
|
-
|
|
30987
|
-
|
|
30988
|
-
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
31000
|
-
|
|
31001
|
-
|
|
31002
|
-
|
|
31003
|
-
|
|
31004
|
-
|
|
31005
|
-
|
|
31006
|
-
|
|
31007
|
-
|
|
31008
|
-
|
|
31009
|
-
|
|
31010
|
-
options.cursor.snapshotRevision,
|
|
31011
|
-
"cursor snapshot revision",
|
|
31012
|
-
)
|
|
31013
|
-
: await readWorkspaceSessionActivityRevision(scopedDb, workspaceId)
|
|
31014
|
-
: "0";
|
|
31015
|
-
const cursorSortRevision = options.cursor
|
|
31016
|
-
? normalizeSessionActivityRevision(options.cursor.sortRevision, "cursor sort revision")
|
|
31017
|
-
: null;
|
|
31737
|
+
const claimLimit = Math.max(
|
|
31738
|
+
1,
|
|
31739
|
+
Math.min(
|
|
31740
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
31741
|
+
Math.floor(options.claimLimit ?? WORK_CLAIM_DISCOVERY_DEFAULT_LIMIT),
|
|
31742
|
+
),
|
|
31743
|
+
);
|
|
31744
|
+
const filters = normalizeSessionDiscoveryFilters(options);
|
|
31745
|
+
const relevanceRequested = filters.query !== null || filters.subject !== null;
|
|
31746
|
+
const orderBy =
|
|
31747
|
+
options.orderBy ?? options.cursor?.orderBy ?? (relevanceRequested ? "relevance" : "createdAt");
|
|
31748
|
+
if (relevanceRequested && orderBy !== "relevance") {
|
|
31749
|
+
throw new Error("sessions_list query and exact subject filters require relevance order");
|
|
31750
|
+
}
|
|
31751
|
+
if (!relevanceRequested && orderBy === "relevance") {
|
|
31752
|
+
throw new Error("sessions_list relevance order requires query or an exact subject filter");
|
|
31753
|
+
}
|
|
31754
|
+
const filterHash = relevanceRequested ? sessionDiscoveryFilterHash(filters) : null;
|
|
31755
|
+
const readPage = async (scopedDb: Database) => {
|
|
31756
|
+
const requestedUpdatedAfter = options.updatedAfter ?? options.cursor?.updatedAfter ?? null;
|
|
31757
|
+
const updatedAfter =
|
|
31758
|
+
requestedUpdatedAfter === null
|
|
31759
|
+
? null
|
|
31760
|
+
: normalizeSessionActivityRevision(requestedUpdatedAfter, "updatedAfter");
|
|
31761
|
+
if (options.cursor?.orderBy !== undefined && options.cursor.orderBy !== orderBy) {
|
|
31762
|
+
throw new Error("sessions_list cursor order does not match the request");
|
|
31763
|
+
}
|
|
31764
|
+
if (options.cursor && options.cursor.updatedAfter !== updatedAfter) {
|
|
31765
|
+
throw new Error("sessions_list cursor incremental filter does not match the request");
|
|
31766
|
+
}
|
|
31767
|
+
if (updatedAfter !== null && orderBy !== "updatedAt") {
|
|
31768
|
+
throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
|
|
31769
|
+
}
|
|
31770
|
+
if (options.cursor && orderBy === "relevance") {
|
|
31018
31771
|
if (
|
|
31019
|
-
|
|
31020
|
-
options.cursor
|
|
31021
|
-
|
|
31772
|
+
!Number.isSafeInteger(options.cursor.sortRank) ||
|
|
31773
|
+
options.cursor.sortRank === null ||
|
|
31774
|
+
options.cursor.sortRank < 0
|
|
31022
31775
|
) {
|
|
31023
|
-
throw new Error("sessions_list
|
|
31024
|
-
}
|
|
31025
|
-
const cursorPredicate = options.cursor
|
|
31026
|
-
? orderBy === "updatedAt"
|
|
31027
|
-
? or(
|
|
31028
|
-
sql`${schema.sessions.activityRevision} < ${cursorSortRevision!}::text::bigint`,
|
|
31029
|
-
and(
|
|
31030
|
-
sql`${schema.sessions.activityRevision} = ${cursorSortRevision!}::text::bigint`,
|
|
31031
|
-
or(
|
|
31032
|
-
sql`${schema.sessions.updatedAt} < ${options.cursor.sortAt}::text::timestamptz`,
|
|
31033
|
-
and(
|
|
31034
|
-
sql`${schema.sessions.updatedAt} = ${options.cursor.sortAt}::text::timestamptz`,
|
|
31035
|
-
lt(schema.sessions.id, options.cursor.id),
|
|
31036
|
-
),
|
|
31037
|
-
),
|
|
31038
|
-
),
|
|
31039
|
-
)
|
|
31040
|
-
: or(
|
|
31041
|
-
// Cast through text deliberately. postgres.js otherwise infers a
|
|
31042
|
-
// timestamptz parameter and serializes this exact cursor string via
|
|
31043
|
-
// JS Date, which discards PostgreSQL's sub-millisecond precision.
|
|
31044
|
-
sql`${schema.sessions.createdAt} < ${options.cursor.sortAt}::text::timestamptz`,
|
|
31045
|
-
and(
|
|
31046
|
-
sql`${schema.sessions.createdAt} = ${options.cursor.sortAt}::text::timestamptz`,
|
|
31047
|
-
lt(schema.sessions.id, options.cursor.id),
|
|
31048
|
-
),
|
|
31049
|
-
)
|
|
31050
|
-
: undefined;
|
|
31051
|
-
const snapshotFilters: SQL[] = [eq(schema.sessions.workspaceId, workspaceId)];
|
|
31052
|
-
if (options.subjectId) {
|
|
31053
|
-
snapshotFilters.push(sql`not exists (
|
|
31054
|
-
select 1
|
|
31055
|
-
from ${schema.slackInteractions} private_slack_interaction
|
|
31056
|
-
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
31057
|
-
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
31058
|
-
and private_slack_interaction.visibility = 'private'
|
|
31059
|
-
and private_slack_interaction.owning_subject_id <> ${options.subjectId}
|
|
31060
|
-
)`);
|
|
31061
|
-
}
|
|
31062
|
-
if (options.authorizationScope) {
|
|
31063
|
-
snapshotFilters.push(sessionAuthorizationScopeFilter(options.authorizationScope));
|
|
31064
|
-
}
|
|
31065
|
-
if (Object.prototype.hasOwnProperty.call(options, "parentSessionId")) {
|
|
31066
|
-
snapshotFilters.push(
|
|
31067
|
-
options.parentSessionId === null
|
|
31068
|
-
? isNull(schema.sessions.parentSessionId)
|
|
31069
|
-
: eq(schema.sessions.parentSessionId, options.parentSessionId!),
|
|
31070
|
-
);
|
|
31071
|
-
}
|
|
31072
|
-
const search = options.search?.trim();
|
|
31073
|
-
if (search) {
|
|
31074
|
-
const pattern = `%${search
|
|
31075
|
-
.replaceAll("\\", "\\\\")
|
|
31076
|
-
.replaceAll("%", "\\%")
|
|
31077
|
-
.replaceAll("_", "\\_")}%`;
|
|
31078
|
-
snapshotFilters.push(
|
|
31079
|
-
or(
|
|
31080
|
-
ilike(schema.sessions.title, pattern),
|
|
31081
|
-
ilike(schema.sessions.initialMessage, pattern),
|
|
31082
|
-
)!,
|
|
31083
|
-
);
|
|
31776
|
+
throw new Error("sessions_list relevance cursor rank is invalid");
|
|
31084
31777
|
}
|
|
31085
|
-
|
|
31086
|
-
|
|
31087
|
-
? sql`${schema.sessions.activityRevision} <= ${snapshotRevision}::text::bigint`
|
|
31088
|
-
: sql`${schema.sessions.createdAt} <= ${snapshotAt}::text::timestamptz`,
|
|
31089
|
-
);
|
|
31090
|
-
if (updatedAfter !== null) {
|
|
31091
|
-
snapshotFilters.push(
|
|
31092
|
-
sql`${schema.sessions.activityRevision} > ${updatedAfter}::text::bigint`,
|
|
31093
|
-
);
|
|
31778
|
+
if (options.cursor.filterHash !== filterHash) {
|
|
31779
|
+
throw new Error("sessions_list cursor relevance filters do not match the request");
|
|
31094
31780
|
}
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
|
|
31105
|
-
|
|
31106
|
-
|
|
31107
|
-
|
|
31108
|
-
|
|
31109
|
-
|
|
31110
|
-
? sql<string>`${schema.sessions.activityRevision}::text`
|
|
31111
|
-
: sql<string>`'0'`,
|
|
31112
|
-
sortAt:
|
|
31113
|
-
orderBy === "updatedAt"
|
|
31114
|
-
? sql<string>`to_char(${schema.sessions.updatedAt} at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
|
|
31115
|
-
: sql<string>`to_char(${schema.sessions.createdAt} at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
|
|
31116
|
-
})
|
|
31117
|
-
.from(schema.sessions)
|
|
31118
|
-
.where(and(...snapshotFilters, cursorPredicate))
|
|
31119
|
-
.orderBy(
|
|
31120
|
-
...(orderBy === "updatedAt"
|
|
31121
|
-
? [
|
|
31122
|
-
desc(schema.sessions.activityRevision),
|
|
31123
|
-
desc(schema.sessions.updatedAt),
|
|
31124
|
-
desc(schema.sessions.id),
|
|
31125
|
-
]
|
|
31126
|
-
: [desc(schema.sessions.createdAt), desc(schema.sessions.id)]),
|
|
31781
|
+
} else if (
|
|
31782
|
+
options.cursor &&
|
|
31783
|
+
(options.cursor.sortRank !== null || options.cursor.filterHash !== null)
|
|
31784
|
+
) {
|
|
31785
|
+
throw new Error("sessions_list chronological cursor cannot carry relevance fields");
|
|
31786
|
+
}
|
|
31787
|
+
const snapshotAt =
|
|
31788
|
+
options.cursor?.snapshotAt ??
|
|
31789
|
+
(
|
|
31790
|
+
await rawRows<{ value: string }>(
|
|
31791
|
+
scopedDb,
|
|
31792
|
+
// The transaction-start cutoff is no later than any statement or
|
|
31793
|
+
// repeatable-read snapshot used below. Later rows therefore belong
|
|
31794
|
+
// to the next traversal instead of entering this cursor mid-scan.
|
|
31795
|
+
sql`select to_char(transaction_timestamp() at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') as value`,
|
|
31127
31796
|
)
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31131
|
-
|
|
31132
|
-
|
|
31133
|
-
|
|
31134
|
-
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
|
|
31141
|
-
|
|
31142
|
-
|
|
31143
|
-
|
|
31144
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
31147
|
-
|
|
31148
|
-
|
|
31797
|
+
)[0]!.value;
|
|
31798
|
+
const snapshotRevision =
|
|
31799
|
+
orderBy !== "createdAt"
|
|
31800
|
+
? options.cursor?.snapshotRevision !== undefined
|
|
31801
|
+
? normalizeSessionActivityRevision(
|
|
31802
|
+
options.cursor.snapshotRevision,
|
|
31803
|
+
"cursor snapshot revision",
|
|
31804
|
+
)
|
|
31805
|
+
: await readWorkspaceSessionActivityRevision(scopedDb, workspaceId)
|
|
31806
|
+
: "0";
|
|
31807
|
+
const cursorSortRevision = options.cursor
|
|
31808
|
+
? normalizeSessionActivityRevision(options.cursor.sortRevision, "cursor sort revision")
|
|
31809
|
+
: null;
|
|
31810
|
+
if (
|
|
31811
|
+
orderBy === "createdAt" &&
|
|
31812
|
+
options.cursor &&
|
|
31813
|
+
(cursorSortRevision !== "0" ||
|
|
31814
|
+
options.cursor.snapshotRevision !== "0" ||
|
|
31815
|
+
options.cursor.sortRank !== null ||
|
|
31816
|
+
options.cursor.filterHash !== null)
|
|
31817
|
+
) {
|
|
31818
|
+
throw new Error("sessions_list created-order cursor cannot carry activity revisions");
|
|
31819
|
+
}
|
|
31820
|
+
const selected = await selectSessionDiscoveryPageRows(scopedDb, workspaceId, {
|
|
31821
|
+
limit,
|
|
31822
|
+
orderBy,
|
|
31823
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
31824
|
+
snapshotAt,
|
|
31825
|
+
snapshotRevision,
|
|
31826
|
+
updatedAfter,
|
|
31827
|
+
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
31828
|
+
...(options.authorizationScope ? { authorizationScope: options.authorizationScope } : {}),
|
|
31829
|
+
filters,
|
|
31830
|
+
});
|
|
31831
|
+
const hasMore = selected.rows.length > limit;
|
|
31832
|
+
const page = selected.rows.slice(0, limit);
|
|
31833
|
+
const ids = page.map((row) => row.id!);
|
|
31834
|
+
if (ids.length === 0) {
|
|
31835
|
+
return {
|
|
31836
|
+
sessions: [],
|
|
31837
|
+
hasMore: false,
|
|
31838
|
+
nextCursor: null,
|
|
31839
|
+
total: selected.total,
|
|
31840
|
+
orderBy,
|
|
31841
|
+
snapshotAt,
|
|
31842
|
+
snapshotRevision,
|
|
31843
|
+
updatedThrough: orderBy === "updatedAt" ? snapshotRevision : null,
|
|
31844
|
+
updatedAfter,
|
|
31845
|
+
filterHash,
|
|
31846
|
+
};
|
|
31847
|
+
}
|
|
31149
31848
|
|
|
31150
|
-
|
|
31151
|
-
|
|
31152
|
-
|
|
31153
|
-
|
|
31154
|
-
|
|
31155
|
-
|
|
31156
|
-
|
|
31157
|
-
|
|
31158
|
-
|
|
31159
|
-
|
|
31160
|
-
|
|
31161
|
-
|
|
31162
|
-
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
})
|
|
31167
|
-
.from(schema.sessionGoals)
|
|
31168
|
-
.where(
|
|
31169
|
-
and(
|
|
31170
|
-
eq(schema.sessionGoals.workspaceId, workspaceId),
|
|
31171
|
-
inArray(schema.sessionGoals.sessionId, ids),
|
|
31172
|
-
),
|
|
31173
|
-
);
|
|
31174
|
-
const goalsBySession = new Map(goals.map((goal) => [goal.sessionId, goal]));
|
|
31175
|
-
const queueCounts = await scopedDb
|
|
31176
|
-
.select({
|
|
31177
|
-
sessionId: schema.sessionTurns.sessionId,
|
|
31178
|
-
count: sql<number>`count(*)::int`,
|
|
31849
|
+
const rootRelatedIds = await sessionIdsCoveredByAuthorizationRoots(
|
|
31850
|
+
scopedDb,
|
|
31851
|
+
workspaceId,
|
|
31852
|
+
ids,
|
|
31853
|
+
options.authorizationScope,
|
|
31854
|
+
);
|
|
31855
|
+
const controls = await evaluateSessionDiscoveryControls(scopedDb, workspaceId, ids);
|
|
31856
|
+
const treeStats = await sessionTreeStatsForSessions(scopedDb, workspaceId, [...rootRelatedIds]);
|
|
31857
|
+
const includeWorkDiscovery = options.includeWorkDiscovery !== false;
|
|
31858
|
+
const workClaims = includeWorkDiscovery
|
|
31859
|
+
? await sessionDiscoveryClaimsForSessions(scopedDb, workspaceId, ids, {
|
|
31860
|
+
snapshotAt,
|
|
31861
|
+
activeOnly: filters.activeOnly,
|
|
31862
|
+
recentHours: filters.recentHours,
|
|
31863
|
+
limit: claimLimit,
|
|
31864
|
+
matchedClaimIds: new Map(page.map((row) => [row.id!, row.matchedClaimId] as const)),
|
|
31179
31865
|
})
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31866
|
+
: new Map<string, { claims: WorkClaimDiscoverySummary[]; truncated: boolean }>();
|
|
31867
|
+
const goals = await scopedDb
|
|
31868
|
+
.select({
|
|
31869
|
+
sessionId: schema.sessionGoals.sessionId,
|
|
31870
|
+
status: schema.sessionGoals.status,
|
|
31871
|
+
text: sql<string>`left(${schema.sessionGoals.text}, ${SESSION_DISCOVERY_GOAL_MAX_CHARS})`,
|
|
31872
|
+
textOriginalChars: sql<number>`char_length(${schema.sessionGoals.text})::integer`,
|
|
31873
|
+
})
|
|
31874
|
+
.from(schema.sessionGoals)
|
|
31875
|
+
.where(
|
|
31876
|
+
and(
|
|
31877
|
+
eq(schema.sessionGoals.workspaceId, workspaceId),
|
|
31878
|
+
inArray(schema.sessionGoals.sessionId, ids),
|
|
31879
|
+
),
|
|
31192
31880
|
);
|
|
31193
|
-
|
|
31194
|
-
|
|
31195
|
-
|
|
31196
|
-
|
|
31197
|
-
|
|
31198
|
-
|
|
31199
|
-
|
|
31200
|
-
|
|
31201
|
-
|
|
31881
|
+
const goalsBySession = new Map(goals.map((goal) => [goal.sessionId, goal]));
|
|
31882
|
+
const queueCounts = await scopedDb
|
|
31883
|
+
.select({
|
|
31884
|
+
sessionId: schema.sessionTurns.sessionId,
|
|
31885
|
+
count: sql<number>`count(*)::int`,
|
|
31886
|
+
})
|
|
31887
|
+
.from(schema.sessionTurns)
|
|
31888
|
+
.where(
|
|
31889
|
+
and(
|
|
31890
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
31891
|
+
inArray(schema.sessionTurns.sessionId, ids),
|
|
31892
|
+
eq(schema.sessionTurns.status, "queued"),
|
|
31893
|
+
inArray(schema.sessionTurns.source, ["user", "api"]),
|
|
31894
|
+
),
|
|
31895
|
+
)
|
|
31896
|
+
.groupBy(schema.sessionTurns.sessionId);
|
|
31897
|
+
const queueBySession = new Map(
|
|
31898
|
+
queueCounts.map((entry) => [entry.sessionId, Number(entry.count)]),
|
|
31899
|
+
);
|
|
31900
|
+
const latestMessages = options.includeLastMessage
|
|
31901
|
+
? await scopedDb
|
|
31902
|
+
.selectDistinctOn([schema.sessionEvents.sessionId], {
|
|
31903
|
+
sessionId: schema.sessionEvents.sessionId,
|
|
31904
|
+
type: schema.sessionEvents.type,
|
|
31905
|
+
// Extract only the bounded textual preview in PostgreSQL. Selecting
|
|
31906
|
+
// the JSON payload here would re-materialize the exact multi-MB
|
|
31907
|
+
// event bodies this compact discovery path exists to avoid.
|
|
31908
|
+
preview: sql<string | null>`left(coalesce(
|
|
31202
31909
|
${schema.sessionEvents.payload}->>'text',
|
|
31203
31910
|
${schema.sessionEvents.payload}->>'message',
|
|
31204
31911
|
${schema.sessionEvents.payload}->>'content'
|
|
31205
31912
|
), ${SESSION_DISCOVERY_MESSAGE_MAX_CHARS})`,
|
|
31206
|
-
|
|
31913
|
+
previewOriginalChars: sql<number | null>`char_length(coalesce(
|
|
31207
31914
|
${schema.sessionEvents.payload}->>'text',
|
|
31208
31915
|
${schema.sessionEvents.payload}->>'message',
|
|
31209
31916
|
${schema.sessionEvents.payload}->>'content'
|
|
31210
31917
|
))::integer`,
|
|
31211
|
-
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
|
|
31240
|
-
|
|
31241
|
-
|
|
31242
|
-
|
|
31243
|
-
|
|
31244
|
-
goal: goal
|
|
31245
|
-
? {
|
|
31246
|
-
status: goal.status as SessionGoalStatus,
|
|
31247
|
-
text: goal.text,
|
|
31248
|
-
textOriginalChars: Number(goal.textOriginalChars),
|
|
31249
|
-
}
|
|
31250
|
-
: null,
|
|
31251
|
-
queuedPromptCount: queueBySession.get(row.id) ?? 0,
|
|
31252
|
-
treeStats:
|
|
31253
|
-
relatedAccess === "root"
|
|
31254
|
-
? (treeStats.get(row.id) ?? EMPTY_SESSION_TREE_STATS)
|
|
31255
|
-
: EMPTY_SESSION_TREE_STATS,
|
|
31256
|
-
latestMessage: latest
|
|
31257
|
-
? {
|
|
31258
|
-
type: latest.type as SessionEventType,
|
|
31259
|
-
preview: latest.preview,
|
|
31260
|
-
previewOriginalChars:
|
|
31261
|
-
latest.previewOriginalChars === null ? null : Number(latest.previewOriginalChars),
|
|
31262
|
-
}
|
|
31263
|
-
: null,
|
|
31264
|
-
createdAt: row.createdAt.toISOString(),
|
|
31265
|
-
updatedAt: row.updatedAt.toISOString(),
|
|
31266
|
-
sortRevision: row.sortRevision,
|
|
31267
|
-
sortAt: row.sortAt,
|
|
31268
|
-
};
|
|
31269
|
-
});
|
|
31270
|
-
const last = page.at(-1);
|
|
31918
|
+
})
|
|
31919
|
+
.from(schema.sessionEvents)
|
|
31920
|
+
.where(
|
|
31921
|
+
and(
|
|
31922
|
+
eq(schema.sessionEvents.workspaceId, workspaceId),
|
|
31923
|
+
inArray(schema.sessionEvents.sessionId, ids),
|
|
31924
|
+
inArray(schema.sessionEvents.type, ["user.message", "agent.message.completed"]),
|
|
31925
|
+
excludeUnclaimedHumanPromptEventFilter(workspaceId),
|
|
31926
|
+
),
|
|
31927
|
+
)
|
|
31928
|
+
.orderBy(schema.sessionEvents.sessionId, desc(schema.sessionEvents.sequence))
|
|
31929
|
+
: [];
|
|
31930
|
+
const latestBySession = new Map(latestMessages.map((entry) => [entry.sessionId, entry]));
|
|
31931
|
+
const sessions = page.map((row): SessionDiscoverySummary => {
|
|
31932
|
+
const sessionId = row.id!;
|
|
31933
|
+
const control = controls.get(sessionId);
|
|
31934
|
+
if (!control) throw new Error(`Effective control missing for session ${sessionId}`);
|
|
31935
|
+
const relatedAccess = rootRelatedIds.has(sessionId) ? "root" : "target";
|
|
31936
|
+
const goal = goalsBySession.get(sessionId);
|
|
31937
|
+
const latest = latestBySession.get(sessionId);
|
|
31938
|
+
const claims = workClaims.get(sessionId) ?? { claims: [], truncated: false };
|
|
31939
|
+
const match =
|
|
31940
|
+
!includeWorkDiscovery ||
|
|
31941
|
+
row.matchClass === null ||
|
|
31942
|
+
row.matchedField === null ||
|
|
31943
|
+
row.scoreBand === null
|
|
31944
|
+
? null
|
|
31945
|
+
: WorkDiscoveryMatchSchema.parse({
|
|
31946
|
+
class: row.matchClass,
|
|
31947
|
+
field: row.matchedField,
|
|
31948
|
+
scoreBand: row.scoreBand,
|
|
31949
|
+
claimId: row.matchedClaimId,
|
|
31950
|
+
});
|
|
31271
31951
|
return {
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31952
|
+
id: sessionId,
|
|
31953
|
+
title: row.title,
|
|
31954
|
+
titleOriginalChars: row.titleOriginalChars === null ? null : Number(row.titleOriginalChars),
|
|
31955
|
+
parentSessionId: relatedAccess === "root" ? row.parentSessionId : null,
|
|
31956
|
+
rootSessionId: relatedAccess === "root" ? row.rootSessionId! : sessionId,
|
|
31957
|
+
nestedAgentDepth: Number(row.nestedAgentDepth),
|
|
31958
|
+
status: row.status as SessionStatus,
|
|
31959
|
+
effectiveControl: projectSessionDiscoveryControlForRelatedAccess(
|
|
31960
|
+
control,
|
|
31961
|
+
sessionId,
|
|
31962
|
+
relatedAccess,
|
|
31963
|
+
),
|
|
31964
|
+
goal: goal
|
|
31965
|
+
? {
|
|
31966
|
+
status: goal.status as SessionGoalStatus,
|
|
31967
|
+
text: goal.text,
|
|
31968
|
+
textOriginalChars: Number(goal.textOriginalChars),
|
|
31969
|
+
}
|
|
31970
|
+
: null,
|
|
31971
|
+
queuedPromptCount: queueBySession.get(sessionId) ?? 0,
|
|
31972
|
+
treeStats:
|
|
31973
|
+
relatedAccess === "root"
|
|
31974
|
+
? (treeStats.get(sessionId) ?? EMPTY_SESSION_TREE_STATS)
|
|
31975
|
+
: EMPTY_SESSION_TREE_STATS,
|
|
31976
|
+
latestMessage: latest
|
|
31977
|
+
? {
|
|
31978
|
+
type: latest.type as SessionEventType,
|
|
31979
|
+
preview: latest.preview,
|
|
31980
|
+
previewOriginalChars:
|
|
31981
|
+
latest.previewOriginalChars === null ? null : Number(latest.previewOriginalChars),
|
|
31982
|
+
}
|
|
31983
|
+
: null,
|
|
31984
|
+
workDiscovery: {
|
|
31985
|
+
claims: claims.claims,
|
|
31986
|
+
claimsTruncated: claims.truncated,
|
|
31987
|
+
match,
|
|
31988
|
+
possibleOverlap: match !== null,
|
|
31989
|
+
advisoryOnly: true,
|
|
31990
|
+
noAdditionalAccess: true,
|
|
31991
|
+
},
|
|
31992
|
+
createdAt: sessionDiscoveryIso(row.createdAt!),
|
|
31993
|
+
updatedAt: sessionDiscoveryIso(row.updatedAt!),
|
|
31994
|
+
sortRevision: row.sortRevision!,
|
|
31995
|
+
sortAt: row.sortAt!,
|
|
31996
|
+
sortRank: row.sortRank === null ? null : Number(row.sortRank),
|
|
31292
31997
|
};
|
|
31293
|
-
}
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
|
|
31297
|
-
|
|
31998
|
+
});
|
|
31999
|
+
const last = page.at(-1);
|
|
32000
|
+
return {
|
|
32001
|
+
sessions,
|
|
32002
|
+
hasMore,
|
|
32003
|
+
nextCursor:
|
|
32004
|
+
hasMore && last
|
|
32005
|
+
? {
|
|
32006
|
+
orderBy,
|
|
32007
|
+
sortRank: last.sortRank === null ? null : Number(last.sortRank),
|
|
32008
|
+
sortRevision: last.sortRevision!,
|
|
32009
|
+
sortAt: last.sortAt!,
|
|
32010
|
+
id: last.id!,
|
|
32011
|
+
snapshotAt,
|
|
32012
|
+
snapshotRevision,
|
|
32013
|
+
updatedAfter,
|
|
32014
|
+
filterHash,
|
|
32015
|
+
}
|
|
32016
|
+
: null,
|
|
32017
|
+
total: selected.total,
|
|
32018
|
+
orderBy,
|
|
32019
|
+
snapshotAt,
|
|
32020
|
+
snapshotRevision,
|
|
32021
|
+
updatedThrough: orderBy === "updatedAt" ? snapshotRevision : null,
|
|
32022
|
+
updatedAfter,
|
|
32023
|
+
filterHash,
|
|
32024
|
+
};
|
|
32025
|
+
};
|
|
32026
|
+
const transactionConfig: PgTransactionConfig =
|
|
32027
|
+
orderBy === "createdAt"
|
|
32028
|
+
? { isolationLevel: "read committed", accessMode: "read only" }
|
|
32029
|
+
: { isolationLevel: "repeatable read", accessMode: "read only" };
|
|
32030
|
+
return options.subjectId
|
|
32031
|
+
? await withWorkspaceSubjectRls(db, workspaceId, options.subjectId, readPage, transactionConfig)
|
|
32032
|
+
: await withWorkspaceRls(db, workspaceId, readPage, transactionConfig);
|
|
31298
32033
|
}
|
|
31299
32034
|
|
|
31300
32035
|
export type SessionDiscoveryAncestor = {
|
|
@@ -31313,10 +32048,34 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31313
32048
|
workspaceId: string,
|
|
31314
32049
|
sessionIds: string[],
|
|
31315
32050
|
authorizationScope?: SessionAuthorizationListScope,
|
|
32051
|
+
subjectId?: string,
|
|
31316
32052
|
): Promise<Map<string, SessionDiscoveryAncestor[]>> {
|
|
31317
32053
|
const targetIds = [...new Set(sessionIds)].slice(0, 100);
|
|
31318
32054
|
if (targetIds.length === 0) return new Map();
|
|
31319
|
-
|
|
32055
|
+
const readPaths = async (scopedDb: Database) => {
|
|
32056
|
+
const authorizationFilters: SQL[] = [eq(schema.sessions.workspaceId, workspaceId)];
|
|
32057
|
+
authorizationFilters.push(
|
|
32058
|
+
subjectId
|
|
32059
|
+
? sql`not exists (
|
|
32060
|
+
select 1
|
|
32061
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
32062
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
32063
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
32064
|
+
and private_slack_interaction.visibility = 'private'
|
|
32065
|
+
and private_slack_interaction.owning_subject_id <> ${subjectId}
|
|
32066
|
+
)`
|
|
32067
|
+
: sql`not exists (
|
|
32068
|
+
select 1
|
|
32069
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
32070
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
32071
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
32072
|
+
and private_slack_interaction.visibility = 'private'
|
|
32073
|
+
)`,
|
|
32074
|
+
);
|
|
32075
|
+
if (authorizationScope) {
|
|
32076
|
+
authorizationFilters.push(sessionAuthorizationScopeFilter(authorizationScope));
|
|
32077
|
+
}
|
|
32078
|
+
const authorizedPredicate = and(...authorizationFilters) ?? sql`false`;
|
|
31320
32079
|
const rows = await rawRows<{
|
|
31321
32080
|
targetId: string;
|
|
31322
32081
|
id: string;
|
|
@@ -31327,7 +32086,15 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31327
32086
|
}>(
|
|
31328
32087
|
scopedDb,
|
|
31329
32088
|
sql`
|
|
31330
|
-
with recursive
|
|
32089
|
+
with recursive authorized_sessions as materialized (
|
|
32090
|
+
select
|
|
32091
|
+
${schema.sessions.id} as id,
|
|
32092
|
+
${schema.sessions.parentSessionId} as parent_session_id,
|
|
32093
|
+
${schema.sessions.title} as title
|
|
32094
|
+
from ${schema.sessions}
|
|
32095
|
+
where ${authorizedPredicate}
|
|
32096
|
+
),
|
|
32097
|
+
lineage as (
|
|
31331
32098
|
select
|
|
31332
32099
|
target.id as target_id,
|
|
31333
32100
|
target.id,
|
|
@@ -31336,9 +32103,8 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31336
32103
|
0::integer as depth,
|
|
31337
32104
|
array[target.id]::uuid[] as path,
|
|
31338
32105
|
false as cycle
|
|
31339
|
-
from
|
|
31340
|
-
where target.
|
|
31341
|
-
and ${inArray(sql`target.id`, targetIds)}
|
|
32106
|
+
from authorized_sessions target
|
|
32107
|
+
where ${inArray(sql`target.id`, targetIds)}
|
|
31342
32108
|
|
|
31343
32109
|
union all
|
|
31344
32110
|
|
|
@@ -31351,9 +32117,7 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31351
32117
|
child.path || parent.id,
|
|
31352
32118
|
parent.id = any(child.path)
|
|
31353
32119
|
from lineage child
|
|
31354
|
-
join
|
|
31355
|
-
on parent.workspace_id = ${workspaceId}
|
|
31356
|
-
and parent.id = child.parent_session_id
|
|
32120
|
+
join authorized_sessions parent on parent.id = child.parent_session_id
|
|
31357
32121
|
where not child.cycle
|
|
31358
32122
|
and child.depth < 64
|
|
31359
32123
|
)
|
|
@@ -31369,24 +32133,9 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31369
32133
|
order by target_id, depth desc
|
|
31370
32134
|
`,
|
|
31371
32135
|
);
|
|
31372
|
-
const candidateIds = [...new Set(rows.filter((row) => !row.cycle).map((row) => row.id))];
|
|
31373
|
-
let allowed = new Set(candidateIds);
|
|
31374
|
-
if (authorizationScope && authorizationScope.kind !== "all" && candidateIds.length > 0) {
|
|
31375
|
-
const allowedRows = await scopedDb
|
|
31376
|
-
.select({ id: schema.sessions.id })
|
|
31377
|
-
.from(schema.sessions)
|
|
31378
|
-
.where(
|
|
31379
|
-
and(
|
|
31380
|
-
eq(schema.sessions.workspaceId, workspaceId),
|
|
31381
|
-
inArray(schema.sessions.id, candidateIds),
|
|
31382
|
-
sessionAuthorizationScopeFilter(authorizationScope),
|
|
31383
|
-
),
|
|
31384
|
-
);
|
|
31385
|
-
allowed = new Set(allowedRows.map((row) => row.id));
|
|
31386
|
-
}
|
|
31387
32136
|
const result = new Map<string, SessionDiscoveryAncestor[]>();
|
|
31388
32137
|
for (const row of rows) {
|
|
31389
|
-
if (row.cycle
|
|
32138
|
+
if (row.cycle) continue;
|
|
31390
32139
|
const path = result.get(row.targetId) ?? [];
|
|
31391
32140
|
path.push({
|
|
31392
32141
|
id: row.id,
|
|
@@ -31396,7 +32145,10 @@ export async function listSessionDiscoveryAncestorPaths(
|
|
|
31396
32145
|
result.set(row.targetId, path);
|
|
31397
32146
|
}
|
|
31398
32147
|
return result;
|
|
31399
|
-
}
|
|
32148
|
+
};
|
|
32149
|
+
return subjectId
|
|
32150
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, readPaths)
|
|
32151
|
+
: await withWorkspaceRls(db, workspaceId, readPaths);
|
|
31400
32152
|
}
|
|
31401
32153
|
|
|
31402
32154
|
export type SessionLineage = {
|
|
@@ -32589,6 +33341,45 @@ export async function listSessionHumanInputRequests(
|
|
|
32589
33341
|
): Promise<SessionHumanInputRequest[]> {
|
|
32590
33342
|
const limit = Math.min(100, Math.max(1, Math.floor(options.limit ?? 50)));
|
|
32591
33343
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
33344
|
+
const actionablePendingIds =
|
|
33345
|
+
options.status === "pending"
|
|
33346
|
+
? await scopedDb.execute<{ id: string }>(sql`
|
|
33347
|
+
select request_row.id
|
|
33348
|
+
from session_human_input_requests request_row
|
|
33349
|
+
join sessions session_row
|
|
33350
|
+
on session_row.workspace_id = request_row.workspace_id
|
|
33351
|
+
and session_row.id = request_row.session_id
|
|
33352
|
+
join session_turns turn_row
|
|
33353
|
+
on turn_row.workspace_id = request_row.workspace_id
|
|
33354
|
+
and turn_row.session_id = request_row.session_id
|
|
33355
|
+
and turn_row.id = request_row.turn_id
|
|
33356
|
+
join session_events trigger_row
|
|
33357
|
+
on trigger_row.workspace_id = turn_row.workspace_id
|
|
33358
|
+
and trigger_row.session_id = turn_row.session_id
|
|
33359
|
+
and trigger_row.id = turn_row.trigger_event_id
|
|
33360
|
+
where request_row.workspace_id = ${workspaceId}
|
|
33361
|
+
and request_row.session_id = ${sessionId}
|
|
33362
|
+
and request_row.status = 'pending'
|
|
33363
|
+
and (request_row.expires_at is null or request_row.expires_at > now())
|
|
33364
|
+
and session_row.status = 'requires_action'
|
|
33365
|
+
and session_row.active_turn_id = request_row.turn_id
|
|
33366
|
+
and turn_row.status = 'requires_action'
|
|
33367
|
+
and turn_row.execution_generation = request_row.turn_generation
|
|
33368
|
+
and not exists (
|
|
33369
|
+
select 1
|
|
33370
|
+
from session_events accepted_row
|
|
33371
|
+
where accepted_row.workspace_id = request_row.workspace_id
|
|
33372
|
+
and accepted_row.session_id = request_row.session_id
|
|
33373
|
+
and accepted_row.turn_id = request_row.turn_id
|
|
33374
|
+
and accepted_row.turn_generation = request_row.turn_generation
|
|
33375
|
+
and accepted_row.type in ('user.approvalDecision', 'user.humanInputResponse')
|
|
33376
|
+
and accepted_row.sequence > trigger_row.sequence
|
|
33377
|
+
)
|
|
33378
|
+
order by request_row.created_at desc, request_row.id desc
|
|
33379
|
+
limit ${limit}
|
|
33380
|
+
`)
|
|
33381
|
+
: null;
|
|
33382
|
+
if (actionablePendingIds !== null && actionablePendingIds.length === 0) return [];
|
|
32592
33383
|
const predicates = [
|
|
32593
33384
|
eq(schema.sessionHumanInputRequests.workspaceId, workspaceId),
|
|
32594
33385
|
eq(schema.sessionHumanInputRequests.sessionId, sessionId),
|
|
@@ -32596,6 +33387,14 @@ export async function listSessionHumanInputRequests(
|
|
|
32596
33387
|
if (options.status) {
|
|
32597
33388
|
predicates.push(eq(schema.sessionHumanInputRequests.status, options.status));
|
|
32598
33389
|
}
|
|
33390
|
+
if (actionablePendingIds !== null) {
|
|
33391
|
+
predicates.push(
|
|
33392
|
+
inArray(
|
|
33393
|
+
schema.sessionHumanInputRequests.id,
|
|
33394
|
+
actionablePendingIds.map((row: { id: string }) => row.id),
|
|
33395
|
+
),
|
|
33396
|
+
);
|
|
33397
|
+
}
|
|
32599
33398
|
const rows = await scopedDb
|
|
32600
33399
|
.select()
|
|
32601
33400
|
.from(schema.sessionHumanInputRequests)
|
|
@@ -32721,6 +33520,13 @@ export type AcceptSessionHumanInputResponseResult =
|
|
|
32721
33520
|
events: SessionEvent[];
|
|
32722
33521
|
workflowWakeRevision: number;
|
|
32723
33522
|
}
|
|
33523
|
+
| {
|
|
33524
|
+
action: "completed";
|
|
33525
|
+
request: SessionHumanInputRequest;
|
|
33526
|
+
event: SessionEvent;
|
|
33527
|
+
events: SessionEvent[];
|
|
33528
|
+
workflowWakeRevision: number | null;
|
|
33529
|
+
}
|
|
32724
33530
|
| {
|
|
32725
33531
|
action: "conflict";
|
|
32726
33532
|
request: SessionHumanInputRequest;
|
|
@@ -32774,10 +33580,37 @@ async function hasAcceptedRequiresActionAdvance(
|
|
|
32774
33580
|
return accepted !== undefined;
|
|
32775
33581
|
}
|
|
32776
33582
|
|
|
33583
|
+
async function humanInputResponseEventForRequest(
|
|
33584
|
+
tx: Database,
|
|
33585
|
+
input: {
|
|
33586
|
+
workspaceId: string;
|
|
33587
|
+
sessionId: string;
|
|
33588
|
+
requestId: string;
|
|
33589
|
+
},
|
|
33590
|
+
): Promise<SessionEvent | null> {
|
|
33591
|
+
const [event] = await tx
|
|
33592
|
+
.select()
|
|
33593
|
+
.from(schema.sessionEvents)
|
|
33594
|
+
.where(
|
|
33595
|
+
and(
|
|
33596
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
33597
|
+
eq(schema.sessionEvents.sessionId, input.sessionId),
|
|
33598
|
+
eq(schema.sessionEvents.type, "user.humanInputResponse"),
|
|
33599
|
+
sql`${schema.sessionEvents.payload} ->> 'requestId' = ${input.requestId}`,
|
|
33600
|
+
),
|
|
33601
|
+
)
|
|
33602
|
+
.orderBy(desc(schema.sessionEvents.sequence))
|
|
33603
|
+
.limit(1);
|
|
33604
|
+
return event ? mapEvent(event) : null;
|
|
33605
|
+
}
|
|
33606
|
+
|
|
32777
33607
|
/**
|
|
32778
|
-
* First-writer-wins settlement for one
|
|
32779
|
-
*
|
|
32780
|
-
*
|
|
33608
|
+
* First-writer-wins settlement for one structured input boundary. Session +
|
|
33609
|
+
* logical turn + current freeze generation are locked with the row; the
|
|
33610
|
+
* creation attempt is provenance and is intentionally not the resume owner.
|
|
33611
|
+
* A committed response replays its original event without another event or
|
|
33612
|
+
* workflow wake. Historical cancelled rows missing that canonical event are
|
|
33613
|
+
* repaired exactly once under the same locks, but never wake terminal work.
|
|
32781
33614
|
*/
|
|
32782
33615
|
export async function acceptSessionHumanInputResponse(
|
|
32783
33616
|
db: Database,
|
|
@@ -32845,25 +33678,105 @@ export async function acceptSessionHumanInputResponse(
|
|
|
32845
33678
|
) {
|
|
32846
33679
|
throw new Error("clientEventId belongs to a different session operation");
|
|
32847
33680
|
}
|
|
32848
|
-
const workflowWakeRevision = await enqueueSessionWorkflowWakeInTransaction(
|
|
32849
|
-
tx as unknown as Database,
|
|
32850
|
-
{
|
|
32851
|
-
accountId: session.accountId,
|
|
32852
|
-
workspaceId: input.workspaceId,
|
|
32853
|
-
sessionId: session.id,
|
|
32854
|
-
temporalWorkflowId: session.temporalWorkflowId ?? `session-${session.id}`,
|
|
32855
|
-
reason: "approval_decision",
|
|
32856
|
-
},
|
|
32857
|
-
);
|
|
32858
33681
|
return {
|
|
32859
|
-
action: "
|
|
33682
|
+
action: "completed",
|
|
32860
33683
|
request: mapSessionHumanInputRequest(request),
|
|
32861
33684
|
event: mapEvent(existing),
|
|
32862
33685
|
events: [],
|
|
32863
|
-
workflowWakeRevision,
|
|
33686
|
+
workflowWakeRevision: null,
|
|
32864
33687
|
} as const;
|
|
32865
33688
|
}
|
|
32866
33689
|
}
|
|
33690
|
+
if (request.status !== "pending") {
|
|
33691
|
+
let event = await humanInputResponseEventForRequest(tx as unknown as Database, {
|
|
33692
|
+
workspaceId: input.workspaceId,
|
|
33693
|
+
sessionId: input.sessionId,
|
|
33694
|
+
requestId: request.id,
|
|
33695
|
+
});
|
|
33696
|
+
let repairedEvents: SessionEvent[] = [];
|
|
33697
|
+
if (
|
|
33698
|
+
!event &&
|
|
33699
|
+
request.status === "cancelled" &&
|
|
33700
|
+
(request.response as { outcome?: unknown } | null)?.outcome === "cancelled"
|
|
33701
|
+
) {
|
|
33702
|
+
const turnLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
33703
|
+
workspaceId: input.workspaceId,
|
|
33704
|
+
controlLock: "already_locked",
|
|
33705
|
+
workspaceLock: "already_locked",
|
|
33706
|
+
turnIds: [request.turnId],
|
|
33707
|
+
});
|
|
33708
|
+
const turn = turnLocks.turns[0];
|
|
33709
|
+
if (!turn || turn.sessionId !== session.id) {
|
|
33710
|
+
throw new SessionControlInvariantError(
|
|
33711
|
+
`Cancelled human-input request ${request.id} has no owning turn`,
|
|
33712
|
+
);
|
|
33713
|
+
}
|
|
33714
|
+
const occurredAt = request.respondedAt ?? request.updatedAt;
|
|
33715
|
+
const [inserted] = await tx
|
|
33716
|
+
.insert(schema.sessionEvents)
|
|
33717
|
+
.values(
|
|
33718
|
+
withLosslessContentWriteVersion(
|
|
33719
|
+
{
|
|
33720
|
+
accountId: session.accountId,
|
|
33721
|
+
workspaceId: input.workspaceId,
|
|
33722
|
+
sessionId: session.id,
|
|
33723
|
+
turnId: request.turnId,
|
|
33724
|
+
turnGeneration: request.turnGeneration,
|
|
33725
|
+
turnAssociation: "current",
|
|
33726
|
+
sequence: session.lastSequence + 1,
|
|
33727
|
+
type: "user.humanInputResponse",
|
|
33728
|
+
payload: { requestId: request.id, response: { outcome: "cancelled" } },
|
|
33729
|
+
clientEventId: null,
|
|
33730
|
+
occurredAt,
|
|
33731
|
+
},
|
|
33732
|
+
"payload",
|
|
33733
|
+
"payloadCodecVersion",
|
|
33734
|
+
),
|
|
33735
|
+
)
|
|
33736
|
+
.returning();
|
|
33737
|
+
if (!inserted) {
|
|
33738
|
+
throw new SessionControlInvariantError(
|
|
33739
|
+
`Cancelled human-input request ${request.id} could not repair its response event`,
|
|
33740
|
+
);
|
|
33741
|
+
}
|
|
33742
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
33743
|
+
accountId: session.accountId,
|
|
33744
|
+
workspaceId: input.workspaceId,
|
|
33745
|
+
sessionId: session.id,
|
|
33746
|
+
sourceKind: "human_input_response",
|
|
33747
|
+
sourceId: inserted.id,
|
|
33748
|
+
turnId: request.turnId,
|
|
33749
|
+
channel: null,
|
|
33750
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
33751
|
+
requestId: request.id,
|
|
33752
|
+
questions: request.questions,
|
|
33753
|
+
response: { outcome: "cancelled" },
|
|
33754
|
+
}),
|
|
33755
|
+
payload: {
|
|
33756
|
+
requestId: request.id,
|
|
33757
|
+
outcome: "cancelled",
|
|
33758
|
+
sourceEventId: inserted.id,
|
|
33759
|
+
},
|
|
33760
|
+
now: occurredAt,
|
|
33761
|
+
});
|
|
33762
|
+
await tx
|
|
33763
|
+
.update(schema.sessions)
|
|
33764
|
+
.set({ lastSequence: session.lastSequence + 1, updatedAt: new Date() })
|
|
33765
|
+
.where(eq(schema.sessions.id, session.id));
|
|
33766
|
+
event = mapEvent(inserted);
|
|
33767
|
+
repairedEvents = [event];
|
|
33768
|
+
}
|
|
33769
|
+
if (!event) {
|
|
33770
|
+
throw new Error(`Terminal human-input request has no response event: ${request.id}`);
|
|
33771
|
+
}
|
|
33772
|
+
return {
|
|
33773
|
+
action: "completed",
|
|
33774
|
+
request: mapSessionHumanInputRequest(request),
|
|
33775
|
+
event,
|
|
33776
|
+
events: repairedEvents,
|
|
33777
|
+
workflowWakeRevision: null,
|
|
33778
|
+
} as const;
|
|
33779
|
+
}
|
|
32867
33780
|
const [turnPreview] = session.activeTurnId
|
|
32868
33781
|
? await tx
|
|
32869
33782
|
.select()
|
|
@@ -32892,7 +33805,7 @@ export async function acceptSessionHumanInputResponse(
|
|
|
32892
33805
|
turn?.status === "requires_action" &&
|
|
32893
33806
|
turn.id === request.turnId &&
|
|
32894
33807
|
turn.executionGeneration === request.turnGeneration;
|
|
32895
|
-
if (
|
|
33808
|
+
if (!boundaryOwnsRequest) {
|
|
32896
33809
|
return {
|
|
32897
33810
|
action: "conflict",
|
|
32898
33811
|
request: mapSessionHumanInputRequest(request),
|
|
@@ -33033,9 +33946,7 @@ export async function acceptSessionHumanInputResponse(
|
|
|
33033
33946
|
events: [mappedEvent],
|
|
33034
33947
|
workflowWakeRevision,
|
|
33035
33948
|
};
|
|
33036
|
-
return
|
|
33037
|
-
? ({ action: "conflict", ...result } as const)
|
|
33038
|
-
: ({ action: "accepted", event: mappedEvent, ...result } as const);
|
|
33949
|
+
return { action: "accepted", event: mappedEvent, ...result } as const;
|
|
33039
33950
|
}),
|
|
33040
33951
|
);
|
|
33041
33952
|
}
|
|
@@ -42096,6 +43007,65 @@ async function lockWorkspaceMutationSessionTx(
|
|
|
42096
43007
|
return session;
|
|
42097
43008
|
}
|
|
42098
43009
|
|
|
43010
|
+
/** True while an exact closed attempt still owns the physical-quiescence gate.
|
|
43011
|
+
* Both user control interruptions and activity-owned recoverable shutdowns use
|
|
43012
|
+
* this predicate so wake delivery cannot turn an unfinished writer drain into
|
|
43013
|
+
* an acknowledged edge-trigger. */
|
|
43014
|
+
async function hasPendingSessionAttemptQuiescenceTx(
|
|
43015
|
+
tx: Database,
|
|
43016
|
+
input: { workspaceId: string; sessionId: string; attemptId?: string },
|
|
43017
|
+
): Promise<boolean> {
|
|
43018
|
+
const [row] = await tx.execute<{ pending: boolean }>(sql`
|
|
43019
|
+
select exists (
|
|
43020
|
+
select 1
|
|
43021
|
+
from session_turn_attempts attempt
|
|
43022
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
43023
|
+
and attempt.session_id = ${input.sessionId}
|
|
43024
|
+
${input.attemptId ? sql`and attempt.id = ${input.attemptId}` : sql``}
|
|
43025
|
+
and attempt.state = 'closed'
|
|
43026
|
+
and attempt.quiesced_at is null
|
|
43027
|
+
and (
|
|
43028
|
+
exists (
|
|
43029
|
+
select 1
|
|
43030
|
+
from session_attempt_interruptions interruption
|
|
43031
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
43032
|
+
and interruption.session_id = attempt.session_id
|
|
43033
|
+
and interruption.attempt_id = attempt.id
|
|
43034
|
+
and interruption.state in ('settled', 'rejected_stale')
|
|
43035
|
+
)
|
|
43036
|
+
or (
|
|
43037
|
+
attempt.outcome = 'interrupted_recoverable'
|
|
43038
|
+
and not exists (
|
|
43039
|
+
select 1
|
|
43040
|
+
from session_turn_attempts successor
|
|
43041
|
+
where successor.account_id = attempt.account_id
|
|
43042
|
+
and successor.workspace_id = attempt.workspace_id
|
|
43043
|
+
and successor.session_id = attempt.session_id
|
|
43044
|
+
and (
|
|
43045
|
+
successor.started_at > attempt.started_at
|
|
43046
|
+
or (
|
|
43047
|
+
successor.started_at = attempt.started_at
|
|
43048
|
+
and successor.id > attempt.id
|
|
43049
|
+
)
|
|
43050
|
+
)
|
|
43051
|
+
)
|
|
43052
|
+
and exists (
|
|
43053
|
+
select 1
|
|
43054
|
+
from session_events event
|
|
43055
|
+
where event.account_id = attempt.account_id
|
|
43056
|
+
and event.workspace_id = attempt.workspace_id
|
|
43057
|
+
and event.session_id = attempt.session_id
|
|
43058
|
+
and event.turn_id = attempt.turn_id
|
|
43059
|
+
and event.turn_attempt_id = attempt.id
|
|
43060
|
+
and event.type = 'turn.recovery.requested'
|
|
43061
|
+
)
|
|
43062
|
+
)
|
|
43063
|
+
)
|
|
43064
|
+
) as pending
|
|
43065
|
+
`);
|
|
43066
|
+
return row?.pending === true;
|
|
43067
|
+
}
|
|
43068
|
+
|
|
42099
43069
|
/**
|
|
42100
43070
|
* Resolve the organization grant identity behind a causal human, and refuse the
|
|
42101
43071
|
* operation when that grant has been revoked or suspended.
|
|
@@ -44073,7 +45043,11 @@ export async function settleRetainedProcess(
|
|
|
44073
45043
|
async (scopedDb) =>
|
|
44074
45044
|
await scopedDb.transaction(async (txRaw) => {
|
|
44075
45045
|
const tx = txRaw as unknown as Database;
|
|
44076
|
-
await lockWorkspaceMutationSessionTx(
|
|
45046
|
+
const session = await lockWorkspaceMutationSessionTx(
|
|
45047
|
+
tx,
|
|
45048
|
+
input.workspaceId,
|
|
45049
|
+
input.sessionId,
|
|
45050
|
+
);
|
|
44077
45051
|
const [process] = await tx
|
|
44078
45052
|
.select()
|
|
44079
45053
|
.from(schema.sandboxRetainedProcesses)
|
|
@@ -44280,6 +45254,22 @@ export async function settleRetainedProcess(
|
|
|
44280
45254
|
where id = ${process.leaseId}
|
|
44281
45255
|
and sandbox_group_id = ${process.sandboxGroupId}
|
|
44282
45256
|
`);
|
|
45257
|
+
if (
|
|
45258
|
+
process.ownerAttemptId &&
|
|
45259
|
+
(await hasPendingSessionAttemptQuiescenceTx(tx, {
|
|
45260
|
+
workspaceId: input.workspaceId,
|
|
45261
|
+
sessionId: input.sessionId,
|
|
45262
|
+
attemptId: process.ownerAttemptId,
|
|
45263
|
+
}))
|
|
45264
|
+
) {
|
|
45265
|
+
await enqueueSessionWorkflowWakeInTransaction(tx, {
|
|
45266
|
+
accountId: input.accountId,
|
|
45267
|
+
workspaceId: input.workspaceId,
|
|
45268
|
+
sessionId: input.sessionId,
|
|
45269
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
45270
|
+
reason: "retained_process_settled_quiescence",
|
|
45271
|
+
});
|
|
45272
|
+
}
|
|
44283
45273
|
return { settled: true, process: mapRetainedProcess(updated) };
|
|
44284
45274
|
}),
|
|
44285
45275
|
);
|
|
@@ -59847,7 +60837,12 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
59847
60837
|
workspaceId,
|
|
59848
60838
|
{
|
|
59849
60839
|
stage: "session_lifecycle_outbox.fail_before_attempt_claim",
|
|
59850
|
-
eventTypes: [
|
|
60840
|
+
eventTypes: [
|
|
60841
|
+
"child_terminal_result",
|
|
60842
|
+
"session.status.changed",
|
|
60843
|
+
"turn.failed",
|
|
60844
|
+
"user.humanInputResponse",
|
|
60845
|
+
],
|
|
59851
60846
|
maxAttempts: 3,
|
|
59852
60847
|
},
|
|
59853
60848
|
async (scopedDb) =>
|
|
@@ -60041,6 +61036,11 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60041
61036
|
const now = new Date();
|
|
60042
61037
|
let sequence = session.lastSequence;
|
|
60043
61038
|
let closedToolEvents: SessionEvent[] = [];
|
|
61039
|
+
let cancelledHumanInputs: Array<{
|
|
61040
|
+
id: string;
|
|
61041
|
+
questions: HumanInputQuestion[];
|
|
61042
|
+
turnGeneration: number;
|
|
61043
|
+
}> = [];
|
|
60044
61044
|
if (turn) {
|
|
60045
61045
|
await cancelTurnInteractionInterventionsInTransaction(tx as unknown as Database, {
|
|
60046
61046
|
accountId: session.accountId,
|
|
@@ -60062,7 +61062,7 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60062
61062
|
);
|
|
60063
61063
|
sequence = closedTools.sequence;
|
|
60064
61064
|
closedToolEvents = closedTools.events;
|
|
60065
|
-
|
|
61065
|
+
cancelledHumanInputs = await tx
|
|
60066
61066
|
.update(schema.sessionHumanInputRequests)
|
|
60067
61067
|
.set({
|
|
60068
61068
|
status: "cancelled",
|
|
@@ -60081,6 +61081,7 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60081
61081
|
)
|
|
60082
61082
|
.returning({
|
|
60083
61083
|
id: schema.sessionHumanInputRequests.id,
|
|
61084
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
60084
61085
|
turnGeneration: schema.sessionHumanInputRequests.turnGeneration,
|
|
60085
61086
|
});
|
|
60086
61087
|
// The child's blocked boundary died with the turn: resolve each
|
|
@@ -60120,6 +61121,21 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60120
61121
|
);
|
|
60121
61122
|
}
|
|
60122
61123
|
const values = [
|
|
61124
|
+
...(turn
|
|
61125
|
+
? cancelledHumanInputs.map((request) => ({
|
|
61126
|
+
accountId: session.accountId,
|
|
61127
|
+
workspaceId,
|
|
61128
|
+
sessionId: input.sessionId,
|
|
61129
|
+
sequence: ++sequence,
|
|
61130
|
+
type: "user.humanInputResponse" as const,
|
|
61131
|
+
payload: { requestId: request.id, response: { outcome: "cancelled" as const } },
|
|
61132
|
+
turnId: turn.id,
|
|
61133
|
+
turnGeneration: request.turnGeneration,
|
|
61134
|
+
turnAttemptId: null,
|
|
61135
|
+
turnAssociation: "current" as const,
|
|
61136
|
+
occurredAt: now,
|
|
61137
|
+
}))
|
|
61138
|
+
: []),
|
|
60123
61139
|
...(turn
|
|
60124
61140
|
? [
|
|
60125
61141
|
{
|
|
@@ -60159,6 +61175,36 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60159
61175
|
.insert(schema.sessionEvents)
|
|
60160
61176
|
.values(withLosslessContentWriteVersion(values, "payload", "payloadCodecVersion"))
|
|
60161
61177
|
.returning();
|
|
61178
|
+
const cancelledHumanInputById = new Map(
|
|
61179
|
+
cancelledHumanInputs.map((request) => [request.id, request]),
|
|
61180
|
+
);
|
|
61181
|
+
for (const event of inserted) {
|
|
61182
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
61183
|
+
const payload = sessionEventPayloadRecord(event.payload, event.payloadCodecVersion);
|
|
61184
|
+
const requestId = typeof payload.requestId === "string" ? payload.requestId : null;
|
|
61185
|
+
const request = requestId ? cancelledHumanInputById.get(requestId) : null;
|
|
61186
|
+
if (!request || !turn) continue;
|
|
61187
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
61188
|
+
accountId: session.accountId,
|
|
61189
|
+
workspaceId,
|
|
61190
|
+
sessionId: input.sessionId,
|
|
61191
|
+
sourceKind: "human_input_response",
|
|
61192
|
+
sourceId: event.id,
|
|
61193
|
+
turnId: turn.id,
|
|
61194
|
+
channel: null,
|
|
61195
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
61196
|
+
requestId: request.id,
|
|
61197
|
+
questions: request.questions,
|
|
61198
|
+
response: { outcome: "cancelled" },
|
|
61199
|
+
}),
|
|
61200
|
+
payload: {
|
|
61201
|
+
requestId: request.id,
|
|
61202
|
+
outcome: "cancelled",
|
|
61203
|
+
sourceEventId: event.id,
|
|
61204
|
+
},
|
|
61205
|
+
now,
|
|
61206
|
+
});
|
|
61207
|
+
}
|
|
60162
61208
|
if (turn) {
|
|
60163
61209
|
const failedEvent = inserted.find((event) => event.type === "turn.failed");
|
|
60164
61210
|
if (!failedEvent) {
|
|
@@ -62620,6 +63666,13 @@ export async function requestSessionTurnRecovery(
|
|
|
62620
63666
|
if (!updatedSession) {
|
|
62621
63667
|
throw new Error(`Active session turn changed while locked: ${input.turnId}`);
|
|
62622
63668
|
}
|
|
63669
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
63670
|
+
accountId: session.accountId,
|
|
63671
|
+
workspaceId,
|
|
63672
|
+
sessionId: input.sessionId,
|
|
63673
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
63674
|
+
reason: "turn_recovery_requested",
|
|
63675
|
+
});
|
|
62623
63676
|
return {
|
|
62624
63677
|
action: "recovering" as const,
|
|
62625
63678
|
events: [...closedTools.events, ...inserted.map(mapEvent)],
|
|
@@ -63239,7 +64292,11 @@ export async function getScheduledTargetSessionExecution(
|
|
|
63239
64292
|
{ latencyMode: latestStarted?.latencyMode },
|
|
63240
64293
|
session.latencyMode as LatencyMode,
|
|
63241
64294
|
),
|
|
63242
|
-
|
|
64295
|
+
// A turn stores omitted `tools` as the non-null database default `[]`.
|
|
64296
|
+
// Only the companion marker distinguishes that inherited state from an
|
|
64297
|
+
// explicit empty override, so never let the array alone narrow a later
|
|
64298
|
+
// scheduled occurrence.
|
|
64299
|
+
tools: (latestStarted?.toolsProvided ? latestStarted.tools : session.tools) as ToolRef[],
|
|
63243
64300
|
sandboxBackend: (latestStarted?.sandboxBackend ?? session.sandboxBackend) as SandboxBackend,
|
|
63244
64301
|
sandboxOs: (latestStarted?.sandboxOs ?? session.sandboxOs) as SandboxOs,
|
|
63245
64302
|
firstPartyMcpTools: session.firstPartyMcpTools as FirstPartyMcpToolName[],
|
|
@@ -64360,33 +65417,12 @@ export async function markSessionWorkflowWakeDelivered(
|
|
|
64360
65417
|
blocker: "pending_agent_steer",
|
|
64361
65418
|
} as const;
|
|
64362
65419
|
}
|
|
64363
|
-
|
|
64364
|
-
|
|
64365
|
-
|
|
64366
|
-
|
|
64367
|
-
|
|
64368
|
-
|
|
64369
|
-
eq(
|
|
64370
|
-
schema.sessionAttemptInterruptions.workspaceId,
|
|
64371
|
-
schema.sessionTurnAttempts.workspaceId,
|
|
64372
|
-
),
|
|
64373
|
-
eq(
|
|
64374
|
-
schema.sessionAttemptInterruptions.sessionId,
|
|
64375
|
-
schema.sessionTurnAttempts.sessionId,
|
|
64376
|
-
),
|
|
64377
|
-
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
64378
|
-
),
|
|
64379
|
-
)
|
|
64380
|
-
.where(
|
|
64381
|
-
and(
|
|
64382
|
-
eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
64383
|
-
eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
|
|
64384
|
-
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
64385
|
-
inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
|
|
64386
|
-
),
|
|
64387
|
-
)
|
|
64388
|
-
.limit(1);
|
|
64389
|
-
if (pendingQuiescence) {
|
|
65420
|
+
if (
|
|
65421
|
+
await hasPendingSessionAttemptQuiescenceTx(tx as unknown as Database, {
|
|
65422
|
+
workspaceId: input.workspaceId,
|
|
65423
|
+
sessionId: input.sessionId,
|
|
65424
|
+
})
|
|
65425
|
+
) {
|
|
64390
65426
|
return {
|
|
64391
65427
|
action: "pending_admission",
|
|
64392
65428
|
blocker: "pending_quiescence",
|
|
@@ -67885,6 +68921,8 @@ export * from "./attached-browser-devices";
|
|
|
67885
68921
|
export * from "./interaction-revisions";
|
|
67886
68922
|
export * from "./interaction-placement-loss";
|
|
67887
68923
|
export * from "./canonical-human-identities";
|
|
68924
|
+
export * from "./managed-auth-session-sets";
|
|
68925
|
+
export * from "./organization-recovery";
|
|
67888
68926
|
export * from "./session-tenancy";
|
|
67889
68927
|
export * from "./governed-learning-activation";
|
|
67890
68928
|
export * from "./automations";
|