@opengeni/db 4.0.0-canary.2 → 4.0.0-canary.3
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 +3 -3
- package/dist/{chunk-B5Y3TCSP.js → chunk-3JGOYOWD.js} +2 -2
- package/dist/{chunk-NPKBYGAK.js → chunk-CZXYNVEU.js} +2 -2
- package/dist/{chunk-L2QFSPFP.js → chunk-DIFDLF2C.js} +2 -2
- package/dist/{chunk-XIYAXH47.js → chunk-GPKHEPVX.js} +2 -2
- package/dist/{chunk-CQF6G4IF.js → chunk-HUCU5BDD.js} +3 -3
- package/dist/{chunk-VU2IK37Y.js → chunk-OWZ36ST7.js} +2 -2
- package/dist/{chunk-DUB3LUVM.js → chunk-R6LIJYG6.js} +140 -8
- package/dist/chunk-R6LIJYG6.js.map +1 -0
- package/dist/{chunk-PWJJKCTF.js → chunk-WU3MSPZS.js} +19 -2
- package/dist/chunk-WU3MSPZS.js.map +1 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +28 -8
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/schema.d.ts +85 -0
- package/dist/schema.js +1 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-control.d.ts +26 -1
- package/dist/session-event-slices.js +2 -2
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/drizzle/0420_workspace_pause_timers.sql +53 -0
- package/package.json +6 -6
- package/src/index.ts +21 -0
- package/src/schema.ts +20 -1
- package/src/session-control.ts +177 -4
- package/dist/chunk-DUB3LUVM.js.map +0 -1
- package/dist/chunk-PWJJKCTF.js.map +0 -1
- /package/dist/{chunk-B5Y3TCSP.js.map → chunk-3JGOYOWD.js.map} +0 -0
- /package/dist/{chunk-NPKBYGAK.js.map → chunk-CZXYNVEU.js.map} +0 -0
- /package/dist/{chunk-L2QFSPFP.js.map → chunk-DIFDLF2C.js.map} +0 -0
- /package/dist/{chunk-XIYAXH47.js.map → chunk-GPKHEPVX.js.map} +0 -0
- /package/dist/{chunk-CQF6G4IF.js.map → chunk-HUCU5BDD.js.map} +0 -0
- /package/dist/{chunk-VU2IK37Y.js.map → chunk-OWZ36ST7.js.map} +0 -0
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
reapManagedAuthIsolatedSessions,
|
|
21
21
|
releaseManagedAuthActorMutationLease,
|
|
22
22
|
validateManagedAuthActorMutationLease
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import "./chunk-
|
|
25
|
-
import "./chunk-
|
|
23
|
+
} from "./chunk-OWZ36ST7.js";
|
|
24
|
+
import "./chunk-GPKHEPVX.js";
|
|
25
|
+
import "./chunk-WU3MSPZS.js";
|
|
26
26
|
import "./chunk-PZ5AY32C.js";
|
|
27
27
|
export {
|
|
28
28
|
ManagedAuthActorMutationInFlightError,
|
package/dist/schema.d.ts
CHANGED
|
@@ -1463,6 +1463,91 @@ export declare const workspaceInferenceControls: import("drizzle-orm/pg-core").P
|
|
|
1463
1463
|
identity: undefined;
|
|
1464
1464
|
generated: undefined;
|
|
1465
1465
|
}, {}, {}>;
|
|
1466
|
+
timerId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1467
|
+
name: "timer_id";
|
|
1468
|
+
tableName: "workspace_inference_controls";
|
|
1469
|
+
dataType: "string";
|
|
1470
|
+
columnType: "PgUUID";
|
|
1471
|
+
data: string;
|
|
1472
|
+
driverParam: string;
|
|
1473
|
+
notNull: false;
|
|
1474
|
+
hasDefault: false;
|
|
1475
|
+
isPrimaryKey: false;
|
|
1476
|
+
isAutoincrement: false;
|
|
1477
|
+
hasRuntimeDefault: false;
|
|
1478
|
+
enumValues: undefined;
|
|
1479
|
+
baseColumn: never;
|
|
1480
|
+
identity: undefined;
|
|
1481
|
+
generated: undefined;
|
|
1482
|
+
}, {}, {}>;
|
|
1483
|
+
timerAction: import("drizzle-orm/pg-core").PgColumn<{
|
|
1484
|
+
name: "timer_action";
|
|
1485
|
+
tableName: "workspace_inference_controls";
|
|
1486
|
+
dataType: "string";
|
|
1487
|
+
columnType: "PgText";
|
|
1488
|
+
data: string;
|
|
1489
|
+
driverParam: string;
|
|
1490
|
+
notNull: false;
|
|
1491
|
+
hasDefault: false;
|
|
1492
|
+
isPrimaryKey: false;
|
|
1493
|
+
isAutoincrement: false;
|
|
1494
|
+
hasRuntimeDefault: false;
|
|
1495
|
+
enumValues: [string, ...string[]];
|
|
1496
|
+
baseColumn: never;
|
|
1497
|
+
identity: undefined;
|
|
1498
|
+
generated: undefined;
|
|
1499
|
+
}, {}, {}>;
|
|
1500
|
+
timerDueAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1501
|
+
name: "timer_due_at";
|
|
1502
|
+
tableName: "workspace_inference_controls";
|
|
1503
|
+
dataType: "date";
|
|
1504
|
+
columnType: "PgTimestamp";
|
|
1505
|
+
data: Date;
|
|
1506
|
+
driverParam: string;
|
|
1507
|
+
notNull: false;
|
|
1508
|
+
hasDefault: false;
|
|
1509
|
+
isPrimaryKey: false;
|
|
1510
|
+
isAutoincrement: false;
|
|
1511
|
+
hasRuntimeDefault: false;
|
|
1512
|
+
enumValues: undefined;
|
|
1513
|
+
baseColumn: never;
|
|
1514
|
+
identity: undefined;
|
|
1515
|
+
generated: undefined;
|
|
1516
|
+
}, {}, {}>;
|
|
1517
|
+
timerPauseForSeconds: import("drizzle-orm/pg-core").PgColumn<{
|
|
1518
|
+
name: "timer_pause_for_seconds";
|
|
1519
|
+
tableName: "workspace_inference_controls";
|
|
1520
|
+
dataType: "number";
|
|
1521
|
+
columnType: "PgInteger";
|
|
1522
|
+
data: number;
|
|
1523
|
+
driverParam: string | number;
|
|
1524
|
+
notNull: false;
|
|
1525
|
+
hasDefault: false;
|
|
1526
|
+
isPrimaryKey: false;
|
|
1527
|
+
isAutoincrement: false;
|
|
1528
|
+
hasRuntimeDefault: false;
|
|
1529
|
+
enumValues: undefined;
|
|
1530
|
+
baseColumn: never;
|
|
1531
|
+
identity: undefined;
|
|
1532
|
+
generated: undefined;
|
|
1533
|
+
}, {}, {}>;
|
|
1534
|
+
timerPauseRevision: import("drizzle-orm/pg-core").PgColumn<{
|
|
1535
|
+
name: "timer_pause_revision";
|
|
1536
|
+
tableName: "workspace_inference_controls";
|
|
1537
|
+
dataType: "number";
|
|
1538
|
+
columnType: "PgBigInt53";
|
|
1539
|
+
data: number;
|
|
1540
|
+
driverParam: string | number;
|
|
1541
|
+
notNull: false;
|
|
1542
|
+
hasDefault: false;
|
|
1543
|
+
isPrimaryKey: false;
|
|
1544
|
+
isAutoincrement: false;
|
|
1545
|
+
hasRuntimeDefault: false;
|
|
1546
|
+
enumValues: undefined;
|
|
1547
|
+
baseColumn: never;
|
|
1548
|
+
identity: undefined;
|
|
1549
|
+
generated: undefined;
|
|
1550
|
+
}, {}, {}>;
|
|
1466
1551
|
workspacePauseRevision: import("drizzle-orm/pg-core").PgColumn<{
|
|
1467
1552
|
name: "workspace_pause_revision";
|
|
1468
1553
|
tableName: "workspace_inference_controls";
|
package/dist/schema.js
CHANGED
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
settleClaimedConnectedMachineBackgroundCommandWithMutation,
|
|
23
23
|
settleConnectedMachineSessionBackgroundCommandWithMutation,
|
|
24
24
|
settleSessionBackgroundCommandForRetainedProcessInTransaction
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-R6LIJYG6.js";
|
|
26
|
+
import "./chunk-HUCU5BDD.js";
|
|
27
|
+
import "./chunk-GPKHEPVX.js";
|
|
28
|
+
import "./chunk-WU3MSPZS.js";
|
|
29
29
|
import "./chunk-PZ5AY32C.js";
|
|
30
30
|
export {
|
|
31
31
|
COMMAND_OUTPUT_DEFAULT_BYTES,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lockSessionEventWriteRows
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-R6LIJYG6.js";
|
|
4
|
+
import "./chunk-HUCU5BDD.js";
|
|
5
5
|
import {
|
|
6
6
|
withSessionActivityRlsContext
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GPKHEPVX.js";
|
|
8
8
|
import {
|
|
9
9
|
fromPostgresLosslessJson,
|
|
10
10
|
sandboxRetainedProcesses,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
sessionEvents,
|
|
13
13
|
sessions,
|
|
14
14
|
withLosslessContentWriteVersion
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-WU3MSPZS.js";
|
|
16
16
|
import "./chunk-PZ5AY32C.js";
|
|
17
17
|
|
|
18
18
|
// src/session-command-output.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SessionMcpApprovalPolicy, type TurnInitiatorContext } from "@opengeni/contracts";
|
|
1
|
+
import { type WorkspacePauseTimerRequest, type SessionMcpApprovalPolicy, type TurnInitiatorContext } from "@opengeni/contracts";
|
|
2
2
|
import type { Database, SessionActivityDatabase } from "./database.js";
|
|
3
3
|
import * as schema from "./schema.js";
|
|
4
4
|
export declare const SESSION_ANCESTRY_LIMIT = 10000;
|
|
@@ -131,6 +131,11 @@ export type WorkspaceControlRow = {
|
|
|
131
131
|
revision: number | string;
|
|
132
132
|
workspaceState: string;
|
|
133
133
|
workspacePauseRevision: number | string | null;
|
|
134
|
+
timerId: string | null;
|
|
135
|
+
timerAction: string | null;
|
|
136
|
+
timerDueAt: Date | string | null;
|
|
137
|
+
timerPauseForSeconds: number | null;
|
|
138
|
+
timerPauseRevision: number | string | null;
|
|
134
139
|
reason: string | null;
|
|
135
140
|
changedBy: string | null;
|
|
136
141
|
changedAt: Date | string | null;
|
|
@@ -584,7 +589,27 @@ export declare function mutateWorkspaceControlInTransaction(db: Database, input:
|
|
|
584
589
|
operationKey: string;
|
|
585
590
|
action: "pause" | "resume";
|
|
586
591
|
reason?: string | null;
|
|
592
|
+
/** Internal timer executor retains/replaces its own timer atomically. */
|
|
593
|
+
timerExecution?: boolean;
|
|
587
594
|
expectedRevision?: number | null;
|
|
588
595
|
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
589
596
|
controlLockTimeoutMs?: number;
|
|
590
597
|
}): Promise<WorkspaceControlMutationResult>;
|
|
598
|
+
export declare class WorkspacePauseTimerInputError extends Error {
|
|
599
|
+
}
|
|
600
|
+
/** Called under workspace RLS. The same control fence serializes timers and human actions. */
|
|
601
|
+
export declare function setWorkspacePauseTimerInTransaction(db: Database, input: WorkspacePauseTimerRequest & {
|
|
602
|
+
accountId: string;
|
|
603
|
+
workspaceId: string;
|
|
604
|
+
subjectId: string;
|
|
605
|
+
}): Promise<{
|
|
606
|
+
workspaceControlEventId: string | null;
|
|
607
|
+
}>;
|
|
608
|
+
/** Discovery is a hint. Identity, deadline and pause ownership are rechecked under the fence. */
|
|
609
|
+
export declare function fireWorkspacePauseTimerInTransaction(db: Database, input: {
|
|
610
|
+
workspaceId: string;
|
|
611
|
+
timerId: string;
|
|
612
|
+
}): Promise<{
|
|
613
|
+
workspaceControlEventId: string | null;
|
|
614
|
+
wakeCount: number;
|
|
615
|
+
} | null>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
rawRows,
|
|
3
3
|
withWorkspaceRls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GPKHEPVX.js";
|
|
5
5
|
import {
|
|
6
6
|
LOSSLESS_JSON_STRING_PREFIX,
|
|
7
7
|
fromPostgresLosslessJson,
|
|
8
8
|
sessionEvents,
|
|
9
9
|
sessionTurns
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WU3MSPZS.js";
|
|
11
11
|
import "./chunk-PZ5AY32C.js";
|
|
12
12
|
|
|
13
13
|
// src/session-event-slices.ts
|
package/dist/session-tenancy.js
CHANGED
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
replayAppliedSessionFork,
|
|
15
15
|
sessionTenancyProductActivated,
|
|
16
16
|
transitionSessionVisibility
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-3JGOYOWD.js";
|
|
18
|
+
import "./chunk-GPKHEPVX.js";
|
|
19
|
+
import "./chunk-WU3MSPZS.js";
|
|
20
20
|
import "./chunk-PZ5AY32C.js";
|
|
21
21
|
export {
|
|
22
22
|
SessionTenancyAccessError,
|
package/dist/video-generation.js
CHANGED
|
@@ -29,9 +29,9 @@ import {
|
|
|
29
29
|
settleVideoGenerationFailure,
|
|
30
30
|
settleVideoGenerationReady,
|
|
31
31
|
updateWorkspaceVideoGenerationPolicy
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import "./chunk-
|
|
34
|
-
import "./chunk-
|
|
32
|
+
} from "./chunk-HUCU5BDD.js";
|
|
33
|
+
import "./chunk-GPKHEPVX.js";
|
|
34
|
+
import "./chunk-WU3MSPZS.js";
|
|
35
35
|
import "./chunk-PZ5AY32C.js";
|
|
36
36
|
export {
|
|
37
37
|
ACTIVE_VIDEO_GENERATION_STATUSES,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Drain old API/control/turn workers before activation. New timer event actions
|
|
3
|
+
-- and manual-control cancellation semantics must be installed together.
|
|
4
|
+
ALTER TABLE workspace_inference_controls
|
|
5
|
+
ADD COLUMN timer_id uuid,
|
|
6
|
+
ADD COLUMN timer_action text,
|
|
7
|
+
ADD COLUMN timer_due_at timestamptz,
|
|
8
|
+
ADD COLUMN timer_pause_for_seconds integer,
|
|
9
|
+
ADD COLUMN timer_pause_revision bigint,
|
|
10
|
+
ADD CONSTRAINT workspace_pause_timer_shape CHECK (
|
|
11
|
+
(timer_id IS NULL AND timer_action IS NULL AND timer_due_at IS NULL
|
|
12
|
+
AND timer_pause_for_seconds IS NULL AND timer_pause_revision IS NULL)
|
|
13
|
+
OR (timer_id IS NOT NULL AND timer_action IS NOT NULL AND timer_due_at IS NOT NULL
|
|
14
|
+
AND ((timer_action = 'pause' AND timer_pause_revision IS NULL)
|
|
15
|
+
OR (timer_action = 'resume' AND timer_pause_revision IS NOT NULL AND timer_pause_for_seconds IS NULL))
|
|
16
|
+
AND (timer_pause_for_seconds IS NULL OR timer_pause_for_seconds BETWEEN 60 AND 2592000))
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX workspace_pause_timer_due_idx ON workspace_inference_controls(timer_due_at, workspace_id)
|
|
19
|
+
WHERE timer_id IS NOT NULL;
|
|
20
|
+
ALTER TABLE workspace_control_events DROP CONSTRAINT workspace_control_events_action_check;
|
|
21
|
+
ALTER TABLE workspace_control_events ADD CONSTRAINT workspace_control_events_action_check
|
|
22
|
+
CHECK (action IN ('pause', 'resume', 'timer_set', 'timer_cancelled'));
|
|
23
|
+
|
|
24
|
+
-- Narrow, read-only inventory capability. FORCE RLS stays enabled; only the
|
|
25
|
+
-- table owner inside this SECURITY DEFINER function gets cross-tenant discovery.
|
|
26
|
+
DO $migration$
|
|
27
|
+
DECLARE data_schema text := current_schema(); owner_name text := current_user;
|
|
28
|
+
BEGIN
|
|
29
|
+
EXECUTE format('CREATE POLICY workspace_pause_timer_discovery ON %I.workspace_inference_controls
|
|
30
|
+
FOR SELECT TO %I USING (current_setting(''opengeni.pause_timer_discovery'', true) = ''1'')',
|
|
31
|
+
data_schema, owner_name);
|
|
32
|
+
EXECUTE format($create$
|
|
33
|
+
CREATE FUNCTION opengeni_private.list_due_workspace_pause_timers(p_limit integer)
|
|
34
|
+
RETURNS TABLE(workspace_id uuid, timer_id uuid)
|
|
35
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
|
|
36
|
+
AS $body$
|
|
37
|
+
DECLARE previous text := current_setting('opengeni.pause_timer_discovery', true);
|
|
38
|
+
BEGIN
|
|
39
|
+
PERFORM set_config('opengeni.pause_timer_discovery', '1', true);
|
|
40
|
+
RETURN QUERY SELECT c.workspace_id, c.timer_id FROM %I.workspace_inference_controls c
|
|
41
|
+
WHERE c.timer_id IS NOT NULL AND c.timer_due_at <= clock_timestamp()
|
|
42
|
+
ORDER BY c.timer_due_at, c.workspace_id LIMIT greatest(1, least(coalesce(p_limit, 100), 1000));
|
|
43
|
+
PERFORM set_config('opengeni.pause_timer_discovery', coalesce(previous, ''), true);
|
|
44
|
+
END $body$;
|
|
45
|
+
$create$, data_schema);
|
|
46
|
+
END $migration$;
|
|
47
|
+
REVOKE ALL ON FUNCTION opengeni_private.list_due_workspace_pause_timers(integer) FROM PUBLIC;
|
|
48
|
+
DO $grant$
|
|
49
|
+
BEGIN
|
|
50
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
51
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.list_due_workspace_pause_timers(integer) TO opengeni_app;
|
|
52
|
+
END IF;
|
|
53
|
+
END $grant$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "4.0.0-canary.
|
|
3
|
+
"version": "4.0.0-canary.3",
|
|
4
4
|
"description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"@opengeni/codemode": "^0.4.27-canary.
|
|
90
|
-
"@opengeni/codex": "^0.2.21-canary.
|
|
91
|
-
"@opengeni/config": "^1.0.0-canary.
|
|
92
|
-
"@opengeni/contracts": "^2.13.0-canary.
|
|
93
|
-
"@opengeni/network": "^0.3.0-canary.
|
|
89
|
+
"@opengeni/codemode": "^0.4.27-canary.3",
|
|
90
|
+
"@opengeni/codex": "^0.2.21-canary.3",
|
|
91
|
+
"@opengeni/config": "^1.0.0-canary.3",
|
|
92
|
+
"@opengeni/contracts": "^2.13.0-canary.3",
|
|
93
|
+
"@opengeni/network": "^0.3.0-canary.3",
|
|
94
94
|
"drizzle-orm": "^0.45.2",
|
|
95
95
|
"postgres": "^3.4.7"
|
|
96
96
|
},
|
package/src/index.ts
CHANGED
|
@@ -75741,6 +75741,15 @@ async function workspaceControlProjection(
|
|
|
75741
75741
|
.limit(1);
|
|
75742
75742
|
if (!control) throw new Error(`Workspace ${workspaceId} has no inference control`);
|
|
75743
75743
|
return {
|
|
75744
|
+
timer: control.timerId
|
|
75745
|
+
? {
|
|
75746
|
+
id: control.timerId,
|
|
75747
|
+
action: control.timerAction as "pause" | "resume",
|
|
75748
|
+
dueAt: control.timerDueAt!.toISOString(),
|
|
75749
|
+
pauseForSeconds: control.timerPauseForSeconds,
|
|
75750
|
+
}
|
|
75751
|
+
: null,
|
|
75752
|
+
serverTime: new Date().toISOString(),
|
|
75744
75753
|
state: control.workspaceState as "active" | "paused",
|
|
75745
75754
|
revision: Number(control.revision),
|
|
75746
75755
|
reason: control.reason,
|
|
@@ -76470,3 +76479,15 @@ export * from "./session-tenancy";
|
|
|
76470
76479
|
export * from "./governed-learning-activation";
|
|
76471
76480
|
export * from "./automations";
|
|
76472
76481
|
export * from "./organization-model-providers";
|
|
76482
|
+
|
|
76483
|
+
export {
|
|
76484
|
+
setWorkspacePauseTimerInTransaction,
|
|
76485
|
+
fireWorkspacePauseTimerInTransaction,
|
|
76486
|
+
} from "./session-control";
|
|
76487
|
+
|
|
76488
|
+
export async function listDueWorkspacePauseTimers(db: Database, limit = 100) {
|
|
76489
|
+
return await rawRows<{ workspace_id: string; timer_id: string }>(
|
|
76490
|
+
db,
|
|
76491
|
+
sql`select * from opengeni_private.list_due_workspace_pause_timers(${limit})`,
|
|
76492
|
+
);
|
|
76493
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -405,6 +405,11 @@ export const workspaceInferenceControls = pgTable(
|
|
|
405
405
|
accountId: uuid("account_id").notNull(),
|
|
406
406
|
revision: bigint("revision", { mode: "number" }).notNull().default(0),
|
|
407
407
|
workspaceState: text("workspace_state").notNull().default("active"),
|
|
408
|
+
timerId: uuid("timer_id"),
|
|
409
|
+
timerAction: text("timer_action"),
|
|
410
|
+
timerDueAt: timestamp("timer_due_at", { withTimezone: true }),
|
|
411
|
+
timerPauseForSeconds: integer("timer_pause_for_seconds"),
|
|
412
|
+
timerPauseRevision: bigint("timer_pause_revision", { mode: "number" }),
|
|
408
413
|
workspacePauseRevision: bigint("workspace_pause_revision", {
|
|
409
414
|
mode: "number",
|
|
410
415
|
}),
|
|
@@ -420,6 +425,20 @@ export const workspaceInferenceControls = pgTable(
|
|
|
420
425
|
columns: [table.workspaceId, table.accountId],
|
|
421
426
|
foreignColumns: [workspaces.id, workspaces.accountId],
|
|
422
427
|
}).onDelete("cascade"),
|
|
428
|
+
timerDue: index("workspace_pause_timer_due_idx")
|
|
429
|
+
.on(table.timerDueAt, table.workspaceId)
|
|
430
|
+
.where(sql`${table.timerId} is not null`),
|
|
431
|
+
timerShape: check(
|
|
432
|
+
"workspace_pause_timer_shape",
|
|
433
|
+
sql`
|
|
434
|
+
(${table.timerId} is null and ${table.timerAction} is null and ${table.timerDueAt} is null
|
|
435
|
+
and ${table.timerPauseForSeconds} is null and ${table.timerPauseRevision} is null)
|
|
436
|
+
or (${table.timerId} is not null and ${table.timerAction} is not null and ${table.timerDueAt} is not null
|
|
437
|
+
and ((${table.timerAction} = 'pause' and ${table.timerPauseRevision} is null)
|
|
438
|
+
or (${table.timerAction} = 'resume' and ${table.timerPauseRevision} is not null and ${table.timerPauseForSeconds} is null))
|
|
439
|
+
and (${table.timerPauseForSeconds} is null or ${table.timerPauseForSeconds} between 60 and 2592000))
|
|
440
|
+
`,
|
|
441
|
+
),
|
|
423
442
|
workspaceAccountIdentity: uniqueIndex("workspace_inference_controls_workspace_account_uq").on(
|
|
424
443
|
table.workspaceId,
|
|
425
444
|
table.accountId,
|
|
@@ -7526,7 +7545,7 @@ export const workspaceControlEvents = pgTable(
|
|
|
7526
7545
|
),
|
|
7527
7546
|
actionValid: check(
|
|
7528
7547
|
"workspace_control_events_action_check",
|
|
7529
|
-
sql`${table.action} in ('pause', 'resume')`,
|
|
7548
|
+
sql`${table.action} in ('pause', 'resume', 'timer_set', 'timer_cancelled')`,
|
|
7530
7549
|
),
|
|
7531
7550
|
}),
|
|
7532
7551
|
);
|
package/src/session-control.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
childPausedClassification,
|
|
5
5
|
McpPersonalConnectionDelegations,
|
|
6
6
|
workspaceControlUtf8Bytes,
|
|
7
|
+
type WorkspacePauseTimerRequest,
|
|
7
8
|
type SessionMcpApprovalPolicy,
|
|
8
9
|
type TurnInitiatorContext,
|
|
9
10
|
} from "@opengeni/contracts";
|
|
@@ -176,6 +177,11 @@ export type WorkspaceControlRow = {
|
|
|
176
177
|
revision: number | string;
|
|
177
178
|
workspaceState: string;
|
|
178
179
|
workspacePauseRevision: number | string | null;
|
|
180
|
+
timerId: string | null;
|
|
181
|
+
timerAction: string | null;
|
|
182
|
+
timerDueAt: Date | string | null;
|
|
183
|
+
timerPauseForSeconds: number | null;
|
|
184
|
+
timerPauseRevision: number | string | null;
|
|
179
185
|
reason: string | null;
|
|
180
186
|
changedBy: string | null;
|
|
181
187
|
changedAt: Date | string | null;
|
|
@@ -546,6 +552,9 @@ export async function lockWorkspaceInferenceControl(
|
|
|
546
552
|
revision,
|
|
547
553
|
workspace_state as "workspaceState",
|
|
548
554
|
workspace_pause_revision as "workspacePauseRevision",
|
|
555
|
+
timer_id as "timerId", timer_action as "timerAction",
|
|
556
|
+
timer_due_at as "timerDueAt", timer_pause_for_seconds as "timerPauseForSeconds",
|
|
557
|
+
timer_pause_revision as "timerPauseRevision",
|
|
549
558
|
reason,
|
|
550
559
|
changed_by as "changedBy",
|
|
551
560
|
changed_at as "changedAt"
|
|
@@ -3921,6 +3930,8 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
3921
3930
|
operationKey: string;
|
|
3922
3931
|
action: "pause" | "resume";
|
|
3923
3932
|
reason?: string | null;
|
|
3933
|
+
/** Internal timer executor retains/replaces its own timer atomically. */
|
|
3934
|
+
timerExecution?: boolean;
|
|
3924
3935
|
expectedRevision?: number | null;
|
|
3925
3936
|
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
3926
3937
|
controlLockTimeoutMs?: number;
|
|
@@ -3986,7 +3997,8 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
3986
3997
|
"workspace pause revision",
|
|
3987
3998
|
);
|
|
3988
3999
|
const changed =
|
|
3989
|
-
input.
|
|
4000
|
+
(!input.timerExecution && workspace.timerId != null) ||
|
|
4001
|
+
(input.action === "pause"
|
|
3990
4002
|
? workspace.workspaceState !== "paused" ||
|
|
3991
4003
|
workspacePauseRevision === null ||
|
|
3992
4004
|
(await workspacePauseEffectsNeeded(db, {
|
|
@@ -3997,7 +4009,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
3997
4009
|
(await continuableWakeRepairNeeded(db, {
|
|
3998
4010
|
workspaceId: input.workspaceId,
|
|
3999
4011
|
rootSessionId: null,
|
|
4000
|
-
}));
|
|
4012
|
+
})));
|
|
4001
4013
|
if (!changed) {
|
|
4002
4014
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
4003
4015
|
result: {
|
|
@@ -4035,6 +4047,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
4035
4047
|
const [updated] = await db
|
|
4036
4048
|
.update(schema.workspaceInferenceControls)
|
|
4037
4049
|
.set({
|
|
4050
|
+
...(!input.timerExecution ? clearWorkspaceTimerFields : {}),
|
|
4038
4051
|
revision,
|
|
4039
4052
|
workspaceState: input.action === "pause" ? "paused" : "active",
|
|
4040
4053
|
workspacePauseRevision: input.action === "pause" ? revision : null,
|
|
@@ -4060,7 +4073,7 @@ export async function mutateWorkspaceControlInTransaction(
|
|
|
4060
4073
|
scope: "workspace",
|
|
4061
4074
|
rootSessionId: null,
|
|
4062
4075
|
action: input.action,
|
|
4063
|
-
automatic:
|
|
4076
|
+
automatic: input.timerExecution === true,
|
|
4064
4077
|
reason: input.reason ?? null,
|
|
4065
4078
|
actor,
|
|
4066
4079
|
});
|
|
@@ -4113,7 +4126,7 @@ async function insertWorkspaceControlEventInTransaction(
|
|
|
4113
4126
|
revision: number;
|
|
4114
4127
|
scope: "workspace" | "session";
|
|
4115
4128
|
rootSessionId: string | null;
|
|
4116
|
-
action: "pause" | "resume";
|
|
4129
|
+
action: "pause" | "resume" | "timer_set" | "timer_cancelled";
|
|
4117
4130
|
automatic: boolean;
|
|
4118
4131
|
reason: string | null;
|
|
4119
4132
|
actor: string;
|
|
@@ -4157,3 +4170,163 @@ async function insertWorkspaceControlEventInTransaction(
|
|
|
4157
4170
|
}
|
|
4158
4171
|
return event.id;
|
|
4159
4172
|
}
|
|
4173
|
+
|
|
4174
|
+
export class WorkspacePauseTimerInputError extends Error {}
|
|
4175
|
+
|
|
4176
|
+
const clearWorkspaceTimerFields = {
|
|
4177
|
+
timerId: null,
|
|
4178
|
+
timerAction: null,
|
|
4179
|
+
timerDueAt: null,
|
|
4180
|
+
timerPauseForSeconds: null,
|
|
4181
|
+
timerPauseRevision: null,
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
/** Called under workspace RLS. The same control fence serializes timers and human actions. */
|
|
4185
|
+
export async function setWorkspacePauseTimerInTransaction(
|
|
4186
|
+
db: Database,
|
|
4187
|
+
input: WorkspacePauseTimerRequest & { accountId: string; workspaceId: string; subjectId: string },
|
|
4188
|
+
): Promise<{ workspaceControlEventId: string | null }> {
|
|
4189
|
+
let workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
4190
|
+
lockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
4191
|
+
});
|
|
4192
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
4193
|
+
accountId: input.accountId,
|
|
4194
|
+
workspaceId: input.workspaceId,
|
|
4195
|
+
actor: { type: "human", subjectId: input.subjectId },
|
|
4196
|
+
action: `workspace.timer.${input.action}`,
|
|
4197
|
+
targetSessionId: null,
|
|
4198
|
+
targetTurnId: null,
|
|
4199
|
+
operationKey: input.clientEventId,
|
|
4200
|
+
canonicalRequestHash: canonicalSessionCommandHash({
|
|
4201
|
+
action: input.action,
|
|
4202
|
+
pauseInSeconds: input.pauseInSeconds ?? 0,
|
|
4203
|
+
pauseForSeconds: input.pauseForSeconds ?? null,
|
|
4204
|
+
expectedRevision: input.expectedRevision,
|
|
4205
|
+
}),
|
|
4206
|
+
});
|
|
4207
|
+
if (reserved.replay)
|
|
4208
|
+
return {
|
|
4209
|
+
workspaceControlEventId:
|
|
4210
|
+
(reserved.receipt.result.workspaceControlEventId as string | null) ?? null,
|
|
4211
|
+
};
|
|
4212
|
+
if (Number(workspace.revision) !== input.expectedRevision)
|
|
4213
|
+
throw new SessionControlConflictError();
|
|
4214
|
+
let action: "pause" | "resume" = "pause";
|
|
4215
|
+
let delay = input.pauseInSeconds ?? 0;
|
|
4216
|
+
let pauseRevision: number | null = null;
|
|
4217
|
+
if (input.action === "set") {
|
|
4218
|
+
if (workspace.workspaceState === "paused") {
|
|
4219
|
+
if (delay !== 0 || input.pauseForSeconds == null) {
|
|
4220
|
+
throw new WorkspacePauseTimerInputError("A paused workspace requires a resume duration");
|
|
4221
|
+
}
|
|
4222
|
+
action = "resume";
|
|
4223
|
+
delay = input.pauseForSeconds;
|
|
4224
|
+
pauseRevision = Number(workspace.workspacePauseRevision);
|
|
4225
|
+
} else if (delay === 0) {
|
|
4226
|
+
await mutateWorkspaceControlInTransaction(db, {
|
|
4227
|
+
accountId: input.accountId,
|
|
4228
|
+
workspaceId: input.workspaceId,
|
|
4229
|
+
actor: { type: "human", subjectId: input.subjectId },
|
|
4230
|
+
operationKey: `timer-pause:${reserved.receipt.id}`,
|
|
4231
|
+
action: "pause",
|
|
4232
|
+
});
|
|
4233
|
+
workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
4234
|
+
action = "resume";
|
|
4235
|
+
delay = input.pauseForSeconds ?? 0;
|
|
4236
|
+
pauseRevision = Number(workspace.workspacePauseRevision);
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
const hasTimer = input.action === "set" && delay > 0;
|
|
4240
|
+
const [clock] = await db.execute<{ now: Date }>(sql`select clock_timestamp() as now`);
|
|
4241
|
+
const revision = nextRevision(workspace);
|
|
4242
|
+
await db
|
|
4243
|
+
.update(schema.workspaceInferenceControls)
|
|
4244
|
+
.set({
|
|
4245
|
+
...clearWorkspaceTimerFields,
|
|
4246
|
+
...(hasTimer
|
|
4247
|
+
? {
|
|
4248
|
+
timerId: reserved.receipt.id,
|
|
4249
|
+
timerAction: action,
|
|
4250
|
+
timerDueAt: new Date(new Date(clock!.now).getTime() + delay * 1000),
|
|
4251
|
+
timerPauseForSeconds: action === "pause" ? (input.pauseForSeconds ?? null) : null,
|
|
4252
|
+
timerPauseRevision: pauseRevision,
|
|
4253
|
+
}
|
|
4254
|
+
: {}),
|
|
4255
|
+
revision,
|
|
4256
|
+
updatedAt: new Date(clock!.now),
|
|
4257
|
+
})
|
|
4258
|
+
.where(eq(schema.workspaceInferenceControls.workspaceId, input.workspaceId));
|
|
4259
|
+
const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
|
|
4260
|
+
accountId: input.accountId,
|
|
4261
|
+
workspaceId: input.workspaceId,
|
|
4262
|
+
revision,
|
|
4263
|
+
scope: "workspace",
|
|
4264
|
+
rootSessionId: null,
|
|
4265
|
+
action: hasTimer ? "timer_set" : "timer_cancelled",
|
|
4266
|
+
automatic: false,
|
|
4267
|
+
reason: null,
|
|
4268
|
+
actor: input.subjectId,
|
|
4269
|
+
});
|
|
4270
|
+
await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
4271
|
+
controlRevision: revision,
|
|
4272
|
+
result: { workspaceControlEventId },
|
|
4273
|
+
});
|
|
4274
|
+
return { workspaceControlEventId };
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
/** Discovery is a hint. Identity, deadline and pause ownership are rechecked under the fence. */
|
|
4278
|
+
export async function fireWorkspacePauseTimerInTransaction(
|
|
4279
|
+
db: Database,
|
|
4280
|
+
input: { workspaceId: string; timerId: string },
|
|
4281
|
+
): Promise<{ workspaceControlEventId: string | null; wakeCount: number } | null> {
|
|
4282
|
+
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
4283
|
+
lockTimeoutMs: 1000,
|
|
4284
|
+
});
|
|
4285
|
+
const [clock] = await db.execute<{ now: Date }>(sql`select clock_timestamp() as now`);
|
|
4286
|
+
if (
|
|
4287
|
+
workspace.timerId !== input.timerId ||
|
|
4288
|
+
!workspace.timerDueAt ||
|
|
4289
|
+
new Date(workspace.timerDueAt).getTime() > new Date(clock!.now).getTime()
|
|
4290
|
+
)
|
|
4291
|
+
return null;
|
|
4292
|
+
const action = workspace.timerAction as "pause" | "resume";
|
|
4293
|
+
// A superseding pause must never be undone by an old timer, even across rolling upgrades.
|
|
4294
|
+
if (
|
|
4295
|
+
action === "resume" &&
|
|
4296
|
+
(workspace.workspaceState !== "paused" ||
|
|
4297
|
+
Number(workspace.workspacePauseRevision) !== Number(workspace.timerPauseRevision))
|
|
4298
|
+
) {
|
|
4299
|
+
await db
|
|
4300
|
+
.update(schema.workspaceInferenceControls)
|
|
4301
|
+
.set(clearWorkspaceTimerFields)
|
|
4302
|
+
.where(eq(schema.workspaceInferenceControls.workspaceId, input.workspaceId));
|
|
4303
|
+
return null;
|
|
4304
|
+
}
|
|
4305
|
+
const result = await mutateWorkspaceControlInTransaction(db, {
|
|
4306
|
+
accountId: workspace.accountId,
|
|
4307
|
+
workspaceId: input.workspaceId,
|
|
4308
|
+
actor: { type: "service", subjectId: "service:workspace-pause-timer" },
|
|
4309
|
+
operationKey: `timer:${input.timerId}:${action}`,
|
|
4310
|
+
action,
|
|
4311
|
+
timerExecution: true,
|
|
4312
|
+
});
|
|
4313
|
+
// Use application time only for display elsewhere; deadlines are database-clock based.
|
|
4314
|
+
const [applied] = await db.execute<{ now: Date }>(sql`select clock_timestamp() as now`);
|
|
4315
|
+
await db
|
|
4316
|
+
.update(schema.workspaceInferenceControls)
|
|
4317
|
+
.set({
|
|
4318
|
+
...clearWorkspaceTimerFields,
|
|
4319
|
+
...(action === "pause" && workspace.timerPauseForSeconds !== null
|
|
4320
|
+
? {
|
|
4321
|
+
timerId: input.timerId,
|
|
4322
|
+
timerAction: "resume",
|
|
4323
|
+
timerDueAt: new Date(
|
|
4324
|
+
new Date(applied!.now).getTime() + workspace.timerPauseForSeconds * 1000,
|
|
4325
|
+
),
|
|
4326
|
+
timerPauseRevision: result.revision,
|
|
4327
|
+
}
|
|
4328
|
+
: {}),
|
|
4329
|
+
})
|
|
4330
|
+
.where(eq(schema.workspaceInferenceControls.workspaceId, input.workspaceId));
|
|
4331
|
+
return result;
|
|
4332
|
+
}
|