@opengeni/db 0.22.2 → 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 +39 -3
- package/dist/index.js +7009 -3882
- 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 +614 -78
- 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
|
@@ -217,11 +217,25 @@ import {
|
|
|
217
217
|
serializeEffectiveSessionControl,
|
|
218
218
|
type SessionDiscoveryControl,
|
|
219
219
|
type SessionCommandActor,
|
|
220
|
+
SessionControlConflictError,
|
|
220
221
|
SessionControlInvariantError,
|
|
221
222
|
updateSessionCommandReceiptResult,
|
|
222
223
|
type SessionTurnAttemptOutcome,
|
|
223
224
|
type WorkspaceControlRow,
|
|
224
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";
|
|
225
239
|
import * as schema from "./schema";
|
|
226
240
|
import {
|
|
227
241
|
AGENT_VISIBLE_MEMORY_STATUSES,
|
|
@@ -243,6 +257,9 @@ import {
|
|
|
243
257
|
export { sql as dbSql } from "drizzle-orm";
|
|
244
258
|
export * from "./session-control";
|
|
245
259
|
export * from "./session-queue-commands";
|
|
260
|
+
export * from "./session-realtime";
|
|
261
|
+
export * from "./session-realtime-context";
|
|
262
|
+
export * from "./session-realtime-ledger";
|
|
246
263
|
export * from "./new-session-drafts";
|
|
247
264
|
export * from "./workspace-instruction-policies";
|
|
248
265
|
export * from "./preference-registry";
|
|
@@ -5388,6 +5405,7 @@ export type SlackBotUserLink = {
|
|
|
5388
5405
|
export type SlackInteractionTriggerKind =
|
|
5389
5406
|
| "app_mention"
|
|
5390
5407
|
| "dm"
|
|
5408
|
+
| "reaction"
|
|
5391
5409
|
| "slash_command"
|
|
5392
5410
|
| "message_shortcut"
|
|
5393
5411
|
| "thread_reply";
|
|
@@ -5412,6 +5430,7 @@ export type SlackInteractionInboxEntry = {
|
|
|
5412
5430
|
attemptCount: number;
|
|
5413
5431
|
retryAt: Date | null;
|
|
5414
5432
|
lastErrorCode: string | null;
|
|
5433
|
+
reactionContextCheckpoint: unknown | null;
|
|
5415
5434
|
processedAt: Date | null;
|
|
5416
5435
|
createdAt: Date;
|
|
5417
5436
|
updatedAt: Date;
|
|
@@ -5557,6 +5576,7 @@ export async function enqueueSlackInteractionInbox(
|
|
|
5557
5576
|
| "attemptCount"
|
|
5558
5577
|
| "retryAt"
|
|
5559
5578
|
| "lastErrorCode"
|
|
5579
|
+
| "reactionContextCheckpoint"
|
|
5560
5580
|
| "processedAt"
|
|
5561
5581
|
| "createdAt"
|
|
5562
5582
|
| "updatedAt"
|
|
@@ -5619,6 +5639,7 @@ export async function settleSlackInteractionInbox(
|
|
|
5619
5639
|
claimHolderId: null,
|
|
5620
5640
|
claimExpiresAt: null,
|
|
5621
5641
|
retryAt: null,
|
|
5642
|
+
reactionContextCheckpoint: null,
|
|
5622
5643
|
processedAt: sql`now()`,
|
|
5623
5644
|
lastErrorCode: input.errorCode ?? null,
|
|
5624
5645
|
updatedAt: sql`now()`,
|
|
@@ -5635,6 +5656,42 @@ export async function settleSlackInteractionInbox(
|
|
|
5635
5656
|
});
|
|
5636
5657
|
}
|
|
5637
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
|
+
|
|
5638
5695
|
export async function releaseSlackInteractionInbox(
|
|
5639
5696
|
db: Database,
|
|
5640
5697
|
input: {
|
|
@@ -5731,6 +5788,57 @@ export async function getSlackInteractionByRoute(
|
|
|
5731
5788
|
});
|
|
5732
5789
|
}
|
|
5733
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
|
+
|
|
5734
5842
|
export async function getSlackInteractionSessionAccess(
|
|
5735
5843
|
db: Database,
|
|
5736
5844
|
workspaceId: string,
|
|
@@ -6178,6 +6286,8 @@ function mapSlackInteractionInbox(
|
|
|
6178
6286
|
attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
|
|
6179
6287
|
retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
|
|
6180
6288
|
lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
|
|
6289
|
+
reactionContextCheckpoint:
|
|
6290
|
+
slackRowValue(row, "reactionContextCheckpoint", "reaction_context_checkpoint") ?? null,
|
|
6181
6291
|
processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
|
|
6182
6292
|
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6183
6293
|
updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
|
|
@@ -16693,6 +16803,7 @@ export type SessionCreateInput = {
|
|
|
16693
16803
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
16694
16804
|
firstPartyMcpTools?: FirstPartyMcpToolName[];
|
|
16695
16805
|
instructions?: string | null;
|
|
16806
|
+
policyRole?: string | null;
|
|
16696
16807
|
parentSessionId?: string | null;
|
|
16697
16808
|
createIdempotencyKey?: string | null;
|
|
16698
16809
|
sandboxGroupId?: string | null;
|
|
@@ -16849,6 +16960,9 @@ async function resolveSessionDepthDecision(
|
|
|
16849
16960
|
if (!parent) {
|
|
16850
16961
|
throw new Error(`Parent session not found: ${parentSessionId}`);
|
|
16851
16962
|
}
|
|
16963
|
+
if (parent.status === "cancelled") {
|
|
16964
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot create children");
|
|
16965
|
+
}
|
|
16852
16966
|
}
|
|
16853
16967
|
|
|
16854
16968
|
const currentParentDepth = parent?.nestedAgentDepth ?? 0;
|
|
@@ -17104,6 +17218,7 @@ async function createSessionInTransaction(
|
|
|
17104
17218
|
firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
|
|
17105
17219
|
initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
17106
17220
|
instructions: input.instructions ?? null,
|
|
17221
|
+
policyRole: input.policyRole ?? null,
|
|
17107
17222
|
parentSessionId: input.parentSessionId ?? null,
|
|
17108
17223
|
parentTurnId,
|
|
17109
17224
|
createIdempotencyKey,
|
|
@@ -21298,6 +21413,29 @@ export async function getActiveSessionHistoryItems(
|
|
|
21298
21413
|
});
|
|
21299
21414
|
}
|
|
21300
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
|
+
|
|
21301
21439
|
/**
|
|
21302
21440
|
* Count of ACTIVE (live, model-facing) history rows for a session. This is the
|
|
21303
21441
|
* length of the history the next turn is seeded from — the dual-write slice
|
|
@@ -36074,6 +36212,8 @@ export type InitializeSessionStartInput = {
|
|
|
36074
36212
|
subjectId: string;
|
|
36075
36213
|
expectedRevision: number;
|
|
36076
36214
|
} | null;
|
|
36215
|
+
/** Persist session.created only; realtime will supply the first human turn. */
|
|
36216
|
+
deferInitialTurn?: boolean;
|
|
36077
36217
|
};
|
|
36078
36218
|
|
|
36079
36219
|
export type InitializeSessionStartResult = {
|
|
@@ -36159,6 +36299,95 @@ export async function initializeSessionStartAtomically(
|
|
|
36159
36299
|
if (!goal) throw new Error("Failed to create initial session goal");
|
|
36160
36300
|
}
|
|
36161
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
|
+
|
|
36162
36391
|
const existingUserEvents = await tx
|
|
36163
36392
|
.select()
|
|
36164
36393
|
.from(schema.sessionEvents)
|
|
@@ -36262,6 +36491,7 @@ export async function initializeSessionStartAtomically(
|
|
|
36262
36491
|
let queueTailPosition = Number(session.queueTailPosition);
|
|
36263
36492
|
if (!turn) {
|
|
36264
36493
|
queueTailPosition += 1;
|
|
36494
|
+
const acceptedAt = new Date();
|
|
36265
36495
|
[turn] = await tx
|
|
36266
36496
|
.insert(schema.sessionTurns)
|
|
36267
36497
|
.values({
|
|
@@ -36293,10 +36523,14 @@ export async function initializeSessionStartAtomically(
|
|
|
36293
36523
|
: {},
|
|
36294
36524
|
lineage: {},
|
|
36295
36525
|
...initiatorColumns(creator),
|
|
36526
|
+
initiatingHumanSubjectId:
|
|
36527
|
+
creator.initiator.kind === "subject" ? creator.initiator.subjectId : null,
|
|
36296
36528
|
personalConnectionDelegations: parsedPersonalConnectionDelegations(
|
|
36297
36529
|
session.initialPersonalConnectionDelegations,
|
|
36298
36530
|
`sessions:${session.workspaceId}:${session.id}:initial`,
|
|
36299
36531
|
),
|
|
36532
|
+
createdAt: acceptedAt,
|
|
36533
|
+
updatedAt: acceptedAt,
|
|
36300
36534
|
})
|
|
36301
36535
|
.returning();
|
|
36302
36536
|
if (!turn) throw new Error("Failed to create initial session turn");
|
|
@@ -36430,6 +36664,7 @@ export async function enqueueSessionTurn(
|
|
|
36430
36664
|
const position = atHead
|
|
36431
36665
|
? Number(lockedSession.queueHeadPosition) - 1
|
|
36432
36666
|
: Number(lockedSession.queueTailPosition) + 1;
|
|
36667
|
+
const acceptedAt = new Date();
|
|
36433
36668
|
const [row] = await tx
|
|
36434
36669
|
.insert(schema.sessionTurns)
|
|
36435
36670
|
.values({
|
|
@@ -36457,7 +36692,11 @@ export async function enqueueSessionTurn(
|
|
|
36457
36692
|
initiator: input.initiator,
|
|
36458
36693
|
context: input.initiatorContext ?? {},
|
|
36459
36694
|
}),
|
|
36695
|
+
initiatingHumanSubjectId:
|
|
36696
|
+
input.initiator.kind === "subject" ? input.initiator.subjectId : null,
|
|
36460
36697
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
36698
|
+
createdAt: acceptedAt,
|
|
36699
|
+
updatedAt: acceptedAt,
|
|
36461
36700
|
})
|
|
36462
36701
|
.returning();
|
|
36463
36702
|
if (!row) {
|
|
@@ -36893,8 +37132,14 @@ export async function claimSessionWorkForAttempt(
|
|
|
36893
37132
|
sessionId,
|
|
36894
37133
|
{ workspaceControl },
|
|
36895
37134
|
);
|
|
36896
|
-
|
|
37135
|
+
let session = prefix.sessions[0];
|
|
36897
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
|
+
);
|
|
36898
37143
|
if (effectiveControl.state !== "active") {
|
|
36899
37144
|
return { action: "unclaimed", reason: "gate-closed" };
|
|
36900
37145
|
}
|
|
@@ -36974,7 +37219,37 @@ export async function claimSessionWorkForAttempt(
|
|
|
36974
37219
|
if (connectorPolicyRows.length > 2048) {
|
|
36975
37220
|
throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
|
|
36976
37221
|
}
|
|
36977
|
-
|
|
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, {
|
|
36978
37253
|
id: input.attemptId,
|
|
36979
37254
|
accountId: session.accountId,
|
|
36980
37255
|
workspaceId,
|
|
@@ -36988,6 +37263,42 @@ export async function claimSessionWorkForAttempt(
|
|
|
36988
37263
|
mcpApprovalPolicies,
|
|
36989
37264
|
connectorActionPolicies: connectorPolicyRows,
|
|
36990
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;
|
|
36991
37302
|
};
|
|
36992
37303
|
if (session.activeTurnId !== null) {
|
|
36993
37304
|
const [activeTurnPreview] = await tx
|
|
@@ -37335,6 +37646,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37335
37646
|
latencyMode: schema.sessionTurns.latencyMode,
|
|
37336
37647
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37337
37648
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
37649
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
37650
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
37651
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37338
37652
|
})
|
|
37339
37653
|
.from(schema.sessionTurns)
|
|
37340
37654
|
.where(
|
|
@@ -37384,8 +37698,15 @@ export async function claimSessionWorkForAttempt(
|
|
|
37384
37698
|
},
|
|
37385
37699
|
),
|
|
37386
37700
|
...initiatorColumns(compactionInitiator),
|
|
37701
|
+
initiatingHumanSubjectId:
|
|
37702
|
+
latestStarted?.initiatingHumanSubjectId ??
|
|
37703
|
+
(latestStarted?.initiatorKind === "subject"
|
|
37704
|
+
? latestStarted.initiatorSubjectId
|
|
37705
|
+
: null),
|
|
37387
37706
|
personalConnectionDelegations: [],
|
|
37388
37707
|
startedAt: now,
|
|
37708
|
+
createdAt: now,
|
|
37709
|
+
updatedAt: now,
|
|
37389
37710
|
})
|
|
37390
37711
|
.returning();
|
|
37391
37712
|
if (!compactionTurn) throw new Error("Failed to create context compaction execution");
|
|
@@ -37634,6 +37955,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37634
37955
|
tools: schema.sessionTurns.tools,
|
|
37635
37956
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37636
37957
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
37958
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
37959
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
37960
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37637
37961
|
})
|
|
37638
37962
|
.from(schema.sessionTurns)
|
|
37639
37963
|
.where(
|
|
@@ -37668,6 +37992,39 @@ export async function claimSessionWorkForAttempt(
|
|
|
37668
37992
|
typeof goalPolicy?.sandboxBackend === "string"
|
|
37669
37993
|
? goalPolicy.sandboxBackend
|
|
37670
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
|
+
}
|
|
37671
38028
|
await tx.execute(sql`set local opengeni.session_inference_claim = '1'`);
|
|
37672
38029
|
const [internalTurn] = await tx
|
|
37673
38030
|
.insert(schema.sessionTurns)
|
|
@@ -37699,8 +38056,11 @@ export async function claimSessionWorkForAttempt(
|
|
|
37699
38056
|
{ id: input.dispatchId, generation: 1, triggerEventId },
|
|
37700
38057
|
),
|
|
37701
38058
|
...initiatorColumns(internalInitiator),
|
|
38059
|
+
initiatingHumanSubjectId,
|
|
37702
38060
|
personalConnectionDelegations: internalPersonalConnectionDelegations,
|
|
37703
38061
|
startedAt: now,
|
|
38062
|
+
createdAt: now,
|
|
38063
|
+
updatedAt: now,
|
|
37704
38064
|
})
|
|
37705
38065
|
.returning();
|
|
37706
38066
|
if (!internalTurn) throw new Error("Failed to create internal update inference");
|
|
@@ -37820,13 +38180,28 @@ export async function claimSessionWorkForAttempt(
|
|
|
37820
38180
|
}),
|
|
37821
38181
|
producerCodexCredentialId: null,
|
|
37822
38182
|
});
|
|
37823
|
-
const
|
|
37824
|
-
|
|
37825
|
-
|
|
37826
|
-
|
|
37827
|
-
|
|
37828
|
-
|
|
37829
|
-
|
|
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
|
+
);
|
|
37830
38205
|
await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
|
|
37831
38206
|
if (delivered.events.length > 0) {
|
|
37832
38207
|
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
@@ -38400,15 +38775,23 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38400
38775
|
throw new Error(`Live interrupted attempt ${attemptId} lost its exact turn ownership`);
|
|
38401
38776
|
}
|
|
38402
38777
|
|
|
38403
|
-
const
|
|
38404
|
-
const
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
38411
|
-
|
|
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";
|
|
38412
38795
|
let sequence = session.lastSequence;
|
|
38413
38796
|
const closedTools = await closePendingSessionToolCallsInTransaction(
|
|
38414
38797
|
tx as unknown as Database,
|
|
@@ -38433,38 +38816,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38433
38816
|
outcome,
|
|
38434
38817
|
closedAt: now,
|
|
38435
38818
|
});
|
|
38436
|
-
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> =
|
|
38819
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = terminalCancel
|
|
38437
38820
|
? [
|
|
38438
38821
|
{
|
|
38439
38822
|
accountId: session.accountId,
|
|
38440
38823
|
workspaceId,
|
|
38441
38824
|
sessionId,
|
|
38442
38825
|
sequence: ++sequence,
|
|
38443
|
-
type: "turn.
|
|
38444
|
-
turnId: turn.id,
|
|
38445
|
-
turnGeneration: turn.executionGeneration,
|
|
38446
|
-
turnAttemptId: attemptId,
|
|
38447
|
-
turnAssociation: "current",
|
|
38448
|
-
payload: sanitizeEventPayload({ reason: "steer" }),
|
|
38449
|
-
occurredAt: now,
|
|
38450
|
-
},
|
|
38451
|
-
{
|
|
38452
|
-
accountId: session.accountId,
|
|
38453
|
-
workspaceId,
|
|
38454
|
-
sessionId,
|
|
38455
|
-
sequence: ++sequence,
|
|
38456
|
-
type: "session.status.changed",
|
|
38457
|
-
payload: sanitizeEventPayload({ status: "queued" }),
|
|
38458
|
-
occurredAt: now,
|
|
38459
|
-
},
|
|
38460
|
-
]
|
|
38461
|
-
: [
|
|
38462
|
-
{
|
|
38463
|
-
accountId: session.accountId,
|
|
38464
|
-
workspaceId,
|
|
38465
|
-
sessionId,
|
|
38466
|
-
sequence: ++sequence,
|
|
38467
|
-
type: "turn.recovery.requested",
|
|
38826
|
+
type: "turn.cancelled",
|
|
38468
38827
|
turnId: turn.id,
|
|
38469
38828
|
turnGeneration: turn.executionGeneration,
|
|
38470
38829
|
turnAttemptId: attemptId,
|
|
@@ -38472,50 +38831,101 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38472
38831
|
payload: sanitizeEventPayload({ reason }),
|
|
38473
38832
|
occurredAt: now,
|
|
38474
38833
|
},
|
|
38475
|
-
|
|
38476
|
-
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38483
|
-
|
|
38484
|
-
|
|
38485
|
-
|
|
38486
|
-
|
|
38487
|
-
|
|
38488
|
-
|
|
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
|
+
];
|
|
38489
38888
|
const eventRows = await tx.insert(schema.sessionEvents).values(eventValues).returning();
|
|
38490
38889
|
await tx
|
|
38491
38890
|
.update(schema.sessionTurns)
|
|
38492
38891
|
.set(
|
|
38493
|
-
|
|
38892
|
+
terminalCancel
|
|
38494
38893
|
? {
|
|
38495
|
-
status: "
|
|
38894
|
+
status: "cancelled",
|
|
38496
38895
|
activeAttemptId: null,
|
|
38497
38896
|
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
38897
|
+
cancelledBy: "system:session_cancelled",
|
|
38898
|
+
cancelReason: reason,
|
|
38498
38899
|
version: turn.version + 1,
|
|
38499
38900
|
finishedAt: turn.finishedAt ?? now,
|
|
38500
38901
|
updatedAt: now,
|
|
38501
38902
|
}
|
|
38502
|
-
:
|
|
38503
|
-
|
|
38504
|
-
|
|
38505
|
-
|
|
38506
|
-
|
|
38507
|
-
|
|
38508
|
-
|
|
38509
|
-
|
|
38510
|
-
|
|
38511
|
-
|
|
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
|
+
},
|
|
38512
38922
|
)
|
|
38513
38923
|
.where(eq(schema.sessionTurns.id, turn.id));
|
|
38514
38924
|
await tx
|
|
38515
38925
|
.update(schema.sessions)
|
|
38516
38926
|
.set({
|
|
38517
|
-
status: steer ? "queued" : "recovering",
|
|
38518
|
-
activeTurnId: steer ? null : turn.id,
|
|
38927
|
+
status: terminalCancel ? "cancelled" : steer ? "queued" : "recovering",
|
|
38928
|
+
activeTurnId: terminalCancel || steer ? null : turn.id,
|
|
38519
38929
|
lastSequence: sequence,
|
|
38520
38930
|
updatedAt: now,
|
|
38521
38931
|
})
|
|
@@ -39348,6 +39758,13 @@ export type ApplySessionTurnSettlementResult =
|
|
|
39348
39758
|
activeTurnId: string | null;
|
|
39349
39759
|
};
|
|
39350
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
|
+
|
|
39351
39768
|
function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAttemptOutcome | null {
|
|
39352
39769
|
switch (status) {
|
|
39353
39770
|
case "completed":
|
|
@@ -39361,6 +39778,40 @@ function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAtte
|
|
|
39361
39778
|
}
|
|
39362
39779
|
}
|
|
39363
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
|
+
|
|
39364
39815
|
/**
|
|
39365
39816
|
* Atomically append terminal/requires-action truth, update the exact turn, and
|
|
39366
39817
|
* transition the owning session. A superseded dispatch or closed control gate
|
|
@@ -39371,6 +39822,7 @@ export async function applySessionTurnSettlement(
|
|
|
39371
39822
|
db: Database,
|
|
39372
39823
|
workspaceId: string,
|
|
39373
39824
|
input: ApplySessionTurnSettlementInput,
|
|
39825
|
+
hooks: ApplySessionTurnSettlementHooks = {},
|
|
39374
39826
|
): Promise<ApplySessionTurnSettlementResult> {
|
|
39375
39827
|
const fromStatuses = input.fromStatuses ?? ["running", "requires_action"];
|
|
39376
39828
|
const eventTypes = [
|
|
@@ -39799,11 +40251,37 @@ export async function applySessionTurnSettlement(
|
|
|
39799
40251
|
});
|
|
39800
40252
|
const inserted =
|
|
39801
40253
|
values.length > 0 ? await tx.insert(schema.sessionEvents).values(values).returning() : [];
|
|
39802
|
-
const terminal =
|
|
39803
|
-
|
|
39804
|
-
input.turnStatus
|
|
39805
|
-
|
|
39806
|
-
|
|
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
|
+
}
|
|
39807
40285
|
if (input.turnStatus === "running") {
|
|
39808
40286
|
// Approval resume re-enters the same logical turn after a new fenced
|
|
39809
40287
|
// dispatch has advanced its trigger. It is an authorized inference
|
|
@@ -40092,6 +40570,20 @@ export async function settleCodexCredentialLeaseLoss(
|
|
|
40092
40570
|
if (!settlementEvent) {
|
|
40093
40571
|
throw new Error("Codex lease-loss settlement did not persist its checkpoint event");
|
|
40094
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
|
+
}
|
|
40095
40587
|
|
|
40096
40588
|
await tx
|
|
40097
40589
|
.update(schema.sessionTurns)
|
|
@@ -40821,6 +41313,22 @@ export async function recoverSessionDispatch(
|
|
|
40821
41313
|
},
|
|
40822
41314
|
])
|
|
40823
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
|
+
});
|
|
40824
41332
|
await tx
|
|
40825
41333
|
.update(schema.sessionTurns)
|
|
40826
41334
|
.set({
|
|
@@ -41559,8 +42067,14 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
|
|
|
41559
42067
|
.for("update")
|
|
41560
42068
|
.limit(1);
|
|
41561
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
|
+
);
|
|
41562
42075
|
const runnable =
|
|
41563
42076
|
session.status !== "cancelled" &&
|
|
42077
|
+
!realtimeActive &&
|
|
41564
42078
|
session.activeTurnId === null &&
|
|
41565
42079
|
effectiveControl.state === "active";
|
|
41566
42080
|
return runnable
|
|
@@ -42053,7 +42567,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
|
|
|
42053
42567
|
.returning();
|
|
42054
42568
|
if (!event) throw new Error("Failed to create system-update pending event");
|
|
42055
42569
|
await mutateSource(tx as unknown as Database, event.id);
|
|
42056
|
-
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";
|
|
42057
42577
|
const wake = shouldWake
|
|
42058
42578
|
? await registerInternalUpdateWakeInTransaction(tx as unknown as Database, {
|
|
42059
42579
|
accountId: session.accountId,
|
|
@@ -42617,6 +43137,21 @@ export async function appendSessionEventsForTurnAttempt(
|
|
|
42617
43137
|
};
|
|
42618
43138
|
});
|
|
42619
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
|
+
}
|
|
42620
43155
|
await tx
|
|
42621
43156
|
.update(schema.sessions)
|
|
42622
43157
|
.set({
|
|
@@ -43041,6 +43576,7 @@ function mapSession(
|
|
|
43041
43576
|
title: row.title ?? null,
|
|
43042
43577
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
43043
43578
|
instructions: row.instructions ?? null,
|
|
43579
|
+
policyRole: row.policyRole ?? null,
|
|
43044
43580
|
resources: row.resources as ResourceRef[],
|
|
43045
43581
|
skills: (row.skills as SessionSkill[]) ?? [],
|
|
43046
43582
|
tools: row.tools as ToolRef[],
|