@opengeni/db 3.4.0-canary.2 → 3.5.2-canary.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-C2AFP7SB.js → chunk-6NYKC5DD.js} +6507 -6040
- package/dist/chunk-6NYKC5DD.js.map +1 -0
- package/dist/{chunk-O6FTELKH.js → chunk-FJHEWYV5.js} +3 -3
- package/dist/{chunk-ZWRIPCTR.js → chunk-NRGUX3M5.js} +2 -2
- package/dist/{chunk-TRBOWUG3.js → chunk-ORKVXHL2.js} +2 -2
- package/dist/{chunk-PYMY3AJW.js → chunk-PERPE2UY.js} +172 -7
- package/dist/chunk-PERPE2UY.js.map +1 -0
- package/dist/chunk-TRCYF26N.js +299 -0
- package/dist/chunk-TRCYF26N.js.map +1 -0
- package/dist/{chunk-V3TRUXFJ.js → chunk-U7NKCX5E.js} +72 -19
- package/dist/chunk-U7NKCX5E.js.map +1 -0
- package/dist/{chunk-H7MXYG7R.js → chunk-X6T2P3L3.js} +2 -2
- package/dist/{chunk-HY3F5Q4E.js → chunk-Y44FKTYU.js} +3 -3
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +1698 -995
- 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 +5 -3
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +37 -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/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/package.json +10 -6
- package/src/canonical-human-identities.ts +105 -27
- package/src/index.ts +420 -103
- 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 +49 -0
- package/src/runtime-posture.ts +132 -6
- package/src/schema.ts +2 -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/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-O6FTELKH.js.map → chunk-FJHEWYV5.js.map} +0 -0
- /package/dist/{chunk-ZWRIPCTR.js.map → chunk-NRGUX3M5.js.map} +0 -0
- /package/dist/{chunk-TRBOWUG3.js.map → chunk-ORKVXHL2.js.map} +0 -0
- /package/dist/{chunk-H7MXYG7R.js.map → chunk-X6T2P3L3.js.map} +0 -0
- /package/dist/{chunk-HY3F5Q4E.js.map → chunk-Y44FKTYU.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -1508,6 +1508,7 @@ export async function ensureManagedAccessForUser(
|
|
|
1508
1508
|
name: string;
|
|
1509
1509
|
emailVerified?: boolean;
|
|
1510
1510
|
provisionFallbackOrganization?: boolean;
|
|
1511
|
+
bindPendingInvitations?: boolean;
|
|
1511
1512
|
},
|
|
1512
1513
|
): Promise<AccessContext> {
|
|
1513
1514
|
return (await ensureManagedAccessForUserWithOrganizationMemberships(db, input)).accessContext;
|
|
@@ -1653,6 +1654,7 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1653
1654
|
name: string;
|
|
1654
1655
|
emailVerified?: boolean;
|
|
1655
1656
|
provisionFallbackOrganization?: boolean;
|
|
1657
|
+
bindPendingInvitations?: boolean;
|
|
1656
1658
|
},
|
|
1657
1659
|
): Promise<ManagedAccessProvisioningResult> {
|
|
1658
1660
|
const subjectId = `user:${input.userId}`;
|
|
@@ -1660,7 +1662,7 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1660
1662
|
return await db.transaction(async (tx) => {
|
|
1661
1663
|
const txDb = tx as unknown as Database;
|
|
1662
1664
|
await setSubjectRlsContext(txDb, subjectId);
|
|
1663
|
-
if (input.emailVerified === true) {
|
|
1665
|
+
if (input.emailVerified === true && input.bindPendingInvitations !== false) {
|
|
1664
1666
|
await rawRows(
|
|
1665
1667
|
txDb,
|
|
1666
1668
|
sql`select bind_pending_organization_invitations_for_verified_email(
|
|
@@ -16176,6 +16178,43 @@ export async function listVariableSets(
|
|
|
16176
16178
|
});
|
|
16177
16179
|
}
|
|
16178
16180
|
|
|
16181
|
+
export async function resolveVariableSetAttachments(
|
|
16182
|
+
db: Database,
|
|
16183
|
+
context: VariableSetAccessContext,
|
|
16184
|
+
variableSetIds: readonly string[],
|
|
16185
|
+
): Promise<Array<Pick<VariableSet, "id" | "scope">>> {
|
|
16186
|
+
if (variableSetIds.length === 0) return [];
|
|
16187
|
+
// Drizzle expands a bare JavaScript array into a parameter list rather than
|
|
16188
|
+
// one PostgreSQL array value. Build the bounded requested relation directly.
|
|
16189
|
+
const requestedValues = sql.join(
|
|
16190
|
+
variableSetIds.map(
|
|
16191
|
+
(variableSetId, index) => sql`(${variableSetId}::uuid, ${index + 1}::integer)`,
|
|
16192
|
+
),
|
|
16193
|
+
sql`, `,
|
|
16194
|
+
);
|
|
16195
|
+
return await withRlsContext(db, context, async (scopedDb) => {
|
|
16196
|
+
await setSubjectRlsContext(scopedDb, context.subjectId);
|
|
16197
|
+
return await rawRows<Pick<VariableSet, "id" | "scope">>(
|
|
16198
|
+
scopedDb,
|
|
16199
|
+
sql`with requested(variable_set_id, ordinal) as (
|
|
16200
|
+
values ${requestedValues}
|
|
16201
|
+
)
|
|
16202
|
+
select
|
|
16203
|
+
resolved.value->>'id' as id,
|
|
16204
|
+
resolved.value->>'scope' as scope
|
|
16205
|
+
from requested
|
|
16206
|
+
cross join lateral list_scoped_variable_sets(
|
|
16207
|
+
${context.accountId}::uuid,
|
|
16208
|
+
${context.workspaceId}::uuid,
|
|
16209
|
+
requested.variable_set_id,
|
|
16210
|
+
null,
|
|
16211
|
+
null
|
|
16212
|
+
) resolved(value)
|
|
16213
|
+
order by requested.ordinal`,
|
|
16214
|
+
);
|
|
16215
|
+
});
|
|
16216
|
+
}
|
|
16217
|
+
|
|
16179
16218
|
export async function getVariableSet(
|
|
16180
16219
|
db: Database,
|
|
16181
16220
|
context: VariableSetAccessContext,
|
|
@@ -29002,6 +29041,43 @@ async function lockSessionPersonalStateExclusive(
|
|
|
29002
29041
|
);
|
|
29003
29042
|
}
|
|
29004
29043
|
|
|
29044
|
+
async function requireSessionPersonalStateWriteAuthority(
|
|
29045
|
+
db: Database,
|
|
29046
|
+
input: {
|
|
29047
|
+
workspaceId: string;
|
|
29048
|
+
subjectId: string;
|
|
29049
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
29050
|
+
},
|
|
29051
|
+
): Promise<void> {
|
|
29052
|
+
const [membership] = await db
|
|
29053
|
+
.select({ id: schema.workspaceMemberships.id })
|
|
29054
|
+
.from(schema.workspaceMemberships)
|
|
29055
|
+
.where(
|
|
29056
|
+
and(
|
|
29057
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
29058
|
+
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
29059
|
+
),
|
|
29060
|
+
)
|
|
29061
|
+
.limit(1);
|
|
29062
|
+
// Managed personal-workspace owners intentionally have no membership row.
|
|
29063
|
+
// The caller may assert this exception only from canonical managed-cookie
|
|
29064
|
+
// authentication; the live organization-membership pointer remains the
|
|
29065
|
+
// database authority and keeps suspended/foreign subjects denied.
|
|
29066
|
+
if (
|
|
29067
|
+
!membership &&
|
|
29068
|
+
!(
|
|
29069
|
+
input.personalWorkspaceOwnerException === true &&
|
|
29070
|
+
(await subjectHasLiveWorkspaceAuthorityInScope(db, {
|
|
29071
|
+
accountId: await accountIdInRlsScope(db),
|
|
29072
|
+
workspaceId: input.workspaceId,
|
|
29073
|
+
subjectId: input.subjectId,
|
|
29074
|
+
}))
|
|
29075
|
+
)
|
|
29076
|
+
) {
|
|
29077
|
+
throw new SessionPinAccessError();
|
|
29078
|
+
}
|
|
29079
|
+
}
|
|
29080
|
+
|
|
29005
29081
|
type SessionPinRow = Pick<
|
|
29006
29082
|
typeof schema.sessionPins.$inferSelect,
|
|
29007
29083
|
| "pinned"
|
|
@@ -30213,36 +30289,7 @@ export async function setSessionPin(
|
|
|
30213
30289
|
// serializes with member removal before changing personal state. A stale
|
|
30214
30290
|
// API grant cannot recreate a pin after removal commits.
|
|
30215
30291
|
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
|
-
}
|
|
30292
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30246
30293
|
const [session] = await tx
|
|
30247
30294
|
.select()
|
|
30248
30295
|
.from(schema.sessions)
|
|
@@ -30371,6 +30418,8 @@ export async function setSessionAttention(
|
|
|
30371
30418
|
acknowledgedThroughSequence?: number | undefined;
|
|
30372
30419
|
activelyWorking?: boolean | undefined;
|
|
30373
30420
|
expectedVersion?: number | undefined;
|
|
30421
|
+
/** See {@link PersonalWorkspaceOwnerException}. Absent means "no exception". */
|
|
30422
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
30374
30423
|
},
|
|
30375
30424
|
): Promise<Session | null> {
|
|
30376
30425
|
return await withWorkspaceSubjectRls(
|
|
@@ -30380,17 +30429,7 @@ export async function setSessionAttention(
|
|
|
30380
30429
|
async (scopedDb) =>
|
|
30381
30430
|
await scopedDb.transaction(async (tx) => {
|
|
30382
30431
|
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();
|
|
30432
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30394
30433
|
|
|
30395
30434
|
const [session] = await tx
|
|
30396
30435
|
.select()
|
|
@@ -30542,6 +30581,8 @@ export async function setSessionArchive(
|
|
|
30542
30581
|
sessionId: string;
|
|
30543
30582
|
archived: boolean;
|
|
30544
30583
|
expectedVersion?: number | undefined;
|
|
30584
|
+
/** See {@link PersonalWorkspaceOwnerException}. Absent means "no exception". */
|
|
30585
|
+
personalWorkspaceOwnerException?: PersonalWorkspaceOwnerException | undefined;
|
|
30545
30586
|
},
|
|
30546
30587
|
): Promise<Session | null> {
|
|
30547
30588
|
return await withWorkspaceSubjectRls(
|
|
@@ -30551,17 +30592,7 @@ export async function setSessionArchive(
|
|
|
30551
30592
|
async (scopedDb) =>
|
|
30552
30593
|
await scopedDb.transaction(async (tx) => {
|
|
30553
30594
|
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();
|
|
30595
|
+
await requireSessionPersonalStateWriteAuthority(tx, input);
|
|
30565
30596
|
|
|
30566
30597
|
const [session] = await tx
|
|
30567
30598
|
.select()
|
|
@@ -32589,6 +32620,45 @@ export async function listSessionHumanInputRequests(
|
|
|
32589
32620
|
): Promise<SessionHumanInputRequest[]> {
|
|
32590
32621
|
const limit = Math.min(100, Math.max(1, Math.floor(options.limit ?? 50)));
|
|
32591
32622
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
32623
|
+
const actionablePendingIds =
|
|
32624
|
+
options.status === "pending"
|
|
32625
|
+
? await scopedDb.execute<{ id: string }>(sql`
|
|
32626
|
+
select request_row.id
|
|
32627
|
+
from session_human_input_requests request_row
|
|
32628
|
+
join sessions session_row
|
|
32629
|
+
on session_row.workspace_id = request_row.workspace_id
|
|
32630
|
+
and session_row.id = request_row.session_id
|
|
32631
|
+
join session_turns turn_row
|
|
32632
|
+
on turn_row.workspace_id = request_row.workspace_id
|
|
32633
|
+
and turn_row.session_id = request_row.session_id
|
|
32634
|
+
and turn_row.id = request_row.turn_id
|
|
32635
|
+
join session_events trigger_row
|
|
32636
|
+
on trigger_row.workspace_id = turn_row.workspace_id
|
|
32637
|
+
and trigger_row.session_id = turn_row.session_id
|
|
32638
|
+
and trigger_row.id = turn_row.trigger_event_id
|
|
32639
|
+
where request_row.workspace_id = ${workspaceId}
|
|
32640
|
+
and request_row.session_id = ${sessionId}
|
|
32641
|
+
and request_row.status = 'pending'
|
|
32642
|
+
and (request_row.expires_at is null or request_row.expires_at > now())
|
|
32643
|
+
and session_row.status = 'requires_action'
|
|
32644
|
+
and session_row.active_turn_id = request_row.turn_id
|
|
32645
|
+
and turn_row.status = 'requires_action'
|
|
32646
|
+
and turn_row.execution_generation = request_row.turn_generation
|
|
32647
|
+
and not exists (
|
|
32648
|
+
select 1
|
|
32649
|
+
from session_events accepted_row
|
|
32650
|
+
where accepted_row.workspace_id = request_row.workspace_id
|
|
32651
|
+
and accepted_row.session_id = request_row.session_id
|
|
32652
|
+
and accepted_row.turn_id = request_row.turn_id
|
|
32653
|
+
and accepted_row.turn_generation = request_row.turn_generation
|
|
32654
|
+
and accepted_row.type in ('user.approvalDecision', 'user.humanInputResponse')
|
|
32655
|
+
and accepted_row.sequence > trigger_row.sequence
|
|
32656
|
+
)
|
|
32657
|
+
order by request_row.created_at desc, request_row.id desc
|
|
32658
|
+
limit ${limit}
|
|
32659
|
+
`)
|
|
32660
|
+
: null;
|
|
32661
|
+
if (actionablePendingIds !== null && actionablePendingIds.length === 0) return [];
|
|
32592
32662
|
const predicates = [
|
|
32593
32663
|
eq(schema.sessionHumanInputRequests.workspaceId, workspaceId),
|
|
32594
32664
|
eq(schema.sessionHumanInputRequests.sessionId, sessionId),
|
|
@@ -32596,6 +32666,14 @@ export async function listSessionHumanInputRequests(
|
|
|
32596
32666
|
if (options.status) {
|
|
32597
32667
|
predicates.push(eq(schema.sessionHumanInputRequests.status, options.status));
|
|
32598
32668
|
}
|
|
32669
|
+
if (actionablePendingIds !== null) {
|
|
32670
|
+
predicates.push(
|
|
32671
|
+
inArray(
|
|
32672
|
+
schema.sessionHumanInputRequests.id,
|
|
32673
|
+
actionablePendingIds.map((row: { id: string }) => row.id),
|
|
32674
|
+
),
|
|
32675
|
+
);
|
|
32676
|
+
}
|
|
32599
32677
|
const rows = await scopedDb
|
|
32600
32678
|
.select()
|
|
32601
32679
|
.from(schema.sessionHumanInputRequests)
|
|
@@ -32721,6 +32799,13 @@ export type AcceptSessionHumanInputResponseResult =
|
|
|
32721
32799
|
events: SessionEvent[];
|
|
32722
32800
|
workflowWakeRevision: number;
|
|
32723
32801
|
}
|
|
32802
|
+
| {
|
|
32803
|
+
action: "completed";
|
|
32804
|
+
request: SessionHumanInputRequest;
|
|
32805
|
+
event: SessionEvent;
|
|
32806
|
+
events: SessionEvent[];
|
|
32807
|
+
workflowWakeRevision: number | null;
|
|
32808
|
+
}
|
|
32724
32809
|
| {
|
|
32725
32810
|
action: "conflict";
|
|
32726
32811
|
request: SessionHumanInputRequest;
|
|
@@ -32774,10 +32859,37 @@ async function hasAcceptedRequiresActionAdvance(
|
|
|
32774
32859
|
return accepted !== undefined;
|
|
32775
32860
|
}
|
|
32776
32861
|
|
|
32862
|
+
async function humanInputResponseEventForRequest(
|
|
32863
|
+
tx: Database,
|
|
32864
|
+
input: {
|
|
32865
|
+
workspaceId: string;
|
|
32866
|
+
sessionId: string;
|
|
32867
|
+
requestId: string;
|
|
32868
|
+
},
|
|
32869
|
+
): Promise<SessionEvent | null> {
|
|
32870
|
+
const [event] = await tx
|
|
32871
|
+
.select()
|
|
32872
|
+
.from(schema.sessionEvents)
|
|
32873
|
+
.where(
|
|
32874
|
+
and(
|
|
32875
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
32876
|
+
eq(schema.sessionEvents.sessionId, input.sessionId),
|
|
32877
|
+
eq(schema.sessionEvents.type, "user.humanInputResponse"),
|
|
32878
|
+
sql`${schema.sessionEvents.payload} ->> 'requestId' = ${input.requestId}`,
|
|
32879
|
+
),
|
|
32880
|
+
)
|
|
32881
|
+
.orderBy(desc(schema.sessionEvents.sequence))
|
|
32882
|
+
.limit(1);
|
|
32883
|
+
return event ? mapEvent(event) : null;
|
|
32884
|
+
}
|
|
32885
|
+
|
|
32777
32886
|
/**
|
|
32778
|
-
* First-writer-wins settlement for one
|
|
32779
|
-
*
|
|
32780
|
-
*
|
|
32887
|
+
* First-writer-wins settlement for one structured input boundary. Session +
|
|
32888
|
+
* logical turn + current freeze generation are locked with the row; the
|
|
32889
|
+
* creation attempt is provenance and is intentionally not the resume owner.
|
|
32890
|
+
* A committed response replays its original event without another event or
|
|
32891
|
+
* workflow wake. Historical cancelled rows missing that canonical event are
|
|
32892
|
+
* repaired exactly once under the same locks, but never wake terminal work.
|
|
32781
32893
|
*/
|
|
32782
32894
|
export async function acceptSessionHumanInputResponse(
|
|
32783
32895
|
db: Database,
|
|
@@ -32845,25 +32957,105 @@ export async function acceptSessionHumanInputResponse(
|
|
|
32845
32957
|
) {
|
|
32846
32958
|
throw new Error("clientEventId belongs to a different session operation");
|
|
32847
32959
|
}
|
|
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
32960
|
return {
|
|
32859
|
-
action: "
|
|
32961
|
+
action: "completed",
|
|
32860
32962
|
request: mapSessionHumanInputRequest(request),
|
|
32861
32963
|
event: mapEvent(existing),
|
|
32862
32964
|
events: [],
|
|
32863
|
-
workflowWakeRevision,
|
|
32965
|
+
workflowWakeRevision: null,
|
|
32864
32966
|
} as const;
|
|
32865
32967
|
}
|
|
32866
32968
|
}
|
|
32969
|
+
if (request.status !== "pending") {
|
|
32970
|
+
let event = await humanInputResponseEventForRequest(tx as unknown as Database, {
|
|
32971
|
+
workspaceId: input.workspaceId,
|
|
32972
|
+
sessionId: input.sessionId,
|
|
32973
|
+
requestId: request.id,
|
|
32974
|
+
});
|
|
32975
|
+
let repairedEvents: SessionEvent[] = [];
|
|
32976
|
+
if (
|
|
32977
|
+
!event &&
|
|
32978
|
+
request.status === "cancelled" &&
|
|
32979
|
+
(request.response as { outcome?: unknown } | null)?.outcome === "cancelled"
|
|
32980
|
+
) {
|
|
32981
|
+
const turnLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
32982
|
+
workspaceId: input.workspaceId,
|
|
32983
|
+
controlLock: "already_locked",
|
|
32984
|
+
workspaceLock: "already_locked",
|
|
32985
|
+
turnIds: [request.turnId],
|
|
32986
|
+
});
|
|
32987
|
+
const turn = turnLocks.turns[0];
|
|
32988
|
+
if (!turn || turn.sessionId !== session.id) {
|
|
32989
|
+
throw new SessionControlInvariantError(
|
|
32990
|
+
`Cancelled human-input request ${request.id} has no owning turn`,
|
|
32991
|
+
);
|
|
32992
|
+
}
|
|
32993
|
+
const occurredAt = request.respondedAt ?? request.updatedAt;
|
|
32994
|
+
const [inserted] = await tx
|
|
32995
|
+
.insert(schema.sessionEvents)
|
|
32996
|
+
.values(
|
|
32997
|
+
withLosslessContentWriteVersion(
|
|
32998
|
+
{
|
|
32999
|
+
accountId: session.accountId,
|
|
33000
|
+
workspaceId: input.workspaceId,
|
|
33001
|
+
sessionId: session.id,
|
|
33002
|
+
turnId: request.turnId,
|
|
33003
|
+
turnGeneration: request.turnGeneration,
|
|
33004
|
+
turnAssociation: "current",
|
|
33005
|
+
sequence: session.lastSequence + 1,
|
|
33006
|
+
type: "user.humanInputResponse",
|
|
33007
|
+
payload: { requestId: request.id, response: { outcome: "cancelled" } },
|
|
33008
|
+
clientEventId: null,
|
|
33009
|
+
occurredAt,
|
|
33010
|
+
},
|
|
33011
|
+
"payload",
|
|
33012
|
+
"payloadCodecVersion",
|
|
33013
|
+
),
|
|
33014
|
+
)
|
|
33015
|
+
.returning();
|
|
33016
|
+
if (!inserted) {
|
|
33017
|
+
throw new SessionControlInvariantError(
|
|
33018
|
+
`Cancelled human-input request ${request.id} could not repair its response event`,
|
|
33019
|
+
);
|
|
33020
|
+
}
|
|
33021
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
33022
|
+
accountId: session.accountId,
|
|
33023
|
+
workspaceId: input.workspaceId,
|
|
33024
|
+
sessionId: session.id,
|
|
33025
|
+
sourceKind: "human_input_response",
|
|
33026
|
+
sourceId: inserted.id,
|
|
33027
|
+
turnId: request.turnId,
|
|
33028
|
+
channel: null,
|
|
33029
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
33030
|
+
requestId: request.id,
|
|
33031
|
+
questions: request.questions,
|
|
33032
|
+
response: { outcome: "cancelled" },
|
|
33033
|
+
}),
|
|
33034
|
+
payload: {
|
|
33035
|
+
requestId: request.id,
|
|
33036
|
+
outcome: "cancelled",
|
|
33037
|
+
sourceEventId: inserted.id,
|
|
33038
|
+
},
|
|
33039
|
+
now: occurredAt,
|
|
33040
|
+
});
|
|
33041
|
+
await tx
|
|
33042
|
+
.update(schema.sessions)
|
|
33043
|
+
.set({ lastSequence: session.lastSequence + 1, updatedAt: new Date() })
|
|
33044
|
+
.where(eq(schema.sessions.id, session.id));
|
|
33045
|
+
event = mapEvent(inserted);
|
|
33046
|
+
repairedEvents = [event];
|
|
33047
|
+
}
|
|
33048
|
+
if (!event) {
|
|
33049
|
+
throw new Error(`Terminal human-input request has no response event: ${request.id}`);
|
|
33050
|
+
}
|
|
33051
|
+
return {
|
|
33052
|
+
action: "completed",
|
|
33053
|
+
request: mapSessionHumanInputRequest(request),
|
|
33054
|
+
event,
|
|
33055
|
+
events: repairedEvents,
|
|
33056
|
+
workflowWakeRevision: null,
|
|
33057
|
+
} as const;
|
|
33058
|
+
}
|
|
32867
33059
|
const [turnPreview] = session.activeTurnId
|
|
32868
33060
|
? await tx
|
|
32869
33061
|
.select()
|
|
@@ -32892,7 +33084,7 @@ export async function acceptSessionHumanInputResponse(
|
|
|
32892
33084
|
turn?.status === "requires_action" &&
|
|
32893
33085
|
turn.id === request.turnId &&
|
|
32894
33086
|
turn.executionGeneration === request.turnGeneration;
|
|
32895
|
-
if (
|
|
33087
|
+
if (!boundaryOwnsRequest) {
|
|
32896
33088
|
return {
|
|
32897
33089
|
action: "conflict",
|
|
32898
33090
|
request: mapSessionHumanInputRequest(request),
|
|
@@ -33033,9 +33225,7 @@ export async function acceptSessionHumanInputResponse(
|
|
|
33033
33225
|
events: [mappedEvent],
|
|
33034
33226
|
workflowWakeRevision,
|
|
33035
33227
|
};
|
|
33036
|
-
return
|
|
33037
|
-
? ({ action: "conflict", ...result } as const)
|
|
33038
|
-
: ({ action: "accepted", event: mappedEvent, ...result } as const);
|
|
33228
|
+
return { action: "accepted", event: mappedEvent, ...result } as const;
|
|
33039
33229
|
}),
|
|
33040
33230
|
);
|
|
33041
33231
|
}
|
|
@@ -42096,6 +42286,65 @@ async function lockWorkspaceMutationSessionTx(
|
|
|
42096
42286
|
return session;
|
|
42097
42287
|
}
|
|
42098
42288
|
|
|
42289
|
+
/** True while an exact closed attempt still owns the physical-quiescence gate.
|
|
42290
|
+
* Both user control interruptions and activity-owned recoverable shutdowns use
|
|
42291
|
+
* this predicate so wake delivery cannot turn an unfinished writer drain into
|
|
42292
|
+
* an acknowledged edge-trigger. */
|
|
42293
|
+
async function hasPendingSessionAttemptQuiescenceTx(
|
|
42294
|
+
tx: Database,
|
|
42295
|
+
input: { workspaceId: string; sessionId: string; attemptId?: string },
|
|
42296
|
+
): Promise<boolean> {
|
|
42297
|
+
const [row] = await tx.execute<{ pending: boolean }>(sql`
|
|
42298
|
+
select exists (
|
|
42299
|
+
select 1
|
|
42300
|
+
from session_turn_attempts attempt
|
|
42301
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
42302
|
+
and attempt.session_id = ${input.sessionId}
|
|
42303
|
+
${input.attemptId ? sql`and attempt.id = ${input.attemptId}` : sql``}
|
|
42304
|
+
and attempt.state = 'closed'
|
|
42305
|
+
and attempt.quiesced_at is null
|
|
42306
|
+
and (
|
|
42307
|
+
exists (
|
|
42308
|
+
select 1
|
|
42309
|
+
from session_attempt_interruptions interruption
|
|
42310
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
42311
|
+
and interruption.session_id = attempt.session_id
|
|
42312
|
+
and interruption.attempt_id = attempt.id
|
|
42313
|
+
and interruption.state in ('settled', 'rejected_stale')
|
|
42314
|
+
)
|
|
42315
|
+
or (
|
|
42316
|
+
attempt.outcome = 'interrupted_recoverable'
|
|
42317
|
+
and not exists (
|
|
42318
|
+
select 1
|
|
42319
|
+
from session_turn_attempts successor
|
|
42320
|
+
where successor.account_id = attempt.account_id
|
|
42321
|
+
and successor.workspace_id = attempt.workspace_id
|
|
42322
|
+
and successor.session_id = attempt.session_id
|
|
42323
|
+
and (
|
|
42324
|
+
successor.started_at > attempt.started_at
|
|
42325
|
+
or (
|
|
42326
|
+
successor.started_at = attempt.started_at
|
|
42327
|
+
and successor.id > attempt.id
|
|
42328
|
+
)
|
|
42329
|
+
)
|
|
42330
|
+
)
|
|
42331
|
+
and exists (
|
|
42332
|
+
select 1
|
|
42333
|
+
from session_events event
|
|
42334
|
+
where event.account_id = attempt.account_id
|
|
42335
|
+
and event.workspace_id = attempt.workspace_id
|
|
42336
|
+
and event.session_id = attempt.session_id
|
|
42337
|
+
and event.turn_id = attempt.turn_id
|
|
42338
|
+
and event.turn_attempt_id = attempt.id
|
|
42339
|
+
and event.type = 'turn.recovery.requested'
|
|
42340
|
+
)
|
|
42341
|
+
)
|
|
42342
|
+
)
|
|
42343
|
+
) as pending
|
|
42344
|
+
`);
|
|
42345
|
+
return row?.pending === true;
|
|
42346
|
+
}
|
|
42347
|
+
|
|
42099
42348
|
/**
|
|
42100
42349
|
* Resolve the organization grant identity behind a causal human, and refuse the
|
|
42101
42350
|
* operation when that grant has been revoked or suspended.
|
|
@@ -44073,7 +44322,11 @@ export async function settleRetainedProcess(
|
|
|
44073
44322
|
async (scopedDb) =>
|
|
44074
44323
|
await scopedDb.transaction(async (txRaw) => {
|
|
44075
44324
|
const tx = txRaw as unknown as Database;
|
|
44076
|
-
await lockWorkspaceMutationSessionTx(
|
|
44325
|
+
const session = await lockWorkspaceMutationSessionTx(
|
|
44326
|
+
tx,
|
|
44327
|
+
input.workspaceId,
|
|
44328
|
+
input.sessionId,
|
|
44329
|
+
);
|
|
44077
44330
|
const [process] = await tx
|
|
44078
44331
|
.select()
|
|
44079
44332
|
.from(schema.sandboxRetainedProcesses)
|
|
@@ -44280,6 +44533,22 @@ export async function settleRetainedProcess(
|
|
|
44280
44533
|
where id = ${process.leaseId}
|
|
44281
44534
|
and sandbox_group_id = ${process.sandboxGroupId}
|
|
44282
44535
|
`);
|
|
44536
|
+
if (
|
|
44537
|
+
process.ownerAttemptId &&
|
|
44538
|
+
(await hasPendingSessionAttemptQuiescenceTx(tx, {
|
|
44539
|
+
workspaceId: input.workspaceId,
|
|
44540
|
+
sessionId: input.sessionId,
|
|
44541
|
+
attemptId: process.ownerAttemptId,
|
|
44542
|
+
}))
|
|
44543
|
+
) {
|
|
44544
|
+
await enqueueSessionWorkflowWakeInTransaction(tx, {
|
|
44545
|
+
accountId: input.accountId,
|
|
44546
|
+
workspaceId: input.workspaceId,
|
|
44547
|
+
sessionId: input.sessionId,
|
|
44548
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
44549
|
+
reason: "retained_process_settled_quiescence",
|
|
44550
|
+
});
|
|
44551
|
+
}
|
|
44283
44552
|
return { settled: true, process: mapRetainedProcess(updated) };
|
|
44284
44553
|
}),
|
|
44285
44554
|
);
|
|
@@ -59847,7 +60116,12 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
59847
60116
|
workspaceId,
|
|
59848
60117
|
{
|
|
59849
60118
|
stage: "session_lifecycle_outbox.fail_before_attempt_claim",
|
|
59850
|
-
eventTypes: [
|
|
60119
|
+
eventTypes: [
|
|
60120
|
+
"child_terminal_result",
|
|
60121
|
+
"session.status.changed",
|
|
60122
|
+
"turn.failed",
|
|
60123
|
+
"user.humanInputResponse",
|
|
60124
|
+
],
|
|
59851
60125
|
maxAttempts: 3,
|
|
59852
60126
|
},
|
|
59853
60127
|
async (scopedDb) =>
|
|
@@ -60041,6 +60315,11 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60041
60315
|
const now = new Date();
|
|
60042
60316
|
let sequence = session.lastSequence;
|
|
60043
60317
|
let closedToolEvents: SessionEvent[] = [];
|
|
60318
|
+
let cancelledHumanInputs: Array<{
|
|
60319
|
+
id: string;
|
|
60320
|
+
questions: HumanInputQuestion[];
|
|
60321
|
+
turnGeneration: number;
|
|
60322
|
+
}> = [];
|
|
60044
60323
|
if (turn) {
|
|
60045
60324
|
await cancelTurnInteractionInterventionsInTransaction(tx as unknown as Database, {
|
|
60046
60325
|
accountId: session.accountId,
|
|
@@ -60062,7 +60341,7 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60062
60341
|
);
|
|
60063
60342
|
sequence = closedTools.sequence;
|
|
60064
60343
|
closedToolEvents = closedTools.events;
|
|
60065
|
-
|
|
60344
|
+
cancelledHumanInputs = await tx
|
|
60066
60345
|
.update(schema.sessionHumanInputRequests)
|
|
60067
60346
|
.set({
|
|
60068
60347
|
status: "cancelled",
|
|
@@ -60081,6 +60360,7 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60081
60360
|
)
|
|
60082
60361
|
.returning({
|
|
60083
60362
|
id: schema.sessionHumanInputRequests.id,
|
|
60363
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
60084
60364
|
turnGeneration: schema.sessionHumanInputRequests.turnGeneration,
|
|
60085
60365
|
});
|
|
60086
60366
|
// The child's blocked boundary died with the turn: resolve each
|
|
@@ -60120,6 +60400,21 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60120
60400
|
);
|
|
60121
60401
|
}
|
|
60122
60402
|
const values = [
|
|
60403
|
+
...(turn
|
|
60404
|
+
? cancelledHumanInputs.map((request) => ({
|
|
60405
|
+
accountId: session.accountId,
|
|
60406
|
+
workspaceId,
|
|
60407
|
+
sessionId: input.sessionId,
|
|
60408
|
+
sequence: ++sequence,
|
|
60409
|
+
type: "user.humanInputResponse" as const,
|
|
60410
|
+
payload: { requestId: request.id, response: { outcome: "cancelled" as const } },
|
|
60411
|
+
turnId: turn.id,
|
|
60412
|
+
turnGeneration: request.turnGeneration,
|
|
60413
|
+
turnAttemptId: null,
|
|
60414
|
+
turnAssociation: "current" as const,
|
|
60415
|
+
occurredAt: now,
|
|
60416
|
+
}))
|
|
60417
|
+
: []),
|
|
60123
60418
|
...(turn
|
|
60124
60419
|
? [
|
|
60125
60420
|
{
|
|
@@ -60159,6 +60454,36 @@ export async function failSessionWorkBeforeAttemptClaim(
|
|
|
60159
60454
|
.insert(schema.sessionEvents)
|
|
60160
60455
|
.values(withLosslessContentWriteVersion(values, "payload", "payloadCodecVersion"))
|
|
60161
60456
|
.returning();
|
|
60457
|
+
const cancelledHumanInputById = new Map(
|
|
60458
|
+
cancelledHumanInputs.map((request) => [request.id, request]),
|
|
60459
|
+
);
|
|
60460
|
+
for (const event of inserted) {
|
|
60461
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
60462
|
+
const payload = sessionEventPayloadRecord(event.payload, event.payloadCodecVersion);
|
|
60463
|
+
const requestId = typeof payload.requestId === "string" ? payload.requestId : null;
|
|
60464
|
+
const request = requestId ? cancelledHumanInputById.get(requestId) : null;
|
|
60465
|
+
if (!request || !turn) continue;
|
|
60466
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
60467
|
+
accountId: session.accountId,
|
|
60468
|
+
workspaceId,
|
|
60469
|
+
sessionId: input.sessionId,
|
|
60470
|
+
sourceKind: "human_input_response",
|
|
60471
|
+
sourceId: event.id,
|
|
60472
|
+
turnId: turn.id,
|
|
60473
|
+
channel: null,
|
|
60474
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
60475
|
+
requestId: request.id,
|
|
60476
|
+
questions: request.questions,
|
|
60477
|
+
response: { outcome: "cancelled" },
|
|
60478
|
+
}),
|
|
60479
|
+
payload: {
|
|
60480
|
+
requestId: request.id,
|
|
60481
|
+
outcome: "cancelled",
|
|
60482
|
+
sourceEventId: event.id,
|
|
60483
|
+
},
|
|
60484
|
+
now,
|
|
60485
|
+
});
|
|
60486
|
+
}
|
|
60162
60487
|
if (turn) {
|
|
60163
60488
|
const failedEvent = inserted.find((event) => event.type === "turn.failed");
|
|
60164
60489
|
if (!failedEvent) {
|
|
@@ -62620,6 +62945,13 @@ export async function requestSessionTurnRecovery(
|
|
|
62620
62945
|
if (!updatedSession) {
|
|
62621
62946
|
throw new Error(`Active session turn changed while locked: ${input.turnId}`);
|
|
62622
62947
|
}
|
|
62948
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
62949
|
+
accountId: session.accountId,
|
|
62950
|
+
workspaceId,
|
|
62951
|
+
sessionId: input.sessionId,
|
|
62952
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
62953
|
+
reason: "turn_recovery_requested",
|
|
62954
|
+
});
|
|
62623
62955
|
return {
|
|
62624
62956
|
action: "recovering" as const,
|
|
62625
62957
|
events: [...closedTools.events, ...inserted.map(mapEvent)],
|
|
@@ -63239,7 +63571,11 @@ export async function getScheduledTargetSessionExecution(
|
|
|
63239
63571
|
{ latencyMode: latestStarted?.latencyMode },
|
|
63240
63572
|
session.latencyMode as LatencyMode,
|
|
63241
63573
|
),
|
|
63242
|
-
|
|
63574
|
+
// A turn stores omitted `tools` as the non-null database default `[]`.
|
|
63575
|
+
// Only the companion marker distinguishes that inherited state from an
|
|
63576
|
+
// explicit empty override, so never let the array alone narrow a later
|
|
63577
|
+
// scheduled occurrence.
|
|
63578
|
+
tools: (latestStarted?.toolsProvided ? latestStarted.tools : session.tools) as ToolRef[],
|
|
63243
63579
|
sandboxBackend: (latestStarted?.sandboxBackend ?? session.sandboxBackend) as SandboxBackend,
|
|
63244
63580
|
sandboxOs: (latestStarted?.sandboxOs ?? session.sandboxOs) as SandboxOs,
|
|
63245
63581
|
firstPartyMcpTools: session.firstPartyMcpTools as FirstPartyMcpToolName[],
|
|
@@ -64360,33 +64696,12 @@ export async function markSessionWorkflowWakeDelivered(
|
|
|
64360
64696
|
blocker: "pending_agent_steer",
|
|
64361
64697
|
} as const;
|
|
64362
64698
|
}
|
|
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) {
|
|
64699
|
+
if (
|
|
64700
|
+
await hasPendingSessionAttemptQuiescenceTx(tx as unknown as Database, {
|
|
64701
|
+
workspaceId: input.workspaceId,
|
|
64702
|
+
sessionId: input.sessionId,
|
|
64703
|
+
})
|
|
64704
|
+
) {
|
|
64390
64705
|
return {
|
|
64391
64706
|
action: "pending_admission",
|
|
64392
64707
|
blocker: "pending_quiescence",
|
|
@@ -67885,6 +68200,8 @@ export * from "./attached-browser-devices";
|
|
|
67885
68200
|
export * from "./interaction-revisions";
|
|
67886
68201
|
export * from "./interaction-placement-loss";
|
|
67887
68202
|
export * from "./canonical-human-identities";
|
|
68203
|
+
export * from "./managed-auth-session-sets";
|
|
68204
|
+
export * from "./organization-recovery";
|
|
67888
68205
|
export * from "./session-tenancy";
|
|
67889
68206
|
export * from "./governed-learning-activation";
|
|
67890
68207
|
export * from "./automations";
|