@opengeni/db 0.27.7 → 0.27.8
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-EX7CDSGH.js → chunk-M6EOXYHK.js} +6 -1
- package/dist/chunk-M6EOXYHK.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +118 -35
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.js +1 -1
- package/dist/session-control.d.ts +1 -0
- package/drizzle/0170_session_control_wake_revision.sql +133 -0
- package/package.json +1 -1
- package/src/index.ts +160 -37
- package/src/schema.ts +5 -0
- package/src/session-control.ts +11 -4
- package/src/session-queue-commands.ts +3 -0
- package/dist/chunk-EX7CDSGH.js.map +0 -1
package/dist/schema.d.ts
CHANGED
|
@@ -15342,6 +15342,23 @@ export declare const sessionWorkflowWakeOutbox: import("drizzle-orm/pg-core").Pg
|
|
|
15342
15342
|
identity: undefined;
|
|
15343
15343
|
generated: undefined;
|
|
15344
15344
|
}, {}, {}>;
|
|
15345
|
+
controlRevision: import("drizzle-orm/pg-core").PgColumn<{
|
|
15346
|
+
name: "control_revision";
|
|
15347
|
+
tableName: "session_workflow_wake_outbox";
|
|
15348
|
+
dataType: "number";
|
|
15349
|
+
columnType: "PgBigInt53";
|
|
15350
|
+
data: number;
|
|
15351
|
+
driverParam: string | number;
|
|
15352
|
+
notNull: true;
|
|
15353
|
+
hasDefault: true;
|
|
15354
|
+
isPrimaryKey: false;
|
|
15355
|
+
isAutoincrement: false;
|
|
15356
|
+
hasRuntimeDefault: false;
|
|
15357
|
+
enumValues: undefined;
|
|
15358
|
+
baseColumn: never;
|
|
15359
|
+
identity: undefined;
|
|
15360
|
+
generated: undefined;
|
|
15361
|
+
}, {}, {}>;
|
|
15345
15362
|
reason: import("drizzle-orm/pg-core").PgColumn<{
|
|
15346
15363
|
name: "reason";
|
|
15347
15364
|
tableName: "session_workflow_wake_outbox";
|
package/dist/schema.js
CHANGED
|
@@ -259,6 +259,7 @@ export declare function registerSessionWorkflowWakeInTransaction(db: Database, i
|
|
|
259
259
|
sessionId: string;
|
|
260
260
|
temporalWorkflowId: string;
|
|
261
261
|
reason: string;
|
|
262
|
+
controlRequested?: boolean;
|
|
262
263
|
}): Promise<number>;
|
|
263
264
|
/**
|
|
264
265
|
* Internal producers share one outstanding session-level receipt. While a wake
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Preserve control-priority Temporal signals across coalesced session wakes.
|
|
3
|
+
|
|
4
|
+
SET lock_timeout = '5s';
|
|
5
|
+
SET statement_timeout = '10min';
|
|
6
|
+
|
|
7
|
+
ALTER TABLE "session_workflow_wake_outbox"
|
|
8
|
+
ADD COLUMN "control_revision" bigint NOT NULL DEFAULT 0;
|
|
9
|
+
|
|
10
|
+
ALTER TABLE "session_workflow_wake_outbox"
|
|
11
|
+
ADD CONSTRAINT "session_workflow_wake_outbox_control_revision_check"
|
|
12
|
+
CHECK (
|
|
13
|
+
"control_revision" >= 0
|
|
14
|
+
AND "control_revision" <= "wake_revision"
|
|
15
|
+
AND "control_revision" <= 9007199254740991
|
|
16
|
+
) NOT VALID;
|
|
17
|
+
|
|
18
|
+
-- Mixed-version writers do not know the new column. Stamp recognized control
|
|
19
|
+
-- reasons in the database so an old Steer followed by an old Send still leaves
|
|
20
|
+
-- the earlier control revision outstanding until delivery.
|
|
21
|
+
CREATE OR REPLACE FUNCTION opengeni_private.stamp_session_workflow_control_revision()
|
|
22
|
+
RETURNS trigger
|
|
23
|
+
LANGUAGE plpgsql
|
|
24
|
+
SET search_path = pg_catalog
|
|
25
|
+
AS $function$
|
|
26
|
+
BEGIN
|
|
27
|
+
IF NEW.reason IN (
|
|
28
|
+
'prompt_steer',
|
|
29
|
+
'queue_steer',
|
|
30
|
+
'agent_steer',
|
|
31
|
+
'session_cancelled',
|
|
32
|
+
'session_pause_interruption',
|
|
33
|
+
'workspace_pause_interruption',
|
|
34
|
+
'attempt_interruption_settled',
|
|
35
|
+
'attempt_interruption_rejected_stale'
|
|
36
|
+
) AND NEW.wake_revision > NEW.delivered_revision THEN
|
|
37
|
+
NEW.control_revision := greatest(NEW.control_revision, NEW.wake_revision);
|
|
38
|
+
END IF;
|
|
39
|
+
RETURN NEW;
|
|
40
|
+
END
|
|
41
|
+
$function$;
|
|
42
|
+
|
|
43
|
+
DROP TRIGGER IF EXISTS session_workflow_wake_control_revision
|
|
44
|
+
ON "session_workflow_wake_outbox";
|
|
45
|
+
CREATE TRIGGER session_workflow_wake_control_revision
|
|
46
|
+
BEFORE INSERT OR UPDATE OF reason, wake_revision, control_revision
|
|
47
|
+
ON "session_workflow_wake_outbox"
|
|
48
|
+
FOR EACH ROW
|
|
49
|
+
EXECUTE FUNCTION opengeni_private.stamp_session_workflow_control_revision();
|
|
50
|
+
|
|
51
|
+
-- Preserve already-committed ownerless Steer/cancellation wakes during rolling
|
|
52
|
+
-- activation. Active-attempt controls remain independently discoverable from
|
|
53
|
+
-- the interruption ledger below.
|
|
54
|
+
UPDATE "session_workflow_wake_outbox"
|
|
55
|
+
SET "control_revision" = "wake_revision"
|
|
56
|
+
WHERE "delivered_revision" < "wake_revision"
|
|
57
|
+
AND "reason" IN (
|
|
58
|
+
'prompt_steer',
|
|
59
|
+
'queue_steer',
|
|
60
|
+
'agent_steer',
|
|
61
|
+
'session_cancelled',
|
|
62
|
+
'session_pause_interruption',
|
|
63
|
+
'workspace_pause_interruption'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
ALTER TABLE "session_workflow_wake_outbox"
|
|
67
|
+
VALIDATE CONSTRAINT "session_workflow_wake_outbox_control_revision_check";
|
|
68
|
+
|
|
69
|
+
DROP FUNCTION opengeni_private.claim_session_workflow_wakes(integer);
|
|
70
|
+
DO $migration$
|
|
71
|
+
DECLARE target_schema text := current_schema();
|
|
72
|
+
BEGIN
|
|
73
|
+
EXECUTE format($create$
|
|
74
|
+
CREATE FUNCTION opengeni_private.claim_session_workflow_wakes(p_limit integer)
|
|
75
|
+
RETURNS TABLE (
|
|
76
|
+
account_id uuid,
|
|
77
|
+
workspace_id uuid,
|
|
78
|
+
session_id uuid,
|
|
79
|
+
temporal_workflow_id text,
|
|
80
|
+
wake_revision bigint,
|
|
81
|
+
interruption_requested boolean
|
|
82
|
+
)
|
|
83
|
+
LANGUAGE plpgsql
|
|
84
|
+
SECURITY DEFINER
|
|
85
|
+
SET search_path = pg_catalog
|
|
86
|
+
AS $function$
|
|
87
|
+
BEGIN
|
|
88
|
+
RETURN QUERY
|
|
89
|
+
WITH due AS (
|
|
90
|
+
SELECT o.session_id
|
|
91
|
+
FROM %1$I.session_workflow_wake_outbox o
|
|
92
|
+
WHERE o.wake_revision > o.delivered_revision
|
|
93
|
+
AND o.next_attempt_at <= now()
|
|
94
|
+
ORDER BY o.next_attempt_at, o.updated_at, o.session_id
|
|
95
|
+
FOR UPDATE SKIP LOCKED
|
|
96
|
+
LIMIT greatest(1, least(coalesce(p_limit, 100), 1000))
|
|
97
|
+
)
|
|
98
|
+
UPDATE %1$I.session_workflow_wake_outbox o
|
|
99
|
+
SET attempts = o.attempts + 1,
|
|
100
|
+
next_attempt_at = now() + make_interval(
|
|
101
|
+
secs => least(300, greatest(1, power(2, least(o.attempts, 8))::integer))
|
|
102
|
+
),
|
|
103
|
+
updated_at = now()
|
|
104
|
+
FROM due
|
|
105
|
+
WHERE o.session_id = due.session_id
|
|
106
|
+
RETURNING o.account_id, o.workspace_id, o.session_id,
|
|
107
|
+
o.temporal_workflow_id, o.wake_revision,
|
|
108
|
+
o.control_revision > o.delivered_revision
|
|
109
|
+
OR EXISTS (
|
|
110
|
+
SELECT 1
|
|
111
|
+
FROM %1$I.session_attempt_interruptions interruption
|
|
112
|
+
WHERE interruption.workspace_id = o.workspace_id
|
|
113
|
+
AND interruption.session_id = o.session_id
|
|
114
|
+
AND interruption.state IN (
|
|
115
|
+
'pending', 'delivered', 'acknowledged', 'settled', 'rejected_stale'
|
|
116
|
+
)
|
|
117
|
+
) AS interruption_requested;
|
|
118
|
+
END $function$;
|
|
119
|
+
$create$, target_schema);
|
|
120
|
+
END $migration$;
|
|
121
|
+
|
|
122
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer) FROM PUBLIC;
|
|
123
|
+
|
|
124
|
+
DO $$
|
|
125
|
+
BEGIN
|
|
126
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
127
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer)
|
|
128
|
+
TO opengeni_app;
|
|
129
|
+
END IF;
|
|
130
|
+
END $$;
|
|
131
|
+
|
|
132
|
+
RESET statement_timeout;
|
|
133
|
+
RESET lock_timeout;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -37554,6 +37554,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
37554
37554
|
"delivered",
|
|
37555
37555
|
"acknowledged",
|
|
37556
37556
|
"settled",
|
|
37557
|
+
"rejected_stale",
|
|
37557
37558
|
]),
|
|
37558
37559
|
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
37559
37560
|
),
|
|
@@ -38481,28 +38482,6 @@ export async function claimSessionWorkForAttempt(
|
|
|
38481
38482
|
turn: mapSessionTurnForExecution(internalTurn),
|
|
38482
38483
|
};
|
|
38483
38484
|
}
|
|
38484
|
-
const predecessorAttemptId = queuedSteerReplacementAttemptId(queuedTurn.metadata);
|
|
38485
|
-
if (predecessorAttemptId) {
|
|
38486
|
-
const [predecessor] = await tx
|
|
38487
|
-
.select({ quiescedAt: schema.sessionTurnAttempts.quiescedAt })
|
|
38488
|
-
.from(schema.sessionTurnAttempts)
|
|
38489
|
-
.where(
|
|
38490
|
-
and(
|
|
38491
|
-
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
38492
|
-
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
38493
|
-
eq(schema.sessionTurnAttempts.id, predecessorAttemptId),
|
|
38494
|
-
),
|
|
38495
|
-
)
|
|
38496
|
-
.limit(1);
|
|
38497
|
-
if (!predecessor) {
|
|
38498
|
-
throw new SessionControlInvariantError(
|
|
38499
|
-
`Queued Steer ${id} points to missing predecessor attempt ${predecessorAttemptId}`,
|
|
38500
|
-
);
|
|
38501
|
-
}
|
|
38502
|
-
if (!predecessor.quiescedAt) {
|
|
38503
|
-
return { action: "unclaimed", reason: "control-pending" };
|
|
38504
|
-
}
|
|
38505
|
-
}
|
|
38506
38485
|
// The database guard makes this function the only supported
|
|
38507
38486
|
// queued-to-running transition. Raw or stale claimers cannot bypass the
|
|
38508
38487
|
// generation/active-pointer transaction.
|
|
@@ -39136,6 +39115,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
39136
39115
|
interruptions.map((interruption) => interruption.id),
|
|
39137
39116
|
),
|
|
39138
39117
|
);
|
|
39118
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
39119
|
+
accountId: session.accountId,
|
|
39120
|
+
workspaceId,
|
|
39121
|
+
sessionId,
|
|
39122
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
|
|
39123
|
+
reason: "attempt_interruption_rejected_stale",
|
|
39124
|
+
controlRequested: true,
|
|
39125
|
+
});
|
|
39139
39126
|
return {
|
|
39140
39127
|
action: effectiveControl.state === "paused" ? "paused" : "continue",
|
|
39141
39128
|
events: [],
|
|
@@ -39327,6 +39314,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
39327
39314
|
interruptions.map((interruption) => interruption.id),
|
|
39328
39315
|
),
|
|
39329
39316
|
);
|
|
39317
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
39318
|
+
accountId: session.accountId,
|
|
39319
|
+
workspaceId,
|
|
39320
|
+
sessionId,
|
|
39321
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
|
|
39322
|
+
reason: "attempt_interruption_settled",
|
|
39323
|
+
controlRequested: true,
|
|
39324
|
+
});
|
|
39330
39325
|
return {
|
|
39331
39326
|
action: effectiveControl.state === "paused" ? "paused" : "continue",
|
|
39332
39327
|
events: [...closedTools.events, ...eventRows.map(mapEvent)],
|
|
@@ -39355,6 +39350,50 @@ export type SessionWorkPeek =
|
|
|
39355
39350
|
| { kind: "cancellation-wait"; attemptId: string }
|
|
39356
39351
|
| { kind: "idle" };
|
|
39357
39352
|
|
|
39353
|
+
/**
|
|
39354
|
+
* Return the oldest exact attempt whose logical interruption settled but whose
|
|
39355
|
+
* physical quiescence receipt is still missing. This deliberately searches all
|
|
39356
|
+
* attempts: a provider-recovery race can create a newer generation before the
|
|
39357
|
+
* predecessor receipt is reconciled, and looking only at the newest attempt
|
|
39358
|
+
* would strand the replacement forever.
|
|
39359
|
+
*/
|
|
39360
|
+
async function nextSessionAttemptAwaitingQuiescence(
|
|
39361
|
+
db: Database,
|
|
39362
|
+
workspaceId: string,
|
|
39363
|
+
sessionId: string,
|
|
39364
|
+
): Promise<{
|
|
39365
|
+
attemptId: string;
|
|
39366
|
+
} | null> {
|
|
39367
|
+
const [row] = await db
|
|
39368
|
+
.select({
|
|
39369
|
+
attemptId: schema.sessionTurnAttempts.id,
|
|
39370
|
+
})
|
|
39371
|
+
.from(schema.sessionTurnAttempts)
|
|
39372
|
+
.innerJoin(
|
|
39373
|
+
schema.sessionAttemptInterruptions,
|
|
39374
|
+
and(
|
|
39375
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
|
|
39376
|
+
eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
|
|
39377
|
+
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
39378
|
+
),
|
|
39379
|
+
)
|
|
39380
|
+
.where(
|
|
39381
|
+
and(
|
|
39382
|
+
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
39383
|
+
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
39384
|
+
eq(schema.sessionTurnAttempts.state, "closed"),
|
|
39385
|
+
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
39386
|
+
inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
|
|
39387
|
+
),
|
|
39388
|
+
)
|
|
39389
|
+
.orderBy(
|
|
39390
|
+
asc(schema.sessionAttemptInterruptions.requestedAt),
|
|
39391
|
+
asc(schema.sessionAttemptInterruptions.id),
|
|
39392
|
+
)
|
|
39393
|
+
.limit(1);
|
|
39394
|
+
return row ?? null;
|
|
39395
|
+
}
|
|
39396
|
+
|
|
39358
39397
|
async function latestSessionAttemptInterruption(
|
|
39359
39398
|
db: Database,
|
|
39360
39399
|
workspaceId: string,
|
|
@@ -39402,6 +39441,43 @@ async function latestSessionAttemptInterruption(
|
|
|
39402
39441
|
: null;
|
|
39403
39442
|
}
|
|
39404
39443
|
|
|
39444
|
+
async function queuedSteerHasUnquiescedPredecessor(
|
|
39445
|
+
db: Database,
|
|
39446
|
+
workspaceId: string,
|
|
39447
|
+
sessionId: string,
|
|
39448
|
+
predecessorAttemptIds: string[],
|
|
39449
|
+
): Promise<boolean> {
|
|
39450
|
+
if (predecessorAttemptIds.length === 0) return false;
|
|
39451
|
+
const [row] = await db
|
|
39452
|
+
.select({ attemptId: schema.sessionTurnAttempts.id })
|
|
39453
|
+
.from(schema.sessionTurnAttempts)
|
|
39454
|
+
.innerJoin(
|
|
39455
|
+
schema.sessionAttemptInterruptions,
|
|
39456
|
+
and(
|
|
39457
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
|
|
39458
|
+
eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
|
|
39459
|
+
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
39460
|
+
),
|
|
39461
|
+
)
|
|
39462
|
+
.where(
|
|
39463
|
+
and(
|
|
39464
|
+
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
39465
|
+
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
39466
|
+
inArray(schema.sessionTurnAttempts.id, predecessorAttemptIds),
|
|
39467
|
+
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
39468
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
39469
|
+
"pending",
|
|
39470
|
+
"delivered",
|
|
39471
|
+
"acknowledged",
|
|
39472
|
+
"settled",
|
|
39473
|
+
"rejected_stale",
|
|
39474
|
+
]),
|
|
39475
|
+
),
|
|
39476
|
+
)
|
|
39477
|
+
.limit(1);
|
|
39478
|
+
return row !== undefined;
|
|
39479
|
+
}
|
|
39480
|
+
|
|
39405
39481
|
/** Read durable session state without reserving a turn-worker slot or mutating it. */
|
|
39406
39482
|
export async function peekSessionWork(
|
|
39407
39483
|
db: Database,
|
|
@@ -39445,19 +39521,15 @@ export async function peekSessionWork(
|
|
|
39445
39521
|
}
|
|
39446
39522
|
if (effectiveControl.state !== "active") return { kind: "idle" };
|
|
39447
39523
|
|
|
39448
|
-
const
|
|
39524
|
+
const awaitingQuiescence = await nextSessionAttemptAwaitingQuiescence(
|
|
39449
39525
|
scopedDb,
|
|
39450
39526
|
workspaceId,
|
|
39451
39527
|
sessionId,
|
|
39452
39528
|
);
|
|
39453
|
-
if (
|
|
39454
|
-
latestInterruption &&
|
|
39455
|
-
latestInterruption.quiescedAt === null &&
|
|
39456
|
-
latestInterruption.interruptionState === "settled"
|
|
39457
|
-
) {
|
|
39529
|
+
if (awaitingQuiescence) {
|
|
39458
39530
|
return {
|
|
39459
39531
|
kind: "cancellation-wait",
|
|
39460
|
-
attemptId:
|
|
39532
|
+
attemptId: awaitingQuiescence.attemptId,
|
|
39461
39533
|
};
|
|
39462
39534
|
}
|
|
39463
39535
|
|
|
@@ -41315,6 +41387,7 @@ export type RequestSessionTurnRecoveryInput = {
|
|
|
41315
41387
|
attemptId: string;
|
|
41316
41388
|
reason: string;
|
|
41317
41389
|
detail?: Record<string, unknown>;
|
|
41390
|
+
providerRecoveryCount?: number;
|
|
41318
41391
|
fromStatuses?: SessionTurnStatus[];
|
|
41319
41392
|
providerArtifactInvalidation?: {
|
|
41320
41393
|
codexCredentialId: string;
|
|
@@ -41368,6 +41441,25 @@ export async function requestSessionTurnRecovery(
|
|
|
41368
41441
|
{ workspaceControl: locks.control ?? undefined },
|
|
41369
41442
|
);
|
|
41370
41443
|
const turnStatus = (turn?.status as SessionTurnStatus | undefined) ?? null;
|
|
41444
|
+
const [pendingInterruption] = attempt
|
|
41445
|
+
? await tx
|
|
41446
|
+
.select({ id: schema.sessionAttemptInterruptions.id })
|
|
41447
|
+
.from(schema.sessionAttemptInterruptions)
|
|
41448
|
+
.where(
|
|
41449
|
+
and(
|
|
41450
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
|
|
41451
|
+
eq(schema.sessionAttemptInterruptions.sessionId, input.sessionId),
|
|
41452
|
+
eq(schema.sessionAttemptInterruptions.attemptId, input.attemptId),
|
|
41453
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
41454
|
+
"pending",
|
|
41455
|
+
"delivered",
|
|
41456
|
+
"acknowledged",
|
|
41457
|
+
]),
|
|
41458
|
+
),
|
|
41459
|
+
)
|
|
41460
|
+
.limit(1)
|
|
41461
|
+
.for("update")
|
|
41462
|
+
: [];
|
|
41371
41463
|
if (
|
|
41372
41464
|
!locks.workspace ||
|
|
41373
41465
|
!turn ||
|
|
@@ -41381,7 +41473,8 @@ export async function requestSessionTurnRecovery(
|
|
|
41381
41473
|
effectiveControl.state !== "active" ||
|
|
41382
41474
|
session.activeTurnId !== input.turnId ||
|
|
41383
41475
|
!fromStatuses.includes(turnStatus as SessionTurnStatus) ||
|
|
41384
|
-
turn.activeAttemptId !== input.attemptId
|
|
41476
|
+
turn.activeAttemptId !== input.attemptId ||
|
|
41477
|
+
pendingInterruption !== undefined
|
|
41385
41478
|
) {
|
|
41386
41479
|
return {
|
|
41387
41480
|
action: "stale" as const,
|
|
@@ -41392,6 +41485,12 @@ export async function requestSessionTurnRecovery(
|
|
|
41392
41485
|
}
|
|
41393
41486
|
|
|
41394
41487
|
const now = new Date();
|
|
41488
|
+
if (
|
|
41489
|
+
input.providerRecoveryCount !== undefined &&
|
|
41490
|
+
(!Number.isSafeInteger(input.providerRecoveryCount) || input.providerRecoveryCount <= 0)
|
|
41491
|
+
) {
|
|
41492
|
+
throw new Error("providerRecoveryCount must be a positive safe integer");
|
|
41493
|
+
}
|
|
41395
41494
|
let providerArtifactsInvalidated = 0;
|
|
41396
41495
|
if (input.providerArtifactInvalidation) {
|
|
41397
41496
|
const invalidatedHistory = await tx
|
|
@@ -41533,7 +41632,12 @@ export async function requestSessionTurnRecovery(
|
|
|
41533
41632
|
cancelledBy: null,
|
|
41534
41633
|
cancelReason: null,
|
|
41535
41634
|
version: turn.version + 1,
|
|
41536
|
-
metadata:
|
|
41635
|
+
metadata: {
|
|
41636
|
+
...metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
41637
|
+
...(input.providerRecoveryCount !== undefined
|
|
41638
|
+
? { providerRecoveryCount: input.providerRecoveryCount }
|
|
41639
|
+
: {}),
|
|
41640
|
+
},
|
|
41537
41641
|
updatedAt: now,
|
|
41538
41642
|
})
|
|
41539
41643
|
.where(
|
|
@@ -42142,12 +42246,25 @@ export async function getSessionQueueSnapshot(
|
|
|
42142
42246
|
asc(schema.sessionSystemUpdates.createdAt),
|
|
42143
42247
|
asc(schema.sessionSystemUpdates.id),
|
|
42144
42248
|
);
|
|
42249
|
+
const items = rows.map(mapSessionTurn);
|
|
42145
42250
|
const latestInterruption = await latestSessionAttemptInterruption(
|
|
42146
42251
|
scopedDb,
|
|
42147
42252
|
workspaceId,
|
|
42148
42253
|
sessionId,
|
|
42149
42254
|
);
|
|
42150
|
-
const
|
|
42255
|
+
const queuedSteerPredecessorIds = items
|
|
42256
|
+
.map((turn) => queuedSteerReplacementAttemptId(turn.metadata))
|
|
42257
|
+
.filter((attemptId): attemptId is string => attemptId !== null);
|
|
42258
|
+
const stoppingPreviousAttempt =
|
|
42259
|
+
(latestInterruption !== null &&
|
|
42260
|
+
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
42261
|
+
latestInterruption.quiescedAt === null) ||
|
|
42262
|
+
(await queuedSteerHasUnquiescedPredecessor(
|
|
42263
|
+
scopedDb,
|
|
42264
|
+
workspaceId,
|
|
42265
|
+
sessionId,
|
|
42266
|
+
queuedSteerPredecessorIds,
|
|
42267
|
+
));
|
|
42151
42268
|
const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
|
|
42152
42269
|
const hasPendingAgentSteer = pendingInputs.some(
|
|
42153
42270
|
(update) => update.kind === "agent_steer_instruction",
|
|
@@ -42159,10 +42276,7 @@ export async function getSessionQueueSnapshot(
|
|
|
42159
42276
|
version: session.queueVersion,
|
|
42160
42277
|
effectiveControl: serializeEffectiveSessionControl(effectiveControl),
|
|
42161
42278
|
activePersonalConnections,
|
|
42162
|
-
stoppingPreviousAttempt
|
|
42163
|
-
latestInterruption !== null &&
|
|
42164
|
-
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
42165
|
-
latestInterruption.quiescedAt === null,
|
|
42279
|
+
stoppingPreviousAttempt,
|
|
42166
42280
|
items,
|
|
42167
42281
|
pendingInputs: pendingInputs.map((update) => {
|
|
42168
42282
|
const canonical = mapSessionSystemUpdate(update);
|
|
@@ -42201,7 +42315,9 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
|
|
|
42201
42315
|
) {
|
|
42202
42316
|
return attemptId;
|
|
42203
42317
|
}
|
|
42204
|
-
|
|
42318
|
+
// The interruption ledger is authoritative. Historical or malformed display
|
|
42319
|
+
// metadata must not make the read-only queue projection fail.
|
|
42320
|
+
return null;
|
|
42205
42321
|
}
|
|
42206
42322
|
|
|
42207
42323
|
async function enqueueFailedChildOutboxForTurnTx(
|
|
@@ -42418,6 +42534,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42418
42534
|
temporalWorkflowId: string;
|
|
42419
42535
|
reason: string;
|
|
42420
42536
|
notBefore?: Date;
|
|
42537
|
+
controlRequested?: boolean;
|
|
42421
42538
|
},
|
|
42422
42539
|
): Promise<number> {
|
|
42423
42540
|
const now = new Date();
|
|
@@ -42429,6 +42546,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42429
42546
|
workspaceId: input.workspaceId,
|
|
42430
42547
|
sessionId: input.sessionId,
|
|
42431
42548
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
42549
|
+
controlRevision: input.controlRequested ? 1 : 0,
|
|
42432
42550
|
reason: input.reason,
|
|
42433
42551
|
nextAttemptAt,
|
|
42434
42552
|
})
|
|
@@ -42437,6 +42555,9 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42437
42555
|
set: {
|
|
42438
42556
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
42439
42557
|
wakeRevision: sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`,
|
|
42558
|
+
controlRevision: input.controlRequested
|
|
42559
|
+
? sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`
|
|
42560
|
+
: sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
|
|
42440
42561
|
reason: input.reason,
|
|
42441
42562
|
attempts: 0,
|
|
42442
42563
|
// Coalescing a delayed retry must never postpone an already-due wake
|
|
@@ -42462,6 +42583,7 @@ export async function enqueueSessionWorkflowWake(
|
|
|
42462
42583
|
temporalWorkflowId: string;
|
|
42463
42584
|
reason: string;
|
|
42464
42585
|
notBefore?: Date;
|
|
42586
|
+
controlRequested?: boolean;
|
|
42465
42587
|
},
|
|
42466
42588
|
): Promise<number> {
|
|
42467
42589
|
return await withRlsContext(
|
|
@@ -42485,6 +42607,7 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
|
|
|
42485
42607
|
temporalWorkflowId: string;
|
|
42486
42608
|
reason: string;
|
|
42487
42609
|
notBefore?: Date;
|
|
42610
|
+
controlRequested?: boolean;
|
|
42488
42611
|
},
|
|
42489
42612
|
): Promise<number | null> {
|
|
42490
42613
|
return await withRlsContext(
|
package/src/schema.ts
CHANGED
|
@@ -3246,6 +3246,7 @@ export const sessionWorkflowWakeOutbox = pgTable(
|
|
|
3246
3246
|
temporalWorkflowId: text("temporal_workflow_id").notNull(),
|
|
3247
3247
|
wakeRevision: bigint("wake_revision", { mode: "number" }).notNull().default(1),
|
|
3248
3248
|
deliveredRevision: bigint("delivered_revision", { mode: "number" }).notNull().default(0),
|
|
3249
|
+
controlRevision: bigint("control_revision", { mode: "number" }).notNull().default(0),
|
|
3249
3250
|
reason: text("reason").notNull(),
|
|
3250
3251
|
attempts: integer("attempts").notNull().default(0),
|
|
3251
3252
|
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -3262,6 +3263,10 @@ export const sessionWorkflowWakeOutbox = pgTable(
|
|
|
3262
3263
|
"session_workflow_wake_outbox_revision_safe_check",
|
|
3263
3264
|
sql`${table.wakeRevision} <= 9007199254740991 and ${table.deliveredRevision} <= 9007199254740991`,
|
|
3264
3265
|
),
|
|
3266
|
+
controlRevisionValid: check(
|
|
3267
|
+
"session_workflow_wake_outbox_control_revision_check",
|
|
3268
|
+
sql`${table.controlRevision} >= 0 and ${table.controlRevision} <= ${table.wakeRevision} and ${table.controlRevision} <= 9007199254740991`,
|
|
3269
|
+
),
|
|
3265
3270
|
workspaceAccount: foreignKey({
|
|
3266
3271
|
name: "session_workflow_wake_outbox_workspace_account_fk",
|
|
3267
3272
|
columns: [table.workspaceId, table.accountId],
|
package/src/session-control.ts
CHANGED
|
@@ -1494,12 +1494,13 @@ async function registerContinuableWakes(
|
|
|
1494
1494
|
)
|
|
1495
1495
|
), upserted as (
|
|
1496
1496
|
insert into ${schema.sessionWorkflowWakeOutbox} (
|
|
1497
|
-
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1497
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason, control_revision
|
|
1498
1498
|
)
|
|
1499
|
-
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
|
|
1499
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}, 1
|
|
1500
1500
|
from eligible
|
|
1501
1501
|
on conflict (session_id) do update set
|
|
1502
1502
|
wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1503
|
+
control_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1503
1504
|
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1504
1505
|
reason = excluded.reason,
|
|
1505
1506
|
attempts = 0,
|
|
@@ -1611,12 +1612,13 @@ async function registerCancellationWakes(
|
|
|
1611
1612
|
and session.id in (${sessionIds})
|
|
1612
1613
|
), upserted as (
|
|
1613
1614
|
insert into ${schema.sessionWorkflowWakeOutbox} (
|
|
1614
|
-
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1615
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason, control_revision
|
|
1615
1616
|
)
|
|
1616
|
-
select session_id, account_id, workspace_id, temporal_workflow_id, 'session_cancelled'
|
|
1617
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, 'session_cancelled', 1
|
|
1617
1618
|
from eligible
|
|
1618
1619
|
on conflict (session_id) do update set
|
|
1619
1620
|
wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1621
|
+
control_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1620
1622
|
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1621
1623
|
reason = excluded.reason,
|
|
1622
1624
|
attempts = 0,
|
|
@@ -1641,6 +1643,7 @@ export async function registerSessionWorkflowWakeInTransaction(
|
|
|
1641
1643
|
sessionId: string;
|
|
1642
1644
|
temporalWorkflowId: string;
|
|
1643
1645
|
reason: string;
|
|
1646
|
+
controlRequested?: boolean;
|
|
1644
1647
|
},
|
|
1645
1648
|
): Promise<number> {
|
|
1646
1649
|
const [row] = await db
|
|
@@ -1650,6 +1653,7 @@ export async function registerSessionWorkflowWakeInTransaction(
|
|
|
1650
1653
|
workspaceId: input.workspaceId,
|
|
1651
1654
|
sessionId: input.sessionId,
|
|
1652
1655
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
1656
|
+
controlRevision: input.controlRequested ? 1 : 0,
|
|
1653
1657
|
reason: input.reason,
|
|
1654
1658
|
})
|
|
1655
1659
|
.onConflictDoUpdate({
|
|
@@ -1657,6 +1661,9 @@ export async function registerSessionWorkflowWakeInTransaction(
|
|
|
1657
1661
|
set: {
|
|
1658
1662
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
1659
1663
|
wakeRevision: sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`,
|
|
1664
|
+
controlRevision: input.controlRequested
|
|
1665
|
+
? sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`
|
|
1666
|
+
: sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
|
|
1660
1667
|
reason: input.reason,
|
|
1661
1668
|
attempts: 0,
|
|
1662
1669
|
nextAttemptAt: new Date(),
|
|
@@ -1180,6 +1180,7 @@ export async function steerQueuedTurnInTransaction(
|
|
|
1180
1180
|
sessionId: input.sessionId,
|
|
1181
1181
|
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
1182
1182
|
reason: "queue_steer",
|
|
1183
|
+
controlRequested: true,
|
|
1183
1184
|
});
|
|
1184
1185
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1185
1186
|
controlRevision: resumed.revision,
|
|
@@ -1739,6 +1740,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1739
1740
|
sessionId: input.sessionId,
|
|
1740
1741
|
temporalWorkflowId: workflowId,
|
|
1741
1742
|
reason: input.delivery === "steer" ? "prompt_steer" : "prompt_send",
|
|
1743
|
+
controlRequested: input.delivery === "steer",
|
|
1742
1744
|
});
|
|
1743
1745
|
await db.insert(schema.auditEvents).values({
|
|
1744
1746
|
accountId: input.accountId,
|
|
@@ -2183,6 +2185,7 @@ export async function steerAgentSessionInTransaction(
|
|
|
2183
2185
|
sessionId: input.targetSessionId,
|
|
2184
2186
|
temporalWorkflowId: workflowId,
|
|
2185
2187
|
reason: "agent_steer",
|
|
2188
|
+
controlRequested: true,
|
|
2186
2189
|
});
|
|
2187
2190
|
await db
|
|
2188
2191
|
.update(schema.sessions)
|