@opengeni/db 0.22.1 → 0.23.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/{chunk-CYGFLLMN.js → chunk-3UCHDMKG.js} +656 -282
- package/dist/chunk-3UCHDMKG.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-L6ADMZHE.js} +24 -2
- package/dist/chunk-L6ADMZHE.js.map +1 -0
- package/dist/index.d.ts +42 -3
- package/dist/index.js +6919 -3760
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1416 -92
- package/dist/schema.js +11 -1
- package/dist/session-control.d.ts +7 -1
- package/dist/session-queue-commands.d.ts +8 -0
- package/dist/session-realtime-context.d.ts +56 -0
- package/dist/session-realtime-ledger.d.ts +188 -0
- package/dist/session-realtime-mirror.d.ts +30 -0
- package/dist/session-realtime-state.d.ts +2 -0
- package/dist/session-realtime-terminal.d.ts +40 -0
- package/dist/session-realtime.d.ts +59 -0
- package/dist/workspace-instruction-policies-schema.d.ts +239 -0
- package/dist/workspace-instruction-policies.d.ts +44 -0
- package/drizzle/0156_slack_reaction_trigger.sql +49 -0
- package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
- package/drizzle/0158_session_realtime_mode.sql +88 -0
- package/drizzle/0159_session_realtime_ledger.sql +198 -0
- package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
- package/drizzle/0161_session_realtime_context_projection.sql +82 -0
- package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
- package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
- package/drizzle/0164_session_realtime_models.sql +28 -0
- package/package.json +4 -4
- package/src/index.ts +670 -79
- package/src/preference-registry.ts +11 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +10 -0
- package/src/schema.ts +400 -43
- package/src/session-control.ts +596 -21
- package/src/session-queue-commands.ts +76 -7
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +160 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +611 -0
- package/src/workspace-instruction-policies-schema.ts +41 -0
- package/src/workspace-instruction-policies.ts +131 -2
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- package/dist/chunk-CYGFLLMN.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -146,7 +146,12 @@ import {
|
|
|
146
146
|
SubmitHumanInputResponseRequest,
|
|
147
147
|
TurnExecutionPolicyV1,
|
|
148
148
|
} from "@opengeni/contracts";
|
|
149
|
-
import {
|
|
149
|
+
import {
|
|
150
|
+
environmentsEncryptionKeyBytes,
|
|
151
|
+
VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
152
|
+
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
153
|
+
type Settings,
|
|
154
|
+
} from "@opengeni/config";
|
|
150
155
|
import { boundModelToolOutputItem, isCodexBilledModel } from "@opengeni/codex";
|
|
151
156
|
// Re-exported so consumers get the whole codex-billed detection surface (the pure
|
|
152
157
|
// prefix test + the credential-aware predicates below) from a single import.
|
|
@@ -212,11 +217,25 @@ import {
|
|
|
212
217
|
serializeEffectiveSessionControl,
|
|
213
218
|
type SessionDiscoveryControl,
|
|
214
219
|
type SessionCommandActor,
|
|
220
|
+
SessionControlConflictError,
|
|
215
221
|
SessionControlInvariantError,
|
|
216
222
|
updateSessionCommandReceiptResult,
|
|
217
223
|
type SessionTurnAttemptOutcome,
|
|
218
224
|
type WorkspaceControlRow,
|
|
219
225
|
} from "./session-control";
|
|
226
|
+
import {
|
|
227
|
+
sessionRealtimeIsActiveInTransaction,
|
|
228
|
+
settleExpiredSessionRealtimeInTransaction,
|
|
229
|
+
} from "./session-realtime";
|
|
230
|
+
import {
|
|
231
|
+
isSessionRealtimeDelegationTurnMetadata,
|
|
232
|
+
projectSessionRealtimeDelegationProgressInTransaction,
|
|
233
|
+
projectSessionRealtimeDelegationTerminalInTransaction,
|
|
234
|
+
} from "./session-realtime-ledger";
|
|
235
|
+
import {
|
|
236
|
+
listSessionRealtimeContinuityEntriesInTransaction,
|
|
237
|
+
type SessionRealtimeContinuityEntry,
|
|
238
|
+
} from "./session-realtime-context";
|
|
220
239
|
import * as schema from "./schema";
|
|
221
240
|
import {
|
|
222
241
|
AGENT_VISIBLE_MEMORY_STATUSES,
|
|
@@ -238,6 +257,9 @@ import {
|
|
|
238
257
|
export { sql as dbSql } from "drizzle-orm";
|
|
239
258
|
export * from "./session-control";
|
|
240
259
|
export * from "./session-queue-commands";
|
|
260
|
+
export * from "./session-realtime";
|
|
261
|
+
export * from "./session-realtime-context";
|
|
262
|
+
export * from "./session-realtime-ledger";
|
|
241
263
|
export * from "./new-session-drafts";
|
|
242
264
|
export * from "./workspace-instruction-policies";
|
|
243
265
|
export * from "./preference-registry";
|
|
@@ -5383,6 +5405,7 @@ export type SlackBotUserLink = {
|
|
|
5383
5405
|
export type SlackInteractionTriggerKind =
|
|
5384
5406
|
| "app_mention"
|
|
5385
5407
|
| "dm"
|
|
5408
|
+
| "reaction"
|
|
5386
5409
|
| "slash_command"
|
|
5387
5410
|
| "message_shortcut"
|
|
5388
5411
|
| "thread_reply";
|
|
@@ -5407,6 +5430,7 @@ export type SlackInteractionInboxEntry = {
|
|
|
5407
5430
|
attemptCount: number;
|
|
5408
5431
|
retryAt: Date | null;
|
|
5409
5432
|
lastErrorCode: string | null;
|
|
5433
|
+
reactionContextCheckpoint: unknown | null;
|
|
5410
5434
|
processedAt: Date | null;
|
|
5411
5435
|
createdAt: Date;
|
|
5412
5436
|
updatedAt: Date;
|
|
@@ -5552,6 +5576,7 @@ export async function enqueueSlackInteractionInbox(
|
|
|
5552
5576
|
| "attemptCount"
|
|
5553
5577
|
| "retryAt"
|
|
5554
5578
|
| "lastErrorCode"
|
|
5579
|
+
| "reactionContextCheckpoint"
|
|
5555
5580
|
| "processedAt"
|
|
5556
5581
|
| "createdAt"
|
|
5557
5582
|
| "updatedAt"
|
|
@@ -5614,6 +5639,7 @@ export async function settleSlackInteractionInbox(
|
|
|
5614
5639
|
claimHolderId: null,
|
|
5615
5640
|
claimExpiresAt: null,
|
|
5616
5641
|
retryAt: null,
|
|
5642
|
+
reactionContextCheckpoint: null,
|
|
5617
5643
|
processedAt: sql`now()`,
|
|
5618
5644
|
lastErrorCode: input.errorCode ?? null,
|
|
5619
5645
|
updatedAt: sql`now()`,
|
|
@@ -5630,6 +5656,42 @@ export async function settleSlackInteractionInbox(
|
|
|
5630
5656
|
});
|
|
5631
5657
|
}
|
|
5632
5658
|
|
|
5659
|
+
export async function saveSlackInteractionInboxReactionCheckpoint(
|
|
5660
|
+
db: Database,
|
|
5661
|
+
input: {
|
|
5662
|
+
entry: Pick<
|
|
5663
|
+
SlackInteractionInboxEntry,
|
|
5664
|
+
"id" | "accountId" | "workspaceId" | "connectionId" | "providerEventId" | "providerMessageId"
|
|
5665
|
+
>;
|
|
5666
|
+
claimHolderId: string;
|
|
5667
|
+
checkpoint: unknown;
|
|
5668
|
+
},
|
|
5669
|
+
): Promise<boolean> {
|
|
5670
|
+
return await withRlsContext(db, input.entry, async (scopedDb) => {
|
|
5671
|
+
const rows = await scopedDb
|
|
5672
|
+
.update(schema.slackInteractionInbox)
|
|
5673
|
+
.set({
|
|
5674
|
+
reactionContextCheckpoint: input.checkpoint,
|
|
5675
|
+
updatedAt: sql`now()`,
|
|
5676
|
+
})
|
|
5677
|
+
.where(
|
|
5678
|
+
and(
|
|
5679
|
+
eq(schema.slackInteractionInbox.id, input.entry.id),
|
|
5680
|
+
eq(schema.slackInteractionInbox.accountId, input.entry.accountId),
|
|
5681
|
+
eq(schema.slackInteractionInbox.workspaceId, input.entry.workspaceId),
|
|
5682
|
+
eq(schema.slackInteractionInbox.connectionId, input.entry.connectionId),
|
|
5683
|
+
eq(schema.slackInteractionInbox.providerEventId, input.entry.providerEventId),
|
|
5684
|
+
eq(schema.slackInteractionInbox.providerMessageId, input.entry.providerMessageId),
|
|
5685
|
+
eq(schema.slackInteractionInbox.triggerKind, "reaction"),
|
|
5686
|
+
eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
|
|
5687
|
+
eq(schema.slackInteractionInbox.status, "processing"),
|
|
5688
|
+
),
|
|
5689
|
+
)
|
|
5690
|
+
.returning({ id: schema.slackInteractionInbox.id });
|
|
5691
|
+
return rows.length === 1;
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5633
5695
|
export async function releaseSlackInteractionInbox(
|
|
5634
5696
|
db: Database,
|
|
5635
5697
|
input: {
|
|
@@ -5726,6 +5788,57 @@ export async function getSlackInteractionByRoute(
|
|
|
5726
5788
|
});
|
|
5727
5789
|
}
|
|
5728
5790
|
|
|
5791
|
+
/**
|
|
5792
|
+
* Resolve an already-durable Slack task to its canonical interaction.
|
|
5793
|
+
*
|
|
5794
|
+
* Client event ids are unique only inside one session, so this lookup also
|
|
5795
|
+
* fences the exact workspace and Slack connection. The reservation id is the
|
|
5796
|
+
* canonical Slack session identity both before and after binding; joining it
|
|
5797
|
+
* lets a retry repair a crash after the task committed but before the route was
|
|
5798
|
+
* bound. Two matches indicate a cross-session collision and fail closed rather
|
|
5799
|
+
* than choosing an arbitrary route.
|
|
5800
|
+
*/
|
|
5801
|
+
export async function getSlackInteractionByClientEventId(
|
|
5802
|
+
db: Database,
|
|
5803
|
+
workspaceId: string,
|
|
5804
|
+
connectionId: string,
|
|
5805
|
+
clientEventId: string,
|
|
5806
|
+
): Promise<{ interaction: SlackInteraction; eventSessionId: string } | null> {
|
|
5807
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
5808
|
+
const rows = await scopedDb
|
|
5809
|
+
.select({
|
|
5810
|
+
interaction: schema.slackInteractions,
|
|
5811
|
+
eventSessionId: schema.sessionEvents.sessionId,
|
|
5812
|
+
})
|
|
5813
|
+
.from(schema.slackInteractions)
|
|
5814
|
+
.innerJoin(
|
|
5815
|
+
schema.sessionEvents,
|
|
5816
|
+
and(
|
|
5817
|
+
eq(schema.sessionEvents.workspaceId, schema.slackInteractions.workspaceId),
|
|
5818
|
+
eq(schema.sessionEvents.sessionId, schema.slackInteractions.sessionReservationId),
|
|
5819
|
+
),
|
|
5820
|
+
)
|
|
5821
|
+
.where(
|
|
5822
|
+
and(
|
|
5823
|
+
eq(schema.slackInteractions.workspaceId, workspaceId),
|
|
5824
|
+
eq(schema.slackInteractions.connectionId, connectionId),
|
|
5825
|
+
eq(schema.sessionEvents.clientEventId, clientEventId),
|
|
5826
|
+
eq(schema.sessionEvents.type, "user.message"),
|
|
5827
|
+
),
|
|
5828
|
+
)
|
|
5829
|
+
.limit(2);
|
|
5830
|
+
if (rows.length > 1) {
|
|
5831
|
+
throw new Error("Slack client event id resolved to multiple canonical interactions");
|
|
5832
|
+
}
|
|
5833
|
+
const row = rows[0];
|
|
5834
|
+
if (!row) return null;
|
|
5835
|
+
return {
|
|
5836
|
+
interaction: mapSlackInteraction(row.interaction),
|
|
5837
|
+
eventSessionId: row.eventSessionId,
|
|
5838
|
+
};
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5729
5842
|
export async function getSlackInteractionSessionAccess(
|
|
5730
5843
|
db: Database,
|
|
5731
5844
|
workspaceId: string,
|
|
@@ -6173,6 +6286,8 @@ function mapSlackInteractionInbox(
|
|
|
6173
6286
|
attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
|
|
6174
6287
|
retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
|
|
6175
6288
|
lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
|
|
6289
|
+
reactionContextCheckpoint:
|
|
6290
|
+
slackRowValue(row, "reactionContextCheckpoint", "reaction_context_checkpoint") ?? null,
|
|
6176
6291
|
processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
|
|
6177
6292
|
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6178
6293
|
updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
|
|
@@ -7237,6 +7352,56 @@ export async function loadConnectionCredentialForBroker(
|
|
|
7237
7352
|
);
|
|
7238
7353
|
}
|
|
7239
7354
|
|
|
7355
|
+
export async function workspaceVercelAiGatewayConnectionActive(
|
|
7356
|
+
db: Database,
|
|
7357
|
+
workspaceId: string,
|
|
7358
|
+
): Promise<boolean> {
|
|
7359
|
+
const connections = await listConnectionsMetadata(db, workspaceId, null);
|
|
7360
|
+
return connections.some(
|
|
7361
|
+
(connection) =>
|
|
7362
|
+
connection.subjectId === null &&
|
|
7363
|
+
connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
|
|
7364
|
+
connection.kind === "api_key" &&
|
|
7365
|
+
connection.status === "active" &&
|
|
7366
|
+
connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
7367
|
+
);
|
|
7368
|
+
}
|
|
7369
|
+
|
|
7370
|
+
/** Resolve only the reviewed workspace-shared AI Gateway credential shape. */
|
|
7371
|
+
export async function loadWorkspaceVercelAiGatewayApiKey(
|
|
7372
|
+
db: Database,
|
|
7373
|
+
settings: Settings,
|
|
7374
|
+
workspaceId: string,
|
|
7375
|
+
): Promise<string | null> {
|
|
7376
|
+
const metadata = (await listConnectionsMetadata(db, workspaceId, null)).find(
|
|
7377
|
+
(connection) =>
|
|
7378
|
+
connection.subjectId === null &&
|
|
7379
|
+
connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
|
|
7380
|
+
connection.kind === "api_key" &&
|
|
7381
|
+
connection.status === "active" &&
|
|
7382
|
+
connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
7383
|
+
);
|
|
7384
|
+
if (!metadata) {
|
|
7385
|
+
return null;
|
|
7386
|
+
}
|
|
7387
|
+
const connection = await loadConnectionCredentialForBroker(db, settings, {
|
|
7388
|
+
workspaceId,
|
|
7389
|
+
connectionId: metadata.id,
|
|
7390
|
+
providerDomain: VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
7391
|
+
kind: "api_key",
|
|
7392
|
+
allowSubjectOwned: false,
|
|
7393
|
+
});
|
|
7394
|
+
if (
|
|
7395
|
+
!connection ||
|
|
7396
|
+
connection.status !== "active" ||
|
|
7397
|
+
connection.metadata.credentialRole !== VERCEL_AI_GATEWAY_CONNECTION_ROLE
|
|
7398
|
+
) {
|
|
7399
|
+
return null;
|
|
7400
|
+
}
|
|
7401
|
+
const apiKey = connection.credential.apiKey;
|
|
7402
|
+
return typeof apiKey === "string" && apiKey.trim().length > 0 ? apiKey : null;
|
|
7403
|
+
}
|
|
7404
|
+
|
|
7240
7405
|
export async function recordConnectionTokenRefresh(
|
|
7241
7406
|
db: Database,
|
|
7242
7407
|
input: {
|
|
@@ -16638,6 +16803,7 @@ export type SessionCreateInput = {
|
|
|
16638
16803
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
16639
16804
|
firstPartyMcpTools?: FirstPartyMcpToolName[];
|
|
16640
16805
|
instructions?: string | null;
|
|
16806
|
+
policyRole?: string | null;
|
|
16641
16807
|
parentSessionId?: string | null;
|
|
16642
16808
|
createIdempotencyKey?: string | null;
|
|
16643
16809
|
sandboxGroupId?: string | null;
|
|
@@ -16794,6 +16960,9 @@ async function resolveSessionDepthDecision(
|
|
|
16794
16960
|
if (!parent) {
|
|
16795
16961
|
throw new Error(`Parent session not found: ${parentSessionId}`);
|
|
16796
16962
|
}
|
|
16963
|
+
if (parent.status === "cancelled") {
|
|
16964
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot create children");
|
|
16965
|
+
}
|
|
16797
16966
|
}
|
|
16798
16967
|
|
|
16799
16968
|
const currentParentDepth = parent?.nestedAgentDepth ?? 0;
|
|
@@ -17049,6 +17218,7 @@ async function createSessionInTransaction(
|
|
|
17049
17218
|
firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
|
|
17050
17219
|
initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
17051
17220
|
instructions: input.instructions ?? null,
|
|
17221
|
+
policyRole: input.policyRole ?? null,
|
|
17052
17222
|
parentSessionId: input.parentSessionId ?? null,
|
|
17053
17223
|
parentTurnId,
|
|
17054
17224
|
createIdempotencyKey,
|
|
@@ -21243,6 +21413,29 @@ export async function getActiveSessionHistoryItems(
|
|
|
21243
21413
|
});
|
|
21244
21414
|
}
|
|
21245
21415
|
|
|
21416
|
+
/** Bounded finalized voice turns used only to resume a later realtime call. */
|
|
21417
|
+
export async function getSessionRealtimeContinuityEntries(
|
|
21418
|
+
db: Database,
|
|
21419
|
+
workspaceId: string,
|
|
21420
|
+
sessionId: string,
|
|
21421
|
+
maximumEntries = 20,
|
|
21422
|
+
): Promise<SessionRealtimeContinuityEntry[]> {
|
|
21423
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
21424
|
+
const [session] = await scopedDb
|
|
21425
|
+
.select({ accountId: schema.sessions.accountId })
|
|
21426
|
+
.from(schema.sessions)
|
|
21427
|
+
.where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
|
|
21428
|
+
.limit(1);
|
|
21429
|
+
if (!session) return [];
|
|
21430
|
+
return await listSessionRealtimeContinuityEntriesInTransaction(scopedDb, {
|
|
21431
|
+
accountId: session.accountId,
|
|
21432
|
+
workspaceId,
|
|
21433
|
+
sessionId,
|
|
21434
|
+
maximumEntries,
|
|
21435
|
+
});
|
|
21436
|
+
});
|
|
21437
|
+
}
|
|
21438
|
+
|
|
21246
21439
|
/**
|
|
21247
21440
|
* Count of ACTIVE (live, model-facing) history rows for a session. This is the
|
|
21248
21441
|
* length of the history the next turn is seeded from — the dual-write slice
|
|
@@ -36019,6 +36212,8 @@ export type InitializeSessionStartInput = {
|
|
|
36019
36212
|
subjectId: string;
|
|
36020
36213
|
expectedRevision: number;
|
|
36021
36214
|
} | null;
|
|
36215
|
+
/** Persist session.created only; realtime will supply the first human turn. */
|
|
36216
|
+
deferInitialTurn?: boolean;
|
|
36022
36217
|
};
|
|
36023
36218
|
|
|
36024
36219
|
export type InitializeSessionStartResult = {
|
|
@@ -36104,6 +36299,95 @@ export async function initializeSessionStartAtomically(
|
|
|
36104
36299
|
if (!goal) throw new Error("Failed to create initial session goal");
|
|
36105
36300
|
}
|
|
36106
36301
|
|
|
36302
|
+
if (input.deferInitialTurn) {
|
|
36303
|
+
const [existingCreatedEvent] = await tx
|
|
36304
|
+
.select({ id: schema.sessionEvents.id })
|
|
36305
|
+
.from(schema.sessionEvents)
|
|
36306
|
+
.where(
|
|
36307
|
+
and(
|
|
36308
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
36309
|
+
eq(schema.sessionEvents.sessionId, session.id),
|
|
36310
|
+
eq(schema.sessionEvents.type, "session.created"),
|
|
36311
|
+
),
|
|
36312
|
+
)
|
|
36313
|
+
.limit(1);
|
|
36314
|
+
let sequence = session.lastSequence;
|
|
36315
|
+
let initializedNow = false;
|
|
36316
|
+
let insertedEvents: Array<typeof schema.sessionEvents.$inferSelect> = [];
|
|
36317
|
+
if (!existingCreatedEvent) {
|
|
36318
|
+
insertedEvents = await tx
|
|
36319
|
+
.insert(schema.sessionEvents)
|
|
36320
|
+
.values([
|
|
36321
|
+
{
|
|
36322
|
+
accountId: session.accountId,
|
|
36323
|
+
workspaceId: input.workspaceId,
|
|
36324
|
+
sessionId: session.id,
|
|
36325
|
+
sequence: ++sequence,
|
|
36326
|
+
type: "session.created",
|
|
36327
|
+
payload: sanitizeEventPayload({
|
|
36328
|
+
...input.createdEventPayload,
|
|
36329
|
+
status: "idle",
|
|
36330
|
+
createdBy: creator.initiator,
|
|
36331
|
+
}),
|
|
36332
|
+
},
|
|
36333
|
+
...(goal
|
|
36334
|
+
? [
|
|
36335
|
+
{
|
|
36336
|
+
accountId: session.accountId,
|
|
36337
|
+
workspaceId: input.workspaceId,
|
|
36338
|
+
sessionId: session.id,
|
|
36339
|
+
sequence: ++sequence,
|
|
36340
|
+
type: "goal.set" as const,
|
|
36341
|
+
payload: sanitizeEventPayload({
|
|
36342
|
+
goalId: goal.id,
|
|
36343
|
+
text: goal.text,
|
|
36344
|
+
...(goal.successCriteria
|
|
36345
|
+
? { successCriteria: goal.successCriteria }
|
|
36346
|
+
: {}),
|
|
36347
|
+
version: goal.version,
|
|
36348
|
+
actor: "api",
|
|
36349
|
+
replaced: false,
|
|
36350
|
+
}),
|
|
36351
|
+
},
|
|
36352
|
+
]
|
|
36353
|
+
: []),
|
|
36354
|
+
])
|
|
36355
|
+
.returning();
|
|
36356
|
+
initializedNow = true;
|
|
36357
|
+
}
|
|
36358
|
+
await tx
|
|
36359
|
+
.update(schema.sessions)
|
|
36360
|
+
.set({
|
|
36361
|
+
temporalWorkflowId,
|
|
36362
|
+
lastSequence: sequence,
|
|
36363
|
+
...(initializedNow && session.status === "queued" ? { status: "idle" } : {}),
|
|
36364
|
+
updatedAt: new Date(),
|
|
36365
|
+
})
|
|
36366
|
+
.where(
|
|
36367
|
+
and(
|
|
36368
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
36369
|
+
eq(schema.sessions.id, session.id),
|
|
36370
|
+
),
|
|
36371
|
+
);
|
|
36372
|
+
if (initializedNow && input.consumeNewSessionDraft) {
|
|
36373
|
+
await setSubjectRlsContext(
|
|
36374
|
+
tx as unknown as Database,
|
|
36375
|
+
input.consumeNewSessionDraft.subjectId,
|
|
36376
|
+
);
|
|
36377
|
+
await seedNewSessionDraftInTransaction(tx as unknown as Database, {
|
|
36378
|
+
workspaceId: input.workspaceId,
|
|
36379
|
+
subjectId: input.consumeNewSessionDraft.subjectId,
|
|
36380
|
+
expectedRevision: input.consumeNewSessionDraft.expectedRevision,
|
|
36381
|
+
});
|
|
36382
|
+
}
|
|
36383
|
+
return {
|
|
36384
|
+
events: insertedEvents.map(mapEvent),
|
|
36385
|
+
turn: null,
|
|
36386
|
+
temporalWorkflowId,
|
|
36387
|
+
workflowWakeRevision: null,
|
|
36388
|
+
};
|
|
36389
|
+
}
|
|
36390
|
+
|
|
36107
36391
|
const existingUserEvents = await tx
|
|
36108
36392
|
.select()
|
|
36109
36393
|
.from(schema.sessionEvents)
|
|
@@ -36207,6 +36491,7 @@ export async function initializeSessionStartAtomically(
|
|
|
36207
36491
|
let queueTailPosition = Number(session.queueTailPosition);
|
|
36208
36492
|
if (!turn) {
|
|
36209
36493
|
queueTailPosition += 1;
|
|
36494
|
+
const acceptedAt = new Date();
|
|
36210
36495
|
[turn] = await tx
|
|
36211
36496
|
.insert(schema.sessionTurns)
|
|
36212
36497
|
.values({
|
|
@@ -36238,10 +36523,14 @@ export async function initializeSessionStartAtomically(
|
|
|
36238
36523
|
: {},
|
|
36239
36524
|
lineage: {},
|
|
36240
36525
|
...initiatorColumns(creator),
|
|
36526
|
+
initiatingHumanSubjectId:
|
|
36527
|
+
creator.initiator.kind === "subject" ? creator.initiator.subjectId : null,
|
|
36241
36528
|
personalConnectionDelegations: parsedPersonalConnectionDelegations(
|
|
36242
36529
|
session.initialPersonalConnectionDelegations,
|
|
36243
36530
|
`sessions:${session.workspaceId}:${session.id}:initial`,
|
|
36244
36531
|
),
|
|
36532
|
+
createdAt: acceptedAt,
|
|
36533
|
+
updatedAt: acceptedAt,
|
|
36245
36534
|
})
|
|
36246
36535
|
.returning();
|
|
36247
36536
|
if (!turn) throw new Error("Failed to create initial session turn");
|
|
@@ -36375,6 +36664,7 @@ export async function enqueueSessionTurn(
|
|
|
36375
36664
|
const position = atHead
|
|
36376
36665
|
? Number(lockedSession.queueHeadPosition) - 1
|
|
36377
36666
|
: Number(lockedSession.queueTailPosition) + 1;
|
|
36667
|
+
const acceptedAt = new Date();
|
|
36378
36668
|
const [row] = await tx
|
|
36379
36669
|
.insert(schema.sessionTurns)
|
|
36380
36670
|
.values({
|
|
@@ -36402,7 +36692,11 @@ export async function enqueueSessionTurn(
|
|
|
36402
36692
|
initiator: input.initiator,
|
|
36403
36693
|
context: input.initiatorContext ?? {},
|
|
36404
36694
|
}),
|
|
36695
|
+
initiatingHumanSubjectId:
|
|
36696
|
+
input.initiator.kind === "subject" ? input.initiator.subjectId : null,
|
|
36405
36697
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
36698
|
+
createdAt: acceptedAt,
|
|
36699
|
+
updatedAt: acceptedAt,
|
|
36406
36700
|
})
|
|
36407
36701
|
.returning();
|
|
36408
36702
|
if (!row) {
|
|
@@ -36838,8 +37132,14 @@ export async function claimSessionWorkForAttempt(
|
|
|
36838
37132
|
sessionId,
|
|
36839
37133
|
{ workspaceControl },
|
|
36840
37134
|
);
|
|
36841
|
-
|
|
37135
|
+
let session = prefix.sessions[0];
|
|
36842
37136
|
if (!session) return { action: "unclaimed", reason: "no-work" };
|
|
37137
|
+
// Work and realtime may coexist, but claim remains the lazy lifecycle
|
|
37138
|
+
// cleanup point for an expired voice lease.
|
|
37139
|
+
session = await settleExpiredSessionRealtimeInTransaction(
|
|
37140
|
+
tx as unknown as Database,
|
|
37141
|
+
session,
|
|
37142
|
+
);
|
|
36843
37143
|
if (effectiveControl.state !== "active") {
|
|
36844
37144
|
return { action: "unclaimed", reason: "gate-closed" };
|
|
36845
37145
|
}
|
|
@@ -36919,7 +37219,37 @@ export async function claimSessionWorkForAttempt(
|
|
|
36919
37219
|
if (connectorPolicyRows.length > 2048) {
|
|
36920
37220
|
throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
|
|
36921
37221
|
}
|
|
36922
|
-
|
|
37222
|
+
// New queued, compaction, and internal-update executions are born
|
|
37223
|
+
// while the locked session still has no active-turn pointer. Publish
|
|
37224
|
+
// that ownership edge before freezing governance so the database
|
|
37225
|
+
// functions can require the complete exact-current-attempt chain.
|
|
37226
|
+
// The enclosing transaction keeps this invisible unless every later
|
|
37227
|
+
// claim/event/session settlement step succeeds.
|
|
37228
|
+
if (session.activeTurnId !== turn.id) {
|
|
37229
|
+
if (session.activeTurnId !== null) {
|
|
37230
|
+
throw new SessionControlInvariantError(
|
|
37231
|
+
`Attempt ${input.attemptId} cannot replace active turn ${session.activeTurnId}`,
|
|
37232
|
+
);
|
|
37233
|
+
}
|
|
37234
|
+
const [claimedSession] = await tx
|
|
37235
|
+
.update(schema.sessions)
|
|
37236
|
+
.set({ activeTurnId: turn.id })
|
|
37237
|
+
.where(
|
|
37238
|
+
and(
|
|
37239
|
+
eq(schema.sessions.accountId, session.accountId),
|
|
37240
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
37241
|
+
eq(schema.sessions.id, sessionId),
|
|
37242
|
+
isNull(schema.sessions.activeTurnId),
|
|
37243
|
+
),
|
|
37244
|
+
)
|
|
37245
|
+
.returning({ id: schema.sessions.id });
|
|
37246
|
+
if (!claimedSession) {
|
|
37247
|
+
throw new SessionControlInvariantError(
|
|
37248
|
+
`Attempt ${input.attemptId} could not establish active turn ${turn.id}`,
|
|
37249
|
+
);
|
|
37250
|
+
}
|
|
37251
|
+
}
|
|
37252
|
+
const attempt = await registerSessionTurnAttemptClaim(tx as unknown as Database, {
|
|
36923
37253
|
id: input.attemptId,
|
|
36924
37254
|
accountId: session.accountId,
|
|
36925
37255
|
workspaceId,
|
|
@@ -36933,6 +37263,42 @@ export async function claimSessionWorkForAttempt(
|
|
|
36933
37263
|
mcpApprovalPolicies,
|
|
36934
37264
|
connectorActionPolicies: connectorPolicyRows,
|
|
36935
37265
|
});
|
|
37266
|
+
// Freeze governance only after this exact attempt is durably claimed,
|
|
37267
|
+
// while the claim transaction still owns the session/turn/attempt
|
|
37268
|
+
// locks. Later policy or preference activation applies to a future
|
|
37269
|
+
// attempt and can never move accepted queued/in-flight work.
|
|
37270
|
+
await tx.execute(sql`
|
|
37271
|
+
SELECT snapshot.id
|
|
37272
|
+
FROM workspace_instruction_policy_get_or_create_snapshot(
|
|
37273
|
+
${session.accountId}::uuid,
|
|
37274
|
+
${workspaceId}::uuid,
|
|
37275
|
+
${sessionId}::uuid,
|
|
37276
|
+
${turn.id}::uuid,
|
|
37277
|
+
${input.attemptId}::uuid,
|
|
37278
|
+
${turn.executionGeneration}
|
|
37279
|
+
) snapshot
|
|
37280
|
+
`);
|
|
37281
|
+
const initiatingHumanSubjectId =
|
|
37282
|
+
turn.initiatingHumanSubjectId ??
|
|
37283
|
+
(turn.initiatorKind === "subject" ? turn.initiatorSubjectId : null);
|
|
37284
|
+
if (initiatingHumanSubjectId) {
|
|
37285
|
+
await tx.execute(sql`
|
|
37286
|
+
SELECT snapshot.id
|
|
37287
|
+
FROM preference_registry_get_or_create_snapshot(
|
|
37288
|
+
${session.accountId}::uuid,
|
|
37289
|
+
${workspaceId}::uuid,
|
|
37290
|
+
${sessionId}::uuid,
|
|
37291
|
+
${turn.id}::uuid,
|
|
37292
|
+
${input.attemptId}::uuid,
|
|
37293
|
+
${turn.executionGeneration}
|
|
37294
|
+
) snapshot
|
|
37295
|
+
`);
|
|
37296
|
+
// The preference function applies the immutable initiating human
|
|
37297
|
+
// only transaction-locally. Clear it before the rest of claim
|
|
37298
|
+
// settlement so no unrelated subject-scoped read inherits it.
|
|
37299
|
+
await tx.execute(sql`SELECT set_config('opengeni.subject_id', '', true)`);
|
|
37300
|
+
}
|
|
37301
|
+
return attempt;
|
|
36936
37302
|
};
|
|
36937
37303
|
if (session.activeTurnId !== null) {
|
|
36938
37304
|
const [activeTurnPreview] = await tx
|
|
@@ -37280,6 +37646,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37280
37646
|
latencyMode: schema.sessionTurns.latencyMode,
|
|
37281
37647
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37282
37648
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
37649
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
37650
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
37651
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37283
37652
|
})
|
|
37284
37653
|
.from(schema.sessionTurns)
|
|
37285
37654
|
.where(
|
|
@@ -37329,8 +37698,15 @@ export async function claimSessionWorkForAttempt(
|
|
|
37329
37698
|
},
|
|
37330
37699
|
),
|
|
37331
37700
|
...initiatorColumns(compactionInitiator),
|
|
37701
|
+
initiatingHumanSubjectId:
|
|
37702
|
+
latestStarted?.initiatingHumanSubjectId ??
|
|
37703
|
+
(latestStarted?.initiatorKind === "subject"
|
|
37704
|
+
? latestStarted.initiatorSubjectId
|
|
37705
|
+
: null),
|
|
37332
37706
|
personalConnectionDelegations: [],
|
|
37333
37707
|
startedAt: now,
|
|
37708
|
+
createdAt: now,
|
|
37709
|
+
updatedAt: now,
|
|
37334
37710
|
})
|
|
37335
37711
|
.returning();
|
|
37336
37712
|
if (!compactionTurn) throw new Error("Failed to create context compaction execution");
|
|
@@ -37579,6 +37955,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37579
37955
|
tools: schema.sessionTurns.tools,
|
|
37580
37956
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37581
37957
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
37958
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
37959
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
37960
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37582
37961
|
})
|
|
37583
37962
|
.from(schema.sessionTurns)
|
|
37584
37963
|
.where(
|
|
@@ -37613,6 +37992,39 @@ export async function claimSessionWorkForAttempt(
|
|
|
37613
37992
|
typeof goalPolicy?.sandboxBackend === "string"
|
|
37614
37993
|
? goalPolicy.sandboxBackend
|
|
37615
37994
|
: (latestStarted?.sandboxBackend ?? session.sandboxBackend);
|
|
37995
|
+
let initiatingHumanSubjectId =
|
|
37996
|
+
internalInitiator.initiator.kind === "subject"
|
|
37997
|
+
? internalInitiator.initiator.subjectId
|
|
37998
|
+
: null;
|
|
37999
|
+
if (!initiatingHumanSubjectId && routingGoalUpdate) {
|
|
38000
|
+
const causalTurnIdValue =
|
|
38001
|
+
routingGoalUpdate.lineage &&
|
|
38002
|
+
typeof routingGoalUpdate.lineage === "object" &&
|
|
38003
|
+
!Array.isArray(routingGoalUpdate.lineage)
|
|
38004
|
+
? (routingGoalUpdate.lineage as Record<string, unknown>).causalTurnId
|
|
38005
|
+
: null;
|
|
38006
|
+
const causalTurnId = typeof causalTurnIdValue === "string" ? causalTurnIdValue : null;
|
|
38007
|
+
if (causalTurnId) {
|
|
38008
|
+
const [causalTurn] = await tx
|
|
38009
|
+
.select({
|
|
38010
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
38011
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
38012
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
38013
|
+
})
|
|
38014
|
+
.from(schema.sessionTurns)
|
|
38015
|
+
.where(
|
|
38016
|
+
and(
|
|
38017
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
38018
|
+
eq(schema.sessionTurns.sessionId, sessionId),
|
|
38019
|
+
eq(schema.sessionTurns.id, causalTurnId),
|
|
38020
|
+
),
|
|
38021
|
+
)
|
|
38022
|
+
.limit(1);
|
|
38023
|
+
initiatingHumanSubjectId =
|
|
38024
|
+
causalTurn?.initiatingHumanSubjectId ??
|
|
38025
|
+
(causalTurn?.initiatorKind === "subject" ? causalTurn.initiatorSubjectId : null);
|
|
38026
|
+
}
|
|
38027
|
+
}
|
|
37616
38028
|
await tx.execute(sql`set local opengeni.session_inference_claim = '1'`);
|
|
37617
38029
|
const [internalTurn] = await tx
|
|
37618
38030
|
.insert(schema.sessionTurns)
|
|
@@ -37644,8 +38056,11 @@ export async function claimSessionWorkForAttempt(
|
|
|
37644
38056
|
{ id: input.dispatchId, generation: 1, triggerEventId },
|
|
37645
38057
|
),
|
|
37646
38058
|
...initiatorColumns(internalInitiator),
|
|
38059
|
+
initiatingHumanSubjectId,
|
|
37647
38060
|
personalConnectionDelegations: internalPersonalConnectionDelegations,
|
|
37648
38061
|
startedAt: now,
|
|
38062
|
+
createdAt: now,
|
|
38063
|
+
updatedAt: now,
|
|
37649
38064
|
})
|
|
37650
38065
|
.returning();
|
|
37651
38066
|
if (!internalTurn) throw new Error("Failed to create internal update inference");
|
|
@@ -37765,13 +38180,28 @@ export async function claimSessionWorkForAttempt(
|
|
|
37765
38180
|
}),
|
|
37766
38181
|
producerCodexCredentialId: null,
|
|
37767
38182
|
});
|
|
37768
|
-
const
|
|
37769
|
-
|
|
37770
|
-
|
|
37771
|
-
|
|
37772
|
-
|
|
37773
|
-
|
|
37774
|
-
|
|
38183
|
+
const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
|
|
38184
|
+
// Cross-session updates are already projected through
|
|
38185
|
+
// delegation.context.append. Keep them pending instead of consuming
|
|
38186
|
+
// them as hidden context on the provider-delegated ordinary turn.
|
|
38187
|
+
const delivered = providerDelegatedTurn
|
|
38188
|
+
? {
|
|
38189
|
+
count: 0,
|
|
38190
|
+
lastSequence: session.lastSequence,
|
|
38191
|
+
triggerEventId: null,
|
|
38192
|
+
historyItemId: null,
|
|
38193
|
+
historyItem: null,
|
|
38194
|
+
updates: [] as Array<typeof schema.sessionSystemUpdates.$inferSelect>,
|
|
38195
|
+
events: [] as Array<typeof schema.sessionEvents.$inferInsert>,
|
|
38196
|
+
event: null,
|
|
38197
|
+
}
|
|
38198
|
+
: await deliverPendingUpdates(
|
|
38199
|
+
session.accountId,
|
|
38200
|
+
row.id,
|
|
38201
|
+
row.executionGeneration,
|
|
38202
|
+
session.lastSequence + 1,
|
|
38203
|
+
now,
|
|
38204
|
+
);
|
|
37775
38205
|
await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
|
|
37776
38206
|
if (delivered.events.length > 0) {
|
|
37777
38207
|
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
@@ -38345,15 +38775,23 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38345
38775
|
throw new Error(`Live interrupted attempt ${attemptId} lost its exact turn ownership`);
|
|
38346
38776
|
}
|
|
38347
38777
|
|
|
38348
|
-
const
|
|
38349
|
-
const
|
|
38350
|
-
|
|
38351
|
-
|
|
38352
|
-
|
|
38353
|
-
|
|
38354
|
-
|
|
38355
|
-
|
|
38356
|
-
|
|
38778
|
+
const terminalCancel = session.status === "cancelled";
|
|
38779
|
+
const steer =
|
|
38780
|
+
!terminalCancel && interruptions.some((interruption) => interruption.kind === "steer");
|
|
38781
|
+
const outcome: SessionTurnAttemptOutcome = terminalCancel
|
|
38782
|
+
? "cancelled"
|
|
38783
|
+
: steer
|
|
38784
|
+
? "superseded"
|
|
38785
|
+
: "interrupted_recoverable";
|
|
38786
|
+
const reason = terminalCancel
|
|
38787
|
+
? "session_cancelled"
|
|
38788
|
+
: steer
|
|
38789
|
+
? "steer"
|
|
38790
|
+
: interruptions.some((interruption) => interruption.kind === "workspace_pause")
|
|
38791
|
+
? "workspace_pause"
|
|
38792
|
+
: interruptions.some((interruption) => interruption.kind === "maintenance")
|
|
38793
|
+
? "maintenance"
|
|
38794
|
+
: "session_pause";
|
|
38357
38795
|
let sequence = session.lastSequence;
|
|
38358
38796
|
const closedTools = await closePendingSessionToolCallsInTransaction(
|
|
38359
38797
|
tx as unknown as Database,
|
|
@@ -38378,38 +38816,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38378
38816
|
outcome,
|
|
38379
38817
|
closedAt: now,
|
|
38380
38818
|
});
|
|
38381
|
-
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> =
|
|
38819
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = terminalCancel
|
|
38382
38820
|
? [
|
|
38383
38821
|
{
|
|
38384
38822
|
accountId: session.accountId,
|
|
38385
38823
|
workspaceId,
|
|
38386
38824
|
sessionId,
|
|
38387
38825
|
sequence: ++sequence,
|
|
38388
|
-
type: "turn.
|
|
38389
|
-
turnId: turn.id,
|
|
38390
|
-
turnGeneration: turn.executionGeneration,
|
|
38391
|
-
turnAttemptId: attemptId,
|
|
38392
|
-
turnAssociation: "current",
|
|
38393
|
-
payload: sanitizeEventPayload({ reason: "steer" }),
|
|
38394
|
-
occurredAt: now,
|
|
38395
|
-
},
|
|
38396
|
-
{
|
|
38397
|
-
accountId: session.accountId,
|
|
38398
|
-
workspaceId,
|
|
38399
|
-
sessionId,
|
|
38400
|
-
sequence: ++sequence,
|
|
38401
|
-
type: "session.status.changed",
|
|
38402
|
-
payload: sanitizeEventPayload({ status: "queued" }),
|
|
38403
|
-
occurredAt: now,
|
|
38404
|
-
},
|
|
38405
|
-
]
|
|
38406
|
-
: [
|
|
38407
|
-
{
|
|
38408
|
-
accountId: session.accountId,
|
|
38409
|
-
workspaceId,
|
|
38410
|
-
sessionId,
|
|
38411
|
-
sequence: ++sequence,
|
|
38412
|
-
type: "turn.recovery.requested",
|
|
38826
|
+
type: "turn.cancelled",
|
|
38413
38827
|
turnId: turn.id,
|
|
38414
38828
|
turnGeneration: turn.executionGeneration,
|
|
38415
38829
|
turnAttemptId: attemptId,
|
|
@@ -38417,50 +38831,101 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38417
38831
|
payload: sanitizeEventPayload({ reason }),
|
|
38418
38832
|
occurredAt: now,
|
|
38419
38833
|
},
|
|
38420
|
-
|
|
38421
|
-
|
|
38422
|
-
|
|
38423
|
-
|
|
38424
|
-
|
|
38425
|
-
|
|
38426
|
-
|
|
38427
|
-
|
|
38428
|
-
|
|
38429
|
-
|
|
38430
|
-
|
|
38431
|
-
|
|
38432
|
-
|
|
38433
|
-
|
|
38834
|
+
]
|
|
38835
|
+
: steer
|
|
38836
|
+
? [
|
|
38837
|
+
{
|
|
38838
|
+
accountId: session.accountId,
|
|
38839
|
+
workspaceId,
|
|
38840
|
+
sessionId,
|
|
38841
|
+
sequence: ++sequence,
|
|
38842
|
+
type: "turn.superseded",
|
|
38843
|
+
turnId: turn.id,
|
|
38844
|
+
turnGeneration: turn.executionGeneration,
|
|
38845
|
+
turnAttemptId: attemptId,
|
|
38846
|
+
turnAssociation: "current",
|
|
38847
|
+
payload: sanitizeEventPayload({ reason: "steer" }),
|
|
38848
|
+
occurredAt: now,
|
|
38849
|
+
},
|
|
38850
|
+
{
|
|
38851
|
+
accountId: session.accountId,
|
|
38852
|
+
workspaceId,
|
|
38853
|
+
sessionId,
|
|
38854
|
+
sequence: ++sequence,
|
|
38855
|
+
type: "session.status.changed",
|
|
38856
|
+
payload: sanitizeEventPayload({ status: "queued" }),
|
|
38857
|
+
occurredAt: now,
|
|
38858
|
+
},
|
|
38859
|
+
]
|
|
38860
|
+
: [
|
|
38861
|
+
{
|
|
38862
|
+
accountId: session.accountId,
|
|
38863
|
+
workspaceId,
|
|
38864
|
+
sessionId,
|
|
38865
|
+
sequence: ++sequence,
|
|
38866
|
+
type: "turn.recovery.requested",
|
|
38867
|
+
turnId: turn.id,
|
|
38868
|
+
turnGeneration: turn.executionGeneration,
|
|
38869
|
+
turnAttemptId: attemptId,
|
|
38870
|
+
turnAssociation: "current",
|
|
38871
|
+
payload: sanitizeEventPayload({ reason }),
|
|
38872
|
+
occurredAt: now,
|
|
38873
|
+
},
|
|
38874
|
+
{
|
|
38875
|
+
accountId: session.accountId,
|
|
38876
|
+
workspaceId,
|
|
38877
|
+
sessionId,
|
|
38878
|
+
sequence: ++sequence,
|
|
38879
|
+
type: "session.status.changed",
|
|
38880
|
+
turnId: turn.id,
|
|
38881
|
+
turnGeneration: turn.executionGeneration,
|
|
38882
|
+
turnAttemptId: attemptId,
|
|
38883
|
+
turnAssociation: "current",
|
|
38884
|
+
payload: sanitizeEventPayload({ status: "recovering" }),
|
|
38885
|
+
occurredAt: now,
|
|
38886
|
+
},
|
|
38887
|
+
];
|
|
38434
38888
|
const eventRows = await tx.insert(schema.sessionEvents).values(eventValues).returning();
|
|
38435
38889
|
await tx
|
|
38436
38890
|
.update(schema.sessionTurns)
|
|
38437
38891
|
.set(
|
|
38438
|
-
|
|
38892
|
+
terminalCancel
|
|
38439
38893
|
? {
|
|
38440
|
-
status: "
|
|
38894
|
+
status: "cancelled",
|
|
38441
38895
|
activeAttemptId: null,
|
|
38442
38896
|
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
38897
|
+
cancelledBy: "system:session_cancelled",
|
|
38898
|
+
cancelReason: reason,
|
|
38443
38899
|
version: turn.version + 1,
|
|
38444
38900
|
finishedAt: turn.finishedAt ?? now,
|
|
38445
38901
|
updatedAt: now,
|
|
38446
38902
|
}
|
|
38447
|
-
:
|
|
38448
|
-
|
|
38449
|
-
|
|
38450
|
-
|
|
38451
|
-
|
|
38452
|
-
|
|
38453
|
-
|
|
38454
|
-
|
|
38455
|
-
|
|
38456
|
-
|
|
38903
|
+
: steer
|
|
38904
|
+
? {
|
|
38905
|
+
status: "superseded",
|
|
38906
|
+
activeAttemptId: null,
|
|
38907
|
+
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
38908
|
+
version: turn.version + 1,
|
|
38909
|
+
finishedAt: turn.finishedAt ?? now,
|
|
38910
|
+
updatedAt: now,
|
|
38911
|
+
}
|
|
38912
|
+
: {
|
|
38913
|
+
status: "recovering",
|
|
38914
|
+
activeAttemptId: null,
|
|
38915
|
+
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
38916
|
+
cancelledBy: null,
|
|
38917
|
+
cancelReason: null,
|
|
38918
|
+
version: turn.version + 1,
|
|
38919
|
+
finishedAt: null,
|
|
38920
|
+
updatedAt: now,
|
|
38921
|
+
},
|
|
38457
38922
|
)
|
|
38458
38923
|
.where(eq(schema.sessionTurns.id, turn.id));
|
|
38459
38924
|
await tx
|
|
38460
38925
|
.update(schema.sessions)
|
|
38461
38926
|
.set({
|
|
38462
|
-
status: steer ? "queued" : "recovering",
|
|
38463
|
-
activeTurnId: steer ? null : turn.id,
|
|
38927
|
+
status: terminalCancel ? "cancelled" : steer ? "queued" : "recovering",
|
|
38928
|
+
activeTurnId: terminalCancel || steer ? null : turn.id,
|
|
38464
38929
|
lastSequence: sequence,
|
|
38465
38930
|
updatedAt: now,
|
|
38466
38931
|
})
|
|
@@ -39293,6 +39758,13 @@ export type ApplySessionTurnSettlementResult =
|
|
|
39293
39758
|
activeTurnId: string | null;
|
|
39294
39759
|
};
|
|
39295
39760
|
|
|
39761
|
+
export type ApplySessionTurnSettlementHooks = {
|
|
39762
|
+
/** Test-only failure injection after terminal projection but before commit. */
|
|
39763
|
+
afterRealtimeDelegationProjection?:
|
|
39764
|
+
| ((projection: { entryId: string; turnId: string }) => void | Promise<void>)
|
|
39765
|
+
| undefined;
|
|
39766
|
+
};
|
|
39767
|
+
|
|
39296
39768
|
function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAttemptOutcome | null {
|
|
39297
39769
|
switch (status) {
|
|
39298
39770
|
case "completed":
|
|
@@ -39306,6 +39778,40 @@ function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAtte
|
|
|
39306
39778
|
}
|
|
39307
39779
|
}
|
|
39308
39780
|
|
|
39781
|
+
type TerminalSessionTurnStatus = "completed" | "failed" | "cancelled" | "superseded";
|
|
39782
|
+
type TerminalSessionTurnEventType =
|
|
39783
|
+
| "turn.completed"
|
|
39784
|
+
| "turn.failed"
|
|
39785
|
+
| "turn.cancelled"
|
|
39786
|
+
| "turn.superseded";
|
|
39787
|
+
|
|
39788
|
+
function terminalSessionTurnEventType(
|
|
39789
|
+
status: TerminalSessionTurnStatus,
|
|
39790
|
+
): TerminalSessionTurnEventType {
|
|
39791
|
+
switch (status) {
|
|
39792
|
+
case "completed":
|
|
39793
|
+
return "turn.completed";
|
|
39794
|
+
case "failed":
|
|
39795
|
+
return "turn.failed";
|
|
39796
|
+
case "cancelled":
|
|
39797
|
+
return "turn.cancelled";
|
|
39798
|
+
case "superseded":
|
|
39799
|
+
return "turn.superseded";
|
|
39800
|
+
}
|
|
39801
|
+
}
|
|
39802
|
+
|
|
39803
|
+
function isTerminalSessionTurnStatus(
|
|
39804
|
+
status: SessionTurnStatus,
|
|
39805
|
+
): status is TerminalSessionTurnStatus {
|
|
39806
|
+
return ["completed", "failed", "cancelled", "superseded"].includes(status);
|
|
39807
|
+
}
|
|
39808
|
+
|
|
39809
|
+
function sessionEventPayloadRecord(payload: unknown): Record<string, unknown> {
|
|
39810
|
+
return payload && typeof payload === "object" && !Array.isArray(payload)
|
|
39811
|
+
? (payload as Record<string, unknown>)
|
|
39812
|
+
: {};
|
|
39813
|
+
}
|
|
39814
|
+
|
|
39309
39815
|
/**
|
|
39310
39816
|
* Atomically append terminal/requires-action truth, update the exact turn, and
|
|
39311
39817
|
* transition the owning session. A superseded dispatch or closed control gate
|
|
@@ -39316,6 +39822,7 @@ export async function applySessionTurnSettlement(
|
|
|
39316
39822
|
db: Database,
|
|
39317
39823
|
workspaceId: string,
|
|
39318
39824
|
input: ApplySessionTurnSettlementInput,
|
|
39825
|
+
hooks: ApplySessionTurnSettlementHooks = {},
|
|
39319
39826
|
): Promise<ApplySessionTurnSettlementResult> {
|
|
39320
39827
|
const fromStatuses = input.fromStatuses ?? ["running", "requires_action"];
|
|
39321
39828
|
const eventTypes = [
|
|
@@ -39744,11 +40251,37 @@ export async function applySessionTurnSettlement(
|
|
|
39744
40251
|
});
|
|
39745
40252
|
const inserted =
|
|
39746
40253
|
values.length > 0 ? await tx.insert(schema.sessionEvents).values(values).returning() : [];
|
|
39747
|
-
const terminal =
|
|
39748
|
-
|
|
39749
|
-
input.turnStatus
|
|
39750
|
-
|
|
39751
|
-
|
|
40254
|
+
const terminal = isTerminalSessionTurnStatus(input.turnStatus);
|
|
40255
|
+
if (isTerminalSessionTurnStatus(input.turnStatus)) {
|
|
40256
|
+
const terminalType = terminalSessionTurnEventType(input.turnStatus);
|
|
40257
|
+
const persistedTerminal = inserted.find((event) => event.type === terminalType);
|
|
40258
|
+
const projection = await projectSessionRealtimeDelegationTerminalInTransaction(
|
|
40259
|
+
tx as unknown as Database,
|
|
40260
|
+
{
|
|
40261
|
+
accountId: session.accountId,
|
|
40262
|
+
workspaceId,
|
|
40263
|
+
sessionId: input.sessionId,
|
|
40264
|
+
turnId: input.turnId,
|
|
40265
|
+
turnStatus: input.turnStatus,
|
|
40266
|
+
terminalEvent: {
|
|
40267
|
+
type: terminalType,
|
|
40268
|
+
payload: persistedTerminal
|
|
40269
|
+
? sessionEventPayloadRecord(persistedTerminal.payload)
|
|
40270
|
+
: {
|
|
40271
|
+
code: "delegation_terminal_event_missing",
|
|
40272
|
+
error: `Delegated turn reached ${input.turnStatus} without its canonical terminal event.`,
|
|
40273
|
+
},
|
|
40274
|
+
},
|
|
40275
|
+
now,
|
|
40276
|
+
},
|
|
40277
|
+
);
|
|
40278
|
+
if (projection) {
|
|
40279
|
+
await hooks.afterRealtimeDelegationProjection?.({
|
|
40280
|
+
entryId: projection.entry.id,
|
|
40281
|
+
turnId: input.turnId,
|
|
40282
|
+
});
|
|
40283
|
+
}
|
|
40284
|
+
}
|
|
39752
40285
|
if (input.turnStatus === "running") {
|
|
39753
40286
|
// Approval resume re-enters the same logical turn after a new fenced
|
|
39754
40287
|
// dispatch has advanced its trigger. It is an authorized inference
|
|
@@ -40037,6 +40570,20 @@ export async function settleCodexCredentialLeaseLoss(
|
|
|
40037
40570
|
if (!settlementEvent) {
|
|
40038
40571
|
throw new Error("Codex lease-loss settlement did not persist its checkpoint event");
|
|
40039
40572
|
}
|
|
40573
|
+
if (!input.checkpointDurable) {
|
|
40574
|
+
await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
|
|
40575
|
+
accountId: input.accountId,
|
|
40576
|
+
workspaceId: input.workspaceId,
|
|
40577
|
+
sessionId: input.sessionId,
|
|
40578
|
+
turnId: input.turnId,
|
|
40579
|
+
turnStatus: "failed",
|
|
40580
|
+
terminalEvent: {
|
|
40581
|
+
type: "turn.failed",
|
|
40582
|
+
payload: sessionEventPayloadRecord(settlementEvent.payload),
|
|
40583
|
+
},
|
|
40584
|
+
now,
|
|
40585
|
+
});
|
|
40586
|
+
}
|
|
40040
40587
|
|
|
40041
40588
|
await tx
|
|
40042
40589
|
.update(schema.sessionTurns)
|
|
@@ -40766,6 +41313,22 @@ export async function recoverSessionDispatch(
|
|
|
40766
41313
|
},
|
|
40767
41314
|
])
|
|
40768
41315
|
.returning();
|
|
41316
|
+
const failedEvent = inserted.find((event) => event.type === "turn.failed");
|
|
41317
|
+
if (!failedEvent) {
|
|
41318
|
+
throw new Error("Worker-death exhaustion did not persist its terminal event");
|
|
41319
|
+
}
|
|
41320
|
+
await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
|
|
41321
|
+
accountId: session.accountId,
|
|
41322
|
+
workspaceId,
|
|
41323
|
+
sessionId: input.sessionId,
|
|
41324
|
+
turnId: turn.id,
|
|
41325
|
+
turnStatus: "failed",
|
|
41326
|
+
terminalEvent: {
|
|
41327
|
+
type: "turn.failed",
|
|
41328
|
+
payload: sessionEventPayloadRecord(failedEvent.payload),
|
|
41329
|
+
},
|
|
41330
|
+
now,
|
|
41331
|
+
});
|
|
40769
41332
|
await tx
|
|
40770
41333
|
.update(schema.sessionTurns)
|
|
40771
41334
|
.set({
|
|
@@ -41504,8 +42067,14 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
|
|
|
41504
42067
|
.for("update")
|
|
41505
42068
|
.limit(1);
|
|
41506
42069
|
if (!workspace || !session) throw new Error(`Session not found: ${input.sessionId}`);
|
|
42070
|
+
const realtimeActive = await sessionRealtimeIsActiveInTransaction(
|
|
42071
|
+
tx as unknown as Database,
|
|
42072
|
+
input.workspaceId,
|
|
42073
|
+
input.sessionId,
|
|
42074
|
+
);
|
|
41507
42075
|
const runnable =
|
|
41508
42076
|
session.status !== "cancelled" &&
|
|
42077
|
+
!realtimeActive &&
|
|
41509
42078
|
session.activeTurnId === null &&
|
|
41510
42079
|
effectiveControl.state === "active";
|
|
41511
42080
|
return runnable
|
|
@@ -41998,7 +42567,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
|
|
|
41998
42567
|
.returning();
|
|
41999
42568
|
if (!event) throw new Error("Failed to create system-update pending event");
|
|
42000
42569
|
await mutateSource(tx as unknown as Database, event.id);
|
|
42001
|
-
const
|
|
42570
|
+
const realtimeActive = await sessionRealtimeIsActiveInTransaction(
|
|
42571
|
+
tx as unknown as Database,
|
|
42572
|
+
input.workspaceId,
|
|
42573
|
+
input.sessionId,
|
|
42574
|
+
);
|
|
42575
|
+
const shouldWake =
|
|
42576
|
+
!realtimeActive && session.activeTurnId === null && effectiveControl.state === "active";
|
|
42002
42577
|
const wake = shouldWake
|
|
42003
42578
|
? await registerInternalUpdateWakeInTransaction(tx as unknown as Database, {
|
|
42004
42579
|
accountId: session.accountId,
|
|
@@ -42562,6 +43137,21 @@ export async function appendSessionEventsForTurnAttempt(
|
|
|
42562
43137
|
};
|
|
42563
43138
|
});
|
|
42564
43139
|
const inserted = await tx.insert(schema.sessionEvents).values(values).returning();
|
|
43140
|
+
if (fence.allowed) {
|
|
43141
|
+
await projectSessionRealtimeDelegationProgressInTransaction(tx as unknown as Database, {
|
|
43142
|
+
accountId: session.accountId,
|
|
43143
|
+
workspaceId,
|
|
43144
|
+
sessionId,
|
|
43145
|
+
turnId,
|
|
43146
|
+
events: inserted.map((event) => ({
|
|
43147
|
+
id: event.id,
|
|
43148
|
+
sequence: event.sequence,
|
|
43149
|
+
type: event.type,
|
|
43150
|
+
payload: event.payload,
|
|
43151
|
+
})),
|
|
43152
|
+
now,
|
|
43153
|
+
});
|
|
43154
|
+
}
|
|
42565
43155
|
await tx
|
|
42566
43156
|
.update(schema.sessions)
|
|
42567
43157
|
.set({
|
|
@@ -42986,6 +43576,7 @@ function mapSession(
|
|
|
42986
43576
|
title: row.title ?? null,
|
|
42987
43577
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
42988
43578
|
instructions: row.instructions ?? null,
|
|
43579
|
+
policyRole: row.policyRole ?? null,
|
|
42989
43580
|
resources: row.resources as ResourceRef[],
|
|
42990
43581
|
skills: (row.skills as SessionSkill[]) ?? [],
|
|
42991
43582
|
tools: row.tools as ToolRef[],
|