@opengeni/db 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical-human-identities.js +2 -2
- package/dist/{chunk-F32YT7MP.js → chunk-FPVAD4CO.js} +2 -2
- package/dist/{chunk-TAUTWOS5.js → chunk-GYO7ED2Q.js} +2 -2
- package/dist/{chunk-P3RE4D2B.js → chunk-HSOWSYOX.js} +2 -2
- package/dist/{chunk-CQZ4QSP5.js → chunk-VTPRIXKG.js} +2 -2
- package/dist/{chunk-JC5QPLUM.js → chunk-Z7VYMNPZ.js} +47 -2
- package/dist/chunk-Z7VYMNPZ.js.map +1 -0
- package/dist/editable-artifact-durable-export.js +1 -1
- package/dist/editable-artifacts.js +2 -2
- package/dist/index.d.ts +29 -0
- package/dist/index.js +365 -21
- package/dist/index.js.map +1 -1
- package/dist/persistence-errors.d.ts +8 -0
- package/dist/session-tenancy.js +2 -2
- package/dist/video-generation.js +2 -2
- package/drizzle/0238_recover_unclaimed_session_turns.sql +307 -0
- package/package.json +4 -4
- package/src/index.ts +468 -17
- package/src/persistence-errors.ts +60 -1
- package/dist/chunk-JC5QPLUM.js.map +0 -1
- /package/dist/{chunk-F32YT7MP.js.map → chunk-FPVAD4CO.js.map} +0 -0
- /package/dist/{chunk-TAUTWOS5.js.map → chunk-GYO7ED2Q.js.map} +0 -0
- /package/dist/{chunk-P3RE4D2B.js.map → chunk-HSOWSYOX.js.map} +0 -0
- /package/dist/{chunk-CQZ4QSP5.js.map → chunk-VTPRIXKG.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -45617,6 +45617,7 @@ export async function materializeGoalContinuation(
|
|
|
45617
45617
|
if (
|
|
45618
45618
|
!goalRead ||
|
|
45619
45619
|
goalRead.status !== "active" ||
|
|
45620
|
+
session.status === "failed" ||
|
|
45620
45621
|
session.status === "cancelled" ||
|
|
45621
45622
|
effectiveControl.state !== "active"
|
|
45622
45623
|
) {
|
|
@@ -46974,9 +46975,14 @@ export async function claimSessionWorkForAttempt(
|
|
|
46974
46975
|
input: ClaimSessionWorkForAttemptInput,
|
|
46975
46976
|
): Promise<ClaimSessionWorkForAttemptResult> {
|
|
46976
46977
|
const { sessionId, workflowId } = input;
|
|
46977
|
-
return await
|
|
46978
|
+
return await retrySessionActivityRls(
|
|
46978
46979
|
db,
|
|
46979
46980
|
workspaceId,
|
|
46981
|
+
{
|
|
46982
|
+
stage: "session_attempts.claim",
|
|
46983
|
+
eventTypes: ["session.turn.attempt_claimed"],
|
|
46984
|
+
maxAttempts: 3,
|
|
46985
|
+
},
|
|
46980
46986
|
async (scopedDb) =>
|
|
46981
46987
|
await withSessionActivitySavepoint(scopedDb, async (tx) => {
|
|
46982
46988
|
const deliverPendingUpdates = async (
|
|
@@ -50003,6 +50009,405 @@ export async function peekSessionWork(
|
|
|
50003
50009
|
});
|
|
50004
50010
|
}
|
|
50005
50011
|
|
|
50012
|
+
export type FailSessionWorkBeforeAttemptClaimInput = {
|
|
50013
|
+
accountId: string;
|
|
50014
|
+
sessionId: string;
|
|
50015
|
+
workflowId: string;
|
|
50016
|
+
trigger: SessionWorkTrigger;
|
|
50017
|
+
error: string;
|
|
50018
|
+
};
|
|
50019
|
+
|
|
50020
|
+
export type FailSessionWorkBeforeAttemptClaimResult =
|
|
50021
|
+
| { action: "failed"; turnId: string | null; events: SessionEvent[] }
|
|
50022
|
+
| { action: "terminal"; turnId: null; events: [] }
|
|
50023
|
+
| { action: "stale"; turnId: null; events: [] };
|
|
50024
|
+
|
|
50025
|
+
/**
|
|
50026
|
+
* Terminally settle a permanent admission failure that happened before an
|
|
50027
|
+
* attempt row existed. The workflow supplies the exact trigger recorded in
|
|
50028
|
+
* Temporal history; this transaction re-evaluates it under the canonical
|
|
50029
|
+
* control -> workspace -> session -> turn lock order and refuses stale,
|
|
50030
|
+
* paused, capacity-blocked, or concurrently claimed work. No attempt row is
|
|
50031
|
+
* fabricated because provider/tool execution never started.
|
|
50032
|
+
*/
|
|
50033
|
+
export async function failSessionWorkBeforeAttemptClaim(
|
|
50034
|
+
db: Database,
|
|
50035
|
+
workspaceId: string,
|
|
50036
|
+
input: FailSessionWorkBeforeAttemptClaimInput,
|
|
50037
|
+
): Promise<FailSessionWorkBeforeAttemptClaimResult> {
|
|
50038
|
+
return await retrySessionActivityRls(
|
|
50039
|
+
db,
|
|
50040
|
+
workspaceId,
|
|
50041
|
+
{
|
|
50042
|
+
stage: "session_lifecycle_outbox.fail_before_attempt_claim",
|
|
50043
|
+
eventTypes: ["child_terminal_result", "session.status.changed", "turn.failed"],
|
|
50044
|
+
maxAttempts: 3,
|
|
50045
|
+
},
|
|
50046
|
+
async (scopedDb) =>
|
|
50047
|
+
await scopedDb.transaction(async (tx) => {
|
|
50048
|
+
const baseLocks = await lockChildLifecycleOutboxWriteRowsTx(
|
|
50049
|
+
tx as unknown as Database,
|
|
50050
|
+
workspaceId,
|
|
50051
|
+
{ sessionId: input.sessionId },
|
|
50052
|
+
);
|
|
50053
|
+
const session = baseLocks.session;
|
|
50054
|
+
if (session.accountId !== input.accountId) {
|
|
50055
|
+
throw new SessionControlInvariantError(
|
|
50056
|
+
`Session ${input.sessionId} does not belong to account ${input.accountId}`,
|
|
50057
|
+
);
|
|
50058
|
+
}
|
|
50059
|
+
if (session.status === "failed" || session.status === "cancelled") {
|
|
50060
|
+
return { action: "terminal", turnId: null, events: [] } as const;
|
|
50061
|
+
}
|
|
50062
|
+
if (
|
|
50063
|
+
session.temporalWorkflowId !== null &&
|
|
50064
|
+
session.temporalWorkflowId !== input.workflowId
|
|
50065
|
+
) {
|
|
50066
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50067
|
+
}
|
|
50068
|
+
const effectiveControl = await evaluateSessionControl(
|
|
50069
|
+
tx as unknown as Database,
|
|
50070
|
+
workspaceId,
|
|
50071
|
+
input.sessionId,
|
|
50072
|
+
{ workspaceControl: baseLocks.control ?? undefined },
|
|
50073
|
+
);
|
|
50074
|
+
if (!baseLocks.workspace || effectiveControl.state !== "active") {
|
|
50075
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50076
|
+
}
|
|
50077
|
+
|
|
50078
|
+
// The session lock serializes this check with claim. If another worker
|
|
50079
|
+
// committed ownership while the activity response was ambiguous, its
|
|
50080
|
+
// attempt is authoritative and this terminal path must no-op.
|
|
50081
|
+
const [liveAttempt] = await tx
|
|
50082
|
+
.select({ id: schema.sessionTurnAttempts.id })
|
|
50083
|
+
.from(schema.sessionTurnAttempts)
|
|
50084
|
+
.where(
|
|
50085
|
+
and(
|
|
50086
|
+
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
50087
|
+
eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
|
|
50088
|
+
inArray(schema.sessionTurnAttempts.state, ["claimed", "running"]),
|
|
50089
|
+
),
|
|
50090
|
+
)
|
|
50091
|
+
.limit(1);
|
|
50092
|
+
if (liveAttempt) {
|
|
50093
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50094
|
+
}
|
|
50095
|
+
|
|
50096
|
+
let turn: typeof schema.sessionTurns.$inferSelect | null = null;
|
|
50097
|
+
if (session.activeTurnId) {
|
|
50098
|
+
const exactLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
50099
|
+
workspaceId,
|
|
50100
|
+
controlLock: "already_locked",
|
|
50101
|
+
workspaceLock: "already_locked",
|
|
50102
|
+
turnIds: [session.activeTurnId],
|
|
50103
|
+
});
|
|
50104
|
+
turn = exactLocks.turns[0] ?? null;
|
|
50105
|
+
if (!turn || turn.accountId !== session.accountId || turn.sessionId !== input.sessionId) {
|
|
50106
|
+
// The claim failed on this same locked corrupt pointer. There is no
|
|
50107
|
+
// safe turn identity to settle, but retrying can never repair it;
|
|
50108
|
+
// fail the owning session and its session-level obligations below.
|
|
50109
|
+
turn = null;
|
|
50110
|
+
}
|
|
50111
|
+
if (turn && isTerminalSessionTurnStatus(turn.status as SessionTurnStatus)) {
|
|
50112
|
+
// Repair the corrupt session pointer without writing a second
|
|
50113
|
+
// terminal event onto an already-terminal logical turn.
|
|
50114
|
+
turn = null;
|
|
50115
|
+
} else if (turn && input.trigger.kind === "approval") {
|
|
50116
|
+
if (
|
|
50117
|
+
turn.status !== "requires_action" ||
|
|
50118
|
+
!(await isNewerApprovalTrigger(
|
|
50119
|
+
tx as unknown as Database,
|
|
50120
|
+
workspaceId,
|
|
50121
|
+
input.sessionId,
|
|
50122
|
+
turn.triggerEventId,
|
|
50123
|
+
input.trigger.triggerEventId,
|
|
50124
|
+
))
|
|
50125
|
+
) {
|
|
50126
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50127
|
+
}
|
|
50128
|
+
} else if (turn) {
|
|
50129
|
+
if (turn.status === "requires_action") {
|
|
50130
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50131
|
+
}
|
|
50132
|
+
if (turn.status === "waiting_capacity") {
|
|
50133
|
+
const [codexWaiter] = await tx
|
|
50134
|
+
.select({ id: schema.codexCapacityWaiters.id })
|
|
50135
|
+
.from(schema.codexCapacityWaiters)
|
|
50136
|
+
.where(
|
|
50137
|
+
and(
|
|
50138
|
+
eq(schema.codexCapacityWaiters.workspaceId, workspaceId),
|
|
50139
|
+
eq(schema.codexCapacityWaiters.sessionId, input.sessionId),
|
|
50140
|
+
eq(schema.codexCapacityWaiters.status, "waiting"),
|
|
50141
|
+
),
|
|
50142
|
+
)
|
|
50143
|
+
.limit(1);
|
|
50144
|
+
const xaiWaiter = await getXaiCapacityWaitForSessionInTransaction(
|
|
50145
|
+
tx as unknown as Database,
|
|
50146
|
+
workspaceId,
|
|
50147
|
+
input.sessionId,
|
|
50148
|
+
);
|
|
50149
|
+
if (codexWaiter || xaiWaiter) {
|
|
50150
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50151
|
+
}
|
|
50152
|
+
}
|
|
50153
|
+
}
|
|
50154
|
+
} else {
|
|
50155
|
+
if (input.trigger.kind === "approval") {
|
|
50156
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50157
|
+
}
|
|
50158
|
+
const [pendingAgentSteer] = await tx
|
|
50159
|
+
.select({ id: schema.sessionSystemUpdates.id })
|
|
50160
|
+
.from(schema.sessionSystemUpdates)
|
|
50161
|
+
.where(
|
|
50162
|
+
and(
|
|
50163
|
+
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
50164
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
50165
|
+
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
50166
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
50167
|
+
),
|
|
50168
|
+
)
|
|
50169
|
+
.orderBy(
|
|
50170
|
+
desc(schema.sessionSystemUpdates.createdAt),
|
|
50171
|
+
desc(schema.sessionSystemUpdates.id),
|
|
50172
|
+
)
|
|
50173
|
+
.limit(1);
|
|
50174
|
+
const queuedRows = await rawRows<{ id: string }>(
|
|
50175
|
+
tx as unknown as Database,
|
|
50176
|
+
sql`select id from session_turns
|
|
50177
|
+
where workspace_id = ${workspaceId} and session_id = ${input.sessionId}
|
|
50178
|
+
and status = 'queued' and source in ('user', 'api')
|
|
50179
|
+
and (
|
|
50180
|
+
${Boolean(pendingAgentSteer)} = false
|
|
50181
|
+
or metadata->>'delivery' = 'steer'
|
|
50182
|
+
)
|
|
50183
|
+
order by position asc, created_at asc, id asc
|
|
50184
|
+
limit 1`,
|
|
50185
|
+
);
|
|
50186
|
+
const queuedPreview = queuedRows[0];
|
|
50187
|
+
if (queuedPreview) {
|
|
50188
|
+
const exactLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
50189
|
+
workspaceId,
|
|
50190
|
+
controlLock: "already_locked",
|
|
50191
|
+
workspaceLock: "already_locked",
|
|
50192
|
+
turnIds: [queuedPreview.id],
|
|
50193
|
+
});
|
|
50194
|
+
turn = exactLocks.turns[0] ?? null;
|
|
50195
|
+
if (
|
|
50196
|
+
!turn ||
|
|
50197
|
+
turn.accountId !== session.accountId ||
|
|
50198
|
+
turn.sessionId !== input.sessionId ||
|
|
50199
|
+
turn.status !== "queued" ||
|
|
50200
|
+
turn.activeAttemptId !== null
|
|
50201
|
+
) {
|
|
50202
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50203
|
+
}
|
|
50204
|
+
} else if (!session.compactRequested) {
|
|
50205
|
+
if (
|
|
50206
|
+
!pendingAgentSteer &&
|
|
50207
|
+
(await latestFinishedTurnHasFailureCodeTx(
|
|
50208
|
+
tx as unknown as Database,
|
|
50209
|
+
workspaceId,
|
|
50210
|
+
input.sessionId,
|
|
50211
|
+
"context_compaction_failed",
|
|
50212
|
+
))
|
|
50213
|
+
) {
|
|
50214
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50215
|
+
}
|
|
50216
|
+
const [pendingUpdate] = await tx
|
|
50217
|
+
.select({ id: schema.sessionSystemUpdates.id })
|
|
50218
|
+
.from(schema.sessionSystemUpdates)
|
|
50219
|
+
.where(
|
|
50220
|
+
and(
|
|
50221
|
+
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
50222
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
50223
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
50224
|
+
),
|
|
50225
|
+
)
|
|
50226
|
+
.limit(1)
|
|
50227
|
+
.for("update");
|
|
50228
|
+
if (!pendingUpdate) {
|
|
50229
|
+
return { action: "stale", turnId: null, events: [] } as const;
|
|
50230
|
+
}
|
|
50231
|
+
}
|
|
50232
|
+
}
|
|
50233
|
+
|
|
50234
|
+
const now = new Date();
|
|
50235
|
+
let sequence = session.lastSequence;
|
|
50236
|
+
let closedToolEvents: SessionEvent[] = [];
|
|
50237
|
+
if (turn) {
|
|
50238
|
+
await cancelTurnInteractionInterventionsInTransaction(tx as unknown as Database, {
|
|
50239
|
+
accountId: session.accountId,
|
|
50240
|
+
workspaceId,
|
|
50241
|
+
sessionId: input.sessionId,
|
|
50242
|
+
turnId: turn.id,
|
|
50243
|
+
});
|
|
50244
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(
|
|
50245
|
+
tx as unknown as Database,
|
|
50246
|
+
{
|
|
50247
|
+
accountId: session.accountId,
|
|
50248
|
+
workspaceId,
|
|
50249
|
+
sessionId: input.sessionId,
|
|
50250
|
+
turnId: turn.id,
|
|
50251
|
+
reason: "turn_failed",
|
|
50252
|
+
sequence,
|
|
50253
|
+
now,
|
|
50254
|
+
},
|
|
50255
|
+
);
|
|
50256
|
+
sequence = closedTools.sequence;
|
|
50257
|
+
closedToolEvents = closedTools.events;
|
|
50258
|
+
await tx
|
|
50259
|
+
.update(schema.sessionHumanInputRequests)
|
|
50260
|
+
.set({
|
|
50261
|
+
status: "cancelled",
|
|
50262
|
+
response: { outcome: "cancelled" },
|
|
50263
|
+
respondedBy: "system:turn_failed",
|
|
50264
|
+
respondedAt: now,
|
|
50265
|
+
updatedAt: now,
|
|
50266
|
+
})
|
|
50267
|
+
.where(
|
|
50268
|
+
and(
|
|
50269
|
+
eq(schema.sessionHumanInputRequests.workspaceId, workspaceId),
|
|
50270
|
+
eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
|
|
50271
|
+
eq(schema.sessionHumanInputRequests.turnId, turn.id),
|
|
50272
|
+
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
50273
|
+
),
|
|
50274
|
+
);
|
|
50275
|
+
}
|
|
50276
|
+
await settleSessionMaintenanceInTransaction(tx as unknown as Database, {
|
|
50277
|
+
accountId: session.accountId,
|
|
50278
|
+
workspaceId,
|
|
50279
|
+
sessionId: input.sessionId,
|
|
50280
|
+
});
|
|
50281
|
+
if (!turn) {
|
|
50282
|
+
await tx
|
|
50283
|
+
.update(schema.sessionSystemUpdates)
|
|
50284
|
+
.set({ state: "failed" })
|
|
50285
|
+
.where(
|
|
50286
|
+
and(
|
|
50287
|
+
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
50288
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
50289
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
50290
|
+
),
|
|
50291
|
+
);
|
|
50292
|
+
}
|
|
50293
|
+
const values = [
|
|
50294
|
+
...(turn
|
|
50295
|
+
? [
|
|
50296
|
+
{
|
|
50297
|
+
accountId: session.accountId,
|
|
50298
|
+
workspaceId,
|
|
50299
|
+
sessionId: input.sessionId,
|
|
50300
|
+
sequence: ++sequence,
|
|
50301
|
+
type: "turn.failed" as const,
|
|
50302
|
+
payload: {
|
|
50303
|
+
triggerEventId: turn.triggerEventId,
|
|
50304
|
+
code: "pre_claim_failure",
|
|
50305
|
+
error: input.error,
|
|
50306
|
+
},
|
|
50307
|
+
turnId: turn.id,
|
|
50308
|
+
turnGeneration: turn.executionGeneration,
|
|
50309
|
+
turnAttemptId: null,
|
|
50310
|
+
turnAssociation: "current" as const,
|
|
50311
|
+
occurredAt: now,
|
|
50312
|
+
},
|
|
50313
|
+
]
|
|
50314
|
+
: []),
|
|
50315
|
+
{
|
|
50316
|
+
accountId: session.accountId,
|
|
50317
|
+
workspaceId,
|
|
50318
|
+
sessionId: input.sessionId,
|
|
50319
|
+
sequence: ++sequence,
|
|
50320
|
+
type: "session.status.changed" as const,
|
|
50321
|
+
payload: { status: "failed", code: "pre_claim_failure" },
|
|
50322
|
+
turnId: turn?.id ?? null,
|
|
50323
|
+
turnGeneration: turn?.executionGeneration ?? null,
|
|
50324
|
+
turnAttemptId: null,
|
|
50325
|
+
turnAssociation: turn ? ("current" as const) : null,
|
|
50326
|
+
occurredAt: now,
|
|
50327
|
+
},
|
|
50328
|
+
];
|
|
50329
|
+
const inserted = await tx
|
|
50330
|
+
.insert(schema.sessionEvents)
|
|
50331
|
+
.values(withLosslessContentWriteVersion(values, "payload", "payloadCodecVersion"))
|
|
50332
|
+
.returning();
|
|
50333
|
+
if (turn) {
|
|
50334
|
+
const failedEvent = inserted.find((event) => event.type === "turn.failed");
|
|
50335
|
+
if (!failedEvent) {
|
|
50336
|
+
throw new SessionControlInvariantError(
|
|
50337
|
+
`Pre-claim failure for turn ${turn.id} has no terminal event`,
|
|
50338
|
+
);
|
|
50339
|
+
}
|
|
50340
|
+
await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
|
|
50341
|
+
accountId: session.accountId,
|
|
50342
|
+
workspaceId,
|
|
50343
|
+
sessionId: input.sessionId,
|
|
50344
|
+
turnId: turn.id,
|
|
50345
|
+
turnStatus: "failed",
|
|
50346
|
+
terminalEvent: {
|
|
50347
|
+
id: failedEvent.id,
|
|
50348
|
+
type: "turn.failed",
|
|
50349
|
+
payload: sessionEventPayloadRecord(
|
|
50350
|
+
failedEvent.payload,
|
|
50351
|
+
failedEvent.payloadCodecVersion,
|
|
50352
|
+
),
|
|
50353
|
+
},
|
|
50354
|
+
now,
|
|
50355
|
+
});
|
|
50356
|
+
await tx
|
|
50357
|
+
.update(schema.sessionTurns)
|
|
50358
|
+
.set({
|
|
50359
|
+
status: "failed",
|
|
50360
|
+
activeAttemptId: null,
|
|
50361
|
+
version: turn.version + 1,
|
|
50362
|
+
finishedAt: now,
|
|
50363
|
+
updatedAt: now,
|
|
50364
|
+
})
|
|
50365
|
+
.where(
|
|
50366
|
+
and(
|
|
50367
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
50368
|
+
eq(schema.sessionTurns.sessionId, input.sessionId),
|
|
50369
|
+
eq(schema.sessionTurns.id, turn.id),
|
|
50370
|
+
),
|
|
50371
|
+
);
|
|
50372
|
+
await enqueueFailedChildOutboxForTurnTx(
|
|
50373
|
+
tx as unknown as Database,
|
|
50374
|
+
workspaceId,
|
|
50375
|
+
session,
|
|
50376
|
+
turn,
|
|
50377
|
+
);
|
|
50378
|
+
} else {
|
|
50379
|
+
await enqueueFailedChildOutboxWithoutTurnTx(
|
|
50380
|
+
tx as unknown as Database,
|
|
50381
|
+
workspaceId,
|
|
50382
|
+
session,
|
|
50383
|
+
sequence,
|
|
50384
|
+
);
|
|
50385
|
+
}
|
|
50386
|
+
await tx
|
|
50387
|
+
.update(schema.sessions)
|
|
50388
|
+
.set({
|
|
50389
|
+
status: "failed",
|
|
50390
|
+
activeTurnId: null,
|
|
50391
|
+
compactRequested: false,
|
|
50392
|
+
lastSequence: sequence,
|
|
50393
|
+
queueVersion: session.queueVersion + 1,
|
|
50394
|
+
updatedAt: now,
|
|
50395
|
+
})
|
|
50396
|
+
.where(
|
|
50397
|
+
and(
|
|
50398
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
50399
|
+
eq(schema.sessions.id, input.sessionId),
|
|
50400
|
+
),
|
|
50401
|
+
);
|
|
50402
|
+
return {
|
|
50403
|
+
action: "failed",
|
|
50404
|
+
turnId: turn?.id ?? null,
|
|
50405
|
+
events: [...closedToolEvents, ...inserted.map(mapEvent)],
|
|
50406
|
+
} as const;
|
|
50407
|
+
}),
|
|
50408
|
+
);
|
|
50409
|
+
}
|
|
50410
|
+
|
|
50006
50411
|
/**
|
|
50007
50412
|
* Child lifecycle transactions discover immutable parentage only after taking
|
|
50008
50413
|
* the control/workspace prefix. They must then lock child and parent together,
|
|
@@ -52804,14 +53209,16 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
|
|
|
52804
53209
|
return null;
|
|
52805
53210
|
}
|
|
52806
53211
|
|
|
52807
|
-
async function
|
|
53212
|
+
async function enqueueFailedChildOutboxTx(
|
|
52808
53213
|
tx: Database,
|
|
52809
53214
|
workspaceId: string,
|
|
52810
|
-
session: Pick<
|
|
52811
|
-
|
|
53215
|
+
session: Pick<
|
|
53216
|
+
typeof schema.sessions.$inferSelect,
|
|
53217
|
+
"id" | "accountId" | "parentSessionId" | "parentTurnId"
|
|
53218
|
+
>,
|
|
53219
|
+
input: { turnId: string | null; dedupeKey: string },
|
|
52812
53220
|
): Promise<void> {
|
|
52813
53221
|
if (!session.parentSessionId) return;
|
|
52814
|
-
const dedupeKey = `child-completion:${turn.sessionId}:turn:${turn.id}`;
|
|
52815
53222
|
const personalConnectionDelegations = session.parentTurnId
|
|
52816
53223
|
? await personalConnectionDelegationsForTurnInTransaction(
|
|
52817
53224
|
tx,
|
|
@@ -52859,26 +53266,26 @@ async function enqueueFailedChildOutboxForTurnTx(
|
|
|
52859
53266
|
withLosslessContentWriteVersion(
|
|
52860
53267
|
withLosslessContentWriteVersion(
|
|
52861
53268
|
{
|
|
52862
|
-
accountId:
|
|
53269
|
+
accountId: session.accountId,
|
|
52863
53270
|
workspaceId,
|
|
52864
|
-
sourceSessionId:
|
|
53271
|
+
sourceSessionId: session.id,
|
|
52865
53272
|
targetSessionId: session.parentSessionId,
|
|
52866
|
-
dedupeKey,
|
|
53273
|
+
dedupeKey: input.dedupeKey,
|
|
52867
53274
|
kind: "child_terminal_result",
|
|
52868
53275
|
classification: "failure",
|
|
52869
|
-
sourceId:
|
|
53276
|
+
sourceId: session.id,
|
|
52870
53277
|
summary: "Child session failed; inspect the durable child timeline.",
|
|
52871
53278
|
payload: {
|
|
52872
53279
|
type: "child_terminal_result",
|
|
52873
|
-
childSessionId:
|
|
53280
|
+
childSessionId: session.id,
|
|
52874
53281
|
status: "failed",
|
|
52875
|
-
turnId:
|
|
53282
|
+
...(input.turnId ? { turnId: input.turnId } : {}),
|
|
52876
53283
|
},
|
|
52877
53284
|
lineage: {
|
|
52878
|
-
childSessionId:
|
|
53285
|
+
childSessionId: session.id,
|
|
52879
53286
|
parentSessionId: session.parentSessionId,
|
|
52880
53287
|
...(session.parentTurnId ? { parentTurnId: session.parentTurnId } : {}),
|
|
52881
|
-
turnId:
|
|
53288
|
+
...(input.turnId ? { turnId: input.turnId } : {}),
|
|
52882
53289
|
...(xaiAuthoritySubjectId ? { xaiAuthoritySubjectId } : {}),
|
|
52883
53290
|
},
|
|
52884
53291
|
personalConnectionDelegations,
|
|
@@ -52899,6 +53306,41 @@ async function enqueueFailedChildOutboxForTurnTx(
|
|
|
52899
53306
|
});
|
|
52900
53307
|
}
|
|
52901
53308
|
|
|
53309
|
+
async function enqueueFailedChildOutboxForTurnTx(
|
|
53310
|
+
tx: Database,
|
|
53311
|
+
workspaceId: string,
|
|
53312
|
+
session: Pick<
|
|
53313
|
+
typeof schema.sessions.$inferSelect,
|
|
53314
|
+
"id" | "accountId" | "parentSessionId" | "parentTurnId"
|
|
53315
|
+
>,
|
|
53316
|
+
turn: Pick<typeof schema.sessionTurns.$inferSelect, "id" | "accountId" | "sessionId">,
|
|
53317
|
+
): Promise<void> {
|
|
53318
|
+
if (turn.accountId !== session.accountId || turn.sessionId !== session.id) {
|
|
53319
|
+
throw new SessionControlInvariantError(
|
|
53320
|
+
`Failed child turn ${turn.id} lost session ${session.id} ownership`,
|
|
53321
|
+
);
|
|
53322
|
+
}
|
|
53323
|
+
await enqueueFailedChildOutboxTx(tx, workspaceId, session, {
|
|
53324
|
+
turnId: turn.id,
|
|
53325
|
+
dedupeKey: `child-completion:${turn.sessionId}:turn:${turn.id}`,
|
|
53326
|
+
});
|
|
53327
|
+
}
|
|
53328
|
+
|
|
53329
|
+
async function enqueueFailedChildOutboxWithoutTurnTx(
|
|
53330
|
+
tx: Database,
|
|
53331
|
+
workspaceId: string,
|
|
53332
|
+
session: Pick<
|
|
53333
|
+
typeof schema.sessions.$inferSelect,
|
|
53334
|
+
"id" | "accountId" | "parentSessionId" | "parentTurnId"
|
|
53335
|
+
>,
|
|
53336
|
+
terminalSequence: number,
|
|
53337
|
+
): Promise<void> {
|
|
53338
|
+
await enqueueFailedChildOutboxTx(tx, workspaceId, session, {
|
|
53339
|
+
turnId: null,
|
|
53340
|
+
dedupeKey: `child-completion:${session.id}:pre-claim:${terminalSequence}`,
|
|
53341
|
+
});
|
|
53342
|
+
}
|
|
53343
|
+
|
|
52902
53344
|
type ChildTerminalResultPayload = Extract<
|
|
52903
53345
|
SessionSystemUpdatePayload,
|
|
52904
53346
|
{ type: "child_terminal_result" }
|
|
@@ -53106,10 +53548,19 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
53106
53548
|
: sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
|
|
53107
53549
|
reason: input.reason,
|
|
53108
53550
|
attempts: 0,
|
|
53109
|
-
// Coalescing a delayed retry must never postpone an
|
|
53110
|
-
// owned by another producer.
|
|
53111
|
-
//
|
|
53112
|
-
|
|
53551
|
+
// Coalescing a delayed retry must never postpone an undelivered wake
|
|
53552
|
+
// owned by another producer. Once every prior revision is delivered,
|
|
53553
|
+
// however, this is a new obligation and must honor its own deadline;
|
|
53554
|
+
// retaining the old (usually past) deadline would defeat backoff.
|
|
53555
|
+
nextAttemptAt: sql`case
|
|
53556
|
+
when ${schema.sessionWorkflowWakeOutbox.wakeRevision}
|
|
53557
|
+
> ${schema.sessionWorkflowWakeOutbox.deliveredRevision}
|
|
53558
|
+
then least(
|
|
53559
|
+
${schema.sessionWorkflowWakeOutbox.nextAttemptAt},
|
|
53560
|
+
${nextAttemptAt.toISOString()}::timestamptz
|
|
53561
|
+
)
|
|
53562
|
+
else ${nextAttemptAt.toISOString()}::timestamptz
|
|
53563
|
+
end`,
|
|
53113
53564
|
lastError: null,
|
|
53114
53565
|
updatedAt: now,
|
|
53115
53566
|
},
|
|
@@ -25,6 +25,26 @@ export type PersistenceFailureDetails = {
|
|
|
25
25
|
|
|
26
26
|
const SQLSTATE_KEYS = ["sqlState", "sqlstate", "code"] as const;
|
|
27
27
|
const NESTED_ERROR_KEYS = ["cause", "original", "driverError", "error", "errors"] as const;
|
|
28
|
+
const RETRYABLE_DATABASE_TRANSPORT_CODES = new Set([
|
|
29
|
+
// postgres.js connection lifecycle failures.
|
|
30
|
+
"CONNECTION_CLOSED",
|
|
31
|
+
"CONNECTION_DESTROYED",
|
|
32
|
+
"CONNECTION_ENDED",
|
|
33
|
+
"CONNECT_TIMEOUT",
|
|
34
|
+
// Node socket/DNS failures surfaced unchanged by postgres.js.
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"ECONNABORTED",
|
|
37
|
+
"ECONNREFUSED",
|
|
38
|
+
"ECONNRESET",
|
|
39
|
+
"EHOSTDOWN",
|
|
40
|
+
"EHOSTUNREACH",
|
|
41
|
+
"ENETDOWN",
|
|
42
|
+
"ENETRESET",
|
|
43
|
+
"ENETUNREACH",
|
|
44
|
+
"ENOTFOUND",
|
|
45
|
+
"EPIPE",
|
|
46
|
+
"ETIMEDOUT",
|
|
47
|
+
]);
|
|
28
48
|
const DATABASE_ERROR_NAMES = new Set(["DatabaseError", "DrizzleQueryError", "PostgresError"]);
|
|
29
49
|
const DATABASE_DIAGNOSTIC_KEYS = [
|
|
30
50
|
"severity",
|
|
@@ -72,6 +92,9 @@ export function nestedPostgresSqlState(error: unknown): string | null {
|
|
|
72
92
|
const value = current[key];
|
|
73
93
|
if (typeof value !== "string" || !/^[0-9A-Z]{5}$/i.test(value)) continue;
|
|
74
94
|
const normalized = value.toUpperCase();
|
|
95
|
+
// Node transport codes such as EPIPE happen to be five characters but
|
|
96
|
+
// are not PostgreSQL SQLSTATEs.
|
|
97
|
+
if (RETRYABLE_DATABASE_TRANSPORT_CODES.has(normalized)) continue;
|
|
75
98
|
if (normalized === "40P01" || normalized === "40001") return normalized;
|
|
76
99
|
fallback ??= normalized;
|
|
77
100
|
}
|
|
@@ -94,13 +117,49 @@ export function isRetryablePersistenceSqlState(sqlState: string | null): boolean
|
|
|
94
117
|
return sqlState === "40P01" || sqlState === "40001";
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Recognize only explicit postgres.js/Node transport codes, including nested
|
|
122
|
+
* driver causes. These failures can arrive as plain Errors with no SQLSTATE or
|
|
123
|
+
* database diagnostic fields when the connection dies before PostgreSQL can
|
|
124
|
+
* answer. Messages are intentionally ignored: they are unstable and may
|
|
125
|
+
* contain connection detail.
|
|
126
|
+
*/
|
|
127
|
+
export function isRetryableDatabaseTransportFailure(error: unknown): boolean {
|
|
128
|
+
const queue: unknown[] = [error];
|
|
129
|
+
const seen = new Set<unknown>();
|
|
130
|
+
while (queue.length > 0 && seen.size < 64) {
|
|
131
|
+
const current = queue.shift();
|
|
132
|
+
if (!isRecord(current) || seen.has(current)) continue;
|
|
133
|
+
seen.add(current);
|
|
134
|
+
|
|
135
|
+
for (const key of ["code", "errno"] as const) {
|
|
136
|
+
const value = current[key];
|
|
137
|
+
if (
|
|
138
|
+
typeof value === "string" &&
|
|
139
|
+
RETRYABLE_DATABASE_TRANSPORT_CODES.has(value.toUpperCase())
|
|
140
|
+
) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const key of NESTED_ERROR_KEYS) {
|
|
146
|
+
const nested = current[key];
|
|
147
|
+
if (Array.isArray(nested)) queue.push(...nested);
|
|
148
|
+
else if (nested !== undefined) queue.push(nested);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
97
154
|
/**
|
|
98
155
|
* Distinguish database/ORM failures from expected domain exceptions when a
|
|
99
156
|
* driver omitted SQLSTATE. This checks shape only; callers retain the original
|
|
100
157
|
* failure independently as canonical error evidence.
|
|
101
158
|
*/
|
|
102
159
|
export function isDatabasePersistenceFailure(error: unknown): boolean {
|
|
103
|
-
if (nestedPostgresSqlState(error) !== null)
|
|
160
|
+
if (isRetryableDatabaseTransportFailure(error) || nestedPostgresSqlState(error) !== null) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
104
163
|
|
|
105
164
|
const queue: unknown[] = [error];
|
|
106
165
|
const seen = new Set<unknown>();
|